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
|
@@ -157,6 +157,11 @@ import {
|
|
|
157
157
|
import { LEGACY_MAX_CONCURRENT_EXTERNAL_CALLS } from '../governor/policy';
|
|
158
158
|
import { requireRuntimeAuthorityDescriptor } from '../execution-capabilities';
|
|
159
159
|
import { stringifyPostgresJson } from '../postgres-json';
|
|
160
|
+
import {
|
|
161
|
+
buildPlayLaunchIdentity,
|
|
162
|
+
withPlayLaunchIdentityDigest,
|
|
163
|
+
} from '../launch-identity';
|
|
164
|
+
import { PlayRunLaunchConflictError } from '../play-runs/admission/contracts';
|
|
160
165
|
import { mintPlayRunAttemptRuntimeToken } from '../runtime-authority';
|
|
161
166
|
import { withActiveSpan } from '../../observability/tracing';
|
|
162
167
|
import type {
|
|
@@ -269,7 +274,10 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
|
|
|
269
274
|
// deployment retries do not re-enter the old cross-table DDL transaction.
|
|
270
275
|
// v45 adds the bucket-leading expiration index used by rate-reservation
|
|
271
276
|
// cleanup. Shared schemas build it concurrently through the deploy migrator.
|
|
272
|
-
|
|
277
|
+
// v46 adds a bounded provider-scope Retry-After hold with shared adaptive-rate
|
|
278
|
+
// state. The scope owns learning and dispatch across runs and tool rules;
|
|
279
|
+
// existing deadline rows expand additively and initialize on their first seed.
|
|
280
|
+
export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 46;
|
|
273
281
|
const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
|
|
274
282
|
'scheduler_schema_migrations';
|
|
275
283
|
export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
|
|
@@ -1065,6 +1073,8 @@ export type PostgresSchedulerSchemaShapeRequirements = Readonly<{
|
|
|
1065
1073
|
cronScheduleSchema?: boolean;
|
|
1066
1074
|
/** v43 added per-run fair-share tracking for rate buckets; v42 ignores it. */
|
|
1067
1075
|
rateRequesterFairShare?: boolean;
|
|
1076
|
+
/** v45 added durable Retry-After holds shared across rules and runs. */
|
|
1077
|
+
rateScopeRetryAfter?: boolean;
|
|
1068
1078
|
}>;
|
|
1069
1079
|
|
|
1070
1080
|
export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
@@ -1371,6 +1381,31 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1371
1381
|
)
|
|
1372
1382
|
)
|
|
1373
1383
|
)
|
|
1384
|
+
AND (
|
|
1385
|
+
$36::boolean = false
|
|
1386
|
+
OR (
|
|
1387
|
+
to_regclass($37) IS NOT NULL
|
|
1388
|
+
AND (
|
|
1389
|
+
SELECT count(DISTINCT column_name)
|
|
1390
|
+
FROM information_schema.columns
|
|
1391
|
+
WHERE table_schema = $1
|
|
1392
|
+
AND table_name = 'rate_scope_holds'
|
|
1393
|
+
AND column_name IN (
|
|
1394
|
+
'bucket_id', 'retry_at_ms', 'refill_per_sec', 'capacity', 'max_rps',
|
|
1395
|
+
'tokens', 'refilled_at_ms', 'next_dispatch_at_ms', 'seen_backpressure',
|
|
1396
|
+
'cool_until_ms', 'last_ramp_at_ms', 'rate_observation_started_at_ms',
|
|
1397
|
+
'rate_successes', 'rate_recovery_interval_ms',
|
|
1398
|
+
'rate_throttle_ceiling_rps'
|
|
1399
|
+
)
|
|
1400
|
+
) = 15
|
|
1401
|
+
AND (
|
|
1402
|
+
SELECT count(DISTINCT column_name)
|
|
1403
|
+
FROM information_schema.columns
|
|
1404
|
+
WHERE table_schema = $1 AND table_name = 'rate_bucket'
|
|
1405
|
+
AND column_name IN ('rate_observation_started_at_ms', 'rate_successes', 'rate_recovery_interval_ms')
|
|
1406
|
+
) = 3
|
|
1407
|
+
)
|
|
1408
|
+
)
|
|
1374
1409
|
AND (
|
|
1375
1410
|
$32::boolean = false
|
|
1376
1411
|
OR (
|
|
@@ -1695,6 +1730,8 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
|
|
|
1695
1730
|
`${quotedSchema}.${quoteIdent(POSTGRES_SCHEDULER_CRON_SCHEDULES_TABLE)}`,
|
|
1696
1731
|
requirements.rateRequesterFairShare !== false,
|
|
1697
1732
|
`${quotedSchema}.${quoteIdent('rate_requesters')}`,
|
|
1733
|
+
requirements.rateScopeRetryAfter !== false,
|
|
1734
|
+
`${quotedSchema}.${quoteIdent('rate_scope_holds')}`,
|
|
1698
1735
|
],
|
|
1699
1736
|
);
|
|
1700
1737
|
if (currentShape.rows[0]?.ready !== true) return false;
|
|
@@ -2166,6 +2203,99 @@ async function ensurePostgresSchedulerCronScheduleSchema(
|
|
|
2166
2203
|
`);
|
|
2167
2204
|
}
|
|
2168
2205
|
|
|
2206
|
+
/**
|
|
2207
|
+
* Install the launch-admission functions on every bootstrap path.
|
|
2208
|
+
*
|
|
2209
|
+
* These are function-body compatibility changes, not schema DDL. An already
|
|
2210
|
+
* current scheduler schema takes the fast path below, so keeping these only in
|
|
2211
|
+
* the full bootstrap leaves that estate with the old whole-envelope equality.
|
|
2212
|
+
*/
|
|
2213
|
+
async function ensurePostgresSchedulerLaunchAdmissionFunctions(
|
|
2214
|
+
client: PostgresSchedulerQueryClient,
|
|
2215
|
+
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
2216
|
+
): Promise<void> {
|
|
2217
|
+
const quotedSchema = quoteIdent(schemaName(options));
|
|
2218
|
+
await client.query(`
|
|
2219
|
+
CREATE OR REPLACE FUNCTION ${quotedSchema}.runtime_launch_admission_identity(
|
|
2220
|
+
launch_json jsonb
|
|
2221
|
+
) RETURNS jsonb
|
|
2222
|
+
LANGUAGE sql
|
|
2223
|
+
IMMUTABLE
|
|
2224
|
+
STRICT
|
|
2225
|
+
PARALLEL SAFE
|
|
2226
|
+
AS $$
|
|
2227
|
+
SELECT jsonb_set(
|
|
2228
|
+
jsonb_set(
|
|
2229
|
+
launch_json - ARRAY[
|
|
2230
|
+
'launchIdentityDigest',
|
|
2231
|
+
'executorToken',
|
|
2232
|
+
'inputFileId',
|
|
2233
|
+
'preloadedDbSessionUnwrapKey',
|
|
2234
|
+
'preloadedDbSessions',
|
|
2235
|
+
'submittedAtMs',
|
|
2236
|
+
'runtimeDeployVersion',
|
|
2237
|
+
'absurdReleaseId',
|
|
2238
|
+
'absurdReleaseEnvironment',
|
|
2239
|
+
'coordinatorUrl',
|
|
2240
|
+
'coordinatorInternalToken',
|
|
2241
|
+
'baseUrl',
|
|
2242
|
+
'runtimeApiBaseUrl',
|
|
2243
|
+
'vercelProtectionBypassToken'
|
|
2244
|
+
]::text[],
|
|
2245
|
+
'{maxConcurrentExternalCalls}',
|
|
2246
|
+
coalesce(
|
|
2247
|
+
launch_json->'maxConcurrentExternalCalls',
|
|
2248
|
+
to_jsonb(${LEGACY_MAX_CONCURRENT_EXTERNAL_CALLS}::integer)
|
|
2249
|
+
),
|
|
2250
|
+
true
|
|
2251
|
+
),
|
|
2252
|
+
'{maxConcurrentRows}',
|
|
2253
|
+
coalesce(launch_json->'maxConcurrentRows', 'null'::jsonb),
|
|
2254
|
+
true
|
|
2255
|
+
)
|
|
2256
|
+
$$
|
|
2257
|
+
`);
|
|
2258
|
+
await client.query(`
|
|
2259
|
+
CREATE OR REPLACE FUNCTION ${quotedSchema}.runtime_launch_admission_matches(
|
|
2260
|
+
existing_launch_json jsonb,
|
|
2261
|
+
incoming_launch_json jsonb
|
|
2262
|
+
) RETURNS boolean
|
|
2263
|
+
LANGUAGE sql
|
|
2264
|
+
IMMUTABLE
|
|
2265
|
+
STRICT
|
|
2266
|
+
PARALLEL SAFE
|
|
2267
|
+
AS $$
|
|
2268
|
+
SELECT CASE
|
|
2269
|
+
WHEN nullif(btrim(coalesce(existing_launch_json->>'launchIdentityDigest', '')), '') IS NOT NULL
|
|
2270
|
+
AND nullif(btrim(coalesce(incoming_launch_json->>'launchIdentityDigest', '')), '') IS NOT NULL
|
|
2271
|
+
THEN btrim(existing_launch_json->>'launchIdentityDigest')
|
|
2272
|
+
= btrim(incoming_launch_json->>'launchIdentityDigest')
|
|
2273
|
+
AND (
|
|
2274
|
+
NOT (
|
|
2275
|
+
existing_launch_json ? 'runtimeSandboxPlacementPolicyId'
|
|
2276
|
+
AND incoming_launch_json ? 'runtimeSandboxPlacementPolicyId'
|
|
2277
|
+
)
|
|
2278
|
+
OR existing_launch_json->'runtimeSandboxPlacementPolicyId'
|
|
2279
|
+
IS NOT DISTINCT FROM incoming_launch_json->'runtimeSandboxPlacementPolicyId'
|
|
2280
|
+
)
|
|
2281
|
+
WHEN existing_launch_json ? 'runtimeSandboxPlacementPolicyId'
|
|
2282
|
+
AND incoming_launch_json ? 'runtimeSandboxPlacementPolicyId'
|
|
2283
|
+
THEN ${quotedSchema}.runtime_launch_admission_identity(
|
|
2284
|
+
existing_launch_json - 'launchIdentityDigest'
|
|
2285
|
+
) = ${quotedSchema}.runtime_launch_admission_identity(
|
|
2286
|
+
incoming_launch_json - 'launchIdentityDigest'
|
|
2287
|
+
)
|
|
2288
|
+
ELSE ${quotedSchema}.runtime_launch_admission_identity(
|
|
2289
|
+
existing_launch_json - 'launchIdentityDigest'
|
|
2290
|
+
) - ARRAY['runtimeSandboxPlacementPolicyId', 'launchIdentityDigest']::text[] =
|
|
2291
|
+
${quotedSchema}.runtime_launch_admission_identity(
|
|
2292
|
+
incoming_launch_json - 'launchIdentityDigest'
|
|
2293
|
+
) - ARRAY['runtimeSandboxPlacementPolicyId', 'launchIdentityDigest']::text[]
|
|
2294
|
+
END
|
|
2295
|
+
$$
|
|
2296
|
+
`);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2169
2299
|
export async function ensurePostgresSchedulerSchema(
|
|
2170
2300
|
client: PostgresSchedulerQueryClient,
|
|
2171
2301
|
options?: Pick<PostgresSchedulerOptions, 'schema'> & {
|
|
@@ -2300,6 +2430,7 @@ export async function ensurePostgresSchedulerSchema(
|
|
|
2300
2430
|
const hasLegacyAdmissionRows =
|
|
2301
2431
|
legacyAdmissionRows?.rows[0]?.present === true;
|
|
2302
2432
|
if (schemaCoreIsCurrent) {
|
|
2433
|
+
await ensurePostgresSchedulerLaunchAdmissionFunctions(client, options);
|
|
2303
2434
|
if (!schemaIsCurrent) {
|
|
2304
2435
|
await ensurePostgresSchedulerTerminalComputeBillingOutboxTrigger(
|
|
2305
2436
|
client,
|
|
@@ -3671,6 +3802,7 @@ export async function ensurePostgresSchedulerSchema(
|
|
|
3671
3802
|
[POSTGRES_SCHEDULER_SCHEMA_VERSION],
|
|
3672
3803
|
);
|
|
3673
3804
|
await ensurePostgresSchedulerCronScheduleSchema(client, options);
|
|
3805
|
+
await ensurePostgresSchedulerLaunchAdmissionFunctions(client, options);
|
|
3674
3806
|
if (hadOutbox.rows[0]?.present !== true) {
|
|
3675
3807
|
await client.query(
|
|
3676
3808
|
`
|
|
@@ -4377,6 +4509,45 @@ export async function submitPostgresAbsurdPlayRun(
|
|
|
4377
4509
|
};
|
|
4378
4510
|
}
|
|
4379
4511
|
|
|
4512
|
+
function launchIdentityConflictFields(
|
|
4513
|
+
existingLaunchJson: unknown,
|
|
4514
|
+
incomingLaunch: PlaySchedulerSubmitInput,
|
|
4515
|
+
): string[] {
|
|
4516
|
+
if (
|
|
4517
|
+
!existingLaunchJson ||
|
|
4518
|
+
typeof existingLaunchJson !== 'object' ||
|
|
4519
|
+
Array.isArray(existingLaunchJson)
|
|
4520
|
+
) {
|
|
4521
|
+
return [];
|
|
4522
|
+
}
|
|
4523
|
+
const existingIdentity = buildPlayLaunchIdentity(
|
|
4524
|
+
existingLaunchJson as Record<string, unknown>,
|
|
4525
|
+
);
|
|
4526
|
+
const incomingIdentity = buildPlayLaunchIdentity(incomingLaunch);
|
|
4527
|
+
const fields = Object.keys(incomingIdentity).filter(
|
|
4528
|
+
(field) =>
|
|
4529
|
+
stableStringify(existingIdentity[field]) !==
|
|
4530
|
+
stableStringify(incomingIdentity[field]),
|
|
4531
|
+
);
|
|
4532
|
+
if (
|
|
4533
|
+
Object.prototype.hasOwnProperty.call(
|
|
4534
|
+
existingLaunchJson,
|
|
4535
|
+
'runtimeSandboxPlacementPolicyId',
|
|
4536
|
+
) &&
|
|
4537
|
+
Object.prototype.hasOwnProperty.call(
|
|
4538
|
+
incomingLaunch,
|
|
4539
|
+
'runtimeSandboxPlacementPolicyId',
|
|
4540
|
+
) &&
|
|
4541
|
+
stableStringify(
|
|
4542
|
+
(existingLaunchJson as Record<string, unknown>)
|
|
4543
|
+
.runtimeSandboxPlacementPolicyId,
|
|
4544
|
+
) !== stableStringify(incomingLaunch.runtimeSandboxPlacementPolicyId)
|
|
4545
|
+
) {
|
|
4546
|
+
fields.push('runtimeSandboxPlacementPolicyId');
|
|
4547
|
+
}
|
|
4548
|
+
return fields.sort();
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4380
4551
|
async function submitPostgresSchedulerRunTransaction(
|
|
4381
4552
|
client: PostgresSchedulerQueryClient,
|
|
4382
4553
|
input: PlaySchedulerSubmitInput,
|
|
@@ -4415,7 +4586,10 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
4415
4586
|
existingOnly: requestedExistingOnly,
|
|
4416
4587
|
...durableInput
|
|
4417
4588
|
} = input;
|
|
4418
|
-
|
|
4589
|
+
// Every scheduler writer converges through this seam, including the cron
|
|
4590
|
+
// successor path and Workpool retries. Stamp after ephemeral admission proof
|
|
4591
|
+
// fields are removed so they can never change the durable launch identity.
|
|
4592
|
+
input = withPlayLaunchIdentityDigest(durableInput);
|
|
4419
4593
|
const existingOnlySql = requestedExistingOnly === true ? 'true' : 'false';
|
|
4420
4594
|
const releaseId = input.absurdReleaseId?.trim() || null;
|
|
4421
4595
|
const legacyPreviewRelease =
|
|
@@ -4499,6 +4673,7 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
4499
4673
|
run_created: boolean;
|
|
4500
4674
|
task_created: boolean | null;
|
|
4501
4675
|
task_recovery_required: boolean;
|
|
4676
|
+
existing_launch_json: unknown | null;
|
|
4502
4677
|
}>(
|
|
4503
4678
|
`
|
|
4504
4679
|
WITH run_lock AS MATERIALIZED (
|
|
@@ -4546,6 +4721,7 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
4546
4721
|
coalesce(runs.admission_json, runs.launch_json),
|
|
4547
4722
|
$8::jsonb
|
|
4548
4723
|
) AS matches_launch,
|
|
4724
|
+
coalesce(runs.admission_json, runs.launch_json) AS launch_json,
|
|
4549
4725
|
runs.absurd_task_id,
|
|
4550
4726
|
runs.status
|
|
4551
4727
|
FROM admission_lock
|
|
@@ -4752,6 +4928,7 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
4752
4928
|
AND existing_run.absurd_task_id IS NULL
|
|
4753
4929
|
)
|
|
4754
4930
|
) AS task_recovery_required,
|
|
4931
|
+
(SELECT launch_json FROM existing_run) AS existing_launch_json,
|
|
4755
4932
|
CASE WHEN EXISTS (SELECT 1 FROM run_insert)
|
|
4756
4933
|
OR EXISTS (SELECT 1 FROM run_recovery)
|
|
4757
4934
|
THEN pg_notify($10, $11)
|
|
@@ -4808,9 +4985,13 @@ async function submitPostgresSchedulerRunTransaction(
|
|
|
4808
4985
|
);
|
|
4809
4986
|
}
|
|
4810
4987
|
if (row?.accepted === false && row.existing_run && !row.matching_existing) {
|
|
4811
|
-
throw new
|
|
4812
|
-
|
|
4813
|
-
|
|
4988
|
+
throw new PlayRunLaunchConflictError(input.runId, {
|
|
4989
|
+
conflictReason: 'launch_identity_mismatch',
|
|
4990
|
+
conflictingFields: launchIdentityConflictFields(
|
|
4991
|
+
row.existing_launch_json,
|
|
4992
|
+
input,
|
|
4993
|
+
),
|
|
4994
|
+
});
|
|
4814
4995
|
}
|
|
4815
4996
|
if (row?.accepted === false && !row?.existing_run && requestedExistingOnly) {
|
|
4816
4997
|
throw new Error(
|
|
@@ -5112,6 +5293,14 @@ export class PostgresSchedulerCronScheduleConfigurationError extends Error {
|
|
|
5112
5293
|
}
|
|
5113
5294
|
}
|
|
5114
5295
|
|
|
5296
|
+
/** A deterministic Run identity conflict cannot be repaired by retrying the same fire. */
|
|
5297
|
+
export class PostgresSchedulerCronLaunchConflictError extends PostgresSchedulerCronScheduleConfigurationError {
|
|
5298
|
+
constructor(message: string) {
|
|
5299
|
+
super(message);
|
|
5300
|
+
this.name = 'PostgresSchedulerCronLaunchConflictError';
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5115
5304
|
/**
|
|
5116
5305
|
* The timer stores the facts needed to reconstruct a pinned admission, not a
|
|
5117
5306
|
* previously prepared launch. Executor tokens, preloaded session keys,
|
|
@@ -5456,7 +5645,7 @@ function cronOccurrenceLaunch(input: {
|
|
|
5456
5645
|
`Cron schedule "${input.schedule.scheduleId}" did not receive a fresh authority for its pinned occurrence.`,
|
|
5457
5646
|
);
|
|
5458
5647
|
}
|
|
5459
|
-
return {
|
|
5648
|
+
return withPlayLaunchIdentityDigest({
|
|
5460
5649
|
...input.launch,
|
|
5461
5650
|
schedulerSchema:
|
|
5462
5651
|
input.schedulerSchema ?? input.launch.schedulerSchema ?? null,
|
|
@@ -5469,7 +5658,7 @@ function cronOccurrenceLaunch(input: {
|
|
|
5469
5658
|
),
|
|
5470
5659
|
triggerSource: 'cron',
|
|
5471
5660
|
requiresRunCreatedProjection: true,
|
|
5472
|
-
};
|
|
5661
|
+
});
|
|
5473
5662
|
}
|
|
5474
5663
|
|
|
5475
5664
|
export type FirePostgresSchedulerCronScheduleInput = {
|
|
@@ -5492,14 +5681,26 @@ export type FirePostgresSchedulerCronScheduleInput = {
|
|
|
5492
5681
|
* CTE inserts the immutable Run, its play-run task, run.created fact, next
|
|
5493
5682
|
* cron-fire task, and next cursor. A task replay after commit sees a changed
|
|
5494
5683
|
* cursor and is a harmless `superseded` no-op.
|
|
5684
|
+
*
|
|
5685
|
+
* A re-fire of the same occurrence whose deterministic Run already exists is
|
|
5686
|
+
* an idempotent re-fire, not a no-op. The cursor advances when the existing
|
|
5687
|
+
* launch identity matches, even if this statement did not create the Run.
|
|
5688
|
+
* A genuine identity mismatch is returned as `stalled` so it cannot silently
|
|
5689
|
+
* freeze the schedule behind an indistinguishable replay.
|
|
5495
5690
|
*/
|
|
5496
5691
|
export async function firePostgresSchedulerCronSchedule(
|
|
5497
5692
|
client: PostgresSchedulerQueryClient,
|
|
5498
5693
|
input: FirePostgresSchedulerCronScheduleInput,
|
|
5499
5694
|
options?: Pick<PostgresSchedulerOptions, 'schema'>,
|
|
5500
5695
|
): Promise<
|
|
5501
|
-
| {
|
|
5696
|
+
| {
|
|
5697
|
+
status: 'admitted';
|
|
5698
|
+
runId: string;
|
|
5699
|
+
successorDueAtMs: number;
|
|
5700
|
+
reused: boolean;
|
|
5701
|
+
}
|
|
5502
5702
|
| { status: 'superseded' }
|
|
5703
|
+
| { status: 'stalled'; runId: string; reason: string }
|
|
5503
5704
|
> {
|
|
5504
5705
|
assertPostgresSchedulerCronScheduleInput(input);
|
|
5505
5706
|
const schedule = await readPostgresSchedulerCronSchedule(
|
|
@@ -5566,7 +5767,11 @@ export async function firePostgresSchedulerCronSchedule(
|
|
|
5566
5767
|
orgId: launch.orgId,
|
|
5567
5768
|
});
|
|
5568
5769
|
const result = await client.query<{
|
|
5569
|
-
|
|
5770
|
+
advanced: boolean;
|
|
5771
|
+
run_created: boolean;
|
|
5772
|
+
schedule_current: boolean;
|
|
5773
|
+
existing_run: boolean;
|
|
5774
|
+
matching_existing: boolean;
|
|
5570
5775
|
release_accepting: boolean;
|
|
5571
5776
|
}>(
|
|
5572
5777
|
`
|
|
@@ -5591,16 +5796,28 @@ export async function firePostgresSchedulerCronSchedule(
|
|
|
5591
5796
|
AND release_gate.accepting
|
|
5592
5797
|
FOR UPDATE
|
|
5593
5798
|
), existing_run AS MATERIALIZED (
|
|
5594
|
-
SELECT runs.run_id
|
|
5799
|
+
SELECT runs.run_id,
|
|
5800
|
+
${quoteIdent(schemaName(options))}.runtime_launch_admission_matches(
|
|
5801
|
+
coalesce(runs.admission_json, runs.launch_json),
|
|
5802
|
+
$6::jsonb
|
|
5803
|
+
) AS matches_launch
|
|
5595
5804
|
FROM matching_schedule
|
|
5596
5805
|
JOIN ${tableName('runs', options)} AS runs ON runs.run_id = $5::text
|
|
5806
|
+
), occurrence AS MATERIALIZED (
|
|
5807
|
+
SELECT EXISTS (SELECT 1 FROM matching_schedule) AS schedule_current,
|
|
5808
|
+
EXISTS (SELECT 1 FROM existing_run) AS existing_run,
|
|
5809
|
+
coalesce(
|
|
5810
|
+
(SELECT matches_launch FROM existing_run),
|
|
5811
|
+
false
|
|
5812
|
+
) AS matching_existing
|
|
5597
5813
|
), play_task_request AS (
|
|
5598
5814
|
SELECT $7::text AS queue_name,
|
|
5599
5815
|
$8::text AS task_name,
|
|
5600
5816
|
$9::jsonb AS params,
|
|
5601
5817
|
$10::jsonb AS options
|
|
5602
|
-
|
|
5603
|
-
|
|
5818
|
+
FROM occurrence
|
|
5819
|
+
WHERE occurrence.schedule_current
|
|
5820
|
+
AND NOT occurrence.existing_run
|
|
5604
5821
|
), play_task_spawn AS MATERIALIZED (
|
|
5605
5822
|
SELECT spawned.task_id::text AS task_id
|
|
5606
5823
|
FROM play_task_request
|
|
@@ -5636,15 +5853,25 @@ export async function firePostgresSchedulerCronSchedule(
|
|
|
5636
5853
|
'coordinatorInternalToken',
|
|
5637
5854
|
'vercelProtectionBypassToken'
|
|
5638
5855
|
]::text[]
|
|
5639
|
-
|
|
5640
|
-
|
|
5856
|
+
FROM occurrence
|
|
5857
|
+
WHERE occurrence.schedule_current
|
|
5858
|
+
AND NOT occurrence.existing_run
|
|
5641
5859
|
RETURNING run_id
|
|
5860
|
+
), occurrence_admitted AS MATERIALIZED (
|
|
5861
|
+
SELECT true AS admitted
|
|
5862
|
+
FROM occurrence
|
|
5863
|
+
WHERE EXISTS (SELECT 1 FROM run_insert)
|
|
5864
|
+
OR (
|
|
5865
|
+
occurrence.schedule_current
|
|
5866
|
+
AND occurrence.existing_run
|
|
5867
|
+
AND occurrence.matching_existing
|
|
5868
|
+
)
|
|
5642
5869
|
), successor_task_request AS (
|
|
5643
5870
|
SELECT $11::text AS queue_name,
|
|
5644
5871
|
$12::text AS task_name,
|
|
5645
5872
|
$13::jsonb AS params,
|
|
5646
5873
|
$14::jsonb AS options
|
|
5647
|
-
WHERE EXISTS (SELECT 1 FROM
|
|
5874
|
+
WHERE EXISTS (SELECT 1 FROM occurrence_admitted)
|
|
5648
5875
|
), successor_task_spawn AS MATERIALIZED (
|
|
5649
5876
|
SELECT spawned.task_id::text AS task_id
|
|
5650
5877
|
FROM successor_task_request
|
|
@@ -5718,10 +5945,14 @@ export async function firePostgresSchedulerCronSchedule(
|
|
|
5718
5945
|
AND schedule.state = 'active'
|
|
5719
5946
|
AND schedule.current_due_at = to_timestamp($3::double precision / 1000.0)
|
|
5720
5947
|
AND schedule.current_fire_generation = $15::bigint
|
|
5721
|
-
AND EXISTS (SELECT 1 FROM
|
|
5948
|
+
AND EXISTS (SELECT 1 FROM occurrence_admitted)
|
|
5722
5949
|
RETURNING schedule_id
|
|
5723
5950
|
)
|
|
5724
|
-
SELECT EXISTS (SELECT 1 FROM schedule_advance) AS
|
|
5951
|
+
SELECT EXISTS (SELECT 1 FROM schedule_advance) AS advanced,
|
|
5952
|
+
EXISTS (SELECT 1 FROM run_insert) AS run_created,
|
|
5953
|
+
(SELECT schedule_current FROM occurrence) AS schedule_current,
|
|
5954
|
+
(SELECT existing_run FROM occurrence) AS existing_run,
|
|
5955
|
+
(SELECT matching_existing FROM occurrence) AS matching_existing,
|
|
5725
5956
|
coalesce(
|
|
5726
5957
|
(SELECT accepting
|
|
5727
5958
|
FROM ${tableName('runtime_release_admission', options)}
|
|
@@ -5748,19 +5979,34 @@ export async function firePostgresSchedulerCronSchedule(
|
|
|
5748
5979
|
releaseAdmissionKey,
|
|
5749
5980
|
],
|
|
5750
5981
|
);
|
|
5751
|
-
|
|
5982
|
+
const fired = result.rows[0];
|
|
5983
|
+
if (fired?.release_accepting !== true) {
|
|
5752
5984
|
throw new Error(
|
|
5753
5985
|
`Cron schedule "${schedule.scheduleId}" cannot fire because Absurd release "${releaseAdmissionKey}" is not accepting admissions.`,
|
|
5754
5986
|
);
|
|
5755
5987
|
}
|
|
5756
|
-
if (
|
|
5757
|
-
return {
|
|
5988
|
+
if (fired.advanced === true) {
|
|
5989
|
+
return {
|
|
5990
|
+
status: 'admitted',
|
|
5991
|
+
runId: launch.runId,
|
|
5992
|
+
successorDueAtMs,
|
|
5993
|
+
reused: fired.run_created !== true,
|
|
5994
|
+
};
|
|
5758
5995
|
}
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5996
|
+
if (
|
|
5997
|
+
fired.schedule_current === true &&
|
|
5998
|
+
fired.existing_run === true &&
|
|
5999
|
+
fired.matching_existing !== true
|
|
6000
|
+
) {
|
|
6001
|
+
return {
|
|
6002
|
+
status: 'stalled',
|
|
6003
|
+
runId: launch.runId,
|
|
6004
|
+
reason:
|
|
6005
|
+
`Cron schedule "${schedule.scheduleId}" generation ${schedule.generation} cannot advance past ` +
|
|
6006
|
+
`occurrence ${schedule.dueAtMs}: Run "${launch.runId}" already exists with a different launch identity.`,
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
6009
|
+
return { status: 'superseded' };
|
|
5764
6010
|
}
|
|
5765
6011
|
|
|
5766
6012
|
/** Fence a malformed/invalid timer so it cannot repeatedly consume workers. */
|
|
@@ -14,6 +14,9 @@ export type PlayExecutionSuspension =
|
|
|
14
14
|
boundaryId: string;
|
|
15
15
|
eventKey: string;
|
|
16
16
|
timeoutMs: number;
|
|
17
|
+
eventExpiresAtMs?: number;
|
|
18
|
+
/** A terminal timeout's deliberate long park, not its original event wait. */
|
|
19
|
+
postTimeoutPark?: boolean;
|
|
17
20
|
messageRef?: {
|
|
18
21
|
channel: string;
|
|
19
22
|
ts: string;
|
|
@@ -21,10 +24,13 @@ export type PlayExecutionSuspension =
|
|
|
21
24
|
}
|
|
22
25
|
| {
|
|
23
26
|
kind: 'integration_event_batch';
|
|
27
|
+
/** Earliest pending provider row; absence preserves older event-only waits. */
|
|
28
|
+
providerWakeAtMs?: number;
|
|
24
29
|
boundaries: Array<{
|
|
25
30
|
boundaryId: string;
|
|
26
31
|
eventKey: string;
|
|
27
32
|
timeoutMs: number;
|
|
33
|
+
eventExpiresAtMs?: number;
|
|
28
34
|
messageRef?: {
|
|
29
35
|
channel: string;
|
|
30
36
|
ts: string;
|
|
@@ -123,6 +129,7 @@ export class PlayRowExecutionSuspendedError extends Error {
|
|
|
123
129
|
boundaryId: string;
|
|
124
130
|
eventKey: string;
|
|
125
131
|
timeoutMs: number;
|
|
132
|
+
eventExpiresAtMs?: number;
|
|
126
133
|
messageRef?: {
|
|
127
134
|
channel: string;
|
|
128
135
|
ts: string;
|
|
@@ -133,6 +140,7 @@ export class PlayRowExecutionSuspendedError extends Error {
|
|
|
133
140
|
boundaryId: string;
|
|
134
141
|
eventKey: string;
|
|
135
142
|
timeoutMs: number;
|
|
143
|
+
eventExpiresAtMs?: number;
|
|
136
144
|
messageRef?: {
|
|
137
145
|
channel: string;
|
|
138
146
|
ts: string;
|
|
@@ -146,6 +154,19 @@ export class PlayRowExecutionSuspendedError extends Error {
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
/** A provider wait is a control handoff, never a failed cell or receipt. */
|
|
158
|
+
export class PlayRowProviderBackpressureError extends Error {
|
|
159
|
+
constructor(
|
|
160
|
+
readonly provider: string,
|
|
161
|
+
readonly retryAtMs: number,
|
|
162
|
+
) {
|
|
163
|
+
super(
|
|
164
|
+
`Play row is waiting for ${provider} provider backpressure to clear.`,
|
|
165
|
+
);
|
|
166
|
+
this.name = 'PlayRowProviderBackpressureError';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
export function isPlayRowExecutionSuspendedError(
|
|
150
171
|
value: unknown,
|
|
151
172
|
): value is PlayRowExecutionSuspendedError {
|