mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
fix(slack): pre-set shuttingDown before app.stop() to prevent orphaned ping intervals (#56646)
Merged via squash with admin override.
Prepared head SHA: f1c91d50b0
Note: required red lanes are currently inherited from latest origin/main, not introduced by this PR.
Co-authored-by: hsiaoa <70124331+hsiaoa@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
This commit is contained in:
@@ -611,6 +611,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/forum topics: keep native `/new` and `/reset` routed to the active topic by preserving the topic target on forum-thread command context. (#35963)
|
||||
- Status/port diagnostics: treat single-process dual-stack loopback gateway listeners as healthy in `openclaw status --all`, suppressing false "port already in use" conflict warnings. (#53398) Thanks @DanWebb1949.
|
||||
- CLI/Docker: treat loopback private-host CLI gateway connects as local for silent pairing auto-approval, while keeping remote backend and public-host CLI connects behind pairing. (#55113) Thanks @sar618.
|
||||
- Slack/socket mode: mark the underlying socket client as shutting down before provider stop paths call Bolt teardown so stale-socket restarts stop leaking orphaned ping reconnect loops. (#56646) Thanks @hsiaoa.
|
||||
|
||||
## 2026.3.24
|
||||
|
||||
|
||||
@@ -104,4 +104,18 @@ describe("slack socket reconnect helpers", () => {
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
|
||||
it("marks the socket client as shutting down before stop runs", async () => {
|
||||
const app = {
|
||||
receiver: { client: { shuttingDown: false } },
|
||||
stop: vi.fn().mockImplementation(async () => {
|
||||
expect(app.receiver.client.shuttingDown).toBe(true);
|
||||
}),
|
||||
};
|
||||
|
||||
await __testing.gracefulStopSlackApp(app);
|
||||
|
||||
expect(app.stop).toHaveBeenCalledTimes(1);
|
||||
expect(app.receiver.client.shuttingDown).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,9 @@ type SlackBoltResolvedExports = {
|
||||
App: SlackAppConstructor;
|
||||
HTTPReceiver: SlackHttpReceiverConstructor;
|
||||
};
|
||||
type SlackSocketShutdownClient = {
|
||||
shuttingDown?: boolean;
|
||||
};
|
||||
type Constructor = abstract new (...args: never[]) => unknown;
|
||||
|
||||
function isConstructorFunction<T extends Constructor>(value: unknown): value is T {
|
||||
@@ -171,6 +174,29 @@ function publishSlackDisconnectedStatus(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSlackSocketShutdownClient(app: unknown): SlackSocketShutdownClient | undefined {
|
||||
if (!app || typeof app !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const receiver = Reflect.get(app, "receiver");
|
||||
if (!receiver || typeof receiver !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const client = Reflect.get(receiver, "client");
|
||||
if (!client || typeof client !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return client as SlackSocketShutdownClient;
|
||||
}
|
||||
|
||||
async function gracefulStopSlackApp(app: { stop: () => unknown }) {
|
||||
const socketClient = resolveSlackSocketShutdownClient(app);
|
||||
if (socketClient) {
|
||||
socketClient.shuttingDown = true;
|
||||
}
|
||||
await Promise.resolve(app.stop()).catch(() => undefined);
|
||||
}
|
||||
|
||||
function formatSlackResolvedLabel(params: {
|
||||
input: string;
|
||||
id: string;
|
||||
@@ -202,7 +228,6 @@ function formatSlackUserResolved(entry: SlackUserResolution): string {
|
||||
extra: entry.note ? [entry.note] : [],
|
||||
});
|
||||
}
|
||||
|
||||
export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
||||
@@ -319,6 +344,15 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
clientOptions,
|
||||
},
|
||||
);
|
||||
|
||||
// Pre-set shuttingDown on the SocketModeClient before app.stop() to prevent
|
||||
// a race where the library's internal ping timeout fires disconnect() before
|
||||
// shuttingDown is set, causing orphaned reconnects with leaked ping intervals.
|
||||
// See: openclaw/openclaw#56508
|
||||
const gracefulStop = async () => {
|
||||
await gracefulStopSlackApp(app);
|
||||
};
|
||||
|
||||
const slackHttpHandler =
|
||||
slackMode === "http" && receiver
|
||||
? async (req: IncomingMessage, res: ServerResponse) => {
|
||||
@@ -529,7 +563,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
|
||||
const stopOnAbort = () => {
|
||||
if (opts.abortSignal?.aborted && slackMode === "socket") {
|
||||
void app.stop();
|
||||
void gracefulStop();
|
||||
}
|
||||
};
|
||||
opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
|
||||
@@ -607,7 +641,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
disconnect.error ? ` (${formatUnknownError(disconnect.error)})` : ""
|
||||
}`,
|
||||
);
|
||||
await app.stop().catch(() => undefined);
|
||||
await gracefulStop();
|
||||
try {
|
||||
await sleepWithAbort(delayMs, opts.abortSignal);
|
||||
} catch {
|
||||
@@ -628,7 +662,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
||||
unregisterHttpHandler?.();
|
||||
await execApprovalsHandler?.stop().catch(() => undefined);
|
||||
await app.stop().catch(() => undefined);
|
||||
await gracefulStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,6 +675,8 @@ export const __testing = {
|
||||
formatSlackUserResolved,
|
||||
publishSlackConnectedStatus,
|
||||
publishSlackDisconnectedStatus,
|
||||
resolveSlackSocketShutdownClient,
|
||||
gracefulStopSlackApp,
|
||||
resolveSlackRuntimeGroupPolicy: resolveOpenProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveSlackBoltInterop,
|
||||
|
||||
Reference in New Issue
Block a user