You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Reject storing and committing after a failed store until clear() - #322
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.
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.
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.
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
Add regression tests for storer failure rejection and recovery
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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 withNo 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:
createStorer,createLazyStorer,createEagerStorer) reused afterstore()threw.BatchStorer.store()throws to the caller, but the background flush,flush()orclose()committed the partial state automatically.Serializer/TypedSerializer. Both reuse one internal storer, so theserialize()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()andstoreRoot()were not affected: they use a new storer per call.Fix
Storer (
BinaryStorer)Errors.store()andcommit()throw aPersistenceExceptionwith 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, successfulstore()calls on the same storer.BatchStorerstore(),store(),flush(),commit()andclose()throw untilclear().clear()andreinitialize()now wait for a store that is in progress on another thread. Before, aclear()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/TypedSerializerTypedSerializer. The nextserialize()produces clean output.Javadoc
Storer.store(Object),Storer.commit(),Storer.clear()and theBatchStorerclass documentation describe the new contract.Behaviour change
store()orcommit()on a storer after a failedstore()now throws untilclear().BatchStorer, one failedstore()blocks the whole batch. That includes stores from other threads that already returned, until someone callsclear(). Nothing is lost silently: every laterstore(),flush()andclose()throws, and the background flush logs the failure.Verification
store()is rejected;clear()and storing again persist the complete graph;BatchStorer:flush()andclose()are rejected,clear()then storing again works, the background flush does not commit, andclear()during another thread's store leaves the storage intact.Serializer/TypedSerializer: after a failedserialize(), the next output matches a fresh serializer's output and round-trips.store()threw.Not addressed here
BatchStorerfails when a batch contains the first instance of a new enum type:maxSizeorflushCycleends inStackOverflowError;close()throwsBatchStorer is already closed.This already happens without this change, and it will be fixed separately.