@serfab/cadre-host 1.13.0 → 1.15.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 +347 -423
- package/dist/bin/host.d.ts +6 -3
- package/dist/bin/host.js +332 -524
- package/dist/bin/host.js.map +1 -1
- package/dist/bin/join-output.d.ts +56 -0
- package/dist/bin/join-output.js +92 -0
- package/dist/bin/join-output.js.map +1 -0
- package/dist/bin/nat-output.d.ts +54 -0
- package/dist/bin/nat-output.js +138 -0
- package/dist/bin/nat-output.js.map +1 -0
- package/dist/bin/ui-access.d.ts +22 -0
- package/dist/bin/ui-access.js +44 -0
- package/dist/bin/ui-access.js.map +1 -0
- package/dist/hosted/claim-details.d.ts +66 -0
- package/dist/hosted/claim-details.js +60 -0
- package/dist/hosted/claim-details.js.map +1 -0
- package/dist/hosted/hosted-node-service.d.ts +325 -0
- package/dist/hosted/hosted-node-service.js +678 -0
- package/dist/hosted/hosted-node-service.js.map +1 -0
- package/dist/hosted/hosted-node-store.d.ts +40 -0
- package/dist/hosted/hosted-node-store.js +126 -0
- package/dist/hosted/hosted-node-store.js.map +1 -0
- package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
- package/dist/hosted/hosted-node-supervisor.js +396 -0
- package/dist/hosted/hosted-node-supervisor.js.map +1 -0
- package/dist/hosted/hosted-node-watcher.d.ts +95 -0
- package/dist/hosted/hosted-node-watcher.js +260 -0
- package/dist/hosted/hosted-node-watcher.js.map +1 -0
- package/dist/hosted/index.d.ts +16 -0
- package/dist/hosted/index.js +13 -0
- package/dist/hosted/index.js.map +1 -0
- package/dist/hosted/node-status.d.ts +51 -0
- package/dist/hosted/node-status.js +77 -0
- package/dist/hosted/node-status.js.map +1 -0
- package/dist/hosted/types.d.ts +141 -0
- package/dist/hosted/types.js +22 -0
- package/dist/hosted/types.js.map +1 -0
- package/dist/index.d.ts +12 -24
- package/dist/index.js +9 -19
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +27 -34
- package/dist/installer/config.js +46 -15
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +9 -19
- package/dist/installer/index.js +21 -85
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/paths.d.ts +0 -2
- package/dist/installer/paths.js +0 -4
- package/dist/installer/paths.js.map +1 -1
- package/dist/installer/wizard.d.ts +12 -9
- package/dist/installer/wizard.js +15 -14
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/address-resolver.d.ts +18 -17
- package/dist/nat/address-resolver.js +79 -25
- package/dist/nat/address-resolver.js.map +1 -1
- package/dist/nat/address-watch.d.ts +37 -0
- package/dist/nat/address-watch.js +84 -0
- package/dist/nat/address-watch.js.map +1 -0
- package/dist/nat/ddns/duckdns.d.ts +2 -1
- package/dist/nat/ddns/duckdns.js +5 -1
- package/dist/nat/ddns/duckdns.js.map +1 -1
- package/dist/nat/ddns/index.d.ts +6 -0
- package/dist/nat/ddns/index.js.map +1 -1
- package/dist/nat/index.d.ts +13 -9
- package/dist/nat/index.js +7 -5
- package/dist/nat/index.js.map +1 -1
- package/dist/nat/nat-service.d.ts +220 -77
- package/dist/nat/nat-service.js +653 -239
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/nat-store.d.ts +12 -5
- package/dist/nat/nat-store.js +124 -29
- package/dist/nat/nat-store.js.map +1 -1
- package/dist/nat/network-mode.d.ts +19 -0
- package/dist/nat/network-mode.js +32 -0
- package/dist/nat/network-mode.js.map +1 -0
- package/dist/nat/port-mapper.d.ts +83 -79
- package/dist/nat/port-mapper.js +148 -189
- package/dist/nat/port-mapper.js.map +1 -1
- package/dist/nat/reachability.d.ts +32 -15
- package/dist/nat/reachability.js +110 -22
- package/dist/nat/reachability.js.map +1 -1
- package/dist/nat/types.d.ts +98 -27
- package/dist/nat/types.js +8 -5
- package/dist/nat/types.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
- package/dist/orchestrator/host-process-orchestrator.js +84 -233
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
- package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
- package/dist/orchestrator/identity-file.js.map +1 -0
- package/dist/orchestrator/index.d.ts +2 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +6 -6
- package/dist/orchestrator/node-identity.js +7 -7
- package/dist/orchestrator/node-identity.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +5 -6
- package/dist/orchestrator/port-allocator.js +9 -11
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +3 -5
- package/dist/orchestrator/state-store.js +2 -10
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +33 -22
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +4 -0
- package/dist/package-version.js.map +1 -0
- package/dist/server/error-handler.d.ts +5 -25
- package/dist/server/error-handler.js +11 -74
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +7 -11
- package/dist/server/events/types.js +2 -1
- package/dist/server/events/types.js.map +1 -1
- package/dist/server/index.d.ts +11 -37
- package/dist/server/index.js +30 -48
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/hosted-nodes.d.ts +28 -0
- package/dist/server/routes/hosted-nodes.js +68 -0
- package/dist/server/routes/hosted-nodes.js.map +1 -0
- package/dist/server/routes/nat.d.ts +4 -1
- package/dist/server/routes/nat.js +13 -2
- package/dist/server/routes/nat.js.map +1 -1
- package/dist/server/routes/nodes.d.ts +6 -20
- package/dist/server/routes/nodes.js +7 -86
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +3 -7
- package/dist/server/routes/settings.js +18 -8
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +2 -25
- package/dist/server/routes/status.js +3 -43
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/server.js +8 -8
- package/dist/server/settings-store.d.ts +2 -2
- package/dist/server/settings-store.js +2 -2
- package/dist/server/static.js +1 -2
- package/dist/server/static.js.map +1 -1
- package/dist/ui/assets/index-BHd8CWZA.css +1 -0
- package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
- package/dist/ui/assets/index-Ccr2GjPT.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/store.d.ts +1 -1
- package/dist/update/store.js +1 -1
- package/dist/update/types.d.ts +1 -1
- package/dist/update/types.js +1 -1
- package/package.json +5 -7
- package/src/__tests__/cli-nat.smoke.test.ts +219 -212
- package/src/__tests__/cli.smoke.test.ts +2 -2
- package/src/__tests__/global-setup.ts +2 -3
- package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
- package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
- package/src/__tests__/orchestrator-handle-contract.ts +134 -134
- package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
- package/src/__tests__/orchestrator-ports.test.ts +124 -143
- package/src/__tests__/orchestrator-push.test.ts +138 -146
- package/src/__tests__/orchestrator.test.ts +1017 -1063
- package/src/__tests__/ui-access.test.ts +30 -0
- package/src/bin/host.ts +397 -572
- package/src/bin/join-output.ts +120 -0
- package/src/bin/nat-output.ts +188 -0
- package/src/bin/ui-access.ts +50 -0
- package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
- package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
- package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
- package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
- package/src/hosted/__tests__/test-invitation.ts +33 -0
- package/src/hosted/claim-details.ts +103 -0
- package/src/hosted/hosted-node-service.ts +814 -0
- package/src/hosted/hosted-node-store.ts +144 -0
- package/src/hosted/hosted-node-supervisor.ts +439 -0
- package/src/hosted/hosted-node-watcher.ts +288 -0
- package/src/hosted/index.ts +53 -0
- package/src/hosted/node-status.ts +97 -0
- package/src/hosted/types.ts +160 -0
- package/src/index.ts +72 -91
- package/src/installer/__tests__/config.test.ts +15 -40
- package/src/installer/__tests__/installer.smoke.test.ts +184 -263
- package/src/installer/__tests__/wizard.test.ts +25 -13
- package/src/installer/config.ts +201 -175
- package/src/installer/index.ts +252 -327
- package/src/installer/paths.ts +104 -109
- package/src/installer/wizard.ts +24 -26
- package/src/nat/__tests__/address-resolver.test.ts +107 -82
- package/src/nat/__tests__/nat-service.test.ts +608 -515
- package/src/nat/__tests__/nat-store.test.ts +49 -12
- package/src/nat/__tests__/network-mode.test.ts +38 -0
- package/src/nat/__tests__/port-mapper.test.ts +30 -191
- package/src/nat/__tests__/reachability.test.ts +96 -13
- package/src/nat/address-resolver.ts +94 -52
- package/src/nat/address-watch.ts +92 -0
- package/src/nat/ddns/duckdns.ts +5 -1
- package/src/nat/ddns/index.ts +6 -0
- package/src/nat/index.ts +33 -13
- package/src/nat/nat-service.ts +942 -501
- package/src/nat/nat-store.ts +126 -31
- package/src/nat/network-mode.ts +36 -0
- package/src/nat/port-mapper.ts +241 -329
- package/src/nat/reachability.ts +155 -34
- package/src/nat/types.ts +110 -37
- package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
- package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
- package/src/orchestrator/index.ts +9 -10
- package/src/orchestrator/node-identity.ts +41 -41
- package/src/orchestrator/port-allocator.ts +71 -73
- package/src/orchestrator/state-store.ts +76 -90
- package/src/orchestrator/types.ts +34 -23
- package/src/package-version.ts +4 -0
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +81 -118
- package/src/server/__tests__/fakes.ts +15 -18
- package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
- package/src/server/__tests__/nodes-route.test.ts +114 -283
- package/src/server/__tests__/publishers.test.ts +193 -180
- package/src/server/__tests__/server.smoke.test.ts +134 -223
- package/src/server/__tests__/settings-route.test.ts +210 -195
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/status-route.test.ts +82 -121
- package/src/server/error-handler.ts +112 -178
- package/src/server/events/types.ts +20 -24
- package/src/server/index.ts +223 -267
- package/src/server/routes/hosted-nodes.ts +83 -0
- package/src/server/routes/nat.ts +75 -63
- package/src/server/routes/nodes.ts +96 -188
- package/src/server/routes/settings.ts +20 -15
- package/src/server/routes/status.ts +5 -67
- package/src/server/server.ts +8 -8
- package/src/server/settings-store.ts +2 -2
- package/src/server/static.ts +1 -2
- package/src/update/index.ts +2 -2
- package/src/update/store.ts +1 -1
- package/src/update/types.ts +1 -1
- package/dist/auth/duration.d.ts +0 -1
- package/dist/auth/duration.js +0 -32
- package/dist/auth/duration.js.map +0 -1
- package/dist/auth/index.d.ts +0 -11
- package/dist/auth/index.js +0 -9
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/self-label.d.ts +0 -36
- package/dist/auth/self-label.js +0 -57
- package/dist/auth/self-label.js.map +0 -1
- package/dist/auth/trust-circle-store.d.ts +0 -26
- package/dist/auth/trust-circle-store.js +0 -129
- package/dist/auth/trust-circle-store.js.map +0 -1
- package/dist/auth/trust-circle.d.ts +0 -156
- package/dist/auth/trust-circle.js +0 -307
- package/dist/auth/trust-circle.js.map +0 -1
- package/dist/auth/types.d.ts +0 -84
- package/dist/auth/types.js +0 -24
- package/dist/auth/types.js.map +0 -1
- package/dist/donation/donation-service.d.ts +0 -389
- package/dist/donation/donation-service.js +0 -828
- package/dist/donation/donation-service.js.map +0 -1
- package/dist/donation/donation-store.d.ts +0 -51
- package/dist/donation/donation-store.js +0 -152
- package/dist/donation/donation-store.js.map +0 -1
- package/dist/donation/donation-supervisor.js +0 -364
- package/dist/donation/donation-supervisor.js.map +0 -1
- package/dist/donation/grant-service.d.ts +0 -80
- package/dist/donation/grant-service.js +0 -210
- package/dist/donation/grant-service.js.map +0 -1
- package/dist/donation/grant-store.d.ts +0 -38
- package/dist/donation/grant-store.js +0 -135
- package/dist/donation/grant-store.js.map +0 -1
- package/dist/donation/index.d.ts +0 -20
- package/dist/donation/index.js +0 -17
- package/dist/donation/index.js.map +0 -1
- package/dist/donation/types.d.ts +0 -224
- package/dist/donation/types.js +0 -40
- package/dist/donation/types.js.map +0 -1
- package/dist/installer/identity.js.map +0 -1
- package/dist/owner/index.d.ts +0 -11
- package/dist/owner/index.js +0 -11
- package/dist/owner/index.js.map +0 -1
- package/dist/owner/owner-node-client.d.ts +0 -110
- package/dist/owner/owner-node-client.js +0 -184
- package/dist/owner/owner-node-client.js.map +0 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
- package/dist/server/routes/bootstrap-node-validation.js +0 -197
- package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
- package/dist/server/routes/grants-admin.d.ts +0 -29
- package/dist/server/routes/grants-admin.js +0 -58
- package/dist/server/routes/grants-admin.js.map +0 -1
- package/dist/server/routes/grants.d.ts +0 -28
- package/dist/server/routes/grants.js +0 -165
- package/dist/server/routes/grants.js.map +0 -1
- package/dist/server/routes/provision-request-validation.d.ts +0 -45
- package/dist/server/routes/provision-request-validation.js +0 -97
- package/dist/server/routes/provision-request-validation.js.map +0 -1
- package/dist/server/routes/strands.d.ts +0 -20
- package/dist/server/routes/strands.js +0 -38
- package/dist/server/routes/strands.js.map +0 -1
- package/dist/server/routes/trust-circle.d.ts +0 -17
- package/dist/server/routes/trust-circle.js +0 -35
- package/dist/server/routes/trust-circle.js.map +0 -1
- package/dist/strands/index.d.ts +0 -7
- package/dist/strands/index.js +0 -6
- package/dist/strands/index.js.map +0 -1
- package/dist/strands/strand-service.d.ts +0 -50
- package/dist/strands/strand-service.js +0 -103
- package/dist/strands/strand-service.js.map +0 -1
- package/dist/strands/types.d.ts +0 -93
- package/dist/strands/types.js +0 -23
- package/dist/strands/types.js.map +0 -1
- package/dist/ui/assets/index-C6nELNfC.js +0 -23
- package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
- package/dist/ui/assets/index-TtIYSeZO.css +0 -1
- package/src/__tests__/cli-invite.smoke.test.ts +0 -192
- package/src/__tests__/host-owner.smoke.test.ts +0 -123
- package/src/__tests__/orchestrator-owner.test.ts +0 -234
- package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
- package/src/auth/__tests__/duration.test.ts +0 -28
- package/src/auth/__tests__/self-label.test.ts +0 -85
- package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
- package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
- package/src/auth/__tests__/trust-circle.test.ts +0 -445
- package/src/auth/duration.ts +0 -33
- package/src/auth/index.ts +0 -21
- package/src/auth/self-label.ts +0 -74
- package/src/auth/trust-circle-store.ts +0 -155
- package/src/auth/trust-circle.ts +0 -395
- package/src/auth/types.ts +0 -99
- package/src/donation/__tests__/donation-service.test.ts +0 -1193
- package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
- package/src/donation/__tests__/grant-service.test.ts +0 -213
- package/src/donation/__tests__/grant-store.test.ts +0 -113
- package/src/donation/donation-service.ts +0 -1033
- package/src/donation/donation-store.ts +0 -176
- package/src/donation/donation-supervisor.ts +0 -411
- package/src/donation/grant-service.ts +0 -252
- package/src/donation/grant-store.ts +0 -152
- package/src/donation/index.ts +0 -65
- package/src/donation/types.ts +0 -260
- package/src/owner/__tests__/owner-node-client.test.ts +0 -194
- package/src/owner/index.ts +0 -12
- package/src/owner/owner-node-client.ts +0 -259
- package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
- package/src/server/__tests__/grants-admin-route.test.ts +0 -171
- package/src/server/__tests__/grants-route.test.ts +0 -394
- package/src/server/__tests__/strands-route.test.ts +0 -234
- package/src/server/routes/bootstrap-node-validation.ts +0 -206
- package/src/server/routes/grants-admin.ts +0 -74
- package/src/server/routes/grants.ts +0 -193
- package/src/server/routes/provision-request-validation.ts +0 -105
- package/src/server/routes/strands.ts +0 -54
- package/src/server/routes/trust-circle.ts +0 -49
- package/src/strands/__tests__/strand-service.test.ts +0 -165
- package/src/strands/index.ts +0 -14
- package/src/strands/strand-service.ts +0 -135
- package/src/strands/types.ts +0 -102
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { errorMessage } from './node-status.js';
|
|
3
|
+
const log = debug('cadre:host:hosted-node-supervisor');
|
|
4
|
+
/**
|
|
5
|
+
* Unit of the doubling respawn backoff: the wait after `n` consecutive failed
|
|
6
|
+
* attempts is `base * 2^n`, capped at {@link HOSTED_NODE_RESPAWN_BACKOFF_MAX_MS}.
|
|
7
|
+
* So the first retry waits 10s (one attempt is already recorded by then), the
|
|
8
|
+
* second 20s, and so on. A node with no recorded attempt is respawned
|
|
9
|
+
* immediately.
|
|
10
|
+
*/
|
|
11
|
+
export const HOSTED_NODE_RESPAWN_BACKOFF_BASE_MS = 5000;
|
|
12
|
+
/**
|
|
13
|
+
* Ceiling on the doubling respawn backoff. 5 minutes.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: not reachable at the current {@link HOSTED_NODE_RESPAWN_MAX_ATTEMPTS} of 5
|
|
16
|
+
* — the longest wait actually served is the one before the 5th attempt,
|
|
17
|
+
* `5s * 2^4` = 80s, and once that attempt fails or its node dies the supervisor
|
|
18
|
+
* gives up rather than waiting again. The cap only starts clamping
|
|
19
|
+
* once a record can reach 6 recorded attempts (`5s * 2^6` = 320s), so raising
|
|
20
|
+
* the attempt cap without revisiting this one changes nothing.
|
|
21
|
+
*/
|
|
22
|
+
export const HOSTED_NODE_RESPAWN_BACKOFF_MAX_MS = 5 * 60000;
|
|
23
|
+
/**
|
|
24
|
+
* Consecutive respawn attempts after which the host stops trying and marks the
|
|
25
|
+
* node `error` — a node that will not stay up is a host-side fault its cadre
|
|
26
|
+
* cannot see, so surface it instead of spinning forever.
|
|
27
|
+
*
|
|
28
|
+
* Every attempt counts, whether its spawn failed or succeeded: a node that spawns
|
|
29
|
+
* and then dies before {@link HOSTED_NODE_RESPAWN_HEALTHY_MS} refills the budget is
|
|
30
|
+
* as much a crash loop as one that never spawns. Give-up happens on whichever
|
|
31
|
+
* comes first — an attempt that throws with the cap reached, or a pass that finds
|
|
32
|
+
* the node down with the cap already reached.
|
|
33
|
+
*/
|
|
34
|
+
export const HOSTED_NODE_RESPAWN_MAX_ATTEMPTS = 5;
|
|
35
|
+
/**
|
|
36
|
+
* How long a respawned node must stay up before its attempt budget is refilled.
|
|
37
|
+
* The last respawn produced something that survived, so the next crash starts
|
|
38
|
+
* from a fresh budget rather than inheriting an old crash loop's count.
|
|
39
|
+
*/
|
|
40
|
+
export const HOSTED_NODE_RESPAWN_HEALTHY_MS = 10 * 60000;
|
|
41
|
+
/** How often the supervisor sweep runs while cadre-host is up. 1 minute. */
|
|
42
|
+
export const HOSTED_NODE_RESPAWN_SWEEP_MS = 60000;
|
|
43
|
+
/**
|
|
44
|
+
* Statuses the supervisor considers. `spawning` is a join still in flight (its
|
|
45
|
+
* own code path owns the child); `error` is a node the host gave up on, or whose
|
|
46
|
+
* invitation failed, which comes back only by an explicit Retry or reset.
|
|
47
|
+
*/
|
|
48
|
+
const SUPERVISED_STATUSES = new Set([
|
|
49
|
+
'unclaimed',
|
|
50
|
+
'joining',
|
|
51
|
+
'joined',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* HostedNodeSupervisor — owns the invariant *an `unclaimed`, `joining` or `joined`
|
|
55
|
+
* node is expected to be running*.
|
|
56
|
+
*
|
|
57
|
+
* Nothing else re-spawns a hosted node: `/api/nodes` is read-only. So without
|
|
58
|
+
* this class a crashed, OOM-killed, or reboot-killed node stays dead with its
|
|
59
|
+
* record still reading `joined`, silently costing its cadre a node.
|
|
60
|
+
*
|
|
61
|
+
* One code path ({@link reconcile}) behind three triggers:
|
|
62
|
+
*
|
|
63
|
+
* - **host startup** — one pass after `orchestrator.init()`, which catches every
|
|
64
|
+
* node that died while the host was down;
|
|
65
|
+
* - **exit signal** — the orchestrator's `onStateChange`, so a crash is noticed
|
|
66
|
+
* in milliseconds rather than at the next sweep;
|
|
67
|
+
* - **periodic sweep** — the backstop for deaths no exit event covers (host was
|
|
68
|
+
* down, listener missed, orchestrator re-attached to an already-dead pid).
|
|
69
|
+
*
|
|
70
|
+
* Passes are serialized: `HostedNodeService.respawn` is not itself serialized, and
|
|
71
|
+
* two overlapping respawns of one id both spawn a child while the second drops
|
|
72
|
+
* the first's orchestrator handle (see its docstring). Every pass, and a deliberate
|
|
73
|
+
* {@link restart}, runs on the service's respawn queue
|
|
74
|
+
* (`HostedNodeService.serializeRespawns`), which the service's Retry shares.
|
|
75
|
+
*
|
|
76
|
+
* The supervisor reconciles records to handles, never handles to records: a
|
|
77
|
+
* handle no record names (`hosted-nodes.json` lost, or a handle an older build
|
|
78
|
+
* persisted) is logged at startup and left running, since stopping it could end
|
|
79
|
+
* a node that is serving its cadre. `cadre-host node remove <id>` reclaims it.
|
|
80
|
+
*/
|
|
81
|
+
export class HostedNodeSupervisor {
|
|
82
|
+
constructor(opts) {
|
|
83
|
+
/** At most one exit-triggered pass queued at a time (crash storms coalesce). */
|
|
84
|
+
this.exitPassQueued = false;
|
|
85
|
+
/** Set by `stop()` so an in-flight pass abandons its remaining records. */
|
|
86
|
+
this.disposed = false;
|
|
87
|
+
this.service = opts.service;
|
|
88
|
+
this.store = opts.store;
|
|
89
|
+
this.orchestrator = opts.orchestrator;
|
|
90
|
+
this.now = opts.now ?? (() => new Date());
|
|
91
|
+
log('HostedNodeSupervisor initialized');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One reconcile pass over the store. Serialized against every other pass, so
|
|
95
|
+
* a caller may get a queued pass rather than an immediate one. Returns the ids
|
|
96
|
+
* it respawned in *its own* pass.
|
|
97
|
+
*/
|
|
98
|
+
reconcile() {
|
|
99
|
+
return this.service.serializeRespawns(() => this.reconcileOnce());
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Stop a running node and start it again, for a change the node picks up
|
|
103
|
+
* only at start — its public addresses (`NatService.onNodeAddressesStale`).
|
|
104
|
+
* Serialized with the reconcile passes, so it never overlaps a respawn or a
|
|
105
|
+
* give-up of the same record, and it re-reads the record first: a record that
|
|
106
|
+
* is no longer supervised, has no handle, or whose node is not
|
|
107
|
+
* running is left alone (a node that is down is the crash path's).
|
|
108
|
+
*
|
|
109
|
+
* Does not spend the record's respawn budget. A restart whose respawn throws
|
|
110
|
+
* rejects; the node is then down, and the exit event its stop raised has queued
|
|
111
|
+
* a reconcile pass that takes it over through the crash path, with its counting.
|
|
112
|
+
*/
|
|
113
|
+
restart(id) {
|
|
114
|
+
return this.service.serializeRespawns(() => this.restartOnce(id));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Subscribe to child exits, start the periodic sweep, and run the startup
|
|
118
|
+
* pass. Idempotent — a second call while started is a no-op.
|
|
119
|
+
*/
|
|
120
|
+
start() {
|
|
121
|
+
if (this.timer)
|
|
122
|
+
return;
|
|
123
|
+
this.disposed = false;
|
|
124
|
+
// Subscribe before the first pass so an exit during it is not missed.
|
|
125
|
+
this.unsubscribe = this.orchestrator.onStateChange((info) => { this.onNodeState(info); });
|
|
126
|
+
this.timer = setInterval(() => { this.sweep('timer'); }, HOSTED_NODE_RESPAWN_SWEEP_MS);
|
|
127
|
+
this.timer.unref();
|
|
128
|
+
this.logOrphanHandles();
|
|
129
|
+
this.sweep('startup');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Stop the timer and unsubscribe. A pass already in flight abandons its
|
|
133
|
+
* remaining records rather than being awaited — every record it has not
|
|
134
|
+
* reached is picked up by the next host start's startup pass.
|
|
135
|
+
*/
|
|
136
|
+
stop() {
|
|
137
|
+
this.disposed = true;
|
|
138
|
+
if (this.timer) {
|
|
139
|
+
clearInterval(this.timer);
|
|
140
|
+
this.timer = undefined;
|
|
141
|
+
}
|
|
142
|
+
this.unsubscribe?.();
|
|
143
|
+
this.unsubscribe = undefined;
|
|
144
|
+
log('HostedNodeSupervisor stopped');
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Name every orchestrator handle that no record claims, once at startup. Not
|
|
148
|
+
* reclaimed (see the class docstring): the admin ends it with
|
|
149
|
+
* `cadre-host node remove <id>`.
|
|
150
|
+
*/
|
|
151
|
+
logOrphanHandles() {
|
|
152
|
+
let known;
|
|
153
|
+
try {
|
|
154
|
+
known = new Set(this.store.list().map((n) => n.id));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log('orphan check could not list hosted nodes: %s', errorMessage(err));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const handle of this.orchestrator.listNodes()) {
|
|
161
|
+
if (known.has(handle.id))
|
|
162
|
+
continue;
|
|
163
|
+
console.error(`hosted node ${handle.id} (${handle.status}) has an orchestrator handle but no record in hosted-nodes.json; `
|
|
164
|
+
+ 'it is left as it is. Run `cadre-host node remove ' + handle.id + '` to stop it and delete its data.');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Fire-and-forget pass for the timer / exit / startup triggers. */
|
|
168
|
+
sweep(trigger) {
|
|
169
|
+
void this.reconcile()
|
|
170
|
+
.then((ids) => {
|
|
171
|
+
if (ids.length)
|
|
172
|
+
log('%s sweep respawned %d hosted node(s): %o', trigger, ids.length, ids);
|
|
173
|
+
})
|
|
174
|
+
.catch((err) => {
|
|
175
|
+
console.error(`hosted node respawn sweep failed: ${errorMessage(err)}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Exit-event trigger. Deliberately cheap: it does not map the handle back to a
|
|
180
|
+
* record (that needs a store read per event), it just asks for a pass.
|
|
181
|
+
*/
|
|
182
|
+
onNodeState(info) {
|
|
183
|
+
if (info.status !== 'stopped')
|
|
184
|
+
return;
|
|
185
|
+
if (this.exitPassQueued)
|
|
186
|
+
return;
|
|
187
|
+
this.exitPassQueued = true;
|
|
188
|
+
void this.reconcile()
|
|
189
|
+
.catch((err) => { console.error(`hosted node respawn sweep failed: ${errorMessage(err)}`); })
|
|
190
|
+
.finally(() => { this.exitPassQueued = false; });
|
|
191
|
+
}
|
|
192
|
+
/** The unserialized body of one pass. */
|
|
193
|
+
async reconcileOnce() {
|
|
194
|
+
const respawned = [];
|
|
195
|
+
let candidates;
|
|
196
|
+
try {
|
|
197
|
+
candidates = this.store.list().filter((n) => SUPERVISED_STATUSES.has(n.status));
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// A malformed hosted-nodes.json throws on every load; log rather than let an
|
|
201
|
+
// unhandled rejection escape the timer.
|
|
202
|
+
log('reconcile could not list hosted nodes: %s', errorMessage(err));
|
|
203
|
+
return respawned;
|
|
204
|
+
}
|
|
205
|
+
for (const node of candidates) {
|
|
206
|
+
if (this.disposed)
|
|
207
|
+
break;
|
|
208
|
+
try {
|
|
209
|
+
const id = await this.reconcileOne(node);
|
|
210
|
+
if (id)
|
|
211
|
+
respawned.push(id);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
// One bad record must never end the sweep — the others are still down.
|
|
215
|
+
log('reconcile of hosted node %s failed: %s', node.id, errorMessage(err));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return respawned;
|
|
219
|
+
}
|
|
220
|
+
async restartOnce(id) {
|
|
221
|
+
if (this.disposed)
|
|
222
|
+
return;
|
|
223
|
+
const node = this.store.get(id);
|
|
224
|
+
if (!node?.dockerId || !SUPERVISED_STATUSES.has(node.status))
|
|
225
|
+
return;
|
|
226
|
+
if (!(await this.orchestrator.isRunning(node.dockerId)))
|
|
227
|
+
return;
|
|
228
|
+
await this.orchestrator.stopContainer(node.dockerId);
|
|
229
|
+
const result = await this.service.respawn(id, { countAttempt: false });
|
|
230
|
+
log('restarted hosted node %s: %s', id, result.outcome);
|
|
231
|
+
}
|
|
232
|
+
/** Reconcile one record. Returns its id when this pass respawned it. */
|
|
233
|
+
async reconcileOne(node) {
|
|
234
|
+
// No handle means nothing was ever spawned — `join` owns that record until
|
|
235
|
+
// it writes one, and replaying it here would race that join.
|
|
236
|
+
if (!node.dockerId)
|
|
237
|
+
return undefined;
|
|
238
|
+
if (await this.orchestrator.isRunning(node.dockerId)) {
|
|
239
|
+
this.refillBudgetIfHealthy(node);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
// The budget is spent, and the last attempt did not stay up long enough to
|
|
243
|
+
// refill it. Checked here and not only when `respawn` throws: a spawn that
|
|
244
|
+
// succeeds and then dies at once (a crash on boot, a port clash) never
|
|
245
|
+
// throws, so a catch-only check would respawn it forever.
|
|
246
|
+
// NOTE: a record at the cap whose last respawn was still up but not yet
|
|
247
|
+
// refilled when the host went down is given up on host restart rather than
|
|
248
|
+
// tried once more; if that is ever seen, skip this check when the host
|
|
249
|
+
// started after `respawn.lastAttemptAt`.
|
|
250
|
+
const attempts = node.respawn?.attempts ?? 0;
|
|
251
|
+
if (attempts >= HOSTED_NODE_RESPAWN_MAX_ATTEMPTS) {
|
|
252
|
+
await this.giveUp(node.id, attempts, 'the node did not stay running after its last respawn');
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (!this.backoffElapsed(node)) {
|
|
256
|
+
log('hosted node %s is down but still inside its respawn backoff', node.id);
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
return this.attemptRespawn(node);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Whether enough time has passed since the last attempt. A record with no
|
|
263
|
+
* attempt history (or an unparsable timestamp — never wedge a node on bad
|
|
264
|
+
* data) is eligible immediately.
|
|
265
|
+
*/
|
|
266
|
+
backoffElapsed(node) {
|
|
267
|
+
const respawn = node.respawn;
|
|
268
|
+
if (!respawn)
|
|
269
|
+
return true;
|
|
270
|
+
const last = Date.parse(respawn.lastAttemptAt);
|
|
271
|
+
if (Number.isNaN(last))
|
|
272
|
+
return true;
|
|
273
|
+
const delay = Math.min(HOSTED_NODE_RESPAWN_BACKOFF_BASE_MS * 2 ** respawn.attempts, HOSTED_NODE_RESPAWN_BACKOFF_MAX_MS);
|
|
274
|
+
return this.now().getTime() - last >= delay;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Clear the attempt count once a respawned node has been up long enough to
|
|
278
|
+
* count as healthy, so the next crash gets a full budget instead of inheriting
|
|
279
|
+
* the previous crash loop's.
|
|
280
|
+
*/
|
|
281
|
+
refillBudgetIfHealthy(node) {
|
|
282
|
+
if (!this.budgetRefillDue(node))
|
|
283
|
+
return;
|
|
284
|
+
// Re-read before writing: `node` comes from the snapshot this pass started
|
|
285
|
+
// from, which can be several awaits old, and `store.put` replaces the whole
|
|
286
|
+
// row — writing the stale copy back would undo a claim or a removal that
|
|
287
|
+
// landed mid-pass.
|
|
288
|
+
const current = this.store.get(node.id);
|
|
289
|
+
if (!current?.respawn || !this.budgetRefillDue(current))
|
|
290
|
+
return;
|
|
291
|
+
try {
|
|
292
|
+
this.store.put({ ...current, respawn: { ...current.respawn, attempts: 0 } });
|
|
293
|
+
log('hosted node %s is healthy again — respawn budget refilled', node.id);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
// Costs one attempt off the next crash's budget, nothing more.
|
|
297
|
+
log('failed to refill respawn budget for %s: %s', node.id, errorMessage(err));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Whether a record carries a crash-loop count old enough to clear. An
|
|
302
|
+
* unparsable timestamp is treated as not-due — unlike the backoff check,
|
|
303
|
+
* being wrong here would erase real attempt history rather than free a node.
|
|
304
|
+
*/
|
|
305
|
+
budgetRefillDue(node) {
|
|
306
|
+
const respawn = node.respawn;
|
|
307
|
+
if (!respawn || respawn.attempts === 0)
|
|
308
|
+
return false;
|
|
309
|
+
const last = Date.parse(respawn.lastAttemptAt);
|
|
310
|
+
if (Number.isNaN(last))
|
|
311
|
+
return false;
|
|
312
|
+
return this.now().getTime() - last > HOSTED_NODE_RESPAWN_HEALTHY_MS;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* One respawn attempt, with the give-up check on failure. (The check for an
|
|
316
|
+
* attempt that spawned but then died lives in {@link reconcileOne}.)
|
|
317
|
+
*/
|
|
318
|
+
async attemptRespawn(node) {
|
|
319
|
+
try {
|
|
320
|
+
const result = await this.service.respawn(node.id);
|
|
321
|
+
switch (result.outcome) {
|
|
322
|
+
case 'abandoned':
|
|
323
|
+
// The node was removed (or given up on) while the spawn was in flight
|
|
324
|
+
// and the ending won. Nothing threw, so no attempt is persisted and the
|
|
325
|
+
// give-up path is never entered — correct: this is not a failed attempt.
|
|
326
|
+
log('respawn of hosted node %s was abandoned (record went %s mid-spawn)', node.id, result.status ?? 'missing');
|
|
327
|
+
return undefined;
|
|
328
|
+
case 'respawned':
|
|
329
|
+
log('respawned hosted node %s → %s', node.id, result.node.dockerId);
|
|
330
|
+
return node.id;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
const message = errorMessage(err);
|
|
335
|
+
// `respawn` persists the incremented counter; re-read it rather than
|
|
336
|
+
// recomputing, so a give-up decision is made on what is actually on disk.
|
|
337
|
+
const attempts = this.store.get(node.id)?.respawn?.attempts ?? 0;
|
|
338
|
+
log('respawn attempt %d for hosted node %s failed: %s', attempts, node.id, message);
|
|
339
|
+
if (attempts >= HOSTED_NODE_RESPAWN_MAX_ATTEMPTS) {
|
|
340
|
+
await this.giveUp(node.id, attempts, message);
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Stop trying: mark the node `error` and stop whatever child the record
|
|
347
|
+
* still names.
|
|
348
|
+
*
|
|
349
|
+
* The record is written BEFORE the stop for the same reason `remove` does
|
|
350
|
+
* it in that order — the stop fires `onStateChange`, and a pass triggered by
|
|
351
|
+
* it must not see "node gone, record still `joined`" and start respawning
|
|
352
|
+
* again.
|
|
353
|
+
*
|
|
354
|
+
* The child is stopped but NOT removed: `removeContainer` deletes the workdir,
|
|
355
|
+
* and the workdir holds the identity key the node's cadre approved. A later
|
|
356
|
+
* `remove()` (or `reset()`) on an `error` record reclaims it.
|
|
357
|
+
*
|
|
358
|
+
* NOTE: an `error`-after-give-up record therefore keeps its workdir until
|
|
359
|
+
* someone removes it. If stale `error` workdirs ever pile up on real hosts,
|
|
360
|
+
* extend the reap sweep in `hosted-node-service.ts` to cover them.
|
|
361
|
+
*/
|
|
362
|
+
async giveUp(id, attempts, lastError) {
|
|
363
|
+
const node = this.store.get(id);
|
|
364
|
+
if (!node)
|
|
365
|
+
return;
|
|
366
|
+
// The record may have been removed or given up on while the failing attempt
|
|
367
|
+
// was in flight — a `remove` mid-pass is exactly why `respawn` threw. A stop
|
|
368
|
+
// here would fire against a child `remove` already reclaimed.
|
|
369
|
+
if (!SUPERVISED_STATUSES.has(node.status)) {
|
|
370
|
+
log('hosted node %s went %s before give-up — leaving it alone', id, node.status);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.store.put({
|
|
374
|
+
...node,
|
|
375
|
+
status: 'error',
|
|
376
|
+
error: `respawn gave up after ${attempts} attempts: ${lastError}`,
|
|
377
|
+
updatedAt: this.now().toISOString(),
|
|
378
|
+
});
|
|
379
|
+
log('gave up respawning hosted node %s after %d attempts: %s', id, attempts, lastError);
|
|
380
|
+
this.service.emit({ kind: 'changed', id });
|
|
381
|
+
if (node.dockerId) {
|
|
382
|
+
try {
|
|
383
|
+
await this.orchestrator.stopContainer(node.dockerId);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
// The record names a live handle in the ordinary case — a failed respawn
|
|
387
|
+
// restores the handle it dropped, and a respawn that spawned but failed
|
|
388
|
+
// to record writes the new one onto the record. Still guarded: a handle
|
|
389
|
+
// can be lost to a host restart (state.json gone, or the child already
|
|
390
|
+
// reaped), and the child is dead either way, which is why we are here.
|
|
391
|
+
log('failed to stop container %s while giving up: %s', node.dockerId, errorMessage(err));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
//# sourceMappingURL=hosted-node-supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosted-node-supervisor.js","sourceRoot":"","sources":["../../src/hosted/hosted-node-supervisor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { NodeStateListener } from '../orchestrator/types.js';
|
|
2
|
+
import type { HostedNodeStore } from './hosted-node-store.js';
|
|
3
|
+
import type { HostedNodeChange } from './types.js';
|
|
4
|
+
/** How often an unclaimed node's `/status` is read for the claim, and a joining node's for its redemption. */
|
|
5
|
+
export declare const HOSTED_NODE_CLAIM_POLL_MS = 2000;
|
|
6
|
+
/** How often a joined node's `/status` is read for its connection count. */
|
|
7
|
+
export declare const HOSTED_NODE_CONNECTED_POLL_MS = 15000;
|
|
8
|
+
/**
|
|
9
|
+
* The orchestrator surface the watcher needs: the exit/start signal, when the orchestrator
|
|
10
|
+
* has one, and the stop of a node whose invitation failed or that an owner removed.
|
|
11
|
+
*/
|
|
12
|
+
export interface WatchedOrchestrator {
|
|
13
|
+
onStateChange?(listener: NodeStateListener): () => void;
|
|
14
|
+
stopContainer(dockerId: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export interface HostedNodeWatcherOptions {
|
|
17
|
+
store: HostedNodeStore;
|
|
18
|
+
orchestrator: WatchedOrchestrator;
|
|
19
|
+
now: () => Date;
|
|
20
|
+
/** Where a claim or a liveness change is reported (`HostedNodeService.emit`). */
|
|
21
|
+
emit: (change: HostedNodeChange) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Follows each hosted node's `/status`: an `unclaimed` record becomes `joined` when
|
|
25
|
+
* the node reports a finished claim; a `joining` record becomes `joined` when a member
|
|
26
|
+
* accepted its invitation, or `error` when the redemption failed; a `joined`
|
|
27
|
+
* record's `connected` follows the node's control-connection count; and any of them
|
|
28
|
+
* becomes `error` when the node reports that an owner removed it from its cadre.
|
|
29
|
+
* Best-effort: a poll that fails is retried on the next tick, and nothing here throws
|
|
30
|
+
* out of the timer.
|
|
31
|
+
*
|
|
32
|
+
* One timer at the claim cadence drives all three: every tick polls every unclaimed and
|
|
33
|
+
* joining node, and each joined node every {@link HOSTED_NODE_CONNECTED_POLL_MS}, or at once when it
|
|
34
|
+
* was poked — the orchestrator's state change on a respawn pokes the node, and the
|
|
35
|
+
* poke stands until a poll succeeds, so a respawned child is noticed on its first
|
|
36
|
+
* answer rather than at the next 15 s mark. Polls are serialized on one promise
|
|
37
|
+
* tail so a timer pass and a poked pass cannot both write one record.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: the 2 s timer ticks while nothing is unclaimed (one cached store read, no
|
|
40
|
+
* I/O per tick); if it ever shows up in a profile, stop it when no record is
|
|
41
|
+
* unclaimed and restart it from `poke`.
|
|
42
|
+
*/
|
|
43
|
+
export declare class HostedNodeWatcher {
|
|
44
|
+
private readonly store;
|
|
45
|
+
private readonly orchestrator;
|
|
46
|
+
private readonly now;
|
|
47
|
+
private readonly emit;
|
|
48
|
+
private tail;
|
|
49
|
+
private timer?;
|
|
50
|
+
private unsubscribe?;
|
|
51
|
+
/** When each joined node's `/status` last answered (ms since epoch). */
|
|
52
|
+
private readonly lastPolledAt;
|
|
53
|
+
/** Joined nodes to poll on the next pass regardless of cadence. */
|
|
54
|
+
private readonly due;
|
|
55
|
+
constructor(opts: HostedNodeWatcherOptions);
|
|
56
|
+
/** Start the timer and the state-change subscription, and poll once. Idempotent. */
|
|
57
|
+
start(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
/** Ask for a pass now; `id` names a joined node to poll ahead of its cadence. Fire-and-forget. */
|
|
60
|
+
poke(id?: string): void;
|
|
61
|
+
/** One pass over every unclaimed, joining and due joined record, serialized against the others. */
|
|
62
|
+
poll(): Promise<void>;
|
|
63
|
+
private pollOnce;
|
|
64
|
+
/** Drop the cadence and poke bookkeeping of nodes no longer on record, so neither map grows with removals. */
|
|
65
|
+
private forgetRemoved;
|
|
66
|
+
private shouldPoll;
|
|
67
|
+
private pollOne;
|
|
68
|
+
/** An unclaimed node that reports a finished claim is `joined`, with the party and owner the claim named. */
|
|
69
|
+
private applyUnclaimed;
|
|
70
|
+
/**
|
|
71
|
+
* A joining node whose redemption settled: `joined` with the member that admitted it and
|
|
72
|
+
* the invitation's issuer as its owner, or `error` with the reason and whether Retry may
|
|
73
|
+
* help. A failed node's child is stopped after the write, as a give-up's is, and kept
|
|
74
|
+
* with its workdir for Retry; the write comes first so the exit the stop raises finds
|
|
75
|
+
* a record the supervisor leaves alone. Still `pending`: nothing.
|
|
76
|
+
*/
|
|
77
|
+
private applyJoining;
|
|
78
|
+
/**
|
|
79
|
+
* A node that reports an owner removed it from its cadre: `error`, not retryable, its child
|
|
80
|
+
* stopped after the write as a failed invitation's is, and its workdir kept for Remove (or a
|
|
81
|
+
* claim node's Reset). The child had already stopped its node, and stays up only to report
|
|
82
|
+
* the removal, so stopping it loses nothing. The party and owner come from the report, for an
|
|
83
|
+
* `unclaimed` record whose claim this watcher never saw: its own party is the placeholder.
|
|
84
|
+
*/
|
|
85
|
+
private applyRemoved;
|
|
86
|
+
/** Stop a node's child after its record went `error`; a failed stop is logged, the record stands. */
|
|
87
|
+
private stopChild;
|
|
88
|
+
/**
|
|
89
|
+
* A joined node's `connected` follows its control-connection count; published only on a
|
|
90
|
+
* change. Its `/status` keeps reporting the invitation for as long as the child that
|
|
91
|
+
* redeemed it runs, and whatever it says there is ignored: the node is a member, and the
|
|
92
|
+
* record says so.
|
|
93
|
+
*/
|
|
94
|
+
private applyJoined;
|
|
95
|
+
}
|