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
|
@@ -956,6 +956,9 @@ var ComponentLifecycle = class {
|
|
|
956
956
|
getSignalStatus() {
|
|
957
957
|
return this.manager.getSignalStatus();
|
|
958
958
|
}
|
|
959
|
+
getShutdownEscalationStatus() {
|
|
960
|
+
return this.manager.getShutdownEscalationStatus();
|
|
961
|
+
}
|
|
959
962
|
triggerReload() {
|
|
960
963
|
return this.manager.triggerReload();
|
|
961
964
|
}
|
|
@@ -1134,6 +1137,15 @@ var LifecycleManagerEvents = class {
|
|
|
1134
1137
|
lifecycleManagerShutdownCompleted(input) {
|
|
1135
1138
|
this.emit("lifecycle-manager:shutdown-completed", input);
|
|
1136
1139
|
}
|
|
1140
|
+
lifecycleManagerShutdownEscalationArmed(input) {
|
|
1141
|
+
this.emit("lifecycle-manager:shutdown-escalation-armed", input);
|
|
1142
|
+
}
|
|
1143
|
+
lifecycleManagerShutdownEscalationExpired(input) {
|
|
1144
|
+
this.emit("lifecycle-manager:shutdown-escalation-expired", input);
|
|
1145
|
+
}
|
|
1146
|
+
lifecycleManagerShutdownEscalationForced(input) {
|
|
1147
|
+
this.emit("lifecycle-manager:shutdown-escalation-forced", input);
|
|
1148
|
+
}
|
|
1137
1149
|
componentStarting(name) {
|
|
1138
1150
|
this.emit("component:starting", { name });
|
|
1139
1151
|
}
|
|
@@ -1201,6 +1213,16 @@ var LifecycleManagerEvents = class {
|
|
|
1201
1213
|
code: info?.code
|
|
1202
1214
|
});
|
|
1203
1215
|
}
|
|
1216
|
+
componentStalledResolved(name, stallInfo, stalledDurationMS) {
|
|
1217
|
+
this.emit("component:stalled-resolved", {
|
|
1218
|
+
name,
|
|
1219
|
+
stallInfo,
|
|
1220
|
+
stalledDurationMS
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
componentUnexpectedStop(name, error) {
|
|
1224
|
+
this.emit("component:unexpected-stop", { name, error });
|
|
1225
|
+
}
|
|
1204
1226
|
componentShutdownForceCompleted(name) {
|
|
1205
1227
|
this.emit("component:shutdown-force-completed", { name });
|
|
1206
1228
|
}
|
|
@@ -1210,8 +1232,8 @@ var LifecycleManagerEvents = class {
|
|
|
1210
1232
|
componentStartupRollback(name) {
|
|
1211
1233
|
this.emit("component:startup-rollback", { name });
|
|
1212
1234
|
}
|
|
1213
|
-
signalShutdown(method) {
|
|
1214
|
-
this.emit("signal:shutdown", { method });
|
|
1235
|
+
signalShutdown(method, isAlreadyShuttingDown = false) {
|
|
1236
|
+
this.emit("signal:shutdown", { method, isAlreadyShuttingDown });
|
|
1215
1237
|
}
|
|
1216
1238
|
signalReload() {
|
|
1217
1239
|
this.emit("signal:reload", void 0);
|
|
@@ -1387,6 +1409,25 @@ var lifecycleManagerErrCodes = {
|
|
|
1387
1409
|
StopTimeout: "StopTimeout"
|
|
1388
1410
|
};
|
|
1389
1411
|
|
|
1412
|
+
// src/lib/lifecycle-manager/constants.ts
|
|
1413
|
+
var LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS = "Cannot unregister during bulk operation";
|
|
1414
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND = "Component not found";
|
|
1415
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING = "Component not running";
|
|
1416
|
+
var LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED = "Component is stalled";
|
|
1417
|
+
var LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS = "Bulk startup in progress";
|
|
1418
|
+
var LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS = "Shutdown in progress";
|
|
1419
|
+
var LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR = "Unknown error";
|
|
1420
|
+
var LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP = "Auto-detaching process signals on last component stop";
|
|
1421
|
+
var LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN = "Logger exit called during shutdown, waiting...";
|
|
1422
|
+
var LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED = "Message handler failed: {{error.message}}";
|
|
1423
|
+
var LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT = "Graceful shutdown timed out";
|
|
1424
|
+
var LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT = "Force shutdown timed out";
|
|
1425
|
+
var LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Optional component stopped unexpectedly during startup, continuing: {{error.message}}";
|
|
1426
|
+
var LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP = "Required component stopped unexpectedly during startup: {{error.message}}";
|
|
1427
|
+
var LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS = "Cannot register component while shutdown is in progress (isShuttingDown=true).";
|
|
1428
|
+
var LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP = "Cannot register component during startup when it is a required dependency for other components.";
|
|
1429
|
+
var LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE = "Component instance is already registered.";
|
|
1430
|
+
|
|
1390
1431
|
// src/lib/process-signal-manager.ts
|
|
1391
1432
|
import { ulid } from "ulid";
|
|
1392
1433
|
import readline from "readline";
|
|
@@ -1921,6 +1962,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1921
1962
|
attachSignalsBeforeStartup;
|
|
1922
1963
|
attachSignalsOnStart;
|
|
1923
1964
|
detachSignalsOnStop;
|
|
1965
|
+
repeatedShutdownRequestPolicy;
|
|
1924
1966
|
// Component management
|
|
1925
1967
|
components = [];
|
|
1926
1968
|
runningComponents = /* @__PURE__ */ new Set();
|
|
@@ -1930,8 +1972,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1930
1972
|
componentTimestamps = /* @__PURE__ */ new Map();
|
|
1931
1973
|
componentErrors = /* @__PURE__ */ new Map();
|
|
1932
1974
|
componentStartAttemptTokens = /* @__PURE__ */ new Map();
|
|
1975
|
+
// Use per-stop ULIDs instead of incrementing counters because a stalled
|
|
1976
|
+
// component can be unregistered and replaced by a same-name instance before
|
|
1977
|
+
// the old floating stop promise settles.
|
|
1978
|
+
componentStopAttemptTokens = /* @__PURE__ */ new Map();
|
|
1979
|
+
pendingForceStopWaiters = /* @__PURE__ */ new Map();
|
|
1980
|
+
unexpectedStopsDuringStartup = /* @__PURE__ */ new Map();
|
|
1933
1981
|
// State flags
|
|
1934
1982
|
isStarting = false;
|
|
1983
|
+
autoAttachedSignalsDuringStartup = false;
|
|
1935
1984
|
isStarted = false;
|
|
1936
1985
|
isShuttingDown = false;
|
|
1937
1986
|
// Unique token used to detect shutdowns that happened during async start().
|
|
@@ -1940,6 +1989,17 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1940
1989
|
pendingLoggerExitResolve = null;
|
|
1941
1990
|
shutdownMethod = null;
|
|
1942
1991
|
lastShutdownResult = null;
|
|
1992
|
+
repeatedShutdownExpiryTimer = null;
|
|
1993
|
+
repeatedShutdownRequestState = {
|
|
1994
|
+
requestCount: 0,
|
|
1995
|
+
firstMethod: null,
|
|
1996
|
+
latestMethod: null,
|
|
1997
|
+
firstRequestAt: null,
|
|
1998
|
+
latestRequestAt: null,
|
|
1999
|
+
repeatedWindowStartedAt: null,
|
|
2000
|
+
hasTriggeredForceShutdown: false,
|
|
2001
|
+
remainsArmedUntil: null
|
|
2002
|
+
};
|
|
1943
2003
|
// Signal management
|
|
1944
2004
|
processSignalManager = null;
|
|
1945
2005
|
onReloadRequested;
|
|
@@ -1966,6 +2026,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
1966
2026
|
this.attachSignalsBeforeStartup = options.attachSignalsBeforeStartup ?? false;
|
|
1967
2027
|
this.attachSignalsOnStart = options.attachSignalsOnStart ?? false;
|
|
1968
2028
|
this.detachSignalsOnStop = options.detachSignalsOnStop ?? false;
|
|
2029
|
+
const repeatedShutdownRequestPolicy = options.repeatedShutdownRequestPolicy;
|
|
2030
|
+
if (repeatedShutdownRequestPolicy === void 0) {
|
|
2031
|
+
this.repeatedShutdownRequestPolicy = void 0;
|
|
2032
|
+
} else {
|
|
2033
|
+
const hasFiniteExplicitArmedAfterFailureMS = Number.isFinite(
|
|
2034
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS
|
|
2035
|
+
);
|
|
2036
|
+
const forceAfterCount = finiteClampMin(
|
|
2037
|
+
repeatedShutdownRequestPolicy.forceAfterCount,
|
|
2038
|
+
1,
|
|
2039
|
+
3
|
|
2040
|
+
);
|
|
2041
|
+
const withinMS = finiteClampMin(
|
|
2042
|
+
repeatedShutdownRequestPolicy.withinMS,
|
|
2043
|
+
0,
|
|
2044
|
+
2e3
|
|
2045
|
+
);
|
|
2046
|
+
const armedAfterFailureMS = finiteClampMin(
|
|
2047
|
+
repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
2048
|
+
0,
|
|
2049
|
+
withinMS * forceAfterCount
|
|
2050
|
+
);
|
|
2051
|
+
this.repeatedShutdownRequestPolicy = {
|
|
2052
|
+
forceAfterCount,
|
|
2053
|
+
withinMS,
|
|
2054
|
+
armedAfterFailureMS,
|
|
2055
|
+
countManualRetriesTowardEscalation: repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation ?? false,
|
|
2056
|
+
hasExplicitArmedAfterFailureMS: hasFiniteExplicitArmedAfterFailureMS,
|
|
2057
|
+
onForceShutdown: repeatedShutdownRequestPolicy.onForceShutdown
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
1969
2060
|
this.onReloadRequested = options.onReloadRequested;
|
|
1970
2061
|
this.onInfoRequested = options.onInfoRequested;
|
|
1971
2062
|
this.onDebugRequested = options.onDebugRequested;
|
|
@@ -2038,7 +2129,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2038
2129
|
*/
|
|
2039
2130
|
async unregisterComponent(name, options) {
|
|
2040
2131
|
if (this.isStarting || this.isShuttingDown) {
|
|
2041
|
-
this.logger.entity(name).warn(
|
|
2132
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS, {
|
|
2042
2133
|
params: {
|
|
2043
2134
|
isStarting: this.isStarting,
|
|
2044
2135
|
isShuttingDown: this.isShuttingDown
|
|
@@ -2047,7 +2138,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2047
2138
|
return {
|
|
2048
2139
|
success: false,
|
|
2049
2140
|
componentName: name,
|
|
2050
|
-
reason:
|
|
2141
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_OPERATION_IN_PROGRESS,
|
|
2051
2142
|
code: "bulk_operation_in_progress",
|
|
2052
2143
|
wasStopped: false,
|
|
2053
2144
|
wasRegistered: this.hasComponent(name)
|
|
@@ -2055,11 +2146,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2055
2146
|
}
|
|
2056
2147
|
const component = this.getComponent(name);
|
|
2057
2148
|
if (!component) {
|
|
2058
|
-
this.logger.entity(name).warn(
|
|
2149
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND);
|
|
2059
2150
|
return {
|
|
2060
2151
|
success: false,
|
|
2061
2152
|
componentName: name,
|
|
2062
|
-
reason:
|
|
2153
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
2063
2154
|
code: "component_not_found",
|
|
2064
2155
|
wasStopped: false,
|
|
2065
2156
|
wasRegistered: false
|
|
@@ -2072,7 +2163,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2072
2163
|
return {
|
|
2073
2164
|
success: false,
|
|
2074
2165
|
componentName: name,
|
|
2075
|
-
reason:
|
|
2166
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
2076
2167
|
code: "stop_failed",
|
|
2077
2168
|
stopFailureReason: "stalled",
|
|
2078
2169
|
wasStopped: false,
|
|
@@ -2124,10 +2215,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2124
2215
|
wasStopped = true;
|
|
2125
2216
|
}
|
|
2126
2217
|
this.components = this.components.filter((c) => c.getName() !== name);
|
|
2218
|
+
component._clearUnexpectedStopHandler();
|
|
2127
2219
|
this.componentStates.delete(name);
|
|
2128
2220
|
this.componentTimestamps.delete(name);
|
|
2129
2221
|
this.componentErrors.delete(name);
|
|
2130
2222
|
this.componentStartAttemptTokens.delete(name);
|
|
2223
|
+
this.componentStopAttemptTokens.delete(name);
|
|
2224
|
+
this.pendingForceStopWaiters.delete(name);
|
|
2131
2225
|
this.stalledComponents.delete(name);
|
|
2132
2226
|
this.runningComponents.delete(name);
|
|
2133
2227
|
this.updateStartedFlag();
|
|
@@ -2474,7 +2568,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2474
2568
|
startedComponents: [],
|
|
2475
2569
|
failedOptionalComponents: [],
|
|
2476
2570
|
skippedDueToDependency: [],
|
|
2477
|
-
reason:
|
|
2571
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2478
2572
|
code: "shutdown_in_progress",
|
|
2479
2573
|
durationMS: Date.now() - startTime
|
|
2480
2574
|
};
|
|
@@ -2534,6 +2628,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2534
2628
|
};
|
|
2535
2629
|
}
|
|
2536
2630
|
this.isStarting = true;
|
|
2631
|
+
this.autoAttachedSignalsDuringStartup = false;
|
|
2632
|
+
this.unexpectedStopsDuringStartup.clear();
|
|
2633
|
+
this.resetRepeatedShutdownRequestState();
|
|
2537
2634
|
this.shutdownMethod = null;
|
|
2538
2635
|
this.lastShutdownResult = null;
|
|
2539
2636
|
const didAutoAttachSignalsForBulkStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("bulk startup") : false;
|
|
@@ -2665,13 +2762,49 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2665
2762
|
error: result.error,
|
|
2666
2763
|
durationMS: Date.now() - startTime
|
|
2667
2764
|
};
|
|
2765
|
+
} else if (result.code === "component_unexpected_stop") {
|
|
2766
|
+
this.unexpectedStopsDuringStartup.delete(name);
|
|
2767
|
+
const error = result.error || new Error(
|
|
2768
|
+
result.reason || `Component "${name}" stopped unexpectedly`
|
|
2769
|
+
);
|
|
2770
|
+
if (component.isOptional()) {
|
|
2771
|
+
if (!failedOptionalComponents.some((entry) => entry.name === name)) {
|
|
2772
|
+
failedOptionalComponents.push({ name, error });
|
|
2773
|
+
}
|
|
2774
|
+
this.logger.entity(name).warn(
|
|
2775
|
+
LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
2776
|
+
{
|
|
2777
|
+
params: { error }
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
} else {
|
|
2781
|
+
this.logger.entity(name).error(
|
|
2782
|
+
LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
2783
|
+
{
|
|
2784
|
+
params: { error }
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
await this.rollbackStartup(startedComponents);
|
|
2788
|
+
return {
|
|
2789
|
+
success: false,
|
|
2790
|
+
startedComponents: [],
|
|
2791
|
+
failedOptionalComponents,
|
|
2792
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2793
|
+
reason: error.message,
|
|
2794
|
+
code: "component_unexpected_stop",
|
|
2795
|
+
error,
|
|
2796
|
+
durationMS: Date.now() - startTime
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2668
2799
|
} else {
|
|
2669
2800
|
if (component.isOptional()) {
|
|
2670
2801
|
this.logger.entity(name).warn(
|
|
2671
2802
|
"Optional component failed to start, continuing: {{error.message}}",
|
|
2672
2803
|
{
|
|
2673
2804
|
params: {
|
|
2674
|
-
error: result.error || new Error(
|
|
2805
|
+
error: result.error || new Error(
|
|
2806
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2807
|
+
)
|
|
2675
2808
|
}
|
|
2676
2809
|
}
|
|
2677
2810
|
);
|
|
@@ -2685,14 +2818,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2685
2818
|
}
|
|
2686
2819
|
failedOptionalComponents.push({
|
|
2687
2820
|
name,
|
|
2688
|
-
error: result.error || new Error(
|
|
2821
|
+
error: result.error || new Error(
|
|
2822
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2823
|
+
)
|
|
2689
2824
|
});
|
|
2690
2825
|
} else {
|
|
2691
2826
|
this.logger.entity(name).error(
|
|
2692
2827
|
"Required component failed to start, rolling back: {{error.message}}",
|
|
2693
2828
|
{
|
|
2694
2829
|
params: {
|
|
2695
|
-
error: result.error || new Error(
|
|
2830
|
+
error: result.error || new Error(
|
|
2831
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
2832
|
+
)
|
|
2696
2833
|
}
|
|
2697
2834
|
}
|
|
2698
2835
|
);
|
|
@@ -2709,6 +2846,25 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2709
2846
|
};
|
|
2710
2847
|
}
|
|
2711
2848
|
}
|
|
2849
|
+
const unexpectedStopResult2 = this.consumeUnexpectedStopsDuringStartup(
|
|
2850
|
+
startedComponents,
|
|
2851
|
+
failedOptionalComponents
|
|
2852
|
+
);
|
|
2853
|
+
startedComponents.splice(0, startedComponents.length);
|
|
2854
|
+
startedComponents.push(...unexpectedStopResult2.startedComponents);
|
|
2855
|
+
if (unexpectedStopResult2.requiredFailure) {
|
|
2856
|
+
await this.rollbackStartup(startedComponents);
|
|
2857
|
+
return {
|
|
2858
|
+
success: false,
|
|
2859
|
+
startedComponents: [],
|
|
2860
|
+
failedOptionalComponents,
|
|
2861
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2862
|
+
reason: unexpectedStopResult2.requiredFailure.error.message,
|
|
2863
|
+
code: "component_unexpected_stop",
|
|
2864
|
+
error: unexpectedStopResult2.requiredFailure.error,
|
|
2865
|
+
durationMS: Date.now() - startTime
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2712
2868
|
}
|
|
2713
2869
|
if (hasTimedOut) {
|
|
2714
2870
|
const durationMS2 = Date.now() - startTime;
|
|
@@ -2732,6 +2888,25 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2732
2888
|
code: "startup_timeout"
|
|
2733
2889
|
};
|
|
2734
2890
|
}
|
|
2891
|
+
const unexpectedStopResult = this.consumeUnexpectedStopsDuringStartup(
|
|
2892
|
+
startedComponents,
|
|
2893
|
+
failedOptionalComponents
|
|
2894
|
+
);
|
|
2895
|
+
startedComponents.splice(0, startedComponents.length);
|
|
2896
|
+
startedComponents.push(...unexpectedStopResult.startedComponents);
|
|
2897
|
+
if (unexpectedStopResult.requiredFailure) {
|
|
2898
|
+
await this.rollbackStartup(startedComponents);
|
|
2899
|
+
return {
|
|
2900
|
+
success: false,
|
|
2901
|
+
startedComponents: [],
|
|
2902
|
+
failedOptionalComponents,
|
|
2903
|
+
skippedDueToDependency: Array.from(skippedDueToDependency),
|
|
2904
|
+
reason: unexpectedStopResult.requiredFailure.error.message,
|
|
2905
|
+
code: "component_unexpected_stop",
|
|
2906
|
+
error: unexpectedStopResult.requiredFailure.error,
|
|
2907
|
+
durationMS: Date.now() - startTime
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2735
2910
|
this.updateStartedFlag();
|
|
2736
2911
|
const skippedComponentsArray = [
|
|
2737
2912
|
...Array.from(skippedDueToDependency),
|
|
@@ -2763,10 +2938,12 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2763
2938
|
if (timeoutHandle) {
|
|
2764
2939
|
clearTimeout(timeoutHandle);
|
|
2765
2940
|
}
|
|
2766
|
-
|
|
2941
|
+
this.isStarting = false;
|
|
2942
|
+
if (didAutoAttachSignalsForBulkStartup || this.autoAttachedSignalsDuringStartup) {
|
|
2767
2943
|
this.autoDetachSignalsIfIdle("failed bulk startup");
|
|
2768
2944
|
}
|
|
2769
|
-
this.
|
|
2945
|
+
this.autoAttachedSignalsDuringStartup = false;
|
|
2946
|
+
this.unexpectedStopsDuringStartup.clear();
|
|
2770
2947
|
}
|
|
2771
2948
|
}
|
|
2772
2949
|
/**
|
|
@@ -2830,7 +3007,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2830
3007
|
return {
|
|
2831
3008
|
success: false,
|
|
2832
3009
|
componentName: name,
|
|
2833
|
-
reason:
|
|
3010
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
|
|
2834
3011
|
code: "startup_in_progress"
|
|
2835
3012
|
};
|
|
2836
3013
|
}
|
|
@@ -2841,7 +3018,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2841
3018
|
return {
|
|
2842
3019
|
success: false,
|
|
2843
3020
|
componentName: name,
|
|
2844
|
-
reason:
|
|
3021
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2845
3022
|
code: "shutdown_in_progress"
|
|
2846
3023
|
};
|
|
2847
3024
|
}
|
|
@@ -2875,7 +3052,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2875
3052
|
return {
|
|
2876
3053
|
success: false,
|
|
2877
3054
|
componentName: name,
|
|
2878
|
-
reason: this.isStarting ?
|
|
3055
|
+
reason: this.isStarting ? LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS : LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
2879
3056
|
code: this.isStarting ? "startup_in_progress" : "shutdown_in_progress"
|
|
2880
3057
|
};
|
|
2881
3058
|
}
|
|
@@ -2974,6 +3151,51 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
2974
3151
|
shutdownMethod: this.shutdownMethod
|
|
2975
3152
|
};
|
|
2976
3153
|
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Get status information about repeated shutdown escalation configuration and runtime state.
|
|
3156
|
+
*/
|
|
3157
|
+
getShutdownEscalationStatus() {
|
|
3158
|
+
if (this.repeatedShutdownRequestPolicy === void 0) {
|
|
3159
|
+
return {
|
|
3160
|
+
configured: false,
|
|
3161
|
+
isShuttingDown: this.isShuttingDown,
|
|
3162
|
+
isArmed: false,
|
|
3163
|
+
forceAfterCount: null,
|
|
3164
|
+
withinMS: null,
|
|
3165
|
+
armedAfterFailureMS: null,
|
|
3166
|
+
armedAfterFailureMSSource: null,
|
|
3167
|
+
requestCount: 0,
|
|
3168
|
+
firstMethod: null,
|
|
3169
|
+
latestMethod: null,
|
|
3170
|
+
firstRequestAt: null,
|
|
3171
|
+
latestRequestAt: null,
|
|
3172
|
+
repeatedWindowStartedAt: null,
|
|
3173
|
+
armedUntil: null,
|
|
3174
|
+
hasTriggeredForceShutdown: false
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
3178
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
3179
|
+
const isArmed = armedUntil !== null;
|
|
3180
|
+
return {
|
|
3181
|
+
configured: true,
|
|
3182
|
+
isShuttingDown: this.isShuttingDown,
|
|
3183
|
+
isArmed,
|
|
3184
|
+
forceAfterCount: this.repeatedShutdownRequestPolicy.forceAfterCount,
|
|
3185
|
+
withinMS: this.repeatedShutdownRequestPolicy.withinMS,
|
|
3186
|
+
armedAfterFailureMS: this.repeatedShutdownRequestPolicy.armedAfterFailureMS,
|
|
3187
|
+
armedAfterFailureMSSource: this.repeatedShutdownRequestPolicy.hasExplicitArmedAfterFailureMS ? "explicit" : "derived",
|
|
3188
|
+
countManualRetriesTowardEscalation: this.repeatedShutdownRequestPolicy.countManualRetriesTowardEscalation,
|
|
3189
|
+
requestCount: this.repeatedShutdownRequestState.requestCount,
|
|
3190
|
+
firstMethod: this.repeatedShutdownRequestState.firstMethod,
|
|
3191
|
+
latestMethod: this.repeatedShutdownRequestState.latestMethod,
|
|
3192
|
+
firstRequestAt: this.repeatedShutdownRequestState.firstRequestAt,
|
|
3193
|
+
latestRequestAt: this.repeatedShutdownRequestState.latestRequestAt,
|
|
3194
|
+
repeatedWindowStartedAt: this.repeatedShutdownRequestState.repeatedWindowStartedAt,
|
|
3195
|
+
armedUntil: isArmed ? armedUntil : null,
|
|
3196
|
+
hasTriggeredForceShutdown: this.repeatedShutdownRequestState.hasTriggeredForceShutdown
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
2977
3199
|
/**
|
|
2978
3200
|
* Enable Logger exit hook integration
|
|
2979
3201
|
*
|
|
@@ -3012,7 +3234,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3012
3234
|
if (this.isShuttingDown) {
|
|
3013
3235
|
if (isFirstExit && this.pendingLoggerExitResolve === null) {
|
|
3014
3236
|
this.logger.debug(
|
|
3015
|
-
|
|
3237
|
+
LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN,
|
|
3016
3238
|
{
|
|
3017
3239
|
params: { exitCode }
|
|
3018
3240
|
}
|
|
@@ -3021,7 +3243,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3021
3243
|
this.pendingLoggerExitResolve = resolve;
|
|
3022
3244
|
});
|
|
3023
3245
|
}
|
|
3024
|
-
this.logger.debug(
|
|
3246
|
+
this.logger.debug(LIFECYCLE_MANAGER_LOG_LOGGER_EXIT_DURING_SHUTDOWN, {
|
|
3025
3247
|
params: { exitCode }
|
|
3026
3248
|
});
|
|
3027
3249
|
return { action: "wait" };
|
|
@@ -3111,7 +3333,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3111
3333
|
return {
|
|
3112
3334
|
name,
|
|
3113
3335
|
healthy: false,
|
|
3114
|
-
message:
|
|
3336
|
+
message: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
3115
3337
|
checkedAt: startTime,
|
|
3116
3338
|
durationMS: 0,
|
|
3117
3339
|
error: null,
|
|
@@ -3124,7 +3346,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3124
3346
|
return {
|
|
3125
3347
|
name,
|
|
3126
3348
|
healthy: false,
|
|
3127
|
-
message: isStalled ?
|
|
3349
|
+
message: isStalled ? LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED : LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
3128
3350
|
checkedAt: startTime,
|
|
3129
3351
|
durationMS: Date.now() - startTime,
|
|
3130
3352
|
error: null,
|
|
@@ -3340,7 +3562,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3340
3562
|
result = component.onMessage(payload, from);
|
|
3341
3563
|
} catch (error) {
|
|
3342
3564
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3343
|
-
this.logger.entity(componentName).error(
|
|
3565
|
+
this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
|
|
3344
3566
|
params: { error: err, from }
|
|
3345
3567
|
});
|
|
3346
3568
|
this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
|
|
@@ -3400,7 +3622,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3400
3622
|
};
|
|
3401
3623
|
} catch (error) {
|
|
3402
3624
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3403
|
-
this.logger.entity(componentName).error(
|
|
3625
|
+
this.logger.entity(componentName).error(LIFECYCLE_MANAGER_LOG_MESSAGE_HANDLER_FAILED, {
|
|
3404
3626
|
params: { error: err, from, timeoutMS }
|
|
3405
3627
|
});
|
|
3406
3628
|
this.lifecycleEvents.componentMessageFailed(componentName, from, err, {
|
|
@@ -3673,7 +3895,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3673
3895
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3674
3896
|
name: componentName,
|
|
3675
3897
|
reason: "shutdown_in_progress",
|
|
3676
|
-
message:
|
|
3898
|
+
message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
|
|
3677
3899
|
registrationIndexBefore,
|
|
3678
3900
|
registrationIndexAfter: registrationIndexBefore,
|
|
3679
3901
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3685,7 +3907,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3685
3907
|
targetComponentName,
|
|
3686
3908
|
registrationIndexBefore,
|
|
3687
3909
|
code: "shutdown_in_progress",
|
|
3688
|
-
reason:
|
|
3910
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_SHUTDOWN_IN_PROGRESS,
|
|
3689
3911
|
targetFound: void 0
|
|
3690
3912
|
});
|
|
3691
3913
|
}
|
|
@@ -3696,7 +3918,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3696
3918
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3697
3919
|
name: componentName,
|
|
3698
3920
|
reason: "startup_in_progress",
|
|
3699
|
-
message:
|
|
3921
|
+
message: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
|
|
3700
3922
|
registrationIndexBefore,
|
|
3701
3923
|
registrationIndexAfter: registrationIndexBefore,
|
|
3702
3924
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3708,7 +3930,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3708
3930
|
targetComponentName,
|
|
3709
3931
|
registrationIndexBefore,
|
|
3710
3932
|
code: "startup_in_progress",
|
|
3711
|
-
reason:
|
|
3933
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_REGISTER_REQUIRED_DEPENDENCY_DURING_STARTUP,
|
|
3712
3934
|
targetFound: void 0
|
|
3713
3935
|
});
|
|
3714
3936
|
}
|
|
@@ -3717,7 +3939,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3717
3939
|
this.lifecycleEvents.componentRegistrationRejected({
|
|
3718
3940
|
name: componentName,
|
|
3719
3941
|
reason: "duplicate_instance",
|
|
3720
|
-
message:
|
|
3942
|
+
message: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
|
|
3721
3943
|
registrationIndexBefore,
|
|
3722
3944
|
registrationIndexAfter: registrationIndexBefore,
|
|
3723
3945
|
requestedPosition: isInsertAction ? { position, targetComponentName } : void 0,
|
|
@@ -3729,7 +3951,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3729
3951
|
targetComponentName,
|
|
3730
3952
|
registrationIndexBefore,
|
|
3731
3953
|
code: "duplicate_instance",
|
|
3732
|
-
reason:
|
|
3954
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_DUPLICATE_COMPONENT_INSTANCE,
|
|
3733
3955
|
targetFound: void 0
|
|
3734
3956
|
});
|
|
3735
3957
|
}
|
|
@@ -3999,9 +4221,26 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
3999
4221
|
code: "already_in_progress"
|
|
4000
4222
|
};
|
|
4001
4223
|
}
|
|
4224
|
+
this.normalizeRepeatedShutdownRequestStateArmedStatus();
|
|
4225
|
+
const repeatedShutdownPolicy = this.repeatedShutdownRequestPolicy;
|
|
4226
|
+
const isManualRetryWhileArmed = repeatedShutdownPolicy !== void 0 && method === "manual" && this.repeatedShutdownRequestState.firstRequestAt !== null && this.repeatedShutdownRequestState.remainsArmedUntil !== null;
|
|
4227
|
+
if (isManualRetryWhileArmed) {
|
|
4228
|
+
if (repeatedShutdownPolicy.countManualRetriesTowardEscalation) {
|
|
4229
|
+
this.handleRepeatedShutdownRequest(method);
|
|
4230
|
+
} else {
|
|
4231
|
+
this.resetRepeatedShutdownRequestState();
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
if (this.repeatedShutdownRequestState.remainsArmedUntil !== null) {
|
|
4235
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
4236
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = null;
|
|
4237
|
+
}
|
|
4002
4238
|
this.isShuttingDown = true;
|
|
4003
4239
|
this.shutdownToken = ulid2();
|
|
4004
4240
|
this.shutdownMethod = method;
|
|
4241
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt === null) {
|
|
4242
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
4243
|
+
}
|
|
4005
4244
|
const isDuringStartup = this.isStarting;
|
|
4006
4245
|
this.logger.info("Stopping all components", { params: { method } });
|
|
4007
4246
|
this.lifecycleEvents.lifecycleManagerShutdownInitiated(
|
|
@@ -4026,8 +4265,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4026
4265
|
const runningComponentsToStop = shutdownOrder.filter(
|
|
4027
4266
|
(name) => this.isComponentRunning(name) || shouldRetryStalled && stalledComponentNames.has(name)
|
|
4028
4267
|
);
|
|
4029
|
-
const stoppedComponents =
|
|
4030
|
-
const stalledComponents = [];
|
|
4268
|
+
const stoppedComponents = /* @__PURE__ */ new Set();
|
|
4031
4269
|
let hasTimedOut = false;
|
|
4032
4270
|
let timeoutHandle;
|
|
4033
4271
|
try {
|
|
@@ -4058,34 +4296,37 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4058
4296
|
this.logger.entity(name).info("Stopping component");
|
|
4059
4297
|
const isRunning = this.isComponentRunning(name);
|
|
4060
4298
|
const isStalled = stalledComponentNames.has(name);
|
|
4299
|
+
const currentState = this.componentStates.get(name);
|
|
4300
|
+
if (currentState === "stopped") {
|
|
4301
|
+
stoppedComponents.add(name);
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
4061
4304
|
const result2 = isRunning ? await this.stopComponentInternal(name) : shouldRetryStalled && isStalled ? await this.retryStalledComponent(name) : isStalled ? {
|
|
4062
4305
|
success: false,
|
|
4063
4306
|
componentName: name,
|
|
4064
|
-
reason:
|
|
4307
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4065
4308
|
code: "component_stalled",
|
|
4066
4309
|
status: this.getComponentStatus(name)
|
|
4067
4310
|
} : {
|
|
4068
4311
|
success: false,
|
|
4069
4312
|
componentName: name,
|
|
4070
|
-
reason:
|
|
4313
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4071
4314
|
code: "component_not_running",
|
|
4072
4315
|
status: this.getComponentStatus(name)
|
|
4073
4316
|
};
|
|
4074
4317
|
if (result2.success) {
|
|
4075
|
-
stoppedComponents.
|
|
4318
|
+
stoppedComponents.add(name);
|
|
4076
4319
|
} else {
|
|
4077
4320
|
this.logger.entity(name).error(
|
|
4078
4321
|
"Component failed to stop, continuing with others: {{error.message}}",
|
|
4079
4322
|
{
|
|
4080
4323
|
params: {
|
|
4081
|
-
error: result2.error || new Error(
|
|
4324
|
+
error: result2.error || new Error(
|
|
4325
|
+
result2.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
4326
|
+
)
|
|
4082
4327
|
}
|
|
4083
4328
|
}
|
|
4084
4329
|
);
|
|
4085
|
-
const stallInfo = this.stalledComponents.get(name);
|
|
4086
|
-
if (stallInfo) {
|
|
4087
|
-
stalledComponents.push(stallInfo);
|
|
4088
|
-
}
|
|
4089
4330
|
if (shouldHaltOnStall) {
|
|
4090
4331
|
this.logger.warn(
|
|
4091
4332
|
"Halting shutdown after stall (haltOnStall=true)",
|
|
@@ -4101,18 +4342,40 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4101
4342
|
} else {
|
|
4102
4343
|
await shutdownOperation();
|
|
4103
4344
|
}
|
|
4345
|
+
const finalStalledNames = /* @__PURE__ */ new Set();
|
|
4346
|
+
for (const name of runningComponentsToStop) {
|
|
4347
|
+
if (this.stalledComponents.has(name)) {
|
|
4348
|
+
finalStalledNames.add(name);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (!shouldRetryStalled) {
|
|
4352
|
+
for (const name of stalledComponentNames) {
|
|
4353
|
+
if (this.stalledComponents.has(name)) {
|
|
4354
|
+
finalStalledNames.add(name);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
for (const name of runningComponentsToStop) {
|
|
4359
|
+
if (!finalStalledNames.has(name) && this.componentStates.get(name) === "stopped") {
|
|
4360
|
+
stoppedComponents.add(name);
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
const stalledComponents = Array.from(finalStalledNames).map((name) => this.stalledComponents.get(name)).filter((stallInfo) => !!stallInfo);
|
|
4104
4364
|
const durationMS = Date.now() - startTime;
|
|
4105
4365
|
const isSuccess = !hasTimedOut && stalledComponents.length === 0;
|
|
4106
|
-
this.logger[isSuccess ? "success" : "warn"](
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4366
|
+
this.logger[isSuccess ? "success" : "warn"](
|
|
4367
|
+
isSuccess ? "Shutdown completed successfully" : "Shutdown attempt completed with stalled components or timeout",
|
|
4368
|
+
{
|
|
4369
|
+
params: {
|
|
4370
|
+
stopped: stoppedComponents.size,
|
|
4371
|
+
stalled: stalledComponents.length,
|
|
4372
|
+
durationMS
|
|
4373
|
+
}
|
|
4111
4374
|
}
|
|
4112
|
-
|
|
4375
|
+
);
|
|
4113
4376
|
const result = {
|
|
4114
4377
|
success: isSuccess,
|
|
4115
|
-
stoppedComponents,
|
|
4378
|
+
stoppedComponents: Array.from(stoppedComponents),
|
|
4116
4379
|
stalledComponents,
|
|
4117
4380
|
durationMS,
|
|
4118
4381
|
timedOut: hasTimedOut || void 0,
|
|
@@ -4127,6 +4390,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4127
4390
|
method,
|
|
4128
4391
|
duringStartup: isDuringStartup
|
|
4129
4392
|
});
|
|
4393
|
+
if (isSuccess) {
|
|
4394
|
+
this.resetRepeatedShutdownRequestState();
|
|
4395
|
+
} else {
|
|
4396
|
+
this.armRepeatedShutdownAfterFailure();
|
|
4397
|
+
}
|
|
4130
4398
|
return result;
|
|
4131
4399
|
} finally {
|
|
4132
4400
|
if (timeoutHandle) {
|
|
@@ -4158,7 +4426,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4158
4426
|
return {
|
|
4159
4427
|
success: false,
|
|
4160
4428
|
componentName: name,
|
|
4161
|
-
reason:
|
|
4429
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4162
4430
|
code: "component_not_found"
|
|
4163
4431
|
};
|
|
4164
4432
|
}
|
|
@@ -4169,12 +4437,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4169
4437
|
return {
|
|
4170
4438
|
success: false,
|
|
4171
4439
|
componentName: name,
|
|
4172
|
-
reason:
|
|
4440
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4173
4441
|
code: "component_not_running",
|
|
4174
4442
|
status: this.getComponentStatus(name)
|
|
4175
4443
|
};
|
|
4176
4444
|
}
|
|
4177
4445
|
this.logger.entity(name).warn("Retrying stalled component shutdown (force phase)");
|
|
4446
|
+
if (component.onShutdownForce) {
|
|
4447
|
+
this.issueStopAttemptToken(name);
|
|
4448
|
+
}
|
|
4178
4449
|
return this.shutdownComponentForce(name, component, {
|
|
4179
4450
|
gracefulPhaseRan: false,
|
|
4180
4451
|
gracefulTimedOut: false,
|
|
@@ -4194,7 +4465,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4194
4465
|
return {
|
|
4195
4466
|
success: false,
|
|
4196
4467
|
componentName: name,
|
|
4197
|
-
reason:
|
|
4468
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_SHUTDOWN_IN_PROGRESS,
|
|
4198
4469
|
code: "shutdown_in_progress"
|
|
4199
4470
|
};
|
|
4200
4471
|
}
|
|
@@ -4206,7 +4477,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4206
4477
|
return {
|
|
4207
4478
|
success: false,
|
|
4208
4479
|
componentName: name,
|
|
4209
|
-
reason:
|
|
4480
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_BULK_STARTUP_IN_PROGRESS,
|
|
4210
4481
|
code: "startup_in_progress"
|
|
4211
4482
|
};
|
|
4212
4483
|
}
|
|
@@ -4216,7 +4487,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4216
4487
|
return {
|
|
4217
4488
|
success: false,
|
|
4218
4489
|
componentName: name,
|
|
4219
|
-
reason:
|
|
4490
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4220
4491
|
code: "component_not_found"
|
|
4221
4492
|
};
|
|
4222
4493
|
}
|
|
@@ -4225,7 +4496,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4225
4496
|
return {
|
|
4226
4497
|
success: false,
|
|
4227
4498
|
componentName: name,
|
|
4228
|
-
reason:
|
|
4499
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4229
4500
|
code: "component_stalled",
|
|
4230
4501
|
status: this.getComponentStatus(name)
|
|
4231
4502
|
};
|
|
@@ -4289,6 +4560,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4289
4560
|
const timeoutMS = component.startupTimeoutMS;
|
|
4290
4561
|
const startAttemptToken = ulid2();
|
|
4291
4562
|
this.componentStartAttemptTokens.set(name, startAttemptToken);
|
|
4563
|
+
component._setUnexpectedStopHandler(
|
|
4564
|
+
(error) => this.handleComponentUnexpectedStop(name, startAttemptToken, error)
|
|
4565
|
+
);
|
|
4292
4566
|
const shutdownTokenAtStart = this.shutdownToken;
|
|
4293
4567
|
const didAutoAttachSignalsForComponentStartup = this.attachSignalsBeforeStartup ? this.autoAttachSignals("component startup") : false;
|
|
4294
4568
|
let timeoutHandle;
|
|
@@ -4331,6 +4605,18 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4331
4605
|
} else {
|
|
4332
4606
|
await startPromise;
|
|
4333
4607
|
}
|
|
4608
|
+
if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
4609
|
+
component._clearUnexpectedStopHandler();
|
|
4610
|
+
const error = this.componentErrors.get(name) ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
|
|
4611
|
+
return {
|
|
4612
|
+
success: false,
|
|
4613
|
+
componentName: name,
|
|
4614
|
+
reason: error.message,
|
|
4615
|
+
code: "component_unexpected_stop",
|
|
4616
|
+
error,
|
|
4617
|
+
status: this.getComponentStatus(name)
|
|
4618
|
+
};
|
|
4619
|
+
}
|
|
4334
4620
|
if (this.isShuttingDown || shutdownTokenAtStart !== this.shutdownToken) {
|
|
4335
4621
|
this.componentStates.set(name, "running");
|
|
4336
4622
|
this.runningComponents.add(name);
|
|
@@ -4358,6 +4644,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4358
4644
|
this.componentStates.set(name, "running");
|
|
4359
4645
|
this.runningComponents.add(name);
|
|
4360
4646
|
this.stalledComponents.delete(name);
|
|
4647
|
+
if (shouldForceStalled) {
|
|
4648
|
+
this.issueStopAttemptToken(name);
|
|
4649
|
+
}
|
|
4361
4650
|
this.updateStartedFlag();
|
|
4362
4651
|
if (this.attachSignalsOnStart && this.runningComponents.size === 1) {
|
|
4363
4652
|
this.autoAttachSignals("first component start");
|
|
@@ -4377,9 +4666,24 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4377
4666
|
status: this.getComponentStatus(name)
|
|
4378
4667
|
};
|
|
4379
4668
|
} catch (error) {
|
|
4669
|
+
component._clearUnexpectedStopHandler();
|
|
4380
4670
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4671
|
+
const isStartupTimeout = err instanceof ComponentStartTimeoutError && err.additionalInfo.componentName === name;
|
|
4672
|
+
const unexpectedStopError = this.componentErrors.get(name);
|
|
4673
|
+
if (this.componentStartAttemptTokens.get(name) === startAttemptToken && this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name) && (isStartupTimeout || unexpectedStopError instanceof Error)) {
|
|
4674
|
+
return {
|
|
4675
|
+
success: false,
|
|
4676
|
+
componentName: name,
|
|
4677
|
+
reason: unexpectedStopError?.message || `Component "${name}" stopped unexpectedly during startup`,
|
|
4678
|
+
code: "component_unexpected_stop",
|
|
4679
|
+
error: unexpectedStopError || new Error(
|
|
4680
|
+
`Component "${name}" stopped unexpectedly during startup`
|
|
4681
|
+
),
|
|
4682
|
+
status: this.getComponentStatus(name)
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4381
4685
|
this.componentErrors.set(name, err);
|
|
4382
|
-
if (
|
|
4686
|
+
if (isStartupTimeout) {
|
|
4383
4687
|
this.componentStates.set(name, "starting-timed-out");
|
|
4384
4688
|
this.logger.entity(name).error("Component startup timed out: {{error.message}}", {
|
|
4385
4689
|
params: { error: err }
|
|
@@ -4424,7 +4728,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4424
4728
|
return {
|
|
4425
4729
|
success: false,
|
|
4426
4730
|
componentName: name,
|
|
4427
|
-
reason:
|
|
4731
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_FOUND,
|
|
4428
4732
|
code: "component_not_found"
|
|
4429
4733
|
};
|
|
4430
4734
|
}
|
|
@@ -4432,7 +4736,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4432
4736
|
return {
|
|
4433
4737
|
success: false,
|
|
4434
4738
|
componentName: name,
|
|
4435
|
-
reason:
|
|
4739
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_STALLED,
|
|
4436
4740
|
code: "component_stalled",
|
|
4437
4741
|
status: this.getComponentStatus(name)
|
|
4438
4742
|
};
|
|
@@ -4441,7 +4745,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4441
4745
|
return {
|
|
4442
4746
|
success: false,
|
|
4443
4747
|
componentName: name,
|
|
4444
|
-
reason:
|
|
4748
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_COMPONENT_NOT_RUNNING,
|
|
4445
4749
|
code: "component_not_running",
|
|
4446
4750
|
status: this.getComponentStatus(name)
|
|
4447
4751
|
};
|
|
@@ -4457,6 +4761,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4457
4761
|
};
|
|
4458
4762
|
}
|
|
4459
4763
|
if (options?.forceImmediate) {
|
|
4764
|
+
if (component.onShutdownForce) {
|
|
4765
|
+
this.issueStopAttemptToken(name);
|
|
4766
|
+
}
|
|
4767
|
+
component._clearUnexpectedStopHandler();
|
|
4460
4768
|
return this.shutdownComponentForce(name, component, {
|
|
4461
4769
|
gracefulPhaseRan: false,
|
|
4462
4770
|
gracefulTimedOut: false,
|
|
@@ -4593,9 +4901,11 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4593
4901
|
* Calls stop() with timeout
|
|
4594
4902
|
*/
|
|
4595
4903
|
async shutdownComponentGraceful(name, component, options) {
|
|
4904
|
+
component._clearUnexpectedStopHandler();
|
|
4596
4905
|
this.componentStates.set(name, "stopping");
|
|
4597
4906
|
this.logger.entity(name).info("Graceful shutdown started");
|
|
4598
4907
|
this.lifecycleEvents.componentStopping(name);
|
|
4908
|
+
const stopAttemptToken = this.issueStopAttemptToken(name);
|
|
4599
4909
|
const timeoutMS = options?.timeout ?? component.shutdownGracefulTimeoutMS;
|
|
4600
4910
|
let timeoutHandle;
|
|
4601
4911
|
try {
|
|
@@ -4616,7 +4926,16 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4616
4926
|
);
|
|
4617
4927
|
}
|
|
4618
4928
|
}
|
|
4619
|
-
Promise.resolve(stopPromise).
|
|
4929
|
+
Promise.resolve(stopPromise).then(
|
|
4930
|
+
() => this.handleLateStopResolution(
|
|
4931
|
+
name,
|
|
4932
|
+
stopAttemptToken,
|
|
4933
|
+
"graceful"
|
|
4934
|
+
),
|
|
4935
|
+
() => {
|
|
4936
|
+
}
|
|
4937
|
+
// Intentionally ignore errors after timeout
|
|
4938
|
+
).catch(() => {
|
|
4620
4939
|
});
|
|
4621
4940
|
reject(
|
|
4622
4941
|
new ComponentStopTimeoutError({
|
|
@@ -4635,9 +4954,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4635
4954
|
this.stalledComponents.delete(name);
|
|
4636
4955
|
this.updateStartedFlag();
|
|
4637
4956
|
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
4638
|
-
this.logger.info(
|
|
4639
|
-
"Auto-detaching process signals on last component stop"
|
|
4640
|
-
);
|
|
4957
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
4641
4958
|
this.detachSignals();
|
|
4642
4959
|
}
|
|
4643
4960
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
@@ -4660,15 +4977,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4660
4977
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4661
4978
|
this.componentErrors.set(name, err);
|
|
4662
4979
|
if (err instanceof ComponentStopTimeoutError && err.additionalInfo.componentName === name) {
|
|
4663
|
-
this.logger.entity(name).warn(
|
|
4980
|
+
this.logger.entity(name).warn(LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT);
|
|
4664
4981
|
this.lifecycleEvents.componentStopTimeout(name, err, {
|
|
4665
4982
|
timeoutMS,
|
|
4666
|
-
reason:
|
|
4983
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT
|
|
4667
4984
|
});
|
|
4668
4985
|
return {
|
|
4669
4986
|
success: false,
|
|
4670
4987
|
componentName: name,
|
|
4671
|
-
reason:
|
|
4988
|
+
reason: LIFECYCLE_MANAGER_MESSAGE_GRACEFUL_SHUTDOWN_TIMED_OUT,
|
|
4672
4989
|
code: "component_shutdown_timeout",
|
|
4673
4990
|
error: err,
|
|
4674
4991
|
status: this.getComponentStatus(name)
|
|
@@ -4711,8 +5028,6 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4711
5028
|
gracefulTimedOut: context.gracefulTimedOut
|
|
4712
5029
|
}
|
|
4713
5030
|
});
|
|
4714
|
-
const timeoutMS = component.shutdownForceTimeoutMS;
|
|
4715
|
-
let timeoutHandle;
|
|
4716
5031
|
if (!component.onShutdownForce) {
|
|
4717
5032
|
const stallInfo = {
|
|
4718
5033
|
name,
|
|
@@ -4746,6 +5061,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4746
5061
|
status: this.getComponentStatus(name)
|
|
4747
5062
|
};
|
|
4748
5063
|
}
|
|
5064
|
+
const timeoutMS = component.shutdownForceTimeoutMS;
|
|
5065
|
+
const { promise: stoppedDuringForcePromise, cleanup: cleanupForceWaiter } = this.createPendingForceStopWaiter(name);
|
|
5066
|
+
let timeoutHandle;
|
|
4749
5067
|
try {
|
|
4750
5068
|
const forcePromise = component.onShutdownForce();
|
|
4751
5069
|
if (timeoutMS > 0) {
|
|
@@ -4764,23 +5082,44 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4764
5082
|
);
|
|
4765
5083
|
}
|
|
4766
5084
|
}
|
|
4767
|
-
|
|
5085
|
+
const forceAttemptToken = this.componentStopAttemptTokens.get(name) ?? ulid2();
|
|
5086
|
+
Promise.resolve(forcePromise).then(
|
|
5087
|
+
() => this.handleLateStopResolution(
|
|
5088
|
+
name,
|
|
5089
|
+
forceAttemptToken,
|
|
5090
|
+
"force"
|
|
5091
|
+
),
|
|
5092
|
+
() => {
|
|
5093
|
+
}
|
|
5094
|
+
// Intentionally ignore errors after timeout
|
|
5095
|
+
).catch(() => {
|
|
4768
5096
|
});
|
|
4769
|
-
reject(
|
|
5097
|
+
reject(
|
|
5098
|
+
new Error(LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT)
|
|
5099
|
+
);
|
|
4770
5100
|
}, timeoutMS);
|
|
4771
5101
|
});
|
|
4772
|
-
await Promise.race([
|
|
5102
|
+
await Promise.race([
|
|
5103
|
+
forcePromise,
|
|
5104
|
+
timeoutPromise,
|
|
5105
|
+
stoppedDuringForcePromise
|
|
5106
|
+
]);
|
|
4773
5107
|
} else {
|
|
4774
|
-
await forcePromise;
|
|
5108
|
+
await Promise.race([forcePromise, stoppedDuringForcePromise]);
|
|
5109
|
+
}
|
|
5110
|
+
if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
5111
|
+
return {
|
|
5112
|
+
success: true,
|
|
5113
|
+
componentName: name,
|
|
5114
|
+
status: this.getComponentStatus(name)
|
|
5115
|
+
};
|
|
4775
5116
|
}
|
|
4776
5117
|
this.componentStates.set(name, "stopped");
|
|
4777
5118
|
this.runningComponents.delete(name);
|
|
4778
5119
|
this.stalledComponents.delete(name);
|
|
4779
5120
|
this.updateStartedFlag();
|
|
4780
5121
|
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
4781
|
-
this.logger.info(
|
|
4782
|
-
"Auto-detaching process signals on last component stop"
|
|
4783
|
-
);
|
|
5122
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
4784
5123
|
this.detachSignals();
|
|
4785
5124
|
}
|
|
4786
5125
|
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
@@ -4801,8 +5140,15 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4801
5140
|
status: this.getComponentStatus(name)
|
|
4802
5141
|
};
|
|
4803
5142
|
} catch (error) {
|
|
5143
|
+
if (this.componentStates.get(name) === "stopped" && !this.runningComponents.has(name)) {
|
|
5144
|
+
return {
|
|
5145
|
+
success: true,
|
|
5146
|
+
componentName: name,
|
|
5147
|
+
status: this.getComponentStatus(name)
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
4804
5150
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
4805
|
-
const isTimeout = err.message ===
|
|
5151
|
+
const isTimeout = err.message === LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT;
|
|
4806
5152
|
const stallInfo = {
|
|
4807
5153
|
name,
|
|
4808
5154
|
phase: "force",
|
|
@@ -4833,12 +5179,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4833
5179
|
return {
|
|
4834
5180
|
success: false,
|
|
4835
5181
|
componentName: name,
|
|
4836
|
-
reason: isTimeout ?
|
|
5182
|
+
reason: isTimeout ? LIFECYCLE_MANAGER_MESSAGE_FORCE_SHUTDOWN_TIMED_OUT : err.message,
|
|
4837
5183
|
code: isTimeout ? "component_shutdown_timeout" : "unknown_error",
|
|
4838
5184
|
error: err,
|
|
4839
5185
|
status: this.getComponentStatus(name)
|
|
4840
5186
|
};
|
|
4841
5187
|
} finally {
|
|
5188
|
+
cleanupForceWaiter();
|
|
4842
5189
|
if (timeoutHandle) {
|
|
4843
5190
|
clearTimeout(timeoutHandle);
|
|
4844
5191
|
}
|
|
@@ -4915,7 +5262,9 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4915
5262
|
"Failed to stop component during rollback, continuing: {{error.message}}",
|
|
4916
5263
|
{
|
|
4917
5264
|
params: {
|
|
4918
|
-
error: result.error || new Error(
|
|
5265
|
+
error: result.error || new Error(
|
|
5266
|
+
result.reason || LIFECYCLE_MANAGER_MESSAGE_UNKNOWN_ERROR
|
|
5267
|
+
)
|
|
4919
5268
|
}
|
|
4920
5269
|
}
|
|
4921
5270
|
);
|
|
@@ -4929,10 +5278,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4929
5278
|
}
|
|
4930
5279
|
this.logger.info(`Auto-attaching process signals on ${trigger}`);
|
|
4931
5280
|
this.attachSignals();
|
|
5281
|
+
if (this.isStarting) {
|
|
5282
|
+
this.autoAttachedSignalsDuringStartup = true;
|
|
5283
|
+
}
|
|
4932
5284
|
return true;
|
|
4933
5285
|
}
|
|
4934
5286
|
autoDetachSignalsIfIdle(trigger) {
|
|
4935
|
-
if (!this.detachSignalsOnStop || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
5287
|
+
if (!this.detachSignalsOnStop || this.isStarting || this.runningComponents.size > 0 || !this.processSignalManager?.getStatus().isAttached) {
|
|
4936
5288
|
return;
|
|
4937
5289
|
}
|
|
4938
5290
|
this.logger.info(`Auto-detaching process signals after ${trigger}`);
|
|
@@ -4974,6 +5326,210 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
4974
5326
|
}).catch(() => {
|
|
4975
5327
|
});
|
|
4976
5328
|
}
|
|
5329
|
+
consumeUnexpectedStopsDuringStartup(startedComponents, failedOptionalComponents) {
|
|
5330
|
+
if (this.unexpectedStopsDuringStartup.size === 0) {
|
|
5331
|
+
return { startedComponents: [...startedComponents] };
|
|
5332
|
+
}
|
|
5333
|
+
const remainingStartedComponents = [];
|
|
5334
|
+
let requiredFailure;
|
|
5335
|
+
for (const name of startedComponents) {
|
|
5336
|
+
const startupStopError = this.unexpectedStopsDuringStartup.get(name);
|
|
5337
|
+
if (startupStopError === void 0) {
|
|
5338
|
+
remainingStartedComponents.push(name);
|
|
5339
|
+
continue;
|
|
5340
|
+
}
|
|
5341
|
+
this.unexpectedStopsDuringStartup.delete(name);
|
|
5342
|
+
const error = startupStopError ?? new Error(`Component "${name}" stopped unexpectedly during startup`);
|
|
5343
|
+
const component = this.getComponent(name);
|
|
5344
|
+
if (component?.isOptional()) {
|
|
5345
|
+
if (!failedOptionalComponents.some((entry) => entry.name === name)) {
|
|
5346
|
+
failedOptionalComponents.push({ name, error });
|
|
5347
|
+
}
|
|
5348
|
+
this.logger.entity(name).warn(
|
|
5349
|
+
LIFECYCLE_MANAGER_LOG_OPTIONAL_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
5350
|
+
{
|
|
5351
|
+
params: { error }
|
|
5352
|
+
}
|
|
5353
|
+
);
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
this.logger.entity(name).error(
|
|
5357
|
+
LIFECYCLE_MANAGER_LOG_REQUIRED_COMPONENT_UNEXPECTED_STOP_DURING_STARTUP,
|
|
5358
|
+
{
|
|
5359
|
+
params: { error }
|
|
5360
|
+
}
|
|
5361
|
+
);
|
|
5362
|
+
requiredFailure ??= { name, error };
|
|
5363
|
+
}
|
|
5364
|
+
return {
|
|
5365
|
+
startedComponents: remainingStartedComponents,
|
|
5366
|
+
requiredFailure
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5369
|
+
/**
|
|
5370
|
+
* Issues and returns a unique stop attempt token for a component.
|
|
5371
|
+
*
|
|
5372
|
+
* Each stop attempt (graceful or force-retry) gets a unique token.
|
|
5373
|
+
* The late-resolution handler captures this token in its closure so it can
|
|
5374
|
+
* skip any stall entries that were created by a *later* stop attempt — e.g. a
|
|
5375
|
+
* force-retry that also timed out after the original graceful promise floated
|
|
5376
|
+
* in the background.
|
|
5377
|
+
*/
|
|
5378
|
+
issueStopAttemptToken(name) {
|
|
5379
|
+
const next = ulid2();
|
|
5380
|
+
this.componentStopAttemptTokens.set(name, next);
|
|
5381
|
+
return next;
|
|
5382
|
+
}
|
|
5383
|
+
createPendingForceStopWaiter(name) {
|
|
5384
|
+
let isResolved = false;
|
|
5385
|
+
let waiters = this.pendingForceStopWaiters.get(name);
|
|
5386
|
+
if (!waiters) {
|
|
5387
|
+
waiters = /* @__PURE__ */ new Set();
|
|
5388
|
+
this.pendingForceStopWaiters.set(name, waiters);
|
|
5389
|
+
}
|
|
5390
|
+
let resolveWaiter;
|
|
5391
|
+
const promise = new Promise((resolve) => {
|
|
5392
|
+
resolveWaiter = () => {
|
|
5393
|
+
if (isResolved) {
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
isResolved = true;
|
|
5397
|
+
resolve();
|
|
5398
|
+
};
|
|
5399
|
+
});
|
|
5400
|
+
waiters.add(resolveWaiter);
|
|
5401
|
+
return {
|
|
5402
|
+
promise,
|
|
5403
|
+
cleanup: () => {
|
|
5404
|
+
const pending = this.pendingForceStopWaiters.get(name);
|
|
5405
|
+
if (!pending) {
|
|
5406
|
+
return;
|
|
5407
|
+
}
|
|
5408
|
+
pending.delete(resolveWaiter);
|
|
5409
|
+
if (pending.size === 0) {
|
|
5410
|
+
this.pendingForceStopWaiters.delete(name);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
};
|
|
5414
|
+
}
|
|
5415
|
+
resolvePendingForceStopWaiters(name) {
|
|
5416
|
+
const waiters = this.pendingForceStopWaiters.get(name);
|
|
5417
|
+
if (!waiters || waiters.size === 0) {
|
|
5418
|
+
return;
|
|
5419
|
+
}
|
|
5420
|
+
this.pendingForceStopWaiters.delete(name);
|
|
5421
|
+
for (const resolve of waiters) {
|
|
5422
|
+
resolve();
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
/**
|
|
5426
|
+
* Called when a stop promise eventually resolves after its timeout path already fired.
|
|
5427
|
+
*
|
|
5428
|
+
* Usually this means a previously stalled component's original stop() or
|
|
5429
|
+
* onShutdownForce() promise finally resolved, so the manager can clear the
|
|
5430
|
+
* stall and transition the component to stopped without a manual retry.
|
|
5431
|
+
*
|
|
5432
|
+
* There is one extra overlap case for graceful stop(): stop() can resolve
|
|
5433
|
+
* after the graceful timeout but before onShutdownForce() itself times out.
|
|
5434
|
+
* In that window no stall entry exists yet, but the component still finished
|
|
5435
|
+
* stopping cleanly, so we finalize it here and let the later force-timeout
|
|
5436
|
+
* path observe the already-stopped state and no-op. This overlap fix is
|
|
5437
|
+
* scoped to the same stop token and will not cross a later retry attempt.
|
|
5438
|
+
*
|
|
5439
|
+
* Two guards prevent stale floating promises from incorrectly clearing state:
|
|
5440
|
+
*
|
|
5441
|
+
* 1. token guard — if a newer stop attempt (e.g. a retryStalled
|
|
5442
|
+
* force-retry) has started since this promise was launched, its token
|
|
5443
|
+
* won't match and we bail out immediately.
|
|
5444
|
+
*
|
|
5445
|
+
* 2. state/stall guard — if the component was unregistered, restarted, or
|
|
5446
|
+
* already cleared by another path, there will be neither a matching stall
|
|
5447
|
+
* entry nor the force-phase overlap state, so we bail out.
|
|
5448
|
+
*/
|
|
5449
|
+
handleLateStopResolution(name, token, source) {
|
|
5450
|
+
if (this.componentStopAttemptTokens.get(name) !== token) {
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5453
|
+
const currentState = this.componentStates.get(name);
|
|
5454
|
+
const stallInfo = this.stalledComponents.get(name);
|
|
5455
|
+
const isCompletedDuringForcePhase = source === "graceful" && !stallInfo && currentState === "force-stopping";
|
|
5456
|
+
if (stallInfo && currentState !== "stalled") {
|
|
5457
|
+
this.stalledComponents.delete(name);
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
if (!stallInfo && !isCompletedDuringForcePhase) {
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
5463
|
+
const stalledDurationMS = stallInfo ? Date.now() - stallInfo.stalledAt : void 0;
|
|
5464
|
+
if (stallInfo) {
|
|
5465
|
+
this.stalledComponents.delete(name);
|
|
5466
|
+
}
|
|
5467
|
+
this.componentStates.set(name, "stopped");
|
|
5468
|
+
this.runningComponents.delete(name);
|
|
5469
|
+
this.componentErrors.set(name, null);
|
|
5470
|
+
this.updateStartedFlag();
|
|
5471
|
+
this.resolvePendingForceStopWaiters(name);
|
|
5472
|
+
if (this.detachSignalsOnStop && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
5473
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
5474
|
+
this.detachSignals();
|
|
5475
|
+
}
|
|
5476
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
5477
|
+
startedAt: null,
|
|
5478
|
+
stoppedAt: null
|
|
5479
|
+
};
|
|
5480
|
+
timestamps.stoppedAt = Date.now();
|
|
5481
|
+
this.componentTimestamps.set(name, timestamps);
|
|
5482
|
+
this.logger.entity(name).info(
|
|
5483
|
+
stallInfo ? "Stalled component completed stop late, stall cleared" : "Graceful stop completed after force phase started",
|
|
5484
|
+
stalledDurationMS ? { params: { stalledDurationMS } } : void 0
|
|
5485
|
+
);
|
|
5486
|
+
if (source === "force") {
|
|
5487
|
+
this.lifecycleEvents.componentShutdownForceCompleted(name);
|
|
5488
|
+
}
|
|
5489
|
+
if (stallInfo && stalledDurationMS !== void 0) {
|
|
5490
|
+
this.lifecycleEvents.componentStalledResolved(
|
|
5491
|
+
name,
|
|
5492
|
+
stallInfo,
|
|
5493
|
+
stalledDurationMS
|
|
5494
|
+
);
|
|
5495
|
+
}
|
|
5496
|
+
this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
|
|
5497
|
+
}
|
|
5498
|
+
handleComponentUnexpectedStop(name, startAttemptToken, error) {
|
|
5499
|
+
const currentState = this.componentStates.get(name);
|
|
5500
|
+
if (
|
|
5501
|
+
// Startup-time self-stops are valid too: start() may still be awaiting
|
|
5502
|
+
// some async work while an internal listener has already observed that
|
|
5503
|
+
// the component died and reported it.
|
|
5504
|
+
currentState !== "starting" && currentState !== "running" || this.componentStartAttemptTokens.get(name) !== startAttemptToken
|
|
5505
|
+
) {
|
|
5506
|
+
return false;
|
|
5507
|
+
}
|
|
5508
|
+
this.runningComponents.delete(name);
|
|
5509
|
+
this.componentStates.set(name, "stopped");
|
|
5510
|
+
this.componentErrors.set(name, error ?? null);
|
|
5511
|
+
if (this.isStarting) {
|
|
5512
|
+
this.unexpectedStopsDuringStartup.set(name, error ?? null);
|
|
5513
|
+
}
|
|
5514
|
+
this.updateStartedFlag();
|
|
5515
|
+
if (this.detachSignalsOnStop && !this.isStarting && this.runningComponents.size === 0 && this.processSignalManager) {
|
|
5516
|
+
this.logger.info(LIFECYCLE_MANAGER_LOG_AUTO_DETACH_LAST_COMPONENT_STOP);
|
|
5517
|
+
this.detachSignals();
|
|
5518
|
+
}
|
|
5519
|
+
const timestamps = this.componentTimestamps.get(name) ?? {
|
|
5520
|
+
startedAt: null,
|
|
5521
|
+
stoppedAt: null
|
|
5522
|
+
};
|
|
5523
|
+
timestamps.stoppedAt = Date.now();
|
|
5524
|
+
this.componentTimestamps.set(name, timestamps);
|
|
5525
|
+
this.logger.entity(name).warn(
|
|
5526
|
+
error ? `Component stopped unexpectedly: ${error.message}` : "Component stopped unexpectedly",
|
|
5527
|
+
{ params: { error } }
|
|
5528
|
+
);
|
|
5529
|
+
this.lifecycleEvents.componentUnexpectedStop(name, error);
|
|
5530
|
+
this.lifecycleEvents.componentStopped(name, this.getComponentStatus(name));
|
|
5531
|
+
return true;
|
|
5532
|
+
}
|
|
4977
5533
|
/**
|
|
4978
5534
|
* Safe emit wrapper - prevents event handler errors from breaking lifecycle
|
|
4979
5535
|
*/
|
|
@@ -5255,121 +5811,377 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5255
5811
|
}
|
|
5256
5812
|
/**
|
|
5257
5813
|
* Handle shutdown signal - initiates stopAllComponents().
|
|
5258
|
-
*
|
|
5814
|
+
*
|
|
5815
|
+
* Four cases depending on the current shutdown state:
|
|
5816
|
+
*
|
|
5817
|
+
* 1. **Active shutdown** (`isShuttingDown = true`): escalate through the
|
|
5818
|
+
* repeated-shutdown policy if configured, otherwise log and discard.
|
|
5819
|
+
* Emits `signal:shutdown` with `isAlreadyShuttingDown: true` and returns
|
|
5820
|
+
* without starting another shutdown.
|
|
5821
|
+
*
|
|
5822
|
+
* 2. **Armed post-failure** (previous shutdown finished, armed window still
|
|
5823
|
+
* open): count the request toward the escalation window, emit
|
|
5824
|
+
* `signal:shutdown` with `isAlreadyShuttingDown: false`, then start a
|
|
5825
|
+
* new `stopAllComponents()` run to retry.
|
|
5826
|
+
*
|
|
5827
|
+
* 3. **Armed post-failure expired** (armed window opened but has since
|
|
5828
|
+
* elapsed): expire the stale state, treat the request as a fresh
|
|
5829
|
+
* shutdown (falls through to case 4).
|
|
5830
|
+
*
|
|
5831
|
+
* 4. **Fresh shutdown** (no prior shutdown state): seed escalation tracking
|
|
5832
|
+
* if policy is configured, emit `signal:shutdown` with
|
|
5833
|
+
* `isAlreadyShuttingDown: false`, and start `stopAllComponents()`.
|
|
5834
|
+
*
|
|
5835
|
+
* In all cases `signal:shutdown` is emitted exactly once.
|
|
5259
5836
|
*/
|
|
5260
5837
|
handleShutdownRequest(method) {
|
|
5261
5838
|
if (this.isShuttingDown) {
|
|
5262
|
-
this.
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5839
|
+
this.lifecycleEvents.signalShutdown(method, true);
|
|
5840
|
+
if (this.handleRepeatedShutdownRequest(method)) {
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
let didEmitShutdownSignal = false;
|
|
5845
|
+
let shouldSeedRepeatedShutdownState = this.repeatedShutdownRequestPolicy !== void 0;
|
|
5846
|
+
if (this.repeatedShutdownRequestPolicy && this.repeatedShutdownRequestState.firstRequestAt !== null && this.normalizeRepeatedShutdownRequestStateArmedStatus()) {
|
|
5847
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5848
|
+
didEmitShutdownSignal = true;
|
|
5849
|
+
shouldSeedRepeatedShutdownState = false;
|
|
5850
|
+
this.handleRepeatedShutdownRequest(method);
|
|
5851
|
+
}
|
|
5852
|
+
if (shouldSeedRepeatedShutdownState) {
|
|
5853
|
+
this.seedRepeatedShutdownRequestState(method);
|
|
5266
5854
|
}
|
|
5267
5855
|
this.logger.info("Shutdown signal received", { params: { method } });
|
|
5268
|
-
|
|
5856
|
+
if (!didEmitShutdownSignal) {
|
|
5857
|
+
this.lifecycleEvents.signalShutdown(method, false);
|
|
5858
|
+
}
|
|
5269
5859
|
void this.stopAllComponentsInternal(method, {
|
|
5270
5860
|
...this.shutdownOptions
|
|
5271
5861
|
});
|
|
5272
5862
|
}
|
|
5273
5863
|
/**
|
|
5274
|
-
*
|
|
5275
|
-
*
|
|
5276
|
-
* When called from signal handlers (source='signal'), the Promise is started
|
|
5277
|
-
* but not awaited due to Node.js signal handler constraints. Components are
|
|
5278
|
-
* still notified and the work completes, but return values are not accessible.
|
|
5279
|
-
*
|
|
5280
|
-
* When called from manual triggers (source='trigger'), the Promise is awaited
|
|
5281
|
-
* and results are returned for programmatic use.
|
|
5864
|
+
* Tracks repeated shutdown requests during an active shutdown and optionally
|
|
5865
|
+
* invokes the configured force shutdown callback when the threshold is reached.
|
|
5282
5866
|
*
|
|
5283
|
-
* @
|
|
5867
|
+
* @returns true when the request was consumed as part of the repeated-shutdown
|
|
5868
|
+
* escalation flow, false when the caller should treat it as a fresh shutdown request
|
|
5284
5869
|
*/
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
await result;
|
|
5293
|
-
}
|
|
5294
|
-
return {
|
|
5295
|
-
signal: "reload",
|
|
5296
|
-
results: [],
|
|
5297
|
-
timedOut: false,
|
|
5298
|
-
code: "ok"
|
|
5299
|
-
};
|
|
5870
|
+
handleRepeatedShutdownRequest(method) {
|
|
5871
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
5872
|
+
if (!policy) {
|
|
5873
|
+
this.logger.warn("Shutdown already in progress, ignoring signal", {
|
|
5874
|
+
params: { method }
|
|
5875
|
+
});
|
|
5876
|
+
return true;
|
|
5300
5877
|
}
|
|
5301
|
-
|
|
5878
|
+
const now = Date.now();
|
|
5879
|
+
const state = this.repeatedShutdownRequestState;
|
|
5880
|
+
if (state.remainsArmedUntil !== null) {
|
|
5881
|
+
if (now >= state.remainsArmedUntil) {
|
|
5882
|
+
this.expireRepeatedShutdownRequestState();
|
|
5883
|
+
return false;
|
|
5884
|
+
}
|
|
5885
|
+
this.refreshRepeatedShutdownArmedWindow(now);
|
|
5886
|
+
}
|
|
5887
|
+
const shouldStartNewWindow = state.repeatedWindowStartedAt === null || now - state.repeatedWindowStartedAt > policy.withinMS;
|
|
5888
|
+
if (shouldStartNewWindow) {
|
|
5889
|
+
state.requestCount = 1;
|
|
5890
|
+
state.repeatedWindowStartedAt = now;
|
|
5891
|
+
} else {
|
|
5892
|
+
state.requestCount++;
|
|
5893
|
+
}
|
|
5894
|
+
state.latestMethod = method;
|
|
5895
|
+
state.latestRequestAt = now;
|
|
5896
|
+
this.logger.warn(
|
|
5897
|
+
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",
|
|
5898
|
+
{
|
|
5899
|
+
params: {
|
|
5900
|
+
method,
|
|
5901
|
+
requestCount: state.requestCount,
|
|
5902
|
+
firstMethod: state.firstMethod,
|
|
5903
|
+
latestMethod: state.latestMethod,
|
|
5904
|
+
firstRequestAt: state.firstRequestAt,
|
|
5905
|
+
latestRequestAt: state.latestRequestAt,
|
|
5906
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5907
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5908
|
+
withinMS: policy.withinMS,
|
|
5909
|
+
forceAfterCount: policy.forceAfterCount
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
);
|
|
5913
|
+
if (
|
|
5914
|
+
// Force escalation is single-fire per shutdown cycle. Later requests are
|
|
5915
|
+
// still logged but do not re-enter user force-shutdown logic.
|
|
5916
|
+
state.hasTriggeredForceShutdown || state.requestCount < policy.forceAfterCount || state.firstMethod === null || state.firstRequestAt === null || state.latestMethod === null || state.latestRequestAt === null
|
|
5917
|
+
) {
|
|
5918
|
+
return true;
|
|
5919
|
+
}
|
|
5920
|
+
state.hasTriggeredForceShutdown = true;
|
|
5921
|
+
const context = {
|
|
5922
|
+
requestCount: state.requestCount,
|
|
5923
|
+
firstMethod: state.firstMethod,
|
|
5924
|
+
latestMethod: state.latestMethod,
|
|
5925
|
+
firstRequestAt: state.firstRequestAt,
|
|
5926
|
+
latestRequestAt: state.latestRequestAt,
|
|
5927
|
+
isShuttingDown: this.isShuttingDown,
|
|
5928
|
+
wasArmedAfterFailure: state.remainsArmedUntil !== null
|
|
5929
|
+
};
|
|
5930
|
+
this.logger.warn(
|
|
5931
|
+
"Repeated shutdown request threshold reached, invoking force shutdown handler",
|
|
5932
|
+
{
|
|
5933
|
+
params: {
|
|
5934
|
+
method,
|
|
5935
|
+
requestCount: context.requestCount,
|
|
5936
|
+
firstMethod: context.firstMethod,
|
|
5937
|
+
latestMethod: context.latestMethod,
|
|
5938
|
+
firstRequestAt: context.firstRequestAt,
|
|
5939
|
+
latestRequestAt: context.latestRequestAt,
|
|
5940
|
+
repeatedWindowStartedAt: state.repeatedWindowStartedAt,
|
|
5941
|
+
remainsArmedUntil: state.remainsArmedUntil,
|
|
5942
|
+
withinMS: policy.withinMS,
|
|
5943
|
+
forceAfterCount: policy.forceAfterCount
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
);
|
|
5947
|
+
safeHandleCallback(
|
|
5948
|
+
"repeatedShutdownRequestPolicy.onForceShutdown",
|
|
5949
|
+
policy.onForceShutdown,
|
|
5950
|
+
context
|
|
5951
|
+
);
|
|
5952
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationForced({
|
|
5953
|
+
firstMethod: context.firstMethod,
|
|
5954
|
+
latestMethod: context.latestMethod,
|
|
5955
|
+
requestCount: context.requestCount,
|
|
5956
|
+
firstRequestAt: context.firstRequestAt,
|
|
5957
|
+
latestRequestAt: context.latestRequestAt,
|
|
5958
|
+
wasArmedAfterFailure: context.wasArmedAfterFailure
|
|
5959
|
+
});
|
|
5960
|
+
return true;
|
|
5302
5961
|
}
|
|
5303
5962
|
/**
|
|
5304
|
-
*
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5963
|
+
* Clears repeated shutdown request tracking so a new shutdown cycle starts fresh.
|
|
5964
|
+
*/
|
|
5965
|
+
resetRepeatedShutdownRequestState() {
|
|
5966
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
5967
|
+
this.repeatedShutdownRequestState = {
|
|
5968
|
+
requestCount: 0,
|
|
5969
|
+
firstMethod: null,
|
|
5970
|
+
latestMethod: null,
|
|
5971
|
+
firstRequestAt: null,
|
|
5972
|
+
latestRequestAt: null,
|
|
5973
|
+
repeatedWindowStartedAt: null,
|
|
5974
|
+
hasTriggeredForceShutdown: false,
|
|
5975
|
+
remainsArmedUntil: null
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
/**
|
|
5979
|
+
* Clear any pending expiration timer for the post-failure escalation window.
|
|
5980
|
+
*/
|
|
5981
|
+
clearRepeatedShutdownExpiryTimer() {
|
|
5982
|
+
if (this.repeatedShutdownExpiryTimer === null) {
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
clearTimeout(this.repeatedShutdownExpiryTimer);
|
|
5986
|
+
this.repeatedShutdownExpiryTimer = null;
|
|
5987
|
+
}
|
|
5988
|
+
/**
|
|
5989
|
+
* Returns whether post-failure escalation remains armed after first
|
|
5990
|
+
* normalizing any stale timer-backed state.
|
|
5308
5991
|
*
|
|
5309
|
-
*
|
|
5992
|
+
* The method can expire old armed windows as a side effect because the timer
|
|
5993
|
+
* callback may not have run yet on a delayed event loop. Callers use this
|
|
5994
|
+
* when they need the effective runtime truth, not just the last timer write.
|
|
5310
5995
|
*/
|
|
5311
|
-
|
|
5312
|
-
this.
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5996
|
+
normalizeRepeatedShutdownRequestStateArmedStatus(now = Date.now()) {
|
|
5997
|
+
const armedUntil = this.repeatedShutdownRequestState.remainsArmedUntil;
|
|
5998
|
+
if (armedUntil === null) {
|
|
5999
|
+
return false;
|
|
6000
|
+
}
|
|
6001
|
+
if (now >= armedUntil) {
|
|
6002
|
+
this.expireRepeatedShutdownRequestState();
|
|
6003
|
+
return false;
|
|
6004
|
+
}
|
|
6005
|
+
return true;
|
|
6006
|
+
}
|
|
6007
|
+
/**
|
|
6008
|
+
* Transition armed post-failure escalation state into its expired/reset state.
|
|
6009
|
+
*/
|
|
6010
|
+
expireRepeatedShutdownRequestState() {
|
|
6011
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6012
|
+
const state = this.repeatedShutdownRequestState;
|
|
6013
|
+
if (!policy || state.remainsArmedUntil === null) {
|
|
6014
|
+
return;
|
|
6015
|
+
}
|
|
6016
|
+
const armedUntil = state.remainsArmedUntil;
|
|
6017
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
6018
|
+
const expiredState = {
|
|
6019
|
+
firstMethod: state.firstMethod,
|
|
6020
|
+
latestMethod: state.latestMethod,
|
|
6021
|
+
requestCount: state.requestCount,
|
|
6022
|
+
armedUntil
|
|
6023
|
+
};
|
|
6024
|
+
this.logger.warn(
|
|
6025
|
+
"Repeated shutdown escalation window expired, clearing previous shutdown state",
|
|
6026
|
+
{
|
|
6027
|
+
params: {
|
|
6028
|
+
remainsArmedUntil: armedUntil,
|
|
6029
|
+
withinMS: policy.withinMS,
|
|
6030
|
+
forceAfterCount: policy.forceAfterCount
|
|
6031
|
+
}
|
|
5319
6032
|
}
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
6033
|
+
);
|
|
6034
|
+
if (expiredState.firstMethod !== null) {
|
|
6035
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationExpired({
|
|
6036
|
+
firstMethod: expiredState.firstMethod,
|
|
6037
|
+
latestMethod: expiredState.latestMethod,
|
|
6038
|
+
requestCount: expiredState.requestCount,
|
|
6039
|
+
armedUntil: expiredState.armedUntil
|
|
6040
|
+
});
|
|
5326
6041
|
}
|
|
5327
|
-
|
|
6042
|
+
this.resetRepeatedShutdownRequestState();
|
|
5328
6043
|
}
|
|
5329
6044
|
/**
|
|
5330
|
-
*
|
|
6045
|
+
* Arms or refreshes the post-failure escalation window and its expiration timer.
|
|
6046
|
+
*/
|
|
6047
|
+
refreshRepeatedShutdownArmedWindow(now = Date.now()) {
|
|
6048
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6049
|
+
if (!policy) {
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
this.clearRepeatedShutdownExpiryTimer();
|
|
6053
|
+
const armedUntil = now + policy.armedAfterFailureMS;
|
|
6054
|
+
this.repeatedShutdownRequestState.remainsArmedUntil = armedUntil;
|
|
6055
|
+
this.repeatedShutdownExpiryTimer = setTimeout(() => {
|
|
6056
|
+
this.expireRepeatedShutdownRequestState();
|
|
6057
|
+
}, policy.armedAfterFailureMS);
|
|
6058
|
+
this.repeatedShutdownExpiryTimer.unref();
|
|
6059
|
+
}
|
|
6060
|
+
/**
|
|
6061
|
+
* Seeds shutdown escalation tracking for a new shutdown cycle.
|
|
5331
6062
|
*
|
|
5332
|
-
*
|
|
5333
|
-
*
|
|
6063
|
+
* The first shutdown trigger starts graceful shutdown and arms escalation with
|
|
6064
|
+
* an effective post-start count of 0. Later shutdown requests can then count
|
|
6065
|
+
* toward the configured force threshold regardless of whether the shutdown
|
|
6066
|
+
* started from a signal, keyboard shortcut, or direct API call.
|
|
6067
|
+
*/
|
|
6068
|
+
seedRepeatedShutdownRequestState(method) {
|
|
6069
|
+
const now = Date.now();
|
|
6070
|
+
this.repeatedShutdownRequestState = {
|
|
6071
|
+
requestCount: 0,
|
|
6072
|
+
firstMethod: method,
|
|
6073
|
+
latestMethod: method,
|
|
6074
|
+
firstRequestAt: now,
|
|
6075
|
+
latestRequestAt: now,
|
|
6076
|
+
repeatedWindowStartedAt: null,
|
|
6077
|
+
hasTriggeredForceShutdown: false,
|
|
6078
|
+
remainsArmedUntil: null
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
/**
|
|
6082
|
+
* Preserves a short-lived post-failure escalation window after shutdown
|
|
6083
|
+
* returns unsuccessfully so operators can keep pressing shutdown without
|
|
6084
|
+
* losing the existing force count the moment the graceful attempt finishes.
|
|
6085
|
+
*/
|
|
6086
|
+
armRepeatedShutdownAfterFailure() {
|
|
6087
|
+
const policy = this.repeatedShutdownRequestPolicy;
|
|
6088
|
+
const state = this.repeatedShutdownRequestState;
|
|
6089
|
+
if (!policy || policy.armedAfterFailureMS <= 0 || // armedAfterFailureMS = 0 disables post-failure arming
|
|
6090
|
+
state.firstRequestAt === null || state.hasTriggeredForceShutdown) {
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
this.refreshRepeatedShutdownArmedWindow();
|
|
6094
|
+
const armedUntil = state.remainsArmedUntil;
|
|
6095
|
+
if (state.firstMethod !== null && armedUntil !== null) {
|
|
6096
|
+
this.lifecycleEvents.lifecycleManagerShutdownEscalationArmed({
|
|
6097
|
+
firstMethod: state.firstMethod,
|
|
6098
|
+
requestCount: state.requestCount,
|
|
6099
|
+
armedUntil
|
|
6100
|
+
});
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
/**
|
|
6104
|
+
* Shared dispatch path for reload/info/debug requests. Logs the dispatch,
|
|
6105
|
+
* emits the signal event, then either invokes the user-supplied callback
|
|
6106
|
+
* (passing the broadcast function so the user controls when/whether to
|
|
6107
|
+
* broadcast) or broadcasts directly when no callback is configured.
|
|
5334
6108
|
*
|
|
5335
|
-
*
|
|
6109
|
+
* When called from signal handlers (source='signal'), the Promise is started
|
|
6110
|
+
* but not awaited — Node.js signal handlers cannot return values, so results
|
|
6111
|
+
* are not accessible. Components are still notified and the work completes.
|
|
6112
|
+
* When called from manual triggers (source='trigger'), the Promise is awaited
|
|
6113
|
+
* and results are returned for programmatic use.
|
|
5336
6114
|
*/
|
|
5337
|
-
async
|
|
5338
|
-
this.logger.info(
|
|
5339
|
-
|
|
5340
|
-
if (
|
|
5341
|
-
const
|
|
5342
|
-
const result = this.onDebugRequested(broadcastFn);
|
|
6115
|
+
async handleSignalRequest(descriptor, source) {
|
|
6116
|
+
this.logger.info(descriptor.dispatchedLogLabel, { params: { source } });
|
|
6117
|
+
descriptor.emitSignal();
|
|
6118
|
+
if (descriptor.customCallback) {
|
|
6119
|
+
const result = descriptor.customCallback(descriptor.broadcast);
|
|
5343
6120
|
if (isPromise(result)) {
|
|
5344
6121
|
await result;
|
|
5345
6122
|
}
|
|
5346
6123
|
return {
|
|
5347
|
-
signal:
|
|
6124
|
+
signal: descriptor.signal,
|
|
5348
6125
|
results: [],
|
|
5349
6126
|
timedOut: false,
|
|
5350
6127
|
code: "ok"
|
|
5351
6128
|
};
|
|
5352
6129
|
}
|
|
5353
|
-
return
|
|
6130
|
+
return descriptor.broadcast();
|
|
6131
|
+
}
|
|
6132
|
+
async handleReloadRequest(source = "trigger") {
|
|
6133
|
+
return this.handleSignalRequest(
|
|
6134
|
+
{
|
|
6135
|
+
signal: "reload",
|
|
6136
|
+
dispatchedLogLabel: "Reload dispatched",
|
|
6137
|
+
emitSignal: () => this.lifecycleEvents.signalReload(),
|
|
6138
|
+
customCallback: this.onReloadRequested,
|
|
6139
|
+
broadcast: () => this.broadcastReload()
|
|
6140
|
+
},
|
|
6141
|
+
source
|
|
6142
|
+
);
|
|
6143
|
+
}
|
|
6144
|
+
async handleInfoRequest(source = "trigger") {
|
|
6145
|
+
return this.handleSignalRequest(
|
|
6146
|
+
{
|
|
6147
|
+
signal: "info",
|
|
6148
|
+
dispatchedLogLabel: "Info dispatched",
|
|
6149
|
+
emitSignal: () => this.lifecycleEvents.signalInfo(),
|
|
6150
|
+
customCallback: this.onInfoRequested,
|
|
6151
|
+
broadcast: () => this.broadcastInfo()
|
|
6152
|
+
},
|
|
6153
|
+
source
|
|
6154
|
+
);
|
|
6155
|
+
}
|
|
6156
|
+
async handleDebugRequest(source = "trigger") {
|
|
6157
|
+
return this.handleSignalRequest(
|
|
6158
|
+
{
|
|
6159
|
+
signal: "debug",
|
|
6160
|
+
dispatchedLogLabel: "Debug dispatched",
|
|
6161
|
+
emitSignal: () => this.lifecycleEvents.signalDebug(),
|
|
6162
|
+
customCallback: this.onDebugRequested,
|
|
6163
|
+
broadcast: () => this.broadcastDebug()
|
|
6164
|
+
},
|
|
6165
|
+
source
|
|
6166
|
+
);
|
|
5354
6167
|
}
|
|
5355
6168
|
/**
|
|
5356
|
-
*
|
|
5357
|
-
*
|
|
5358
|
-
*
|
|
6169
|
+
* Shared signal broadcast pipeline used by reload/info/debug.
|
|
6170
|
+
* Iterates running components, runs the picked handler with timeout, and
|
|
6171
|
+
* aggregates per-component results into a SignalBroadcastResult.
|
|
5359
6172
|
*/
|
|
5360
|
-
async
|
|
6173
|
+
async runSignalBroadcast(descriptor) {
|
|
5361
6174
|
const results = [];
|
|
5362
|
-
const
|
|
6175
|
+
const targets = this.components.filter(
|
|
5363
6176
|
(component) => this.runningComponents.has(component.getName())
|
|
5364
6177
|
);
|
|
5365
6178
|
if (this.isStarting) {
|
|
5366
|
-
this.logger.info(
|
|
5367
|
-
"Reload during startup: only reloading already-started components"
|
|
5368
|
-
);
|
|
6179
|
+
this.logger.info(descriptor.startupLog);
|
|
5369
6180
|
}
|
|
5370
|
-
for (const component of
|
|
6181
|
+
for (const component of targets) {
|
|
5371
6182
|
const name = component.getName();
|
|
5372
|
-
|
|
6183
|
+
const handler = descriptor.pickHandler(component);
|
|
6184
|
+
if (!handler) {
|
|
5373
6185
|
results.push({
|
|
5374
6186
|
name,
|
|
5375
6187
|
called: false,
|
|
@@ -5379,13 +6191,13 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5379
6191
|
});
|
|
5380
6192
|
continue;
|
|
5381
6193
|
}
|
|
5382
|
-
|
|
6194
|
+
descriptor.emitStarted(name);
|
|
5383
6195
|
const timeoutMS = component.signalTimeoutMS;
|
|
5384
6196
|
let timeoutHandle;
|
|
5385
6197
|
const timeoutResult = { timedOut: true };
|
|
5386
6198
|
try {
|
|
5387
|
-
const
|
|
5388
|
-
const handlerPromise = isPromise(
|
|
6199
|
+
const handlerResult = handler();
|
|
6200
|
+
const handlerPromise = isPromise(handlerResult) ? handlerResult : Promise.resolve(handlerResult);
|
|
5389
6201
|
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5390
6202
|
handlerPromise,
|
|
5391
6203
|
new Promise((resolve) => {
|
|
@@ -5395,7 +6207,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5395
6207
|
})
|
|
5396
6208
|
]) : await handlerPromise;
|
|
5397
6209
|
if (outcome === timeoutResult) {
|
|
5398
|
-
this.logger.entity(name).warn(
|
|
6210
|
+
this.logger.entity(name).warn(descriptor.timeoutLog, {
|
|
5399
6211
|
params: { timeoutMS }
|
|
5400
6212
|
});
|
|
5401
6213
|
Promise.resolve(handlerPromise).catch(() => {
|
|
@@ -5408,7 +6220,7 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5408
6220
|
code: "timeout"
|
|
5409
6221
|
});
|
|
5410
6222
|
} else {
|
|
5411
|
-
|
|
6223
|
+
descriptor.emitCompleted(name);
|
|
5412
6224
|
results.push({
|
|
5413
6225
|
name,
|
|
5414
6226
|
called: true,
|
|
@@ -5419,10 +6231,10 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5419
6231
|
}
|
|
5420
6232
|
} catch (error) {
|
|
5421
6233
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
5422
|
-
this.logger.entity(name).error(
|
|
6234
|
+
this.logger.entity(name).error(descriptor.errorLog, {
|
|
5423
6235
|
params: { error: err }
|
|
5424
6236
|
});
|
|
5425
|
-
|
|
6237
|
+
descriptor.emitFailed(name, err);
|
|
5426
6238
|
results.push({
|
|
5427
6239
|
name,
|
|
5428
6240
|
called: true,
|
|
@@ -5443,108 +6255,45 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5443
6255
|
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5444
6256
|
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5445
6257
|
return {
|
|
5446
|
-
signal:
|
|
6258
|
+
signal: descriptor.signal,
|
|
5447
6259
|
results,
|
|
5448
6260
|
timedOut: hasTimeout,
|
|
5449
6261
|
code
|
|
5450
6262
|
};
|
|
5451
6263
|
}
|
|
6264
|
+
/**
|
|
6265
|
+
* Broadcast reload signal to all running components.
|
|
6266
|
+
* Calls onReload() on components that implement it.
|
|
6267
|
+
* Continues on errors - collects all results.
|
|
6268
|
+
*/
|
|
6269
|
+
async broadcastReload() {
|
|
6270
|
+
return this.runSignalBroadcast({
|
|
6271
|
+
signal: "reload",
|
|
6272
|
+
pickHandler: (component) => component.onReload?.bind(component),
|
|
6273
|
+
startupLog: "Reload during startup: only reloading already-started components",
|
|
6274
|
+
timeoutLog: "Reload handler timed out",
|
|
6275
|
+
errorLog: "Reload failed: {{error.message}}",
|
|
6276
|
+
emitStarted: (name) => this.lifecycleEvents.componentReloadStarted(name),
|
|
6277
|
+
emitCompleted: (name) => this.lifecycleEvents.componentReloadCompleted(name),
|
|
6278
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentReloadFailed(name, error)
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
5452
6281
|
/**
|
|
5453
6282
|
* Broadcast info signal to all running components.
|
|
5454
6283
|
* Calls onInfo() on components that implement it.
|
|
5455
6284
|
* Continues on errors - collects all results.
|
|
5456
6285
|
*/
|
|
5457
6286
|
async broadcastInfo() {
|
|
5458
|
-
|
|
5459
|
-
const componentsToNotify = this.components.filter(
|
|
5460
|
-
(component) => this.runningComponents.has(component.getName())
|
|
5461
|
-
);
|
|
5462
|
-
if (this.isStarting) {
|
|
5463
|
-
this.logger.info(
|
|
5464
|
-
"Info during startup: only notifying already-started components"
|
|
5465
|
-
);
|
|
5466
|
-
}
|
|
5467
|
-
for (const component of componentsToNotify) {
|
|
5468
|
-
const name = component.getName();
|
|
5469
|
-
if (!component.onInfo) {
|
|
5470
|
-
results.push({
|
|
5471
|
-
name,
|
|
5472
|
-
called: false,
|
|
5473
|
-
error: null,
|
|
5474
|
-
timedOut: false,
|
|
5475
|
-
code: "no_handler"
|
|
5476
|
-
});
|
|
5477
|
-
continue;
|
|
5478
|
-
}
|
|
5479
|
-
this.lifecycleEvents.componentInfoStarted(name);
|
|
5480
|
-
const timeoutMS = component.signalTimeoutMS;
|
|
5481
|
-
let timeoutHandle;
|
|
5482
|
-
const timeoutResult = { timedOut: true };
|
|
5483
|
-
try {
|
|
5484
|
-
const result = component.onInfo();
|
|
5485
|
-
const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
|
|
5486
|
-
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5487
|
-
handlerPromise,
|
|
5488
|
-
new Promise((resolve) => {
|
|
5489
|
-
timeoutHandle = setTimeout(() => {
|
|
5490
|
-
resolve(timeoutResult);
|
|
5491
|
-
}, timeoutMS);
|
|
5492
|
-
})
|
|
5493
|
-
]) : await handlerPromise;
|
|
5494
|
-
if (outcome === timeoutResult) {
|
|
5495
|
-
this.logger.entity(name).warn("Info handler timed out", {
|
|
5496
|
-
params: { timeoutMS }
|
|
5497
|
-
});
|
|
5498
|
-
Promise.resolve(handlerPromise).catch(() => {
|
|
5499
|
-
});
|
|
5500
|
-
results.push({
|
|
5501
|
-
name,
|
|
5502
|
-
called: true,
|
|
5503
|
-
error: null,
|
|
5504
|
-
timedOut: true,
|
|
5505
|
-
code: "timeout"
|
|
5506
|
-
});
|
|
5507
|
-
} else {
|
|
5508
|
-
this.lifecycleEvents.componentInfoCompleted(name);
|
|
5509
|
-
results.push({
|
|
5510
|
-
name,
|
|
5511
|
-
called: true,
|
|
5512
|
-
error: null,
|
|
5513
|
-
timedOut: false,
|
|
5514
|
-
code: "called"
|
|
5515
|
-
});
|
|
5516
|
-
}
|
|
5517
|
-
} catch (error) {
|
|
5518
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
5519
|
-
this.logger.entity(name).error("Info handler failed: {{error.message}}", {
|
|
5520
|
-
params: { error: err }
|
|
5521
|
-
});
|
|
5522
|
-
this.lifecycleEvents.componentInfoFailed(name, err);
|
|
5523
|
-
results.push({
|
|
5524
|
-
name,
|
|
5525
|
-
called: true,
|
|
5526
|
-
error: err,
|
|
5527
|
-
timedOut: false,
|
|
5528
|
-
code: "error"
|
|
5529
|
-
});
|
|
5530
|
-
} finally {
|
|
5531
|
-
if (timeoutHandle) {
|
|
5532
|
-
clearTimeout(timeoutHandle);
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
|
-
}
|
|
5536
|
-
const calledResults = results.filter((result) => result.called);
|
|
5537
|
-
const hasError = calledResults.some((result) => result.error);
|
|
5538
|
-
const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
|
|
5539
|
-
const hasTimeout = calledResults.some((result) => result.timedOut);
|
|
5540
|
-
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5541
|
-
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5542
|
-
return {
|
|
6287
|
+
return this.runSignalBroadcast({
|
|
5543
6288
|
signal: "info",
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
6289
|
+
pickHandler: (component) => component.onInfo?.bind(component),
|
|
6290
|
+
startupLog: "Info during startup: only notifying already-started components",
|
|
6291
|
+
timeoutLog: "Info handler timed out",
|
|
6292
|
+
errorLog: "Info handler failed: {{error.message}}",
|
|
6293
|
+
emitStarted: (name) => this.lifecycleEvents.componentInfoStarted(name),
|
|
6294
|
+
emitCompleted: (name) => this.lifecycleEvents.componentInfoCompleted(name),
|
|
6295
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentInfoFailed(name, error)
|
|
6296
|
+
});
|
|
5548
6297
|
}
|
|
5549
6298
|
/**
|
|
5550
6299
|
* Broadcast debug signal to all running components.
|
|
@@ -5552,96 +6301,16 @@ var LifecycleManager = class extends EventEmitterProtected {
|
|
|
5552
6301
|
* Continues on errors - collects all results.
|
|
5553
6302
|
*/
|
|
5554
6303
|
async broadcastDebug() {
|
|
5555
|
-
|
|
5556
|
-
const componentsToNotify = this.components.filter(
|
|
5557
|
-
(component) => this.runningComponents.has(component.getName())
|
|
5558
|
-
);
|
|
5559
|
-
if (this.isStarting) {
|
|
5560
|
-
this.logger.info(
|
|
5561
|
-
"Debug during startup: only notifying already-started components"
|
|
5562
|
-
);
|
|
5563
|
-
}
|
|
5564
|
-
for (const component of componentsToNotify) {
|
|
5565
|
-
const name = component.getName();
|
|
5566
|
-
if (!component.onDebug) {
|
|
5567
|
-
results.push({
|
|
5568
|
-
name,
|
|
5569
|
-
called: false,
|
|
5570
|
-
error: null,
|
|
5571
|
-
timedOut: false,
|
|
5572
|
-
code: "no_handler"
|
|
5573
|
-
});
|
|
5574
|
-
continue;
|
|
5575
|
-
}
|
|
5576
|
-
this.lifecycleEvents.componentDebugStarted(name);
|
|
5577
|
-
const timeoutMS = component.signalTimeoutMS;
|
|
5578
|
-
let timeoutHandle;
|
|
5579
|
-
const timeoutResult = { timedOut: true };
|
|
5580
|
-
try {
|
|
5581
|
-
const result = component.onDebug();
|
|
5582
|
-
const handlerPromise = isPromise(result) ? result : Promise.resolve(result);
|
|
5583
|
-
const outcome = timeoutMS > 0 ? await Promise.race([
|
|
5584
|
-
handlerPromise,
|
|
5585
|
-
new Promise((resolve) => {
|
|
5586
|
-
timeoutHandle = setTimeout(() => {
|
|
5587
|
-
resolve(timeoutResult);
|
|
5588
|
-
}, timeoutMS);
|
|
5589
|
-
})
|
|
5590
|
-
]) : await handlerPromise;
|
|
5591
|
-
if (outcome === timeoutResult) {
|
|
5592
|
-
this.logger.entity(name).warn("Debug handler timed out", {
|
|
5593
|
-
params: { timeoutMS }
|
|
5594
|
-
});
|
|
5595
|
-
Promise.resolve(handlerPromise).catch(() => {
|
|
5596
|
-
});
|
|
5597
|
-
results.push({
|
|
5598
|
-
name,
|
|
5599
|
-
called: true,
|
|
5600
|
-
error: null,
|
|
5601
|
-
timedOut: true,
|
|
5602
|
-
code: "timeout"
|
|
5603
|
-
});
|
|
5604
|
-
} else {
|
|
5605
|
-
this.lifecycleEvents.componentDebugCompleted(name);
|
|
5606
|
-
results.push({
|
|
5607
|
-
name,
|
|
5608
|
-
called: true,
|
|
5609
|
-
error: null,
|
|
5610
|
-
timedOut: false,
|
|
5611
|
-
code: "called"
|
|
5612
|
-
});
|
|
5613
|
-
}
|
|
5614
|
-
} catch (error) {
|
|
5615
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
5616
|
-
this.logger.entity(name).error("Debug handler failed: {{error.message}}", {
|
|
5617
|
-
params: { error: err }
|
|
5618
|
-
});
|
|
5619
|
-
this.lifecycleEvents.componentDebugFailed(name, err);
|
|
5620
|
-
results.push({
|
|
5621
|
-
name,
|
|
5622
|
-
called: true,
|
|
5623
|
-
error: err,
|
|
5624
|
-
timedOut: false,
|
|
5625
|
-
code: "error"
|
|
5626
|
-
});
|
|
5627
|
-
} finally {
|
|
5628
|
-
if (timeoutHandle) {
|
|
5629
|
-
clearTimeout(timeoutHandle);
|
|
5630
|
-
}
|
|
5631
|
-
}
|
|
5632
|
-
}
|
|
5633
|
-
const calledResults = results.filter((result) => result.called);
|
|
5634
|
-
const hasError = calledResults.some((result) => result.error);
|
|
5635
|
-
const isAllError = calledResults.length > 0 && calledResults.every((result) => result.error);
|
|
5636
|
-
const hasTimeout = calledResults.some((result) => result.timedOut);
|
|
5637
|
-
const isAllTimeout = calledResults.length > 0 && calledResults.every((result) => result.timedOut);
|
|
5638
|
-
const code = hasError ? isAllError ? "error" : "partial_error" : hasTimeout ? isAllTimeout ? "timeout" : "partial_timeout" : "ok";
|
|
5639
|
-
return {
|
|
6304
|
+
return this.runSignalBroadcast({
|
|
5640
6305
|
signal: "debug",
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
6306
|
+
pickHandler: (component) => component.onDebug?.bind(component),
|
|
6307
|
+
startupLog: "Debug during startup: only notifying already-started components",
|
|
6308
|
+
timeoutLog: "Debug handler timed out",
|
|
6309
|
+
errorLog: "Debug handler failed: {{error.message}}",
|
|
6310
|
+
emitStarted: (name) => this.lifecycleEvents.componentDebugStarted(name),
|
|
6311
|
+
emitCompleted: (name) => this.lifecycleEvents.componentDebugCompleted(name),
|
|
6312
|
+
emitFailed: (name, error) => this.lifecycleEvents.componentDebugFailed(name, error)
|
|
6313
|
+
});
|
|
5645
6314
|
}
|
|
5646
6315
|
};
|
|
5647
6316
|
|
|
@@ -5667,6 +6336,10 @@ var BaseComponent = class {
|
|
|
5667
6336
|
name;
|
|
5668
6337
|
/** Reference to component-scoped lifecycle (set by manager when registered) */
|
|
5669
6338
|
lifecycle;
|
|
6339
|
+
/** @internal Set by LifecycleManager while the component is running. */
|
|
6340
|
+
_unexpectedStopHandler;
|
|
6341
|
+
/** @internal Incremented whenever the unexpected-stop handler is re-armed or cleared. */
|
|
6342
|
+
_unexpectedStopGeneration = 0;
|
|
5670
6343
|
/**
|
|
5671
6344
|
* Create a new component
|
|
5672
6345
|
*
|
|
@@ -5701,6 +6374,24 @@ var BaseComponent = class {
|
|
|
5701
6374
|
// Default if undefined/null/non-finite
|
|
5702
6375
|
);
|
|
5703
6376
|
}
|
|
6377
|
+
/** @internal Called by LifecycleManager after a successful start. */
|
|
6378
|
+
_setUnexpectedStopHandler(handler) {
|
|
6379
|
+
this._unexpectedStopGeneration += 1;
|
|
6380
|
+
this._unexpectedStopHandler = handler;
|
|
6381
|
+
const generation = this._unexpectedStopGeneration;
|
|
6382
|
+
this.reportUnexpectedStop = (error) => {
|
|
6383
|
+
if (this._unexpectedStopGeneration !== generation) {
|
|
6384
|
+
return false;
|
|
6385
|
+
}
|
|
6386
|
+
return this._unexpectedStopHandler?.(error) ?? false;
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
/** @internal Called by LifecycleManager when stop begins or component is unregistered. */
|
|
6390
|
+
_clearUnexpectedStopHandler() {
|
|
6391
|
+
this._unexpectedStopGeneration += 1;
|
|
6392
|
+
this._unexpectedStopHandler = void 0;
|
|
6393
|
+
this.reportUnexpectedStop = () => false;
|
|
6394
|
+
}
|
|
5704
6395
|
/**
|
|
5705
6396
|
* Get component name
|
|
5706
6397
|
*/
|
|
@@ -5719,6 +6410,38 @@ var BaseComponent = class {
|
|
|
5719
6410
|
isOptional() {
|
|
5720
6411
|
return this.optional;
|
|
5721
6412
|
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Run-scoped unexpected-stop callback. Rebound by LifecycleManager on each
|
|
6415
|
+
* successful start so captured references from older runs go stale.
|
|
6416
|
+
*/
|
|
6417
|
+
reportUnexpectedStop = () => false;
|
|
6418
|
+
/**
|
|
6419
|
+
* Get this component's own status from the manager's perspective.
|
|
6420
|
+
*
|
|
6421
|
+
* Equivalent to `this.lifecycle.getComponentStatus(this.getName())` but without
|
|
6422
|
+
* needing to pass the name. Returns `undefined` if the component is not registered.
|
|
6423
|
+
*
|
|
6424
|
+
* Check `status?.state === 'running'` to test whether the component is currently running.
|
|
6425
|
+
*/
|
|
6426
|
+
getSelfStatus() {
|
|
6427
|
+
return this.lifecycle?.getComponentStatus(this.name);
|
|
6428
|
+
}
|
|
6429
|
+
/**
|
|
6430
|
+
* Capture a run-scoped unexpected-stop reporter for async listeners created during start().
|
|
6431
|
+
*
|
|
6432
|
+
* Unlike calling `this.reportUnexpectedStop()` later, the returned callback becomes a no-op
|
|
6433
|
+
* once the component is stopped, unregistered, or restarted. This prevents stale listeners
|
|
6434
|
+
* from a previous run from stopping a newer run of the same component instance.
|
|
6435
|
+
*/
|
|
6436
|
+
getUnexpectedStopReporter() {
|
|
6437
|
+
const generation = this._unexpectedStopGeneration;
|
|
6438
|
+
return (error) => {
|
|
6439
|
+
if (this._unexpectedStopGeneration !== generation) {
|
|
6440
|
+
return false;
|
|
6441
|
+
}
|
|
6442
|
+
return this._unexpectedStopHandler?.(error) ?? false;
|
|
6443
|
+
};
|
|
6444
|
+
}
|
|
5722
6445
|
};
|
|
5723
6446
|
export {
|
|
5724
6447
|
BaseComponent,
|