mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:30:45 +00:00
fix(agents): stop leaking session lock exit listeners (#65469)
* fix(agents): stop leaking session lock exit listeners * Update src/agents/session-write-lock.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -210,6 +210,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Heartbeat/scheduling: spread interval heartbeats across stable per-agent phases derived from gateway identity, so provider traffic is distributed more uniformly across the configured interval instead of clustering around startup-relative times. (#64560) Thanks @odysseus0.
|
||||
- Config/media: accept `tools.media.asyncCompletion.directSend` in strict config validation so gateways no longer reject the generated-schema-backed async media completion setting at startup. (#63618) Thanks @qiziAI.
|
||||
- Telegram/exec: preserve delayed exec completion routing for forum topics by pinning background exec completions to the topic where the run started even if the session route later drifts. (#64580) thanks @jalehman.
|
||||
- Agents/locks: unregister the session write-lock `exit` cleanup handler during teardown so repeated lock lifecycle resets stop stacking process listeners in long-running gateway processes. (#65391) Thanks @adminfedres and @vincentkoc.
|
||||
|
||||
## 2026.4.9
|
||||
|
||||
|
||||
@@ -426,6 +426,20 @@ describe("acquireSessionWriteLock", () => {
|
||||
await expect(fs.access(lockPath)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not accumulate exit listeners across reset cycles", async () => {
|
||||
const baselineExitListeners = process.listenerCount("exit");
|
||||
|
||||
await withTempSessionLockFile(async ({ sessionFile }) => {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
|
||||
await lock.release();
|
||||
resetSessionWriteLockStateForTest();
|
||||
expect(process.listenerCount("exit")).toBe(baselineExitListeners);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps other signal listeners registered", () => {
|
||||
const keepAlive = () => {};
|
||||
const originalKill = process.kill.bind(process);
|
||||
|
||||
@@ -49,6 +49,7 @@ const MAX_LOCK_HOLD_MS = 2_147_000_000;
|
||||
|
||||
type CleanupState = {
|
||||
registered: boolean;
|
||||
exitHandler?: () => void;
|
||||
cleanupHandlers: Map<CleanupSignal, () => void>;
|
||||
};
|
||||
|
||||
@@ -72,6 +73,7 @@ function resolveCleanupState(): CleanupState {
|
||||
if (!proc[CLEANUP_STATE_KEY]) {
|
||||
proc[CLEANUP_STATE_KEY] = {
|
||||
registered: false,
|
||||
exitHandler: undefined,
|
||||
cleanupHandlers: new Map<CleanupSignal, () => void>(),
|
||||
};
|
||||
}
|
||||
@@ -254,12 +256,13 @@ function handleTerminationSignal(signal: CleanupSignal): void {
|
||||
|
||||
function registerCleanupHandlers(): void {
|
||||
const cleanupState = resolveCleanupState();
|
||||
if (!cleanupState.registered) {
|
||||
cleanupState.registered = true;
|
||||
cleanupState.registered = true;
|
||||
if (!cleanupState.exitHandler) {
|
||||
// Cleanup on normal exit and process.exit() calls
|
||||
process.on("exit", () => {
|
||||
cleanupState.exitHandler = () => {
|
||||
releaseAllLocksSync();
|
||||
});
|
||||
};
|
||||
process.on("exit", cleanupState.exitHandler);
|
||||
}
|
||||
|
||||
ensureWatchdogStarted(DEFAULT_WATCHDOG_INTERVAL_MS);
|
||||
@@ -281,6 +284,10 @@ function registerCleanupHandlers(): void {
|
||||
|
||||
function unregisterCleanupHandlers(): void {
|
||||
const cleanupState = resolveCleanupState();
|
||||
if (cleanupState.exitHandler) {
|
||||
process.off("exit", cleanupState.exitHandler);
|
||||
cleanupState.exitHandler = undefined;
|
||||
}
|
||||
for (const [signal, handler] of cleanupState.cleanupHandlers) {
|
||||
process.off(signal, handler);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user