lifecycleion 0.0.12 → 0.0.14
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 +2 -2
- package/dist/lib/lifecycle-manager/index.cjs +1057 -334
- package/dist/lib/lifecycle-manager/index.cjs.map +1 -1
- package/dist/lib/lifecycle-manager/index.d.cts +381 -25
- package/dist/lib/lifecycle-manager/index.d.ts +381 -25
- package/dist/lib/lifecycle-manager/index.js +1057 -334
- package/dist/lib/lifecycle-manager/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -1251,6 +1263,16 @@ var LifecycleManagerEvents = class {
|
|
|
1251
1263
|
code: info?.code
|
|
1252
1264
|
});
|
|
1253
1265
|
}
|
|
1266
|
+
componentStalledResolved(name, stallInfo, stalledDurationMS) {
|
|
1267
|
+
this.emit("component:stalled-resolved", {
|
|
1268
|
+
name,
|
|
1269
|
+
stallInfo,
|
|
1270
|
+
stalledDurationMS
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
componentUnexpectedStop(name, error) {
|
|
1274
|
+
this.emit("component:unexpected-stop", { name, error });
|
|
1275
|
+
}
|
|
1254
1276
|
componentShutdownForceCompleted(name) {
|
|
1255
1277
|
this.emit("component:shutdown-force-completed", { name });
|
|
1256
1278
|
}
|
|
@@ -1260,8 +1282,8 @@ var LifecycleManagerEvents = class {
|
|
|
1260
1282
|
componentStartupRollback(name) {
|
|
1261
1283
|
this.emit("component:startup-rollback", { name });
|
|
1262
1284
|
}
|
|
1263
|
-
signalShutdown(method) {
|
|
1264
|
-
this.emit("signal:shutdown", { method });
|
|
1285
|
+
signalShutdown(method, isAlreadyShuttingDown = false) {
|
|
1286
|
+
this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
|
|
1265
1287
|
}
|
|
1266
1288
|
signalReload() {
|
|
1267
1289
|
this.emit("signal:reload", void 0);
|
|
@@ -1437,6 +1459,25 @@ var lifecycleManagerErrCodes = {
|
|
|
1437
1459
|
StopTimeout: "StopTimeout"
|
|
1438
1460
|
};
|
|
1439
1461
|
|
|
1462
|
+
// src/lib/lifecycle-manager/constants.ts
|
|
1463
|
+
var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
|
|
1464
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
|
|
1465
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
|
|
1466
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
|
|
1467
|
+
var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
|
|
1468
|
+
var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
|
|
1469
|
+
var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
|
|
1470
|
+
var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
|
|
1471
|
+
var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
|
|
1472
|
+
var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
|
|
1473
|
+
var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
|
|
1474
|
+
var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
|
|
1475
|
+
var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
|
|
1476
|
+
var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
|
|
1477
|
+
var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
|
|
1478
|
+
var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
|
|
1479
|
+
var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
|
|
1480
|
+
|
|
1440
1481
|
// src/lib/process-signal-manager.ts
|
|
1441
1482
|
var import_ulid = require("ulid");
|
|
1442
1483
|
var import_readline = __toESM(require("readline"), 1);
|
|
@@ -1971,6 +2012,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1971
2012
|
attachSignalsBeforeStartup;
|
|
1972
2013
|
attachSignalsOnStart;
|
|
1973
2014
|
detachSignalsOnStop;
|
|
2015
|
+
repeatedShutdownRequestPolicy;
|
|
1974
2016
|
// Component management
|
|
1975
2017
|
components = [];
|
|
1976
2018
|
runningComponents = /* @__PURE__ */ new Set();
|
|
@@ -1980,8 +2022,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1980
2022
|
componentTimestamps = /* @__PURE__ */ new Map();
|
|
1981
2023
|
componentErrors = /* @__PURE__ */ new Map();
|
|
1982
2024
|
componentStartAttemptTokens = /* @__PURE__ */ new Map();
|
|
2025
|
+
// Use per-stop ULIDs instead of incrementing counters because a stalled
|
|
2026
|
+
// component can be unregistered and replaced by a same-name instance before
|
|
2027
|
+
// the old floating stop promise settles.
|
|
2028
|
+
componentStopAttemptTokens = /* @__PURE__ */ new Map();
|
|
2029
|
+
pendingForceStopWaiters = /* @__PURE__ */ new Map();
|
|
2030
|
+
unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
|
|
1983
2031
|
// State flags
|
|
1984
2032
|
isStarting = false;
|
|
2033
|
+
autoAttachedSignalsDuringStartup = false;
|
|
1985
2034
|
isStarted = false;
|
|
1986
2035
|
isShuttingDown = false;
|
|
1987
2036
|
// Unique token used to detect shutdowns that happened during async start().
|
|
@@ -1990,6 +2039,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1990
2039
|
pendingLoggerExitResolve = null;
|
|
1991
2040
|
shutdownMethod = null;
|
|
1992
2041
|
lastShutdownResult = null;
|
|
2042
|
+
repeatedShutdownExpiryTimer = null;
|
|
2043
|
+
repeatedShutdownRequestState = {
|
|
2044
|
+
requestCount: 0,
|
|
2045
|
+
firstMethod: null,
|
|
2046
|
+
latestMethod: null,
|
|
2047
|
+
firstRequestAt: null,
|
|
2048
|
+
latestRequestAt: null,
|
|
2049
|
+
repeatedWindowStartedAt: null,
|
|
2050
|
+
hasTriggeredForceShutdown: false,
|
|
2051
|
+
remainsArmedUntil: null
|
|
2052
|
+
};
|
|
1993
2053
|
// Signal management
|
|
1994
2054
|
processSignalManager = null;
|
|
1995
2055
|
onReloadRequested;
|
|
@@ -2016,6 +2076,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2016
2076
|
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
2017
2077
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
2018
2078
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
2079
|
+
const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
|
|
2080
|
+
if (repeatedShutdownRequestPolicy === void 0) {
|
|
2081
|
+
this.repeatedShutdownRequestPolicy = void 0;
|
|
2082
|
+
} else {
|
|
2083
|
+
const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
|
|
2084
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS
|
|
2085
|
+
);
|
|
2086
|
+
const forceAfterCount = finiteClampMin(
|
|
2087
|
+
repeatedShutdownRequestPolicy.forceAfterCount,
|
|
2088
|
+
1,
|
|
2089
|
+
3
|
|
2090
|
+
);
|
|
2091
|
+
const withinMS = finiteClampMin(
|
|
2092
|
+
repeatedShutdownRequestPolicy.withinMS,
|
|
2093
|
+
0,
|
|
2094
|
+
2e3
|
|
2095
|
+
);
|
|
2096
|
+
const armedAfterFailureMS = finiteClampMin(
|
|
2097
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
2098
|
+
0,
|
|
2099
|
+
withinMS * forceAfterCount
|
|
2100
|
+
);
|
|
2101
|
+
this.repeatedShutdownRequestPolicy = {
|
|
2102
|
+
forceAfterCount,
|
|
2103
|
+
withinMS,
|
|
2104
|
+
armedAfterFailureMS,
|
|
2105
|
+
countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
|
|
2106
|
+
hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
|
|
2107
|
+
onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2019
2110
|
this.onReloadRequested = options.onReloadRequested;
|
|
2020
2111
|
this.onInfoRequested = options.onInfoRequested;
|
|
2021
2112
|
this.onDebugRequested = options.onDebugRequested;
|
|
@@ -2088,7 +2179,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2088
2179
|
*/
|
|
2089
2180
|
async unregisterComponent(name, options) {
|
|
2090
2181
|
if (this.isStarting || this.isShuttingDown) {
|
|
2091
|
-
this.logger.entity(name).warn(
|
|
2182
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
|
|
2092
2183
|
params: {
|
|
2093
2184
|
isStarting: this.isStarting,
|
|
2094
2185
|
isShuttingDown: this.isShuttingDown
|
|
@@ -2097,7 +2188,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2097
2188
|
return {
|
|
2098
2189
|
success: false,
|
|
2099
2190
|
componentName: name,
|
|
2100
|
-
reason:
|
|
2191
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
|
|
2101
2192
|
code: "bulk_operation_in_progress",
|
|
2102
2193
|
wasStopped: false,
|
|
2103
2194
|
wasRegistered: this.hasComponent(name)
|
|
@@ -2105,11 +2196,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2105
2196
|
}
|
|
2106
2197
|
const component = this.getComponent(name);
|
|
2107
2198
|
if (!component) {
|
|
2108
|
-
this.logger.entity(name).warn(
|
|
2199
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
|
|
2109
2200
|
return {
|
|
2110
2201
|
success: false,
|
|
2111
2202
|
componentName: name,
|
|
2112
|
-
reason:
|
|
2203
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
2113
2204
|
code: "component_not_found",
|
|
2114
2205
|
wasStopped: false,
|
|
2115
2206
|
wasRegistered: false
|
|
@@ -2122,7 +2213,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2122
2213
|
return {
|
|
2123
2214
|
success: false,
|
|
2124
2215
|
componentName: name,
|
|
2125
|
-
reason:
|
|
2216
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
2126
2217
|
code: "stop_failed",
|
|
2127
2218
|
stopFailureReason: "stalled",
|
|
2128
2219
|
wasStopped: false,
|
|
@@ -2174,10 +2265,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2174
2265
|
wasStopped = true;
|
|
2175
2266
|
}
|
|
2176
2267
|
this.components = this.components.filter((c) => c.getName() !== name);
|
|
2268
|
+
component._clearUnexpectedStopHandler();
|
|
2177
2269
|
this.componentStates.delete(name);
|
|
2178
2270
|
this.componentTimestamps.delete(name);
|
|
2179
2271
|
this.componentErrors.delete(name);
|
|
2180
2272
|
this.componentStartAttemptTokens.delete(name);
|
|
2273
|
+
this.componentStopAttemptTokens.delete(name);
|
|
2274
|
+
this.pendingForceStopWaiters.delete(name);
|
|
2181
2275
|
this.stalledComponents.delete(name);
|
|
2182
2276
|
this.runningComponents.delete(name);
|
|
2183
2277
|
this.updateStartedFlag();
|
|
@@ -2524,7 +2618,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2524
2618
|
startedComponents: [],
|
|
2525
2619
|
failedOptionalComponents: [],
|
|
2526
2620
|
skippedDueToDependency: [],
|
|
2527
|
-
reason:
|
|
2621
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2528
2622
|
code: "shutdown_in_progress",
|
|
2529
2623
|
durationMS: Date.now() - startTime
|
|
2530
2624
|
};
|
|
@@ -2584,6 +2678,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2584
2678
|
};
|
|
2585
2679
|
}
|
|
2586
2680
|
this.isStarting = true;
|
|
2681
|
+
this.autoAttachedSignalsDuringStartup = false;
|
|
2682
|
+
this.unexpectedStopsDuringStartup.clear();
|
|
2683
|
+
this.resetRepeatedShutdownRequestState();
|
|
2587
2684
|
this.shutdownMethod = null;
|
|
2588
2685
|
this.lastShutdownResult = null;
|
|
2589
2686
|
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
@@ -2715,13 +2812,49 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2715
2812
|
error: result.error,
|
|
2716
2813
|
durationMS: Date.now() - startTime
|
|
2717
2814
|
};
|
|
2815
|
+
} else if (result.code === "component_unexpected_stop") {
|
|
2816
|
+
this.unexpectedStopsDuringStartup.delete(name);
|
|
2817
|
+
const error = result.error || new Error(
|
|
2818
|
+
result.reason || `Component "${name}" stopped unexpectedly`
|
|
2819
|
+
);
|
|
2820
|
+
if (component.isOptional()) {
|
|
2821
|
+
if (!failedOptionalComponents.some((entry) => entry.name === name)) {
|
|
2822
|
+
failedOptionalComponents.push({ name, error });
|
|
2823
|
+
}
|
|
2824
|
+
this.logger.entity(name).warn(
|
|
2825
|
+
LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
2826
|
+
{
|
|
2827
|
+
params: { error }
|
|
2828
|
+
}
|
|
2829
|
+
);
|
|
2830
|
+
} else {
|
|
2831
|
+
this.logger.entity(name).error(
|
|
2832
|
+
LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
2833
|
+
{
|
|
2834
|
+
params: { error }
|
|
2835
|
+
}
|
|
2836
|
+
);
|
|
2837
|
+
await this.rollbackStartup(startedComponents);
|
|
2838
|
+
return {
|
|
2839
|
+
success: false,
|
|
2840
|
+
startedComponents: [],
|
|
2841
|
+
failedOptionalComponents,
|
|
2842
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2843
|
+
reason: error.message,
|
|
2844
|
+
code: "component_unexpected_stop",
|
|
2845
|
+
error,
|
|
2846
|
+
durationMS: Date.now() - startTime
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2718
2849
|
} else {
|
|
2719
2850
|
if (component.isOptional()) {
|
|
2720
2851
|
this.logger.entity(name).warn(
|
|
2721
2852
|
"Optional component failed to start, continuing: {{error.message}}",
|
|
2722
2853
|
{
|
|
2723
2854
|
params: {
|
|
2724
|
-
error: result.error || new Error(
|
|
2855
|
+
error: result.error || new Error(
|
|
2856
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2857
|
+
)
|
|
2725
2858
|
}
|
|
2726
2859
|
}
|
|
2727
2860
|
);
|
|
@@ -2735,14 +2868,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2735
2868
|
}
|
|
2736
2869
|
failedOptionalComponents.push({
|
|
2737
2870
|
name,
|
|
2738
|
-
error: result.error || new Error(
|
|
2871
|
+
error: result.error || new Error(
|
|
2872
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2873
|
+
)
|
|
2739
2874
|
});
|
|
2740
2875
|
} else {
|
|
2741
2876
|
this.logger.entity(name).error(
|
|
2742
2877
|
"Required component failed to start, rolling back: {{error.message}}",
|
|
2743
2878
|
{
|
|
2744
2879
|
params: {
|
|
2745
|
-
error: result.error || new Error(
|
|
2880
|
+
error: result.error || new Error(
|
|
2881
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2882
|
+
)
|
|
2746
2883
|
}
|
|
2747
2884
|
}
|
|
2748
2885
|
);
|
|
@@ -2759,6 +2896,25 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2759
2896
|
};
|
|
2760
2897
|
}
|
|
2761
2898
|
}
|
|
2899
|
+
const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
|
|
2900
|
+
startedComponents,
|
|
2901
|
+
failedOptionalComponents
|
|
2902
|
+
);
|
|
2903
|
+
startedComponents.splice(0, startedComponents.length);
|
|
2904
|
+
startedComponents.push(...unexpectedStopResult2.startedComponents);
|
|
2905
|
+
if (unexpectedStopResult2.requiredFailure) {
|
|
2906
|
+
await this.rollbackStartup(startedComponents);
|
|
2907
|
+
return {
|
|
2908
|
+
success: false,
|
|
2909
|
+
startedComponents: [],
|
|
2910
|
+
failedOptionalComponents,
|
|
2911
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2912
|
+
reason: unexpectedStopResult2.requiredFailure.error.message,
|
|
2913
|
+
code: "component_unexpected_stop",
|
|
2914
|
+
error: unexpectedStopResult2.requiredFailure.error,
|
|
2915
|
+
durationMS: Date.now() - startTime
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2762
2918
|
}
|
|
2763
2919
|
if (hasTimedOut) {
|
|
2764
2920
|
const durationMS2 = Date.now() - startTime;
|
|
@@ -2782,6 +2938,25 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2782
2938
|
code: "startup_timeout"
|
|
2783
2939
|
};
|
|
2784
2940
|
}
|
|
2941
|
+
const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
|
|
2942
|
+
startedComponents,
|
|
2943
|
+
failedOptionalComponents
|
|
2944
|
+
);
|
|
2945
|
+
startedComponents.splice(0, startedComponents.length);
|
|
2946
|
+
startedComponents.push(...unexpectedStopResult.startedComponents);
|
|
2947
|
+
if (unexpectedStopResult.requiredFailure) {
|
|
2948
|
+
await this.rollbackStartup(startedComponents);
|
|
2949
|
+
return {
|
|
2950
|
+
success: false,
|
|
2951
|
+
startedComponents: [],
|
|
2952
|
+
failedOptionalComponents,
|
|
2953
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2954
|
+
reason: unexpectedStopResult.requiredFailure.error.message,
|
|
2955
|
+
code: "component_unexpected_stop",
|
|
2956
|
+
error: unexpectedStopResult.requiredFailure.error,
|
|
2957
|
+
durationMS: Date.now() - startTime
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2785
2960
|
this.updateStartedFlag();
|
|
2786
2961
|
const skippedComponentsArray = [
|
|
2787
2962
|
...Array.from(skippedDueToDependency),
|
|
@@ -2813,10 +2988,12 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2813
2988
|
if (timeoutHandle) {
|
|
2814
2989
|
clearTimeout(timeoutHandle);
|
|
2815
2990
|
}
|
|
2816
|
-
|
|
2991
|
+
this.isStarting = false;
|
|
2992
|
+
if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
|
|
2817
2993
|
this.autoDetachSignalsIfIdle("failed bulk startup");
|
|
2818
2994
|
}
|
|
2819
|
-
this.
|
|
2995
|
+
this.autoAttachedSignalsDuringStartup = false;
|
|
2996
|
+
this.unexpectedStopsDuringStartup.clear();
|
|
2820
2997
|
}
|
|
2821
2998
|
}
|
|
2822
2999
|
/**
|
|
@@ -2880,7 +3057,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2880
3057
|
return {
|
|
2881
3058
|
success: false,
|
|
2882
3059
|
componentName: name,
|
|
2883
|
-
reason:
|
|
3060
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
|
|
2884
3061
|
code: "startup_in_progress"
|
|
2885
3062
|
};
|
|
2886
3063
|
}
|
|
@@ -2891,7 +3068,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2891
3068
|
return {
|
|
2892
3069
|
success: false,
|
|
2893
3070
|
componentName: name,
|
|
2894
|
-
reason:
|
|
3071
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2895
3072
|
code: "shutdown_in_progress"
|
|
2896
3073
|
};
|
|
2897
3074
|
}
|
|
@@ -2925,7 +3102,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2925
3102
|
return {
|
|
2926
3103
|
success: false,
|
|
2927
3104
|
componentName: name,
|
|
2928
|
-
reason: this.isStarting ?
|
|
3105
|
+
reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2929
3106
|
code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
|
|
2930
3107
|
};
|
|
2931
3108
|
}
|
|
@@ -3024,6 +3201,51 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3024
3201
|
shutdownMethod: this.shutdownMethod
|
|
3025
3202
|
};
|
|
3026
3203
|
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Get status information about repeated shutdown escalation configuration and runtime state.
|
|
3206
|
+
*/
|
|
3207
|
+
getShutdownEscalationStatus() {
|
|
3208
|
+
if (this.repeatedShutdownRequestPolicy === void 0) {
|
|
3209
|
+
return {
|
|
3210
|
+
configured: false,
|
|
3211
|
+
isShuttingDown: this.isShuttingDown,
|
|
3212
|
+
isArmed: false,
|
|
3213
|
+
forceAfterCount: null,
|
|
3214
|
+
withinMS: null,
|
|
3215
|
+
armedAfterFailureMS: null,
|
|
3216
|
+
armedAfterFailureMSSource: null,
|
|
3217
|
+
requestCount: 0,
|
|
3218
|
+
firstMethod: null,
|
|
3219
|
+
latestMethod: null,
|
|
3220
|
+
firstRequestAt: null,
|
|
3221
|
+
latestRequestAt: null,
|
|
3222
|
+
repeatedWindowStartedAt: null,
|
|
3223
|
+
armedUntil: null,
|
|
3224
|
+
hasTriggeredForceShutdown: false
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
3228
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
3229
|
+
const isArmed = armedUntil !== null;
|
|
3230
|
+
return {
|
|
3231
|
+
configured: true,
|
|
3232
|
+
isShuttingDown: this.isShuttingDown,
|
|
3233
|
+
isArmed,
|
|
3234
|
+
forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
|
|
3235
|
+
withinMS: this.repeatedShutdownRequestPolicy.withinMS,
|
|
3236
|
+
armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
3237
|
+
armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
|
|
3238
|
+
countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
|
|
3239
|
+
requestCount: this.repeatedShutdownRequestState.requestCount,
|
|
3240
|
+
firstMethod: this.repeatedShutdownRequestState.firstMethod,
|
|
3241
|
+
latestMethod: this.repeatedShutdownRequestState.latestMethod,
|
|
3242
|
+
firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
|
|
3243
|
+
latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
|
|
3244
|
+
repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
|
|
3245
|
+
armedUntil: isArmed ? armedUntil : null,
|
|
3246
|
+
hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3027
3249
|
/**
|
|
3028
3250
|
* Enable Logger exit hook integration
|
|
3029
3251
|
*
|
|
@@ -3062,7 +3284,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3062
3284
|
if (this.isShuttingDown) {
|
|
3063
3285
|
if (isFirstExit && this.pendingLoggerExitResolve === null) {
|
|
3064
3286
|
this.logger.debug(
|
|
3065
|
-
|
|
3287
|
+
LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
|
|
3066
3288
|
{
|
|
3067
3289
|
params: { exitCode }
|
|
3068
3290
|
}
|
|
@@ -3071,7 +3293,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3071
3293
|
this.pendingLoggerExitResolve = resolve;
|
|
3072
3294
|
});
|
|
3073
3295
|
}
|
|
3074
|
-
this.logger.debug(
|
|
3296
|
+
this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
|
|
3075
3297
|
params: { exitCode }
|
|
3076
3298
|
});
|
|
3077
3299
|
return { action: "wait" };
|
|
@@ -3161,7 +3383,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3161
3383
|
return {
|
|
3162
3384
|
name,
|
|
3163
3385
|
healthy: false,
|
|
3164
|
-
message:
|
|
3386
|
+
message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
3165
3387
|
checkedAt: startTime,
|
|
3166
3388
|
durationMS: 0,
|
|
3167
3389
|
error: null,
|
|
@@ -3174,7 +3396,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3174
3396
|
return {
|
|
3175
3397
|
name,
|
|
3176
3398
|
healthy: false,
|
|
3177
|
-
message: isStalled ?
|
|
3399
|
+
message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
3178
3400
|
checkedAt: startTime,
|
|
3179
3401
|
durationMS: Date.now() - startTime,
|
|
3180
3402
|
error: null,
|
|
@@ -3390,7 +3612,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3390
3612
|
result = component.onMessage(payload, from);
|
|
3391
3613
|
} catch (error) {
|
|
3392
3614
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3393
|
-
this.logger.entity(componentName).error(
|
|
3615
|
+
this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
|
|
3394
3616
|
params: { error: err, from }
|
|
3395
3617
|
});
|
|
3396
3618
|
this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
|
|
@@ -3450,7 +3672,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3450
3672
|
};
|
|
3451
3673
|
} catch (error) {
|
|
3452
3674
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3453
|
-
this.logger.entity(componentName).error(
|
|
3675
|
+
this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
|
|
3454
3676
|
params: { error: err, from, timeoutMS }
|
|
3455
3677
|
});
|
|
3456
3678
|
this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
|
|
@@ -3723,7 +3945,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3723
3945
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3724
3946
|
name: componentName,
|
|
3725
3947
|
reason: "shutdown_in_progress",
|
|
3726
|
-
message:
|
|
3948
|
+
message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
|
|
3727
3949
|
registrationIndexBefore,
|
|
3728
3950
|
registrationIndexAfter: registrationIndexBefore,
|
|
3729
3951
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3735,7 +3957,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3735
3957
|
targetComponentName,
|
|
3736
3958
|
registrationIndexBefore,
|
|
3737
3959
|
code: "shutdown_in_progress",
|
|
3738
|
-
reason:
|
|
3960
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
|
|
3739
3961
|
targetFound: void 0
|
|
3740
3962
|
});
|
|
3741
3963
|
}
|
|
@@ -3746,7 +3968,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3746
3968
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3747
3969
|
name: componentName,
|
|
3748
3970
|
reason: "startup_in_progress",
|
|
3749
|
-
message:
|
|
3971
|
+
message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
|
|
3750
3972
|
registrationIndexBefore,
|
|
3751
3973
|
registrationIndexAfter: registrationIndexBefore,
|
|
3752
3974
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3758,7 +3980,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3758
3980
|
targetComponentName,
|
|
3759
3981
|
registrationIndexBefore,
|
|
3760
3982
|
code: "startup_in_progress",
|
|
3761
|
-
reason:
|
|
3983
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
|
|
3762
3984
|
targetFound: void 0
|
|
3763
3985
|
});
|
|
3764
3986
|
}
|
|
@@ -3767,7 +3989,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3767
3989
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3768
3990
|
name: componentName,
|
|
3769
3991
|
reason: "duplicate_instance",
|
|
3770
|
-
message:
|
|
3992
|
+
message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
|
|
3771
3993
|
registrationIndexBefore,
|
|
3772
3994
|
registrationIndexAfter: registrationIndexBefore,
|
|
3773
3995
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3779,7 +4001,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3779
4001
|
targetComponentName,
|
|
3780
4002
|
registrationIndexBefore,
|
|
3781
4003
|
code: "duplicate_instance",
|
|
3782
|
-
reason:
|
|
4004
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
|
|
3783
4005
|
targetFound: void 0
|
|
3784
4006
|
});
|
|
3785
4007
|
}
|
|
@@ -4049,9 +4271,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4049
4271
|
code: "already_in_progress"
|
|
4050
4272
|
};
|
|
4051
4273
|
}
|
|
4274
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
4275
|
+
const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
|
|
4276
|
+
const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
|
|
4277
|
+
if (isManualRetryWhileArmed) {
|
|
4278
|
+
if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
|
|
4279
|
+
this.handleRepeatedShutdownRequest(method);
|
|
4280
|
+
} else {
|
|
4281
|
+
this.resetRepeatedShutdownRequestState();
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
|
|
4285
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
4286
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = null;
|
|
4287
|
+
}
|
|
4052
4288
|
this.isShuttingDown = true;
|
|
4053
4289
|
this.shutdownToken = (0, import_ulid2.ulid)();
|
|
4054
4290
|
this.shutdownMethod = method;
|
|
4291
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
|
|
4292
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
4293
|
+
}
|
|
4055
4294
|
const isDuringStartup = this.isStarting;
|
|
4056
4295
|
this.logger.info("Stopping all components", { params: { method } });
|
|
4057
4296
|
this.lifecycleEvents.lifecycleManagerShutdownInitiated(
|
|
@@ -4076,8 +4315,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4076
4315
|
const runningComponentsToStop = shutdownOrder.filter(
|
|
4077
4316
|
(name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
|
|
4078
4317
|
);
|
|
4079
|
-
const stoppedComponents =
|
|
4080
|
-
const stalledComponents = [];
|
|
4318
|
+
const stoppedComponents = /* @__PURE__ */ new Set();
|
|
4081
4319
|
let hasTimedOut = false;
|
|
4082
4320
|
let timeoutHandle;
|
|
4083
4321
|
try {
|
|
@@ -4108,34 +4346,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4108
4346
|
this.logger.entity(name).info("Stopping component");
|
|
4109
4347
|
const isRunning = this.isComponentRunning(name);
|
|
4110
4348
|
const isStalled = stalledComponentNames.has(name);
|
|
4349
|
+
const currentState = this.componentStates.get(name);
|
|
4350
|
+
if (currentState === "stopped") {
|
|
4351
|
+
stoppedComponents.add(name);
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4111
4354
|
const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
|
|
4112
4355
|
success: false,
|
|
4113
4356
|
componentName: name,
|
|
4114
|
-
reason:
|
|
4357
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4115
4358
|
code: "component_stalled",
|
|
4116
4359
|
status: this.getComponentStatus(name)
|
|
4117
4360
|
} : {
|
|
4118
4361
|
success: false,
|
|
4119
4362
|
componentName: name,
|
|
4120
|
-
reason:
|
|
4363
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4121
4364
|
code: "component_not_running",
|
|
4122
4365
|
status: this.getComponentStatus(name)
|
|
4123
4366
|
};
|
|
4124
4367
|
if (result2.success) {
|
|
4125
|
-
stoppedComponents.
|
|
4368
|
+
stoppedComponents.add(name);
|
|
4126
4369
|
} else {
|
|
4127
4370
|
this.logger.entity(name).error(
|
|
4128
4371
|
"Component failed to stop, continuing with others: {{error.message}}",
|
|
4129
4372
|
{
|
|
4130
4373
|
params: {
|
|
4131
|
-
error: result2.error || new Error(
|
|
4374
|
+
error: result2.error || new Error(
|
|
4375
|
+
result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
4376
|
+
)
|
|
4132
4377
|
}
|
|
4133
4378
|
}
|
|
4134
4379
|
);
|
|
4135
|
-
const stallInfo = this.stalledComponents.get(name);
|
|
4136
|
-
if (stallInfo) {
|
|
4137
|
-
stalledComponents.push(stallInfo);
|
|
4138
|
-
}
|
|
4139
4380
|
if (shouldHaltOnStall) {
|
|
4140
4381
|
this.logger.warn(
|
|
4141
4382
|
"Halting shutdown after stall (haltOnStall=true)",
|
|
@@ -4151,18 +4392,40 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4151
4392
|
} else {
|
|
4152
4393
|
await shutdownOperation();
|
|
4153
4394
|
}
|
|
4395
|
+
const finalStalledNames = /* @__PURE__ */ new Set();
|
|
4396
|
+
for (const name of runningComponentsToStop) {
|
|
4397
|
+
if (this.stalledComponents.has(name)) {
|
|
4398
|
+
finalStalledNames.add(name);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
if (!shouldRetryStalled) {
|
|
4402
|
+
for (const name of stalledComponentNames) {
|
|
4403
|
+
if (this.stalledComponents.has(name)) {
|
|
4404
|
+
finalStalledNames.add(name);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
for (const name of runningComponentsToStop) {
|
|
4409
|
+
if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
|
|
4410
|
+
stoppedComponents.add(name);
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
|
|
4154
4414
|
const durationMS = Date.now() - startTime;
|
|
4155
4415
|
const isSuccess = !hasTimedOut && stalledComponents.length === 0;
|
|
4156
|
-
this.logger[isSuccess ? "success" : "warn"](
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4416
|
+
this.logger[isSuccess ? "success" : "warn"](
|
|
4417
|
+
isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
|
|
4418
|
+
{
|
|
4419
|
+
params: {
|
|
4420
|
+
stopped: stoppedComponents.size,
|
|
4421
|
+
stalled: stalledComponents.length,
|
|
4422
|
+
durationMS
|
|
4423
|
+
}
|
|
4161
4424
|
}
|
|
4162
|
-
|
|
4425
|
+
);
|
|
4163
4426
|
const result = {
|
|
4164
4427
|
success: isSuccess,
|
|
4165
|
-
stoppedComponents,
|
|
4428
|
+
stoppedComponents: Array.from(stoppedComponents),
|
|
4166
4429
|
stalledComponents,
|
|
4167
4430
|
durationMS,
|
|
4168
4431
|
timedOut: hasTimedOut || void 0,
|
|
@@ -4177,6 +4440,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4177
4440
|
method,
|
|
4178
4441
|
duringStartup: isDuringStartup
|
|
4179
4442
|
});
|
|
4443
|
+
if (isSuccess) {
|
|
4444
|
+
this.resetRepeatedShutdownRequestState();
|
|
4445
|
+
} else {
|
|
4446
|
+
this.armRepeatedShutdownAfterFailure();
|
|
4447
|
+
}
|
|
4180
4448
|
return result;
|
|
4181
4449
|
} finally {
|
|
4182
4450
|
if (timeoutHandle) {
|
|
@@ -4208,7 +4476,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4208
4476
|
return {
|
|
4209
4477
|
success: false,
|
|
4210
4478
|
componentName: name,
|
|
4211
|
-
reason:
|
|
4479
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4212
4480
|
code: "component_not_found"
|
|
4213
4481
|
};
|
|
4214
4482
|
}
|
|
@@ -4219,12 +4487,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4219
4487
|
return {
|
|
4220
4488
|
success: false,
|
|
4221
4489
|
componentName: name,
|
|
4222
|
-
reason:
|
|
4490
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4223
4491
|
code: "component_not_running",
|
|
4224
4492
|
status: this.getComponentStatus(name)
|
|
4225
4493
|
};
|
|
4226
4494
|
}
|
|
4227
4495
|
this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
|
|
4496
|
+
if (component.onShutdownForce) {
|
|
4497
|
+
this.issueStopAttemptToken(name);
|
|
4498
|
+
}
|
|
4228
4499
|
return this.shutdownComponentForce(name, component, {
|
|
4229
4500
|
gracefulPhaseRan: false,
|
|
4230
4501
|
gracefulTimedOut: false,
|
|
@@ -4244,7 +4515,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4244
4515
|
return {
|
|
4245
4516
|
success: false,
|
|
4246
4517
|
componentName: name,
|
|
4247
|
-
reason:
|
|
4518
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
4248
4519
|
code: "shutdown_in_progress"
|
|
4249
4520
|
};
|
|
4250
4521
|
}
|
|
@@ -4256,7 +4527,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4256
4527
|
return {
|
|
4257
4528
|
success: false,
|
|
4258
4529
|
componentName: name,
|
|
4259
|
-
reason:
|
|
4530
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
|
|
4260
4531
|
code: "startup_in_progress"
|
|
4261
4532
|
};
|
|
4262
4533
|
}
|
|
@@ -4266,7 +4537,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4266
4537
|
return {
|
|
4267
4538
|
success: false,
|
|
4268
4539
|
componentName: name,
|
|
4269
|
-
reason:
|
|
4540
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4270
4541
|
code: "component_not_found"
|
|
4271
4542
|
};
|
|
4272
4543
|
}
|
|
@@ -4275,7 +4546,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4275
4546
|
return {
|
|
4276
4547
|
success: false,
|
|
4277
4548
|
componentName: name,
|
|
4278
|
-
reason:
|
|
4549
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4279
4550
|
code: "component_stalled",
|
|
4280
4551
|
status: this.getComponentStatus(name)
|
|
4281
4552
|
};
|
|
@@ -4339,6 +4610,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4339
4610
|
const timeoutMS = component.startupTimeoutMS;
|
|
4340
4611
|
const startAttemptToken = (0, import_ulid2.ulid)();
|
|
4341
4612
|
this.componentStartAttemptTokens.set(name, startAttemptToken);
|
|
4613
|
+
component._setUnexpectedStopHandler(
|
|
4614
|
+
(error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
|
|
4615
|
+
);
|
|
4342
4616
|
const shutdownTokenAtStart = this.shutdownToken;
|
|
4343
4617
|
const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
|
|
4344
4618
|
let timeoutHandle;
|
|
@@ -4381,6 +4655,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4381
4655
|
} else {
|
|
4382
4656
|
await startPromise;
|
|
4383
4657
|
}
|
|
4658
|
+
if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
4659
|
+
component._clearUnexpectedStopHandler();
|
|
4660
|
+
const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
|
|
4661
|
+
return {
|
|
4662
|
+
success: false,
|
|
4663
|
+
componentName: name,
|
|
4664
|
+
reason: error.message,
|
|
4665
|
+
code: "component_unexpected_stop",
|
|
4666
|
+
error,
|
|
4667
|
+
status: this.getComponentStatus(name)
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4384
4670
|
if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
|
|
4385
4671
|
this.componentStates.set(name, "running");
|
|
4386
4672
|
this.runningComponents.add(name);
|
|
@@ -4408,6 +4694,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4408
4694
|
this.componentStates.set(name, "running");
|
|
4409
4695
|
this.runningComponents.add(name);
|
|
4410
4696
|
this.stalledComponents.delete(name);
|
|
4697
|
+
if (shouldForceStalled) {
|
|
4698
|
+
this.issueStopAttemptToken(name);
|
|
4699
|
+
}
|
|
4411
4700
|
this.updateStartedFlag();
|
|
4412
4701
|
if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
|
|
4413
4702
|
this.autoAttachSignals("first component start");
|
|
@@ -4427,9 +4716,24 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4427
4716
|
status: this.getComponentStatus(name)
|
|
4428
4717
|
};
|
|
4429
4718
|
} catch (error) {
|
|
4719
|
+
component._clearUnexpectedStopHandler();
|
|
4430
4720
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4721
|
+
const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
|
|
4722
|
+
const unexpectedStopError = this.componentErrors.get(name);
|
|
4723
|
+
if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
|
|
4724
|
+
return {
|
|
4725
|
+
success: false,
|
|
4726
|
+
componentName: name,
|
|
4727
|
+
reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
|
|
4728
|
+
code: "component_unexpected_stop",
|
|
4729
|
+
error: unexpectedStopError || new Error(
|
|
4730
|
+
`Component "${name}" stopped unexpectedly during startup`
|
|
4731
|
+
),
|
|
4732
|
+
status: this.getComponentStatus(name)
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4431
4735
|
this.componentErrors.set(name, err);
|
|
4432
|
-
if (
|
|
4736
|
+
if (isStartupTimeout) {
|
|
4433
4737
|
this.componentStates.set(name, "starting-timed-out");
|
|
4434
4738
|
this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
|
|
4435
4739
|
params: { error: err }
|
|
@@ -4474,7 +4778,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4474
4778
|
return {
|
|
4475
4779
|
success: false,
|
|
4476
4780
|
componentName: name,
|
|
4477
|
-
reason:
|
|
4781
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4478
4782
|
code: "component_not_found"
|
|
4479
4783
|
};
|
|
4480
4784
|
}
|
|
@@ -4482,7 +4786,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4482
4786
|
return {
|
|
4483
4787
|
success: false,
|
|
4484
4788
|
componentName: name,
|
|
4485
|
-
reason:
|
|
4789
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4486
4790
|
code: "component_stalled",
|
|
4487
4791
|
status: this.getComponentStatus(name)
|
|
4488
4792
|
};
|
|
@@ -4491,7 +4795,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4491
4795
|
return {
|
|
4492
4796
|
success: false,
|
|
4493
4797
|
componentName: name,
|
|
4494
|
-
reason:
|
|
4798
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4495
4799
|
code: "component_not_running",
|
|
4496
4800
|
status: this.getComponentStatus(name)
|
|
4497
4801
|
};
|
|
@@ -4507,6 +4811,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4507
4811
|
};
|
|
4508
4812
|
}
|
|
4509
4813
|
if (options?.forceImmediate) {
|
|
4814
|
+
if (component.onShutdownForce) {
|
|
4815
|
+
this.issueStopAttemptToken(name);
|
|
4816
|
+
}
|
|
4817
|
+
component._clearUnexpectedStopHandler();
|
|
4510
4818
|
return this.shutdownComponentForce(name, component, {
|
|
4511
4819
|
gracefulPhaseRan: false,
|
|
4512
4820
|
gracefulTimedOut: false,
|
|
@@ -4643,9 +4951,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4643
4951
|
* Calls stop() with timeout
|
|
4644
4952
|
*/
|
|
4645
4953
|
async shutdownComponentGraceful(name, component, options) {
|
|
4954
|
+
component._clearUnexpectedStopHandler();
|
|
4646
4955
|
this.componentStates.set(name, "stopping");
|
|
4647
4956
|
this.logger.entity(name).info("Graceful shutdown started");
|
|
4648
4957
|
this.lifecycleEvents.componentStopping(name);
|
|
4958
|
+
const stopAttemptToken = this.issueStopAttemptToken(name);
|
|
4649
4959
|
const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
|
|
4650
4960
|
let timeoutHandle;
|
|
4651
4961
|
try {
|
|
@@ -4666,7 +4976,16 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4666
4976
|
);
|
|
4667
4977
|
}
|
|
4668
4978
|
}
|
|
4669
|
-
Promise.resolve(stopPromise).
|
|
4979
|
+
Promise.resolve(stopPromise).then(
|
|
4980
|
+
() => this.handleLateStopResolution(
|
|
4981
|
+
name,
|
|
4982
|
+
stopAttemptToken,
|
|
4983
|
+
"graceful"
|
|
4984
|
+
),
|
|
4985
|
+
() => {
|
|
4986
|
+
}
|
|
4987
|
+
// Intentionally ignore errors after timeout
|
|
4988
|
+
).catch(() => {
|
|
4670
4989
|
});
|
|
4671
4990
|
reject(
|
|
4672
4991
|
new ComponentStopTimeoutError({
|
|
@@ -4685,9 +5004,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4685
5004
|
this.stalledComponents.delete(name);
|
|
4686
5005
|
this.updateStartedFlag();
|
|
4687
5006
|
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
4688
|
-
this.logger.info(
|
|
4689
|
-
"Auto-detaching process signals on last component stop"
|
|
4690
|
-
);
|
|
5007
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
4691
5008
|
this.detachSignals();
|
|
4692
5009
|
}
|
|
4693
5010
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
@@ -4710,15 +5027,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4710
5027
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4711
5028
|
this.componentErrors.set(name, err);
|
|
4712
5029
|
if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
|
|
4713
|
-
this.logger.entity(name).warn(
|
|
5030
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
|
|
4714
5031
|
this.lifecycleEvents.componentStopTimeout(name, err, {
|
|
4715
5032
|
timeoutMS,
|
|
4716
|
-
reason:
|
|
5033
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
|
|
4717
5034
|
});
|
|
4718
5035
|
return {
|
|
4719
5036
|
success: false,
|
|
4720
5037
|
componentName: name,
|
|
4721
|
-
reason:
|
|
5038
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
|
|
4722
5039
|
code: "component_shutdown_timeout",
|
|
4723
5040
|
error: err,
|
|
4724
5041
|
status: this.getComponentStatus(name)
|
|
@@ -4761,8 +5078,6 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4761
5078
|
gracefulTimedOut: context.gracefulTimedOut
|
|
4762
5079
|
}
|
|
4763
5080
|
});
|
|
4764
|
-
const timeoutMS = component.shutdownForceTimeoutMS;
|
|
4765
|
-
let timeoutHandle;
|
|
4766
5081
|
if (!component.onShutdownForce) {
|
|
4767
5082
|
const stallInfo = {
|
|
4768
5083
|
name,
|
|
@@ -4796,6 +5111,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4796
5111
|
status: this.getComponentStatus(name)
|
|
4797
5112
|
};
|
|
4798
5113
|
}
|
|
5114
|
+
const timeoutMS = component.shutdownForceTimeoutMS;
|
|
5115
|
+
const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
|
|
5116
|
+
let timeoutHandle;
|
|
4799
5117
|
try {
|
|
4800
5118
|
const forcePromise = component.onShutdownForce();
|
|
4801
5119
|
if (timeoutMS > 0) {
|
|
@@ -4814,23 +5132,44 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4814
5132
|
);
|
|
4815
5133
|
}
|
|
4816
5134
|
}
|
|
4817
|
-
|
|
5135
|
+
const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? (0, import_ulid2.ulid)();
|
|
5136
|
+
Promise.resolve(forcePromise).then(
|
|
5137
|
+
() => this.handleLateStopResolution(
|
|
5138
|
+
name,
|
|
5139
|
+
forceAttemptToken,
|
|
5140
|
+
"force"
|
|
5141
|
+
),
|
|
5142
|
+
() => {
|
|
5143
|
+
}
|
|
5144
|
+
// Intentionally ignore errors after timeout
|
|
5145
|
+
).catch(() => {
|
|
4818
5146
|
});
|
|
4819
|
-
reject(
|
|
5147
|
+
reject(
|
|
5148
|
+
new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
|
|
5149
|
+
);
|
|
4820
5150
|
}, timeoutMS);
|
|
4821
5151
|
});
|
|
4822
|
-
await Promise.race([
|
|
5152
|
+
await Promise.race([
|
|
5153
|
+
forcePromise,
|
|
5154
|
+
timeoutPromise,
|
|
5155
|
+
stoppedDuringForcePromise
|
|
5156
|
+
]);
|
|
4823
5157
|
} else {
|
|
4824
|
-
await forcePromise;
|
|
5158
|
+
await Promise.race([forcePromise, stoppedDuringForcePromise]);
|
|
5159
|
+
}
|
|
5160
|
+
if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
5161
|
+
return {
|
|
5162
|
+
success: true,
|
|
5163
|
+
componentName: name,
|
|
5164
|
+
status: this.getComponentStatus(name)
|
|
5165
|
+
};
|
|
4825
5166
|
}
|
|
4826
5167
|
this.componentStates.set(name, "stopped");
|
|
4827
5168
|
this.runningComponents.delete(name);
|
|
4828
5169
|
this.stalledComponents.delete(name);
|
|
4829
5170
|
this.updateStartedFlag();
|
|
4830
5171
|
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
4831
|
-
this.logger.info(
|
|
4832
|
-
"Auto-detaching process signals on last component stop"
|
|
4833
|
-
);
|
|
5172
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
4834
5173
|
this.detachSignals();
|
|
4835
5174
|
}
|
|
4836
5175
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
@@ -4851,8 +5190,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4851
5190
|
status: this.getComponentStatus(name)
|
|
4852
5191
|
};
|
|
4853
5192
|
} catch (error) {
|
|
5193
|
+
if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
5194
|
+
return {
|
|
5195
|
+
success: true,
|
|
5196
|
+
componentName: name,
|
|
5197
|
+
status: this.getComponentStatus(name)
|
|
5198
|
+
};
|
|
5199
|
+
}
|
|
4854
5200
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4855
|
-
const isTimeout = err.message ===
|
|
5201
|
+
const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
|
|
4856
5202
|
const stallInfo = {
|
|
4857
5203
|
name,
|
|
4858
5204
|
phase: "force",
|
|
@@ -4883,12 +5229,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4883
5229
|
return {
|
|
4884
5230
|
success: false,
|
|
4885
5231
|
componentName: name,
|
|
4886
|
-
reason: isTimeout ?
|
|
5232
|
+
reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
|
|
4887
5233
|
code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
|
|
4888
5234
|
error: err,
|
|
4889
5235
|
status: this.getComponentStatus(name)
|
|
4890
5236
|
};
|
|
4891
5237
|
} finally {
|
|
5238
|
+
cleanupForceWaiter();
|
|
4892
5239
|
if (timeoutHandle) {
|
|
4893
5240
|
clearTimeout(timeoutHandle);
|
|
4894
5241
|
}
|
|
@@ -4965,7 +5312,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4965
5312
|
"Failed to stop component during rollback, continuing: {{error.message}}",
|
|
4966
5313
|
{
|
|
4967
5314
|
params: {
|
|
4968
|
-
error: result.error || new Error(
|
|
5315
|
+
error: result.error || new Error(
|
|
5316
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
5317
|
+
)
|
|
4969
5318
|
}
|
|
4970
5319
|
}
|
|
4971
5320
|
);
|
|
@@ -4979,10 +5328,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4979
5328
|
}
|
|
4980
5329
|
this.logger.info(`Auto-attaching process signals on ${trigger}`);
|
|
4981
5330
|
this.attachSignals();
|
|
5331
|
+
if (this.isStarting) {
|
|
5332
|
+
this.autoAttachedSignalsDuringStartup = true;
|
|
5333
|
+
}
|
|
4982
5334
|
return true;
|
|
4983
5335
|
}
|
|
4984
5336
|
autoDetachSignalsIfIdle(trigger) {
|
|
4985
|
-
if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
5337
|
+
if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
4986
5338
|
return;
|
|
4987
5339
|
}
|
|
4988
5340
|
this.logger.info(`Auto-detaching process signals after ${trigger}`);
|
|
@@ -5024,6 +5376,210 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5024
5376
|
}).catch(() => {
|
|
5025
5377
|
});
|
|
5026
5378
|
}
|
|
5379
|
+
consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
|
|
5380
|
+
if (this.unexpectedStopsDuringStartup.size === 0) {
|
|
5381
|
+
return { startedComponents: [...startedComponents] };
|
|
5382
|
+
}
|
|
5383
|
+
const remainingStartedComponents = [];
|
|
5384
|
+
let requiredFailure;
|
|
5385
|
+
for (const name of startedComponents) {
|
|
5386
|
+
const startupStopError = this.unexpectedStopsDuringStartup.get(name);
|
|
5387
|
+
if (startupStopError === void 0) {
|
|
5388
|
+
remainingStartedComponents.push(name);
|
|
5389
|
+
continue;
|
|
5390
|
+
}
|
|
5391
|
+
this.unexpectedStopsDuringStartup.delete(name);
|
|
5392
|
+
const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
|
|
5393
|
+
const component = this.getComponent(name);
|
|
5394
|
+
if (component?.isOptional()) {
|
|
5395
|
+
if (!failedOptionalComponents.some((entry) => entry.name === name)) {
|
|
5396
|
+
failedOptionalComponents.push({ name, error });
|
|
5397
|
+
}
|
|
5398
|
+
this.logger.entity(name).warn(
|
|
5399
|
+
LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
5400
|
+
{
|
|
5401
|
+
params: { error }
|
|
5402
|
+
}
|
|
5403
|
+
);
|
|
5404
|
+
continue;
|
|
5405
|
+
}
|
|
5406
|
+
this.logger.entity(name).error(
|
|
5407
|
+
LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
5408
|
+
{
|
|
5409
|
+
params: { error }
|
|
5410
|
+
}
|
|
5411
|
+
);
|
|
5412
|
+
requiredFailure ??= { name, error };
|
|
5413
|
+
}
|
|
5414
|
+
return {
|
|
5415
|
+
startedComponents: remainingStartedComponents,
|
|
5416
|
+
requiredFailure
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
/**
|
|
5420
|
+
* Issues and returns a unique stop attempt token for a component.
|
|
5421
|
+
*
|
|
5422
|
+
* Each stop attempt (graceful or force-retry) gets a unique token.
|
|
5423
|
+
* The late-resolution handler captures this token in its closure so it can
|
|
5424
|
+
* skip any stall entries that were created by a *later* stop attempt — e.g. a
|
|
5425
|
+
* force-retry that also timed out after the original graceful promise floated
|
|
5426
|
+
* in the background.
|
|
5427
|
+
*/
|
|
5428
|
+
issueStopAttemptToken(name) {
|
|
5429
|
+
const next = (0, import_ulid2.ulid)();
|
|
5430
|
+
this.componentStopAttemptTokens.set(name, next);
|
|
5431
|
+
return next;
|
|
5432
|
+
}
|
|
5433
|
+
createPendingForceStopWaiter(name) {
|
|
5434
|
+
let isResolved = false;
|
|
5435
|
+
let waiters = this.pendingForceStopWaiters.get(name);
|
|
5436
|
+
if (!waiters) {
|
|
5437
|
+
waiters = /* @__PURE__ */ new Set();
|
|
5438
|
+
this.pendingForceStopWaiters.set(name, waiters);
|
|
5439
|
+
}
|
|
5440
|
+
let resolveWaiter;
|
|
5441
|
+
const promise = new Promise((resolve) => {
|
|
5442
|
+
resolveWaiter = () => {
|
|
5443
|
+
if (isResolved) {
|
|
5444
|
+
return;
|
|
5445
|
+
}
|
|
5446
|
+
isResolved = true;
|
|
5447
|
+
resolve();
|
|
5448
|
+
};
|
|
5449
|
+
});
|
|
5450
|
+
waiters.add(resolveWaiter);
|
|
5451
|
+
return {
|
|
5452
|
+
promise,
|
|
5453
|
+
cleanup: () => {
|
|
5454
|
+
const pending = this.pendingForceStopWaiters.get(name);
|
|
5455
|
+
if (!pending) {
|
|
5456
|
+
return;
|
|
5457
|
+
}
|
|
5458
|
+
pending.delete(resolveWaiter);
|
|
5459
|
+
if (pending.size === 0) {
|
|
5460
|
+
this.pendingForceStopWaiters.delete(name);
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5465
|
+
resolvePendingForceStopWaiters(name) {
|
|
5466
|
+
const waiters = this.pendingForceStopWaiters.get(name);
|
|
5467
|
+
if (!waiters || waiters.size === 0) {
|
|
5468
|
+
return;
|
|
5469
|
+
}
|
|
5470
|
+
this.pendingForceStopWaiters.delete(name);
|
|
5471
|
+
for (const resolve of waiters) {
|
|
5472
|
+
resolve();
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
/**
|
|
5476
|
+
* Called when a stop promise eventually resolves after its timeout path already fired.
|
|
5477
|
+
*
|
|
5478
|
+
* Usually this means a previously stalled component's original stop() or
|
|
5479
|
+
* onShutdownForce() promise finally resolved, so the manager can clear the
|
|
5480
|
+
* stall and transition the component to stopped without a manual retry.
|
|
5481
|
+
*
|
|
5482
|
+
* There is one extra overlap case for graceful stop(): stop() can resolve
|
|
5483
|
+
* after the graceful timeout but before onShutdownForce() itself times out.
|
|
5484
|
+
* In that window no stall entry exists yet, but the component still finished
|
|
5485
|
+
* stopping cleanly, so we finalize it here and let the later force-timeout
|
|
5486
|
+
* path observe the already-stopped state and no-op. This overlap fix is
|
|
5487
|
+
* scoped to the same stop token and will not cross a later retry attempt.
|
|
5488
|
+
*
|
|
5489
|
+
* Two guards prevent stale floating promises from incorrectly clearing state:
|
|
5490
|
+
*
|
|
5491
|
+
* 1. token guard — if a newer stop attempt (e.g. a retryStalled
|
|
5492
|
+
* force-retry) has started since this promise was launched, its token
|
|
5493
|
+
* won't match and we bail out immediately.
|
|
5494
|
+
*
|
|
5495
|
+
* 2. state/stall guard — if the component was unregistered, restarted, or
|
|
5496
|
+
* already cleared by another path, there will be neither a matching stall
|
|
5497
|
+
* entry nor the force-phase overlap state, so we bail out.
|
|
5498
|
+
*/
|
|
5499
|
+
handleLateStopResolution(name, token, source) {
|
|
5500
|
+
if (this.componentStopAttemptTokens.get(name) !== token) {
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
const currentState = this.componentStates.get(name);
|
|
5504
|
+
const stallInfo = this.stalledComponents.get(name);
|
|
5505
|
+
const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
|
|
5506
|
+
if (stallInfo && currentState !== "stalled") {
|
|
5507
|
+
this.stalledComponents.delete(name);
|
|
5508
|
+
return;
|
|
5509
|
+
}
|
|
5510
|
+
if (!stallInfo && !isCompletedDuringForcePhase) {
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
|
|
5514
|
+
if (stallInfo) {
|
|
5515
|
+
this.stalledComponents.delete(name);
|
|
5516
|
+
}
|
|
5517
|
+
this.componentStates.set(name, "stopped");
|
|
5518
|
+
this.runningComponents.delete(name);
|
|
5519
|
+
this.componentErrors.set(name, null);
|
|
5520
|
+
this.updateStartedFlag();
|
|
5521
|
+
this.resolvePendingForceStopWaiters(name);
|
|
5522
|
+
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
5523
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
5524
|
+
this.detachSignals();
|
|
5525
|
+
}
|
|
5526
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
5527
|
+
startedAt: null,
|
|
5528
|
+
stoppedAt: null
|
|
5529
|
+
};
|
|
5530
|
+
timestamps.stoppedAt = Date.now();
|
|
5531
|
+
this.componentTimestamps.set(name, timestamps);
|
|
5532
|
+
this.logger.entity(name).info(
|
|
5533
|
+
stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
|
|
5534
|
+
stalledDurationMS ? { params: { stalledDurationMS } } : void 0
|
|
5535
|
+
);
|
|
5536
|
+
if (source === "force") {
|
|
5537
|
+
this.lifecycleEvents.componentShutdownForceCompleted(name);
|
|
5538
|
+
}
|
|
5539
|
+
if (stallInfo && stalledDurationMS !== void 0) {
|
|
5540
|
+
this.lifecycleEvents.componentStalledResolved(
|
|
5541
|
+
name,
|
|
5542
|
+
stallInfo,
|
|
5543
|
+
stalledDurationMS
|
|
5544
|
+
);
|
|
5545
|
+
}
|
|
5546
|
+
this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
|
|
5547
|
+
}
|
|
5548
|
+
handleComponentUnexpectedStop(name, startAttemptToken, error) {
|
|
5549
|
+
const currentState = this.componentStates.get(name);
|
|
5550
|
+
if (
|
|
5551
|
+
// Startup-time self-stops are valid too: start() may still be awaiting
|
|
5552
|
+
// some async work while an internal listener has already observed that
|
|
5553
|
+
// the component died and reported it.
|
|
5554
|
+
currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
|
|
5555
|
+
) {
|
|
5556
|
+
return false;
|
|
5557
|
+
}
|
|
5558
|
+
this.runningComponents.delete(name);
|
|
5559
|
+
this.componentStates.set(name, "stopped");
|
|
5560
|
+
this.componentErrors.set(name, error ?? null);
|
|
5561
|
+
if (this.isStarting) {
|
|
5562
|
+
this.unexpectedStopsDuringStartup.set(name, error ?? null);
|
|
5563
|
+
}
|
|
5564
|
+
this.updateStartedFlag();
|
|
5565
|
+
if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
5566
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
5567
|
+
this.detachSignals();
|
|
5568
|
+
}
|
|
5569
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
5570
|
+
startedAt: null,
|
|
5571
|
+
stoppedAt: null
|
|
5572
|
+
};
|
|
5573
|
+
timestamps.stoppedAt = Date.now();
|
|
5574
|
+
this.componentTimestamps.set(name, timestamps);
|
|
5575
|
+
this.logger.entity(name).warn(
|
|
5576
|
+
error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
|
|
5577
|
+
{ params: { error } }
|
|
5578
|
+
);
|
|
5579
|
+
this.lifecycleEvents.componentUnexpectedStop(name, error);
|
|
5580
|
+
this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
|
|
5581
|
+
return true;
|
|
5582
|
+
}
|
|
5027
5583
|
/**
|
|
5028
5584
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
5029
5585
|
*/
|
|
@@ -5305,121 +5861,377 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5305
5861
|
}
|
|
5306
5862
|
/**
|
|
5307
5863
|
* Handle shutdown signal - initiates stopAllComponents().
|
|
5308
|
-
*
|
|
5864
|
+
*
|
|
5865
|
+
* Four cases depending on the current shutdown state:
|
|
5866
|
+
*
|
|
5867
|
+
* 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
|
|
5868
|
+
* repeated-shutdown policy if configured, otherwise log and discard.
|
|
5869
|
+
* Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
|
|
5870
|
+
* without starting another shutdown.
|
|
5871
|
+
*
|
|
5872
|
+
* 2. **Armed post-failure** (previous shutdown finished, armed window still
|
|
5873
|
+
* open): count the request toward the escalation window, emit
|
|
5874
|
+
* `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
|
|
5875
|
+
* new `stopAllComponents()` run to retry.
|
|
5876
|
+
*
|
|
5877
|
+
* 3. **Armed post-failure expired** (armed window opened but has since
|
|
5878
|
+
* elapsed): expire the stale state, treat the request as a fresh
|
|
5879
|
+
* shutdown (falls through to case 4).
|
|
5880
|
+
*
|
|
5881
|
+
* 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
|
|
5882
|
+
* if policy is configured, emit `signal:shutdown` with
|
|
5883
|
+
* `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
|
|
5884
|
+
*
|
|
5885
|
+
* In all cases `signal:shutdown` is emitted exactly once.
|
|
5309
5886
|
*/
|
|
5310
5887
|
handleShutdownRequest(method) {
|
|
5311
5888
|
if (this.isShuttingDown) {
|
|
5312
|
-
this.
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5889
|
+
this.lifecycleEvents.signalShutdown(method, true);
|
|
5890
|
+
if (this.handleRepeatedShutdownRequest(method)) {
|
|
5891
|
+
return;
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
let didEmitShutdownSignal = false;
|
|
5895
|
+
let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
|
|
5896
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
|
|
5897
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5898
|
+
didEmitShutdownSignal = true;
|
|
5899
|
+
shouldSeedRepeatedShutdownState = false;
|
|
5900
|
+
this.handleRepeatedShutdownRequest(method);
|
|
5901
|
+
}
|
|
5902
|
+
if (shouldSeedRepeatedShutdownState) {
|
|
5903
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
5316
5904
|
}
|
|
5317
5905
|
this.logger.info("Shutdown signal received", { params: { method } });
|
|
5318
|
-
|
|
5906
|
+
if (!didEmitShutdownSignal) {
|
|
5907
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5908
|
+
}
|
|
5319
5909
|
void this.stopAllComponentsInternal(method, {
|
|
5320
5910
|
...this.shutdownOptions
|
|
5321
5911
|
});
|
|
5322
5912
|
}
|
|
5323
5913
|
/**
|
|
5324
|
-
*
|
|
5325
|
-
*
|
|
5326
|
-
* When called from signal handlers (source='signal'), the Promise is started
|
|
5327
|
-
* but not awaited due to Node.js signal handler constraints. Components are
|
|
5328
|
-
* still notified and the work completes, but return values are not accessible.
|
|
5329
|
-
*
|
|
5330
|
-
* When called from manual triggers (source='trigger'), the Promise is awaited
|
|
5331
|
-
* and results are returned for programmatic use.
|
|
5914
|
+
* Tracks repeated shutdown requests during an active shutdown and optionally
|
|
5915
|
+
* invokes the configured force shutdown callback when the threshold is reached.
|
|
5332
5916
|
*
|
|
5333
|
-
* @
|
|
5917
|
+
* @returns true when the request was consumed as part of the repeated-shutdown
|
|
5918
|
+
* escalation flow, false when the caller should treat it as a fresh shutdown request
|
|
5334
5919
|
*/
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
await result;
|
|
5343
|
-
}
|
|
5344
|
-
return {
|
|
5345
|
-
signal: "reload",
|
|
5346
|
-
results: [],
|
|
5347
|
-
timedOut: false,
|
|
5348
|
-
code: "ok"
|
|
5349
|
-
};
|
|
5920
|
+
handleRepeatedShutdownRequest(method) {
|
|
5921
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5922
|
+
if (!policy) {
|
|
5923
|
+
this.logger.warn("Shutdown already in progress, ignoring signal", {
|
|
5924
|
+
params: { method }
|
|
5925
|
+
});
|
|
5926
|
+
return true;
|
|
5350
5927
|
}
|
|
5351
|
-
|
|
5928
|
+
const now = Date.now();
|
|
5929
|
+
const state = this.repeatedShutdownRequestState;
|
|
5930
|
+
if (state.remainsArmedUntil !== null) {
|
|
5931
|
+
if (now >= state.remainsArmedUntil) {
|
|
5932
|
+
this.expireRepeatedShutdownRequestState();
|
|
5933
|
+
return false;
|
|
5934
|
+
}
|
|
5935
|
+
this.refreshRepeatedShutdownArmedWindow(now);
|
|
5936
|
+
}
|
|
5937
|
+
const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
|
|
5938
|
+
if (shouldStartNewWindow) {
|
|
5939
|
+
state.requestCount = 1;
|
|
5940
|
+
state.repeatedWindowStartedAt = now;
|
|
5941
|
+
} else {
|
|
5942
|
+
state.requestCount++;
|
|
5943
|
+
}
|
|
5944
|
+
state.latestMethod = method;
|
|
5945
|
+
state.latestRequestAt = now;
|
|
5946
|
+
this.logger.warn(
|
|
5947
|
+
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",
|
|
5948
|
+
{
|
|
5949
|
+
params: {
|
|
5950
|
+
method,
|
|
5951
|
+
requestCount: state.requestCount,
|
|
5952
|
+
firstMethod: state.firstMethod,
|
|
5953
|
+
latestMethod: state.latestMethod,
|
|
5954
|
+
firstRequestAt: state.firstRequestAt,
|
|
5955
|
+
latestRequestAt: state.latestRequestAt,
|
|
5956
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5957
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5958
|
+
withinMS: policy.withinMS,
|
|
5959
|
+
forceAfterCount: policy.forceAfterCount
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
);
|
|
5963
|
+
if (
|
|
5964
|
+
// Force escalation is single-fire per shutdown cycle. Later requests are
|
|
5965
|
+
// still logged but do not re-enter user force-shutdown logic.
|
|
5966
|
+
state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
|
|
5967
|
+
) {
|
|
5968
|
+
return true;
|
|
5969
|
+
}
|
|
5970
|
+
state.hasTriggeredForceShutdown = true;
|
|
5971
|
+
const context = {
|
|
5972
|
+
requestCount: state.requestCount,
|
|
5973
|
+
firstMethod: state.firstMethod,
|
|
5974
|
+
latestMethod: state.latestMethod,
|
|
5975
|
+
firstRequestAt: state.firstRequestAt,
|
|
5976
|
+
latestRequestAt: state.latestRequestAt,
|
|
5977
|
+
isShuttingDown: this.isShuttingDown,
|
|
5978
|
+
wasArmedAfterFailure: state.remainsArmedUntil !== null
|
|
5979
|
+
};
|
|
5980
|
+
this.logger.warn(
|
|
5981
|
+
"Repeated shutdown request threshold reached, invoking force shutdown handler",
|
|
5982
|
+
{
|
|
5983
|
+
params: {
|
|
5984
|
+
method,
|
|
5985
|
+
requestCount: context.requestCount,
|
|
5986
|
+
firstMethod: context.firstMethod,
|
|
5987
|
+
latestMethod: context.latestMethod,
|
|
5988
|
+
firstRequestAt: context.firstRequestAt,
|
|
5989
|
+
latestRequestAt: context.latestRequestAt,
|
|
5990
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5991
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5992
|
+
withinMS: policy.withinMS,
|
|
5993
|
+
forceAfterCount: policy.forceAfterCount
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
);
|
|
5997
|
+
safeHandleCallback(
|
|
5998
|
+
"repeatedShutdownRequestPolicy.onForceShutdown",
|
|
5999
|
+
policy.onForceShutdown,
|
|
6000
|
+
context
|
|
6001
|
+
);
|
|
6002
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
|
|
6003
|
+
firstMethod: context.firstMethod,
|
|
6004
|
+
latestMethod: context.latestMethod,
|
|
6005
|
+
requestCount: context.requestCount,
|
|
6006
|
+
firstRequestAt: context.firstRequestAt,
|
|
6007
|
+
latestRequestAt: context.latestRequestAt,
|
|
6008
|
+
wasArmedAfterFailure: context.wasArmedAfterFailure
|
|
6009
|
+
});
|
|
6010
|
+
return true;
|
|
5352
6011
|
}
|
|
5353
6012
|
/**
|
|
5354
|
-
*
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
6013
|
+
* Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
|
|
6014
|
+
*/
|
|
6015
|
+
resetRepeatedShutdownRequestState() {
|
|
6016
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
6017
|
+
this.repeatedShutdownRequestState = {
|
|
6018
|
+
requestCount: 0,
|
|
6019
|
+
firstMethod: null,
|
|
6020
|
+
latestMethod: null,
|
|
6021
|
+
firstRequestAt: null,
|
|
6022
|
+
latestRequestAt: null,
|
|
6023
|
+
repeatedWindowStartedAt: null,
|
|
6024
|
+
hasTriggeredForceShutdown: false,
|
|
6025
|
+
remainsArmedUntil: null
|
|
6026
|
+
};
|
|
6027
|
+
}
|
|
6028
|
+
/**
|
|
6029
|
+
* Clear any pending expiration timer for the post-failure escalation window.
|
|
6030
|
+
*/
|
|
6031
|
+
clearRepeatedShutdownExpiryTimer() {
|
|
6032
|
+
if (this.repeatedShutdownExpiryTimer === null) {
|
|
6033
|
+
return;
|
|
6034
|
+
}
|
|
6035
|
+
clearTimeout(this.repeatedShutdownExpiryTimer);
|
|
6036
|
+
this.repeatedShutdownExpiryTimer = null;
|
|
6037
|
+
}
|
|
6038
|
+
/**
|
|
6039
|
+
* Returns whether post-failure escalation remains armed after first
|
|
6040
|
+
* normalizing any stale timer-backed state.
|
|
5358
6041
|
*
|
|
5359
|
-
*
|
|
6042
|
+
* The method can expire old armed windows as a side effect because the timer
|
|
6043
|
+
* callback may not have run yet on a delayed event loop. Callers use this
|
|
6044
|
+
* when they need the effective runtime truth, not just the last timer write.
|
|
5360
6045
|
*/
|
|
5361
|
-
|
|
5362
|
-
this.
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
6046
|
+
normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
|
|
6047
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
6048
|
+
if (armedUntil === null) {
|
|
6049
|
+
return false;
|
|
6050
|
+
}
|
|
6051
|
+
if (now >= armedUntil) {
|
|
6052
|
+
this.expireRepeatedShutdownRequestState();
|
|
6053
|
+
return false;
|
|
6054
|
+
}
|
|
6055
|
+
return true;
|
|
6056
|
+
}
|
|
6057
|
+
/**
|
|
6058
|
+
* Transition armed post-failure escalation state into its expired/reset state.
|
|
6059
|
+
*/
|
|
6060
|
+
expireRepeatedShutdownRequestState() {
|
|
6061
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6062
|
+
const state = this.repeatedShutdownRequestState;
|
|
6063
|
+
if (!policy || state.remainsArmedUntil === null) {
|
|
6064
|
+
return;
|
|
6065
|
+
}
|
|
6066
|
+
const armedUntil = state.remainsArmedUntil;
|
|
6067
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
6068
|
+
const expiredState = {
|
|
6069
|
+
firstMethod: state.firstMethod,
|
|
6070
|
+
latestMethod: state.latestMethod,
|
|
6071
|
+
requestCount: state.requestCount,
|
|
6072
|
+
armedUntil
|
|
6073
|
+
};
|
|
6074
|
+
this.logger.warn(
|
|
6075
|
+
"Repeated shutdown escalation window expired, clearing previous shutdown state",
|
|
6076
|
+
{
|
|
6077
|
+
params: {
|
|
6078
|
+
remainsArmedUntil: armedUntil,
|
|
6079
|
+
withinMS: policy.withinMS,
|
|
6080
|
+
forceAfterCount: policy.forceAfterCount
|
|
6081
|
+
}
|
|
5369
6082
|
}
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
6083
|
+
);
|
|
6084
|
+
if (expiredState.firstMethod !== null) {
|
|
6085
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
|
|
6086
|
+
firstMethod: expiredState.firstMethod,
|
|
6087
|
+
latestMethod: expiredState.latestMethod,
|
|
6088
|
+
requestCount: expiredState.requestCount,
|
|
6089
|
+
armedUntil: expiredState.armedUntil
|
|
6090
|
+
});
|
|
5376
6091
|
}
|
|
5377
|
-
|
|
6092
|
+
this.resetRepeatedShutdownRequestState();
|
|
5378
6093
|
}
|
|
5379
6094
|
/**
|
|
5380
|
-
*
|
|
6095
|
+
* Arms or refreshes the post-failure escalation window and its expiration timer.
|
|
6096
|
+
*/
|
|
6097
|
+
refreshRepeatedShutdownArmedWindow(now = Date.now()) {
|
|
6098
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6099
|
+
if (!policy) {
|
|
6100
|
+
return;
|
|
6101
|
+
}
|
|
6102
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
6103
|
+
const armedUntil = now + policy.armedAfterFailureMS;
|
|
6104
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
|
|
6105
|
+
this.repeatedShutdownExpiryTimer = setTimeout(() => {
|
|
6106
|
+
this.expireRepeatedShutdownRequestState();
|
|
6107
|
+
}, policy.armedAfterFailureMS);
|
|
6108
|
+
this.repeatedShutdownExpiryTimer.unref();
|
|
6109
|
+
}
|
|
6110
|
+
/**
|
|
6111
|
+
* Seeds shutdown escalation tracking for a new shutdown cycle.
|
|
5381
6112
|
*
|
|
5382
|
-
*
|
|
5383
|
-
*
|
|
6113
|
+
* The first shutdown trigger starts graceful shutdown and arms escalation with
|
|
6114
|
+
* an effective post-start count of 0. Later shutdown requests can then count
|
|
6115
|
+
* toward the configured force threshold regardless of whether the shutdown
|
|
6116
|
+
* started from a signal, keyboard shortcut, or direct API call.
|
|
6117
|
+
*/
|
|
6118
|
+
seedRepeatedShutdownRequestState(method) {
|
|
6119
|
+
const now = Date.now();
|
|
6120
|
+
this.repeatedShutdownRequestState = {
|
|
6121
|
+
requestCount: 0,
|
|
6122
|
+
firstMethod: method,
|
|
6123
|
+
latestMethod: method,
|
|
6124
|
+
firstRequestAt: now,
|
|
6125
|
+
latestRequestAt: now,
|
|
6126
|
+
repeatedWindowStartedAt: null,
|
|
6127
|
+
hasTriggeredForceShutdown: false,
|
|
6128
|
+
remainsArmedUntil: null
|
|
6129
|
+
};
|
|
6130
|
+
}
|
|
6131
|
+
/**
|
|
6132
|
+
* Preserves a short-lived post-failure escalation window after shutdown
|
|
6133
|
+
* returns unsuccessfully so operators can keep pressing shutdown without
|
|
6134
|
+
* losing the existing force count the moment the graceful attempt finishes.
|
|
6135
|
+
*/
|
|
6136
|
+
armRepeatedShutdownAfterFailure() {
|
|
6137
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6138
|
+
const state = this.repeatedShutdownRequestState;
|
|
6139
|
+
if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
|
|
6140
|
+
state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
this.refreshRepeatedShutdownArmedWindow();
|
|
6144
|
+
const armedUntil = state.remainsArmedUntil;
|
|
6145
|
+
if (state.firstMethod !== null && armedUntil !== null) {
|
|
6146
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
|
|
6147
|
+
firstMethod: state.firstMethod,
|
|
6148
|
+
requestCount: state.requestCount,
|
|
6149
|
+
armedUntil
|
|
6150
|
+
});
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
/**
|
|
6154
|
+
* Shared dispatch path for reload/info/debug requests. Logs the dispatch,
|
|
6155
|
+
* emits the signal event, then either invokes the user-supplied callback
|
|
6156
|
+
* (passing the broadcast function so the user controls when/whether to
|
|
6157
|
+
* broadcast) or broadcasts directly when no callback is configured.
|
|
5384
6158
|
*
|
|
5385
|
-
*
|
|
6159
|
+
* When called from signal handlers (source='signal'), the Promise is started
|
|
6160
|
+
* but not awaited — Node.js signal handlers cannot return values, so results
|
|
6161
|
+
* are not accessible. Components are still notified and the work completes.
|
|
6162
|
+
* When called from manual triggers (source='trigger'), the Promise is awaited
|
|
6163
|
+
* and results are returned for programmatic use.
|
|
5386
6164
|
*/
|
|
5387
|
-
async
|
|
5388
|
-
this.logger.info(
|
|
5389
|
-
|
|
5390
|
-
if (
|
|
5391
|
-
const
|
|
5392
|
-
const result = this.onDebugRequested(broadcastFn);
|
|
6165
|
+
async handleSignalRequest(descriptor, source) {
|
|
6166
|
+
this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
|
|
6167
|
+
descriptor.emitSignal();
|
|
6168
|
+
if (descriptor.customCallback) {
|
|
6169
|
+
const result = descriptor.customCallback(descriptor.broadcast);
|
|
5393
6170
|
if (isPromise(result)) {
|
|
5394
6171
|
await result;
|
|
5395
6172
|
}
|
|
5396
6173
|
return {
|
|
5397
|
-
signal:
|
|
6174
|
+
signal: descriptor.signal,
|
|
5398
6175
|
results: [],
|
|
5399
6176
|
timedOut: false,
|
|
5400
6177
|
code: "ok"
|
|
5401
6178
|
};
|
|
5402
6179
|
}
|
|
5403
|
-
return
|
|
6180
|
+
return descriptor.broadcast();
|
|
6181
|
+
}
|
|
6182
|
+
async handleReloadRequest(source = "trigger") {
|
|
6183
|
+
return this.handleSignalRequest(
|
|
6184
|
+
{
|
|
6185
|
+
signal: "reload",
|
|
6186
|
+
dispatchedLogLabel: "Reload dispatched",
|
|
6187
|
+
emitSignal: () => this.lifecycleEvents.signalReload(),
|
|
6188
|
+
customCallback: this.onReloadRequested,
|
|
6189
|
+
broadcast: () => this.broadcastReload()
|
|
6190
|
+
},
|
|
6191
|
+
source
|
|
6192
|
+
);
|
|
6193
|
+
}
|
|
6194
|
+
async handleInfoRequest(source = "trigger") {
|
|
6195
|
+
return this.handleSignalRequest(
|
|
6196
|
+
{
|
|
6197
|
+
signal: "info",
|
|
6198
|
+
dispatchedLogLabel: "Info dispatched",
|
|
6199
|
+
emitSignal: () => this.lifecycleEvents.signalInfo(),
|
|
6200
|
+
customCallback: this.onInfoRequested,
|
|
6201
|
+
broadcast: () => this.broadcastInfo()
|
|
6202
|
+
},
|
|
6203
|
+
source
|
|
6204
|
+
);
|
|
6205
|
+
}
|
|
6206
|
+
async handleDebugRequest(source = "trigger") {
|
|
6207
|
+
return this.handleSignalRequest(
|
|
6208
|
+
{
|
|
6209
|
+
signal: "debug",
|
|
6210
|
+
dispatchedLogLabel: "Debug dispatched",
|
|
6211
|
+
emitSignal: () => this.lifecycleEvents.signalDebug(),
|
|
6212
|
+
customCallback: this.onDebugRequested,
|
|
6213
|
+
broadcast: () => this.broadcastDebug()
|
|
6214
|
+
},
|
|
6215
|
+
source
|
|
6216
|
+
);
|
|
5404
6217
|
}
|
|
5405
6218
|
/**
|
|
5406
|
-
*
|
|
5407
|
-
*
|
|
5408
|
-
*
|
|
6219
|
+
* Shared signal broadcast pipeline used by reload/info/debug.
|
|
6220
|
+
* Iterates running components, runs the picked handler with timeout, and
|
|
6221
|
+
* aggregates per-component results into a SignalBroadcastResult.
|
|
5409
6222
|
*/
|
|
5410
|
-
async
|
|
6223
|
+
async runSignalBroadcast(descriptor) {
|
|
5411
6224
|
const results = [];
|
|
5412
|
-
const
|
|
6225
|
+
const targets = this.components.filter(
|
|
5413
6226
|
(component) => this.runningComponents.has(component.getName())
|
|
5414
6227
|
);
|
|
5415
6228
|
if (this.isStarting) {
|
|
5416
|
-
this.logger.info(
|
|
5417
|
-
"Reload during startup: only reloading already-started components"
|
|
5418
|
-
);
|
|
6229
|
+
this.logger.info(descriptor.startupLog);
|
|
5419
6230
|
}
|
|
5420
|
-
for (const component of
|
|
6231
|
+
for (const component of targets) {
|
|
5421
6232
|
const name = component.getName();
|
|
5422
|
-
|
|
6233
|
+
const handler = descriptor.pickHandler(component);
|
|
6234
|
+
if (!handler) {
|
|
5423
6235
|
results.push({
|
|
5424
6236
|
name,
|
|
5425
6237
|
called: false,
|
|
@@ -5429,13 +6241,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5429
6241
|
});
|
|
5430
6242
|
continue;
|
|
5431
6243
|
}
|
|
5432
|
-
|
|
6244
|
+
descriptor.emitStarted(name);
|
|
5433
6245
|
const timeoutMS = component.signalTimeoutMS;
|
|
5434
6246
|
let timeoutHandle;
|
|
5435
6247
|
const timeoutResult = { timedOut: true };
|
|
5436
6248
|
try {
|
|
5437
|
-
const
|
|
5438
|
-
const handlerPromise = isPromise(
|
|
6249
|
+
const handlerResult = handler();
|
|
6250
|
+
const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
|
|
5439
6251
|
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5440
6252
|
handlerPromise,
|
|
5441
6253
|
new Promise((resolve) => {
|
|
@@ -5445,7 +6257,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5445
6257
|
})
|
|
5446
6258
|
]) : await handlerPromise;
|
|
5447
6259
|
if (outcome === timeoutResult) {
|
|
5448
|
-
this.logger.entity(name).warn(
|
|
6260
|
+
this.logger.entity(name).warn(descriptor.timeoutLog, {
|
|
5449
6261
|
params: { timeoutMS }
|
|
5450
6262
|
});
|
|
5451
6263
|
Promise.resolve(handlerPromise).catch(() => {
|
|
@@ -5458,7 +6270,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5458
6270
|
code: "timeout"
|
|
5459
6271
|
});
|
|
5460
6272
|
} else {
|
|
5461
|
-
|
|
6273
|
+
descriptor.emitCompleted(name);
|
|
5462
6274
|
results.push({
|
|
5463
6275
|
name,
|
|
5464
6276
|
called: true,
|
|
@@ -5469,10 +6281,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5469
6281
|
}
|
|
5470
6282
|
} catch (error) {
|
|
5471
6283
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
5472
|
-
this.logger.entity(name).error(
|
|
6284
|
+
this.logger.entity(name).error(descriptor.errorLog, {
|
|
5473
6285
|
params: { error: err }
|
|
5474
6286
|
});
|
|
5475
|
-
|
|
6287
|
+
descriptor.emitFailed(name, err);
|
|
5476
6288
|
results.push({
|
|
5477
6289
|
name,
|
|
5478
6290
|
called: true,
|
|
@@ -5493,108 +6305,45 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5493
6305
|
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5494
6306
|
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5495
6307
|
return {
|
|
5496
|
-
signal:
|
|
6308
|
+
signal: descriptor.signal,
|
|
5497
6309
|
results,
|
|
5498
6310
|
timedOut: hasTimeout,
|
|
5499
6311
|
code
|
|
5500
6312
|
};
|
|
5501
6313
|
}
|
|
6314
|
+
/**
|
|
6315
|
+
* Broadcast reload signal to all running components.
|
|
6316
|
+
* Calls onReload() on components that implement it.
|
|
6317
|
+
* Continues on errors - collects all results.
|
|
6318
|
+
*/
|
|
6319
|
+
async broadcastReload() {
|
|
6320
|
+
return this.runSignalBroadcast({
|
|
6321
|
+
signal: "reload",
|
|
6322
|
+
pickHandler: (component) => component.onReload?.bind(component),
|
|
6323
|
+
startupLog: "Reload during startup: only reloading already-started components",
|
|
6324
|
+
timeoutLog: "Reload handler timed out",
|
|
6325
|
+
errorLog: "Reload failed: {{error.message}}",
|
|
6326
|
+
emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
|
|
6327
|
+
emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
|
|
6328
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
|
|
6329
|
+
});
|
|
6330
|
+
}
|
|
5502
6331
|
/**
|
|
5503
6332
|
* Broadcast info signal to all running components.
|
|
5504
6333
|
* Calls onInfo() on components that implement it.
|
|
5505
6334
|
* Continues on errors - collects all results.
|
|
5506
6335
|
*/
|
|
5507
6336
|
async broadcastInfo() {
|
|
5508
|
-
|
|
5509
|
-
const componentsToNotify = this.components.filter(
|
|
5510
|
-
(component) => this.runningComponents.has(component.getName())
|
|
5511
|
-
);
|
|
5512
|
-
if (this.isStarting) {
|
|
5513
|
-
this.logger.info(
|
|
5514
|
-
"Info during startup: only notifying already-started components"
|
|
5515
|
-
);
|
|
5516
|
-
}
|
|
5517
|
-
for (const component of componentsToNotify) {
|
|
5518
|
-
const name = component.getName();
|
|
5519
|
-
if (!component.onInfo) {
|
|
5520
|
-
results.push({
|
|
5521
|
-
name,
|
|
5522
|
-
called: false,
|
|
5523
|
-
error: null,
|
|
5524
|
-
timedOut: false,
|
|
5525
|
-
code: "no_handler"
|
|
5526
|
-
});
|
|
5527
|
-
continue;
|
|
5528
|
-
}
|
|
5529
|
-
this.lifecycleEvents.componentInfoStarted(name);
|
|
5530
|
-
const timeoutMS = component.signalTimeoutMS;
|
|
5531
|
-
let timeoutHandle;
|
|
5532
|
-
const timeoutResult = { timedOut: true };
|
|
5533
|
-
try {
|
|
5534
|
-
const result = component.onInfo();
|
|
5535
|
-
const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
|
|
5536
|
-
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5537
|
-
handlerPromise,
|
|
5538
|
-
new Promise((resolve) => {
|
|
5539
|
-
timeoutHandle = setTimeout(() => {
|
|
5540
|
-
resolve(timeoutResult);
|
|
5541
|
-
}, timeoutMS);
|
|
5542
|
-
})
|
|
5543
|
-
]) : await handlerPromise;
|
|
5544
|
-
if (outcome === timeoutResult) {
|
|
5545
|
-
this.logger.entity(name).warn("Info handler timed out", {
|
|
5546
|
-
params: { timeoutMS }
|
|
5547
|
-
});
|
|
5548
|
-
Promise.resolve(handlerPromise).catch(() => {
|
|
5549
|
-
});
|
|
5550
|
-
results.push({
|
|
5551
|
-
name,
|
|
5552
|
-
called: true,
|
|
5553
|
-
error: null,
|
|
5554
|
-
timedOut: true,
|
|
5555
|
-
code: "timeout"
|
|
5556
|
-
});
|
|
5557
|
-
} else {
|
|
5558
|
-
this.lifecycleEvents.componentInfoCompleted(name);
|
|
5559
|
-
results.push({
|
|
5560
|
-
name,
|
|
5561
|
-
called: true,
|
|
5562
|
-
error: null,
|
|
5563
|
-
timedOut: false,
|
|
5564
|
-
code: "called"
|
|
5565
|
-
});
|
|
5566
|
-
}
|
|
5567
|
-
} catch (error) {
|
|
5568
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
5569
|
-
this.logger.entity(name).error("Info handler failed: {{error.message}}", {
|
|
5570
|
-
params: { error: err }
|
|
5571
|
-
});
|
|
5572
|
-
this.lifecycleEvents.componentInfoFailed(name, err);
|
|
5573
|
-
results.push({
|
|
5574
|
-
name,
|
|
5575
|
-
called: true,
|
|
5576
|
-
error: err,
|
|
5577
|
-
timedOut: false,
|
|
5578
|
-
code: "error"
|
|
5579
|
-
});
|
|
5580
|
-
} finally {
|
|
5581
|
-
if (timeoutHandle) {
|
|
5582
|
-
clearTimeout(timeoutHandle);
|
|
5583
|
-
}
|
|
5584
|
-
}
|
|
5585
|
-
}
|
|
5586
|
-
const calledResults = results.filter((result) => result.called);
|
|
5587
|
-
const hasError = calledResults.some((result) => result.error);
|
|
5588
|
-
const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
|
|
5589
|
-
const hasTimeout = calledResults.some((result) => result.timedOut);
|
|
5590
|
-
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5591
|
-
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5592
|
-
return {
|
|
6337
|
+
return this.runSignalBroadcast({
|
|
5593
6338
|
signal: "info",
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
6339
|
+
pickHandler: (component) => component.onInfo?.bind(component),
|
|
6340
|
+
startupLog: "Info during startup: only notifying already-started components",
|
|
6341
|
+
timeoutLog: "Info handler timed out",
|
|
6342
|
+
errorLog: "Info handler failed: {{error.message}}",
|
|
6343
|
+
emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
|
|
6344
|
+
emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
|
|
6345
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
|
|
6346
|
+
});
|
|
5598
6347
|
}
|
|
5599
6348
|
/**
|
|
5600
6349
|
* Broadcast debug signal to all running components.
|
|
@@ -5602,96 +6351,16 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5602
6351
|
* Continues on errors - collects all results.
|
|
5603
6352
|
*/
|
|
5604
6353
|
async broadcastDebug() {
|
|
5605
|
-
|
|
5606
|
-
const componentsToNotify = this.components.filter(
|
|
5607
|
-
(component) => this.runningComponents.has(component.getName())
|
|
5608
|
-
);
|
|
5609
|
-
if (this.isStarting) {
|
|
5610
|
-
this.logger.info(
|
|
5611
|
-
"Debug during startup: only notifying already-started components"
|
|
5612
|
-
);
|
|
5613
|
-
}
|
|
5614
|
-
for (const component of componentsToNotify) {
|
|
5615
|
-
const name = component.getName();
|
|
5616
|
-
if (!component.onDebug) {
|
|
5617
|
-
results.push({
|
|
5618
|
-
name,
|
|
5619
|
-
called: false,
|
|
5620
|
-
error: null,
|
|
5621
|
-
timedOut: false,
|
|
5622
|
-
code: "no_handler"
|
|
5623
|
-
});
|
|
5624
|
-
continue;
|
|
5625
|
-
}
|
|
5626
|
-
this.lifecycleEvents.componentDebugStarted(name);
|
|
5627
|
-
const timeoutMS = component.signalTimeoutMS;
|
|
5628
|
-
let timeoutHandle;
|
|
5629
|
-
const timeoutResult = { timedOut: true };
|
|
5630
|
-
try {
|
|
5631
|
-
const result = component.onDebug();
|
|
5632
|
-
const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
|
|
5633
|
-
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5634
|
-
handlerPromise,
|
|
5635
|
-
new Promise((resolve) => {
|
|
5636
|
-
timeoutHandle = setTimeout(() => {
|
|
5637
|
-
resolve(timeoutResult);
|
|
5638
|
-
}, timeoutMS);
|
|
5639
|
-
})
|
|
5640
|
-
]) : await handlerPromise;
|
|
5641
|
-
if (outcome === timeoutResult) {
|
|
5642
|
-
this.logger.entity(name).warn("Debug handler timed out", {
|
|
5643
|
-
params: { timeoutMS }
|
|
5644
|
-
});
|
|
5645
|
-
Promise.resolve(handlerPromise).catch(() => {
|
|
5646
|
-
});
|
|
5647
|
-
results.push({
|
|
5648
|
-
name,
|
|
5649
|
-
called: true,
|
|
5650
|
-
error: null,
|
|
5651
|
-
timedOut: true,
|
|
5652
|
-
code: "timeout"
|
|
5653
|
-
});
|
|
5654
|
-
} else {
|
|
5655
|
-
this.lifecycleEvents.componentDebugCompleted(name);
|
|
5656
|
-
results.push({
|
|
5657
|
-
name,
|
|
5658
|
-
called: true,
|
|
5659
|
-
error: null,
|
|
5660
|
-
timedOut: false,
|
|
5661
|
-
code: "called"
|
|
5662
|
-
});
|
|
5663
|
-
}
|
|
5664
|
-
} catch (error) {
|
|
5665
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
5666
|
-
this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
|
|
5667
|
-
params: { error: err }
|
|
5668
|
-
});
|
|
5669
|
-
this.lifecycleEvents.componentDebugFailed(name, err);
|
|
5670
|
-
results.push({
|
|
5671
|
-
name,
|
|
5672
|
-
called: true,
|
|
5673
|
-
error: err,
|
|
5674
|
-
timedOut: false,
|
|
5675
|
-
code: "error"
|
|
5676
|
-
});
|
|
5677
|
-
} finally {
|
|
5678
|
-
if (timeoutHandle) {
|
|
5679
|
-
clearTimeout(timeoutHandle);
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
}
|
|
5683
|
-
const calledResults = results.filter((result) => result.called);
|
|
5684
|
-
const hasError = calledResults.some((result) => result.error);
|
|
5685
|
-
const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
|
|
5686
|
-
const hasTimeout = calledResults.some((result) => result.timedOut);
|
|
5687
|
-
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5688
|
-
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5689
|
-
return {
|
|
6354
|
+
return this.runSignalBroadcast({
|
|
5690
6355
|
signal: "debug",
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
6356
|
+
pickHandler: (component) => component.onDebug?.bind(component),
|
|
6357
|
+
startupLog: "Debug during startup: only notifying already-started components",
|
|
6358
|
+
timeoutLog: "Debug handler timed out",
|
|
6359
|
+
errorLog: "Debug handler failed: {{error.message}}",
|
|
6360
|
+
emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
|
|
6361
|
+
emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
|
|
6362
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
|
|
6363
|
+
});
|
|
5695
6364
|
}
|
|
5696
6365
|
};
|
|
5697
6366
|
|
|
@@ -5717,6 +6386,10 @@ var BaseComponent = class {
|
|
|
5717
6386
|
name;
|
|
5718
6387
|
/** Reference to component-scoped lifecycle (set by manager when registered) */
|
|
5719
6388
|
lifecycle;
|
|
6389
|
+
/** @internal Set by LifecycleManager while the component is running. */
|
|
6390
|
+
_unexpectedStopHandler;
|
|
6391
|
+
/** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
|
|
6392
|
+
_unexpectedStopGeneration = 0;
|
|
5720
6393
|
/**
|
|
5721
6394
|
* Create a new component
|
|
5722
6395
|
*
|
|
@@ -5751,6 +6424,24 @@ var BaseComponent = class {
|
|
|
5751
6424
|
// Default if undefined/null/non-finite
|
|
5752
6425
|
);
|
|
5753
6426
|
}
|
|
6427
|
+
/** @internal Called by LifecycleManager after a successful start. */
|
|
6428
|
+
_setUnexpectedStopHandler(handler) {
|
|
6429
|
+
this._unexpectedStopGeneration += 1;
|
|
6430
|
+
this._unexpectedStopHandler = handler;
|
|
6431
|
+
const generation = this._unexpectedStopGeneration;
|
|
6432
|
+
this.reportUnexpectedStop = (error) => {
|
|
6433
|
+
if (this._unexpectedStopGeneration !== generation) {
|
|
6434
|
+
return false;
|
|
6435
|
+
}
|
|
6436
|
+
return this._unexpectedStopHandler?.(error) ?? false;
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
6439
|
+
/** @internal Called by LifecycleManager when stop begins or component is unregistered. */
|
|
6440
|
+
_clearUnexpectedStopHandler() {
|
|
6441
|
+
this._unexpectedStopGeneration += 1;
|
|
6442
|
+
this._unexpectedStopHandler = void 0;
|
|
6443
|
+
this.reportUnexpectedStop = () => false;
|
|
6444
|
+
}
|
|
5754
6445
|
/**
|
|
5755
6446
|
* Get component name
|
|
5756
6447
|
*/
|
|
@@ -5769,6 +6460,38 @@ var BaseComponent = class {
|
|
|
5769
6460
|
isOptional() {
|
|
5770
6461
|
return this.optional;
|
|
5771
6462
|
}
|
|
6463
|
+
/**
|
|
6464
|
+
* Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
|
|
6465
|
+
* successful start so captured references from older runs go stale.
|
|
6466
|
+
*/
|
|
6467
|
+
reportUnexpectedStop = () => false;
|
|
6468
|
+
/**
|
|
6469
|
+
* Get this component's own status from the manager's perspective.
|
|
6470
|
+
*
|
|
6471
|
+
* Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
|
|
6472
|
+
* needing to pass the name. Returns `undefined` if the component is not registered.
|
|
6473
|
+
*
|
|
6474
|
+
* Check `status?.state === 'running'` to test whether the component is currently running.
|
|
6475
|
+
*/
|
|
6476
|
+
getSelfStatus() {
|
|
6477
|
+
return this.lifecycle?.getComponentStatus(this.name);
|
|
6478
|
+
}
|
|
6479
|
+
/**
|
|
6480
|
+
* Capture a run-scoped unexpected-stop reporter for async listeners created during start().
|
|
6481
|
+
*
|
|
6482
|
+
* Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
|
|
6483
|
+
* once the component is stopped, unregistered, or restarted. This prevents stale listeners
|
|
6484
|
+
* from a previous run from stopping a newer run of the same component instance.
|
|
6485
|
+
*/
|
|
6486
|
+
getUnexpectedStopReporter() {
|
|
6487
|
+
const generation = this._unexpectedStopGeneration;
|
|
6488
|
+
return (error) => {
|
|
6489
|
+
if (this._unexpectedStopGeneration !== generation) {
|
|
6490
|
+
return false;
|
|
6491
|
+
}
|
|
6492
|
+
return this._unexpectedStopHandler?.(error) ?? false;
|
|
6493
|
+
};
|
|
6494
|
+
}
|
|
5772
6495
|
};
|
|
5773
6496
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5774
6497
|
0 && (module.exports = {
|