@vzn/vx 0.0.638 → 0.0.640
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 +1 -1
- package/docs/caching.md +152 -95
- package/docs/cli.md +225 -372
- package/docs/execution.md +22 -47
- package/docs/features.md +14 -16
- package/docs/patterns.md +6 -8
- package/docs/schema.md +268 -215
- package/docs/security.md +1 -1
- package/package.json +5 -5
- package/schemas/last.json +7 -4
- package/schemas/show.json +14 -14
- package/skills/vx/SKILL.md +10 -7
- package/src/cache/cache.ts +44 -11
- package/src/cache/file-hashes.ts +7 -12
- package/src/cache/fs-clock.ts +62 -0
- package/src/cache/git-inputs.ts +8 -2
- package/src/cache/index.ts +5 -2
- package/src/cache/inputs.ts +154 -83
- package/src/cache/key-fold.ts +23 -3
- package/src/cache/layer.ts +33 -18
- package/src/cache/layered-cache.ts +108 -315
- package/src/cache/linked-inputs.ts +87 -0
- package/src/cache/plugin-cache.ts +2 -10
- package/src/cache/remote-lane.ts +383 -0
- package/src/cache/run-history.ts +24 -8
- package/src/cache/run-store.ts +4 -4
- package/src/cache/schema.ts +6 -4
- package/src/cache/scoped-cache.ts +7 -0
- package/src/cli/completions.ts +2 -2
- package/src/cli/foreign-flags.ts +11 -23
- package/src/cli/help.ts +6 -31
- package/src/cli/info.ts +2 -1
- package/src/cli/init.ts +23 -11
- package/src/cli/last.ts +30 -4
- package/src/cli/plan-format.ts +5 -58
- package/src/cli/plugin-commands.ts +2 -2
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +23 -185
- package/src/cli/select.ts +16 -6
- package/src/cli/show.ts +15 -6
- package/src/cli/upgrade.ts +215 -55
- package/src/cli/watch-cycle.ts +2 -1
- package/src/cli/watch-filter.ts +22 -6
- package/src/cli/watch-judge.ts +49 -5
- package/src/cli/watch-set.ts +18 -8
- package/src/cli/watch.ts +103 -16
- package/src/cli/why.ts +9 -7
- package/src/config.ts +19 -21
- package/src/exec/executor.ts +5 -5
- package/src/exec/sandbox-runtime.ts +1 -1
- package/src/exec/sandbox-violations.ts +3 -1
- package/src/graph/dependency-spec.ts +2 -2
- package/src/graph/folds.ts +236 -0
- package/src/graph/index.ts +17 -2
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +45 -31
- package/src/graph/task-graph.ts +190 -58
- package/src/graph/task-status.ts +95 -0
- package/src/{orchestrator → history}/failure-mode.ts +1 -1
- package/src/{orchestrator → history}/history.ts +1 -1
- package/src/history/index.ts +48 -0
- package/src/{orchestrator → history}/metrics.ts +7 -20
- package/src/{orchestrator → history}/miss-reason.ts +1 -1
- package/src/index.ts +11 -29
- package/src/orchestrator/affected-tasks.ts +20 -19
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +16 -22
- package/src/orchestrator/excluded-keys.ts +4 -5
- package/src/orchestrator/execute-args.ts +80 -0
- package/src/orchestrator/execute-task.ts +50 -411
- package/src/orchestrator/fingerprint-watch.ts +2 -2
- package/src/orchestrator/index.ts +2 -73
- package/src/orchestrator/keyed-projects.ts +12 -51
- package/src/orchestrator/local-shortcircuit.ts +1 -1
- package/src/orchestrator/miss-save.ts +33 -3
- package/src/orchestrator/options.ts +1 -36
- package/src/orchestrator/output-host.ts +4 -3
- package/src/orchestrator/persistent-task.ts +321 -0
- package/src/orchestrator/persistent.ts +130 -3
- package/src/orchestrator/placement.ts +5 -5
- package/src/orchestrator/plan.ts +19 -6
- package/src/orchestrator/plugin-host.ts +129 -69
- package/src/orchestrator/plugin.ts +18 -8
- package/src/orchestrator/prepare.ts +40 -34
- package/src/orchestrator/projects.ts +7 -10
- package/src/orchestrator/remote-prefetch.ts +14 -1
- package/src/orchestrator/run-artifacts.ts +5 -177
- package/src/orchestrator/run-context.ts +55 -0
- package/src/orchestrator/run-failures.ts +15 -3
- package/src/orchestrator/run-records.ts +35 -3
- package/src/orchestrator/run-report.ts +151 -171
- package/src/orchestrator/run-settings.ts +1 -16
- package/src/orchestrator/run-steps.ts +57 -0
- package/src/orchestrator/run-stop.ts +153 -0
- package/src/orchestrator/run.ts +293 -604
- package/src/orchestrator/sandbox-fix.ts +1 -17
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +14 -6
- package/src/orchestrator/stable-keys.ts +164 -27
- package/src/orchestrator/steps.ts +70 -0
- package/src/orchestrator/task-hash.ts +34 -25
- package/src/orchestrator/task-log-buffer.ts +1 -1
- package/src/orchestrator/task-process.ts +94 -0
- package/src/orchestrator/telemetry-host.ts +4 -4
- package/src/orchestrator/telemetry.ts +14 -86
- package/src/orchestrator/upstream.ts +4 -181
- package/src/{orchestrator → report}/events.ts +41 -8
- package/src/{orchestrator → report}/forecast.ts +35 -2
- package/src/{orchestrator → report}/framed-output.ts +40 -34
- package/src/report/index.ts +36 -0
- package/src/{orchestrator → report}/logger.ts +158 -20
- package/src/{orchestrator → report}/status-line.ts +88 -7
- package/src/{orchestrator → report}/summary.ts +126 -35
- package/src/report/tally.ts +83 -0
- package/src/settings.ts +12 -39
- package/src/util/colors.ts +44 -4
- package/src/util/git-ignored.ts +143 -0
- package/src/util/index.ts +19 -4
- package/src/util/paths.ts +1 -1
- package/src/util/report.ts +13 -5
- package/src/util/settle.ts +14 -39
- package/src/util/tail.ts +1 -1
- package/src/util/ui.ts +17 -6
- package/src/workspace/affected.ts +81 -2
- package/src/workspace/config-imports.ts +1 -1
- package/src/workspace/config-schema.ts +142 -143
- package/src/workspace/lockfile.ts +1 -1
- package/src/workspace/migration.ts +19 -3
- package/schemas/summary.json +0 -133
- package/src/cache/chained-cache.ts +0 -280
- package/src/orchestrator/tally.ts +0 -112
- /package/src/{orchestrator → report}/path-links.ts +0 -0
- /package/src/{orchestrator → report}/plain-output.ts +0 -0
- /package/src/{orchestrator → report}/terminal-signals.ts +0 -0
|
@@ -24,13 +24,14 @@ import {
|
|
|
24
24
|
putRunEntry,
|
|
25
25
|
writesRunStore,
|
|
26
26
|
type PluginCache,
|
|
27
|
-
type
|
|
27
|
+
type ResolvedRemote,
|
|
28
28
|
ScopedCache,
|
|
29
29
|
applyGitEnumeration,
|
|
30
30
|
gitPathspecs,
|
|
31
31
|
MAX_SCOPED_PATHSPECS,
|
|
32
32
|
startGitEnumeration,
|
|
33
33
|
lazyGitEnumeration,
|
|
34
|
+
measureFsClockLag,
|
|
34
35
|
} from '../cache/index.js'
|
|
35
36
|
import {
|
|
36
37
|
buildPackageGraph,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
expandRequested,
|
|
51
52
|
type GraphRules,
|
|
52
53
|
isGroupTask,
|
|
54
|
+
refuseFiltersThroughGroups,
|
|
53
55
|
type TaskNode,
|
|
54
56
|
undeclaredDepsError,
|
|
55
57
|
unresolvedRequests,
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
hasHook,
|
|
62
64
|
resolveCache,
|
|
63
65
|
teardownPlugins,
|
|
66
|
+
type ScheduleRun,
|
|
64
67
|
} from './plugin-host.js'
|
|
65
68
|
import {
|
|
66
69
|
discoverProjects,
|
|
@@ -78,11 +81,11 @@ import { applyGraphStage } from './graph-stage.js'
|
|
|
78
81
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
79
82
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
80
83
|
import type { VxPlugin } from './plugin.js'
|
|
84
|
+
import { askIgnoredOutputs, gatedOutputs } from './stable-keys.js'
|
|
81
85
|
import { currentBranch } from './run-context.js'
|
|
82
86
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
83
|
-
import type
|
|
87
|
+
import { type Logger, warnOf } from '../report/index.js'
|
|
84
88
|
import type { RunOptions } from './options.js'
|
|
85
|
-
import { refuseFiltersThroughGroups } from './upstream.js'
|
|
86
89
|
import { runRules } from '../settings.js'
|
|
87
90
|
|
|
88
91
|
export interface PreparedRun {
|
|
@@ -113,9 +116,10 @@ export interface PreparedRun {
|
|
|
113
116
|
* merged over the structural baseline by the scheduler). Empty when no
|
|
114
117
|
* plugin declares the stage. Asked only by a run with a task to start:
|
|
115
118
|
* the stage reads history (148 ms at 8,002 tasks) to order work a
|
|
116
|
-
* nothing-changed run never dispatches. Runs the stage once
|
|
119
|
+
* nothing-changed run never dispatches. Runs the stage once: `run`
|
|
120
|
+
* times each plugin's call and hears its later `ctx.refine` sets.
|
|
117
121
|
*/
|
|
118
|
-
priorities: () => Promise<ReadonlyMap<string, number>>
|
|
122
|
+
priorities: (run?: ScheduleRun) => Promise<ReadonlyMap<string, number>>
|
|
119
123
|
/**
|
|
120
124
|
* The run's branch (`run.branch`, else git's, else the CI provider's):
|
|
121
125
|
* what its history records and the timings a forecast reads first.
|
|
@@ -241,9 +245,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
241
245
|
(reused !== undefined ? gitOfDiscovery(reused.projects) : undefined) ??
|
|
242
246
|
lazyGitEnumeration(workspaceRoot)
|
|
243
247
|
if (wholeTree) void git.start()
|
|
244
|
-
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (
|
|
245
|
-
log.status(m),
|
|
246
|
-
)
|
|
248
|
+
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, warnOf(log))
|
|
247
249
|
mark('workspace config')
|
|
248
250
|
let learnedBranch: string | null | undefined
|
|
249
251
|
const branch = (): string | null =>
|
|
@@ -257,20 +259,16 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
257
259
|
const cacheDir = options.cacheDir
|
|
258
260
|
? path.resolve(options.cwd, options.cacheDir)
|
|
259
261
|
: resolveCacheDir(workspaceRoot, workspaceConfig)
|
|
262
|
+
// Before any racy window is judged: a file system behind this process's
|
|
263
|
+
// clock dated a mid-command edit before the command (fs-clock.ts).
|
|
264
|
+
measureFsClockLag(cacheDir)
|
|
260
265
|
// Asked now, read when the cache opens: a repository with no remote is
|
|
261
266
|
// named by its root commit, a git spawn the discovery below overlaps.
|
|
262
267
|
const storeRoot = options.cacheDir ? null : resolveStoreRoot(workspaceRoot, workspaceConfig)
|
|
263
268
|
const projectMetas =
|
|
264
269
|
reused !== undefined
|
|
265
270
|
? reused.projects
|
|
266
|
-
: await discoverProjects(
|
|
267
|
-
workspaceRoot,
|
|
268
|
-
readLock,
|
|
269
|
-
plugins,
|
|
270
|
-
cacheDir,
|
|
271
|
-
(m) => log.status(m),
|
|
272
|
-
git,
|
|
273
|
-
)
|
|
271
|
+
: await discoverProjects(workspaceRoot, readLock, plugins, cacheDir, warnOf(log), git)
|
|
274
272
|
mark('discover projects')
|
|
275
273
|
|
|
276
274
|
// SCOPED config loading: configs are programs, and evaluating 1090
|
|
@@ -368,7 +366,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
368
366
|
projectMetas,
|
|
369
367
|
packageGraph,
|
|
370
368
|
configs: readLock,
|
|
371
|
-
warn: (
|
|
369
|
+
warn: warnOf(log),
|
|
372
370
|
}
|
|
373
371
|
let loaded: LoadedProjects
|
|
374
372
|
try {
|
|
@@ -464,21 +462,22 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
464
462
|
// winning prevents double-wrapping when the workspace also declares a
|
|
465
463
|
// cache plugin.
|
|
466
464
|
let cache: CacheLayer
|
|
467
|
-
const remotes:
|
|
465
|
+
const remotes: ResolvedRemote[] = []
|
|
468
466
|
if (options.remoteCache) {
|
|
469
|
-
|
|
470
|
-
|
|
467
|
+
remotes.push({
|
|
468
|
+
remote: options.remoteCache,
|
|
469
|
+
read: policy.remoteRead,
|
|
470
|
+
write: policy.remoteWrite,
|
|
471
|
+
onError: (err) => warnOf(log)(`[vx] remote cache: ${err.message}`),
|
|
472
|
+
})
|
|
471
473
|
}
|
|
472
474
|
try {
|
|
473
475
|
cache = options.remoteCache
|
|
474
|
-
? new LayeredCache(localCache,
|
|
475
|
-
policy,
|
|
476
|
-
onRemoteError: (err) => log.status(`[vx] remote cache: ${err.message}`),
|
|
477
|
-
})
|
|
476
|
+
? new LayeredCache(localCache, remotes)
|
|
478
477
|
: await resolveCache(plugins, {
|
|
479
478
|
workspaceRoot,
|
|
480
479
|
cacheDir,
|
|
481
|
-
warn: (
|
|
480
|
+
warn: warnOf(log),
|
|
482
481
|
scope: cacheScope,
|
|
483
482
|
localCache,
|
|
484
483
|
policy,
|
|
@@ -489,7 +488,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
489
488
|
// refused by name; the local handle opened above must not leak with it,
|
|
490
489
|
// nor what another plugin's factory opened: the factories have run
|
|
491
490
|
// (C-4, item 1029's contract).
|
|
492
|
-
await teardownPlugins(plugins, (
|
|
491
|
+
await teardownPlugins(plugins, warnOf(log))
|
|
493
492
|
localCache.close()
|
|
494
493
|
throw err
|
|
495
494
|
}
|
|
@@ -581,7 +580,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
581
580
|
workspaceRoot,
|
|
582
581
|
cacheDir,
|
|
583
582
|
projectMetas,
|
|
584
|
-
warn: (
|
|
583
|
+
warn: warnOf(log),
|
|
585
584
|
rules: graphRules,
|
|
586
585
|
})
|
|
587
586
|
// `--affected` selects over the FINAL graph: an edge or an input a
|
|
@@ -675,14 +674,19 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
675
674
|
let usesWorkspaceInputs = false
|
|
676
675
|
for (const n of nodes.values()) {
|
|
677
676
|
graphDirs.add(n.projectDir)
|
|
678
|
-
if ((n.config.
|
|
677
|
+
if ((n.config.inputs?.workspaceFiles?.length ?? 0) > 0) usesWorkspaceInputs = true
|
|
679
678
|
}
|
|
680
679
|
const projectDirs = [...graphDirs]
|
|
681
|
-
const
|
|
680
|
+
const enumerating =
|
|
681
|
+
git.started ??
|
|
682
682
|
startGitEnumeration(
|
|
683
683
|
workspaceRoot,
|
|
684
684
|
gitPathspecs(workspaceRoot, projectDirs, usesWorkspaceInputs),
|
|
685
|
-
)
|
|
685
|
+
)
|
|
686
|
+
// While git walks: the gate's pass over every task cost 2-8 ms at
|
|
687
|
+
// 1,000 projects when it ran before the spawn.
|
|
688
|
+
const settleIgnored = askIgnoredOutputs(gatedOutputs(nodes, workspaceRoot), workspaceRoot)
|
|
689
|
+
const enumeration = await enumerating
|
|
686
690
|
await applyGitEnumeration(
|
|
687
691
|
enumeration,
|
|
688
692
|
workspaceRoot,
|
|
@@ -691,6 +695,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
691
695
|
usesWorkspaceInputs,
|
|
692
696
|
localCache,
|
|
693
697
|
)
|
|
698
|
+
await settleIgnored(gitFilesCache)
|
|
694
699
|
mark('git enumeration')
|
|
695
700
|
if (hasGraphHook && !hooksFirst) await graphStage()
|
|
696
701
|
// Args after `--` go to requested commands only; with none among them
|
|
@@ -706,7 +711,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
706
711
|
}
|
|
707
712
|
}
|
|
708
713
|
if (hasHook(plugins, 'key')) {
|
|
709
|
-
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (
|
|
714
|
+
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: warnOf(log) })
|
|
710
715
|
}
|
|
711
716
|
refuseFiltersThroughGroups(nodes)
|
|
712
717
|
const exclude = options.excludeDependencies
|
|
@@ -744,7 +749,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
744
749
|
return own
|
|
745
750
|
}
|
|
746
751
|
let scheduled: Promise<ReadonlyMap<string, number>> | undefined
|
|
747
|
-
const priorities = (): Promise<ReadonlyMap<string, number>> =>
|
|
752
|
+
const priorities = (run?: ScheduleRun): Promise<ReadonlyMap<string, number>> =>
|
|
748
753
|
(scheduled ??= hasHook(plugins, 'schedule')
|
|
749
754
|
? applyScheduleHooks(
|
|
750
755
|
plugins,
|
|
@@ -752,11 +757,12 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
752
757
|
{
|
|
753
758
|
workspaceRoot,
|
|
754
759
|
cacheDir,
|
|
755
|
-
warn: (
|
|
760
|
+
warn: warnOf(log),
|
|
756
761
|
localCache,
|
|
757
762
|
branch: branch(),
|
|
758
763
|
},
|
|
759
764
|
cacheFor,
|
|
765
|
+
run,
|
|
760
766
|
)
|
|
761
767
|
: Promise.resolve(new Map()))
|
|
762
768
|
|
|
@@ -790,7 +796,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
790
796
|
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
791
797
|
}
|
|
792
798
|
} catch (err) {
|
|
793
|
-
await teardownPlugins(plugins, (
|
|
799
|
+
await teardownPlugins(plugins, warnOf(log))
|
|
794
800
|
cache.close()
|
|
795
801
|
throw err
|
|
796
802
|
}
|
|
@@ -258,7 +258,7 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
258
258
|
// Turbo spells the root package `//` (`dependsOn: ['//#lint']`), as vx
|
|
259
259
|
// names the root project unless something names it; a named root's
|
|
260
260
|
// spelling is rewritten to its name once, here, for the closure, the
|
|
261
|
-
// graph and `
|
|
261
|
+
// graph and `inputs.tasks` alike. With no root project it stays,
|
|
262
262
|
// and the graph's refusal says so.
|
|
263
263
|
const root = path.resolve(workspaceRoot)
|
|
264
264
|
const rootName = args.projectMetas.find((m) => path.resolve(m.dir) === root)?.name
|
|
@@ -357,10 +357,7 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
357
357
|
*/
|
|
358
358
|
const DEFAULT_BUILD: TaskConfig = Object.freeze({
|
|
359
359
|
dependsOn: Object.freeze(['^build']),
|
|
360
|
-
|
|
361
|
-
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
362
|
-
outputs: Object.freeze({ files: Object.freeze([]) }),
|
|
363
|
-
}),
|
|
360
|
+
inputs: Object.freeze({ files: Object.freeze(['**']) }),
|
|
364
361
|
})
|
|
365
362
|
|
|
366
363
|
/** The default `build`, which a menu of the tasks a user declared leaves out. */
|
|
@@ -374,7 +371,7 @@ function withDefaultBuild(config: ProjectConfig): ProjectConfig {
|
|
|
374
371
|
}
|
|
375
372
|
|
|
376
373
|
/**
|
|
377
|
-
* `config` with Turbo's `//#task` (in `dependsOn` and `
|
|
374
|
+
* `config` with Turbo's `//#task` (in `dependsOn` and `inputs.tasks`)
|
|
378
375
|
* spelled `<root>#task`; the same object when it holds none. A copy, not
|
|
379
376
|
* an edit: a loaded config may be a cached or a frozen one.
|
|
380
377
|
*/
|
|
@@ -386,14 +383,14 @@ function rootSpelled(config: ProjectConfig, rootName: string): ProjectConfig {
|
|
|
386
383
|
let tasks: NonNullable<ProjectConfig['tasks']> | undefined
|
|
387
384
|
for (const [name, task] of Object.entries(config.tasks ?? {})) {
|
|
388
385
|
const dependsOn = fix(task.dependsOn)
|
|
389
|
-
const inputTasks = fix(task.
|
|
390
|
-
if (dependsOn === task.dependsOn && inputTasks === task.
|
|
386
|
+
const inputTasks = fix(task.inputs?.tasks)
|
|
387
|
+
if (dependsOn === task.dependsOn && inputTasks === task.inputs?.tasks) continue
|
|
391
388
|
tasks ??= { ...config.tasks }
|
|
392
389
|
tasks[name] = {
|
|
393
390
|
...task,
|
|
394
391
|
...(dependsOn !== undefined ? { dependsOn: [...dependsOn] } : {}),
|
|
395
|
-
...(task.
|
|
396
|
-
? {
|
|
392
|
+
...(task.inputs !== undefined && inputTasks !== undefined
|
|
393
|
+
? { inputs: { ...task.inputs, tasks: [...inputTasks] } }
|
|
397
394
|
: {}),
|
|
398
395
|
}
|
|
399
396
|
}
|
|
@@ -41,6 +41,11 @@ export interface PrefetchArgs {
|
|
|
41
41
|
concurrency: number
|
|
42
42
|
/** When false (remote reads disabled), no prefetch fires. */
|
|
43
43
|
remoteRead: boolean
|
|
44
|
+
/**
|
|
45
|
+
* With reads off and writes on (`--force`), the keys go to
|
|
46
|
+
* `probeBeforeUpload` instead, so a key a remote holds is not re-sent.
|
|
47
|
+
*/
|
|
48
|
+
remoteWrite: boolean
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|
|
@@ -55,7 +60,15 @@ export interface PrefetchArgs {
|
|
|
55
60
|
* is always correct.
|
|
56
61
|
*/
|
|
57
62
|
export function startRemotePrefetch(args: PrefetchArgs): Promise<void> {
|
|
58
|
-
if (!args.remoteRead)
|
|
63
|
+
if (!args.remoteRead) {
|
|
64
|
+
if (!args.remoteWrite || args.cache.probeBeforeUpload === undefined) return Promise.resolve()
|
|
65
|
+
const hashes = deriveStableKeys(args).then((keys) => [...new Set(keys.map((k) => k.hash))])
|
|
66
|
+
args.cache.probeBeforeUpload(hashes)
|
|
67
|
+
return hashes.then(
|
|
68
|
+
() => {},
|
|
69
|
+
() => {},
|
|
70
|
+
)
|
|
71
|
+
}
|
|
59
72
|
// Detached from EXECUTION: the caller does NOT await this before
|
|
60
73
|
// scheduling, so prefetch network latency overlaps the run. But the
|
|
61
74
|
// caller DOES await the returned handle before closing the cache, so
|
|
@@ -1,182 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// cpu_ms, peak_rss_bytes, hrtime spans).
|
|
6
|
-
// Default path: <cacheDir>/runs/<run_id>.json.
|
|
7
|
-
//
|
|
8
|
-
// --profile[=<path>] → Chrome-trace JSON of the run. Each task is
|
|
9
|
-
// a single complete event (`ph: 'X'`) with
|
|
10
|
-
// ts / dur in microseconds derived from the
|
|
11
|
-
// hrtime spans the runner already captures.
|
|
12
|
-
// Default path: profile.json (cwd-relative).
|
|
13
|
-
//
|
|
14
|
-
// Both are no-ops if the corresponding RunOptions field is undefined.
|
|
1
|
+
// `--profile[=<path>]`: Chrome-trace JSON of the run. Each task is a single
|
|
2
|
+
// complete event (`ph: 'X'`) with ts / dur in microseconds derived from the
|
|
3
|
+
// hrtime spans the runner already captures. Default path: profile.json
|
|
4
|
+
// (cwd-relative).
|
|
15
5
|
|
|
16
6
|
import path from 'node:path'
|
|
17
|
-
import {
|
|
18
|
-
import { tallyOutcomes, type Tally } from './tally.js'
|
|
19
|
-
import type { FlakyFinding } from './failure-mode.js'
|
|
20
|
-
import { isCacheHit } from './telemetry.js'
|
|
21
|
-
import { ranNoCache } from './events.js'
|
|
22
|
-
|
|
23
|
-
export interface SummarizeArgs {
|
|
24
|
-
/** Empty string → default path; otherwise the explicit file path. */
|
|
25
|
-
target: string
|
|
26
|
-
cacheDir: string
|
|
27
|
-
cwd: string
|
|
28
|
-
runId: string
|
|
29
|
-
startedAtMs: number
|
|
30
|
-
endedAtMs: number
|
|
31
|
-
totalMs: number
|
|
32
|
-
/** The run's verdict — the same value the CLI turns into the exit code. */
|
|
33
|
-
ok: boolean
|
|
34
|
-
/** The process's exit code where it is not `ok ? 0 : 1`: a stopping signal's. */
|
|
35
|
-
exitCode?: number
|
|
36
|
-
outcomes: readonly TaskOutcome[]
|
|
37
|
-
/** What `detectFlaky` found this run; each row's flaky note, typed. */
|
|
38
|
-
flaky?: readonly FlakyFinding[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** One task of `--summarize`: the wire, stated by `schemas/summary.json`. */
|
|
42
|
-
export interface SummaryTaskJson {
|
|
43
|
-
id: string
|
|
44
|
-
project: string
|
|
45
|
-
task: string
|
|
46
|
-
status: TaskOutcome['status']
|
|
47
|
-
exitCode: number
|
|
48
|
-
durationMs: number
|
|
49
|
-
hash: string | null
|
|
50
|
-
restored?: boolean
|
|
51
|
-
noCache?: true
|
|
52
|
-
flaky?: { passes: number; failures: number; attempts: number }
|
|
53
|
-
cpuMs?: number
|
|
54
|
-
peakRssBytes?: number
|
|
55
|
-
storedDurationMs?: number
|
|
56
|
-
storedCpuMs?: number
|
|
57
|
-
storedPeakRssBytes?: number
|
|
58
|
-
admissionHeldMs?: number
|
|
59
|
-
blockedBy?: string
|
|
60
|
-
timedOut?: true
|
|
61
|
-
sandboxViolations?: number
|
|
62
|
-
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
63
|
-
wallclockStartNs?: string
|
|
64
|
-
wallclockEndNs?: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The `--summarize` document. */
|
|
68
|
-
export interface RunSummaryJson {
|
|
69
|
-
runId: string
|
|
70
|
-
ok: boolean
|
|
71
|
-
exitCode: number
|
|
72
|
-
startedAt: string
|
|
73
|
-
endedAt: string
|
|
74
|
-
totalMs: number
|
|
75
|
-
/** What the hits skipped: the sum of their `storedDurationMs`. */
|
|
76
|
-
savedMs: number
|
|
77
|
-
tasks: SummaryTaskJson[]
|
|
78
|
-
aborted: SummaryTaskJson[]
|
|
79
|
-
summary: Tally
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** One task's entry. Shared by `tasks` and `aborted` so they read alike. */
|
|
83
|
-
function taskEntry(o: TaskOutcome, flaky?: FlakyFinding): SummaryTaskJson {
|
|
84
|
-
return {
|
|
85
|
-
id: o.node.id,
|
|
86
|
-
project: o.node.projectName,
|
|
87
|
-
task: o.node.taskName,
|
|
88
|
-
status: o.status,
|
|
89
|
-
exitCode: o.exitCode,
|
|
90
|
-
durationMs: o.durationMs,
|
|
91
|
-
hash: o.hash ?? null,
|
|
92
|
-
// On a hit: whether it restored outputs or found them up to date, as
|
|
93
|
-
// `summary`'s restoredLocal / restoredRemote / upToDate count it.
|
|
94
|
-
...(isCacheHit(o.status) ? { restored: o.restored === true } : {}),
|
|
95
|
-
// A task with no `cache` block executes every run by design; without
|
|
96
|
-
// the flag a consumer computing a hit rate cannot tell a miss from a
|
|
97
|
-
// task that could never hit (its `hash` is still set — dependents fold
|
|
98
|
-
// it). Present only when true, so every other row is byte-identical.
|
|
99
|
-
...(ranNoCache(o) ? { noCache: true } : {}),
|
|
100
|
-
// Present only when the run proved the task flaky: this exact key
|
|
101
|
-
// failed after it had passed (this run counted), or the task
|
|
102
|
-
// needed a retry. A consumer gating on `failed` can tell a break from a
|
|
103
|
-
// flake without the history; every other row is byte-identical.
|
|
104
|
-
...(flaky !== undefined
|
|
105
|
-
? { flaky: { passes: flaky.passes, failures: flaky.failures, attempts: flaky.attempts } }
|
|
106
|
-
: {}),
|
|
107
|
-
...(o.cpuMs !== undefined ? { cpuMs: o.cpuMs } : {}),
|
|
108
|
-
...(o.peakRssBytes !== undefined ? { peakRssBytes: o.peakRssBytes } : {}),
|
|
109
|
-
// A hit's row names what the PRODUCING execution used under its own
|
|
110
|
-
// keys, so a consumer never reads a remote worker's peak as this run's.
|
|
111
|
-
...(o.storedDurationMs !== undefined ? { storedDurationMs: o.storedDurationMs } : {}),
|
|
112
|
-
...(o.storedCpuMs !== undefined ? { storedCpuMs: o.storedCpuMs } : {}),
|
|
113
|
-
...(o.storedPeakRssBytes !== undefined ? { storedPeakRssBytes: o.storedPeakRssBytes } : {}),
|
|
114
|
-
// Present only when an `admit` policy held the task with a worker free.
|
|
115
|
-
...(o.admissionHeldMs !== undefined ? { admissionHeldMs: o.admissionHeldMs } : {}),
|
|
116
|
-
// A skipped row names the failed (or aborted) task at the root of what
|
|
117
|
-
// blocked it; a fail-fast skip has none.
|
|
118
|
-
...(o.blockedBy !== undefined ? { blockedBy: o.blockedBy } : {}),
|
|
119
|
-
// A failure vx's own `timeout` killed; absent on every other row.
|
|
120
|
-
...(o.timedOut === true ? { timedOut: true } : {}),
|
|
121
|
-
// The sandbox's violation count; present only on a task it recorded any for.
|
|
122
|
-
...(o.sandboxViolations !== undefined ? { sandboxViolations: o.sandboxViolations } : {}),
|
|
123
|
-
// A persistent task that never became ready, and why.
|
|
124
|
-
...(o.notReady !== undefined ? { notReady: o.notReady } : {}),
|
|
125
|
-
// hrtime spans are bigints → emit as strings so JSON.parse on
|
|
126
|
-
// the consumer side doesn't truncate the ns precision.
|
|
127
|
-
...(o.wallclockStartNs !== undefined ? { wallclockStartNs: String(o.wallclockStartNs) } : {}),
|
|
128
|
-
...(o.wallclockEndNs !== undefined ? { wallclockEndNs: String(o.wallclockEndNs) } : {}),
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function writeRunSummary(args: SummarizeArgs): Promise<string> {
|
|
133
|
-
const outPath =
|
|
134
|
-
args.target === ''
|
|
135
|
-
? path.join(args.cacheDir, 'runs', `${args.runId}.json`)
|
|
136
|
-
: path.resolve(args.cwd, args.target)
|
|
137
|
-
await Bun.write(outPath, JSON.stringify(runSummaryJson(args), null, 2))
|
|
138
|
-
return outPath
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The `--summarize` document, which `vx run --format json` prints. */
|
|
142
|
-
export function runSummaryJson(
|
|
143
|
-
args: Omit<SummarizeArgs, 'target' | 'cacheDir' | 'cwd'>,
|
|
144
|
-
): RunSummaryJson {
|
|
145
|
-
// `tasks` and `summary` must describe the same population, or one artifact
|
|
146
|
-
// contradicts itself (a group task listed in `tasks` but absent from
|
|
147
|
-
// `summary.total`). `tallyOutcomes` owns the rule; mirror its filter here.
|
|
148
|
-
const counted = args.outcomes.filter((o) => !isGroupTask(o.node) && o.status !== 'aborted')
|
|
149
|
-
// A task killed by a shutdown signal is in no bucket and no total, yet it
|
|
150
|
-
// makes the run red. Listing it separately keeps `tasks.length ===
|
|
151
|
-
// summary.total` while leaving the artifact able to explain a non-zero
|
|
152
|
-
// exit — the terminal has said so in its Aborted section all along, and a
|
|
153
|
-
// parser must not be told less than a human is.
|
|
154
|
-
const aborted = args.outcomes.filter((o) => !isGroupTask(o.node) && o.status === 'aborted')
|
|
155
|
-
const flakyById = new Map((args.flaky ?? []).map((f) => [f.taskId, f]))
|
|
156
|
-
const exitCode = args.exitCode ?? (args.ok ? 0 : 1)
|
|
157
|
-
const payload: RunSummaryJson = {
|
|
158
|
-
runId: args.runId,
|
|
159
|
-
// The run-level verdict, first: a consumer gating on this artifact must
|
|
160
|
-
// not have to re-derive it by re-implementing the bucket rules.
|
|
161
|
-
ok: exitCode === 0,
|
|
162
|
-
exitCode,
|
|
163
|
-
startedAt: new Date(args.startedAtMs).toISOString(),
|
|
164
|
-
endedAt: new Date(args.endedAtMs).toISOString(),
|
|
165
|
-
totalMs: args.totalMs,
|
|
166
|
-
// The summary row's `saved`, by the same rule: hits only, groups aside.
|
|
167
|
-
savedMs: counted.reduce(
|
|
168
|
-
(ms, o) => (isCacheHit(o.status) ? ms + (o.storedDurationMs ?? 0) : ms),
|
|
169
|
-
0,
|
|
170
|
-
),
|
|
171
|
-
tasks: counted.map((o) => taskEntry(o, flakyById.get(o.node.id))),
|
|
172
|
-
aborted: aborted.map((o) => taskEntry(o)),
|
|
173
|
-
// The FULL list: `tallyOutcomes` applies the same group/aborted
|
|
174
|
-
// exclusions internally, so every counted bucket is unchanged by passing
|
|
175
|
-
// it — but `summary.aborted` is only non-zero if it sees them.
|
|
176
|
-
summary: tallyOutcomes(args.outcomes),
|
|
177
|
-
}
|
|
178
|
-
return payload
|
|
179
|
-
}
|
|
7
|
+
import type { TaskOutcome } from '../graph/index.js'
|
|
180
8
|
|
|
181
9
|
export interface ProfileArgs {
|
|
182
10
|
target: string
|
|
@@ -15,6 +15,8 @@ import os from 'node:os'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { executablePath, gitEnv, relPosix, ulid, xxh3hex } from '../util/index.js'
|
|
17
17
|
import { resolveSetting } from '../settings.js'
|
|
18
|
+
import { VERSION } from '../version.js'
|
|
19
|
+
import type { RunContextRecord } from './telemetry.js'
|
|
18
20
|
|
|
19
21
|
export interface GitContext {
|
|
20
22
|
/** `git rev-parse HEAD`, or null outside a repo / on failure. */
|
|
@@ -522,3 +524,56 @@ export function currentBranch(
|
|
|
522
524
|
const set = resolveSetting('run.branch', workspace, flag, env).value as string | undefined
|
|
523
525
|
return set ?? captureGitContext(workspaceRoot, null, env).branch
|
|
524
526
|
}
|
|
527
|
+
|
|
528
|
+
export interface TelemetryRunContextArgs {
|
|
529
|
+
runId: string
|
|
530
|
+
/** The invocation, already masked. */
|
|
531
|
+
command: string
|
|
532
|
+
requestedTasks: readonly string[]
|
|
533
|
+
cachePolicy: string
|
|
534
|
+
concurrency: number
|
|
535
|
+
flow: RunContextRecord['flow']
|
|
536
|
+
git: GitContext
|
|
537
|
+
ci: CiContext
|
|
538
|
+
host: HostContext
|
|
539
|
+
workspaceRoot: string
|
|
540
|
+
/** `remote.origin.url` as the run's enumeration read it; see `captureWorkspaceIdentity`. */
|
|
541
|
+
originUrl: string | null | undefined
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* The run-context record telemetry sinks receive: the invocation header's
|
|
546
|
+
* git/CI/host data shaped as the export contract. Paid only when a sink can
|
|
547
|
+
* exist: the workspace identity may write `.vx/cache/workspace-id`.
|
|
548
|
+
*/
|
|
549
|
+
export function telemetryRunContext(a: TelemetryRunContextArgs): RunContextRecord {
|
|
550
|
+
const ws = captureWorkspaceIdentity(a.workspaceRoot, a.originUrl)
|
|
551
|
+
const { ci } = a
|
|
552
|
+
return {
|
|
553
|
+
runId: a.runId,
|
|
554
|
+
vxVersion: VERSION,
|
|
555
|
+
command: a.command,
|
|
556
|
+
requestedTasks: [...a.requestedTasks],
|
|
557
|
+
cachePolicy: a.cachePolicy,
|
|
558
|
+
concurrency: a.concurrency,
|
|
559
|
+
flow: a.flow,
|
|
560
|
+
commitSha: a.git.commitSha,
|
|
561
|
+
branch: a.git.branch,
|
|
562
|
+
defaultBranch: captureDefaultBranch(process.env, a.workspaceRoot),
|
|
563
|
+
dirty: a.git.dirty,
|
|
564
|
+
ci: ci.ci,
|
|
565
|
+
ciProvider: ci.provider,
|
|
566
|
+
...(ci.runUrl !== undefined ? { ciRunUrl: ci.runUrl } : {}),
|
|
567
|
+
...(ci.change !== undefined ? { ciChange: ci.change } : {}),
|
|
568
|
+
...(ci.pipeline !== undefined ? { ciPipeline: ci.pipeline } : {}),
|
|
569
|
+
...(ci.job !== undefined ? { ciJob: ci.job } : {}),
|
|
570
|
+
...(ci.attempt !== undefined ? { ciAttempt: ci.attempt } : {}),
|
|
571
|
+
host: a.host.host,
|
|
572
|
+
os: a.host.os,
|
|
573
|
+
arch: a.host.arch,
|
|
574
|
+
workspaceId: ws.id,
|
|
575
|
+
workspaceName: ws.name,
|
|
576
|
+
...(ws.repository !== undefined ? { repository: ws.repository } : {}),
|
|
577
|
+
...(ws.path !== undefined ? { workspacePath: ws.path } : {}),
|
|
578
|
+
}
|
|
579
|
+
}
|
|
@@ -8,9 +8,9 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
10
10
|
import { decodeOutputLog } from './output-log.js'
|
|
11
|
-
import { PASS_STATUSES } from '
|
|
12
|
-
import { splitTaskId } from '../util/index.js'
|
|
13
|
-
import { fileLocations, fileMemo, type OutputLocation } from '
|
|
11
|
+
import { PASS_STATUSES } from '../history/index.js'
|
|
12
|
+
import { isFsRefusal, splitTaskId } from '../util/index.js'
|
|
13
|
+
import { fileLocations, fileMemo, type OutputLocation } from '../report/index.js'
|
|
14
14
|
|
|
15
15
|
/** Failed runs whose output is kept; run ids are UUIDv7, so names sort by age. */
|
|
16
16
|
const KEPT = 50
|
|
@@ -78,14 +78,26 @@ function failuresOf(outcomes: readonly TaskOutcome[]): StoredFailure[] {
|
|
|
78
78
|
/**
|
|
79
79
|
* Write the run's failures, when any outcome carries output, and drop all
|
|
80
80
|
* but the newest `KEPT` files. Atomic: a reader sees the whole file or none.
|
|
81
|
+
* A refused write (a full disk, a read-only cache dir) costs only this
|
|
82
|
+
* output: `warn` says so and the run's verdict stands.
|
|
81
83
|
*/
|
|
82
84
|
export function writeRunFailures(
|
|
83
85
|
cacheDir: string,
|
|
84
86
|
runId: string,
|
|
85
87
|
outcomes: readonly TaskOutcome[],
|
|
88
|
+
warn: (message: string) => void,
|
|
86
89
|
): void {
|
|
87
90
|
const tasks = failuresOf(outcomes)
|
|
88
91
|
if (tasks.length === 0) return
|
|
92
|
+
try {
|
|
93
|
+
keep(cacheDir, runId, tasks)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (!isFsRefusal(err)) throw err
|
|
96
|
+
warn(`[vx] failed tasks' output not kept: ${err.message} — the verdict above stands`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function keep(cacheDir: string, runId: string, tasks: TaskFailure[]): void {
|
|
89
101
|
const dir = dirOf(cacheDir)
|
|
90
102
|
mkdirSync(dir, { recursive: true })
|
|
91
103
|
const file = path.join(dir, `${runId}.json`)
|
|
@@ -10,7 +10,8 @@ import type { InvocationRecord, RunRecord } from '../cache/index.js'
|
|
|
10
10
|
import { isGroupTask, type TaskOutcome } from '../graph/index.js'
|
|
11
11
|
import { VERSION } from '../version.js'
|
|
12
12
|
import type { CiContext, GitContext, HostContext } from './run-context.js'
|
|
13
|
-
import {
|
|
13
|
+
import { taskTelemetryOf, type TaskTelemetry } from './telemetry.js'
|
|
14
|
+
import { isCacheHit } from '../graph/index.js'
|
|
14
15
|
|
|
15
16
|
export interface RunRecordsInput {
|
|
16
17
|
outcomes: readonly TaskOutcome[]
|
|
@@ -32,12 +33,13 @@ export interface RunRecordsInput {
|
|
|
32
33
|
concurrency: number
|
|
33
34
|
flow: InvocationRecord['flow']
|
|
34
35
|
forwardArgs?: readonly string[] | undefined
|
|
35
|
-
tags: Record<string, string>
|
|
36
36
|
git: GitContext
|
|
37
37
|
ci: CiContext
|
|
38
38
|
host: HostContext
|
|
39
39
|
/** Build the telemetry mirror; a run with no sink allocates none. */
|
|
40
40
|
withTelemetry: boolean
|
|
41
|
+
/** The run's remote failures by key, so far (`Cache.traffic`). */
|
|
42
|
+
remoteTraffic?: ReadonlyMap<string, { failures: readonly string[] }>
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface RunRecords {
|
|
@@ -46,6 +48,15 @@ export interface RunRecords {
|
|
|
46
48
|
telemetryTasks: TaskTelemetry[]
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** A key's remote failures as its `runs` row stores them; none when it met none. */
|
|
52
|
+
function remoteErrorOf(
|
|
53
|
+
traffic: ReadonlyMap<string, { failures: readonly string[] }> | undefined,
|
|
54
|
+
hash: string | undefined,
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const failures = hash === undefined ? undefined : traffic?.get(hash)?.failures
|
|
57
|
+
return failures === undefined || failures.length === 0 ? undefined : failures.join('; ')
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* Every non-group, non-aborted outcome gets a row. An outcome with NO hash
|
|
51
62
|
* (a `skipped` task never probed the cache; a `persistent` one is never
|
|
@@ -71,6 +82,7 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
71
82
|
if (o.status === 'aborted') continue
|
|
72
83
|
if (input.withTelemetry) telemetryTasks.push(taskTelemetryOf(o))
|
|
73
84
|
const blockedBy = o.blockedBy ?? input.tainted?.get(o.node.id)
|
|
85
|
+
const remoteError = remoteErrorOf(input.remoteTraffic, o.hash)
|
|
74
86
|
runs.push({
|
|
75
87
|
...(o.hash !== undefined ? { hash: o.hash } : {}),
|
|
76
88
|
project: o.node.projectName,
|
|
@@ -113,6 +125,7 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
113
125
|
...(o.timedOut === true ? { timedOut: true as const } : {}),
|
|
114
126
|
...(o.sandboxViolations !== undefined ? { sandboxViolations: o.sandboxViolations } : {}),
|
|
115
127
|
...(o.notReady !== undefined ? { notReady: o.notReady } : {}),
|
|
128
|
+
...(remoteError !== undefined ? { remoteError } : {}),
|
|
116
129
|
})
|
|
117
130
|
if (o.status === 'failed') failedCount++
|
|
118
131
|
if (o.status === 'cache-hit') hitLocalCount++
|
|
@@ -151,7 +164,26 @@ export function assembleRunRecords(input: RunRecordsInput): RunRecords {
|
|
|
151
164
|
os: input.host.os,
|
|
152
165
|
arch: input.host.arch,
|
|
153
166
|
vxVersion: VERSION,
|
|
154
|
-
tags: JSON.stringify(input.tags),
|
|
155
167
|
}
|
|
156
168
|
return { runs, invocation, telemetryTasks }
|
|
157
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The remote failures uploads met after the run's rows went in (the uploads
|
|
173
|
+
* drain later): each row whose text is now short of its key's is rewritten.
|
|
174
|
+
*/
|
|
175
|
+
export function recordLateRemoteErrors(
|
|
176
|
+
cache: {
|
|
177
|
+
readonly traffic: { readonly keys: ReadonlyMap<string, { failures: readonly string[] }> }
|
|
178
|
+
recordRemoteErrors(runId: string, errors: readonly (readonly [string, string])[]): void
|
|
179
|
+
},
|
|
180
|
+
runs: readonly RunRecord[],
|
|
181
|
+
runId: string,
|
|
182
|
+
): void {
|
|
183
|
+
const late: [string, string][] = []
|
|
184
|
+
for (const r of runs) {
|
|
185
|
+
const now = remoteErrorOf(cache.traffic.keys, r.hash)
|
|
186
|
+
if (now !== undefined && now !== r.remoteError) late.push([r.hash!, now])
|
|
187
|
+
}
|
|
188
|
+
if (late.length > 0) cache.recordRemoteErrors(runId, late)
|
|
189
|
+
}
|