always increment finishedQueries counter if incrementing allQueries - #13786
always increment finishedQueries counter if incrementing allQueries#13786jkt-signal wants to merge 2 commits into
Conversation
|
(Unfortunately, I don't appear to have the rights to apply the RFC label as suggested by the commit process document, so I'm marking this PR ready for review instead.) |
gxglass
left a comment
There was a problem hiding this comment.
Local AI review:
PR 13786 — always increment finishedQueries counter if incrementing allQueries
Intent
The storage-server query actors (getValueQ, getKeyValuesQ, getMappedKeyValuesQ, getKeyValuesStreamQ, getKeyQ) increment counters.allQueries at entry and counters.finishedQueries at exit as separate statements. If the actor throws/returns/cancels between them, finishedQueries is skipped, so the in-flight gauge (allQueries - finishedQueries, e.g. maxQueryQueue) drifts upward permanently. This PR adds an RAII CountedSection (++start in ctor, ++end in dtor) held as a state variable, so finishedQueries is incremented on every exit path. The move-assign-swap idiom neatly matches Flow's default-construct-then-assign state initialization without double-counting.
Correctness
- The counter-balancing intent is sound and the
CountedSectiondesign (default ctor + move-assign swap + no move ctor) is correctly tailored to Flow'sstate-variable init, so allQueries/finishedQueries stay paired on normal completion. - But it introduces a use-after-free on the SS-teardown-with-in-flight-query path (see Bugs). The old code only touched
countersfrom the actor body (normal completion, while the SS is alive); moving thefinishedQueriesincrement into a destructor makes it also fire on cancellation, including the cancellation that happens while the owningStorageServeris being destroyed.
Bugs
- Use-after-free of a destructed
Counterduring StorageServer teardown —fdbrpc/include/fdbrpc/Stats.h(CountedSection::~CountedSection) +fdbserver/storageserver.actor.cpp(data->actors.add(getValueQ(...))).- Confirmed member ordering on
release-7.3(via GitHub API):ACTOR/ActorCollection actors;is declared at line 1313, the} counters;member at line 1617. Members destruct in reverse declaration order, so in~StorageServercounters(and itsCounter finishedQueries, whose implicit dtor destructs theInt64MetricHandle metricmember) is destroyed first, and theactorscollection is destroyed last. - The query actors are owned by
data->actors(e.g.data->actors.add(getValueQ(data, req)), 7.3 line 4108). Whenactorsdestructs it cancels any still-in-flight query actor, which destroys itsstate CountedSection cs, whose destructor runs++data->counters.finishedQueries→Counter::operator+=→metric += delta, dereferencing the already-destructedInt64MetricHandle.Counterhas no trivial destructor (it ownsInt64MetricHandle), so this is genuine use-after-destruction, crash-capable in an ASAN build and UB otherwise. - Failure scenario: a storage server is removed/excluded/killed (or its
storageServeractor is otherwise cancelled) while agetValueQ/getKeyValuesQ/… is in flight — common in simulation (SS attrition with concurrent reads).~StorageServerdestroyscounters, thenactorscancels the pending query, whoseCountedSectiondestructor increments the freedfinishedQueries. - This is a regression introduced by the PR: the pre-change code incremented
finishedQueriesonly in the actor body, which never runs on the teardown-cancellation path, so it never touchedcountersafter destruction.
- Confirmed member ordering on
Omissions
- No test exercises the SS-teardown-with-in-flight-query cancellation path (which is exactly where the RAII change diverges from the old behavior).
Alternatives / Suggested fix
- Ensure
countersoutlives any actor whose destructor may touch it: declare thecountersmember beforeactorsinStorageServer(soactorsdestructs first, cancelling the query actors whilecountersis still alive). Alternatively, drain/canceldata->actorsexplicitly beforecountersis torn down, or haveCountedSectioncapture something that guarantees the counter's lifetime. Notemainhas the sameactors-before-countersordering (actors ~L1185, counters member ~L1410), so any future forward-port of this pattern needs the same reordering to be safe.
Verdict
NEEDS-CHANGES — The RAII counter-balancing is the right idea, but as written it moves the finishedQueries increment into a destructor that runs on query-actor cancellation, and on release-7.3 (confirmed member order: actors before counters) that cancellation happens during ~StorageServer after counters is already destroyed — a use-after-free of the Counter's Int64MetricHandle when a storage server is torn down with an in-flight read. Fix by making counters outlive actors (member reorder) or otherwise guaranteeing the counter's lifetime across the destructor increment.
|
Yep, I think the bug noted in the review is real. I don't see any way in which methods of any member of |
…structor-order issue
and done. |
Result of foundationdb-pr-73 on Linux CentOS 7
|
Result of foundationdb-pr-clang-73 on Linux CentOS 7
|
Result of foundationdb-pr-clang on Linux RHEL 9
|
Result of foundationdb-pr on Linux RHEL 9
|
Result of foundationdb-pr-cluster-tests-73 on Linux CentOS 7
|
gxglass
left a comment
There was a problem hiding this comment.
AI review attached below.
A comment mentions that main doesn't generate a usable fdbserver. Can you describe that in more detail? In general we rely on main not to broken for day to day development.
Result of foundationdb-pr-macos on macOS 14.x
|
Result of foundationdb-pr-macos-m1 on macOS 14.x
|
I'll build and try again now to confirm, but last time I tried, This link 404s for me; perhaps it is not public? |
The link works now 🤷 Yep, good catch, will fix the missed I don't really agree that I will supply a couple unit tests for |
On the tests the agent likes to just say "no new tests" if it doesn't see any. If there is no prior art in the area and it is not obvious how to write a test case for the code then I do not think that nit would block LGTM. (This is a purely generic answer because agent reviews very often nit about "no new tests" - I haven't looked at specifics in this area - use your judgement.) Based on "will supply a couple unit tests" I'm anticipating 1+ more commits. Can you tag me for re-review on this PR when it's all ready. |
Storage server processes publish a pair of counters
total_queriesandfinished_queries(as well as a derived special counterquery_queue_max) tracking how many read operations have been started and finished (and the high-water-mark for the number of pending queries—those that have been started, but not finished—in a status-fetch interval).Unfortunately, the counter underlying
total_querieswas incremented at the beginning of one of several actors, and the matching increment tofinished_querieswas done at the end, with several interveningwaits. If the actor was cancelled (or experienced another non-returnable error),finished_querieswas not incremented, resulting in a permanent drift betweentotal_queriesandall_queriesand apparently endless growth inquery_queue_max.The easiest way to demonstrate this behavior is to have two different clients alternate between setting watches and updating values for the same keys—for example, run two simultaneous instances of this simple Java program against a single database, and monitor its status to see
query_queue_maxon storage servers go up permanently every time you run the test.To solve this problem, we add a simple class
CountedSectiontoflow/Stats.hto bump two counters in RAII-enforced balance; once a localCountedSectionhas bumped the first counter, it will always eventually bump the second one whether it is destroyed as a result of normal exit or exception. (Of course, if you store one in a heap object and leak it, you will still experience drift, so, uh, don't do that.)CountedSectionis made move-only so you can use it in actor state, sincestatevariables seem to always be default-constructed and then assigned to their initializers even if the initialization comes before the firstwaitin an actor.One of the actors involved actually updates a second counter pair,
getMappedRangeQueriesandfinishedGetMappedRangeQueries, in a way vulnerable to the same problem, so we useCountedSectionto address that as well, though I have not seen any instances of these getting out of sync (entirely due to lack of trying).Some other metrics updated at the ends of the affected actors—mostly latency metrics, but also some tagged size distributions—are not addressed by this change. Arguably they should not be, since including latency and response size of interrupted queries may significantly throw off the statistics in a not-necessarily-useful way.
Leaving this as an RFC because I'm not sufficiently familiar with the FoundationDB testing setup to write a good test; the Java program linked above is a great demonstration but is not trivially adapted to an integration test (and presumably we'd like tests of the fdbserver itself to be C++, not dependent on the Java bindings). Advice for how to test the change greatly appreciated!
This PR is against the 7.3 branch, rather than
main, because all the relevant actors have been rewritten to use C++20 standard coroutines in the interim, and while I strongly suspect the same bug applies equally inmain, I was not able to confirm this becausemaindoes not currently build a usable fdb for me. Porting forward tomainshould be relatively trivial and I'm happy to do so if requested.