Isolate coroutine statistics across async reads (#15142) - #15142
joshkang97 wants to merge 1 commit into
Conversation
|
@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 266.9s. |
Claude Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.✅ 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 `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
a5c537f to
5ccb54c
Compare
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
5ccb54c to
e1f82a7
Compare
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
e1f82a7 to
6e29b5f
Compare
Claude Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit 6e29b5f SummaryWell-designed fix for a real TLS stats isolation bug in coroutine reads. The approach of moving stats ownership to the High-severity findings (0): Full review (click to expand)Findings🟡 MEDIUMM1.
|
| Context | Affected? | Analysis |
|---|---|---|
| WritePreparedTxnDB | Yes | INSTALL_COROUTINE_STATS_CONTEXT_SCOPE removed from all 4 methods. Stats now correctly handled by CoroDB::CoGet wrapper before virtual dispatch. Safe. |
| CompactedDBImpl | Yes | Same removal, same correct wrapping. Safe. |
| CoroStackableDB | Indirectly | GetCoroutine just forwards to inner DB. Stats scope in CoGet wraps the entire dispatch chain. Safe. |
| db_stress | Yes | BlockingAsyncCallback::OnComplete() signature updated. No stats usage. Safe. |
| db_bench | Yes | PrepareCoroutineJobPerfContext moved inside loop, now disables iostats. Correct for per-operation capture pattern. |
| Sync DB::Get callers | No | Stats TLS only modified by async/coroutine paths. |
| ReadOnly DB | No | No coroutine support. |
| User-defined timestamps | No | Orthogonal to stats mechanism. |
Positive Observations
- Moving stats ownership to the
CoroDBboundary is a clean architectural improvement that eliminates the need for every DB implementation to independently manage stats, reducing error surface for future stackable DB implementations. - The disabled-config fast path in
CoroutineStatsContextScope(skipping folly RequestContext setup) avoids overhead for stats-disabled requests. - The
AsyncReadStatsScopeRAII class for the sync fallback is clean and prevents resource leaks. - The test improvements (ManualExecutor, disabled-stats task, post-completion TLS verification) are well-crafted.
- The 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 `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
```
Reviewed By: xingbowang
Differential Revision: D117017089
6e29b5f to
ea3b2ec
Compare
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit ea3b2ec SummaryWell-structured fix for a real correctness bug in coroutine stats isolation. The "capture-and-disable" pattern is clean, the macro elimination simplifies maintenance, and the API change unifies stats consumption. No high-severity issues found. High-severity findings (0): No high-severity findings. Full review (click to expand)Findings🟡 MEDIUMM1.
|
| Context | Affected? | Analysis |
|---|---|---|
| WritePreparedTxnDB | YES | INSTALL_COROUTINE_STATS_CONTEXT_SCOPE removed. Stats now handled at CoroDB::CoGet layer before dispatching to WritePreparedTxnDB::GetCoroutine. Correct — the stackable DB chain is wrapped by the scope. |
| CompactedDBImpl | YES | Same as above. Stats scope wraps the entire GetCoroutine call chain. |
| CoroStackableDB | OK | Forwards to inner CoroDB. Stats scope at CoroDB::CoGet wraps the full stackable chain. |
| Sync fallback (no executor) | YES | CoroutineStatsContextScope wraps sync db->Get() call. TLS stats disabled after scope exits. Correct. |
| db_bench | YES | PrepareCoroutineJobPerfContext moved inside loop. Correctly re-sets stats before each CoGet since CoGet now disables TLS after each call. |
| db_stress | YES | BlockingAsyncCallback::OnComplete signature updated. No stats usage, so the change is transparent. |
Positive Observations
- Clean architectural improvement: Moving stats ownership from scattered DB implementations to the CoroDB boundary is the right layering. This eliminates the need for each DB impl to remember to add the stats macro.
- Good bug fix: The core bug (stats-disabled requests inheriting enabled TLS state) was real and could cause incorrect metrics in production.
- Allocation avoidance: When stats are disabled, no
EnabledCoroutineStatsRequestDataorShallowCopyRequestContextScopeGuardis allocated. This is a performance improvement for the common "no stats" path. - Test improvement: Using
ManualExecutorwithcollectAlland adding a disabled-stats task directly tests the reported bug scenario. - Consistent disable-on-exit: The "always disable TLS on exit" invariant is simple and prevents the class of bugs where enabled state leaks between requests.
ℹ️ 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
|
This pull request has been merged in ac0f4ae. |
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.Reviewed By: xingbowang
Differential Revision: D117017089