Skip to content

feat(engine): add renew operation to StoreHandler#1792

Merged
jfallows merged 2 commits into
developfrom
feature/store-handler-renew
May 23, 2026
Merged

feat(engine): add renew operation to StoreHandler#1792
jfallows merged 2 commits into
developfrom
feature/store-handler-renew

Conversation

@jfallows
Copy link
Copy Markdown
Contributor

Summary

  • Adds StoreHandler.renew(key, token, ttl, completion) to the engine SPI for callers that hold a coordination lock longer than its initial TTL — they schedule renewals at an interval shorter than the lease TTL, and a failed renewal signals that ownership has been lost so the caller can surrender state and let a new owner take over.
  • Brings the in-tree TestStoreHandler up to the same contract surface as the lock/unlock/watch SPI introduced in Add lock, unlock, and watch operations to StoreHandler #1790, so spec-level ITs can be written against type: test without depending on store-memory for correctness. TestStoreContext now supplies per-store watcher and lock maps shared across workers (mirroring the existing supplyEntries pattern); the new TestWatcher record carries the registering worker's signaler so cross-worker notify dispatches listener invocations onto the right thread.
  • store-memory and TestStoreHandler both implement renew with an atomic ConcurrentMap.replace against the previously-observed lock entry — token match under unexpired TTL returns the original token; mismatch or expiry returns null. Expired entries are evicted opportunistically, matching the unlock cleanup behaviour.

Commits

  • 02630399 test(engine): share watchers and locks per store across workers in TestStoreHandler — the cross-worker test infrastructure that the renew SPI tests depend on.
  • aba18122 feat(engine): add renew operation to StoreHandler — the SPI method plus store-memory and TestStoreHandler implementations, TestBindingFactory renew assertion, and the store.renew.yaml spec config.

Consumer

This SPI is consumed by binding-mcp's cache lifecycle-lock-hold change (on a separate branch, depends on this PR). That change schedules renew at leaseTtl / 3 for the worker that owns a binding's upstream lifecycle stream, so a single cache owner stays uninterrupted during normal operation and a holding node's crash triggers TTL-bounded takeover by another worker.

Test plan

  • ./mvnw clean verify -pl runtime/store-memory -amMemoryStoreHandlerIT 5/5 (including new shouldRenew).
  • ./mvnw clean install -DskipTests -DskipITs -pl runtime/engine -am — engine compiles after both commits.
  • CI verifies the full reactor.

https://claude.ai/code/session_01Gx5yC2CuFd54Fyoy7kL3qg


Generated by Claude Code

claude added 2 commits May 23, 2026 00:27
…stStoreHandler

The engine test store handler is reused across multiple specs to exercise
store lock/unlock/watch (introduced in PR #1790). Each worker constructs
its own TestStoreHandler, so per-store state — entries, watchers, locks —
must be shared via TestStoreContext to give cross-worker semantics that
match what production store implementations (memory, redis, hazelcast)
provide.

Extends TestStoreContext to supply per-store watcher and lock maps
(mirroring the existing supplyEntries pattern, keyed by storeConfig.id).
TestStoreHandler now stores TestWatcher records that carry the
registering worker's signaler, so a put on worker A correctly dispatches
the listener back onto the registering worker's I/O thread instead of
firing inline on whichever worker performed the put. Without this, a
listener registered on worker A but fired by worker B would access
state from the wrong thread, violating the engine's single-threaded-per-
worker contract.

The new TestLockEntry record carries the lease token and expiresAt for
ownership-checked unlock. TestStoreHandler.lock uses ConcurrentMap.compute
for atomic acquire-or-fail; unlock uses ConcurrentMap.computeIfPresent
with a token check so a worker that never acquired the lock cannot
release another worker's lock.

This brings the in-tree test store implementation up to the same
contract surface as the lock/unlock/watch SPI requires, so spec-level
ITs that rely on those operations can be written against `type: test`
without depending on `store-memory` for correctness.
Adds StoreHandler.renew(key, token, ttl, completion) to the engine SPI,
following the same ownership-checked, async-completion contract as
unlock. Callers that hold a coordination lock for longer than its
initial TTL — e.g. a singleton worker that owns a binding-scoped
resource for the lifetime of the binding — schedule renewals at an
interval shorter than the lease TTL. A failed renewal signals that
ownership has been lost (the lock was reacquired by another holder
after a TTL expiry), giving callers a deterministic cue to surrender
state and let the new owner take over.

store-memory and the engine TestStoreHandler implement renew with an
atomic ConcurrentMap.replace against the previously-observed
LockEntry: if the token matches the unexpired current holder, the
entry is replaced with a renewed expiresAt and the original token is
returned; otherwise null is returned. Expired entries are evicted
opportunistically, mirroring the unlock cleanup behaviour.

TestBindingFactory gains a renew assertion alongside the existing
lock/unlock/watch ops. The new spec config store-memory.spec/config/
store.renew.yaml exercises the full acquire-renew-release cycle for
the IT.
Copy link
Copy Markdown
Contributor Author

@jfallows jfallows left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jfallows jfallows self-assigned this May 23, 2026
@jfallows jfallows merged commit 9525778 into develop May 23, 2026
74 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants