deepline 0.3.184 → 0.3.185
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/shared_libs/play-runtime/app-runtime-api.ts +53 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +751 -193
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +437 -150
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +47 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +22 -2
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +11 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +102 -12
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +136 -28
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +80 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +897 -207
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +120 -34
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +82 -2
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +7 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +1 -0
- 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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveRowConcurrency,
|
|
22
22
|
} from './policy';
|
|
23
23
|
import {
|
|
24
|
+
DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
|
|
24
25
|
type PacingPermit,
|
|
25
26
|
type PacingRule,
|
|
26
27
|
type ProviderConcurrencyGrant,
|
|
@@ -50,6 +51,8 @@ export interface ProviderPermit extends WorkLease {
|
|
|
50
51
|
readonly concurrencyGrant?: ProviderConcurrencyGrant | null;
|
|
51
52
|
/** Effective rate-controller reading for this physical provider permit. */
|
|
52
53
|
readonly pacingGrant?: ProviderPacingGrant | null;
|
|
54
|
+
/** Signed shared-scope proof consumed by the execution gateway after queueing. */
|
|
55
|
+
readonly gatewayDispatchProof?: PacingPermit['gatewayDispatchProof'];
|
|
53
56
|
/**
|
|
54
57
|
* Return an acquired provider permit before an outbound request started.
|
|
55
58
|
* Unlike `release`, this returns the rate reservation as well as any
|
|
@@ -64,6 +67,8 @@ export interface ProviderPermit extends WorkLease {
|
|
|
64
67
|
* backend's lease block when that block is still live; otherwise refunds.
|
|
65
68
|
*/
|
|
66
69
|
park(): Promise<void>;
|
|
70
|
+
/** Recheck shared provider state immediately before network dispatch. */
|
|
71
|
+
validate?(): Promise<boolean>;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
export type BudgetKind = 'toolCall' | 'retry' | 'waterfallStep';
|
|
@@ -174,7 +179,7 @@ export interface PlayExecutionGovernor {
|
|
|
174
179
|
*/
|
|
175
180
|
acquireProviderPermit(
|
|
176
181
|
toolId: string,
|
|
177
|
-
opts?: { signal?: AbortSignal },
|
|
182
|
+
opts?: { signal?: AbortSignal; yieldProviderHolds?: boolean },
|
|
178
183
|
): Promise<ProviderPermit>;
|
|
179
184
|
|
|
180
185
|
/**
|
|
@@ -226,6 +231,8 @@ export interface PlayExecutionGovernor {
|
|
|
226
231
|
*/
|
|
227
232
|
toolId?: string;
|
|
228
233
|
retryAfterMs: number;
|
|
234
|
+
/** Absolute provider retry deadline from the observed response. */
|
|
235
|
+
retryAtMs?: number;
|
|
229
236
|
/** Provider residence for the rejected physical request, when observed. */
|
|
230
237
|
latencyMs?: number | null;
|
|
231
238
|
/** Server-authored context for attributing this physical provider 429. */
|
|
@@ -434,6 +441,15 @@ export function createPlayExecutionGovernor(
|
|
|
434
441
|
provider,
|
|
435
442
|
concurrencyGrant: rateLease.concurrencyGrant ?? null,
|
|
436
443
|
pacingGrant: rateLease.pacingGrant ?? null,
|
|
444
|
+
gatewayDispatchProof: rateLease.gatewayDispatchProof ?? null,
|
|
445
|
+
...(rateLease.validate
|
|
446
|
+
? {
|
|
447
|
+
validate: async () => {
|
|
448
|
+
if (settled) return false;
|
|
449
|
+
return await rateLease.validate!();
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
: {}),
|
|
437
453
|
release() {
|
|
438
454
|
if (settled) return;
|
|
439
455
|
settled = true;
|
|
@@ -487,16 +503,37 @@ export function createPlayExecutionGovernor(
|
|
|
487
503
|
// Passthrough: hand the DB-backed pacer the declared rules verbatim (or
|
|
488
504
|
// the default-pacing shape when undeclared). No in-memory admission — the
|
|
489
505
|
// row is authoritative.
|
|
490
|
-
const
|
|
506
|
+
const fallback = defaultPacingForTool(toolId, policy);
|
|
507
|
+
const pacing = declaredPacing ?? {
|
|
508
|
+
...fallback,
|
|
509
|
+
provider: resolvedPacing?.provider ?? fallback.provider,
|
|
510
|
+
};
|
|
511
|
+
if (!declaredPacing) {
|
|
512
|
+
// bucketId is the authenticated quota scope. Unknown tools sharing a
|
|
513
|
+
// credential must not each mint an independent copy of the default rate.
|
|
514
|
+
// Start its one shared lane at one probe/sec; the DB-owned AIMD quickly
|
|
515
|
+
// ramps on sustained success and reacts to Retry-After if the provider
|
|
516
|
+
// tells us the quota is lower.
|
|
517
|
+
pacing.rules = pacing.rules.map((rule) => ({
|
|
518
|
+
...rule,
|
|
519
|
+
ruleId: 'default:provider-scope',
|
|
520
|
+
requestsPerWindow: DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS,
|
|
521
|
+
windowMs: 1_000,
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
491
524
|
providerByTool.set(toolId, pacing.provider);
|
|
492
525
|
pacingRulesByTool.set(toolId, pacing.rules);
|
|
493
526
|
return pacing;
|
|
494
527
|
}
|
|
528
|
+
const fallback = defaultPacingForTool(toolId, policy);
|
|
495
529
|
const pacing = adaptiveAdmission.resolvePacing({
|
|
496
530
|
orgId: input.scope.orgId,
|
|
497
531
|
toolId,
|
|
498
532
|
declaredPacing,
|
|
499
|
-
fallbackPacing:
|
|
533
|
+
fallbackPacing: {
|
|
534
|
+
...fallback,
|
|
535
|
+
provider: resolvedPacing?.provider ?? fallback.provider,
|
|
536
|
+
},
|
|
500
537
|
});
|
|
501
538
|
providerByTool.set(toolId, pacing.provider);
|
|
502
539
|
pacingRulesByTool.set(toolId, pacing.rules);
|
|
@@ -623,8 +660,13 @@ export function createPlayExecutionGovernor(
|
|
|
623
660
|
rateScopeToken: rateScope[1],
|
|
624
661
|
rules: pacing.rules,
|
|
625
662
|
signal: opts?.signal,
|
|
663
|
+
yieldProviderHolds: opts?.yieldProviderHolds,
|
|
626
664
|
});
|
|
627
|
-
return providerPermitFrom(
|
|
665
|
+
return providerPermitFrom(
|
|
666
|
+
pacing.provider,
|
|
667
|
+
rateLease,
|
|
668
|
+
localConcurrencySlot,
|
|
669
|
+
);
|
|
628
670
|
} catch (error) {
|
|
629
671
|
localConcurrencySlot?.release();
|
|
630
672
|
throw error;
|
|
@@ -727,6 +769,7 @@ export function createPlayExecutionGovernor(
|
|
|
727
769
|
bucketId: rateScope[0],
|
|
728
770
|
...(rateScope[1] ? { rateScopeToken: rateScope[1] } : {}),
|
|
729
771
|
cooldownMs: bp.retryAfterMs,
|
|
772
|
+
...(bp.retryAtMs != null ? { retryAtMs: bp.retryAtMs } : {}),
|
|
730
773
|
...(rules ? { rules } : {}),
|
|
731
774
|
...(bp.latencyMs != null ? { latencyMs: bp.latencyMs } : {}),
|
|
732
775
|
...(bp.concurrencyGrant
|
|
@@ -188,7 +188,9 @@ export const SHARED_EXECUTION_POLICY: ResolvedExecutionPolicy = {
|
|
|
188
188
|
maxWaterfallStepExecutions: 5_000_000,
|
|
189
189
|
},
|
|
190
190
|
pacing: {
|
|
191
|
-
//
|
|
191
|
+
// Keep the generic fallback rate. The shared Postgres backend separately
|
|
192
|
+
// cold-starts unknown provider scopes at one probe/sec.
|
|
193
|
+
// Declared providers (rate-limit-definitions.ts) keep their own rates.
|
|
192
194
|
defaultProviderRequestsPerSecond: 10,
|
|
193
195
|
// This is the scheduler's resident-call envelope, not an RPS target.
|
|
194
196
|
// Keep it aligned with the explicit run ceiling so an RPS-only provider
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const DEFAULT_ADVISORY_PROVIDER_MAX_RPS = 100;
|
|
16
16
|
|
|
17
|
+
/** Cold-start rate for an unknown provider bucket shared across a fleet key. */
|
|
18
|
+
export const DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS = 1;
|
|
19
|
+
|
|
20
|
+
/** AIMD state cooldown cap; provider Retry-After holds remain uncapped. */
|
|
21
|
+
export const PROVIDER_AIMD_COOLDOWN_CAP_MS = 45 * 60 * 1_000;
|
|
22
|
+
|
|
17
23
|
/** A single resolved rate rule for a bucket (from rate-limit-definitions). */
|
|
18
24
|
export interface PacingRule {
|
|
19
25
|
readonly ruleId: string;
|
|
@@ -83,10 +89,18 @@ export interface PacingPermit {
|
|
|
83
89
|
* Absent on backends that keep no local block; callers use `cancel` then.
|
|
84
90
|
*/
|
|
85
91
|
park?(): Promise<void>;
|
|
92
|
+
/** Recheck the durable provider-scope hold immediately before dispatch. */
|
|
93
|
+
validate?(): Promise<boolean>;
|
|
86
94
|
/** Server-authored context for attributing this request's provider 429. */
|
|
87
95
|
readonly concurrencyGrant?: ProviderConcurrencyGrant | null;
|
|
88
96
|
/** Effective shared rate for every app-runtime provider permit. */
|
|
89
97
|
readonly pacingGrant?: ProviderPacingGrant | null;
|
|
98
|
+
/** Signed shared-scope proof for revalidation after the execution-gateway queue. */
|
|
99
|
+
readonly gatewayDispatchProof?: {
|
|
100
|
+
readonly bucketId: string;
|
|
101
|
+
readonly rateScopeToken: string;
|
|
102
|
+
readonly retryAtMsAtGrant: number;
|
|
103
|
+
} | null;
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
/**
|
|
@@ -118,6 +132,8 @@ export interface RateStateBackend {
|
|
|
118
132
|
rateScopeToken?: string | null;
|
|
119
133
|
rules: readonly PacingRule[];
|
|
120
134
|
signal?: AbortSignal;
|
|
135
|
+
/** Managed Plays hand a shared provider hold to durable row suspension. */
|
|
136
|
+
yieldProviderHolds?: boolean;
|
|
121
137
|
}): Promise<PacingPermit>;
|
|
122
138
|
|
|
123
139
|
/**
|
|
@@ -140,6 +156,8 @@ export interface RateStateBackend {
|
|
|
140
156
|
bucketId: string;
|
|
141
157
|
rateScopeToken?: string | null;
|
|
142
158
|
cooldownMs: number;
|
|
159
|
+
/** Absolute upstream retry deadline, stable across transport retries. */
|
|
160
|
+
retryAtMs?: number;
|
|
143
161
|
/** Resolved provider rules for the rejected physical attempt. */
|
|
144
162
|
rules?: readonly PacingRule[];
|
|
145
163
|
/** Provider residence measured at the HTTP boundary. */
|
|
@@ -15,6 +15,9 @@ export function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
|
|
|
15
15
|
suspendedInputRows: frame.suspendedInputRows.map((row) => ({
|
|
16
16
|
inputIndex: row.inputIndex,
|
|
17
17
|
data: { ...row.data },
|
|
18
|
+
...(row.authoredInputDigest
|
|
19
|
+
? { authoredInputDigest: row.authoredInputDigest }
|
|
20
|
+
: {}),
|
|
18
21
|
})),
|
|
19
22
|
}
|
|
20
23
|
: {}),
|
|
@@ -79,6 +79,8 @@ export type PlayRunnerRateStateConcurrencyGrant = {
|
|
|
79
79
|
export type PlayRunnerRateStateAcquireResult = {
|
|
80
80
|
granted: number;
|
|
81
81
|
waitMs: number;
|
|
82
|
+
/** This receiver serves rate_state_validate. Absent on pre-expansion readers. */
|
|
83
|
+
scopeValidationSupported?: boolean;
|
|
82
84
|
/**
|
|
83
85
|
* Gateway clock used to produce `scheduledAtMs`. Runners translate the
|
|
84
86
|
* schedule into relative delays so cross-machine clock skew cannot compress
|
|
@@ -108,8 +110,8 @@ export type PlayRunnerRateStateAcquireResult = {
|
|
|
108
110
|
*/
|
|
109
111
|
coolUntilMs?: number;
|
|
110
112
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
+
* Most recent provider Retry-After deadline, surfaced for diagnostics. The
|
|
114
|
+
* authoritative admission gate is the durable provider-scope hold row.
|
|
113
115
|
*/
|
|
114
116
|
claimedRetryAtMs?: number;
|
|
115
117
|
};
|
|
@@ -128,6 +130,12 @@ export type PlayRunnerRateStatePenalizeInput = {
|
|
|
128
130
|
bucketId: string;
|
|
129
131
|
rateScopeToken?: string | null;
|
|
130
132
|
cooldownMs: number;
|
|
133
|
+
/** Absolute provider retry deadline captured with the observed response. */
|
|
134
|
+
retryAtMs?: number;
|
|
135
|
+
/** Unflushed confirmed successes, sharing acquire's replay-safe identity. */
|
|
136
|
+
observedSuccesses?: number;
|
|
137
|
+
concurrencyObservations?: PlayRunnerRateStateConcurrencyObservation[];
|
|
138
|
+
concurrencyObservationBatch?: PlayRunnerRateStateConcurrencyObservationBatch;
|
|
131
139
|
/**
|
|
132
140
|
* The same resolved rules that admitted the attempt. Optional during the
|
|
133
141
|
* rolling deploy window: old runners retain rate-only backpressure handling.
|
|
@@ -140,6 +148,18 @@ export type PlayRunnerRateStatePenalizeInput = {
|
|
|
140
148
|
schedulerSchema?: string | null;
|
|
141
149
|
};
|
|
142
150
|
|
|
151
|
+
export type PlayRunnerRateStateValidateInput = {
|
|
152
|
+
bucketId: string;
|
|
153
|
+
rateScopeToken?: string | null;
|
|
154
|
+
schedulerSchema?: string | null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type PlayRunnerRateStateValidateResult = {
|
|
158
|
+
allowed: boolean;
|
|
159
|
+
serverNowMs: number;
|
|
160
|
+
retryAtMs: number;
|
|
161
|
+
};
|
|
162
|
+
|
|
143
163
|
export type PlayRunnerBudgetCharge = {
|
|
144
164
|
key: string;
|
|
145
165
|
amount: number;
|
|
@@ -711,6 +711,7 @@ export interface RuntimeResourceGovernor {
|
|
|
711
711
|
acquireProviderPermit(input: {
|
|
712
712
|
toolId: string;
|
|
713
713
|
signal?: AbortSignal;
|
|
714
|
+
yieldProviderHolds?: boolean;
|
|
714
715
|
}): Promise<RuntimeProviderPermit>;
|
|
715
716
|
/**
|
|
716
717
|
* Non-blocking provider admission from capacity this process already holds.
|
|
@@ -748,6 +749,7 @@ export interface RuntimeResourceGovernor {
|
|
|
748
749
|
provider: string;
|
|
749
750
|
toolId?: string;
|
|
750
751
|
retryAfterMs: number;
|
|
752
|
+
retryAtMs?: number;
|
|
751
753
|
/** Provider residence for the rejected physical request, when observed. */
|
|
752
754
|
latencyMs?: number | null;
|
|
753
755
|
/** Server-authored context for attributing this physical provider 429. */
|
|
@@ -1585,7 +1587,10 @@ export function createRuntimeResourceGovernor(input: {
|
|
|
1585
1587
|
const startedAt = Date.now();
|
|
1586
1588
|
const lease = await input.executionGovernor.acquireProviderPermit(
|
|
1587
1589
|
providerInput.toolId,
|
|
1588
|
-
{
|
|
1590
|
+
{
|
|
1591
|
+
signal: providerInput.signal,
|
|
1592
|
+
yieldProviderHolds: providerInput.yieldProviderHolds,
|
|
1593
|
+
},
|
|
1589
1594
|
);
|
|
1590
1595
|
observe({
|
|
1591
1596
|
providerResourceKey: lease.provider,
|
|
@@ -241,15 +241,17 @@ export function providerExhaustedMessage(input: {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
244
|
+
* Control signal from the DB-authoritative pacer when a provider hold exceeds
|
|
245
|
+
* the in-process wait threshold. The next provider call is not dispatched.
|
|
246
|
+
* Managed durable Plays translate this into a persisted run/row wait and resume
|
|
247
|
+
* after the deadline; callers without durable boundaries receive a repairable
|
|
248
|
+
* provider-exhausted outcome. This signal is not a terminal row failure in the
|
|
249
|
+
* managed durable path.
|
|
249
250
|
*/
|
|
250
251
|
export class ProviderExhaustedError extends Error {
|
|
251
252
|
readonly code = PROVIDER_EXHAUSTED_CODE;
|
|
252
253
|
readonly provider: string;
|
|
254
|
+
readonly publicDetails: ToolExecutionError['publicDetails'];
|
|
253
255
|
/** ISO 8601 retry deadline (from the server Retry-After / cooldown). */
|
|
254
256
|
readonly retryAt: string;
|
|
255
257
|
constructor(input: { provider: string; retryAtMs: number; cause?: unknown }) {
|
|
@@ -264,6 +266,10 @@ export class ProviderExhaustedError extends Error {
|
|
|
264
266
|
);
|
|
265
267
|
this.name = 'ProviderExhaustedError';
|
|
266
268
|
this.provider = input.provider;
|
|
269
|
+
this.publicDetails =
|
|
270
|
+
input.cause instanceof ToolExecutionError
|
|
271
|
+
? input.cause.publicDetails
|
|
272
|
+
: null;
|
|
267
273
|
this.retryAt = Number.isFinite(Number(input.retryAtMs))
|
|
268
274
|
? new Date(Number(input.retryAtMs)).toISOString()
|
|
269
275
|
: new Date(0).toISOString();
|
|
@@ -85,6 +85,8 @@ export function createRuntimeFlow<Input>(input: {
|
|
|
85
85
|
export type RuntimeDispatchProviderPermit = RuntimeFlowLease & {
|
|
86
86
|
cancel(): Promise<void>;
|
|
87
87
|
park?(): Promise<void>;
|
|
88
|
+
/** Recheck a shared provider hold at the physical dispatch boundary. */
|
|
89
|
+
validate?(): Promise<boolean>;
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
export type RuntimeDispatchFlowInput = {
|
|
@@ -92,6 +94,11 @@ export type RuntimeDispatchFlowInput = {
|
|
|
92
94
|
signal?: AbortSignal;
|
|
93
95
|
/** Fixtures consume provider capacity but acquire app transport after delay. */
|
|
94
96
|
requiresTransportPermit: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Defer shared-hold validation until the request callback reaches its actual
|
|
99
|
+
* outbound send. The callback must call the supplied boundary exactly once.
|
|
100
|
+
*/
|
|
101
|
+
deferProviderPermitValidation?: boolean;
|
|
95
102
|
/** Emits the request-side transport wait observed by the caller's metrics. */
|
|
96
103
|
onTransportAdmitted?: (waitMs: number) => void;
|
|
97
104
|
};
|
|
@@ -114,12 +121,18 @@ type RuntimeDispatchPermit<
|
|
|
114
121
|
*/
|
|
115
122
|
export type RuntimeDispatchScope = {
|
|
116
123
|
/**
|
|
117
|
-
* Run the one physical request.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
124
|
+
* Run the one physical request. Deferred-validation callers must invoke
|
|
125
|
+
* `beforeProviderDispatch` immediately before the outbound send, after any
|
|
126
|
+
* async setup/queueing. `beforeRelease` executes after the response is known
|
|
127
|
+
* but while the provider lease is still valid, so a caller can durably
|
|
128
|
+
* record response-bound feedback that the lease authorizes.
|
|
120
129
|
*/
|
|
121
130
|
request<Output>(
|
|
122
|
-
work: (
|
|
131
|
+
work: (
|
|
132
|
+
beforeProviderDispatch: (
|
|
133
|
+
onPermitInvalidated?: () => void,
|
|
134
|
+
) => boolean | Promise<boolean>,
|
|
135
|
+
) => Output | Promise<Output>,
|
|
123
136
|
beforeRelease?: () => void | Promise<void>,
|
|
124
137
|
): Promise<Output>;
|
|
125
138
|
};
|
|
@@ -130,10 +143,12 @@ export type RuntimeDispatchFlow<
|
|
|
130
143
|
/**
|
|
131
144
|
* Admit and run exactly one physical provider request.
|
|
132
145
|
*
|
|
133
|
-
* Work
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
146
|
+
* Work must call `beforeProviderDispatch` at the actual outbound-send edge.
|
|
147
|
+
* That is where shared Retry-After state is checked: not before async
|
|
148
|
+
* request preparation. Work that exits before that call returns the provider
|
|
149
|
+
* permit through its cancellation path. Once the send starts, the permit is
|
|
150
|
+
* released after the response body is consumed, even if it cannot be
|
|
151
|
+
* verified.
|
|
137
152
|
*/
|
|
138
153
|
run<Output>(
|
|
139
154
|
input: RuntimeDispatchFlowInput,
|
|
@@ -264,8 +279,10 @@ export function createRuntimeDispatchFlow<
|
|
|
264
279
|
admission: RuntimeDispatchFlowInput,
|
|
265
280
|
work: (scope: RuntimeDispatchScope) => Output | Promise<Output>,
|
|
266
281
|
): Promise<Output> {
|
|
267
|
-
|
|
282
|
+
let permit = await admit(admission);
|
|
268
283
|
let transportLease = permit.integrationRequestLease;
|
|
284
|
+
let requiresTransportPermitForReacquire =
|
|
285
|
+
admission.requiresTransportPermit;
|
|
269
286
|
let requestStarted = false;
|
|
270
287
|
let requestUsed = false;
|
|
271
288
|
let scopeOpen = true;
|
|
@@ -284,7 +301,11 @@ export function createRuntimeDispatchFlow<
|
|
|
284
301
|
try {
|
|
285
302
|
const output = await work({
|
|
286
303
|
request: <RequestOutput>(
|
|
287
|
-
request: (
|
|
304
|
+
request: (
|
|
305
|
+
beforeProviderDispatch: (
|
|
306
|
+
onPermitInvalidated?: () => void,
|
|
307
|
+
) => boolean | Promise<boolean>,
|
|
308
|
+
) => RequestOutput | Promise<RequestOutput>,
|
|
288
309
|
beforeRelease?: () => void | Promise<void>,
|
|
289
310
|
): Promise<RequestOutput> => {
|
|
290
311
|
if (!scopeOpen) {
|
|
@@ -301,10 +322,69 @@ export function createRuntimeDispatchFlow<
|
|
|
301
322
|
}
|
|
302
323
|
requestUsed = true;
|
|
303
324
|
requestPromise = (async () => {
|
|
325
|
+
let dispatchStarted = false;
|
|
326
|
+
const beforeProviderDispatch = (
|
|
327
|
+
onPermitInvalidated?: () => void,
|
|
328
|
+
): boolean | Promise<boolean> => {
|
|
329
|
+
if (dispatchStarted) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
'Runtime dispatch flow permits exactly one physical provider dispatch.',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (!permit.providerPermit.validate) {
|
|
335
|
+
// Preserve the zero-round-trip in-memory path: deferred
|
|
336
|
+
// callers still cross the exact send boundary, but do not
|
|
337
|
+
// pay an artificial microtask when there is nothing shared
|
|
338
|
+
// to revalidate.
|
|
339
|
+
dispatchStarted = true;
|
|
340
|
+
requestStarted = true;
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return validateAndStartProviderDispatch(onPermitInvalidated);
|
|
344
|
+
};
|
|
345
|
+
const validateAndStartProviderDispatch = async (
|
|
346
|
+
onPermitInvalidated?: () => void,
|
|
347
|
+
): Promise<boolean> => {
|
|
348
|
+
let replaced = false;
|
|
349
|
+
while (
|
|
350
|
+
permit.providerPermit.validate &&
|
|
351
|
+
!(await permit.providerPermit.validate())
|
|
352
|
+
) {
|
|
353
|
+
requestStarted = false;
|
|
354
|
+
onPermitInvalidated?.();
|
|
355
|
+
await releasePermit();
|
|
356
|
+
permit = await admit({
|
|
357
|
+
...admission,
|
|
358
|
+
requiresTransportPermit:
|
|
359
|
+
requiresTransportPermitForReacquire,
|
|
360
|
+
});
|
|
361
|
+
transportLease = permit.integrationRequestLease;
|
|
362
|
+
requestStarted = false;
|
|
363
|
+
permitReleased = false;
|
|
364
|
+
replaced = true;
|
|
365
|
+
}
|
|
366
|
+
dispatchStarted = true;
|
|
367
|
+
if (admission.deferProviderPermitValidation) {
|
|
368
|
+
requestStarted = true;
|
|
369
|
+
}
|
|
370
|
+
return replaced;
|
|
371
|
+
};
|
|
304
372
|
try {
|
|
373
|
+
if (!admission.deferProviderPermitValidation) {
|
|
374
|
+
// Preserve the ordinary API: its request callback begins
|
|
375
|
+
// the physical send immediately. Callers with asynchronous
|
|
376
|
+
// preparation opt into deferred validation and invoke this
|
|
377
|
+
// same boundary at the final outbound edge.
|
|
378
|
+
if (permit.providerPermit.validate) {
|
|
379
|
+
await beforeProviderDispatch();
|
|
380
|
+
} else {
|
|
381
|
+
dispatchStarted = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
305
384
|
if (transportLease) {
|
|
306
385
|
admission.onTransportAdmitted?.(0);
|
|
307
386
|
} else {
|
|
387
|
+
requiresTransportPermitForReacquire = true;
|
|
308
388
|
const transportStartedAt = Date.now();
|
|
309
389
|
transportLease = await input.waitForTransportSlot({
|
|
310
390
|
signal: admission.signal,
|
|
@@ -313,8 +393,18 @@ export function createRuntimeDispatchFlow<
|
|
|
313
393
|
Date.now() - transportStartedAt,
|
|
314
394
|
);
|
|
315
395
|
}
|
|
316
|
-
|
|
317
|
-
|
|
396
|
+
if (!admission.deferProviderPermitValidation) {
|
|
397
|
+
// Preserve the cancellation boundary for a permit whose
|
|
398
|
+
// transport wait fails before the request callback begins.
|
|
399
|
+
requestStarted = true;
|
|
400
|
+
}
|
|
401
|
+
const result = await request(beforeProviderDispatch);
|
|
402
|
+
if (!dispatchStarted) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
'Runtime dispatch request completed without crossing its provider-dispatch boundary.',
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
318
408
|
} finally {
|
|
319
409
|
// Feedback is response-bound, but it is not allowed to strand
|
|
320
410
|
// the capacity that made the response possible. Preserve the
|
|
@@ -47,6 +47,7 @@ export const RUNTIME_OPERATION_CAPABILITIES = {
|
|
|
47
47
|
rate_state_acquire: 'rate.acquire',
|
|
48
48
|
rate_state_release: 'rate.acquire',
|
|
49
49
|
rate_state_penalize: 'rate.acquire',
|
|
50
|
+
rate_state_validate: 'rate.acquire',
|
|
50
51
|
get_runtime_traffic_policy: 'rate.acquire',
|
|
51
52
|
governor_budget_charge: 'budget.charge',
|
|
52
53
|
resolve_secret: 'secret.resolve',
|
|
@@ -69,6 +70,13 @@ export const RUNTIME_OPERATION_CONTRACT: DeployedOperationContract =
|
|
|
69
70
|
kind: 'equivalent-fallback',
|
|
70
71
|
action: 'complete_runtime_step_receipts',
|
|
71
72
|
},
|
|
73
|
+
// Permit validation cannot safely fall back to a cached ticket: an old
|
|
74
|
+
// receiver would ignore the durable provider Retry-After hold. New
|
|
75
|
+
// workers therefore fail closed with this typed compatibility result.
|
|
76
|
+
rate_state_validate: {
|
|
77
|
+
kind: 'feature-unavailable',
|
|
78
|
+
code: 'RATE_STATE_VALIDATE_UNAVAILABLE',
|
|
79
|
+
},
|
|
72
80
|
},
|
|
73
81
|
});
|
|
74
82
|
|