@ultimat3/core 27.6.0 → 27.7.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 +4 -1
- package/package.json +2 -2
- package/src/index.ts +5 -1
- package/src/lifecycle-readiness.ts +35 -10
- package/src/lifecycle.ts +11 -3
package/README.md
CHANGED
|
@@ -494,7 +494,10 @@ match with `sealAll()`; uniqueness cannot be held across keys.
|
|
|
494
494
|
**bound**. `ReadinessCheck` is `() => boolean` and must stay synchronous — a probe that awaits its
|
|
495
495
|
dependency turns a slow dependency into a wedged endpoint and then a restart loop; keep a boolean
|
|
496
496
|
fresh and let the check read it. It returns an unregister. `HealthReport.checks` is a map of name
|
|
497
|
-
→ `'ok' | 'failing'`, so "alert on check failures by check name" is writable.
|
|
497
|
+
→ `'ok' | 'degraded' | 'failing'`, so "alert on check failures by check name" is writable.
|
|
498
|
+
`registerReadinessCheck(name, check, { onFailure: 'degraded' })` is a dependency the role serves
|
|
499
|
+
without (the bus of a role that only publishes to it): its `false` is `'degraded'`, never a 503
|
|
500
|
+
on `/readyz` in either mode, and a 503 on `/readyz?deep=1`.
|
|
498
501
|
- **`HealthReport.registered` is the third state.** `checks: {}` reads identically for "every check
|
|
499
502
|
passed" and "nobody registered one", and an **empty registry is still ready** — reported, never
|
|
500
503
|
enforced, so a role with no dependency does not have to invent a check to boot. Read `registered`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.7.0",
|
|
4
4
|
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ultimat3/schema": "27.
|
|
40
|
+
"@ultimat3/schema": "27.7.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/index.ts
CHANGED
|
@@ -646,7 +646,11 @@ export {
|
|
|
646
646
|
READINESS_GRACE_DEFAULT_MS,
|
|
647
647
|
READINESS_GRACE_MAX_MS,
|
|
648
648
|
} from './lifecycle-grace';
|
|
649
|
-
export type {
|
|
649
|
+
export type {
|
|
650
|
+
ReadinessCheck,
|
|
651
|
+
ReadinessCheckOptions,
|
|
652
|
+
ReadinessStatus,
|
|
653
|
+
} from './lifecycle-readiness';
|
|
650
654
|
export type { SignalHandlerOptions } from './lifecycle-signals';
|
|
651
655
|
export { drainSignals, installSignalHandlers } from './lifecycle-signals';
|
|
652
656
|
export { isSelfOrigin, listeningOrigins, markListening, resetListeners } from './listeners';
|
|
@@ -4,7 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { UltimateError } from './errors';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* `'degraded'` is a failed check the role serves WITHOUT (`onFailure: 'degraded'`): reported by
|
|
9
|
+
* name, never a 503 on `/readyz` in either mode, and a 503 on `/readyz?deep=1` — the probe routes
|
|
10
|
+
* traffic on the first and a monitor alerts on the second.
|
|
11
|
+
*/
|
|
12
|
+
export type ReadinessStatus = 'ok' | 'degraded' | 'failing';
|
|
13
|
+
|
|
14
|
+
export interface ReadinessCheckOptions {
|
|
15
|
+
/**
|
|
16
|
+
* What a `false` (or a throw) from this check is. `'failing'`, the default, is a dependency the
|
|
17
|
+
* role cannot serve without. `'degraded'` is one it can — the bus of a role that only publishes
|
|
18
|
+
* to it: pulling every replica from the ingress for a dependency no served request waits on is
|
|
19
|
+
* the outage the probe exists to prevent.
|
|
20
|
+
*/
|
|
21
|
+
readonly onFailure?: 'failing' | 'degraded' | undefined;
|
|
22
|
+
}
|
|
8
23
|
|
|
9
24
|
/**
|
|
10
25
|
* Synchronous, and that is the design, not a limitation. **Do not widen this to
|
|
@@ -23,13 +38,22 @@ export type ReadinessStatus = 'ok' | 'failing';
|
|
|
23
38
|
*/
|
|
24
39
|
export type ReadinessCheck = () => boolean;
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
interface Registered {
|
|
42
|
+
readonly check: ReadinessCheck;
|
|
43
|
+
readonly onFailure: 'failing' | 'degraded';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checks = new Map<string, Registered>();
|
|
27
47
|
|
|
28
48
|
/**
|
|
29
49
|
* Register a named readiness check. Returns its unregister — the same shape as `onShutdown`, and
|
|
30
50
|
* owned by whoever can be started twice, for the same reason.
|
|
31
51
|
*/
|
|
32
|
-
export function registerReadinessCheck(
|
|
52
|
+
export function registerReadinessCheck(
|
|
53
|
+
name: string,
|
|
54
|
+
check: ReadinessCheck,
|
|
55
|
+
options: ReadinessCheckOptions = {},
|
|
56
|
+
): () => void {
|
|
33
57
|
if (checks.has(name)) {
|
|
34
58
|
throw new UltimateError({
|
|
35
59
|
code: 'X_READINESS_CHECK_DUPLICATE',
|
|
@@ -38,9 +62,10 @@ export function registerReadinessCheck(name: string, check: ReadinessCheck): ()
|
|
|
38
62
|
meta: { name },
|
|
39
63
|
});
|
|
40
64
|
}
|
|
41
|
-
|
|
65
|
+
const registered: Registered = { check, onFailure: options.onFailure ?? 'failing' };
|
|
66
|
+
checks.set(name, registered);
|
|
42
67
|
return () => {
|
|
43
|
-
if (checks.get(name) ===
|
|
68
|
+
if (checks.get(name) === registered) checks.delete(name);
|
|
44
69
|
};
|
|
45
70
|
}
|
|
46
71
|
|
|
@@ -55,8 +80,8 @@ export function clearReadinessChecks(): void {
|
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
/**
|
|
58
|
-
* Every check, run now, by name. A check that throws is `failing`
|
|
59
|
-
* and is handed to `onThrow`, which `lifecycle.ts` routes through its total `report`.
|
|
83
|
+
* Every check, run now, by name. A check that throws is its `onFailure` (`failing` unless it was
|
|
84
|
+
* registered degradable) — never an unhandled error — and is handed to `onThrow`, which `lifecycle.ts` routes through its total `report`.
|
|
60
85
|
*
|
|
61
86
|
* Built through `Object.fromEntries`, never by assigning `results[name]`: assignment to the one
|
|
62
87
|
* name `__proto__` sets the PROTOTYPE instead of adding a key, so that check vanished from the
|
|
@@ -67,11 +92,11 @@ export function runReadinessChecks(
|
|
|
67
92
|
onThrow: (name: string, thrown: unknown) => void,
|
|
68
93
|
): Readonly<Record<string, ReadinessStatus>> {
|
|
69
94
|
const results: [string, ReadinessStatus][] = [];
|
|
70
|
-
for (const [name, check] of checks) {
|
|
95
|
+
for (const [name, { check, onFailure }] of checks) {
|
|
71
96
|
try {
|
|
72
|
-
results.push([name, check() ? 'ok' :
|
|
97
|
+
results.push([name, check() ? 'ok' : onFailure]);
|
|
73
98
|
} catch (thrown) {
|
|
74
|
-
results.push([name,
|
|
99
|
+
results.push([name, onFailure]);
|
|
75
100
|
onThrow(name, thrown);
|
|
76
101
|
}
|
|
77
102
|
}
|
package/src/lifecycle.ts
CHANGED
|
@@ -374,9 +374,15 @@ export function drain(signal = 'manual'): Promise<void> {
|
|
|
374
374
|
return drainPromise;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
export function healthReport(
|
|
377
|
+
export function healthReport(
|
|
378
|
+
mode: ReadinessMode = readinessMode,
|
|
379
|
+
/** `/readyz?deep=1`: a `'degraded'` check is not ready either. Never the probe's reading. */
|
|
380
|
+
strict = false,
|
|
381
|
+
): HealthReport {
|
|
378
382
|
const checks = readinessChecks();
|
|
379
|
-
const dependencies = Object.values(checks).every(
|
|
383
|
+
const dependencies = Object.values(checks).every(
|
|
384
|
+
(status) => status === 'ok' || (!strict && status === 'degraded'),
|
|
385
|
+
);
|
|
380
386
|
return {
|
|
381
387
|
state,
|
|
382
388
|
// `ready` is the same predicate `/readyz` answers on, so a body and its status can never
|
|
@@ -405,9 +411,11 @@ export function healthzPayload(): HealthPayload {
|
|
|
405
411
|
/**
|
|
406
412
|
* Readiness: may this instance receive traffic? 503 while starting or draining, and — in the
|
|
407
413
|
* default `'dependencies'` mode, or always with `deep` (`/readyz?deep=1`) — while any check fails.
|
|
414
|
+
* A `'degraded'` check (`ReadinessCheckOptions.onFailure`) is a 503 under `deep` ALONE.
|
|
408
415
|
*/
|
|
409
416
|
export function readyzPayload(options: { readonly deep?: boolean } = {}): HealthPayload {
|
|
410
|
-
const
|
|
417
|
+
const deep = options.deep === true;
|
|
418
|
+
const body = healthReport(deep ? 'dependencies' : readinessMode, deep);
|
|
411
419
|
return { ok: body.ready, status: body.ready ? 200 : 503, body };
|
|
412
420
|
}
|
|
413
421
|
|