lifecycleion 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/lib/lifecycle-manager/index.cjs +422 -14
- package/dist/lib/lifecycle-manager/index.cjs.map +1 -1
- package/dist/lib/lifecycle-manager/index.d.cts +284 -3
- package/dist/lib/lifecycle-manager/index.d.ts +284 -3
- package/dist/lib/lifecycle-manager/index.js +422 -14
- package/dist/lib/lifecycle-manager/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -956,6 +956,9 @@ var ComponentLifecycle = class {
|
|
|
956
956
|
getSignalStatus() {
|
|
957
957
|
return this.manager.getSignalStatus();
|
|
958
958
|
}
|
|
959
|
+
getShutdownEscalationStatus() {
|
|
960
|
+
return this.manager.getShutdownEscalationStatus();
|
|
961
|
+
}
|
|
959
962
|
triggerReload() {
|
|
960
963
|
return this.manager.triggerReload();
|
|
961
964
|
}
|
|
@@ -1134,6 +1137,15 @@ var LifecycleManagerEvents = class {
|
|
|
1134
1137
|
lifecycleManagerShutdownCompleted(input) {
|
|
1135
1138
|
this.emit("lifecycle-manager:shutdown-completed", input);
|
|
1136
1139
|
}
|
|
1140
|
+
lifecycleManagerShutdownEscalationArmed(input) {
|
|
1141
|
+
this.emit("lifecycle-manager:shutdown-escalation-armed", input);
|
|
1142
|
+
}
|
|
1143
|
+
lifecycleManagerShutdownEscalationExpired(input) {
|
|
1144
|
+
this.emit("lifecycle-manager:shutdown-escalation-expired", input);
|
|
1145
|
+
}
|
|
1146
|
+
lifecycleManagerShutdownEscalationForced(input) {
|
|
1147
|
+
this.emit("lifecycle-manager:shutdown-escalation-forced", input);
|
|
1148
|
+
}
|
|
1137
1149
|
componentStarting(name) {
|
|
1138
1150
|
this.emit("component:starting", { name });
|
|
1139
1151
|
}
|
|
@@ -1210,8 +1222,8 @@ var LifecycleManagerEvents = class {
|
|
|
1210
1222
|
componentStartupRollback(name) {
|
|
1211
1223
|
this.emit("component:startup-rollback", { name });
|
|
1212
1224
|
}
|
|
1213
|
-
signalShutdown(method) {
|
|
1214
|
-
this.emit("signal:shutdown", { method });
|
|
1225
|
+
signalShutdown(method, isAlreadyShuttingDown = false) {
|
|
1226
|
+
this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
|
|
1215
1227
|
}
|
|
1216
1228
|
signalReload() {
|
|
1217
1229
|
this.emit("signal:reload", void 0);
|
|
@@ -1921,6 +1933,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1921
1933
|
attachSignalsBeforeStartup;
|
|
1922
1934
|
attachSignalsOnStart;
|
|
1923
1935
|
detachSignalsOnStop;
|
|
1936
|
+
repeatedShutdownRequestPolicy;
|
|
1924
1937
|
// Component management
|
|
1925
1938
|
components = [];
|
|
1926
1939
|
runningComponents = /* @__PURE__ */ new Set();
|
|
@@ -1940,6 +1953,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1940
1953
|
pendingLoggerExitResolve = null;
|
|
1941
1954
|
shutdownMethod = null;
|
|
1942
1955
|
lastShutdownResult = null;
|
|
1956
|
+
repeatedShutdownExpiryTimer = null;
|
|
1957
|
+
repeatedShutdownRequestState = {
|
|
1958
|
+
requestCount: 0,
|
|
1959
|
+
firstMethod: null,
|
|
1960
|
+
latestMethod: null,
|
|
1961
|
+
firstRequestAt: null,
|
|
1962
|
+
latestRequestAt: null,
|
|
1963
|
+
repeatedWindowStartedAt: null,
|
|
1964
|
+
hasTriggeredForceShutdown: false,
|
|
1965
|
+
remainsArmedUntil: null
|
|
1966
|
+
};
|
|
1943
1967
|
// Signal management
|
|
1944
1968
|
processSignalManager = null;
|
|
1945
1969
|
onReloadRequested;
|
|
@@ -1966,6 +1990,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1966
1990
|
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
1967
1991
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
1968
1992
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
1993
|
+
const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
|
|
1994
|
+
if (repeatedShutdownRequestPolicy === void 0) {
|
|
1995
|
+
this.repeatedShutdownRequestPolicy = void 0;
|
|
1996
|
+
} else {
|
|
1997
|
+
const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
|
|
1998
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS
|
|
1999
|
+
);
|
|
2000
|
+
const forceAfterCount = finiteClampMin(
|
|
2001
|
+
repeatedShutdownRequestPolicy.forceAfterCount,
|
|
2002
|
+
1,
|
|
2003
|
+
3
|
|
2004
|
+
);
|
|
2005
|
+
const withinMS = finiteClampMin(
|
|
2006
|
+
repeatedShutdownRequestPolicy.withinMS,
|
|
2007
|
+
0,
|
|
2008
|
+
2e3
|
|
2009
|
+
);
|
|
2010
|
+
const armedAfterFailureMS = finiteClampMin(
|
|
2011
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
2012
|
+
0,
|
|
2013
|
+
withinMS * forceAfterCount
|
|
2014
|
+
);
|
|
2015
|
+
this.repeatedShutdownRequestPolicy = {
|
|
2016
|
+
forceAfterCount,
|
|
2017
|
+
withinMS,
|
|
2018
|
+
armedAfterFailureMS,
|
|
2019
|
+
countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
|
|
2020
|
+
hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
|
|
2021
|
+
onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
1969
2024
|
this.onReloadRequested = options.onReloadRequested;
|
|
1970
2025
|
this.onInfoRequested = options.onInfoRequested;
|
|
1971
2026
|
this.onDebugRequested = options.onDebugRequested;
|
|
@@ -2534,6 +2589,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2534
2589
|
};
|
|
2535
2590
|
}
|
|
2536
2591
|
this.isStarting = true;
|
|
2592
|
+
this.resetRepeatedShutdownRequestState();
|
|
2537
2593
|
this.shutdownMethod = null;
|
|
2538
2594
|
this.lastShutdownResult = null;
|
|
2539
2595
|
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
@@ -2974,6 +3030,51 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2974
3030
|
shutdownMethod: this.shutdownMethod
|
|
2975
3031
|
};
|
|
2976
3032
|
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Get status information about repeated shutdown escalation configuration and runtime state.
|
|
3035
|
+
*/
|
|
3036
|
+
getShutdownEscalationStatus() {
|
|
3037
|
+
if (this.repeatedShutdownRequestPolicy === void 0) {
|
|
3038
|
+
return {
|
|
3039
|
+
configured: false,
|
|
3040
|
+
isShuttingDown: this.isShuttingDown,
|
|
3041
|
+
isArmed: false,
|
|
3042
|
+
forceAfterCount: null,
|
|
3043
|
+
withinMS: null,
|
|
3044
|
+
armedAfterFailureMS: null,
|
|
3045
|
+
armedAfterFailureMSSource: null,
|
|
3046
|
+
requestCount: 0,
|
|
3047
|
+
firstMethod: null,
|
|
3048
|
+
latestMethod: null,
|
|
3049
|
+
firstRequestAt: null,
|
|
3050
|
+
latestRequestAt: null,
|
|
3051
|
+
repeatedWindowStartedAt: null,
|
|
3052
|
+
armedUntil: null,
|
|
3053
|
+
hasTriggeredForceShutdown: false
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
3057
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
3058
|
+
const isArmed = armedUntil !== null;
|
|
3059
|
+
return {
|
|
3060
|
+
configured: true,
|
|
3061
|
+
isShuttingDown: this.isShuttingDown,
|
|
3062
|
+
isArmed,
|
|
3063
|
+
forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
|
|
3064
|
+
withinMS: this.repeatedShutdownRequestPolicy.withinMS,
|
|
3065
|
+
armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
3066
|
+
armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
|
|
3067
|
+
countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
|
|
3068
|
+
requestCount: this.repeatedShutdownRequestState.requestCount,
|
|
3069
|
+
firstMethod: this.repeatedShutdownRequestState.firstMethod,
|
|
3070
|
+
latestMethod: this.repeatedShutdownRequestState.latestMethod,
|
|
3071
|
+
firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
|
|
3072
|
+
latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
|
|
3073
|
+
repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
|
|
3074
|
+
armedUntil: isArmed ? armedUntil : null,
|
|
3075
|
+
hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
2977
3078
|
/**
|
|
2978
3079
|
* Enable Logger exit hook integration
|
|
2979
3080
|
*
|
|
@@ -3999,9 +4100,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3999
4100
|
code: "already_in_progress"
|
|
4000
4101
|
};
|
|
4001
4102
|
}
|
|
4103
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
4104
|
+
const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
|
|
4105
|
+
const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
|
|
4106
|
+
if (isManualRetryWhileArmed) {
|
|
4107
|
+
if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
|
|
4108
|
+
this.handleRepeatedShutdownRequest(method);
|
|
4109
|
+
} else {
|
|
4110
|
+
this.resetRepeatedShutdownRequestState();
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
|
|
4114
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
4115
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = null;
|
|
4116
|
+
}
|
|
4002
4117
|
this.isShuttingDown = true;
|
|
4003
4118
|
this.shutdownToken = ulid2();
|
|
4004
4119
|
this.shutdownMethod = method;
|
|
4120
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
|
|
4121
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
4122
|
+
}
|
|
4005
4123
|
const isDuringStartup = this.isStarting;
|
|
4006
4124
|
this.logger.info("Stopping all components", { params: { method } });
|
|
4007
4125
|
this.lifecycleEvents.lifecycleManagerShutdownInitiated(
|
|
@@ -4101,15 +4219,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4101
4219
|
} else {
|
|
4102
4220
|
await shutdownOperation();
|
|
4103
4221
|
}
|
|
4222
|
+
if (!shouldRetryStalled) {
|
|
4223
|
+
for (const name of stalledComponentNames) {
|
|
4224
|
+
const stallInfo = this.stalledComponents.get(name);
|
|
4225
|
+
if (stallInfo && !stalledComponents.some((component) => component.name === name)) {
|
|
4226
|
+
stalledComponents.push(stallInfo);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4104
4230
|
const durationMS = Date.now() - startTime;
|
|
4105
4231
|
const isSuccess = !hasTimedOut && stalledComponents.length === 0;
|
|
4106
|
-
this.logger[isSuccess ? "success" : "warn"](
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4232
|
+
this.logger[isSuccess ? "success" : "warn"](
|
|
4233
|
+
isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
|
|
4234
|
+
{
|
|
4235
|
+
params: {
|
|
4236
|
+
stopped: stoppedComponents.length,
|
|
4237
|
+
stalled: stalledComponents.length,
|
|
4238
|
+
durationMS
|
|
4239
|
+
}
|
|
4111
4240
|
}
|
|
4112
|
-
|
|
4241
|
+
);
|
|
4113
4242
|
const result = {
|
|
4114
4243
|
success: isSuccess,
|
|
4115
4244
|
stoppedComponents,
|
|
@@ -4127,6 +4256,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4127
4256
|
method,
|
|
4128
4257
|
duringStartup: isDuringStartup
|
|
4129
4258
|
});
|
|
4259
|
+
if (isSuccess) {
|
|
4260
|
+
this.resetRepeatedShutdownRequestState();
|
|
4261
|
+
} else {
|
|
4262
|
+
this.armRepeatedShutdownAfterFailure();
|
|
4263
|
+
}
|
|
4130
4264
|
return result;
|
|
4131
4265
|
} finally {
|
|
4132
4266
|
if (timeoutHandle) {
|
|
@@ -5255,21 +5389,295 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5255
5389
|
}
|
|
5256
5390
|
/**
|
|
5257
5391
|
* Handle shutdown signal - initiates stopAllComponents().
|
|
5258
|
-
*
|
|
5392
|
+
*
|
|
5393
|
+
* Four cases depending on the current shutdown state:
|
|
5394
|
+
*
|
|
5395
|
+
* 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
|
|
5396
|
+
* repeated-shutdown policy if configured, otherwise log and discard.
|
|
5397
|
+
* Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
|
|
5398
|
+
* without starting another shutdown.
|
|
5399
|
+
*
|
|
5400
|
+
* 2. **Armed post-failure** (previous shutdown finished, armed window still
|
|
5401
|
+
* open): count the request toward the escalation window, emit
|
|
5402
|
+
* `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
|
|
5403
|
+
* new `stopAllComponents()` run to retry.
|
|
5404
|
+
*
|
|
5405
|
+
* 3. **Armed post-failure expired** (armed window opened but has since
|
|
5406
|
+
* elapsed): expire the stale state, treat the request as a fresh
|
|
5407
|
+
* shutdown (falls through to case 4).
|
|
5408
|
+
*
|
|
5409
|
+
* 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
|
|
5410
|
+
* if policy is configured, emit `signal:shutdown` with
|
|
5411
|
+
* `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
|
|
5412
|
+
*
|
|
5413
|
+
* In all cases `signal:shutdown` is emitted exactly once.
|
|
5259
5414
|
*/
|
|
5260
5415
|
handleShutdownRequest(method) {
|
|
5261
5416
|
if (this.isShuttingDown) {
|
|
5262
|
-
this.
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5417
|
+
this.lifecycleEvents.signalShutdown(method, true);
|
|
5418
|
+
if (this.handleRepeatedShutdownRequest(method)) {
|
|
5419
|
+
return;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
let didEmitShutdownSignal = false;
|
|
5423
|
+
let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
|
|
5424
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
|
|
5425
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5426
|
+
didEmitShutdownSignal = true;
|
|
5427
|
+
shouldSeedRepeatedShutdownState = false;
|
|
5428
|
+
this.handleRepeatedShutdownRequest(method);
|
|
5429
|
+
}
|
|
5430
|
+
if (shouldSeedRepeatedShutdownState) {
|
|
5431
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
5266
5432
|
}
|
|
5267
5433
|
this.logger.info("Shutdown signal received", { params: { method } });
|
|
5268
|
-
|
|
5434
|
+
if (!didEmitShutdownSignal) {
|
|
5435
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5436
|
+
}
|
|
5269
5437
|
void this.stopAllComponentsInternal(method, {
|
|
5270
5438
|
...this.shutdownOptions
|
|
5271
5439
|
});
|
|
5272
5440
|
}
|
|
5441
|
+
/**
|
|
5442
|
+
* Tracks repeated shutdown requests during an active shutdown and optionally
|
|
5443
|
+
* invokes the configured force shutdown callback when the threshold is reached.
|
|
5444
|
+
*
|
|
5445
|
+
* @returns true when the request was consumed as part of the repeated-shutdown
|
|
5446
|
+
* escalation flow, false when the caller should treat it as a fresh shutdown request
|
|
5447
|
+
*/
|
|
5448
|
+
handleRepeatedShutdownRequest(method) {
|
|
5449
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5450
|
+
if (!policy) {
|
|
5451
|
+
this.logger.warn("Shutdown already in progress, ignoring signal", {
|
|
5452
|
+
params: { method }
|
|
5453
|
+
});
|
|
5454
|
+
return true;
|
|
5455
|
+
}
|
|
5456
|
+
const now = Date.now();
|
|
5457
|
+
const state = this.repeatedShutdownRequestState;
|
|
5458
|
+
if (state.remainsArmedUntil !== null) {
|
|
5459
|
+
if (now >= state.remainsArmedUntil) {
|
|
5460
|
+
this.expireRepeatedShutdownRequestState();
|
|
5461
|
+
return false;
|
|
5462
|
+
}
|
|
5463
|
+
this.refreshRepeatedShutdownArmedWindow(now);
|
|
5464
|
+
}
|
|
5465
|
+
const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
|
|
5466
|
+
if (shouldStartNewWindow) {
|
|
5467
|
+
state.requestCount = 1;
|
|
5468
|
+
state.repeatedWindowStartedAt = now;
|
|
5469
|
+
} else {
|
|
5470
|
+
state.requestCount++;
|
|
5471
|
+
}
|
|
5472
|
+
state.latestMethod = method;
|
|
5473
|
+
state.latestRequestAt = now;
|
|
5474
|
+
this.logger.warn(
|
|
5475
|
+
this.isShuttingDown ? "Shutdown already in progress, tracking repeated signal" : "Previous shutdown attempt finished with stalled components or timeout, escalation window still armed, tracking repeated request",
|
|
5476
|
+
{
|
|
5477
|
+
params: {
|
|
5478
|
+
method,
|
|
5479
|
+
requestCount: state.requestCount,
|
|
5480
|
+
firstMethod: state.firstMethod,
|
|
5481
|
+
latestMethod: state.latestMethod,
|
|
5482
|
+
firstRequestAt: state.firstRequestAt,
|
|
5483
|
+
latestRequestAt: state.latestRequestAt,
|
|
5484
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5485
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5486
|
+
withinMS: policy.withinMS,
|
|
5487
|
+
forceAfterCount: policy.forceAfterCount
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
);
|
|
5491
|
+
if (
|
|
5492
|
+
// Force escalation is single-fire per shutdown cycle. Later requests are
|
|
5493
|
+
// still logged but do not re-enter user force-shutdown logic.
|
|
5494
|
+
state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
|
|
5495
|
+
) {
|
|
5496
|
+
return true;
|
|
5497
|
+
}
|
|
5498
|
+
state.hasTriggeredForceShutdown = true;
|
|
5499
|
+
const context = {
|
|
5500
|
+
requestCount: state.requestCount,
|
|
5501
|
+
firstMethod: state.firstMethod,
|
|
5502
|
+
latestMethod: state.latestMethod,
|
|
5503
|
+
firstRequestAt: state.firstRequestAt,
|
|
5504
|
+
latestRequestAt: state.latestRequestAt,
|
|
5505
|
+
isShuttingDown: this.isShuttingDown,
|
|
5506
|
+
wasArmedAfterFailure: state.remainsArmedUntil !== null
|
|
5507
|
+
};
|
|
5508
|
+
this.logger.warn(
|
|
5509
|
+
"Repeated shutdown request threshold reached, invoking force shutdown handler",
|
|
5510
|
+
{
|
|
5511
|
+
params: {
|
|
5512
|
+
method,
|
|
5513
|
+
requestCount: context.requestCount,
|
|
5514
|
+
firstMethod: context.firstMethod,
|
|
5515
|
+
latestMethod: context.latestMethod,
|
|
5516
|
+
firstRequestAt: context.firstRequestAt,
|
|
5517
|
+
latestRequestAt: context.latestRequestAt,
|
|
5518
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5519
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5520
|
+
withinMS: policy.withinMS,
|
|
5521
|
+
forceAfterCount: policy.forceAfterCount
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
);
|
|
5525
|
+
safeHandleCallback(
|
|
5526
|
+
"repeatedShutdownRequestPolicy.onForceShutdown",
|
|
5527
|
+
policy.onForceShutdown,
|
|
5528
|
+
context
|
|
5529
|
+
);
|
|
5530
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
|
|
5531
|
+
firstMethod: context.firstMethod,
|
|
5532
|
+
latestMethod: context.latestMethod,
|
|
5533
|
+
requestCount: context.requestCount,
|
|
5534
|
+
firstRequestAt: context.firstRequestAt,
|
|
5535
|
+
latestRequestAt: context.latestRequestAt,
|
|
5536
|
+
wasArmedAfterFailure: context.wasArmedAfterFailure
|
|
5537
|
+
});
|
|
5538
|
+
return true;
|
|
5539
|
+
}
|
|
5540
|
+
/**
|
|
5541
|
+
* Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
|
|
5542
|
+
*/
|
|
5543
|
+
resetRepeatedShutdownRequestState() {
|
|
5544
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5545
|
+
this.repeatedShutdownRequestState = {
|
|
5546
|
+
requestCount: 0,
|
|
5547
|
+
firstMethod: null,
|
|
5548
|
+
latestMethod: null,
|
|
5549
|
+
firstRequestAt: null,
|
|
5550
|
+
latestRequestAt: null,
|
|
5551
|
+
repeatedWindowStartedAt: null,
|
|
5552
|
+
hasTriggeredForceShutdown: false,
|
|
5553
|
+
remainsArmedUntil: null
|
|
5554
|
+
};
|
|
5555
|
+
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Clear any pending expiration timer for the post-failure escalation window.
|
|
5558
|
+
*/
|
|
5559
|
+
clearRepeatedShutdownExpiryTimer() {
|
|
5560
|
+
if (this.repeatedShutdownExpiryTimer === null) {
|
|
5561
|
+
return;
|
|
5562
|
+
}
|
|
5563
|
+
clearTimeout(this.repeatedShutdownExpiryTimer);
|
|
5564
|
+
this.repeatedShutdownExpiryTimer = null;
|
|
5565
|
+
}
|
|
5566
|
+
/**
|
|
5567
|
+
* Returns whether post-failure escalation remains armed after first
|
|
5568
|
+
* normalizing any stale timer-backed state.
|
|
5569
|
+
*
|
|
5570
|
+
* The method can expire old armed windows as a side effect because the timer
|
|
5571
|
+
* callback may not have run yet on a delayed event loop. Callers use this
|
|
5572
|
+
* when they need the effective runtime truth, not just the last timer write.
|
|
5573
|
+
*/
|
|
5574
|
+
normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
|
|
5575
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
5576
|
+
if (armedUntil === null) {
|
|
5577
|
+
return false;
|
|
5578
|
+
}
|
|
5579
|
+
if (now >= armedUntil) {
|
|
5580
|
+
this.expireRepeatedShutdownRequestState();
|
|
5581
|
+
return false;
|
|
5582
|
+
}
|
|
5583
|
+
return true;
|
|
5584
|
+
}
|
|
5585
|
+
/**
|
|
5586
|
+
* Transition armed post-failure escalation state into its expired/reset state.
|
|
5587
|
+
*/
|
|
5588
|
+
expireRepeatedShutdownRequestState() {
|
|
5589
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5590
|
+
const state = this.repeatedShutdownRequestState;
|
|
5591
|
+
if (!policy || state.remainsArmedUntil === null) {
|
|
5592
|
+
return;
|
|
5593
|
+
}
|
|
5594
|
+
const armedUntil = state.remainsArmedUntil;
|
|
5595
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5596
|
+
const expiredState = {
|
|
5597
|
+
firstMethod: state.firstMethod,
|
|
5598
|
+
latestMethod: state.latestMethod,
|
|
5599
|
+
requestCount: state.requestCount,
|
|
5600
|
+
armedUntil
|
|
5601
|
+
};
|
|
5602
|
+
this.logger.warn(
|
|
5603
|
+
"Repeated shutdown escalation window expired, clearing previous shutdown state",
|
|
5604
|
+
{
|
|
5605
|
+
params: {
|
|
5606
|
+
remainsArmedUntil: armedUntil,
|
|
5607
|
+
withinMS: policy.withinMS,
|
|
5608
|
+
forceAfterCount: policy.forceAfterCount
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
);
|
|
5612
|
+
if (expiredState.firstMethod !== null) {
|
|
5613
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
|
|
5614
|
+
firstMethod: expiredState.firstMethod,
|
|
5615
|
+
latestMethod: expiredState.latestMethod,
|
|
5616
|
+
requestCount: expiredState.requestCount,
|
|
5617
|
+
armedUntil: expiredState.armedUntil
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
this.resetRepeatedShutdownRequestState();
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Arms or refreshes the post-failure escalation window and its expiration timer.
|
|
5624
|
+
*/
|
|
5625
|
+
refreshRepeatedShutdownArmedWindow(now = Date.now()) {
|
|
5626
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5627
|
+
if (!policy) {
|
|
5628
|
+
return;
|
|
5629
|
+
}
|
|
5630
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5631
|
+
const armedUntil = now + policy.armedAfterFailureMS;
|
|
5632
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
|
|
5633
|
+
this.repeatedShutdownExpiryTimer = setTimeout(() => {
|
|
5634
|
+
this.expireRepeatedShutdownRequestState();
|
|
5635
|
+
}, policy.armedAfterFailureMS);
|
|
5636
|
+
this.repeatedShutdownExpiryTimer.unref();
|
|
5637
|
+
}
|
|
5638
|
+
/**
|
|
5639
|
+
* Seeds shutdown escalation tracking for a new shutdown cycle.
|
|
5640
|
+
*
|
|
5641
|
+
* The first shutdown trigger starts graceful shutdown and arms escalation with
|
|
5642
|
+
* an effective post-start count of 0. Later shutdown requests can then count
|
|
5643
|
+
* toward the configured force threshold regardless of whether the shutdown
|
|
5644
|
+
* started from a signal, keyboard shortcut, or direct API call.
|
|
5645
|
+
*/
|
|
5646
|
+
seedRepeatedShutdownRequestState(method) {
|
|
5647
|
+
const now = Date.now();
|
|
5648
|
+
this.repeatedShutdownRequestState = {
|
|
5649
|
+
requestCount: 0,
|
|
5650
|
+
firstMethod: method,
|
|
5651
|
+
latestMethod: method,
|
|
5652
|
+
firstRequestAt: now,
|
|
5653
|
+
latestRequestAt: now,
|
|
5654
|
+
repeatedWindowStartedAt: null,
|
|
5655
|
+
hasTriggeredForceShutdown: false,
|
|
5656
|
+
remainsArmedUntil: null
|
|
5657
|
+
};
|
|
5658
|
+
}
|
|
5659
|
+
/**
|
|
5660
|
+
* Preserves a short-lived post-failure escalation window after shutdown
|
|
5661
|
+
* returns unsuccessfully so operators can keep pressing shutdown without
|
|
5662
|
+
* losing the existing force count the moment the graceful attempt finishes.
|
|
5663
|
+
*/
|
|
5664
|
+
armRepeatedShutdownAfterFailure() {
|
|
5665
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5666
|
+
const state = this.repeatedShutdownRequestState;
|
|
5667
|
+
if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
|
|
5668
|
+
state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
this.refreshRepeatedShutdownArmedWindow();
|
|
5672
|
+
const armedUntil = state.remainsArmedUntil;
|
|
5673
|
+
if (state.firstMethod !== null && armedUntil !== null) {
|
|
5674
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
|
|
5675
|
+
firstMethod: state.firstMethod,
|
|
5676
|
+
requestCount: state.requestCount,
|
|
5677
|
+
armedUntil
|
|
5678
|
+
});
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5273
5681
|
/**
|
|
5274
5682
|
* Handle reload request - calls custom callback or broadcasts to components.
|
|
5275
5683
|
*
|