@velum-labs/routekit-daemon 0.18.0 → 0.18.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 +11 -6
- package/dist/host-protocol.d.ts +88 -0
- package/dist/host-protocol.js +10 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.js +556 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +213 -71
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +168 -0
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* ports behind that front door; config/account reload builds a complete new
|
|
7
7
|
* generation before atomically switching new traffic and draining the old.
|
|
8
8
|
*/
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
10
11
|
import { basename, dirname, join } from "node:path";
|
|
11
12
|
import { AccountActivityCoordinator, AccountAuthCoordinator, accountStoreEntries, CLIPROXY_API_KEY_ENV, CLIPROXY_BASE_URL_ENV, cliproxyAccountEntries, cliproxyAccountMatchesKind, cliproxyApiKey, cliproxyAuthDirectory, cliproxyBaseUrl, cliproxyCredentialValid, defaultSubscriptionAccountDirectory, RateLimitTracker, removeCliproxyAccount, removeSubscriptionAccount, renameSubscriptionAccount, sanitizeSubscriptionLabel, subscriptionAccountIdentity, subscriptionCredentialFingerprint } from "@velum-labs/routekit-accounts";
|
|
12
13
|
import { configuredProviderIds, globalRouterConfigPath, parseRouterConfigDocument, routekitHome, writeRouterConfig } from "@velum-labs/routekit-config";
|
|
13
|
-
import { createRouteKitControlHandler, ROUTEKIT_CONTROL_CAPABILITY } from "@velum-labs/routekit-control";
|
|
14
|
+
import { createRouteKitControlHandler, MUTATING_ROUTEKIT_METHODS, ROUTEKIT_CONTROL_CAPABILITY, ROUTEKIT_DAEMON_ROLL_CAPABILITY } from "@velum-labs/routekit-control";
|
|
14
15
|
import { resolveCodexStartupModel, resolveLeaderboardConfig, startSwitchingGatewayProxy } from "@velum-labs/routekit-gateway";
|
|
15
16
|
import { accountKindForCliproxyAuthType, PROVIDERS, resolveAccountConnector } from "@velum-labs/routekit-registry";
|
|
16
17
|
import { startRouter } from "@velum-labs/routekit-router";
|
|
@@ -20,6 +21,7 @@ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
|
20
21
|
import { cleanupAccountTransaction, markAccountTransactionCommitted, prepareAccountTransaction, recoverAccountTransactions, rollbackAccountTransaction } from "./account-transaction.js";
|
|
21
22
|
import { CallAttributionStore, callInspection } from "./call-attribution-store.js";
|
|
22
23
|
import { createCliproxySidecar } from "./cliproxy-sidecar.js";
|
|
24
|
+
import { DAEMON_HOST_PROTOCOL_VERSION } from "./host-protocol.js";
|
|
23
25
|
import { aggregateInspections, buildLeaderboardResult, defaultLeaderboardWindow, LeaderboardRollupStore } from "./leaderboard.js";
|
|
24
26
|
import { DaemonTelemetry, DEFAULT_TELEMETRY_HOST, GatewayTelemetryAggregator, resolveTelemetryProjectKey } from "./telemetry.js";
|
|
25
27
|
export const ROUTEKIT_DAEMON_KIND = "daemon";
|
|
@@ -54,11 +56,11 @@ function resolveDataToken(home, input, tokens) {
|
|
|
54
56
|
throw new Error("RouteKit data-plane token is empty");
|
|
55
57
|
return { token: ensured.token, path };
|
|
56
58
|
}
|
|
57
|
-
function daemonPublicRecordPath(home) {
|
|
59
|
+
export function daemonPublicRecordPath(home) {
|
|
58
60
|
return join(home, "services", "daemon.public.json");
|
|
59
61
|
}
|
|
60
62
|
/** Publish a secret-free discovery file peers can read across OS accounts. */
|
|
61
|
-
function writeDaemonPublicRecord(home, record) {
|
|
63
|
+
export function writeDaemonPublicRecord(home, record) {
|
|
62
64
|
const servicesDir = join(home, "services");
|
|
63
65
|
mkdirSync(home, { recursive: true, mode: SERVICE_HOME_MODE });
|
|
64
66
|
chmodSync(home, SERVICE_HOME_MODE);
|
|
@@ -68,7 +70,7 @@ function writeDaemonPublicRecord(home, record) {
|
|
|
68
70
|
writeFileAtomic(path, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o644 });
|
|
69
71
|
chmodSync(path, 0o644);
|
|
70
72
|
}
|
|
71
|
-
function removeDaemonPublicRecord(home) {
|
|
73
|
+
export function removeDaemonPublicRecord(home) {
|
|
72
74
|
rmSync(daemonPublicRecordPath(home), { force: true });
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -101,7 +103,7 @@ function dataTokenForPrincipal(tokens, cache, ownerToken, principal) {
|
|
|
101
103
|
function revisionPath(home) {
|
|
102
104
|
return join(home, "daemon-revisions.json");
|
|
103
105
|
}
|
|
104
|
-
function
|
|
106
|
+
export function readDaemonRevisions(home) {
|
|
105
107
|
try {
|
|
106
108
|
const parsed = JSON.parse(readFileSync(revisionPath(home), "utf8"));
|
|
107
109
|
return {
|
|
@@ -118,7 +120,7 @@ function readRevisions(home) {
|
|
|
118
120
|
return { config: 0, accounts: 0, daemon: 0 };
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
|
-
function
|
|
123
|
+
export function writeDaemonRevisions(home, revisions) {
|
|
122
124
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
123
125
|
writeFileAtomic(revisionPath(home), `${JSON.stringify(revisions, null, 2)}\n`, {
|
|
124
126
|
mode: 0o600
|
|
@@ -248,26 +250,29 @@ export async function startRouteKitDaemon(options) {
|
|
|
248
250
|
const dataAuth = resolveDataToken(home, options, tokens);
|
|
249
251
|
dataTokenCache.set("default", dataAuth.token);
|
|
250
252
|
const store = createServiceRecordStore({ home, product: ROUTEKIT_PRODUCT });
|
|
253
|
+
const hosted = options.hosted;
|
|
251
254
|
// Held for the daemon's whole lifetime. Lifecycle clients use daemon.lock
|
|
252
255
|
// while this authority lock prevents any second daemon from becoming live.
|
|
253
|
-
const authority =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
const authority = hosted === undefined
|
|
257
|
+
? await acquireLifecycleLock(join(store.directory, "daemon-authority.lock"), {
|
|
258
|
+
timeoutMs: 30_000,
|
|
259
|
+
onWait: async () => {
|
|
260
|
+
const existing = store.read(ROUTEKIT_DAEMON_KIND);
|
|
261
|
+
return existing !== undefined && (await healthyControl(existing))
|
|
262
|
+
? new ControlError({
|
|
263
|
+
code: "conflict",
|
|
264
|
+
message: `RouteKit daemon is already running (pid ${existing.pid})`
|
|
265
|
+
})
|
|
266
|
+
: undefined;
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
: undefined;
|
|
265
270
|
let accountRecovery;
|
|
266
271
|
try {
|
|
267
272
|
accountRecovery = recoverAccountTransactions(home);
|
|
268
273
|
}
|
|
269
274
|
catch (error) {
|
|
270
|
-
authority
|
|
275
|
+
authority?.release();
|
|
271
276
|
throw error;
|
|
272
277
|
}
|
|
273
278
|
let control;
|
|
@@ -282,8 +287,8 @@ export async function startRouteKitDaemon(options) {
|
|
|
282
287
|
let record;
|
|
283
288
|
let closed = false;
|
|
284
289
|
let draining = false;
|
|
285
|
-
let lifecycle = "running";
|
|
286
|
-
let revisions =
|
|
290
|
+
let lifecycle = hosted?.initiallyPaused === true ? "paused" : "running";
|
|
291
|
+
let revisions = readDaemonRevisions(home);
|
|
287
292
|
let currentDocument = canonicalConfigDocument(configPath);
|
|
288
293
|
let currentConfig = parseConfigDocument(currentDocument);
|
|
289
294
|
let mutationTail = Promise.resolve();
|
|
@@ -300,26 +305,27 @@ export async function startRouteKitDaemon(options) {
|
|
|
300
305
|
};
|
|
301
306
|
const startedAt = new Date().toISOString();
|
|
302
307
|
try {
|
|
303
|
-
const previous = store.read(ROUTEKIT_DAEMON_KIND);
|
|
304
|
-
if (previous !== undefined &&
|
|
305
|
-
previous.pid !== process.pid &&
|
|
306
|
-
(await healthyControl(previous))) {
|
|
308
|
+
const previous = hosted === undefined ? store.read(ROUTEKIT_DAEMON_KIND) : undefined;
|
|
309
|
+
if (hosted === undefined && previous !== undefined && previous.pid !== process.pid && (await healthyControl(previous))) {
|
|
307
310
|
throw new ControlError({
|
|
308
311
|
code: "conflict",
|
|
309
312
|
message: `RouteKit daemon is already running (pid ${previous.pid})`
|
|
310
313
|
});
|
|
311
314
|
}
|
|
312
|
-
if (previous !== undefined && previous.pid !== process.pid) {
|
|
315
|
+
if (hosted === undefined && previous !== undefined && previous.pid !== process.pid) {
|
|
313
316
|
// A live-but-unhealthy daemon is not safe to replace under its feet.
|
|
314
317
|
throw new ControlError({
|
|
315
318
|
code: "unavailable",
|
|
316
319
|
message: `RouteKit daemon pid ${previous.pid} is alive but its control plane is unhealthy; stop it before recovery`
|
|
317
320
|
});
|
|
318
321
|
}
|
|
319
|
-
const generation =
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
const generation = hosted?.generation ??
|
|
323
|
+
nextServiceGeneration(Math.max(previous?.generation ?? 0, revisions.daemon));
|
|
324
|
+
if (hosted === undefined) {
|
|
325
|
+
revisions.daemon = generation;
|
|
326
|
+
writeDaemonRevisions(home, revisions);
|
|
327
|
+
}
|
|
328
|
+
const sidecar = hosted?.sidecar ?? createCliproxySidecar({ env });
|
|
323
329
|
sidecarRef = sidecar;
|
|
324
330
|
let leaderboardConfig = resolveLeaderboardConfig(currentConfig);
|
|
325
331
|
const callAttributions = new CallAttributionStore({
|
|
@@ -430,12 +436,16 @@ export async function startRouteKitDaemon(options) {
|
|
|
430
436
|
};
|
|
431
437
|
}
|
|
432
438
|
});
|
|
433
|
-
portless =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
+
portless =
|
|
440
|
+
hosted === undefined
|
|
441
|
+
? await createPortlessSession(options.portless ?? env.ROUTEKIT_PORTLESS !== "0", {
|
|
442
|
+
project: "routekit",
|
|
443
|
+
ownerLabel: "routekit-daemon",
|
|
444
|
+
bareNames: []
|
|
445
|
+
})
|
|
446
|
+
: undefined;
|
|
447
|
+
const dataUrl = hosted?.dataUrl() ??
|
|
448
|
+
(portless?.enabled === true ? portless.register("gateway", proxy.port()) : proxy.url());
|
|
439
449
|
const replaceRouter = async (nextConfig, nextDocument, input) => {
|
|
440
450
|
// Sidecar reconcile runs before the generation commits; any failure
|
|
441
451
|
// below must put the sidecar back to the still-live currentConfig.
|
|
@@ -463,7 +473,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
463
473
|
try {
|
|
464
474
|
if (input.write)
|
|
465
475
|
writeRouterConfig(configPath, nextConfig);
|
|
466
|
-
|
|
476
|
+
writeDaemonRevisions(home, nextRevisions);
|
|
467
477
|
await input.beforeSwap?.();
|
|
468
478
|
}
|
|
469
479
|
catch (error) {
|
|
@@ -472,7 +482,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
472
482
|
chmodSync(configPath, 0o600);
|
|
473
483
|
}
|
|
474
484
|
revisions = previousRevisions;
|
|
475
|
-
|
|
485
|
+
writeDaemonRevisions(home, previousRevisions);
|
|
476
486
|
await candidate.close();
|
|
477
487
|
await sidecar.reconcile(wantsCliproxySidecar(currentConfig));
|
|
478
488
|
throw error;
|
|
@@ -515,17 +525,22 @@ export async function startRouteKitDaemon(options) {
|
|
|
515
525
|
handlers = {
|
|
516
526
|
"daemon.status": async () => ({
|
|
517
527
|
pid: process.pid,
|
|
528
|
+
workerPid: process.pid,
|
|
529
|
+
hostPid: hosted?.hostPid ?? process.pid,
|
|
530
|
+
hostStartedAt: hosted?.hostStartedAt ?? startedAt,
|
|
518
531
|
startedAt,
|
|
519
532
|
packageVersion: options.packageVersion,
|
|
520
533
|
protocolVersion: CONTROL_PROTOCOL_VERSION,
|
|
534
|
+
hostProtocolVersion: hosted === undefined ? 0 : DAEMON_HOST_PROTOCOL_VERSION,
|
|
521
535
|
generation,
|
|
522
536
|
configRevision: revisions.config,
|
|
523
537
|
accountRevision: revisions.accounts,
|
|
524
538
|
controlUrl: control?.url ?? "",
|
|
525
|
-
dataUrl,
|
|
539
|
+
dataUrl: hosted?.dataUrl() ?? dataUrl,
|
|
526
540
|
dataPort: proxy?.port() ?? 0,
|
|
527
541
|
supervisor: supervisorFromEnv(env),
|
|
528
|
-
draining
|
|
542
|
+
draining,
|
|
543
|
+
rolling: hosted?.rolling() ?? false
|
|
529
544
|
}),
|
|
530
545
|
"daemon.reload": async (params) => {
|
|
531
546
|
await serializeMutation(async () => {
|
|
@@ -545,9 +560,66 @@ export async function startRouteKitDaemon(options) {
|
|
|
545
560
|
accountRevision: revisions.accounts
|
|
546
561
|
};
|
|
547
562
|
},
|
|
563
|
+
"daemon.roll": async (params, context) => {
|
|
564
|
+
if (context.principal?.role !== "ephemeral") {
|
|
565
|
+
throw new ControlError({
|
|
566
|
+
code: "unauthorized",
|
|
567
|
+
message: "daemon roll requires the local service credential"
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (options.onRollRequested === undefined) {
|
|
571
|
+
throw new ControlError({
|
|
572
|
+
code: "upgrade_required",
|
|
573
|
+
message: "this daemon does not support rolling process replacement"
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const startedAt = Date.now();
|
|
577
|
+
const supervisor = ["systemd", "launchd", "detached"].includes(supervisorFromEnv(env))
|
|
578
|
+
? supervisorFromEnv(env)
|
|
579
|
+
: "unknown";
|
|
580
|
+
const toVersion = params.candidate?.expectedVersion ?? options.packageVersion;
|
|
581
|
+
daemonTelemetry?.capture("routekit.daemon_lifecycle", {
|
|
582
|
+
action: "roll_started",
|
|
583
|
+
outcome: "success",
|
|
584
|
+
supervisor,
|
|
585
|
+
version: options.packageVersion,
|
|
586
|
+
reason: params.reason,
|
|
587
|
+
from_version: options.packageVersion,
|
|
588
|
+
to_version: toVersion
|
|
589
|
+
});
|
|
590
|
+
try {
|
|
591
|
+
const result = await options.onRollRequested(params);
|
|
592
|
+
daemonTelemetry?.capture("routekit.daemon_lifecycle", {
|
|
593
|
+
action: "roll_committed",
|
|
594
|
+
outcome: "success",
|
|
595
|
+
supervisor,
|
|
596
|
+
version: result.packageVersion,
|
|
597
|
+
reason: params.reason,
|
|
598
|
+
from_version: options.packageVersion,
|
|
599
|
+
to_version: result.packageVersion,
|
|
600
|
+
duration_bucket: durationBucket(Date.now() - startedAt)
|
|
601
|
+
});
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
daemonTelemetry?.capture("routekit.daemon_lifecycle", {
|
|
606
|
+
action: "roll_failed",
|
|
607
|
+
outcome: "error",
|
|
608
|
+
supervisor,
|
|
609
|
+
version: options.packageVersion,
|
|
610
|
+
reason: params.reason,
|
|
611
|
+
from_version: options.packageVersion,
|
|
612
|
+
to_version: toVersion,
|
|
613
|
+
rollback_stage: "candidate",
|
|
614
|
+
duration_bucket: durationBucket(Date.now() - startedAt)
|
|
615
|
+
});
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
},
|
|
548
619
|
"daemon.prepareShutdown": async (params) => {
|
|
549
|
-
if (lifecycle
|
|
620
|
+
if (lifecycle === "quiescing" || lifecycle === "draining" || lifecycle === "closed") {
|
|
550
621
|
return { accepted: true };
|
|
622
|
+
}
|
|
551
623
|
lifecycle = "quiescing";
|
|
552
624
|
draining = true;
|
|
553
625
|
await mutationTail;
|
|
@@ -1653,16 +1725,29 @@ export async function startRouteKitDaemon(options) {
|
|
|
1653
1725
|
version: options.packageVersion
|
|
1654
1726
|
});
|
|
1655
1727
|
};
|
|
1656
|
-
const
|
|
1728
|
+
const routeKitDispatch = createRouteKitControlHandler(handlers, {
|
|
1657
1729
|
onCommitted: (method, params, durationMs) => captureOperation(method, params, "success", durationMs),
|
|
1658
1730
|
onControlError: (method, params, _code, durationMs) => captureOperation(method, params, "error", durationMs)
|
|
1659
1731
|
});
|
|
1732
|
+
const dispatch = async (method, params, context) => {
|
|
1733
|
+
if (lifecycle === "paused" && MUTATING_ROUTEKIT_METHODS.has(method)) {
|
|
1734
|
+
throw new ControlError({
|
|
1735
|
+
code: "unavailable",
|
|
1736
|
+
message: "RouteKit daemon is synchronizing a replacement worker"
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
return await routeKitDispatch(method, params, context);
|
|
1740
|
+
};
|
|
1660
1741
|
control = await startControlServer({
|
|
1661
1742
|
handler: dispatch,
|
|
1662
|
-
token: generateControlToken(),
|
|
1743
|
+
token: hosted?.controlToken ?? generateControlToken(),
|
|
1663
1744
|
product: ROUTEKIT_PRODUCT,
|
|
1664
1745
|
packageVersion: options.packageVersion,
|
|
1665
|
-
|
|
1746
|
+
port: options.controlPort,
|
|
1747
|
+
capabilities: [
|
|
1748
|
+
ROUTEKIT_CONTROL_CAPABILITY,
|
|
1749
|
+
...(hosted === undefined ? [] : [ROUTEKIT_DAEMON_ROLL_CAPABILITY])
|
|
1750
|
+
],
|
|
1666
1751
|
authorize: (presented) => {
|
|
1667
1752
|
const principal = tokens.resolve(presented, "control");
|
|
1668
1753
|
if (principal === undefined)
|
|
@@ -1678,7 +1763,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1678
1763
|
console.error(`RouteKit ${operation} failed (request ${context.requestId}):`, error);
|
|
1679
1764
|
}
|
|
1680
1765
|
});
|
|
1681
|
-
|
|
1766
|
+
const workerRecordInput = {
|
|
1682
1767
|
kind: ROUTEKIT_DAEMON_KIND,
|
|
1683
1768
|
pid: process.pid,
|
|
1684
1769
|
...(processIdentity(process.pid) !== undefined
|
|
@@ -1693,7 +1778,7 @@ export async function startRouteKitDaemon(options) {
|
|
|
1693
1778
|
dataUrl,
|
|
1694
1779
|
dataPort: proxy.port(),
|
|
1695
1780
|
host: options.host ?? "127.0.0.1",
|
|
1696
|
-
portless: portless
|
|
1781
|
+
portless: portless?.enabled ?? false,
|
|
1697
1782
|
drainGraceMs,
|
|
1698
1783
|
authTokenFile: dataAuth.path,
|
|
1699
1784
|
generation,
|
|
@@ -1701,18 +1786,24 @@ export async function startRouteKitDaemon(options) {
|
|
|
1701
1786
|
...(process.argv[1] !== undefined ? { binPath: process.argv[1] } : {}),
|
|
1702
1787
|
args: redactedProcessArgs(process.argv.slice(2)),
|
|
1703
1788
|
cwd: process.cwd()
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1789
|
+
};
|
|
1790
|
+
record =
|
|
1791
|
+
hosted === undefined
|
|
1792
|
+
? store.write(workerRecordInput)
|
|
1793
|
+
: { product: ROUTEKIT_PRODUCT, owner: ROUTEKIT_PRODUCT, ...workerRecordInput };
|
|
1794
|
+
if (hosted === undefined) {
|
|
1795
|
+
writeDaemonPublicRecord(home, {
|
|
1796
|
+
product: ROUTEKIT_PRODUCT,
|
|
1797
|
+
kind: ROUTEKIT_DAEMON_KIND,
|
|
1798
|
+
url: control.url,
|
|
1799
|
+
port: control.port,
|
|
1800
|
+
generation,
|
|
1801
|
+
protocolVersion: CONTROL_PROTOCOL_VERSION,
|
|
1802
|
+
dataUrl,
|
|
1803
|
+
dataPort: proxy.port(),
|
|
1804
|
+
startedAt
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1716
1807
|
daemonTelemetry.capture("routekit.daemon_lifecycle", {
|
|
1717
1808
|
action: "started",
|
|
1718
1809
|
outcome: "success",
|
|
@@ -1745,13 +1836,16 @@ export async function startRouteKitDaemon(options) {
|
|
|
1745
1836
|
await activeRouter?.close();
|
|
1746
1837
|
accountActivity?.close();
|
|
1747
1838
|
accountAuth?.close();
|
|
1748
|
-
|
|
1839
|
+
if (hosted === undefined)
|
|
1840
|
+
await sidecar.close();
|
|
1749
1841
|
await control?.close();
|
|
1750
|
-
if (
|
|
1751
|
-
portless
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1842
|
+
if (hosted === undefined) {
|
|
1843
|
+
if (portless?.enabled)
|
|
1844
|
+
portless.unregister("gateway");
|
|
1845
|
+
store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
|
|
1846
|
+
removeDaemonPublicRecord(home);
|
|
1847
|
+
authority?.release();
|
|
1848
|
+
}
|
|
1755
1849
|
lifecycle = "closed";
|
|
1756
1850
|
})();
|
|
1757
1851
|
return closeRun;
|
|
@@ -1770,6 +1864,42 @@ export async function startRouteKitDaemon(options) {
|
|
|
1770
1864
|
dataUrl,
|
|
1771
1865
|
controlUrl: control.url,
|
|
1772
1866
|
close,
|
|
1867
|
+
retire: async (graceMs = drainGraceMs) => {
|
|
1868
|
+
if (lifecycle === "closed" || lifecycle === "draining")
|
|
1869
|
+
return;
|
|
1870
|
+
lifecycle = "quiescing";
|
|
1871
|
+
draining = true;
|
|
1872
|
+
await mutationTail;
|
|
1873
|
+
lifecycle = "draining";
|
|
1874
|
+
await Promise.all([proxy?.retire(graceMs), control?.retire(Math.min(graceMs, 2_000))]);
|
|
1875
|
+
await activeRouter?.close();
|
|
1876
|
+
accountActivity?.close();
|
|
1877
|
+
accountAuth?.close();
|
|
1878
|
+
gatewayTelemetry?.close();
|
|
1879
|
+
await daemonTelemetry?.shutdown();
|
|
1880
|
+
lifecycle = "closed";
|
|
1881
|
+
},
|
|
1882
|
+
pauseMutations: async () => {
|
|
1883
|
+
if (lifecycle !== "running") {
|
|
1884
|
+
throw new ControlError({ code: "unavailable", message: "RouteKit daemon is not mutable" });
|
|
1885
|
+
}
|
|
1886
|
+
lifecycle = "paused";
|
|
1887
|
+
await mutationTail;
|
|
1888
|
+
return {
|
|
1889
|
+
configRevision: revisions.config,
|
|
1890
|
+
accountRevision: revisions.accounts,
|
|
1891
|
+
configHash: createHash("sha256").update(currentDocument).digest("hex")
|
|
1892
|
+
};
|
|
1893
|
+
},
|
|
1894
|
+
resumeMutations: () => {
|
|
1895
|
+
if (lifecycle === "paused")
|
|
1896
|
+
lifecycle = "running";
|
|
1897
|
+
},
|
|
1898
|
+
snapshot: () => ({
|
|
1899
|
+
configRevision: revisions.config,
|
|
1900
|
+
accountRevision: revisions.accounts,
|
|
1901
|
+
configHash: createHash("sha256").update(currentDocument).digest("hex")
|
|
1902
|
+
}),
|
|
1773
1903
|
reload: async () => {
|
|
1774
1904
|
await handlers["daemon.reload"]({}, {
|
|
1775
1905
|
signal: new AbortController().signal,
|
|
@@ -1785,14 +1915,26 @@ export async function startRouteKitDaemon(options) {
|
|
|
1785
1915
|
await activeRouter?.close();
|
|
1786
1916
|
accountActivity?.close();
|
|
1787
1917
|
accountAuth?.close();
|
|
1788
|
-
|
|
1918
|
+
if (hosted === undefined)
|
|
1919
|
+
await sidecarRef?.close();
|
|
1789
1920
|
await control?.close();
|
|
1790
|
-
if (
|
|
1791
|
-
portless
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1921
|
+
if (hosted === undefined) {
|
|
1922
|
+
if (portless?.enabled)
|
|
1923
|
+
portless.unregister("gateway");
|
|
1924
|
+
if (record !== undefined)
|
|
1925
|
+
store.remove(ROUTEKIT_DAEMON_KIND, { ifPid: process.pid });
|
|
1926
|
+
removeDaemonPublicRecord(home);
|
|
1927
|
+
authority?.release();
|
|
1928
|
+
}
|
|
1796
1929
|
throw error;
|
|
1797
1930
|
}
|
|
1798
1931
|
}
|
|
1932
|
+
export { ROUTEKIT_DAEMON_WORKER_ENV } from "./host-protocol.js";
|
|
1933
|
+
export async function startRouteKitDaemonHost(options) {
|
|
1934
|
+
const daemonHost = await import("./host.js");
|
|
1935
|
+
return await daemonHost.startRouteKitDaemonHost(options);
|
|
1936
|
+
}
|
|
1937
|
+
export async function runRouteKitDaemonWorker(options) {
|
|
1938
|
+
const daemonWorker = await import("./worker.js");
|
|
1939
|
+
return await daemonWorker.runRouteKitDaemonWorker(options);
|
|
1940
|
+
}
|
package/dist/worker.d.ts
ADDED
package/dist/worker.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import cluster from "node:cluster";
|
|
2
|
+
import { DAEMON_HOST_PROTOCOL_VERSION, ROUTEKIT_DAEMON_CONTROL_PORT_ENV, ROUTEKIT_DAEMON_CONTROL_TOKEN_ENV, ROUTEKIT_DAEMON_DATA_PORT_ENV, ROUTEKIT_DAEMON_DATA_URL_ENV, ROUTEKIT_DAEMON_GENERATION_ENV, ROUTEKIT_DAEMON_HOST_PID_ENV, ROUTEKIT_DAEMON_HOST_STARTED_AT_ENV, ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV } from "./host-protocol.js";
|
|
3
|
+
import { startRouteKitDaemon } from "./index.js";
|
|
4
|
+
function requiredEnv(env, name) {
|
|
5
|
+
const value = env[name];
|
|
6
|
+
if (value === undefined || value.length === 0)
|
|
7
|
+
throw new Error(`missing hosted worker environment ${name}`);
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function positiveInteger(value, name) {
|
|
11
|
+
const parsed = Number.parseInt(value, 10);
|
|
12
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
13
|
+
throw new Error(`invalid hosted worker ${name}`);
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
function send(message) {
|
|
17
|
+
if (typeof process.send !== "function")
|
|
18
|
+
throw new Error("hosted daemon worker has no IPC channel");
|
|
19
|
+
process.send(message);
|
|
20
|
+
}
|
|
21
|
+
export async function runRouteKitDaemonWorker(options) {
|
|
22
|
+
if (!cluster.isWorker)
|
|
23
|
+
throw new Error("daemon worker must run as a cluster worker");
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
const generation = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_GENERATION_ENV), "generation");
|
|
26
|
+
const hostPid = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_HOST_PID_ENV), "host pid");
|
|
27
|
+
const hostStartedAt = requiredEnv(env, ROUTEKIT_DAEMON_HOST_STARTED_AT_ENV);
|
|
28
|
+
const controlToken = requiredEnv(env, ROUTEKIT_DAEMON_CONTROL_TOKEN_ENV);
|
|
29
|
+
const dataPort = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_DATA_PORT_ENV), "data port");
|
|
30
|
+
const controlPort = positiveInteger(requiredEnv(env, ROUTEKIT_DAEMON_CONTROL_PORT_ENV), "control port");
|
|
31
|
+
let dataUrl = requiredEnv(env, ROUTEKIT_DAEMON_DATA_URL_ENV);
|
|
32
|
+
let rolling = env[ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV] === "1";
|
|
33
|
+
let sidecarState = { managed: true, running: false };
|
|
34
|
+
let nextRequestId = 0;
|
|
35
|
+
const pending = new Map();
|
|
36
|
+
let running;
|
|
37
|
+
const requestHost = async (request) => {
|
|
38
|
+
const requestId = `${process.pid}-${++nextRequestId}`;
|
|
39
|
+
const response = new Promise((resolve, reject) => {
|
|
40
|
+
pending.set(requestId, { resolve: (value) => resolve(value), reject });
|
|
41
|
+
});
|
|
42
|
+
send({ ...request, requestId });
|
|
43
|
+
return await response;
|
|
44
|
+
};
|
|
45
|
+
const sidecar = {
|
|
46
|
+
reconcile: async (wanted) => {
|
|
47
|
+
sidecarState = await requestHost({ type: "host.sidecar", operation: "reconcile", wanted });
|
|
48
|
+
},
|
|
49
|
+
refresh: async () => {
|
|
50
|
+
sidecarState = await requestHost({ type: "host.sidecar", operation: "refresh" });
|
|
51
|
+
},
|
|
52
|
+
running: () => sidecarState.running,
|
|
53
|
+
managed: () => sidecarState.managed,
|
|
54
|
+
reachable: async (timeoutMs) => {
|
|
55
|
+
const reachable = await requestHost({
|
|
56
|
+
type: "host.sidecar",
|
|
57
|
+
operation: "reachable",
|
|
58
|
+
timeoutMs
|
|
59
|
+
});
|
|
60
|
+
sidecarState = { ...sidecarState, running: reachable };
|
|
61
|
+
return reachable;
|
|
62
|
+
},
|
|
63
|
+
close: async () => { }
|
|
64
|
+
};
|
|
65
|
+
const onRollRequested = async (params) => await requestHost({ type: "host.roll", params });
|
|
66
|
+
process.on("message", (message) => {
|
|
67
|
+
if (message.type === "host.response") {
|
|
68
|
+
const waiter = pending.get(message.requestId);
|
|
69
|
+
if (waiter === undefined)
|
|
70
|
+
return;
|
|
71
|
+
pending.delete(message.requestId);
|
|
72
|
+
if (message.ok)
|
|
73
|
+
waiter.resolve(message.result);
|
|
74
|
+
else
|
|
75
|
+
waiter.reject(new Error(message.error));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!message.type.startsWith("worker."))
|
|
79
|
+
return;
|
|
80
|
+
const request = message;
|
|
81
|
+
void (async () => {
|
|
82
|
+
try {
|
|
83
|
+
let result;
|
|
84
|
+
switch (request.type) {
|
|
85
|
+
case "worker.pause":
|
|
86
|
+
result = await running?.pauseMutations();
|
|
87
|
+
break;
|
|
88
|
+
case "worker.resume":
|
|
89
|
+
running?.resumeMutations();
|
|
90
|
+
rolling = false;
|
|
91
|
+
break;
|
|
92
|
+
case "worker.hostState":
|
|
93
|
+
dataUrl = request.dataUrl;
|
|
94
|
+
rolling = request.rolling;
|
|
95
|
+
break;
|
|
96
|
+
case "worker.retire":
|
|
97
|
+
await running?.retire(request.graceMs);
|
|
98
|
+
result = { retired: true };
|
|
99
|
+
break;
|
|
100
|
+
case "worker.shutdown":
|
|
101
|
+
await running?.close();
|
|
102
|
+
result = { closed: true };
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
send({ type: "worker.response", requestId: request.requestId, ok: true, result });
|
|
106
|
+
if (request.type === "worker.retire" || request.type === "worker.shutdown") {
|
|
107
|
+
setImmediate(() => process.exit(0));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
send({
|
|
112
|
+
type: "worker.response",
|
|
113
|
+
requestId: request.requestId,
|
|
114
|
+
ok: false,
|
|
115
|
+
error: error instanceof Error ? error.message : String(error)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
});
|
|
120
|
+
process.once("disconnect", () => {
|
|
121
|
+
void running?.close().finally(() => process.exit(1));
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
running = await startRouteKitDaemon({
|
|
125
|
+
...options,
|
|
126
|
+
port: dataPort,
|
|
127
|
+
controlPort,
|
|
128
|
+
hosted: {
|
|
129
|
+
generation,
|
|
130
|
+
controlToken,
|
|
131
|
+
dataUrl: () => dataUrl,
|
|
132
|
+
hostPid,
|
|
133
|
+
hostStartedAt,
|
|
134
|
+
rolling: () => rolling,
|
|
135
|
+
sidecar,
|
|
136
|
+
initiallyPaused: env[ROUTEKIT_DAEMON_INITIAL_PAUSED_ENV] === "1"
|
|
137
|
+
},
|
|
138
|
+
onRollRequested,
|
|
139
|
+
onShutdownRequested: (reason) => send({ type: "host.shutdown", reason })
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (process.connected)
|
|
144
|
+
process.disconnect();
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
const snapshot = running.snapshot();
|
|
148
|
+
send({
|
|
149
|
+
type: "worker.ready",
|
|
150
|
+
protocolVersion: DAEMON_HOST_PROTOCOL_VERSION,
|
|
151
|
+
workerPid: process.pid,
|
|
152
|
+
...(running.record.processIdentity !== undefined
|
|
153
|
+
? { workerProcessIdentity: running.record.processIdentity }
|
|
154
|
+
: {}),
|
|
155
|
+
workerStartedAt: running.record.startedAt,
|
|
156
|
+
packageVersion: options.packageVersion,
|
|
157
|
+
generation,
|
|
158
|
+
configRevision: snapshot.configRevision,
|
|
159
|
+
accountRevision: snapshot.accountRevision,
|
|
160
|
+
configHash: snapshot.configHash,
|
|
161
|
+
controlUrl: running.controlUrl,
|
|
162
|
+
controlPort: running.record.port,
|
|
163
|
+
dataUrl: running.dataUrl,
|
|
164
|
+
dataPort: running.record.dataPort ?? 0,
|
|
165
|
+
...(process.argv[1] !== undefined ? { binPath: process.argv[1] } : {})
|
|
166
|
+
});
|
|
167
|
+
return await new Promise(() => undefined);
|
|
168
|
+
}
|