Skip to content

Reject storing and committing after a failed store until clear() - #322

Merged
hg-ms merged 3 commits into
mainfrom
storer-commit-after-failed-store
Oct 30, 2026
Merged

hg-ms merged 3 commits into
mainfrom
storer-commit-after-failed-store

Conversation

@hg-ms

@hg-ms hg-ms commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Problem

When store() throws partway through an object graph, for example because a type handler hits an unpersistable value, the storer keeps everything it had collected up to that point:

  • objects registered for storing but never serialized,
  • the entity that failed, half-written: its record is reserved before the type handler fills it,
  • commit listeners and other per-store state of the failed store.

A later commit() on the same storer used to succeed. It wrote that partial graph and registered the never-serialized objects as stored. Nothing failed at that point. The storage then failed on the next start with No entity found for objectId … and could not be restarted. Storing the graph again did not repair it, because a lazy store skips objects that are already registered.

Affected:

  • Explicit storers (createStorer, createLazyStorer, createEagerStorer) reused after store() threw.
  • BatchStorer. store() throws to the caller, but the background flush, flush() or close() committed the partial state automatically.
  • Serializer / TypedSerializer. Both reuse one internal storer, so the serialize() call after a failed one also emitted the leftover partial graph. Example: 550 bytes instead of 190 for a three-element list, and deserializing those bytes failed.

storageManager.store(), storeAll() and storeRoot() were not affected: they use a new storer per call.

Fix

Storer (BinaryStorer)

  • The storer records the first failure of a store call's item processing, including Errors.
  • While a failure is recorded, store() and commit() throw a PersistenceException with the original failure as cause. The commit check runs before anything is written, merged into the object registry or passed to commit listeners.
  • clear() discards the partial state and makes the storer usable again. There is deliberately no automatic clear: it would silently drop earlier, successful store() calls on the same storer.
  • A failure before anything was serialized, such as an unpersistable root object, records nothing. Earlier stores on that storer can still be committed.
  • A failed commit (write error) does not mark the storer as failed.

BatchStorer

  • After a failed store(), store(), flush(), commit() and close() throw until clear().
  • The background flush skips a failed batch. It logs the failure instead of failing on every tick.
  • clear() and reinitialize() now wait for a store that is in progress on another thread. Before, a clear() from one thread while another thread was storing let the store continue into the cleared state, and the storage could not be restarted afterwards.

Serializer / TypedSerializer

  • Both clear their internal storer when storing or committing fails, including the type info store of TypedSerializer. The next serialize() produces clean output.

Javadoc

  • Storer.store(Object), Storer.commit(), Storer.clear() and the BatchStorer class documentation describe the new contract.

Behaviour change

  • Calling store() or commit() on a storer after a failed store() now throws until clear().
  • For a BatchStorer, one failed store() blocks the whole batch. That includes stores from other threads that already returned, until someone calls clear(). Nothing is lost silently: every later store(), flush() and close() throws, and the background flush logs the failure.

Verification

  • Scenario tests, each followed by a storage restart:
    • a commit after a failed store is rejected, both with unprocessed objects and with a half-written entity;
    • the original failure is the cause of the rejection;
    • a further store() is rejected;
    • clear() and storing again persist the complete graph;
    • an unpersistable root does not block the commit;
    • BatchStorer: flush() and close() are rejected, clear() then storing again works, the background flush does not commit, and clear() during another thread's store leaves the storage intact.
  • Serializer / TypedSerializer: after a failed serialize(), the next output matches a fresh serializer's output and round-trips.
  • Mutation check: without recording the failure, 6 of 9 storer scenario tests fail.
  • Existing tests:
    • serializer build with integration tests and javadoc check: 943 run, 0 failures, 4 skipped;
    • store integration tests: 1418 run, 0 failures, 83 skipped;
    • GigaMap module: 1242 run, 0 failures, 1 skipped.
  • Independent review: found no false rejection. No production code in serializer or store keeps using a storer after its store() threw.

Not addressed here

BatchStorer fails when a batch contains the first instance of a new enum type:

  • a flush triggered by maxSize or flushCycle ends in StackOverflowError;
  • close() throws BatchStorer is already closed.

This already happens without this change, and it will be fixed separately.

hg-ms added 2 commits October 30, 2026 10:58
A store() that threw left its partially serialized graph in the storer;
  a later commit, or a BatchStorer flush or close, persisted it: dangling
  references, a half-written entity and never-written objects registered
  as stored. The storage could not be restarted.

  The storer now records the first failure of item processing and rejects
  further stores and commits until clear(). A BatchStorer reports a failed
  batch once and does not flush it. BatchStorer.clear() and reinitialize()
  wait for an in-flight store. Serializer and TypedSerializer clear their
  internal storer when storing or committing fails.

Copilot AI 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.

🟡 Changes recommended

Type-info storage failures can discard pending definitions and cause retries to produce unreadable output, while rejected final batch flushes leave the storer closed and prevent documented recovery.

2 open issues
What changed in this PR

This PR protects persistence and serializer reuse from partial object graphs left by failed stores.

Changes:

  • Records item-processing failures and blocks further stores or commits until the storer is cleared.
  • Adds failed-batch handling and synchronizes batch clearing and reinitialization with ongoing stores.
  • Clears internal serializer state after store or commit failures and documents the recovery contract.
File Description
serializer/​src/​main/​java/​org/​eclipse/​serializer/​TypedSerializer.java Shares failure cleanup across data and type-info storage.
serializer/​src/​main/​java/​org/​eclipse/​serializer/​Serializer.java Clears the internal storer after store or commit failures.
persistence/​persistence/​src/​main/​java/​org/​eclipse/​serializer/​persistence/​types/​Storer.java Documents failure rejection and recovery through clearing.
persistence/​persistence/​src/​main/​java/​org/​eclipse/​serializer/​persistence/​types/​BatchStorer.java Documents failed-batch behavior and recovery.
persistence/​binary/​src/​main/​java/​org/​eclipse/​serializer/​persistence/​binary/​types/​BinaryStorer.java Tracks store failures, guards commits, and coordinates batch resets and background flushing.

Review effort: Balanced

Copilot AI 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.

🔵 Needs a closer look

The failed-store guard lacks regression tests for rejection without side effects, original-cause preservation including Errors, recovery after clear(), and valid reuse after failures that should not poison the storer.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Add regression tests for storer failure rejection and recovery

persistence/​binary/​src/​main/​java/​org/​eclipse/​serializer/​persistence/​binary/​types/​BinaryStorer.java:691

This PR adds no checked-in regression tests for the failed-store state, although existing integration tests such as HealingDeferredLinkTest exercise BinaryStorer.store() and commit(). Add tests for subsequent store/commit rejection, no target writes, registry merges or listener notifications on rejection, preservation of the original cause (including an Error), and recovery after clear(). Also verify that an unpersistable root and a target write failure do not poison the storer, so the guard does not reject valid reuse.

2 resolved since last review

Review effort: Balanced

@hg-ms
hg-ms requested a review from fh-ms October 30, 2026 12:40
@hg-ms
hg-ms merged commit cc7c6df into main Oct 30, 2026
20 checks passed
@hg-ms
hg-ms deleted the storer-commit-after-failed-store branch October 30, 2026 12:55
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.

3 participants