@vzn/vx 0.0.18 → 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/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -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/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// LayeredCache — composes the local cache with a remote HTTP cache.
|
|
2
2
|
//
|
|
3
|
-
// Read path: try local. On miss, try remote; on remote hit,
|
|
4
|
-
// the artifact
|
|
3
|
+
// Read path: try local. On miss, try remote; on remote hit, stream
|
|
4
|
+
// the artifact body into local so the next read is a local hit.
|
|
5
5
|
//
|
|
6
6
|
// Write path: write to local synchronously. Upload the local artifact
|
|
7
7
|
// to remote as a fire-and-forget background task; failures log a
|
|
@@ -42,10 +42,23 @@ import { FULL_CACHE_POLICY, type OutputDirRow } from './cache.js'
|
|
|
42
42
|
* policy gating, in-flight dedup, remote provenance, and the never-fail
|
|
43
43
|
* contract (implementations THROW on failure; LayeredCache degrades every
|
|
44
44
|
* throw to a cache miss via `onRemoteError`). The artifact bytes are the
|
|
45
|
-
* local `<hash>.tar.zst` verbatim.
|
|
46
|
-
*
|
|
45
|
+
* local `<hash>.tar.zst` verbatim. The wires live in plugin packages
|
|
46
|
+
* (`@vzn/vx-migrate`'s `turboCache()` and `nxCache()`, `@vzn/vx-reapi`); see
|
|
47
|
+
* docs/modules/layered-cache.md.
|
|
48
|
+
*
|
|
49
|
+
* Core awaits every call and bounds none: a `get` that never settles
|
|
50
|
+
* holds its task and a `put` the run's upload drain, so each request
|
|
51
|
+
* carries the layer's own deadline (every first-party layer has one; the
|
|
52
|
+
* plugin guide's example shows it).
|
|
47
53
|
*/
|
|
48
54
|
export interface RemoteCacheLayer {
|
|
55
|
+
/**
|
|
56
|
+
* Where the layer's artifacts live, named in every degrade warning
|
|
57
|
+
* (`upload <hash> to <endpoint> failed: …`): a base URL or `host:port`.
|
|
58
|
+
* Never a credential: a URL's `user:pass@`, query and fragment are
|
|
59
|
+
* dropped before it is printed.
|
|
60
|
+
*/
|
|
61
|
+
readonly endpoint?: string
|
|
49
62
|
/** Existence probe (drives the plan path's `--dry` remote prediction). */
|
|
50
63
|
has(hash: string): Promise<boolean>
|
|
51
64
|
/**
|
|
@@ -57,11 +70,47 @@ export interface RemoteCacheLayer {
|
|
|
57
70
|
* info; use per-hash".
|
|
58
71
|
*/
|
|
59
72
|
hasMany?(hashes: readonly string[]): Promise<Set<string> | null>
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Fetch an artifact; `null` = miss. `body` is read once, by core,
|
|
75
|
+
* straight into the local cache — an HTTP wire returns its `fetch`
|
|
76
|
+
* `Response`, a chunked wire `new Response(stream)`, bytes in hand
|
|
77
|
+
* `new Blob([bytes])`. `durationMs` is the producing task's duration
|
|
78
|
+
* when the wire carries it.
|
|
79
|
+
*/
|
|
80
|
+
get(hash: string): Promise<{ body: Blob | Response; durationMs: number | undefined } | null>
|
|
81
|
+
/**
|
|
82
|
+
* Store an artifact (fire-and-forget from LayeredCache's perspective).
|
|
83
|
+
* `body` is file-backed (`Bun.file`) when the local store holds the
|
|
84
|
+
* artifact, so a plugin that streams it never holds it whole.
|
|
85
|
+
*/
|
|
86
|
+
put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What a remote layer resolves is a plugin's, so its shape is a boundary
|
|
91
|
+
* (item 252): a `get` that resolved `{ body: 'abc' }` was reported as
|
|
92
|
+
* "corrupt artifact … not a readable archive" — the bytes blamed for the
|
|
93
|
+
* plugin's shape — and a `hasMany` that resolved an array reached the
|
|
94
|
+
* prefetch pass's `.has()`. Named here and degraded to a miss, like every
|
|
95
|
+
* other remote failure.
|
|
96
|
+
*/
|
|
97
|
+
function invalidRemoteResult(what: string): Error {
|
|
98
|
+
return new Error(
|
|
99
|
+
`remote cache layer returned an invalid result: ${what} — a plugin bug, degraded to a miss`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isBody(value: unknown): value is Blob | Response {
|
|
104
|
+
return value instanceof Blob || value instanceof Response
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function describeValue(value: unknown): string {
|
|
108
|
+
if (value === null) return 'null'
|
|
109
|
+
if (Array.isArray(value)) return 'an array'
|
|
110
|
+
// The pre-stream contract's shape, named so an unported layer reads its fix.
|
|
111
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value))
|
|
112
|
+
return `a ${value.constructor.name}`
|
|
113
|
+
return typeof value === 'object' ? 'an object' : typeof value
|
|
65
114
|
}
|
|
66
115
|
|
|
67
116
|
/**
|
|
@@ -70,8 +119,51 @@ export interface RemoteCacheLayer {
|
|
|
70
119
|
*/
|
|
71
120
|
const UPLOAD_CONCURRENCY = 4
|
|
72
121
|
|
|
122
|
+
/** The seam call a failure happened in, as the warning names it. */
|
|
123
|
+
type RemoteOp = 'probe' | 'download' | 'upload'
|
|
124
|
+
|
|
125
|
+
const PREPOSITION: Record<RemoteOp, string> = { probe: 'at', download: 'from', upload: 'to' }
|
|
126
|
+
|
|
127
|
+
/** The layer's `endpoint` as it may be printed: no userinfo, query or fragment. */
|
|
128
|
+
function printableEndpoint(endpoint: unknown): string | undefined {
|
|
129
|
+
if (typeof endpoint !== 'string' || endpoint === '') return undefined
|
|
130
|
+
if (!URL.canParse(endpoint)) return endpoint
|
|
131
|
+
const u = new URL(endpoint)
|
|
132
|
+
if (u.username === '' && u.password === '' && u.search === '' && u.hash === '') return endpoint
|
|
133
|
+
u.username = ''
|
|
134
|
+
u.password = ''
|
|
135
|
+
u.search = ''
|
|
136
|
+
u.hash = ''
|
|
137
|
+
return u.toString()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What makes two failures "the same way": the error's `code` when it
|
|
142
|
+
* carries one (a gRPC status, an errno, Bun's `ConnectionRefused`), because
|
|
143
|
+
* gRPC writes the elapsed time into its message and no two deadline messages
|
|
144
|
+
* match; else the message with the artifact's hash taken out, so a 500 on
|
|
145
|
+
* one hash and a 500 on the next are one class. The operation is not part of
|
|
146
|
+
* it: an unreachable server fails the probe, the download and the upload
|
|
147
|
+
* alike, and that is one fact.
|
|
148
|
+
*/
|
|
149
|
+
function failureClass(err: Error, hash: string | undefined): string {
|
|
150
|
+
const code = (err as { code?: unknown }).code
|
|
151
|
+
if (typeof code === 'string' || typeof code === 'number') return `${err.name}\0${code}`
|
|
152
|
+
return hash === undefined ? err.message : err.message.replaceAll(hash, '\0')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A batch probe names no one hash, so its line counts them. */
|
|
156
|
+
function batchOf(hashes: readonly string[]): { batch: string } {
|
|
157
|
+
return { batch: `of ${hashes.length} artifact${hashes.length === 1 ? '' : 's'}` }
|
|
158
|
+
}
|
|
159
|
+
|
|
73
160
|
export interface LayeredCacheOptions {
|
|
74
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Called for the remote failures the layer degrades to a miss, once per
|
|
163
|
+
* failure class per layer (`upload <hash> to <endpoint> failed: <cause>`,
|
|
164
|
+
* with the original error as `cause`); the repeats are counted and said
|
|
165
|
+
* once more at `close()` (`<n> more requests failed the same way: <cause>`).
|
|
166
|
+
*/
|
|
75
167
|
onRemoteError?: (err: Error) => void
|
|
76
168
|
/**
|
|
77
169
|
* The 4-axis read/write policy. The local slice (read/write) is
|
|
@@ -120,6 +212,15 @@ export class LayeredCache implements CacheLayer {
|
|
|
120
212
|
private drainWaiters: Array<() => void> = []
|
|
121
213
|
|
|
122
214
|
private readonly policy: CachePolicy
|
|
215
|
+
private readonly endpoint: string | undefined
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One entry per failure class said: its first line went out, `repeats`
|
|
219
|
+
* counts the ones held back. A run's requests are concurrent, so an
|
|
220
|
+
* unreachable server otherwise printed its bare runtime message once per
|
|
221
|
+
* request (three lines for one task over the Turbo wire, item 749).
|
|
222
|
+
*/
|
|
223
|
+
private readonly reported = new Map<string, { cause: string; repeats: number }>()
|
|
123
224
|
|
|
124
225
|
constructor(
|
|
125
226
|
readonly local: Cache,
|
|
@@ -127,10 +228,11 @@ export class LayeredCache implements CacheLayer {
|
|
|
127
228
|
private readonly options: LayeredCacheOptions = {},
|
|
128
229
|
) {
|
|
129
230
|
this.policy = options.policy ?? FULL_CACHE_POLICY
|
|
231
|
+
this.endpoint = printableEndpoint(remote.endpoint)
|
|
130
232
|
}
|
|
131
233
|
|
|
132
|
-
|
|
133
|
-
return
|
|
234
|
+
key(input: CacheKeyInput): Promise<string> {
|
|
235
|
+
return this.local.key(input)
|
|
134
236
|
}
|
|
135
237
|
|
|
136
238
|
async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
|
|
@@ -140,7 +242,7 @@ export class LayeredCache implements CacheLayer {
|
|
|
140
242
|
// point): pullFromRemote registers the `inflight` entry SYNCHRONOUSLY,
|
|
141
243
|
// so a concurrent markRemoteAbsent can't clobber a pending pull — a
|
|
142
244
|
// guard done here (behind an async local.has) would reopen that race.
|
|
143
|
-
return
|
|
245
|
+
return this.pullFromRemote(hash, ctx)
|
|
144
246
|
}
|
|
145
247
|
|
|
146
248
|
/**
|
|
@@ -154,9 +256,22 @@ export class LayeredCache implements CacheLayer {
|
|
|
154
256
|
async remoteHasMany(hashes: readonly string[]): Promise<Set<string> | null> {
|
|
155
257
|
if (!this.policy.remoteRead || this.remote.hasMany === undefined) return null
|
|
156
258
|
try {
|
|
157
|
-
return await
|
|
259
|
+
// `return await`, deliberately: the catch below is the never-fail
|
|
260
|
+
// contract, and a returned promise's rejection would sail past it.
|
|
261
|
+
const found: unknown = await this.remote.hasMany(hashes)
|
|
262
|
+
if (found !== null && found !== undefined && !(found instanceof Set)) {
|
|
263
|
+
this.reportRemoteError(
|
|
264
|
+
'probe',
|
|
265
|
+
batchOf(hashes),
|
|
266
|
+
invalidRemoteResult(
|
|
267
|
+
`hasMany() resolved ${describeValue(found)} (expected a Set of the hashes present, or null)`,
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
return found ?? null
|
|
158
273
|
} catch (err) {
|
|
159
|
-
this.reportRemoteError(err)
|
|
274
|
+
this.reportRemoteError('probe', batchOf(hashes), err)
|
|
160
275
|
return null
|
|
161
276
|
}
|
|
162
277
|
}
|
|
@@ -224,7 +339,7 @@ export class LayeredCache implements CacheLayer {
|
|
|
224
339
|
try {
|
|
225
340
|
return (await this.remote.has(hash)) ? 'remote' : null
|
|
226
341
|
} catch (err) {
|
|
227
|
-
this.reportRemoteError(err)
|
|
342
|
+
this.reportRemoteError('probe', hash, err)
|
|
228
343
|
return null
|
|
229
344
|
}
|
|
230
345
|
}
|
|
@@ -256,16 +371,31 @@ export class LayeredCache implements CacheLayer {
|
|
|
256
371
|
// or remote if a concurrent prefetch set it).
|
|
257
372
|
if ((await this.local.has(hash)) === 'local') return true
|
|
258
373
|
|
|
259
|
-
let remoteResult
|
|
374
|
+
let remoteResult: unknown
|
|
260
375
|
try {
|
|
261
376
|
remoteResult = await this.remote.get(hash)
|
|
262
377
|
} catch (err) {
|
|
263
|
-
this.reportRemoteError(err)
|
|
378
|
+
this.reportRemoteError('download', hash, err)
|
|
264
379
|
return false
|
|
265
380
|
}
|
|
266
381
|
if (!remoteResult) return false
|
|
382
|
+
if (typeof remoteResult !== 'object' || !isBody((remoteResult as { body?: unknown }).body)) {
|
|
383
|
+
const shape =
|
|
384
|
+
typeof remoteResult !== 'object'
|
|
385
|
+
? describeValue(remoteResult)
|
|
386
|
+
: `body is ${describeValue((remoteResult as { body?: unknown }).body)}`
|
|
387
|
+
this.reportRemoteError(
|
|
388
|
+
'download',
|
|
389
|
+
hash,
|
|
390
|
+
invalidRemoteResult(
|
|
391
|
+
`get() resolved ${shape} (expected { body: Blob | Response, durationMs } or null)`,
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
const remoteBody = remoteResult as { body: Blob | Response; durationMs?: number }
|
|
267
397
|
|
|
268
|
-
// Ingest the remote
|
|
398
|
+
// Ingest the remote body into local using the caller-supplied
|
|
269
399
|
// taskId/command plus the remote-reported durationMs. The remote
|
|
270
400
|
// layer carries durationMs as an HTTP header (x-artifact-duration);
|
|
271
401
|
// taskId + command come from the orchestrator's TaskNode in scope.
|
|
@@ -275,16 +405,21 @@ export class LayeredCache implements CacheLayer {
|
|
|
275
405
|
const meta: IngestMeta = {
|
|
276
406
|
taskId: ctx?.taskId ?? `${hash}#unknown`,
|
|
277
407
|
command: ctx?.command ?? '',
|
|
278
|
-
|
|
408
|
+
// A layer's number is taken only as a finite non-negative one: a NaN
|
|
409
|
+
// failed the entry row's NOT NULL after the artifact was renamed into
|
|
410
|
+
// place, which threw a valid hit away and orphaned its bytes, and a
|
|
411
|
+
// negative or infinite one was stored and replayed (item 944).
|
|
412
|
+
durationMs: durationOf(remoteBody.durationMs),
|
|
413
|
+
...(ctx?.outputs !== undefined ? { outputs: ctx.outputs } : {}),
|
|
279
414
|
}
|
|
280
415
|
try {
|
|
281
|
-
await this.local.ingest(hash,
|
|
416
|
+
await this.local.ingest(hash, remoteBody.body, meta)
|
|
282
417
|
} catch (err) {
|
|
283
418
|
// The bytes came off the network — a corrupt/truncated remote
|
|
284
419
|
// artifact must degrade to a cache miss (task re-executes), not
|
|
285
420
|
// crash the run. Local-layer reads outside this block still
|
|
286
421
|
// propagate: local corruption is a real fault, not a network one.
|
|
287
|
-
this.reportRemoteError(err)
|
|
422
|
+
this.reportRemoteError('download', hash, err)
|
|
288
423
|
return false
|
|
289
424
|
}
|
|
290
425
|
this.remoteSourced.add(hash)
|
|
@@ -295,8 +430,8 @@ export class LayeredCache implements CacheLayer {
|
|
|
295
430
|
return this.local.outputsPath(hash)
|
|
296
431
|
}
|
|
297
432
|
|
|
298
|
-
|
|
299
|
-
return
|
|
433
|
+
hashFile(filePath: string): Promise<string> {
|
|
434
|
+
return this.local.hashFile(filePath)
|
|
300
435
|
}
|
|
301
436
|
|
|
302
437
|
async restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> {
|
|
@@ -315,16 +450,13 @@ export class LayeredCache implements CacheLayer {
|
|
|
315
450
|
// cache. Errors are logged, not propagated: the task already
|
|
316
451
|
// succeeded; we don't fail it on cache-server issues.
|
|
317
452
|
//
|
|
318
|
-
// The
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// RSS scaled with a run's total miss artifact bytes rather than with
|
|
322
|
-
// the pool — any cold monorepo run whose remote uploads slower than the
|
|
323
|
-
// build produces artifacts held every pending one at once. Reading in
|
|
324
|
-
// the job caps resident artifact bytes at UPLOAD_CONCURRENCY. The
|
|
453
|
+
// The job hands the plugin a file-backed Blob over the local artifact,
|
|
454
|
+
// opened when the plugin reads it: a queued job holds a path, not a
|
|
455
|
+
// buffer, and a plugin that streams the Blob never holds it whole. The
|
|
325
456
|
// artifact is content-addressed and immutable, so a deferred read sees
|
|
326
457
|
// the same bytes; if a concurrent `vx cache prune` removed it first the
|
|
327
|
-
// read throws and this upload is skipped — the never-fail
|
|
458
|
+
// plugin's read throws and this upload is skipped — the never-fail
|
|
459
|
+
// contract.
|
|
328
460
|
const hash = args.hash
|
|
329
461
|
const durationMs = args.entry.durationMs
|
|
330
462
|
// Local writes disabled (`--cache=local:,remote:rw`): there is no
|
|
@@ -338,16 +470,17 @@ export class LayeredCache implements CacheLayer {
|
|
|
338
470
|
try {
|
|
339
471
|
packed = await this.local.packArtifactBytes(args)
|
|
340
472
|
} catch (err) {
|
|
341
|
-
this.reportRemoteError(err)
|
|
473
|
+
this.reportRemoteError('upload', hash, err)
|
|
342
474
|
return
|
|
343
475
|
}
|
|
344
476
|
}
|
|
345
477
|
this.enqueueUpload(async () => {
|
|
346
478
|
try {
|
|
347
|
-
const
|
|
348
|
-
|
|
479
|
+
const body =
|
|
480
|
+
packed !== undefined ? new Blob([packed]) : Bun.file(this.local.outputsPath(hash))
|
|
481
|
+
await this.remote.put(hash, body, { durationMs })
|
|
349
482
|
} catch (err) {
|
|
350
|
-
this.reportRemoteError(err)
|
|
483
|
+
this.reportRemoteError('upload', hash, err)
|
|
351
484
|
}
|
|
352
485
|
})
|
|
353
486
|
}
|
|
@@ -384,8 +517,8 @@ export class LayeredCache implements CacheLayer {
|
|
|
384
517
|
}
|
|
385
518
|
}
|
|
386
519
|
|
|
387
|
-
async ingest(hash: string,
|
|
388
|
-
await this.local.ingest(hash,
|
|
520
|
+
async ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> {
|
|
521
|
+
await this.local.ingest(hash, body, meta)
|
|
389
522
|
}
|
|
390
523
|
|
|
391
524
|
loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> {
|
|
@@ -401,6 +534,9 @@ export class LayeredCache implements CacheLayer {
|
|
|
401
534
|
recordOutputDirs(hash: string, projectDir: string, prefixes: readonly string[]): Promise<void> {
|
|
402
535
|
return this.local.recordOutputDirs(hash, projectDir, prefixes)
|
|
403
536
|
}
|
|
537
|
+
recordOutputStamps(hash: string, projectDir: string, workspaceRoot: string): void {
|
|
538
|
+
this.local.recordOutputStamps(hash, projectDir, workspaceRoot)
|
|
539
|
+
}
|
|
404
540
|
|
|
405
541
|
loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
|
|
406
542
|
return this.local.loadOutputDirsBatch(hashes)
|
|
@@ -410,14 +546,6 @@ export class LayeredCache implements CacheLayer {
|
|
|
410
546
|
return this.local.outputDirsCurrent(projectDir, rows)
|
|
411
547
|
}
|
|
412
548
|
|
|
413
|
-
recordRun(run: RunRecord): void {
|
|
414
|
-
this.local.recordRun(run)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
recordRuns(runs: readonly RunRecord[]): void {
|
|
418
|
-
this.local.recordRuns(runs)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
549
|
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
422
550
|
this.local.recordRunBundle(bundle)
|
|
423
551
|
}
|
|
@@ -426,16 +554,46 @@ export class LayeredCache implements CacheLayer {
|
|
|
426
554
|
return this.local.stats(opts)
|
|
427
555
|
}
|
|
428
556
|
|
|
429
|
-
|
|
430
|
-
return
|
|
557
|
+
prune(options: PruneOptions): Promise<PruneResult> {
|
|
558
|
+
return this.local.prune(options)
|
|
431
559
|
}
|
|
432
560
|
|
|
561
|
+
/** `run()` drains the uploads and prefetches first, so the repeat counts are final here. */
|
|
433
562
|
close(): void {
|
|
563
|
+
for (const { cause, repeats } of this.reported.values()) {
|
|
564
|
+
if (repeats === 0) continue
|
|
565
|
+
this.emit(
|
|
566
|
+
new Error(
|
|
567
|
+
`${repeats} more request${repeats === 1 ? '' : 's'} failed the same way: ${cause}`,
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
this.reported.clear()
|
|
434
572
|
this.local.close()
|
|
435
573
|
}
|
|
436
574
|
|
|
437
|
-
|
|
575
|
+
/**
|
|
576
|
+
* `subject` is the artifact's hash, or the batch for a `hasMany`. The
|
|
577
|
+
* wire's own message says what went wrong but not on what or where: an
|
|
578
|
+
* upload deadline read `The operation timed out.` with no upload, hash or
|
|
579
|
+
* server in it (item 749).
|
|
580
|
+
*/
|
|
581
|
+
private reportRemoteError(op: RemoteOp, subject: string | { batch: string }, err: unknown): void {
|
|
438
582
|
const e = err instanceof Error ? err : new Error(String(err))
|
|
583
|
+
const hash = typeof subject === 'string' ? subject : undefined
|
|
584
|
+
const cls = failureClass(e, hash)
|
|
585
|
+
const seen = this.reported.get(cls)
|
|
586
|
+
if (seen !== undefined) {
|
|
587
|
+
seen.repeats++
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
this.reported.set(cls, { cause: e.message, repeats: 0 })
|
|
591
|
+
const what = typeof subject === 'string' ? subject : subject.batch
|
|
592
|
+
const where = this.endpoint === undefined ? '' : ` ${PREPOSITION[op]} ${this.endpoint}`
|
|
593
|
+
this.emit(new Error(`${op} ${what}${where} failed: ${e.message}`, { cause: e }))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private emit(e: Error): void {
|
|
439
597
|
// The remote cache is fully optional: NO remote failure — a 500, a
|
|
440
598
|
// network drop, a corrupt artifact, even a throwing onRemoteError
|
|
441
599
|
// callback — may ever fail the run. We report and degrade to a
|
|
@@ -452,3 +610,7 @@ export class LayeredCache implements CacheLayer {
|
|
|
452
610
|
}
|
|
453
611
|
}
|
|
454
612
|
}
|
|
613
|
+
|
|
614
|
+
function durationOf(ms: unknown): number {
|
|
615
|
+
return typeof ms === 'number' && ms >= 0 && ms <= Number.MAX_SAFE_INTEGER ? Math.round(ms) : 0
|
|
616
|
+
}
|