@btravstack/core 0.14.0 → 0.16.0
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 +30 -4
- package/dist/index.cjs +216 -93
- package/dist/index.d.cts +147 -12
- package/dist/index.d.mts +147 -12
- package/dist/index.mjs +216 -94
- package/package.json +7 -8
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsyncResult, Result } from "unthrown";
|
|
1
|
+
import { AsyncResult, FailureView, Result } from "unthrown";
|
|
2
2
|
import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
4
|
//#region src/instrumentation.d.ts
|
|
@@ -47,6 +47,16 @@ export declare class HealthCheckFailed extends HealthCheckFailed_base<{
|
|
|
47
47
|
type HealthCheck = {
|
|
48
48
|
readonly name: string;
|
|
49
49
|
readonly check: () => AsyncResult<void, HealthCheckFailed>;
|
|
50
|
+
/**
|
|
51
|
+
* How long this component gets to answer, overriding the fold's own default.
|
|
52
|
+
*
|
|
53
|
+
* It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
|
|
54
|
+
* over a warm pool and an SMTP relay's greeting are two orders of magnitude
|
|
55
|
+
* apart, and one number for both is either a false unhealthy or a probe that
|
|
56
|
+
* outlives the orchestrator's patience. A component with nothing to say about
|
|
57
|
+
* its own latency omits it.
|
|
58
|
+
*/
|
|
59
|
+
readonly timeoutMs?: number;
|
|
50
60
|
};
|
|
51
61
|
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
52
62
|
/**
|
|
@@ -68,6 +78,10 @@ type HealthReport = {
|
|
|
68
78
|
readonly status: "healthy" | "unhealthy";
|
|
69
79
|
readonly components: readonly ComponentHealth[];
|
|
70
80
|
};
|
|
81
|
+
type HealthOptions = {
|
|
82
|
+
/** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
|
|
83
|
+
readonly timeoutMs?: number;
|
|
84
|
+
};
|
|
71
85
|
/**
|
|
72
86
|
* Runs every check and folds the results into one report — the whole app is
|
|
73
87
|
* unhealthy if any single component is.
|
|
@@ -80,8 +94,15 @@ type HealthReport = {
|
|
|
80
94
|
* a defecting `AsyncResult` alike are recovered into an unhealthy line —
|
|
81
95
|
* escaped, the first would reach the kernel's own `uncaughtException` handler
|
|
82
96
|
* and the second would leave `/healthz` hanging with nothing written.
|
|
97
|
+
*
|
|
98
|
+
* **A check that never settles is the third case, and it needed a deadline
|
|
99
|
+
* rather than a recovery.** Neither recovery above can see one: there is no
|
|
100
|
+
* error and no defect, only silence, so `/healthz` held a socket open per hit
|
|
101
|
+
* while kubelet timed out against a report that named nothing. Each check now
|
|
102
|
+
* races {@link HealthOptions.timeoutMs}, and the component that hung is named
|
|
103
|
+
* in the report like any other unhealthy one.
|
|
83
104
|
*/
|
|
84
|
-
export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
105
|
+
export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
|
|
85
106
|
//#endregion
|
|
86
107
|
//#region src/observability.d.ts
|
|
87
108
|
/**
|
|
@@ -236,11 +257,15 @@ type Operation = {
|
|
|
236
257
|
/**
|
|
237
258
|
* Whether a tracing observer should open a span (default `true`).
|
|
238
259
|
*
|
|
239
|
-
* `false` is for a component whose spans already come from somewhere better
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
260
|
+
* `false` is for a component whose spans already come from somewhere better —
|
|
261
|
+
* a vendor tracer reaching below what the contributor can see, where a second
|
|
262
|
+
* span would cost one more per operation for strictly less information.
|
|
263
|
+
* Counting and timing still happen.
|
|
264
|
+
*
|
|
265
|
+
* **No starter here passes `false` today.** `@btravstack/prisma` did, while
|
|
266
|
+
* Prisma 7's engine-level `@prisma/instrumentation` was the better source;
|
|
267
|
+
* Prisma 8 has no engine and ships no tracer, so its middleware is the only
|
|
268
|
+
* span there is and it opens one.
|
|
244
269
|
*
|
|
245
270
|
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
246
271
|
* why it rides the operation rather than being configured on the observer.
|
|
@@ -292,6 +317,21 @@ export declare const noObserver: () => Settle;
|
|
|
292
317
|
* twice and doubles a failure count.
|
|
293
318
|
*/
|
|
294
319
|
export declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
|
|
320
|
+
/**
|
|
321
|
+
* {@link observe}, wrapped around one call: every observer is started before
|
|
322
|
+
* `call` runs and settled from whichever channel it comes back on — `ok` on
|
|
323
|
+
* success, `error` carrying the `Err` or the defect as `cause` otherwise. The
|
|
324
|
+
* wrapper is transparent to the `Result`: whatever `call` answers is what the
|
|
325
|
+
* caller receives.
|
|
326
|
+
*
|
|
327
|
+
* `settled` replaces either default where a starter has more to say: `ok`
|
|
328
|
+
* receives the value (a cache reports a hit or a miss), `failure` the
|
|
329
|
+
* `FailureView` (a store reports a missing object as an ordinary answer).
|
|
330
|
+
*/
|
|
331
|
+
export declare const observed: <T, E>(observers: readonly ((operation: Operation) => Settle)[], operation: Operation, call: () => AsyncResult<T, E>, settled?: {
|
|
332
|
+
readonly ok?: (value: T) => Settled;
|
|
333
|
+
readonly failure?: (failure: FailureView<E, T>) => Settled;
|
|
334
|
+
}) => AsyncResult<T, E>;
|
|
295
335
|
//#endregion
|
|
296
336
|
//#region src/clock.d.ts
|
|
297
337
|
type Clock = {
|
|
@@ -425,6 +465,33 @@ type Serving<Info = never> = {
|
|
|
425
465
|
readonly drain: (signal: AbortSignal) => AsyncResult<void, never>;
|
|
426
466
|
readonly stop: () => AsyncResult<void, never>;
|
|
427
467
|
readonly info?: Info;
|
|
468
|
+
/**
|
|
469
|
+
* Settles when the runtime has stopped serving **on its own account** —
|
|
470
|
+
* nobody asked it to, and it is not coming back.
|
|
471
|
+
*
|
|
472
|
+
* Without this a runtime has no way to say so, and the kernel's lifecycle
|
|
473
|
+
* only ever moves on a signal or a `stop()` call. So a Temporal worker whose
|
|
474
|
+
* `run()` rejected mid-flight, or an AMQP consumer the server cancelled and
|
|
475
|
+
* nothing re-subscribed, left the process alive and `/readyz` answering
|
|
476
|
+
* `200` — a pod in a Service's endpoints, consuming nothing. The orchestrator
|
|
477
|
+
* has no way to learn that on its own: liveness answers from `building`
|
|
478
|
+
* onward, and readiness is the lifecycle's, not the transport's.
|
|
479
|
+
*
|
|
480
|
+
* The kernel races it against its own shutdown deferred and reports
|
|
481
|
+
* `reason: "runtimeStopped"`, which `runMain` exits `1` for — a restart,
|
|
482
|
+
* which is the right answer to a transport that has given up.
|
|
483
|
+
*
|
|
484
|
+
* **Optional, and a runtime that cannot know omits it.** An HTTP server that
|
|
485
|
+
* is listening is serving; there is no third state for it to report, and a
|
|
486
|
+
* field it had to write `OkAsync()` into would be a promise it could not
|
|
487
|
+
* keep. A `never`-settling `AsyncResult` is the other honest spelling for a
|
|
488
|
+
* runtime that could report this and has nothing to say yet.
|
|
489
|
+
*
|
|
490
|
+
* It says nothing about WHY. The runtime that knows writes that line itself,
|
|
491
|
+
* through `Observers` or its own diagnostics; this channel exists so the
|
|
492
|
+
* process ends, not so the reason travels.
|
|
493
|
+
*/
|
|
494
|
+
readonly stopped?: () => AsyncResult<void, never>;
|
|
428
495
|
};
|
|
429
496
|
type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
430
497
|
readonly name: string;
|
|
@@ -512,7 +579,28 @@ type DrainReport = {
|
|
|
512
579
|
* reporting, where `inFlightAtStart - abandoned` can go negative.
|
|
513
580
|
*/
|
|
514
581
|
readonly completed: number;
|
|
515
|
-
/**
|
|
582
|
+
/**
|
|
583
|
+
* Units still open at the deadline. The exit-code decision reads this.
|
|
584
|
+
*
|
|
585
|
+
* **It means "no longer AWAITED", not "did not finish".** The kernel stops
|
|
586
|
+
* waiting and aborts each unit's signal; it cannot cancel work, and the
|
|
587
|
+
* transport underneath keeps running on its own clock. An abandoned AMQP
|
|
588
|
+
* delivery usually goes on to be acked, and an abandoned Temporal activity
|
|
589
|
+
* goes on to complete — after this report said they did not.
|
|
590
|
+
*
|
|
591
|
+
* **The consequence, stated because nothing else states it: the process may
|
|
592
|
+
* not end by itself.** `runMain` sets an exit code and deliberately never
|
|
593
|
+
* calls `process.exit()`, so the process ends when the event loop empties —
|
|
594
|
+
* and a transport still winding down is holding it open. Nothing here
|
|
595
|
+
* terminates it.
|
|
596
|
+
*
|
|
597
|
+
* Under a Kubernetes-initiated shutdown that ends at
|
|
598
|
+
* `terminationGracePeriodSeconds`, with SIGKILL, so exit `2` is what the
|
|
599
|
+
* report SAYS rather than what the orchestrator observes. Outside that
|
|
600
|
+
* lifecycle there is no SIGKILL: the process simply stays alive until the
|
|
601
|
+
* transport is done. `kubectl logs --previous` carries the report either
|
|
602
|
+
* way, which is what it is for.
|
|
603
|
+
*/
|
|
516
604
|
readonly abandoned: number;
|
|
517
605
|
};
|
|
518
606
|
//#endregion
|
|
@@ -547,6 +635,18 @@ type KernelEvent = {
|
|
|
547
635
|
} | {
|
|
548
636
|
readonly type: "drained";
|
|
549
637
|
readonly report: DrainReport;
|
|
638
|
+
} | {
|
|
639
|
+
/**
|
|
640
|
+
* The kernel stopped WAITING for a phase that has no deadline of its own,
|
|
641
|
+
* and reported anyway. Without it a stop that ran long and a stop that
|
|
642
|
+
* finished are the same two lines on stderr (`stopping`, `exited`), which
|
|
643
|
+
* is the silence `ExitReport.abandonedAt` exists to end.
|
|
644
|
+
*/
|
|
645
|
+
readonly type: "stoppedWaiting";
|
|
646
|
+
/** `"stop"` for `Serving.stop` and the finalisers, `"build"` for a graph that never served. */
|
|
647
|
+
readonly phase: "build" | "stop";
|
|
648
|
+
/** The deadline that expired, or `undefined` when a second signal cut the wait short. */
|
|
649
|
+
readonly afterMs: number | undefined;
|
|
550
650
|
} | {
|
|
551
651
|
readonly type: "stopping";
|
|
552
652
|
} | {
|
|
@@ -575,22 +675,41 @@ type ExitReport = {
|
|
|
575
675
|
readonly drain: DrainReport | undefined;
|
|
576
676
|
readonly teardownErrors: readonly TeardownError[];
|
|
577
677
|
readonly uptimeMs: number;
|
|
678
|
+
/**
|
|
679
|
+
* Set when the kernel stopped WAITING for a phase that has no deadline of its
|
|
680
|
+
* own — `"stop"` for a `Serving.stop` or a finaliser still running at
|
|
681
|
+
* `stopTimeoutMs`, `"build"` for a graph abandoned before it ever served.
|
|
682
|
+
*
|
|
683
|
+
* It is "stopped waiting", not "cancelled": nothing here can cancel a
|
|
684
|
+
* finaliser, so a wedged one can still hold the event loop past this report
|
|
685
|
+
* and end in SIGKILL. What the field buys is that the report EXISTS and says
|
|
686
|
+
* which phase ran long — where the alternative was a process stuck in
|
|
687
|
+
* `stopping` with no event, no code and no exit report at all.
|
|
688
|
+
*/
|
|
689
|
+
readonly abandonedAt?: "build" | "stop";
|
|
578
690
|
};
|
|
579
691
|
type StartOptions = {
|
|
580
692
|
/**
|
|
581
693
|
* The environment the graph is configured from, provided to it as the `Env`
|
|
582
|
-
* port and read for the kernel's own
|
|
583
|
-
* `process.env`.
|
|
694
|
+
* port and read for the kernel's own variables. Defaults to `process.env`.
|
|
584
695
|
*/
|
|
585
696
|
readonly env?: Environment;
|
|
586
697
|
readonly clock?: Clock;
|
|
587
698
|
readonly signals?: boolean;
|
|
588
699
|
/**
|
|
589
|
-
* The probe server's port. Unset,
|
|
590
|
-
* (default `9000`)
|
|
700
|
+
* The probe server's port and interface. Unset, they are bound from
|
|
701
|
+
* `PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`) in
|
|
702
|
+
* `env`; `false` disables the probe server and reads neither.
|
|
703
|
+
*
|
|
704
|
+
* `0.0.0.0` matches `HOST`'s own default for `HOST`'s own reason: a kubelet
|
|
705
|
+
* `httpGet` probe connects over the POD IP, so the loopback-only bind this
|
|
706
|
+
* used to hardcode could not answer the probe shape every manifest reaches
|
|
707
|
+
* for first. Pin `host: "127.0.0.1"` where the port is shared with something
|
|
708
|
+
* else on the host, and probe it with `exec` from inside the container.
|
|
591
709
|
*/
|
|
592
710
|
readonly probes?: {
|
|
593
711
|
readonly port: number;
|
|
712
|
+
readonly host?: string;
|
|
594
713
|
} | false;
|
|
595
714
|
/**
|
|
596
715
|
* How long readiness stays false before the runtime is told to stop
|
|
@@ -608,6 +727,21 @@ type StartOptions = {
|
|
|
608
727
|
* together, in the same manifest.
|
|
609
728
|
*/
|
|
610
729
|
readonly drainTimeoutMs?: number;
|
|
730
|
+
/**
|
|
731
|
+
* How long the kernel waits for `Serving.stop` and the application scope's
|
|
732
|
+
* finalisers together before it stops waiting and reports
|
|
733
|
+
* `ExitReport.abandonedAt: "stop"`. Unset, it is bound from
|
|
734
|
+
* `STOP_TIMEOUT_MS` in `env` (default `5_000`).
|
|
735
|
+
*
|
|
736
|
+
* It exists because beat 3's deadline covers in-flight WORK and nothing
|
|
737
|
+
* covered the teardown: a `release` that never settles — a socket to a host
|
|
738
|
+
* that stopped answering — left the phase at `stopping` with no exit report,
|
|
739
|
+
* which is the one artefact the lifecycle exists to produce. The three
|
|
740
|
+
* timings are cumulative against the pod's
|
|
741
|
+
* `terminationGracePeriodSeconds`, so the defaults sum to 30 s exactly
|
|
742
|
+
* (`5_000 + 20_000 + 5_000`): raise one and raise the grace period with it.
|
|
743
|
+
*/
|
|
744
|
+
readonly stopTimeoutMs?: number;
|
|
611
745
|
readonly onEvent?: EventSink;
|
|
612
746
|
};
|
|
613
747
|
type RunningApp<E, Info = never> = {
|
|
@@ -690,6 +824,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
690
824
|
* | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
|
|
691
825
|
* | drained with work abandoned | `2` |
|
|
692
826
|
* | exited with teardown errors | `2` |
|
|
827
|
+
* | stopped waiting for the teardown or a build (`ExitReport.abandonedAt`) | `2` |
|
|
693
828
|
* | stopped by an uncaught exception or unhandled rejection | `70` |
|
|
694
829
|
* | a defect | `70` |
|
|
695
830
|
*
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnyPort, Context, DependencyGate, Module, PortClass, PortInstance, Scope, SeedEntry, ServiceOf } from "@btravstack/di";
|
|
2
|
-
import { AsyncResult, Result } from "unthrown";
|
|
2
|
+
import { AsyncResult, FailureView, Result } from "unthrown";
|
|
3
3
|
import { Env, Environment } from "@btravstack/config";
|
|
4
4
|
//#region src/instrumentation.d.ts
|
|
5
5
|
/**
|
|
@@ -47,6 +47,16 @@ export declare class HealthCheckFailed extends HealthCheckFailed_base<{
|
|
|
47
47
|
type HealthCheck = {
|
|
48
48
|
readonly name: string;
|
|
49
49
|
readonly check: () => AsyncResult<void, HealthCheckFailed>;
|
|
50
|
+
/**
|
|
51
|
+
* How long this component gets to answer, overriding the fold's own default.
|
|
52
|
+
*
|
|
53
|
+
* It is on the CONTRIBUTION because only the contributor knows: a `SELECT 1`
|
|
54
|
+
* over a warm pool and an SMTP relay's greeting are two orders of magnitude
|
|
55
|
+
* apart, and one number for both is either a false unhealthy or a probe that
|
|
56
|
+
* outlives the orchestrator's patience. A component with nothing to say about
|
|
57
|
+
* its own latency omits it.
|
|
58
|
+
*/
|
|
59
|
+
readonly timeoutMs?: number;
|
|
50
60
|
};
|
|
51
61
|
declare const HealthChecks_base: import("@btravstack/di").ManyPortClass<"HealthChecks">;
|
|
52
62
|
/**
|
|
@@ -68,6 +78,10 @@ type HealthReport = {
|
|
|
68
78
|
readonly status: "healthy" | "unhealthy";
|
|
69
79
|
readonly components: readonly ComponentHealth[];
|
|
70
80
|
};
|
|
81
|
+
type HealthOptions = {
|
|
82
|
+
/** Per check, not for the fold: the checks run concurrently. Default {@link DEFAULT_HEALTH_TIMEOUT_MS}. */
|
|
83
|
+
readonly timeoutMs?: number;
|
|
84
|
+
};
|
|
71
85
|
/**
|
|
72
86
|
* Runs every check and folds the results into one report — the whole app is
|
|
73
87
|
* unhealthy if any single component is.
|
|
@@ -80,8 +94,15 @@ type HealthReport = {
|
|
|
80
94
|
* a defecting `AsyncResult` alike are recovered into an unhealthy line —
|
|
81
95
|
* escaped, the first would reach the kernel's own `uncaughtException` handler
|
|
82
96
|
* and the second would leave `/healthz` hanging with nothing written.
|
|
97
|
+
*
|
|
98
|
+
* **A check that never settles is the third case, and it needed a deadline
|
|
99
|
+
* rather than a recovery.** Neither recovery above can see one: there is no
|
|
100
|
+
* error and no defect, only silence, so `/healthz` held a socket open per hit
|
|
101
|
+
* while kubelet timed out against a report that named nothing. Each check now
|
|
102
|
+
* races {@link HealthOptions.timeoutMs}, and the component that hung is named
|
|
103
|
+
* in the report like any other unhealthy one.
|
|
83
104
|
*/
|
|
84
|
-
export declare const runHealthChecks: (checks: readonly HealthCheck[]) => AsyncResult<HealthReport, never>;
|
|
105
|
+
export declare const runHealthChecks: (checks: readonly HealthCheck[], options?: HealthOptions) => AsyncResult<HealthReport, never>;
|
|
85
106
|
//#endregion
|
|
86
107
|
//#region src/observability.d.ts
|
|
87
108
|
/**
|
|
@@ -236,11 +257,15 @@ type Operation = {
|
|
|
236
257
|
/**
|
|
237
258
|
* Whether a tracing observer should open a span (default `true`).
|
|
238
259
|
*
|
|
239
|
-
* `false` is for a component whose spans already come from somewhere better
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
260
|
+
* `false` is for a component whose spans already come from somewhere better —
|
|
261
|
+
* a vendor tracer reaching below what the contributor can see, where a second
|
|
262
|
+
* span would cost one more per operation for strictly less information.
|
|
263
|
+
* Counting and timing still happen.
|
|
264
|
+
*
|
|
265
|
+
* **No starter here passes `false` today.** `@btravstack/prisma` did, while
|
|
266
|
+
* Prisma 7's engine-level `@prisma/instrumentation` was the better source;
|
|
267
|
+
* Prisma 8 has no engine and ships no tracer, so its middleware is the only
|
|
268
|
+
* span there is and it opens one.
|
|
244
269
|
*
|
|
245
270
|
* Whether a span is worth opening is the CONTRIBUTOR's knowledge, which is
|
|
246
271
|
* why it rides the operation rather than being configured on the observer.
|
|
@@ -292,6 +317,21 @@ export declare const noObserver: () => Settle;
|
|
|
292
317
|
* twice and doubles a failure count.
|
|
293
318
|
*/
|
|
294
319
|
export declare const observe: (observers: readonly ((operation: Operation) => Settle)[], operation: Operation) => Settle;
|
|
320
|
+
/**
|
|
321
|
+
* {@link observe}, wrapped around one call: every observer is started before
|
|
322
|
+
* `call` runs and settled from whichever channel it comes back on — `ok` on
|
|
323
|
+
* success, `error` carrying the `Err` or the defect as `cause` otherwise. The
|
|
324
|
+
* wrapper is transparent to the `Result`: whatever `call` answers is what the
|
|
325
|
+
* caller receives.
|
|
326
|
+
*
|
|
327
|
+
* `settled` replaces either default where a starter has more to say: `ok`
|
|
328
|
+
* receives the value (a cache reports a hit or a miss), `failure` the
|
|
329
|
+
* `FailureView` (a store reports a missing object as an ordinary answer).
|
|
330
|
+
*/
|
|
331
|
+
export declare const observed: <T, E>(observers: readonly ((operation: Operation) => Settle)[], operation: Operation, call: () => AsyncResult<T, E>, settled?: {
|
|
332
|
+
readonly ok?: (value: T) => Settled;
|
|
333
|
+
readonly failure?: (failure: FailureView<E, T>) => Settled;
|
|
334
|
+
}) => AsyncResult<T, E>;
|
|
295
335
|
//#endregion
|
|
296
336
|
//#region src/clock.d.ts
|
|
297
337
|
type Clock = {
|
|
@@ -425,6 +465,33 @@ type Serving<Info = never> = {
|
|
|
425
465
|
readonly drain: (signal: AbortSignal) => AsyncResult<void, never>;
|
|
426
466
|
readonly stop: () => AsyncResult<void, never>;
|
|
427
467
|
readonly info?: Info;
|
|
468
|
+
/**
|
|
469
|
+
* Settles when the runtime has stopped serving **on its own account** —
|
|
470
|
+
* nobody asked it to, and it is not coming back.
|
|
471
|
+
*
|
|
472
|
+
* Without this a runtime has no way to say so, and the kernel's lifecycle
|
|
473
|
+
* only ever moves on a signal or a `stop()` call. So a Temporal worker whose
|
|
474
|
+
* `run()` rejected mid-flight, or an AMQP consumer the server cancelled and
|
|
475
|
+
* nothing re-subscribed, left the process alive and `/readyz` answering
|
|
476
|
+
* `200` — a pod in a Service's endpoints, consuming nothing. The orchestrator
|
|
477
|
+
* has no way to learn that on its own: liveness answers from `building`
|
|
478
|
+
* onward, and readiness is the lifecycle's, not the transport's.
|
|
479
|
+
*
|
|
480
|
+
* The kernel races it against its own shutdown deferred and reports
|
|
481
|
+
* `reason: "runtimeStopped"`, which `runMain` exits `1` for — a restart,
|
|
482
|
+
* which is the right answer to a transport that has given up.
|
|
483
|
+
*
|
|
484
|
+
* **Optional, and a runtime that cannot know omits it.** An HTTP server that
|
|
485
|
+
* is listening is serving; there is no third state for it to report, and a
|
|
486
|
+
* field it had to write `OkAsync()` into would be a promise it could not
|
|
487
|
+
* keep. A `never`-settling `AsyncResult` is the other honest spelling for a
|
|
488
|
+
* runtime that could report this and has nothing to say yet.
|
|
489
|
+
*
|
|
490
|
+
* It says nothing about WHY. The runtime that knows writes that line itself,
|
|
491
|
+
* through `Observers` or its own diagnostics; this channel exists so the
|
|
492
|
+
* process ends, not so the reason travels.
|
|
493
|
+
*/
|
|
494
|
+
readonly stopped?: () => AsyncResult<void, never>;
|
|
428
495
|
};
|
|
429
496
|
type Runtime<Resolves extends AnyPort = never, Info = never> = {
|
|
430
497
|
readonly name: string;
|
|
@@ -512,7 +579,28 @@ type DrainReport = {
|
|
|
512
579
|
* reporting, where `inFlightAtStart - abandoned` can go negative.
|
|
513
580
|
*/
|
|
514
581
|
readonly completed: number;
|
|
515
|
-
/**
|
|
582
|
+
/**
|
|
583
|
+
* Units still open at the deadline. The exit-code decision reads this.
|
|
584
|
+
*
|
|
585
|
+
* **It means "no longer AWAITED", not "did not finish".** The kernel stops
|
|
586
|
+
* waiting and aborts each unit's signal; it cannot cancel work, and the
|
|
587
|
+
* transport underneath keeps running on its own clock. An abandoned AMQP
|
|
588
|
+
* delivery usually goes on to be acked, and an abandoned Temporal activity
|
|
589
|
+
* goes on to complete — after this report said they did not.
|
|
590
|
+
*
|
|
591
|
+
* **The consequence, stated because nothing else states it: the process may
|
|
592
|
+
* not end by itself.** `runMain` sets an exit code and deliberately never
|
|
593
|
+
* calls `process.exit()`, so the process ends when the event loop empties —
|
|
594
|
+
* and a transport still winding down is holding it open. Nothing here
|
|
595
|
+
* terminates it.
|
|
596
|
+
*
|
|
597
|
+
* Under a Kubernetes-initiated shutdown that ends at
|
|
598
|
+
* `terminationGracePeriodSeconds`, with SIGKILL, so exit `2` is what the
|
|
599
|
+
* report SAYS rather than what the orchestrator observes. Outside that
|
|
600
|
+
* lifecycle there is no SIGKILL: the process simply stays alive until the
|
|
601
|
+
* transport is done. `kubectl logs --previous` carries the report either
|
|
602
|
+
* way, which is what it is for.
|
|
603
|
+
*/
|
|
516
604
|
readonly abandoned: number;
|
|
517
605
|
};
|
|
518
606
|
//#endregion
|
|
@@ -547,6 +635,18 @@ type KernelEvent = {
|
|
|
547
635
|
} | {
|
|
548
636
|
readonly type: "drained";
|
|
549
637
|
readonly report: DrainReport;
|
|
638
|
+
} | {
|
|
639
|
+
/**
|
|
640
|
+
* The kernel stopped WAITING for a phase that has no deadline of its own,
|
|
641
|
+
* and reported anyway. Without it a stop that ran long and a stop that
|
|
642
|
+
* finished are the same two lines on stderr (`stopping`, `exited`), which
|
|
643
|
+
* is the silence `ExitReport.abandonedAt` exists to end.
|
|
644
|
+
*/
|
|
645
|
+
readonly type: "stoppedWaiting";
|
|
646
|
+
/** `"stop"` for `Serving.stop` and the finalisers, `"build"` for a graph that never served. */
|
|
647
|
+
readonly phase: "build" | "stop";
|
|
648
|
+
/** The deadline that expired, or `undefined` when a second signal cut the wait short. */
|
|
649
|
+
readonly afterMs: number | undefined;
|
|
550
650
|
} | {
|
|
551
651
|
readonly type: "stopping";
|
|
552
652
|
} | {
|
|
@@ -575,22 +675,41 @@ type ExitReport = {
|
|
|
575
675
|
readonly drain: DrainReport | undefined;
|
|
576
676
|
readonly teardownErrors: readonly TeardownError[];
|
|
577
677
|
readonly uptimeMs: number;
|
|
678
|
+
/**
|
|
679
|
+
* Set when the kernel stopped WAITING for a phase that has no deadline of its
|
|
680
|
+
* own — `"stop"` for a `Serving.stop` or a finaliser still running at
|
|
681
|
+
* `stopTimeoutMs`, `"build"` for a graph abandoned before it ever served.
|
|
682
|
+
*
|
|
683
|
+
* It is "stopped waiting", not "cancelled": nothing here can cancel a
|
|
684
|
+
* finaliser, so a wedged one can still hold the event loop past this report
|
|
685
|
+
* and end in SIGKILL. What the field buys is that the report EXISTS and says
|
|
686
|
+
* which phase ran long — where the alternative was a process stuck in
|
|
687
|
+
* `stopping` with no event, no code and no exit report at all.
|
|
688
|
+
*/
|
|
689
|
+
readonly abandonedAt?: "build" | "stop";
|
|
578
690
|
};
|
|
579
691
|
type StartOptions = {
|
|
580
692
|
/**
|
|
581
693
|
* The environment the graph is configured from, provided to it as the `Env`
|
|
582
|
-
* port and read for the kernel's own
|
|
583
|
-
* `process.env`.
|
|
694
|
+
* port and read for the kernel's own variables. Defaults to `process.env`.
|
|
584
695
|
*/
|
|
585
696
|
readonly env?: Environment;
|
|
586
697
|
readonly clock?: Clock;
|
|
587
698
|
readonly signals?: boolean;
|
|
588
699
|
/**
|
|
589
|
-
* The probe server's port. Unset,
|
|
590
|
-
* (default `9000`)
|
|
700
|
+
* The probe server's port and interface. Unset, they are bound from
|
|
701
|
+
* `PROBE_PORT` (default `9000`) and `PROBE_HOST` (default `0.0.0.0`) in
|
|
702
|
+
* `env`; `false` disables the probe server and reads neither.
|
|
703
|
+
*
|
|
704
|
+
* `0.0.0.0` matches `HOST`'s own default for `HOST`'s own reason: a kubelet
|
|
705
|
+
* `httpGet` probe connects over the POD IP, so the loopback-only bind this
|
|
706
|
+
* used to hardcode could not answer the probe shape every manifest reaches
|
|
707
|
+
* for first. Pin `host: "127.0.0.1"` where the port is shared with something
|
|
708
|
+
* else on the host, and probe it with `exec` from inside the container.
|
|
591
709
|
*/
|
|
592
710
|
readonly probes?: {
|
|
593
711
|
readonly port: number;
|
|
712
|
+
readonly host?: string;
|
|
594
713
|
} | false;
|
|
595
714
|
/**
|
|
596
715
|
* How long readiness stays false before the runtime is told to stop
|
|
@@ -608,6 +727,21 @@ type StartOptions = {
|
|
|
608
727
|
* together, in the same manifest.
|
|
609
728
|
*/
|
|
610
729
|
readonly drainTimeoutMs?: number;
|
|
730
|
+
/**
|
|
731
|
+
* How long the kernel waits for `Serving.stop` and the application scope's
|
|
732
|
+
* finalisers together before it stops waiting and reports
|
|
733
|
+
* `ExitReport.abandonedAt: "stop"`. Unset, it is bound from
|
|
734
|
+
* `STOP_TIMEOUT_MS` in `env` (default `5_000`).
|
|
735
|
+
*
|
|
736
|
+
* It exists because beat 3's deadline covers in-flight WORK and nothing
|
|
737
|
+
* covered the teardown: a `release` that never settles — a socket to a host
|
|
738
|
+
* that stopped answering — left the phase at `stopping` with no exit report,
|
|
739
|
+
* which is the one artefact the lifecycle exists to produce. The three
|
|
740
|
+
* timings are cumulative against the pod's
|
|
741
|
+
* `terminationGracePeriodSeconds`, so the defaults sum to 30 s exactly
|
|
742
|
+
* (`5_000 + 20_000 + 5_000`): raise one and raise the grace period with it.
|
|
743
|
+
*/
|
|
744
|
+
readonly stopTimeoutMs?: number;
|
|
611
745
|
readonly onEvent?: EventSink;
|
|
612
746
|
};
|
|
613
747
|
type RunningApp<E, Info = never> = {
|
|
@@ -690,6 +824,7 @@ export declare const start: <X, E, N>(module: Module<X, E, N> & StartGate<X, N>,
|
|
|
690
824
|
* | a configuration port that could not be bound (`ConfigInvalid`, `PROBE_PORT` included) | `78` |
|
|
691
825
|
* | drained with work abandoned | `2` |
|
|
692
826
|
* | exited with teardown errors | `2` |
|
|
827
|
+
* | stopped waiting for the teardown or a build (`ExitReport.abandonedAt`) | `2` |
|
|
693
828
|
* | stopped by an uncaught exception or unhandled rejection | `70` |
|
|
694
829
|
* | a defect | `70` |
|
|
695
830
|
*
|