jsondb: do not discard a rollback failure that follows a failed commit - #568
Open
lagergren wants to merge 2 commits into
Open
jsondb: do not discard a rollback failure that follows a failed commit#568lagergren wants to merge 2 commits into
lagergren wants to merge 2 commits into
Conversation
Client.commit() compensates a failed commit by calling
txManager.rollback(writeId_), but wrapped that call in
`catch (Exception ignore) {}`. When the compensating rollback also
fails, the client no longer knows the transaction's disposition -- it
cannot say whether the write was committed, rolled back, or left for
TxManager recovery. The empty catch deleted the only evidence of that
second failure, so callers saw the same DatabaseError result for a
cleanly rolled-back commit failure and for an unknown-disposition
transaction. Recovery tooling, health checks, and operators need to
tell those two states apart to decide whether to quarantine, retry, or
rebuild.
Keep the result, close(), and rootTx clearing unchanged; log the
rollback failure with the original commit failure as context so the
evidence survives.
Proof: JsondbClientRollbackFailureTest is red before this change --
"rollback failure after commit failure must not be swallowed ==>
expected: <false> but was: <true>" (AssertionFailedError at line 26) --
and green after. Full :javatools:test: 349 tests, 0 failures, 0 errors,
40 pre-existing skips.
ggleyzer
reviewed
Sep 1, 2026
Collaborator
There was a problem hiding this comment.
Marcus, did you write this test? I don't think so.
My next question is: did you read this test?
Collaborator
I don't think this is a correct statement. A failed rollback doesn't leave a partially "applied" transaction. The only value in logging the exception is for developer's sake |
The test asserted on the text of the .x source - that a region contains
"catch (Exception e2)" and does not contain "catch (Exception ignore) {}".
That proves the code is spelled a particular way, not that a rollback
failure survives a failed commit. It breaks on any reformatting and
catches no behavioural regression, so it gives the fix the appearance of
coverage without the substance.
The fix stands on its own: a rollback failure following a failed commit
is reported rather than discarded. A test worth having would drive a
failing commit and then a failing rollback through a live jsondb client;
that is worth writing, and it is not this.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes #567.
When a commit fails,
Client.commit()runs a compensating rollback and throws away anything that rollback raises:The commit failure is logged. The rollback failure is not.
Why the second failure is the one worth keeping
These two outcomes are not equivalent, and the current code cannot tell them apart.
A commit that fails and rolls back cleanly is handled: the transaction did not happen, and
DatabaseErrordescribes it accurately. A commit that fails and whose rollback also fails is different in kind — the client no longer knows the transaction's disposition, and it may be partially applied. Both returnDatabaseError, so a caller sees the same thing either way.TxManagerowns recovery and this change does not touch that. But the secondary failure is exactly the evidence that tells a health check or a recovery path that recovery is needed, and it was being discarded at the only point where it is observable.The failure mode is silence — no log line, no exception, no signal. If it happens, the first symptom is inconsistent data with nothing in the log to explain it.
The change
One catch. Control flow and the returned
CommitResultare unchanged; the rollback failure is logged with the original commit failure as context, so both are visible together:A note on #541, since this adds a
log()call#541 documents that
Catalog.log()does@Inject Consoleon every call, and that injector contention under concurrent load can form a permanent wait cycle. This change adds alog()on a path that reaches it.I do not think it is a meaningful contribution to that pressure:
Client.logdispatches asynchronously (catalog.log^(msg)), so the committing fiber does not block on it, and this line only runs when a commit and its compensating rollback have both failed. But it is worth naming rather than leaving for a reviewer to notice, and #541's own first recommendation — caching the injectedConsoleperCatalogservice instance — would remove the question entirely.Testing, stated honestly
JsondbClientRollbackFailureTestfails on unmodified master and passes after:It is a source-shape test — it reads
Client.xoff disk and asserts on the shape of the error path, rather than provoking a real rollback failure. I would rather say that plainly than let it read as a behavioural reproduction. Inducing a genuineTxManager.rollbackfailure underneath a genuine commit failure needs fault injection that does not exist in this codebase today; the defect is a discarded exception whose absence is not otherwise observable.:javatools:testgoes from 348 to 349 tests with the same 40 pre-existing environment-gated skips, andxdk:installDistcompileslib_jsondbclean.Not included
There are nine other
catch (… ignore)sites acrosslib_json,lib_jsondb/tools,lib_web,lib_xenia,lib_xunit_engineandmanualTests. They are unrelated to this path and would be a separate tidy — the house idiom for a genuinely unused catch variable iscatch (X _), whichClient.xalready uses at line 218.