Skip to content

fix: retry a declined typed-structure save instead of returning stranded bytes - #8

Open
kriszyp wants to merge 1 commit into
mainfrom
kris/typed-save-cas-retry
Open

fix: retry a declined typed-structure save instead of returning stranded bytes#8
kriszyp wants to merge 1 commit into
mainfrom
kris/typed-save-cas-retry

Conversation

@kriszyp

@kriszyp kriszyp commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Completes #6. That PR made _saveTypedStructures forward isCompatible so a CAS-ing saveStructures can decline a save — but nothing acted on the decline. _saveTypedStructures dropped the return value, and _structonEncode returned the encoded bytes either way.

A struct encoding is a bare reference to a typed structure id, so it is only meaningful once that structure is durably saved. With #6 alone, a declined save leaves the record pointing at an id that was never persisted — or, when the winning writer assigned that id to a different shape, at another record's structure, so it decodes as the wrong fields. In other words #6 converted "clobber durable, break the previously written records" into "drop the save, break the record we just returned."

msgpackr's own pack call site already enforces the contract this PR restores: on a false return it marks the structures uninitialized and re-packs, with the comment "re-packing as-is would re-emit the same record pointing at an unpersisted structure" (pack.js:190-199).

Fix

  • _saveTypedStructures returns the saveStructures result.
  • _structonEncode's struct path retries once on a decline: reload the durable dictionary (_loadStructures), re-mint, re-save. The retry pass always re-attempts the save — an empty durable leaves our unpersisted mint in place, so "no new structure this pass" does not imply the referenced id is persisted.
  • A second decline throws instead of returning bytes that reference an unpersisted structure.
  • The capped plain-fallback path is best-effort (reload + one re-save, no throw): result there carries no typed-structure reference, so a decline can only strand previously written struct data, not this record.

Scope

Standalone path only (a base without SUPPORTS_STRUCT_HOOKS — cbor-x, msgpackr v1). The fast path saves through the base's own pack call site, which already handles the decline. _saveTypedStructures is unreachable on the fast path (the constructor returns before installing the encode wrapper).

Test plan

  • mocha tests/test.js tests/test-cbor-x.js -u tdd — 101 passing (3 new)
  • New suite structon – declined structure save is retried (standalone) with a CAS-ing durable store:
    • a stale writer whose save is declined re-mints against durable; both writers' records decode correctly afterward
    • a persistently declining store throws (declined twice) after exactly one retry, rather than returning stranded bytes
    • a committed save is unaffected — no reload, no retry
  • Counterfactual: with the index.js change reverted, the first test fails with a deep-equal mismatch (the stale writer's record decodes as the other writer's fields — the exact corruption) and the second fails on the missing throw.

Note on publishing

structon main is one commit (#6) ahead of the published 1.0.7. Recommend releasing this together with #6 rather than publishing #6 alone.

🤖 Generated by an LLM (Claude Opus 5), with @kriszyp.

…ded bytes

Forwarding isCompatible (e3c8e44) made a CAS-ing saveStructures able to decline
a typed-structure save on the standalone path, but _saveTypedStructures dropped
the return value and _structonEncode returned the encoded bytes regardless. A
struct encoding is a bare reference to a typed structure id, so a declined save
left the record pointing at an id that was never persisted -- or, when the
winning writer had assigned that id to a different shape, at another record's
structure, which decodes as the wrong fields.

msgpackr's own pack call site handles this by marking the structures
uninitialized and re-packing (pack.js:190-199). Do the same here: return the
saveStructures result, and on a decline reload the durable dictionary and
re-mint once. A second decline throws rather than returning bytes that
reference an unpersisted structure. The capped plain-fallback path carries no
typed-structure reference, so a decline there is best-effort re-saved (it can
only strand previously written struct data, not this record) and does not throw.

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the _structonEncode method in index.js to handle cases where saving typed structures is declined due to concurrent writes. It introduces a retry mechanism that reloads the structures and attempts to re-encode, throwing an error if the save is declined twice to prevent returning bytes with unpersisted structure IDs. Additionally, _saveTypedStructures now returns the result of saveStructures, and comprehensive unit tests have been added in tests/test.js to verify this retry and error-handling behavior. There are no review comments, so I have no feedback to provide.

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.

1 participant