@beam-network/sdk 0.6.0 → 0.7.0
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 +30 -3
- package/dist/client.d.ts +6 -1
- package/dist/client.js +220 -72
- package/dist/models.d.ts +75 -0
- package/dist/nats-control.d.ts +13 -2
- package/dist/nats-control.js +117 -30
- package/dist/performance.d.ts +39 -0
- package/dist/performance.js +124 -0
- package/dist/provider-signing.d.ts +20 -0
- package/dist/provider-signing.js +55 -14
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SdkPerformanceCollector, SourceSignatureHistory, currentSdkPerformance, measureSdkPhase } from "./performance.js";
|
|
2
|
+
import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, releaseProviderClients, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, signSourceChunk, boundedGrantExpiry, isHuggingFaceProvider } from "./provider-signing.js";
|
|
2
3
|
import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
|
|
3
4
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
4
5
|
export class BeamRouteRecoveryPendingError extends Error {
|
|
@@ -53,11 +54,15 @@ export class BeamClient {
|
|
|
53
54
|
routeSigningConcurrency;
|
|
54
55
|
routeSigningConcurrencyOverridden;
|
|
55
56
|
multipartControlConcurrency;
|
|
57
|
+
onDiagnostics;
|
|
58
|
+
diagnosticPending = false;
|
|
56
59
|
recoverySigners = new Map();
|
|
57
60
|
integrityAuditSigners = new Map();
|
|
61
|
+
integrityGrantCache = new Map();
|
|
58
62
|
integrityAuditSubmissions = new Map();
|
|
59
63
|
huggingFaceUploads = new Map();
|
|
60
64
|
constructor(options = {}) {
|
|
65
|
+
this.onDiagnostics = options.onDiagnostics;
|
|
61
66
|
if (!options.apiKey?.trim()) {
|
|
62
67
|
throw new Error("apiKey is required.");
|
|
63
68
|
}
|
|
@@ -237,7 +242,7 @@ export class BeamClient {
|
|
|
237
242
|
};
|
|
238
243
|
const routeStreamLock = new AsyncMutex();
|
|
239
244
|
const releaseInitialStream = await routeStreamLock.acquire();
|
|
240
|
-
|
|
245
|
+
const recoveryLease = {
|
|
241
246
|
transferId,
|
|
242
247
|
planFingerprint: input.planFingerprint,
|
|
243
248
|
coordinateChecksum: input.coordinateChecksum,
|
|
@@ -249,19 +254,20 @@ export class BeamClient {
|
|
|
249
254
|
throw new Error(attached.error ?? attached.message ?? "route replay failed");
|
|
250
255
|
});
|
|
251
256
|
}
|
|
252
|
-
}
|
|
257
|
+
};
|
|
258
|
+
this.control.registerRecoveryLease(recoveryLease);
|
|
253
259
|
try {
|
|
254
260
|
const attached = await streamRoutes(input.chunkRoutes, input.multipartGroupManifest, input.routeGenerationId, input.urlsExpiresAt);
|
|
255
261
|
if (!attached.success)
|
|
256
|
-
this.control.releaseRecoveryLease(transferId);
|
|
262
|
+
this.control.releaseRecoveryLease(transferId, recoveryLease);
|
|
257
263
|
return attached;
|
|
258
264
|
}
|
|
259
265
|
catch (error) {
|
|
260
266
|
if (isRecoverableRouteStreamError(error)) {
|
|
261
|
-
this.control.continueRecoveryLease(transferId);
|
|
267
|
+
this.control.continueRecoveryLease(transferId, recoveryLease);
|
|
262
268
|
throw new BeamRouteRecoveryPendingError(transferId, error);
|
|
263
269
|
}
|
|
264
|
-
this.control.releaseRecoveryLease(transferId);
|
|
270
|
+
this.control.releaseRecoveryLease(transferId, recoveryLease);
|
|
265
271
|
throw error;
|
|
266
272
|
}
|
|
267
273
|
finally {
|
|
@@ -294,7 +300,8 @@ export class BeamClient {
|
|
|
294
300
|
destinationsById.set(preparedDestination.destination_id, destination);
|
|
295
301
|
});
|
|
296
302
|
await input.throwIfCancelled?.();
|
|
297
|
-
const
|
|
303
|
+
const discoveryTelemetry = new SdkPerformanceCollector();
|
|
304
|
+
const preparedSources = await discoveryTelemetry.measure("sdk.discovery", () => Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal }))));
|
|
298
305
|
await input.throwIfCancelled?.();
|
|
299
306
|
const huggingFace = await this.planHuggingFaceUploads({
|
|
300
307
|
sources: retainedRecoveryInput.sources,
|
|
@@ -345,10 +352,18 @@ export class BeamClient {
|
|
|
345
352
|
const assertOwnership = () => input.signal?.throwIfAborted();
|
|
346
353
|
const routeStreamLock = new AsyncMutex();
|
|
347
354
|
const releaseInitialStream = await routeStreamLock.acquire();
|
|
355
|
+
const sourceSignatureHistory = new SourceSignatureHistory(!resume);
|
|
348
356
|
const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
|
|
357
|
+
const telemetry = new SdkPerformanceCollector(sourceSignatureHistory);
|
|
358
|
+
telemetry.seedDiscovery(discoveryTelemetry);
|
|
359
|
+
telemetry.gauge("signing_configured_limit", this.routeSigningConcurrency);
|
|
360
|
+
telemetry.gauge("multipart_configured_limit", this.multipartControlConcurrency);
|
|
361
|
+
telemetry.gauge("signing_limit", this.routeSigningConcurrency);
|
|
362
|
+
telemetry.gauge("multipart_limit", this.multipartControlConcurrency);
|
|
349
363
|
const pendingRoutes = new Set();
|
|
350
364
|
let signingConcurrency = this.routeSigningConcurrency;
|
|
351
365
|
let signedInWindow = 0;
|
|
366
|
+
let activeSigning = 0;
|
|
352
367
|
let signingWindowStartedAt = performance.now();
|
|
353
368
|
let routeStream = null;
|
|
354
369
|
let routeStreamBeginAttempted = false;
|
|
@@ -376,6 +391,15 @@ export class BeamClient {
|
|
|
376
391
|
signedUrlFlow: requestedSignedUrlFlow
|
|
377
392
|
});
|
|
378
393
|
routeStream = new RouteStreamSender(this.control, {
|
|
394
|
+
telemetry,
|
|
395
|
+
onDiagnostics: (summary) => {
|
|
396
|
+
if (!this.onDiagnostics || this.diagnosticPending)
|
|
397
|
+
return;
|
|
398
|
+
this.diagnosticPending = true;
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
void Promise.resolve().then(() => this.onDiagnostics?.(summary)).catch(() => { }).finally(() => { this.diagnosticPending = false; });
|
|
401
|
+
}, 0);
|
|
402
|
+
},
|
|
379
403
|
streamId,
|
|
380
404
|
transferId: prepared.transfer_id,
|
|
381
405
|
routeGenerationId,
|
|
@@ -388,7 +412,7 @@ export class BeamClient {
|
|
|
388
412
|
routeStreamBeginAttempted = true;
|
|
389
413
|
await routeStream.begin();
|
|
390
414
|
multipartGroupWaiters = createMultipartGroupWaiters(prepared, destinationsById);
|
|
391
|
-
multipartManifestTask = createMultipartGroupManifest({
|
|
415
|
+
multipartManifestTask = telemetry.measure("sdk.multipart_create", () => createMultipartGroupManifest({
|
|
392
416
|
prepared,
|
|
393
417
|
destinationsById,
|
|
394
418
|
multipartUploads,
|
|
@@ -400,14 +424,14 @@ export class BeamClient {
|
|
|
400
424
|
assertOwnership();
|
|
401
425
|
recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
|
|
402
426
|
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
403
|
-
await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
|
|
427
|
+
await telemetry.measure("sdk.multipart_callback", async () => input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest)));
|
|
404
428
|
await routeStream.addManifestGroups([state.manifest]);
|
|
405
429
|
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
406
430
|
},
|
|
407
431
|
onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
|
|
408
|
-
});
|
|
432
|
+
}));
|
|
409
433
|
const streamNextCompletedRoute = async () => {
|
|
410
|
-
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
434
|
+
const settled = await telemetry.measure("sdk.producer_wait", () => Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route })))));
|
|
411
435
|
pendingRoutes.delete(settled.pending);
|
|
412
436
|
assertOwnership();
|
|
413
437
|
await routeStream.addRoute(settled.route);
|
|
@@ -416,6 +440,8 @@ export class BeamClient {
|
|
|
416
440
|
const elapsedMs = performance.now() - signingWindowStartedAt;
|
|
417
441
|
if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
|
|
418
442
|
signingConcurrency = Math.min(256, signingConcurrency * 2);
|
|
443
|
+
telemetry.increment("concurrency_changes");
|
|
444
|
+
telemetry.gauge("signing_limit", signingConcurrency);
|
|
419
445
|
}
|
|
420
446
|
signedInWindow = 0;
|
|
421
447
|
signingWindowStartedAt = performance.now();
|
|
@@ -423,6 +449,11 @@ export class BeamClient {
|
|
|
423
449
|
};
|
|
424
450
|
for (const chunk of materializePlanChunks(prepared.plan_descriptor, prepared.transfer_id)) {
|
|
425
451
|
await throwIfCancelled?.(prepared.transfer_id);
|
|
452
|
+
// This promise belongs to this chunk and signing generation only. Its
|
|
453
|
+
// immutable credential/range identity and actual expiry cannot be renewed.
|
|
454
|
+
let sourceUses = 0;
|
|
455
|
+
const sourceGrant = telemetry.measure("sdk.source_signing", () => signSourceChunk({ source: sourcesById.get(chunk.source_id), fallbackUrl: chunk.source_url, chunk, expiresIn, fetchImpl: this.fetchImpl })).then(grant => { telemetry.sourceCreated(chunk.chunk_index); return grant; });
|
|
456
|
+
void sourceGrant.catch(() => { });
|
|
426
457
|
for (const target of chunk.destinations) {
|
|
427
458
|
const pendingRoute = (async () => {
|
|
428
459
|
await throwIfCancelled?.(prepared.transfer_id);
|
|
@@ -439,13 +470,16 @@ export class BeamClient {
|
|
|
439
470
|
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
440
471
|
const upload = isDirectPutDestination(destination)
|
|
441
472
|
? undefined
|
|
442
|
-
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
473
|
+
: await telemetry.measure("sdk.multipart_ready_wait", async () => multipartGroupWaiters.get(multipartGroupId)?.promise);
|
|
443
474
|
if (!isDirectPutDestination(destination) && !upload)
|
|
444
475
|
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
445
|
-
|
|
476
|
+
telemetry.gauge("signing_active_peak", ++activeSigning);
|
|
477
|
+
return telemetry.measure("sdk.signing", () => this.signProviderRoute({
|
|
446
478
|
chunk,
|
|
447
479
|
target,
|
|
448
480
|
source,
|
|
481
|
+
sourceGrant: telemetry.measure("sdk.source_grant_wait", () => sourceGrant).then(grant => { if (sourceUses++ > 0)
|
|
482
|
+
telemetry.counters.source_reuses++; return grant; }),
|
|
449
483
|
destination,
|
|
450
484
|
expiresIn,
|
|
451
485
|
...(upload ? { upload } : {}),
|
|
@@ -456,11 +490,12 @@ export class BeamClient {
|
|
|
456
490
|
partNumber: typeof metadata.part_number === "number"
|
|
457
491
|
? metadata.part_number
|
|
458
492
|
: multipartPartNumber(chunk.source_chunk_index),
|
|
459
|
-
});
|
|
493
|
+
})).finally(() => { activeSigning--; });
|
|
460
494
|
})();
|
|
461
495
|
pendingRoutes.add(pendingRoute);
|
|
496
|
+
telemetry.gauge("signing_pending_peak", pendingRoutes.size);
|
|
462
497
|
if (pendingRoutes.size >= signingConcurrency)
|
|
463
|
-
await streamNextCompletedRoute
|
|
498
|
+
await telemetry.measure("sdk.signing_queue", streamNextCompletedRoute);
|
|
464
499
|
}
|
|
465
500
|
}
|
|
466
501
|
while (pendingRoutes.size)
|
|
@@ -475,8 +510,8 @@ export class BeamClient {
|
|
|
475
510
|
}
|
|
476
511
|
catch (error) {
|
|
477
512
|
if (input.signal?.aborted) {
|
|
478
|
-
this.stopRecoverySigner(prepared.transfer_id);
|
|
479
|
-
this.control.releaseRecoveryLease(prepared.transfer_id);
|
|
513
|
+
this.stopRecoverySigner(prepared.transfer_id, recoveryLease);
|
|
514
|
+
this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
480
515
|
await Promise.allSettled(pendingRoutes);
|
|
481
516
|
if (multipartManifestTask)
|
|
482
517
|
await Promise.allSettled([multipartManifestTask]);
|
|
@@ -489,10 +524,10 @@ export class BeamClient {
|
|
|
489
524
|
await Promise.allSettled([multipartManifestTask]);
|
|
490
525
|
if (!recoveryReplay) {
|
|
491
526
|
if (foregroundCancelled) {
|
|
492
|
-
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
527
|
+
this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
493
528
|
}
|
|
494
529
|
else if (isRecoverableRouteStreamError(error)) {
|
|
495
|
-
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
530
|
+
this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
496
531
|
throw new BeamRouteRecoveryPendingError(prepared.transfer_id, error);
|
|
497
532
|
}
|
|
498
533
|
else {
|
|
@@ -510,27 +545,16 @@ export class BeamClient {
|
|
|
510
545
|
// The retained recovery input owns the same destination objects used by
|
|
511
546
|
// multipart cleanup. Releasing it earlier scrubs their scoped credentials
|
|
512
547
|
// before abortMultipartUpload can clean up already-created uploads.
|
|
513
|
-
this.control.releaseRecoveryLease(prepared.transfer_id);
|
|
548
|
+
this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
514
549
|
}
|
|
515
550
|
}
|
|
516
551
|
}
|
|
517
552
|
throw error;
|
|
518
553
|
}
|
|
519
554
|
};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
destinationsById,
|
|
524
|
-
multipartUploads: recoveryMultipartUploads,
|
|
525
|
-
signal: input.signal,
|
|
526
|
-
expiresIn
|
|
527
|
-
});
|
|
528
|
-
const stopOwnedRecovery = () => {
|
|
529
|
-
this.stopRecoverySigner(prepared.transfer_id);
|
|
530
|
-
this.control.releaseRecoveryLease(prepared.transfer_id);
|
|
531
|
-
};
|
|
532
|
-
input.signal?.addEventListener("abort", stopOwnedRecovery, { once: true });
|
|
533
|
-
this.control.registerRecoveryLease({
|
|
555
|
+
// The lease object is this owner's identity: releases, recovery requests, and
|
|
556
|
+
// signer shutdowns below only act while it is still the registered lease.
|
|
557
|
+
const recoveryLease = {
|
|
534
558
|
transferId: prepared.transfer_id,
|
|
535
559
|
planFingerprint: prepared.plan_fingerprint,
|
|
536
560
|
coordinateChecksum: prepared.coordinate_checksum,
|
|
@@ -539,23 +563,39 @@ export class BeamClient {
|
|
|
539
563
|
},
|
|
540
564
|
disposeSecrets: () => {
|
|
541
565
|
input.signal?.removeEventListener("abort", stopOwnedRecovery);
|
|
566
|
+
releaseProviderClients([...retainedRecoveryInput.sources, ...retainedRecoveryInput.destinations]);
|
|
542
567
|
clearRecoverySecrets(retainedRecoveryInput);
|
|
543
568
|
}
|
|
569
|
+
};
|
|
570
|
+
await this.startProviderRouteRecoverySigner({
|
|
571
|
+
owner: recoveryLease,
|
|
572
|
+
prepared,
|
|
573
|
+
sourcesById,
|
|
574
|
+
destinationsById,
|
|
575
|
+
multipartUploads: recoveryMultipartUploads,
|
|
576
|
+
signal: input.signal,
|
|
577
|
+
expiresIn
|
|
544
578
|
});
|
|
579
|
+
const stopOwnedRecovery = () => {
|
|
580
|
+
this.stopRecoverySigner(prepared.transfer_id, recoveryLease);
|
|
581
|
+
this.control.releaseRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
582
|
+
};
|
|
583
|
+
input.signal?.addEventListener("abort", stopOwnedRecovery, { once: true });
|
|
584
|
+
this.control.registerRecoveryLease(recoveryLease);
|
|
545
585
|
try {
|
|
546
586
|
try {
|
|
547
587
|
assertOwnership();
|
|
548
588
|
await input.onPrepared?.(prepared);
|
|
549
589
|
}
|
|
550
590
|
catch (error) {
|
|
551
|
-
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
591
|
+
this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
552
592
|
throw error;
|
|
553
593
|
}
|
|
554
594
|
try {
|
|
555
595
|
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
556
596
|
}
|
|
557
597
|
catch (error) {
|
|
558
|
-
this.control.continueRecoveryLease(prepared.transfer_id);
|
|
598
|
+
this.control.continueRecoveryLease(prepared.transfer_id, recoveryLease);
|
|
559
599
|
throw error;
|
|
560
600
|
}
|
|
561
601
|
await streamPreparedRoutes(prepared.route_generation_id, false);
|
|
@@ -740,6 +780,7 @@ export class BeamClient {
|
|
|
740
780
|
metadata: directPutRouteMetadata(input.target.metadata)
|
|
741
781
|
},
|
|
742
782
|
source: input.source,
|
|
783
|
+
sourceGrant: input.sourceGrant,
|
|
743
784
|
destination: input.destination,
|
|
744
785
|
expiresIn: input.expiresIn,
|
|
745
786
|
destUrl,
|
|
@@ -751,6 +792,7 @@ export class BeamClient {
|
|
|
751
792
|
chunk: input.chunk,
|
|
752
793
|
target: { ...input.target, metadata: partRouteMetadata(input.target.metadata) },
|
|
753
794
|
source: input.source,
|
|
795
|
+
sourceGrant: input.sourceGrant,
|
|
754
796
|
destination: input.destination,
|
|
755
797
|
expiresIn: input.expiresIn,
|
|
756
798
|
fetchImpl: this.fetchImpl
|
|
@@ -768,6 +810,7 @@ export class BeamClient {
|
|
|
768
810
|
metadata: partRouteMetadata(input.target.metadata)
|
|
769
811
|
},
|
|
770
812
|
source: input.source,
|
|
813
|
+
sourceGrant: input.sourceGrant,
|
|
771
814
|
destination: input.destination,
|
|
772
815
|
expiresIn: input.expiresIn,
|
|
773
816
|
uploadId: upload.uploadId,
|
|
@@ -791,6 +834,7 @@ export class BeamClient {
|
|
|
791
834
|
input.signal?.throwIfAborted();
|
|
792
835
|
if (payload.route_generation_id.length === 0)
|
|
793
836
|
throw new Error("route recovery generation is required");
|
|
837
|
+
const sourceGrants = new Map();
|
|
794
838
|
const routes = await mapOrderedWithConcurrency(payload.chunks, this.routeSigningConcurrency, async (requested) => {
|
|
795
839
|
input.signal?.throwIfAborted();
|
|
796
840
|
if (requested.route_generation_id !== payload.route_generation_id) {
|
|
@@ -821,11 +865,22 @@ export class BeamClient {
|
|
|
821
865
|
multipartUploads: input.multipartUploads,
|
|
822
866
|
expiresIn: input.expiresIn
|
|
823
867
|
});
|
|
868
|
+
const sourceKey = JSON.stringify([chunk.source_id, chunk.chunk_index]);
|
|
869
|
+
let sourceGrant = sourceGrants.get(sourceKey);
|
|
870
|
+
if (!sourceGrant) {
|
|
871
|
+
sourceGrant = signSourceChunk({ source, chunk, fallbackUrl: chunk.source_url, expiresIn: input.expiresIn, fetchImpl: this.fetchImpl });
|
|
872
|
+
sourceGrants.set(sourceKey, sourceGrant);
|
|
873
|
+
void sourceGrant.catch(() => { });
|
|
874
|
+
}
|
|
824
875
|
const route = await this.signProviderRoute({
|
|
876
|
+
sourceGrant,
|
|
825
877
|
chunk,
|
|
826
878
|
target: {
|
|
827
879
|
...target,
|
|
828
|
-
|
|
880
|
+
// Direct-PUT destinations (Hippius, Hugging Face) keep the planned
|
|
881
|
+
// per-chunk key exactly as initial materialization does; only
|
|
882
|
+
// multipart destinations sign parts against the final object key.
|
|
883
|
+
object_key: isDirectPutDestination(destination) ? target.object_key : requested.final_object_key,
|
|
829
884
|
metadata: {
|
|
830
885
|
...(target.metadata ?? {}),
|
|
831
886
|
...(requested.destination_metadata ?? {}),
|
|
@@ -861,16 +916,33 @@ export class BeamClient {
|
|
|
861
916
|
chunk_routes: routes
|
|
862
917
|
};
|
|
863
918
|
});
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
919
|
+
if (input.signal?.aborted) {
|
|
920
|
+
stop();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.stopRecoverySigner(input.prepared.transfer_id);
|
|
924
|
+
this.integrityAuditSigners.set(input.prepared.transfer_id, (challenge) => this.buildProviderIntegrityAuditGrants({
|
|
925
|
+
challenge, prepared: input.prepared, sourcesById: input.sourcesById, destinationsById: input.destinationsById, expiresIn: input.expiresIn
|
|
926
|
+
}));
|
|
927
|
+
const registration = { owner: input.owner, stop };
|
|
928
|
+
this.recoverySigners.set(input.prepared.transfer_id, registration);
|
|
929
|
+
try {
|
|
930
|
+
const stopIntegrity = await this.control.serveIntegritySigner(input.prepared.transfer_id, challenge => {
|
|
931
|
+
input.signal?.throwIfAborted();
|
|
932
|
+
return this.signedIntegrityGrants(challenge);
|
|
872
933
|
});
|
|
873
|
-
|
|
934
|
+
if (input.signal?.aborted || this.recoverySigners.get(input.prepared.transfer_id) !== registration) {
|
|
935
|
+
stop();
|
|
936
|
+
stopIntegrity();
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
registration.stop = () => { stop(); stopIntegrity(); };
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
// Status-driven signing remains available if the optional subscription fails.
|
|
943
|
+
if (input.signal?.aborted)
|
|
944
|
+
this.stopRecoverySigner(input.prepared.transfer_id, input.owner);
|
|
945
|
+
}
|
|
874
946
|
}
|
|
875
947
|
async recoveryMultipartUploadState(input) {
|
|
876
948
|
const existing = input.multipartUploads.get(input.requested.multipart_group_id);
|
|
@@ -937,21 +1009,56 @@ export class BeamClient {
|
|
|
937
1009
|
if (!signer)
|
|
938
1010
|
throw new Error("integrity audit signer unavailable");
|
|
939
1011
|
const existing = this.integrityAuditSubmissions.get(challenge.audit_id);
|
|
940
|
-
|
|
1012
|
+
const cached = this.integrityGrantCache.get(challenge.audit_id);
|
|
1013
|
+
if (cached && cached.fingerprint !== JSON.stringify(challenge))
|
|
1014
|
+
throw new Error("integrity audit identity changed");
|
|
1015
|
+
if (existing && cached && cached.expiresAt > Date.now() + 30_000) {
|
|
941
1016
|
await existing;
|
|
942
1017
|
return;
|
|
943
1018
|
}
|
|
944
|
-
const submission =
|
|
1019
|
+
const submission = this.signedIntegrityGrants(challenge).then(async (payload) => {
|
|
1020
|
+
const result = await this.control.request("transfer.integrity_audit_grants", payload, {
|
|
1021
|
+
transferId: challenge.transfer_id, idempotencyKey: `transfer:${challenge.transfer_id}:integrity-audit:${challenge.audit_id}:${payload.submitted_at}`
|
|
1022
|
+
});
|
|
1023
|
+
if (result.published !== true)
|
|
1024
|
+
throw new Error("integrity audit delivery unavailable");
|
|
1025
|
+
});
|
|
945
1026
|
this.integrityAuditSubmissions.set(challenge.audit_id, submission);
|
|
946
1027
|
try {
|
|
947
1028
|
await submission;
|
|
948
1029
|
}
|
|
949
1030
|
catch (error) {
|
|
950
|
-
this.integrityAuditSubmissions.
|
|
1031
|
+
if (this.integrityAuditSubmissions.get(challenge.audit_id) === submission)
|
|
1032
|
+
this.integrityAuditSubmissions.delete(challenge.audit_id);
|
|
951
1033
|
throw error;
|
|
952
1034
|
}
|
|
953
1035
|
}
|
|
954
|
-
|
|
1036
|
+
signedIntegrityGrants(challenge) {
|
|
1037
|
+
const fingerprint = JSON.stringify(challenge);
|
|
1038
|
+
const cached = this.integrityGrantCache.get(challenge.audit_id);
|
|
1039
|
+
if (cached && cached.fingerprint !== fingerprint)
|
|
1040
|
+
return Promise.reject(new Error("integrity audit identity changed"));
|
|
1041
|
+
if (cached && cached.expiresAt > Date.now() + 30_000)
|
|
1042
|
+
return cached.payload;
|
|
1043
|
+
if (!cached && this.integrityGrantCache.size >= 1024)
|
|
1044
|
+
return Promise.reject(new Error("integrity signer capacity unavailable"));
|
|
1045
|
+
const signer = this.integrityAuditSigners.get(challenge.transfer_id);
|
|
1046
|
+
if (!signer)
|
|
1047
|
+
return Promise.reject(new Error("integrity audit signer unavailable"));
|
|
1048
|
+
this.integrityAuditSubmissions.delete(challenge.audit_id);
|
|
1049
|
+
const entry = { transferId: challenge.transfer_id, expiresAt: 0, fingerprint, payload: Promise.resolve({}) };
|
|
1050
|
+
entry.payload = signer(challenge).then(payload => {
|
|
1051
|
+
const chunks = payload.chunks;
|
|
1052
|
+
entry.expiresAt = Math.min(...chunks.flatMap(chunk => [Date.parse(chunk.source.expires_at), Date.parse(chunk.destination.expires_at)]));
|
|
1053
|
+
return payload;
|
|
1054
|
+
}).catch(error => { if (this.integrityGrantCache.get(challenge.audit_id) === entry)
|
|
1055
|
+
this.integrityGrantCache.delete(challenge.audit_id); throw error; });
|
|
1056
|
+
// Pending grants are shared too. Actual expiry replaces this sentinel on completion.
|
|
1057
|
+
entry.expiresAt = Infinity;
|
|
1058
|
+
this.integrityGrantCache.set(challenge.audit_id, entry);
|
|
1059
|
+
return entry.payload;
|
|
1060
|
+
}
|
|
1061
|
+
async buildProviderIntegrityAuditGrants(input) {
|
|
955
1062
|
if (input.challenge.transfer_id !== input.prepared.transfer_id) {
|
|
956
1063
|
throw new Error("integrity audit challenge transfer mismatch");
|
|
957
1064
|
}
|
|
@@ -978,6 +1085,7 @@ export class BeamClient {
|
|
|
978
1085
|
}
|
|
979
1086
|
const sourceEtag = plannedSource?.metadata?.etag;
|
|
980
1087
|
const sourceVersionId = plannedSource?.metadata?.version_id;
|
|
1088
|
+
const grantExpiresAt = new Date(Math.floor(Date.now() / 1000) * 1000 + input.expiresIn * 1000).toISOString();
|
|
981
1089
|
const [sourceGrant, destinationGrant] = await Promise.all([
|
|
982
1090
|
signSourceReadRange({
|
|
983
1091
|
source,
|
|
@@ -998,31 +1106,30 @@ export class BeamClient {
|
|
|
998
1106
|
fetchImpl: this.fetchImpl
|
|
999
1107
|
})
|
|
1000
1108
|
]);
|
|
1001
|
-
const grantExpiresAt = expiresAtIso(input.expiresIn);
|
|
1002
1109
|
const { orchestrator_id: _orchestratorId, orchestrator_hotkey: _orchestratorHotkey, worker_id: _workerId, ...grantChunk } = chunk;
|
|
1003
1110
|
return {
|
|
1004
1111
|
...grantChunk,
|
|
1005
|
-
source: { ...sourceGrant, expires_at: grantExpiresAt },
|
|
1006
|
-
destination: { ...destinationGrant, expires_at: grantExpiresAt }
|
|
1112
|
+
source: { ...sourceGrant, expires_at: boundedGrantExpiry(sourceGrant.url, grantExpiresAt) },
|
|
1113
|
+
destination: { ...destinationGrant, expires_at: boundedGrantExpiry(destinationGrant.url, grantExpiresAt) }
|
|
1007
1114
|
};
|
|
1008
1115
|
});
|
|
1009
|
-
|
|
1010
|
-
transfer_id: input.challenge.transfer_id,
|
|
1011
|
-
audit_id: input.challenge.audit_id,
|
|
1012
|
-
submitted_at: submittedAt,
|
|
1013
|
-
chunks
|
|
1014
|
-
}, {
|
|
1015
|
-
transferId: input.challenge.transfer_id,
|
|
1016
|
-
idempotencyKey: `transfer:${input.challenge.transfer_id}:integrity-audit:${input.challenge.audit_id}`
|
|
1017
|
-
});
|
|
1116
|
+
return { transfer_id: input.challenge.transfer_id, audit_id: input.challenge.audit_id, submitted_at: submittedAt, chunks };
|
|
1018
1117
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1118
|
+
/** Stop a transfer's recovery and integrity signers; with `owner`, only if that owner installed them. */
|
|
1119
|
+
stopRecoverySigner(transferId, owner) {
|
|
1120
|
+
const signer = this.recoverySigners.get(transferId);
|
|
1121
|
+
if (owner && signer?.owner !== owner)
|
|
1122
|
+
return;
|
|
1123
|
+
if (signer) {
|
|
1124
|
+
signer.stop();
|
|
1023
1125
|
this.recoverySigners.delete(transferId);
|
|
1024
1126
|
}
|
|
1025
1127
|
this.integrityAuditSigners.delete(transferId);
|
|
1128
|
+
for (const [auditId, entry] of this.integrityGrantCache)
|
|
1129
|
+
if (entry.transferId === transferId) {
|
|
1130
|
+
this.integrityGrantCache.delete(auditId);
|
|
1131
|
+
this.integrityAuditSubmissions.delete(auditId);
|
|
1132
|
+
}
|
|
1026
1133
|
}
|
|
1027
1134
|
stopAllRecoverySigners() {
|
|
1028
1135
|
for (const transferId of [...this.recoverySigners.keys()])
|
|
@@ -1118,7 +1225,7 @@ export class BeamClient {
|
|
|
1118
1225
|
}
|
|
1119
1226
|
}
|
|
1120
1227
|
}
|
|
1121
|
-
const ROUTE_STREAM_BATCH_ROUTES =
|
|
1228
|
+
const ROUTE_STREAM_BATCH_ROUTES = 1_024;
|
|
1122
1229
|
class AsyncMutex {
|
|
1123
1230
|
tail = Promise.resolve();
|
|
1124
1231
|
async acquire() {
|
|
@@ -1143,6 +1250,8 @@ class AsyncMutex {
|
|
|
1143
1250
|
class RouteStreamSender {
|
|
1144
1251
|
control;
|
|
1145
1252
|
options;
|
|
1253
|
+
started = performance.now();
|
|
1254
|
+
telemetry;
|
|
1146
1255
|
streamId;
|
|
1147
1256
|
checksum = new RouteKeysChecksum();
|
|
1148
1257
|
batch = [];
|
|
@@ -1151,12 +1260,15 @@ class RouteStreamSender {
|
|
|
1151
1260
|
routeCount = 0;
|
|
1152
1261
|
sendTail = Promise.resolve();
|
|
1153
1262
|
sendError = null;
|
|
1263
|
+
sending = false;
|
|
1154
1264
|
constructor(control, options) {
|
|
1155
1265
|
this.control = control;
|
|
1156
1266
|
this.options = options;
|
|
1157
1267
|
this.streamId = options.streamId;
|
|
1268
|
+
this.telemetry = options.telemetry ?? new SdkPerformanceCollector();
|
|
1158
1269
|
}
|
|
1159
1270
|
async begin() {
|
|
1271
|
+
this.telemetry.startDelaySampling();
|
|
1160
1272
|
await this.control.request("transfer.route_stream.begin", compact({
|
|
1161
1273
|
transfer_id: this.options.transferId,
|
|
1162
1274
|
route_generation_id: this.options.routeGenerationId,
|
|
@@ -1173,6 +1285,7 @@ class RouteStreamSender {
|
|
|
1173
1285
|
});
|
|
1174
1286
|
}
|
|
1175
1287
|
async addManifestGroups(groups) {
|
|
1288
|
+
const started = performance.now();
|
|
1176
1289
|
if (!groups.length)
|
|
1177
1290
|
return;
|
|
1178
1291
|
await Promise.all(groups.map(async (group) => {
|
|
@@ -1188,8 +1301,12 @@ class RouteStreamSender {
|
|
|
1188
1301
|
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:manifest:${identity}`
|
|
1189
1302
|
});
|
|
1190
1303
|
}));
|
|
1304
|
+
this.telemetry.observe("sdk.manifest_ack", performance.now() - started);
|
|
1305
|
+
this.telemetry.mark("first_manifest_ms");
|
|
1191
1306
|
}
|
|
1192
1307
|
async addRoute(route) {
|
|
1308
|
+
this.telemetry.mark("first_route_ms");
|
|
1309
|
+
const assemblyStarted = performance.now();
|
|
1193
1310
|
const deliveryIndex = signedRouteDeliveryIndex(route);
|
|
1194
1311
|
if (deliveryIndex === undefined) {
|
|
1195
1312
|
throw new Error("route delivery_index is required");
|
|
@@ -1203,6 +1320,8 @@ class RouteStreamSender {
|
|
|
1203
1320
|
this.seenDeliveryIndices.add(deliveryIndex);
|
|
1204
1321
|
this.routeCount += 1;
|
|
1205
1322
|
this.batch.push({ ...route, delivery_index: deliveryIndex });
|
|
1323
|
+
this.telemetry.gauge("buffered_batches_peak", 1 + Number(this.sending));
|
|
1324
|
+
this.telemetry.observe("sdk.route_assembly", performance.now() - assemblyStarted);
|
|
1206
1325
|
if (this.batch.length >= ROUTE_STREAM_BATCH_ROUTES) {
|
|
1207
1326
|
await this.enqueueFlush();
|
|
1208
1327
|
}
|
|
@@ -1212,11 +1331,17 @@ class RouteStreamSender {
|
|
|
1212
1331
|
this.seenDeliveryIndices.size !== this.options.totalRoutes) {
|
|
1213
1332
|
throw new Error(`route stream has incomplete or duplicate delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
|
|
1214
1333
|
}
|
|
1334
|
+
this.telemetry.mark("final_flush_ms");
|
|
1215
1335
|
await this.enqueueFlush();
|
|
1216
1336
|
await this.sendTail;
|
|
1217
1337
|
if (this.sendError)
|
|
1218
1338
|
throw this.sendError;
|
|
1339
|
+
this.telemetry.observe("sdk.preparation", performance.now() - this.started);
|
|
1340
|
+
this.telemetry.finish();
|
|
1341
|
+
const sdkPerformance = this.telemetry.snapshot(this.control.supportsPerformanceV2?.(this.options.transferId) ?? false);
|
|
1342
|
+
this.options.onDiagnostics?.(sdkPerformance);
|
|
1219
1343
|
return this.control.request("transfer.route_stream.complete", {
|
|
1344
|
+
sdk_performance: sdkPerformance,
|
|
1220
1345
|
transfer_id: this.options.transferId,
|
|
1221
1346
|
route_generation_id: this.options.routeGenerationId,
|
|
1222
1347
|
stream_id: this.streamId,
|
|
@@ -1231,10 +1356,11 @@ class RouteStreamSender {
|
|
|
1231
1356
|
async enqueueFlush() {
|
|
1232
1357
|
if (!this.batch.length)
|
|
1233
1358
|
return;
|
|
1234
|
-
await this.sendTail;
|
|
1359
|
+
await this.telemetry.measure("sdk.buffer_wait", () => this.sendTail);
|
|
1235
1360
|
if (this.sendError)
|
|
1236
1361
|
throw this.sendError;
|
|
1237
1362
|
const routes = this.batch.splice(0, this.batch.length);
|
|
1363
|
+
const encodingStarted = performance.now();
|
|
1238
1364
|
const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
|
|
1239
1365
|
transfer_id: this.options.transferId,
|
|
1240
1366
|
route_generation_id: this.options.routeGenerationId,
|
|
@@ -1242,20 +1368,29 @@ class RouteStreamSender {
|
|
|
1242
1368
|
batch_id: `${this.streamId}:estimate`,
|
|
1243
1369
|
batch_index: this.batchIndex
|
|
1244
1370
|
}, routes);
|
|
1371
|
+
this.telemetry.observe("sdk.batch_encode", performance.now() - encodingStarted);
|
|
1372
|
+
this.sending = true;
|
|
1245
1373
|
for (const chunk of chunks) {
|
|
1374
|
+
const queuedAt = performance.now();
|
|
1246
1375
|
const batchIndex = this.batchIndex;
|
|
1247
1376
|
this.batchIndex += 1;
|
|
1248
1377
|
this.sendTail = this.sendTail.then(async () => {
|
|
1378
|
+
this.telemetry.observe("sdk.batch_queue", performance.now() - queuedAt);
|
|
1249
1379
|
if (this.sendError)
|
|
1250
1380
|
return;
|
|
1251
1381
|
try {
|
|
1382
|
+
const checksumStarted = performance.now();
|
|
1252
1383
|
const chunkChecksum = new RouteKeysChecksum();
|
|
1253
1384
|
await chunkChecksum.addMany(chunk.map(routeKeyForSignedRoute));
|
|
1254
1385
|
this.checksum.merge(chunkChecksum);
|
|
1255
1386
|
const routeChecksum = chunkChecksum.value();
|
|
1256
1387
|
const coordinateChecksum = await routeCoordinateChecksum(chunk);
|
|
1257
1388
|
const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
|
|
1258
|
-
|
|
1389
|
+
this.telemetry.observe("sdk.batch_checksum", performance.now() - checksumStarted);
|
|
1390
|
+
this.telemetry.gauge("batch_routes_max", chunk.length);
|
|
1391
|
+
this.telemetry.mark("first_batch_ms");
|
|
1392
|
+
this.telemetry.counters.route_batches++;
|
|
1393
|
+
await this.telemetry.measure("sdk.batch_ack", () => this.control.request("transfer.route_stream.batch", {
|
|
1259
1394
|
transfer_id: this.options.transferId,
|
|
1260
1395
|
route_generation_id: this.options.routeGenerationId,
|
|
1261
1396
|
stream_id: this.streamId,
|
|
@@ -1267,15 +1402,17 @@ class RouteStreamSender {
|
|
|
1267
1402
|
}, {
|
|
1268
1403
|
transferId: this.options.transferId,
|
|
1269
1404
|
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:batch:${batchIndex}:${coordinateChecksum}`
|
|
1270
|
-
});
|
|
1405
|
+
}));
|
|
1271
1406
|
}
|
|
1272
1407
|
catch (error) {
|
|
1273
1408
|
this.sendError = error;
|
|
1274
1409
|
}
|
|
1275
1410
|
});
|
|
1276
1411
|
}
|
|
1412
|
+
this.sendTail = this.sendTail.finally(() => { this.sending = false; });
|
|
1277
1413
|
}
|
|
1278
1414
|
async abort() {
|
|
1415
|
+
this.telemetry.finish();
|
|
1279
1416
|
this.sendError ??= new Error("route_stream_aborted");
|
|
1280
1417
|
await this.sendTail;
|
|
1281
1418
|
}
|
|
@@ -1511,6 +1648,9 @@ function createDeferred() {
|
|
|
1511
1648
|
resolve = resolvePromise;
|
|
1512
1649
|
reject = rejectPromise;
|
|
1513
1650
|
});
|
|
1651
|
+
// A waiter may be rejected after its route loop already stopped (for example when
|
|
1652
|
+
// ownership is fenced mid-stream); awaiting callers still observe the rejection.
|
|
1653
|
+
promise.catch(() => undefined);
|
|
1514
1654
|
return { promise, resolve, reject };
|
|
1515
1655
|
}
|
|
1516
1656
|
function createMultipartGroupWaiters(prepared, destinationsById) {
|
|
@@ -1562,7 +1702,12 @@ function restoreProviderMultipartIdentities(prepared, destinations, identities,
|
|
|
1562
1702
|
}
|
|
1563
1703
|
async function createMultipartGroupManifest(input) {
|
|
1564
1704
|
const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
|
|
1705
|
+
const queuedAt = performance.now();
|
|
1706
|
+
const telemetry = currentSdkPerformance();
|
|
1707
|
+
let active = 0;
|
|
1565
1708
|
const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
|
|
1709
|
+
telemetry?.observe("sdk.multipart_queue", performance.now() - queuedAt);
|
|
1710
|
+
telemetry?.gauge("multipart_active_peak", ++active);
|
|
1566
1711
|
let multipartGroupId = null;
|
|
1567
1712
|
try {
|
|
1568
1713
|
const destinationConfig = input.destinationsById.get(destination.destination_id);
|
|
@@ -1580,12 +1725,12 @@ async function createMultipartGroupManifest(input) {
|
|
|
1580
1725
|
const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
|
|
1581
1726
|
input.signal?.throwIfAborted();
|
|
1582
1727
|
const retainedUpload = input.multipartUploads.get(multipartGroupId);
|
|
1583
|
-
const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
|
|
1728
|
+
const uploadId = retainedUpload?.uploadId ?? await measureSdkPhase("sdk.multipart_provider", () => createMultipartUpload({
|
|
1584
1729
|
destination: destinationConfig,
|
|
1585
1730
|
objectKey: finalObjectKey,
|
|
1586
1731
|
metadata: finalObjectMetadata,
|
|
1587
1732
|
signal: input.signal
|
|
1588
|
-
});
|
|
1733
|
+
}));
|
|
1589
1734
|
const createdUpload = !retainedUpload;
|
|
1590
1735
|
if (createdUpload) {
|
|
1591
1736
|
input.multipartUploads.set(multipartGroupId, {
|
|
@@ -1648,6 +1793,9 @@ async function createMultipartGroupManifest(input) {
|
|
|
1648
1793
|
input.onGroupFailed(multipartGroupId, error);
|
|
1649
1794
|
return { ok: false, error };
|
|
1650
1795
|
}
|
|
1796
|
+
finally {
|
|
1797
|
+
active--;
|
|
1798
|
+
}
|
|
1651
1799
|
});
|
|
1652
1800
|
const failure = results.find((result) => !result.ok);
|
|
1653
1801
|
if (failure && !failure.ok) {
|