Skip to content

Commit 3383cd2

Browse files
committed
Fix bug: We need to pass the return type explicitly to support deserialization to the return type of the method.
1 parent 8c56573 commit 3383cd2

File tree

4 files changed

+114
-35
lines changed

4 files changed

+114
-35
lines changed

powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/PowertoolsIdempotency.java

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,48 @@
3030
* <p>This class is thread-safe. All operations delegate to the underlying persistence store
3131
* which handles concurrent access safely.</p>
3232
*
33-
* <p>Example usage with Supplier:</p>
33+
* <p><strong>Important:</strong> Always call {@link Idempotency#registerLambdaContext(Context)}
34+
* at the start of your handler to enable proper timeout handling.</p>
35+
*
36+
* <p>Example usage with Function (single parameter):</p>
3437
* <pre>{@code
35-
* public List<String> handleRequest(SQSEvent event, Context context) {
38+
* public Basket handleRequest(Product input, Context context) {
3639
* Idempotency.registerLambdaContext(context);
37-
* return event.getRecords().stream()
38-
* .map(record -> PowertoolsIdempotency.makeIdempotent(
39-
* record.getBody(), // used as idempotency key
40-
* () -> processPayment(record.getMessageId(), record.getBody())))
41-
* .collect(Collectors.toList());
40+
* return PowertoolsIdempotency.makeIdempotent(this::processProduct, input, Basket.class);
41+
* }
42+
*
43+
* private Basket processProduct(Product product) {
44+
* // business logic
4245
* }
4346
* }</pre>
4447
*
45-
* <p>Example usage with Function:</p>
48+
* <p>Example usage with Supplier (multi-parameter methods):</p>
4649
* <pre>{@code
47-
* public Basket handleRequest(Product input, Context context) {
50+
* public String handleRequest(SQSEvent event, Context context) {
4851
* Idempotency.registerLambdaContext(context);
49-
* return PowertoolsIdempotency.makeIdempotent(this::process, input);
52+
* return PowertoolsIdempotency.makeIdempotent(
53+
* event.getRecords().get(0).getBody(),
54+
* () -> processPayment(orderId, amount, currency),
55+
* String.class
56+
* );
5057
* }
5158
* }</pre>
5259
*
53-
* <p>When different methods use the same payload as idempotency key, use
54-
* {@link #makeIdempotent(String, Object, Supplier)} with explicit function names
60+
* <p>When different methods use the same payload as idempotency key, use explicit function names
5561
* to differentiate between them:</p>
5662
* <pre>{@code
5763
* // Different methods, same payload
58-
* PowertoolsIdempotency.makeIdempotent("processPayment", orderId, () -> processPayment(orderId));
59-
* PowertoolsIdempotency.makeIdempotent("refundPayment", orderId, () -> refundPayment(orderId));
64+
* PowertoolsIdempotency.makeIdempotent("processPayment", orderId,
65+
* () -> processPayment(orderId), String.class);
66+
*
67+
* PowertoolsIdempotency.makeIdempotent("refundPayment", orderId,
68+
* () -> refundPayment(orderId), String.class);
6069
* }</pre>
6170
*
6271
* @see Idempotency
63-
* @see #makeIdempotent(Object, Supplier)
64-
* @see #makeIdempotent(String, Object, Supplier)
65-
* @see #makeIdempotent(Function, Object)
72+
* @see #makeIdempotent(Object, Supplier, Class)
73+
* @see #makeIdempotent(String, Object, Supplier, Class)
74+
* @see #makeIdempotent(Function, Object, Class)
6675
*/
6776
public final class PowertoolsIdempotency {
6877

@@ -80,20 +89,22 @@ private PowertoolsIdempotency() {
8089
* such as batch processors.</p>
8190
*
8291
* <p>This method is suitable for making methods idempotent that have more than one parameter.
83-
* For simple single-parameter methods, {@link #makeIdempotent(Function, Object)} is more intuitive.</p>
92+
* For simple single-parameter methods, {@link #makeIdempotent(Function, Object, Class)} is more intuitive.</p>
8493
*
8594
* <p><strong>Note:</strong> If you need to call different functions with the same payload,
86-
* use {@link #makeIdempotent(String, Object, Supplier)} to specify distinct function names.
95+
* use {@link #makeIdempotent(String, Object, Supplier, Class)} to specify distinct function names.
8796
* This ensures each function has its own idempotency scope.</p>
8897
*
8998
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
9099
* @param function the function to make idempotent
100+
* @param returnType the class of the return type for deserialization
91101
* @param <T> the return type of the function
92102
* @return the result of the function execution (either fresh or cached)
93103
* @throws Throwable if the function execution fails
94104
*/
95-
public static <T> T makeIdempotent(Object idempotencyKey, Supplier<T> function) throws Throwable {
96-
return makeIdempotent(DEFAULT_FUNCTION_NAME, idempotencyKey, function);
105+
public static <T> T makeIdempotent(Object idempotencyKey, Supplier<T> function, Class<T> returnType)
106+
throws Throwable {
107+
return makeIdempotent(DEFAULT_FUNCTION_NAME, idempotencyKey, function, returnType);
97108
}
98109

99110
/**
@@ -102,25 +113,24 @@ public static <T> T makeIdempotent(Object idempotencyKey, Supplier<T> function)
102113
* <p>This method is thread-safe and can be used in parallel processing scenarios
103114
* such as batch processors.</p>
104115
*
105-
* <p>Note: The return type is inferred from the actual result. For cached responses,
106-
* deserialization uses the runtime type of the stored result.</p>
107-
*
108116
* @param functionName the name of the function (used for persistence store configuration)
109117
* @param idempotencyKey the key used for idempotency (will be converted to JSON)
110118
* @param function the function to make idempotent
119+
* @param returnType the class of the return type for deserialization
111120
* @param <T> the return type of the function
112121
* @return the result of the function execution (either fresh or cached)
113122
* @throws Throwable if the function execution fails
114123
*/
115124
@SuppressWarnings("unchecked")
116-
public static <T> T makeIdempotent(String functionName, Object idempotencyKey, Supplier<T> function)
125+
public static <T> T makeIdempotent(String functionName, Object idempotencyKey, Supplier<T> function,
126+
Class<T> returnType)
117127
throws Throwable {
118128
JsonNode payload = JsonConfig.get().getObjectMapper().valueToTree(idempotencyKey);
119129
Context lambdaContext = Idempotency.getInstance().getConfig().getLambdaContext();
120130

121131
IdempotencyHandler handler = new IdempotencyHandler(
122132
function::get,
123-
Object.class,
133+
returnType,
124134
functionName,
125135
payload,
126136
lambdaContext);
@@ -133,20 +143,21 @@ public static <T> T makeIdempotent(String functionName, Object idempotencyKey, S
133143
* Makes a function with one parameter idempotent.
134144
* The parameter is used as the idempotency key.
135145
*
136-
* <p>For functions with more than one parameter, use {@link #makeIdempotent(Object, Supplier)} instead.</p>
146+
* <p>For functions with more than one parameter, use {@link #makeIdempotent(Object, Supplier, Class)} instead.</p>
137147
*
138148
* <p><strong>Note:</strong> If you need to call different functions with the same argument,
139-
* use {@link #makeIdempotent(String, Object, Supplier)} to specify distinct function names.</p>
149+
* use {@link #makeIdempotent(String, Object, Supplier, Class)} to specify distinct function names.</p>
140150
*
141151
* @param function the function to make idempotent (method reference)
142152
* @param arg the argument to pass to the function (also used as idempotency key)
153+
* @param returnType the class of the return type for deserialization
143154
* @param <T> the argument type
144155
* @param <R> the return type
145156
* @return the result of the function execution (either fresh or cached)
146157
* @throws Throwable if the function execution fails
147158
*/
148-
public static <T, R> R makeIdempotent(Function<T, R> function, T arg) throws Throwable {
149-
return makeIdempotent(DEFAULT_FUNCTION_NAME, arg, () -> function.apply(arg));
159+
public static <T, R> R makeIdempotent(Function<T, R> function, T arg, Class<R> returnType) throws Throwable {
160+
return makeIdempotent(DEFAULT_FUNCTION_NAME, arg, () -> function.apply(arg), returnType);
150161
}
151162

152163
}

powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/PowertoolsIdempotencyTest.java

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import static org.mockito.Mockito.spy;
2020
import static org.mockito.Mockito.verify;
2121

22+
import java.time.Instant;
23+
import java.util.HashMap;
24+
import java.util.Map;
2225
import java.util.OptionalInt;
2326

2427
import org.junit.jupiter.api.Test;
@@ -31,11 +34,14 @@
3134
import com.fasterxml.jackson.databind.JsonNode;
3235

3336
import software.amazon.lambda.powertools.common.stubs.TestLambdaContext;
37+
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException;
38+
import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException;
3439
import software.amazon.lambda.powertools.idempotency.handlers.PowertoolsIdempotencyFunction;
3540
import software.amazon.lambda.powertools.idempotency.handlers.PowertoolsIdempotencyMultiArgFunction;
3641
import software.amazon.lambda.powertools.idempotency.model.Basket;
3742
import software.amazon.lambda.powertools.idempotency.model.Product;
3843
import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore;
44+
import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
3945

4046
@ExtendWith(MockitoExtension.class)
4147
class PowertoolsIdempotencyTest {
@@ -83,7 +89,8 @@ void testMakeIdempotentWithFunctionName() throws Throwable {
8389
.configure();
8490
Idempotency.registerLambdaContext(context);
8591

86-
String result = PowertoolsIdempotency.makeIdempotent("myFunction", "test-key", () -> "test-result");
92+
String result = PowertoolsIdempotency.makeIdempotent("myFunction", "test-key", () -> "test-result",
93+
String.class);
8794

8895
assertThat(result).isEqualTo("test-result");
8996

@@ -100,7 +107,7 @@ void testMakeIdempotentWithMethodReferenceUsesDefaultName() throws Throwable {
100107
.configure();
101108
Idempotency.registerLambdaContext(context);
102109

103-
String result = PowertoolsIdempotency.makeIdempotent("test-key", this::helperMethod);
110+
String result = PowertoolsIdempotency.makeIdempotent("test-key", this::helperMethod, String.class);
104111

105112
assertThat(result).isEqualTo("helper-result");
106113

@@ -122,7 +129,7 @@ void testMakeIdempotentWithFunctionOverload() throws Throwable {
122129
Idempotency.registerLambdaContext(context);
123130

124131
Product p = new Product(42, "test product", 10);
125-
Basket result = PowertoolsIdempotency.makeIdempotent(this::processProduct, p);
132+
Basket result = PowertoolsIdempotency.makeIdempotent(this::processProduct, p, Basket.class);
126133

127134
assertThat(result.getProducts()).hasSize(1);
128135
assertThat(result.getProducts().get(0)).isEqualTo(p);
@@ -166,6 +173,66 @@ void firstCall_withExplicitIdempotencyKey_shouldPutInStore() {
166173
assertThat(resultCaptor.getValue()).isEqualTo(basket);
167174
}
168175

176+
@Test
177+
void secondCall_shouldRetrieveFromCacheAndDeserialize() throws Throwable {
178+
// Use in-memory persistence store to test actual serialization/deserialization
179+
Map<String, DataRecord> data = new HashMap<>();
180+
BasePersistenceStore inMemoryStore = new BasePersistenceStore() {
181+
@Override
182+
public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoundException {
183+
DataRecord dr = data.get(idempotencyKey);
184+
if (dr == null) {
185+
throw new IdempotencyItemNotFoundException(idempotencyKey);
186+
}
187+
return dr;
188+
}
189+
190+
@Override
191+
public void putRecord(DataRecord dr, Instant now) throws IdempotencyItemAlreadyExistsException {
192+
if (data.containsKey(dr.getIdempotencyKey())) {
193+
throw new IdempotencyItemAlreadyExistsException();
194+
}
195+
data.put(dr.getIdempotencyKey(), dr);
196+
}
197+
198+
@Override
199+
public void updateRecord(DataRecord dr) {
200+
data.put(dr.getIdempotencyKey(), dr);
201+
}
202+
203+
@Override
204+
public void deleteRecord(String idempotencyKey) {
205+
data.remove(idempotencyKey);
206+
}
207+
};
208+
209+
Idempotency.config()
210+
.withPersistenceStore(inMemoryStore)
211+
.configure();
212+
Idempotency.registerLambdaContext(context);
213+
214+
Product p = new Product(42, "test product", 10);
215+
int[] callCount = { 0 };
216+
217+
// First call - executes function and stores result
218+
Basket result1 = PowertoolsIdempotency.makeIdempotent(p, () -> {
219+
callCount[0]++;
220+
return processProduct(p);
221+
}, Basket.class);
222+
assertThat(result1.getProducts()).hasSize(1);
223+
assertThat(callCount[0]).isEqualTo(1);
224+
225+
// Second call - should retrieve from cache, deserialize, and NOT execute function
226+
Basket result2 = PowertoolsIdempotency.makeIdempotent(p, () -> {
227+
callCount[0]++;
228+
return processProduct(p);
229+
}, Basket.class);
230+
assertThat(result2.getProducts()).hasSize(1);
231+
assertThat(result2.getProducts().get(0).getId()).isEqualTo(42);
232+
assertThat(result2.getProducts().get(0).getName()).isEqualTo("test product");
233+
assertThat(callCount[0]).isEqualTo(1); // Function should NOT be called again
234+
}
235+
169236
@Test
170237
void concurrentInvocations_shouldNotLeakContext() throws Exception {
171238
Idempotency.config()

powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/PowertoolsIdempotencyFunction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public boolean processCalled() {
3737
public Basket handleRequest(Product input, Context context) {
3838
Idempotency.registerLambdaContext(context);
3939
try {
40-
return PowertoolsIdempotency.makeIdempotent(this::process, input);
40+
return PowertoolsIdempotency.makeIdempotent(this::process, input, Basket.class);
4141
} catch (Throwable e) {
4242
throw new RuntimeException(e);
4343
}

powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/PowertoolsIdempotencyMultiArgFunction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ public String getExtraData() {
4242
public Basket handleRequest(Product input, Context context) {
4343
Idempotency.registerLambdaContext(context);
4444
try {
45-
return PowertoolsIdempotency.makeIdempotent(input.getId(), () -> process(input, "extra-data"));
45+
return PowertoolsIdempotency.makeIdempotent(input.getId(), () -> process(input, "extra-data"),
46+
Basket.class);
4647
} catch (Throwable e) {
4748
throw new RuntimeException(e);
4849
}

0 commit comments

Comments
 (0)