fix(worker): decide who deletes a WorkerWrapper with one atomic state - #479
adrian-niculescu wants to merge 1 commit into
Conversation
The worker thread and the parent's teardown each checked one flag and set another, in opposite orders, so a parent that tore down while a child worker was ending could delete the wrapper under the child's ~Runtime, or leave it to nobody. One Holders state replaces both flags: each side lets go with a compare-and-swap, and whoever finds itself last deletes.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesWorker lifetime ownership
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~30 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant Parent
participant ObjectManager
participant WorkerWrapper
participant BackgroundLooper
participant Runtime
Parent->>ObjectManager: Dispose worker wrapper
ObjectManager->>WorkerWrapper: ReleaseFromParent
BackgroundLooper->>Runtime: Tear down worker runtime
BackgroundLooper->>WorkerWrapper: ReleaseFromWorkerThread
WorkerWrapper->>Parent: Post worker-ended notification
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This change reworks how the native worker wrapper's memory is released between the parent thread and the worker's background thread, replacing a simpler weak-reference flag with a coordinated ownership handoff, and adds a stress test that repeatedly terminates and closes a worker with a running child. Verification of the core ownership handoff, the object finalizer re-registration, and the shared worker registry's thread-safety found no double-free, leak, or race introduced by this change, so it appears safe to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (3 skipped: 3 unsupported.)
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. A rabbit guards the holder state, Comment |
A worker that ends while a child worker of its own is still running (terminated by its parent, or calling
close()) reads freed memory on the child's thread. With AddressSanitizer on the TestRunner the new spec aborts onmain:The same applies to an embedder that shuts the main runtime down while workers run. Without a sanitizer it is usually silent; when the freed byte happens to read non-zero it becomes a double free.
The parent's teardown terminates its children and, a few lines later, disposes their Worker objects, so both threads reach the end of a child's
WorkerWrappertogether. Who deletes it was decided by two flags checked in opposite orders: the worker publishedisDisposed_and later readisWeak_, the parent readisDisposed_and then either deleted or setisWeak_. That allows:isDisposed_, while the child's~Runtimestill reaches the wrapper through itsCaches::Workersentry (any other runtime's~Runtimewalking the registry can hit the stale entry too);This replaces both flags with one atomic
Holders { Parent, Both, WorkerThread }. It starts asParent, andStart()makes itBoth. The worker thread lets go as its last touch of the wrapper, after its registry entry is removed (both branches): CASBoth -> Parent, or delete if it findsWorkerThread. The parent's final disposal does CASBoth -> WorkerThread, or deletes if it findsParent. The finalizer only deletes inParent. Exactly one CAS out ofBothsucceeds, so exactly one side ends up last. Registry readers stay safe becauseForEachholds the map mutex across its callback and the worker'sRemovehas to get that mutex before the wrapper can become deletable by anyone. A wrapper that never reachedStart()staysParent, so its finalizer can free it, which the old flags never allowed.isDisposed_remains only as the worker thread's own "teardown has begun" flag.The new spec in
WorkerLifetimeTests.jsruns 24 parent/child rounds, alternatingterminate()andclose(). It needs ASan to fail onmain; with the change the full suite is clean under ASan apart from the thread QoS specs, which read a different QoS class back when the sanitizer interposes thread creation.Summary by CodeRabbit
Bug Fixes
Tests