Skip to content

Commit 85df091

Browse files
committed
Fix abort listener leak in fsm runtime
Signed-off-by: Vladislav Polyakov <polRk@ydb.tech>
1 parent 01777e2 commit 85df091

File tree

4 files changed

+32
-6
lines changed

4 files changed

+32
-6
lines changed

.changeset/mghxjzdsqj.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@ydbjs/fsm': patch
3+
---
4+
5+
Fix memory leak in event ingestion caused by `AbortSignal.any()`
6+
7+
`AbortSignal.any()` registers event listeners on all source signals but never removes them, leading to a listener leak every time an event source is ingested. Replace it with `linkSignals` from `@ydbjs/abortable@^6.1.0`, which uses `Symbol.dispose` to clean up listeners when the ingestion task completes.
8+
9+
Additional improvements:
10+
11+
- Use `combined.signal` instead of `combined` directly for abort checks (correct API usage)
12+
- Separate abort signal checks from internal state checks for better readability and clearer control flow

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/fsm/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
"test": "tsc -b tsconfig.test.json && vitest --run",
4343
"attw": "attw --pack --profile esm-only"
4444
},
45+
"dependencies": {
46+
"@ydbjs/abortable": "^6.1.0"
47+
},
4548
"files": [
4649
"dist",
4750
"!dist/.tsbuildinfo",

packages/fsm/src/runtime.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { linkSignals } from '@ydbjs/abortable'
2+
3+
import { AsyncQueue } from './queue.js'
14
import type {
25
EffectRuntime,
36
IngestHandle,
@@ -7,7 +10,6 @@ import type {
710
RuntimeOptions,
811
TransitionRuntime,
912
} from './types.js'
10-
import { AsyncQueue } from './queue.js'
1113

1214
type InternalEventEnvelope<E> = {
1315
event: E
@@ -87,14 +89,16 @@ class Runtime<
8789
}
8890

8991
let ac = new AbortController()
90-
let combined = signal
91-
? AbortSignal.any([signal, ac.signal, this.signal])
92-
: AbortSignal.any([ac.signal, this.signal])
92+
let combined = linkSignals(signal, ac.signal, this.signal)
9393

9494
let task = (async () => {
9595
try {
9696
for await (let input of source) {
97-
if (combined.aborted || this.#closing || this.#closed || this.#destroyed) {
97+
if (combined.signal.aborted) {
98+
return
99+
}
100+
101+
if (this.#closing || this.#closed || this.#destroyed) {
98102
return
99103
}
100104

@@ -104,7 +108,11 @@ class Runtime<
104108
}
105109
}
106110
} catch (error) {
107-
if (combined.aborted || this.#closing || this.#closed || this.#destroyed) {
111+
if (combined.signal.aborted) {
112+
return
113+
}
114+
115+
if (this.#closing || this.#closed || this.#destroyed) {
108116
return
109117
}
110118

0 commit comments

Comments
 (0)