@vzn/vx 0.0.21 → 0.0.22
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 +21 -18
- package/package.json +6 -6
- package/src/bin.ts +52 -3
- package/src/cache/archive.ts +170 -21
- package/src/cache/cache.ts +667 -288
- package/src/cache/chained-cache.ts +14 -2
- package/src/cache/config-evals.ts +29 -6
- package/src/cache/file-hashes.ts +63 -35
- package/src/cache/git-inputs.ts +398 -69
- package/src/cache/index.ts +8 -1
- package/src/cache/inputs.ts +403 -102
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +81 -11
- package/src/cache/layered-cache.ts +199 -32
- package/src/cache/output-index.ts +109 -10
- package/src/cache/run-history.ts +8 -3
- package/src/cache/zstd.ts +53 -30
- package/src/cli/cache.ts +51 -21
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +35 -9
- package/src/cli/info.ts +49 -7
- package/src/cli/init.ts +17 -5
- package/src/cli/last.ts +96 -21
- package/src/cli/lock.ts +34 -17
- package/src/cli/plan-format.ts +4 -4
- package/src/cli/plugin-commands.ts +20 -5
- package/src/cli/run.ts +55 -12
- package/src/cli/select.ts +23 -13
- package/src/cli/show.ts +9 -4
- package/src/cli/upgrade.ts +134 -14
- package/src/cli/watch.ts +611 -102
- package/src/cli/why.ts +8 -16
- package/src/cli/workspace-config.ts +21 -9
- package/src/config.ts +46 -9
- package/src/exec/executor.ts +51 -5
- package/src/exec/index.ts +10 -0
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/runner.ts +327 -82
- package/src/exec/sandbox-binds.ts +34 -5
- package/src/exec/sandbox-paths.ts +22 -2
- package/src/exec/sandbox-runtime.ts +627 -102
- package/src/exec/sandbox-violations.ts +36 -23
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +12 -2
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +182 -134
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +16 -2
- package/src/orchestrator/admission.ts +41 -17
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +22 -7
- package/src/orchestrator/doctor.ts +122 -14
- package/src/orchestrator/events.ts +71 -2
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +331 -37
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +81 -14
- package/src/orchestrator/history.ts +6 -7
- package/src/orchestrator/hit-restore.ts +82 -18
- package/src/orchestrator/index.ts +7 -1
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +152 -32
- package/src/orchestrator/logger.ts +116 -20
- package/src/orchestrator/metrics.ts +156 -60
- package/src/orchestrator/miss-save.ts +106 -43
- package/src/orchestrator/options.ts +30 -4
- package/src/orchestrator/persistent.ts +41 -21
- package/src/orchestrator/placement.ts +55 -25
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +17 -23
- package/src/orchestrator/plugin.ts +31 -8
- package/src/orchestrator/prepare.ts +99 -13
- package/src/orchestrator/projects.ts +19 -8
- package/src/orchestrator/remote-prefetch.ts +21 -1
- package/src/orchestrator/run-artifacts.ts +9 -0
- package/src/orchestrator/run-context.ts +19 -4
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +6 -23
- package/src/orchestrator/run-report.ts +4 -2
- package/src/orchestrator/run.ts +264 -57
- package/src/orchestrator/sandbox-request.ts +388 -62
- package/src/orchestrator/save-lane.ts +4 -7
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +86 -30
- package/src/orchestrator/stable-keys.ts +202 -10
- package/src/orchestrator/status-line.ts +33 -1
- package/src/orchestrator/summary.ts +49 -2
- package/src/orchestrator/task-hash.ts +87 -7
- package/src/orchestrator/task-log-buffer.ts +7 -7
- package/src/orchestrator/telemetry-host.ts +16 -7
- package/src/orchestrator/telemetry.ts +69 -28
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +32 -3
- package/src/util/paths.ts +147 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +4 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +272 -64
- package/src/workspace/config-cache.ts +262 -32
- package/src/workspace/config-eval.ts +49 -12
- package/src/workspace/config-imports.ts +126 -5
- package/src/workspace/config-schema.ts +237 -41
- package/src/workspace/filter.ts +33 -18
- package/src/workspace/fingerprint.ts +29 -11
- package/src/workspace/index.ts +7 -0
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +33 -11
- package/src/workspace/migrate-scripts.ts +60 -7
- package/src/workspace/migration.ts +40 -1
- package/src/workspace/package-graph.ts +154 -16
- package/src/workspace/project-loader.ts +227 -77
- package/src/workspace/workspace.ts +173 -23
package/src/graph/scheduler.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
import { isUserError } from '../util/index.js'
|
|
1
|
+
import { fsRefusalHint, isFsRefusal, isUserError } from '../util/index.js'
|
|
2
|
+
import { computeReverseDepCount, mergePriorities, tieredReverseDepCount } from './priorities.js'
|
|
2
3
|
import type { TaskNode } from './task-graph.js'
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by `execute` for a restore-tier task that turned out to have
|
|
7
|
+
* nothing to restore (its artifact vanished after the up-front probe). It
|
|
8
|
+
* may be running ahead of its deps, so it cannot run its command in that
|
|
9
|
+
* slot: the scheduler releases the slot and dispatches it again as an
|
|
10
|
+
* exec-tier task once its deps are done. It records no outcome, and its
|
|
11
|
+
* dependents wait for the second dispatch.
|
|
12
|
+
*/
|
|
13
|
+
export class RestoreDemoted extends Error {
|
|
14
|
+
constructor(readonly taskId: string) {
|
|
15
|
+
super(`${taskId}: restore demoted to a run`)
|
|
16
|
+
this.name = 'RestoreDemoted'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
export type TaskStatus =
|
|
5
21
|
| 'success'
|
|
6
22
|
| 'cache-hit'
|
|
@@ -60,6 +76,26 @@ export interface TaskOutcome {
|
|
|
60
76
|
* outcomes only; never folded into any key.
|
|
61
77
|
*/
|
|
62
78
|
groupUpstream?: readonly TaskOutcome[]
|
|
79
|
+
/**
|
|
80
|
+
* On a `skipped` outcome: the task at the ROOT of what blocked it — the
|
|
81
|
+
* failed (or aborted) upstream, followed through any chain of skips
|
|
82
|
+
* between. Absent when the skip was fail-fast's (no upstream failed).
|
|
83
|
+
* The footer, `--summarize`, the telemetry record and the run row all
|
|
84
|
+
* read this one field (item 267).
|
|
85
|
+
*/
|
|
86
|
+
blockedBy?: string
|
|
87
|
+
/**
|
|
88
|
+
* On a `failed` outcome: vx's own `timeout` killed the final attempt.
|
|
89
|
+
* The exit is the shell's 143, and every label says "timed out" instead
|
|
90
|
+
* of reading it as a signal (item 268).
|
|
91
|
+
*/
|
|
92
|
+
timedOut?: true
|
|
93
|
+
/**
|
|
94
|
+
* On a failed persistent task: why it never became ready — the readiness
|
|
95
|
+
* deadline fired, the child exited first (`exitCode` is then its own), or
|
|
96
|
+
* the spawn itself failed. Every label reads this instead of the exit.
|
|
97
|
+
*/
|
|
98
|
+
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
63
99
|
/** Executor-reported placement label (`ExecuteResult.where`) — set only
|
|
64
100
|
* when the task ran somewhere other than this host. Telemetry-only. */
|
|
65
101
|
where?: string
|
|
@@ -163,7 +199,9 @@ export interface ScheduleOptions {
|
|
|
163
199
|
* it);
|
|
164
200
|
* - bypasses the `failedDep`→`skipped` check (its key is independent
|
|
165
201
|
* of dep success — pure-input transitive hashing — so a valid cached
|
|
166
|
-
* output is reported `cache-hit` regardless of a dep failing)
|
|
202
|
+
* output is reported `cache-hit` regardless of a dep failing);
|
|
203
|
+
* - leaves the tier when `execute` throws `RestoreDemoted`, and is
|
|
204
|
+
* dispatched again once its deps are done, with the dep check.
|
|
167
205
|
* It still runs through `execute()` (so the logger frame is unchanged);
|
|
168
206
|
* the orchestrator's execute reuses the up-front probe, so there is no
|
|
169
207
|
* second cache.get. When undefined/empty, behavior is byte-identical.
|
|
@@ -188,96 +226,6 @@ export interface ScheduleOptions {
|
|
|
188
226
|
admit?: (id: string, running: ReadonlySet<string>) => boolean
|
|
189
227
|
}
|
|
190
228
|
|
|
191
|
-
/**
|
|
192
|
-
* Compute, for each task in the graph, how many OTHER tasks are
|
|
193
|
-
* transitively blocked on it. Tasks with the highest count are the
|
|
194
|
-
* most valuable to schedule first — finishing them unlocks the most
|
|
195
|
-
* downstream work and minimizes worker idle time at the end of the
|
|
196
|
-
* run. Matches Nx's `calculateReverseDeps`-driven schedule sort
|
|
197
|
-
* (`packages/nx/src/tasks-runner/tasks-schedule.ts:166-207`).
|
|
198
|
-
*/
|
|
199
|
-
// Exported for the perf guard in tests/scheduler.test.ts, which times
|
|
200
|
-
// this function in isolation — end-to-end runGraph timing drowns the
|
|
201
|
-
// closure cost in task-promise overhead and flakes on slow CI.
|
|
202
|
-
export function computeReverseDepCount(nodes: Map<string, TaskNode>): Map<string, number> {
|
|
203
|
-
// Exact transitive-dependent COUNTS need closure SETS (diamonds
|
|
204
|
-
// double-count under naive summing). Set-of-strings closures are
|
|
205
|
-
// O(N²) entries and took 8.5s on a 1090-package, 100-layer repo;
|
|
206
|
-
// bitsets make the same closure O(E·N/32) time and N²/8 bits of
|
|
207
|
-
// memory (3270 tasks ≈ 1.3 MB) — single-digit ms at that scale.
|
|
208
|
-
const ids = [...nodes.keys()]
|
|
209
|
-
const index = new Map<string, number>()
|
|
210
|
-
for (let i = 0; i < ids.length; i++) index.set(ids[i]!, i)
|
|
211
|
-
const n = ids.length
|
|
212
|
-
const words = (n + 31) >>> 5
|
|
213
|
-
|
|
214
|
-
// Direct dependents as index lists + in-degree for the topo pass.
|
|
215
|
-
const directReverse: number[][] = Array.from({ length: n }, () => [])
|
|
216
|
-
const indegree = new Uint32Array(n)
|
|
217
|
-
for (const node of nodes.values()) {
|
|
218
|
-
const ni = index.get(node.id)!
|
|
219
|
-
for (const dep of node.deps) {
|
|
220
|
-
const di = index.get(dep)
|
|
221
|
-
if (di === undefined) continue
|
|
222
|
-
directReverse[di]!.push(ni)
|
|
223
|
-
indegree[ni]!++
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Kahn topo order over dependency edges (deps before dependents).
|
|
228
|
-
// Insertion order is topo today, but the closure's correctness
|
|
229
|
-
// must not hinge on an unstated property of buildTaskGraph.
|
|
230
|
-
const topo = new Int32Array(n)
|
|
231
|
-
let head = 0
|
|
232
|
-
let tail = 0
|
|
233
|
-
for (let i = 0; i < n; i++) if (indegree[i] === 0) topo[tail++] = i
|
|
234
|
-
while (head < tail) {
|
|
235
|
-
const v = topo[head++]!
|
|
236
|
-
for (const r of directReverse[v]!) {
|
|
237
|
-
if (--indegree[r]! === 0) topo[tail++] = r
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Reverse-topo sweep: every direct dependent's closure is final
|
|
242
|
-
// before its dependency folds it in. closure[i] = bitset over node
|
|
243
|
-
// indices of i's transitive dependents.
|
|
244
|
-
const closure = new Uint32Array(n * words)
|
|
245
|
-
const counts = new Map<string, number>()
|
|
246
|
-
for (let t = tail - 1; t >= 0; t--) {
|
|
247
|
-
const i = topo[t]!
|
|
248
|
-
const base = i * words
|
|
249
|
-
for (const r of directReverse[i]!) {
|
|
250
|
-
closure[base + (r >>> 5)]! |= 1 << (r & 31)
|
|
251
|
-
const rbase = r * words
|
|
252
|
-
for (let w = 0; w < words; w++) closure[base + w]! |= closure[rbase + w]!
|
|
253
|
-
}
|
|
254
|
-
let count = 0
|
|
255
|
-
for (let w = 0; w < words; w++) {
|
|
256
|
-
let v = closure[base + w]!
|
|
257
|
-
v = v - ((v >>> 1) & 0x55555555)
|
|
258
|
-
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
|
|
259
|
-
count += (((v + (v >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24
|
|
260
|
-
}
|
|
261
|
-
counts.set(ids[i]!, count)
|
|
262
|
-
}
|
|
263
|
-
// Cycle-stranded nodes (never topo-visited) can't occur — the graph
|
|
264
|
-
// builder rejects cycles — but a missing Map entry would silently
|
|
265
|
-
// sort as undefined, so default them defensively to 0.
|
|
266
|
-
for (const id of ids) if (!counts.has(id)) counts.set(id, 0)
|
|
267
|
-
return counts
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Run the task graph. Independent tasks run in parallel up to `concurrency`.
|
|
272
|
-
* If a task fails, its dependents are marked `skipped` but unrelated tasks
|
|
273
|
-
* keep running so the user gets maximum information per invocation.
|
|
274
|
-
*
|
|
275
|
-
* Scheduling: when more than one task is ready, the scheduler picks the
|
|
276
|
-
* one that blocks the most downstream work (most transitive reverse
|
|
277
|
-
* dependents). Ties break in graph-insertion order (which is the topo
|
|
278
|
-
* order produced by `buildTaskGraph`). Minimizes worker idle at the
|
|
279
|
-
* end of the run.
|
|
280
|
-
*/
|
|
281
229
|
/**
|
|
282
230
|
* A binary max-heap of ready task ids, ordered by (priority DESC, enqueue-seq
|
|
283
231
|
* ASC) so `pop()` returns the highest-priority task and equal-priority ties
|
|
@@ -310,8 +258,18 @@ class ReadyHeap {
|
|
|
310
258
|
}
|
|
311
259
|
/**
|
|
312
260
|
* `seq` defaults to a fresh monotonic counter. A parked-then-repushed
|
|
313
|
-
* task passes its ORIGINAL seq back in
|
|
314
|
-
*
|
|
261
|
+
* task passes its ORIGINAL seq back in, which makes FIFO-among-equals a
|
|
262
|
+
* property of the heap rather than of the tick loop's shape.
|
|
263
|
+
*
|
|
264
|
+
* It is NOT the difference between correct and incorrect order today,
|
|
265
|
+
* and the comment here used to claim it was ("a fresh seq would demote
|
|
266
|
+
* it behind later arrivals"). It would not: `parked` is a subsequence
|
|
267
|
+
* of the tick's pop order, so it is already in ranking order, and every
|
|
268
|
+
* push that follows takes a later seq — so fresh seqs handed out in
|
|
269
|
+
* parked order reproduce the same ranking. Measured over 800 randomized
|
|
270
|
+
* scenarios (random DAGs, costs, budgets, concurrency, failures, all
|
|
271
|
+
* three continue modes): zero order differences, item 493. Kept because
|
|
272
|
+
* it holds without that argument.
|
|
315
273
|
*/
|
|
316
274
|
push(id: string, seq: number = this.next++): void {
|
|
317
275
|
this.ids.push(id)
|
|
@@ -353,6 +311,17 @@ class ReadyHeap {
|
|
|
353
311
|
}
|
|
354
312
|
}
|
|
355
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Run the task graph. Independent tasks run in parallel up to `concurrency`.
|
|
316
|
+
* If a task fails, its dependents are marked `skipped` but unrelated tasks
|
|
317
|
+
* keep running so the user gets maximum information per invocation.
|
|
318
|
+
*
|
|
319
|
+
* Scheduling: when more than one task is ready, the scheduler picks the
|
|
320
|
+
* one that blocks the most downstream work (most transitive reverse
|
|
321
|
+
* dependents). Ties break in graph-insertion order (which is the topo
|
|
322
|
+
* order produced by `buildTaskGraph`). Minimizes worker idle at the
|
|
323
|
+
* end of the run.
|
|
324
|
+
*/
|
|
356
325
|
export async function runGraph(options: ScheduleOptions): Promise<Map<string, TaskOutcome>> {
|
|
357
326
|
const { nodes, concurrency, execute, onStart, onFinish } = options
|
|
358
327
|
const continueMode = options.continueMode ?? 'deps-ok'
|
|
@@ -381,7 +350,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
381
350
|
// priorities map (e.g. from history-aware critical-path estimation),
|
|
382
351
|
// use those weights. Falls back to the reverse-deps-count heuristic
|
|
383
352
|
// for nodes the caller didn't score, so partial coverage works.
|
|
384
|
-
const baseline =
|
|
353
|
+
const baseline =
|
|
354
|
+
options.restoreTier !== undefined && options.restoreTier.size > 0
|
|
355
|
+
? tieredReverseDepCount(nodes, options.restoreTier)
|
|
356
|
+
: computeReverseDepCount(nodes)
|
|
385
357
|
const priority: ReadonlyMap<string, number> = options.priorities
|
|
386
358
|
? mergePriorities(baseline, options.priorities)
|
|
387
359
|
: baseline
|
|
@@ -395,6 +367,10 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
395
367
|
// idle capacity (or run when an exec is blocked on a restorable dep and
|
|
396
368
|
// nothing else is runnable).
|
|
397
369
|
const restoreTier = options.restoreTier
|
|
370
|
+
// Restore-tier tasks whose execute threw `RestoreDemoted`: from then on
|
|
371
|
+
// they are exec-tier tasks, gated on their deps like any other.
|
|
372
|
+
const demoted = new Set<string>()
|
|
373
|
+
const inRestoreTier = (id: string): boolean => restoreTier?.has(id) === true && !demoted.has(id)
|
|
398
374
|
const execReady = new ReadyHeap(priority)
|
|
399
375
|
const restoreReady = new ReadyHeap(priority)
|
|
400
376
|
// A restore-tier task is dep-independent (a stable hit's restore needs
|
|
@@ -440,16 +416,14 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
440
416
|
const hasRoom = (id: string): boolean => {
|
|
441
417
|
const pool = poolOf?.(id)
|
|
442
418
|
if (pool === undefined) {
|
|
443
|
-
return
|
|
444
|
-
? activeRestore < restoreConcurrency
|
|
445
|
-
: active < concurrency
|
|
419
|
+
return inRestoreTier(id) ? activeRestore < restoreConcurrency : active < concurrency
|
|
446
420
|
}
|
|
447
421
|
return (poolActive.get(pool.name) ?? 0) < pool.capacity
|
|
448
422
|
}
|
|
449
423
|
const admit = (id: string): (() => void) => {
|
|
450
424
|
const pool = poolOf?.(id)
|
|
451
425
|
if (pool === undefined) {
|
|
452
|
-
if (
|
|
426
|
+
if (inRestoreTier(id)) {
|
|
453
427
|
activeRestore++
|
|
454
428
|
return () => {
|
|
455
429
|
activeRestore--
|
|
@@ -467,7 +441,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
467
441
|
}
|
|
468
442
|
// The policy sees exec-tier local tasks only; a restore is a tar
|
|
469
443
|
// extract and a pooled task runs on someone else's capacity.
|
|
470
|
-
const local = (id: string): boolean => !
|
|
444
|
+
const local = (id: string): boolean => !inRestoreTier(id) && poolOf?.(id) === undefined
|
|
471
445
|
const admits = (id: string): boolean => !local(id) || admitPolicy!(id, running)
|
|
472
446
|
// With no policy nothing reads `running`, so nothing is tracked: the
|
|
473
447
|
// count-only dispatch allocates no closure and touches no set per task.
|
|
@@ -496,14 +470,54 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
496
470
|
const m = err instanceof Error ? err.message : String(err)
|
|
497
471
|
process.stderr.write(`[vx] onFinish observer threw for ${id}: ${m}\n`)
|
|
498
472
|
}
|
|
473
|
+
// A restore-tier task runs before its deps, but its DEPENDENTS keep
|
|
474
|
+
// the order `dependsOn` states: they are released once its own deps
|
|
475
|
+
// have settled too. Released at its own finish, an `e2e` ran beside
|
|
476
|
+
// the `db:migrate` two edges up whenever the middle task was a hit,
|
|
477
|
+
// and ran green after that dep failed (item 963).
|
|
478
|
+
if (inRestoreTier(id) && (pending.get(id) ?? 0) > 0) {
|
|
479
|
+
heldRelease.add(id)
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
release(id)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// A restore-tier task whose deps did not all succeed is still a
|
|
486
|
+
// `cache-hit` (its key is theirs, not their outcome), but it passes the
|
|
487
|
+
// block down: its dependents are skipped as a failed dep's would be.
|
|
488
|
+
// Its root: a failed or aborted dep, or the root a skipped or blocked
|
|
489
|
+
// one carries.
|
|
490
|
+
const blockedRestores = new Map<string, string | undefined>()
|
|
491
|
+
const heldRelease = new Set<string>()
|
|
492
|
+
const release = (id: string): void => {
|
|
493
|
+
if (inRestoreTier(id) && continueMode !== 'always') {
|
|
494
|
+
for (const dep of (nodes.get(id) as TaskNode).deps) {
|
|
495
|
+
const u = outcomes.get(dep)
|
|
496
|
+
if (u?.status === 'failed' || u?.status === 'aborted') {
|
|
497
|
+
blockedRestores.set(id, dep)
|
|
498
|
+
break
|
|
499
|
+
}
|
|
500
|
+
if (u?.status === 'skipped') {
|
|
501
|
+
blockedRestores.set(id, u.blockedBy)
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
if (blockedRestores.has(dep)) {
|
|
505
|
+
blockedRestores.set(id, blockedRestores.get(dep))
|
|
506
|
+
break
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
499
510
|
const ds = dependents.get(id)
|
|
500
511
|
if (!ds) return
|
|
501
512
|
for (const d of ds) {
|
|
502
513
|
const rem = (pending.get(d) ?? 0) - 1
|
|
503
514
|
pending.set(d, rem)
|
|
515
|
+
if (rem !== 0) continue
|
|
504
516
|
// Restore-tier dependents were already enqueued at startup (they
|
|
505
|
-
// don't wait on deps);
|
|
506
|
-
|
|
517
|
+
// don't wait on deps); one that finished early releases its own
|
|
518
|
+
// dependents now. Only an exec-tier dependent is enqueued.
|
|
519
|
+
if (heldRelease.delete(d)) release(d)
|
|
520
|
+
else if (!inRestoreTier(d)) execReady.push(d)
|
|
507
521
|
}
|
|
508
522
|
}
|
|
509
523
|
|
|
@@ -522,7 +536,7 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
522
536
|
const aborted = (): boolean => options.signal?.aborted === true
|
|
523
537
|
const willSkip = (id: string): boolean => {
|
|
524
538
|
if (failFastTripped || aborted()) return true
|
|
525
|
-
if (
|
|
539
|
+
if (inRestoreTier(id)) return false
|
|
526
540
|
if (continueMode === 'always') return false
|
|
527
541
|
const node = nodes.get(id) as TaskNode
|
|
528
542
|
return node.deps.some((d) => {
|
|
@@ -533,7 +547,12 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
533
547
|
// from them — under the key a healthy run derives, since the fold
|
|
534
548
|
// takes the upstream's INPUT key — so the next run replays those
|
|
535
549
|
// bytes as a green hit.
|
|
536
|
-
return
|
|
550
|
+
return (
|
|
551
|
+
u?.status === 'failed' ||
|
|
552
|
+
u?.status === 'skipped' ||
|
|
553
|
+
u?.status === 'aborted' ||
|
|
554
|
+
blockedRestores.has(d)
|
|
555
|
+
)
|
|
537
556
|
})
|
|
538
557
|
}
|
|
539
558
|
|
|
@@ -594,11 +613,23 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
594
613
|
// because dependents are pushed when `pending` hits 0 regardless
|
|
595
614
|
// of outcome — keeps the propagation logic in one place.
|
|
596
615
|
if (willSkip(id)) {
|
|
616
|
+
// The root of the block: a failed or aborted upstream names
|
|
617
|
+
// itself; a skipped one hands down its own root.
|
|
618
|
+
const blocker = upstream.find(
|
|
619
|
+
(u) => u !== undefined && (u.status === 'failed' || u.status === 'aborted'),
|
|
620
|
+
)
|
|
621
|
+
const viaSkip = upstream.find((u) => u !== undefined && u.status === 'skipped')
|
|
622
|
+
const viaRestore = node.deps.find((d) => blockedRestores.has(d))
|
|
623
|
+
const blockedBy =
|
|
624
|
+
blocker?.node.id ??
|
|
625
|
+
viaSkip?.blockedBy ??
|
|
626
|
+
(viaRestore === undefined ? undefined : blockedRestores.get(viaRestore))
|
|
597
627
|
finishOne(id, {
|
|
598
628
|
node,
|
|
599
629
|
status: aborted() ? 'aborted' : 'skipped',
|
|
600
630
|
exitCode: 1,
|
|
601
631
|
durationMs: 0,
|
|
632
|
+
...(blockedBy !== undefined ? { blockedBy } : {}),
|
|
602
633
|
})
|
|
603
634
|
continue
|
|
604
635
|
}
|
|
@@ -614,18 +645,28 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
614
645
|
heldMs > 0 ? { ...o, admissionHeldMs: heldMs } : o
|
|
615
646
|
// Crash-isolated observer hook — a throwing onStart must not abort
|
|
616
647
|
// the dispatch loop (it would strand the tick with the slot held).
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
648
|
+
// A demoted task's second dispatch is the same task, already started.
|
|
649
|
+
if (!demoted.has(id)) {
|
|
650
|
+
try {
|
|
651
|
+
onStart?.(node)
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const m = err instanceof Error ? err.message : String(err)
|
|
654
|
+
process.stderr.write(`[vx] onStart observer threw for ${id}: ${m}\n`)
|
|
655
|
+
}
|
|
622
656
|
}
|
|
623
657
|
|
|
624
658
|
// `.then(onFulfilled, onRejected)` — NOT `.then(f).catch(g)`. The
|
|
625
659
|
// rejection arm handles ONLY `execute()` rejecting; a throw from
|
|
626
|
-
// the fulfillment arm
|
|
627
|
-
//
|
|
628
|
-
//
|
|
660
|
+
// the fulfillment arm must NOT also run the rejection arm, or
|
|
661
|
+
// `active` releases twice — and a double release wedges the count
|
|
662
|
+
// gate. NOT `onFinish`, which this comment used to cite: it is
|
|
663
|
+
// isolated inside `finishOne`, so it never reaches here (which is
|
|
664
|
+
// why the row named for it cannot tell the two shapes apart —
|
|
665
|
+
// item 560). What is left unisolated is the caller's
|
|
666
|
+
// `settledOf`, and under THIS shape a throw there strands the
|
|
667
|
+
// task's outcome and hangs the run rather than double-releasing.
|
|
668
|
+
// The two-arm form is still the correct choice; there is simply
|
|
669
|
+
// no reachable throw that makes the difference observable.
|
|
629
670
|
execute(node, upstream).then(
|
|
630
671
|
(raw) => {
|
|
631
672
|
const outcome = withHold(raw)
|
|
@@ -647,6 +688,14 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
647
688
|
void settled.then(done, done)
|
|
648
689
|
},
|
|
649
690
|
(err: unknown) => {
|
|
691
|
+
if (err instanceof RestoreDemoted) {
|
|
692
|
+
leave()
|
|
693
|
+
untrack()
|
|
694
|
+
demoted.add(id)
|
|
695
|
+
if (pending.get(id) === 0) execReady.push(id)
|
|
696
|
+
tick()
|
|
697
|
+
return
|
|
698
|
+
}
|
|
650
699
|
const message = err instanceof Error ? err.message : String(err)
|
|
651
700
|
const outcome: TaskOutcome = withHold({
|
|
652
701
|
node,
|
|
@@ -664,20 +713,37 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
664
713
|
// plainly, never as an "internal error".
|
|
665
714
|
if (isUserError(err)) {
|
|
666
715
|
process.stderr.write(`[vx] ${id}: ${message}\n`)
|
|
716
|
+
} else if (isFsRefusal(err)) {
|
|
717
|
+
process.stderr.write(`[vx] ${id}: ${message} — ${fsRefusalHint(err)}\n`)
|
|
667
718
|
} else {
|
|
668
719
|
const named = err instanceof Error && err.name !== 'Error' ? `${err.name}: ` : ''
|
|
669
|
-
|
|
720
|
+
// A wrapped error's cause is the fact the reader needs (a
|
|
721
|
+
// CorruptArtifactError over an ENOENT is a race, not a bad
|
|
722
|
+
// archive); print it, since the stack is not printed here.
|
|
723
|
+
const cause =
|
|
724
|
+
err instanceof Error && err.cause instanceof Error ? err.cause : undefined
|
|
725
|
+
const because =
|
|
726
|
+
cause === undefined
|
|
727
|
+
? ''
|
|
728
|
+
: ` (cause: ${(cause as NodeJS.ErrnoException).code ?? cause.name}: ${cause.message})`
|
|
729
|
+
process.stderr.write(`[vx] internal error in ${id}: ${named}${message}${because}\n`)
|
|
670
730
|
}
|
|
671
731
|
tick()
|
|
672
732
|
},
|
|
673
733
|
)
|
|
674
734
|
}
|
|
675
735
|
|
|
676
|
-
// Repush parked ids with their ORIGINAL seqs
|
|
677
|
-
//
|
|
736
|
+
// Repush parked ids with their ORIGINAL seqs, so the next tick's
|
|
737
|
+
// admission pass does not depend on `parked` having been built in
|
|
738
|
+
// ranking order (it is, but see `push`).
|
|
678
739
|
for (const [id, seq] of parked) execReady.push(id, seq)
|
|
679
740
|
|
|
680
|
-
|
|
741
|
+
// The outcome count alone: a slot is released before its outcome
|
|
742
|
+
// lands (`leave()` precedes `finishOne` in both arms, and a skip
|
|
743
|
+
// takes none), so a task still holding one has no outcome yet, and
|
|
744
|
+
// the lane counters could add nothing — deleting both from this
|
|
745
|
+
// condition survived the whole core suite (item 644).
|
|
746
|
+
if (outcomes.size === nodes.size) {
|
|
681
747
|
resolved = true
|
|
682
748
|
resolve(outcomes)
|
|
683
749
|
}
|
|
@@ -686,21 +752,3 @@ export async function runGraph(options: ScheduleOptions): Promise<Map<string, Ta
|
|
|
686
752
|
tick()
|
|
687
753
|
})
|
|
688
754
|
}
|
|
689
|
-
|
|
690
|
-
export function mergePriorities(
|
|
691
|
-
baseline: ReadonlyMap<string, number>,
|
|
692
|
-
overrides: ReadonlyMap<string, number>,
|
|
693
|
-
): ReadonlyMap<string, number> {
|
|
694
|
-
// The override caller scored "expected critical-path duration" in ms.
|
|
695
|
-
// Baseline reverse-deps counts are bounded by N; scale the override so
|
|
696
|
-
// it sorts above the baseline for any node it covers, and add the
|
|
697
|
-
// baseline as a tie-break for parity within the override set.
|
|
698
|
-
const SCALE = 1 << 20
|
|
699
|
-
const out = new Map<string, number>()
|
|
700
|
-
for (const [id, w] of baseline) out.set(id, w)
|
|
701
|
-
for (const [id, w] of overrides) {
|
|
702
|
-
const b = baseline.get(id) ?? 0
|
|
703
|
-
out.set(id, w * SCALE + b)
|
|
704
|
-
}
|
|
705
|
-
return out
|
|
706
|
-
}
|