@theford/runly 0.0.0-stage → 0.20.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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/dist/cli/client.d.ts +80 -0
  4. package/dist/cli/client.js +294 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/daemon-unit.d.ts +58 -0
  7. package/dist/cli/daemon-unit.js +289 -0
  8. package/dist/cli/daemon-unit.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +1482 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/install.d.ts +9 -0
  13. package/dist/cli/install.js +53 -0
  14. package/dist/cli/install.js.map +1 -0
  15. package/dist/cli/logs.d.ts +43 -0
  16. package/dist/cli/logs.js +232 -0
  17. package/dist/cli/logs.js.map +1 -0
  18. package/dist/cli/tether.d.ts +3 -0
  19. package/dist/cli/tether.js +65 -0
  20. package/dist/cli/tether.js.map +1 -0
  21. package/dist/cli/watchdog.d.ts +1 -0
  22. package/dist/cli/watchdog.js +81 -0
  23. package/dist/cli/watchdog.js.map +1 -0
  24. package/dist/core/builds.d.ts +20 -0
  25. package/dist/core/builds.js +93 -0
  26. package/dist/core/builds.js.map +1 -0
  27. package/dist/core/caller-env.d.ts +17 -0
  28. package/dist/core/caller-env.js +77 -0
  29. package/dist/core/caller-env.js.map +1 -0
  30. package/dist/core/diagnostics.d.ts +68 -0
  31. package/dist/core/diagnostics.js +34 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/env-vars.d.ts +35 -0
  34. package/dist/core/env-vars.js +32 -0
  35. package/dist/core/env-vars.js.map +1 -0
  36. package/dist/core/events.d.ts +10 -0
  37. package/dist/core/events.js +42 -0
  38. package/dist/core/events.js.map +1 -0
  39. package/dist/core/exec.d.ts +22 -0
  40. package/dist/core/exec.js +84 -0
  41. package/dist/core/exec.js.map +1 -0
  42. package/dist/core/journal.d.ts +227 -0
  43. package/dist/core/journal.js +395 -0
  44. package/dist/core/journal.js.map +1 -0
  45. package/dist/core/logs.d.ts +56 -0
  46. package/dist/core/logs.js +259 -0
  47. package/dist/core/logs.js.map +1 -0
  48. package/dist/core/manifest.d.ts +292 -0
  49. package/dist/core/manifest.js +179 -0
  50. package/dist/core/manifest.js.map +1 -0
  51. package/dist/core/namespaces.d.ts +7 -0
  52. package/dist/core/namespaces.js +59 -0
  53. package/dist/core/namespaces.js.map +1 -0
  54. package/dist/core/paths.d.ts +31 -0
  55. package/dist/core/paths.js +65 -0
  56. package/dist/core/paths.js.map +1 -0
  57. package/dist/core/policy.d.ts +94 -0
  58. package/dist/core/policy.js +122 -0
  59. package/dist/core/policy.js.map +1 -0
  60. package/dist/core/ports.d.ts +52 -0
  61. package/dist/core/ports.js +94 -0
  62. package/dist/core/ports.js.map +1 -0
  63. package/dist/core/presets.d.ts +25 -0
  64. package/dist/core/presets.js +86 -0
  65. package/dist/core/presets.js.map +1 -0
  66. package/dist/core/procscan.d.ts +111 -0
  67. package/dist/core/procscan.js +342 -0
  68. package/dist/core/procscan.js.map +1 -0
  69. package/dist/core/retention.d.ts +97 -0
  70. package/dist/core/retention.js +322 -0
  71. package/dist/core/retention.js.map +1 -0
  72. package/dist/core/sleep.d.ts +51 -0
  73. package/dist/core/sleep.js +99 -0
  74. package/dist/core/sleep.js.map +1 -0
  75. package/dist/core/tree-ledger.d.ts +39 -0
  76. package/dist/core/tree-ledger.js +92 -0
  77. package/dist/core/tree-ledger.js.map +1 -0
  78. package/dist/core/types.d.ts +21 -0
  79. package/dist/core/types.js +6 -0
  80. package/dist/core/types.js.map +1 -0
  81. package/dist/core/units.d.ts +20 -0
  82. package/dist/core/units.js +69 -0
  83. package/dist/core/units.js.map +1 -0
  84. package/dist/core/upkeep.d.ts +90 -0
  85. package/dist/core/upkeep.js +176 -0
  86. package/dist/core/upkeep.js.map +1 -0
  87. package/dist/core/util.d.ts +46 -0
  88. package/dist/core/util.js +168 -0
  89. package/dist/core/util.js.map +1 -0
  90. package/dist/core/version.d.ts +39 -0
  91. package/dist/core/version.js +144 -0
  92. package/dist/core/version.js.map +1 -0
  93. package/dist/core/worktree.d.ts +51 -0
  94. package/dist/core/worktree.js +309 -0
  95. package/dist/core/worktree.js.map +1 -0
  96. package/dist/daemon/budget.d.ts +155 -0
  97. package/dist/daemon/budget.js +288 -0
  98. package/dist/daemon/budget.js.map +1 -0
  99. package/dist/daemon/election.d.ts +9 -0
  100. package/dist/daemon/election.js +140 -0
  101. package/dist/daemon/election.js.map +1 -0
  102. package/dist/daemon/engine.d.ts +1703 -0
  103. package/dist/daemon/engine.js +5305 -0
  104. package/dist/daemon/engine.js.map +1 -0
  105. package/dist/daemon/index.d.ts +2 -0
  106. package/dist/daemon/index.js +412 -0
  107. package/dist/daemon/index.js.map +1 -0
  108. package/dist/daemon/proxy.d.ts +108 -0
  109. package/dist/daemon/proxy.js +615 -0
  110. package/dist/daemon/proxy.js.map +1 -0
  111. package/dist/daemon/supervisor.d.ts +101 -0
  112. package/dist/daemon/supervisor.js +493 -0
  113. package/dist/daemon/supervisor.js.map +1 -0
  114. package/dist/drivers/appliances.d.ts +11 -0
  115. package/dist/drivers/appliances.js +174 -0
  116. package/dist/drivers/appliances.js.map +1 -0
  117. package/dist/drivers/datastores.d.ts +144 -0
  118. package/dist/drivers/datastores.js +775 -0
  119. package/dist/drivers/datastores.js.map +1 -0
  120. package/dist/drivers/preview.d.ts +69 -0
  121. package/dist/drivers/preview.js +605 -0
  122. package/dist/drivers/preview.js.map +1 -0
  123. package/dist/drivers/types.d.ts +70 -0
  124. package/dist/drivers/types.js +13 -0
  125. package/dist/drivers/types.js.map +1 -0
  126. package/package.json +63 -4
  127. package/schema/backlot.schema.json +549 -0
  128. package/schema/runly.schema.json +556 -0
@@ -0,0 +1,155 @@
1
+ import type { ResourceSpec } from '../core/manifest.js';
2
+ import type { Budget } from '../core/policy.js';
3
+ import { BrokerError } from '../core/util.js';
4
+ export interface Cost {
5
+ memoryBytes: number;
6
+ cpu: number;
7
+ /** False when the manifest did not say and a default was assumed. */
8
+ declared: boolean;
9
+ }
10
+ /** What a service or appliance costs running and building — declared, or the conservative default. */
11
+ export declare function costsOf(spec: ResourceSpec | undefined, budget: Budget): {
12
+ run: Cost;
13
+ build: Cost;
14
+ };
15
+ export interface NeedItem {
16
+ kind: 'build' | 'start' | 'appliance';
17
+ name: string;
18
+ cost: Cost;
19
+ /** A build that will not run (its `when:` inputs are unchanged): listed, costs nothing. */
20
+ skipped?: string;
21
+ /** Builds sharing a wave run at once and their costs add up; a build without one runs alone. */
22
+ wave?: number;
23
+ }
24
+ /**
25
+ * The budget refused (it can never fit, its queue is full, the wait ran out,
26
+ * or the caller went away). Nothing about the environment failed, so a bind
27
+ * refused this way must not count toward the hygiene escalation (decision 0007).
28
+ */
29
+ export declare class BudgetRefusal extends BrokerError {
30
+ }
31
+ export interface Need {
32
+ items: NeedItem[];
33
+ /** Run resources of everything started — held for the whole operation. */
34
+ start: {
35
+ memoryBytes: number;
36
+ cpu: number;
37
+ };
38
+ /** The costliest build wave (builds of one wave run at once, so their costs add up) — held only while building. */
39
+ build: {
40
+ memoryBytes: number;
41
+ cpu: number;
42
+ };
43
+ }
44
+ export declare function needOf(items: NeedItem[]): Need;
45
+ export declare const total: (n: Need) => {
46
+ memoryBytes: number;
47
+ cpu: number;
48
+ };
49
+ export interface Committed {
50
+ memoryBytes: number;
51
+ cpu: number;
52
+ /** One line per contributor, for a refusal or `runly plan`. */
53
+ items: Array<{
54
+ what: string;
55
+ memoryBytes: number;
56
+ cpu: number;
57
+ }>;
58
+ }
59
+ export interface Machine {
60
+ totalBytes: number;
61
+ /** Linux MemAvailable; null where the platform has no honest equivalent (macOS). */
62
+ availableBytes: number | null;
63
+ load1: number;
64
+ cores: number;
65
+ /** Linux PSI `/proc/pressure/cpu` "some" (percent of time a runnable task waited); null where absent. */
66
+ cpuPressure?: {
67
+ avg10: number;
68
+ avg60: number;
69
+ } | null;
70
+ }
71
+ /** Linux pressure stall information for the CPU, or null (no PSI: macOS, an old kernel, a container without it). */
72
+ export declare function readCpuPressure(): {
73
+ avg10: number;
74
+ avg60: number;
75
+ } | null;
76
+ export declare function readMachine(): Machine;
77
+ export interface Verdict {
78
+ fits: boolean;
79
+ /** Can never fit, whatever is released. */
80
+ impossible?: string;
81
+ /** What it would wait for. */
82
+ waitFor?: string;
83
+ committed: Committed;
84
+ machine: Machine;
85
+ budget: Budget;
86
+ }
87
+ /** One in-flight admission: its start part until released, its build part until the build phase ends. */
88
+ export declare class Reservation {
89
+ readonly id: number;
90
+ readonly label: string;
91
+ start: {
92
+ memoryBytes: number;
93
+ cpu: number;
94
+ };
95
+ build: {
96
+ memoryBytes: number;
97
+ cpu: number;
98
+ };
99
+ private readonly onRelease;
100
+ constructor(id: number, label: string, start: {
101
+ memoryBytes: number;
102
+ cpu: number;
103
+ }, build: {
104
+ memoryBytes: number;
105
+ cpu: number;
106
+ }, onRelease: (r: Reservation) => void);
107
+ released: boolean;
108
+ readonly admittedAt: number;
109
+ /** The build phase is over: its share goes back. */
110
+ releaseBuild(): void;
111
+ /**
112
+ * One of the services this reservation starts is now running, and is
113
+ * counted as such (decision 0036): its share leaves the reservation, or
114
+ * `ps` and every other admission saw it twice until the whole bind ended.
115
+ */
116
+ consume(cost: {
117
+ memoryBytes: number;
118
+ cpu: number;
119
+ }): void;
120
+ release(): void;
121
+ }
122
+ export declare class LoadBudget {
123
+ private readonly policy;
124
+ private readonly committedRunning;
125
+ private readonly machine;
126
+ private reservations;
127
+ private queue;
128
+ private ticket;
129
+ /** A moving estimate of how long an admission is held, for the queue ETA. */
130
+ private holdMs;
131
+ constructor(policy: () => Budget, committedRunning: () => Committed, machine?: () => Machine);
132
+ /** Running services plus every in-flight reservation. */
133
+ committed(): Committed;
134
+ /** Would `need` be admitted right now (ignoring the queue)? `skipLoadGate`: a wake of a recently running service. */
135
+ check(need: Need, opts?: {
136
+ skipLoadGate?: boolean;
137
+ }): Verdict;
138
+ /** Queue position (1-based) of a label, for `runly plan`. */
139
+ queueLength(): number;
140
+ /**
141
+ * Wait until `need` fits and it is this request's turn; returns the
142
+ * reservation, which the caller MUST release. Fails clearly when the need
143
+ * can never fit, the queue is full, or the wait runs out.
144
+ */
145
+ admit(need: Need, label: string, opts?: {
146
+ onWait?: (position: number, etaMs: number, waitFor: string) => void;
147
+ waitMs?: number;
148
+ source?: string;
149
+ /** The caller went away: a queued request is dropped instead of binding for nobody. */
150
+ signal?: AbortSignal;
151
+ /** A wake of a service that ran recently: the CPU gate is skipped, memory still applies. */
152
+ skipLoadGate?: boolean;
153
+ }): Promise<Reservation>;
154
+ private reserve;
155
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * The server-wide load budget (decision 0036).
3
+ *
4
+ * Every `up` (and every start on demand) states what it needs before it
5
+ * builds or starts anything: the run resources of each service and appliance
6
+ * it will start, plus its builds — those of one dependency wave run at once, so
7
+ * the costliest wave's SUM is what is held, and only while it builds. A start's
8
+ * share is handed back as each service starts (from then on the running service
9
+ * itself is counted). The budget admits it when
10
+ *
11
+ * - what runly has committed (running services' and in-flight builds'
12
+ * declared resources, across every stack this daemon serves) plus the
13
+ * need fits under the memory and cpu budget, and
14
+ * - on Linux, MemAvailable minus the need stays above the reserve (work
15
+ * that is not runly's — another daemon, a human's IDE), and
16
+ * - the CPU is not saturated: on Linux with PSI, `/proc/pressure/cpu`
17
+ * "some" is below `cpuPressure` percent over BOTH the last 10 s and the
18
+ * last 60 s (a sustained stall, not a spike that is already over);
19
+ * elsewhere the 1-minute load average is below `loadPerCore` x cores. A
20
+ * wake of a service that ran recently skips this gate (the memory gates
21
+ * still apply): it resumes what the box already carried.
22
+ *
23
+ * Otherwise the request queues, FIFO, with its position and an estimate, for
24
+ * at most `waitMs`, and then fails with what it waited for. A need of nothing
25
+ * (every build skipped, nothing to start) never queues. A need that can never
26
+ * fit (bigger than the whole budget) fails at once. Idle-stopped services
27
+ * release their share by no longer running. A refusal is a BudgetRefusal: the
28
+ * environment did not fail, so it never counts toward hygiene escalation.
29
+ */
30
+ import { readFileSync } from 'node:fs';
31
+ import { cpus, loadavg, totalmem } from 'node:os';
32
+ import { BrokerError } from '../core/util.js';
33
+ import { formatDuration, formatSize, parseSize } from '../core/units.js';
34
+ /** What a service or appliance costs running and building — declared, or the conservative default. */
35
+ export function costsOf(spec, budget) {
36
+ const runMem = parseSize(spec?.memory);
37
+ const buildMem = parseSize(spec?.build?.memory);
38
+ return {
39
+ run: {
40
+ memoryBytes: runMem ?? budget.defaultRun.memoryBytes,
41
+ cpu: spec?.cpu ?? budget.defaultRun.cpu,
42
+ declared: runMem !== undefined && spec?.cpu !== undefined,
43
+ },
44
+ build: {
45
+ memoryBytes: buildMem ?? budget.defaultBuild.memoryBytes,
46
+ cpu: spec?.build?.cpu ?? budget.defaultBuild.cpu,
47
+ declared: buildMem !== undefined && spec?.build?.cpu !== undefined,
48
+ },
49
+ };
50
+ }
51
+ /**
52
+ * The budget refused (it can never fit, its queue is full, the wait ran out,
53
+ * or the caller went away). Nothing about the environment failed, so a bind
54
+ * refused this way must not count toward the hygiene escalation (decision 0007).
55
+ */
56
+ export class BudgetRefusal extends BrokerError {
57
+ }
58
+ export function needOf(items) {
59
+ const start = { memoryBytes: 0, cpu: 0 };
60
+ const waves = new Map();
61
+ items.forEach((it, i) => {
62
+ if (it.skipped)
63
+ return;
64
+ if (it.kind === 'build') {
65
+ const key = it.wave === undefined ? `solo:${i}` : `wave:${it.wave}`;
66
+ const w = waves.get(key) ?? { memoryBytes: 0, cpu: 0 };
67
+ w.memoryBytes += it.cost.memoryBytes;
68
+ w.cpu += it.cost.cpu;
69
+ waves.set(key, w);
70
+ }
71
+ else {
72
+ start.memoryBytes += it.cost.memoryBytes;
73
+ start.cpu += it.cost.cpu;
74
+ }
75
+ });
76
+ const build = { memoryBytes: 0, cpu: 0 };
77
+ for (const w of waves.values()) {
78
+ build.memoryBytes = Math.max(build.memoryBytes, w.memoryBytes);
79
+ build.cpu = Math.max(build.cpu, w.cpu);
80
+ }
81
+ return { items, start, build };
82
+ }
83
+ export const total = (n) => ({ memoryBytes: n.start.memoryBytes + n.build.memoryBytes, cpu: n.start.cpu + n.build.cpu });
84
+ /** Linux pressure stall information for the CPU, or null (no PSI: macOS, an old kernel, a container without it). */
85
+ export function readCpuPressure() {
86
+ if (process.platform !== 'linux')
87
+ return null;
88
+ try {
89
+ const line = readFileSync('/proc/pressure/cpu', 'utf8').split('\n').find((l) => l.startsWith('some '));
90
+ const avg10 = Number(/avg10=([\d.]+)/.exec(line ?? '')?.[1]);
91
+ const avg60 = Number(/avg60=([\d.]+)/.exec(line ?? '')?.[1]);
92
+ return Number.isFinite(avg10) && Number.isFinite(avg60) ? { avg10, avg60 } : null;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ export function readMachine() {
99
+ let availableBytes = null;
100
+ if (process.platform === 'linux') {
101
+ try {
102
+ const kb = /MemAvailable:\s+(\d+)\s+kB/.exec(readFileSync('/proc/meminfo', 'utf8'))?.[1];
103
+ if (kb !== undefined)
104
+ availableBytes = Number(kb) * 1024;
105
+ }
106
+ catch {
107
+ /* unreadable: the accounting half still applies */
108
+ }
109
+ }
110
+ return { totalBytes: totalmem(), availableBytes, load1: loadavg()[0] ?? 0, cores: cpus().length || 1, cpuPressure: readCpuPressure() };
111
+ }
112
+ /** One in-flight admission: its start part until released, its build part until the build phase ends. */
113
+ export class Reservation {
114
+ id;
115
+ label;
116
+ start;
117
+ build;
118
+ onRelease;
119
+ constructor(id, label, start, build, onRelease) {
120
+ this.id = id;
121
+ this.label = label;
122
+ this.start = start;
123
+ this.build = build;
124
+ this.onRelease = onRelease;
125
+ }
126
+ released = false;
127
+ admittedAt = Date.now();
128
+ /** The build phase is over: its share goes back. */
129
+ releaseBuild() {
130
+ this.build = { memoryBytes: 0, cpu: 0 };
131
+ }
132
+ /**
133
+ * One of the services this reservation starts is now running, and is
134
+ * counted as such (decision 0036): its share leaves the reservation, or
135
+ * `ps` and every other admission saw it twice until the whole bind ended.
136
+ */
137
+ consume(cost) {
138
+ this.start = {
139
+ memoryBytes: Math.max(0, this.start.memoryBytes - cost.memoryBytes),
140
+ cpu: Math.max(0, this.start.cpu - cost.cpu),
141
+ };
142
+ }
143
+ release() {
144
+ if (this.released)
145
+ return;
146
+ this.released = true;
147
+ this.onRelease(this);
148
+ }
149
+ }
150
+ export class LoadBudget {
151
+ policy;
152
+ committedRunning;
153
+ machine;
154
+ reservations = new Map();
155
+ queue = [];
156
+ ticket = 0;
157
+ /** A moving estimate of how long an admission is held, for the queue ETA. */
158
+ holdMs = 60_000;
159
+ constructor(policy, committedRunning, machine = readMachine) {
160
+ this.policy = policy;
161
+ this.committedRunning = committedRunning;
162
+ this.machine = machine;
163
+ }
164
+ /** Running services plus every in-flight reservation. */
165
+ committed() {
166
+ const base = this.committedRunning();
167
+ const items = [...base.items];
168
+ let memoryBytes = base.memoryBytes;
169
+ let cpu = base.cpu;
170
+ for (const r of this.reservations.values()) {
171
+ const m = r.start.memoryBytes + r.build.memoryBytes;
172
+ const c = r.start.cpu + r.build.cpu;
173
+ if (m === 0 && c === 0)
174
+ continue;
175
+ memoryBytes += m;
176
+ cpu += c;
177
+ items.push({ what: `in flight: ${r.label}`, memoryBytes: m, cpu: c });
178
+ }
179
+ return { memoryBytes, cpu, items };
180
+ }
181
+ /** Would `need` be admitted right now (ignoring the queue)? `skipLoadGate`: a wake of a recently running service. */
182
+ check(need, opts = {}) {
183
+ const budget = this.policy();
184
+ const committed = this.committed();
185
+ const machine = this.machine();
186
+ const want = total(need);
187
+ if (!budget.enabled || (want.memoryBytes === 0 && want.cpu === 0))
188
+ return { fits: true, committed, machine, budget };
189
+ if (want.memoryBytes > budget.memoryBytes) {
190
+ return { fits: false, impossible: `it needs ${formatSize(want.memoryBytes)} of memory and the whole budget is ${formatSize(budget.memoryBytes)} (BACKLOT_BUDGET_MEMORY)`, committed, machine, budget };
191
+ }
192
+ if (want.cpu > budget.cpu) {
193
+ return { fits: false, impossible: `it needs ${want.cpu} cpu and the whole budget is ${budget.cpu} (BACKLOT_BUDGET_CPU)`, committed, machine, budget };
194
+ }
195
+ const reasons = [];
196
+ if (committed.memoryBytes + want.memoryBytes > budget.memoryBytes) {
197
+ reasons.push(`memory: needs ${formatSize(want.memoryBytes)}, the budget has ${formatSize(Math.max(0, budget.memoryBytes - committed.memoryBytes))} of ${formatSize(budget.memoryBytes)} free (runly has committed ${formatSize(committed.memoryBytes)})`);
198
+ }
199
+ if (committed.cpu + want.cpu > budget.cpu) {
200
+ reasons.push(`cpu: needs ${round(want.cpu)}, the budget has ${round(Math.max(0, budget.cpu - committed.cpu))} of ${round(budget.cpu)} free`);
201
+ }
202
+ if (machine.availableBytes !== null && machine.availableBytes - want.memoryBytes < budget.reserveBytes) {
203
+ reasons.push(`free memory: the box has ${formatSize(machine.availableBytes)} available and keeps ${formatSize(budget.reserveBytes)} in reserve (BACKLOT_BUDGET_RESERVE)`);
204
+ }
205
+ if (!opts.skipLoadGate) {
206
+ const psi = machine.cpuPressure ?? null;
207
+ if (psi) {
208
+ // PSI says what load average only hints at: how much of the time
209
+ // runnable work actually WAITED for a CPU. Both windows must be over
210
+ // the threshold, so a spike that is already over does not hold anyone.
211
+ if (budget.cpuPressure < 100 && psi.avg10 >= budget.cpuPressure && psi.avg60 >= budget.cpuPressure) {
212
+ reasons.push(`cpu pressure: runnable work waited ${psi.avg10.toFixed(0)}% of the last 10s and ${psi.avg60.toFixed(0)}% of the last minute (limit ${budget.cpuPressure}%, BACKLOT_BUDGET_CPU_PRESSURE)`);
213
+ }
214
+ }
215
+ else if (machine.load1 > budget.loadPerCore * machine.cores) {
216
+ reasons.push(`load: ${machine.load1.toFixed(1)} on ${machine.cores} cores is above ${budget.loadPerCore} per core (BACKLOT_BUDGET_LOAD_PER_CORE)`);
217
+ }
218
+ }
219
+ return { fits: reasons.length === 0, waitFor: reasons.join('; ') || undefined, committed, machine, budget };
220
+ }
221
+ /** Queue position (1-based) of a label, for `runly plan`. */
222
+ queueLength() {
223
+ return this.queue.length;
224
+ }
225
+ /**
226
+ * Wait until `need` fits and it is this request's turn; returns the
227
+ * reservation, which the caller MUST release. Fails clearly when the need
228
+ * can never fit, the queue is full, or the wait runs out.
229
+ */
230
+ async admit(need, label, opts = {}) {
231
+ const budget = this.policy();
232
+ const source = opts.source ?? 'budget';
233
+ const gone = () => new BudgetRefusal('env-error', `'${label}' was dropped from the load budget's queue: its caller disconnected`, source);
234
+ if (opts.signal?.aborted)
235
+ throw gone();
236
+ const want = total(need);
237
+ // Nothing to build or start costs nothing: it never waits behind anyone.
238
+ if (want.memoryBytes === 0 && want.cpu === 0)
239
+ return this.reserve(need, label);
240
+ const first = this.check(need, opts);
241
+ if (first.impossible) {
242
+ throw new BudgetRefusal('env-error', `the load budget can never admit '${label}': ${first.impossible}. Declare smaller resources: in the manifest or raise the budget (decision 0036)`, source);
243
+ }
244
+ if (this.queue.length === 0 && first.fits)
245
+ return this.reserve(need, label);
246
+ if (this.queue.length >= budget.maxQueue) {
247
+ throw new BudgetRefusal('env-error', `the load budget's queue is full (${this.queue.length} waiting, BACKLOT_BUDGET_MAX_QUEUE) — '${label}' was not queued; retry later`, source);
248
+ }
249
+ const ticket = ++this.ticket;
250
+ this.queue.push({ ticket, label });
251
+ const started = Date.now();
252
+ const limit = opts.waitMs ?? budget.waitMs;
253
+ let lastBeat = 0;
254
+ try {
255
+ for (;;) {
256
+ if (opts.signal?.aborted)
257
+ throw gone();
258
+ const position = this.queue.findIndex((q) => q.ticket === ticket) + 1;
259
+ const verdict = this.check(need, opts);
260
+ if (position === 1 && verdict.fits)
261
+ return this.reserve(need, label);
262
+ const waitFor = position > 1 ? `${position - 1} request(s) ahead in the queue${verdict.waitFor ? `; ${verdict.waitFor}` : ''}` : (verdict.waitFor ?? 'its turn');
263
+ if (Date.now() - started > limit) {
264
+ throw new BudgetRefusal('env-error', `waited ${formatDuration(Date.now() - started)} for the load budget and '${label}' still does not fit — ${waitFor}. ` +
265
+ `Stop what you do not need ('runly down', 'runly ps --all' shows what runs), or raise the budget (BACKLOT_BUDGET_MEMORY, BACKLOT_BUDGET_CPU; decision 0036)`, source);
266
+ }
267
+ if (opts.onWait && Date.now() - lastBeat >= 1000) {
268
+ lastBeat = Date.now();
269
+ opts.onWait(position, Math.max(1000, position * this.holdMs - (Date.now() - started)), waitFor);
270
+ }
271
+ await new Promise((r) => setTimeout(r, 250));
272
+ }
273
+ }
274
+ finally {
275
+ this.queue = this.queue.filter((q) => q.ticket !== ticket);
276
+ }
277
+ }
278
+ reserve(need, label) {
279
+ const r = new Reservation(++this.ticket, label, { ...need.start }, { ...need.build }, (done) => {
280
+ this.reservations.delete(done.id);
281
+ this.holdMs = Math.round(this.holdMs * 0.7 + (Date.now() - done.admittedAt) * 0.3);
282
+ });
283
+ this.reservations.set(r.id, r);
284
+ return r;
285
+ }
286
+ }
287
+ const round = (n) => Math.round(n * 10) / 10;
288
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/daemon/budget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Try to become the daemon for this state root.
3
+ *
4
+ * Returns true if we own it and may proceed to bind the socket, false if a
5
+ * live daemon already holds it and we should concede.
6
+ */
7
+ export declare function electSelf(path?: string): boolean;
8
+ /** Release the lock, but only if it is still ours. */
9
+ export declare function releaseSelf(path?: string): void;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Daemon singleton election.
3
+ *
4
+ * The socket cannot elect a leader by itself. A daemon that finds a socket file
5
+ * must decide whether it is live or a leftover from a crash, and the only way
6
+ * to answer that is to ping it — but between the ping and the bind, a second
7
+ * daemon can win the socket, and unlinking "the stale socket" then deletes a
8
+ * LIVE one. Both daemons end up bound, both sweep the same journal, and each
9
+ * reaps the other's processes.
10
+ *
11
+ * So election happens on a lock file first, using the same identity rule as
12
+ * process reclaim (decision 0019): a holder is real only if its recorded pid
13
+ * AND start time still match a live process. Only the winner is allowed to
14
+ * touch the socket.
15
+ */
16
+ import { openSync, writeSync, closeSync, readFileSync, renameSync, rmSync, unlinkSync, statSync } from 'node:fs';
17
+ import { lockPath } from '../core/paths.js';
18
+ import { sameProcess, startTime } from '../core/procscan.js';
19
+ const readClaim = (path) => {
20
+ try {
21
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
22
+ return typeof raw?.pid === 'number' ? raw : undefined;
23
+ }
24
+ catch {
25
+ return undefined; // absent, truncated, or mid-write
26
+ }
27
+ };
28
+ const selfClaim = () => ({ pid: process.pid, startTime: startTime(process.pid) });
29
+ /** Is this claim held by a process that is genuinely still running? */
30
+ function claimIsLive(claim) {
31
+ if (!claim)
32
+ return false;
33
+ if (claim.pid === process.pid)
34
+ return false; // our own stale lock from a previous life
35
+ return sameProcess(claim.pid, claim.startTime);
36
+ }
37
+ /** Was this lock written within the last few seconds? */
38
+ function recentlyTouched(path, withinMs = 3000) {
39
+ try {
40
+ return Date.now() - statSync(path).mtimeMs < withinMs;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /** Deliberately synchronous: election runs before the daemon serves anything. */
47
+ function sleepMs(ms) {
48
+ const until = Date.now() + ms;
49
+ while (Date.now() < until) {
50
+ /* spin briefly */
51
+ }
52
+ }
53
+ /**
54
+ * Try to become the daemon for this state root.
55
+ *
56
+ * Returns true if we own it and may proceed to bind the socket, false if a
57
+ * live daemon already holds it and we should concede.
58
+ */
59
+ export function electSelf(path = lockPath()) {
60
+ for (let attempt = 0; attempt < 5; attempt++) {
61
+ // Build the payload BEFORE creating the file. Computing it afterwards left
62
+ // the lock existing but EMPTY for as long as the claim took to assemble —
63
+ // and on platforms where identity comes from `ps` that is tens of
64
+ // milliseconds. A racing daemon read the empty file, concluded the holder
65
+ // was dead, stole the lock, and both ended up bound.
66
+ const claim = JSON.stringify(selfClaim());
67
+ try {
68
+ const fd = openSync(path, 'wx', 0o600);
69
+ try {
70
+ writeSync(fd, claim);
71
+ }
72
+ finally {
73
+ closeSync(fd);
74
+ }
75
+ return true;
76
+ }
77
+ catch (err) {
78
+ if (err.code !== 'EEXIST')
79
+ throw err;
80
+ }
81
+ const held = readClaim(path);
82
+ if (claimIsLive(held))
83
+ return false; // a real daemon owns this state root
84
+ // An unreadable lock that was just touched is a claim mid-write, not a
85
+ // stale one. Give the writer a moment rather than racing it.
86
+ if (!held && recentlyTouched(path)) {
87
+ sleepMs(50);
88
+ if (claimIsLive(readClaim(path)))
89
+ return false;
90
+ }
91
+ // The holder is dead (or the file is corrupt). Break the lock by writing a
92
+ // fresh claim to a private path and renaming it over the lock — rename is
93
+ // atomic, so concurrent breakers cannot interleave a partial file. Several
94
+ // may rename; the last one wins, and everyone re-reads to find out who that
95
+ // was. Only the process that reads its own pid back proceeds.
96
+ const tmp = `${path}.${process.pid}`;
97
+ try {
98
+ const fd = openSync(tmp, 'w', 0o600);
99
+ try {
100
+ writeSync(fd, JSON.stringify(selfClaim()));
101
+ }
102
+ finally {
103
+ closeSync(fd);
104
+ }
105
+ renameSync(tmp, path);
106
+ }
107
+ catch {
108
+ try {
109
+ rmSync(tmp, { force: true });
110
+ }
111
+ catch {
112
+ /* best-effort */
113
+ }
114
+ continue;
115
+ }
116
+ const now = readClaim(path);
117
+ if (now?.pid === process.pid)
118
+ return true;
119
+ if (claimIsLive(now))
120
+ return false;
121
+ // Another breaker won the rename but has since died — go round again.
122
+ }
123
+ // Contention that never settled. Conceding is the safe answer: a missing
124
+ // daemon is a clear error the client reports, whereas two daemons corrupt
125
+ // shared state silently.
126
+ return false;
127
+ }
128
+ /** Release the lock, but only if it is still ours. */
129
+ export function releaseSelf(path = lockPath()) {
130
+ const held = readClaim(path);
131
+ if (held?.pid !== process.pid)
132
+ return;
133
+ try {
134
+ unlinkSync(path);
135
+ }
136
+ catch {
137
+ /* already gone */
138
+ }
139
+ }
140
+ //# sourceMappingURL=election.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"election.js","sourceRoot":"","sources":["../../src/daemon/election.ts"],"names":[],"mappings":"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"}