@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/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: its end would fail the task in the run, which
166
- // runs it again elsewhere once this worker leaves.
167
- if (quit.signal.aborted) return
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: the orchestrator reads the remotes alone, so its
209
- // dependants come here and its output goes with the `end` (2026-10-25).
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
- const wired: WiredResult =
212
- (await artifacts.uploaded?.(saved)) === false
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: wired,
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 me.send('orchestrator', part)
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
- let ran = false
302
+ const t: { startedAt: number; ranAt?: number } = { startedAt: Date.now() }
303
+ times.set(m.task, t)
246
304
  const done = (): void => {
247
- if (ran) return
248
- ran = true
249
- void me
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
- await me
272
- .send('orchestrator', { type: 'fault', task: m.task, reason })
273
- .catch(unsent('fault'))
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 0
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, sent again (its lease ended while the
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 0
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.