fix(meteor-promise): create pool fibers without inherited async context - #8
Open
jbaczuk-qualia wants to merge 1 commit into
Open
jbaczuk-qualia wants to merge 1 commit into
jbaczuk-qualia wants to merge 1 commit into
Conversation
Pool fibers are shared between unrelated callers, but each one was created inside whatever AsyncLocalStorage context happened to be active when the pool needed to grow, and the fibers fork copies that context onto the fiber's own AsyncResource. The fiber's own scope is what is active while it parks itself with Fiber.yield() between jobs, so every idle yield reported the creator's context (e.g. the OpenTelemetry root span of a method that finished days ago) for the rest of the process. In prod this showed up as ~11k/s of meteor_fiber_yields_total attributed to methods that were not running. - Create pool fibers inside a module-load-time AsyncResource so they inherit no stores from their creator. - Flag the fiber with _meteorPromisePoolIdle around the idle yield so instrumentation wrapping Fiber.yield can tell the park apart from a real yield. - Add a test that fails against the previous fiber_pool.js. Job callbacks still run inside their own per-job AsyncResource and are unaffected. meteor-promise 0.9.1-2 -> 0.9.1-3, promise 0.12.2 -> 0.12.3. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jbaczuk-qualia
commented
Sep 23, 2026
| // Call Fiber.yield() to await further instructions. | ||
| // Call Fiber.yield() to await further instructions. Flag the fiber as idle so that | ||
| // instrumentation wrapping Fiber.yield can tell this park apart from a real yield. | ||
| fiber._meteorPromisePoolIdle = true; |
Author
There was a problem hiding this comment.
flag this fiber as a idle pool fiber before yielding so we can detect it in the app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
meteor-promise runs every
Promise.asyncApply/ fibered.thencallback on a fiber from a shared pool. Between jobs each pool fiber parks itself withFiber.yield()in its own async scope. The fibers fork gives every fiber anAsyncResourcewhose AsyncLocalStorage stores are copied from the creator once, so a pool fiber created while method M was running carries M's OpenTelemetry context forever, and every idle yield it ever makes is attributed to M.On
qualia-impacttitlethis produced ~11k/s ofmeteor_fiber_yields_totallabelledrefreshDocs,IncomingWires.OrderMatches.refresh, etc. while none of those methods were running (the process had created 205 fibers in 12 hours; all of the traffic was the customer's GraphQL poller going through the pool).Fix
AsyncResource(PRISTINE_ASYNC_SCOPE) so they inherit no stores at all.fiber._meteorPromisePoolIdlearound the idleFiber.yield()soFiber.yieldinstrumentation can label the park explicitly (qualia's prom-client will count it asfiber_pool/idle; companion PR in the qualia repo).Job callbacks still run inside their per-job
AsyncResource(entry._ar) and see the submitter's context as before. Meteor environment variables were already unaffected:fibers_async.jsresets_meteor_dynamicson fiber start, and jobs read them through the job scope.Test
test/tests.js› "fiber pool async context": grows the pool under ALS storecreator, then runs 50 trivial jobs underuserand asserts no yield seescreator, jobs seeuser, and the flagged idle yields see no store. Fails against the previousfiber_pool.jswitha yield during the user phase saw the creator's store; passes with this change. Runs only on the native-Promise pass (thepromisepolyfill's asap scheduler batches unrelated reactions under one async context).Verified on Node 18.16.1 (same as prod).
Versions
meteor-promise
0.9.1-2 → 0.9.1-3,promisepackage0.12.2 → 0.12.3(Npm.dependsupdated).🤖 Generated with Claude Code