deepline 0.1.227 → 0.1.229
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/apps/play-runner-workers/src/coordinator-entry.ts +76 -1842
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +0 -113
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +144 -1000
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/ledger-event-batches.ts +81 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +1 -50
- package/dist/bundling-sources/sdk/src/client.ts +24 -2
- package/dist/bundling-sources/sdk/src/play.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +382 -31
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +84 -79
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +201 -705
- package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +3 -10
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -47
- package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +0 -2
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +12 -200
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +2 -17
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -9
- package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-scope.ts +16 -64
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +5 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +0 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +81 -18
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +0 -36
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +3 -21
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +9 -1
- package/dist/bundling-sources/shared_libs/play-runtime/transport-error-diagnostics.ts +63 -0
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -0
- package/dist/cli/index.js +78 -11
- package/dist/cli/index.mjs +78 -11
- package/dist/{compiler-manifest-BhgZ23A4.d.ts → compiler-manifest-CYcwzSOJ.d.mts} +2 -0
- package/dist/{compiler-manifest-BhgZ23A4.d.mts → compiler-manifest-CYcwzSOJ.d.ts} +2 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +14 -5
- package/dist/index.mjs +14 -5
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/package.json +1 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/child-manifest-resolver.ts +0 -108
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +0 -374
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +0 -203
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +0 -14
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +0 -121
|
@@ -129,13 +129,10 @@ type RuntimeApiRequest =
|
|
|
129
129
|
idempotencyKey?: string;
|
|
130
130
|
}
|
|
131
131
|
| {
|
|
132
|
-
action: '
|
|
132
|
+
action: 'start_run';
|
|
133
133
|
idempotencyKey?: string;
|
|
134
134
|
playName: string;
|
|
135
135
|
runId: string;
|
|
136
|
-
parentRunId?: string | null;
|
|
137
|
-
rootRunId?: string | null;
|
|
138
|
-
workflowFamilyKey?: string | null;
|
|
139
136
|
artifactStorageKey?: string | null;
|
|
140
137
|
artifactHash?: string | null;
|
|
141
138
|
graphHash?: string | null;
|
|
@@ -380,6 +377,8 @@ function applyRetryJitter(delayMs: number): number {
|
|
|
380
377
|
return Math.round(half + Math.random() * half);
|
|
381
378
|
}
|
|
382
379
|
const APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
380
|
+
const APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG =
|
|
381
|
+
'[perf][worker.receipt_api.transport]';
|
|
383
382
|
const RUN_STATUS_LEDGER_SNAPSHOT_CACHE_LIMIT = 1_000;
|
|
384
383
|
const runStatusLedgerSnapshots = new Map<string, PlayRunLedgerSnapshot>();
|
|
385
384
|
// Positional log cursor per run attempt: count of liveLogs lines already
|
|
@@ -454,7 +453,7 @@ async function appendRunEventBatchesViaAppRuntime(
|
|
|
454
453
|
input: { playId: string; events: readonly PlayRunLedgerEvent[] },
|
|
455
454
|
): Promise<void> {
|
|
456
455
|
for (const events of partitionRunEventsForAppRuntime(input.events)) {
|
|
457
|
-
await
|
|
456
|
+
await appendRunEventsViaAppRuntimeRaw(context, {
|
|
458
457
|
playId: input.playId,
|
|
459
458
|
events,
|
|
460
459
|
});
|
|
@@ -504,6 +503,20 @@ function isRetryableAppRuntimeResponse(input: {
|
|
|
504
503
|
if (!isRetryableAppRuntimeAction(input.action)) {
|
|
505
504
|
return false;
|
|
506
505
|
}
|
|
506
|
+
// The app runtime may explicitly classify an otherwise-client-error status
|
|
507
|
+
// as transient. Keep the structured delivery contract authoritative rather
|
|
508
|
+
// than reducing every 4xx to permanent at the worker boundary.
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(input.body) as unknown;
|
|
511
|
+
if (
|
|
512
|
+
parsed &&
|
|
513
|
+
typeof parsed === 'object' &&
|
|
514
|
+
!Array.isArray(parsed) &&
|
|
515
|
+
(parsed as { retryable?: unknown }).retryable === true
|
|
516
|
+
) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
} catch {}
|
|
507
520
|
if (
|
|
508
521
|
input.action === 'append_run_events' &&
|
|
509
522
|
input.status === 500 &&
|
|
@@ -583,7 +596,7 @@ function appRuntimeErrorCode(response: Response, body: string): string | null {
|
|
|
583
596
|
}
|
|
584
597
|
|
|
585
598
|
export function isTransientAppRuntimeFailureBody(body: string): boolean {
|
|
586
|
-
return /WorkerOverloaded|"\s*code\s*"\s*:\s*"\s*InternalServerError\s*"|Your request couldn't be completed\. Try again later\.|timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|tuple concurrently updated|
|
|
599
|
+
return /WorkerOverloaded|"\s*code\s*"\s*:\s*"\s*InternalServerError\s*"|Your request couldn't be completed\. Try again later\.|timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|tuple concurrently updated|RUNTIME_SCHEDULER_(?:SATURATED|UNAVAILABLE)|Runtime scheduler DB pool is saturated|Runtime scheduler DB circuit breaker is open|OptimisticConcurrencyControlFailure|changed while this mutation was being run|Documents read from or written to the .* table changed while this mutation/i.test(
|
|
587
600
|
body,
|
|
588
601
|
);
|
|
589
602
|
}
|
|
@@ -657,7 +670,7 @@ function isRetryableAppRuntimeAction(
|
|
|
657
670
|
action === 'release_runtime_step_receipt' ||
|
|
658
671
|
action === 'save_results' ||
|
|
659
672
|
action === 'skip_runtime_step_receipt' ||
|
|
660
|
-
action === '
|
|
673
|
+
action === 'start_run'
|
|
661
674
|
);
|
|
662
675
|
}
|
|
663
676
|
|
|
@@ -733,14 +746,146 @@ function sleep(ms: number): Promise<void> {
|
|
|
733
746
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
734
747
|
}
|
|
735
748
|
|
|
749
|
+
type AppRuntimeRetryFailureKind =
|
|
750
|
+
| 'connection_refused'
|
|
751
|
+
| 'connection_reset'
|
|
752
|
+
| 'connection_timeout'
|
|
753
|
+
| 'dns'
|
|
754
|
+
| 'fetch_failed'
|
|
755
|
+
| 'http_error'
|
|
756
|
+
| 'request_timeout'
|
|
757
|
+
| 'response_body_error'
|
|
758
|
+
| 'response_body_timeout'
|
|
759
|
+
| 'retryable_http'
|
|
760
|
+
| 'transport_error';
|
|
761
|
+
|
|
762
|
+
type AppRuntimeRetryFailure = {
|
|
763
|
+
attempt: number;
|
|
764
|
+
failureKind: AppRuntimeRetryFailureKind;
|
|
765
|
+
attemptElapsedMs: number;
|
|
766
|
+
retryDelayMs: number;
|
|
767
|
+
httpStatus?: number;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
type AppRuntimeRetryTelemetry = {
|
|
771
|
+
enabled: boolean;
|
|
772
|
+
startedAt: number;
|
|
773
|
+
retryCount: number;
|
|
774
|
+
retrySleepMs: number;
|
|
775
|
+
failures: AppRuntimeRetryFailure[];
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
function appRuntimeTransportFailureKind(
|
|
779
|
+
error: unknown,
|
|
780
|
+
): AppRuntimeRetryFailureKind {
|
|
781
|
+
if (isRequestTimeoutAbort(error)) return 'request_timeout';
|
|
782
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
783
|
+
if (/\b(?:ENOTFOUND|EAI_AGAIN)\b|getaddrinfo|\bdns\b/i.test(message)) {
|
|
784
|
+
return 'dns';
|
|
785
|
+
}
|
|
786
|
+
if (/\bECONNRESET\b|connection reset/i.test(message)) {
|
|
787
|
+
return 'connection_reset';
|
|
788
|
+
}
|
|
789
|
+
if (/\bECONNREFUSED\b|connection refused/i.test(message)) {
|
|
790
|
+
return 'connection_refused';
|
|
791
|
+
}
|
|
792
|
+
if (
|
|
793
|
+
/\bETIMEDOUT\b|UND_ERR_CONNECT_TIMEOUT|connection (?:timeout|timed out)/i.test(
|
|
794
|
+
message,
|
|
795
|
+
)
|
|
796
|
+
) {
|
|
797
|
+
return 'connection_timeout';
|
|
798
|
+
}
|
|
799
|
+
return /fetch failed/i.test(message) ? 'fetch_failed' : 'transport_error';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function recordAppRuntimeRetryFailure(input: {
|
|
803
|
+
telemetry: AppRuntimeRetryTelemetry;
|
|
804
|
+
attempt: number;
|
|
805
|
+
failureKind: AppRuntimeRetryFailureKind;
|
|
806
|
+
attemptStartedAt: number;
|
|
807
|
+
retryDelayMs: number;
|
|
808
|
+
httpStatus?: number;
|
|
809
|
+
}): void {
|
|
810
|
+
if (!input.telemetry.enabled) return;
|
|
811
|
+
// Receipt actions have at most eight attempts. Keep the terminal eighth
|
|
812
|
+
// failure instead of truncating the causally important final observation.
|
|
813
|
+
if (input.telemetry.failures.length < 8) {
|
|
814
|
+
input.telemetry.failures.push({
|
|
815
|
+
attempt: input.attempt,
|
|
816
|
+
failureKind: input.failureKind,
|
|
817
|
+
attemptElapsedMs: Date.now() - input.attemptStartedAt,
|
|
818
|
+
retryDelayMs: input.retryDelayMs,
|
|
819
|
+
...(input.httpStatus === undefined
|
|
820
|
+
? {}
|
|
821
|
+
: { httpStatus: input.httpStatus }),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (input.retryDelayMs > 0) {
|
|
825
|
+
input.telemetry.retryCount += 1;
|
|
826
|
+
input.telemetry.retrySleepMs += input.retryDelayMs;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function emitAppRuntimeRetrySummary(input: {
|
|
831
|
+
telemetry: AppRuntimeRetryTelemetry;
|
|
832
|
+
action: RuntimeApiRequest['action'];
|
|
833
|
+
outcome: 'failed_after_retry' | 'recovered_after_retry';
|
|
834
|
+
attempts: number;
|
|
835
|
+
maxAttempts: number;
|
|
836
|
+
finalAttemptStartedAt: number;
|
|
837
|
+
}): void {
|
|
838
|
+
if (!input.telemetry.enabled || input.telemetry.retryCount === 0) return;
|
|
839
|
+
const payload = {
|
|
840
|
+
action: input.action,
|
|
841
|
+
outcome: input.outcome,
|
|
842
|
+
attempts: input.attempts,
|
|
843
|
+
maxAttempts: input.maxAttempts,
|
|
844
|
+
totalElapsedMs: Date.now() - input.telemetry.startedAt,
|
|
845
|
+
retrySleepMs: input.telemetry.retrySleepMs,
|
|
846
|
+
finalAttemptElapsedMs: Date.now() - input.finalAttemptStartedAt,
|
|
847
|
+
failures: input.telemetry.failures,
|
|
848
|
+
};
|
|
849
|
+
// Observability must never alter receipt execution. The payload contains
|
|
850
|
+
// primitives from a fixed taxonomy only: no URL, token, body, raw error,
|
|
851
|
+
// run/play/org id, receipt key, input, or output.
|
|
852
|
+
try {
|
|
853
|
+
const serialized = JSON.stringify(payload);
|
|
854
|
+
if (input.outcome === 'failed_after_retry') {
|
|
855
|
+
console.warn(APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG, serialized);
|
|
856
|
+
} else {
|
|
857
|
+
console.info(APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG, serialized);
|
|
858
|
+
}
|
|
859
|
+
} catch {}
|
|
860
|
+
}
|
|
861
|
+
|
|
736
862
|
async function retryAppRuntimeBodyTimeoutOrThrow(input: {
|
|
737
863
|
action: RuntimeApiRequest['action'];
|
|
738
864
|
attempt: number;
|
|
739
865
|
maxAttempts: number;
|
|
740
866
|
error: unknown;
|
|
867
|
+
telemetry: AppRuntimeRetryTelemetry;
|
|
868
|
+
attemptStartedAt: number;
|
|
869
|
+
httpStatus?: number;
|
|
741
870
|
retryAfterMs?: number | null;
|
|
742
871
|
}): Promise<void> {
|
|
743
872
|
if (!isRequestTimeoutAbort(input.error)) {
|
|
873
|
+
recordAppRuntimeRetryFailure({
|
|
874
|
+
telemetry: input.telemetry,
|
|
875
|
+
attempt: input.attempt,
|
|
876
|
+
failureKind: 'response_body_error',
|
|
877
|
+
attemptStartedAt: input.attemptStartedAt,
|
|
878
|
+
retryDelayMs: 0,
|
|
879
|
+
httpStatus: input.httpStatus,
|
|
880
|
+
});
|
|
881
|
+
emitAppRuntimeRetrySummary({
|
|
882
|
+
telemetry: input.telemetry,
|
|
883
|
+
action: input.action,
|
|
884
|
+
outcome: 'failed_after_retry',
|
|
885
|
+
attempts: input.attempt,
|
|
886
|
+
maxAttempts: input.maxAttempts,
|
|
887
|
+
finalAttemptStartedAt: input.attemptStartedAt,
|
|
888
|
+
});
|
|
744
889
|
throw input.error;
|
|
745
890
|
}
|
|
746
891
|
if (
|
|
@@ -750,14 +895,37 @@ async function retryAppRuntimeBodyTimeoutOrThrow(input: {
|
|
|
750
895
|
error: input.error,
|
|
751
896
|
})
|
|
752
897
|
) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
input.retryAfterMs ?? 0,
|
|
757
|
-
),
|
|
898
|
+
const retryDelayMs = Math.max(
|
|
899
|
+
applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
|
|
900
|
+
input.retryAfterMs ?? 0,
|
|
758
901
|
);
|
|
902
|
+
recordAppRuntimeRetryFailure({
|
|
903
|
+
telemetry: input.telemetry,
|
|
904
|
+
attempt: input.attempt,
|
|
905
|
+
failureKind: 'response_body_timeout',
|
|
906
|
+
attemptStartedAt: input.attemptStartedAt,
|
|
907
|
+
retryDelayMs,
|
|
908
|
+
httpStatus: input.httpStatus,
|
|
909
|
+
});
|
|
910
|
+
await sleep(retryDelayMs);
|
|
759
911
|
return;
|
|
760
912
|
}
|
|
913
|
+
recordAppRuntimeRetryFailure({
|
|
914
|
+
telemetry: input.telemetry,
|
|
915
|
+
attempt: input.attempt,
|
|
916
|
+
failureKind: 'response_body_timeout',
|
|
917
|
+
attemptStartedAt: input.attemptStartedAt,
|
|
918
|
+
retryDelayMs: 0,
|
|
919
|
+
httpStatus: input.httpStatus,
|
|
920
|
+
});
|
|
921
|
+
emitAppRuntimeRetrySummary({
|
|
922
|
+
telemetry: input.telemetry,
|
|
923
|
+
action: input.action,
|
|
924
|
+
outcome: 'failed_after_retry',
|
|
925
|
+
attempts: input.attempt,
|
|
926
|
+
maxAttempts: input.maxAttempts,
|
|
927
|
+
finalAttemptStartedAt: input.attemptStartedAt,
|
|
928
|
+
});
|
|
761
929
|
throw new AppRuntimeApiTransportError({
|
|
762
930
|
action: input.action,
|
|
763
931
|
attempts: input.attempt,
|
|
@@ -786,6 +954,40 @@ export class AppRuntimeApiTransportError extends Error {
|
|
|
786
954
|
}
|
|
787
955
|
}
|
|
788
956
|
|
|
957
|
+
/**
|
|
958
|
+
* A non-2xx response from the app runtime boundary.
|
|
959
|
+
*
|
|
960
|
+
* Keep the response contract structured here so durable callers can decide
|
|
961
|
+
* whether to retry or block a delivery without parsing an error message.
|
|
962
|
+
*/
|
|
963
|
+
export class AppRuntimeApiResponseError extends Error {
|
|
964
|
+
readonly status: number;
|
|
965
|
+
readonly code: string | null;
|
|
966
|
+
readonly requestId: string | null;
|
|
967
|
+
readonly retryable: boolean;
|
|
968
|
+
|
|
969
|
+
constructor(input: {
|
|
970
|
+
action: RuntimeApiRequest['action'];
|
|
971
|
+
status: number;
|
|
972
|
+
code?: string | null;
|
|
973
|
+
requestId?: string | null;
|
|
974
|
+
retryable: boolean;
|
|
975
|
+
detail: string;
|
|
976
|
+
}) {
|
|
977
|
+
super(
|
|
978
|
+
`App runtime API ${input.action} failed with status ${input.status}` +
|
|
979
|
+
`${input.code ? ` code=${input.code}` : ''}` +
|
|
980
|
+
`${input.requestId ? ` request_id=${input.requestId}` : ''}: ` +
|
|
981
|
+
input.detail,
|
|
982
|
+
);
|
|
983
|
+
this.name = 'AppRuntimeApiResponseError';
|
|
984
|
+
this.status = input.status;
|
|
985
|
+
this.code = input.code?.trim() || null;
|
|
986
|
+
this.requestId = input.requestId?.trim() || null;
|
|
987
|
+
this.retryable = input.retryable;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
789
991
|
function resolveAppRuntimeApiUrl(context: WorkerRuntimeApiContext): string {
|
|
790
992
|
const baseUrl = context.baseUrl.trim().replace(/\/$/, '');
|
|
791
993
|
return `${baseUrl}${PLAY_RUNTIME_API_COMPAT_PATH}`;
|
|
@@ -811,6 +1013,13 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
811
1013
|
});
|
|
812
1014
|
|
|
813
1015
|
const maxAttempts = appRuntimeMaxAttempts(body.action);
|
|
1016
|
+
const retryTelemetry: AppRuntimeRetryTelemetry = {
|
|
1017
|
+
enabled: isRuntimeStepReceiptAction(body.action),
|
|
1018
|
+
startedAt: Date.now(),
|
|
1019
|
+
retryCount: 0,
|
|
1020
|
+
retrySleepMs: 0,
|
|
1021
|
+
failures: [],
|
|
1022
|
+
};
|
|
814
1023
|
const requestTimeoutMs = Math.max(
|
|
815
1024
|
1,
|
|
816
1025
|
Math.floor(
|
|
@@ -819,6 +1028,7 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
819
1028
|
);
|
|
820
1029
|
|
|
821
1030
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1031
|
+
const attemptStartedAt = Date.now();
|
|
822
1032
|
let response: Response;
|
|
823
1033
|
try {
|
|
824
1034
|
response = await runtimeFetch(resolveAppRuntimeApiUrl(context), {
|
|
@@ -843,11 +1053,34 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
843
1053
|
attempt < maxAttempts &&
|
|
844
1054
|
isRetryableAppRuntimeFetchError({ action: body.action, error })
|
|
845
1055
|
) {
|
|
846
|
-
|
|
847
|
-
|
|
1056
|
+
const retryDelayMs = applyRetryJitter(
|
|
1057
|
+
appRuntimeRetryDelayMs(body.action, attempt),
|
|
848
1058
|
);
|
|
1059
|
+
recordAppRuntimeRetryFailure({
|
|
1060
|
+
telemetry: retryTelemetry,
|
|
1061
|
+
attempt,
|
|
1062
|
+
failureKind: appRuntimeTransportFailureKind(error),
|
|
1063
|
+
attemptStartedAt,
|
|
1064
|
+
retryDelayMs,
|
|
1065
|
+
});
|
|
1066
|
+
await sleep(retryDelayMs);
|
|
849
1067
|
continue;
|
|
850
1068
|
}
|
|
1069
|
+
recordAppRuntimeRetryFailure({
|
|
1070
|
+
telemetry: retryTelemetry,
|
|
1071
|
+
attempt,
|
|
1072
|
+
failureKind: appRuntimeTransportFailureKind(error),
|
|
1073
|
+
attemptStartedAt,
|
|
1074
|
+
retryDelayMs: 0,
|
|
1075
|
+
});
|
|
1076
|
+
emitAppRuntimeRetrySummary({
|
|
1077
|
+
telemetry: retryTelemetry,
|
|
1078
|
+
action: body.action,
|
|
1079
|
+
outcome: 'failed_after_retry',
|
|
1080
|
+
attempts: attempt,
|
|
1081
|
+
maxAttempts,
|
|
1082
|
+
finalAttemptStartedAt: attemptStartedAt,
|
|
1083
|
+
});
|
|
851
1084
|
throw new AppRuntimeApiTransportError({
|
|
852
1085
|
action: body.action,
|
|
853
1086
|
attempts: attempt,
|
|
@@ -856,13 +1089,25 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
856
1089
|
}
|
|
857
1090
|
if (response.ok) {
|
|
858
1091
|
try {
|
|
859
|
-
|
|
1092
|
+
const parsed = (await response.json()) as TResponse;
|
|
1093
|
+
emitAppRuntimeRetrySummary({
|
|
1094
|
+
telemetry: retryTelemetry,
|
|
1095
|
+
action: body.action,
|
|
1096
|
+
outcome: 'recovered_after_retry',
|
|
1097
|
+
attempts: attempt,
|
|
1098
|
+
maxAttempts,
|
|
1099
|
+
finalAttemptStartedAt: attemptStartedAt,
|
|
1100
|
+
});
|
|
1101
|
+
return parsed;
|
|
860
1102
|
} catch (error) {
|
|
861
1103
|
await retryAppRuntimeBodyTimeoutOrThrow({
|
|
862
1104
|
action: body.action,
|
|
863
1105
|
attempt,
|
|
864
1106
|
maxAttempts,
|
|
865
1107
|
error,
|
|
1108
|
+
telemetry: retryTelemetry,
|
|
1109
|
+
attemptStartedAt,
|
|
1110
|
+
httpStatus: response.status,
|
|
866
1111
|
});
|
|
867
1112
|
continue;
|
|
868
1113
|
}
|
|
@@ -875,6 +1120,22 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
875
1120
|
isRequestTimeoutAbort(error) &&
|
|
876
1121
|
!isRetryableAppRuntimeResponseStatus(response.status)
|
|
877
1122
|
) {
|
|
1123
|
+
recordAppRuntimeRetryFailure({
|
|
1124
|
+
telemetry: retryTelemetry,
|
|
1125
|
+
attempt,
|
|
1126
|
+
failureKind: 'response_body_timeout',
|
|
1127
|
+
attemptStartedAt,
|
|
1128
|
+
retryDelayMs: 0,
|
|
1129
|
+
httpStatus: response.status,
|
|
1130
|
+
});
|
|
1131
|
+
emitAppRuntimeRetrySummary({
|
|
1132
|
+
telemetry: retryTelemetry,
|
|
1133
|
+
action: body.action,
|
|
1134
|
+
outcome: 'failed_after_retry',
|
|
1135
|
+
attempts: attempt,
|
|
1136
|
+
maxAttempts,
|
|
1137
|
+
finalAttemptStartedAt: attemptStartedAt,
|
|
1138
|
+
});
|
|
878
1139
|
throw new Error(
|
|
879
1140
|
`App runtime API ${body.action} failed with status ${response.status}: response body timed out`,
|
|
880
1141
|
{ cause: error },
|
|
@@ -885,6 +1146,9 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
885
1146
|
attempt,
|
|
886
1147
|
maxAttempts,
|
|
887
1148
|
error,
|
|
1149
|
+
telemetry: retryTelemetry,
|
|
1150
|
+
attemptStartedAt,
|
|
1151
|
+
httpStatus: response.status,
|
|
888
1152
|
retryAfterMs: appRuntimeRetryAfterMs(response, ''),
|
|
889
1153
|
});
|
|
890
1154
|
continue;
|
|
@@ -897,22 +1161,61 @@ async function postAppRuntimeApi<TResponse>(
|
|
|
897
1161
|
body: responseText,
|
|
898
1162
|
})
|
|
899
1163
|
) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
appRuntimeRetryAfterMs(response, responseText) ?? 0,
|
|
904
|
-
),
|
|
1164
|
+
const retryDelayMs = Math.max(
|
|
1165
|
+
applyRetryJitter(appRuntimeRetryDelayMs(body.action, attempt)),
|
|
1166
|
+
appRuntimeRetryAfterMs(response, responseText) ?? 0,
|
|
905
1167
|
);
|
|
1168
|
+
recordAppRuntimeRetryFailure({
|
|
1169
|
+
telemetry: retryTelemetry,
|
|
1170
|
+
attempt,
|
|
1171
|
+
failureKind: 'retryable_http',
|
|
1172
|
+
attemptStartedAt,
|
|
1173
|
+
retryDelayMs,
|
|
1174
|
+
httpStatus: response.status,
|
|
1175
|
+
});
|
|
1176
|
+
await sleep(retryDelayMs);
|
|
906
1177
|
continue;
|
|
907
1178
|
}
|
|
1179
|
+
recordAppRuntimeRetryFailure({
|
|
1180
|
+
telemetry: retryTelemetry,
|
|
1181
|
+
attempt,
|
|
1182
|
+
failureKind: isRetryableAppRuntimeResponse({
|
|
1183
|
+
action: body.action,
|
|
1184
|
+
status: response.status,
|
|
1185
|
+
body: responseText,
|
|
1186
|
+
})
|
|
1187
|
+
? 'retryable_http'
|
|
1188
|
+
: 'http_error',
|
|
1189
|
+
attemptStartedAt,
|
|
1190
|
+
retryDelayMs: 0,
|
|
1191
|
+
httpStatus: response.status,
|
|
1192
|
+
});
|
|
1193
|
+
emitAppRuntimeRetrySummary({
|
|
1194
|
+
telemetry: retryTelemetry,
|
|
1195
|
+
action: body.action,
|
|
1196
|
+
outcome: 'failed_after_retry',
|
|
1197
|
+
attempts: attempt,
|
|
1198
|
+
maxAttempts,
|
|
1199
|
+
finalAttemptStartedAt: attemptStartedAt,
|
|
1200
|
+
});
|
|
908
1201
|
const code = appRuntimeErrorCode(response, responseText);
|
|
909
1202
|
const requestId = response.headers.get('x-deepline-request-id')?.trim();
|
|
910
|
-
throw new
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1203
|
+
throw new AppRuntimeApiResponseError({
|
|
1204
|
+
action: body.action,
|
|
1205
|
+
status: response.status,
|
|
1206
|
+
code,
|
|
1207
|
+
requestId,
|
|
1208
|
+
// Immediate request retries are intentionally selective, but durable
|
|
1209
|
+
// delivery must never classify an unrecognized upstream 5xx as poison.
|
|
1210
|
+
retryable:
|
|
1211
|
+
response.status >= 500 ||
|
|
1212
|
+
isRetryableAppRuntimeResponse({
|
|
1213
|
+
action: body.action,
|
|
1214
|
+
status: response.status,
|
|
1215
|
+
body: responseText,
|
|
1216
|
+
}),
|
|
1217
|
+
detail: summarizeAppRuntimeErrorBody(responseText),
|
|
1218
|
+
});
|
|
916
1219
|
}
|
|
917
1220
|
|
|
918
1221
|
throw new Error(`App runtime API ${body.action} failed after retries.`);
|
|
@@ -1239,7 +1542,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
|
|
|
1239
1542
|
);
|
|
1240
1543
|
}
|
|
1241
1544
|
|
|
1242
|
-
|
|
1545
|
+
async function appendRunEventsViaAppRuntimeRaw(
|
|
1243
1546
|
context: WorkerRuntimeApiContext,
|
|
1244
1547
|
input: {
|
|
1245
1548
|
playId: string;
|
|
@@ -1253,14 +1556,62 @@ export async function appendRunEventsViaAppRuntime(
|
|
|
1253
1556
|
});
|
|
1254
1557
|
}
|
|
1255
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* Append Run Ledger events through the bounded transport used by every runtime
|
|
1561
|
+
* producer. Scheduler terminal outbox events can carry a complete log replay,
|
|
1562
|
+
* so this boundary must never forward an arbitrarily large event array or log
|
|
1563
|
+
* event directly to Convex.
|
|
1564
|
+
*/
|
|
1565
|
+
export async function appendRunEventsViaAppRuntime(
|
|
1566
|
+
context: WorkerRuntimeApiContext,
|
|
1567
|
+
input: {
|
|
1568
|
+
playId: string;
|
|
1569
|
+
events: PlayRunLedgerEvent[];
|
|
1570
|
+
idempotencyKey?: string;
|
|
1571
|
+
},
|
|
1572
|
+
): Promise<void> {
|
|
1573
|
+
const batches = partitionRunEventsForAppRuntime(input.events);
|
|
1574
|
+
if (batches.length <= 1) {
|
|
1575
|
+
await appendRunEventsViaAppRuntimeRaw(context, input);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Terminal idempotency keys record a completed delivery. Sending the same
|
|
1580
|
+
// key on an earlier log-only batch would make Convex drop the terminal batch,
|
|
1581
|
+
// so attach it to the batch that contains the final terminal event only.
|
|
1582
|
+
let terminalBatchIndex = -1;
|
|
1583
|
+
for (let index = batches.length - 1; index >= 0; index -= 1) {
|
|
1584
|
+
if (
|
|
1585
|
+
batches[index]!.some(
|
|
1586
|
+
(event) =>
|
|
1587
|
+
event.type === 'run.completed' ||
|
|
1588
|
+
event.type === 'run.failed' ||
|
|
1589
|
+
event.type === 'run.cancelled',
|
|
1590
|
+
)
|
|
1591
|
+
) {
|
|
1592
|
+
terminalBatchIndex = index;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const idempotencyBatchIndex =
|
|
1597
|
+
terminalBatchIndex >= 0 ? terminalBatchIndex : batches.length - 1;
|
|
1598
|
+
|
|
1599
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
1600
|
+
await appendRunEventsViaAppRuntimeRaw(context, {
|
|
1601
|
+
playId: input.playId,
|
|
1602
|
+
events: batches[index]!,
|
|
1603
|
+
...(input.idempotencyKey && index === idempotencyBatchIndex
|
|
1604
|
+
? { idempotencyKey: input.idempotencyKey }
|
|
1605
|
+
: {}),
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1256
1610
|
export async function startRunViaAppRuntime(
|
|
1257
1611
|
context: WorkerRuntimeApiContext,
|
|
1258
1612
|
input: {
|
|
1259
1613
|
playName: string;
|
|
1260
1614
|
runId: string;
|
|
1261
|
-
parentRunId?: string | null;
|
|
1262
|
-
rootRunId?: string | null;
|
|
1263
|
-
workflowFamilyKey?: string | null;
|
|
1264
1615
|
artifactStorageKey?: string | null;
|
|
1265
1616
|
artifactHash?: string | null;
|
|
1266
1617
|
graphHash?: string | null;
|
|
@@ -1275,7 +1626,7 @@ export async function startRunViaAppRuntime(
|
|
|
1275
1626
|
},
|
|
1276
1627
|
): Promise<void> {
|
|
1277
1628
|
await postAppRuntimeApi<{ ok: true }>(context, {
|
|
1278
|
-
action: '
|
|
1629
|
+
action: 'start_run',
|
|
1279
1630
|
...input,
|
|
1280
1631
|
});
|
|
1281
1632
|
}
|