@vzn/vx-dte 0.0.651 → 0.0.653
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 +164 -60
- package/package.json +3 -3
- package/src/dispatch.ts +540 -30
- package/src/forecast.ts +1 -2
- package/src/github.ts +0 -3
- package/src/index.ts +1 -0
- package/src/init.ts +9 -10
- package/src/local.ts +112 -28
- package/src/messages.ts +26 -5
- package/src/orchestrator.ts +678 -233
- package/src/planner.ts +1470 -192
- package/src/plugin.ts +242 -33
- package/src/seal.ts +159 -43
- package/src/server-fit.ts +80 -0
- package/src/tree.ts +215 -7
- package/src/usage.ts +65 -3
- package/src/warm-ups.ts +72 -6
- package/src/wire.ts +119 -49
- package/src/worker.ts +174 -19
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ Website: <https://vznjs.github.io/vx/> · source: <https://github.com/vznjs/vx>
|
|
|
5
5
|
Distributed task execution for vx: one `vx run` spread over workers your
|
|
6
6
|
own CI starts, replayed here as one run. The run you start is the
|
|
7
7
|
orchestrator. It runs no task itself, only restores cache hits: it starts
|
|
8
|
-
workers and hands the ready tasks to the workers with room, critical path first
|
|
8
|
+
workers and hands the ready tasks to the workers with room, critical path first,
|
|
9
|
+
ranked by history and ranked again when fetched timings land. A worker runs it in its own checkout and saves the task's
|
|
9
10
|
output and the files its declared outputs left to the workspace's cache,
|
|
10
11
|
under the task's key. They land here before the task ends, so the run
|
|
11
12
|
reports the task as its own. No file travels through the relay: it
|
|
@@ -64,12 +65,12 @@ the initiator.
|
|
|
64
65
|
anyone who reads the repository's Actions sees. A dispatch GitHub
|
|
65
66
|
answers without a run id (204) is refused, so its workers exit.
|
|
66
67
|
|
|
67
|
-
| Who | `info` / `body`
|
|
68
|
-
| ------------- |
|
|
69
|
-
| worker `join` | `info: { "noise": <message 1>, "token"? }`
|
|
70
|
-
| orchestrator | `{ "type": "hello", "noise": <message 2>, "token"? }`
|
|
71
|
-
| orchestrator | `{ "type": "refused", "reason" }`, plain, then `remove`
|
|
72
|
-
| either, then | `
|
|
68
|
+
| Who | `info` / `body` |
|
|
69
|
+
| ------------- | ---------------------------------------------------------- |
|
|
70
|
+
| worker `join` | `info: { "noise": <message 1>, "token"? }` |
|
|
71
|
+
| orchestrator | `{ "type": "hello", "noise": <message 2>, "token"? }` |
|
|
72
|
+
| orchestrator | `{ "type": "refused", "reason" }`, plain, then `remove` |
|
|
73
|
+
| either, then | `["<n>.<ciphertext>", …]`, or `["+", …]` with more to come |
|
|
73
74
|
|
|
74
75
|
The orchestrator checks each worker the moment the relay says it joined,
|
|
75
76
|
and `remove`s one whose handshake fails. Message 1's payload is the
|
|
@@ -78,17 +79,28 @@ body is Noise transport messages, base64, each at most 65535 bytes and
|
|
|
78
79
|
naming its nonce `n` (Noise §11.4, since a relay may reorder); each is
|
|
79
80
|
sealed with associated data `<first n>.<parts>` of its body, so no part
|
|
80
81
|
opens in another body, and a body's parts take consecutive nonces, so
|
|
81
|
-
none is repeated or reordered in it. A body
|
|
82
|
+
none is repeated or reordered in it. A body of more than 5 parts goes
|
|
83
|
+
over several sends of 5, each but the last led by `"+"`, so no frame
|
|
84
|
+
passes the relay's 1 MiB; the receiver holds them until the last and
|
|
85
|
+
opens the body whole. A body whose first nonce was seen before is a
|
|
82
86
|
replay and is dropped; one older than a body already opened (the relay
|
|
83
87
|
held it back: the wire's numbers are outside the seal) or that does not
|
|
84
|
-
open loses its sender. The
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
open loses its sender. The plaintext is a lead byte, the length of
|
|
89
|
+
every body its sender sealed before (4 bytes, big-endian, mod 2³²), and
|
|
90
|
+
then, `1`, its JSON, an array of the messages sent together (up to 64
|
|
91
|
+
KiB), or `0`, when smaller so, its zstd frame's length (4 bytes) and
|
|
92
|
+
the frame: compressed before sealing, since no
|
|
93
|
+
relay or socket compresses what is sealed. The orchestrator's are
|
|
94
|
+
compressed against the last 16 KiB of its bodies' JSON to that worker
|
|
95
|
+
as their dictionary, so a batch's env and paths said before cost a few
|
|
96
|
+
bytes; a worker's go without one, which would cost the orchestrator
|
|
97
|
+
some 100 µs to open each. A body whose length is not
|
|
98
|
+
the receiver's own count was sealed after one it never opened, which the
|
|
99
|
+
relay kept back, and loses its sender. It
|
|
88
100
|
is padded with trailing spaces to its size class, a power of two from
|
|
89
|
-
|
|
101
|
+
64 bytes, then whole transport messages, so the relay learns no size
|
|
90
102
|
within a class. Inside, each send is numbered and acknowledged
|
|
91
|
-
end to end, and a quiet worker reports (Switchboard § Delivery, § Liveness).
|
|
103
|
+
end to end (`{ "n", "b", "a"? }`: its number, body and ack), and a quiet worker reports (Switchboard § Delivery, § Liveness).
|
|
92
104
|
|
|
93
105
|
## Try it
|
|
94
106
|
|
|
@@ -104,7 +116,7 @@ import { dte, local } from '@vzn/vx-dte'
|
|
|
104
116
|
export default defineWorkspace({
|
|
105
117
|
plugins: [
|
|
106
118
|
dte({
|
|
107
|
-
workers: { small: { max: 4, cpu: 2, memory: '8GB' } },
|
|
119
|
+
workers: { linux: { machines: { small: { max: 4, cpu: 2, memory: '8GB' } } } },
|
|
108
120
|
adapter: local({ prepare: 'bun install' }),
|
|
109
121
|
}),
|
|
110
122
|
],
|
|
@@ -120,8 +132,9 @@ runs here as always (on GitHub Actions, `github()` with a relay names one
|
|
|
120
132
|
itself). It is a random UUID, so no one can guess a run to
|
|
121
133
|
open it first or join it; vx and the relay refuse any other. `local()` starts each worker as a process in its own
|
|
122
134
|
git worktree of the tracked files as they are, uncommitted changes
|
|
123
|
-
included
|
|
124
|
-
|
|
135
|
+
included, and a file mid-merge or flagged `assume-unchanged` or
|
|
136
|
+
`skip-worktree` as it is on disk (untracked files are not: a task that keys
|
|
137
|
+
one fails), at the
|
|
125
138
|
workspace's place in it (a workspace in a subdirectory of its repository too),
|
|
126
139
|
saving to the run's store (a relative `cache.dir` or `--cache-dir` too).
|
|
127
140
|
A run from a git hook works too: the worktree and its worker drop the
|
|
@@ -130,7 +143,10 @@ index being committed is left as it is. When a worker fails, its row names
|
|
|
130
143
|
the error line and the log that holds its whole stderr and all `prepare`
|
|
131
144
|
printed. At the run's end,
|
|
132
145
|
or on Ctrl-C, vx exits without waiting for the worktrees to be removed:
|
|
133
|
-
a detached shell
|
|
146
|
+
a detached shell waits up to 5 s for the workers to leave, kills any
|
|
147
|
+
still running, removes the worktrees and prunes git's records. A run whose vx
|
|
148
|
+
was killed (`kill -9`) removes none; the next `local()` run in the
|
|
149
|
+
repository removes them, once their run's process is gone.
|
|
134
150
|
|
|
135
151
|
`docker()` starts each worker as a container held to its size: `--cpus`
|
|
136
152
|
and `--memory` come from `cpu` and `memory`. The container clones this
|
|
@@ -148,7 +164,7 @@ names its error line and the log of its whole stderr and all `prepare` printed:
|
|
|
148
164
|
|
|
149
165
|
```ts
|
|
150
166
|
dte({
|
|
151
|
-
workers: { small: { max: 4, cpu: 2, memory: '4GB' } },
|
|
167
|
+
workers: { linux: { machines: { small: { max: 4, cpu: 2, memory: '4GB' } } } },
|
|
152
168
|
adapter: docker({ image: 'my/bun-git', prepare: 'bun install' }),
|
|
153
169
|
})
|
|
154
170
|
```
|
|
@@ -222,7 +238,7 @@ export default defineWorkspace({
|
|
|
222
238
|
plugins: [
|
|
223
239
|
dte({
|
|
224
240
|
relay: 'https://relay.example.com',
|
|
225
|
-
workers: { small: { max: 8, cpu: 4, memory: '16GB' } },
|
|
241
|
+
workers: { linux: { machines: { small: { max: 8, cpu: 4, memory: '16GB' } } } },
|
|
226
242
|
adapter: github(),
|
|
227
243
|
}),
|
|
228
244
|
],
|
|
@@ -285,7 +301,7 @@ workspace in a subdirectory of its repository gets them at the repository's
|
|
|
285
301
|
root, its jobs running in that directory. Each size runs on
|
|
286
302
|
its platform's GitHub-hosted runner (`ubuntu-latest` for none named,
|
|
287
303
|
`ubuntu-24.04-arm`, `macos-latest`, `macos-15-intel`), or on the label
|
|
288
|
-
`
|
|
304
|
+
a machine's `runsOn: 'ubuntu-latest-16-cores'` names, as
|
|
289
305
|
`gitlab({ tags })` does. It installs with the package manager your lockfile
|
|
290
306
|
names and runs vx as `bun vx`, which runs the repo's own and never
|
|
291
307
|
downloads one, as `pnpm exec` does (Yarn too: Bun reads no Plug'n'Play,
|
|
@@ -326,7 +342,7 @@ jobs:
|
|
|
326
342
|
id-token: write
|
|
327
343
|
# A worker of a run that is over cancels its own run.
|
|
328
344
|
actions: write
|
|
329
|
-
runs-on: ${{ fromJSON('{"small":"ubuntu-latest"}')[inputs.size] }}
|
|
345
|
+
runs-on: ${{ fromJSON('{"linux.small":"ubuntu-latest"}')[inputs.size] }}
|
|
330
346
|
timeout-minutes: 30
|
|
331
347
|
strategy:
|
|
332
348
|
fail-fast: false
|
|
@@ -381,7 +397,7 @@ export default defineWorkspace({
|
|
|
381
397
|
plugins: [
|
|
382
398
|
dte({
|
|
383
399
|
relay: 'https://relay.example.com',
|
|
384
|
-
workers: { small: { max: 4, cpu: 2, memory: '8GB' } },
|
|
400
|
+
workers: { linux: { machines: { small: { max: 4, cpu: 2, memory: '8GB' } } } },
|
|
385
401
|
adapter: gitlab(),
|
|
386
402
|
}),
|
|
387
403
|
],
|
|
@@ -437,12 +453,12 @@ vx:
|
|
|
437
453
|
- bun install --frozen-lockfile
|
|
438
454
|
script: [bun vx dte worker]
|
|
439
455
|
|
|
440
|
-
'vx-worker small':
|
|
456
|
+
'vx-worker linux.small':
|
|
441
457
|
extends: .vx-worker
|
|
442
458
|
parallel:
|
|
443
459
|
matrix:
|
|
444
460
|
- VX_DTE_SLOT: ['1', '2', '3', '4']
|
|
445
|
-
rules: [{ if: $VX_DTE_SIZE == "small" && $VX_DTE_SLOT =~ $VX_DTE_PICK }]
|
|
461
|
+
rules: [{ if: $VX_DTE_SIZE == "linux.small" && $VX_DTE_SLOT =~ $VX_DTE_PICK }]
|
|
446
462
|
```
|
|
447
463
|
|
|
448
464
|
`gitlab({ tags })` names each size's runner tags. Each job's ID token
|
|
@@ -485,6 +501,13 @@ use your own instead.
|
|
|
485
501
|
done. The run starts a worker of each
|
|
486
502
|
platform its work needs. A task none of whose platforms any size has
|
|
487
503
|
refuses the run before a worker starts.
|
|
504
|
+
- A task naming `exec.worker` runs only on machines of that worker type;
|
|
505
|
+
any other on the first type, in declared order, with a machine of one
|
|
506
|
+
of its platforms. So a type listed last (a costly Mac) gets only the
|
|
507
|
+
tasks no other type can take. Core refuses the run before a worker
|
|
508
|
+
starts when no type has the name, or none of its machines runs one of
|
|
509
|
+
the task's platforms. A worker that joins on its own is of the first
|
|
510
|
+
type with its platform.
|
|
488
511
|
- Tasks waiting for room go out longest tail first: a task's recorded
|
|
489
512
|
duration and its longest chain of dependants, so a long chain that got
|
|
490
513
|
ready late does not queue behind many short tasks ready at once.
|
|
@@ -501,9 +524,16 @@ use your own instead.
|
|
|
501
524
|
A sandbox the worker cannot host (no `bwrap` in its image) fails the
|
|
502
525
|
task there, as it fails here; the worker stays in the run.
|
|
503
526
|
- A persistent task a task depends on starts on the worker that runs the
|
|
504
|
-
task, once per worker, ready before it, and stops
|
|
527
|
+
task, once per worker, ready before it, and stops there once no task left
|
|
528
|
+
in the run uses it (the run's `unneeded`), or when the worker leaves;
|
|
505
529
|
the orchestrator never starts it, and a worker no dependant reaches
|
|
506
|
-
never does (2026-10-22).
|
|
530
|
+
never does (2026-10-22). Its CPU is learned on the worker each second
|
|
531
|
+
as `cores = base + each running dependant's cost`: the base from looks
|
|
532
|
+
with no dependant running, each cost from looks with it (a look on a
|
|
533
|
+
full machine only raises them). Each dependant's cost is added to its
|
|
534
|
+
CPU, so its history books it; the base is booked once on each worker
|
|
535
|
+
that starts the server, in placement and in the plan, and kept for the
|
|
536
|
+
next run under the commit, as warm-ups are.
|
|
507
537
|
- A task whose key hashes a runtime probe (`inputs.runtime`, such as
|
|
508
538
|
`bun --version`) runs on a worker, which answers the probe first and
|
|
509
539
|
fails the task when it answers otherwise than this machine did. A path
|
|
@@ -525,9 +555,13 @@ use your own instead.
|
|
|
525
555
|
or of files a formatter changed) is sent in parts, none of it dropped.
|
|
526
556
|
An order too long for one frame (a task with thousands of deps) sends
|
|
527
557
|
its deps ahead of it the same way.
|
|
558
|
+
- A worker gets each dependency in full once. Later orders name it, so a
|
|
559
|
+
deep graph's orders stay small.
|
|
528
560
|
- A task runs with the orchestrator's environment under the worker's
|
|
529
561
|
root. `HOME`, `TMPDIR`, `USER` and the like are the worker's own, unless
|
|
530
|
-
the task's `env.define` sets them
|
|
562
|
+
the task's `env.define` sets them or its `inputs.env` keys them: a keyed
|
|
563
|
+
one is the orchestrator's value, or unset where it is unset there, so
|
|
564
|
+
the worker runs on what the key saw; `npm_execpath` is the workspace's
|
|
531
565
|
package manager on the worker, as core sets it on a local task.
|
|
532
566
|
- A worker restores, by key, the declared outputs of every task the one
|
|
533
567
|
it runs depends on, each after its own dependencies (an additive task's
|
|
@@ -538,7 +572,9 @@ use your own instead.
|
|
|
538
572
|
leaves no file. One waits there up to 10 s; past that, with that worker
|
|
539
573
|
gone or of another platform, or with its uncached upstreams on two
|
|
540
574
|
workers, the upstream runs again where the dependant can go, and the
|
|
541
|
-
dependant follows it.
|
|
575
|
+
dependant follows it. One whose `exec.worker` or `exec.platforms` rules
|
|
576
|
+
out where its dependant runs refuses the run before a worker starts,
|
|
577
|
+
naming both: cache it, or run both alike. A task whose upload no remote took after three
|
|
542
578
|
tries (too big, a 429, a reset connection) is its worker's fault: that
|
|
543
579
|
worker takes no more tasks and the task runs on another; a second fault
|
|
544
580
|
fails the run. Before a task runs, its
|
|
@@ -560,20 +596,37 @@ use your own instead.
|
|
|
560
596
|
`--no-cache` or read-only run, a dependant of a failure under
|
|
561
597
|
`--continue=always`, one that left a process running that may still
|
|
562
598
|
write its outputs, one a stopped run cancelled), and one that left a tracked file under its project
|
|
563
|
-
changed on its worker (a formatter's input),
|
|
564
|
-
|
|
599
|
+
changed on its worker (a formatter's input), or that found an untracked,
|
|
600
|
+
unignored file under its project there (its outputs and its dependencies'
|
|
601
|
+
aside), which this machine never sees: core withholds its own save when
|
|
602
|
+
the key read that file or its inputs declare it, and says so.
|
|
565
603
|
- Under `--force`, a success is saved under its key (which keeps an entry
|
|
566
604
|
it already holds, as core does) and under the run's key, which this
|
|
567
605
|
machine and the dependants' workers restore: the bytes this run built,
|
|
568
606
|
never the entry it bypassed.
|
|
569
607
|
- A worker whose tracked files differ from the orchestrator's (another
|
|
570
608
|
tree, or the same commit with a tracked file changed, such as by a
|
|
571
|
-
prepare step) is refused
|
|
572
|
-
|
|
609
|
+
prepare step) is refused, and the orchestrator says so as it happens:
|
|
610
|
+
`[vx] dte: refused worker <id>: <reason>`. Both sides read their tracked files as they
|
|
611
|
+
are on disk, a file flagged `assume-unchanged` or `skip-worktree` too
|
|
612
|
+
(one a sparse checkout left off disk is its index's), so the same change
|
|
613
|
+
on both is the same tree.
|
|
614
|
+
- A worker runs the commit's bytes and modes, which are the ones the key
|
|
615
|
+
read: the run's snapshot stages each file's executable bit as on disk
|
|
616
|
+
(under `core.fileMode=false` too, as core keys it), and a worker writes
|
|
617
|
+
back as the commit holds it each file a git filter of its own checked
|
|
618
|
+
out otherwise (`text`, `eol`, `ident`, `filter` such as LFS,
|
|
619
|
+
`working-tree-encoding`, `core.autocrlf`). A task whose key hashes a
|
|
620
|
+
tracked file that is on disk other than the commit holds it (CRLF such
|
|
621
|
+
a filter wrote, an expanded ident, an LFS file's content) fails, naming
|
|
622
|
+
the file: no worker builds on those bytes.
|
|
573
623
|
- A task whose key hashes a file the run's commit lacks (untracked, or
|
|
574
624
|
gitignored and named outright, or inside a submodule, whose files the
|
|
575
625
|
commit holds only as a gitlink) fails, naming the file: no worker has it.
|
|
576
626
|
A dependency's declared output is not one: its worker restores it first.
|
|
627
|
+
A task whose project the commit lacks (its config untracked) fails,
|
|
628
|
+
naming the project, and a run whose `.vx/workspace.*` is untracked is
|
|
629
|
+
refused before a worker starts.
|
|
577
630
|
- A task's `exec.timeout` counts from its start on its worker: the wait
|
|
578
631
|
for a worker still booting is not its run (the executor declares
|
|
579
632
|
`timeouts`).
|
|
@@ -582,11 +635,22 @@ use your own instead.
|
|
|
582
635
|
another. A task two such workers were sent fails, naming both; one whose
|
|
583
636
|
send failed does not count. With no worker of the run left
|
|
584
637
|
to take it and none starting, it fails at once.
|
|
638
|
+
- A task that failed and retries (its `retries`, or the one retry a
|
|
639
|
+
known-flaky task gets) runs on another worker than the attempt that
|
|
640
|
+
failed, as core asks (`ExecuteRequest.attempt`): the worker, or what
|
|
641
|
+
ran beside it, may be the flake. With no other worker of its platform
|
|
642
|
+
in the run, it runs there again rather than wait.
|
|
585
643
|
- A relay that answers nothing for 10 s, every try counted, fails every task not done
|
|
586
644
|
(`the relay is unreachable`); none runs here, which may be far smaller
|
|
587
645
|
than the workers. What finished is saved, so a rerun resumes from it.
|
|
588
|
-
|
|
589
|
-
|
|
646
|
+
The footer counts each worker that had joined `lost`, with what it ran.
|
|
647
|
+
A fault (a `5xx`, a dropped or refused connection) is tried again 3
|
|
648
|
+
times, by the orchestrator and workers alike, and the third failed
|
|
649
|
+
retry ends them (`no socket in 3 dials`). The waits are random, from
|
|
650
|
+
up to 250 ms doubling to up to half a heartbeat, about 3 s at most in
|
|
651
|
+
all on the default, so a relay down or restarting is asked through
|
|
652
|
+
only when it is back within them: one down under a second usually is,
|
|
653
|
+
one down 2 s usually fails the run.
|
|
590
654
|
- The relay stores nothing, so a restart forgets the run. The orchestrator
|
|
591
655
|
joins again as its host and each worker joins again as itself, for two
|
|
592
656
|
heartbeats (4 s on the default); tasks keep running meanwhile, and what
|
|
@@ -594,9 +658,12 @@ use your own instead.
|
|
|
594
658
|
keeps what it runs, and one sent a task it runs already
|
|
595
659
|
does not run it again, so no work repeats. One not back by then
|
|
596
660
|
ends: a worker's tasks run elsewhere, an orchestrator's run fails.
|
|
597
|
-
Joining has the same 10 s in all
|
|
661
|
+
Joining has the same 10 s in all and the same 3 retries, its waits
|
|
662
|
+
spread over the 10 s (1.25 s doubling, jittered to half: 4.4 to 8.75 s
|
|
663
|
+
in all), so a relay or network down a few seconds is still joined. A
|
|
598
664
|
relay silent that long fails the run with `cannot connect to the relay
|
|
599
|
-
<origin>: no answer in 10 s
|
|
665
|
+
<origin>: no answer in 10 s`; four failed tries say why and how long,
|
|
666
|
+
as `cannot connect to the relay <origin>: ENOTFOUND, 4 dials in 9.1 s`.
|
|
600
667
|
- A worker that never joins says why on its row (`failed exited 1:
|
|
601
668
|
…`); its job ending, a success too, is what says so. Once the first
|
|
602
669
|
worker of a size joins, each other of that size not joined within twice
|
|
@@ -613,8 +680,8 @@ use your own instead.
|
|
|
613
680
|
- Only `leave` ends a participant at once (2026-10-25): the
|
|
614
681
|
orchestrator's at its run's end is the final call that closes the run,
|
|
615
682
|
and a worker's when it is done. Either whose socket drops without it
|
|
616
|
-
dials again, 3 times within 10 s, and the relay keeps it,
|
|
617
|
-
for
|
|
683
|
+
dials again, 3 times as above and within 10 s, and the relay keeps it,
|
|
684
|
+
and its run, for 10 s; past either the run is over. Either side hearing nothing
|
|
618
685
|
from the other for 3 heartbeats (6 s on the default) loses it, and the
|
|
619
686
|
rest follows from that: a worker that said nothing for half of it (3 s)
|
|
620
687
|
sends its report; the orchestrator never speaks on a timer, only orders
|
|
@@ -657,14 +724,16 @@ on its frame, in that worker's own color, by size and the order the run
|
|
|
657
724
|
started it:
|
|
658
725
|
|
|
659
726
|
```
|
|
660
|
-
⏺︎ 308ms success miss c#build · small#1
|
|
727
|
+
⏺︎ 308ms success miss c#build · linux.small#1
|
|
661
728
|
```
|
|
662
729
|
|
|
663
730
|
An idle worker the rest of the run cannot keep busy leaves (`left`): one
|
|
664
731
|
beyond the peak of the cores the remaining tasks keep busy, so a lull
|
|
665
732
|
between two wide parts keeps it. One a remaining task needs for its size
|
|
666
733
|
stays, and so does one a later task needs when its size's `max` is
|
|
667
|
-
spent, since none could start in its place.
|
|
734
|
+
spent, since none could start in its place. With that `max` spent, the
|
|
735
|
+
last worker of its platform also stays while any task of that platform
|
|
736
|
+
has not ended: a hit whose restore fails runs after all. A lost worker's tasks run again on
|
|
668
737
|
another, and the run starts one in its place. A worker is lost when its
|
|
669
738
|
socket closes, when it says nothing for 3 heartbeats (6 s), or at
|
|
670
739
|
once when its adapter sees its process or job end. Workers start at the first
|
|
@@ -677,7 +746,7 @@ workspace's cache under its commit, and the next commit's run plans by it
|
|
|
677
746
|
before any worker of that size joins.
|
|
678
747
|
|
|
679
748
|
The `workers` rows, live while the run goes and again in the footer,
|
|
680
|
-
give each worker its status (`starting`, `online`, `left`, `
|
|
749
|
+
give each worker its status (`starting`, `online`, `left`, `lost`, or `failed`
|
|
681
750
|
with why, such as `exited 1: Cannot find package '@vzn/vx-dte'`; once the
|
|
682
751
|
run is over, one still starting is `not joined`), the tasks it
|
|
683
752
|
has done, failed or had aborted by a stop, and the ones it runs now; while the run goes, those that left
|
|
@@ -695,28 +764,32 @@ declares and its machine reports, green when they agree and red when
|
|
|
695
764
|
not:
|
|
696
765
|
|
|
697
766
|
```
|
|
698
|
-
workers small#1 online 3 done · 2 running
|
|
699
|
-
small#2 starting
|
|
767
|
+
workers linux.small#1 online 3 done · 2 running
|
|
768
|
+
linux.small#2 starting
|
|
700
769
|
local online 1 done
|
|
701
770
|
```
|
|
702
771
|
|
|
703
772
|
```
|
|
704
|
-
workers small#1 stopped 5 done · warm-up 0.1s
|
|
773
|
+
workers linux.small#1 stopped 5 done · warm-up 0.1s
|
|
705
774
|
declared 2c/8G · actual 4c/16G
|
|
706
|
-
small#2 lost 1 done · warm-up 0.2s
|
|
775
|
+
linux.small#2 lost 1 done · warm-up 0.2s
|
|
707
776
|
declared 2c/8G · actual 4c/16G
|
|
708
777
|
local online 1 done
|
|
709
778
|
actual 10c/32G
|
|
710
779
|
```
|
|
711
780
|
|
|
712
781
|
A run over that started none (every task a hit) says
|
|
713
|
-
`workers none started: no task needed one
|
|
782
|
+
`workers none started: no task needed one`; one where a task waited
|
|
783
|
+
on a worker and none joined says
|
|
784
|
+
`workers none joined: a task waited on one`, or, when the run refused
|
|
785
|
+
workers, `workers none joined: 2 refused (<first reason>)`.
|
|
714
786
|
|
|
715
787
|
Under each worker, and `local`, the footer then spells out what it did:
|
|
716
788
|
its time online, busy and idle; its CPU peak and average in cores and
|
|
717
789
|
the cores the run booked for the tasks it was running, on average (a
|
|
718
|
-
booked from a task's start to its command's end); its cpu utilization
|
|
719
|
-
|
|
790
|
+
booked from a task's start to its command's end); its cpu utilization from its first task's send to its last
|
|
791
|
+
one's end, the warm-up and the wait for a first task left out: its machine's (any process's busy core-time over
|
|
792
|
+
the core-time it had, so what is left is idle cores), its tasks' and its worker process's; its memory peak and average, when its tasks reported any; and, for a worker, the messages and
|
|
720
793
|
bytes the run sent it and received from it. The `probe` line says when
|
|
721
794
|
core's cache probe answered, how many tasks it left to run (held in no
|
|
722
795
|
cache, or not answered for) and how many of them it found in none. The `asked` line says each
|
|
@@ -733,20 +806,24 @@ the worker started, any process's, as its last frame said. Where the
|
|
|
733
806
|
rest went, the worker says in its own log as it leaves: `cpu over 61.2s:
|
|
734
807
|
machine 2.80 cores · tasks 2.00 cores · this worker 0.15 cores · its other
|
|
735
808
|
children 0.05 cores · most besides: Runner.Worker 0.30 cores`, the last
|
|
736
|
-
from one `ps` as it starts and one as it leaves. The `total` line is the cpu utilization of every
|
|
809
|
+
from one `ps` as it starts and one as it leaves. The `total` line is the same cpu utilization of every
|
|
737
810
|
machine together, and the share of their history the run's tasks used:
|
|
738
811
|
|
|
739
812
|
```
|
|
740
|
-
small#1 stopped 5 done · warm-up 0.2s
|
|
813
|
+
linux.small#1 stopped 5 done · warm-up 0.2s
|
|
741
814
|
declared 4c/14G · actual 4c/14G
|
|
742
815
|
online 12.1s · busy 9.8s · idle 2.3s
|
|
743
|
-
cpu peak 3.6 · used avg 2.4 cores · machine avg 2.9 · booked avg 3.8
|
|
816
|
+
cpu peak 3.6 · used avg 2.4 cores · machine avg 2.9 · booked avg 3.8
|
|
817
|
+
cpu utilization machine 78% · tasks 64% · worker 2%
|
|
744
818
|
memory peak 1.2 GB · avg 640.0 MB
|
|
745
819
|
sent 6 messages · 3.1 MB, received 15 messages · 8.4 MB
|
|
746
820
|
probe at 0.4s: 5 tasks left to run, 5 known misses
|
|
747
821
|
asked 1 at 0.5s for 5 tasks (5 measured), plan 12.0s
|
|
748
822
|
ideal 10.8s: each task as long as it took, placed perfectly
|
|
749
|
-
|
|
823
|
+
best 2 × linux.small (4c/14G): 11.4s, the fewest of its sizes
|
|
824
|
+
more 3 × linux.small (4c/14G): 10.8s, past its sizes' max
|
|
825
|
+
any 1 × (8c/28G): 10.9s, of any cores, the least core time
|
|
826
|
+
total cpu utilization machine 74% · tasks 60% · worker 2%
|
|
750
827
|
```
|
|
751
828
|
|
|
752
829
|
`vx run --profile[=<path>]` adds the run in time to the profile it
|
|
@@ -776,6 +853,17 @@ real warm-up. That ideal end (`TaskExecutor.idealMs`) sits beside the
|
|
|
776
853
|
real time in the run summary and the footer's `ideal` line, and
|
|
777
854
|
`@vzn/vx-ci` prints it.
|
|
778
855
|
|
|
856
|
+
The same replay says which workers to declare next time
|
|
857
|
+
(`TaskExecutor.advice`), each from an empty run with the plan's own
|
|
858
|
+
rules (the fewest that end it within its margin of the soonest): `best`
|
|
859
|
+
is how many of each size; `more`, shown only when more than a size's
|
|
860
|
+
`max` would end the run sooner past that margin, is how many it would
|
|
861
|
+
take; `any` is one shape for each platform, of 2 to 64 cores (memory a
|
|
862
|
+
core as the platform's sizes have most), the one ending as soon for the
|
|
863
|
+
least core time. Raise a size's `max`, or change its `cpu` and
|
|
864
|
+
`memory`, by them. They are in the footer and the run summary, and
|
|
865
|
+
`@vzn/vx-ci` prints them under "Workers to declare".
|
|
866
|
+
|
|
779
867
|
## Environment
|
|
780
868
|
|
|
781
869
|
What `@vzn/vx-dte` reads from the CI job it runs in:
|
|
@@ -809,7 +897,12 @@ What `@vzn/vx-dte` reads from the CI job it runs in:
|
|
|
809
897
|
process.env.CF_ACCESS_CLIENT_ID, … }`); an unset value is left out, and
|
|
810
898
|
a name the dial sets itself (`authorization`, `content-type`,
|
|
811
899
|
`relay-proof`) is refused. The rest:
|
|
812
|
-
- `workers` names each `
|
|
900
|
+
- `workers` names each worker type, a `WorkerType` `{ machines }`, in order of
|
|
901
|
+
preference (`exec.worker` picks one). Each machine is a size the
|
|
902
|
+
adapter may start, named `<type>.<machine>` (`linux.small`) in the
|
|
903
|
+
run's output and an adapter's options, and is a `WorkerSize`,
|
|
904
|
+
`{ cpu, memory, max?, platform?, runsOn? }` (`runsOn`: the runner
|
|
905
|
+
label `vx dte init` writes for it): its cores
|
|
813
906
|
and memory (bytes, or `'8GB'`, `'512MiB'`), which the run counts on
|
|
814
907
|
when it starts one, until the worker reports what its machine has,
|
|
815
908
|
how many of it one run may start (absent, no cap), and its machines'
|
|
@@ -817,16 +910,28 @@ process.env.CF_ACCESS_CLIENT_ID, … }`); an unset value is left out, and
|
|
|
817
910
|
absent, planned as this machine's and not checked on join). With no adapter, the sizes' platforms are
|
|
818
911
|
the ones joining workers may have. Only work counts:
|
|
819
912
|
a task that missed the cache, or waits on a cached one that did, and
|
|
820
|
-
every uncached task, which runs every time; groups and hits need no worker.
|
|
913
|
+
every uncached task, which runs every time; groups and hits need no worker. The run plays
|
|
821
914
|
that work out on its workers and on N more (`src/planner.ts`), each new
|
|
822
915
|
one joining after its size's warm-up: this run's first of that size,
|
|
823
916
|
else an earlier run's, else any size's this run, else 20 s,
|
|
824
917
|
and takes the fewest N that end within 5% (5 s at least) of the best.
|
|
918
|
+
It plays only two warm-ups and the longest task ahead; past that, the
|
|
919
|
+
rest ends no sooner than its longest chain, nor than its work spread
|
|
920
|
+
on the cores it has, and the next plan, a warm-up on, sees further.
|
|
825
921
|
Sizes of different platforms come in turns, so of those N it drops
|
|
826
922
|
each the plan ends as soon without, its platform's work no more than
|
|
827
923
|
5 s later.
|
|
828
924
|
That margin is spent once: later plans keep its deadline until new
|
|
829
925
|
work is known. A task waiting on one that runs waits until it ends.
|
|
926
|
+
Then the run follows that plan: a start it wants later is asked when
|
|
927
|
+
due, and a worker idle with its planned work done goes, once every
|
|
928
|
+
task planned on it has started. It plays again
|
|
929
|
+
only when reality leaves the plan: new work, work that leaves without
|
|
930
|
+
running, a worker idle at its planned end with a task planned on it not
|
|
931
|
+
started, a worker that joins, leaves or warms up off plan, a task past
|
|
932
|
+
its time or ending sooner, one of no known time ending, a task starting
|
|
933
|
+
with its time, CPU or memory more than 5% off (1 s, 0.05 cores, 64 MiB
|
|
934
|
+
at least), or its window moving on.
|
|
830
935
|
What is left, running tasks too, takes as many times its history as
|
|
831
936
|
the run's finished tasks took theirs (once they had 20 s of it; never
|
|
832
937
|
less): a task past its forecast would otherwise be planned to end now,
|
|
@@ -876,7 +981,7 @@ HEAD^`), and every task after one (`src/forecast.ts`): a lockfile or
|
|
|
876
981
|
- `adapter` is the `DteAdapter` that starts workers, of the sizes in
|
|
877
982
|
`workers`: at least one with `max` above 0, or `dte()` is refused.
|
|
878
983
|
Absent, the run starts none and uses the workers that join `relay`.
|
|
879
|
-
- `relay` is the relay's URL, http(s)
|
|
984
|
+
- `relay` is the relay's base URL, http(s), without `/v1`, `?query` or `#fragment`; absent, the adapter's own.
|
|
880
985
|
- `token` answers the relay's bearer token (default `VX_DTE_TOKEN`). It
|
|
881
986
|
is asked again a minute before the token's `exp`, when it has one.
|
|
882
987
|
- `unmeasured`, `{ cpu, memory }` (default 2 cores, `'4GB'`), is what a
|
|
@@ -907,7 +1012,7 @@ HEAD^`), and every task after one (`src/forecast.ts`): a lockfile or
|
|
|
907
1012
|
- `DteAdapter` is `{ spawn(worker: WorkerSpawn), relay?(), close?(), cancel?(ids), init?(setup: DteSetup), runId?(), adopt?(worker), report?(warn), keyless?, workerRuns?() }`; `init` gets a `DteSetup` (workspace root, its directory in the repository, relay, sizes, the `PackageManager` read from the lockfile, the tasks asked for) and returns a `DteInit`: the CI files `vx dte init` writes and the steps it prints; `report` hands it the run's warning channel as the run starts, where `github()` says a status poll or a cancel the CI API refused, by status and body; `adopt` hands each worker the CI started before the run to `worker(id, size)`, which counts it and returns its spawn; `cancel` stops workers the run no longer needs that have not joined, and may answer with those it left running, which the run takes (with `late`, workers given up on joining, stopped whatever their job holds); `runId` names the run, in the adapter's own CI, when `VX_DTE_RUN` is unset; `workerId` names a worker in a job its CI started for several, when `VX_DTE_WORKER` is unset; `keyless` workers prove themselves by GitHub OIDC, and `workerRuns` names the workflow runs whose jobs alone may join as them (`undefined`: any job of the repository).
|
|
908
1013
|
`WorkerSpawn` is `{ run, relay, size, max, cpu, memory, index, id, workspaceRoot, commit, cacheDir?, env }`,
|
|
909
1014
|
with `memory` in bytes and `max` its size's (`Infinity` uncapped),
|
|
910
|
-
where `id` is the worker's name (`small#2`, numbered within its size), `commit` the
|
|
1015
|
+
where `id` is the worker's name (`linux.small#2`, numbered within its size), `commit` the
|
|
911
1016
|
tracked files as the run saw them at its start (uncommitted changes included), whose
|
|
912
1017
|
tree a worker must check out, `cacheDir` the run's cache dir (absolute) when the
|
|
913
1018
|
workspace names one, for workers on this machine to save where the run reads, and
|
|
@@ -916,10 +1021,9 @@ HEAD^`), and every task after one (`src/forecast.ts`): a lockfile or
|
|
|
916
1021
|
of its own on loopback. `DockerOptions` is `{ image, prepare? }`.
|
|
917
1022
|
- `github(options?: GithubOptions)` dispatches workers as matrix jobs of
|
|
918
1023
|
one workflow, one dispatch per size and demand (at most 256 workers,
|
|
919
|
-
a matrix's limit). `GithubOptions` is `{ workflow?,
|
|
1024
|
+
a matrix's limit). `GithubOptions` is `{ workflow?, env? }`:
|
|
920
1025
|
`workflow` defaults to `vx-worker.yml`, a file name in
|
|
921
|
-
`.github/workflows` other than `ci.yml`, the run's own; `
|
|
922
|
-
of each size, which `vx dte init` writes; `env` is variables every
|
|
1026
|
+
`.github/workflows` other than `ci.yml`, the run's own; `env` is variables every
|
|
923
1027
|
worker gets (one-line strings, no secret-named one: the worker job's
|
|
924
1028
|
log shows them), sent as the
|
|
925
1029
|
dispatch's `env` input only when set, which `vx dte init`'s workflow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-dte",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.653",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Distributed task execution for vx: one run spread over workers your own CI spawns, joined through a relay.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"bun": ">=1.4.4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@vzn/vx-dte-server": "0.0.
|
|
32
|
+
"@vzn/vx-dte-server": "0.0.653"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@vzn/vx": "^0.0.
|
|
35
|
+
"@vzn/vx": "^0.0.653"
|
|
36
36
|
},
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|