@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/models.d.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
export type SignedUrlFlow = "signed_url";
|
|
2
|
+
export interface SdkPerformanceSummary {
|
|
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";
|
|
8
|
+
measurements: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
count: number;
|
|
11
|
+
work_ms: number;
|
|
12
|
+
max_ms: number;
|
|
13
|
+
histogram?: number[];
|
|
14
|
+
}>;
|
|
15
|
+
counters: {
|
|
16
|
+
source_signatures: number;
|
|
17
|
+
source_reuses: number;
|
|
18
|
+
route_batches: number;
|
|
19
|
+
source_renewals?: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
2
22
|
export interface BeamClientOptions {
|
|
23
|
+
/** Best effort, at most one bounded summary per route stream. Never awaited. */
|
|
24
|
+
onDiagnostics?: (summary: SdkPerformanceSummary) => void | Promise<void>;
|
|
3
25
|
apiKey?: string;
|
|
4
26
|
natsUrl?: string;
|
|
5
27
|
natsWsUrl?: string;
|
|
@@ -141,7 +163,50 @@ export interface IntegrityAuditChallenge {
|
|
|
141
163
|
range_bytes: number;
|
|
142
164
|
chunks: IntegrityAuditChallengeChunk[];
|
|
143
165
|
}
|
|
166
|
+
export interface TransferPerformance {
|
|
167
|
+
recovery_bandwidth?: {
|
|
168
|
+
excluded_tasks: number;
|
|
169
|
+
accepted_bytes: number;
|
|
170
|
+
};
|
|
171
|
+
deadline?: {
|
|
172
|
+
version: "assignment-deadline/v1";
|
|
173
|
+
decisions: number;
|
|
174
|
+
selected_min_ms: number;
|
|
175
|
+
selected_max_ms: number;
|
|
176
|
+
estimated_max_ms: number;
|
|
177
|
+
sample_count_max: number;
|
|
178
|
+
data_age_max_ms: number | null;
|
|
179
|
+
reasons: Record<string, number>;
|
|
180
|
+
};
|
|
181
|
+
sdk_detail?: SdkPerformanceSummary;
|
|
182
|
+
schema_version: "transfer-performance/v1";
|
|
183
|
+
runtime_epoch: string;
|
|
184
|
+
coverage: "complete" | "restarted";
|
|
185
|
+
elapsed_ms: number;
|
|
186
|
+
/** Work can overlap; these durations must not be summed into elapsed time. */
|
|
187
|
+
measurements: Record<string, {
|
|
188
|
+
count: number;
|
|
189
|
+
work_ms: number;
|
|
190
|
+
max_ms: number;
|
|
191
|
+
}>;
|
|
192
|
+
events: Record<string, {
|
|
193
|
+
count: number;
|
|
194
|
+
first_ms: number;
|
|
195
|
+
last_ms: number;
|
|
196
|
+
}>;
|
|
197
|
+
sdk_counters: {
|
|
198
|
+
source_signatures: number;
|
|
199
|
+
source_reuses: number;
|
|
200
|
+
route_batches: number;
|
|
201
|
+
source_renewals?: number;
|
|
202
|
+
};
|
|
203
|
+
detail_dropped: number;
|
|
204
|
+
unmeasured?: string[];
|
|
205
|
+
measurement_clocks?: Record<string, "runtime" | "sdk" | "fraud-service">;
|
|
206
|
+
sdk_report_received?: boolean;
|
|
207
|
+
}
|
|
144
208
|
export interface TransferStatusInfo {
|
|
209
|
+
performance?: TransferPerformance | null;
|
|
145
210
|
transfer_id: string;
|
|
146
211
|
name?: string;
|
|
147
212
|
status: string;
|
|
@@ -190,6 +255,8 @@ export interface TransferTerminalSignalWaiter {
|
|
|
190
255
|
close(): Promise<void>;
|
|
191
256
|
}
|
|
192
257
|
export interface S3ProviderConfig {
|
|
258
|
+
/** Physical storage location; independent of the signing region. */
|
|
259
|
+
storage_location?: string;
|
|
193
260
|
provider: "s3";
|
|
194
261
|
id?: string;
|
|
195
262
|
bucket: string;
|
|
@@ -201,6 +268,8 @@ export interface S3ProviderConfig {
|
|
|
201
268
|
endpoint_url?: string;
|
|
202
269
|
}
|
|
203
270
|
export interface R2ProviderConfig {
|
|
271
|
+
/** Physical storage location; independent of the signing region. */
|
|
272
|
+
storage_location?: string;
|
|
204
273
|
provider: "r2";
|
|
205
274
|
id?: string;
|
|
206
275
|
bucket: string;
|
|
@@ -211,6 +280,8 @@ export interface R2ProviderConfig {
|
|
|
211
280
|
endpoint_url?: string;
|
|
212
281
|
}
|
|
213
282
|
export interface S3CompatibleProviderConfig {
|
|
283
|
+
/** Physical storage location; independent of the signing region. */
|
|
284
|
+
storage_location?: string;
|
|
214
285
|
provider: string;
|
|
215
286
|
driver?: "s3-compatible";
|
|
216
287
|
id?: string;
|
|
@@ -225,6 +296,8 @@ export interface S3CompatibleProviderConfig {
|
|
|
225
296
|
account_id?: string;
|
|
226
297
|
}
|
|
227
298
|
export interface HippiusProviderConfig {
|
|
299
|
+
/** Physical storage location; independent of the signing region. */
|
|
300
|
+
storage_location?: string;
|
|
228
301
|
provider: "hippius";
|
|
229
302
|
id?: string;
|
|
230
303
|
bucket: string;
|
|
@@ -234,6 +307,8 @@ export interface HippiusProviderConfig {
|
|
|
234
307
|
}
|
|
235
308
|
export type HuggingFaceRepoType = "model" | "dataset" | "space" | "kernel" | "bucket";
|
|
236
309
|
export interface HuggingFaceProviderConfig {
|
|
310
|
+
/** Physical storage location; independent of the signing region. */
|
|
311
|
+
storage_location?: string;
|
|
237
312
|
provider: "huggingface";
|
|
238
313
|
id?: string;
|
|
239
314
|
/** Namespace and repo or bucket name separated by a slash, for example `org/dataset`. */
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IntegrityAuditChallenge } from "./models.js";
|
|
1
2
|
import { type ConnectionOptions } from "nats";
|
|
2
3
|
import type { MultipartGroupManifest, SignedChunkRoute, TransferTerminalSignalWaiter } from "./models.js";
|
|
3
4
|
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v7";
|
|
@@ -134,18 +135,28 @@ export declare class BeamTransferControl {
|
|
|
134
135
|
private readonly recoveryOperations;
|
|
135
136
|
private readonly recoveryRequestedEpoch;
|
|
136
137
|
private readonly runtimeEpochs;
|
|
138
|
+
private readonly runtimeCapabilities;
|
|
137
139
|
private helloTimer;
|
|
138
140
|
constructor(options: TransferControlOptions);
|
|
139
141
|
close(): Promise<void>;
|
|
142
|
+
serveIntegritySigner(transferId: string, handler: (challenge: IntegrityAuditChallenge) => Promise<Record<string, unknown>>): Promise<() => void>;
|
|
140
143
|
serveRouteRecoverySigner(transferId: string, handler: (payload: RouteRecoverySignRequestPayload) => Promise<RouteRecoverySignReplyPayload>): Promise<() => void>;
|
|
141
144
|
registerRecoveryLease(lease: TransferRecoveryLease): () => void;
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Release a transfer's recovery lease, disposing its retained secrets.
|
|
147
|
+
*
|
|
148
|
+
* With `owner`, release only when that exact lease is still registered, so a
|
|
149
|
+
* fenced-off owner can never release a replacement owner's lease.
|
|
150
|
+
*/
|
|
151
|
+
releaseRecoveryLease(transferId: string, owner?: TransferRecoveryLease): void;
|
|
152
|
+
/** Request recovery for the registered lease; with `owner`, only if that lease is still registered. */
|
|
153
|
+
continueRecoveryLease(transferId: string, owner?: TransferRecoveryLease): void;
|
|
144
154
|
request<T>(messageType: TransferClientMessageType, payload: Record<string, unknown>, options?: LifecycleRequestOptions): Promise<T>;
|
|
145
155
|
openTerminalSignalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
146
156
|
splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: SignedChunkRoute[]): SignedChunkRoute[][];
|
|
147
157
|
private connection;
|
|
148
158
|
private ensureHelloMonitor;
|
|
159
|
+
supportsPerformanceV2(transferId: string): boolean;
|
|
149
160
|
private pollRuntimeHello;
|
|
150
161
|
private observeRuntimeEpoch;
|
|
151
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";
|
|
@@ -60,9 +61,12 @@ export class BeamTransferControl {
|
|
|
60
61
|
terminalWaitCancellations = new Set();
|
|
61
62
|
routeRecoverySignerCancellations = new Set();
|
|
62
63
|
recoveryLeases = new Map();
|
|
64
|
+
// Keyed by lease identity: a replacement owner's recovery must never be
|
|
65
|
+
// coalesced into (and silently dropped behind) a fenced-off owner's loop.
|
|
63
66
|
recoveryOperations = new Map();
|
|
64
67
|
recoveryRequestedEpoch = new Map();
|
|
65
68
|
runtimeEpochs = new Map();
|
|
69
|
+
runtimeCapabilities = new Map();
|
|
66
70
|
helloTimer = null;
|
|
67
71
|
constructor(options) {
|
|
68
72
|
this.apiKey = options.apiKey;
|
|
@@ -106,6 +110,58 @@ export class BeamTransferControl {
|
|
|
106
110
|
if (nc && !nc.isClosed())
|
|
107
111
|
await nc.drain();
|
|
108
112
|
}
|
|
113
|
+
async serveIntegritySigner(transferId, handler) {
|
|
114
|
+
const nc = await this.connection();
|
|
115
|
+
const subject = this.routeRecoverySignSubject(transferId).replace(/route_recovery_sign$/, "integrity_sign");
|
|
116
|
+
let active = true;
|
|
117
|
+
let pending = null;
|
|
118
|
+
let auditId = "";
|
|
119
|
+
let challengeFingerprint = "";
|
|
120
|
+
let awaiting = 0;
|
|
121
|
+
const subscription = nc.subscribe(subject, { callback: (error, message) => {
|
|
122
|
+
if (!active || error || !message.reply)
|
|
123
|
+
return;
|
|
124
|
+
if (awaiting >= 32) {
|
|
125
|
+
message.respond(new TextEncoder().encode('{"retry":true}'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
awaiting++;
|
|
129
|
+
void (async () => {
|
|
130
|
+
try {
|
|
131
|
+
const request = JSON.parse(new TextDecoder().decode(message.data));
|
|
132
|
+
if (request.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION || request.capability !== "integrity-signing/v1"
|
|
133
|
+
|| request.producer !== "transfer-runtime" || request.environment !== this.environment || request.key_prefix !== this.keyPrefix
|
|
134
|
+
|| request.transfer_id !== transferId || request.challenge?.transfer_id !== transferId || typeof request.request_id !== "string")
|
|
135
|
+
throw new Error("integrity_sign_scope_mismatch");
|
|
136
|
+
const fingerprint = JSON.stringify(request.challenge);
|
|
137
|
+
if (pending && (auditId !== request.challenge.audit_id || challengeFingerprint !== fingerprint))
|
|
138
|
+
throw new Error("integrity_sign_conflicting_audit");
|
|
139
|
+
if (!pending) {
|
|
140
|
+
auditId = request.challenge.audit_id;
|
|
141
|
+
challengeFingerprint = fingerprint;
|
|
142
|
+
pending = handler(request.challenge).finally(() => { pending = null; });
|
|
143
|
+
}
|
|
144
|
+
const payload = await pending;
|
|
145
|
+
if (active)
|
|
146
|
+
message.respond(new TextEncoder().encode(JSON.stringify({ schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
|
|
147
|
+
capability: "integrity-signing/v1", environment: this.environment, key_prefix: this.keyPrefix,
|
|
148
|
+
transfer_id: transferId, request_id: request.request_id, payload })));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
message.respond(new TextEncoder().encode('{"retry":true}'));
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
awaiting--;
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
} });
|
|
158
|
+
await nc.flush();
|
|
159
|
+
const stop = () => { if (!active)
|
|
160
|
+
return; active = false; subscription.unsubscribe(); this.routeRecoverySignerCancellations.delete(stop); };
|
|
161
|
+
this.routeRecoverySignerCancellations.add(stop);
|
|
162
|
+
void this.pollRuntimeHello(transferShardId(transferId, this.shardCount));
|
|
163
|
+
return stop;
|
|
164
|
+
}
|
|
109
165
|
async serveRouteRecoverySigner(transferId, handler) {
|
|
110
166
|
const nc = await this.connection();
|
|
111
167
|
const subject = this.routeRecoverySignSubject(transferId);
|
|
@@ -165,23 +221,20 @@ export class BeamTransferControl {
|
|
|
165
221
|
this.ensureHelloMonitor();
|
|
166
222
|
const shardId = transferShardId(lease.transferId, this.shardCount);
|
|
167
223
|
void this.pollRuntimeHello(shardId);
|
|
168
|
-
return () =>
|
|
169
|
-
const current = this.recoveryLeases.get(lease.transferId);
|
|
170
|
-
if (current !== lease)
|
|
171
|
-
return;
|
|
172
|
-
this.recoveryLeases.delete(lease.transferId);
|
|
173
|
-
this.recoveryRequestedEpoch.delete(lease.transferId);
|
|
174
|
-
lease.disposeSecrets?.();
|
|
175
|
-
if (!this.recoveryLeases.size && this.helloTimer) {
|
|
176
|
-
clearInterval(this.helloTimer);
|
|
177
|
-
this.helloTimer = null;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
224
|
+
return () => this.releaseRecoveryLease(lease.transferId, lease);
|
|
180
225
|
}
|
|
181
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Release a transfer's recovery lease, disposing its retained secrets.
|
|
228
|
+
*
|
|
229
|
+
* With `owner`, release only when that exact lease is still registered, so a
|
|
230
|
+
* fenced-off owner can never release a replacement owner's lease.
|
|
231
|
+
*/
|
|
232
|
+
releaseRecoveryLease(transferId, owner) {
|
|
182
233
|
const lease = this.recoveryLeases.get(transferId);
|
|
183
234
|
if (!lease)
|
|
184
235
|
return;
|
|
236
|
+
if (owner && lease !== owner)
|
|
237
|
+
return;
|
|
185
238
|
this.recoveryLeases.delete(transferId);
|
|
186
239
|
this.recoveryRequestedEpoch.delete(transferId);
|
|
187
240
|
lease.disposeSecrets?.();
|
|
@@ -190,10 +243,13 @@ export class BeamTransferControl {
|
|
|
190
243
|
this.helloTimer = null;
|
|
191
244
|
}
|
|
192
245
|
}
|
|
193
|
-
|
|
246
|
+
/** Request recovery for the registered lease; with `owner`, only if that lease is still registered. */
|
|
247
|
+
continueRecoveryLease(transferId, owner) {
|
|
194
248
|
const lease = this.recoveryLeases.get(transferId);
|
|
195
249
|
if (!lease || this.closed)
|
|
196
250
|
return;
|
|
251
|
+
if (owner && lease !== owner)
|
|
252
|
+
return;
|
|
197
253
|
const shardId = transferShardId(transferId, this.shardCount);
|
|
198
254
|
const epoch = this.runtimeEpochs.get(shardId);
|
|
199
255
|
this.recoveryRequestedEpoch.set(transferId, epoch ? `${epoch.runtimeEpoch}:${epoch.transportEpoch}` : `foreground:${randomId()}`);
|
|
@@ -223,7 +279,12 @@ export class BeamTransferControl {
|
|
|
223
279
|
producer: "sdk",
|
|
224
280
|
payload
|
|
225
281
|
};
|
|
282
|
+
const encodeStarted = performance.now();
|
|
226
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
|
+
}
|
|
227
288
|
if (bytes.byteLength > this.maxPayloadBytes) {
|
|
228
289
|
throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
|
|
229
290
|
}
|
|
@@ -434,9 +495,19 @@ export class BeamTransferControl {
|
|
|
434
495
|
}, 5_000);
|
|
435
496
|
this.helloTimer.unref?.();
|
|
436
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
|
+
}
|
|
437
502
|
async pollRuntimeHello(shardId) {
|
|
438
503
|
try {
|
|
439
|
-
|
|
504
|
+
try {
|
|
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 ?? [] });
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
await this.request("runtime.hello", {}, { idempotencyKey: `runtime:hello:${shardId}`, shardId });
|
|
510
|
+
}
|
|
440
511
|
}
|
|
441
512
|
catch {
|
|
442
513
|
// Infinite NATS reconnect continues in the background; the next hello reconciles epochs.
|
|
@@ -451,6 +522,7 @@ export class BeamTransferControl {
|
|
|
451
522
|
return;
|
|
452
523
|
if (previous.runtimeEpoch === runtimeEpoch && previous.transportEpoch === transportEpoch)
|
|
453
524
|
return;
|
|
525
|
+
this.runtimeCapabilities.delete(shardId);
|
|
454
526
|
if (previous.runtimeEpoch !== runtimeEpoch) {
|
|
455
527
|
this.authToken = null;
|
|
456
528
|
this.authTokenExpiresAt = 0;
|
|
@@ -463,13 +535,14 @@ export class BeamTransferControl {
|
|
|
463
535
|
}
|
|
464
536
|
}
|
|
465
537
|
recoverLease(lease) {
|
|
466
|
-
const existing = this.recoveryOperations.get(lease
|
|
538
|
+
const existing = this.recoveryOperations.get(lease);
|
|
467
539
|
if (existing)
|
|
468
540
|
return existing;
|
|
469
|
-
|
|
470
|
-
|
|
541
|
+
const owns = () => !this.closed && this.recoveryLeases.get(lease.transferId) === lease;
|
|
542
|
+
let replaced = false;
|
|
543
|
+
const operation = (async () => {
|
|
471
544
|
let attempt = 0;
|
|
472
|
-
while (
|
|
545
|
+
while (owns()) {
|
|
473
546
|
const requestedEpoch = this.recoveryRequestedEpoch.get(lease.transferId);
|
|
474
547
|
const routeGenerationId = randomId();
|
|
475
548
|
try {
|
|
@@ -479,39 +552,53 @@ export class BeamTransferControl {
|
|
|
479
552
|
coordinate_checksum: lease.coordinateChecksum,
|
|
480
553
|
route_generation_id: routeGenerationId
|
|
481
554
|
}, { transferId: lease.transferId, idempotencyKey: `transfer:${lease.transferId}:resume:${routeGenerationId}` });
|
|
555
|
+
if (!owns()) {
|
|
556
|
+
// A fenced-off owner must neither act on nor release a replacement's lease.
|
|
557
|
+
replaced = true;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
482
560
|
if (response.recovery === "terminal") {
|
|
483
|
-
this.releaseRecoveryLease(lease.transferId);
|
|
484
|
-
if (this.recoveryOperations.get(lease.transferId) === operation) {
|
|
485
|
-
this.recoveryOperations.delete(lease.transferId);
|
|
486
|
-
}
|
|
561
|
+
this.releaseRecoveryLease(lease.transferId, lease);
|
|
487
562
|
return;
|
|
488
563
|
}
|
|
489
564
|
if (response.route_replay_required || response.recovery === "route_replay_required") {
|
|
490
565
|
await lease.replayRoutes(routeGenerationId);
|
|
566
|
+
if (!owns()) {
|
|
567
|
+
replaced = true;
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
491
570
|
}
|
|
492
571
|
if (this.recoveryRequestedEpoch.get(lease.transferId) !== requestedEpoch) {
|
|
493
572
|
attempt = 0;
|
|
494
573
|
continue;
|
|
495
574
|
}
|
|
496
|
-
if (this.recoveryOperations.get(lease.transferId) === operation) {
|
|
497
|
-
this.recoveryOperations.delete(lease.transferId);
|
|
498
|
-
}
|
|
499
575
|
return;
|
|
500
576
|
}
|
|
501
577
|
catch (error) {
|
|
578
|
+
if (!owns()) {
|
|
579
|
+
replaced = true;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
502
582
|
if (!isRetryableLifecycleError(error)) {
|
|
503
|
-
this.releaseRecoveryLease(lease.transferId);
|
|
583
|
+
this.releaseRecoveryLease(lease.transferId, lease);
|
|
504
584
|
return;
|
|
505
585
|
}
|
|
506
586
|
attempt += 1;
|
|
507
587
|
await sleepWithJitter(Math.min(30_000, 500 * (2 ** Math.min(attempt, 6))));
|
|
508
588
|
}
|
|
509
589
|
}
|
|
590
|
+
replaced = !this.closed;
|
|
510
591
|
})().finally(() => {
|
|
511
|
-
if (this.recoveryOperations.get(lease
|
|
512
|
-
this.recoveryOperations.delete(lease
|
|
592
|
+
if (this.recoveryOperations.get(lease) === operation)
|
|
593
|
+
this.recoveryOperations.delete(lease);
|
|
594
|
+
if (!replaced || this.closed)
|
|
595
|
+
return;
|
|
596
|
+
// The recovery this loop was driving still matters: hand it to the replacement owner.
|
|
597
|
+
const current = this.recoveryLeases.get(lease.transferId);
|
|
598
|
+
if (current && current !== lease)
|
|
599
|
+
void this.recoverLease(current);
|
|
513
600
|
});
|
|
514
|
-
this.recoveryOperations.set(lease
|
|
601
|
+
this.recoveryOperations.set(lease, operation);
|
|
515
602
|
return operation;
|
|
516
603
|
}
|
|
517
604
|
async resolveAuthToken() {
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
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?;
|
|
21
|
+
private readonly measurements;
|
|
22
|
+
private readonly gauges;
|
|
23
|
+
private readonly milestones;
|
|
24
|
+
private dropped;
|
|
25
|
+
readonly counters: {
|
|
26
|
+
source_signatures: number;
|
|
27
|
+
source_reuses: number;
|
|
28
|
+
route_batches: number;
|
|
29
|
+
};
|
|
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;
|
|
39
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
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;
|
|
44
|
+
measurements = new Map();
|
|
45
|
+
gauges = {};
|
|
46
|
+
milestones = {};
|
|
47
|
+
dropped = 0;
|
|
48
|
+
counters = { source_signatures: 0, source_reuses: 0, route_batches: 0 };
|
|
49
|
+
observe(name, milliseconds) {
|
|
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++;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
value.count++;
|
|
59
|
+
value.work_ms += milliseconds;
|
|
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]++;
|
|
63
|
+
this.measurements.set(name, value);
|
|
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
|
+
}
|
|
84
|
+
async measure(name, work) {
|
|
85
|
+
const start = performance.now();
|
|
86
|
+
try {
|
|
87
|
+
return await context.run(this, work);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
this.observe(name, performance.now() - start);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
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 } : {}) };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -63,7 +63,15 @@ export declare function inspectDestinationObject(destination: ProviderDestinatio
|
|
|
63
63
|
versionId: string | undefined;
|
|
64
64
|
metadata: Record<string, string>;
|
|
65
65
|
}>;
|
|
66
|
+
export declare function boundedGrantExpiry(url: string, upperBound: string): string;
|
|
67
|
+
export type SourceRouteGrant = {
|
|
68
|
+
url: string;
|
|
69
|
+
headers: Record<string, string>;
|
|
70
|
+
expiresAt: string;
|
|
71
|
+
};
|
|
72
|
+
export declare function signSourceChunk(input: Parameters<typeof signSourceRoute>[0]): Promise<SourceRouteGrant>;
|
|
66
73
|
export declare function signDestinationRoute(input: {
|
|
74
|
+
sourceGrant?: Promise<SourceRouteGrant>;
|
|
67
75
|
chunk: ChunkSigningPlanItem;
|
|
68
76
|
target: ChunkDestinationSigningTarget;
|
|
69
77
|
source?: ProviderSourceConfig;
|
|
@@ -109,6 +117,16 @@ export declare function signDestinationReadRange(input: {
|
|
|
109
117
|
url: string;
|
|
110
118
|
headers: Record<string, string>;
|
|
111
119
|
}>;
|
|
120
|
+
declare function signSourceRoute(input: {
|
|
121
|
+
source?: ProviderSourceConfig;
|
|
122
|
+
fallbackUrl: string;
|
|
123
|
+
chunk: ChunkSigningPlanItem;
|
|
124
|
+
expiresIn: number;
|
|
125
|
+
fetchImpl?: typeof fetch;
|
|
126
|
+
}): Promise<{
|
|
127
|
+
url: string;
|
|
128
|
+
headers: Record<string, string>;
|
|
129
|
+
}>;
|
|
112
130
|
export declare function signDestinationUrl(input: {
|
|
113
131
|
contentMd5?: string;
|
|
114
132
|
destination: ProviderDestinationConfig;
|
|
@@ -118,8 +136,10 @@ export declare function signDestinationUrl(input: {
|
|
|
118
136
|
expiresIn: number;
|
|
119
137
|
fetchImpl?: typeof fetch;
|
|
120
138
|
}): Promise<string>;
|
|
139
|
+
export declare function releaseProviderClients(configs: Array<ProviderSourceConfig | ProviderDestinationConfig>): void;
|
|
121
140
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
122
141
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
|
123
142
|
export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
|
|
124
143
|
export declare function s3CompatibleForcePathStyle(source: AnyS3CompatibleProviderConfig, endpoint?: string | undefined): boolean | undefined;
|
|
125
144
|
export declare function isHuggingFaceProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is HuggingFaceProviderConfig;
|
|
145
|
+
export {};
|