-
Notifications
You must be signed in to change notification settings - Fork 2
v3.0.2: run shards rediscovery in separate traces #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| reviews: | ||
| auto_review: | ||
| enabled: true | ||
| base_branches: | ||
| - main | ||
| - "grok/*" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { AsyncLocalStorage } from "async_hooks"; | ||
| import { createHash } from "crypto"; | ||
| import { inspect } from "util"; | ||
| import compact from "lodash/compact"; | ||
|
|
@@ -399,6 +400,43 @@ export function runInVoid( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * A snapshot of the "clean" async context, captured at module load time, i.e. | ||
| * BEFORE any request-scoped async context (such as an incoming HTTP request, and | ||
| * thus the APM/tracing "trace id" that tracers propagate through | ||
| * AsyncLocalStorage) could possibly have been established. Calling the returned | ||
| * runner executes a callback within that captured clean context. | ||
| * | ||
| * We deliberately use AsyncLocalStorage.snapshot() and NOT AsyncResource: both | ||
| * can detach an async context, but AsyncLocalStorage (including .snapshot()) is | ||
| * supported across runtimes (Node, Deno, Bun), whereas AsyncResource is a | ||
| * non-functional stub in some of them. If .snapshot() is unavailable (very old | ||
| * runtimes), we degrade gracefully to running the callback inline (no | ||
| * detachment). | ||
| */ | ||
| const runInRootAsyncContext: <T>(func: () => T) => T = | ||
| typeof AsyncLocalStorage?.snapshot === "function" | ||
| ? AsyncLocalStorage.snapshot() | ||
| : (func) => func(); | ||
|
Comment on lines
+403
to
+420
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the file and print the relevant region around the snippet and call sites
ls -la
sed -n '360,520p' src/internal/misc.ts | nl -ba | sed -n '1,220p'
# Find all references to runInRootAsyncContext / maybeRunInSeparateTrace
rg -n "runInRootAsyncContext|maybeRunInSeparateTrace|AsyncLocalStorage\.snapshot" -S src/internal/misc.ts src/internal -g'*.ts'Repository: dimikot/ent-framework Length of output: 2381 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show the relevant region with line numbers (cat -n)
sed -n '360,520p' src/internal/misc.ts | cat -n
# Search for the key symbols/usages across the repo (scoped to TS)
rg -n "runInRootAsyncContext|maybeRunInSeparateTrace|AsyncLocalStorage\.snapshot" -S src -g'*.ts'Repository: dimikot/ent-framework Length of output: 6187 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,120p' src/internal/misc.ts | cat -n
sed -n '380,470p' src/internal/misc.ts | cat -n
sed -n '120,210p' src/internal/CachedRefreshedValue.ts | cat -nRepository: dimikot/ent-framework Length of output: 13354 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find all usages of maybeRunInSeparateTrace and where misc.ts is imported
rg -n "maybeRunInSeparateTrace\\(" -S src -g'*.ts'
rg -n "import\\s*\\{\\s*maybeRunInSeparateTrace\\b" -S src -g'*.ts'
# Search for AsyncLocalStorage instantiation / usage across the repo
rg -n "new\\s+AsyncLocalStorage\\b|AsyncLocalStorage\\." -S src -g'*.ts'Repository: dimikot/ent-framework Length of output: 175 🌐 Web query:
💡 Result: The AsyncLocalStorage.snapshot method is a static method in Node.js [1][2][3]. It captures the current execution context and returns a function that, when called, executes the provided function within that captured context [1][4]. It is designed to be called directly on the AsyncLocalStorage class, rather than on an instance of the class [1][5]. Citations:
Don’t freeze the “clean” detached AsyncLocalStorage runner at module init
Regression idea: import 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Runs an async function detached from the async context (and thus from the | ||
| * APM/tracing trace id, if any) that is active at the call site: the callback | ||
| * runs in the "clean" context that was captured at module load time instead. | ||
| * Each call starts its own fresh async subtree rooted in that clean context, so: | ||
| * - async_hooks-based tracers observe each call as a separate, brand-new trace | ||
| * (the tracer sees no active trace and starts a new one), and | ||
| * - all the async work spawned within a single call shares that one trace. | ||
| * | ||
| * This is used for background loops (e.g. Shards rediscovery) which are kicked | ||
| * off lazily by the very first query: without this, every subsequent timer- | ||
| * triggered loop iteration would forever remain attached to (and would thus | ||
| * pollute) the trace id of that very first query. | ||
| */ | ||
| export async function maybeRunInSeparateTrace<T>(keepCallerTrace: boolean, func: () => Promise<T>): Promise<T> { | ||
| return keepCallerTrace ? func() : runInRootAsyncContext(func); | ||
| } | ||
|
|
||
| /** | ||
| * A typesafe-way to invariant the object's key presence and being | ||
| * non-undefined. It is not always working for union types: sometimes it asserts | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 202
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 11768
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 1102
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 424
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 3932
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 96
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 4620
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 181
🏁 Script executed:
Repository: dimikot/ent-framework
Length of output: 101
Fix:
refreshAndWait()manual refresh should run the next pass in the requesting caller’s trace, not the loop starter’s trace.skipDelayonly flips thekeepCallerTraceboolean (lines 164-167). WhenkeepCallerTracebecomestrue,maybeRunInSeparateTrace(true)just runs inline (func()), so the next resolver pass still executes inrefreshLoop()’s current AsyncLocalStorage context (the loop starter), not the caller that invokedrefreshAndWait(). A regression should show two distinct trace IDs when two callers interact with the same in-flight loop (starting in caller A, triggeringrefreshAndWait()from caller B).depsTimeoutBodyat lines 169-183) callsthis.options.deps.handler()withoutmaybeRunInSeparateTrace, so timer-driven deps checks can also attach to the loop starter’s trace despite the comment’s “timer/deps change must NOT pollute” intent.🤖 Prompt for AI Agents