Skip to content

Post-review amendments to the versioned CAS (lib-data-store-state-redis 1.2.1) [release] - #108

Merged
pditommaso merged 1 commit into
masterfrom
versioned-store-post-review-amendments
Aug 4, 2026
Merged

Post-review amendments to the versioned CAS (lib-data-store-state-redis 1.2.1) [release]#108
pditommaso merged 1 commit into
masterfrom
versioned-store-post-review-amendments

Conversation

@pditommaso

Copy link
Copy Markdown
Contributor

Amendments from the post-merge review of #107 (#107 (comment)).

Refuse a window-overrunning frame-looking head (point 4)

A foreign entry whose head starts {"@v": with a digit run of 22+ digits leaves no terminator inside the 28-byte head peek, so the frame pattern fell through to version 0 like an unframed entry — and a version-0 witness (the legacy-adoption flow) could blindly overwrite it. Store-written frames are never affected: a TSID is a positive long, so their head is at most 26 bytes.

The fix is confined to the fallthrough: one guard in the Lua (if not ver and string.match(head, '^{"@v":%d') then return 0 end) and the mirrored branch in LocalStateProvider.versionOf returning null, which the existing comparison already treats as never-match — replaceIf itself is unchanged on both providers. The new 22-digit test reproduced the blind overwrite before the fix and passes after, on both providers and at store level. The VersionProvider/Lua docs now state the refusal guarantee instead of the window caveat.

Restore plain-store test coverage (point 2)

AbstractStateStoreTest exercises the plain AbstractStateStore again (TTL expiry, custom TTL, both putIfAbsent forms, request-id mapping — still published contract, consumed by lib-pairing and lib-cmd-queue-redis); the versioned features moved to the new VersionedStateStoreTest. The duplicated should get and put a value feature is deduped; the pre-#107 test of the removed replaceIf(key, expected, value) API is not resurrected.

Make serialize/deserialize final (point 3)

An override dropping the frame would silently turn every CAS into a blind version-0 write. Non-breaking: nothing overrides them; payload customization goes through the encoding strategy.

Document the re-read rule (point 1)

No write returns the stamped version — a successful replaceIf invalidates the caller's own witness too. The README retry comment now names every cause of a false return, and the re-read-before-chaining rule is spelled out. Per review discussion, no replaceIfAndGet is added: no caller needs it today.

Releases lib-data-store-state-redis 1.2.1. check green for the module and both in-repo consumers.

🤖 Generated with Claude Code

…is 1.2.1) [release]

Amendments from the post-merge review of #107:

- Refuse a versioned replace against a foreign frame-looking head whose
  digit run overruns the inspected head window (22+ digits): previously
  it fell through to version 0 like an unframed entry, so a version-0
  witness could overwrite it. Store-written frames (at most 26 bytes)
  are unaffected. Covered on both providers and at store level.
- Make VersionedStateStore serialize/deserialize final: an override
  dropping the frame would silently turn every CAS into a blind
  version-0 write.
- Restore the plain-store coverage narrowed by #107: AbstractStateStoreTest
  exercises AbstractStateStore again; versioned features moved to the
  new VersionedStateStoreTest.
- Document that no write returns the stamped version - a successful
  replaceIf invalidates the caller's own witness too, re-read before
  chaining - and scope the README retry comment to every cause of a
  false return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jonmarti

jonmarti commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

LGTM.

All four points of #107 (comment) have been addressed.

Only one gap spotted: the re-read rule landed in the README but not in the replaceIf javadoc, which still describes false as "the key does not exist or the entry was written since the version carried by the value". Since the choice was to document rather than change the API, the doc should be where the mistake gets made, i.e. at the call site. One line on both overloads:

A successful replace invalidates the caller's own witness: the value passed in still carries the version of the read it derives from, so re-read before chaining another replace

Point 1 — documented rather than changed in the API. Agreed, given no caller needs replaceIfAndGet today.

Point 2 — fixed. Plain-store coverage is back: TTL expiry, custom TTL, both putIfAbsent forms, both request-id cases. Versioned features split out cleanly into VersionedStateStoreTest.

Point 3 — fixed. final on both, nothing broke.

Point 4 — fixed. Swept 17–30 digits on both providers: refused at every count. Also confirmed the guard doesn't over-refuse anything legitimate:

genuine frame, matching witness → true    (still works)
genuine frame, stale witness    → false
legacy unframed, witness 0      → true    (adoption still works)
largest real TSID (19 digits)   → true
empty-object frame {"@v":42}    → true
21-digit lookalike              → false
22-digit lookalike              → false   (was true before this PR)

55 tests green in the module, plus cmd-queue and pairing.

@jonmarti jonmarti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Approving. Only the replaceIf javadoc line from my review above is left

@pditommaso
pditommaso merged commit 29236dc into master Aug 4, 2026
3 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