@camstack/server 1.2.689 → 1.2.691
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/CHANGELOG.md +19 -0
- package/dist/agent/agent-heap-watch.js +38 -0
- package/dist/agent/agent-status-metrics.js +15 -0
- package/dist/agent/main.js +20 -2
- package/dist/api/core/server-management.provider.js +14 -1
- package/dist/api/trpc/generated-cap-routers.js +16 -0
- package/dist/api/trpc/trpc.router.js +1 -1
- package/dist/core/moleculer/moleculer.service.js +8 -0
- package/dist/launch-args.js +255 -0
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.691] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- An unpaired agent now shows its real memory on its own page instead of unknown.
|
|
15
|
+
|
|
16
|
+
## [1.2.690] - 2026-10-29
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The agent reports its memory, reclaims stranded pages and gets a heap ceiling sized to the host; a node's launch parameters (heap ceiling, allocator) come from the server and can be overridden per node from the hub
|
|
21
|
+
|
|
10
22
|
## [1.2.689] - 2026-10-29
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -17,6 +29,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
29
|
- Generated routers for the pet-fountain capability
|
|
18
30
|
- Agents report their own process memory in the topology (the core process showed 0), and an unreadable memory shows as unknown
|
|
19
31
|
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The agent now reports its own [mem] agent-main heartbeat and reclaims stranded memory, as the hub does
|
|
35
|
+
- The hub tells its addon runners which telemetry categories anyone consumes, so unused ones stop crossing the process boundary
|
|
36
|
+
- Node launch parameters (heap ceiling, allocator env) are decided by a closure module, with a per-node override and a source line in the boot log
|
|
37
|
+
- A node's heap-ceiling override can be read and set through server-management (applies at the next restart)
|
|
38
|
+
|
|
20
39
|
## [1.2.688] - 2026-10-29
|
|
21
40
|
|
|
22
41
|
### Changed
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startAgentHeapWatch = startAgentHeapWatch;
|
|
4
|
+
/**
|
|
5
|
+
* The agent root's own memory, on the record and returned to the OS.
|
|
6
|
+
*
|
|
7
|
+
* hub-main has had the `[mem]` heartbeat and the stranded-page reclaimer since
|
|
8
|
+
* 2026-08; the agent root process (container AND desktop — both reach
|
|
9
|
+
* `startAgent` through the same launcher) had neither, so a measured 0.8–1.4 GB
|
|
10
|
+
* agent root emitted no line at all and never gave a page back. This is the same
|
|
11
|
+
* mechanism, not a second one: `startHeapWatch` with the V8 reclaimer, labelled
|
|
12
|
+
* `agent-main`.
|
|
13
|
+
*/
|
|
14
|
+
const system_1 = require("@camstack/system");
|
|
15
|
+
const DEFAULT_DEPS = {
|
|
16
|
+
start: system_1.startHeapWatch,
|
|
17
|
+
createReclaimer: system_1.createV8Reclaimer,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Start the heartbeat + reclaim for the agent root. Returns the stop function.
|
|
21
|
+
* `createV8Reclaimer()` returns undefined when V8 declines; the heartbeat then
|
|
22
|
+
* runs report-only, exactly as hub-main does.
|
|
23
|
+
*/
|
|
24
|
+
function startAgentHeapWatch(logger, deps = DEFAULT_DEPS) {
|
|
25
|
+
const reclaimer = deps.createReclaimer();
|
|
26
|
+
const sink = {
|
|
27
|
+
info: (line) => {
|
|
28
|
+
logger.info(line);
|
|
29
|
+
},
|
|
30
|
+
warn: (line) => {
|
|
31
|
+
logger.warn(line);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return deps.start(system_1.AGENT_MAIN_HEAP_WATCH_LABEL, sink, undefined, reclaimer === undefined ? undefined : { reclaim: reclaimer }, undefined, undefined,
|
|
35
|
+
// Say who chose the ceiling: the default ceiling below is applied by the
|
|
36
|
+
// launcher's argv, and an absent flag means the image/desktop predates it.
|
|
37
|
+
true);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveStatusMetrics = resolveStatusMetrics;
|
|
4
|
+
/**
|
|
5
|
+
* The metrics source `$agent.status` reads (and so the agent's own local page).
|
|
6
|
+
*
|
|
7
|
+
* A PAIRED agent boots the `metrics-provider` addon, so the registry answers. An
|
|
8
|
+
* UNPAIRED agent boots no addon at all (agent-boot-mode.ts), the registry has no
|
|
9
|
+
* provider, and the page read memory as unknown although the host reader works. The
|
|
10
|
+
* fallback is the SAME native reader the addon registers - never a second formula -
|
|
11
|
+
* and it is asked only when the registry has nothing.
|
|
12
|
+
*/
|
|
13
|
+
function resolveStatusMetrics(fromRegistry, fallback) {
|
|
14
|
+
return () => fromRegistry() ?? fallback();
|
|
15
|
+
}
|
package/dist/agent/main.js
CHANGED
|
@@ -59,6 +59,7 @@ const agent_node_certificates_js_1 = require("./agent-node-certificates.js");
|
|
|
59
59
|
const agent_reconnect_js_1 = require("./agent-reconnect.js");
|
|
60
60
|
const serial_queue_js_1 = require("./serial-queue.js");
|
|
61
61
|
const agent_boot_mode_js_1 = require("./agent-boot-mode.js");
|
|
62
|
+
const agent_status_metrics_js_1 = require("./agent-status-metrics.js");
|
|
62
63
|
const agent_self_update_js_1 = require("./agent-self-update.js");
|
|
63
64
|
const update_check_scheduler_js_1 = require("../core/updates/update-check-scheduler.js");
|
|
64
65
|
const agent_infra_capabilities_js_1 = require("./agent-infra-capabilities.js");
|
|
@@ -76,6 +77,7 @@ const register_agent_cap_dispatch_js_1 = require("./register-agent-cap-dispatch.
|
|
|
76
77
|
// entries flow through it (console output + hub forwarding).
|
|
77
78
|
// ---------------------------------------------------------------------------
|
|
78
79
|
const system_2 = require("@camstack/system");
|
|
80
|
+
const agent_heap_watch_js_1 = require("./agent-heap-watch.js");
|
|
79
81
|
/** A reconnect (stop + rebuild + start) that takes longer than this is abandoned. */
|
|
80
82
|
const RECONNECT_DEADLINE_MS = 90_000;
|
|
81
83
|
/** How often the agent re-evaluates whether to ask its hub "am I still a member?" (D857). */
|
|
@@ -97,6 +99,11 @@ const DATA_NODEID_CAPS = new Set(types_2.ALL_CAPABILITY_DEFINITIONS.filter((c) =
|
|
|
97
99
|
// Main
|
|
98
100
|
// ---------------------------------------------------------------------------
|
|
99
101
|
async function startAgent(configPath) {
|
|
102
|
+
// The agent root's own memory: `[mem] agent-main` heartbeat + the stranded-page
|
|
103
|
+
// reclaim hub-main has had since 2026-08. Started first so a slow boot is on the
|
|
104
|
+
// record; lines reach the hub through the same LogManager the addons use.
|
|
105
|
+
const heapWatchLogger = agentLogManager.createLogger().withTags({ addonId: 'heap-watch' });
|
|
106
|
+
(0, agent_heap_watch_js_1.startAgentHeapWatch)(heapWatchLogger);
|
|
100
107
|
const config = (0, agent_config_js_1.loadAgentConfig)(configPath);
|
|
101
108
|
// Derive CAMSTACK_HUB_URL from the configured hub address so remote agents
|
|
102
109
|
// no longer need a second, redundant env var set by hand (the cross-node
|
|
@@ -431,6 +438,7 @@ async function startAgent(configPath) {
|
|
|
431
438
|
withTags: () => consoleLogger,
|
|
432
439
|
};
|
|
433
440
|
const capabilityRegistry = new system_1.CapabilityRegistry(consoleLogger);
|
|
441
|
+
let unpairedMetrics = null;
|
|
434
442
|
// Declare all capabilities the agent uses — required before registerProvider()
|
|
435
443
|
const agentCapabilities = [
|
|
436
444
|
types_2.storageCapability,
|
|
@@ -480,7 +488,7 @@ async function startAgent(configPath) {
|
|
|
480
488
|
dataDir: config.dataDir,
|
|
481
489
|
runnerBootCheck: () => agentRunnerBootCheck?.current(),
|
|
482
490
|
});
|
|
483
|
-
capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService));
|
|
491
|
+
capabilityRegistry.registerProvider('server-management', agent_update_service_js_1.AGENT_RUNTIME_ADDON_ID, (0, server_management_provider_js_1.buildServerManagementProvider)(agentUpdateService, config.dataDir));
|
|
484
492
|
// The agent's certificates (D856): mesh leaf swap with automatic rollback, and the
|
|
485
493
|
// hub-issued HTTPS leaf hot-swapped into the sniffing listener. The listener is
|
|
486
494
|
// created later in boot, so it is read through a getter.
|
|
@@ -547,7 +555,17 @@ async function startAgent(configPath) {
|
|
|
547
555
|
// Resolve the current metrics-provider cap lazily from the registry.
|
|
548
556
|
// The cap is registered during bootCoreAddons but may not exist in test
|
|
549
557
|
// scenarios — `null` is a documented fallback for both.
|
|
550
|
-
|
|
558
|
+
// An UNPAIRED agent boots no addon, so the registry has no provider: its own page
|
|
559
|
+
// reads the same native reader the addon would register (see agent-status-metrics.ts).
|
|
560
|
+
getMetricsProvider: (0, agent_status_metrics_js_1.resolveStatusMetrics)(() => capabilityRegistry.getSingleton('metrics-provider'), () => {
|
|
561
|
+
if ((0, agent_boot_mode_js_1.decideAgentBootMode)({ paired: (0, agent_pairing_store_js_1.isAgentPaired)(config.configPath) }) === 'full')
|
|
562
|
+
return null;
|
|
563
|
+
if (unpairedMetrics === null) {
|
|
564
|
+
unpairedMetrics = new system_1.NativeMetricsProvider();
|
|
565
|
+
unpairedMetrics.startSampling(5000);
|
|
566
|
+
}
|
|
567
|
+
return unpairedMetrics;
|
|
568
|
+
}),
|
|
551
569
|
agentVersion: readAgentVersion(),
|
|
552
570
|
// Drives `$agent.reload` — re-runs the same discovery pass the bootstrap
|
|
553
571
|
// does at startup, picking up tarballs just landed via `$agent.deploy`.
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildLaunchOverrideMethods = buildLaunchOverrideMethods;
|
|
3
4
|
exports.buildServerManagementProvider = buildServerManagementProvider;
|
|
4
|
-
|
|
5
|
+
const launch_args_js_1 = require("../../launch-args.js");
|
|
6
|
+
/** The per-node launch override methods (ADR 0970), backed by the node's data dir. */
|
|
7
|
+
function buildLaunchOverrideMethods(dataDir) {
|
|
8
|
+
return {
|
|
9
|
+
getLaunchOverrides: () => Promise.resolve({ heapMb: (0, launch_args_js_1.readHeapOverrideMb)(dataDir) }),
|
|
10
|
+
setLaunchOverrides: (input) => {
|
|
11
|
+
(0, launch_args_js_1.writeHeapOverrideMb)(dataDir, input.heapMb);
|
|
12
|
+
return Promise.resolve({ heapMb: input.heapMb, restartRequired: true });
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function buildServerManagementProvider(service, dataDir) {
|
|
5
17
|
if (service === null)
|
|
6
18
|
return null;
|
|
7
19
|
return {
|
|
@@ -10,5 +22,6 @@ function buildServerManagementProvider(service) {
|
|
|
10
22
|
applyServerUpdate: (input) => service.applyServerUpdate(input),
|
|
11
23
|
rollbackServerUpdate: () => service.rollbackServerUpdate(),
|
|
12
24
|
restartServer: () => Promise.resolve(service.restartNode()),
|
|
25
|
+
...buildLaunchOverrideMethods(dataDir),
|
|
13
26
|
};
|
|
14
27
|
}
|
|
@@ -11047,6 +11047,22 @@ function createCapRouter_serverManagement(getProvider, createRemoteProxy) {
|
|
|
11047
11047
|
const p = resolveProvider('server-management', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
11048
11048
|
return p.restartServer();
|
|
11049
11049
|
}),
|
|
11050
|
+
getLaunchOverrides: trpc_middleware_js_1.adminProcedure
|
|
11051
|
+
.input(zod_1.z.object({ nodeId: zod_1.z.string().optional() }).optional())
|
|
11052
|
+
.output(types_126.serverManagementCapability.methods.getLaunchOverrides.output)
|
|
11053
|
+
.query(async ({ input, ctx }) => {
|
|
11054
|
+
const p = resolveProvider('server-management', input?.nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
11055
|
+
return p.getLaunchOverrides();
|
|
11056
|
+
}),
|
|
11057
|
+
setLaunchOverrides: trpc_middleware_js_1.adminProcedure
|
|
11058
|
+
.input(types_126.serverManagementCapability.methods.setLaunchOverrides.input.loose())
|
|
11059
|
+
.output(types_126.serverManagementCapability.methods.setLaunchOverrides.output)
|
|
11060
|
+
.mutation(async ({ input, ctx }) => {
|
|
11061
|
+
const { nodeId, ...methodInput } = input;
|
|
11062
|
+
const p = resolveProvider('server-management', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
11063
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
11064
|
+
return p.setLaunchOverrides(methodInput);
|
|
11065
|
+
}),
|
|
11050
11066
|
});
|
|
11051
11067
|
}
|
|
11052
11068
|
function createCapRouter_settingsStore(getProvider, createRemoteProxy) {
|
|
@@ -254,7 +254,7 @@ function buildServerProviders(services) {
|
|
|
254
254
|
integrations: wrap(() => (0, cap_providers_js_1.buildIntegrationsProvider)(services.addonRegistry, services.eventBus, services.loggingService, services.capabilityRegistry)),
|
|
255
255
|
nodes: wrap(() => (0, cap_providers_js_1.buildNodesProvider)(services.agentRegistry, services.moleculer, services.addonRegistry, (0, cap_providers_js_1.createNodeRootPackageLookup)(services.moleculer, services.serverUpdateService), services.loggingService.createLogger('nodes'))),
|
|
256
256
|
// server-management — runtime-updatable root package (phase 1: hub only).
|
|
257
|
-
'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService)),
|
|
257
|
+
'server-management': wrap(() => (0, server_management_provider_js_1.buildServerManagementProvider)(services.serverUpdateService, path.resolve(process.env['CAMSTACK_DATA'] ?? 'camstack-data'))),
|
|
258
258
|
addons: wrap((ctx) => (0, cap_providers_js_1.buildAddonsProvider)(services.addonRegistry, services.addonPackageService, services.loggingService, services.moleculer, services.configService, ctx, services.agentRegistry)),
|
|
259
259
|
// webrtc-session — the resolved broker singleton, wrapped per request to
|
|
260
260
|
// inject the server-read User-Agent into signaling attribution (the broker
|
|
@@ -759,6 +759,14 @@ class MoleculerService {
|
|
|
759
759
|
// agents (non-retaining nodes).
|
|
760
760
|
const hubChildRegistry = this.localChildRegistry;
|
|
761
761
|
(0, system_1.setNodeEventInterest)(this.broker, () => hubChildRegistry.aggregateEventInterest());
|
|
762
|
+
// Producer-side suppression (D8): tell each forked child which categories
|
|
763
|
+
// someone else wants — siblings plus the hub's own in-process subscribers
|
|
764
|
+
// (the admin live stream is one) — so a high-frequency category nobody
|
|
765
|
+
// wants never leaves its process. Hub only: an agent cannot see the hub's
|
|
766
|
+
// subscribers, so its children keep sending everything. A remote node's
|
|
767
|
+
// interest in a ring-denied category is NOT visible here (ADR 0969).
|
|
768
|
+
hubChildRegistry.setEventInterestSource(() => (0, system_1.getLocalEventInterest)(this.broker));
|
|
769
|
+
(0, system_1.onLocalEventInterestChange)(this.broker, () => hubChildRegistry.refreshEventInterest());
|
|
762
770
|
}
|
|
763
771
|
}
|
|
764
772
|
/**
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ALLOCATOR_ENV_DEFAULTS = exports.OVERRIDE_MAX_MB = exports.OVERRIDE_MIN_MB = exports.AGENT_HEAP_CAP_MB = exports.AGENT_HEAP_FLOOR_MB = exports.HEAP_CEILING_SOURCE_ENV = exports.LAUNCH_OVERRIDES_FILE = void 0;
|
|
37
|
+
exports.computedAgentHeapMb = computedAgentHeapMb;
|
|
38
|
+
exports.planLaunch = planLaunch;
|
|
39
|
+
exports.renderLaunchPlan = renderLaunchPlan;
|
|
40
|
+
exports.overridesPath = overridesPath;
|
|
41
|
+
exports.readHeapOverrideMb = readHeapOverrideMb;
|
|
42
|
+
exports.writeHeapOverrideMb = writeHeapOverrideMb;
|
|
43
|
+
exports.readCgroupLimitBytes = readCgroupLimitBytes;
|
|
44
|
+
exports.runLaunchCli = runLaunchCli;
|
|
45
|
+
/**
|
|
46
|
+
* NODE LAUNCH ARGUMENTS — decided by the server closure, executed by the image.
|
|
47
|
+
*
|
|
48
|
+
* `node <closure>/dist/launch-args.js --role hub|agent [--allocator-env]` prints
|
|
49
|
+
* what the launcher should add to the node's command line, so the Docker
|
|
50
|
+
* entrypoint, the Electron shell and the Windows service supervisor carry NO copy
|
|
51
|
+
* of the formula: changing it (or a node's override) needs a closure update and a
|
|
52
|
+
* restart, never a new image or app.
|
|
53
|
+
*
|
|
54
|
+
* Zero dependencies (node built-ins only): it runs BEFORE the closure's own
|
|
55
|
+
* `node_modules` resolution, from either the active closure or the baked seed.
|
|
56
|
+
*
|
|
57
|
+
* ## Output (stdout, one record per line, `\n` separated)
|
|
58
|
+
*
|
|
59
|
+
* arg --max-old-space-size=2048 an extra node argv entry (flags only)
|
|
60
|
+
* env NAME=value an environment variable to export
|
|
61
|
+
* source override|legacy-env|computed|v8-default
|
|
62
|
+
* warn <free text> something the launcher should log
|
|
63
|
+
*
|
|
64
|
+
* Nothing else is printed. A launcher treats any line it does not recognise as
|
|
65
|
+
* absent, and a non-zero exit, a timeout or empty output as "no extra args".
|
|
66
|
+
*
|
|
67
|
+
* ## The heap ceiling
|
|
68
|
+
*
|
|
69
|
+
* - `override` — `<dataDir>/launch-overrides.json` `{ "heapMb": N }`, set from
|
|
70
|
+
* the hub (`serverManagement.setLaunchOverrides`) and persisted on the node.
|
|
71
|
+
* - `legacy-env` — `CAMSTACK_HUB_HEAP_MB` (hub) / `CAMSTACK_AGENT_HEAP_MB`
|
|
72
|
+
* (agent), still honoured when no override is set.
|
|
73
|
+
* - `computed` — AGENT only: clamp(min(RAM, cgroup limit) / 4, 1536, 4096) MB.
|
|
74
|
+
* V8's own quarter-of-RAM rule with a floor above every measured agent RSS
|
|
75
|
+
* (0.8-1.4 GB), so it bounds a runaway and never a healthy root.
|
|
76
|
+
* - `v8-default` — HUB only, nothing set: no flag. A hardcoded 1536 killed the
|
|
77
|
+
* hub four times in 15 h (see the history in the Dockerfile); V8's default
|
|
78
|
+
* here was 4288 MB and is still bounded.
|
|
79
|
+
*
|
|
80
|
+
* Set on argv, never NODE_OPTIONS: forked addon runners are sized separately.
|
|
81
|
+
*/
|
|
82
|
+
const fs = __importStar(require("node:fs"));
|
|
83
|
+
const os = __importStar(require("node:os"));
|
|
84
|
+
const path = __importStar(require("node:path"));
|
|
85
|
+
exports.LAUNCH_OVERRIDES_FILE = 'launch-overrides.json';
|
|
86
|
+
/** Env var carrying {@link CeilingSource} to the node so its heartbeat can say who chose. */
|
|
87
|
+
exports.HEAP_CEILING_SOURCE_ENV = 'CAMSTACK_HEAP_CEILING_SOURCE';
|
|
88
|
+
exports.AGENT_HEAP_FLOOR_MB = 1536;
|
|
89
|
+
exports.AGENT_HEAP_CAP_MB = 4096;
|
|
90
|
+
/** An override outside this range is ignored with a warning, never applied. */
|
|
91
|
+
exports.OVERRIDE_MIN_MB = 512;
|
|
92
|
+
exports.OVERRIDE_MAX_MB = 65536;
|
|
93
|
+
/**
|
|
94
|
+
* The native allocator pair, welded: glibc reads MALLOC_ARENA_MAX once at malloc
|
|
95
|
+
* init, so it must exist before `exec node`; sharp restores libvips concurrency
|
|
96
|
+
* from 1 to `availableParallelism()` the moment it appears, so the pair travels
|
|
97
|
+
* together or not at all. Defaults only: an operator value already in the
|
|
98
|
+
* environment wins (the launcher applies them with `: "${NAME:=value}"` semantics).
|
|
99
|
+
*/
|
|
100
|
+
exports.ALLOCATOR_ENV_DEFAULTS = {
|
|
101
|
+
MALLOC_ARENA_MAX: '2',
|
|
102
|
+
VIPS_CONCURRENCY: '1',
|
|
103
|
+
};
|
|
104
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
105
|
+
/** cgroup v1 reports "unlimited" as a value near 2^63. */
|
|
106
|
+
const CGROUP_UNLIMITED_BYTES = 2 ** 60;
|
|
107
|
+
function validMb(value) {
|
|
108
|
+
return Number.isInteger(value) && value >= exports.OVERRIDE_MIN_MB && value <= exports.OVERRIDE_MAX_MB;
|
|
109
|
+
}
|
|
110
|
+
/** The agent's computed ceiling for `host`, in MB. */
|
|
111
|
+
function computedAgentHeapMb(host) {
|
|
112
|
+
const limits = [host.totalMemBytes, host.cgroupLimitBytes ?? Number.POSITIVE_INFINITY].filter((n) => Number.isFinite(n) && n > 0);
|
|
113
|
+
if (limits.length === 0)
|
|
114
|
+
return exports.AGENT_HEAP_FLOOR_MB;
|
|
115
|
+
const quarterMb = Math.floor(Math.min(...limits) / BYTES_PER_MB / 4);
|
|
116
|
+
return Math.min(exports.AGENT_HEAP_CAP_MB, Math.max(exports.AGENT_HEAP_FLOOR_MB, quarterMb));
|
|
117
|
+
}
|
|
118
|
+
function planLaunch(input) {
|
|
119
|
+
const warnings = [];
|
|
120
|
+
let source = input.role === 'agent' ? 'computed' : 'v8-default';
|
|
121
|
+
let heapMb = input.role === 'agent' ? computedAgentHeapMb(input.host) : null;
|
|
122
|
+
const legacy = input.legacyEnv;
|
|
123
|
+
if (legacy !== undefined && legacy !== '') {
|
|
124
|
+
const mb = /^[1-9][0-9]*$/.test(legacy) ? Number(legacy) : Number.NaN;
|
|
125
|
+
if (validMb(mb)) {
|
|
126
|
+
source = 'legacy-env';
|
|
127
|
+
heapMb = mb;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
warnings.push(`ignoring the legacy heap env "${legacy}": not an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (input.overrideMb !== null) {
|
|
134
|
+
if (validMb(input.overrideMb)) {
|
|
135
|
+
source = 'override';
|
|
136
|
+
heapMb = input.overrideMb;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
warnings.push(`ignoring the heap override ${input.overrideMb}: not an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
args: heapMb === null ? [] : [`--max-old-space-size=${heapMb}`],
|
|
144
|
+
env: {
|
|
145
|
+
[exports.HEAP_CEILING_SOURCE_ENV]: source,
|
|
146
|
+
...(input.allocatorEnv ? exports.ALLOCATOR_ENV_DEFAULTS : {}),
|
|
147
|
+
},
|
|
148
|
+
source,
|
|
149
|
+
warnings,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function renderLaunchPlan(plan) {
|
|
153
|
+
const lines = [
|
|
154
|
+
...plan.args.map((a) => `arg ${a}`),
|
|
155
|
+
...Object.entries(plan.env).map(([k, v]) => `env ${k}=${v}`),
|
|
156
|
+
`source ${plan.source}`,
|
|
157
|
+
...plan.warnings.map((w) => `warn ${w.replace(/[\r\n]+/g, ' ')}`),
|
|
158
|
+
];
|
|
159
|
+
return `${lines.join('\n')}\n`;
|
|
160
|
+
}
|
|
161
|
+
const realFs = {
|
|
162
|
+
readFile(file) {
|
|
163
|
+
try {
|
|
164
|
+
return fs.readFileSync(file, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
function overridesPath(dataDir) {
|
|
172
|
+
return path.join(dataDir, exports.LAUNCH_OVERRIDES_FILE);
|
|
173
|
+
}
|
|
174
|
+
/** The persisted heap override in MB; `null` when absent, unreadable or not a number. */
|
|
175
|
+
function readHeapOverrideMb(dataDir, io = realFs) {
|
|
176
|
+
const raw = io.readFile(overridesPath(dataDir));
|
|
177
|
+
if (raw === null)
|
|
178
|
+
return null;
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(raw);
|
|
181
|
+
if (typeof parsed !== 'object' || parsed === null || !('heapMb' in parsed))
|
|
182
|
+
return null;
|
|
183
|
+
const value = parsed.heapMb;
|
|
184
|
+
return typeof value === 'number' ? value : null;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Persist (or, with `null`, clear) the heap override. Validated against the same
|
|
192
|
+
* range the launcher enforces, so a value the next launch would ignore is refused
|
|
193
|
+
* here instead of silently doing nothing. Written atomically (temp + rename).
|
|
194
|
+
*/
|
|
195
|
+
function writeHeapOverrideMb(dataDir, heapMb) {
|
|
196
|
+
const file = overridesPath(dataDir);
|
|
197
|
+
if (heapMb === null) {
|
|
198
|
+
fs.rmSync(file, { force: true });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!validMb(heapMb)) {
|
|
202
|
+
throw new Error(`heap override must be an integer in ${exports.OVERRIDE_MIN_MB}-${exports.OVERRIDE_MAX_MB} MB, got ${heapMb}`);
|
|
203
|
+
}
|
|
204
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
205
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
206
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ heapMb })}\n`, { mode: 0o600 });
|
|
207
|
+
fs.renameSync(tmp, file);
|
|
208
|
+
}
|
|
209
|
+
function readCgroupLimitBytes(io = realFs, files) {
|
|
210
|
+
for (const file of files ?? [
|
|
211
|
+
'/sys/fs/cgroup/memory.max',
|
|
212
|
+
'/sys/fs/cgroup/memory/memory.limit_in_bytes',
|
|
213
|
+
]) {
|
|
214
|
+
const raw = io.readFile(file)?.trim();
|
|
215
|
+
if (raw === undefined || !/^[0-9]+$/.test(raw))
|
|
216
|
+
continue;
|
|
217
|
+
const bytes = Number(raw);
|
|
218
|
+
if (bytes > 0 && bytes < CGROUP_UNLIMITED_BYTES)
|
|
219
|
+
return bytes;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function parseRole(argv) {
|
|
224
|
+
const i = argv.indexOf('--role');
|
|
225
|
+
const value = i >= 0 ? argv[i + 1] : undefined;
|
|
226
|
+
return value === 'hub' || value === 'agent' ? value : null;
|
|
227
|
+
}
|
|
228
|
+
/** Run the CLI; returns the stdout text and the exit code. Never throws. */
|
|
229
|
+
function runLaunchCli(argv, deps) {
|
|
230
|
+
const role = parseRole(argv);
|
|
231
|
+
if (role === null)
|
|
232
|
+
return { stdout: '', code: 2 };
|
|
233
|
+
const dataDir = path.resolve(deps.env['CAMSTACK_DATA'] ?? 'camstack-data');
|
|
234
|
+
const legacyName = role === 'hub' ? 'CAMSTACK_HUB_HEAP_MB' : 'CAMSTACK_AGENT_HEAP_MB';
|
|
235
|
+
const plan = planLaunch({
|
|
236
|
+
role,
|
|
237
|
+
host: {
|
|
238
|
+
totalMemBytes: deps.totalMemBytes,
|
|
239
|
+
cgroupLimitBytes: readCgroupLimitBytes(deps.io, deps.cgroupFiles),
|
|
240
|
+
},
|
|
241
|
+
overrideMb: readHeapOverrideMb(dataDir, deps.io),
|
|
242
|
+
legacyEnv: deps.env[legacyName],
|
|
243
|
+
allocatorEnv: argv.includes('--allocator-env'),
|
|
244
|
+
});
|
|
245
|
+
return { stdout: renderLaunchPlan(plan), code: 0 };
|
|
246
|
+
}
|
|
247
|
+
if (require.main === module) {
|
|
248
|
+
const result = runLaunchCli(process.argv.slice(2), {
|
|
249
|
+
env: process.env,
|
|
250
|
+
io: realFs,
|
|
251
|
+
totalMemBytes: os.totalmem(),
|
|
252
|
+
});
|
|
253
|
+
process.stdout.write(result.stdout);
|
|
254
|
+
process.exitCode = result.code;
|
|
255
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.691",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./package.json": "./package.json",
|
|
25
25
|
"./main.js": "./dist/main.js",
|
|
26
26
|
"./launcher.js": "./dist/launcher.js",
|
|
27
|
-
"./starter.js": "./dist/starter.js"
|
|
27
|
+
"./starter.js": "./dist/starter.js",
|
|
28
|
+
"./launch-args.js": "./dist/launch-args.js"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"build": "../../scripts/tsc7 -p tsconfig.build.json && tsup && node scripts/copy-closure-patches.mjs",
|
|
@@ -49,10 +50,10 @@
|
|
|
49
50
|
]
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
53
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
53
|
+
"@camstack/addon-admin-ui": "1.2.518",
|
|
54
|
+
"@camstack/addon-agent-ui": "1.2.293",
|
|
54
55
|
"@camstack/addon-auth": "1.2.170",
|
|
55
|
-
"@camstack/addon-benchmark": "1.2.
|
|
56
|
+
"@camstack/addon-benchmark": "1.2.292",
|
|
56
57
|
"@camstack/addon-decoder-nodeav": "1.2.144",
|
|
57
58
|
"@camstack/addon-notifiers": "1.2.623",
|
|
58
59
|
"@camstack/addon-pipeline": "1.2.634",
|
|
@@ -60,9 +61,9 @@
|
|
|
60
61
|
"@camstack/addon-post-analysis": "1.2.651",
|
|
61
62
|
"@camstack/develop": "0.1.39",
|
|
62
63
|
"@camstack/sdk": "1.2.154",
|
|
63
|
-
"@camstack/system": "1.2.
|
|
64
|
-
"@camstack/types": "1.2.
|
|
65
|
-
"@camstack/ui-library": "1.2.
|
|
64
|
+
"@camstack/system": "1.2.519",
|
|
65
|
+
"@camstack/types": "1.2.445",
|
|
66
|
+
"@camstack/ui-library": "1.2.351",
|
|
66
67
|
"@fastify/compress": "^9.0.0",
|
|
67
68
|
"@fastify/cookie": "^11.0.2",
|
|
68
69
|
"@fastify/cors": "^11.2.0",
|