Class IdempotencyResolver
Idempotent
protection (issue #91): derives the key, consults the
IdempotencyStore, decides between
returning the cached outcome and re-running the
call body, then writes the new outcome back to the store on a
cache miss.
The resolver is the wiring layer that connects the primitives
shipped in PR #108 (IdempotencyKey, IdempotencyStore,
IdempotencyEntry) to a real call site. Call sites
(ToolMethodDispatcher today; the
action executor in a follow-up) hand the resolver an
Idempotent annotation, the tool / action identifier, the
argument map, the optional IdempotencyKeySupplier for the
KeyStrategy.EXPLICIT path, and a Callable that runs
the underlying body. The resolver does not know or care what the
body does.
Cache-hit semantics
Decided byIdempotent.onCacheHit():
RetryBehavior.RETURN_CACHED— return the cached result (success or failure as-cached) without re-running the body.RetryBehavior.RETURN_CACHED_IF_SUCCESS— return cached results; on a cached failure, ignore the cache and re-run the body (fresh attempt, fresh failure cached ifIdempotent.cacheFailures()is on).RetryBehavior.FAIL_FAST— throwIdempotencyExceptionwithIdempotencyException.IdempotencyConflict.FAIL_FAST_HITon any cache hit.
Failure caching
Off by default (Idempotent.cacheFailures()). When on, the
resolver caches a failure entry
after a body throw, then re-throws the original. Subsequent retries
see the cached failure and apply the onCacheHit policy.
Concurrency
Best-effort dedup, not single-flight: two parallel calls with the same key both miss the cache, both run, and the later writer wins. Acceptable for the primary use case (sequential retries of one call) and matches theInMemoryIdempotencyStore's lazy-eviction
model. Strict single-flight (collapse parallel calls to one
execution) is reserved for a future store-side primitive.- Since:
- 0.8.6 (issue #91 — wiring follow-up to PR #108)
-
Method Summary
Modifier and TypeMethodDescriptionstatic StringderiveKey(KeyStrategy strategy, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier) Compute the idempotency key for(toolName, input)understrategy.static Objectexecute(Idempotent annotation, IdempotencyStore store, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier, Callable<Object> body) Runbodyunder the protection described byannotation, routing throughstore.static Objectexecute(Idempotent annotation, IdempotencyStore store, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier, Callable<Object> body, boolean strictStoreWrites) Same asexecute(Idempotent, IdempotencyStore, String, Map, IdempotencyKeySupplier, Callable), with strict store-write semantics.
-
Method Details
-
execute
public static Object execute(Idempotent annotation, IdempotencyStore store, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier, Callable<Object> body) throws ExceptionRunbodyunder the protection described byannotation, routing throughstore.When
annotationisnullor its strategy isKeyStrategy.NONE, the resolver short-circuits and runsbodyunprotected — same observable behaviour asbody.call()but funnelled through the resolver so call sites have one entry point.- Parameters:
annotation- theIdempotentannotation discovered on the tool / action;nullmeans "no protection, just run the body"store- the store to consult; ignored whenannotation == nullor strategy isKeyStrategy.NONEtoolName- identifies the call site — namespaces the key so two tools with identical inputs don't collideinput- the call's argument map; treated as empty when nullkeySupplier- optional supplier forKeyStrategy.EXPLICIT; required when strategy is EXPLICIT, ignored otherwisebody- the underlying call to run on a cache miss- Returns:
- the body's result on a miss, or the cached result on a hit
- Throws:
Exception- re-throws whateverbodythrows on a miss (and on a re-run after a cached failure)IdempotencyException- on FAIL_FAST cache hit, missing EXPLICIT supplier, or supplier throwing
-
execute
public static Object execute(Idempotent annotation, IdempotencyStore store, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier, Callable<Object> body, boolean strictStoreWrites) throws ExceptionSame asexecute(Idempotent, IdempotencyStore, String, Map, IdempotencyKeySupplier, Callable), with strict store-write semantics.When
strictStoreWritesistrue, a failedIdempotencyStore.put(java.lang.String, com.tnsai.idempotency.IdempotencyEntry, java.time.Duration)after the body ran successfully surfaces as anIdempotencyException(IdempotencyException.IdempotencyConflict.STORE_UNAVAILABLE) instead of a warn-and-continue: for payment-grade tools a lost record is a future duplicate, so the caller must see the failure and reconcile. The failure-caching path is unaffected because it is reached only when the body itself threw: a failed success-write surfaces to the caller and stores nothing, rather than being recorded as the body's own failure.- Throws:
Exception- Since:
- 0.16.0 — this seven-argument signature. The six-argument form it replaces is gone; see the BREAKING entry in CHANGELOG.md.
-
deriveKey
public static String deriveKey(KeyStrategy strategy, String toolName, Map<String, Object> input, IdempotencyKeySupplier keySupplier) Compute the idempotency key for(toolName, input)understrategy. Public so call sites that need the key for an out-of-band purpose (e.g. injecting an HTTPIdempotency-Keyheader inWebServiceExecutor, issue #91 phase 2) can derive it identically to whatexecute(com.tnsai.idempotency.Idempotent, com.tnsai.idempotency.IdempotencyStore, java.lang.String, java.util.Map<java.lang.String, java.lang.Object>, com.tnsai.idempotency.IdempotencyKeySupplier, java.util.concurrent.Callable<java.lang.Object>)would derive internally.The returned key is the one an upstream service sees, and it never carries caller identity. Inside an
InvocationContext,execute(com.tnsai.idempotency.Idempotent, com.tnsai.idempotency.IdempotencyStore, java.lang.String, java.util.Map<java.lang.String, java.lang.Object>, com.tnsai.idempotency.IdempotencyKeySupplier, java.util.concurrent.Callable<java.lang.Object>)additionally prefixes it with the caller's partition before reading or writing the store, so stored results stay separated per caller.Always pair this with the same
keySupplierthatexecute(com.tnsai.idempotency.Idempotent, com.tnsai.idempotency.IdempotencyStore, java.lang.String, java.util.Map<java.lang.String, java.lang.Object>, com.tnsai.idempotency.IdempotencyKeySupplier, java.util.concurrent.Callable<java.lang.Object>)sees, otherwise EXPLICIT calls would diverge —IdempotencyKeySupplierimplementations should be deterministic so the double-call (one for the header, one for the cache lookup) returns the same string.- Throws:
IdempotencyException- same conditions as the internal path — EXPLICIT without a supplier, EXPLICIT supplier throwing, or supplier returning blank- Since:
- 0.8.6 (issue #91 phase 2)
-