@vzn/vx-dte 0.0.649 → 0.0.651
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 +42 -29
- package/package.json +3 -3
- package/src/github.ts +16 -5
- package/src/messages.ts +38 -27
- package/src/orchestrator.ts +138 -152
- package/src/plugin.ts +20 -12
- package/src/seal.ts +11 -4
- package/src/tree.ts +7 -1
- package/src/wire-host.ts +98 -0
- package/src/wire-thread.ts +168 -0
- package/src/wire.ts +87 -66
- package/src/worker.ts +86 -58
package/src/worker.ts
CHANGED
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
import { childrenCpuMs, describeCpu, readPs, topBetween } from './cpu-account.js'
|
|
34
34
|
import {
|
|
35
35
|
bytesOf,
|
|
36
|
-
HELD_EVERY,
|
|
37
36
|
HOST_ENV,
|
|
38
37
|
PART_LISTS,
|
|
39
38
|
type DepOrder,
|
|
@@ -47,7 +46,7 @@ import {
|
|
|
47
46
|
import { leftUnder, names, outputFiles } from './outputs.js'
|
|
48
47
|
import { snapshot } from './tree.js'
|
|
49
48
|
import { sealWorker, type Proof } from './seal.js'
|
|
50
|
-
import { join, RelayError, unsent as unsentBy } from './wire.js'
|
|
49
|
+
import { join, ORCHESTRATOR_SILENT, RelayError, unsent as unsentBy } from './wire.js'
|
|
51
50
|
|
|
52
51
|
export async function runWorker(opts: {
|
|
53
52
|
relay: string
|
|
@@ -152,7 +151,27 @@ export async function runWorker(opts: {
|
|
|
152
151
|
const onTerm = onSignal('SIGTERM')
|
|
153
152
|
process.once('SIGINT', onInt)
|
|
154
153
|
process.once('SIGTERM', onTerm)
|
|
154
|
+
/** Aborted as this worker leaves: what runs here then is stopped, not ended. */
|
|
155
|
+
const leaving = new AbortController()
|
|
155
156
|
const quitCode = (): number => 128 + constants.signals[quit.signal.reason as 'SIGINT' | 'SIGTERM']
|
|
157
|
+
/**
|
|
158
|
+
* The run is over for this worker (410), its job no failure. An end on
|
|
159
|
+
* the orchestrator's silence stops what runs here, and is said: three
|
|
160
|
+
* workers' every task stopped in one second, each job green and silent,
|
|
161
|
+
* and the cause was found only in the run's profile (CI, 2026-10-29).
|
|
162
|
+
*/
|
|
163
|
+
const ended = (err: RelayError): number => {
|
|
164
|
+
if (err.message === ORCHESTRATOR_SILENT)
|
|
165
|
+
ui.print(
|
|
166
|
+
'err',
|
|
167
|
+
ui.status(
|
|
168
|
+
'warn',
|
|
169
|
+
'vx dte worker',
|
|
170
|
+
'the orchestrator went silent: the tasks running here were stopped, and the run gives them to other workers',
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
return 0
|
|
174
|
+
}
|
|
156
175
|
|
|
157
176
|
/** Saves the task's files (none when `bare`) and output under `key` and says so. */
|
|
158
177
|
const reply = async (
|
|
@@ -162,9 +181,11 @@ export async function runWorker(opts: {
|
|
|
162
181
|
res: WiredResult & { output: CapturedChunk[] },
|
|
163
182
|
bare = false,
|
|
164
183
|
): Promise<void> => {
|
|
165
|
-
// Stopped by a signal
|
|
166
|
-
//
|
|
167
|
-
|
|
184
|
+
// Stopped by a signal, or by this worker leaving (its orchestrator went
|
|
185
|
+
// silent): its end would fail the task in the run, which runs it again
|
|
186
|
+
// elsewhere once this worker leaves. One such end reached the
|
|
187
|
+
// orchestrator and failed the run (2026-11-05).
|
|
188
|
+
if (quit.signal.aborted || leaving.signal.aborted) return
|
|
168
189
|
const { output, ...result } = res
|
|
169
190
|
// Forced: the key keeps an entry it holds, so the run's machines restore
|
|
170
191
|
// the fresh bytes under the run key.
|
|
@@ -205,26 +226,29 @@ export async function runWorker(opts: {
|
|
|
205
226
|
true,
|
|
206
227
|
)
|
|
207
228
|
}
|
|
208
|
-
// No remote took it
|
|
209
|
-
//
|
|
229
|
+
// No remote took it after its tries: the run's other machines read the
|
|
230
|
+
// remotes alone, so the task is this worker's fault, run elsewhere.
|
|
210
231
|
const saved = keys.at(-1)!
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
? { ...result, unshared: true, output: pieces(output) }
|
|
214
|
-
: result
|
|
232
|
+
if ((await artifacts.uploaded?.(saved)) === false)
|
|
233
|
+
throw new Error(`no remote cache took its entry ${saved} (this worker's log says why)`)
|
|
215
234
|
if (result.exitCode === 0)
|
|
216
235
|
for (const k of keys) {
|
|
217
236
|
present.set(k, Promise.resolve())
|
|
218
237
|
holds.set(k, names(m.project, m.outputs))
|
|
219
238
|
}
|
|
239
|
+
const endedAt = Date.now()
|
|
240
|
+
const t = times.get(m.task)
|
|
220
241
|
const end: ToOrchestrator = {
|
|
221
242
|
type: 'end',
|
|
222
243
|
task: m.task,
|
|
223
244
|
key: saved,
|
|
224
|
-
result
|
|
245
|
+
result,
|
|
246
|
+
startedAt: t?.startedAt ?? endedAt,
|
|
247
|
+
ranAt: t?.ranAt ?? endedAt,
|
|
248
|
+
endedAt,
|
|
225
249
|
load: frame(),
|
|
226
250
|
}
|
|
227
|
-
for (const part of parts(end)) await
|
|
251
|
+
for (const part of parts(end)) await say(part)
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
// A message the relay would not take is said, each kind once (2026-10-23):
|
|
@@ -240,15 +264,47 @@ export async function runWorker(opts: {
|
|
|
240
264
|
what,
|
|
241
265
|
'the orchestrator',
|
|
242
266
|
)
|
|
267
|
+
/** Each running task's times: begun, and its command's end once it ran. */
|
|
268
|
+
const times = new Map<string, { startedAt: number; ranAt?: number }>()
|
|
269
|
+
/** Tasks whose command ended, not said yet: each rides the next message. */
|
|
270
|
+
const ran = new Set<string>()
|
|
271
|
+
/** The last batch read: its report names what it runs as of that batch. */
|
|
272
|
+
let batch = 0
|
|
273
|
+
/**
|
|
274
|
+
* `m` to the orchestrator in one frame with its machine's load, which
|
|
275
|
+
* every frame carries, and each waiting `ran`: none is ever sent alone.
|
|
276
|
+
*/
|
|
277
|
+
const say = (event: ToOrchestrator): Promise<void> => {
|
|
278
|
+
const m = { ...event, load: { ...frame(), at: Date.now() } } as ToOrchestrator
|
|
279
|
+
// Its `end` says it ran.
|
|
280
|
+
if (m.type === 'end') ran.delete(m.task)
|
|
281
|
+
const riding: ToOrchestrator[] = []
|
|
282
|
+
for (const task of ran) {
|
|
283
|
+
const t = times.get(task)
|
|
284
|
+
if (t?.ranAt !== undefined)
|
|
285
|
+
riding.push({ type: 'ran', task, startedAt: t.startedAt, ranAt: t.ranAt })
|
|
286
|
+
}
|
|
287
|
+
ran.clear()
|
|
288
|
+
return me.send(
|
|
289
|
+
'orchestrator',
|
|
290
|
+
riding.length === 0 ? m : { type: 'together', messages: [...riding, m] },
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
// A heartbeat with nothing said: its live report, which is how the
|
|
294
|
+
// orchestrator hears it. Never a second in one: any message carries load.
|
|
295
|
+
me.quiet(() => {
|
|
296
|
+
void say({ type: 'report', running: [...running.keys()], batch, load: frame() }).catch(
|
|
297
|
+
unsent('report'),
|
|
298
|
+
)
|
|
299
|
+
})
|
|
243
300
|
/** Runs a task the orchestrator sent: it fits what this worker has free. */
|
|
244
301
|
const start = (m: RunOrder): void => {
|
|
245
|
-
|
|
302
|
+
const t: { startedAt: number; ranAt?: number } = { startedAt: Date.now() }
|
|
303
|
+
times.set(m.task, t)
|
|
246
304
|
const done = (): void => {
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.send('orchestrator', { type: 'ran', task: m.task, load: frame() })
|
|
251
|
-
.catch(unsent('ran'))
|
|
305
|
+
if (t.ranAt !== undefined) return
|
|
306
|
+
t.ranAt = Date.now()
|
|
307
|
+
ran.add(m.task)
|
|
252
308
|
}
|
|
253
309
|
const p = runTask(m, done)
|
|
254
310
|
.catch(async (err: unknown) => {
|
|
@@ -268,11 +324,13 @@ export async function runWorker(opts: {
|
|
|
268
324
|
}
|
|
269
325
|
const reason = err instanceof Error ? err.message : String(err)
|
|
270
326
|
ui.print('err', ui.status('error', m.task, `could not run: ${reason}`))
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
327
|
+
ran.delete(m.task)
|
|
328
|
+
await say({ type: 'fault', task: m.task, reason }).catch(unsent('fault'))
|
|
329
|
+
})
|
|
330
|
+
.finally(() => {
|
|
331
|
+
work.delete(p)
|
|
332
|
+
times.delete(m.task)
|
|
274
333
|
})
|
|
275
|
-
.finally(() => work.delete(p))
|
|
276
334
|
work.add(p)
|
|
277
335
|
}
|
|
278
336
|
|
|
@@ -302,10 +360,6 @@ export async function runWorker(opts: {
|
|
|
302
360
|
const runTask = async (m: RunOrder, free: () => void): Promise<void> => {
|
|
303
361
|
const stop = new AbortController()
|
|
304
362
|
running.set(m.task, stop)
|
|
305
|
-
// Not awaited: the task does not wait on the relay to say it began.
|
|
306
|
-
void me
|
|
307
|
-
.send('orchestrator', { type: 'start', task: m.task, load: frame() })
|
|
308
|
-
.catch(unsent('start'))
|
|
309
363
|
try {
|
|
310
364
|
const differs = await runtimeDiffers(m)
|
|
311
365
|
if (differs !== undefined) {
|
|
@@ -510,13 +564,6 @@ export async function runWorker(opts: {
|
|
|
510
564
|
}
|
|
511
565
|
}
|
|
512
566
|
|
|
513
|
-
// What it holds, every `HELD_EVERY` heartbeats: the orchestrator's lease on each task.
|
|
514
|
-
const held = setInterval(() => {
|
|
515
|
-
if (running.size === 0) return
|
|
516
|
-
void me
|
|
517
|
-
.send('orchestrator', { type: 'held', running: [...running.keys()], load: frame() })
|
|
518
|
-
.catch(unsent('held'))
|
|
519
|
-
}, HELD_EVERY * me.heartbeatMs)
|
|
520
567
|
try {
|
|
521
568
|
for (;;) {
|
|
522
569
|
let messages
|
|
@@ -524,7 +571,7 @@ export async function runWorker(opts: {
|
|
|
524
571
|
messages = await me.poll(quit.signal)
|
|
525
572
|
} catch (err) {
|
|
526
573
|
if (quit.signal.aborted) return quitCode()
|
|
527
|
-
if (err instanceof RelayError && err.status === 410) return
|
|
574
|
+
if (err instanceof RelayError && err.status === 410) return ended(err)
|
|
528
575
|
// The relay lost it (silent too long, or restarted): the run gave its
|
|
529
576
|
// tasks to others, so this job has nothing left and is no failure.
|
|
530
577
|
if (err instanceof RelayError && err.status === 404) {
|
|
@@ -568,13 +615,13 @@ export async function runWorker(opts: {
|
|
|
568
615
|
// Every batch of a run names the same orchestrator root.
|
|
569
616
|
root = m.root
|
|
570
617
|
realRoot = m.realRoot
|
|
618
|
+
batch = m.n
|
|
571
619
|
if (m.sandboxes !== undefined) {
|
|
572
620
|
if (floor === undefined) floor = localFloor(m.sandboxes)
|
|
573
621
|
else await floor.rearm(m.sandboxes)
|
|
574
622
|
}
|
|
575
623
|
for (const t of m.tasks) {
|
|
576
|
-
// One it runs already
|
|
577
|
-
// relay was away): it is not run twice.
|
|
624
|
+
// One it runs already is not run twice.
|
|
578
625
|
if (running.has(t.task)) continue
|
|
579
626
|
const deps = ahead.get(t.task)
|
|
580
627
|
ahead.delete(t.task)
|
|
@@ -585,13 +632,13 @@ export async function runWorker(opts: {
|
|
|
585
632
|
}
|
|
586
633
|
} catch (err) {
|
|
587
634
|
// The run closed under a poll or an answer: nothing is left for this worker.
|
|
588
|
-
if (err instanceof RelayError && err.status === 410) return
|
|
635
|
+
if (err instanceof RelayError && err.status === 410) return ended(err)
|
|
589
636
|
throw err
|
|
590
637
|
} finally {
|
|
591
638
|
process.off('SIGINT', onInt)
|
|
592
639
|
process.off('SIGTERM', onTerm)
|
|
593
|
-
clearInterval(held)
|
|
594
640
|
trees.stop()
|
|
641
|
+
leaving.abort()
|
|
595
642
|
for (const c of running.values()) c.abort()
|
|
596
643
|
await Promise.all(work)
|
|
597
644
|
const onlineMs = performance.now() - started.at
|
|
@@ -988,25 +1035,6 @@ const gb = (n: number): string => `${(n / 1e9).toFixed(1)} GB`
|
|
|
988
1035
|
/** The most of a result one send carries: a relay frame is 1 MiB, which sealing grows by a third. */
|
|
989
1036
|
const PART_BYTES = 256 * 1024
|
|
990
1037
|
|
|
991
|
-
/** `output` cut so each chunk fits a part, escaped as JSON grows it up to six times. */
|
|
992
|
-
function pieces(output: readonly CapturedChunk[]): CapturedChunk[] {
|
|
993
|
-
const most = PART_BYTES / 8
|
|
994
|
-
return output.flatMap((c) => {
|
|
995
|
-
if (c.text.length <= most) return [c]
|
|
996
|
-
const cut: CapturedChunk[] = []
|
|
997
|
-
for (let i = 0; i < c.text.length;) {
|
|
998
|
-
let end = Math.min(i + most, c.text.length)
|
|
999
|
-
// Not inside a surrogate pair: each half replayed alone printed as
|
|
1000
|
-
// two replacement characters (2026-10-25).
|
|
1001
|
-
const last = c.text.charCodeAt(end - 1)
|
|
1002
|
-
if (end < c.text.length && last >= 0xd800 && last <= 0xdbff) end--
|
|
1003
|
-
cut.push({ ...c, text: c.text.slice(i, end) })
|
|
1004
|
-
i = end
|
|
1005
|
-
}
|
|
1006
|
-
return cut
|
|
1007
|
-
})
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
1038
|
/**
|
|
1011
1039
|
* `end` as its sends: itself when it fits one frame, else its long lists
|
|
1012
1040
|
* first, in parts that each fit, and then the rest of it.
|