deepline 0.3.184 → 0.3.186
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 +4 -0
- 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/launch-identity.ts +229 -0
- package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.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/runtime-telemetry.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -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 +271 -25
- 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 +29 -3
- package/dist/cli/index.mjs +29 -3
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +2 -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
|
@@ -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
|
|
|
@@ -41,6 +41,12 @@ export type PlayRunLaunchConflictTelemetryInput = {
|
|
|
41
41
|
artifactHash?: string | null;
|
|
42
42
|
graphHash?: string | null;
|
|
43
43
|
inputSha256?: string | null;
|
|
44
|
+
/** Named-intent digest of the incoming durable launch envelope. */
|
|
45
|
+
launchIdentityDigest?: string | null;
|
|
46
|
+
/** Admission-side conflict classification when available. */
|
|
47
|
+
conflictReason?: string | null;
|
|
48
|
+
/** Named launch-intent fields that differed, when the persisted envelope is available. */
|
|
49
|
+
conflictingFields?: readonly string[] | null;
|
|
44
50
|
error?: unknown;
|
|
45
51
|
};
|
|
46
52
|
|
|
@@ -113,6 +119,9 @@ export function buildPlayRunLaunchConflictTelemetry(
|
|
|
113
119
|
revisionId: input.revisionId ?? null,
|
|
114
120
|
artifactHash: input.artifactHash ?? null,
|
|
115
121
|
graphHash: input.graphHash ?? null,
|
|
122
|
+
launchIdentityDigest: input.launchIdentityDigest ?? null,
|
|
123
|
+
conflictReason: input.conflictReason ?? null,
|
|
124
|
+
conflictingFields: input.conflictingFields ?? null,
|
|
116
125
|
triage:
|
|
117
126
|
level === 'error'
|
|
118
127
|
? {
|
|
@@ -216,6 +216,13 @@ export type PlaySchedulerSubmitInput = {
|
|
|
216
216
|
runtimeTestFaultHeader?: string | null;
|
|
217
217
|
/** Request-scoped, dev-only runtime policy overrides for black-box durability tests. */
|
|
218
218
|
testPolicyOverrides?: RuntimeTestPolicyOverrides | null;
|
|
219
|
+
/**
|
|
220
|
+
* Digest of the named intent behind this launch. Runtime admission compares
|
|
221
|
+
* this optional field instead of treating transport and execution details as
|
|
222
|
+
* immutable identity. Older persisted rows omit it and keep the legacy
|
|
223
|
+
* comparison until they are gone.
|
|
224
|
+
*/
|
|
225
|
+
launchIdentityDigest?: string | null;
|
|
219
226
|
/** Millisecond epoch timestamp captured immediately before scheduler submit. */
|
|
220
227
|
submittedAtMs?: number;
|
|
221
228
|
/** Scheduler backend that owns orchestration for this run. */
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -1288,10 +1288,14 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1288
1288
|
boundaryId: suspension.boundaryId,
|
|
1289
1289
|
eventKey: suspension.eventKey,
|
|
1290
1290
|
timeoutMs: suspension.timeoutMs,
|
|
1291
|
+
...(suspension.eventExpiresAtMs !== undefined
|
|
1292
|
+
? { eventExpiresAtMs: suspension.eventExpiresAtMs }
|
|
1293
|
+
: {}),
|
|
1294
|
+
...(suspension.postTimeoutPark ? { postTimeoutPark: true } : {}),
|
|
1291
1295
|
...(suspension.messageRef ? { messageRef: suspension.messageRef } : {}),
|
|
1292
1296
|
},
|
|
1293
1297
|
claimSignal: input.claimSignal,
|
|
1294
|
-
});
|
|
1298
|
+
}).then((result) => result.checkpoint);
|
|
1295
1299
|
}
|
|
1296
1300
|
|
|
1297
1301
|
if (suspension.kind === 'integration_event_batch') {
|
|
@@ -1299,13 +1303,25 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1299
1303
|
for (const boundary of [...suspension.boundaries].sort(
|
|
1300
1304
|
(a, b) => a.timeoutMs - b.timeoutMs,
|
|
1301
1305
|
)) {
|
|
1302
|
-
|
|
1306
|
+
// An earlier event may have resolved while the provider timer elapsed.
|
|
1307
|
+
// Do not enter another event wait after that provider row is due.
|
|
1308
|
+
if (
|
|
1309
|
+
suspension.providerWakeAtMs !== undefined &&
|
|
1310
|
+
suspension.providerWakeAtMs <= Date.now() &&
|
|
1311
|
+
boundary.eventExpiresAtMs !== undefined &&
|
|
1312
|
+
boundary.eventExpiresAtMs > Date.now()
|
|
1313
|
+
)
|
|
1314
|
+
return resolvedCheckpoint;
|
|
1315
|
+
const resolved = await resolveIntegrationEvent({
|
|
1303
1316
|
ctx,
|
|
1304
1317
|
runId,
|
|
1305
1318
|
checkpoint: resolvedCheckpoint,
|
|
1306
1319
|
boundary,
|
|
1307
1320
|
claimSignal: input.claimSignal,
|
|
1321
|
+
providerWakeAtMs: suspension.providerWakeAtMs,
|
|
1308
1322
|
});
|
|
1323
|
+
resolvedCheckpoint = resolved.checkpoint;
|
|
1324
|
+
if (resolved.providerWake) return resolvedCheckpoint;
|
|
1309
1325
|
}
|
|
1310
1326
|
return resolvedCheckpoint;
|
|
1311
1327
|
}
|
|
@@ -1382,13 +1398,16 @@ async function resolveIntegrationEvent(input: {
|
|
|
1382
1398
|
boundaryId: string;
|
|
1383
1399
|
eventKey: string;
|
|
1384
1400
|
timeoutMs: number;
|
|
1401
|
+
eventExpiresAtMs?: number;
|
|
1402
|
+
postTimeoutPark?: boolean;
|
|
1385
1403
|
messageRef?: { channel: string; ts: string };
|
|
1386
1404
|
};
|
|
1405
|
+
providerWakeAtMs?: number;
|
|
1387
1406
|
claimSignal: (
|
|
1388
1407
|
eventKey: string,
|
|
1389
1408
|
boundaryId: string | null,
|
|
1390
1409
|
) => Promise<PostgresSchedulerSignalRecord | null>;
|
|
1391
|
-
}): Promise<PlayCheckpoint> {
|
|
1410
|
+
}): Promise<{ checkpoint: PlayCheckpoint; providerWake: boolean }> {
|
|
1392
1411
|
const { ctx, runId, checkpoint, boundary } = input;
|
|
1393
1412
|
// Suspensions written before `messageRef` became part of the suspension
|
|
1394
1413
|
// contract still have the armed Slack message reference in their checkpoint.
|
|
@@ -1403,31 +1422,89 @@ async function resolveIntegrationEvent(input: {
|
|
|
1403
1422
|
(checkpointBoundary?.kind === 'integration_event'
|
|
1404
1423
|
? checkpointBoundary.messageRef
|
|
1405
1424
|
: undefined);
|
|
1425
|
+
const checkpointOutput =
|
|
1426
|
+
checkpointBoundary?.kind === 'integration_event'
|
|
1427
|
+
? checkpointBoundary.output
|
|
1428
|
+
: null;
|
|
1429
|
+
const postTimeoutPark =
|
|
1430
|
+
boundary.postTimeoutPark === true ||
|
|
1431
|
+
(checkpointOutput !== null &&
|
|
1432
|
+
typeof checkpointOutput === 'object' &&
|
|
1433
|
+
!Array.isArray(checkpointOutput) &&
|
|
1434
|
+
(checkpointOutput as Record<string, unknown>).timed_out === true);
|
|
1435
|
+
const originalEventExpiresAtMs =
|
|
1436
|
+
boundary.eventExpiresAtMs ??
|
|
1437
|
+
(checkpointBoundary?.kind === 'integration_event'
|
|
1438
|
+
? checkpointBoundary.eventExpiresAtMs
|
|
1439
|
+
: undefined);
|
|
1440
|
+
const terminalParkAnchorMs =
|
|
1441
|
+
checkpointBoundary?.kind === 'integration_event'
|
|
1442
|
+
? (checkpointBoundary.completedAt ?? Date.now())
|
|
1443
|
+
: Date.now();
|
|
1444
|
+
const eventExpiresAtMs = postTimeoutPark
|
|
1445
|
+
? (boundary.eventExpiresAtMs ??
|
|
1446
|
+
terminalParkAnchorMs + Math.max(0, boundary.timeoutMs))
|
|
1447
|
+
: (originalEventExpiresAtMs ??
|
|
1448
|
+
Date.now() + Math.max(0, boundary.timeoutMs));
|
|
1449
|
+
// Old event checkpoints have only an Absurd step timer, not an absolute
|
|
1450
|
+
// expiry. Reuse that original step instead of granting a new full timeout
|
|
1451
|
+
// whenever a provider row wakes.
|
|
1452
|
+
const providerWakeAtMs =
|
|
1453
|
+
postTimeoutPark || originalEventExpiresAtMs === undefined
|
|
1454
|
+
? undefined
|
|
1455
|
+
: input.providerWakeAtMs;
|
|
1406
1456
|
|
|
1407
1457
|
// Belt-and-suspenders: an early durable signal short-circuits the wait.
|
|
1408
|
-
const early =
|
|
1458
|
+
const early = postTimeoutPark
|
|
1459
|
+
? null
|
|
1460
|
+
: await input.claimSignal(boundary.eventKey, boundary.boundaryId);
|
|
1409
1461
|
if (early) {
|
|
1410
|
-
return
|
|
1411
|
-
checkpoint
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
timeoutMs: boundary.timeoutMs,
|
|
1415
|
-
...(messageRef ? { messageRef } : {}),
|
|
1416
|
-
output: absurdIntegrationEventBoundaryOutput({
|
|
1462
|
+
return {
|
|
1463
|
+
checkpoint: absurdCheckpointWithIntegrationEvent({
|
|
1464
|
+
checkpoint,
|
|
1465
|
+
boundaryId: boundary.boundaryId,
|
|
1417
1466
|
eventKey: boundary.eventKey,
|
|
1418
|
-
|
|
1419
|
-
messageRef,
|
|
1420
|
-
|
|
1467
|
+
timeoutMs: boundary.timeoutMs,
|
|
1468
|
+
...(messageRef ? { messageRef } : {}),
|
|
1469
|
+
output: absurdIntegrationEventBoundaryOutput({
|
|
1470
|
+
eventKey: boundary.eventKey,
|
|
1471
|
+
signalData: signalData(early),
|
|
1472
|
+
messageRef,
|
|
1473
|
+
timedOut: false,
|
|
1474
|
+
}),
|
|
1421
1475
|
}),
|
|
1422
|
-
|
|
1476
|
+
providerWake: false,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (
|
|
1481
|
+
providerWakeAtMs !== undefined &&
|
|
1482
|
+
providerWakeAtMs <= Date.now() &&
|
|
1483
|
+
eventExpiresAtMs > Date.now()
|
|
1484
|
+
) {
|
|
1485
|
+
return { checkpoint: absurdCheckpointBase(checkpoint), providerWake: true };
|
|
1423
1486
|
}
|
|
1424
1487
|
|
|
1425
1488
|
const eventName = absurdIntegrationEventName(runId, boundary.eventKey);
|
|
1489
|
+
const eventWaitMs = Math.max(0, eventExpiresAtMs - Date.now());
|
|
1490
|
+
const providerWaitMs =
|
|
1491
|
+
providerWakeAtMs === undefined
|
|
1492
|
+
? Number.POSITIVE_INFINITY
|
|
1493
|
+
: Math.max(0, providerWakeAtMs - Date.now());
|
|
1494
|
+
const waitMs = Math.min(eventWaitMs, providerWaitMs);
|
|
1426
1495
|
let timedOut = false;
|
|
1427
1496
|
try {
|
|
1428
1497
|
await ctx.awaitEvent(eventName, {
|
|
1429
|
-
|
|
1430
|
-
timeout
|
|
1498
|
+
// Absurd persists waits by step name. A provider timer must never
|
|
1499
|
+
// inherit an earlier event timeout (or vice versa) on replay.
|
|
1500
|
+
stepName: postTimeoutPark
|
|
1501
|
+
? `${boundary.boundaryId}:terminal-park:${terminalParkAnchorMs}`
|
|
1502
|
+
: providerWakeAtMs === undefined
|
|
1503
|
+
? boundary.boundaryId
|
|
1504
|
+
: `${boundary.boundaryId}:provider-wake:${providerWakeAtMs}`,
|
|
1505
|
+
// Absurd floors seconds. Round up so a positive remainder never wakes
|
|
1506
|
+
// before the provider's authoritative not-before time.
|
|
1507
|
+
timeout: Math.ceil(waitMs / 1_000),
|
|
1431
1508
|
});
|
|
1432
1509
|
} catch (error) {
|
|
1433
1510
|
if (error instanceof TimeoutError) {
|
|
@@ -1438,6 +1515,25 @@ async function resolveIntegrationEvent(input: {
|
|
|
1438
1515
|
}
|
|
1439
1516
|
}
|
|
1440
1517
|
|
|
1518
|
+
if (postTimeoutPark) {
|
|
1519
|
+
return {
|
|
1520
|
+
checkpoint: absurdCheckpointWithIntegrationEvent({
|
|
1521
|
+
checkpoint,
|
|
1522
|
+
boundaryId: boundary.boundaryId,
|
|
1523
|
+
eventKey: boundary.eventKey,
|
|
1524
|
+
timeoutMs: boundary.timeoutMs,
|
|
1525
|
+
...(messageRef ? { messageRef } : {}),
|
|
1526
|
+
output:
|
|
1527
|
+
checkpointOutput !== null &&
|
|
1528
|
+
typeof checkpointOutput === 'object' &&
|
|
1529
|
+
!Array.isArray(checkpointOutput)
|
|
1530
|
+
? (checkpointOutput as Record<string, unknown>)
|
|
1531
|
+
: { timed_out: true },
|
|
1532
|
+
}),
|
|
1533
|
+
providerWake: false,
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1441
1537
|
// Claim the authoritative durable signal row for the payload — the absurd
|
|
1442
1538
|
// event carries no payload we trust. On timeout, sweep once more before
|
|
1443
1539
|
// giving up (mirrors today's belt-and-suspenders resume).
|
|
@@ -1445,20 +1541,32 @@ async function resolveIntegrationEvent(input: {
|
|
|
1445
1541
|
boundary.eventKey,
|
|
1446
1542
|
boundary.boundaryId,
|
|
1447
1543
|
);
|
|
1544
|
+
if (
|
|
1545
|
+
!claimed &&
|
|
1546
|
+
timedOut &&
|
|
1547
|
+
providerWakeAtMs !== undefined &&
|
|
1548
|
+
providerWakeAtMs < eventExpiresAtMs &&
|
|
1549
|
+
Date.now() < eventExpiresAtMs
|
|
1550
|
+
) {
|
|
1551
|
+
return { checkpoint: absurdCheckpointBase(checkpoint), providerWake: true };
|
|
1552
|
+
}
|
|
1448
1553
|
const resolvedTimedOut = timedOut && !claimed;
|
|
1449
|
-
return
|
|
1450
|
-
checkpoint
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
timeoutMs: boundary.timeoutMs,
|
|
1454
|
-
...(messageRef ? { messageRef } : {}),
|
|
1455
|
-
output: absurdIntegrationEventBoundaryOutput({
|
|
1554
|
+
return {
|
|
1555
|
+
checkpoint: absurdCheckpointWithIntegrationEvent({
|
|
1556
|
+
checkpoint,
|
|
1557
|
+
boundaryId: boundary.boundaryId,
|
|
1456
1558
|
eventKey: boundary.eventKey,
|
|
1457
|
-
|
|
1458
|
-
messageRef,
|
|
1459
|
-
|
|
1559
|
+
timeoutMs: boundary.timeoutMs,
|
|
1560
|
+
...(messageRef ? { messageRef } : {}),
|
|
1561
|
+
output: absurdIntegrationEventBoundaryOutput({
|
|
1562
|
+
eventKey: boundary.eventKey,
|
|
1563
|
+
signalData: signalData(claimed),
|
|
1564
|
+
messageRef,
|
|
1565
|
+
timedOut: resolvedTimedOut,
|
|
1566
|
+
}),
|
|
1460
1567
|
}),
|
|
1461
|
-
|
|
1568
|
+
providerWake: false,
|
|
1569
|
+
};
|
|
1462
1570
|
}
|
|
1463
1571
|
|
|
1464
1572
|
/**
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One quota scope owns both learning evidence and the dispatch budget. Rule
|
|
3
|
+
* rows retain configured ceilings and concurrency; they must not independently
|
|
4
|
+
* learn from copies of the same provider feedback. These CTEs are shared by
|
|
5
|
+
* pure-rate and concurrency-bearing admission, in the same atomic grant SQL.
|
|
6
|
+
*/
|
|
7
|
+
const AIMD_RECOVERY_STEP_MS = 1_000;
|
|
8
|
+
// Increase relative to the learned rate, not by a fixed RPS amount: a long
|
|
9
|
+
// Retry-After can imply a very small quota, where even +1 RPS would be an
|
|
10
|
+
// enormous and repeated over-probe. A 25% clean-success step still recovers a
|
|
11
|
+
// healthy 1->5 RPS provider in eight observations instead of seventeen.
|
|
12
|
+
const AIMD_ADDITIVE_STEP_FRACTION = 0.25;
|
|
13
|
+
|
|
14
|
+
export function acquireRateScopeSql(
|
|
15
|
+
table: string,
|
|
16
|
+
successesParameter: string,
|
|
17
|
+
): string {
|
|
18
|
+
return `provider_hold_rows AS (
|
|
19
|
+
SELECT * FROM ${table} WHERE bucket_id = $1 FOR UPDATE
|
|
20
|
+
), provider_hold AS (
|
|
21
|
+
SELECT h.*,
|
|
22
|
+
LEAST(
|
|
23
|
+
GREATEST(CASE WHEN h.seen_backpressure THEN 1 ELSE h.capacity END, h.refill_per_sec),
|
|
24
|
+
h.tokens + GREATEST(0, nv.now_ms - h.refilled_at_ms) * h.refill_per_sec / 1000.0
|
|
25
|
+
) AS tokens_after_refill,
|
|
26
|
+
${successesParameter}::int = 0
|
|
27
|
+
AND nv.now_ms - GREATEST(h.refilled_at_ms, h.next_dispatch_at_ms, h.retry_at_ms)
|
|
28
|
+
> GREATEST(1000, 2000.0 / h.refill_per_sec) AS observation_was_idle,
|
|
29
|
+
CASE WHEN ${successesParameter}::int > 0
|
|
30
|
+
AND h.rate_observation_started_at_ms IS NOT NULL
|
|
31
|
+
AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
|
|
32
|
+
THEN LEAST(h.rate_recovery_interval_ms, ${AIMD_RECOVERY_STEP_MS})
|
|
33
|
+
ELSE h.rate_recovery_interval_ms END
|
|
34
|
+
AND h.retry_at_ms <= nv.now_ms
|
|
35
|
+
THEN LEAST(h.max_rps, CASE WHEN h.seen_backpressure
|
|
36
|
+
THEN h.refill_per_sec * (1 + ${AIMD_ADDITIVE_STEP_FRACTION})
|
|
37
|
+
ELSE h.refill_per_sec * 2 END)
|
|
38
|
+
ELSE h.refill_per_sec END AS next_refill_per_sec,
|
|
39
|
+
CASE WHEN ${successesParameter}::int > 0
|
|
40
|
+
AND h.rate_observation_started_at_ms IS NOT NULL
|
|
41
|
+
AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
|
|
42
|
+
THEN LEAST(h.rate_recovery_interval_ms, ${AIMD_RECOVERY_STEP_MS})
|
|
43
|
+
ELSE h.rate_recovery_interval_ms END
|
|
44
|
+
AND h.retry_at_ms <= nv.now_ms
|
|
45
|
+
THEN nv.now_ms ELSE h.last_ramp_at_ms END AS next_ramp_at_ms
|
|
46
|
+
FROM provider_hold_rows h CROSS JOIN now_value nv
|
|
47
|
+
)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function updateRateScopeSql(table: string): string {
|
|
51
|
+
return `updated_scope AS (
|
|
52
|
+
UPDATE ${table} h SET
|
|
53
|
+
tokens = ph.tokens_after_refill - (SELECT granted FROM grant_calc),
|
|
54
|
+
refilled_at_ms = nv.now_ms,
|
|
55
|
+
refill_per_sec = ph.next_refill_per_sec,
|
|
56
|
+
last_ramp_at_ms = ph.next_ramp_at_ms,
|
|
57
|
+
rate_observation_started_at_ms = CASE
|
|
58
|
+
WHEN (SELECT granted FROM grant_calc) > 0 AND ph.observation_was_idle THEN nv.now_ms
|
|
59
|
+
WHEN (SELECT granted FROM grant_calc) > 0 THEN COALESCE(ph.rate_observation_started_at_ms, nv.now_ms)
|
|
60
|
+
ELSE ph.rate_observation_started_at_ms END,
|
|
61
|
+
rate_successes = CASE
|
|
62
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
63
|
+
AND ph.rate_observation_started_at_ms IS NULL THEN 0
|
|
64
|
+
WHEN (SELECT granted FROM grant_calc) > 0 AND ph.observation_was_idle THEN 0
|
|
65
|
+
ELSE ph.rate_successes END,
|
|
66
|
+
rate_throttle_ceiling_rps = CASE
|
|
67
|
+
WHEN (SELECT granted FROM grant_calc) > 0
|
|
68
|
+
AND ph.rate_observation_started_at_ms IS NULL THEN NULL
|
|
69
|
+
ELSE ph.rate_throttle_ceiling_rps END,
|
|
70
|
+
next_dispatch_at_ms = CASE WHEN (SELECT granted FROM grant_calc) > 0 THEN
|
|
71
|
+
(SELECT first_at_ms FROM schedule_calc)
|
|
72
|
+
+ CEIL((SELECT granted FROM grant_calc)::numeric / (SELECT burst_size FROM schedule_calc))::bigint
|
|
73
|
+
* (SELECT spacing_ms FROM schedule_calc) * (SELECT burst_size FROM schedule_calc)
|
|
74
|
+
ELSE ph.next_dispatch_at_ms END
|
|
75
|
+
FROM provider_hold ph CROSS JOIN now_value nv
|
|
76
|
+
WHERE h.bucket_id = $1
|
|
77
|
+
RETURNING h.bucket_id, h.rate_observation_started_at_ms, h.rate_successes,
|
|
78
|
+
h.rate_recovery_interval_ms, h.seen_backpressure
|
|
79
|
+
)`;
|
|
80
|
+
}
|