deepline 0.3.323 → 0.3.325
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/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +454 -129
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +407 -37
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +18 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-http-request.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +15 -5
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +7 -2
- package/dist/bundling-sources/shared_libs/play-runtime/provider-row-retry-queue.ts +65 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +7 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +34 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +116 -15
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +59 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +294 -37
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +73 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +41 -3
- package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +9 -0
- package/dist/cli/{auth-4TYSEEPE.mjs → auth-PIZESRPC.mjs} +4 -4
- package/dist/cli/auth-RXCD4GOV.js +31 -0
- package/dist/cli/{billing-HYX3OWXB.js → billing-DQKFCQV5.js} +70 -70
- package/dist/cli/{billing-QDER3JLM.mjs → billing-HSL6VTCM.mjs} +4 -4
- package/dist/cli/{chunk-VN433ENM.mjs → chunk-5FHU7TV4.mjs} +2 -2
- package/dist/cli/{chunk-ZF6WOSMW.mjs → chunk-5TYU3UIS.mjs} +1 -1
- package/dist/cli/{chunk-YY6EASRE.mjs → chunk-6RMKHN7S.mjs} +4 -4
- package/dist/cli/{chunk-CXJND6CJ.mjs → chunk-6VZDSBK6.mjs} +2 -2
- package/dist/cli/{chunk-JGTV5QNG.mjs → chunk-7MD453WV.mjs} +6 -6
- package/dist/cli/{chunk-3OTHDXTV.mjs → chunk-AQUNQJ5Y.mjs} +1 -1
- package/dist/cli/{chunk-FNKBVKQX.js → chunk-CV5GDCKN.js} +15 -15
- package/dist/cli/{chunk-WOG5IKUF.js → chunk-DLW7X2UK.js} +220 -220
- package/dist/cli/{chunk-BC3E7GEW.mjs → chunk-H5TJBRSC.mjs} +5 -5
- package/dist/cli/{chunk-LXUY7LRE.js → chunk-HPTQEWCF.js} +60 -60
- package/dist/cli/{chunk-GUPKB4UE.js → chunk-HUKWYHBG.js} +2 -2
- package/dist/cli/{chunk-BEG62AQW.js → chunk-K7FC5IUU.js} +1 -1
- package/dist/cli/{chunk-I7TAO4XH.js → chunk-KZNFYOHX.js} +6 -6
- package/dist/cli/{chunk-UMUH2FVZ.js → chunk-MNKDSR2J.js} +6 -6
- package/dist/cli/{chunk-UE55ECMI.mjs → chunk-NWBARG3F.mjs} +1 -1
- package/dist/cli/{chunk-KLIHE4X5.js → chunk-OOPSX57I.js} +6 -6
- package/dist/cli/{chunk-3I4KDUKY.mjs → chunk-OPERWJR6.mjs} +3 -3
- package/dist/cli/{chunk-HUQVA5MR.js → chunk-PBNZXZCW.js} +48 -48
- package/dist/cli/{chunk-HSZD4VBE.js → chunk-PVDQ55PS.js} +4 -4
- package/dist/cli/{chunk-HJN5KR42.mjs → chunk-PWV2M4LW.mjs} +1 -1
- package/dist/cli/{chunk-M2WAAIM4.js → chunk-QQ2FG5W3.js} +6 -6
- package/dist/cli/{chunk-OQPG3ND3.js → chunk-UYFWTIKF.js} +26 -26
- package/dist/cli/{chunk-2HYJDTQT.mjs → chunk-WW4XZIVM.mjs} +2 -2
- package/dist/cli/{chunk-FGD2M25K.mjs → chunk-XIVUB36F.mjs} +1 -1
- package/dist/cli/{chunk-W2V624T6.mjs → chunk-YE3O2UHM.mjs} +1 -1
- package/dist/cli/{chunk-EWFKMQFO.js → chunk-YXRPWMGN.js} +50 -50
- package/dist/cli/client-562MULXK.js +20 -0
- package/dist/cli/{client-CEX7EHUM.mjs → client-57SR4KVM.mjs} +3 -3
- package/dist/cli/{csv-GBPRJ6YU.js → csv-5DJSC6BD.js} +12 -12
- package/dist/cli/{csv-LR5KWOIK.mjs → csv-G2FRMOXL.mjs} +3 -3
- package/dist/cli/{db-VFKXAHQA.js → db-NVO7P4VO.js} +20 -20
- package/dist/cli/{db-4BMCUU34.mjs → db-WWZEXM3N.mjs} +4 -4
- package/dist/cli/{describe-VTLU5KX5.mjs → describe-26OMF3A4.mjs} +8 -8
- package/dist/cli/{describe-BYWGWALB.js → describe-M26LHDBQ.js} +15 -15
- package/dist/cli/{executions-NGREVSY6.mjs → executions-2LLHY3IL.mjs} +4 -4
- package/dist/cli/{executions-DU7I6YEU.js → executions-KI47LPKE.js} +6 -6
- package/dist/cli/{feedback-PHPAWVHC.js → feedback-2Y26C2BZ.js} +6 -6
- package/dist/cli/{feedback-GUY2CCGF.mjs → feedback-O26VL2OF.mjs} +3 -3
- package/dist/cli/git-KGPX5YPD.js +23 -0
- package/dist/cli/{git-22YV3DFJ.mjs → git-UVPGZN3K.mjs} +4 -4
- package/dist/cli/index.js +75 -75
- package/dist/cli/index.mjs +31 -31
- package/dist/cli/{legacy-noops-V72GCF5R.js → legacy-noops-H6Y3SEVB.js} +7 -7
- package/dist/cli/{legacy-noops-YEQDVI6U.mjs → legacy-noops-IK5CH72U.mjs} +4 -4
- package/dist/cli/{monitors-MVJBQDQG.js → monitors-JL6TTQCI.js} +85 -85
- package/dist/cli/{monitors-QOCAMBRV.mjs → monitors-SFVVMAJT.mjs} +4 -4
- package/dist/cli/{org-DMIOMJK6.js → org-OVMAZCQS.js} +60 -60
- package/dist/cli/{org-A53XV2UQ.mjs → org-SSDONL57.mjs} +4 -4
- package/dist/cli/play-3DB4MSHN.js +152 -0
- package/dist/cli/{play-73L74CUW.mjs → play-X6NPACUO.mjs} +7 -7
- package/dist/cli/{providers-PEOAZYM6.js → providers-E24JDWSR.js} +7 -7
- package/dist/cli/{providers-6LDD5X5G.mjs → providers-ONUBYYY4.mjs} +4 -4
- package/dist/cli/{quickstart-PXMC66UG.mjs → quickstart-B7NIXACV.mjs} +3 -3
- package/dist/cli/{quickstart-33IPDK2I.js → quickstart-TB7C3DLG.js} +5 -5
- package/dist/cli/{search-FJEJDKPA.mjs → search-N3QUIATW.mjs} +5 -5
- package/dist/cli/search-NJ637YGQ.js +26 -0
- package/dist/cli/{secrets-3SCAG3T7.js → secrets-CPCVFDYB.js} +13 -13
- package/dist/cli/{secrets-Q3ZWHE5S.mjs → secrets-V7Z4M67T.mjs} +4 -4
- package/dist/cli/{sessions-QJB4LSNZ.js → sessions-E4FIZLQP.js} +5 -5
- package/dist/cli/{sessions-RY3SCVN7.mjs → sessions-PDOBKAKO.mjs} +4 -4
- package/dist/cli/{settings-VTYJKMI6.mjs → settings-QYHYHZDI.mjs} +7 -7
- package/dist/cli/{settings-DLAE6POM.js → settings-UVM2WDE4.js} +74 -74
- package/dist/cli/{setup-LM2VWSFE.js → setup-3JFAXQLJ.js} +41 -41
- package/dist/cli/{setup-UYQ4V4GP.mjs → setup-4RLAETDW.mjs} +6 -6
- package/dist/cli/skills-BYRYG64R.js +28 -0
- package/dist/cli/{skills-2OKHLQF7.mjs → skills-ZX2JSWOZ.mjs} +4 -4
- package/dist/cli/{tools-F5RYANCG.mjs → tools-EGXCVIPJ.mjs} +6 -6
- package/dist/cli/tools-TAWOAY5X.js +38 -0
- package/dist/cli/{update-5FDHGSNE.mjs → update-7FEKRQFE.mjs} +6 -6
- package/dist/cli/update-H24GEBXM.js +28 -0
- package/dist/cli/{workflow-EQMKPCUV.js → workflow-54VCKIG2.js} +15 -15
- package/dist/cli/{workflow-TWQH53ZO.mjs → workflow-7V7TTTKK.mjs} +5 -5
- package/dist/{client-BV68GMPo.d.ts → client-Bb4Jjzmj.d.ts} +7 -1
- package/dist/{client-D9ACjiRC.d.mts → client-COa-3GHK.d.mts} +7 -1
- package/dist/{compiler-manifest-BXcZ4P4W.d.mts → compiler-manifest-CsnWj-AK.d.mts} +6 -0
- package/dist/{compiler-manifest-BXcZ4P4W.d.ts → compiler-manifest-CsnWj-AK.d.ts} +6 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/index.mjs +3 -3
- package/dist/install-integrity.json +79 -78
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/cli/auth-KWPQ4JVV.js +0 -31
- package/dist/cli/client-YMQ2QFV7.js +0 -20
- package/dist/cli/git-W5NEI2BP.js +0 -23
- package/dist/cli/play-NBFCGFVC.js +0 -152
- package/dist/cli/search-FPUK2H6Y.js +0 -26
- package/dist/cli/skills-5NH6JX5H.js +0 -28
- package/dist/cli/tools-ZPGDY3I2.js +0 -38
- package/dist/cli/update-W5KBXMKC.js +0 -28
package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts
CHANGED
|
@@ -10,11 +10,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_ADVISORY_PROVIDER_MAX_RPS,
|
|
12
12
|
DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
|
|
13
|
+
DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS,
|
|
13
14
|
noopPacingPermit,
|
|
14
15
|
PROVIDER_AIMD_COOLDOWN_CAP_MS,
|
|
15
16
|
type PacingPermit,
|
|
16
17
|
type ProviderConcurrencyGrant,
|
|
17
18
|
type ProviderPacingGrant,
|
|
19
|
+
type ProviderPermitValidationOptions,
|
|
18
20
|
type PacingRule,
|
|
19
21
|
type RateStateBackend,
|
|
20
22
|
type RateStateBackendKind,
|
|
@@ -105,7 +107,11 @@ function abortReason(signal: AbortSignal): Error {
|
|
|
105
107
|
: new Error('Rate-state acquire aborted.');
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
function sleepWithAbort(
|
|
110
|
+
function sleepWithAbort(
|
|
111
|
+
ms: number,
|
|
112
|
+
signal?: AbortSignal,
|
|
113
|
+
interrupted?: Promise<void>,
|
|
114
|
+
): Promise<void> {
|
|
109
115
|
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
110
116
|
return new Promise((resolve, reject) => {
|
|
111
117
|
const cleanup = () => signal?.removeEventListener('abort', onAbort);
|
|
@@ -120,6 +126,11 @@ function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
120
126
|
};
|
|
121
127
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
122
128
|
if (signal?.aborted) onAbort();
|
|
129
|
+
void interrupted?.then(() => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
cleanup();
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
123
134
|
});
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -129,6 +140,7 @@ function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
129
140
|
* hold more than a moment of a shared provider window.
|
|
130
141
|
*/
|
|
131
142
|
const RATE_BLOCK_HORIZON_MS = 1_000;
|
|
143
|
+
const UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS = 500;
|
|
132
144
|
const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
|
|
133
145
|
/**
|
|
134
146
|
* Jitter fraction applied to a server-supplied `waitMs` so a synchronized wave
|
|
@@ -154,6 +166,7 @@ interface LeasedBlock {
|
|
|
154
166
|
concurrencyGrant: ProviderConcurrencyGrant | null,
|
|
155
167
|
pacingGrant: ProviderPacingGrant | null,
|
|
156
168
|
scopeRetryAtMsAtGrant: number,
|
|
169
|
+
dispatchValidationSupported: boolean,
|
|
157
170
|
]
|
|
158
171
|
>;
|
|
159
172
|
rateScopeToken: string | null;
|
|
@@ -209,7 +222,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
209
222
|
private readonly port: AppRuntimeRatePort;
|
|
210
223
|
private readonly schedulerSchema: string | null;
|
|
211
224
|
private readonly now: () => number;
|
|
212
|
-
private readonly sleep: (
|
|
225
|
+
private readonly sleep: (
|
|
226
|
+
ms: number,
|
|
227
|
+
signal?: AbortSignal,
|
|
228
|
+
interrupted?: Promise<void>,
|
|
229
|
+
) => Promise<void>;
|
|
213
230
|
private readonly jitter: () => number;
|
|
214
231
|
private readonly onStoreFailure: (info: {
|
|
215
232
|
bucketId: string;
|
|
@@ -218,15 +235,33 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
218
235
|
private readonly blocks = new Map<string, LeasedBlock>();
|
|
219
236
|
private readonly refills = new Map<string, Promise<void>>();
|
|
220
237
|
private readonly pendingReleases = new Map<string, PendingRelease>();
|
|
238
|
+
private readonly leaseReleaseSignals = new Map<
|
|
239
|
+
string,
|
|
240
|
+
{ generation: number; waiters: Set<() => void> }
|
|
241
|
+
>();
|
|
221
242
|
private readonly validationFlights = new Map<
|
|
222
243
|
string,
|
|
223
244
|
{ generation: number; promise: Promise<PlayRunnerRateStateValidateResult> }
|
|
224
245
|
>();
|
|
225
246
|
private readonly validationGenerations = new Map<string, number>();
|
|
247
|
+
private readonly validatedDispatchCadences = new Map<
|
|
248
|
+
string,
|
|
249
|
+
{
|
|
250
|
+
atMs: number;
|
|
251
|
+
lastAtMs: number;
|
|
252
|
+
used: number;
|
|
253
|
+
burstSize: number;
|
|
254
|
+
spacingMs: number;
|
|
255
|
+
}
|
|
256
|
+
>();
|
|
226
257
|
private readonly validationScopes = new Set<string>();
|
|
227
258
|
private readonly pendingPenaltyWrites = new Map<string, Promise<void>>();
|
|
228
259
|
private readonly serverClockOffsetsMs = new Map<string, number>();
|
|
229
260
|
private readonly localProviderRetryAtMs = new Map<string, number>();
|
|
261
|
+
private readonly scheduledPermitWaits = new Map<
|
|
262
|
+
string,
|
|
263
|
+
Set<{ retryAtMsAtGrant: number; invalidate: () => void }>
|
|
264
|
+
>();
|
|
230
265
|
/**
|
|
231
266
|
* Last locally synthesized dispatch slot per lane. During a rolling deploy,
|
|
232
267
|
* a gateway that omits its global schedule can return another block while
|
|
@@ -240,6 +275,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
240
275
|
* instead of a fixed floor, so a 200-row map fans into ~one round trip.
|
|
241
276
|
*/
|
|
242
277
|
private readonly waiters = new Map<string, number>();
|
|
278
|
+
/** Due retries precede fresh draws; already assigned tickets keep ownership. */
|
|
279
|
+
private readonly retryAdmissions = new Map<string, Set<Promise<void>>>();
|
|
243
280
|
/** Last-known server cooldown per bucket (surfaced for PROVIDER_EXHAUSTED). */
|
|
244
281
|
private readonly cooldowns = new Map<string, RateStateCooldown>();
|
|
245
282
|
private readonly providerStalls = new Map<
|
|
@@ -396,6 +433,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
396
433
|
[...rules].sort((a, b) => a.ruleId.localeCompare(b.ruleId)),
|
|
397
434
|
),
|
|
398
435
|
);
|
|
436
|
+
if (this.retryAdmissions.get(laneKey)?.size) return null;
|
|
399
437
|
const block = this.liveBlock(bucketId, laneKey);
|
|
400
438
|
if (!block) return null;
|
|
401
439
|
const next = block.permits[0];
|
|
@@ -410,6 +448,75 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
410
448
|
rules: readonly PacingRule[];
|
|
411
449
|
signal?: AbortSignal;
|
|
412
450
|
yieldProviderHolds?: boolean;
|
|
451
|
+
retryPriority?: boolean;
|
|
452
|
+
}): Promise<PacingPermit> {
|
|
453
|
+
if (!input.retryPriority || input.rules.length === 0)
|
|
454
|
+
return this.acquireFromLane(input);
|
|
455
|
+
const ordered = [...input.rules].sort((a, b) =>
|
|
456
|
+
a.ruleId.localeCompare(b.ruleId),
|
|
457
|
+
);
|
|
458
|
+
const laneKey = rateLaneKey(input.bucketId, rulesSignature(ordered));
|
|
459
|
+
let complete!: () => void;
|
|
460
|
+
const pending = new Promise<void>((resolve) => {
|
|
461
|
+
complete = resolve;
|
|
462
|
+
});
|
|
463
|
+
const retries =
|
|
464
|
+
this.retryAdmissions.get(laneKey) ?? new Set<Promise<void>>();
|
|
465
|
+
retries.add(pending);
|
|
466
|
+
this.retryAdmissions.set(laneKey, retries);
|
|
467
|
+
try {
|
|
468
|
+
return await this.acquireFromLane(input);
|
|
469
|
+
} finally {
|
|
470
|
+
retries.delete(pending);
|
|
471
|
+
if (retries.size === 0) this.retryAdmissions.delete(laneKey);
|
|
472
|
+
complete();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async waitForRetryAdmissions(
|
|
477
|
+
laneKey: string,
|
|
478
|
+
bucketId: string,
|
|
479
|
+
acquireStartedAt: number,
|
|
480
|
+
signal?: AbortSignal,
|
|
481
|
+
): Promise<void> {
|
|
482
|
+
const retries = this.retryAdmissions.get(laneKey);
|
|
483
|
+
if (!retries?.size) return;
|
|
484
|
+
const remainingMs = Math.max(
|
|
485
|
+
0,
|
|
486
|
+
PROVIDER_ADMISSION_WAIT_BUDGET_MS - (this.now() - acquireStartedAt),
|
|
487
|
+
);
|
|
488
|
+
await new Promise<void>((resolve, reject) => {
|
|
489
|
+
const onAbort = () => finish(abortReason(signal!));
|
|
490
|
+
const timer = setTimeout(
|
|
491
|
+
() =>
|
|
492
|
+
finish(
|
|
493
|
+
new ProviderAdmissionWaitTimeoutError({
|
|
494
|
+
provider: providerNameFromBucketId(bucketId),
|
|
495
|
+
waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
|
|
496
|
+
}),
|
|
497
|
+
),
|
|
498
|
+
remainingMs,
|
|
499
|
+
);
|
|
500
|
+
timer.unref();
|
|
501
|
+
const finish = (error?: unknown) => {
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
signal?.removeEventListener('abort', onAbort);
|
|
504
|
+
if (error !== undefined) reject(error);
|
|
505
|
+
else resolve();
|
|
506
|
+
};
|
|
507
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
508
|
+
if (signal?.aborted) onAbort();
|
|
509
|
+
else void Promise.all([...retries]).then(() => finish());
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async acquireFromLane(input: {
|
|
514
|
+
bucketId: string;
|
|
515
|
+
rateScopeToken?: string | null;
|
|
516
|
+
rules: readonly PacingRule[];
|
|
517
|
+
signal?: AbortSignal;
|
|
518
|
+
yieldProviderHolds?: boolean;
|
|
519
|
+
retryPriority?: boolean;
|
|
413
520
|
}): Promise<PacingPermit> {
|
|
414
521
|
const { bucketId, rules, signal } = input;
|
|
415
522
|
if (rules.length === 0) return noopPacingPermit();
|
|
@@ -439,6 +546,19 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
439
546
|
? signal.reason
|
|
440
547
|
: new Error('Rate-state acquire aborted.');
|
|
441
548
|
}
|
|
549
|
+
if (!input.retryPriority && this.retryAdmissions.get(laneKey)?.size) {
|
|
550
|
+
await this.waitForRetryAdmissions(
|
|
551
|
+
laneKey,
|
|
552
|
+
bucketId,
|
|
553
|
+
acquireStartedAt,
|
|
554
|
+
signal,
|
|
555
|
+
);
|
|
556
|
+
// A new retry may have joined while the previous preferred call drew
|
|
557
|
+
// its permit. Never let a fresh continuation consume that next grant.
|
|
558
|
+
if (this.retryAdmissions.get(laneKey)?.size) continue;
|
|
559
|
+
this.throwIfPacerExhausted(bucketId, input.yieldProviderHolds === true);
|
|
560
|
+
signal?.throwIfAborted();
|
|
561
|
+
}
|
|
442
562
|
const localPermit = this.drawFromBlock(bucketId, laneKey);
|
|
443
563
|
if (localPermit) {
|
|
444
564
|
const waitMs = localPermit.scheduledAtMs - this.now();
|
|
@@ -458,7 +578,20 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
458
578
|
});
|
|
459
579
|
}
|
|
460
580
|
try {
|
|
461
|
-
if (
|
|
581
|
+
if (
|
|
582
|
+
waitMs > 0 &&
|
|
583
|
+
!(await this.waitForScheduledPermit(
|
|
584
|
+
bucketId,
|
|
585
|
+
localPermit.retryAtMsAtGrant,
|
|
586
|
+
waitMs,
|
|
587
|
+
signal,
|
|
588
|
+
))
|
|
589
|
+
) {
|
|
590
|
+
await this.cancelUndispatchedPermit(localPermit);
|
|
591
|
+
const pendingPenalty = this.pendingPenaltyWrites.get(bucketId);
|
|
592
|
+
if (pendingPenalty) await pendingPenalty;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
462
595
|
} catch (error) {
|
|
463
596
|
await this.cancelUndispatchedPermit(localPermit);
|
|
464
597
|
throw error;
|
|
@@ -468,7 +601,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
468
601
|
throw abortReason(signal);
|
|
469
602
|
}
|
|
470
603
|
if (
|
|
471
|
-
this.now() - acquireStartedAt >=
|
|
604
|
+
this.now() - acquireStartedAt >=
|
|
605
|
+
PROVIDER_ADMISSION_WAIT_BUDGET_MS
|
|
472
606
|
) {
|
|
473
607
|
await this.cancelUndispatchedPermit(localPermit);
|
|
474
608
|
throw new ProviderAdmissionWaitTimeoutError({
|
|
@@ -532,12 +666,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
532
666
|
// (10 rps declared, 100 rps learned) to ten permits per round trip, which on
|
|
533
667
|
// a hot map is a round trip per permit. The store's own token floor, not
|
|
534
668
|
// this number, is what bounds the grant.
|
|
535
|
-
const
|
|
669
|
+
const learnedRps = this.effectiveRps.get(laneKey);
|
|
670
|
+
const recoveringUnknownProvider =
|
|
671
|
+
learnedRps !== undefined &&
|
|
672
|
+
learnedRps < DEFAULT_UNKNOWN_PROVIDER_FAST_RECOVERY_RPS &&
|
|
673
|
+
rules.some((rule) => rule.ruleId === 'default:provider-scope') &&
|
|
674
|
+
rules.every((rule) => rule.ruleId.startsWith('default:'));
|
|
675
|
+
// Below recovered capacity, the next clean observation can double the
|
|
676
|
+
// pace after 500ms. Do not reserve a full second at the old slower pace:
|
|
677
|
+
// that schedule debt would delay grants even after learning succeeds.
|
|
678
|
+
const horizonMs = Math.min(
|
|
679
|
+
recoveringUnknownProvider
|
|
680
|
+
? UNKNOWN_PROVIDER_RECOVERY_BLOCK_HORIZON_MS
|
|
681
|
+
: RATE_BLOCK_HORIZON_MS,
|
|
682
|
+
leasedBlockTtlMs(rules),
|
|
683
|
+
);
|
|
536
684
|
const rateBudget = Math.max(
|
|
537
685
|
1,
|
|
538
|
-
Math.floor(
|
|
539
|
-
((this.effectiveRps.get(laneKey) ?? configuredRps) * horizonMs) / 1_000,
|
|
540
|
-
),
|
|
686
|
+
Math.floor(((learnedRps ?? configuredRps) * horizonMs) / 1_000),
|
|
541
687
|
);
|
|
542
688
|
if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
|
|
543
689
|
const waiters = this.waiters.get(laneKey) ?? 1;
|
|
@@ -596,6 +742,16 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
596
742
|
return;
|
|
597
743
|
}
|
|
598
744
|
const existing = this.cooldowns.get(bucketId);
|
|
745
|
+
// Expiry ends the wait, not the hold's fence. An acquire may discover a
|
|
746
|
+
// remote throttle after its deadline; wake tickets minted before it now.
|
|
747
|
+
if (claimedRetryAtMs > (this.localProviderRetryAtMs.get(bucketId) ?? 0)) {
|
|
748
|
+
this.recordLocalProviderRetryAt(bucketId, claimedRetryAtMs);
|
|
749
|
+
this.validationGenerations.set(
|
|
750
|
+
bucketId,
|
|
751
|
+
(this.validationGenerations.get(bucketId) ?? 0) + 1,
|
|
752
|
+
);
|
|
753
|
+
this.validationFlights.delete(bucketId);
|
|
754
|
+
}
|
|
599
755
|
this.cooldowns.set(bucketId, {
|
|
600
756
|
coolUntilMs: Math.max(
|
|
601
757
|
existing && existing.coolUntilMs > now ? existing.coolUntilMs : 0,
|
|
@@ -612,6 +768,50 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
612
768
|
});
|
|
613
769
|
}
|
|
614
770
|
|
|
771
|
+
private recordLocalProviderRetryAt(
|
|
772
|
+
bucketId: string,
|
|
773
|
+
retryAtMs: number,
|
|
774
|
+
): void {
|
|
775
|
+
if (retryAtMs <= (this.localProviderRetryAtMs.get(bucketId) ?? 0)) return;
|
|
776
|
+
this.localProviderRetryAtMs.set(bucketId, retryAtMs);
|
|
777
|
+
for (const waiting of this.scheduledPermitWaits.get(bucketId) ?? []) {
|
|
778
|
+
if (waiting.retryAtMsAtGrant < retryAtMs) waiting.invalidate();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** A newer hold invalidates old tickets, including their obsolete sleep. */
|
|
783
|
+
private async waitForScheduledPermit(
|
|
784
|
+
bucketId: string,
|
|
785
|
+
retryAtMsAtGrant: number,
|
|
786
|
+
waitMs: number,
|
|
787
|
+
signal?: AbortSignal,
|
|
788
|
+
): Promise<boolean> {
|
|
789
|
+
let invalidated = false;
|
|
790
|
+
let invalidate!: () => void;
|
|
791
|
+
const interrupted = new Promise<void>((resolve) => {
|
|
792
|
+
invalidate = () => {
|
|
793
|
+
invalidated = true;
|
|
794
|
+
resolve();
|
|
795
|
+
};
|
|
796
|
+
});
|
|
797
|
+
const waiting = { retryAtMsAtGrant, invalidate };
|
|
798
|
+
const waiters = this.scheduledPermitWaits.get(bucketId) ?? new Set();
|
|
799
|
+
waiters.add(waiting);
|
|
800
|
+
this.scheduledPermitWaits.set(bucketId, waiters);
|
|
801
|
+
try {
|
|
802
|
+
if ((this.localProviderRetryAtMs.get(bucketId) ?? 0) > retryAtMsAtGrant)
|
|
803
|
+
return false;
|
|
804
|
+
await Promise.race([
|
|
805
|
+
this.sleep(waitMs, signal, interrupted),
|
|
806
|
+
interrupted,
|
|
807
|
+
]);
|
|
808
|
+
return !invalidated;
|
|
809
|
+
} finally {
|
|
810
|
+
waiters.delete(waiting);
|
|
811
|
+
if (waiters.size === 0) this.scheduledPermitWaits.delete(bucketId);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
615
815
|
/**
|
|
616
816
|
* Revalidate a cached ticket against the shared provider hold. Concurrent
|
|
617
817
|
* rows in this runner share one in-flight read per provider bucket; completed
|
|
@@ -622,6 +822,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
622
822
|
bucketId: string,
|
|
623
823
|
rateScopeToken: string | null,
|
|
624
824
|
scopeRetryAtMsAtGrant: number,
|
|
825
|
+
dispatchValidationAtGateway = false,
|
|
625
826
|
): Promise<boolean> {
|
|
626
827
|
const validate = this.port.validate;
|
|
627
828
|
if (!validate || !this.validationScopes.has(bucketId)) return true;
|
|
@@ -638,6 +839,18 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
638
839
|
return false;
|
|
639
840
|
}
|
|
640
841
|
|
|
842
|
+
// Integration execute carries the signed scope and grant snapshot to
|
|
843
|
+
// this receiver's existing post-queue validator. A preceding worker RPC
|
|
844
|
+
// only adds another admission round trip; it cannot make that later
|
|
845
|
+
// check fresher. Direct HTTP and pre-expansion receivers retain the read.
|
|
846
|
+
if (dispatchValidationAtGateway && rateScopeToken) {
|
|
847
|
+
if (localRetryAtMs > scopeRetryAtMsAtGrant) {
|
|
848
|
+
this.drainBlocksForBucket(bucketId, localRetryAtMs);
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
|
|
641
854
|
const generation = this.validationGenerations.get(bucketId) ?? 0;
|
|
642
855
|
let flight = this.validationFlights.get(bucketId);
|
|
643
856
|
if (!flight || flight.generation !== generation) {
|
|
@@ -664,7 +877,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
664
877
|
}
|
|
665
878
|
if (!result.allowed) {
|
|
666
879
|
const retryAtMs = result.retryAtMs;
|
|
667
|
-
this.localProviderRetryAtMs.
|
|
880
|
+
if (retryAtMs > (this.localProviderRetryAtMs.get(bucketId) ?? 0)) {
|
|
881
|
+
this.validationGenerations.set(bucketId, generation + 1);
|
|
882
|
+
this.validationFlights.delete(bucketId);
|
|
883
|
+
}
|
|
884
|
+
this.recordLocalProviderRetryAt(bucketId, retryAtMs);
|
|
668
885
|
this.drainBlocksForBucket(bucketId);
|
|
669
886
|
this.recordCooldown(bucketId, {
|
|
670
887
|
coolUntilMs: Math.min(
|
|
@@ -676,13 +893,12 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
676
893
|
});
|
|
677
894
|
return false;
|
|
678
895
|
}
|
|
679
|
-
if (localRetryAtMs <= result.serverNowMs) {
|
|
680
|
-
this.localProviderRetryAtMs.delete(bucketId);
|
|
681
|
-
}
|
|
682
896
|
// The hold's deadline is monotonic. Even after it expires, a ticket
|
|
683
897
|
// minted before that hold belongs to the old pace and must be replaced.
|
|
684
898
|
// Preserve newer tickets another waiter may already have acquired.
|
|
685
899
|
if (result.retryAtMs > scopeRetryAtMsAtGrant) {
|
|
900
|
+
this.validationGenerations.set(bucketId, generation + 1);
|
|
901
|
+
this.validationFlights.delete(bucketId);
|
|
686
902
|
this.drainBlocksForBucket(bucketId, result.retryAtMs);
|
|
687
903
|
return false;
|
|
688
904
|
}
|
|
@@ -781,7 +997,25 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
781
997
|
await Promise.resolve();
|
|
782
998
|
if (signal?.aborted) return;
|
|
783
999
|
}
|
|
1000
|
+
// A legacy receiver cannot revalidate a grant at dispatch. Persist the
|
|
1001
|
+
// known hold before asking the store for any replacement, including a
|
|
1002
|
+
// newer penalty registered while an earlier one was completing.
|
|
1003
|
+
const pendingPenalty = this.pendingPenaltyWrites.get(bucketId);
|
|
1004
|
+
if (pendingPenalty) {
|
|
1005
|
+
await pendingPenalty;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
784
1008
|
try {
|
|
1009
|
+
let releaseSignal = this.leaseReleaseSignals.get(bucketId);
|
|
1010
|
+
if (
|
|
1011
|
+
!releaseSignal &&
|
|
1012
|
+
ordered.some((rule) => rule.maxConcurrency != null)
|
|
1013
|
+
) {
|
|
1014
|
+
releaseSignal = { generation: 0, waiters: new Set() };
|
|
1015
|
+
this.leaseReleaseSignals.set(bucketId, releaseSignal);
|
|
1016
|
+
}
|
|
1017
|
+
// A completed release can make an in-flight zero-grant response stale.
|
|
1018
|
+
const releaseGeneration = releaseSignal?.generation;
|
|
785
1019
|
const successKey = pendingSuccessKey(bucketId, ordered);
|
|
786
1020
|
const feedback = this.snapshotSuccessFeedback(successKey);
|
|
787
1021
|
const response = await this.port.acquire({
|
|
@@ -854,7 +1088,32 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
854
1088
|
// and single-flight another refill if still empty. ±10% jitter so a
|
|
855
1089
|
// synchronized wave re-probes at spread-out times; capped by
|
|
856
1090
|
// MAX_ACQUIRE_SLEEP_MS.
|
|
857
|
-
|
|
1091
|
+
const waitMs = this.jitteredWaitMs(response.waitMs);
|
|
1092
|
+
const serverNowMs = response.serverNowMs ?? this.serverNow(bucketId);
|
|
1093
|
+
const authoritativeHold =
|
|
1094
|
+
(response.claimedRetryAtMs ?? 0) > serverNowMs ||
|
|
1095
|
+
(response.coolUntilMs ?? 0) > serverNowMs;
|
|
1096
|
+
if (!releaseSignal || authoritativeHold) {
|
|
1097
|
+
await this.sleep(waitMs);
|
|
1098
|
+
} else {
|
|
1099
|
+
// Lease expiry predicts when a saturated scope can admit again;
|
|
1100
|
+
// our own confirmed release is fresher evidence. Re-query the store,
|
|
1101
|
+
// which still owns rate/concurrency admission and provider holds.
|
|
1102
|
+
let wake!: () => void;
|
|
1103
|
+
const released = new Promise<void>((resolve) => {
|
|
1104
|
+
wake = resolve;
|
|
1105
|
+
});
|
|
1106
|
+
releaseSignal.waiters.add(wake);
|
|
1107
|
+
try {
|
|
1108
|
+
if (releaseSignal.generation !== releaseGeneration) return;
|
|
1109
|
+
await Promise.race([
|
|
1110
|
+
this.sleep(waitMs, undefined, released),
|
|
1111
|
+
released,
|
|
1112
|
+
]);
|
|
1113
|
+
} finally {
|
|
1114
|
+
releaseSignal.waiters.delete(wake);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
858
1117
|
return;
|
|
859
1118
|
} catch (error) {
|
|
860
1119
|
// A provider hold is not a store failure. Preserve its deadline for
|
|
@@ -898,6 +1157,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
898
1157
|
serverNowMs?: unknown;
|
|
899
1158
|
scheduledAtMs?: unknown;
|
|
900
1159
|
claimedRetryAtMs?: number;
|
|
1160
|
+
dispatchValidationSupported?: boolean;
|
|
901
1161
|
},
|
|
902
1162
|
): void {
|
|
903
1163
|
const hasConcurrency = ordered.some((rule) => rule.maxConcurrency != null);
|
|
@@ -1103,6 +1363,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1103
1363
|
concurrencyGrants[index] ?? null,
|
|
1104
1364
|
pacingGrant,
|
|
1105
1365
|
Math.max(0, response.claimedRetryAtMs ?? 0),
|
|
1366
|
+
response.dispatchValidationSupported === true,
|
|
1106
1367
|
],
|
|
1107
1368
|
),
|
|
1108
1369
|
rulesKey,
|
|
@@ -1145,23 +1406,6 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1145
1406
|
});
|
|
1146
1407
|
}
|
|
1147
1408
|
|
|
1148
|
-
this.validationGenerations.set(
|
|
1149
|
-
input.bucketId,
|
|
1150
|
-
(this.validationGenerations.get(input.bucketId) ?? 0) + 1,
|
|
1151
|
-
);
|
|
1152
|
-
this.validationFlights.delete(input.bucketId);
|
|
1153
|
-
this.localProviderRetryAtMs.set(
|
|
1154
|
-
input.bucketId,
|
|
1155
|
-
Math.max(this.localProviderRetryAtMs.get(input.bucketId) ?? 0, retryAtMs),
|
|
1156
|
-
);
|
|
1157
|
-
this.recordCooldown(input.bucketId, {
|
|
1158
|
-
coolUntilMs: Math.min(
|
|
1159
|
-
retryAtMs,
|
|
1160
|
-
this.serverNow(input.bucketId) + PROVIDER_AIMD_COOLDOWN_CAP_MS,
|
|
1161
|
-
),
|
|
1162
|
-
claimedRetryAtMs: retryAtMs,
|
|
1163
|
-
});
|
|
1164
|
-
this.drainBlocksForBucket(input.bucketId);
|
|
1165
1409
|
const successKey = pendingSuccessKey(input.bucketId, input.rules ?? []);
|
|
1166
1410
|
const feedback = this.snapshotSuccessFeedback(successKey);
|
|
1167
1411
|
const previousPenalty = this.pendingPenaltyWrites.get(input.bucketId);
|
|
@@ -1185,6 +1429,21 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1185
1429
|
}),
|
|
1186
1430
|
]).then(() => undefined);
|
|
1187
1431
|
this.pendingPenaltyWrites.set(input.bucketId, penaltyWrite);
|
|
1432
|
+
// Register the durable intent before invalidation wakes assigned callers.
|
|
1433
|
+
this.validationGenerations.set(
|
|
1434
|
+
input.bucketId,
|
|
1435
|
+
(this.validationGenerations.get(input.bucketId) ?? 0) + 1,
|
|
1436
|
+
);
|
|
1437
|
+
this.validationFlights.delete(input.bucketId);
|
|
1438
|
+
this.recordLocalProviderRetryAt(input.bucketId, retryAtMs);
|
|
1439
|
+
this.recordCooldown(input.bucketId, {
|
|
1440
|
+
coolUntilMs: Math.min(
|
|
1441
|
+
retryAtMs,
|
|
1442
|
+
this.serverNow(input.bucketId) + PROVIDER_AIMD_COOLDOWN_CAP_MS,
|
|
1443
|
+
),
|
|
1444
|
+
claimedRetryAtMs: retryAtMs,
|
|
1445
|
+
});
|
|
1446
|
+
this.drainBlocksForBucket(input.bucketId);
|
|
1188
1447
|
try {
|
|
1189
1448
|
await penaltyWrite;
|
|
1190
1449
|
} catch (error) {
|
|
@@ -1305,6 +1564,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1305
1564
|
concurrencyGrant,
|
|
1306
1565
|
pacingGrant,
|
|
1307
1566
|
scopeRetryAtMsAtGrant,
|
|
1567
|
+
dispatchValidationSupported,
|
|
1308
1568
|
] = scheduled;
|
|
1309
1569
|
let settled = false;
|
|
1310
1570
|
const refund = async (): Promise<void> => {
|
|
@@ -1329,17 +1589,108 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1329
1589
|
bucketId,
|
|
1330
1590
|
rateScopeToken,
|
|
1331
1591
|
retryAtMsAtGrant: scopeRetryAtMsAtGrant,
|
|
1592
|
+
...(dispatchValidationSupported
|
|
1593
|
+
? { dispatchValidationSupported: true }
|
|
1594
|
+
: {}),
|
|
1332
1595
|
}
|
|
1333
1596
|
: null,
|
|
1334
|
-
validate: async (
|
|
1597
|
+
validate: async (
|
|
1598
|
+
signal?: AbortSignal,
|
|
1599
|
+
options?: ProviderPermitValidationOptions,
|
|
1600
|
+
) => {
|
|
1335
1601
|
try {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1602
|
+
const startedAtMs = this.now();
|
|
1603
|
+
while (true) {
|
|
1604
|
+
signal?.throwIfAborted();
|
|
1605
|
+
if (settled) return false;
|
|
1606
|
+
if (
|
|
1607
|
+
!(await this.validateProviderPermit(
|
|
1608
|
+
bucketId,
|
|
1609
|
+
rateScopeToken,
|
|
1610
|
+
scopeRetryAtMsAtGrant,
|
|
1611
|
+
dispatchValidationSupported &&
|
|
1612
|
+
options?.dispatchValidationAtGateway === true,
|
|
1613
|
+
))
|
|
1614
|
+
)
|
|
1615
|
+
return false;
|
|
1616
|
+
const generation = this.validationGenerations.get(bucketId) ?? 0;
|
|
1617
|
+
while (true) {
|
|
1618
|
+
signal?.throwIfAborted();
|
|
1619
|
+
if (settled) return false;
|
|
1620
|
+
if (
|
|
1621
|
+
(this.validationGenerations.get(bucketId) ?? 0) !== generation
|
|
1622
|
+
)
|
|
1623
|
+
break;
|
|
1624
|
+
if (
|
|
1625
|
+
(this.localProviderRetryAtMs.get(bucketId) ?? 0) >
|
|
1626
|
+
this.serverNow(bucketId)
|
|
1627
|
+
)
|
|
1628
|
+
return false;
|
|
1629
|
+
const targetRequestsPerSecond =
|
|
1630
|
+
this.effectiveRps.get(laneKey) ??
|
|
1631
|
+
pacingGrant?.targetRequestsPerSecond;
|
|
1632
|
+
if (targetRequestsPerSecond == null) return true;
|
|
1633
|
+
// A delayed shared validation must not collapse reserved slots
|
|
1634
|
+
// into a burst. Advance this cursor only when a send is ready;
|
|
1635
|
+
// sleeping/cancelled callers never reserve future positions.
|
|
1636
|
+
// The database owns admission and the app rechecks provider
|
|
1637
|
+
// holds at arrival. This local fence only delays spent permits.
|
|
1638
|
+
const spacingMs = Math.max(
|
|
1639
|
+
1,
|
|
1640
|
+
Math.ceil(1_000 / targetRequestsPerSecond),
|
|
1641
|
+
);
|
|
1642
|
+
const burstSize = Math.max(
|
|
1643
|
+
1,
|
|
1644
|
+
Math.floor(targetRequestsPerSecond / 100),
|
|
1645
|
+
);
|
|
1646
|
+
const previous = this.validatedDispatchCadences.get(bucketId);
|
|
1647
|
+
const nowMs = this.now();
|
|
1648
|
+
const sameCadence =
|
|
1649
|
+
previous?.spacingMs === spacingMs &&
|
|
1650
|
+
previous.burstSize === burstSize;
|
|
1651
|
+
// A fresh authoritative rate also governs already prepared
|
|
1652
|
+
// permits. Carry debt from the last send when the rate changes;
|
|
1653
|
+
// a staggered burst's first send can precede it by an interval.
|
|
1654
|
+
const previousBoundaryMs = previous
|
|
1655
|
+
? (sameCadence ? previous.atMs : previous.lastAtMs) +
|
|
1656
|
+
spacingMs * burstSize
|
|
1657
|
+
: nowMs;
|
|
1658
|
+
const inBurst =
|
|
1659
|
+
sameCadence &&
|
|
1660
|
+
previous.used < burstSize &&
|
|
1661
|
+
nowMs < previousBoundaryMs;
|
|
1662
|
+
const waitMs = inBurst
|
|
1663
|
+
? 0
|
|
1664
|
+
: Math.max(0, previousBoundaryMs - nowMs);
|
|
1665
|
+
if (
|
|
1666
|
+
nowMs - startedAtMs + waitMs >=
|
|
1667
|
+
PROVIDER_ADMISSION_WAIT_BUDGET_MS
|
|
1668
|
+
) {
|
|
1669
|
+
throw new ProviderAdmissionWaitTimeoutError({
|
|
1670
|
+
provider: providerNameFromBucketId(bucketId),
|
|
1671
|
+
waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (waitMs > 0) {
|
|
1675
|
+
await this.sleep(waitMs, signal);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
this.validatedDispatchCadences.set(bucketId, {
|
|
1679
|
+
atMs: inBurst ? previous.atMs : nowMs,
|
|
1680
|
+
lastAtMs: nowMs,
|
|
1681
|
+
used: inBurst ? previous.used + 1 : 1,
|
|
1682
|
+
burstSize,
|
|
1683
|
+
spacingMs,
|
|
1684
|
+
});
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1341
1688
|
} catch (error) {
|
|
1342
|
-
if (
|
|
1689
|
+
if (
|
|
1690
|
+
signal?.aborted ||
|
|
1691
|
+
error instanceof ProviderAdmissionWaitTimeoutError ||
|
|
1692
|
+
error instanceof RateStateValidationUnavailableError
|
|
1693
|
+
) {
|
|
1343
1694
|
throw error;
|
|
1344
1695
|
}
|
|
1345
1696
|
const normalized = normalizeError(error);
|
|
@@ -1450,6 +1801,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1450
1801
|
permit: PacingPermit;
|
|
1451
1802
|
scheduledAtMs: number;
|
|
1452
1803
|
reservationId: string | null;
|
|
1804
|
+
retryAtMsAtGrant: number;
|
|
1453
1805
|
} | null {
|
|
1454
1806
|
const block = this.liveBlock(bucketId, laneKey);
|
|
1455
1807
|
if (!block) return null;
|
|
@@ -1461,6 +1813,15 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1461
1813
|
permit: this.permitFor(bucketId, laneKey, block, scheduled),
|
|
1462
1814
|
scheduledAtMs: scheduled[0],
|
|
1463
1815
|
reservationId: scheduled[2],
|
|
1816
|
+
// Legacy gateways do not attest a hold snapshot. Their newly assigned
|
|
1817
|
+
// tickets retain existing behavior after an expired local hold; a later
|
|
1818
|
+
// local 429 still wakes them. Modern snapshots invalidate stale grants.
|
|
1819
|
+
retryAtMsAtGrant: this.validationScopes.has(bucketId)
|
|
1820
|
+
? scheduled[5]
|
|
1821
|
+
: Math.max(
|
|
1822
|
+
scheduled[5],
|
|
1823
|
+
this.localProviderRetryAtMs.get(bucketId) ?? 0,
|
|
1824
|
+
),
|
|
1464
1825
|
};
|
|
1465
1826
|
}
|
|
1466
1827
|
|
|
@@ -1529,6 +1890,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1529
1890
|
refundReservationIds: [input.reservationId],
|
|
1530
1891
|
schedulerSchema: this.schedulerSchema,
|
|
1531
1892
|
});
|
|
1893
|
+
if (input.leaseIds.length > 0) this.signalLeaseRelease(input.bucketId);
|
|
1532
1894
|
} catch (error) {
|
|
1533
1895
|
const normalized = normalizeError(error);
|
|
1534
1896
|
this.onStoreFailure({
|
|
@@ -1582,6 +1944,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1582
1944
|
leaseIds,
|
|
1583
1945
|
schedulerSchema: this.schedulerSchema,
|
|
1584
1946
|
});
|
|
1947
|
+
this.signalLeaseRelease(pending.bucketId);
|
|
1585
1948
|
} catch (error) {
|
|
1586
1949
|
const normalized = normalizeError(error);
|
|
1587
1950
|
this.pendingReleaseFailure = new Error(
|
|
@@ -1610,6 +1973,13 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
1610
1973
|
this.pendingReleaseFailure = null;
|
|
1611
1974
|
throw error;
|
|
1612
1975
|
}
|
|
1976
|
+
|
|
1977
|
+
private signalLeaseRelease(bucketId: string): void {
|
|
1978
|
+
const signal = this.leaseReleaseSignals.get(bucketId);
|
|
1979
|
+
if (!signal) return;
|
|
1980
|
+
signal.generation += 1;
|
|
1981
|
+
for (const wake of signal.waiters) wake();
|
|
1982
|
+
}
|
|
1613
1983
|
}
|
|
1614
1984
|
|
|
1615
1985
|
/**
|