deepline 0.3.245 → 0.3.247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/client.ts +38 -49
- package/dist/bundling-sources/sdk/src/http.ts +10 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/read-errors.ts +19 -0
- package/dist/bundling-sources/sdk/src/runs/watch-polling.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +8 -5
- package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +118 -77
- package/dist/bundling-sources/shared_libs/play-runtime/transient-service-error.ts +3 -2
- package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/retained-pipeline-graph.ts +12 -6
- package/dist/bundling-sources/shared_libs/plays/static-pipeline-storage-codec.ts +2 -7
- package/dist/cli/index.js +36 -66
- package/dist/cli/index.mjs +36 -66
- package/dist/index.js +25 -32
- package/dist/index.mjs +25 -32
- package/dist/install-integrity.json +1 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/bundling-sources/sdk/src/stream-reconnect.ts +0 -47
|
@@ -113,7 +113,10 @@ import type { MonitorFleetDefinition } from './monitor-fleets.js';
|
|
|
113
113
|
import type { PlayStagedFileRef } from './plays/local-file-discovery.js';
|
|
114
114
|
import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest.js';
|
|
115
115
|
import type { EnrichCompiledConfig } from './cli/enrich-play-compiler.js';
|
|
116
|
-
import {
|
|
116
|
+
import {
|
|
117
|
+
RUNTIME_ENVIRONMENT_TOKEN_HEADER,
|
|
118
|
+
RETAINED_PIPELINE_RECEIVERS_HEADER,
|
|
119
|
+
} from '../../shared_libs/play-runtime/coordinator-headers.js';
|
|
117
120
|
import {
|
|
118
121
|
normalizePlayRuntimeEnvironment,
|
|
119
122
|
normalizePlayRuntimeNamespace,
|
|
@@ -123,6 +126,7 @@ import {
|
|
|
123
126
|
} from '../../shared_libs/play-runtime/runtime-environment.js';
|
|
124
127
|
import {
|
|
125
128
|
decodePlayRunPublicStatus,
|
|
129
|
+
isTerminalPlayRunLifecycleStatus,
|
|
126
130
|
type PlayRunPublicStatus,
|
|
127
131
|
} from '../../shared_libs/play-runtime/run-lifecycle-policy.js';
|
|
128
132
|
export type SlackNotificationTarget =
|
|
@@ -209,7 +213,6 @@ function notificationObjectKey(kind: 'group' | 'destination') {
|
|
|
209
213
|
return `${kind}.${globalThis.crypto.randomUUID()}`;
|
|
210
214
|
}
|
|
211
215
|
|
|
212
|
-
const TERMINAL_PLAY_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
213
216
|
const MONITOR_NON_RETRYABLE_MUTATION_OPTIONS = {
|
|
214
217
|
maxRetries: 0,
|
|
215
218
|
exactUrlOnly: true,
|
|
@@ -403,7 +406,12 @@ function runtimeSelectionHeaders(
|
|
|
403
406
|
'RUNTIME_ENVIRONMENT_TOKEN_REQUIRED',
|
|
404
407
|
);
|
|
405
408
|
}
|
|
406
|
-
|
|
409
|
+
const receivers =
|
|
410
|
+
process.env.DEEPLINE_RUNTIME_RETAINED_PIPELINE_RECEIVERS?.trim();
|
|
411
|
+
return {
|
|
412
|
+
[RUNTIME_ENVIRONMENT_TOKEN_HEADER]: token,
|
|
413
|
+
...(receivers ? { [RETAINED_PIPELINE_RECEIVERS_HEADER]: receivers } : {}),
|
|
414
|
+
};
|
|
407
415
|
}
|
|
408
416
|
|
|
409
417
|
function normalizeTestPolicyOverrides(
|
|
@@ -2306,7 +2314,9 @@ function normalizePlayRunStart(raw: Record<string, unknown>): PlayRunStart {
|
|
|
2306
2314
|
...(runPackage.run.dashboardUrl
|
|
2307
2315
|
? { dashboardUrl: runPackage.run.dashboardUrl }
|
|
2308
2316
|
: {}),
|
|
2309
|
-
...(
|
|
2317
|
+
...(isTerminalPlayRunLifecycleStatus(status)
|
|
2318
|
+
? { finalStatus: runPackage }
|
|
2319
|
+
: {}),
|
|
2310
2320
|
package: runPackage,
|
|
2311
2321
|
};
|
|
2312
2322
|
}
|
|
@@ -2469,18 +2479,22 @@ function getPlayLiveEventPayload(
|
|
|
2469
2479
|
: {};
|
|
2470
2480
|
}
|
|
2471
2481
|
|
|
2472
|
-
|
|
2482
|
+
/** Run lifecycle events are the only events that can finish a watch. */
|
|
2483
|
+
function playLiveRunStatus(event: PlayLiveEvent): PlayStatus['status'] | null {
|
|
2473
2484
|
if (
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
value === 'completed' ||
|
|
2478
|
-
value === 'failed' ||
|
|
2479
|
-
value === 'cancelled'
|
|
2485
|
+
event.type !== 'play.run.snapshot' &&
|
|
2486
|
+
event.type !== 'play.run.status' &&
|
|
2487
|
+
event.type !== 'play.run.final_status'
|
|
2480
2488
|
) {
|
|
2481
|
-
return
|
|
2489
|
+
return null;
|
|
2482
2490
|
}
|
|
2483
|
-
|
|
2491
|
+
const payload = getPlayLiveEventPayload(event);
|
|
2492
|
+
return (
|
|
2493
|
+
decodePlayRunPublicStatus(payload.status) ??
|
|
2494
|
+
(isPlayRunPackage(payload)
|
|
2495
|
+
? decodePlayRunPublicStatus(payload.run.status)
|
|
2496
|
+
: null)
|
|
2497
|
+
);
|
|
2484
2498
|
}
|
|
2485
2499
|
|
|
2486
2500
|
function appendPlayLiveLogLines(
|
|
@@ -2545,12 +2559,7 @@ function updatePlayLiveStatusState(
|
|
|
2545
2559
|
: isPlayRunPackage(payload)
|
|
2546
2560
|
? payload.run.id
|
|
2547
2561
|
: state.runId;
|
|
2548
|
-
const status =
|
|
2549
|
-
normalizeLiveStatus(payload.status) ??
|
|
2550
|
-
(isPlayRunPackage(payload)
|
|
2551
|
-
? normalizeLiveStatus(payload.run.status)
|
|
2552
|
-
: null) ??
|
|
2553
|
-
state.status;
|
|
2562
|
+
const status = playLiveRunStatus(event) ?? state.status;
|
|
2554
2563
|
const progressPayload = isRecord(payload.progress) ? payload.progress : {};
|
|
2555
2564
|
// Snapshots no longer REPLACE accumulated logs (ADR-0009): the snapshot
|
|
2556
2565
|
// only retains a bounded tail, so replacing would clobber the seq-keyed
|
|
@@ -4221,13 +4230,8 @@ export class DeeplineClient {
|
|
|
4221
4230
|
runId: string,
|
|
4222
4231
|
options?: RunsWatchOptions,
|
|
4223
4232
|
): Promise<PlayStatus> {
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
status: 'running',
|
|
4227
|
-
logs: [],
|
|
4228
|
-
lastLogSeq: 0,
|
|
4229
|
-
latest: null,
|
|
4230
|
-
};
|
|
4233
|
+
// Forward events without retaining their log history. Only lifecycle
|
|
4234
|
+
// evidence is needed here; the final package comes from the durable read.
|
|
4231
4235
|
let terminalObserved = false;
|
|
4232
4236
|
for await (const event of this.watchRunEvents(runId, {
|
|
4233
4237
|
signal: options?.signal,
|
|
@@ -4235,19 +4239,20 @@ export class DeeplineClient {
|
|
|
4235
4239
|
onRetry: options?.onRetry ?? options?.onReconnect,
|
|
4236
4240
|
})) {
|
|
4237
4241
|
options?.onEvent?.(event);
|
|
4238
|
-
|
|
4239
|
-
if (status && TERMINAL_PLAY_STATUSES.has(status.status)) {
|
|
4242
|
+
if (isTerminalPlayRunLifecycleStatus(playLiveRunStatus(event))) {
|
|
4240
4243
|
terminalObserved = true;
|
|
4241
4244
|
}
|
|
4242
4245
|
}
|
|
4243
4246
|
if (options?.signal?.aborted) {
|
|
4244
4247
|
throw new DeeplineError('Run watch aborted.', undefined, 'ABORTED');
|
|
4245
4248
|
}
|
|
4246
|
-
if (terminalObserved) {
|
|
4247
|
-
return await this.getRunStatus(runId);
|
|
4248
|
-
}
|
|
4249
4249
|
const refreshed = await this.getRunStatus(runId);
|
|
4250
|
-
if (
|
|
4250
|
+
if (
|
|
4251
|
+
terminalObserved ||
|
|
4252
|
+
isTerminalPlayRunLifecycleStatus(refreshed.status)
|
|
4253
|
+
) {
|
|
4254
|
+
return refreshed;
|
|
4255
|
+
}
|
|
4251
4256
|
throw new DeeplineError(
|
|
4252
4257
|
`Run watch for ${runId} ended before a terminal status.`,
|
|
4253
4258
|
undefined,
|
|
@@ -4474,23 +4479,7 @@ export class DeeplineClient {
|
|
|
4474
4479
|
`/api/v2/runs/${encodeURIComponent(runId)}${query}`,
|
|
4475
4480
|
{ headers: { 'x-deepline-run-status': '1' } },
|
|
4476
4481
|
);
|
|
4477
|
-
const responseRunStatus = isPlayRunPackage(response)
|
|
4478
|
-
? response.runStatus
|
|
4479
|
-
: isPlayRunPackage(response.package)
|
|
4480
|
-
? response.package.runStatus
|
|
4481
|
-
: response.runStatus;
|
|
4482
|
-
const responseOverview = isPlayRunPackage(response)
|
|
4483
|
-
? response.overview
|
|
4484
|
-
: isPlayRunPackage(response.package)
|
|
4485
|
-
? response.package.overview
|
|
4486
|
-
: response.overview;
|
|
4487
4482
|
const status = normalizePlayStatus(response);
|
|
4488
|
-
if (isRecord(responseRunStatus)) {
|
|
4489
|
-
status.runStatus = responseRunStatus as unknown as RunGetStatus;
|
|
4490
|
-
}
|
|
4491
|
-
if (isRecord(responseOverview)) {
|
|
4492
|
-
status.overview = responseOverview as unknown as RunLifecycleStatus;
|
|
4493
|
-
}
|
|
4494
4483
|
if (options?.failedLogs !== true || status.status !== 'failed') {
|
|
4495
4484
|
return status;
|
|
4496
4485
|
}
|
|
@@ -5796,7 +5785,7 @@ export class DeeplineClient {
|
|
|
5796
5785
|
options?.onProgress?.(status);
|
|
5797
5786
|
}
|
|
5798
5787
|
|
|
5799
|
-
if (
|
|
5788
|
+
if (isTerminalPlayRunLifecycleStatus(status.status)) {
|
|
5800
5789
|
terminalObserved = status;
|
|
5801
5790
|
}
|
|
5802
5791
|
}
|
|
@@ -38,6 +38,7 @@ import { readSdkSkillsLocalVersion } from './skills-version.js';
|
|
|
38
38
|
import { deploymentProtectionHeaders } from './deployment-protection.js';
|
|
39
39
|
import {
|
|
40
40
|
ABSURD_RELEASE_OVERRIDE_HEADER,
|
|
41
|
+
RETAINED_PIPELINE_RECEIVERS_HEADER,
|
|
41
42
|
COORDINATOR_INTERNAL_TOKEN_HEADER,
|
|
42
43
|
COORDINATOR_URL_OVERRIDE_HEADER,
|
|
43
44
|
PREFERRED_RECEIPT_GATEWAY_MACHINE_ID_HEADER,
|
|
@@ -345,6 +346,15 @@ export class HttpClient {
|
|
|
345
346
|
if (runtimeTestFault?.trim()) {
|
|
346
347
|
headers[PLAY_RUNTIME_TEST_FAULT_HEADER] = runtimeTestFault.trim();
|
|
347
348
|
}
|
|
349
|
+
const retainedReceivers =
|
|
350
|
+
typeof process !== 'undefined'
|
|
351
|
+
? process.env?.DEEPLINE_RUNTIME_RETAINED_PIPELINE_RECEIVERS?.trim()
|
|
352
|
+
: undefined;
|
|
353
|
+
if (retainedReceivers && coordinatorInternalToken?.trim()) {
|
|
354
|
+
headers[RETAINED_PIPELINE_RECEIVERS_HEADER] = retainedReceivers;
|
|
355
|
+
headers[COORDINATOR_INTERNAL_TOKEN_HEADER] =
|
|
356
|
+
coordinatorInternalToken.trim();
|
|
357
|
+
}
|
|
348
358
|
if (absurdReleaseOverride?.trim() && coordinatorInternalToken?.trim()) {
|
|
349
359
|
headers[ABSURD_RELEASE_OVERRIDE_HEADER] = absurdReleaseOverride.trim();
|
|
350
360
|
}
|
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.247',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DeeplineError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
export function isTransientPlayRunReadError(error: unknown): boolean {
|
|
4
|
+
if (
|
|
5
|
+
error instanceof DeeplineError &&
|
|
6
|
+
error.code === 'PLAY_STREAM_NETWORK_ABORTED'
|
|
7
|
+
) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (error instanceof DeeplineError && typeof error.statusCode === 'number') {
|
|
11
|
+
// 5xx responses and network-level failures may recover. Client errors,
|
|
12
|
+
// especially 404, stay visible instead of disappearing into a retry loop.
|
|
13
|
+
return error.statusCode >= 500 && error.statusCode < 600;
|
|
14
|
+
}
|
|
15
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
16
|
+
return /auth validation backend timed out|coordinator \/submit(?:\?[^ ]*)? 5\d\d|Worker threw exception|Internal Server Error|Service Unavailable|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
|
|
17
|
+
text,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type PlayRunStreamDiffState,
|
|
8
8
|
type PlayRunStreamEvent,
|
|
9
9
|
} from '../../../shared_libs/play-runtime/run-snapshot-stream.js';
|
|
10
|
-
import { isTransientPlayRunReadError } from '
|
|
10
|
+
import { isTransientPlayRunReadError } from './read-errors.js';
|
|
11
11
|
|
|
12
12
|
/** The watch endpoint is a small authenticated REST read, polled every two seconds. */
|
|
13
13
|
export const RUN_WATCH_POLL_INTERVAL_MS = 2_000;
|
|
@@ -849,6 +849,24 @@
|
|
|
849
849
|
"failureAlert": "convex_action_wrapper",
|
|
850
850
|
"domainReportRequired": true
|
|
851
851
|
},
|
|
852
|
+
{
|
|
853
|
+
"id": "convex.cleanup-expired-org-day-usage-shards",
|
|
854
|
+
"name": "Cleanup expired org-day usage shards",
|
|
855
|
+
"scheduler": "convex_cron",
|
|
856
|
+
"cadence": "47 5 * * *",
|
|
857
|
+
"timezone": "UTC",
|
|
858
|
+
"source": "packages/convex/crons.ts",
|
|
859
|
+
"locator": "cleanup expired org-day usage shards",
|
|
860
|
+
"owner": "billing",
|
|
861
|
+
"criticality": "maintenance",
|
|
862
|
+
"purpose": "Delete expired org-day usage shards independently of display rollup folding.",
|
|
863
|
+
"successDefinition": "Each expired shard page is deleted and full pages schedule a continuation.",
|
|
864
|
+
"failureDefinition": "Expired shards cannot be deleted or a required continuation is not scheduled.",
|
|
865
|
+
"startGraceMs": 120000,
|
|
866
|
+
"runTimeoutMs": 1800000,
|
|
867
|
+
"failureAlert": "convex_action_wrapper",
|
|
868
|
+
"domainReportRequired": true
|
|
869
|
+
},
|
|
852
870
|
{
|
|
853
871
|
"id": "convex.record-neon-managed-project-usage",
|
|
854
872
|
"name": "Record Neon managed project usage",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
|
|
11
11
|
'convex.fold-usage-rollups',
|
|
12
|
+
'convex.cleanup-expired-org-day-usage-shards',
|
|
12
13
|
'convex.record-neon-managed-project-usage',
|
|
13
14
|
'convex.cleanup-expired-mcp-oauth-codes',
|
|
14
15
|
'convex.cleanup-expired-api-key-claims',
|
|
@@ -19063,11 +19063,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19063
19063
|
ambiguousTransportReplayAllowed ||
|
|
19064
19064
|
fixtureReplaySafe)
|
|
19065
19065
|
) {
|
|
19066
|
-
//
|
|
19067
|
-
//
|
|
19068
|
-
//
|
|
19069
|
-
|
|
19070
|
-
|
|
19066
|
+
// Redeliver a fenced request to the same saved outcome. Its
|
|
19067
|
+
// attempt and any prior provider-429 proof belong to the logical
|
|
19068
|
+
// invocation; only a definite retryable HTTP outcome advances it.
|
|
19069
|
+
// Ordinary transport retries retain their physical attempt count.
|
|
19070
|
+
if (!usesDurableInvocationFence) {
|
|
19071
|
+
providerRateLimitRetryFromInvocationAttempt = null;
|
|
19072
|
+
invocationAttempt += 1;
|
|
19073
|
+
}
|
|
19071
19074
|
await this.governor.chargeBudget('retry');
|
|
19072
19075
|
const retryAfterMs =
|
|
19073
19076
|
TOOL_RETRY_AFTER_FALLBACK_MS * transportAttempt;
|
|
@@ -22,6 +22,16 @@ export const RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER =
|
|
|
22
22
|
*/
|
|
23
23
|
export const RUNTIME_ENVIRONMENT_TOKEN_HEADER =
|
|
24
24
|
'x-deepline-runtime-environment-token';
|
|
25
|
+
/** Authenticated target-owner assertion, issued only after deployment reproof. */
|
|
26
|
+
export const RETAINED_PIPELINE_RECEIVERS_HEADER =
|
|
27
|
+
'x-deepline-retained-pipeline-receivers';
|
|
28
|
+
export type RetainedPipelineReceivers = {
|
|
29
|
+
namespace: string;
|
|
30
|
+
schedulerSchema?: string;
|
|
31
|
+
backend: string;
|
|
32
|
+
callbackOrigin: string;
|
|
33
|
+
runtimeApiOrigin: string;
|
|
34
|
+
};
|
|
25
35
|
/**
|
|
26
36
|
* Internal top-level `profile=absurd` launches, including deploy canaries, use
|
|
27
37
|
* this header to pin an exact candidate release before activation. Public API
|
|
@@ -41,7 +41,7 @@ const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
|
41
41
|
* therefore a network/projection concurrency budget, deliberately independent
|
|
42
42
|
* of the projector's much smaller Postgres pool.
|
|
43
43
|
*
|
|
44
|
-
* A single successful probe
|
|
44
|
+
* A single successful probe opens bounded delivery slots; an idle projector still
|
|
45
45
|
* performs only one indexed head read per safety wake. `SKIP LOCKED` leases
|
|
46
46
|
* distinct runs, retaining strict causal ordering within every run.
|
|
47
47
|
*/
|
|
@@ -130,6 +130,7 @@ export function startRunLedgerOutboxRecovery(
|
|
|
130
130
|
let unsubscribeNotify: (() => void) | undefined;
|
|
131
131
|
let unsubscribeState: (() => void) | undefined;
|
|
132
132
|
let requestDrain: (reason: string) => void = () => undefined;
|
|
133
|
+
let wakeDrainer: (() => void) | null = null;
|
|
133
134
|
|
|
134
135
|
const clearWakeTimer = () => {
|
|
135
136
|
if (!timer) return;
|
|
@@ -162,7 +163,10 @@ export function startRunLedgerOutboxRecovery(
|
|
|
162
163
|
requested = true;
|
|
163
164
|
if (cooldownUntilMs !== null && now() < cooldownUntilMs) return;
|
|
164
165
|
clearWakeTimer();
|
|
165
|
-
if (drainPromise)
|
|
166
|
+
if (drainPromise) {
|
|
167
|
+
wakeDrainer?.();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
166
170
|
drainPromise = drain(reason)
|
|
167
171
|
.catch((error) => {
|
|
168
172
|
// drain() handles expected database/projection faults itself. This is a
|
|
@@ -179,7 +183,6 @@ export function startRunLedgerOutboxRecovery(
|
|
|
179
183
|
};
|
|
180
184
|
|
|
181
185
|
const recordProjectionRetry = (retrying: number) => {
|
|
182
|
-
failureCount += 1;
|
|
183
186
|
logger.warn('[run-ledger-outbox.recovery_retry]', {
|
|
184
187
|
stage: 'projection',
|
|
185
188
|
retrying,
|
|
@@ -205,6 +208,7 @@ export function startRunLedgerOutboxRecovery(
|
|
|
205
208
|
delayMs,
|
|
206
209
|
error: error instanceof Error ? error.message : String(error),
|
|
207
210
|
});
|
|
211
|
+
if (closed) return;
|
|
208
212
|
clearWakeTimer();
|
|
209
213
|
cooldownUntilMs = now() + delayMs;
|
|
210
214
|
timer = setTimer(() => {
|
|
@@ -216,6 +220,7 @@ export function startRunLedgerOutboxRecovery(
|
|
|
216
220
|
};
|
|
217
221
|
|
|
218
222
|
const scheduleNextDue = async (): Promise<void> => {
|
|
223
|
+
if (closed) return;
|
|
219
224
|
let dueAt: Date | null;
|
|
220
225
|
try {
|
|
221
226
|
dueAt = await deps.readNextDueAt();
|
|
@@ -246,7 +251,47 @@ export function startRunLedgerOutboxRecovery(
|
|
|
246
251
|
timer.unref?.();
|
|
247
252
|
};
|
|
248
253
|
|
|
254
|
+
type DeliveryOutcome = {
|
|
255
|
+
delivery: Promise<DeliveryOutcome>;
|
|
256
|
+
} & (
|
|
257
|
+
| { status: 'fulfilled'; value: PostgresSchedulerOutboxProjectionResult }
|
|
258
|
+
| { status: 'rejected' | 'skipped' }
|
|
259
|
+
);
|
|
260
|
+
|
|
249
261
|
const drain = async (reason: string): Promise<void> => {
|
|
262
|
+
const volume = { attempted: 0, projected: 0, retrying: 0, blocked: 0 };
|
|
263
|
+
const reportVolume = () => {
|
|
264
|
+
if (volume.attempted === 0) return;
|
|
265
|
+
logger.info('[run-ledger-outbox.recovery_wave]', {
|
|
266
|
+
reason,
|
|
267
|
+
parallelism,
|
|
268
|
+
...volume,
|
|
269
|
+
});
|
|
270
|
+
volume.attempted = 0;
|
|
271
|
+
volume.projected = 0;
|
|
272
|
+
volume.retrying = 0;
|
|
273
|
+
volume.blocked = 0;
|
|
274
|
+
};
|
|
275
|
+
const recordResult = (
|
|
276
|
+
result: PostgresSchedulerOutboxProjectionResult,
|
|
277
|
+
resetFailures = true,
|
|
278
|
+
) => {
|
|
279
|
+
volume.attempted += result.attempted;
|
|
280
|
+
volume.projected += result.projected;
|
|
281
|
+
volume.retrying += result.retrying;
|
|
282
|
+
volume.blocked += result.failed;
|
|
283
|
+
if (result.retrying > 0) {
|
|
284
|
+
// Each retrying run owns its durable retry clock. Independent runs
|
|
285
|
+
// remain eligible; this is not a controller-wide delivery cooldown.
|
|
286
|
+
recordProjectionRetry(result.retrying);
|
|
287
|
+
} else if (resetFailures) {
|
|
288
|
+
failureCount = 0;
|
|
289
|
+
}
|
|
290
|
+
// Streaming delivery has no wave barrier at which to report progress.
|
|
291
|
+
// Keep logs bounded per committed volume, and flush at idle/shutdown.
|
|
292
|
+
if (volume.attempted >= parallelism) reportVolume();
|
|
293
|
+
};
|
|
294
|
+
|
|
250
295
|
while (requested && !closed) {
|
|
251
296
|
requested = false;
|
|
252
297
|
// Probe first so an idle projector issues one small indexed read per
|
|
@@ -260,86 +305,81 @@ export function startRunLedgerOutboxRecovery(
|
|
|
260
305
|
scheduleFailureRetry('dispatch', error);
|
|
261
306
|
return;
|
|
262
307
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
},
|
|
268
|
-
...(first.attempted === 0
|
|
269
|
-
? []
|
|
270
|
-
: await Promise.allSettled(
|
|
271
|
-
Array.from({ length: parallelism - 1 }, () =>
|
|
272
|
-
deps.dispatchOnce(),
|
|
273
|
-
),
|
|
274
|
-
)),
|
|
275
|
-
];
|
|
276
|
-
const rejected = settled.find(
|
|
277
|
-
(result): result is PromiseRejectedResult =>
|
|
278
|
-
result.status === 'rejected',
|
|
279
|
-
);
|
|
280
|
-
if (rejected) {
|
|
281
|
-
scheduleFailureRetry('dispatch', rejected.reason);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const results = settled.map(
|
|
286
|
-
(result) =>
|
|
287
|
-
(
|
|
288
|
-
result as PromiseFulfilledResult<PostgresSchedulerOutboxProjectionResult>
|
|
289
|
-
).value,
|
|
290
|
-
);
|
|
291
|
-
const retrying = results.reduce(
|
|
292
|
-
(total, result) => total + result.retrying,
|
|
293
|
-
0,
|
|
294
|
-
);
|
|
295
|
-
const attempted = results.reduce(
|
|
296
|
-
(total, result) => total + result.attempted,
|
|
297
|
-
0,
|
|
298
|
-
);
|
|
299
|
-
const projected = results.reduce(
|
|
300
|
-
(total, result) => total + result.projected,
|
|
301
|
-
0,
|
|
302
|
-
);
|
|
303
|
-
const blocked = results.reduce(
|
|
304
|
-
(total, result) => total + result.failed,
|
|
305
|
-
0,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
if (retrying > 0) {
|
|
309
|
-
// The failed row owns its own durable retry clock. Do NOT stop this
|
|
310
|
-
// drain: the claim query preserves causal order within a run, while
|
|
311
|
-
// independent runs are safe to project now. Returning here turns one
|
|
312
|
-
// transient projection failure into global head-of-line blocking when
|
|
313
|
-
// this is the only healthy projector replica.
|
|
314
|
-
recordProjectionRetry(retrying);
|
|
315
|
-
} else {
|
|
316
|
-
failureCount = 0;
|
|
308
|
+
recordResult(first);
|
|
309
|
+
if (first.attempted === 0) {
|
|
310
|
+
await scheduleNextDue();
|
|
311
|
+
continue;
|
|
317
312
|
}
|
|
318
313
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
314
|
+
const active = new Set<Promise<DeliveryOutcome>>();
|
|
315
|
+
let dispatchFailed = false;
|
|
316
|
+
const startDelivery = () => {
|
|
317
|
+
const delivery: Promise<DeliveryOutcome> = Promise.resolve().then(
|
|
318
|
+
async (): Promise<DeliveryOutcome> => {
|
|
319
|
+
if (closed || dispatchFailed)
|
|
320
|
+
return { delivery, status: 'skipped' };
|
|
321
|
+
try {
|
|
322
|
+
return {
|
|
323
|
+
delivery,
|
|
324
|
+
status: 'fulfilled',
|
|
325
|
+
value: await deps.dispatchOnce(),
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
// Stop refills as soon as this rejection is observed, including
|
|
329
|
+
// when another already-completed slot wins Promise.race first.
|
|
330
|
+
if (!dispatchFailed) {
|
|
331
|
+
dispatchFailed = true;
|
|
332
|
+
scheduleFailureRetry('dispatch', error);
|
|
333
|
+
}
|
|
334
|
+
return { delivery, status: 'rejected' };
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
active.add(delivery);
|
|
339
|
+
};
|
|
340
|
+
const fillSlots = () => {
|
|
341
|
+
if (closed || dispatchFailed) return;
|
|
342
|
+
while (active.size < parallelism) {
|
|
343
|
+
startDelivery();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
fillSlots();
|
|
347
|
+
while (active.size > 0) {
|
|
348
|
+
const wake = new Promise<null>((resolve) => {
|
|
349
|
+
wakeDrainer = () => resolve(null);
|
|
330
350
|
});
|
|
351
|
+
const outcome = await Promise.race([...active, wake]);
|
|
352
|
+
wakeDrainer = null;
|
|
353
|
+
if (outcome !== null) {
|
|
354
|
+
active.delete(outcome.delivery);
|
|
355
|
+
if (outcome.status === 'fulfilled') {
|
|
356
|
+
recordResult(outcome.value, !dispatchFailed);
|
|
357
|
+
if (outcome.value.attempted > 0 && !closed && !dispatchFailed) {
|
|
358
|
+
// Refill this completed slot immediately. A slower independent
|
|
359
|
+
// run never owns another run's delivery capacity.
|
|
360
|
+
startDelivery();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
requested &&
|
|
366
|
+
!closed &&
|
|
367
|
+
!dispatchFailed &&
|
|
368
|
+
active.size < parallelism
|
|
369
|
+
) {
|
|
370
|
+
// A new notification can use slots that previously found no work,
|
|
371
|
+
// even while another slot is still awaiting its gateway response.
|
|
372
|
+
// Keep the hint while all slots are occupied: an already-started
|
|
373
|
+
// empty claim does not establish idle after this notification.
|
|
374
|
+
requested = false;
|
|
375
|
+
fillSlots();
|
|
376
|
+
}
|
|
331
377
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// its own durable retry, so the next pass naturally skips that run while
|
|
335
|
-
// moving unrelated runs forward. A permanent failure is settled once and
|
|
336
|
-
// likewise cannot block another run's causal prefix.
|
|
337
|
-
if (attempted > 0) {
|
|
338
|
-
requested = true;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
378
|
+
reportVolume();
|
|
379
|
+
if (dispatchFailed || closed) return;
|
|
341
380
|
await scheduleNextDue();
|
|
342
381
|
}
|
|
382
|
+
reportVolume();
|
|
343
383
|
};
|
|
344
384
|
|
|
345
385
|
// Subscribe first, then scan. Notifications are lossy by design, so every
|
|
@@ -353,6 +393,7 @@ export function startRunLedgerOutboxRecovery(
|
|
|
353
393
|
close: async () => {
|
|
354
394
|
if (closed) return;
|
|
355
395
|
closed = true;
|
|
396
|
+
wakeDrainer?.();
|
|
356
397
|
clearWakeTimer();
|
|
357
398
|
cooldownUntilMs = null;
|
|
358
399
|
unsubscribeNotify?.();
|
|
@@ -20,12 +20,13 @@ export function transientServiceUnavailableDetail(
|
|
|
20
20
|
) {
|
|
21
21
|
return detail;
|
|
22
22
|
}
|
|
23
|
-
// Convex uses explicit ServiceUnavailable
|
|
23
|
+
// Convex uses explicit ServiceUnavailable, ExpiredInQueue and
|
|
24
|
+
// TooManyConcurrentRequests admission responses, as
|
|
24
25
|
// well as this generic InternalServerError envelope for short control-plane
|
|
25
26
|
// outages. The latter text is intentionally exact: application exceptions
|
|
26
27
|
// must not become retryable merely because they mention an internal-server
|
|
27
28
|
// error.
|
|
28
|
-
return /ServiceUnavailable|Service temporarily unavailable|temporarily unavailable \(Code:\s*\d+\)|"code"\s*:\s*"ExpiredInQueue"|"code"\s*:\s*"InternalServerError"[\s\S]*(?:Your request couldn't be completed\. Try again later\.|Your request timed out performing too many system operations\.)/i.test(
|
|
29
|
+
return /ServiceUnavailable|Service temporarily unavailable|temporarily unavailable \(Code:\s*\d+\)|"code"\s*:\s*"(?:ExpiredInQueue|TooManyConcurrentRequests)"|"code"\s*:\s*"InternalServerError"[\s\S]*(?:Your request couldn't be completed\. Try again later\.|Your request timed out performing too many system operations\.)/i.test(
|
|
29
30
|
detail,
|
|
30
31
|
)
|
|
31
32
|
? detail
|
|
@@ -4,9 +4,11 @@ import { staticPipelineStorageCodec } from './static-pipeline-storage-codec';
|
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
retainedPlayPipelineFields,
|
|
7
|
+
WRITE_RETAINED_PIPELINE_GRAPHS,
|
|
7
8
|
type RetainedPlayContract,
|
|
8
9
|
type RetainedPipelineCarrier,
|
|
9
10
|
encodeRetainedPlayPipelines,
|
|
11
|
+
createRetainedPlayPipelinesForTransport,
|
|
10
12
|
hydrateRetainedPlayPipelines,
|
|
11
13
|
prepareRetainedPlayPipelinesForTransport,
|
|
12
14
|
type RetainedPipelineGraph,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* This is an additive named representation. Readers must be deployed before
|
|
7
7
|
* WRITE_RETAINED_PIPELINE_GRAPHS is enabled in a separate release.
|
|
8
8
|
*/
|
|
9
|
-
export const WRITE_RETAINED_PIPELINE_GRAPHS =
|
|
9
|
+
export const WRITE_RETAINED_PIPELINE_GRAPHS = true;
|
|
10
10
|
|
|
11
11
|
type Scalar = null | boolean | number | string;
|
|
12
12
|
type GraphValue = Scalar | { ref: number };
|
|
@@ -255,12 +255,18 @@ function hydrateContractRoots(
|
|
|
255
255
|
return result;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/**
|
|
258
|
+
/** Create a graph only when the owner has selected reader-capable receivers. */
|
|
259
|
+
export function createRetainedPlayPipelinesForTransport<T>(input: T): T {
|
|
260
|
+
return WRITE_RETAINED_PIPELINE_GRAPHS
|
|
261
|
+
? encodeRetainedPlayPipelines(input)
|
|
262
|
+
: prepareRetainedPlayPipelinesForTransport(input);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Forward an immutable launch in its original representation. */
|
|
259
266
|
export function prepareRetainedPlayPipelinesForTransport<T>(input: T): T {
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
return
|
|
263
|
-
(record(input) && input.retainedPipelineGraph !== undefined)
|
|
267
|
+
// A historical launch may still call an older pinned app after adoption.
|
|
268
|
+
// New workers and projectors must not upgrade that receiver's wire format.
|
|
269
|
+
return record(input) && input.retainedPipelineGraph !== undefined
|
|
264
270
|
? encodeRetainedPlayPipelines(input)
|
|
265
271
|
: input;
|
|
266
272
|
}
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
retainedPlayPipelineFields,
|
|
11
11
|
encodeRetainedPlayPipelines,
|
|
12
12
|
hydrateRetainedPlayPipelines,
|
|
13
|
-
WRITE_RETAINED_PIPELINE_GRAPHS,
|
|
14
13
|
type RetainedPipelineGraph,
|
|
15
14
|
type RetainedPipelineCarrier,
|
|
16
15
|
} from './retained-pipeline-graph';
|
|
@@ -29,7 +28,7 @@ function readExecutionPipeline(
|
|
|
29
28
|
return readStoredStaticPipeline(record);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
/**
|
|
31
|
+
/** Preserve the representation selected by the immutable launch owner. */
|
|
33
32
|
function executionStorageFields(
|
|
34
33
|
pipeline: PlayStaticPipeline | null | undefined,
|
|
35
34
|
carrier?: RetainedPipelineCarrier,
|
|
@@ -38,11 +37,7 @@ function executionStorageFields(
|
|
|
38
37
|
staticPipelineV2?: ReturnType<typeof createStoredStaticPipelineV2>;
|
|
39
38
|
retainedPipelineGraph?: RetainedPipelineGraph;
|
|
40
39
|
} {
|
|
41
|
-
if (
|
|
42
|
-
(!WRITE_RETAINED_PIPELINE_GRAPHS &&
|
|
43
|
-
carrier?.retainedPipelineGraph === undefined) ||
|
|
44
|
-
pipeline === undefined
|
|
45
|
-
) {
|
|
40
|
+
if (carrier?.retainedPipelineGraph === undefined || pipeline === undefined) {
|
|
46
41
|
return createStaticPipelineStorageFields(pipeline);
|
|
47
42
|
}
|
|
48
43
|
const compact = encodeRetainedPlayPipelines(
|