deepline 0.3.229 → 0.3.231
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/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +47 -32
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +38 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +8 -3
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +39 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +293 -4
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +29 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +50 -10
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +187 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +52 -2
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +30 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +118 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +135 -21
- package/dist/cli/index.js +109 -7
- package/dist/cli/index.mjs +109 -7
- package/dist/index.js +93 -2
- package/dist/index.mjs +93 -2
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* One capacity policy for the Absurd runtime.
|
|
3
3
|
*
|
|
4
4
|
* Postgres owns backlog. The active lane adds workers from durable claimable
|
|
5
|
-
* demand
|
|
5
|
+
* demand; historical lanes stay cold unless they own
|
|
6
6
|
* runnable work. Keep these values together so the worker, scaler, gateway,
|
|
7
7
|
* and tests cannot independently invent concurrency or timeout budgets.
|
|
8
8
|
*/
|
|
@@ -19,14 +19,20 @@ export const RUNTIME_CAPACITY_POLICY = {
|
|
|
19
19
|
/** Warm floor for availability and deploy rollovers. */
|
|
20
20
|
activeLaneMachines: 2,
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Bound active-lane burst demand. Each worker
|
|
23
23
|
* has one elected direct wake listener and a two-client pooled scheduler
|
|
24
24
|
* budget; scaling past this point amplifies connection pressure faster
|
|
25
25
|
* than it adds useful control-plane throughput.
|
|
26
26
|
*/
|
|
27
27
|
maxActiveLaneMachines: 8,
|
|
28
|
+
/**
|
|
29
|
+
* Shared started/pending worker budget across every release lane. Existing
|
|
30
|
+
* claim owners and rollback holds are retained; restoring the active warm
|
|
31
|
+
* floor may temporarily exceed this budget without forcing any worker off.
|
|
32
|
+
*/
|
|
33
|
+
maxFleetMachines: 8,
|
|
28
34
|
workerSlotsPerMachine: 8,
|
|
29
|
-
/**
|
|
35
|
+
/** Queue-age diagnostic threshold; age alone never grants worker slots. */
|
|
30
36
|
scaleUpQueueAgeMs: 1_000,
|
|
31
37
|
/** Bound one Fly reconciliation without turning backlog into a stampede. */
|
|
32
38
|
maxScaleUpMachinesPerReconcile: 4,
|
|
@@ -75,9 +81,9 @@ export const ABSURD_GLOBAL_RUN_CONCURRENCY =
|
|
|
75
81
|
RUNTIME_CAPACITY_POLICY.absurd.workerSlotsPerMachine;
|
|
76
82
|
|
|
77
83
|
/**
|
|
78
|
-
* Active releases use
|
|
79
|
-
*
|
|
80
|
-
*
|
|
84
|
+
* Active releases use claimable plus running count to set the target, with
|
|
85
|
+
* growth per observation bounded. Queue age is diagnostic, not additional
|
|
86
|
+
* runnable demand. A short/empty observation ends burst growth; the
|
|
81
87
|
* caller supplies idleForMs to hold excess capacity through a quiet window.
|
|
82
88
|
*
|
|
83
89
|
* Historical releases stay cold until they have immediately runnable or
|
|
@@ -105,14 +111,6 @@ export function desiredAbsurdLaneMachines(input: {
|
|
|
105
111
|
);
|
|
106
112
|
let desired = Math.max(policy.activeLaneMachines, demandedMachines);
|
|
107
113
|
|
|
108
|
-
if (
|
|
109
|
-
claimableRuns > 0 &&
|
|
110
|
-
nonNegativeNumber(input.oldestClaimableAgeMs ?? 0) >=
|
|
111
|
-
policy.scaleUpQueueAgeMs
|
|
112
|
-
) {
|
|
113
|
-
desired = Math.max(desired, currentMachines + 1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
114
|
desired = Math.min(
|
|
117
115
|
desired,
|
|
118
116
|
currentMachines + policy.maxScaleUpMachinesPerReconcile,
|
|
@@ -129,6 +127,172 @@ export function desiredAbsurdLaneMachines(input: {
|
|
|
129
127
|
return desired;
|
|
130
128
|
}
|
|
131
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Allocate only requested starts/creates against the physical worker fleet.
|
|
132
|
+
* The caller counts all started and pending Machines, including drainers,
|
|
133
|
+
* unhealthy workers, fresh claim owners, and rollback holds. Planned stops do
|
|
134
|
+
* not free budget until Fly confirms them. This function never removes workers.
|
|
135
|
+
*
|
|
136
|
+
* Restore the active warm floor first, even when that reports budget debt.
|
|
137
|
+
* Give freed budget to historical runnable lanes in round-robin order before
|
|
138
|
+
* active burst growth. Persist nextCursor between reconciliations so repeated
|
|
139
|
+
* single-slot openings do not always favor the first historical release.
|
|
140
|
+
*/
|
|
141
|
+
export function planAbsurdWorkerFleetGrowth(input: {
|
|
142
|
+
existingMachines: number;
|
|
143
|
+
maxFleetMachines?: number;
|
|
144
|
+
activeWarmFloorMissing: number;
|
|
145
|
+
demands: readonly {
|
|
146
|
+
releaseId: string;
|
|
147
|
+
active: boolean;
|
|
148
|
+
startMachineIds: readonly string[];
|
|
149
|
+
createCount: number;
|
|
150
|
+
}[];
|
|
151
|
+
cursor?: string;
|
|
152
|
+
}): {
|
|
153
|
+
allocations: {
|
|
154
|
+
releaseId: string;
|
|
155
|
+
startMachineIds: string[];
|
|
156
|
+
createCount: number;
|
|
157
|
+
deferredCount: number;
|
|
158
|
+
}[];
|
|
159
|
+
nextCursor: string;
|
|
160
|
+
physicalCount: number;
|
|
161
|
+
projectedPhysicalCount: number;
|
|
162
|
+
budgetDebt: number;
|
|
163
|
+
deferredCount: number;
|
|
164
|
+
warmFloorGrowth: number;
|
|
165
|
+
maxFleetMachines: number;
|
|
166
|
+
} {
|
|
167
|
+
const physicalCount = requireMachineCount(input.existingMachines);
|
|
168
|
+
const maxFleetMachines = requireMachineCount(
|
|
169
|
+
input.maxFleetMachines ?? RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines,
|
|
170
|
+
);
|
|
171
|
+
const warmFloorMissing = requireMachineCount(input.activeWarmFloorMissing);
|
|
172
|
+
const cursor = input.cursor ?? '';
|
|
173
|
+
if (typeof cursor !== 'string') {
|
|
174
|
+
throw new Error('Worker fleet cursor must be a release ID.');
|
|
175
|
+
}
|
|
176
|
+
const releases = new Set<string>();
|
|
177
|
+
const machines = new Set<string>();
|
|
178
|
+
const lanes = input.demands.map((demand) => {
|
|
179
|
+
if (!demand.releaseId || releases.has(demand.releaseId)) {
|
|
180
|
+
throw new Error('Worker fleet demands must have unique release IDs.');
|
|
181
|
+
}
|
|
182
|
+
releases.add(demand.releaseId);
|
|
183
|
+
for (const machineId of demand.startMachineIds) {
|
|
184
|
+
if (!machineId || machines.has(machineId)) {
|
|
185
|
+
throw new Error('Worker fleet starts must have unique Machine IDs.');
|
|
186
|
+
}
|
|
187
|
+
machines.add(machineId);
|
|
188
|
+
}
|
|
189
|
+
const createCount = requireMachineCount(demand.createCount);
|
|
190
|
+
return {
|
|
191
|
+
demand,
|
|
192
|
+
remaining: requireMachineCount(
|
|
193
|
+
demand.startMachineIds.length + createCount,
|
|
194
|
+
),
|
|
195
|
+
allocation: {
|
|
196
|
+
releaseId: demand.releaseId,
|
|
197
|
+
startMachineIds: [] as string[],
|
|
198
|
+
createCount: 0,
|
|
199
|
+
deferredCount: 0,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
const activeLanes = lanes.filter((lane) => lane.demand.active);
|
|
204
|
+
if (
|
|
205
|
+
activeLanes.length > 1 ||
|
|
206
|
+
(warmFloorMissing > 0 && activeLanes.length !== 1)
|
|
207
|
+
) {
|
|
208
|
+
throw new Error('Worker fleet warm capacity requires one active release.');
|
|
209
|
+
}
|
|
210
|
+
const activeLane = activeLanes[0];
|
|
211
|
+
const grant = (lane: (typeof lanes)[number]) => {
|
|
212
|
+
const startIndex = lane.allocation.startMachineIds.length;
|
|
213
|
+
if (startIndex < lane.demand.startMachineIds.length) {
|
|
214
|
+
lane.allocation.startMachineIds.push(
|
|
215
|
+
lane.demand.startMachineIds[startIndex],
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
lane.allocation.createCount += 1;
|
|
219
|
+
}
|
|
220
|
+
lane.remaining -= 1;
|
|
221
|
+
};
|
|
222
|
+
const warmFloorGrowth = Math.min(
|
|
223
|
+
warmFloorMissing,
|
|
224
|
+
activeLane?.remaining ?? 0,
|
|
225
|
+
);
|
|
226
|
+
for (let seat = 0; seat < warmFloorGrowth; seat += 1) grant(activeLane!);
|
|
227
|
+
let available = Math.max(
|
|
228
|
+
0,
|
|
229
|
+
maxFleetMachines - physicalCount - warmFloorGrowth,
|
|
230
|
+
);
|
|
231
|
+
const historical = lanes
|
|
232
|
+
.filter((lane) => !lane.demand.active && lane.remaining > 0)
|
|
233
|
+
.sort((a, b) =>
|
|
234
|
+
a.demand.releaseId < b.demand.releaseId
|
|
235
|
+
? -1
|
|
236
|
+
: a.demand.releaseId > b.demand.releaseId
|
|
237
|
+
? 1
|
|
238
|
+
: 0,
|
|
239
|
+
);
|
|
240
|
+
let nextCursor = cursor;
|
|
241
|
+
let historicalRemaining = historical.reduce(
|
|
242
|
+
(sum, lane) => sum + lane.remaining,
|
|
243
|
+
0,
|
|
244
|
+
);
|
|
245
|
+
// The requester set changes as lanes run out of work or disappear. A
|
|
246
|
+
// positional cursor would silently move across releases and can repeatedly
|
|
247
|
+
// skip a continuously waiting lane. Resume after the last granted identity.
|
|
248
|
+
const afterCursor = historical.findIndex(
|
|
249
|
+
(lane) => lane.demand.releaseId > cursor,
|
|
250
|
+
);
|
|
251
|
+
let index = afterCursor >= 0 ? afterCursor : 0;
|
|
252
|
+
while (available > 0 && historicalRemaining > 0) {
|
|
253
|
+
const lane = historical[index];
|
|
254
|
+
if (lane.remaining > 0) {
|
|
255
|
+
grant(lane);
|
|
256
|
+
available -= 1;
|
|
257
|
+
historicalRemaining -= 1;
|
|
258
|
+
nextCursor = lane.demand.releaseId;
|
|
259
|
+
}
|
|
260
|
+
index = (index + 1) % historical.length;
|
|
261
|
+
}
|
|
262
|
+
while (available > 0 && activeLane && activeLane.remaining > 0) {
|
|
263
|
+
grant(activeLane);
|
|
264
|
+
available -= 1;
|
|
265
|
+
}
|
|
266
|
+
let allocatedCount = 0;
|
|
267
|
+
let deferredCount = 0;
|
|
268
|
+
const allocations = lanes.map((lane) => {
|
|
269
|
+
allocatedCount +=
|
|
270
|
+
lane.allocation.startMachineIds.length + lane.allocation.createCount;
|
|
271
|
+
deferredCount += lane.remaining;
|
|
272
|
+
return { ...lane.allocation, deferredCount: lane.remaining };
|
|
273
|
+
});
|
|
274
|
+
const projectedPhysicalCount = requireMachineCount(
|
|
275
|
+
physicalCount + allocatedCount,
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
allocations,
|
|
279
|
+
nextCursor,
|
|
280
|
+
physicalCount,
|
|
281
|
+
projectedPhysicalCount,
|
|
282
|
+
budgetDebt: Math.max(0, projectedPhysicalCount - maxFleetMachines),
|
|
283
|
+
deferredCount,
|
|
284
|
+
warmFloorGrowth,
|
|
285
|
+
maxFleetMachines,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function requireMachineCount(value: number): number {
|
|
290
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
291
|
+
throw new Error('Worker fleet counts must be nonnegative safe integers.');
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
132
296
|
/**
|
|
133
297
|
* Execution-relay demand is the exact upstream admission state, not request
|
|
134
298
|
* count. Active permits consume capacity and waiting requests are an early
|
|
@@ -246,6 +410,15 @@ export function assertRuntimeCapacityPolicy(): void {
|
|
|
246
410
|
'Active-lane minimum Machines cannot exceed the active-lane maximum.',
|
|
247
411
|
);
|
|
248
412
|
}
|
|
413
|
+
if (
|
|
414
|
+
!Number.isSafeInteger(RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines) ||
|
|
415
|
+
RUNTIME_CAPACITY_POLICY.absurd.maxFleetMachines <
|
|
416
|
+
RUNTIME_CAPACITY_POLICY.absurd.activeLaneMachines
|
|
417
|
+
) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
'Worker fleet budget must cover the active-lane warm floor.',
|
|
420
|
+
);
|
|
421
|
+
}
|
|
249
422
|
const relay = RUNTIME_CAPACITY_POLICY.executionRelay;
|
|
250
423
|
if (
|
|
251
424
|
relay.minimumMachines > relay.maxMachines ||
|
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
export type RuntimeWorkCounters = {
|
|
3
3
|
logicalCalls: number;
|
|
4
4
|
providerRequests: number;
|
|
5
|
+
/** Older resource snapshots did not retain this scalar. */
|
|
6
|
+
provider429s?: number;
|
|
5
7
|
};
|
|
6
8
|
|
|
9
|
+
/** Sequence actual runner invocations within one scheduler generation. */
|
|
10
|
+
export function nextWorkCounterInvocationSeq(
|
|
11
|
+
checkpoint: unknown,
|
|
12
|
+
attempt: number,
|
|
13
|
+
): number {
|
|
14
|
+
const previous = record(checkpoint);
|
|
15
|
+
if (previous?.workCounterInvocationAttempt !== attempt) return 1;
|
|
16
|
+
const previousSeq = count(previous.workCounterInvocationSeq);
|
|
17
|
+
if (previousSeq === null) return 1;
|
|
18
|
+
if (previousSeq === Number.MAX_SAFE_INTEGER) {
|
|
19
|
+
throw new Error('Work counter invocation sequence exhausted.');
|
|
20
|
+
}
|
|
21
|
+
return previousSeq + 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
function record(value: unknown): Record<string, unknown> | null {
|
|
8
25
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
9
26
|
? (value as Record<string, unknown>)
|
|
@@ -16,6 +33,26 @@ function count(value: unknown): number | null {
|
|
|
16
33
|
: null;
|
|
17
34
|
}
|
|
18
35
|
|
|
36
|
+
export function runtimeWorkCountersFromStats(
|
|
37
|
+
stats: unknown,
|
|
38
|
+
): RuntimeWorkCounters | null {
|
|
39
|
+
const root = record(stats);
|
|
40
|
+
const compact = record(root?.runtimeWorkCounters);
|
|
41
|
+
if (compact) {
|
|
42
|
+
const logicalCalls = count(compact.logicalCalls);
|
|
43
|
+
const providerRequests = count(compact.providerRequests);
|
|
44
|
+
const provider429s = count(compact.provider429s);
|
|
45
|
+
if (
|
|
46
|
+
logicalCalls !== null &&
|
|
47
|
+
providerRequests !== null &&
|
|
48
|
+
provider429s !== null
|
|
49
|
+
) {
|
|
50
|
+
return { logicalCalls, providerRequests, provider429s };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
export function runtimeWorkCountersFromResources(
|
|
20
57
|
resources: unknown,
|
|
21
58
|
): RuntimeWorkCounters | null {
|
|
@@ -26,12 +63,25 @@ export function runtimeWorkCountersFromResources(
|
|
|
26
63
|
if (logicalCalls === null || !providers) return null;
|
|
27
64
|
|
|
28
65
|
let providerRequests = 0;
|
|
66
|
+
let provider429s = 0;
|
|
67
|
+
let provider429sKnown = true;
|
|
29
68
|
for (const [key, value] of Object.entries(providers)) {
|
|
30
69
|
if (!key.startsWith('tool:')) continue;
|
|
31
70
|
const requests = count(record(record(value)?.residence)?.count);
|
|
32
71
|
if (requests === null) return null;
|
|
33
72
|
providerRequests += requests;
|
|
34
|
-
|
|
73
|
+
const rejected = count(record(value)?.provider429Count);
|
|
74
|
+
if (rejected === null) provider429sKnown = false;
|
|
75
|
+
else provider429s += rejected;
|
|
76
|
+
if (
|
|
77
|
+
!Number.isSafeInteger(providerRequests) ||
|
|
78
|
+
!Number.isSafeInteger(provider429s)
|
|
79
|
+
)
|
|
80
|
+
return null;
|
|
35
81
|
}
|
|
36
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
logicalCalls,
|
|
84
|
+
providerRequests,
|
|
85
|
+
...(provider429sKnown ? { provider429s } : {}),
|
|
86
|
+
};
|
|
37
87
|
}
|
package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from '../ctx-types';
|
|
13
13
|
import type {
|
|
14
14
|
PlayRunnerExecutionConfig,
|
|
15
|
+
PlayRunnerEvent,
|
|
15
16
|
PlayRunnerLogEvent,
|
|
16
17
|
PlayRunnerResult,
|
|
17
18
|
} from '../protocol';
|
|
@@ -37,6 +38,9 @@ export type SandboxRunnerProgress = {
|
|
|
37
38
|
onCheckpoint?: (checkpoint: PlayCheckpoint) => void;
|
|
38
39
|
onRowUpdate?: (update: PlayRowUpdate) => void;
|
|
39
40
|
onExecutionEvent?: (event: PlayExecutionEvent) => void;
|
|
41
|
+
onWorkCounters?: (
|
|
42
|
+
event: Extract<PlayRunnerEvent, { type: 'work_counters' }>,
|
|
43
|
+
) => void;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -147,6 +151,7 @@ function createRunnerCallbacks(input: {
|
|
|
147
151
|
onCheckpoint: input.progress?.onCheckpoint,
|
|
148
152
|
onRowUpdate: input.progress?.onRowUpdate,
|
|
149
153
|
onExecutionEvent: input.progress?.onExecutionEvent,
|
|
154
|
+
onWorkCounters: input.progress?.onWorkCounters,
|
|
150
155
|
readDetachedRunnerReadiness: async (baselineHeartbeatAt) =>
|
|
151
156
|
await input.readiness({ baselineHeartbeatAt }),
|
|
152
157
|
onRuntimeResourceAcquired: async (resource) => {
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -2764,8 +2764,34 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
2764
2764
|
replayAllTerminalLogs: input.fromDetachedRunner,
|
|
2765
2765
|
terminalLogProducerAttempt: input.terminalLogProducerAttempt,
|
|
2766
2766
|
});
|
|
2767
|
-
const suspendedCheckpoint =
|
|
2768
|
-
input.runnerResult.checkpoint ?? checkpoint ?? null
|
|
2767
|
+
const suspendedCheckpoint = absurdCheckpointBase(
|
|
2768
|
+
input.runnerResult.checkpoint ?? checkpoint ?? null,
|
|
2769
|
+
);
|
|
2770
|
+
const segment = input.runnerResult.stats?.runtimeWorkCounterSegment as
|
|
2771
|
+
| { invocationSeq?: unknown; segmentId?: unknown }
|
|
2772
|
+
| undefined;
|
|
2773
|
+
if (
|
|
2774
|
+
typeof segment?.invocationSeq === 'number' &&
|
|
2775
|
+
Number.isSafeInteger(segment.invocationSeq) &&
|
|
2776
|
+
segment.invocationSeq >= 1 &&
|
|
2777
|
+
typeof segment.segmentId === 'string' &&
|
|
2778
|
+
segment.segmentId.length > 0
|
|
2779
|
+
) {
|
|
2780
|
+
// The checkpoint is persisted by the existing suspension transaction.
|
|
2781
|
+
// A resumed runner in this generation gets the next segment sequence;
|
|
2782
|
+
// a crash before that commit cannot silently reuse a finalized segment.
|
|
2783
|
+
const previous = suspendedCheckpoint.workCounterInvocationSeq;
|
|
2784
|
+
suspendedCheckpoint.workCounterInvocationSeq = Math.max(
|
|
2785
|
+
suspendedCheckpoint.workCounterInvocationAttempt === generation &&
|
|
2786
|
+
typeof previous === 'number' &&
|
|
2787
|
+
Number.isSafeInteger(previous) &&
|
|
2788
|
+
previous >= 0
|
|
2789
|
+
? previous
|
|
2790
|
+
: 0,
|
|
2791
|
+
segment.invocationSeq,
|
|
2792
|
+
);
|
|
2793
|
+
suspendedCheckpoint.workCounterInvocationAttempt = generation;
|
|
2794
|
+
}
|
|
2769
2795
|
const engine = createAbsurdEngineSuspensionAdapter({
|
|
2770
2796
|
ctx,
|
|
2771
2797
|
runId,
|
|
@@ -3439,6 +3465,8 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
3439
3465
|
type: 'execution_event',
|
|
3440
3466
|
event,
|
|
3441
3467
|
}),
|
|
3468
|
+
onWorkCounters: (event) =>
|
|
3469
|
+
progressReporter.onRunnerEvent(event),
|
|
3442
3470
|
},
|
|
3443
3471
|
interpretResult: ({ result }) => {
|
|
3444
3472
|
throwRetryableRuntimeInitializationFailure(result);
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from '../protocol';
|
|
27
27
|
import type { PlaySchedulerSubmitInput } from '../scheduler-backend';
|
|
28
28
|
import type { DocflowObservationRecord } from '../docflow-observation';
|
|
29
|
+
import { runtimeWorkCountersFromStats } from '../runtime-work-counters';
|
|
29
30
|
import {
|
|
30
31
|
buildPlayRunLedgerEventsFromStatusPatch,
|
|
31
32
|
createEmptyPlayRunLedgerSnapshot,
|
|
@@ -231,8 +232,7 @@ export type PostgresSchedulerProgressReporter = {
|
|
|
231
232
|
};
|
|
232
233
|
|
|
233
234
|
export type PostgresSchedulerDurableLogMode =
|
|
234
|
-
| '
|
|
235
|
-
| 'direct_worker';
|
|
235
|
+
'progress_reporter' | 'direct_worker';
|
|
236
236
|
|
|
237
237
|
function shouldPublishDurableLogEvent(
|
|
238
238
|
mode: PostgresSchedulerDurableLogMode,
|
|
@@ -387,7 +387,10 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
387
387
|
let liveLogs: string[] = [];
|
|
388
388
|
let liveLogsTotalCount = input.initialLiveLogTotalCount ?? 0;
|
|
389
389
|
let liveLogsDirty = false;
|
|
390
|
+
let workCounters: RuntimeStatusUpdate['workCounters'];
|
|
391
|
+
let workCountersDirty = false;
|
|
390
392
|
let lastFlushAt = 0;
|
|
393
|
+
let delayedFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
391
394
|
let totalRows = 0;
|
|
392
395
|
let activeMapNodeId: string | null = null;
|
|
393
396
|
let pendingDatasetLifecycleEvents: Array<
|
|
@@ -464,9 +467,11 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
464
467
|
const nextActivityObservations = [
|
|
465
468
|
...pendingActivityObservations.values(),
|
|
466
469
|
];
|
|
470
|
+
const nextWorkCounters = workCountersDirty ? workCounters : undefined;
|
|
467
471
|
pendingActivityObservations.clear();
|
|
468
472
|
liveNodeProgressDirty = false;
|
|
469
473
|
liveLogsDirty = false;
|
|
474
|
+
workCountersDirty = false;
|
|
470
475
|
|
|
471
476
|
try {
|
|
472
477
|
if (await isCurrentAttempt()) {
|
|
@@ -479,6 +484,7 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
479
484
|
liveLogProducerAttempt: producerAttempt,
|
|
480
485
|
datasetLifecycleEvents: nextDatasetLifecycleEvents,
|
|
481
486
|
activityObservations: nextActivityObservations,
|
|
487
|
+
workCounters: nextWorkCounters,
|
|
482
488
|
terminalLogReplay: request.terminalLogReplay,
|
|
483
489
|
lastCheckpointAt: Date.now(),
|
|
484
490
|
});
|
|
@@ -489,6 +495,7 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
489
495
|
datasetLifecycleEvents: nextDatasetLifecycleEvents,
|
|
490
496
|
activityObservations: nextActivityObservations,
|
|
491
497
|
});
|
|
498
|
+
workCountersDirty = true;
|
|
492
499
|
console.warn('[postgres-scheduler.progress_flush_failed]', {
|
|
493
500
|
runId: launch.runId,
|
|
494
501
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -520,18 +527,35 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
520
527
|
!force &&
|
|
521
528
|
!liveNodeProgressDirty &&
|
|
522
529
|
!liveLogsDirty &&
|
|
530
|
+
!workCountersDirty &&
|
|
523
531
|
pendingDatasetLifecycleEvents.length === 0 &&
|
|
524
532
|
pendingActivityObservations.size === 0
|
|
525
533
|
) {
|
|
534
|
+
if (delayedFlushTimer) {
|
|
535
|
+
clearTimeout(delayedFlushTimer);
|
|
536
|
+
delayedFlushTimer = null;
|
|
537
|
+
}
|
|
526
538
|
return activeProgressUpload ?? Promise.resolve();
|
|
527
539
|
}
|
|
528
540
|
const now = Date.now();
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
541
|
+
const intervalMs = getAdaptiveRunEventIntervalMs(totalRows);
|
|
542
|
+
if (!force && now - lastFlushAt < intervalMs) {
|
|
543
|
+
if (!delayedFlushTimer) {
|
|
544
|
+
delayedFlushTimer = setTimeout(
|
|
545
|
+
() => {
|
|
546
|
+
delayedFlushTimer = null;
|
|
547
|
+
void flush().catch(() => undefined);
|
|
548
|
+
},
|
|
549
|
+
Math.max(1, intervalMs - (now - lastFlushAt)),
|
|
550
|
+
);
|
|
551
|
+
delayedFlushTimer.unref?.();
|
|
552
|
+
}
|
|
533
553
|
return activeProgressUpload ?? Promise.resolve();
|
|
534
554
|
}
|
|
555
|
+
if (delayedFlushTimer) {
|
|
556
|
+
clearTimeout(delayedFlushTimer);
|
|
557
|
+
delayedFlushTimer = null;
|
|
558
|
+
}
|
|
535
559
|
lastFlushAt = now;
|
|
536
560
|
const requested = new Promise<void>((resolve, reject) => {
|
|
537
561
|
pendingProgressUpload = mergeProgressFlushRequest(
|
|
@@ -1057,6 +1081,62 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
1057
1081
|
return;
|
|
1058
1082
|
}
|
|
1059
1083
|
const type = (event as { type?: unknown }).type;
|
|
1084
|
+
if (type === 'work_counters') {
|
|
1085
|
+
const observed = event as {
|
|
1086
|
+
attempt?: unknown;
|
|
1087
|
+
invocationSeq?: unknown;
|
|
1088
|
+
segmentId?: unknown;
|
|
1089
|
+
logicalCalls?: unknown;
|
|
1090
|
+
providerRequests?: unknown;
|
|
1091
|
+
provider429s?: unknown;
|
|
1092
|
+
};
|
|
1093
|
+
if (
|
|
1094
|
+
typeof observed.attempt !== 'number' ||
|
|
1095
|
+
!Number.isSafeInteger(observed.attempt) ||
|
|
1096
|
+
observed.attempt < 0 ||
|
|
1097
|
+
typeof observed.invocationSeq !== 'number' ||
|
|
1098
|
+
!Number.isSafeInteger(observed.invocationSeq) ||
|
|
1099
|
+
observed.invocationSeq < 1 ||
|
|
1100
|
+
typeof observed.segmentId !== 'string' ||
|
|
1101
|
+
observed.segmentId.length === 0 ||
|
|
1102
|
+
(producerAttempt !== undefined &&
|
|
1103
|
+
observed.attempt !== producerAttempt) ||
|
|
1104
|
+
![
|
|
1105
|
+
observed.logicalCalls,
|
|
1106
|
+
observed.providerRequests,
|
|
1107
|
+
observed.provider429s,
|
|
1108
|
+
].every(
|
|
1109
|
+
(value) =>
|
|
1110
|
+
typeof value === 'number' &&
|
|
1111
|
+
Number.isSafeInteger(value) &&
|
|
1112
|
+
value >= 0,
|
|
1113
|
+
)
|
|
1114
|
+
)
|
|
1115
|
+
return;
|
|
1116
|
+
const next = {
|
|
1117
|
+
attempt: observed.attempt,
|
|
1118
|
+
invocationSeq: observed.invocationSeq,
|
|
1119
|
+
segmentId: observed.segmentId,
|
|
1120
|
+
logicalCalls: observed.logicalCalls as number,
|
|
1121
|
+
providerRequests: observed.providerRequests as number,
|
|
1122
|
+
provider429s: observed.provider429s as number,
|
|
1123
|
+
final: false,
|
|
1124
|
+
};
|
|
1125
|
+
if (
|
|
1126
|
+
workCounters &&
|
|
1127
|
+
workCounters.attempt === next.attempt &&
|
|
1128
|
+
workCounters.invocationSeq === next.invocationSeq &&
|
|
1129
|
+
workCounters.segmentId === next.segmentId &&
|
|
1130
|
+
workCounters.logicalCalls >= next.logicalCalls &&
|
|
1131
|
+
workCounters.providerRequests >= next.providerRequests &&
|
|
1132
|
+
workCounters.provider429s >= next.provider429s
|
|
1133
|
+
)
|
|
1134
|
+
return;
|
|
1135
|
+
workCounters = next;
|
|
1136
|
+
workCountersDirty = true;
|
|
1137
|
+
scheduleFlush();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1060
1140
|
if (type === 'log') {
|
|
1061
1141
|
const logEvent = event as { line?: unknown; source?: unknown };
|
|
1062
1142
|
const rawLine = logEvent.line;
|
|
@@ -1291,6 +1371,29 @@ export function createPostgresSchedulerProgressReporter(input: {
|
|
|
1291
1371
|
skipTerminalLogReplay?: boolean;
|
|
1292
1372
|
} = {},
|
|
1293
1373
|
): Promise<void> => {
|
|
1374
|
+
const terminalCounters = runtimeWorkCountersFromStats(result?.stats);
|
|
1375
|
+
const terminalSegment = result?.stats?.runtimeWorkCounterSegment as
|
|
1376
|
+
{ invocationSeq?: unknown; segmentId?: unknown } | undefined;
|
|
1377
|
+
if (
|
|
1378
|
+
terminalCounters &&
|
|
1379
|
+
terminalCounters.provider429s !== undefined &&
|
|
1380
|
+
typeof terminalSegment?.invocationSeq === 'number' &&
|
|
1381
|
+
Number.isSafeInteger(terminalSegment.invocationSeq) &&
|
|
1382
|
+
terminalSegment.invocationSeq >= 1 &&
|
|
1383
|
+
typeof terminalSegment.segmentId === 'string' &&
|
|
1384
|
+
terminalSegment.segmentId.length > 0
|
|
1385
|
+
) {
|
|
1386
|
+
workCounters = {
|
|
1387
|
+
attempt: producerAttempt ?? workCounters?.attempt ?? 0,
|
|
1388
|
+
invocationSeq: terminalSegment.invocationSeq,
|
|
1389
|
+
segmentId: terminalSegment.segmentId,
|
|
1390
|
+
logicalCalls: terminalCounters.logicalCalls,
|
|
1391
|
+
providerRequests: terminalCounters.providerRequests,
|
|
1392
|
+
provider429s: terminalCounters.provider429s,
|
|
1393
|
+
final: true,
|
|
1394
|
+
};
|
|
1395
|
+
workCountersDirty = true;
|
|
1396
|
+
}
|
|
1294
1397
|
// Mark started steps terminal with real timestamps. Static branches that
|
|
1295
1398
|
// never started remain non-executed; suspended runs stay in-flight.
|
|
1296
1399
|
if (result?.status === 'failed') {
|
|
@@ -1409,6 +1512,15 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
|
|
|
1409
1512
|
}),
|
|
1410
1513
|
source: 'worker',
|
|
1411
1514
|
});
|
|
1515
|
+
if (finalUpdate.workCounters) {
|
|
1516
|
+
progressEvents.push({
|
|
1517
|
+
type: 'work.counters',
|
|
1518
|
+
runId: input.launch.runId,
|
|
1519
|
+
source: 'worker',
|
|
1520
|
+
occurredAt: finalUpdate.lastCheckpointAt ?? Date.now(),
|
|
1521
|
+
...finalUpdate.workCounters,
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1412
1524
|
const datasetEvents: PlayRunLedgerEvent[] = (
|
|
1413
1525
|
finalUpdate.datasetLifecycleEvents ?? []
|
|
1414
1526
|
).map((event) => ({
|