diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index a5fe3dbe3..f162cf5d4 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -99,6 +99,10 @@ public interface RedisOperations { Boolean move(K key, int dbIndex); + byte[] dump(K key); + + void restore(K key, byte[] value, long timeToLive, TimeUnit unit); + Long getExpire(K key); void watch(K keys); diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 5a8a04617..d462f283e 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -617,6 +617,50 @@ public class RedisTemplate extends RedisAccessor implements RedisOperation }, true); } + /** + * Executes the Redis dump command and returns the results. Redis uses a + * non-standard serialization mechanism and includes checksum information, + * thus the raw bytes are returned as opposed to deserializing with + * valueSerializer. Use the return value of dump as the value argument to restore + * + * @param key The key to dump + * @return results The results of the dump operation + */ + public byte[] dump(K key) { + final byte[] rawKey = rawKey(key); + + return execute(new RedisCallback() { + public byte[] doInRedis(RedisConnection connection) { + return connection.dump(rawKey); + } + }, true); + } + + /** + * Executes the Redis restore command. The value passed in should be the exact + * serialized data returned from {@link #dump(Object)}, since Redis uses a + * non-standard serialization mechanism. + * + * + * @param key The key to restore + * @param value The value to restore, as returned by {@link #dump(Object)} + * @param timeToLive An expiration for the restored key, or 0 for no expiration + * @param unit The time unit for timeToLive + * @throws RedisSystemException if the key you are attempting to restore already + * exists. + * + */ + public void restore(K key, final byte[] value, long timeToLive, TimeUnit unit) { + final byte[] rawKey = rawKey(key); + final long rawTimeout = TimeoutUtils.toMillis(timeToLive, unit); + + execute(new RedisCallback() { + public Boolean doInRedis(RedisConnection connection) { + connection.restore(rawKey, rawTimeout, value); + return null; + } + }, true); + } public void multi() { execute(new RedisCallback() { diff --git a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java index caa47ddd7..08eb7b7f0 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java +++ b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java @@ -38,12 +38,33 @@ abstract public class TimeoutUtils { * @return The converted timeout */ public static long toSeconds(long timeout, TimeUnit unit) { - long seconds = unit.toSeconds(timeout); + return roundUpIfNecessary(timeout, unit.toSeconds(timeout)); + + } + + /** + * Converts the given timeout to milliseconds. + *

+ * Since a 0 timeout blocks some Redis ops indefinitely, this method will + * return 1 if the original value is greater than 0 but is truncated to 0 on + * conversion. + * + * @param timeout + * The timeout to convert + * @param unit + * The timeout's unit + * @return The converted timeout + */ + public static long toMillis(long timeout, TimeUnit unit) { + return roundUpIfNecessary(timeout, unit.toMillis(timeout)); + } + + private static long roundUpIfNecessary(long timeout, long convertedTimeout) { // A 0 timeout blocks some Redis ops indefinitely, round up if that's // not the intention - if (timeout > 0 && seconds == 0) { - seconds = 1; + if (timeout > 0 && convertedTimeout == 0) { + return 1; } - return seconds; + return convertedTimeout; } } \ No newline at end of file diff --git a/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java new file mode 100644 index 000000000..d97138350 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.TestCondition; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.springframework.data.redis.SpinBarrier.waitFor; + +/** + * + * Integration test of {@link RedisTemplate} + * + * @author Jennifer Hickey + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class RedisTemplateTests { + + @Autowired + private RedisTemplate redisTemplate; + + @After + public void tearDown() { + redisTemplate.getConnectionFactory().getConnection().flushDb(); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testDumpAndRestoreNoTtl() { + redisTemplate.boundValueOps("testing").set("123"); + byte[] serializedValue = redisTemplate.dump("testing"); + assertNotNull(serializedValue); + redisTemplate.delete("testing"); + redisTemplate.restore("testing", serializedValue, 0, TimeUnit.SECONDS); + assertEquals("123", redisTemplate.boundValueOps("testing").get()); + } + + @Test + @IfProfileValue(name = "redisVersion", value = "2.6") + public void testRestoreTtl() { + redisTemplate.boundValueOps("testing").set("123"); + byte[] serializedValue = redisTemplate.dump("testing"); + assertNotNull(serializedValue); + redisTemplate.delete("testing"); + redisTemplate.restore("testing", serializedValue, 200, TimeUnit.MILLISECONDS); + assertEquals("123", redisTemplate.boundValueOps("testing").get()); + waitFor(new TestCondition() { + public boolean passes() { + return (!redisTemplate.hasKey("testing")); + } + }, 400); + } + +} diff --git a/src/test/java/org/springframework/data/redis/core/TimeoutUtilsTests.java b/src/test/java/org/springframework/data/redis/core/TimeoutUtilsTests.java index da05d7e37..b76c76fe6 100644 --- a/src/test/java/org/springframework/data/redis/core/TimeoutUtilsTests.java +++ b/src/test/java/org/springframework/data/redis/core/TimeoutUtilsTests.java @@ -40,24 +40,54 @@ public class TimeoutUtilsTests { } @Test - public void testConvertZero() { + public void testConvertZeroSeconds() { assertEquals(0, TimeoutUtils.toSeconds(0, TimeUnit.MINUTES)); } @Test - public void testConvertNegativeGreaterThanNegativeOne() { + public void testConvertNegativeSecondsGreaterThanNegativeOne() { // Ensure we convert this to 0 as before, though ideally we wouldn't accept negative values assertEquals(0,TimeoutUtils.toSeconds(-123, TimeUnit.MILLISECONDS)); } @Test - public void testConvertNegativeEqualNegativeOne() { + public void testConvertNegativeSecondsEqualNegativeOne() { assertEquals(-1,TimeoutUtils.toSeconds(-1111, TimeUnit.MILLISECONDS)); } @Test - public void testConvertNegativeLessThanNegativeOne() { + public void testConvertNegativeSecondsLessThanNegativeOne() { assertEquals(-2,TimeoutUtils.toSeconds(-2344, TimeUnit.MILLISECONDS)); } + @Test + public void testConvertMoreThanOneMilli() { + assertEquals(2, TimeoutUtils.toMillis(2010, TimeUnit.MICROSECONDS)); + } + + @Test + public void testConvertLessThanOneMilli() { + assertEquals(1, TimeoutUtils.toMillis(999, TimeUnit.NANOSECONDS)); + } + + @Test + public void testConvertZeroMillis() { + assertEquals(0, TimeoutUtils.toMillis(0, TimeUnit.SECONDS)); + } + + @Test + public void testConvertNegativeMillisGreaterThanNegativeOne() { + // Ensure we convert this to 0 as before, though ideally we wouldn't accept negative values + assertEquals(0,TimeoutUtils.toMillis(-123, TimeUnit.MICROSECONDS)); + } + + @Test + public void testConvertNegativeMillisEqualNegativeOne() { + assertEquals(-1,TimeoutUtils.toMillis(-1111, TimeUnit.MICROSECONDS)); + } + + @Test + public void testConvertNegativeMillisLessThanNegativeOne() { + assertEquals(-2,TimeoutUtils.toMillis(-2344, TimeUnit.MICROSECONDS)); + } } diff --git a/src/test/resources/org/springframework/data/redis/core/RedisTemplateTests-context.xml b/src/test/resources/org/springframework/data/redis/core/RedisTemplateTests-context.xml new file mode 100644 index 000000000..6ae329887 --- /dev/null +++ b/src/test/resources/org/springframework/data/redis/core/RedisTemplateTests-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file