@beam-network/sdk 0.6.1 → 0.8.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 +48 -3
- package/dist/client.d.ts +91 -2
- package/dist/client.js +170 -24
- package/dist/models.d.ts +105 -12
- package/dist/nats-control.d.ts +2 -0
- package/dist/nats-control.js +14 -1
- package/dist/performance.d.ts +30 -3
- package/dist/performance.js +104 -5
- package/dist/provider-signing.js +15 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,8 +39,7 @@ const transfer = await beam.createTransfer({
|
|
|
39
39
|
secret_access_key: "aws-secret-key"
|
|
40
40
|
})
|
|
41
41
|
],
|
|
42
|
-
name: "r2-to-s3-report"
|
|
43
|
-
testMode: true
|
|
42
|
+
name: "r2-to-s3-report"
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
const status = await beam.waitForTransfer(transfer.transfer_id);
|
|
@@ -49,7 +48,32 @@ await beam.close();
|
|
|
49
48
|
```
|
|
50
49
|
|
|
51
50
|
The main `createTransfer` API is provider-aware and strictly typed for S3, R2, S3-compatible, Hippius, and Hugging Face configs.
|
|
52
|
-
|
|
51
|
+
|
|
52
|
+
## Storage Credentials
|
|
53
|
+
|
|
54
|
+
Source and destination credentials must not be restricted to specific IP addresses or networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different networks, so restricted credentials make the transfer fail.
|
|
55
|
+
|
|
56
|
+
## Failed Transfers
|
|
57
|
+
|
|
58
|
+
`waitForTransfer` rejects a failed transfer with `BeamTransferFailedError`, whose `errorMessage` is BeamCore's `error_message` verbatim. When the storage refused Beam's requests, the error is the `BeamStorageAccessError` subclass, with `code` set to `source_access_denied` or `destination_access_denied`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { BeamStorageAccessError, BeamTransferFailedError } from "@beam-network/sdk";
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await beam.waitForTransfer(transfer.transfer_id);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error instanceof BeamStorageAccessError) {
|
|
67
|
+
// For example: "destination_access_denied: The destination storage refused Beam's requests (403 AccessDenied). ..."
|
|
68
|
+
console.error(error.code, error.errorMessage);
|
|
69
|
+
} else if (error instanceof BeamTransferFailedError) {
|
|
70
|
+
console.error(error.errorMessage);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Callers that poll `transferStatus` themselves can pass a failed status to `transferFailedError(transferId, status.error_message)` to get the same classification.
|
|
53
77
|
|
|
54
78
|
## S3-Compatible Providers
|
|
55
79
|
|
|
@@ -195,3 +219,24 @@ Multipart control helpers (`createMultipartUpload`, `listMultipartParts`, `compl
|
|
|
195
219
|
Route streaming flushes at 1,024 routes, with no timer, and sends the final partial batch at stream completion. Signing overlaps acknowledged publication through bounded buffering; existing encoded-payload limits still apply. A source grant is reused across destinations only within the same signing generation and actual expiry.
|
|
196
220
|
|
|
197
221
|
The optional `onDiagnostics` callback receives bounded preparation measurements and source-grant reuse counts. Callbacks are best effort, may be dropped under load, and do not affect transfer outcomes. Durations may overlap; do not sum them into elapsed transfer time. Transfer status also exposes a typed `performance` summary when supported by Core.
|
|
222
|
+
|
|
223
|
+
Optional `storage_location` (`StorageLocation` in Go) describes the physical
|
|
224
|
+
storage location. It is separate from the region used to sign provider requests.
|
|
225
|
+
Leave it unset when unknown; a signing region such as R2's `auto` is not a location.
|
|
226
|
+
|
|
227
|
+
When supported by Core, diagnostics use `sdk-performance/v2`: bounded histograms,
|
|
228
|
+
preparation milestones, concurrency high-water marks, and separate provider,
|
|
229
|
+
callback, manifest, signing, and transport waits. Histogram bins are noncumulative,
|
|
230
|
+
with upper bounds in milliseconds of 0.1, 0.5, 1, 2, 5, 10, 25, 50, 100, 250,
|
|
231
|
+
500, 1000, 2500, 5000, 10000, 30000, 120000, then overflow. `unmeasured` explicitly
|
|
232
|
+
identifies unavailable measurements. Process CPU includes other concurrent work
|
|
233
|
+
in the SDK process; it is not transfer-exclusive CPU. Detailed reporting can be
|
|
234
|
+
disabled with `BEAM_SDK_PERFORMANCE_DETAILS=false`. Older peers retain v1 reports.
|
|
235
|
+
No credentials or storage grants are included in these diagnostics.
|
|
236
|
+
|
|
237
|
+
`sdk.producer_wait` measures waiting for a signed route separately from publication
|
|
238
|
+
backpressure. Configured-limit gauges preserve starting limits; effective-limit
|
|
239
|
+
gauges report the highest limit reached. Optional `source_renewals` counts source
|
|
240
|
+
grants recreated in later preparation generations. A bounded 16 KiB history tracks
|
|
241
|
+
131,072 chunk indices; the counter is omitted after takeover or beyond that bound.
|
|
242
|
+
It does not include separate recovery-control signing operations.
|
package/dist/client.d.ts
CHANGED
|
@@ -19,6 +19,33 @@ export declare class BeamProviderTransferError extends AggregateError {
|
|
|
19
19
|
cleanupError?: unknown;
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
/** Failure codes BeamCore reports when source or destination storage refuses Beam's requests. */
|
|
23
|
+
export type BeamStorageAccessErrorCode = "source_access_denied" | "destination_access_denied";
|
|
24
|
+
/** A transfer that BeamCore reported as failed. */
|
|
25
|
+
export declare class BeamTransferFailedError extends Error {
|
|
26
|
+
readonly transferId: string;
|
|
27
|
+
/** The transfer's `error_message` from BeamCore, verbatim; null when BeamCore sent none. */
|
|
28
|
+
readonly errorMessage: string | null;
|
|
29
|
+
constructor(transferId: string, errorMessage: string | null);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The source or destination storage refused Beam's requests (`source_access_denied` or
|
|
33
|
+
* `destination_access_denied`). `errorMessage` carries BeamCore's explanation verbatim.
|
|
34
|
+
*
|
|
35
|
+
* Check that the credentials allow the operation on this bucket and path and are not restricted
|
|
36
|
+
* to specific IP addresses or networks (for example Cloudflare R2 API-token client IP filtering,
|
|
37
|
+
* S3 bucket policies with `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many
|
|
38
|
+
* workers on different networks, so restricted credentials make the transfer fail.
|
|
39
|
+
*/
|
|
40
|
+
export declare class BeamStorageAccessError extends BeamTransferFailedError {
|
|
41
|
+
readonly code: BeamStorageAccessErrorCode;
|
|
42
|
+
constructor(transferId: string, errorMessage: string, code: BeamStorageAccessErrorCode);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds the error for a failed transfer status: a {@link BeamStorageAccessError} when the
|
|
46
|
+
* server message starts with a storage access code, otherwise a {@link BeamTransferFailedError}.
|
|
47
|
+
*/
|
|
48
|
+
export declare function transferFailedError(transferId: string, errorMessage: string | null): BeamTransferFailedError;
|
|
22
49
|
export declare class BeamApiError extends Error {
|
|
23
50
|
readonly status: number;
|
|
24
51
|
readonly body: string;
|
|
@@ -29,7 +56,6 @@ type TransferPrepareInput = {
|
|
|
29
56
|
sources: PreparedHttpSource[];
|
|
30
57
|
destinations: PreparedDestination[];
|
|
31
58
|
name?: string;
|
|
32
|
-
testMode?: boolean;
|
|
33
59
|
urlsExpiresAt?: string;
|
|
34
60
|
signedUrlFlow?: SignedUrlFlow;
|
|
35
61
|
idempotencyKey?: string;
|
|
@@ -54,22 +80,62 @@ export declare class BeamClient {
|
|
|
54
80
|
constructor(options?: BeamClientOptions);
|
|
55
81
|
close(): Promise<void>;
|
|
56
82
|
openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
83
|
+
/**
|
|
84
|
+
* Creates a transfer from raw source and destination configs (`transfer.create`).
|
|
85
|
+
*
|
|
86
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
87
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
88
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
89
|
+
* networks, so restricted credentials make the transfer fail.
|
|
90
|
+
*/
|
|
57
91
|
createRawTransfer(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
|
92
|
+
/**
|
|
93
|
+
* Signs provider sources and destinations, prepares the transfer, streams its signed routes
|
|
94
|
+
* and, unless `distribute` is false, starts it. Alias of {@link prepareProviderTransfer}.
|
|
95
|
+
*
|
|
96
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
97
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
98
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
99
|
+
* networks, so restricted credentials make the transfer fail.
|
|
100
|
+
*/
|
|
58
101
|
createTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
102
|
+
/**
|
|
103
|
+
* Takes over a prepared provider transfer in a new process, reusing its multipart uploads.
|
|
104
|
+
*
|
|
105
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
106
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
107
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
108
|
+
* networks, so restricted credentials make the transfer fail.
|
|
109
|
+
*/
|
|
59
110
|
resumeProviderTransfer(input: ProviderTransferResumeInput): Promise<TransferPrepareResponse>;
|
|
60
111
|
transferStatus(transferId: string): Promise<TransferStatusInfo>;
|
|
61
112
|
distributeTransfer(transferId: string): Promise<DistributeResponse>;
|
|
62
113
|
private requestTransferCancellation;
|
|
63
114
|
cancelTransfer(transferId: string): Promise<TransferCancelResponse>;
|
|
115
|
+
/**
|
|
116
|
+
* Asks BeamCore for the compact plan a transfer would use without creating it (`transfer.plan`).
|
|
117
|
+
*
|
|
118
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
119
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
120
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
121
|
+
* networks, so restricted credentials make the transfer fail.
|
|
122
|
+
*/
|
|
64
123
|
planTransfer(input: {
|
|
65
124
|
sources: PlanningHttpSource[];
|
|
66
125
|
destinations: PreparedDestination[];
|
|
67
126
|
name?: string;
|
|
68
|
-
testMode?: boolean;
|
|
69
127
|
urlsExpiresAt?: string;
|
|
70
128
|
signedUrlFlow?: SignedUrlFlow;
|
|
71
129
|
chunkSize?: number;
|
|
72
130
|
}): Promise<TransferPlanResponse>;
|
|
131
|
+
/**
|
|
132
|
+
* Prepares a transfer from already-signed HTTP sources and destinations (`transfer.prepare`).
|
|
133
|
+
*
|
|
134
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
135
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
136
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
137
|
+
* networks, so restricted credentials make the transfer fail.
|
|
138
|
+
*/
|
|
73
139
|
prepareTransfer(input: TransferPrepareInput): Promise<TransferPrepareResponse>;
|
|
74
140
|
private prepareTransferWithRequestKey;
|
|
75
141
|
attachSignedUrls(transferId: string, input: {
|
|
@@ -87,6 +153,15 @@ export declare class BeamClient {
|
|
|
87
153
|
urlsExpiresAt?: string;
|
|
88
154
|
autoDistribute?: boolean;
|
|
89
155
|
}): Promise<AttachSignedUrlsResponse>;
|
|
156
|
+
/**
|
|
157
|
+
* Signs provider sources and destinations, prepares the transfer, streams its signed routes
|
|
158
|
+
* and, unless `distribute` is false, starts it.
|
|
159
|
+
*
|
|
160
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
161
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
162
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
163
|
+
* networks, so restricted credentials make the transfer fail.
|
|
164
|
+
*/
|
|
90
165
|
prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
91
166
|
private executeProviderTransfer;
|
|
92
167
|
/**
|
|
@@ -113,7 +188,21 @@ export declare class BeamClient {
|
|
|
113
188
|
/** Stop a transfer's recovery and integrity signers; with `owner`, only if that owner installed them. */
|
|
114
189
|
private stopRecoverySigner;
|
|
115
190
|
private stopAllRecoverySigners;
|
|
191
|
+
/**
|
|
192
|
+
* Creates a raw transfer and distributes it.
|
|
193
|
+
*
|
|
194
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
195
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
196
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
197
|
+
* networks, so restricted credentials make the transfer fail.
|
|
198
|
+
*/
|
|
116
199
|
createAndDistribute(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
|
200
|
+
/**
|
|
201
|
+
* Waits until the transfer completes. A failed transfer rejects with a
|
|
202
|
+
* {@link BeamTransferFailedError} carrying BeamCore's `error_message` verbatim, or with its
|
|
203
|
+
* {@link BeamStorageAccessError} subclass when the source or destination storage refused
|
|
204
|
+
* Beam's requests (`source_access_denied`, `destination_access_denied`).
|
|
205
|
+
*/
|
|
117
206
|
waitForTransfer(transferId: string, options?: {
|
|
118
207
|
timeoutMs?: number;
|
|
119
208
|
pollIntervalMs?: number;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SdkPerformanceCollector } from "./performance.js";
|
|
1
|
+
import { SdkPerformanceCollector, SourceSignatureHistory, currentSdkPerformance, measureSdkPhase } from "./performance.js";
|
|
2
2
|
import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, releaseProviderClients, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, signSourceChunk, boundedGrantExpiry, isHuggingFaceProvider } from "./provider-signing.js";
|
|
3
3
|
import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
|
|
4
4
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
@@ -36,6 +36,54 @@ export class BeamProviderTransferError extends AggregateError {
|
|
|
36
36
|
this.cause = input.cause;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
const STORAGE_ACCESS_ERROR_CODES = [
|
|
40
|
+
"source_access_denied",
|
|
41
|
+
"destination_access_denied"
|
|
42
|
+
];
|
|
43
|
+
/** A transfer that BeamCore reported as failed. */
|
|
44
|
+
export class BeamTransferFailedError extends Error {
|
|
45
|
+
transferId;
|
|
46
|
+
/** The transfer's `error_message` from BeamCore, verbatim; null when BeamCore sent none. */
|
|
47
|
+
errorMessage;
|
|
48
|
+
constructor(transferId, errorMessage) {
|
|
49
|
+
super(`Transfer ${transferId} failed: ${errorMessage ?? "unknown error"}`);
|
|
50
|
+
this.name = "BeamTransferFailedError";
|
|
51
|
+
this.transferId = transferId;
|
|
52
|
+
this.errorMessage = errorMessage;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The source or destination storage refused Beam's requests (`source_access_denied` or
|
|
57
|
+
* `destination_access_denied`). `errorMessage` carries BeamCore's explanation verbatim.
|
|
58
|
+
*
|
|
59
|
+
* Check that the credentials allow the operation on this bucket and path and are not restricted
|
|
60
|
+
* to specific IP addresses or networks (for example Cloudflare R2 API-token client IP filtering,
|
|
61
|
+
* S3 bucket policies with `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many
|
|
62
|
+
* workers on different networks, so restricted credentials make the transfer fail.
|
|
63
|
+
*/
|
|
64
|
+
export class BeamStorageAccessError extends BeamTransferFailedError {
|
|
65
|
+
code;
|
|
66
|
+
constructor(transferId, errorMessage, code) {
|
|
67
|
+
super(transferId, errorMessage);
|
|
68
|
+
this.name = "BeamStorageAccessError";
|
|
69
|
+
this.code = code;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds the error for a failed transfer status: a {@link BeamStorageAccessError} when the
|
|
74
|
+
* server message starts with a storage access code, otherwise a {@link BeamTransferFailedError}.
|
|
75
|
+
*/
|
|
76
|
+
export function transferFailedError(transferId, errorMessage) {
|
|
77
|
+
const code = errorMessage === null ? undefined : storageAccessErrorCode(errorMessage);
|
|
78
|
+
return code && errorMessage !== null
|
|
79
|
+
? new BeamStorageAccessError(transferId, errorMessage, code)
|
|
80
|
+
: new BeamTransferFailedError(transferId, errorMessage);
|
|
81
|
+
}
|
|
82
|
+
function storageAccessErrorCode(errorMessage) {
|
|
83
|
+
const separator = errorMessage.indexOf(":");
|
|
84
|
+
const prefix = (separator === -1 ? errorMessage : errorMessage.slice(0, separator)).trim();
|
|
85
|
+
return STORAGE_ACCESS_ERROR_CODES.find((code) => code === prefix);
|
|
86
|
+
}
|
|
39
87
|
export class BeamApiError extends Error {
|
|
40
88
|
status;
|
|
41
89
|
body;
|
|
@@ -94,6 +142,14 @@ export class BeamClient {
|
|
|
94
142
|
validateId(transferId, "transferId");
|
|
95
143
|
return this.control.openTerminalSignalWaiter(transferId);
|
|
96
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates a transfer from raw source and destination configs (`transfer.create`).
|
|
147
|
+
*
|
|
148
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
149
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
150
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
151
|
+
* networks, so restricted credentials make the transfer fail.
|
|
152
|
+
*/
|
|
97
153
|
async createRawTransfer(input) {
|
|
98
154
|
const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
|
|
99
155
|
const body = compact({
|
|
@@ -106,7 +162,6 @@ export class BeamClient {
|
|
|
106
162
|
merkle_root: input.merkleRoot,
|
|
107
163
|
chunk_hashes: input.chunkHashes,
|
|
108
164
|
callbacks: input.callbacks,
|
|
109
|
-
test_mode: input.testMode || undefined,
|
|
110
165
|
progressive_mode: input.progressiveMode || undefined,
|
|
111
166
|
signed_url_flow: input.signedUrlFlow ?? "signed_url"
|
|
112
167
|
});
|
|
@@ -115,9 +170,26 @@ export class BeamClient {
|
|
|
115
170
|
idempotencyKey: `transfer:${transferId}:create`
|
|
116
171
|
});
|
|
117
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Signs provider sources and destinations, prepares the transfer, streams its signed routes
|
|
175
|
+
* and, unless `distribute` is false, starts it. Alias of {@link prepareProviderTransfer}.
|
|
176
|
+
*
|
|
177
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
178
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
179
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
180
|
+
* networks, so restricted credentials make the transfer fail.
|
|
181
|
+
*/
|
|
118
182
|
createTransfer(input) {
|
|
119
183
|
return this.prepareProviderTransfer(input);
|
|
120
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Takes over a prepared provider transfer in a new process, reusing its multipart uploads.
|
|
187
|
+
*
|
|
188
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
189
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
190
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
191
|
+
* networks, so restricted credentials make the transfer fail.
|
|
192
|
+
*/
|
|
121
193
|
async resumeProviderTransfer(input) {
|
|
122
194
|
validateId(input.transferId, "transferId");
|
|
123
195
|
return this.executeProviderTransfer(input, input);
|
|
@@ -159,12 +231,19 @@ export class BeamClient {
|
|
|
159
231
|
this.stopRecoverySigner(transferId);
|
|
160
232
|
return result;
|
|
161
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Asks BeamCore for the compact plan a transfer would use without creating it (`transfer.plan`).
|
|
236
|
+
*
|
|
237
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
238
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
239
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
240
|
+
* networks, so restricted credentials make the transfer fail.
|
|
241
|
+
*/
|
|
162
242
|
async planTransfer(input) {
|
|
163
243
|
const result = await this.control.request("transfer.plan", compact({
|
|
164
244
|
sources: input.sources,
|
|
165
245
|
destinations: input.destinations,
|
|
166
246
|
name: input.name,
|
|
167
|
-
test_mode: input.testMode || undefined,
|
|
168
247
|
chunk_size: input.chunkSize,
|
|
169
248
|
urls_expires_at: input.urlsExpiresAt,
|
|
170
249
|
signed_url_flow: input.signedUrlFlow ?? "signed_url"
|
|
@@ -173,6 +252,14 @@ export class BeamClient {
|
|
|
173
252
|
validateCompactTransferPlan(result.plan_descriptor, result.signed_url_flow);
|
|
174
253
|
return result;
|
|
175
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Prepares a transfer from already-signed HTTP sources and destinations (`transfer.prepare`).
|
|
257
|
+
*
|
|
258
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
259
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
260
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
261
|
+
* networks, so restricted credentials make the transfer fail.
|
|
262
|
+
*/
|
|
176
263
|
prepareTransfer(input) {
|
|
177
264
|
return this.prepareTransferWithRequestKey(input);
|
|
178
265
|
}
|
|
@@ -187,7 +274,6 @@ export class BeamClient {
|
|
|
187
274
|
sources: input.sources,
|
|
188
275
|
destinations: input.destinations,
|
|
189
276
|
name: input.name,
|
|
190
|
-
test_mode: input.testMode || undefined,
|
|
191
277
|
chunk_size: input.chunkSize,
|
|
192
278
|
urls_expires_at: input.urlsExpiresAt,
|
|
193
279
|
signed_url_flow: input.signedUrlFlow ?? "signed_url"
|
|
@@ -274,6 +360,15 @@ export class BeamClient {
|
|
|
274
360
|
releaseInitialStream();
|
|
275
361
|
}
|
|
276
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Signs provider sources and destinations, prepares the transfer, streams its signed routes
|
|
365
|
+
* and, unless `distribute` is false, starts it.
|
|
366
|
+
*
|
|
367
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
368
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
369
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
370
|
+
* networks, so restricted credentials make the transfer fail.
|
|
371
|
+
*/
|
|
277
372
|
async prepareProviderTransfer(input) {
|
|
278
373
|
return this.executeProviderTransfer(input);
|
|
279
374
|
}
|
|
@@ -300,10 +395,9 @@ export class BeamClient {
|
|
|
300
395
|
destinationsById.set(preparedDestination.destination_id, destination);
|
|
301
396
|
});
|
|
302
397
|
await input.throwIfCancelled?.();
|
|
303
|
-
const
|
|
304
|
-
const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal })));
|
|
398
|
+
const discoveryTelemetry = new SdkPerformanceCollector();
|
|
399
|
+
const preparedSources = await discoveryTelemetry.measure("sdk.discovery", () => Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal }))));
|
|
305
400
|
await input.throwIfCancelled?.();
|
|
306
|
-
const discoveryMs = performance.now() - discoveryStarted;
|
|
307
401
|
const huggingFace = await this.planHuggingFaceUploads({
|
|
308
402
|
sources: retainedRecoveryInput.sources,
|
|
309
403
|
preparedSources,
|
|
@@ -317,7 +411,6 @@ export class BeamClient {
|
|
|
317
411
|
sources: preparedSources,
|
|
318
412
|
destinations: preparedDestinations,
|
|
319
413
|
name: input.name,
|
|
320
|
-
testMode: input.testMode,
|
|
321
414
|
chunkSize: huggingFace.chunkSize ?? input.chunkSize,
|
|
322
415
|
signedUrlFlow: requestedSignedUrlFlow,
|
|
323
416
|
...(resume ? { transferId: resume.transferId } : { idempotencyKey: input.idempotencyKey, routeGenerationId: input.routeGenerationId })
|
|
@@ -353,13 +446,18 @@ export class BeamClient {
|
|
|
353
446
|
const assertOwnership = () => input.signal?.throwIfAborted();
|
|
354
447
|
const routeStreamLock = new AsyncMutex();
|
|
355
448
|
const releaseInitialStream = await routeStreamLock.acquire();
|
|
449
|
+
const sourceSignatureHistory = new SourceSignatureHistory(!resume);
|
|
356
450
|
const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
|
|
357
|
-
const telemetry = new SdkPerformanceCollector();
|
|
358
|
-
|
|
359
|
-
|
|
451
|
+
const telemetry = new SdkPerformanceCollector(sourceSignatureHistory);
|
|
452
|
+
telemetry.seedDiscovery(discoveryTelemetry);
|
|
453
|
+
telemetry.gauge("signing_configured_limit", this.routeSigningConcurrency);
|
|
454
|
+
telemetry.gauge("multipart_configured_limit", this.multipartControlConcurrency);
|
|
455
|
+
telemetry.gauge("signing_limit", this.routeSigningConcurrency);
|
|
456
|
+
telemetry.gauge("multipart_limit", this.multipartControlConcurrency);
|
|
360
457
|
const pendingRoutes = new Set();
|
|
361
458
|
let signingConcurrency = this.routeSigningConcurrency;
|
|
362
459
|
let signedInWindow = 0;
|
|
460
|
+
let activeSigning = 0;
|
|
363
461
|
let signingWindowStartedAt = performance.now();
|
|
364
462
|
let routeStream = null;
|
|
365
463
|
let routeStreamBeginAttempted = false;
|
|
@@ -420,14 +518,14 @@ export class BeamClient {
|
|
|
420
518
|
assertOwnership();
|
|
421
519
|
recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
|
|
422
520
|
validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
|
|
423
|
-
await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
|
|
521
|
+
await telemetry.measure("sdk.multipart_callback", async () => input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest)));
|
|
424
522
|
await routeStream.addManifestGroups([state.manifest]);
|
|
425
523
|
multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
|
|
426
524
|
},
|
|
427
525
|
onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
|
|
428
526
|
}));
|
|
429
527
|
const streamNextCompletedRoute = async () => {
|
|
430
|
-
const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
|
|
528
|
+
const settled = await telemetry.measure("sdk.producer_wait", () => Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route })))));
|
|
431
529
|
pendingRoutes.delete(settled.pending);
|
|
432
530
|
assertOwnership();
|
|
433
531
|
await routeStream.addRoute(settled.route);
|
|
@@ -436,6 +534,8 @@ export class BeamClient {
|
|
|
436
534
|
const elapsedMs = performance.now() - signingWindowStartedAt;
|
|
437
535
|
if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
|
|
438
536
|
signingConcurrency = Math.min(256, signingConcurrency * 2);
|
|
537
|
+
telemetry.increment("concurrency_changes");
|
|
538
|
+
telemetry.gauge("signing_limit", signingConcurrency);
|
|
439
539
|
}
|
|
440
540
|
signedInWindow = 0;
|
|
441
541
|
signingWindowStartedAt = performance.now();
|
|
@@ -445,10 +545,9 @@ export class BeamClient {
|
|
|
445
545
|
await throwIfCancelled?.(prepared.transfer_id);
|
|
446
546
|
// This promise belongs to this chunk and signing generation only. Its
|
|
447
547
|
// immutable credential/range identity and actual expiry cannot be renewed.
|
|
448
|
-
|
|
548
|
+
let sourceUses = 0;
|
|
549
|
+
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; });
|
|
449
550
|
void sourceGrant.catch(() => { });
|
|
450
|
-
telemetry.counters.source_signatures++;
|
|
451
|
-
telemetry.counters.source_reuses += Math.max(0, chunk.destinations.length - 1);
|
|
452
551
|
for (const target of chunk.destinations) {
|
|
453
552
|
const pendingRoute = (async () => {
|
|
454
553
|
await throwIfCancelled?.(prepared.transfer_id);
|
|
@@ -465,14 +564,16 @@ export class BeamClient {
|
|
|
465
564
|
const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
|
|
466
565
|
const upload = isDirectPutDestination(destination)
|
|
467
566
|
? undefined
|
|
468
|
-
: await multipartGroupWaiters.get(multipartGroupId)?.promise;
|
|
567
|
+
: await telemetry.measure("sdk.multipart_ready_wait", async () => multipartGroupWaiters.get(multipartGroupId)?.promise);
|
|
469
568
|
if (!isDirectPutDestination(destination) && !upload)
|
|
470
569
|
throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
|
|
570
|
+
telemetry.gauge("signing_active_peak", ++activeSigning);
|
|
471
571
|
return telemetry.measure("sdk.signing", () => this.signProviderRoute({
|
|
472
572
|
chunk,
|
|
473
573
|
target,
|
|
474
574
|
source,
|
|
475
|
-
sourceGrant,
|
|
575
|
+
sourceGrant: telemetry.measure("sdk.source_grant_wait", () => sourceGrant).then(grant => { if (sourceUses++ > 0)
|
|
576
|
+
telemetry.counters.source_reuses++; return grant; }),
|
|
476
577
|
destination,
|
|
477
578
|
expiresIn,
|
|
478
579
|
...(upload ? { upload } : {}),
|
|
@@ -483,11 +584,12 @@ export class BeamClient {
|
|
|
483
584
|
partNumber: typeof metadata.part_number === "number"
|
|
484
585
|
? metadata.part_number
|
|
485
586
|
: multipartPartNumber(chunk.source_chunk_index),
|
|
486
|
-
}));
|
|
587
|
+
})).finally(() => { activeSigning--; });
|
|
487
588
|
})();
|
|
488
589
|
pendingRoutes.add(pendingRoute);
|
|
590
|
+
telemetry.gauge("signing_pending_peak", pendingRoutes.size);
|
|
489
591
|
if (pendingRoutes.size >= signingConcurrency)
|
|
490
|
-
await streamNextCompletedRoute
|
|
592
|
+
await telemetry.measure("sdk.signing_queue", streamNextCompletedRoute);
|
|
491
593
|
}
|
|
492
594
|
}
|
|
493
595
|
while (pendingRoutes.size)
|
|
@@ -1127,6 +1229,14 @@ export class BeamClient {
|
|
|
1127
1229
|
for (const transferId of [...this.recoverySigners.keys()])
|
|
1128
1230
|
this.stopRecoverySigner(transferId);
|
|
1129
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Creates a raw transfer and distributes it.
|
|
1234
|
+
*
|
|
1235
|
+
* Source and destination credentials must not be restricted to specific IP addresses or
|
|
1236
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
1237
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
1238
|
+
* networks, so restricted credentials make the transfer fail.
|
|
1239
|
+
*/
|
|
1130
1240
|
async createAndDistribute(input) {
|
|
1131
1241
|
const transfer = await this.createRawTransfer(input);
|
|
1132
1242
|
if (transfer.success) {
|
|
@@ -1134,6 +1244,12 @@ export class BeamClient {
|
|
|
1134
1244
|
}
|
|
1135
1245
|
return transfer;
|
|
1136
1246
|
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Waits until the transfer completes. A failed transfer rejects with a
|
|
1249
|
+
* {@link BeamTransferFailedError} carrying BeamCore's `error_message` verbatim, or with its
|
|
1250
|
+
* {@link BeamStorageAccessError} subclass when the source or destination storage refused
|
|
1251
|
+
* Beam's requests (`source_access_denied`, `destination_access_denied`).
|
|
1252
|
+
*/
|
|
1137
1253
|
async waitForTransfer(transferId, options = {}) {
|
|
1138
1254
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
1139
1255
|
const pollIntervalMs = options.pollIntervalMs ?? 15_000;
|
|
@@ -1166,7 +1282,7 @@ export class BeamClient {
|
|
|
1166
1282
|
return status;
|
|
1167
1283
|
}
|
|
1168
1284
|
if (status.status === "failed") {
|
|
1169
|
-
throw
|
|
1285
|
+
throw transferFailedError(transferId, status.error_message);
|
|
1170
1286
|
}
|
|
1171
1287
|
if (status.status === "cancelled") {
|
|
1172
1288
|
throw new Error("Transfer cancelled");
|
|
@@ -1252,6 +1368,7 @@ class RouteStreamSender {
|
|
|
1252
1368
|
routeCount = 0;
|
|
1253
1369
|
sendTail = Promise.resolve();
|
|
1254
1370
|
sendError = null;
|
|
1371
|
+
sending = false;
|
|
1255
1372
|
constructor(control, options) {
|
|
1256
1373
|
this.control = control;
|
|
1257
1374
|
this.options = options;
|
|
@@ -1259,6 +1376,7 @@ class RouteStreamSender {
|
|
|
1259
1376
|
this.telemetry = options.telemetry ?? new SdkPerformanceCollector();
|
|
1260
1377
|
}
|
|
1261
1378
|
async begin() {
|
|
1379
|
+
this.telemetry.startDelaySampling();
|
|
1262
1380
|
await this.control.request("transfer.route_stream.begin", compact({
|
|
1263
1381
|
transfer_id: this.options.transferId,
|
|
1264
1382
|
route_generation_id: this.options.routeGenerationId,
|
|
@@ -1275,6 +1393,7 @@ class RouteStreamSender {
|
|
|
1275
1393
|
});
|
|
1276
1394
|
}
|
|
1277
1395
|
async addManifestGroups(groups) {
|
|
1396
|
+
const started = performance.now();
|
|
1278
1397
|
if (!groups.length)
|
|
1279
1398
|
return;
|
|
1280
1399
|
await Promise.all(groups.map(async (group) => {
|
|
@@ -1290,8 +1409,12 @@ class RouteStreamSender {
|
|
|
1290
1409
|
idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:manifest:${identity}`
|
|
1291
1410
|
});
|
|
1292
1411
|
}));
|
|
1412
|
+
this.telemetry.observe("sdk.manifest_ack", performance.now() - started);
|
|
1413
|
+
this.telemetry.mark("first_manifest_ms");
|
|
1293
1414
|
}
|
|
1294
1415
|
async addRoute(route) {
|
|
1416
|
+
this.telemetry.mark("first_route_ms");
|
|
1417
|
+
const assemblyStarted = performance.now();
|
|
1295
1418
|
const deliveryIndex = signedRouteDeliveryIndex(route);
|
|
1296
1419
|
if (deliveryIndex === undefined) {
|
|
1297
1420
|
throw new Error("route delivery_index is required");
|
|
@@ -1305,6 +1428,8 @@ class RouteStreamSender {
|
|
|
1305
1428
|
this.seenDeliveryIndices.add(deliveryIndex);
|
|
1306
1429
|
this.routeCount += 1;
|
|
1307
1430
|
this.batch.push({ ...route, delivery_index: deliveryIndex });
|
|
1431
|
+
this.telemetry.gauge("buffered_batches_peak", 1 + Number(this.sending));
|
|
1432
|
+
this.telemetry.observe("sdk.route_assembly", performance.now() - assemblyStarted);
|
|
1308
1433
|
if (this.batch.length >= ROUTE_STREAM_BATCH_ROUTES) {
|
|
1309
1434
|
await this.enqueueFlush();
|
|
1310
1435
|
}
|
|
@@ -1314,12 +1439,14 @@ class RouteStreamSender {
|
|
|
1314
1439
|
this.seenDeliveryIndices.size !== this.options.totalRoutes) {
|
|
1315
1440
|
throw new Error(`route stream has incomplete or duplicate delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
|
|
1316
1441
|
}
|
|
1442
|
+
this.telemetry.mark("final_flush_ms");
|
|
1317
1443
|
await this.enqueueFlush();
|
|
1318
1444
|
await this.sendTail;
|
|
1319
1445
|
if (this.sendError)
|
|
1320
1446
|
throw this.sendError;
|
|
1321
1447
|
this.telemetry.observe("sdk.preparation", performance.now() - this.started);
|
|
1322
|
-
|
|
1448
|
+
this.telemetry.finish();
|
|
1449
|
+
const sdkPerformance = this.telemetry.snapshot(this.control.supportsPerformanceV2?.(this.options.transferId) ?? false);
|
|
1323
1450
|
this.options.onDiagnostics?.(sdkPerformance);
|
|
1324
1451
|
return this.control.request("transfer.route_stream.complete", {
|
|
1325
1452
|
sdk_performance: sdkPerformance,
|
|
@@ -1341,6 +1468,7 @@ class RouteStreamSender {
|
|
|
1341
1468
|
if (this.sendError)
|
|
1342
1469
|
throw this.sendError;
|
|
1343
1470
|
const routes = this.batch.splice(0, this.batch.length);
|
|
1471
|
+
const encodingStarted = performance.now();
|
|
1344
1472
|
const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
|
|
1345
1473
|
transfer_id: this.options.transferId,
|
|
1346
1474
|
route_generation_id: this.options.routeGenerationId,
|
|
@@ -1348,19 +1476,27 @@ class RouteStreamSender {
|
|
|
1348
1476
|
batch_id: `${this.streamId}:estimate`,
|
|
1349
1477
|
batch_index: this.batchIndex
|
|
1350
1478
|
}, routes);
|
|
1479
|
+
this.telemetry.observe("sdk.batch_encode", performance.now() - encodingStarted);
|
|
1480
|
+
this.sending = true;
|
|
1351
1481
|
for (const chunk of chunks) {
|
|
1482
|
+
const queuedAt = performance.now();
|
|
1352
1483
|
const batchIndex = this.batchIndex;
|
|
1353
1484
|
this.batchIndex += 1;
|
|
1354
1485
|
this.sendTail = this.sendTail.then(async () => {
|
|
1486
|
+
this.telemetry.observe("sdk.batch_queue", performance.now() - queuedAt);
|
|
1355
1487
|
if (this.sendError)
|
|
1356
1488
|
return;
|
|
1357
1489
|
try {
|
|
1490
|
+
const checksumStarted = performance.now();
|
|
1358
1491
|
const chunkChecksum = new RouteKeysChecksum();
|
|
1359
1492
|
await chunkChecksum.addMany(chunk.map(routeKeyForSignedRoute));
|
|
1360
1493
|
this.checksum.merge(chunkChecksum);
|
|
1361
1494
|
const routeChecksum = chunkChecksum.value();
|
|
1362
1495
|
const coordinateChecksum = await routeCoordinateChecksum(chunk);
|
|
1363
1496
|
const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
|
|
1497
|
+
this.telemetry.observe("sdk.batch_checksum", performance.now() - checksumStarted);
|
|
1498
|
+
this.telemetry.gauge("batch_routes_max", chunk.length);
|
|
1499
|
+
this.telemetry.mark("first_batch_ms");
|
|
1364
1500
|
this.telemetry.counters.route_batches++;
|
|
1365
1501
|
await this.telemetry.measure("sdk.batch_ack", () => this.control.request("transfer.route_stream.batch", {
|
|
1366
1502
|
transfer_id: this.options.transferId,
|
|
@@ -1381,8 +1517,10 @@ class RouteStreamSender {
|
|
|
1381
1517
|
}
|
|
1382
1518
|
});
|
|
1383
1519
|
}
|
|
1520
|
+
this.sendTail = this.sendTail.finally(() => { this.sending = false; });
|
|
1384
1521
|
}
|
|
1385
1522
|
async abort() {
|
|
1523
|
+
this.telemetry.finish();
|
|
1386
1524
|
this.sendError ??= new Error("route_stream_aborted");
|
|
1387
1525
|
await this.sendTail;
|
|
1388
1526
|
}
|
|
@@ -1672,7 +1810,12 @@ function restoreProviderMultipartIdentities(prepared, destinations, identities,
|
|
|
1672
1810
|
}
|
|
1673
1811
|
async function createMultipartGroupManifest(input) {
|
|
1674
1812
|
const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
|
|
1813
|
+
const queuedAt = performance.now();
|
|
1814
|
+
const telemetry = currentSdkPerformance();
|
|
1815
|
+
let active = 0;
|
|
1675
1816
|
const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
|
|
1817
|
+
telemetry?.observe("sdk.multipart_queue", performance.now() - queuedAt);
|
|
1818
|
+
telemetry?.gauge("multipart_active_peak", ++active);
|
|
1676
1819
|
let multipartGroupId = null;
|
|
1677
1820
|
try {
|
|
1678
1821
|
const destinationConfig = input.destinationsById.get(destination.destination_id);
|
|
@@ -1690,12 +1833,12 @@ async function createMultipartGroupManifest(input) {
|
|
|
1690
1833
|
const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
|
|
1691
1834
|
input.signal?.throwIfAborted();
|
|
1692
1835
|
const retainedUpload = input.multipartUploads.get(multipartGroupId);
|
|
1693
|
-
const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
|
|
1836
|
+
const uploadId = retainedUpload?.uploadId ?? await measureSdkPhase("sdk.multipart_provider", () => createMultipartUpload({
|
|
1694
1837
|
destination: destinationConfig,
|
|
1695
1838
|
objectKey: finalObjectKey,
|
|
1696
1839
|
metadata: finalObjectMetadata,
|
|
1697
1840
|
signal: input.signal
|
|
1698
|
-
});
|
|
1841
|
+
}));
|
|
1699
1842
|
const createdUpload = !retainedUpload;
|
|
1700
1843
|
if (createdUpload) {
|
|
1701
1844
|
input.multipartUploads.set(multipartGroupId, {
|
|
@@ -1758,6 +1901,9 @@ async function createMultipartGroupManifest(input) {
|
|
|
1758
1901
|
input.onGroupFailed(multipartGroupId, error);
|
|
1759
1902
|
return { ok: false, error };
|
|
1760
1903
|
}
|
|
1904
|
+
finally {
|
|
1905
|
+
active--;
|
|
1906
|
+
}
|
|
1761
1907
|
});
|
|
1762
1908
|
const failure = results.find((result) => !result.ok);
|
|
1763
1909
|
if (failure && !failure.ok) {
|
package/dist/models.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
export type SignedUrlFlow = "signed_url";
|
|
2
2
|
export interface SdkPerformanceSummary {
|
|
3
|
-
|
|
3
|
+
gauges?: Record<string, number>;
|
|
4
|
+
milestones?: Record<string, number>;
|
|
5
|
+
unmeasured?: string[];
|
|
6
|
+
detail_dropped?: number;
|
|
7
|
+
schema_version: "sdk-performance/v1" | "sdk-performance/v2";
|
|
4
8
|
measurements: Array<{
|
|
5
|
-
name:
|
|
9
|
+
name: string;
|
|
6
10
|
count: number;
|
|
7
11
|
work_ms: number;
|
|
8
12
|
max_ms: number;
|
|
13
|
+
histogram?: number[];
|
|
9
14
|
}>;
|
|
10
15
|
counters: {
|
|
11
16
|
source_signatures: number;
|
|
12
17
|
source_reuses: number;
|
|
13
18
|
route_batches: number;
|
|
19
|
+
source_renewals?: number;
|
|
14
20
|
};
|
|
15
21
|
}
|
|
16
22
|
export interface BeamClientOptions {
|
|
@@ -28,6 +34,14 @@ export interface BeamClientOptions {
|
|
|
28
34
|
multipartControlConcurrency?: number;
|
|
29
35
|
fetch?: typeof fetch;
|
|
30
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Raw transfer source.
|
|
39
|
+
*
|
|
40
|
+
* Source credentials must not be restricted to specific IP addresses or networks (for example
|
|
41
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
42
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
43
|
+
* restricted credentials make the transfer fail.
|
|
44
|
+
*/
|
|
31
45
|
export interface SourceConfig {
|
|
32
46
|
type: string;
|
|
33
47
|
bucket?: string;
|
|
@@ -41,6 +55,14 @@ export interface SourceConfig {
|
|
|
41
55
|
url?: string;
|
|
42
56
|
headers?: Record<string, string>;
|
|
43
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Raw transfer destination.
|
|
60
|
+
*
|
|
61
|
+
* Destination credentials must not be restricted to specific IP addresses or networks (for
|
|
62
|
+
* example Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
63
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
64
|
+
* restricted credentials make the transfer fail.
|
|
65
|
+
*/
|
|
44
66
|
export interface DestConfig {
|
|
45
67
|
type: string;
|
|
46
68
|
bucket?: string;
|
|
@@ -67,7 +89,6 @@ export interface TransferCreateRequest {
|
|
|
67
89
|
merkle_root?: string;
|
|
68
90
|
chunk_hashes?: string[];
|
|
69
91
|
callbacks?: CallbackConfig[];
|
|
70
|
-
test_mode?: boolean;
|
|
71
92
|
progressive_mode?: boolean;
|
|
72
93
|
signed_url_flow: SignedUrlFlow;
|
|
73
94
|
}
|
|
@@ -80,10 +101,6 @@ export interface RawTransferCreateInput {
|
|
|
80
101
|
merkleRoot?: string;
|
|
81
102
|
chunkHashes?: string[];
|
|
82
103
|
callbacks?: CallbackConfig[];
|
|
83
|
-
/**
|
|
84
|
-
* Maps to BeamCore test_mode.
|
|
85
|
-
*/
|
|
86
|
-
testMode?: boolean;
|
|
87
104
|
progressiveMode?: boolean;
|
|
88
105
|
signedUrlFlow?: SignedUrlFlow;
|
|
89
106
|
idempotencyKey?: string;
|
|
@@ -158,6 +175,21 @@ export interface IntegrityAuditChallenge {
|
|
|
158
175
|
chunks: IntegrityAuditChallengeChunk[];
|
|
159
176
|
}
|
|
160
177
|
export interface TransferPerformance {
|
|
178
|
+
recovery_bandwidth?: {
|
|
179
|
+
excluded_tasks: number;
|
|
180
|
+
accepted_bytes: number;
|
|
181
|
+
};
|
|
182
|
+
deadline?: {
|
|
183
|
+
version: "assignment-deadline/v1";
|
|
184
|
+
decisions: number;
|
|
185
|
+
selected_min_ms: number;
|
|
186
|
+
selected_max_ms: number;
|
|
187
|
+
estimated_max_ms: number;
|
|
188
|
+
sample_count_max: number;
|
|
189
|
+
data_age_max_ms: number | null;
|
|
190
|
+
reasons: Record<string, number>;
|
|
191
|
+
};
|
|
192
|
+
sdk_detail?: SdkPerformanceSummary;
|
|
161
193
|
schema_version: "transfer-performance/v1";
|
|
162
194
|
runtime_epoch: string;
|
|
163
195
|
coverage: "complete" | "restarted";
|
|
@@ -177,6 +209,7 @@ export interface TransferPerformance {
|
|
|
177
209
|
source_signatures: number;
|
|
178
210
|
source_reuses: number;
|
|
179
211
|
route_batches: number;
|
|
212
|
+
source_renewals?: number;
|
|
180
213
|
};
|
|
181
214
|
detail_dropped: number;
|
|
182
215
|
unmeasured?: string[];
|
|
@@ -232,7 +265,17 @@ export interface TransferTerminalSignalWaiter {
|
|
|
232
265
|
wait(timeoutMs: number): Promise<TransferTerminalEvent | null>;
|
|
233
266
|
close(): Promise<void>;
|
|
234
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Amazon S3 source or destination.
|
|
270
|
+
*
|
|
271
|
+
* Credentials must not be restricted to specific IP addresses or networks (for example
|
|
272
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
273
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
274
|
+
* restricted credentials make the transfer fail.
|
|
275
|
+
*/
|
|
235
276
|
export interface S3ProviderConfig {
|
|
277
|
+
/** Physical storage location; independent of the signing region. */
|
|
278
|
+
storage_location?: string;
|
|
236
279
|
provider: "s3";
|
|
237
280
|
id?: string;
|
|
238
281
|
bucket: string;
|
|
@@ -243,7 +286,17 @@ export interface S3ProviderConfig {
|
|
|
243
286
|
session_token?: string;
|
|
244
287
|
endpoint_url?: string;
|
|
245
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Cloudflare R2 source or destination.
|
|
291
|
+
*
|
|
292
|
+
* Credentials must not be restricted to specific IP addresses or networks (for example
|
|
293
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
294
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
295
|
+
* restricted credentials make the transfer fail.
|
|
296
|
+
*/
|
|
246
297
|
export interface R2ProviderConfig {
|
|
298
|
+
/** Physical storage location; independent of the signing region. */
|
|
299
|
+
storage_location?: string;
|
|
247
300
|
provider: "r2";
|
|
248
301
|
id?: string;
|
|
249
302
|
bucket: string;
|
|
@@ -253,7 +306,17 @@ export interface R2ProviderConfig {
|
|
|
253
306
|
account_id?: string;
|
|
254
307
|
endpoint_url?: string;
|
|
255
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* S3-compatible source or destination (MinIO, Wasabi, Backblaze B2, DigitalOcean Spaces and others).
|
|
311
|
+
*
|
|
312
|
+
* Credentials must not be restricted to specific IP addresses or networks (for example
|
|
313
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
314
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
315
|
+
* restricted credentials make the transfer fail.
|
|
316
|
+
*/
|
|
256
317
|
export interface S3CompatibleProviderConfig {
|
|
318
|
+
/** Physical storage location; independent of the signing region. */
|
|
319
|
+
storage_location?: string;
|
|
257
320
|
provider: string;
|
|
258
321
|
driver?: "s3-compatible";
|
|
259
322
|
id?: string;
|
|
@@ -267,7 +330,17 @@ export interface S3CompatibleProviderConfig {
|
|
|
267
330
|
force_path_style?: boolean;
|
|
268
331
|
account_id?: string;
|
|
269
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Hippius source or destination.
|
|
335
|
+
*
|
|
336
|
+
* Credentials must not be restricted to specific IP addresses or networks (for example
|
|
337
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
338
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
339
|
+
* restricted credentials make the transfer fail.
|
|
340
|
+
*/
|
|
270
341
|
export interface HippiusProviderConfig {
|
|
342
|
+
/** Physical storage location; independent of the signing region. */
|
|
343
|
+
storage_location?: string;
|
|
271
344
|
provider: "hippius";
|
|
272
345
|
id?: string;
|
|
273
346
|
bucket: string;
|
|
@@ -276,7 +349,17 @@ export interface HippiusProviderConfig {
|
|
|
276
349
|
base_url?: string;
|
|
277
350
|
}
|
|
278
351
|
export type HuggingFaceRepoType = "model" | "dataset" | "space" | "kernel" | "bucket";
|
|
352
|
+
/**
|
|
353
|
+
* Hugging Face Hub source or destination.
|
|
354
|
+
*
|
|
355
|
+
* The token and any credentials must not be restricted to specific IP addresses or networks (for example
|
|
356
|
+
* Cloudflare R2 API-token client IP filtering, S3 bucket policies with `aws:SourceIp`, or
|
|
357
|
+
* VPC-only endpoints). Beam moves data through many workers on different networks, so
|
|
358
|
+
* restricted credentials make the transfer fail.
|
|
359
|
+
*/
|
|
279
360
|
export interface HuggingFaceProviderConfig {
|
|
361
|
+
/** Physical storage location; independent of the signing region. */
|
|
362
|
+
storage_location?: string;
|
|
280
363
|
provider: "huggingface";
|
|
281
364
|
id?: string;
|
|
282
365
|
/** Namespace and repo or bucket name separated by a slash, for example `org/dataset`. */
|
|
@@ -328,13 +411,19 @@ export declare const HuggingFaceProviderConfig: Readonly<{
|
|
|
328
411
|
export interface ProviderTransferCreateInput {
|
|
329
412
|
/** Ownership fence. Aborting stops signing/replay without cancelling a replacement owner. */
|
|
330
413
|
signal?: AbortSignal;
|
|
414
|
+
/**
|
|
415
|
+
* Storage to read from. Source credentials must not be restricted to specific IP addresses or
|
|
416
|
+
* networks (for example Cloudflare R2 API-token client IP filtering, S3 bucket policies with
|
|
417
|
+
* `aws:SourceIp`, or VPC-only endpoints). Beam moves data through many workers on different
|
|
418
|
+
* networks, so restricted credentials make the transfer fail.
|
|
419
|
+
*/
|
|
331
420
|
sources: ProviderSourceConfig[];
|
|
332
|
-
destinations: ProviderDestinationConfig[];
|
|
333
|
-
name?: string;
|
|
334
421
|
/**
|
|
335
|
-
*
|
|
422
|
+
* Storage to write to. Destination credentials must not be restricted to specific IP addresses
|
|
423
|
+
* or networks, for the same reason as `sources`.
|
|
336
424
|
*/
|
|
337
|
-
|
|
425
|
+
destinations: ProviderDestinationConfig[];
|
|
426
|
+
name?: string;
|
|
338
427
|
expiresIn?: number;
|
|
339
428
|
/**
|
|
340
429
|
* Defaults to true. Set to false to only prepare and stream signed routes without distribution.
|
|
@@ -368,6 +457,11 @@ export interface ProviderMultipartGroupIdentity {
|
|
|
368
457
|
expectedPartCount: number;
|
|
369
458
|
expiresAt: string;
|
|
370
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* HTTP source for planning and preparing. Its URL and headers must work from any network: Beam
|
|
462
|
+
* moves data through many workers on different networks, so IP- or network-restricted URLs make
|
|
463
|
+
* the transfer fail.
|
|
464
|
+
*/
|
|
371
465
|
export interface PlanningHttpSource {
|
|
372
466
|
source_id: string;
|
|
373
467
|
type: "http";
|
|
@@ -479,7 +573,6 @@ export interface TransferPrepareResponse {
|
|
|
479
573
|
success: boolean;
|
|
480
574
|
transfer_id: string;
|
|
481
575
|
transfer_key?: string;
|
|
482
|
-
test_mode?: boolean;
|
|
483
576
|
chunk_size?: number;
|
|
484
577
|
total_size?: number;
|
|
485
578
|
total_sources?: number;
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -135,6 +135,7 @@ export declare class BeamTransferControl {
|
|
|
135
135
|
private readonly recoveryOperations;
|
|
136
136
|
private readonly recoveryRequestedEpoch;
|
|
137
137
|
private readonly runtimeEpochs;
|
|
138
|
+
private readonly runtimeCapabilities;
|
|
138
139
|
private helloTimer;
|
|
139
140
|
constructor(options: TransferControlOptions);
|
|
140
141
|
close(): Promise<void>;
|
|
@@ -155,6 +156,7 @@ export declare class BeamTransferControl {
|
|
|
155
156
|
splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: SignedChunkRoute[]): SignedChunkRoute[][];
|
|
156
157
|
private connection;
|
|
157
158
|
private ensureHelloMonitor;
|
|
159
|
+
supportsPerformanceV2(transferId: string): boolean;
|
|
158
160
|
private pollRuntimeHello;
|
|
159
161
|
private observeRuntimeEpoch;
|
|
160
162
|
private recoverLease;
|
package/dist/nats-control.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentSdkPerformance } from "./performance.js";
|
|
1
2
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
3
|
import { connect } from "nats";
|
|
3
4
|
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v7";
|
|
@@ -65,6 +66,7 @@ export class BeamTransferControl {
|
|
|
65
66
|
recoveryOperations = new Map();
|
|
66
67
|
recoveryRequestedEpoch = new Map();
|
|
67
68
|
runtimeEpochs = new Map();
|
|
69
|
+
runtimeCapabilities = new Map();
|
|
68
70
|
helloTimer = null;
|
|
69
71
|
constructor(options) {
|
|
70
72
|
this.apiKey = options.apiKey;
|
|
@@ -277,7 +279,12 @@ export class BeamTransferControl {
|
|
|
277
279
|
producer: "sdk",
|
|
278
280
|
payload
|
|
279
281
|
};
|
|
282
|
+
const encodeStarted = performance.now();
|
|
280
283
|
const bytes = encode(envelope);
|
|
284
|
+
if (messageType === "transfer.route_stream.batch") {
|
|
285
|
+
currentSdkPerformance()?.observe("sdk.batch_encode", performance.now() - encodeStarted);
|
|
286
|
+
currentSdkPerformance()?.gauge("batch_bytes_max", bytes.byteLength);
|
|
287
|
+
}
|
|
281
288
|
if (bytes.byteLength > this.maxPayloadBytes) {
|
|
282
289
|
throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
283
290
|
}
|
|
@@ -488,10 +495,15 @@ export class BeamTransferControl {
|
|
|
488
495
|
}, 5_000);
|
|
489
496
|
this.helloTimer.unref?.();
|
|
490
497
|
}
|
|
498
|
+
supportsPerformanceV2(transferId) {
|
|
499
|
+
const value = this.runtimeCapabilities.get(transferShardId(transferId, this.shardCount));
|
|
500
|
+
return !!value && value.until > Date.now() && value.capabilities.includes("sdk-performance/v2");
|
|
501
|
+
}
|
|
491
502
|
async pollRuntimeHello(shardId) {
|
|
492
503
|
try {
|
|
493
504
|
try {
|
|
494
|
-
await this.request("runtime.hello", { capabilities: ["integrity-signing/v1"] }, { idempotencyKey: `runtime:hello:${shardId}:integrity-signing`, shardId });
|
|
505
|
+
const hello = await this.request("runtime.hello", { capabilities: ["integrity-signing/v1", "sdk-performance/v2"] }, { idempotencyKey: `runtime:hello:${shardId}:integrity-signing`, shardId });
|
|
506
|
+
this.runtimeCapabilities.set(shardId, { until: Date.now() + 15_000, capabilities: hello.capabilities ?? [] });
|
|
495
507
|
}
|
|
496
508
|
catch {
|
|
497
509
|
await this.request("runtime.hello", {}, { idempotencyKey: `runtime:hello:${shardId}`, shardId });
|
|
@@ -510,6 +522,7 @@ export class BeamTransferControl {
|
|
|
510
522
|
return;
|
|
511
523
|
if (previous.runtimeEpoch === runtimeEpoch && previous.transportEpoch === transportEpoch)
|
|
512
524
|
return;
|
|
525
|
+
this.runtimeCapabilities.delete(shardId);
|
|
513
526
|
if (previous.runtimeEpoch !== runtimeEpoch) {
|
|
514
527
|
this.authToken = null;
|
|
515
528
|
this.authTokenExpiresAt = 0;
|
package/dist/performance.d.ts
CHANGED
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
import type { SdkPerformanceSummary } from './models.js';
|
|
2
|
+
export declare const currentSdkPerformance: () => SdkPerformanceCollector | undefined;
|
|
3
|
+
export declare const measureSdkPhase: <T>(name: string, work: () => Promise<T>) => Promise<T>;
|
|
4
|
+
/** Bounded identities only: no URLs, credentials or grant bodies. */
|
|
5
|
+
export declare class SourceSignatureHistory {
|
|
6
|
+
complete: boolean;
|
|
7
|
+
private readonly bits;
|
|
8
|
+
constructor(complete?: boolean);
|
|
9
|
+
created(index: number): boolean;
|
|
10
|
+
}
|
|
2
11
|
export declare class SdkPerformanceCollector {
|
|
12
|
+
private readonly sourceHistory?;
|
|
13
|
+
constructor(sourceHistory?: SourceSignatureHistory | undefined);
|
|
14
|
+
private sourceRenewals;
|
|
15
|
+
sourceCreated(index: number): void;
|
|
16
|
+
private readonly started;
|
|
17
|
+
private readonly details;
|
|
18
|
+
private readonly cpu;
|
|
19
|
+
private finished;
|
|
20
|
+
private delayTimer?;
|
|
3
21
|
private readonly measurements;
|
|
22
|
+
private readonly gauges;
|
|
23
|
+
private readonly milestones;
|
|
24
|
+
private dropped;
|
|
4
25
|
readonly counters: {
|
|
5
26
|
source_signatures: number;
|
|
6
27
|
source_reuses: number;
|
|
7
28
|
route_batches: number;
|
|
8
29
|
};
|
|
9
|
-
observe(name:
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
observe(name: string, milliseconds: number): void;
|
|
31
|
+
gauge(name: string, value: number): void;
|
|
32
|
+
increment(name: string): void;
|
|
33
|
+
seedDiscovery(other: SdkPerformanceCollector): void;
|
|
34
|
+
mark(name: string): void;
|
|
35
|
+
measure<T>(name: string, work: () => Promise<T>): Promise<T>;
|
|
36
|
+
startDelaySampling(): void;
|
|
37
|
+
finish(): void;
|
|
38
|
+
snapshot(v2?: boolean): SdkPerformanceSummary;
|
|
12
39
|
}
|
package/dist/performance.js
CHANGED
|
@@ -1,25 +1,124 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const NAMES = ["sdk.discovery", "sdk.multipart_create", "sdk.signing", "sdk.batch_ack", "sdk.buffer_wait", "sdk.preparation", "sdk.source_signing", "sdk.destination_signing", "sdk.source_grant_wait", "sdk.multipart_ready_wait", "sdk.multipart_provider", "sdk.multipart_queue", "sdk.multipart_callback", "sdk.manifest_ack", "sdk.provider_client_setup", "sdk.metadata_request", "sdk.route_assembly", "sdk.batch_encode", "sdk.batch_checksum", "sdk.batch_queue", "sdk.signing_queue", "sdk.process_cpu", "sdk.event_loop_delay", "sdk.producer_wait"];
|
|
3
|
+
const BOUNDS = [0.1, .5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 120000];
|
|
4
|
+
const GAUGES = new Set(["signing_configured_limit", "multipart_configured_limit", "signing_limit", "multipart_limit", "signing_active_peak", "signing_pending_peak", "multipart_active_peak", "buffered_batches_peak", "batch_routes_max", "batch_bytes_max", "concurrency_changes", "source_waiters", "provider_clients_created", "provider_clients_reused"]);
|
|
5
|
+
const MILESTONES = new Set(["first_manifest_ms", "first_route_ms", "first_batch_ms", "final_flush_ms", "prepared_ms"]);
|
|
6
|
+
const context = new AsyncLocalStorage();
|
|
7
|
+
let activeDelaySamplers = 0;
|
|
8
|
+
export const currentSdkPerformance = () => context.getStore();
|
|
9
|
+
export const measureSdkPhase = (name, work) => context.getStore()?.measure(name, work) ?? work();
|
|
10
|
+
/** Bounded identities only: no URLs, credentials or grant bodies. */
|
|
11
|
+
export class SourceSignatureHistory {
|
|
12
|
+
complete;
|
|
13
|
+
bits = new Uint8Array(16_384);
|
|
14
|
+
constructor(complete = true) {
|
|
15
|
+
this.complete = complete;
|
|
16
|
+
}
|
|
17
|
+
created(index) {
|
|
18
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= this.bits.length * 8) {
|
|
19
|
+
this.complete = false;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const byte = index >>> 3, mask = 1 << (index & 7);
|
|
23
|
+
const renewed = (this.bits[byte] & mask) !== 0;
|
|
24
|
+
this.bits[byte] |= mask;
|
|
25
|
+
return renewed;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
1
28
|
export class SdkPerformanceCollector {
|
|
29
|
+
sourceHistory;
|
|
30
|
+
constructor(sourceHistory) {
|
|
31
|
+
this.sourceHistory = sourceHistory;
|
|
32
|
+
}
|
|
33
|
+
sourceRenewals = 0;
|
|
34
|
+
sourceCreated(index) {
|
|
35
|
+
this.counters.source_signatures++;
|
|
36
|
+
if (this.sourceHistory?.created(index))
|
|
37
|
+
this.sourceRenewals++;
|
|
38
|
+
}
|
|
39
|
+
started = performance.now();
|
|
40
|
+
details = process.env.BEAM_SDK_PERFORMANCE_DETAILS !== 'false';
|
|
41
|
+
cpu = process.cpuUsage();
|
|
42
|
+
finished = false;
|
|
43
|
+
delayTimer;
|
|
2
44
|
measurements = new Map();
|
|
45
|
+
gauges = {};
|
|
46
|
+
milestones = {};
|
|
47
|
+
dropped = 0;
|
|
3
48
|
counters = { source_signatures: 0, source_reuses: 0, route_batches: 0 };
|
|
4
49
|
observe(name, milliseconds) {
|
|
5
|
-
|
|
50
|
+
const index = NAMES.indexOf(name);
|
|
51
|
+
if (index < 0 || (!this.details && index >= 6) || !Number.isFinite(milliseconds) || milliseconds < 0 || milliseconds > 86_400_000)
|
|
52
|
+
return;
|
|
53
|
+
const value = this.measurements.get(name) ?? { count: 0, work_ms: 0, max_ms: 0, histogram: Array(18).fill(0) };
|
|
54
|
+
if (value.count >= 1_000_000 || value.work_ms + milliseconds > 86_400_000) {
|
|
55
|
+
this.dropped++;
|
|
6
56
|
return;
|
|
7
|
-
|
|
57
|
+
}
|
|
8
58
|
value.count++;
|
|
9
59
|
value.work_ms += milliseconds;
|
|
10
60
|
value.max_ms = Math.max(value.max_ms, milliseconds);
|
|
61
|
+
const bin = BOUNDS.findIndex(bound => milliseconds <= bound);
|
|
62
|
+
value.histogram[bin < 0 ? 17 : bin]++;
|
|
11
63
|
this.measurements.set(name, value);
|
|
12
64
|
}
|
|
65
|
+
gauge(name, value) {
|
|
66
|
+
if (this.details && GAUGES.has(name) && Number.isFinite(value) && value >= 0)
|
|
67
|
+
this.gauges[name] = Math.min(1_000_000_000, Math.max(this.gauges[name] ?? 0, value));
|
|
68
|
+
}
|
|
69
|
+
increment(name) { this.gauge(name, (this.gauges[name] ?? 0) + 1); }
|
|
70
|
+
seedDiscovery(other) {
|
|
71
|
+
for (const name of ["sdk.discovery", "sdk.metadata_request", "sdk.provider_client_setup"]) {
|
|
72
|
+
const value = other.measurements.get(name);
|
|
73
|
+
if (value)
|
|
74
|
+
this.measurements.set(name, { ...value, histogram: [...value.histogram] });
|
|
75
|
+
}
|
|
76
|
+
for (const name of ["provider_clients_created", "provider_clients_reused"])
|
|
77
|
+
if (other.gauges[name] !== undefined)
|
|
78
|
+
this.gauges[name] = other.gauges[name];
|
|
79
|
+
}
|
|
80
|
+
mark(name) {
|
|
81
|
+
if (this.details && MILESTONES.has(name) && this.milestones[name] === undefined)
|
|
82
|
+
this.milestones[name] = performance.now() - this.started;
|
|
83
|
+
}
|
|
13
84
|
async measure(name, work) {
|
|
14
85
|
const start = performance.now();
|
|
15
86
|
try {
|
|
16
|
-
return await work
|
|
87
|
+
return await context.run(this, work);
|
|
17
88
|
}
|
|
18
89
|
finally {
|
|
19
90
|
this.observe(name, performance.now() - start);
|
|
20
91
|
}
|
|
21
92
|
}
|
|
22
|
-
|
|
23
|
-
|
|
93
|
+
startDelaySampling() {
|
|
94
|
+
if (!this.details || this.delayTimer || this.finished || activeDelaySamplers >= 16)
|
|
95
|
+
return;
|
|
96
|
+
activeDelaySamplers++;
|
|
97
|
+
let last = performance.now();
|
|
98
|
+
this.delayTimer = setInterval(() => {
|
|
99
|
+
const now = performance.now();
|
|
100
|
+
this.observe('sdk.event_loop_delay', Math.max(0, now - last - 100));
|
|
101
|
+
last = now;
|
|
102
|
+
}, 100);
|
|
103
|
+
this.delayTimer.unref();
|
|
104
|
+
}
|
|
105
|
+
finish() {
|
|
106
|
+
if (this.finished)
|
|
107
|
+
return;
|
|
108
|
+
this.finished = true;
|
|
109
|
+
if (this.delayTimer) {
|
|
110
|
+
clearInterval(this.delayTimer);
|
|
111
|
+
this.delayTimer = undefined;
|
|
112
|
+
activeDelaySamplers--;
|
|
113
|
+
}
|
|
114
|
+
const used = process.cpuUsage(this.cpu);
|
|
115
|
+
this.observe('sdk.process_cpu', (used.user + used.system) / 1000);
|
|
116
|
+
this.mark('prepared_ms');
|
|
117
|
+
}
|
|
118
|
+
snapshot(v2 = true) {
|
|
119
|
+
const measurements = [...this.measurements].filter(([name]) => v2 || NAMES.slice(0, 6).includes(name))
|
|
120
|
+
.map(([name, value]) => ({ name, count: value.count, work_ms: value.work_ms, max_ms: value.max_ms, ...(v2 ? { histogram: [...value.histogram] } : {}) }));
|
|
121
|
+
return { schema_version: v2 ? 'sdk-performance/v2' : 'sdk-performance/v1', measurements, counters: { ...this.counters, ...(v2 && this.sourceHistory?.complete ? { source_renewals: this.sourceRenewals } : {}) },
|
|
122
|
+
...(v2 ? { gauges: { ...this.gauges }, milestones: { ...this.milestones }, unmeasured: NAMES.slice(6).filter(name => !this.measurements.has(name)), detail_dropped: this.dropped } : {}) };
|
|
24
123
|
}
|
|
25
124
|
}
|
package/dist/provider-signing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentSdkPerformance, measureSdkPhase } from "./performance.js";
|
|
1
2
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
3
|
import { UploadPartCopyCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
3
4
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
@@ -82,7 +83,7 @@ export async function prepareProviderSource(source, options = {}) {
|
|
|
82
83
|
if (isS3CompatibleProvider(source)) {
|
|
83
84
|
const endpoint = s3CompatibleEndpoint(source);
|
|
84
85
|
const client = createS3CompatibleClient(source, endpoint);
|
|
85
|
-
const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal });
|
|
86
|
+
const head = await measureSdkPhase("sdk.metadata_request", () => client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal }));
|
|
86
87
|
const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
|
|
87
88
|
return {
|
|
88
89
|
source_id: source.id ?? `src_${index}`,
|
|
@@ -113,7 +114,7 @@ export async function prepareProviderSource(source, options = {}) {
|
|
|
113
114
|
size,
|
|
114
115
|
filename: filename(source.key),
|
|
115
116
|
expires_at: expiresAtIso(expiresIn),
|
|
116
|
-
metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
|
|
117
|
+
metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
if (isHuggingFaceProvider(source)) {
|
|
@@ -161,7 +162,7 @@ export async function prepareProviderSourceForPlan(source, options = {}) {
|
|
|
161
162
|
provider: "hippius",
|
|
162
163
|
size,
|
|
163
164
|
filename: filename(source.key),
|
|
164
|
-
metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
|
|
165
|
+
metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
if (isHuggingFaceProvider(source)) {
|
|
@@ -199,6 +200,7 @@ export function prepareProviderDestination(destination, options = {}) {
|
|
|
199
200
|
metadata: {
|
|
200
201
|
bucket: destination.bucket,
|
|
201
202
|
key: destination.key,
|
|
203
|
+
storage_location: destination.storage_location,
|
|
202
204
|
base_url: destination.base_url ?? "https://api.hippius.com"
|
|
203
205
|
}
|
|
204
206
|
};
|
|
@@ -336,14 +338,14 @@ export async function signDestinationRoute(input) {
|
|
|
336
338
|
}
|
|
337
339
|
const destinationExpiresAt = expiresAtIso(input.expiresIn);
|
|
338
340
|
const [destUrl, sourceRoute] = await Promise.all([
|
|
339
|
-
input.destUrl ?? signDestinationUrl({
|
|
341
|
+
input.destUrl ?? measureSdkPhase("sdk.destination_signing", () => signDestinationUrl({
|
|
340
342
|
destination: input.destination,
|
|
341
343
|
objectKey: targetObjectKey,
|
|
342
344
|
uploadId: input.uploadId,
|
|
343
345
|
partNumber: input.partNumber,
|
|
344
346
|
expiresIn: input.expiresIn,
|
|
345
347
|
fetchImpl: input.fetchImpl,
|
|
346
|
-
}),
|
|
348
|
+
})),
|
|
347
349
|
input.sourceGrant ?? signSourceChunk({
|
|
348
350
|
source: input.source,
|
|
349
351
|
fallbackUrl: input.chunk.source_url,
|
|
@@ -522,8 +524,11 @@ function s3ClientCacheKey(source, endpoint) {
|
|
|
522
524
|
function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
523
525
|
const cacheKey = s3ClientCacheKey(source, endpoint);
|
|
524
526
|
const cached = s3ClientCache.get(source);
|
|
525
|
-
if (cached?.identity === cacheKey)
|
|
527
|
+
if (cached?.identity === cacheKey) {
|
|
528
|
+
currentSdkPerformance()?.increment("provider_clients_reused");
|
|
526
529
|
return cached.client;
|
|
530
|
+
}
|
|
531
|
+
const setupStarted = performance.now();
|
|
527
532
|
const client = new S3Client({
|
|
528
533
|
region: s3CompatibleRegion(source),
|
|
529
534
|
endpoint,
|
|
@@ -536,6 +541,8 @@ function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source
|
|
|
536
541
|
}
|
|
537
542
|
});
|
|
538
543
|
s3ClientCache.set(source, { identity: cacheKey, client });
|
|
544
|
+
currentSdkPerformance()?.increment("provider_clients_created");
|
|
545
|
+
currentSdkPerformance()?.observe("sdk.provider_client_setup", performance.now() - setupStarted);
|
|
539
546
|
return client;
|
|
540
547
|
}
|
|
541
548
|
export function isS3CompatibleProvider(config) {
|
|
@@ -586,6 +593,7 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
|
586
593
|
bucket: source.bucket,
|
|
587
594
|
key: source.key,
|
|
588
595
|
region: s3CompatibleRegion(source),
|
|
596
|
+
storage_location: source.storage_location,
|
|
589
597
|
endpoint_url: endpoint,
|
|
590
598
|
account_id: "account_id" in source ? source.account_id : undefined
|
|
591
599
|
});
|
|
@@ -599,6 +607,7 @@ export function isHuggingFaceProvider(config) {
|
|
|
599
607
|
function huggingFaceMetadata(config, etag, commitHash) {
|
|
600
608
|
return compactMetadata({
|
|
601
609
|
driver: "huggingface",
|
|
610
|
+
storage_location: config.storage_location,
|
|
602
611
|
repo_id: config.repo_id,
|
|
603
612
|
repo_type: huggingFaceRepoType(config),
|
|
604
613
|
revision: huggingFaceRevision(config),
|