deepline 0.1.290 → 0.1.292
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +103 -7
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +4 -46
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +159 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +50 -29
- package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +410 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +494 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +0 -343
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +0 -69
|
@@ -54,7 +54,7 @@ const DAYTONA_UPLOAD_ATTEMPT_DEADLINE_MS = 90_000;
|
|
|
54
54
|
const RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN =
|
|
55
55
|
/\bRuntime Postgres\b.*\b(connection timed out|connect timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|Connection terminated|Connection ended unexpectedly)\b/i;
|
|
56
56
|
const RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN =
|
|
57
|
-
/AppRuntimeApiTransportError: App runtime API transport exhausted action=(?:get|claim|mark|complete|fail|heartbeat|release|skip)_runtime_step_receipts?/i;
|
|
57
|
+
/AppRuntimeApiTransportError: (?:App runtime API|Runtime receipt gateway) transport exhausted action=(?:get|claim|mark|complete|fail|heartbeat|release|skip)_runtime_step_receipts?/i;
|
|
58
58
|
const RUNTIME_API_TRANSPORT_RETRY_PATTERN =
|
|
59
59
|
/\bRuntime API request to .+ failed before receiving a response:\s*(?:fetch failed|connection (?:terminated|timed out|closed|reset)|ECONNRESET|ETIMEDOUT|ECONNREFUSED)\b/i;
|
|
60
60
|
const DAYTONA_INFRASTRUCTURE_RETRY_PATTERN =
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
sameOwnerTerminalAttemptEpochSql,
|
|
79
79
|
} from './sheet-attempt-sql';
|
|
80
80
|
import type { MapRowOutcome } from './durability-store';
|
|
81
|
+
import { RUNTIME_CAPACITY_POLICY } from './runtime-capacity-policy';
|
|
81
82
|
import {
|
|
82
83
|
normalizeRuntimeMapInputIndex,
|
|
83
84
|
prepareRuntimeSheetRowsForJsonTransport,
|
|
@@ -349,7 +350,8 @@ const RUNTIME_POSTGRES_RECEIPT_ADMISSION_MAX_QUEUED = 2_000;
|
|
|
349
350
|
// memory independently from lightweight receipt requests; overflow is a
|
|
350
351
|
// retryable 503 and waits outside this long-lived gateway process.
|
|
351
352
|
const RUNTIME_POSTGRES_SHEET_ADMISSION_MAX_QUEUED = 4;
|
|
352
|
-
const RUNTIME_POSTGRES_RECEIPT_ADMISSION_TIMEOUT_MS =
|
|
353
|
+
const RUNTIME_POSTGRES_RECEIPT_ADMISSION_TIMEOUT_MS =
|
|
354
|
+
RUNTIME_CAPACITY_POLICY.receiptGateway.admissionTimeoutMs;
|
|
353
355
|
const RECEIPT_STATUS_QUEUED = RECEIPT_STATUS_CODE.queued;
|
|
354
356
|
const RECEIPT_STATUS_PENDING = RECEIPT_STATUS_CODE.pending;
|
|
355
357
|
const RECEIPT_STATUS_RUNNING = RECEIPT_STATUS_CODE.running;
|
|
@@ -4173,6 +4175,9 @@ export async function claimRuntimeWorkReceipt(
|
|
|
4173
4175
|
input.leaseTtlMs,
|
|
4174
4176
|
PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
4175
4177
|
);
|
|
4178
|
+
// A successful lease-bearing claim is the execution transition. Keep the
|
|
4179
|
+
// stored queued status for mixed-version readers; the runner does not add
|
|
4180
|
+
// a second queued -> running write on the provider hot path.
|
|
4176
4181
|
const claimStatus =
|
|
4177
4182
|
input.leaseAware === true
|
|
4178
4183
|
? RECEIPT_STATUS_QUEUED
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One capacity policy for the Absurd runtime.
|
|
3
|
+
*
|
|
4
|
+
* Postgres owns backlog. The active lane adds workers from durable claimable
|
|
5
|
+
* demand and oldest-claimable age; historical lanes stay cold unless they own
|
|
6
|
+
* runnable work. Keep these values together so the worker, scaler, gateway,
|
|
7
|
+
* and tests cannot independently invent concurrency or timeout budgets.
|
|
8
|
+
*/
|
|
9
|
+
export const RUNTIME_CAPACITY_POLICY = {
|
|
10
|
+
absurd: {
|
|
11
|
+
/** Warm floor for availability and deploy rollovers. */
|
|
12
|
+
activeLaneMachines: 2,
|
|
13
|
+
/** 32 Machines x 8 claim slots permits 256 concurrent launch/resume legs. */
|
|
14
|
+
maxActiveLaneMachines: 32,
|
|
15
|
+
workerSlotsPerMachine: 8,
|
|
16
|
+
perOrgConcurrency: 4,
|
|
17
|
+
/** Queue age is an escape hatch for small-but-stuck backlogs. */
|
|
18
|
+
scaleUpQueueAgeMs: 10_000,
|
|
19
|
+
/** Bound one Fly reconciliation without turning backlog into a stampede. */
|
|
20
|
+
maxScaleUpMachinesPerReconcile: 4,
|
|
21
|
+
/** Do not tear down burst capacity on the first empty observation. */
|
|
22
|
+
scaleDownQuietMs: 120_000,
|
|
23
|
+
},
|
|
24
|
+
receiptGateway: {
|
|
25
|
+
admissionTimeoutMs: 10_000,
|
|
26
|
+
requestTimeoutMs: 30_000,
|
|
27
|
+
},
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const ABSURD_GLOBAL_RUN_CONCURRENCY =
|
|
31
|
+
RUNTIME_CAPACITY_POLICY.absurd.maxActiveLaneMachines *
|
|
32
|
+
RUNTIME_CAPACITY_POLICY.absurd.workerSlotsPerMachine;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Active releases use pg-boss-style burst semantics: claimable count sets the
|
|
36
|
+
* target, an aged claimable queue forces continued growth, and growth per
|
|
37
|
+
* observation is bounded. A short/empty observation ends burst growth; the
|
|
38
|
+
* caller supplies idleForMs to hold excess capacity through a quiet window.
|
|
39
|
+
*
|
|
40
|
+
* Historical releases stay cold until they have immediately runnable or
|
|
41
|
+
* currently claimed work.
|
|
42
|
+
*/
|
|
43
|
+
export function desiredAbsurdLaneMachines(input: {
|
|
44
|
+
active: boolean;
|
|
45
|
+
claimableRuns: number;
|
|
46
|
+
runningRuns?: number;
|
|
47
|
+
currentMachines?: number;
|
|
48
|
+
oldestClaimableAgeMs?: number;
|
|
49
|
+
idleForMs?: number;
|
|
50
|
+
}): number {
|
|
51
|
+
const claimableRuns = nonNegativeInteger(input.claimableRuns);
|
|
52
|
+
const runningRuns = nonNegativeInteger(input.runningRuns ?? 0);
|
|
53
|
+
if (!input.active) return claimableRuns > 0 || runningRuns > 0 ? 1 : 0;
|
|
54
|
+
|
|
55
|
+
const policy = RUNTIME_CAPACITY_POLICY.absurd;
|
|
56
|
+
const currentMachines = Math.max(
|
|
57
|
+
policy.activeLaneMachines,
|
|
58
|
+
nonNegativeInteger(input.currentMachines ?? policy.activeLaneMachines),
|
|
59
|
+
);
|
|
60
|
+
const demandedMachines = Math.ceil(
|
|
61
|
+
(claimableRuns + runningRuns) / policy.workerSlotsPerMachine,
|
|
62
|
+
);
|
|
63
|
+
let desired = Math.max(policy.activeLaneMachines, demandedMachines);
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
claimableRuns > 0 &&
|
|
67
|
+
nonNegativeNumber(input.oldestClaimableAgeMs ?? 0) >=
|
|
68
|
+
policy.scaleUpQueueAgeMs
|
|
69
|
+
) {
|
|
70
|
+
desired = Math.max(desired, currentMachines + 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
desired = Math.min(
|
|
74
|
+
desired,
|
|
75
|
+
currentMachines + policy.maxScaleUpMachinesPerReconcile,
|
|
76
|
+
policy.maxActiveLaneMachines,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
claimableRuns === 0 &&
|
|
81
|
+
runningRuns === 0 &&
|
|
82
|
+
nonNegativeNumber(input.idleForMs ?? 0) < policy.scaleDownQuietMs
|
|
83
|
+
) {
|
|
84
|
+
desired = Math.max(desired, currentMachines);
|
|
85
|
+
}
|
|
86
|
+
return desired;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function nonNegativeInteger(value: number): number {
|
|
90
|
+
return Math.max(0, Math.floor(nonNegativeNumber(value)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function nonNegativeNumber(value: number): number {
|
|
94
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function assertRuntimeCapacityPolicy(): void {
|
|
98
|
+
if (
|
|
99
|
+
RUNTIME_CAPACITY_POLICY.receiptGateway.requestTimeoutMs <=
|
|
100
|
+
RUNTIME_CAPACITY_POLICY.receiptGateway.admissionTimeoutMs
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'Receipt gateway request timeout must exceed its admission timeout.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
RUNTIME_CAPACITY_POLICY.absurd.perOrgConcurrency >
|
|
108
|
+
ABSURD_GLOBAL_RUN_CONCURRENCY
|
|
109
|
+
) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'Per-org runtime concurrency cannot exceed global runtime concurrency.',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
RUNTIME_CAPACITY_POLICY.absurd.activeLaneMachines >
|
|
116
|
+
RUNTIME_CAPACITY_POLICY.absurd.maxActiveLaneMachines
|
|
117
|
+
) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'Active-lane minimum Machines cannot exceed the active-lane maximum.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
assertRuntimeCapacityPolicy();
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AcquireRuntimeReceiptExecutionLockInput,
|
|
3
|
+
ClaimRuntimeStepReceiptInput,
|
|
4
|
+
CompleteRuntimeStepReceiptInput,
|
|
5
|
+
ContextOptions,
|
|
6
|
+
FailRuntimeStepReceiptInput,
|
|
7
|
+
ReleaseRuntimeReceiptExecutionLockInput,
|
|
8
|
+
ReleaseRuntimeStepReceiptInput,
|
|
9
|
+
RuntimeStepReceipt,
|
|
10
|
+
SkipRuntimeStepReceiptInput,
|
|
11
|
+
} from './ctx-types';
|
|
12
|
+
import {
|
|
13
|
+
acquireRuntimeReceiptExecutionLockViaAppRuntime,
|
|
14
|
+
AppRuntimeApiResponseError,
|
|
15
|
+
AppRuntimeApiTransportError,
|
|
16
|
+
claimRuntimeStepReceiptsViaAppRuntime,
|
|
17
|
+
completeRuntimeStepReceiptsViaAppRuntime,
|
|
18
|
+
failRuntimeStepReceiptsViaAppRuntime,
|
|
19
|
+
getRuntimeStepReceiptsViaAppRuntime,
|
|
20
|
+
heartbeatRuntimeStepReceiptsViaAppRuntime,
|
|
21
|
+
isAppRuntimeApiCapacityError,
|
|
22
|
+
releaseRuntimeReceiptExecutionLockViaAppRuntime,
|
|
23
|
+
releaseRuntimeStepReceiptViaAppRuntime,
|
|
24
|
+
skipRuntimeStepReceiptViaAppRuntime,
|
|
25
|
+
type WorkerRuntimeApiContext,
|
|
26
|
+
} from './app-runtime-api';
|
|
27
|
+
import { RuntimeReceiptWriter } from './runtime-receipt-writer';
|
|
28
|
+
|
|
29
|
+
type ReceiptOutput =
|
|
30
|
+
| RuntimeStepReceipt
|
|
31
|
+
| null
|
|
32
|
+
| boolean
|
|
33
|
+
| { ownerExecutionId: string; expiresAt: string };
|
|
34
|
+
|
|
35
|
+
type ReceiptCommand =
|
|
36
|
+
| {
|
|
37
|
+
kind: 'get';
|
|
38
|
+
input: { key: string; runId: string };
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
kind: 'claim';
|
|
42
|
+
input: ClaimRuntimeStepReceiptInput;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
kind: 'settle';
|
|
46
|
+
outcome: 'complete';
|
|
47
|
+
input: CompleteRuntimeStepReceiptInput;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
kind: 'settle';
|
|
51
|
+
outcome: 'fail';
|
|
52
|
+
input: FailRuntimeStepReceiptInput;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
kind: 'heartbeat';
|
|
56
|
+
input: {
|
|
57
|
+
key: string;
|
|
58
|
+
runId: string;
|
|
59
|
+
runAttempt?: number | null;
|
|
60
|
+
leaseId: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
kind: 'settle';
|
|
65
|
+
outcome: 'release';
|
|
66
|
+
input: ReleaseRuntimeStepReceiptInput;
|
|
67
|
+
serial: number;
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
kind: 'settle';
|
|
71
|
+
outcome: 'skip';
|
|
72
|
+
input: SkipRuntimeStepReceiptInput;
|
|
73
|
+
serial: number;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
kind: 'acquire_execution_lock';
|
|
77
|
+
input: AcquireRuntimeReceiptExecutionLockInput;
|
|
78
|
+
serial: number;
|
|
79
|
+
}
|
|
80
|
+
| {
|
|
81
|
+
kind: 'release_execution_lock';
|
|
82
|
+
input: ReleaseRuntimeReceiptExecutionLockInput;
|
|
83
|
+
serial: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type ReceiptHandlerName =
|
|
87
|
+
| 'acquireRuntimeReceiptExecutionLock'
|
|
88
|
+
| 'releaseRuntimeReceiptExecutionLock'
|
|
89
|
+
| 'getRuntimeStepReceipt'
|
|
90
|
+
| 'getRuntimeStepReceipts'
|
|
91
|
+
| 'claimRuntimeStepReceipt'
|
|
92
|
+
| 'claimRuntimeStepReceipts'
|
|
93
|
+
| 'completeRuntimeStepReceipt'
|
|
94
|
+
| 'completeRuntimeStepReceipts'
|
|
95
|
+
| 'releaseRuntimeStepReceipt'
|
|
96
|
+
| 'failRuntimeStepReceipt'
|
|
97
|
+
| 'failRuntimeStepReceipts'
|
|
98
|
+
| 'heartbeatRuntimeStepReceipts'
|
|
99
|
+
| 'skipRuntimeStepReceipt';
|
|
100
|
+
|
|
101
|
+
export type RuntimeReceiptStoreHandlers = Required<
|
|
102
|
+
Pick<ContextOptions, ReceiptHandlerName>
|
|
103
|
+
>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The runner's single Work Receipt Adapter.
|
|
107
|
+
*
|
|
108
|
+
* Receipt-domain batching and gateway transport stay behind this seam. Every
|
|
109
|
+
* command shares one writer, so heterogeneous operations cannot produce
|
|
110
|
+
* concurrent gateway requests. The low-level runtime client is explicitly
|
|
111
|
+
* one-attempt; this Adapter is the sole retry owner.
|
|
112
|
+
*/
|
|
113
|
+
export class RuntimeReceiptStoreAdapter {
|
|
114
|
+
readonly handlers: RuntimeReceiptStoreHandlers;
|
|
115
|
+
readonly #writer: RuntimeReceiptWriter<ReceiptCommand, ReceiptOutput>;
|
|
116
|
+
#serial = 0;
|
|
117
|
+
|
|
118
|
+
constructor(options: {
|
|
119
|
+
context: WorkerRuntimeApiContext;
|
|
120
|
+
playName: string;
|
|
121
|
+
runId: string;
|
|
122
|
+
maxBatchSize?: number;
|
|
123
|
+
maxFlushMs?: number;
|
|
124
|
+
maxBufferedBytes?: number;
|
|
125
|
+
}) {
|
|
126
|
+
const context: WorkerRuntimeApiContext = {
|
|
127
|
+
...options.context,
|
|
128
|
+
retryPolicy: 'none',
|
|
129
|
+
};
|
|
130
|
+
this.#writer = new RuntimeReceiptWriter({
|
|
131
|
+
batchKey: receiptCommandBatchKey,
|
|
132
|
+
maxBatchSize: options.maxBatchSize,
|
|
133
|
+
maxFlushMs: options.maxFlushMs,
|
|
134
|
+
maxBufferedBytes: options.maxBufferedBytes,
|
|
135
|
+
classifyRetryableError: classifyReceiptRetry,
|
|
136
|
+
send: async (commands, { signal }) =>
|
|
137
|
+
await sendReceiptCommands({
|
|
138
|
+
context: { ...context, signal },
|
|
139
|
+
playName: options.playName,
|
|
140
|
+
commands,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const write = <Output extends ReceiptOutput>(
|
|
145
|
+
command: ReceiptCommand,
|
|
146
|
+
): Promise<Output> =>
|
|
147
|
+
this.#writer.write(command, {
|
|
148
|
+
signal: options.context.signal,
|
|
149
|
+
}) as Promise<Output>;
|
|
150
|
+
const writeMany = <Output extends ReceiptOutput>(
|
|
151
|
+
commands: readonly ReceiptCommand[],
|
|
152
|
+
): Promise<Output[]> =>
|
|
153
|
+
Promise.all(commands.map((command) => write<Output>(command)));
|
|
154
|
+
|
|
155
|
+
this.handlers = {
|
|
156
|
+
acquireRuntimeReceiptExecutionLock: (input) =>
|
|
157
|
+
write({
|
|
158
|
+
kind: 'acquire_execution_lock',
|
|
159
|
+
input,
|
|
160
|
+
serial: this.#nextSerial(),
|
|
161
|
+
}),
|
|
162
|
+
releaseRuntimeReceiptExecutionLock: (input) =>
|
|
163
|
+
write({
|
|
164
|
+
kind: 'release_execution_lock',
|
|
165
|
+
input,
|
|
166
|
+
serial: this.#nextSerial(),
|
|
167
|
+
}),
|
|
168
|
+
getRuntimeStepReceipt: ({ key }) =>
|
|
169
|
+
write({
|
|
170
|
+
kind: 'get',
|
|
171
|
+
input: { key, runId: options.runId },
|
|
172
|
+
}),
|
|
173
|
+
getRuntimeStepReceipts: ({ keys }) =>
|
|
174
|
+
writeMany(
|
|
175
|
+
keys.map((key) => ({
|
|
176
|
+
kind: 'get' as const,
|
|
177
|
+
input: { key, runId: options.runId },
|
|
178
|
+
})),
|
|
179
|
+
),
|
|
180
|
+
claimRuntimeStepReceipt: (input) => write({ kind: 'claim', input }),
|
|
181
|
+
claimRuntimeStepReceipts: (input) =>
|
|
182
|
+
writeMany(
|
|
183
|
+
input.keys.map((key, index) => ({
|
|
184
|
+
kind: 'claim' as const,
|
|
185
|
+
input: {
|
|
186
|
+
key,
|
|
187
|
+
runId: input.runId,
|
|
188
|
+
runAttempt: input.runAttempt,
|
|
189
|
+
leaseAware: input.leaseAware,
|
|
190
|
+
reclaimRunning: input.reclaimRunning,
|
|
191
|
+
forceRefresh: input.forceRefresh,
|
|
192
|
+
forceFailedRefresh: input.forceFailedRefresh,
|
|
193
|
+
...(input.leaseIds ? { leaseId: input.leaseIds[index] } : {}),
|
|
194
|
+
},
|
|
195
|
+
})),
|
|
196
|
+
),
|
|
197
|
+
completeRuntimeStepReceipt: (input) =>
|
|
198
|
+
write({ kind: 'settle', outcome: 'complete', input }),
|
|
199
|
+
completeRuntimeStepReceipts: ({ receipts }) =>
|
|
200
|
+
writeMany(
|
|
201
|
+
receipts.map((input) => ({
|
|
202
|
+
kind: 'settle' as const,
|
|
203
|
+
outcome: 'complete' as const,
|
|
204
|
+
input,
|
|
205
|
+
})),
|
|
206
|
+
),
|
|
207
|
+
releaseRuntimeStepReceipt: (input) =>
|
|
208
|
+
write({
|
|
209
|
+
kind: 'settle',
|
|
210
|
+
outcome: 'release',
|
|
211
|
+
input,
|
|
212
|
+
serial: this.#nextSerial(),
|
|
213
|
+
}),
|
|
214
|
+
failRuntimeStepReceipt: (input) =>
|
|
215
|
+
write({ kind: 'settle', outcome: 'fail', input }),
|
|
216
|
+
failRuntimeStepReceipts: ({ receipts }) =>
|
|
217
|
+
writeMany(
|
|
218
|
+
receipts.map((input) => ({
|
|
219
|
+
kind: 'settle' as const,
|
|
220
|
+
outcome: 'fail' as const,
|
|
221
|
+
input,
|
|
222
|
+
})),
|
|
223
|
+
),
|
|
224
|
+
heartbeatRuntimeStepReceipts: (input) =>
|
|
225
|
+
writeMany(
|
|
226
|
+
input.keys.map((key) => ({
|
|
227
|
+
kind: 'heartbeat' as const,
|
|
228
|
+
input: {
|
|
229
|
+
key,
|
|
230
|
+
runId: input.runId,
|
|
231
|
+
runAttempt: input.runAttempt,
|
|
232
|
+
leaseId: input.leaseId,
|
|
233
|
+
},
|
|
234
|
+
})),
|
|
235
|
+
),
|
|
236
|
+
skipRuntimeStepReceipt: (input) =>
|
|
237
|
+
write({
|
|
238
|
+
kind: 'settle',
|
|
239
|
+
outcome: 'skip',
|
|
240
|
+
input,
|
|
241
|
+
serial: this.#nextSerial(),
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
flush(): Promise<void> {
|
|
247
|
+
return this.#writer.flush();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
close(reason?: unknown): Promise<void> {
|
|
251
|
+
return this.#writer.close(reason);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#nextSerial(): number {
|
|
255
|
+
return ++this.#serial;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function receiptCommandBatchKey(command: ReceiptCommand): string {
|
|
260
|
+
switch (command.kind) {
|
|
261
|
+
case 'get':
|
|
262
|
+
return JSON.stringify([command.kind, command.input.runId]);
|
|
263
|
+
case 'claim':
|
|
264
|
+
return JSON.stringify([
|
|
265
|
+
command.kind,
|
|
266
|
+
command.input.runId,
|
|
267
|
+
command.input.runAttempt ?? null,
|
|
268
|
+
command.input.leaseAware === true,
|
|
269
|
+
command.input.reclaimRunning === true,
|
|
270
|
+
command.input.forceRefresh === true,
|
|
271
|
+
command.input.forceFailedRefresh === true,
|
|
272
|
+
command.input.leaseId !== undefined,
|
|
273
|
+
]);
|
|
274
|
+
case 'settle':
|
|
275
|
+
return command.outcome === 'complete' || command.outcome === 'fail'
|
|
276
|
+
? JSON.stringify([command.kind, command.outcome, command.input.runId])
|
|
277
|
+
: `${command.kind}:${command.outcome}:${command.serial}`;
|
|
278
|
+
case 'heartbeat':
|
|
279
|
+
return JSON.stringify([
|
|
280
|
+
command.kind,
|
|
281
|
+
command.input.runId,
|
|
282
|
+
command.input.runAttempt ?? null,
|
|
283
|
+
command.input.leaseId,
|
|
284
|
+
]);
|
|
285
|
+
case 'acquire_execution_lock':
|
|
286
|
+
case 'release_execution_lock':
|
|
287
|
+
return `${command.kind}:${command.serial}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function classifyReceiptRetry(error: unknown): { retryAfterMs: number } | null {
|
|
292
|
+
if (isAppRuntimeApiCapacityError(error)) {
|
|
293
|
+
return { retryAfterMs: error.retryAfterMs };
|
|
294
|
+
}
|
|
295
|
+
if (error instanceof AppRuntimeApiTransportError) {
|
|
296
|
+
return { retryAfterMs: 250 };
|
|
297
|
+
}
|
|
298
|
+
if (error instanceof AppRuntimeApiResponseError && error.retryable) {
|
|
299
|
+
return { retryAfterMs: 250 };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function sendReceiptCommands(input: {
|
|
305
|
+
context: WorkerRuntimeApiContext;
|
|
306
|
+
playName: string;
|
|
307
|
+
commands: readonly ReceiptCommand[];
|
|
308
|
+
}): Promise<readonly ReceiptOutput[]> {
|
|
309
|
+
const first = input.commands[0];
|
|
310
|
+
if (!first) return [];
|
|
311
|
+
switch (first.kind) {
|
|
312
|
+
case 'get': {
|
|
313
|
+
const commands = input.commands as readonly Extract<
|
|
314
|
+
ReceiptCommand,
|
|
315
|
+
{ kind: 'get' }
|
|
316
|
+
>[];
|
|
317
|
+
return await getRuntimeStepReceiptsViaAppRuntime(input.context, {
|
|
318
|
+
playName: input.playName,
|
|
319
|
+
runId: first.input.runId,
|
|
320
|
+
keys: commands.map((command) => command.input.key),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
case 'claim': {
|
|
324
|
+
const commands = input.commands as readonly Extract<
|
|
325
|
+
ReceiptCommand,
|
|
326
|
+
{ kind: 'claim' }
|
|
327
|
+
>[];
|
|
328
|
+
return await claimRuntimeStepReceiptsViaAppRuntime(input.context, {
|
|
329
|
+
playName: input.playName,
|
|
330
|
+
runId: first.input.runId,
|
|
331
|
+
runAttempt: first.input.runAttempt,
|
|
332
|
+
keys: commands.map((command) => command.input.key),
|
|
333
|
+
leaseIds:
|
|
334
|
+
first.input.leaseId === undefined
|
|
335
|
+
? undefined
|
|
336
|
+
: commands.map((command) => command.input.leaseId!),
|
|
337
|
+
leaseAware: first.input.leaseAware,
|
|
338
|
+
reclaimRunning: first.input.reclaimRunning,
|
|
339
|
+
forceRefresh: first.input.forceRefresh,
|
|
340
|
+
forceFailedRefresh: first.input.forceFailedRefresh,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
case 'settle':
|
|
344
|
+
switch (first.outcome) {
|
|
345
|
+
case 'complete': {
|
|
346
|
+
const commands = input.commands as readonly Extract<
|
|
347
|
+
ReceiptCommand,
|
|
348
|
+
{ kind: 'settle'; outcome: 'complete' }
|
|
349
|
+
>[];
|
|
350
|
+
return await completeRuntimeStepReceiptsViaAppRuntime(input.context, {
|
|
351
|
+
playName: input.playName,
|
|
352
|
+
runId: first.input.runId,
|
|
353
|
+
receipts: commands.map((command) => command.input),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
case 'fail': {
|
|
357
|
+
const commands = input.commands as readonly Extract<
|
|
358
|
+
ReceiptCommand,
|
|
359
|
+
{ kind: 'settle'; outcome: 'fail' }
|
|
360
|
+
>[];
|
|
361
|
+
return await failRuntimeStepReceiptsViaAppRuntime(input.context, {
|
|
362
|
+
playName: input.playName,
|
|
363
|
+
runId: first.input.runId,
|
|
364
|
+
receipts: commands.map((command) => command.input),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
case 'release':
|
|
368
|
+
return [
|
|
369
|
+
await releaseRuntimeStepReceiptViaAppRuntime(input.context, {
|
|
370
|
+
playName: input.playName,
|
|
371
|
+
...first.input,
|
|
372
|
+
}),
|
|
373
|
+
];
|
|
374
|
+
case 'skip':
|
|
375
|
+
return [
|
|
376
|
+
await skipRuntimeStepReceiptViaAppRuntime(input.context, {
|
|
377
|
+
playName: input.playName,
|
|
378
|
+
...first.input,
|
|
379
|
+
}),
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
case 'heartbeat': {
|
|
383
|
+
const commands = input.commands as readonly Extract<
|
|
384
|
+
ReceiptCommand,
|
|
385
|
+
{ kind: 'heartbeat' }
|
|
386
|
+
>[];
|
|
387
|
+
return await heartbeatRuntimeStepReceiptsViaAppRuntime(input.context, {
|
|
388
|
+
playName: input.playName,
|
|
389
|
+
runId: first.input.runId,
|
|
390
|
+
runAttempt: first.input.runAttempt,
|
|
391
|
+
leaseId: first.input.leaseId,
|
|
392
|
+
keys: commands.map((command) => command.input.key),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
case 'acquire_execution_lock':
|
|
396
|
+
return [
|
|
397
|
+
await acquireRuntimeReceiptExecutionLockViaAppRuntime(input.context, {
|
|
398
|
+
playName: input.playName,
|
|
399
|
+
...first.input,
|
|
400
|
+
}),
|
|
401
|
+
];
|
|
402
|
+
case 'release_execution_lock':
|
|
403
|
+
return [
|
|
404
|
+
await releaseRuntimeReceiptExecutionLockViaAppRuntime(input.context, {
|
|
405
|
+
playName: input.playName,
|
|
406
|
+
...first.input,
|
|
407
|
+
}),
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
}
|