@vzn/vx-dte 0.0.651 → 0.0.652

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/plugin.ts CHANGED
@@ -20,7 +20,7 @@ import type { Need } from './dispatch.js'
20
20
  import { booked, DEFAULT_STRATEGY, STRATEGIES, type Strategy } from './strategy.js'
21
21
  import type { Orchestrator } from './orchestrator.js'
22
22
  import type { Proof } from './seal.js'
23
- import type { Size } from './planner.js'
23
+ import type { Forecast, Size } from './planner.js'
24
24
 
25
25
  /** One kind of worker the adapter can start. */
26
26
  export interface WorkerSize {
@@ -246,6 +246,12 @@ export function dte(options: DteOptions = {}) {
246
246
  `dte() option "relay" must be an http(s) URL, got ${JSON.stringify(shown(options.relay))}`,
247
247
  )
248
248
  }
249
+ const unrouted = options.relay === undefined ? undefined : unroutable(options.relay)
250
+ if (unrouted !== undefined) {
251
+ throw new UserError(
252
+ `dte() option "relay" ${unrouted}, got ${JSON.stringify(shown(options.relay!))}`,
253
+ )
254
+ }
249
255
  // On GitHub Actions with none given, one for the relay's URL.
250
256
  const token = async (relay: string): Promise<string | undefined> =>
251
257
  (await options.token?.()) ??
@@ -388,6 +394,9 @@ export function dte(options: DteOptions = {}) {
388
394
  }
389
395
  if (!/^https?:\/\//.test(relay))
390
396
  throw new UserError(`vx dte worker: VX_DTE_RELAY must be http(s), got ${shown(relay)}`)
397
+ const unroutedEnv = unroutable(relay)
398
+ if (unroutedEnv !== undefined)
399
+ throw new UserError(`vx dte worker: VX_DTE_RELAY ${unroutedEnv}, got ${shown(relay)}`)
391
400
  const secret = givenSecret()
392
401
  if (secret === undefined && !onGitHub()) {
393
402
  throw new UserError(
@@ -625,6 +634,8 @@ export function dte(options: DteOptions = {}) {
625
634
  // The run's deadline (`Plan.boundMs`), and the work its plans knew.
626
635
  let deadline: number | undefined
627
636
  const known = new Set<string>()
637
+ // The last plan, which stands while what it saw holds (`decide`).
638
+ const last: { forecast?: Forecast } = {}
628
639
  // A plan that needs a worker later plans again then.
629
640
  let again: ReturnType<typeof setTimeout> | undefined
630
641
  // The fewest workers that end the run's known work within a margin of
@@ -667,6 +678,8 @@ export function dte(options: DteOptions = {}) {
667
678
  how,
668
679
  home,
669
680
  queued: o.queued(warmUp),
681
+ last,
682
+ now: performance.now(),
670
683
  })
671
684
  deadline = performance.now() + p.boundMs
672
685
  for (const t of p.tasks) known.add(t.id)
@@ -787,6 +800,8 @@ export function dte(options: DteOptions = {}) {
787
800
  }
788
801
  if (needsIn === undefined) scheduled = forecast
789
802
  else forecast()
803
+ // Core asks the footer and the summary after teardown: the last run's graph.
804
+ const depsOf = (id: string): readonly string[] => (nodes ?? ran)?.get(id)?.deps ?? []
790
805
  return {
791
806
  name: 'dte',
792
807
  remote: true,
@@ -818,9 +833,14 @@ export function dte(options: DteOptions = {}) {
818
833
  replan()
819
834
  return o.execute(req)
820
835
  },
821
- footer: () => o.footer(() => o.idealMs((id) => (nodes ?? ran)?.get(id)?.deps ?? [], sizes)),
836
+ footer: () =>
837
+ o.footer(
838
+ () => o.idealMs(depsOf, sizes),
839
+ () => o.advice(depsOf, sizes),
840
+ ),
822
841
  machines: () => o.machines(),
823
- idealMs: () => o.idealMs((id) => (nodes ?? ran)?.get(id)?.deps ?? [], sizes),
842
+ idealMs: () => o.idealMs(depsOf, sizes),
843
+ advice: () => o.advice(depsOf, sizes),
824
844
  profile: () => o.profile(),
825
845
  running: (id) => o.where(id),
826
846
  demand(remaining, misses) {
@@ -1022,6 +1042,16 @@ export function homeOf(sizes: readonly Size[], host: string): string | undefined
1022
1042
  return of.has(host) && of.size > 1 ? host : undefined
1023
1043
  }
1024
1044
 
1045
+ /**
1046
+ * Why a relay url no route can be appended to is refused: past a `?` or
1047
+ * `#` every socket asked the relay's root, and one naming /v1 asked /v1/v1/….
1048
+ */
1049
+ function unroutable(url: string): string | undefined {
1050
+ if (/[?#]/.test(url)) return 'takes no ?query or #fragment: each route is appended to it'
1051
+ if (/\/v1\/*$/.test(url)) return "is the relay's base, without /v1"
1052
+ return undefined
1053
+ }
1054
+
1025
1055
  /** A relay URL as a refusal may print it: a `user:pass@` would reach the CI log. */
1026
1056
  function shown(url: string): string {
1027
1057
  return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
package/src/wire.ts CHANGED
@@ -51,6 +51,11 @@ const LOST_BEATS = 3
51
51
  * orchestrator's ack answers it: one late frame is not a loss.
52
52
  */
53
53
  const QUIET_BEATS = LOST_BEATS / 2
54
+ /**
55
+ * How long the orchestrator gives its own reaction to a frame (a pump, a
56
+ * batch) to carry the frame's ack, before the ack goes alone.
57
+ */
58
+ const REACT_MS = 20
54
59
  /** A send unacknowledged as long goes again: its ack comes within it. */
55
60
  const RESEND_BEATS = LOST_BEATS / 2
56
61
  /** Why a worker ends on its orchestrator's silence. */
@@ -267,22 +272,45 @@ export async function join(
267
272
  /**
268
273
  * The orchestrator sends only orders and acks: an ack no order carried
269
274
  * goes on its own after half a heartbeat, every peer's in one frame. A
270
- * worker's ride its next send, which it makes within a heartbeat.
275
+ * worker's ride its next send, which it makes within a heartbeat. A peer
276
+ * the orchestrator said nothing to in a heartbeat (a quiet worker's
277
+ * report) is answered once the orchestrator reacted: an order sent in
278
+ * reply carries the ack, and with none it goes alone, `REACT_MS` on.
271
279
  */
272
280
  let acking: ReturnType<typeof setTimeout> | undefined
281
+ let answering: ReturnType<typeof setTimeout> | undefined
282
+ const soon = new Set<string>()
283
+ const ack = (peer: string): void => {
284
+ const upTo = taken.get(peer)?.upTo
285
+ owing.delete(peer)
286
+ said.set(peer, performance.now())
287
+ // quiet: a lost ack only costs a resend, which is dropped as seen
288
+ if (upTo !== undefined) put(peer, { acked: upTo } satisfies Acked).catch(() => undefined)
289
+ }
273
290
  const flushAcks = (): void => {
274
291
  acking = undefined
275
292
  if (left || ended !== undefined) return
276
- for (const peer of [...owing]) {
277
- const upTo = taken.get(peer)?.upTo
278
- owing.delete(peer)
279
- // quiet: a lost ack only costs a resend, which is dropped as seen
280
- if (upTo !== undefined) put(peer, { acked: upTo } satisfies Acked).catch(() => undefined)
281
- }
293
+ for (const peer of [...owing]) ack(peer)
294
+ }
295
+ const answer = (): void => {
296
+ answering = undefined
297
+ const peers = [...soon]
298
+ soon.clear()
299
+ if (left || ended !== undefined) return
300
+ for (const peer of peers) if (owing.has(peer)) ack(peer)
282
301
  }
283
302
  const owe = (peer: string): void => {
284
303
  owing.add(peer)
285
- if (role !== 'orchestrator' || acking !== undefined) return
304
+ if (role !== 'orchestrator') return
305
+ if (performance.now() - (said.get(peer) ?? 0) >= heartbeatMs) {
306
+ soon.add(peer)
307
+ if (answering === undefined) {
308
+ answering = setTimeout(answer, REACT_MS)
309
+ answering.unref()
310
+ }
311
+ return
312
+ }
313
+ if (acking !== undefined) return
286
314
  acking = setTimeout(flushAcks, heartbeatMs / 2)
287
315
  acking.unref()
288
316
  }
package/src/worker.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  HOST_ENV,
37
37
  PART_LISTS,
38
38
  type DepOrder,
39
+ type WiredDep,
39
40
  type Load,
40
41
  type WorkerInfo,
41
42
  type RunOrder,
@@ -114,7 +115,9 @@ export async function runWorker(opts: {
114
115
  const artifacts = opts.artifacts(() => facts)
115
116
  const running = new Map<string, AbortController>()
116
117
  /** Each order's deps sent ahead of its batch, until the batch comes. */
117
- const ahead = new Map<string, DepOrder[]>()
118
+ const ahead = new Map<string, WiredDep[]>()
119
+ /** Each dependency sent whole, by its task: an order after names it alone (`WiredDep`). */
120
+ const known = new Map<string, DepOrder>()
118
121
  const work = new Set<Promise<void>>()
119
122
  let root = opts.root
120
123
  let realRoot = opts.root
@@ -440,6 +443,8 @@ export async function runWorker(opts: {
440
443
  ? undefined
441
444
  : new Set(m.sandbox.keyed.map((k) => path.join(opts.root, k))),
442
445
  m.nested.map((n) => path.join(opts.root, n)),
446
+ undefined,
447
+ (m.sandbox.reads ?? []).map((r) => path.join(opts.root, r)),
443
448
  )
444
449
  const output = new BoundedCapture()
445
450
  const req: ExecuteRequest = {
@@ -621,11 +626,28 @@ export async function runWorker(opts: {
621
626
  else await floor.rearm(m.sandboxes)
622
627
  }
623
628
  for (const t of m.tasks) {
629
+ const sent = ahead.get(t.task)
630
+ ahead.delete(t.task)
631
+ // Learned in the order sent, a task run already too: the run counts them sent.
632
+ let missing: string | undefined
633
+ const deps = (sent === undefined ? t.deps : [...sent, ...t.deps]).map((d): DepOrder => {
634
+ if (typeof d !== 'string') {
635
+ known.set(d.task, d)
636
+ return d
637
+ }
638
+ const got = known.get(d)
639
+ if (got === undefined) missing ??= d
640
+ return got!
641
+ })
624
642
  // One it runs already is not run twice.
625
643
  if (running.has(t.task)) continue
626
- const deps = ahead.get(t.task)
627
- ahead.delete(t.task)
628
- start(deps === undefined ? t : { ...t, deps: [...deps, ...t.deps] })
644
+ if (missing !== undefined) {
645
+ const reason = `was sent ${missing} by name, never whole`
646
+ ui.print('err', ui.status('error', t.task, `could not run: ${reason}`))
647
+ await say({ type: 'fault', task: t.task, reason }).catch(unsent('fault'))
648
+ continue
649
+ }
650
+ start({ ...t, deps })
629
651
  }
630
652
  }
631
653
  }