diff --git a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java index f01804da6..4253959ea 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicDouble.java @@ -18,6 +18,8 @@ package org.springframework.data.redis.support.atomic; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; +import java.util.function.DoubleUnaryOperator; +import java.util.function.DoubleBinaryOperator; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.redis.connection.DataType; @@ -39,6 +41,7 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster */ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyOperations { @@ -218,6 +221,44 @@ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyO return addAndGet(delta) - delta; } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the previous value. + */ + public double getAndUpdate(DoubleUnaryOperator updateFunction) { + + double previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsDouble(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the previous value. + */ + public double getAndAccumulate(double updateValue, DoubleBinaryOperator accumulatorFunction) { + + double previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsDouble(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + /** * Atomically increments by one the current value. * @@ -246,6 +287,44 @@ public class RedisAtomicDouble extends Number implements Serializable, BoundKeyO return operations.increment(key, delta); } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the updated value. + */ + public double updateAndGet(DoubleUnaryOperator updateFunction) { + + double previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsDouble(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the updated value. + */ + public double accumulateAndGet(double updateValue, DoubleBinaryOperator accumulatorFunction) { + + double previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsDouble(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + /** * @return the String representation of the current value. */ diff --git a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java index 265bd8e7a..c6c37badf 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicInteger.java @@ -18,6 +18,8 @@ package org.springframework.data.redis.support.atomic; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; +import java.util.function.IntBinaryOperator; +import java.util.function.IntUnaryOperator; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.redis.connection.DataType; @@ -39,6 +41,7 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster * @see java.util.concurrent.atomic.AtomicInteger */ public class RedisAtomicInteger extends Number implements Serializable, BoundKeyOperations { @@ -216,6 +219,44 @@ public class RedisAtomicInteger extends Number implements Serializable, BoundKey return addAndGet(delta) - delta; } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the previous value. + */ + public int getAndUpdate(IntUnaryOperator updateFunction) { + + int previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsInt(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the previous value. + */ + public int getAndAccumulate(int updateValue, IntBinaryOperator accumulatorFunction) { + + int previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsInt(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + /** * Atomically increment by one the current value. * @@ -244,6 +285,44 @@ public class RedisAtomicInteger extends Number implements Serializable, BoundKey return operations.increment(key, delta).intValue(); } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the updated value. + */ + public int updateAndGet(IntUnaryOperator updateFunction) { + + int previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsInt(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the updated value. + */ + public int accumulateAndGet(int updateValue, IntBinaryOperator accumulatorFunction) { + + int previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsInt(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + /** * @return the String representation of the current value. */ diff --git a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java index c36b31f9a..04e0bc5b9 100644 --- a/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java +++ b/src/main/java/org/springframework/data/redis/support/atomic/RedisAtomicLong.java @@ -18,6 +18,8 @@ package org.springframework.data.redis.support.atomic; import java.io.Serializable; import java.util.Date; import java.util.concurrent.TimeUnit; +import java.util.function.LongBinaryOperator; +import java.util.function.LongUnaryOperator; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.redis.connection.DataType; @@ -40,6 +42,7 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster * @see java.util.concurrent.atomic.AtomicLong */ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOperations { @@ -223,6 +226,44 @@ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOpe return addAndGet(delta) - delta; } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the previous value. + */ + public long getAndUpdate(LongUnaryOperator updateFunction) { + + long previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsLong(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the previous value. + */ + public long getAndAccumulate(long updateValue, LongBinaryOperator accumulatorFunction) { + + long previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsLong(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return previousValue; + } + /** * Atomically increments by one the current value. * @@ -251,6 +292,44 @@ public class RedisAtomicLong extends Number implements Serializable, BoundKeyOpe return operations.increment(key, delta); } + /** + * Atomically update the current value using the given update function. + * + * @param updateFunction the function which calculates the value to set. Should be a pure function (no side effects), + * because it will be applied several times if update attempts fail due to concurrent calls. + * @return the updated value. + */ + public long updateAndGet(LongUnaryOperator updateFunction) { + + long previousValue, newValue; + do { + previousValue = get(); + newValue = updateFunction.applyAsLong(previousValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + + /** + * Atomically update the current value using the given accumulator function. + * The new value is calculated by applying the accumulator function to the current value and the + * given `updateValue`. + * + * @param updateValue the value which will be passed into the accumulator function. + * @param accumulatorFunction the function which calculates the value to set. Should be a pure function (no side + * effects), because it will be applied several times if update attempts fail due to + * concurrent calls. + * @return the updated value. + */ + public long accumulateAndGet(long updateValue, LongBinaryOperator accumulatorFunction) { + + long previousValue, newValue; + do { + previousValue = get(); + newValue = accumulatorFunction.applyAsLong(previousValue, updateValue); + } while (!compareAndSet(previousValue, newValue)); + return newValue; + } + /** * @return the String representation of the current value. */ diff --git a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleTests.java index 860afdb69..e1cea1e3a 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicDoubleTests.java @@ -20,6 +20,9 @@ import static org.assertj.core.api.Assertions.*; import java.util.Collection; import java.util.Date; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.DoubleUnaryOperator; +import java.util.function.DoubleBinaryOperator; import org.assertj.core.data.Offset; import org.junit.After; @@ -43,6 +46,7 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster */ @RunWith(Parameterized.class) public class RedisAtomicDoubleTests extends AbstractRedisAtomicsTests { @@ -225,4 +229,96 @@ public class RedisAtomicDoubleTests extends AbstractRedisAtomicsTests { assertThat(test.getAndSet(2)).isZero(); } + + @Test // DATAREDIS-874 + public void updateAndGetAppliesGivenUpdateFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + double initialValue = 5.3; + double expectedNewValue = 10.6; + doubleCounter.set(initialValue); + + DoubleUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + double result = doubleCounter.updateAndGet(updateFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(doubleCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndUpdateAppliesGivenUpdateFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + double initialValue = 5.3; + double expectedNewValue = 10.6; + doubleCounter.set(initialValue); + + DoubleUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + double result = doubleCounter.getAndUpdate(updateFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(doubleCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void accumulateAndGetAppliesGivenAccumulatorFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + double initialValue = 5.3; + double expectedNewValue = 10.6; + doubleCounter.set(initialValue); + + DoubleBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + double result = doubleCounter.accumulateAndGet(15.9, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(doubleCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndAccumulateAppliesGivenAccumulatorFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + double initialValue = 5.3; + double expectedNewValue = 10.6; + doubleCounter.set(initialValue); + + DoubleBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + double result = doubleCounter.getAndAccumulate(15.9, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(doubleCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } } diff --git a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerTests.java index 35d10c7fb..8cc1ff8e6 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicIntegerTests.java @@ -20,6 +20,8 @@ import static org.assertj.core.api.Assertions.*; import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntBinaryOperator; +import java.util.function.IntUnaryOperator; import org.junit.After; import org.junit.AfterClass; @@ -43,6 +45,7 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster */ @RunWith(Parameterized.class) public class RedisAtomicIntegerTests extends AbstractRedisAtomicsTests { @@ -233,4 +236,96 @@ public class RedisAtomicIntegerTests extends AbstractRedisAtomicsTests { assertThat(test.getAndSet(2)).isZero(); } + + @Test // DATAREDIS-874 + public void updateAndGetAppliesGivenUpdateFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + int initialValue = 5; + int expectedNewValue = 10; + intCounter.set(initialValue); + + IntUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + int result = intCounter.updateAndGet(updateFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(intCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndUpdateAppliesGivenUpdateFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + int initialValue = 5; + int expectedNewValue = 10; + intCounter.set(initialValue); + + IntUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + int result = intCounter.getAndUpdate(updateFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(intCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void accumulateAndGetAppliesGivenAccumulatorFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + int initialValue = 5; + int expectedNewValue = 10; + intCounter.set(initialValue); + + IntBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + int result = intCounter.accumulateAndGet(15, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(intCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndAccumulateAppliesGivenAccumulatorFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + int initialValue = 5; + int expectedNewValue = 10; + intCounter.set(initialValue); + + IntBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + int result = intCounter.getAndAccumulate(15, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(intCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } } diff --git a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongTests.java b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongTests.java index 380341543..8d14ff1c9 100644 --- a/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongTests.java +++ b/src/test/java/org/springframework/data/redis/support/atomic/RedisAtomicLongTests.java @@ -18,6 +18,9 @@ package org.springframework.data.redis.support.atomic; import static org.assertj.core.api.Assertions.*; import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongBinaryOperator; +import java.util.function.LongUnaryOperator; import org.junit.After; import org.junit.AfterClass; @@ -41,6 +44,7 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch + * @author Graham MacMaster */ @RunWith(Parameterized.class) public class RedisAtomicLongTests extends AbstractRedisAtomicsTests { @@ -214,4 +218,96 @@ public class RedisAtomicLongTests extends AbstractRedisAtomicsTests { assertThat(test.getAndSet(2)).isZero(); } + + @Test // DATAREDIS-874 + public void updateAndGetAppliesGivenUpdateFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + long initialValue = 5; + long expectedNewValue = 10; + longCounter.set(initialValue); + + LongUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + long result = longCounter.updateAndGet(updateFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(longCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndUpdateAppliesGivenUpdateFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + long initialValue = 5; + long expectedNewValue = 10; + longCounter.set(initialValue); + + LongUnaryOperator updateFunction = input -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + long result = longCounter.getAndUpdate(updateFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(longCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void accumulateAndGetAppliesGivenAccumulatorFunctionAndReturnsUpdatedValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + long initialValue = 5; + long expectedNewValue = 10; + longCounter.set(initialValue); + + LongBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + long result = longCounter.accumulateAndGet(15L, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(expectedNewValue); + assertThat(longCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } + + @Test // DATAREDIS-874 + public void getAndAccumulateAppliesGivenAccumulatorFunctionAndReturnsOriginalValue() { + + // Arrange + AtomicBoolean operatorHasBeenApplied = new AtomicBoolean(false); + long initialValue = 5; + long expectedNewValue = 10; + longCounter.set(initialValue); + + LongBinaryOperator accumulatorFunction = (x, y) -> { + operatorHasBeenApplied.set(true); + return expectedNewValue; + }; + + // Act + long result = longCounter.getAndAccumulate(15L, accumulatorFunction); + + // Assert + assertThat(result).isEqualTo(initialValue); + assertThat(longCounter.get()).isEqualTo(expectedNewValue); + assertThat(operatorHasBeenApplied.get()).isTrue(); + } }