Skip to content

Commit 6d2e2d5

Browse files
committed
Don't transfer closed spans to new async contexts
This adds another level of protecting to using closed spans in any context when returned by the ScopeManager. It's really tricky to test this in isolation, because fetching the active span, already performs the check. I can't check the private maps of spans in the ScopeManager itself in the test. I've tested it by temporarily removing the `&& span.open` check from `ScopeManager.active()` which confirmed it works.
1 parent 532b827 commit 6d2e2d5

File tree

3 files changed

+37
-8
lines changed

3 files changed

+37
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
bump: "patch"
3+
type: "fix"
4+
---
5+
6+
Do not transfer closed spans for new async contexts in the ScopeManager. Rather than relying on `ScopeManager.active()` and `ScopeManager.root()` to make sure the span is not already closed, also make sure it's not closed when transferring spans around between async contexts.

packages/nodejs/src/__tests__/scope.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ describe("ScopeManager", () => {
6060
asyncTaskWithContext(scopeManager, span, () => scopeManager.active())
6161
).resolves.toBe(span)
6262
})
63+
64+
it("doesn't return active span is span is closed", async () => {
65+
scopeManager = new ScopeManager()
66+
const span = new RootSpan()
67+
68+
scopeManager.enable()
69+
70+
span.close()
71+
await expect(
72+
asyncTaskWithContext(scopeManager, span, () => {
73+
return scopeManager.active()
74+
})
75+
).resolves.toBeUndefined()
76+
})
6377
})
6478

6579
describe(".disable()", () => {

packages/nodejs/src/scope.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,28 @@ export class ScopeManager {
4242
this.#scopes = new Map()
4343

4444
const init = (id: number, type: string, triggerId: number) => {
45+
// Move the current span and root over to the scopes and roots map for
46+
// the child executionAsyncId.
47+
const transferSpanScope = (
48+
list: Map<number, Span | undefined>,
49+
newId: number,
50+
oldId: number
51+
) => {
52+
const span = list.get(oldId)
53+
if (span && span.open) {
54+
list.set(newId, list.get(oldId))
55+
}
56+
}
57+
4558
/**
4659
* We use the `executionAsyncId` here, as using the `triggerId` causes context
4760
* confusion in applications using async/await.
4861
*/
4962
if (type === "PROMISE") {
5063
const currentId = asyncHooks.executionAsyncId()
5164

52-
if (this.#scopes.get(currentId)) {
53-
this.#scopes.set(id, this.#scopes.get(currentId))
54-
this.#roots.set(id, this.#roots.get(currentId))
55-
}
65+
transferSpanScope(this.#scopes, id, currentId)
66+
transferSpanScope(this.#roots, id, currentId)
5667
} else {
5768
/**
5869
* `triggerId` usually equal the ID of the AsyncResource in whose scope we are
@@ -62,10 +73,8 @@ export class ScopeManager {
6273
*
6374
* However, as the `triggerId` can be defined in userland, we choose to respect th
6475
*/
65-
if (this.#scopes.get(triggerId)) {
66-
this.#scopes.set(id, this.#scopes.get(triggerId))
67-
this.#roots.set(id, this.#roots.get(triggerId))
68-
}
76+
transferSpanScope(this.#scopes, id, triggerId)
77+
transferSpanScope(this.#roots, id, triggerId)
6978
}
7079
}
7180

0 commit comments

Comments
 (0)