33import com .byteentropy .idempotency_core .annotation .Idempotent ;
44import com .byteentropy .idempotency_core .model .IdempotencyRecord ;
55import com .byteentropy .idempotency_core .model .IdempotencyStatus ;
6- import com .byteentropy .idempotency_core .storage .IdempotencyStore ;
7- import com .fasterxml .jackson .core .JsonProcessingException ;
8- import com .fasterxml .jackson .databind .DeserializationFeature ;
9- import com .fasterxml .jackson .databind .ObjectMapper ;
10- import com .fasterxml .jackson .databind .SerializationFeature ;
6+ import com .byteentropy .idempotency_core .service .IdempotencyService ;
117import org .aspectj .lang .ProceedingJoinPoint ;
128import org .aspectj .lang .annotation .Around ;
139import org .aspectj .lang .annotation .Aspect ;
2319import org .springframework .stereotype .Component ;
2420import org .springframework .util .StringUtils ;
2521
26- import java .lang .reflect .Method ;
27- import java .nio .charset .StandardCharsets ;
28- import java .security .MessageDigest ;
29- import java .security .NoSuchAlgorithmException ;
30- import java .util .HexFormat ;
3122import java .util .Objects ;
3223
3324@ Aspect
3425@ Component
3526public class IdempotencyAspect implements Ordered {
36-
3727 private static final Logger log = LoggerFactory .getLogger (IdempotencyAspect .class );
38- private final IdempotencyStore store ;
28+
29+ private final IdempotencyService idempotencyService ;
3930 private final ExpressionParser parser = new SpelExpressionParser ();
40- private final ObjectMapper hashMapper ;
4131
4232 @ Value ("${idempotency.default-ttl:3600}" )
4333 private long globalDefaultTtl ;
4434
45- @ Value ("${idempotency.processing-timeout-ms:300000}" )
46- private long processingTimeoutMs ;
47-
48- public IdempotencyAspect (IdempotencyStore store ) {
49- this .store = store ;
50- // Pre-configure a safe mapper for hashing to avoid GC pressure from frequent .copy() calls
51- this .hashMapper = store .getObjectMapper ().copy ()
52- .disable (SerializationFeature .FAIL_ON_EMPTY_BEANS )
53- .disable (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES );
35+ public IdempotencyAspect (IdempotencyService idempotencyService ) {
36+ this .idempotencyService = idempotencyService ;
5437 }
5538
5639 @ Override
@@ -61,77 +44,43 @@ public int getOrder() {
6144 @ Around ("@annotation(com.byteentropy.idempotency_core.annotation.Idempotent)" )
6245 public Object handle (ProceedingJoinPoint joinPoint ) throws Throwable {
6346 MethodSignature signature = (MethodSignature ) joinPoint .getSignature ();
64- Method method = signature .getMethod ();
65- Idempotent idempotent = method .getAnnotation (Idempotent .class );
47+ Idempotent idempotent = signature .getMethod ().getAnnotation (Idempotent .class );
6648
67- return execute (joinPoint , idempotent );
68- }
69-
70- private Object execute (ProceedingJoinPoint joinPoint , Idempotent idempotent ) throws Throwable {
7149 String namespace = idempotent .namespace ();
7250 String key = resolveSpel (joinPoint , idempotent .key ());
7351 long finalTtl = (idempotent .ttl () > 0 ) ? idempotent .ttl () : globalDefaultTtl ;
7452
75- // Validation
76- if (!StringUtils .hasText (namespace )) {
77- throw new IllegalArgumentException ("Idempotency namespace is required." );
78- }
79- if (!StringUtils .hasText (key )) {
80- throw new IllegalArgumentException ("Idempotency key evaluated to empty/null." );
53+ if (!StringUtils .hasText (namespace ) || !StringUtils .hasText (key )) {
54+ throw new IllegalArgumentException ("Idempotency namespace and key are required." );
8155 }
8256
83- String currentRequestHash = generateRequestHash (joinPoint .getArgs ());
57+ String currentHash = idempotencyService .generateHash (joinPoint .getArgs ());
58+ IdempotencyRecord existing = idempotencyService .attemptReservation (namespace , key , currentHash , finalTtl );
8459
85- IdempotencyRecord initial = IdempotencyRecord .builder ()
86- .status (IdempotencyStatus .PROCESSING )
87- .requestHash (currentRequestHash )
88- .timestamp (System .currentTimeMillis ())
89- .build ();
90-
91- // Atomic check-and-reserve
92- Object resultFromStore = store .executeLua (namespace , key , initial , finalTtl );
93-
94- if (resultFromStore != null ) {
95- IdempotencyRecord existing = (IdempotencyRecord ) resultFromStore ;
96-
97- // 1. Handle concurrent processing
60+ if (existing != null ) {
9861 if (existing .getStatus () == IdempotencyStatus .PROCESSING ) {
9962 long elapsed = System .currentTimeMillis () - existing .getTimestamp ();
100- if (elapsed > processingTimeoutMs ) {
101- log .warn ("Ghost lock detected for {}:{}. Clearing and retrying ." , namespace , key );
102- store . delete (namespace , key );
103- return execute (joinPoint , idempotent );
63+ if (elapsed > idempotencyService . getProcessingTimeoutMs () ) {
64+ log .warn ("Ghost lock detected for {}:{}. Retrying ." , namespace , key );
65+ idempotencyService . rollback (namespace , key );
66+ return handle (joinPoint );
10467 }
10568 throw new RuntimeException ("Request is currently being processed." );
10669 }
10770
108- // 2. Validate payload integrity (Same key, different data)
109- if (!Objects .equals (existing .getRequestHash (), currentRequestHash )) {
71+ if (!Objects .equals (existing .getRequestHash (), currentHash )) {
11072 throw new IllegalStateException ("Idempotency Conflict: Key exists with different payload." );
11173 }
11274
113- // 3. Return cached response
114- log .info ("Returning cached response for {}:{}" , namespace , key );
11575 return existing .getResponse ();
11676 }
11777
11878 try {
119- // Execute business logic
12079 Object response = joinPoint .proceed ();
121-
122- IdempotencyRecord completed = IdempotencyRecord .builder ()
123- .status (IdempotencyStatus .COMPLETED )
124- .response (response )
125- .requestHash (currentRequestHash )
126- .timestamp (System .currentTimeMillis ())
127- .build ();
128-
129- store .save (namespace , key , completed , finalTtl );
80+ idempotencyService .commit (namespace , key , currentHash , response , finalTtl );
13081 return response ;
13182 } catch (Throwable e ) {
132- // Clean up the lock on failure so the client can retry
133- log .error ("Execution failed for {}:{}. Clearing lock." , namespace , key );
134- store .delete (namespace , key );
83+ idempotencyService .rollback (namespace , key );
13584 throw e ;
13685 }
13786 }
@@ -150,32 +99,4 @@ private String resolveSpel(ProceedingJoinPoint joinPoint, String spel) {
15099 context .setVariable ("methodName" , signature .getMethod ().getName ());
151100 return parser .parseExpression (spel ).getValue (context , String .class );
152101 }
153-
154- private String generateRequestHash (Object [] args ) {
155- if (args == null || args .length == 0 ) return "no-args" ;
156-
157- try {
158- StringBuilder sb = new StringBuilder ();
159- for (Object arg : args ) {
160- if (arg == null ) {
161- sb .append ("null" );
162- } else {
163- sb .append (hashMapper .writeValueAsString (arg ));
164- }
165- }
166-
167- MessageDigest digest = MessageDigest .getInstance ("SHA-256" );
168- byte [] encodedHash = digest .digest (sb .toString ().getBytes (StandardCharsets .UTF_8 ));
169- return HexFormat .of ().formatHex (encodedHash );
170-
171- } catch (NoSuchAlgorithmException e ) {
172- log .error ("SHA-256 not available, falling back to identity hash." );
173- return "fallback-id-" + Objects .hash (args );
174- } catch (JsonProcessingException e ) {
175- log .warn ("Serialization for hashing failed. Falling back to identity hash." );
176- return "fallback-json-" + Objects .hash (args );
177- } catch (Exception e ) {
178- return "hash-err-" + Objects .hash (args );
179- }
180- }
181102}
0 commit comments