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
package/README.md
CHANGED
|
@@ -1006,6 +1006,9 @@ var ComponentLifecycle = class {
|
|
|
1006
1006
|
getSignalStatus() {
|
|
1007
1007
|
return this.manager.getSignalStatus();
|
|
1008
1008
|
}
|
|
1009
|
+
getShutdownEscalationStatus() {
|
|
1010
|
+
return this.manager.getShutdownEscalationStatus();
|
|
1011
|
+
}
|
|
1009
1012
|
triggerReload() {
|
|
1010
1013
|
return this.manager.triggerReload();
|
|
1011
1014
|
}
|
|
@@ -1184,6 +1187,15 @@ var LifecycleManagerEvents = class {
|
|
|
1184
1187
|
lifecycleManagerShutdownCompleted(input) {
|
|
1185
1188
|
this.emit("lifecycle-manager:shutdown-completed", input);
|
|
1186
1189
|
}
|
|
1190
|
+
lifecycleManagerShutdownEscalationArmed(input) {
|
|
1191
|
+
this.emit("lifecycle-manager:shutdown-escalation-armed", input);
|
|
1192
|
+
}
|
|
1193
|
+
lifecycleManagerShutdownEscalationExpired(input) {
|
|
1194
|
+
this.emit("lifecycle-manager:shutdown-escalation-expired", input);
|
|
1195
|
+
}
|
|
1196
|
+
lifecycleManagerShutdownEscalationForced(input) {
|
|
1197
|
+
this.emit("lifecycle-manager:shutdown-escalation-forced", input);
|
|
1198
|
+
}
|
|
1187
1199
|
componentStarting(name) {
|
|
1188
1200
|
this.emit("component:starting", { name });
|
|
1189
1201
|
}
|
|
@@ -1260,8 +1272,8 @@ var LifecycleManagerEvents = class {
|
|
|
1260
1272
|
componentStartupRollback(name) {
|
|
1261
1273
|
this.emit("component:startup-rollback", { name });
|
|
1262
1274
|
}
|
|
1263
|
-
signalShutdown(method) {
|
|
1264
|
-
this.emit("signal:shutdown", { method });
|
|
1275
|
+
signalShutdown(method, isAlreadyShuttingDown = false) {
|
|
1276
|
+
this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
|
|
1265
1277
|
}
|
|
1266
1278
|
signalReload() {
|
|
1267
1279
|
this.emit("signal:reload", void 0);
|
|
@@ -1971,6 +1983,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1971
1983
|
attachSignalsBeforeStartup;
|
|
1972
1984
|
attachSignalsOnStart;
|
|
1973
1985
|
detachSignalsOnStop;
|
|
1986
|
+
repeatedShutdownRequestPolicy;
|
|
1974
1987
|
// Component management
|
|
1975
1988
|
components = [];
|
|
1976
1989
|
runningComponents = /* @__PURE__ */ new Set();
|
|
@@ -1990,6 +2003,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1990
2003
|
pendingLoggerExitResolve = null;
|
|
1991
2004
|
shutdownMethod = null;
|
|
1992
2005
|
lastShutdownResult = null;
|
|
2006
|
+
repeatedShutdownExpiryTimer = null;
|
|
2007
|
+
repeatedShutdownRequestState = {
|
|
2008
|
+
requestCount: 0,
|
|
2009
|
+
firstMethod: null,
|
|
2010
|
+
latestMethod: null,
|
|
2011
|
+
firstRequestAt: null,
|
|
2012
|
+
latestRequestAt: null,
|
|
2013
|
+
repeatedWindowStartedAt: null,
|
|
2014
|
+
hasTriggeredForceShutdown: false,
|
|
2015
|
+
remainsArmedUntil: null
|
|
2016
|
+
};
|
|
1993
2017
|
// Signal management
|
|
1994
2018
|
processSignalManager = null;
|
|
1995
2019
|
onReloadRequested;
|
|
@@ -2016,6 +2040,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2016
2040
|
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
2017
2041
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
2018
2042
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
2043
|
+
const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
|
|
2044
|
+
if (repeatedShutdownRequestPolicy === void 0) {
|
|
2045
|
+
this.repeatedShutdownRequestPolicy = void 0;
|
|
2046
|
+
} else {
|
|
2047
|
+
const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
|
|
2048
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS
|
|
2049
|
+
);
|
|
2050
|
+
const forceAfterCount = finiteClampMin(
|
|
2051
|
+
repeatedShutdownRequestPolicy.forceAfterCount,
|
|
2052
|
+
1,
|
|
2053
|
+
3
|
|
2054
|
+
);
|
|
2055
|
+
const withinMS = finiteClampMin(
|
|
2056
|
+
repeatedShutdownRequestPolicy.withinMS,
|
|
2057
|
+
0,
|
|
2058
|
+
2e3
|
|
2059
|
+
);
|
|
2060
|
+
const armedAfterFailureMS = finiteClampMin(
|
|
2061
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
2062
|
+
0,
|
|
2063
|
+
withinMS * forceAfterCount
|
|
2064
|
+
);
|
|
2065
|
+
this.repeatedShutdownRequestPolicy = {
|
|
2066
|
+
forceAfterCount,
|
|
2067
|
+
withinMS,
|
|
2068
|
+
armedAfterFailureMS,
|
|
2069
|
+
countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
|
|
2070
|
+
hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
|
|
2071
|
+
onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2019
2074
|
this.onReloadRequested = options.onReloadRequested;
|
|
2020
2075
|
this.onInfoRequested = options.onInfoRequested;
|
|
2021
2076
|
this.onDebugRequested = options.onDebugRequested;
|
|
@@ -2584,6 +2639,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2584
2639
|
};
|
|
2585
2640
|
}
|
|
2586
2641
|
this.isStarting = true;
|
|
2642
|
+
this.resetRepeatedShutdownRequestState();
|
|
2587
2643
|
this.shutdownMethod = null;
|
|
2588
2644
|
this.lastShutdownResult = null;
|
|
2589
2645
|
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
@@ -3024,6 +3080,51 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3024
3080
|
shutdownMethod: this.shutdownMethod
|
|
3025
3081
|
};
|
|
3026
3082
|
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Get status information about repeated shutdown escalation configuration and runtime state.
|
|
3085
|
+
*/
|
|
3086
|
+
getShutdownEscalationStatus() {
|
|
3087
|
+
if (this.repeatedShutdownRequestPolicy === void 0) {
|
|
3088
|
+
return {
|
|
3089
|
+
configured: false,
|
|
3090
|
+
isShuttingDown: this.isShuttingDown,
|
|
3091
|
+
isArmed: false,
|
|
3092
|
+
forceAfterCount: null,
|
|
3093
|
+
withinMS: null,
|
|
3094
|
+
armedAfterFailureMS: null,
|
|
3095
|
+
armedAfterFailureMSSource: null,
|
|
3096
|
+
requestCount: 0,
|
|
3097
|
+
firstMethod: null,
|
|
3098
|
+
latestMethod: null,
|
|
3099
|
+
firstRequestAt: null,
|
|
3100
|
+
latestRequestAt: null,
|
|
3101
|
+
repeatedWindowStartedAt: null,
|
|
3102
|
+
armedUntil: null,
|
|
3103
|
+
hasTriggeredForceShutdown: false
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
3107
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
3108
|
+
const isArmed = armedUntil !== null;
|
|
3109
|
+
return {
|
|
3110
|
+
configured: true,
|
|
3111
|
+
isShuttingDown: this.isShuttingDown,
|
|
3112
|
+
isArmed,
|
|
3113
|
+
forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
|
|
3114
|
+
withinMS: this.repeatedShutdownRequestPolicy.withinMS,
|
|
3115
|
+
armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
3116
|
+
armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
|
|
3117
|
+
countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
|
|
3118
|
+
requestCount: this.repeatedShutdownRequestState.requestCount,
|
|
3119
|
+
firstMethod: this.repeatedShutdownRequestState.firstMethod,
|
|
3120
|
+
latestMethod: this.repeatedShutdownRequestState.latestMethod,
|
|
3121
|
+
firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
|
|
3122
|
+
latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
|
|
3123
|
+
repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
|
|
3124
|
+
armedUntil: isArmed ? armedUntil : null,
|
|
3125
|
+
hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3027
3128
|
/**
|
|
3028
3129
|
* Enable Logger exit hook integration
|
|
3029
3130
|
*
|
|
@@ -4049,9 +4150,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4049
4150
|
code: "already_in_progress"
|
|
4050
4151
|
};
|
|
4051
4152
|
}
|
|
4153
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
4154
|
+
const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
|
|
4155
|
+
const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
|
|
4156
|
+
if (isManualRetryWhileArmed) {
|
|
4157
|
+
if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
|
|
4158
|
+
this.handleRepeatedShutdownRequest(method);
|
|
4159
|
+
} else {
|
|
4160
|
+
this.resetRepeatedShutdownRequestState();
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
|
|
4164
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
4165
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = null;
|
|
4166
|
+
}
|
|
4052
4167
|
this.isShuttingDown = true;
|
|
4053
4168
|
this.shutdownToken = (0, import_ulid2.ulid)();
|
|
4054
4169
|
this.shutdownMethod = method;
|
|
4170
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
|
|
4171
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
4172
|
+
}
|
|
4055
4173
|
const isDuringStartup = this.isStarting;
|
|
4056
4174
|
this.logger.info("Stopping all components", { params: { method } });
|
|
4057
4175
|
this.lifecycleEvents.lifecycleManagerShutdownInitiated(
|
|
@@ -4151,15 +4269,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4151
4269
|
} else {
|
|
4152
4270
|
await shutdownOperation();
|
|
4153
4271
|
}
|
|
4272
|
+
if (!shouldRetryStalled) {
|
|
4273
|
+
for (const name of stalledComponentNames) {
|
|
4274
|
+
const stallInfo = this.stalledComponents.get(name);
|
|
4275
|
+
if (stallInfo && !stalledComponents.some((component) => component.name === name)) {
|
|
4276
|
+
stalledComponents.push(stallInfo);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4154
4280
|
const durationMS = Date.now() - startTime;
|
|
4155
4281
|
const isSuccess = !hasTimedOut && stalledComponents.length === 0;
|
|
4156
|
-
this.logger[isSuccess ? "success" : "warn"](
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4282
|
+
this.logger[isSuccess ? "success" : "warn"](
|
|
4283
|
+
isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
|
|
4284
|
+
{
|
|
4285
|
+
params: {
|
|
4286
|
+
stopped: stoppedComponents.length,
|
|
4287
|
+
stalled: stalledComponents.length,
|
|
4288
|
+
durationMS
|
|
4289
|
+
}
|
|
4161
4290
|
}
|
|
4162
|
-
|
|
4291
|
+
);
|
|
4163
4292
|
const result = {
|
|
4164
4293
|
success: isSuccess,
|
|
4165
4294
|
stoppedComponents,
|
|
@@ -4177,6 +4306,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4177
4306
|
method,
|
|
4178
4307
|
duringStartup: isDuringStartup
|
|
4179
4308
|
});
|
|
4309
|
+
if (isSuccess) {
|
|
4310
|
+
this.resetRepeatedShutdownRequestState();
|
|
4311
|
+
} else {
|
|
4312
|
+
this.armRepeatedShutdownAfterFailure();
|
|
4313
|
+
}
|
|
4180
4314
|
return result;
|
|
4181
4315
|
} finally {
|
|
4182
4316
|
if (timeoutHandle) {
|
|
@@ -5305,21 +5439,295 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5305
5439
|
}
|
|
5306
5440
|
/**
|
|
5307
5441
|
* Handle shutdown signal - initiates stopAllComponents().
|
|
5308
|
-
*
|
|
5442
|
+
*
|
|
5443
|
+
* Four cases depending on the current shutdown state:
|
|
5444
|
+
*
|
|
5445
|
+
* 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
|
|
5446
|
+
* repeated-shutdown policy if configured, otherwise log and discard.
|
|
5447
|
+
* Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
|
|
5448
|
+
* without starting another shutdown.
|
|
5449
|
+
*
|
|
5450
|
+
* 2. **Armed post-failure** (previous shutdown finished, armed window still
|
|
5451
|
+
* open): count the request toward the escalation window, emit
|
|
5452
|
+
* `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
|
|
5453
|
+
* new `stopAllComponents()` run to retry.
|
|
5454
|
+
*
|
|
5455
|
+
* 3. **Armed post-failure expired** (armed window opened but has since
|
|
5456
|
+
* elapsed): expire the stale state, treat the request as a fresh
|
|
5457
|
+
* shutdown (falls through to case 4).
|
|
5458
|
+
*
|
|
5459
|
+
* 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
|
|
5460
|
+
* if policy is configured, emit `signal:shutdown` with
|
|
5461
|
+
* `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
|
|
5462
|
+
*
|
|
5463
|
+
* In all cases `signal:shutdown` is emitted exactly once.
|
|
5309
5464
|
*/
|
|
5310
5465
|
handleShutdownRequest(method) {
|
|
5311
5466
|
if (this.isShuttingDown) {
|
|
5312
|
-
this.
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5467
|
+
this.lifecycleEvents.signalShutdown(method, true);
|
|
5468
|
+
if (this.handleRepeatedShutdownRequest(method)) {
|
|
5469
|
+
return;
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
let didEmitShutdownSignal = false;
|
|
5473
|
+
let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
|
|
5474
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
|
|
5475
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5476
|
+
didEmitShutdownSignal = true;
|
|
5477
|
+
shouldSeedRepeatedShutdownState = false;
|
|
5478
|
+
this.handleRepeatedShutdownRequest(method);
|
|
5479
|
+
}
|
|
5480
|
+
if (shouldSeedRepeatedShutdownState) {
|
|
5481
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
5316
5482
|
}
|
|
5317
5483
|
this.logger.info("Shutdown signal received", { params: { method } });
|
|
5318
|
-
|
|
5484
|
+
if (!didEmitShutdownSignal) {
|
|
5485
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5486
|
+
}
|
|
5319
5487
|
void this.stopAllComponentsInternal(method, {
|
|
5320
5488
|
...this.shutdownOptions
|
|
5321
5489
|
});
|
|
5322
5490
|
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Tracks repeated shutdown requests during an active shutdown and optionally
|
|
5493
|
+
* invokes the configured force shutdown callback when the threshold is reached.
|
|
5494
|
+
*
|
|
5495
|
+
* @returns true when the request was consumed as part of the repeated-shutdown
|
|
5496
|
+
* escalation flow, false when the caller should treat it as a fresh shutdown request
|
|
5497
|
+
*/
|
|
5498
|
+
handleRepeatedShutdownRequest(method) {
|
|
5499
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5500
|
+
if (!policy) {
|
|
5501
|
+
this.logger.warn("Shutdown already in progress, ignoring signal", {
|
|
5502
|
+
params: { method }
|
|
5503
|
+
});
|
|
5504
|
+
return true;
|
|
5505
|
+
}
|
|
5506
|
+
const now = Date.now();
|
|
5507
|
+
const state = this.repeatedShutdownRequestState;
|
|
5508
|
+
if (state.remainsArmedUntil !== null) {
|
|
5509
|
+
if (now >= state.remainsArmedUntil) {
|
|
5510
|
+
this.expireRepeatedShutdownRequestState();
|
|
5511
|
+
return false;
|
|
5512
|
+
}
|
|
5513
|
+
this.refreshRepeatedShutdownArmedWindow(now);
|
|
5514
|
+
}
|
|
5515
|
+
const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
|
|
5516
|
+
if (shouldStartNewWindow) {
|
|
5517
|
+
state.requestCount = 1;
|
|
5518
|
+
state.repeatedWindowStartedAt = now;
|
|
5519
|
+
} else {
|
|
5520
|
+
state.requestCount++;
|
|
5521
|
+
}
|
|
5522
|
+
state.latestMethod = method;
|
|
5523
|
+
state.latestRequestAt = now;
|
|
5524
|
+
this.logger.warn(
|
|
5525
|
+
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",
|
|
5526
|
+
{
|
|
5527
|
+
params: {
|
|
5528
|
+
method,
|
|
5529
|
+
requestCount: state.requestCount,
|
|
5530
|
+
firstMethod: state.firstMethod,
|
|
5531
|
+
latestMethod: state.latestMethod,
|
|
5532
|
+
firstRequestAt: state.firstRequestAt,
|
|
5533
|
+
latestRequestAt: state.latestRequestAt,
|
|
5534
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5535
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5536
|
+
withinMS: policy.withinMS,
|
|
5537
|
+
forceAfterCount: policy.forceAfterCount
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
);
|
|
5541
|
+
if (
|
|
5542
|
+
// Force escalation is single-fire per shutdown cycle. Later requests are
|
|
5543
|
+
// still logged but do not re-enter user force-shutdown logic.
|
|
5544
|
+
state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
|
|
5545
|
+
) {
|
|
5546
|
+
return true;
|
|
5547
|
+
}
|
|
5548
|
+
state.hasTriggeredForceShutdown = true;
|
|
5549
|
+
const context = {
|
|
5550
|
+
requestCount: state.requestCount,
|
|
5551
|
+
firstMethod: state.firstMethod,
|
|
5552
|
+
latestMethod: state.latestMethod,
|
|
5553
|
+
firstRequestAt: state.firstRequestAt,
|
|
5554
|
+
latestRequestAt: state.latestRequestAt,
|
|
5555
|
+
isShuttingDown: this.isShuttingDown,
|
|
5556
|
+
wasArmedAfterFailure: state.remainsArmedUntil !== null
|
|
5557
|
+
};
|
|
5558
|
+
this.logger.warn(
|
|
5559
|
+
"Repeated shutdown request threshold reached, invoking force shutdown handler",
|
|
5560
|
+
{
|
|
5561
|
+
params: {
|
|
5562
|
+
method,
|
|
5563
|
+
requestCount: context.requestCount,
|
|
5564
|
+
firstMethod: context.firstMethod,
|
|
5565
|
+
latestMethod: context.latestMethod,
|
|
5566
|
+
firstRequestAt: context.firstRequestAt,
|
|
5567
|
+
latestRequestAt: context.latestRequestAt,
|
|
5568
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5569
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5570
|
+
withinMS: policy.withinMS,
|
|
5571
|
+
forceAfterCount: policy.forceAfterCount
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
);
|
|
5575
|
+
safeHandleCallback(
|
|
5576
|
+
"repeatedShutdownRequestPolicy.onForceShutdown",
|
|
5577
|
+
policy.onForceShutdown,
|
|
5578
|
+
context
|
|
5579
|
+
);
|
|
5580
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
|
|
5581
|
+
firstMethod: context.firstMethod,
|
|
5582
|
+
latestMethod: context.latestMethod,
|
|
5583
|
+
requestCount: context.requestCount,
|
|
5584
|
+
firstRequestAt: context.firstRequestAt,
|
|
5585
|
+
latestRequestAt: context.latestRequestAt,
|
|
5586
|
+
wasArmedAfterFailure: context.wasArmedAfterFailure
|
|
5587
|
+
});
|
|
5588
|
+
return true;
|
|
5589
|
+
}
|
|
5590
|
+
/**
|
|
5591
|
+
* Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
|
|
5592
|
+
*/
|
|
5593
|
+
resetRepeatedShutdownRequestState() {
|
|
5594
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5595
|
+
this.repeatedShutdownRequestState = {
|
|
5596
|
+
requestCount: 0,
|
|
5597
|
+
firstMethod: null,
|
|
5598
|
+
latestMethod: null,
|
|
5599
|
+
firstRequestAt: null,
|
|
5600
|
+
latestRequestAt: null,
|
|
5601
|
+
repeatedWindowStartedAt: null,
|
|
5602
|
+
hasTriggeredForceShutdown: false,
|
|
5603
|
+
remainsArmedUntil: null
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5606
|
+
/**
|
|
5607
|
+
* Clear any pending expiration timer for the post-failure escalation window.
|
|
5608
|
+
*/
|
|
5609
|
+
clearRepeatedShutdownExpiryTimer() {
|
|
5610
|
+
if (this.repeatedShutdownExpiryTimer === null) {
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
5613
|
+
clearTimeout(this.repeatedShutdownExpiryTimer);
|
|
5614
|
+
this.repeatedShutdownExpiryTimer = null;
|
|
5615
|
+
}
|
|
5616
|
+
/**
|
|
5617
|
+
* Returns whether post-failure escalation remains armed after first
|
|
5618
|
+
* normalizing any stale timer-backed state.
|
|
5619
|
+
*
|
|
5620
|
+
* The method can expire old armed windows as a side effect because the timer
|
|
5621
|
+
* callback may not have run yet on a delayed event loop. Callers use this
|
|
5622
|
+
* when they need the effective runtime truth, not just the last timer write.
|
|
5623
|
+
*/
|
|
5624
|
+
normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
|
|
5625
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
5626
|
+
if (armedUntil === null) {
|
|
5627
|
+
return false;
|
|
5628
|
+
}
|
|
5629
|
+
if (now >= armedUntil) {
|
|
5630
|
+
this.expireRepeatedShutdownRequestState();
|
|
5631
|
+
return false;
|
|
5632
|
+
}
|
|
5633
|
+
return true;
|
|
5634
|
+
}
|
|
5635
|
+
/**
|
|
5636
|
+
* Transition armed post-failure escalation state into its expired/reset state.
|
|
5637
|
+
*/
|
|
5638
|
+
expireRepeatedShutdownRequestState() {
|
|
5639
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5640
|
+
const state = this.repeatedShutdownRequestState;
|
|
5641
|
+
if (!policy || state.remainsArmedUntil === null) {
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
const armedUntil = state.remainsArmedUntil;
|
|
5645
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5646
|
+
const expiredState = {
|
|
5647
|
+
firstMethod: state.firstMethod,
|
|
5648
|
+
latestMethod: state.latestMethod,
|
|
5649
|
+
requestCount: state.requestCount,
|
|
5650
|
+
armedUntil
|
|
5651
|
+
};
|
|
5652
|
+
this.logger.warn(
|
|
5653
|
+
"Repeated shutdown escalation window expired, clearing previous shutdown state",
|
|
5654
|
+
{
|
|
5655
|
+
params: {
|
|
5656
|
+
remainsArmedUntil: armedUntil,
|
|
5657
|
+
withinMS: policy.withinMS,
|
|
5658
|
+
forceAfterCount: policy.forceAfterCount
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
);
|
|
5662
|
+
if (expiredState.firstMethod !== null) {
|
|
5663
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
|
|
5664
|
+
firstMethod: expiredState.firstMethod,
|
|
5665
|
+
latestMethod: expiredState.latestMethod,
|
|
5666
|
+
requestCount: expiredState.requestCount,
|
|
5667
|
+
armedUntil: expiredState.armedUntil
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
this.resetRepeatedShutdownRequestState();
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* Arms or refreshes the post-failure escalation window and its expiration timer.
|
|
5674
|
+
*/
|
|
5675
|
+
refreshRepeatedShutdownArmedWindow(now = Date.now()) {
|
|
5676
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5677
|
+
if (!policy) {
|
|
5678
|
+
return;
|
|
5679
|
+
}
|
|
5680
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5681
|
+
const armedUntil = now + policy.armedAfterFailureMS;
|
|
5682
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
|
|
5683
|
+
this.repeatedShutdownExpiryTimer = setTimeout(() => {
|
|
5684
|
+
this.expireRepeatedShutdownRequestState();
|
|
5685
|
+
}, policy.armedAfterFailureMS);
|
|
5686
|
+
this.repeatedShutdownExpiryTimer.unref();
|
|
5687
|
+
}
|
|
5688
|
+
/**
|
|
5689
|
+
* Seeds shutdown escalation tracking for a new shutdown cycle.
|
|
5690
|
+
*
|
|
5691
|
+
* The first shutdown trigger starts graceful shutdown and arms escalation with
|
|
5692
|
+
* an effective post-start count of 0. Later shutdown requests can then count
|
|
5693
|
+
* toward the configured force threshold regardless of whether the shutdown
|
|
5694
|
+
* started from a signal, keyboard shortcut, or direct API call.
|
|
5695
|
+
*/
|
|
5696
|
+
seedRepeatedShutdownRequestState(method) {
|
|
5697
|
+
const now = Date.now();
|
|
5698
|
+
this.repeatedShutdownRequestState = {
|
|
5699
|
+
requestCount: 0,
|
|
5700
|
+
firstMethod: method,
|
|
5701
|
+
latestMethod: method,
|
|
5702
|
+
firstRequestAt: now,
|
|
5703
|
+
latestRequestAt: now,
|
|
5704
|
+
repeatedWindowStartedAt: null,
|
|
5705
|
+
hasTriggeredForceShutdown: false,
|
|
5706
|
+
remainsArmedUntil: null
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
/**
|
|
5710
|
+
* Preserves a short-lived post-failure escalation window after shutdown
|
|
5711
|
+
* returns unsuccessfully so operators can keep pressing shutdown without
|
|
5712
|
+
* losing the existing force count the moment the graceful attempt finishes.
|
|
5713
|
+
*/
|
|
5714
|
+
armRepeatedShutdownAfterFailure() {
|
|
5715
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5716
|
+
const state = this.repeatedShutdownRequestState;
|
|
5717
|
+
if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
|
|
5718
|
+
state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5721
|
+
this.refreshRepeatedShutdownArmedWindow();
|
|
5722
|
+
const armedUntil = state.remainsArmedUntil;
|
|
5723
|
+
if (state.firstMethod !== null && armedUntil !== null) {
|
|
5724
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
|
|
5725
|
+
firstMethod: state.firstMethod,
|
|
5726
|
+
requestCount: state.requestCount,
|
|
5727
|
+
armedUntil
|
|
5728
|
+
});
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5323
5731
|
/**
|
|
5324
5732
|
* Handle reload request - calls custom callback or broadcasts to components.
|
|
5325
5733
|
*
|