Isolate coroutine statistics across async reads - #15142
Conversation
Summary:
Coroutine statistics use TLS, but coroutine reads can interleave on the same executor thread. Previously, when a stats-enabled request suspended, it saved its counters without disabling the executor's TLS configuration. A stats-disabled request running next could inherit those enabled settings and collect statistics unexpectedly. Stats setup also lived in individual DB implementations, so wrapper early returns and future stackable DB implementations could bypass it.
Move statistics ownership to the public `CoroDB` and callback-based async read boundaries, before virtual dispatch. Each operation captures its caller's configuration, enabled requests preserve their counters across suspensions, and every suspension or completion leaves executor TLS disabled. Because the scope wraps stackable DB dispatch, current and future `CoroStackableDB` implementations automatically inherit the correct behavior without adding their own stats reset or scope.
For consistency between coroutine and callback-based reads, the experimental callback API now uses TLS for both configuring and consuming statistics. `AsyncCallback::OnComplete()` no longer receives context arguments; RocksDB publishes the completed per-operation counters to TLS before invoking it.
```
Before
time -------------------------------------------------------------->
stats-on A: onSet(enable A) -- work -- onUnset(save A) ...... onSet(A)
executor TLS: [A enabled] ------------> [still enabled] ------> [A enabled]
stats-off B: run
^ inherits A's enabled TLS
After
time -------------------------------------------------------------->
stats-on A: onSet(enable A) -- work -- onUnset(save A, disable) ... onSet(A)
executor TLS: [A enabled] ------------> [disabled] ----------------> [A enabled]
stats-off B: run
^ remains stats-disabled
```
Differential Revision: D117017089
|
@joshkang97 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D117017089. |
✅ clang-tidy: No findings on changed linesCompleted in 296.3s. |
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit a5c537f SummaryWell-designed fix for a real TLS stats isolation bug in coroutine/async reads. The approach of moving stats ownership to the CoroDB boundary (before virtual dispatch) is sound and eliminates duplicated stats setup across DB implementations. The breaking AsyncCallback API change is justified given the experimental status. High-severity findings (0): Full review (click to expand)Findings🟡 MEDIUMM1.
|
| Context | Affected by this PR? | Stats isolation correct? | Notes |
|---|---|---|---|
| WritePreparedTxnDB | YES (INSTALL macro removed) | YES | Stats now managed at CoroDB::CoGet boundary before dispatch |
| CompactedDBImpl | YES (INSTALL macro removed) | YES | Same as above |
| DBImpl | YES (INSTALL macro removed) | YES | Same as above |
| CoroStackableDB | Indirectly | YES | GetCoroutine/MultiGetCoroutine called from within CoroDB::CoGet which manages stats scope |
| ReadOnly/Secondary DB | NO | N/A | Don't implement CoroDB |
| GetAsync sync fallback | YES (AsyncReadStatsScope) | YES | Always disables stats on exit |
| db_bench coroutine reads | YES | YES | Per-iteration PrepareCoroutineJobPerfContext + MergeCoroutineJobPerfContext works correctly |
| db_stress | YES (OnComplete signature) | YES | Updated, doesn't use stats |
Assumption stress test results:
-
"Every suspension/completion leaves TLS disabled" -- Verified.
onUnset()callsDisableCoroutineStatsInTLS(). Scope destructor calls it. Short-circuit constructor calls it.AsyncReadStatsScopedestructor calls it. -
"Stats-disabled requests don't accumulate stats" -- Verified. When
IsCoroutineStatsEnabled(stats_config)returns false, noEnabledCoroutineStatsRequestDatais created, no folly request context is installed, and TLS is disabled. -
"Stats data survives DisableCoroutineStatsInTLS for reading" -- Verified.
DisableCoroutineStatsInTLSonly setsperf_levelandiostats_disabledflag. The actual counter values inget_perf_context()andget_iostats_context()are untouched.MergeCoroutineJobPerfContextreads the counters, not the flags.
Positive Observations
- Clean elimination of the
INSTALL_COROUTINE_STATS_CONTEXT_SCOPEmacro, which was duplicated across 4 files and required each implementation to independently manage stats. - The short-circuit path for disabled configs avoids unnecessary
folly::RequestDataallocation. - The test correctly validates the new invariant that TLS is disabled after
blockingWaitreturns. - The
EnabledCoroutineStatsRequestDatanaming clearly distinguishes it from the disabled path. - Release notes are appropriately concise.
ℹ️ About this response
Generated by Claude Code.
Review methodology: claude_md/ci_review_prompt.md
Limitations:
- Claude may miss context from files not in the diff
- Large PRs may be truncated
- Always apply human judgment to AI suggestions
Commands:
/claude-review [context]— Request a code review/claude-query <question>— Ask about the PR or codebase
Summary:
Coroutine statistics use TLS, but coroutine reads can interleave on the same executor thread. Previously, when a stats-enabled request suspended, it saved its counters without disabling the executor's TLS configuration. A stats-disabled request running next could inherit those enabled settings and collect statistics unexpectedly. Stats setup also lived in individual DB implementations, so wrapper early returns and future stackable DB implementations could bypass it.
Move statistics ownership to the public
CoroDBand callback-based async read boundaries, before virtual dispatch. Each operation captures its caller's configuration, enabled requests preserve their counters across suspensions, and every suspension or completion leaves executor TLS disabled. Because the scope wraps stackable DB dispatch, current and futureCoroStackableDBimplementations automatically inherit the correct behavior without adding their own stats reset or scope.For consistency between coroutine and callback-based reads, the experimental callback API now uses TLS for both configuring and consuming statistics.
AsyncCallback::OnComplete()no longer receives context arguments; RocksDB publishes the completed per-operation counters to TLS before invoking it.Differential Revision: D117017089