Skip to content

fix(decorators): start no SWR refresh thread in a child forked without at-fork hooks (LAB-9129) - #573

Merged
27Bslash6 merged 1 commit into
mainfrom
lab-9129-swr-hookless-fork
Oct 12, 2026
Merged

27Bslash6 merged 1 commit into
mainfrom
lab-9129-swr-hookless-fork

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

In a child forked from C, such as a uWSGI worker without --py-call-uwsgi-fork-hooks with the app loaded in the master (uWSGI's default), cachekit already starts no L1 cleanup thread, no invalidation listener and no metrics worker, because such a fork skips CPython's after-fork repair and a thread started there can hang in Thread.start() or crash the interpreter. The two sync stale-while-revalidate refresh dispatches still started one: an L1-only (backend=None) function's stale hit started cachekit-swr-refresh, and an L2 SWR function's stale hit started cachekit-swr-revalidate. SWR is on by default.

Change

  • Both sync dispatches in decorators/wrapper.py now check l1_cache._forked_without_hooks() on a stale hit, before they take a refresh slot or deep-copy the arguments. That is one getpid() on a stale hit that would refresh; the plain hit path is unchanged. In such a child the stale value is served until the entry expires (L1-only) or hard-expires (L2 stale_ttl), and the first read after that recomputes it. Nothing logs on the skipped path, because the same fork skips logging's at-fork lock reset.
  • In L1-only mode the refreshing marker that ObjectCache.get_with_swr set is left in place, so later stale hits on that entry ask for no refresh and pay no getpid(). The marker goes with the entry through remove(), so the cache's marker invariant (a marker always matches the live entry's generation) holds.
  • The async dispatches schedule a task with asyncio.create_task, not a thread, and are unchanged.
  • The detection is the existing one, so it needs cachekit imported before the fork, as for L1 cleanup.

Tests

  • tests/unit/test_swr_decorator.py::TestSWRForkIsolation::test_child_forked_without_hooks_serves_stale_and_starts_no_refresh, parametrized over libc's fork() (through ctypes.PyDLL(None).fork) and os.fork(). A fresh interpreter warms an L1-only and an L2 SWR function, ends its other threads and forks. The child records Thread.start() instead of starting, makes both entries stale, and calls each once. After the C fork it asks for no thread, serves both stale values without recomputing them, and logs nothing at WARNING or above. After os.fork() it asks for both refreshes.
  • Each gate removed on its own fails the C-fork case (['cachekit-swr-refresh'] and ['cachekit-swr-revalidate'] respectively). The os.fork() case fails if either gate fires in a child the at-fork hooks reached.
  • The fresh-interpreter runner (run_fresh, print_outcome) moves to tests/utils/fork_helpers.py and is shared with the L1 cleanup C-fork test in tests/unit/test_l1_memory_bounds.py.
  • The new test passed 100 of 100 runs (60 C-fork, 40 os.fork()) at 20 concurrent runs. Unit, critical, docs, doctest and markdown-docs suites pass on CPython 3.14.7, and the fork-related suites on 3.12.12.

Docs: docs/free-threading.md (the SWR and ObjectCache rows), docs/features/l1-invalidation.md (the uWSGI and L1-only fork paragraphs) and the ObjectCache docstring. Companion docs-site change: cachekit-io/docs#174, to merge once this ships in a release.

Closes LAB-9129

…t at-fork hooks

A child forked from C (uWSGI without --py-call-uwsgi-fork-hooks, with the
app loaded in the master) skips CPython's after-fork repair, so a thread
started there can hang in Thread.start() or crash the interpreter. L1
cleanup, the invalidation listener and the metrics worker already start
nothing there; the two sync SWR refresh dispatches still did.

Both now check l1_cache._forked_without_hooks() on a stale hit, before
they take a refresh slot: one getpid() on that path, none on a plain hit.
The stale value is served until the entry expires, and the first read
after that recomputes it. Nothing logs on the skipped path. The async
paths schedule a task, not a thread, and are unchanged.

The new test forks through libc's fork() in a fresh interpreter and
records Thread.start() in the child: it asks for no refresh after a C
fork and for both after os.fork(). The fresh-interpreter runner moves to
tests/utils/fork_helpers.py, shared with the L1 cleanup C-fork test.
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: cachekit-io/cachekit-py/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 50905c9a-b792-413e-844f-731ede4e894e

📥 Commits

Reviewing files that changed from the base of the PR and between 277aac3 and c741be5.


📒 Files selected for processing (7)
  • docs/features/l1-invalidation.md
  • docs/free-threading.md
  • src/cachekit/decorators/wrapper.py
  • src/cachekit/object_cache.py
  • tests/unit/test_l1_memory_bounds.py
  • tests/unit/test_swr_decorator.py
  • tests/utils/fork_helpers.py

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.



Summary by CodeRabbit

  • Bug Fixes
    • In forked child processes without fork hooks, synchronous stale-while-revalidate now serves cached values without starting refresh threads or revalidating the backing cache. Values are recomputed after they expire.
    • Added coverage for stale-value behaviour across fork methods.
  • Documentation
    • Clarified stale-value handling, refresh behaviour and potential lock hangs when using synchronous caches after a fork without hooks.

Walkthrough

The synchronous wrapper now suppresses L1 and L2 stale-entry refresh threads in forked children without at-fork hooks. Those children serve stale values until hard expiry, then recompute on a later read. Fork tests compare this behaviour with os.fork().

Changes

Synchronous refresh after fork

Layer / File(s) Summary
Suppress synchronous refresh after fork
src/cachekit/decorators/wrapper.py, src/cachekit/object_cache.py, docs/features/l1-invalidation.md, docs/free-threading.md
The sync wrapper skips L1 and L2 stale-entry refresh scheduling in a child forked without at-fork hooks. Documentation describes stale serving until expiry, recomputation on a later read, and the fork-related behaviour.
Test forked stale-hit behaviour
tests/utils/fork_helpers.py, tests/unit/test_swr_decorator.py, tests/unit/test_l1_memory_bounds.py
Fork tests check stale values, refresh attempts, recomputation counts, and warnings for libc fork() and os.fork(). A shared helper runs outcomes in a fresh interpreter, and an existing test now uses it.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to c741b

Forked children can serve stale values without starting refresh threads, then recompute after expiry. No merge-blocking issue was identified.

Pre-merge checks | Passed 3 | Failed 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check Warning The description provides detailed motivation, implementation, testing, and documentation context, but it does not follow the required template. It omits the Type of Change, Security Checklist, Testing… Rewrite the description using the repository template. Add all required headings and complete the relevant checkboxes, including the bug-fix type, security checks, test status, backward-compatibility statement, and any additional reviewer n…
Docstring Coverage Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 5 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: preventing synchronous SWR refresh threads in children forked without at-fork hooks.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Description check

Explanation

The description provides detailed motivation, implementation, testing, and documentation context, but it does not follow the required template. It omits the Type of Change, Security Checklist, Testing checklist, Backward Compatibility, and Additional Notes sections.

Resolution

Rewrite the description using the repository template. Add all required headings and complete the relevant checkboxes, including the bug-fix type, security checks, test status, backward-compatibility statement, and any additional reviewer notes.


Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 5 files. (2 skipped: 2 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • 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 12, 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

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Oct 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@27Bslash6
27Bslash6 merged commit dc69e28 into main Oct 12, 2026
38 checks passed
@27Bslash6
27Bslash6 deleted the lab-9129-swr-hookless-fork branch October 12, 2026 15:49
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