@vzn/vx-reapi 0.0.594 → 0.0.596

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 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. Past that the cache layer never holds an artifact whole: `put` digests the file-backed
227
- `Blob` vx hands it in one pass over its stream, asks `FindMissingBlobs`, and
228
- uploads from a second pass: past the batch limit (about 4 MiB) the file is
229
- read `chunkBytes` at a time as the ByteStream write drains, identity-encoded
230
- (the artifact is zstd already). `get` returns the ByteStream read as a
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.594",
3
+ "version": "0.0.596",
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": [
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "peerDependencies": {
42
- "@vzn/vx": "^0.0.594"
42
+ "@vzn/vx": "^0.0.596"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
package/src/cache.ts CHANGED
@@ -159,32 +159,32 @@ export class ReapiRemoteCache {
159
159
  }
160
160
 
161
161
  /**
162
- * A large artifact takes two passes over `body`, never one in memory: the
163
- * digest first (the CAS address must be known before the server is asked),
164
- * then the upload from a second read of the stream.
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
- let digest: Digest
168
- if (body.size <= SMALL_PUT_BYTES) {
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,16 +12,25 @@
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 { executorFallback, isLiteralPattern, isUserError, normalizeGlob, UserError } from '@vzn/vx'
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,
26
+ decodeDigestBytes,
19
27
  decodeTreeWithBytes,
20
28
  digestWith,
21
29
  encodeAction,
22
30
  encodeCommand,
23
31
  encodeTree,
24
32
  mapBounded,
33
+ readVarintAt,
25
34
  sha256,
26
35
  type Blob,
27
36
  type FileGraft,
@@ -196,12 +205,12 @@ export function decodeExecuteResponseBytes(buf: Uint8Array): DecodedExecuteRespo
196
205
  const out: DecodedExecuteResponse = { serverLogs: [] }
197
206
  let i = 0
198
207
  while (i < buf.length) {
199
- const [key, k] = readVarint(buf, i)
208
+ const [key, k] = readVarintAt(buf, i)
200
209
  i = k
201
210
  const field = key >>> 3
202
211
  const wire = key & 7
203
212
  if (wire === 2) {
204
- const [len, l] = readVarint(buf, i)
213
+ const [len, l] = readVarintAt(buf, i)
205
214
  i = l
206
215
  const slice = buf.subarray(i, i + len)
207
216
  i += len
@@ -212,7 +221,7 @@ export function decodeExecuteResponseBytes(buf: Uint8Array): DecodedExecuteRespo
212
221
  if (entry !== undefined) out.serverLogs.push(entry)
213
222
  } else if (field === 5) out.message = new TextDecoder().decode(slice)
214
223
  } else if (wire === 0) {
215
- const [v, n] = readVarint(buf, i)
224
+ const [v, n] = readVarintAt(buf, i)
216
225
  i = n
217
226
  if (field === 2) out.cachedResult = v === 1
218
227
  } else if (wire === 5) i += 4
@@ -227,16 +236,16 @@ function decodeRpcStatus(buf: Uint8Array): { code: number; message: string } {
227
236
  const st = { code: 0, message: '' }
228
237
  let i = 0
229
238
  while (i < buf.length) {
230
- const [key, k] = readVarint(buf, i)
239
+ const [key, k] = readVarintAt(buf, i)
231
240
  i = k
232
241
  const field = key >>> 3
233
242
  const wire = key & 7
234
243
  if (wire === 0) {
235
- const [v, n] = readVarint(buf, i)
244
+ const [v, n] = readVarintAt(buf, i)
236
245
  i = n
237
246
  if (field === 1) st.code = v
238
247
  } else if (wire === 2) {
239
- const [len, l] = readVarint(buf, i)
248
+ const [len, l] = readVarintAt(buf, i)
240
249
  i = l
241
250
  if (field === 2) st.message = new TextDecoder().decode(buf.subarray(i, i + len))
242
251
  i += len
@@ -254,10 +263,10 @@ function decodeLogEntry(
254
263
  let humanReadable = false
255
264
  let i = 0
256
265
  while (i < buf.length) {
257
- const [key, k] = readVarint(buf, i)
266
+ const [key, k] = readVarintAt(buf, i)
258
267
  i = k
259
268
  if ((key & 7) !== 2) break
260
- const [len, l] = readVarint(buf, i)
269
+ const [len, l] = readVarintAt(buf, i)
261
270
  i = l
262
271
  const slice = buf.subarray(i, i + len)
263
272
  i += len
@@ -265,15 +274,15 @@ function decodeLogEntry(
265
274
  else if (key >>> 3 === 2) {
266
275
  let j = 0
267
276
  while (j < slice.length) {
268
- const [k2, j2] = readVarint(slice, j)
277
+ const [k2, j2] = readVarintAt(slice, j)
269
278
  j = j2
270
279
  if ((k2 & 7) === 2) {
271
- const [len2, j3] = readVarint(slice, j)
280
+ const [len2, j3] = readVarintAt(slice, j)
272
281
  j = j3
273
- if (k2 >>> 3 === 1) digest = decodeDigest(slice.subarray(j, j + len2))
282
+ if (k2 >>> 3 === 1) digest = decodeDigestBytes(slice.subarray(j, j + len2))
274
283
  j += len2
275
284
  } else if ((k2 & 7) === 0) {
276
- const [v, j3] = readVarint(slice, j)
285
+ const [v, j3] = readVarintAt(slice, j)
277
286
  j = j3
278
287
  if (k2 >>> 3 === 2) humanReadable = v === 1
279
288
  } else break
@@ -288,16 +297,16 @@ function decodeActionResult(buf: Uint8Array): ActionResult {
288
297
  const legacyLinks: { path: string; target: string }[] = []
289
298
  let i = 0
290
299
  while (i < buf.length) {
291
- const [key, k] = readVarint(buf, i)
300
+ const [key, k] = readVarintAt(buf, i)
292
301
  i = k
293
302
  const field = key >>> 3
294
303
  const wire = key & 7
295
304
  if (wire === 0) {
296
- const [v, n] = readVarint(buf, i)
305
+ const [v, n] = readVarintAt(buf, i)
297
306
  i = n
298
307
  if (field === 4) res.exit_code = v | 0
299
308
  } else if (wire === 2) {
300
- const [len, l] = readVarint(buf, i)
309
+ const [len, l] = readVarintAt(buf, i)
301
310
  i = l
302
311
  const slice = buf.subarray(i, i + len)
303
312
  i += len
@@ -308,9 +317,9 @@ function decodeActionResult(buf: Uint8Array): ActionResult {
308
317
  if (field === 2) (res.output_files ??= []).push(decodeOutputFile(slice))
309
318
  else if (field === 3) (res.output_directories ??= []).push(decodeOutputDirectory(slice))
310
319
  else if (field === 5) res.stdout_raw = slice
311
- else if (field === 6) res.stdout_digest = decodeDigest(slice)
320
+ else if (field === 6) res.stdout_digest = decodeDigestBytes(slice)
312
321
  else if (field === 7) res.stderr_raw = slice
313
- else if (field === 8) res.stderr_digest = decodeDigest(slice)
322
+ else if (field === 8) res.stderr_digest = decodeDigestBytes(slice)
314
323
  else if (field === 9) res.execution_metadata = decodeExecutedActionMetadata(slice)
315
324
  else if (field === 12) (res.output_symlinks ??= []).push(decodeOutputSymlink(slice))
316
325
  // output_file_symlinks (10) and output_directory_symlinks (11): all a
@@ -340,20 +349,20 @@ function decodeOutputFile(buf: Uint8Array): {
340
349
  }
341
350
  let i = 0
342
351
  while (i < buf.length) {
343
- const [key, k] = readVarint(buf, i)
352
+ const [key, k] = readVarintAt(buf, i)
344
353
  i = k
345
354
  const field = key >>> 3
346
355
  const wire = key & 7
347
356
  if (wire === 2) {
348
- const [len, l] = readVarint(buf, i)
357
+ const [len, l] = readVarintAt(buf, i)
349
358
  i = l
350
359
  const slice = buf.subarray(i, i + len)
351
360
  i += len
352
361
  if (field === 1) out.path = PATH_UTF8.decode(slice)
353
- else if (field === 2) out.digest = decodeDigest(slice)
362
+ else if (field === 2) out.digest = decodeDigestBytes(slice)
354
363
  else if (field === 5 && len > 0) out.contents = slice
355
364
  } else if (wire === 0) {
356
- const [v, n] = readVarint(buf, i)
365
+ const [v, n] = readVarintAt(buf, i)
357
366
  i = n
358
367
  if (field === 4) out.is_executable = v === 1
359
368
  } else break
@@ -366,10 +375,10 @@ function decodeOutputSymlink(buf: Uint8Array): { path: string; target: string }
366
375
  const out = { path: '', target: '' }
367
376
  let i = 0
368
377
  while (i < buf.length) {
369
- const [key, k] = readVarint(buf, i)
378
+ const [key, k] = readVarintAt(buf, i)
370
379
  i = k
371
380
  if ((key & 7) !== 2) break
372
- const [len, l] = readVarint(buf, i)
381
+ const [len, l] = readVarintAt(buf, i)
373
382
  i = l
374
383
  const slice = buf.subarray(i, i + len)
375
384
  i += len
@@ -391,12 +400,12 @@ function decodeExecutedActionMetadata(
391
400
  const meta: NonNullable<ActionResult['execution_metadata']> = {}
392
401
  let i = 0
393
402
  while (i < buf.length) {
394
- const [key, k] = readVarint(buf, i)
403
+ const [key, k] = readVarintAt(buf, i)
395
404
  i = k
396
405
  const field = key >>> 3
397
406
  const wire = key & 7
398
407
  if (wire === 2) {
399
- const [len, l] = readVarint(buf, i)
408
+ const [len, l] = readVarintAt(buf, i)
400
409
  i = l
401
410
  const slice = buf.subarray(i, i + len)
402
411
  i += len
@@ -404,7 +413,7 @@ function decodeExecutedActionMetadata(
404
413
  else if (field === 7) meta.execution_start_timestamp = decodeTimestamp(slice)
405
414
  else if (field === 8) meta.execution_completed_timestamp = decodeTimestamp(slice)
406
415
  } else if (wire === 0) {
407
- const [, n] = readVarint(buf, i)
416
+ const [, n] = readVarintAt(buf, i)
408
417
  i = n
409
418
  } else break
410
419
  }
@@ -416,10 +425,10 @@ function decodeTimestamp(buf: Uint8Array): { seconds?: string; nanos?: number }
416
425
  const ts: { seconds?: string; nanos?: number } = {}
417
426
  let i = 0
418
427
  while (i < buf.length) {
419
- const [key, k] = readVarint(buf, i)
428
+ const [key, k] = readVarintAt(buf, i)
420
429
  i = k
421
430
  if ((key & 7) !== 0) break
422
- const [v, n] = readVarint(buf, i)
431
+ const [v, n] = readVarintAt(buf, i)
423
432
  i = n
424
433
  if (key >>> 3 === 1) ts.seconds = String(v)
425
434
  else if (key >>> 3 === 2) ts.nanos = v
@@ -434,67 +443,26 @@ function decodeOutputDirectory(buf: Uint8Array): { path: string; tree_digest: Di
434
443
  const out = { path: '', tree_digest: { hash: '', size_bytes: 0 } }
435
444
  let i = 0
436
445
  while (i < buf.length) {
437
- const [key, k] = readVarint(buf, i)
446
+ const [key, k] = readVarintAt(buf, i)
438
447
  i = k
439
448
  const field = key >>> 3
440
449
  const wire = key & 7
441
450
  if (wire === 0) {
442
- const [, n] = readVarint(buf, i)
451
+ const [, n] = readVarintAt(buf, i)
443
452
  i = n
444
453
  continue
445
454
  }
446
455
  if (wire !== 2) break
447
- const [len, l] = readVarint(buf, i)
456
+ const [len, l] = readVarintAt(buf, i)
448
457
  i = l
449
458
  const slice = buf.subarray(i, i + len)
450
459
  i += len
451
460
  if (field === 1) out.path = PATH_UTF8.decode(slice)
452
- else if (field === 3) out.tree_digest = decodeDigest(slice)
461
+ else if (field === 3) out.tree_digest = decodeDigestBytes(slice)
453
462
  }
454
463
  return out
455
464
  }
456
465
 
457
- function decodeDigest(buf: Uint8Array): Digest {
458
- const d: Digest = { hash: '', size_bytes: 0 }
459
- let i = 0
460
- while (i < buf.length) {
461
- const [key, k] = readVarint(buf, i)
462
- i = k
463
- const field = key >>> 3
464
- const wire = key & 7
465
- if (wire === 2) {
466
- const [len, l] = readVarint(buf, i)
467
- i = l
468
- if (field === 1) d.hash = new TextDecoder().decode(buf.subarray(i, i + len))
469
- i += len
470
- } else if (wire === 0) {
471
- const [v, n] = readVarint(buf, i)
472
- i = n
473
- if (field === 2) d.size_bytes = v
474
- } else break
475
- }
476
- return d
477
- }
478
-
479
- function readVarint(buf: Uint8Array, at: number): [number, number] {
480
- let result = 0
481
- let low = 0
482
- let shift = 0
483
- let i = at
484
- for (;;) {
485
- const byte = buf[i++]
486
- if (byte === undefined) break
487
- // Added, not OR-ed: a size of 4 GiB or more wrapped to 32 bits (F-43).
488
- // Past 2^53 the value is a negative int32 sent as ten bytes (an exit
489
- // code): its low 32 bits, which `| 0` reads back as the negative.
490
- result += (byte & 0x7f) * 2 ** shift
491
- if (shift < 32) low |= (byte & 0x7f) << shift
492
- if ((byte & 0x80) === 0) break
493
- shift += 7
494
- }
495
- return [result <= Number.MAX_SAFE_INTEGER ? result : low >>> 0, i]
496
- }
497
-
498
466
  /**
499
467
  * Backstop for an action the server never finishes. Generous on purpose: it
500
468
  * exists so a wedged remote cannot hang a run forever, not to police slow
@@ -1252,12 +1220,9 @@ async function this_readStream(
1252
1220
  return ''
1253
1221
  }
1254
1222
 
1255
- /** Forwarded args are appended shell-quoted, exactly as the local executor does. */
1223
+ /** Forwarded args are placed by core's own `withForwardArgs`, as the local executor's are. */
1256
1224
  function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): string {
1257
- const quoted =
1258
- req.forwardArgs.length === 0
1259
- ? req.command
1260
- : `${req.command} ${req.forwardArgs.map((a) => `'${a.replaceAll("'", `'\\''`)}'`).join(' ')}`
1225
+ const command = withForwardArgs(req.command, req.forwardArgs)
1261
1226
  // A remote action gets NO PATH from this machine — sending one would put a
1262
1227
  // host path in the action digest and split every laptop from every runner.
1263
1228
  // But a task's command is normally a package binary (`oxlint`, `tsc`), and
@@ -1285,7 +1250,7 @@ function fullCommand(req: ExecuteRequest, cdInto: string, projectRel: string): s
1285
1250
  return (
1286
1251
  `VX_ROOT=${root}; ${cd}` +
1287
1252
  `export PATH="$VX_ROOT/node_modules/.bin:$PWD/node_modules/.bin:$PATH"; ` +
1288
- quoted
1253
+ command
1289
1254
  )
1290
1255
  }
1291
1256
 
package/src/merkle.ts CHANGED
@@ -828,7 +828,7 @@ function decodeSymlinkNode(buf: Uint8Array): SymlinkNode {
828
828
  return sl
829
829
  }
830
830
 
831
- function decodeDigestBytes(buf: Uint8Array): Digest {
831
+ export function decodeDigestBytes(buf: Uint8Array): Digest {
832
832
  const d: Digest = { hash: '', size_bytes: 0 }
833
833
  let i = 0
834
834
  while (i < buf.length) {
@@ -850,7 +850,7 @@ function decodeDigestBytes(buf: Uint8Array): Digest {
850
850
  return d
851
851
  }
852
852
 
853
- function readVarintAt(buf: Uint8Array, at: number): [number, number] {
853
+ export function readVarintAt(buf: Uint8Array, at: number): [number, number] {
854
854
  let result = 0
855
855
  let low = 0
856
856
  let shift = 0
package/src/wire.ts CHANGED
@@ -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: