deepline 0.3.127 → 0.3.129
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/dist/bundling-sources/sdk/src/client.ts +34 -9
- package/dist/bundling-sources/sdk/src/http.ts +22 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
- package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
- package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
- package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
- package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
- package/dist/cli/index.js +254 -38
- package/dist/cli/index.mjs +254 -38
- package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
- package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +231 -35
- package/dist/index.mjs +231 -35
- package/dist/install-integrity.json +31 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +65 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts
CHANGED
|
@@ -4,10 +4,13 @@ import {
|
|
|
4
4
|
releaseRateStateViaAppRuntime,
|
|
5
5
|
type WorkerRuntimeApiContext,
|
|
6
6
|
} from '../app-runtime-api';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
|
|
9
10
|
noopPacingPermit,
|
|
10
11
|
type PacingPermit,
|
|
12
|
+
type ProviderConcurrencyGrant,
|
|
13
|
+
type ProviderPacingGrant,
|
|
11
14
|
type PacingRule,
|
|
12
15
|
type RateStateBackend,
|
|
13
16
|
type RateStateBackendKind,
|
|
@@ -29,6 +32,13 @@ import {
|
|
|
29
32
|
* so keep their sleep-toward-it behavior byte-for-byte.
|
|
30
33
|
*/
|
|
31
34
|
export const PROVIDER_EXHAUSTED_MAX_WAIT_MS = 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* Total time one acquirer may spend cycling through window-exhausted refills.
|
|
37
|
+
* Each refill's sleep is bounded, but nothing bounded how many of them a row
|
|
38
|
+
* could take; with several runs sharing one org's provider window a late
|
|
39
|
+
* starter parked here for twenty minutes with its core idle.
|
|
40
|
+
*/
|
|
41
|
+
export const RATE_ACQUIRE_MAX_WAIT_MS = 300_000;
|
|
32
42
|
|
|
33
43
|
/**
|
|
34
44
|
* Permits leased per app-runtime round trip for the direct Node substrate.
|
|
@@ -73,6 +83,12 @@ export const APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE = 8;
|
|
|
73
83
|
const LEASE_BLOCK_MIN_TTL_MS = 250;
|
|
74
84
|
const LEASE_BLOCK_MAX_TTL_MS = 5_000;
|
|
75
85
|
const MAX_ACQUIRE_SLEEP_MS = 5_000;
|
|
86
|
+
/**
|
|
87
|
+
* Horizon used to size the next lease block from the rate actually in force.
|
|
88
|
+
* Kept short so a block stays inside its own TTL and a stalled runner can never
|
|
89
|
+
* hold more than a moment of a shared provider window.
|
|
90
|
+
*/
|
|
91
|
+
const RATE_BLOCK_HORIZON_MS = 1_000;
|
|
76
92
|
const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
|
|
77
93
|
/**
|
|
78
94
|
* Jitter fraction applied to a server-supplied `waitMs` so a synchronized wave
|
|
@@ -90,13 +106,30 @@ export interface RateStateCooldown {
|
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
interface LeasedBlock {
|
|
93
|
-
permits: Array<
|
|
109
|
+
permits: Array<
|
|
110
|
+
[
|
|
111
|
+
scheduledAtMs: number,
|
|
112
|
+
leaseId: string | null,
|
|
113
|
+
reservationId: string | null,
|
|
114
|
+
concurrencyGrant: ProviderConcurrencyGrant | null,
|
|
115
|
+
pacingGrant: ProviderPacingGrant | null,
|
|
116
|
+
]
|
|
117
|
+
>;
|
|
94
118
|
rateScopeToken: string | null;
|
|
95
119
|
expiresAt: number;
|
|
96
120
|
rulesKey: string;
|
|
97
121
|
rules: PacingRule[];
|
|
98
122
|
}
|
|
99
123
|
|
|
124
|
+
type PendingConcurrencyObservations = {
|
|
125
|
+
batch: {
|
|
126
|
+
observerId: string;
|
|
127
|
+
laneId: string;
|
|
128
|
+
sequence: number;
|
|
129
|
+
};
|
|
130
|
+
observations: Array<{ kind: 'success'; latencyMs: number }>;
|
|
131
|
+
};
|
|
132
|
+
|
|
100
133
|
interface PendingRelease {
|
|
101
134
|
bucketId: string;
|
|
102
135
|
rateScopeToken: string | null;
|
|
@@ -148,6 +181,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
148
181
|
private readonly cooldowns = new Map<string, RateStateCooldown>();
|
|
149
182
|
private readonly effectiveRps = new Map<string, number>();
|
|
150
183
|
private readonly pendingSuccesses = new Map<string, number>();
|
|
184
|
+
/**
|
|
185
|
+
* Completion samples travel with the next lease refill. This keeps the
|
|
186
|
+
* Gradient2 checkpoint current without adding one gateway request per row.
|
|
187
|
+
*/
|
|
188
|
+
private readonly pendingConcurrencyObservations = new Map<
|
|
189
|
+
string,
|
|
190
|
+
PendingConcurrencyObservations
|
|
191
|
+
>();
|
|
192
|
+
/** One opaque identity per runtime backend; never derived from customer data. */
|
|
193
|
+
private readonly concurrencyObserverId = randomUUID();
|
|
194
|
+
private readonly concurrencyObservationLanes = new Map<string, string>();
|
|
195
|
+
private readonly nextConcurrencyObservationSequence = new Map<
|
|
196
|
+
string,
|
|
197
|
+
number
|
|
198
|
+
>();
|
|
151
199
|
private pendingReleaseFailure: Error | null = null;
|
|
152
200
|
|
|
153
201
|
constructor(context: WorkerRuntimeApiContext, options: Options = {}) {
|
|
@@ -184,11 +232,16 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
184
232
|
* otherwise reports `coolUntilMs`. No-op when the bucket has no live hold.
|
|
185
233
|
*/
|
|
186
234
|
private throwIfPacerExhausted(bucketId: string): void {
|
|
235
|
+
const exhausted = this.pacerExhaustion(bucketId);
|
|
236
|
+
if (exhausted) throw exhausted;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private pacerExhaustion(bucketId: string): ProviderExhaustedError | null {
|
|
187
240
|
const cooldown = this.cooldowns.get(bucketId);
|
|
188
|
-
if (!cooldown) return;
|
|
241
|
+
if (!cooldown) return null;
|
|
189
242
|
const now = this.now();
|
|
190
243
|
const coolActive = cooldown.coolUntilMs - now > 0;
|
|
191
|
-
if (!coolActive) return;
|
|
244
|
+
if (!coolActive) return null;
|
|
192
245
|
const coolWait = cooldown.coolUntilMs - now;
|
|
193
246
|
const claimActive = cooldown.claimedRetryAtMs - now > 0;
|
|
194
247
|
// Duration alone decides. Neither hold is over threshold → let it sleep.
|
|
@@ -199,7 +252,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
199
252
|
cooldown.claimedRetryAtMs - now > PROVIDER_EXHAUSTED_MAX_WAIT_MS
|
|
200
253
|
)
|
|
201
254
|
) {
|
|
202
|
-
return;
|
|
255
|
+
return null;
|
|
203
256
|
}
|
|
204
257
|
// Retry deadline: the server Retry-After verbatim when present, else the
|
|
205
258
|
// pacer's own cooldown deadline.
|
|
@@ -207,12 +260,36 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
207
260
|
cooldown.claimedRetryAtMs > 0
|
|
208
261
|
? cooldown.claimedRetryAtMs
|
|
209
262
|
: cooldown.coolUntilMs;
|
|
210
|
-
|
|
263
|
+
return new ProviderExhaustedError({
|
|
211
264
|
provider: providerNameFromBucketId(bucketId),
|
|
212
265
|
retryAtMs,
|
|
213
266
|
});
|
|
214
267
|
}
|
|
215
268
|
|
|
269
|
+
tryAcquire(input: {
|
|
270
|
+
bucketId: string;
|
|
271
|
+
rateScopeToken?: string | null;
|
|
272
|
+
rules: readonly PacingRule[];
|
|
273
|
+
}): PacingPermit | null {
|
|
274
|
+
const { bucketId, rules } = input;
|
|
275
|
+
if (rules.length === 0) return noopPacingPermit();
|
|
276
|
+
// A probe reports nothing and repairs nothing: a pending release failure
|
|
277
|
+
// or an exhausted pacer surfaces from the next blocking acquire instead.
|
|
278
|
+
if (this.pendingReleaseFailure || this.pacerExhaustion(bucketId)) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const laneKey = rateLaneKey(
|
|
282
|
+
bucketId,
|
|
283
|
+
rulesSignature([...rules].sort((a, b) => a.ruleId.localeCompare(b.ruleId))),
|
|
284
|
+
);
|
|
285
|
+
const block = this.liveBlock(bucketId, laneKey);
|
|
286
|
+
if (!block) return null;
|
|
287
|
+
const next = block.permits[0];
|
|
288
|
+
if (!next || next[0] > this.now()) return null;
|
|
289
|
+
block.permits.shift();
|
|
290
|
+
return this.permitFor(bucketId, laneKey, block, next);
|
|
291
|
+
}
|
|
292
|
+
|
|
216
293
|
async acquire(input: {
|
|
217
294
|
bucketId: string;
|
|
218
295
|
rateScopeToken?: string | null;
|
|
@@ -239,6 +316,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
239
316
|
// sleep) serves the whole matching wave, then everyone re-draws. This replaces the old
|
|
240
317
|
// per-bucket lock that held across the RPC + up-to-5s sleep and thereby
|
|
241
318
|
// serialized every map row behind one sleeping acquirer.
|
|
319
|
+
const acquireStartedAt = this.now();
|
|
242
320
|
while (true) {
|
|
243
321
|
if (signal?.aborted) {
|
|
244
322
|
throw signal.reason instanceof Error
|
|
@@ -257,6 +335,13 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
257
335
|
}
|
|
258
336
|
return localPermit.permit;
|
|
259
337
|
}
|
|
338
|
+
const waitedMs = this.now() - acquireStartedAt;
|
|
339
|
+
if (waitedMs >= RATE_ACQUIRE_MAX_WAIT_MS) {
|
|
340
|
+
throw new ProviderExhaustedError({
|
|
341
|
+
provider: providerNameFromBucketId(bucketId),
|
|
342
|
+
retryAtMs: this.now() + MAX_ACQUIRE_SLEEP_MS,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
260
345
|
// Count this acquirer as waiting on the refill so the single-flight refill
|
|
261
346
|
// can size its block to the live wave. Decremented once a block lands (or
|
|
262
347
|
// the refill errors) — see the finally in `refillBlock`.
|
|
@@ -291,37 +376,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
291
376
|
laneKey: string,
|
|
292
377
|
rules: readonly PacingRule[],
|
|
293
378
|
): number {
|
|
379
|
+
const floor = APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
|
|
294
380
|
const configuredRps = Math.min(
|
|
295
381
|
...rules.map((rule) => (rule.requestsPerWindow / rule.windowMs) * 1_000),
|
|
296
382
|
);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
const baseRps = (rule.requestsPerWindow / rule.windowMs) * 1_000;
|
|
308
|
-
return rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > baseRps;
|
|
309
|
-
});
|
|
310
|
-
const floor = hasAdvisoryExplorationCeiling
|
|
311
|
-
? Math.min(
|
|
312
|
-
APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE,
|
|
313
|
-
Math.max(1, Math.ceil(effectiveRps)),
|
|
314
|
-
)
|
|
315
|
-
: APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
|
|
316
|
-
const horizonBudget = Math.max(
|
|
383
|
+
// Size the block from the rate ACTUALLY in force (the store's learned
|
|
384
|
+
// `refill_per_sec`, falling back to the declared rate) over a short horizon,
|
|
385
|
+
// and never past what this block's own TTL can spend at that rate — a permit
|
|
386
|
+
// the block cannot hand out before it expires is a provider token burned for
|
|
387
|
+
// nothing. Clamping to `requestsPerWindow` instead pinned a ramped lane
|
|
388
|
+
// (10 rps declared, 100 rps learned) to ten permits per round trip, which on
|
|
389
|
+
// a hot map is a round trip per permit. The store's own token floor, not
|
|
390
|
+
// this number, is what bounds the grant.
|
|
391
|
+
const horizonMs = Math.min(RATE_BLOCK_HORIZON_MS, leasedBlockTtlMs(rules));
|
|
392
|
+
const rateBudget = Math.max(
|
|
317
393
|
1,
|
|
318
|
-
Math.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
horizonBudget,
|
|
322
|
-
...rules
|
|
323
|
-
.map((rule) => rule.requestsPerWindow)
|
|
324
|
-
.filter((value) => Number.isFinite(value) && value > 0),
|
|
394
|
+
Math.floor(
|
|
395
|
+
((this.effectiveRps.get(laneKey) ?? configuredRps) * horizonMs) / 1_000,
|
|
396
|
+
),
|
|
325
397
|
);
|
|
326
398
|
if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
|
|
327
399
|
const waiters = this.waiters.get(laneKey) ?? 1;
|
|
@@ -461,20 +533,25 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
461
533
|
try {
|
|
462
534
|
const successKey = pendingSuccessKey(bucketId, ordered);
|
|
463
535
|
const observedSuccesses = this.pendingSuccesses.get(successKey) ?? 0;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const rateRules = withAdaptiveMaxRps(ordered);
|
|
536
|
+
const pendingConcurrency =
|
|
537
|
+
this.pendingConcurrencyObservations.get(successKey);
|
|
538
|
+
const concurrencyObservations = [
|
|
539
|
+
...(pendingConcurrency?.observations ?? []),
|
|
540
|
+
];
|
|
470
541
|
const response = await this.port.acquire({
|
|
471
542
|
bucketId,
|
|
472
543
|
rateScopeToken,
|
|
473
|
-
// Stamp
|
|
474
|
-
//
|
|
475
|
-
rules:
|
|
476
|
-
requested: this.requestedForBucket(laneKey,
|
|
544
|
+
// Stamp adaptiveMaxRps on default-pacing rules so the DB row's ceiling
|
|
545
|
+
// matches the old in-memory AIMD range.
|
|
546
|
+
rules: withAdaptiveMaxRps(ordered),
|
|
547
|
+
requested: this.requestedForBucket(laneKey, ordered),
|
|
477
548
|
observedSuccesses,
|
|
549
|
+
...(pendingConcurrency
|
|
550
|
+
? {
|
|
551
|
+
concurrencyObservations,
|
|
552
|
+
concurrencyObservationBatch: pendingConcurrency.batch,
|
|
553
|
+
}
|
|
554
|
+
: {}),
|
|
478
555
|
schedulerSchema: this.schedulerSchema,
|
|
479
556
|
});
|
|
480
557
|
if (observedSuccesses > 0) {
|
|
@@ -485,6 +562,40 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
485
562
|
if (remaining > 0) this.pendingSuccesses.set(successKey, remaining);
|
|
486
563
|
else this.pendingSuccesses.delete(successKey);
|
|
487
564
|
}
|
|
565
|
+
if (pendingConcurrency) {
|
|
566
|
+
const pending = this.pendingConcurrencyObservations.get(successKey);
|
|
567
|
+
const remainingSuccesses = this.pendingSuccesses.get(successKey) ?? 0;
|
|
568
|
+
if (
|
|
569
|
+
pending &&
|
|
570
|
+
pending.batch.sequence === pendingConcurrency.batch.sequence
|
|
571
|
+
) {
|
|
572
|
+
const remaining = pending.observations.slice(
|
|
573
|
+
concurrencyObservations.length,
|
|
574
|
+
);
|
|
575
|
+
if (remainingSuccesses > 0 || remaining.length > 0) {
|
|
576
|
+
// Observations that arrived while this acquire was in flight need
|
|
577
|
+
// a new identity. Reusing the acknowledged sequence would make
|
|
578
|
+
// the gateway correctly discard their rate and Gradient2 input.
|
|
579
|
+
this.pendingConcurrencyObservations.set(
|
|
580
|
+
successKey,
|
|
581
|
+
this.newPendingConcurrencyObservations(successKey, remaining),
|
|
582
|
+
);
|
|
583
|
+
} else {
|
|
584
|
+
this.pendingConcurrencyObservations.delete(successKey);
|
|
585
|
+
}
|
|
586
|
+
} else if (
|
|
587
|
+
pending &&
|
|
588
|
+
pending.observations.length > concurrencyObservations.length
|
|
589
|
+
) {
|
|
590
|
+
const remaining = pending.observations.slice(
|
|
591
|
+
concurrencyObservations.length,
|
|
592
|
+
);
|
|
593
|
+
this.pendingConcurrencyObservations.set(
|
|
594
|
+
successKey,
|
|
595
|
+
this.newPendingConcurrencyObservations(successKey, remaining),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
488
599
|
this.recordCooldown(bucketId, response);
|
|
489
600
|
if (
|
|
490
601
|
Number.isFinite(response.effectiveRequestsPerSecond) &&
|
|
@@ -568,6 +679,9 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
568
679
|
response: {
|
|
569
680
|
granted: number;
|
|
570
681
|
leaseIds?: unknown;
|
|
682
|
+
reservationIds?: unknown;
|
|
683
|
+
concurrencyGrants?: unknown;
|
|
684
|
+
effectiveRequestsPerSecond?: unknown;
|
|
571
685
|
serverNowMs?: unknown;
|
|
572
686
|
scheduledAtMs?: unknown;
|
|
573
687
|
},
|
|
@@ -589,6 +703,35 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
589
703
|
`Rate-state store returned ${leaseIds.length} lease tokens for pure-rate bucket ${bucketId}.`,
|
|
590
704
|
);
|
|
591
705
|
}
|
|
706
|
+
const concurrencyGrants = normalizeConcurrencyGrants({
|
|
707
|
+
value: response.concurrencyGrants,
|
|
708
|
+
granted: response.granted,
|
|
709
|
+
hasConcurrency,
|
|
710
|
+
bucketId,
|
|
711
|
+
});
|
|
712
|
+
const effectiveRequestsPerSecond = Number(
|
|
713
|
+
response.effectiveRequestsPerSecond,
|
|
714
|
+
);
|
|
715
|
+
const pacingGrant: ProviderPacingGrant | null =
|
|
716
|
+
Number.isFinite(effectiveRequestsPerSecond) &&
|
|
717
|
+
effectiveRequestsPerSecond > 0
|
|
718
|
+
? { targetRequestsPerSecond: effectiveRequestsPerSecond }
|
|
719
|
+
: null;
|
|
720
|
+
const reservationIds = Array.isArray(response.reservationIds)
|
|
721
|
+
? response.reservationIds.filter(
|
|
722
|
+
(reservationId): reservationId is string =>
|
|
723
|
+
typeof reservationId === 'string' &&
|
|
724
|
+
reservationId.trim().length > 0,
|
|
725
|
+
)
|
|
726
|
+
: [];
|
|
727
|
+
if (
|
|
728
|
+
reservationIds.length > 0 &&
|
|
729
|
+
reservationIds.length !== response.granted
|
|
730
|
+
) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`Rate-state store granted ${response.granted} permits for ${bucketId} with ${reservationIds.length} refundable reservation ids.`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
592
735
|
// Rolling deploy compatibility: an older gateway returns no schedule. It
|
|
593
736
|
// has already debited the block under the legacy token-bucket contract, so
|
|
594
737
|
// keep those permits immediately usable until every gateway serves the new
|
|
@@ -598,11 +741,20 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
598
741
|
: Array.from({ length: response.granted }, () => this.now());
|
|
599
742
|
const serverNowMs = Number(response.serverNowMs);
|
|
600
743
|
const receivedAtMs = this.now();
|
|
744
|
+
let firstGatewayScheduledAtMs = Number.POSITIVE_INFINITY;
|
|
745
|
+
for (const scheduledAtMs of gatewayScheduledAtMs) {
|
|
746
|
+
firstGatewayScheduledAtMs = Math.min(
|
|
747
|
+
firstGatewayScheduledAtMs,
|
|
748
|
+
scheduledAtMs,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
601
751
|
const scheduledAtMs =
|
|
602
752
|
Number.isFinite(serverNowMs) && serverNowMs >= 0
|
|
603
753
|
? gatewayScheduledAtMs.map(
|
|
604
754
|
(scheduledAtMs) =>
|
|
605
|
-
receivedAtMs +
|
|
755
|
+
receivedAtMs +
|
|
756
|
+
Math.max(0, firstGatewayScheduledAtMs - serverNowMs) +
|
|
757
|
+
(scheduledAtMs - firstGatewayScheduledAtMs),
|
|
606
758
|
)
|
|
607
759
|
: gatewayScheduledAtMs;
|
|
608
760
|
if (
|
|
@@ -616,10 +768,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
616
768
|
this.mergeBlock(
|
|
617
769
|
bucketId,
|
|
618
770
|
rateScopeToken,
|
|
619
|
-
scheduledAtMs.map(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
771
|
+
scheduledAtMs.map(
|
|
772
|
+
(
|
|
773
|
+
scheduledAtMs,
|
|
774
|
+
index,
|
|
775
|
+
): [
|
|
776
|
+
number,
|
|
777
|
+
string | null,
|
|
778
|
+
string | null,
|
|
779
|
+
ProviderConcurrencyGrant | null,
|
|
780
|
+
ProviderPacingGrant | null,
|
|
781
|
+
] => [
|
|
782
|
+
scheduledAtMs,
|
|
783
|
+
hasConcurrency ? (leaseIds[index] ?? null) : null,
|
|
784
|
+
reservationIds[index] ?? null,
|
|
785
|
+
concurrencyGrants[index] ?? null,
|
|
786
|
+
pacingGrant,
|
|
787
|
+
],
|
|
788
|
+
),
|
|
623
789
|
rulesKey,
|
|
624
790
|
laneKey,
|
|
625
791
|
ordered,
|
|
@@ -630,6 +796,9 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
630
796
|
bucketId: string;
|
|
631
797
|
rateScopeToken?: string | null;
|
|
632
798
|
cooldownMs: number;
|
|
799
|
+
rules?: readonly PacingRule[];
|
|
800
|
+
latencyMs?: number | null;
|
|
801
|
+
concurrencyGrant?: ProviderConcurrencyGrant | null;
|
|
633
802
|
}): Promise<void> {
|
|
634
803
|
if (input.cooldownMs <= 0) return;
|
|
635
804
|
this.drainBlocksForBucket(input.bucketId);
|
|
@@ -638,6 +807,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
638
807
|
bucketId: input.bucketId,
|
|
639
808
|
rateScopeToken: input.rateScopeToken?.trim() || null,
|
|
640
809
|
cooldownMs: input.cooldownMs,
|
|
810
|
+
...(input.rules ? { rules: [...input.rules] } : {}),
|
|
811
|
+
...(input.latencyMs != null ? { latencyMs: input.latencyMs } : {}),
|
|
812
|
+
...(input.concurrencyGrant
|
|
813
|
+
? { concurrencyGrant: input.concurrencyGrant }
|
|
814
|
+
: {}),
|
|
641
815
|
schedulerSchema: this.schedulerSchema,
|
|
642
816
|
});
|
|
643
817
|
} catch (error) {
|
|
@@ -655,28 +829,91 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
655
829
|
observeSuccess(input: {
|
|
656
830
|
bucketId: string;
|
|
657
831
|
rules: readonly PacingRule[];
|
|
832
|
+
latencyMs?: number | null;
|
|
658
833
|
}): void {
|
|
659
834
|
const key = pendingSuccessKey(input.bucketId, input.rules);
|
|
660
835
|
this.pendingSuccesses.set(
|
|
661
836
|
key,
|
|
662
837
|
Math.min(1_000_000, (this.pendingSuccesses.get(key) ?? 0) + 1),
|
|
663
838
|
);
|
|
839
|
+
const pending =
|
|
840
|
+
this.pendingConcurrencyObservations.get(key) ??
|
|
841
|
+
this.newPendingConcurrencyObservations(key);
|
|
842
|
+
if (
|
|
843
|
+
input.latencyMs != null &&
|
|
844
|
+
Number.isFinite(input.latencyMs) &&
|
|
845
|
+
input.latencyMs > 0
|
|
846
|
+
) {
|
|
847
|
+
// This is only an amortization buffer. It has an explicit byte-equivalent
|
|
848
|
+
// item cap; overflowing samples remain successful rate observations but
|
|
849
|
+
// cannot grow sandbox memory indefinitely.
|
|
850
|
+
if (pending.observations.length < 4_096) {
|
|
851
|
+
pending.observations.push({
|
|
852
|
+
kind: 'success',
|
|
853
|
+
latencyMs: input.latencyMs,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// The same batch identity also protects the rate-success observation.
|
|
858
|
+
// A provider can report a valid success without a usable residence time;
|
|
859
|
+
// retaining an empty Gradient2 list still makes a lost acquire response
|
|
860
|
+
// unable to replay that rate recovery signal.
|
|
861
|
+
this.pendingConcurrencyObservations.set(key, pending);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private newPendingConcurrencyObservations(
|
|
865
|
+
key: string,
|
|
866
|
+
observations: Array<{ kind: 'success'; latencyMs: number }> = [],
|
|
867
|
+
): PendingConcurrencyObservations {
|
|
868
|
+
const laneId =
|
|
869
|
+
this.concurrencyObservationLanes.get(key) ??
|
|
870
|
+
(() => {
|
|
871
|
+
const created = randomUUID();
|
|
872
|
+
this.concurrencyObservationLanes.set(key, created);
|
|
873
|
+
return created;
|
|
874
|
+
})();
|
|
875
|
+
const sequence =
|
|
876
|
+
(this.nextConcurrencyObservationSequence.get(key) ?? 0) + 1;
|
|
877
|
+
this.nextConcurrencyObservationSequence.set(key, sequence);
|
|
878
|
+
return {
|
|
879
|
+
batch: {
|
|
880
|
+
observerId: this.concurrencyObserverId,
|
|
881
|
+
laneId,
|
|
882
|
+
sequence,
|
|
883
|
+
},
|
|
884
|
+
observations,
|
|
885
|
+
};
|
|
664
886
|
}
|
|
665
887
|
|
|
666
888
|
private permitFor(
|
|
667
889
|
bucketId: string,
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
890
|
+
laneKey: string,
|
|
891
|
+
block: LeasedBlock,
|
|
892
|
+
scheduled: LeasedBlock['permits'][number],
|
|
671
893
|
): PacingPermit {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
894
|
+
const { rateScopeToken, rules } = block;
|
|
895
|
+
const [, leaseId, reservationId, concurrencyGrant, pacingGrant] = scheduled;
|
|
896
|
+
let settled = false;
|
|
897
|
+
const refund = async (): Promise<void> => {
|
|
898
|
+
if (!reservationId) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`Rate-state gateway returned an unrefundable permit for ${bucketId}.`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
await this.cancelReserved({
|
|
904
|
+
bucketId,
|
|
905
|
+
rateScopeToken,
|
|
906
|
+
rules,
|
|
907
|
+
leaseIds: leaseId ? [leaseId] : [],
|
|
908
|
+
reservationId,
|
|
909
|
+
});
|
|
910
|
+
};
|
|
676
911
|
return {
|
|
912
|
+
concurrencyGrant,
|
|
913
|
+
pacingGrant,
|
|
677
914
|
release: () => {
|
|
678
|
-
if (
|
|
679
|
-
|
|
915
|
+
if (settled) return;
|
|
916
|
+
settled = true;
|
|
680
917
|
this.releaseReserved(
|
|
681
918
|
bucketId,
|
|
682
919
|
rateScopeToken,
|
|
@@ -684,6 +921,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
684
921
|
leaseId ? [leaseId] : [],
|
|
685
922
|
);
|
|
686
923
|
},
|
|
924
|
+
cancel: async () => {
|
|
925
|
+
if (settled) return;
|
|
926
|
+
settled = true;
|
|
927
|
+
await refund();
|
|
928
|
+
},
|
|
929
|
+
park: async () => {
|
|
930
|
+
if (settled) return;
|
|
931
|
+
settled = true;
|
|
932
|
+
// Only the block this permit was drawn from may take it back: a lane
|
|
933
|
+
// whose block expired, was drained by a penalty, or was replaced has
|
|
934
|
+
// already given up its grant, and the permit must be refunded instead.
|
|
935
|
+
if (this.blocks.get(laneKey) === block && block.expiresAt > this.now()) {
|
|
936
|
+
block.permits.push(scheduled);
|
|
937
|
+
block.permits.sort((a, b) => a[0] - b[0]);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
await refund();
|
|
941
|
+
},
|
|
687
942
|
};
|
|
688
943
|
}
|
|
689
944
|
|
|
@@ -725,10 +980,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
725
980
|
});
|
|
726
981
|
}
|
|
727
982
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
laneKey: string,
|
|
731
|
-
): { permit: PacingPermit; scheduledAtMs: number } | null {
|
|
983
|
+
/** The lane's block, evicting (and releasing the leases of) an expired one. */
|
|
984
|
+
private liveBlock(bucketId: string, laneKey: string): LeasedBlock | null {
|
|
732
985
|
const block = this.blocks.get(laneKey);
|
|
733
986
|
if (!block) return null;
|
|
734
987
|
if (block.expiresAt <= this.now()) {
|
|
@@ -745,19 +998,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
745
998
|
);
|
|
746
999
|
return null;
|
|
747
1000
|
}
|
|
1001
|
+
return block;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private drawFromBlock(
|
|
1005
|
+
bucketId: string,
|
|
1006
|
+
laneKey: string,
|
|
1007
|
+
): { permit: PacingPermit; scheduledAtMs: number } | null {
|
|
1008
|
+
const block = this.liveBlock(bucketId, laneKey);
|
|
1009
|
+
if (!block) return null;
|
|
1010
|
+
// An emptied block stays until it expires: the next refill merges into it,
|
|
1011
|
+
// and a parked permit can only return to the block it was drawn from.
|
|
748
1012
|
const scheduled = block.permits.shift();
|
|
749
|
-
if (!scheduled)
|
|
750
|
-
this.blocks.delete(laneKey);
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
753
|
-
if (block.permits.length === 0) this.blocks.delete(laneKey);
|
|
1013
|
+
if (!scheduled) return null;
|
|
754
1014
|
return {
|
|
755
|
-
permit: this.permitFor(
|
|
756
|
-
bucketId,
|
|
757
|
-
block.rateScopeToken,
|
|
758
|
-
block.rules,
|
|
759
|
-
scheduled[1],
|
|
760
|
-
),
|
|
1015
|
+
permit: this.permitFor(bucketId, laneKey, block, scheduled),
|
|
761
1016
|
scheduledAtMs: scheduled[0],
|
|
762
1017
|
};
|
|
763
1018
|
}
|
|
@@ -776,6 +1031,40 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
776
1031
|
}
|
|
777
1032
|
}
|
|
778
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Return one permit that never reached the provider. This is deliberately a
|
|
1036
|
+
* synchronous acknowledgement boundary: the caller must not re-admit until
|
|
1037
|
+
* the original reservation is durably refunded, or a saturated local slot can
|
|
1038
|
+
* still turn into unbounded rate-token loss.
|
|
1039
|
+
*/
|
|
1040
|
+
private async cancelReserved(input: {
|
|
1041
|
+
bucketId: string;
|
|
1042
|
+
rateScopeToken: string | null;
|
|
1043
|
+
rules: readonly PacingRule[];
|
|
1044
|
+
leaseIds: readonly string[];
|
|
1045
|
+
reservationId: string;
|
|
1046
|
+
}): Promise<void> {
|
|
1047
|
+
try {
|
|
1048
|
+
await this.port.release({
|
|
1049
|
+
bucketId: input.bucketId,
|
|
1050
|
+
rateScopeToken: input.rateScopeToken,
|
|
1051
|
+
rules: [...input.rules],
|
|
1052
|
+
leaseIds: [...input.leaseIds],
|
|
1053
|
+
refundReservationIds: [input.reservationId],
|
|
1054
|
+
schedulerSchema: this.schedulerSchema,
|
|
1055
|
+
});
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
const normalized = normalizeError(error);
|
|
1058
|
+
this.onStoreFailure({
|
|
1059
|
+
bucketId: input.bucketId,
|
|
1060
|
+
error: normalized.message,
|
|
1061
|
+
});
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
`Rate-state cancellation failed for ${input.bucketId}: ${normalized.message}`,
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
779
1068
|
private releaseReserved(
|
|
780
1069
|
bucketId: string,
|
|
781
1070
|
rateScopeToken: string | null,
|
|
@@ -887,6 +1176,71 @@ function pendingSuccessKey(
|
|
|
887
1176
|
return rateLaneKey(bucketId, rulesSignature(rules));
|
|
888
1177
|
}
|
|
889
1178
|
|
|
1179
|
+
function normalizeConcurrencyGrants(input: {
|
|
1180
|
+
value: unknown;
|
|
1181
|
+
granted: number;
|
|
1182
|
+
hasConcurrency: boolean;
|
|
1183
|
+
bucketId: string;
|
|
1184
|
+
}): Array<ProviderConcurrencyGrant | null> {
|
|
1185
|
+
if (!input.hasConcurrency) {
|
|
1186
|
+
if (Array.isArray(input.value) && input.value.length > 0) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Rate-state store returned concurrency attribution for pure-rate bucket ${input.bucketId}.`,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
return Array.from({ length: input.granted }, () => null);
|
|
1192
|
+
}
|
|
1193
|
+
// The field is additive. A pre-expand gateway cannot provide attribution, so
|
|
1194
|
+
// keep its declared-cap behavior and decline to infer a throttle dimension.
|
|
1195
|
+
if (input.value === undefined) {
|
|
1196
|
+
return Array.from({ length: input.granted }, () => null);
|
|
1197
|
+
}
|
|
1198
|
+
if (!Array.isArray(input.value) || input.value.length !== input.granted) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`Rate-state store granted ${input.granted} concurrency permits for ${input.bucketId} with invalid attribution tokens.`,
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
return input.value.map((value) => {
|
|
1204
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`Rate-state store returned an invalid concurrency attribution token for ${input.bucketId}.`,
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
const candidate = value as Record<string, unknown>;
|
|
1210
|
+
const admissionId =
|
|
1211
|
+
typeof candidate.admissionId === 'string'
|
|
1212
|
+
? candidate.admissionId.trim()
|
|
1213
|
+
: '';
|
|
1214
|
+
const inFlightAtStart = Number(candidate.inFlightAtStart);
|
|
1215
|
+
const targetRequestsPerSecond = candidate.targetRequestsPerSecond;
|
|
1216
|
+
const minimumRttMs = candidate.minimumRttMs;
|
|
1217
|
+
if (
|
|
1218
|
+
!admissionId ||
|
|
1219
|
+
admissionId.length > 64 ||
|
|
1220
|
+
!Number.isSafeInteger(inFlightAtStart) ||
|
|
1221
|
+
inFlightAtStart < 1 ||
|
|
1222
|
+
(targetRequestsPerSecond !== null &&
|
|
1223
|
+
(!Number.isFinite(targetRequestsPerSecond) ||
|
|
1224
|
+
Number(targetRequestsPerSecond) <= 0)) ||
|
|
1225
|
+
(minimumRttMs !== null &&
|
|
1226
|
+
(!Number.isFinite(minimumRttMs) || Number(minimumRttMs) <= 0))
|
|
1227
|
+
) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`Rate-state store returned an invalid concurrency attribution token for ${input.bucketId}.`,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
admissionId,
|
|
1234
|
+
inFlightAtStart,
|
|
1235
|
+
targetRequestsPerSecond:
|
|
1236
|
+
targetRequestsPerSecond === null
|
|
1237
|
+
? null
|
|
1238
|
+
: Number(targetRequestsPerSecond),
|
|
1239
|
+
minimumRttMs: minimumRttMs === null ? null : Number(minimumRttMs),
|
|
1240
|
+
};
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
890
1244
|
function leasedBlockTtlMs(rules: readonly PacingRule[]): number {
|
|
891
1245
|
const shortestWindowMs = Math.min(
|
|
892
1246
|
...rules
|