Skip to content

Commit e82cb3b

Browse files
committed
Improve idempotency handling with deterministic key generation
Introduce deterministic idempotency key generation when a client-provided key is absent. The key is derived from the request payload combined with a time window to support safe retries.
1 parent 2ee6f22 commit e82cb3b

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.apache.fineract.commands.service;
2+
3+
4+
import org.apache.fineract.commands.domain.CommandWrapper;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.nio.charset.StandardCharsets;
8+
import java.security.MessageDigest;
9+
import java.util.Base64;
10+
11+
@Component
12+
public class DeterministicIdempotencyKeyGenerator {
13+
14+
private static final int BUCKET_MINUTES = 2;
15+
16+
public String generate(CommandWrapper wrapper) {
17+
String json = wrapper.getJson();
18+
19+
String normalizedPayload = normalize(json);
20+
21+
String bucket = currentTimeBucket();
22+
23+
String raw = normalizedPayload + "|" + bucket;
24+
25+
return sha256(raw);
26+
}
27+
28+
private String normalize(String json) {
29+
// IMPORTANT: make deterministic (order, spacing, etc.)
30+
return json.replaceAll("\\s+", "");
31+
}
32+
33+
private String currentTimeBucket() {
34+
long now = System.currentTimeMillis();
35+
long bucketMillis = BUCKET_MINUTES * 60 * 1000;
36+
long bucket = now / bucketMillis;
37+
return String.valueOf(bucket);
38+
}
39+
40+
private String sha256(String input) {
41+
try {
42+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
43+
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
44+
return Base64.getEncoder().encodeToString(hash);
45+
} catch (Exception e) {
46+
throw new RuntimeException("Failed to generate idempotency key", e);
47+
}
48+
}
49+
}

fineract-core/src/main/java/org/apache/fineract/commands/service/IdempotencyKeyResolver.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,28 @@ public class IdempotencyKeyResolver {
3030

3131
private final FineractRequestContextHolder fineractRequestContextHolder;
3232

33+
private final DeterministicIdempotencyKeyGenerator deterministicGenerator;
34+
3335
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
3436

3537
public String resolve(CommandWrapper wrapper) {
36-
return Optional.ofNullable(wrapper.getIdempotencyKey()).orElseGet(() -> getAttribute().orElseGet(idempotencyKeyGenerator::create));
38+
39+
// If wrapper already has key → use it
40+
if (wrapper.getIdempotencyKey() != null) {
41+
return wrapper.getIdempotencyKey();
42+
}
43+
44+
// If request attribute exists (internal retry)
45+
Optional<String> attributeKey = getAttribute();
46+
if (attributeKey.isPresent()) {
47+
return attributeKey.get();
48+
}
49+
50+
// hybrid logic (external retry)
51+
return deterministicGenerator.generate(wrapper);
3752
}
3853

54+
3955
private Optional<String> getAttribute() {
4056
return Optional.ofNullable(fineractRequestContextHolder.getAttribute(SynchronousCommandProcessingService.IDEMPOTENCY_KEY_ATTRIBUTE))
4157
.map(String::valueOf);

0 commit comments

Comments
 (0)