@vzn/vx-reapi 0.0.591 → 0.0.595
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 +6 -5
- package/package.json +3 -4
- package/src/cache.ts +12 -12
- package/src/executor.ts +19 -12
- package/src/merkle.ts +8 -5
- package/src/wire.ts +8 -4
package/README.md
CHANGED
|
@@ -223,11 +223,12 @@ through; a Tree file's setuid and setgid bits are dropped.
|
|
|
223
223
|
## Artifacts stream
|
|
224
224
|
|
|
225
225
|
An artifact up to 256 KiB is read whole and sent in one batch, with no
|
|
226
|
-
probe first.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
probe first. Up to the batch limit (about 4 MiB) it is read whole once,
|
|
227
|
+
probed with `FindMissingBlobs`, and those bytes sent. Past that the cache
|
|
228
|
+
layer never holds an artifact whole: `put` digests the file-backed `Blob`
|
|
229
|
+
vx hands it in one pass over its stream, asks `FindMissingBlobs`, and
|
|
230
|
+
uploads from a second pass, read `chunkBytes` at a time as the ByteStream
|
|
231
|
+
write drains, identity-encoded (the artifact is zstd already). `get` returns the ByteStream read as a
|
|
231
232
|
`Response`, each message taken from the call as vx writes the previous one
|
|
232
233
|
to disk.
|
|
233
234
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-reapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.595",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Bazel Remote Execution API plugin for vx — remote cache (ActionCache + CAS) against NativeLink, BuildBuddy, Buildbarn or bazel-remote.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,14 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@grpc/grpc-js": "^1.12.0",
|
|
36
|
-
"@grpc/proto-loader": "^0.7.13"
|
|
37
|
-
"protobufjs": "^7.4.0"
|
|
36
|
+
"@grpc/proto-loader": "^0.7.13"
|
|
38
37
|
},
|
|
39
38
|
"publishConfig": {
|
|
40
39
|
"access": "public"
|
|
41
40
|
},
|
|
42
41
|
"peerDependencies": {
|
|
43
|
-
"@vzn/vx": "^0.0.
|
|
42
|
+
"@vzn/vx": "^0.0.595"
|
|
44
43
|
},
|
|
45
44
|
"repository": {
|
|
46
45
|
"type": "git",
|
package/src/cache.ts
CHANGED
|
@@ -159,32 +159,32 @@ export class ReapiRemoteCache {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
163
|
-
* digest first (the CAS address must be known
|
|
164
|
-
* then the upload from a second read of the
|
|
162
|
+
* An artifact past `writeBlob`'s whole read takes two passes over `body`,
|
|
163
|
+
* never one in memory: the digest first (the CAS address must be known
|
|
164
|
+
* before the server is asked), then the upload from a second read of the
|
|
165
|
+
* stream.
|
|
165
166
|
*/
|
|
166
167
|
async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
// Read once, and those bytes hashed and sent, wherever `writeBlob` would
|
|
169
|
+
// read the Blob whole anyway: a second writer of the key renames its
|
|
170
|
+
// artifact over the file, and two reads sent its bytes under this one's
|
|
171
|
+
// digest (F-50).
|
|
172
|
+
const data = body.size <= this.client.wholeWriteBytes ? await body.bytes() : undefined
|
|
173
|
+
const digest = data === undefined ? await streamedDigestOf(body) : digestOf(data)
|
|
174
|
+
if (data !== undefined && data.length <= SMALL_PUT_BYTES) {
|
|
169
175
|
// A small artifact is sent in one batch without asking first: the
|
|
170
176
|
// probe was a round trip of its own to save an upload no larger than
|
|
171
177
|
// it (102 → 71 ms per save at 15 ms one-way, F-26). Content-addressed,
|
|
172
178
|
// so a re-send is harmless.
|
|
173
|
-
// Read once, and those bytes hashed and sent: a second writer of the
|
|
174
|
-
// key renames its artifact over the file, and two reads sent its bytes
|
|
175
|
-
// under this one's digest (F-50).
|
|
176
179
|
// A server whose batch limit is smaller refuses it; stream instead.
|
|
177
|
-
const data = await body.bytes()
|
|
178
|
-
digest = digestOf(data)
|
|
179
180
|
await this.client
|
|
180
181
|
.batchUpdateBlobs([{ digest, data }])
|
|
181
182
|
.catch(() => this.client.writeBlob(digest, data))
|
|
182
183
|
} else {
|
|
183
|
-
digest = await streamedDigestOf(body)
|
|
184
184
|
// Upload only what the server lacks: for a large artifact the probe is
|
|
185
185
|
// cheap next to the bytes it can skip.
|
|
186
186
|
const missing = await this.client.findMissingBlobs([digest])
|
|
187
|
-
if (missing.length > 0) await this.client.writeBlob(digest, body)
|
|
187
|
+
if (missing.length > 0) await this.client.writeBlob(digest, data ?? body)
|
|
188
188
|
}
|
|
189
189
|
await this.client.updateActionResult(actionDigestFor(hash), {
|
|
190
190
|
exit_code: 0,
|
package/src/executor.ts
CHANGED
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
import { mkdir, writeFile, chmod, readlink, realpath, rm, symlink, unlink } from 'node:fs/promises'
|
|
13
13
|
import { constants, existsSync } from 'node:fs'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
executorFallback,
|
|
17
|
+
isLiteralPattern,
|
|
18
|
+
isUserError,
|
|
19
|
+
normalizeGlob,
|
|
20
|
+
UserError,
|
|
21
|
+
withForwardArgs,
|
|
22
|
+
} from '@vzn/vx'
|
|
16
23
|
import type { ExecuteRequest, ExecuteResult, TaskExecutor, TaskPlacement } from '@vzn/vx'
|
|
17
24
|
import {
|
|
18
25
|
buildInputTree,
|
|
@@ -30,6 +37,9 @@ import {
|
|
|
30
37
|
import { execDigestFor } from './cache.js'
|
|
31
38
|
import type { ActionResult, Digest, Directory, Operation, ReapiClient } from './wire.js'
|
|
32
39
|
|
|
40
|
+
// A default decoder strips a leading U+FEFF, which is part of a path.
|
|
41
|
+
const PATH_UTF8 = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Split a coarse output directory into the paths its GLOB actually names.
|
|
35
45
|
*
|
|
@@ -346,7 +356,7 @@ function decodeOutputFile(buf: Uint8Array): {
|
|
|
346
356
|
i = l
|
|
347
357
|
const slice = buf.subarray(i, i + len)
|
|
348
358
|
i += len
|
|
349
|
-
if (field === 1) out.path =
|
|
359
|
+
if (field === 1) out.path = PATH_UTF8.decode(slice)
|
|
350
360
|
else if (field === 2) out.digest = decodeDigest(slice)
|
|
351
361
|
else if (field === 5 && len > 0) out.contents = slice
|
|
352
362
|
} else if (wire === 0) {
|
|
@@ -370,8 +380,8 @@ function decodeOutputSymlink(buf: Uint8Array): { path: string; target: string }
|
|
|
370
380
|
i = l
|
|
371
381
|
const slice = buf.subarray(i, i + len)
|
|
372
382
|
i += len
|
|
373
|
-
if (key >>> 3 === 1) out.path =
|
|
374
|
-
else if (key >>> 3 === 2) out.target =
|
|
383
|
+
if (key >>> 3 === 1) out.path = PATH_UTF8.decode(slice)
|
|
384
|
+
else if (key >>> 3 === 2) out.target = PATH_UTF8.decode(slice)
|
|
375
385
|
}
|
|
376
386
|
return out
|
|
377
387
|
}
|
|
@@ -445,7 +455,7 @@ function decodeOutputDirectory(buf: Uint8Array): { path: string; tree_digest: Di
|
|
|
445
455
|
i = l
|
|
446
456
|
const slice = buf.subarray(i, i + len)
|
|
447
457
|
i += len
|
|
448
|
-
if (field === 1) out.path =
|
|
458
|
+
if (field === 1) out.path = PATH_UTF8.decode(slice)
|
|
449
459
|
else if (field === 3) out.tree_digest = decodeDigest(slice)
|
|
450
460
|
}
|
|
451
461
|
return out
|
|
@@ -1249,12 +1259,9 @@ async function this_readStream(
|
|
|
1249
1259
|
return ''
|
|
1250
1260
|
}
|
|
1251
1261
|
|
|
1252
|
-
/** Forwarded args are
|
|
1262
|
+
/** Forwarded args are placed by core's own `withForwardArgs`, as the local executor's are. */
|
|
1253
1263
|
function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): string {
|
|
1254
|
-
const
|
|
1255
|
-
req.forwardArgs.length === 0
|
|
1256
|
-
? req.command
|
|
1257
|
-
: `${req.command} ${req.forwardArgs.map((a) => `'${a.replaceAll("'", `'\\''`)}'`).join(' ')}`
|
|
1264
|
+
const command = withForwardArgs(req.command, req.forwardArgs)
|
|
1258
1265
|
// A remote action gets NO PATH from this machine — sending one would put a
|
|
1259
1266
|
// host path in the action digest and split every laptop from every runner.
|
|
1260
1267
|
// But a task's command is normally a package binary (`oxlint`, `tsc`), and
|
|
@@ -1282,7 +1289,7 @@ function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): s
|
|
|
1282
1289
|
return (
|
|
1283
1290
|
`VX_ROOT=${root}; ${cd}` +
|
|
1284
1291
|
`export PATH="$VX_ROOT/node_modules/.bin:$PWD/node_modules/.bin:$PATH"; ` +
|
|
1285
|
-
|
|
1292
|
+
command
|
|
1286
1293
|
)
|
|
1287
1294
|
}
|
|
1288
1295
|
|
|
@@ -1342,7 +1349,7 @@ export function commandEnvironment(
|
|
|
1342
1349
|
return [...merged].map(([name, value]) => ({ name, value }))
|
|
1343
1350
|
}
|
|
1344
1351
|
|
|
1345
|
-
|
|
1352
|
+
interface OutputPathSets {
|
|
1346
1353
|
/** v2.1+ `output_paths` — deduped, sorted. */
|
|
1347
1354
|
outputPaths: string[]
|
|
1348
1355
|
/** v2.0 legacy split: wildcard-free globs are files, prefix-derived are directories. */
|
package/src/merkle.ts
CHANGED
|
@@ -14,12 +14,15 @@ import { lstat, readlink } from 'node:fs/promises'
|
|
|
14
14
|
import path from 'node:path'
|
|
15
15
|
import type { Digest, Directory, DirectoryNode, FileNode, SymlinkNode } from './wire.js'
|
|
16
16
|
|
|
17
|
+
// A default decoder strips a leading U+FEFF, which is part of a name.
|
|
18
|
+
const PATH_UTF8 = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
19
|
+
|
|
17
20
|
export interface Blob {
|
|
18
21
|
digest: Digest
|
|
19
22
|
data: Uint8Array
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
interface InputTree {
|
|
23
26
|
/** Digest of the root `Directory` — goes in `Action.input_root_digest`. */
|
|
24
27
|
root: Digest
|
|
25
28
|
/** Every blob the server needs: file contents plus the Directory nodes. */
|
|
@@ -740,7 +743,7 @@ function decodeFileNode(buf: Uint8Array): FileNode {
|
|
|
740
743
|
i = l
|
|
741
744
|
const slice = buf.subarray(i, i + len)
|
|
742
745
|
i += len
|
|
743
|
-
if (field === 1) f.name =
|
|
746
|
+
if (field === 1) f.name = PATH_UTF8.decode(slice)
|
|
744
747
|
else if (field === 2) f.digest = decodeDigestBytes(slice)
|
|
745
748
|
else if (field === 6) f.node_properties = decodeNodeProperties(slice)
|
|
746
749
|
} else if (wire === 0) {
|
|
@@ -802,7 +805,7 @@ function decodeDirectoryNode(buf: Uint8Array): DirectoryNode {
|
|
|
802
805
|
i = l
|
|
803
806
|
const slice = buf.subarray(i, i + len)
|
|
804
807
|
i += len
|
|
805
|
-
if (key >>> 3 === 1) d.name =
|
|
808
|
+
if (key >>> 3 === 1) d.name = PATH_UTF8.decode(slice)
|
|
806
809
|
else if (key >>> 3 === 2) d.digest = decodeDigestBytes(slice)
|
|
807
810
|
}
|
|
808
811
|
return d
|
|
@@ -819,8 +822,8 @@ function decodeSymlinkNode(buf: Uint8Array): SymlinkNode {
|
|
|
819
822
|
i = l
|
|
820
823
|
const slice = buf.subarray(i, i + len)
|
|
821
824
|
i += len
|
|
822
|
-
if (key >>> 3 === 1) sl.name =
|
|
823
|
-
else if (key >>> 3 === 2) sl.target =
|
|
825
|
+
if (key >>> 3 === 1) sl.name = PATH_UTF8.decode(slice)
|
|
826
|
+
else if (key >>> 3 === 2) sl.target = PATH_UTF8.decode(slice)
|
|
824
827
|
}
|
|
825
828
|
return sl
|
|
826
829
|
}
|
package/src/wire.ts
CHANGED
|
@@ -82,7 +82,7 @@ export interface Digest {
|
|
|
82
82
|
size_bytes: number
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
interface ServerCapabilities {
|
|
86
86
|
digestFunctions: string[]
|
|
87
87
|
maxBatchBytes: number
|
|
88
88
|
acUpdateEnabled: boolean
|
|
@@ -1002,6 +1002,11 @@ export class ReapiClient {
|
|
|
1002
1002
|
)
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
|
+
/** The largest `Blob` `writeBlob` reads whole rather than streams. */
|
|
1006
|
+
get wholeWriteBytes(): number {
|
|
1007
|
+
return this.negotiatedBatchBytes || SAFE_BATCH_BYTES
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1005
1010
|
/**
|
|
1006
1011
|
* Upload a blob via ByteStream, chunked at `chunkBytes`, RESUMING an
|
|
1007
1012
|
* interrupted identity upload from the server's committed offset
|
|
@@ -1017,8 +1022,7 @@ export class ReapiClient {
|
|
|
1017
1022
|
* zstd artifact already. A Blob under the limit is small enough to read.
|
|
1018
1023
|
*/
|
|
1019
1024
|
async writeBlob(digest: Digest, source: Uint8Array | Blob): Promise<void> {
|
|
1020
|
-
const streamed =
|
|
1021
|
-
source instanceof Blob && source.size > (this.negotiatedBatchBytes || SAFE_BATCH_BYTES)
|
|
1025
|
+
const streamed = source instanceof Blob && source.size > this.wholeWriteBytes
|
|
1022
1026
|
const body = source instanceof Blob && !streamed ? await source.bytes() : source
|
|
1023
1027
|
const compressed = this.compression && body instanceof Uint8Array
|
|
1024
1028
|
// REAPI carries compression in the RESOURCE NAME:
|
|
@@ -1566,7 +1570,7 @@ export interface Operation {
|
|
|
1566
1570
|
metadata?: { type_url?: string; value?: Uint8Array }
|
|
1567
1571
|
}
|
|
1568
1572
|
|
|
1569
|
-
|
|
1573
|
+
interface ExecuteOptions {
|
|
1570
1574
|
/** The operation's name, once the server has given one. */
|
|
1571
1575
|
onOperation?: (name: string) => void
|
|
1572
1576
|
/** Default TRUE: vx owns the cache decision, so the server must not re-check its own AC. */
|