From 814cc8af9b82dac9337426ab7b526307f910e5e9 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 2 Mar 2023 08:03:52 -0600 Subject: [PATCH] Fixes #42 QueueTimer keeps Node process from terminating gracefully --- packages/core/src/ExceptionlessClient.ts | 3 +++ packages/core/src/Utils.ts | 10 ++++++++++ .../core/src/plugins/default/DuplicateCheckerPlugin.ts | 3 ++- packages/core/src/plugins/default/HeartbeatPlugin.ts | 3 +++ packages/core/src/queue/DefaultEventQueue.ts | 2 ++ packages/node/src/plugins/NodeLifeCyclePlugin.ts | 9 +++++++++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/core/src/ExceptionlessClient.ts b/packages/core/src/ExceptionlessClient.ts index d652c635..21f67c3f 100644 --- a/packages/core/src/ExceptionlessClient.ts +++ b/packages/core/src/ExceptionlessClient.ts @@ -7,6 +7,7 @@ import { EventContext } from "./models/EventContext.js"; import { EventPluginContext } from "./plugins/EventPluginContext.js"; import { EventPluginManager } from "./plugins/EventPluginManager.js"; import { PluginContext } from "./plugins/PluginContext.js"; +import { allowProcessToExitWithoutWaitingForTimerOrInterval } from "./Utils.js"; export class ExceptionlessClient { private _intervalId: ReturnType | undefined; @@ -92,9 +93,11 @@ export class ExceptionlessClient { void SettingsManager.updateSettings(this.config); if (initialDelay < interval) { this._timeoutId = setTimeout(updateSettings, initialDelay); + allowProcessToExitWithoutWaitingForTimerOrInterval(this._timeoutId); } this._intervalId = setInterval(updateSettings, interval); + allowProcessToExitWithoutWaitingForTimerOrInterval(this._intervalId); } } diff --git a/packages/core/src/Utils.ts b/packages/core/src/Utils.ts index 55ad91b4..52530165 100644 --- a/packages/core/src/Utils.ts +++ b/packages/core/src/Utils.ts @@ -308,3 +308,13 @@ export function toError(errorOrMessage: unknown, defaultMessage = "Unknown Error return new Error(JSON.stringify(errorOrMessage) || defaultMessage); } + + +/** + * Unrefs a timeout or interval. When called, the active Timeout object will not require the Node.js event loop to remain active + */ +export function allowProcessToExitWithoutWaitingForTimerOrInterval(timeoutOrIntervalId: ReturnType | ReturnType | undefined): void { + if (typeof timeoutOrIntervalId === "object" && "unref" in timeoutOrIntervalId) { + (timeoutOrIntervalId as { unref: () => ReturnType | ReturnType }).unref(); + } +} diff --git a/packages/core/src/plugins/default/DuplicateCheckerPlugin.ts b/packages/core/src/plugins/default/DuplicateCheckerPlugin.ts index 26ca526b..ccae12ac 100644 --- a/packages/core/src/plugins/default/DuplicateCheckerPlugin.ts +++ b/packages/core/src/plugins/default/DuplicateCheckerPlugin.ts @@ -1,6 +1,6 @@ import { InnerErrorInfo } from "../../models/data/ErrorInfo.js"; import { KnownEventDataKeys } from "../../models/Event.js"; -import { getHashCode } from "../../Utils.js"; +import { allowProcessToExitWithoutWaitingForTimerOrInterval, getHashCode } from "../../Utils.js"; import { EventPluginContext } from "../EventPluginContext.js"; import { IEventPlugin } from "../IEventPlugin.js"; @@ -25,6 +25,7 @@ export class DuplicateCheckerPlugin implements IEventPlugin { public startup(): Promise { clearInterval(this._intervalId); this._intervalId = setInterval(() => void this.submitEvents(), this._interval); + allowProcessToExitWithoutWaitingForTimerOrInterval(this._intervalId); return Promise.resolve(); } diff --git a/packages/core/src/plugins/default/HeartbeatPlugin.ts b/packages/core/src/plugins/default/HeartbeatPlugin.ts index 46a7c279..ffc543a5 100644 --- a/packages/core/src/plugins/default/HeartbeatPlugin.ts +++ b/packages/core/src/plugins/default/HeartbeatPlugin.ts @@ -1,4 +1,5 @@ import { KnownEventDataKeys } from "../../models/Event.js"; +import { allowProcessToExitWithoutWaitingForTimerOrInterval } from "../../Utils.js"; import { EventPluginContext } from "../EventPluginContext.js"; import { IEventPlugin } from "../IEventPlugin.js"; @@ -49,6 +50,8 @@ export class HeartbeatPlugin implements IEventPlugin { () => void context.client.submitSessionHeartbeat(config.currentSessionIdentifier), this._interval ); + + allowProcessToExitWithoutWaitingForTimerOrInterval(this._intervalId); } return Promise.resolve(); diff --git a/packages/core/src/queue/DefaultEventQueue.ts b/packages/core/src/queue/DefaultEventQueue.ts index 579c5729..5929ee3d 100644 --- a/packages/core/src/queue/DefaultEventQueue.ts +++ b/packages/core/src/queue/DefaultEventQueue.ts @@ -3,6 +3,7 @@ import { ILog } from "../logging/ILog.js"; import { Event } from "../models/Event.js"; import { IEventQueue } from "../queue/IEventQueue.js"; import { Response } from "../submission/Response.js"; +import { allowProcessToExitWithoutWaitingForTimerOrInterval } from "../Utils.js"; interface EventQueueItem { file: string, @@ -133,6 +134,7 @@ export class DefaultEventQueue implements IEventQueue { if (!this._queueIntervalId) { // TODO: Fix awaiting promise. this._queueIntervalId = setInterval(() => void this.onProcessQueue(), 10000); + allowProcessToExitWithoutWaitingForTimerOrInterval(this._queueIntervalId); } return Promise.resolve(); diff --git a/packages/node/src/plugins/NodeLifeCyclePlugin.ts b/packages/node/src/plugins/NodeLifeCyclePlugin.ts index 298293cc..696826cf 100644 --- a/packages/node/src/plugins/NodeLifeCyclePlugin.ts +++ b/packages/node/src/plugins/NodeLifeCyclePlugin.ts @@ -17,7 +17,16 @@ export class NodeLifeCyclePlugin implements IEventPlugin { this._client = context.client; + let processingBeforeExit: boolean = false; process.on("beforeExit", (code: number) => { + // NOTE: We need to check if we are already processing a beforeExit event + // as async work will cause the runtime to call this handler again. + if (processingBeforeExit) { + return; + } + + processingBeforeExit = true; + const message = this.getExitCodeReason(code); if (message) { void this._client?.submitLog("beforeExit", message, "Error");