Skip to content

fix(decorators): restore the stats context on every sync exit, interrupts included (LAB-8741) - #568

Merged
27Bslash6 merged 4 commits into
mainfrom
lab-8741-sync-stats-finally
Oct 10, 2026
Merged

27Bslash6 merged 4 commits into
mainfrom
lab-8741-sync-stats-finally

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

An interrupt that leaves a sync @cache call from the decorator's own backend I/O leaves the function-stats ContextVar set in the caller's context. That covers KeyboardInterrupt, a worker timeout's SystemExit and gevent.Timeout, raised from the L2 read, backend resolution or the invalidation listener start. sync_wrapper restored the var by hand on each exit (18 reset_current_function_stats(token) calls). Only three sat in a finally, and the except Exception handlers around the backend I/O never see a BaseException. A process that survives the interrupt (a gevent greenlet, a REPL or notebook after Ctrl-C) then keeps attributing stats to that function outside any decorated call. The CachekitIO backend's metrics headers are one reader of the var.

sync_wrapper now matches async_wrapper: one try/finally opens straight after set_current_function_stats, and its finally holds the only reset. ContextVar.reset raises RuntimeError on a token already used, so the hand resets had to go rather than sit beside a finally.

Review with whitespace hidden: most of the diff is the body moving one level in.

What changed

  • wrapper.py, sync_wrapper. The body moves under one try/finally. All 17 resets outside the miss path's finally are deleted, along with the two try/finally blocks that only reset. Three try: … except Exception: raise wrappers existed only to reset before re-raising, around _l2_scope() and the two interop checks; with the reset gone they are no-ops, so they are removed. async_wrapper calls the same three bare. Every other except clause is unchanged, including the fail-closed re-raises (DecryptionAuthenticationError, KeyringConfigurationError, InteropError, UnsupportedTenantError). The L1 guard's explicit except DecryptionAuthenticationError: raise stays, as it does in async_wrapper. Its comment, and the async twin's, now state exactly what it guards: it keeps the tamper raise ahead of any except Exception later added to that inner try. It cannot stop a broad handler wrapped around the read path from outside; the fail-closed L1 tests pin that. Comments that described the hand resets are removed or updated.
  • Behaviour change on degraded paths. When key generation, breaker admission, client creation or the L2 read fails, the uncached function now runs with the var set; before, the var was reset first. The miss path and every async path already ran the function with the var set.
  • Tests.
    • test_context_reset_on_interrupt_from_backend_io raises KeyboardInterrupt and SystemExit from the L2 read (backend.get) and from backend resolution (the provider's get_backend). It runs in the test's own context, not a copied one. All 4 cases fail on main and pass here.
    • test_async_context_reset_on_interrupt_from_backend_resolution is the async pin. The decorated coroutine is awaited directly, so it runs in the test's task, and the test passes on main too. It raises from backend resolution, not the L2 read. A miss reads L2 inside its single-flight task, and asyncio re-raises an interrupt from a task out of the event loop, not into the awaiting caller.
    • test_context_leak_regression.py has an autouse fixture that starts each test with no stats context and restores it after, so one leaking test fails alone instead of failing every later None check in the worker.
    • test_an_interrupt_through_the_decorator_propagates_as_itself_and_spares_the_breaker (test_redis_error_frames.py) ran the decorated call in contextvars.copy_context().run(...) to contain this leak. It now calls directly and asserts the context is restored. It fails against main's wrapper.py and passes here.
    • test_context_reset_on_backend_init_failure patched cachekit.cache_handler.get_backend_provider, a binding the wrapper never reads (it imports the function), so the provider was never called. It now patches cachekit.decorators.wrapper.get_backend_provider, as the rest of the suite does, and asserts the provider was called.

Verification

  • An AST comparison of sync_wrapper against main removes the reset statements on both sides. With that done, every surviving except clause (14) is identical to its main counterpart. The 3 removed clauses are each except Exception: raise with no sibling handlers. The whole function is identical once do-nothing try blocks are inlined. A deliberately narrowed except (InteropError, KeyringConfigurationError) fails the check. It compares the parsed AST, not text, so a re-indented raise cannot hide in git diff -w.
  • tests/unit + tests/critical, with main merged in: 6202 passed, 18 skipped, 1 xfailed.
  • Full tests/ suite against a local Redis, compared with main on the same Redis: the only failures that differ are timing-based performance tests, which flake on main as well. The tests that failed under -n 4 pass when run serially.
  • Sync hit paths, 100k calls, best of 7, main → this branch: L1-only hit 3787 → 3888 ns; backed L1 hit 14210 → 13333 ns. Both are within noise.
  • ruff check, ruff format --check and basedpyright are clean; basedpyright reports 38 warnings, the same count as on main.

No docs change: no documentation describes when the stats context is reset.

…upts included

sync_wrapper restored the function-stats ContextVar by hand on each exit,
so a BaseException raised by the caller-thread backend I/O (the L2 read,
backend resolution, the invalidation listener start) skipped every reset:
KeyboardInterrupt, a worker timeout's SystemExit or gevent.Timeout left the
var set in the caller's context, and later code there reported that
function's stats. One try/finally now holds the only reset, as in
async_wrapper, and the 18 hand resets are gone. The three try blocks that
only reset and re-raise go with them; every other except clause is
unchanged.

On a degraded path (key generation, breaker rejection, client creation or
the L2 read failed) the uncached function now runs with the var set, as it
already does on the miss path and on every async path.

The backend-init leak test patched get_backend_provider in cache_handler,
a binding the wrapper never reads, so it never reached the provider. It now
patches the wrapper's binding and asserts the provider was called.
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: cachekit-io/cachekit-py/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 119c9d3b-d6d8-4b75-b280-046d238a5e96

📥 Commits

Reviewing files that changed from the base of the PR and between 50ff1c0 and 25f64cd.


📒 Files selected for processing (3)
  • src/cachekit/decorators/wrapper.py
  • tests/unit/backends/test_redis_error_frames.py
  • tests/unit/test_context_leak_regression.py


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kodus-27b

kodus-27b Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Oct 9, 2026
@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.02913% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/cachekit/decorators/wrapper.py 99.02% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

…solate leak tests

The comments on the L1 guard's explicit `except DecryptionAuthenticationError:
raise`, sync and async, said it keeps a tamper raise reaching the caller even
if a later edit wraps the read path in a broad `except Exception`. An outer
handler would still catch the re-raise. The clause only keeps the raise ahead
of an `except Exception` later added to the same inner try. Both comments now
say that, and name the test that pins the raise.

test_context_leak_regression.py gets an autouse fixture that starts each test
with no stats context and restores it after, so one leaking test fails alone
instead of failing every later None check in the same worker.
@kodus-27b

kodus-27b Bot commented Oct 10, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Oct 10, 2026
…text

The test ran the decorated call in a copied context because an interrupt
left the decorator's stats context set. The sync wrapper now restores it on
every exit, so the call runs directly and the test asserts the context is
restored.
@kodus-27b

kodus-27b Bot commented Oct 10, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

@27Bslash6
27Bslash6 merged commit d2dc83a into main Oct 10, 2026
38 checks passed
@27Bslash6
27Bslash6 deleted the lab-8741-sync-stats-finally branch October 10, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant