@vzn/vx-dte 0.0.652 → 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 +135 -62
- package/package.json +3 -3
- package/src/dispatch.ts +39 -4
- 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 +14 -3
- package/src/orchestrator.ts +265 -33
- package/src/planner.ts +183 -55
- package/src/plugin.ts +209 -30
- 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 +84 -42
- package/src/worker.ts +148 -15
package/src/local.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// loopback. For tests, demos
|
|
4
4
|
// and trying a distributed run before a CI starts the workers.
|
|
5
5
|
|
|
6
|
-
import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
7
7
|
import os from 'node:os'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { refuseUnknownOptions, type PluginOptionKinds } from '@vzn/vx'
|
|
@@ -24,7 +24,8 @@ export function local(options: LocalOptions = {}): DteAdapter {
|
|
|
24
24
|
const procs = new Set<Bun.Subprocess>()
|
|
25
25
|
/** A worktree add or a prepare: what close stops at once. */
|
|
26
26
|
const helpers = new Set<Bun.Subprocess>()
|
|
27
|
-
|
|
27
|
+
/** The repositories this run adds worktrees to, whose records close prunes. */
|
|
28
|
+
const roots = new Set<string>()
|
|
28
29
|
const pending = new Set<Promise<unknown>>()
|
|
29
30
|
|
|
30
31
|
// Async throughout: a run starts its workers while it shows its first rows.
|
|
@@ -83,7 +84,14 @@ export function local(options: LocalOptions = {}): DteAdapter {
|
|
|
83
84
|
|
|
84
85
|
async spawn(a) {
|
|
85
86
|
const work = (async () => {
|
|
86
|
-
base ??=
|
|
87
|
+
base ??= (async () => {
|
|
88
|
+
const d = await mkdtemp(path.join(os.tmpdir(), BASE_PREFIX))
|
|
89
|
+
// Whose they are, for a later run to reclaim once this vx is gone.
|
|
90
|
+
await writeFile(path.join(d, OWNER), String(process.pid))
|
|
91
|
+
void reclaim(a.workspaceRoot)
|
|
92
|
+
return d
|
|
93
|
+
})()
|
|
94
|
+
roots.add(a.workspaceRoot)
|
|
87
95
|
const dir = path.join(await base, String(a.index))
|
|
88
96
|
if (closed) return
|
|
89
97
|
// The run's own snapshot: one taken here, after a hit restored a
|
|
@@ -95,7 +103,6 @@ export function local(options: LocalOptions = {}): DteAdapter {
|
|
|
95
103
|
{ ...process.env, GIT_INDEX_FILE: undefined },
|
|
96
104
|
)
|
|
97
105
|
if (add.code !== 0) throw new Error(`git worktree add: ${errorLine(add.err)}`)
|
|
98
|
-
trees.push({ root: a.workspaceRoot, dir })
|
|
99
106
|
// The workspace's own place in the checkout: from the worktree's
|
|
100
107
|
// root, a workspace in a subdirectory of its repository had no
|
|
101
108
|
// `.vx/workspace` above, and every worker exited (2026-10-25).
|
|
@@ -137,38 +144,115 @@ export function local(options: LocalOptions = {}): DteAdapter {
|
|
|
137
144
|
relay?.stop()
|
|
138
145
|
relay = undefined
|
|
139
146
|
for (const p of helpers) group(p, 'SIGTERM')
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await Promise.all([...procs].map((p) => p.exited))
|
|
148
|
-
clearTimeout(grace)
|
|
149
|
-
// A detached shell removes the worktrees after vx exits: removing ones
|
|
150
|
-
// an install filled took seconds after a Ctrl-C (2026-10-21), and a
|
|
151
|
-
// 300k-file tree 2.7 s. `prune` then drops git's record of each.
|
|
152
|
-
const roots = [...new Set(trees.splice(0).map((t) => t.root))]
|
|
147
|
+
// A detached shell waits for what this run started, kills what is
|
|
148
|
+
// left after 5 s, then removes the worktrees, after vx exits:
|
|
149
|
+
// removing ones an install filled took seconds after a Ctrl-C
|
|
150
|
+
// (2026-10-21), and a 300k-file tree 2.7 s. Spawned first: core stops
|
|
151
|
+
// waiting on a teardown after 3 s, and a worker slow to leave kept
|
|
152
|
+
// close from spawning it before vx exited, so every worktree stayed
|
|
153
|
+
// (2026-11-08). `prune` then drops git's record of each.
|
|
153
154
|
if (base !== undefined) {
|
|
154
155
|
const dir = await base
|
|
155
156
|
base = undefined
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
{ stdio: ['ignore', 'ignore', 'ignore'], detached: true },
|
|
166
|
-
).unref()
|
|
157
|
+
const helping = [...helpers].map((p) => p.pid).join(' ')
|
|
158
|
+
const working = [...procs]
|
|
159
|
+
.filter((p) => !helpers.has(p))
|
|
160
|
+
.map((p) => p.pid)
|
|
161
|
+
.join(' ')
|
|
162
|
+
Bun.spawn(['sh', '-c', CLEANUP, 'sh', dir, helping, working, ...roots], {
|
|
163
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
164
|
+
detached: true,
|
|
165
|
+
}).unref()
|
|
167
166
|
}
|
|
167
|
+
roots.clear()
|
|
168
|
+
// Bounded under core's deadline: what still runs after it is the shell's.
|
|
169
|
+
await Promise.race([
|
|
170
|
+
Promise.allSettled([...pending, ...[...procs].map((p) => p.exited)]),
|
|
171
|
+
Bun.sleep(2_000),
|
|
172
|
+
])
|
|
168
173
|
},
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
/**
|
|
178
|
+
* `sh -c CLEANUP sh <base> <helper pids> <worker pids> <roots…>`: waits up
|
|
179
|
+
* to 5 s for the pids, kills each left (a helper's whole group), removes
|
|
180
|
+
* the base and prunes each root's worktree records.
|
|
181
|
+
*/
|
|
182
|
+
const CLEANUP = `d=$1 h=$2 w=$3; shift 3
|
|
183
|
+
i=0
|
|
184
|
+
while [ $i -lt 50 ]; do
|
|
185
|
+
up=; for p in $h $w; do kill -0 $p 2>/dev/null && up=1; done
|
|
186
|
+
[ -z "$up" ] && break
|
|
187
|
+
sleep 0.1; i=$((i+1))
|
|
188
|
+
done
|
|
189
|
+
for p in $h; do kill -9 -$p 2>/dev/null; done
|
|
190
|
+
for p in $w; do kill -9 $p 2>/dev/null; done
|
|
191
|
+
rm -rf "$d"; for r; do git -C "$r" worktree prune; done`
|
|
192
|
+
|
|
193
|
+
const BASE_PREFIX = 'vx-dte-'
|
|
194
|
+
const OWNER = 'owner'
|
|
195
|
+
/** `mkdtemp`'s name: the prefix and six characters. */
|
|
196
|
+
const OWN_BASE = new RegExp(`^${BASE_PREFIX}[A-Za-z0-9]{6}$`)
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The worktrees a run whose vx was killed (SIGKILL, a lost machine) left in
|
|
200
|
+
* `root`'s repository, removed: each under a base whose owner process is
|
|
201
|
+
* gone. A killed run's close never ran, and they stayed for good
|
|
202
|
+
* (2026-11-06). One `git worktree list`, off the run's path.
|
|
203
|
+
*/
|
|
204
|
+
async function reclaim(root: string): Promise<void> {
|
|
205
|
+
try {
|
|
206
|
+
const env: Record<string, string | undefined> = { ...process.env }
|
|
207
|
+
for (const name of GIT_REPO_ENV) delete env[name]
|
|
208
|
+
const list = Bun.spawn(['git', 'worktree', 'list', '--porcelain'], {
|
|
209
|
+
cwd: root,
|
|
210
|
+
env,
|
|
211
|
+
stdin: 'ignore',
|
|
212
|
+
stdout: 'pipe',
|
|
213
|
+
stderr: 'ignore',
|
|
214
|
+
})
|
|
215
|
+
const bases = new Set<string>()
|
|
216
|
+
for (const line of (await new Response(list.stdout).text()).split('\n')) {
|
|
217
|
+
if (!line.startsWith('worktree ')) continue
|
|
218
|
+
const dir = line.slice('worktree '.length)
|
|
219
|
+
const base = path.dirname(dir)
|
|
220
|
+
if (/^\d+$/.test(path.basename(dir)) && OWN_BASE.test(path.basename(base))) bases.add(base)
|
|
221
|
+
}
|
|
222
|
+
const gone: string[] = []
|
|
223
|
+
for (const base of bases) {
|
|
224
|
+
const pid = Number(
|
|
225
|
+
await readFile(path.join(base, OWNER), 'utf8').catch(
|
|
226
|
+
// quiet: a base with no owner is not known to be gone
|
|
227
|
+
() => '',
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
if (Number.isInteger(pid) && pid > 0 && !alive(pid)) gone.push(base)
|
|
231
|
+
}
|
|
232
|
+
if (gone.length === 0) return
|
|
233
|
+
Bun.spawn(
|
|
234
|
+
['sh', '-c', 'r=$1; shift; rm -rf "$@"; git -C "$r" worktree prune', 'sh', root, ...gone],
|
|
235
|
+
{
|
|
236
|
+
env,
|
|
237
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
238
|
+
detached: true,
|
|
239
|
+
},
|
|
240
|
+
).unref()
|
|
241
|
+
} catch {
|
|
242
|
+
// quiet: what is left is reclaimed by the next run
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function alive(pid: number): boolean {
|
|
247
|
+
try {
|
|
248
|
+
process.kill(pid, 0)
|
|
249
|
+
return true
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// Another user's process is alive too.
|
|
252
|
+
return (err as NodeJS.ErrnoException).code === 'EPERM'
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
172
256
|
/** Signal `p`'s process group: a helper leads its own (`detached`). */
|
|
173
257
|
function group(p: Bun.Subprocess, signal: NodeJS.Signals): void {
|
|
174
258
|
try {
|
package/src/messages.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface RunOrder {
|
|
|
54
54
|
cwd: string
|
|
55
55
|
project: string
|
|
56
56
|
env: Record<string, string>
|
|
57
|
+
/** `HOST_ENV` names its key folds as unset: the worker leaves them unset, not its own. */
|
|
58
|
+
unset?: string[]
|
|
57
59
|
/** Its `exec.env.secret`: the names in `env` whose values its saved output masks. */
|
|
58
60
|
secret?: string[]
|
|
59
61
|
timeoutMs?: number
|
|
@@ -65,9 +67,10 @@ export interface RunOrder {
|
|
|
65
67
|
* Its `exec.sandbox`, and for a cached task the projects its key answers
|
|
66
68
|
* for (workspace-relative): the worker builds the request on its own
|
|
67
69
|
* paths and OS (`sandboxRequestFor`), as its orchestrator may run another;
|
|
68
|
-
* `reads`, the dependencies' outputs it may read
|
|
70
|
+
* `reads`, the dependencies' outputs it may read, and `reported`, the
|
|
71
|
+
* heads a refused read under is reported (workspace-relative).
|
|
69
72
|
*/
|
|
70
|
-
sandbox?: { config: SandboxConfig; keyed?: string[]; reads?: string[] }
|
|
73
|
+
sandbox?: { config: SandboxConfig; keyed?: string[]; reads?: string[]; reported?: string[] }
|
|
71
74
|
/** Its persistent dependencies (`ServerSpec`), `cwd` workspace-relative: the worker starts each once. */
|
|
72
75
|
servers?: ServerOrder[]
|
|
73
76
|
/** The runtime probes its key includes, as the orchestrator answered them: the worker's must answer the same. */
|
|
@@ -119,6 +122,8 @@ export type ToWorker =
|
|
|
119
122
|
/** Part of an order's `deps`, ahead of the batch whose order was too long for one frame. */
|
|
120
123
|
| { type: 'deps'; task: string; deps: WiredDep[] }
|
|
121
124
|
| { type: 'cancel'; task: string }
|
|
125
|
+
/** No task left in the run uses persistent task `task`: it stops here. */
|
|
126
|
+
| { type: 'unserve'; task: string }
|
|
122
127
|
/**
|
|
123
128
|
* It was taken into the run, under the name the run's output gives it,
|
|
124
129
|
* and reads the cache as the run does: its refs, and the access the
|
|
@@ -152,8 +157,12 @@ export interface Load {
|
|
|
152
157
|
* `cpu` (one that sent 3, each as a task started, read 0.6 of 2.0 used).
|
|
153
158
|
*/
|
|
154
159
|
readonly avg?: number
|
|
160
|
+
/** Cores the worker's own process used on average since it started: what the run's bookkeeping costs it. */
|
|
161
|
+
readonly self?: number
|
|
155
162
|
/** When it was read, by the worker's clock: its task times are read against it. */
|
|
156
163
|
readonly at?: number
|
|
164
|
+
/** Each server it runs, its base as fitted (`server-fit.ts`): cores it uses with no dependant's work. */
|
|
165
|
+
readonly servers?: Record<string, number>
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
/** A task's times on its worker, epoch ms by the worker's clock: begun, command ended. */
|
|
@@ -219,7 +228,9 @@ export type WiredResult = Omit<ExecuteResult, 'violations' | 'outputs' | 'stdout
|
|
|
219
228
|
|
|
220
229
|
/**
|
|
221
230
|
* Names whose value is the worker's own: the orchestrator sends one only
|
|
222
|
-
* when a task's `env.define` sets it, a value
|
|
231
|
+
* when a task's `env.define` sets it or its `inputs.env` keys it, a value
|
|
232
|
+
* its key includes; a keyed one unset there is unset on the worker too
|
|
233
|
+
* (`RunOrder.unset`).
|
|
223
234
|
*/
|
|
224
235
|
export const HOST_ENV = [
|
|
225
236
|
'HOME',
|