DATAREDIS-523 - Read back TTL into property.

We now read back TTL value from Redis for explicitly @TimeToLive annotated properties.

Original pull request: #208.
This commit is contained in:
Christoph Strobl
2016-07-08 14:03:22 +02:00
committed by Mark Paluch
parent e8a30a79a0
commit c4ddc9e125
5 changed files with 164 additions and 18 deletions

View File

@@ -453,6 +453,7 @@ public class TimeToLiveOnMethod {
----
====
NOTE: Annotating a property explicitly with `@TimeToLive` will read back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no expire associated.
The repository implementation ensures subscription to http://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`.

View File

@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
@@ -39,6 +40,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
import org.springframework.data.keyvalue.core.KeyValueAdapter;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
@@ -58,6 +60,7 @@ import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.util.ByteUtils;
import org.springframework.data.util.CloseableIterator;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
/**
@@ -295,7 +298,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
data.setId(stringId);
data.setKeyspace(stringKeyspace);
return converter.read(type, data);
return readBackTimeToLiveIfSet(binId, converter.read(type, data));
}
/*
@@ -346,29 +349,18 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
final byte[] binKeyspace = toBytes(keyspace);
List<Map<byte[], byte[]>> raw = redisOps.execute(new RedisCallback<List<Map<byte[], byte[]>>>() {
Set<byte[]> ids = redisOps.execute(new RedisCallback<Set<byte[]>>() {
@Override
public List<Map<byte[], byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
final List<Map<byte[], byte[]>> rawData = new ArrayList<Map<byte[], byte[]>>();
Set<byte[]> members = connection.sMembers(binKeyspace);
for (byte[] id : members) {
rawData.add(connection
.hGetAll(createKey(asString(keyspace), getConverter().getConversionService().convert(id, String.class))));
}
return rawData;
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
return connection.sMembers(binKeyspace);
}
});
List<Object> result = new ArrayList<Object>(raw.size());
for (Map<byte[], byte[]> rawData : raw) {
result.add(converter.read(Object.class, new RedisData(rawData)));
List<Object> result = new ArrayList<Object>();
for (byte[] key : ids) {
result.add(get(key, keyspace));
}
return result;
}
@@ -579,6 +571,49 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter
return converter.getConversionService().convert(source, byte[].class);
}
/**
* Read back and set {@link TimeToLive} for the property.
*
* @param key
* @param target
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private <T> T readBackTimeToLiveIfSet(final byte[] key, T target) {
if (target == null || key == null) {
return target;
}
RedisPersistentEntity<?> entity = this.converter.getMappingContext().getPersistentEntity(target.getClass());
if (entity.hasExplictTimeToLiveProperty()) {
PersistentProperty<?> ttlProperty = entity.getExplicitTimeToLiveProperty();
final TimeToLive ttl = ttlProperty.findAnnotation(TimeToLive.class);
Long timeout = redisOps.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
if (ObjectUtils.nullSafeEquals(TimeUnit.SECONDS, ttl.unit())) {
return connection.ttl(key);
}
return connection.pTtl(key, ttl.unit());
}
});
if (timeout != null || (timeout == null && !ttlProperty.getType().isPrimitive())) {
entity.getPropertyAccessor(target).setProperty(ttlProperty,
NumberUtils.convertNumberToTargetClass(timeout, (Class) ttlProperty.getType()));
}
}
return target;
}
/**
* Configure usage of {@link KeyExpirationEventMessageListener}.
*

View File

@@ -20,6 +20,7 @@ import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEnt
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.TimeToLiveAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@@ -59,6 +60,24 @@ public class BasicRedisPersistentEntity<T> extends BasicKeyValuePersistentEntity
return this.timeToLiveAccessor;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#hasExplictTimeToLiveProperty()
*/
@Override
public boolean hasExplictTimeToLiveProperty() {
return getExplicitTimeToLiveProperty() != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#getExplicitTimeToLiveProperty()
*/
@Override
public RedisPersistentProperty getExplicitTimeToLiveProperty() {
return (RedisPersistentProperty) this.getPersistentProperty(TimeToLive.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.BasicPersistentEntity#returnPropertyIfBetterIdPropertyCandidateOrNull(org.springframework.data.mapping.PersistentProperty)

View File

@@ -17,6 +17,7 @@ package org.springframework.data.redis.core.mapping;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.redis.core.TimeToLiveAccessor;
/**
@@ -35,4 +36,18 @@ public interface RedisPersistentEntity<T> extends KeyValuePersistentEntity<T> {
*/
TimeToLiveAccessor getTimeToLiveAccessor();
/**
* @return {@literal true} when a property is annotated with {@link org.springframework.data.redis.core.TimeToLive}.
* @since 1.8
*/
boolean hasExplictTimeToLiveProperty();
/**
* Get the {@link PersistentProperty}
*
* @return can be {@null}.
* @since 1.8
*/
PersistentProperty<? extends PersistentProperty<?>> getExplicitTimeToLiveProperty();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.redis.core;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.number.IsCloseTo.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
@@ -43,6 +44,7 @@ import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.util.ObjectUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
@@ -865,6 +867,72 @@ public class RedisKeyValueTemplateTests {
});
}
/**
* @see DATAREDIS-523
*/
@Test
public void shouldReadBackExplicitTimeToLive() throws InterruptedException {
WithTtl source = new WithTtl();
source.id = "ttl-1";
source.ttl = 5L;
source.value = "5 seconds";
template.insert(source);
Thread.sleep(1100);
WithTtl target = template.findById(source.id, WithTtl.class);
assertThat(target.ttl, is(notNullValue()));
assertThat(target.ttl.doubleValue(), is(closeTo(3D, 1D)));
}
/**
* @see DATAREDIS-523
*/
@Test
public void shouldReadBackExplicitTimeToLiveWhenFetchingList() throws InterruptedException {
WithTtl source = new WithTtl();
source.id = "ttl-1";
source.ttl = 5L;
source.value = "5 seconds";
template.insert(source);
Thread.sleep(1100);
WithTtl target = template.findAll(WithTtl.class).iterator().next();
assertThat(target.ttl, is(notNullValue()));
assertThat(target.ttl.doubleValue(), is(closeTo(3D, 1D)));
}
/**
* @see DATAREDIS-523
*/
@Test
public void shouldReadBackExplicitTimeToLiveAndSetItToMinusOnelIfPersisted() throws InterruptedException {
WithTtl source = new WithTtl();
source.id = "ttl-1";
source.ttl = 5L;
source.value = "5 seconds";
template.insert(source);
nativeTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.persist((WithTtl.class.getName() + ":ttl-1").getBytes());
}
});
WithTtl target = template.findById(source.id, WithTtl.class);
assertThat(target.ttl, is(-1L));
}
@EqualsAndHashCode
@RedisHash("template-test-type-mapping")
static class VariousTypes {
@@ -973,4 +1041,12 @@ public class RedisKeyValueTemplateTests {
}
}
@Data
static class WithTtl {
@Id String id;
String value;
@TimeToLive Long ttl;
}
}