fix: Settle an acquired ledger before reporting it complete - #8088
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a concurrency ordering issue in InboundLedger by ensuring a ledger is fully settled (made immutable) before the acquisition is published as complete, preventing other threads from observing isComplete() while the ledger’s SHAMaps are still mid-sync.
Changes:
- Update
InboundLedger::done()so it settles (setImmutable()) before publishingcomplete_, and treats “have every part” as the precondition to completion. - Adjust
trigger()/receiveNode()to stop settingcomplete_directly; they now only set the “have-*” flags and letdone()publish completion. - Add a unit test to cover the “completed by walk” path and assert that a completed acquisition yields an immutable ledger.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/xrpld/app/ledger/InboundLedger.h | Clarifies contracts: isComplete() implies the ledger is settled; done() documents the required ordering and locking. |
| src/xrpld/app/ledger/detail/InboundLedger.cpp | Enforces “settle first, then publish complete_” and moves completion publication exclusively into done(). |
| src/test/app/InboundLedger_test.cpp | Adds a regression test validating that walk-completed acquisitions are reported complete only after settlement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
e33b9ed to
ddff978
Compare
ddff978 to
3d47fb4
Compare
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
3d47fb4 to
bad799a
Compare
bad799a to
5e1c81b
Compare
1a6c013 to
e4c4175
Compare
e4c4175 to
dd07d7d
Compare
dd07d7d to
c41be78
Compare
isComplete() is read without mtx_, so the flag must not be published before the ledger it describes is settled - a second thread could otherwise take a still-mid-sync ledger, and a mutable ledger reaching LedgerHistory::insert() or LedgerMaster::switchLCL() calls logicError(). done() now owns the publication: it settles the ledger and only then sets complete_, while trigger()/receiveNode() just set have-flags and leave the verdict to done().
c41be78 to
e26eabc
Compare
There was a problem hiding this comment.
This diff correctly restructures InboundLedger's completion signaling so that complete_ is only published from inside done() after the ledger has been settled (setImmutable()), closing the described lock-free-read race with isComplete(). trigger() and receiveNode() now only set have-flags and defer the completion decision to done(), which accepts either an already-set complete_ (tryDB path) or all three have-flags (walk path). The accompanying test (testWalkSettlesBeforeReportingComplete) exercises exactly this ordering by forcing completion to happen via trigger()'s walk rather than tryDB(). I traced through all the changed conditionals (done()'s entry condition, trigger()'s bottom done() call condition, and the removed complete_ = true assignments) and they are internally consistent with the new design; I did not find a correctness bug introduced by this diff. The one residual assumption — that haveHeader_ is already true by the time receiveNode() observes haveState_ && haveTransactions_ — is a pre-existing invariant of the acquisition flow (header is always requested before state/transaction nodes) and isn't something this diff changes, so it's not flagged as a new issue.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Part 9/16 of a stack. Base: part 8 (
bthomee/shamap-invalid-08-judge-abandoned-map).High Level Overview of Change
InboundLedger::done()now settles the ledger (setImmutable()) before publishingcomplete_,closing a race where a concurrent reader —
isComplete()is read withoutmtx_, including byInboundLedgers::acquire()— could otherwise take a ledger whose maps are still mid-sync. A mutableledger reaching
LedgerHistory::insert(),LedgerHolder::set()orLedgerMaster::switchLCL()callslogicError(), which aborts a Release build.Context of Change
isComplete()is read withoutmtx_, byInboundLedgers::acquire()among others, so the flag must notbe published before the ledger it describes is settled.
done()therefore owns the publication: itsettles the ledger and only then sets
complete_, so no reader can see the flag before the ledger isimmutable, and its docstring and
isComplete()'s now say so.trigger()andreceiveNode()report what they hold rather than what it amounts to: each sets thehave-flags and leaves the conclusion to
done(), whichtrigger()reaches once it has every part orhas failed.
tryDB()keeps settingcomplete_itself, since it settles its own result first, sodone()accepts either a flag already set or all three have-flags.trigger()callsdone()withmtx_still held; the mutex is recursive, so the call sites that already hold it further up areunaffected.
testWalkSettlesBeforeReportingCompletedrives an acquisition that only its own walk can finish: theheader and every state node except the deepest are local, so
checkLocal()leaves it incomplete, andthe last node becomes available only afterwards. It asserts
isComplete()together withgetLedger()->isImmutable()— a single thread cannot observe the ordering itself, so what the case pinsis the consequence: that
trigger()still completes an acquisition without setting the flag out oforder.
API Impact
None of the checkboxes below apply: this is an internal
xrpldcorrectness fix with nolibxrpl,public-API, or peer-protocol surface.