@travetto/runtime 7.1.0 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -6
- package/package.json +3 -3
- package/src/shutdown.ts +63 -53
- package/src/trv.d.ts +0 -5
- package/src/watch.ts +10 -35
package/README.md
CHANGED
|
@@ -108,11 +108,6 @@ interface EnvData {
|
|
|
108
108
|
* @default 2s
|
|
109
109
|
*/
|
|
110
110
|
TRV_SHUTDOWN_WAIT: TimeSpan | number;
|
|
111
|
-
/**
|
|
112
|
-
* The time to wait for stdout to drain during shutdown
|
|
113
|
-
* @default 0s
|
|
114
|
-
*/
|
|
115
|
-
TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
|
|
116
111
|
/**
|
|
117
112
|
* The desired runtime module
|
|
118
113
|
*/
|
|
@@ -353,7 +348,7 @@ As a registered shutdown handler, you can do.
|
|
|
353
348
|
import { ShutdownManager } from '@travetto/runtime';
|
|
354
349
|
|
|
355
350
|
export function registerShutdownHandler() {
|
|
356
|
-
ShutdownManager.
|
|
351
|
+
ShutdownManager.signal.addEventListener('abort', () => {
|
|
357
352
|
// Do important work, the framework will wait until all async
|
|
358
353
|
// operations are completed before finishing shutdown
|
|
359
354
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for travetto applications.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"directory": "module/runtime"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/manifest": "^7.1.
|
|
29
|
+
"@travetto/manifest": "^7.1.1",
|
|
30
30
|
"@types/debug": "^4.1.12",
|
|
31
31
|
"debug": "^4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/transformer": "^7.1.
|
|
34
|
+
"@travetto/transformer": "^7.1.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/shutdown.ts
CHANGED
|
@@ -1,100 +1,110 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
|
|
1
3
|
import { Env } from './env.ts';
|
|
2
4
|
import { Util } from './util.ts';
|
|
3
5
|
import { TimeUtil } from './time.ts';
|
|
4
6
|
|
|
7
|
+
const MAPPING = [['restart', 200], ['error', 1], ['quit', 0]] as const;
|
|
8
|
+
export type ShutdownReason = typeof MAPPING[number][0];
|
|
9
|
+
|
|
10
|
+
const REASON_TO_CODE = new Map<ShutdownReason, number>(MAPPING);
|
|
11
|
+
const CODE_TO_REASON = new Map<number, ShutdownReason>(MAPPING.map(([k, v]) => [v, k]));
|
|
12
|
+
|
|
5
13
|
type Handler = (event: Event) => unknown;
|
|
14
|
+
type ShutdownSignal = 'SIGINT' | 'SIGTERM' | 'SIGUSR2' | string | undefined;
|
|
15
|
+
type ShutdownEvent = { signal?: ShutdownSignal, reason?: ShutdownReason | number, exit?: boolean };
|
|
16
|
+
|
|
17
|
+
const isShutdownEvent = (event: unknown): event is ShutdownEvent =>
|
|
18
|
+
typeof event === 'object' && event !== null && 'type' in event && event.type === 'shutdown';
|
|
19
|
+
|
|
20
|
+
const wrapped = async (handler: Handler): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
await handler(new Event('abort'));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error('Error during shutdown handler', err);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* Shutdown manager, allowing for listening for graceful shutdowns
|
|
9
30
|
*/
|
|
10
31
|
export class ShutdownManager {
|
|
11
|
-
|
|
12
32
|
static #shouldIgnoreInterrupt = false;
|
|
13
|
-
static #registered = new
|
|
14
|
-
static #pending: Function[] = [];
|
|
33
|
+
static #registered = new Set<Handler>();
|
|
15
34
|
static #controller = new AbortController();
|
|
16
|
-
static #startedAt: number = 0;
|
|
17
|
-
static #addListener = this.#controller.signal.addEventListener.bind(this.#controller.signal);
|
|
18
|
-
static #removeListener = this.#controller.signal.removeEventListener.bind(this.#controller.signal);
|
|
19
35
|
|
|
20
36
|
static {
|
|
21
|
-
this.#controller.signal.addEventListener = (
|
|
22
|
-
this.#controller.signal.removeEventListener = (
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
this.#controller.signal.addEventListener = (_: 'abort', listener: Handler): void => { this.#registered.add(listener); };
|
|
38
|
+
this.#controller.signal.removeEventListener = (_: 'abort', listener: Handler): void => { this.#registered.delete(listener); };
|
|
39
|
+
process
|
|
40
|
+
.on('message', event => { isShutdownEvent(event) && this.shutdown(event); })
|
|
41
|
+
.on('SIGINT', () => this.shutdown({ signal: 'SIGINT' }))
|
|
42
|
+
.on('SIGUSR2', () => this.shutdown({ signal: 'SIGUSR2' }))
|
|
43
|
+
.on('SIGTERM', () => this.shutdown({ signal: 'SIGTERM' }));
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
static get signal(): AbortSignal {
|
|
28
47
|
return this.#controller.signal;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
/** Disable SIGINT interrupt handling */
|
|
51
|
+
static disableInterrupt(): typeof ShutdownManager {
|
|
32
52
|
this.#shouldIgnoreInterrupt = true;
|
|
53
|
+
return this;
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.on('SIGUSR2', () => this.shutdown('SIGUSR2'))
|
|
40
|
-
.on('SIGTERM', () => this.shutdown('SIGTERM'));
|
|
41
|
-
}
|
|
56
|
+
/** Convert exit code to a reason string */
|
|
57
|
+
static reasonForExitCode(code: number): ShutdownReason {
|
|
58
|
+
return CODE_TO_REASON.get(code) ?? 'error';
|
|
59
|
+
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
/** Trigger a watch signal signal to a subprocess */
|
|
62
|
+
static async shutdownChild(subprocess: ChildProcess, config?: ShutdownEvent): Promise<void> {
|
|
63
|
+
subprocess?.send!({ type: 'shutdown', ...config });
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* Shutdown the application gracefully
|
|
50
68
|
*/
|
|
51
|
-
static async shutdown(
|
|
52
|
-
if (
|
|
69
|
+
static async shutdown(event?: ShutdownEvent): Promise<void> {
|
|
70
|
+
if ((event?.signal === 'SIGINT' && this.#shouldIgnoreInterrupt) || this.#controller.signal.aborted) {
|
|
53
71
|
return;
|
|
54
72
|
}
|
|
55
|
-
if (this.#controller.signal.aborted) {
|
|
56
|
-
if (this.#startedAt && (Date.now() - this.#startedAt) > 500) {
|
|
57
|
-
console.warn('Shutdown already in progress, exiting immediately', { source });
|
|
58
|
-
process.exit(0); // Quit immediately
|
|
59
|
-
} else {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
process // Allow shutdown if anything is still listening
|
|
75
|
+
.removeAllListeners('message')
|
|
76
|
+
.removeAllListeners('SIGINT')
|
|
77
|
+
.removeAllListeners('SIGTERM')
|
|
78
|
+
.removeAllListeners('SIGUSR2');
|
|
79
|
+
|
|
80
|
+
if (event?.signal === 'SIGINT' && process.stdout.isTTY) {
|
|
65
81
|
process.stdout.write('\n');
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.#controller.abort('Shutdown started');
|
|
73
|
-
await Util.queueMacroTask(); // Force the event loop to wait one cycle
|
|
74
|
-
|
|
75
|
-
console.debug('Shutdown started', ...context, { pending: this.#pending.length });
|
|
84
|
+
if (event?.reason !== undefined) {
|
|
85
|
+
const { reason } = event;
|
|
86
|
+
process.exitCode = (typeof reason === 'string' ? REASON_TO_CODE.get(reason) : reason);
|
|
87
|
+
}
|
|
76
88
|
|
|
77
89
|
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000;
|
|
78
|
-
const
|
|
79
|
-
const allPendingTasks = Promise.all(this.#pending.map(fn => Promise.resolve(fn()).catch(err => {
|
|
80
|
-
console.error('Error during shutdown task', err, ...context);
|
|
81
|
-
})));
|
|
90
|
+
const context = { ...event, pid: process.pid, timeout, pending: this.#registered.size };
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
this.#controller.abort('Shutdown started');
|
|
93
|
+
console.debug('Shutdown started', context);
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
const winner = await Promise.race([
|
|
96
|
+
Util.nonBlockingTimeout(timeout).then(() => this),
|
|
97
|
+
Promise.all([...this.#registered].map(wrapped))
|
|
98
|
+
]);
|
|
86
99
|
|
|
87
100
|
if (winner !== this) {
|
|
88
|
-
console.debug('Shutdown completed',
|
|
101
|
+
console.debug('Shutdown completed', context);
|
|
89
102
|
} else {
|
|
90
|
-
console.warn('Shutdown timed out',
|
|
103
|
+
console.warn('Shutdown timed out', context);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
await Util.blockingTimeout(stdoutDrain);
|
|
106
|
+
if (event?.exit) {
|
|
107
|
+
process.exit();
|
|
96
108
|
}
|
|
97
|
-
|
|
98
|
-
process.exit();
|
|
99
109
|
}
|
|
100
110
|
}
|
package/src/trv.d.ts
CHANGED
|
@@ -31,11 +31,6 @@ declare module "@travetto/runtime" {
|
|
|
31
31
|
* @default 2s
|
|
32
32
|
*/
|
|
33
33
|
TRV_SHUTDOWN_WAIT: TimeSpan | number;
|
|
34
|
-
/**
|
|
35
|
-
* The time to wait for stdout to drain during shutdown
|
|
36
|
-
* @default 0s
|
|
37
|
-
*/
|
|
38
|
-
TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
|
|
39
34
|
/**
|
|
40
35
|
* The desired runtime module
|
|
41
36
|
*/
|
package/src/watch.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import type { ChildProcess } from 'node:child_process';
|
|
2
|
-
|
|
3
1
|
import type { CompilerEventPayload, CompilerEventType } from '@travetto/compiler';
|
|
4
2
|
|
|
5
3
|
import { AppError } from './error.ts';
|
|
6
4
|
import { Util } from './util.ts';
|
|
7
5
|
import { RuntimeIndex } from './manifest-index.ts';
|
|
8
|
-
import { ShutdownManager } from './shutdown.ts';
|
|
6
|
+
import { ShutdownManager, type ShutdownReason } from './shutdown.ts';
|
|
9
7
|
import { castTo } from './types.ts';
|
|
10
8
|
|
|
11
9
|
type RetryRunState = {
|
|
12
10
|
iteration: number;
|
|
13
11
|
startTime: number;
|
|
14
12
|
errorIterations: number;
|
|
15
|
-
result?:
|
|
13
|
+
result?: ShutdownReason;
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
type RetryRunConfig = {
|
|
@@ -27,34 +25,6 @@ type RetryRunConfig = {
|
|
|
27
25
|
*/
|
|
28
26
|
export class WatchUtil {
|
|
29
27
|
|
|
30
|
-
static #RESTART_EXIT_CODE = 200;
|
|
31
|
-
|
|
32
|
-
/** Convert exit code to a result type */
|
|
33
|
-
static exitCodeToResult(code: number): RetryRunState['result'] {
|
|
34
|
-
return code === this.#RESTART_EXIT_CODE ? 'restart' : (code !== null && code > 0) ? 'error' : 'stop';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Exit with a restart exit code */
|
|
38
|
-
static exitWithRestart(): void {
|
|
39
|
-
ShutdownManager.shutdown('SIGTERM', this.#RESTART_EXIT_CODE);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Listen for restart signals */
|
|
43
|
-
static listenForSignals(): void {
|
|
44
|
-
ShutdownManager.disableInterrupt();
|
|
45
|
-
process.on('message', event => {
|
|
46
|
-
switch (event) {
|
|
47
|
-
case 'WATCH_RESTART': this.exitWithRestart(); break;
|
|
48
|
-
case 'WATCH_SHUTDOWN': ShutdownManager.shutdown('SIGTERM', 0); break;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Trigger a watch signal signal to a subprocess */
|
|
54
|
-
static triggerSignal(subprocess: ChildProcess, signal: 'WATCH_RESTART' | 'WATCH_SHUTDOWN'): void {
|
|
55
|
-
subprocess.connected && subprocess.send?.(signal);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
28
|
/** Compute the delay before restarting */
|
|
59
29
|
static computeRestartDelay(state: RetryRunState, config: RetryRunConfig): number {
|
|
60
30
|
return state.result === 'error'
|
|
@@ -65,7 +35,7 @@ export class WatchUtil {
|
|
|
65
35
|
/**
|
|
66
36
|
* Run with restart capability
|
|
67
37
|
*/
|
|
68
|
-
static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<
|
|
38
|
+
static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<ShutdownReason>, options?: Partial<RetryRunConfig>): Promise<void> {
|
|
69
39
|
let retryExhausted = false;
|
|
70
40
|
|
|
71
41
|
const state: RetryRunState = {
|
|
@@ -89,7 +59,7 @@ export class WatchUtil {
|
|
|
89
59
|
|
|
90
60
|
state.result = await run({ ...state, signal: ShutdownManager.signal }).catch(() => 'error' as const);
|
|
91
61
|
switch (state.result) {
|
|
92
|
-
case '
|
|
62
|
+
case 'quit': break outer;
|
|
93
63
|
case 'error': state.errorIterations += 1; break;
|
|
94
64
|
case 'restart': {
|
|
95
65
|
state.startTime = Date.now();
|
|
@@ -114,7 +84,12 @@ export class WatchUtil {
|
|
|
114
84
|
options?: Partial<RetryRunConfig>,
|
|
115
85
|
): Promise<void> {
|
|
116
86
|
const { CompilerClient } = await import('@travetto/compiler/src/server/client.ts');
|
|
117
|
-
const client = new CompilerClient(RuntimeIndex.manifest,
|
|
87
|
+
const client = new CompilerClient(RuntimeIndex.manifest, {
|
|
88
|
+
debug: (...args: unknown[]): void => console.debug(...args),
|
|
89
|
+
info: (...args: unknown[]): void => console.info(...args),
|
|
90
|
+
warn: (...args: unknown[]): void => console.warn(...args),
|
|
91
|
+
error: (...args: unknown[]): void => console.error(...args),
|
|
92
|
+
});
|
|
118
93
|
|
|
119
94
|
return this.runWithRetry(async ({ signal }) => {
|
|
120
95
|
await client.waitForState(['compile-end', 'watch-start'], undefined, signal);
|