fix(core): Ensure task runner module and server shut down (no-changelog) (#11801)

Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
Iván Ovejero 2024-11-20 10:55:19 +01:00 committed by GitHub
parent d15b8d0509
commit 2632b1fb7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 27 deletions

View file

@ -17,12 +17,14 @@ describe('TaskRunnerWsServer', () => {
mock(), mock(),
); );
server.start();
expect(setIntervalSpy).toHaveBeenCalledWith( expect(setIntervalSpy).toHaveBeenCalledWith(
expect.any(Function), expect.any(Function),
30 * Time.seconds.toMilliseconds, 30 * Time.seconds.toMilliseconds,
); );
await server.shutdown(); await server.stop();
}); });
it('should clear heartbeat timer on server stop', async () => { it('should clear heartbeat timer on server stop', async () => {
@ -36,8 +38,9 @@ describe('TaskRunnerWsServer', () => {
mock<TaskRunnersConfig>({ path: '/runners', heartbeatInterval: 30 }), mock<TaskRunnersConfig>({ path: '/runners', heartbeatInterval: 30 }),
mock(), mock(),
); );
server.start();
await server.shutdown(); await server.stop();
expect(clearIntervalSpy).toHaveBeenCalled(); expect(clearIntervalSpy).toHaveBeenCalled();
}); });

View file

@ -21,6 +21,16 @@ function heartbeat(this: WebSocket) {
this.isAlive = true; this.isAlive = true;
} }
const enum WsStatusCode {
CloseNormal = 1000,
CloseGoingAway = 1001,
CloseProtocolError = 1002,
CloseUnsupportedData = 1003,
CloseNoStatus = 1005,
CloseAbnormal = 1006,
CloseInvalidData = 1007,
}
@Service() @Service()
export class TaskRunnerWsServer { export class TaskRunnerWsServer {
runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map(); runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map();
@ -33,7 +43,9 @@ export class TaskRunnerWsServer {
private disconnectAnalyzer: DefaultTaskRunnerDisconnectAnalyzer, private disconnectAnalyzer: DefaultTaskRunnerDisconnectAnalyzer,
private readonly taskTunnersConfig: TaskRunnersConfig, private readonly taskTunnersConfig: TaskRunnersConfig,
private readonly runnerLifecycleEvents: RunnerLifecycleEvents, private readonly runnerLifecycleEvents: RunnerLifecycleEvents,
) { ) {}
start() {
this.startHeartbeatChecks(); this.startHeartbeatChecks();
} }
@ -47,7 +59,11 @@ export class TaskRunnerWsServer {
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
for (const [runnerId, connection] of this.runnerConnections.entries()) { for (const [runnerId, connection] of this.runnerConnections.entries()) {
if (!connection.isAlive) { if (!connection.isAlive) {
void this.removeConnection(runnerId, 'failed-heartbeat-check'); void this.removeConnection(
runnerId,
'failed-heartbeat-check',
WsStatusCode.CloseNoStatus,
);
this.runnerLifecycleEvents.emit('runner:failed-heartbeat-check'); this.runnerLifecycleEvents.emit('runner:failed-heartbeat-check');
return; return;
} }
@ -57,17 +73,13 @@ export class TaskRunnerWsServer {
}, heartbeatInterval * Time.seconds.toMilliseconds); }, heartbeatInterval * Time.seconds.toMilliseconds);
} }
async shutdown() { async stop() {
if (this.heartbeatTimer) { if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer); clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined; this.heartbeatTimer = undefined;
} }
await Promise.all( await this.stopConnectedRunners();
Array.from(this.runnerConnections.keys()).map(
async (id) => await this.removeConnection(id, 'shutting-down'),
),
);
} }
setDisconnectAnalyzer(disconnectAnalyzer: DisconnectAnalyzer) { setDisconnectAnalyzer(disconnectAnalyzer: DisconnectAnalyzer) {
@ -141,7 +153,11 @@ export class TaskRunnerWsServer {
); );
} }
async removeConnection(id: TaskRunner['id'], reason: DisconnectReason = 'unknown') { async removeConnection(
id: TaskRunner['id'],
reason: DisconnectReason = 'unknown',
code?: WsStatusCode,
) {
const connection = this.runnerConnections.get(id); const connection = this.runnerConnections.get(id);
if (connection) { if (connection) {
const disconnectError = await this.disconnectAnalyzer.toDisconnectError({ const disconnectError = await this.disconnectAnalyzer.toDisconnectError({
@ -150,7 +166,7 @@ export class TaskRunnerWsServer {
heartbeatInterval: this.taskTunnersConfig.heartbeatInterval, heartbeatInterval: this.taskTunnersConfig.heartbeatInterval,
}); });
this.taskBroker.deregisterRunner(id, disconnectError); this.taskBroker.deregisterRunner(id, disconnectError);
connection.close(); connection.close(code);
this.runnerConnections.delete(id); this.runnerConnections.delete(id);
} }
} }
@ -158,4 +174,14 @@ export class TaskRunnerWsServer {
handleRequest(req: TaskRunnerServerInitRequest, _res: TaskRunnerServerInitResponse) { handleRequest(req: TaskRunnerServerInitRequest, _res: TaskRunnerServerInitResponse) {
this.add(req.query.id, req.ws); this.add(req.query.id, req.ws);
} }
private async stopConnectedRunners() {
// TODO: We should give runners some time to finish their tasks before
// shutting them down
await Promise.all(
Array.from(this.runnerConnections.keys()).map(
async (id) => await this.removeConnection(id, 'shutting-down', WsStatusCode.CloseGoingAway),
),
);
}
} }

View file

@ -2,6 +2,7 @@ import { TaskRunnersConfig } from '@n8n/config';
import * as a from 'node:assert/strict'; import * as a from 'node:assert/strict';
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { OnShutdown } from '@/decorators/on-shutdown';
import type { TaskRunnerProcess } from '@/runners/task-runner-process'; import type { TaskRunnerProcess } from '@/runners/task-runner-process';
import { MissingAuthTokenError } from './errors/missing-auth-token.error'; import { MissingAuthTokenError } from './errors/missing-auth-token.error';
@ -41,16 +42,23 @@ export class TaskRunnerModule {
} }
} }
@OnShutdown()
async stop() { async stop() {
const stopRunnerProcessTask = (async () => {
if (this.taskRunnerProcess) { if (this.taskRunnerProcess) {
await this.taskRunnerProcess.stop(); await this.taskRunnerProcess.stop();
this.taskRunnerProcess = undefined; this.taskRunnerProcess = undefined;
} }
})();
const stopRunnerServerTask = (async () => {
if (this.taskRunnerHttpServer) { if (this.taskRunnerHttpServer) {
await this.taskRunnerHttpServer.stop(); await this.taskRunnerHttpServer.stop();
this.taskRunnerHttpServer = undefined; this.taskRunnerHttpServer = undefined;
} }
})();
await Promise.all([stopRunnerProcessTask, stopRunnerServerTask]);
} }
private async loadTaskManager() { private async loadTaskManager() {

View file

@ -9,8 +9,7 @@ import { parse as parseUrl } from 'node:url';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { Server as WSServer } from 'ws'; import { Server as WSServer } from 'ws';
import { inTest, LOWEST_SHUTDOWN_PRIORITY } from '@/constants'; import { inTest } from '@/constants';
import { OnShutdown } from '@/decorators/on-shutdown';
import { Logger } from '@/logging/logger.service'; import { Logger } from '@/logging/logger.service';
import { bodyParser, rawBodyReader } from '@/middlewares'; import { bodyParser, rawBodyReader } from '@/middlewares';
import { send } from '@/response-helper'; import { send } from '@/response-helper';
@ -69,16 +68,22 @@ export class TaskRunnerServer {
this.configureRoutes(); this.configureRoutes();
} }
@OnShutdown(LOWEST_SHUTDOWN_PRIORITY)
async stop(): Promise<void> { async stop(): Promise<void> {
if (this.wsServer) { if (this.wsServer) {
this.wsServer.close(); this.wsServer.close();
this.wsServer = undefined; this.wsServer = undefined;
} }
const stopHttpServerTask = (async () => {
if (this.server) { if (this.server) {
await new Promise<void>((resolve) => this.server?.close(() => resolve())); await new Promise<void>((resolve) => this.server?.close(() => resolve()));
this.server = undefined; this.server = undefined;
} }
})();
const stopWsServerTask = this.taskRunnerWsServer.stop();
await Promise.all([stopHttpServerTask, stopWsServerTask]);
} }
/** Creates an HTTP server and listens to the configured port */ /** Creates an HTTP server and listens to the configured port */
@ -119,6 +124,8 @@ export class TaskRunnerServer {
maxPayload: this.globalConfig.taskRunners.maxPayload, maxPayload: this.globalConfig.taskRunners.maxPayload,
}); });
this.server.on('upgrade', this.handleUpgradeRequest); this.server.on('upgrade', this.handleUpgradeRequest);
this.taskRunnerWsServer.start();
} }
private async setupErrorHandlers() { private async setupErrorHandlers() {