@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/README.md +33 -2
- package/package.json +3 -3
- package/src/dispatch.ts +501 -26
- package/src/forecast.ts +1 -2
- package/src/messages.ts +14 -4
- package/src/orchestrator.ts +415 -202
- package/src/planner.ts +1322 -172
- package/src/plugin.ts +33 -3
- package/src/wire.ts +36 -8
- package/src/worker.ts +26 -4
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: () =>
|
|
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(
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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'
|
|
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,
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
}
|