deepline 0.3.212 → 0.3.214
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 +137 -32
- package/dist/bundling-sources/sdk/src/compat.ts +2 -0
- package/dist/bundling-sources/sdk/src/deployment-protection.ts +19 -0
- package/dist/bundling-sources/sdk/src/http.ts +3 -7
- package/dist/bundling-sources/sdk/src/index.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +25 -0
- 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/deployment/fly-registry.ts +33 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +53 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +187 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +67 -37
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +12 -3
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -2
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +176 -0
- package/dist/cli/index.js +572 -92
- package/dist/cli/index.mjs +572 -92
- package/dist/index.d.mts +191 -3
- package/dist/index.d.ts +191 -3
- package/dist/index.js +67 -35
- package/dist/index.mjs +67 -35
- package/dist/install-integrity.json +2 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +3 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS } from '../../../plays/sandbox-runtime-limits';
|
|
2
2
|
import { RUNTIME_SANDBOX_MEMORY_ABORT_CODE } from '../../run-failure';
|
|
3
|
+
import { PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER } from '../../runtime-contract';
|
|
4
|
+
import { RUNTIME_CLAIM_HEARTBEAT_DIVISOR } from '../../runtime-constants';
|
|
5
|
+
import { RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS } from '../../runner-app/runtime-heartbeat';
|
|
3
6
|
import {
|
|
4
7
|
RUNTIME_MEMORY_LEDGER_PATH_ENV,
|
|
5
8
|
RUNTIME_MEMORY_PRESSURE_PATH_ENV,
|
|
@@ -116,11 +119,11 @@ export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
|
|
|
116
119
|
RUNTIME_SANDBOX_MEMORY_ABORT_CODE;
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
|
-
* Build the
|
|
120
|
-
* sandbox memory envelope outside the customer-code Node
|
|
121
|
-
* synchronous infinite loop can block the runner's
|
|
122
|
-
* block this separate process; nor can a runner whose heap is
|
|
123
|
-
* its own memory watch reliably.
|
|
122
|
+
* Build the parent process that owns runner liveness, the hard runtime
|
|
123
|
+
* deadline, and the sandbox memory envelope outside the customer-code Node
|
|
124
|
+
* event loop. A synchronous infinite loop can block the runner's timers, but
|
|
125
|
+
* it cannot block this separate process; nor can a runner whose heap is
|
|
126
|
+
* thrashing run its own memory watch reliably.
|
|
124
127
|
*
|
|
125
128
|
* argv (after `node -e <source>`):
|
|
126
129
|
* 1 runnerPath, 2 configPath, 3 runtimeStartedPath, 4 runtimeCompletedPath,
|
|
@@ -185,6 +188,178 @@ let settled = false;
|
|
|
185
188
|
let forceKillTimer = null;
|
|
186
189
|
let runtimeTimer = null;
|
|
187
190
|
let terminalGraceTimer = null;
|
|
191
|
+
let heartbeatTimer = null;
|
|
192
|
+
let heartbeatController = null;
|
|
193
|
+
let heartbeatInFlight = false;
|
|
194
|
+
let heartbeatStopped = false;
|
|
195
|
+
let heartbeatConfigRead = false;
|
|
196
|
+
let heartbeatConfig = null;
|
|
197
|
+
let heartbeatLastSuccessAtMs = 0;
|
|
198
|
+
let heartbeatAttempt = 0;
|
|
199
|
+
let heartbeatLastErrorCode = null;
|
|
200
|
+
let heartbeatStopBeforeLeaseMs = 0;
|
|
201
|
+
let stopRequested = false;
|
|
202
|
+
|
|
203
|
+
function readRunnerHeartbeatConfig() {
|
|
204
|
+
if (heartbeatConfigRead) return heartbeatConfig;
|
|
205
|
+
heartbeatConfigRead = true;
|
|
206
|
+
try {
|
|
207
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
208
|
+
const context = config && config.context;
|
|
209
|
+
const push = context && context.runnerPushExecution;
|
|
210
|
+
const gatewayBaseUrl = String(context && context.receiptGatewayBaseUrl || '').replace(/\\/$/, '');
|
|
211
|
+
const executorToken = context && context.executorToken;
|
|
212
|
+
const runId = push && push.runId;
|
|
213
|
+
const leaseSeconds = Number(push && push.leaseSeconds);
|
|
214
|
+
if (
|
|
215
|
+
!gatewayBaseUrl ||
|
|
216
|
+
typeof executorToken !== 'string' ||
|
|
217
|
+
!executorToken.trim() ||
|
|
218
|
+
typeof runId !== 'string' ||
|
|
219
|
+
!runId.trim() ||
|
|
220
|
+
!Number.isFinite(leaseSeconds) ||
|
|
221
|
+
leaseSeconds <= 0
|
|
222
|
+
) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
heartbeatConfig = {
|
|
226
|
+
gatewayBaseUrl,
|
|
227
|
+
executorToken,
|
|
228
|
+
runId,
|
|
229
|
+
leaseMs: Math.max(1_000, Math.floor(leaseSeconds * 1_000)),
|
|
230
|
+
};
|
|
231
|
+
} catch {}
|
|
232
|
+
return heartbeatConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stopRunnerHeartbeat() {
|
|
236
|
+
heartbeatStopped = true;
|
|
237
|
+
if (heartbeatTimer) {
|
|
238
|
+
clearInterval(heartbeatTimer);
|
|
239
|
+
heartbeatTimer = null;
|
|
240
|
+
}
|
|
241
|
+
if (heartbeatController) {
|
|
242
|
+
heartbeatController.abort();
|
|
243
|
+
heartbeatController = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function runnerHeartbeatErrorCode(error) {
|
|
248
|
+
const code = error && typeof error.code === 'string' ? error.code : '';
|
|
249
|
+
if (/^(?:E[A-Z0-9_]+|ERR_[A-Z0-9_]+|UND_ERR_[A-Z0-9_]+)$/.test(code)) return code;
|
|
250
|
+
const name = error && typeof error.name === 'string' ? error.name : '';
|
|
251
|
+
if (name === 'AbortError' || name === 'TimeoutError') return name;
|
|
252
|
+
return 'unknown';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function startRunnerHeartbeatSupervisor() {
|
|
256
|
+
if (heartbeatTimer || heartbeatStopped || settled) return;
|
|
257
|
+
const config = readRunnerHeartbeatConfig();
|
|
258
|
+
if (!config) return;
|
|
259
|
+
let handoff = null;
|
|
260
|
+
try {
|
|
261
|
+
handoff = JSON.parse(fs.readFileSync(runtimeStartedPath, 'utf8'));
|
|
262
|
+
} catch {}
|
|
263
|
+
const handoffAt = Number(handoff && handoff.lastHeartbeatAtMs);
|
|
264
|
+
heartbeatLastSuccessAtMs = Number.isFinite(handoffAt) ? handoffAt : Date.now();
|
|
265
|
+
const cadenceMs = Math.max(250, Math.floor(config.leaseMs / ${RUNTIME_CLAIM_HEARTBEAT_DIVISOR}));
|
|
266
|
+
heartbeatStopBeforeLeaseMs = Math.max(
|
|
267
|
+
0,
|
|
268
|
+
config.leaseMs - cadenceMs - ${RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS},
|
|
269
|
+
);
|
|
270
|
+
process.stderr.write(
|
|
271
|
+
'[play-runner.heartbeat] watchdog_owner_started ' +
|
|
272
|
+
JSON.stringify({ runId: config.runId, cadenceMs, handoffHeartbeatAgeMs: Math.max(0, Date.now() - heartbeatLastSuccessAtMs) }) +
|
|
273
|
+
'\\n',
|
|
274
|
+
);
|
|
275
|
+
void runnerHeartbeatTick();
|
|
276
|
+
heartbeatTimer = setInterval(runnerHeartbeatTick, cadenceMs);
|
|
277
|
+
heartbeatTimer.unref?.();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runnerHeartbeatTick() {
|
|
281
|
+
if (heartbeatStopped || heartbeatInFlight || settled || !child || child.exitCode !== null) return;
|
|
282
|
+
const config = readRunnerHeartbeatConfig();
|
|
283
|
+
if (!config) return;
|
|
284
|
+
heartbeatInFlight = true;
|
|
285
|
+
heartbeatAttempt += 1;
|
|
286
|
+
const startedAt = Date.now();
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
heartbeatController = controller;
|
|
289
|
+
const timeout = setTimeout(() => controller.abort(), ${RUNNER_HEARTBEAT_REQUEST_TIMEOUT_MS});
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(
|
|
292
|
+
config.gatewayBaseUrl + '/api/v2/plays/runtime',
|
|
293
|
+
{
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: {
|
|
296
|
+
'content-type': 'application/json',
|
|
297
|
+
authorization: 'Bearer ' + config.executorToken,
|
|
298
|
+
${JSON.stringify(PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER)}: require('node:crypto').randomUUID(),
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify({ action: 'runner_heartbeat', runId: config.runId }),
|
|
301
|
+
redirect: 'manual',
|
|
302
|
+
signal: controller.signal,
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
if (response.status >= 300 && response.status < 400) {
|
|
306
|
+
await response.body?.cancel().catch(() => undefined);
|
|
307
|
+
throw Object.assign(new Error('gateway_redirect'), { code: 'redirect_' + response.status });
|
|
308
|
+
}
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
await response.body?.cancel().catch(() => undefined);
|
|
311
|
+
throw Object.assign(new Error('gateway_http_error'), { code: 'http_' + response.status });
|
|
312
|
+
}
|
|
313
|
+
const body = await response.json().catch(() => null);
|
|
314
|
+
if (body && body.ok === false && ['cancelled', 'failed', 'completed'].includes(body.terminal)) {
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
'[play-runner.heartbeat] watchdog_owner_terminal ' +
|
|
317
|
+
JSON.stringify({ runId: config.runId, terminal: body.terminal, attempt: heartbeatAttempt }) +
|
|
318
|
+
'\\n',
|
|
319
|
+
);
|
|
320
|
+
stopRunnerHeartbeat();
|
|
321
|
+
stopChild('runner_scheduler_terminal', { terminal: body.terminal, heartbeatAttempt });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!body || body.ok !== true || (body.schedulerStatus !== undefined && !['queued', 'running', 'waiting'].includes(body.schedulerStatus))) {
|
|
325
|
+
throw Object.assign(new Error('gateway_protocol_error'), { code: 'protocol_error' });
|
|
326
|
+
}
|
|
327
|
+
heartbeatLastSuccessAtMs = Date.now();
|
|
328
|
+
heartbeatLastErrorCode = null;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (heartbeatStopped || settled) return;
|
|
331
|
+
heartbeatLastErrorCode = controller.signal.aborted
|
|
332
|
+
? 'TimeoutError'
|
|
333
|
+
: runnerHeartbeatErrorCode(error);
|
|
334
|
+
const heartbeatAgeMs = Math.max(0, Date.now() - heartbeatLastSuccessAtMs);
|
|
335
|
+
process.stderr.write(
|
|
336
|
+
'[play-runner.heartbeat] watchdog_owner_retrying ' +
|
|
337
|
+
JSON.stringify({ runId: config.runId, attempt: heartbeatAttempt, durationMs: Math.max(0, Date.now() - startedAt), heartbeatAgeMs, errorCode: heartbeatLastErrorCode }) +
|
|
338
|
+
'\\n',
|
|
339
|
+
);
|
|
340
|
+
// Stop before the scheduler's lease expires. A failed request can consume
|
|
341
|
+
// its full timeout, and the next cadence would otherwise let the scheduler
|
|
342
|
+
// win the terminal race and report an ambiguous runner loss.
|
|
343
|
+
if (heartbeatAgeMs >= heartbeatStopBeforeLeaseMs) {
|
|
344
|
+
stopRunnerHeartbeat();
|
|
345
|
+
const diagnostic = {
|
|
346
|
+
heartbeatAttempt,
|
|
347
|
+
heartbeatAgeMs,
|
|
348
|
+
leaseMs: config.leaseMs,
|
|
349
|
+
stopBeforeLeaseMs: heartbeatStopBeforeLeaseMs,
|
|
350
|
+
lastErrorCode: heartbeatLastErrorCode,
|
|
351
|
+
};
|
|
352
|
+
process.stderr.write(
|
|
353
|
+
'RUNTIME_CONTROL_PLANE_UNAVAILABLE: ' + JSON.stringify(diagnostic) + '\\n',
|
|
354
|
+
);
|
|
355
|
+
stopChild('runner_heartbeat_unreachable', diagnostic);
|
|
356
|
+
}
|
|
357
|
+
} finally {
|
|
358
|
+
clearTimeout(timeout);
|
|
359
|
+
if (heartbeatController === controller) heartbeatController = null;
|
|
360
|
+
heartbeatInFlight = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
188
363
|
|
|
189
364
|
function writeTerminationDiagnostic(reason, details = {}) {
|
|
190
365
|
if (!terminationDiagnosticPath) return;
|
|
@@ -204,6 +379,8 @@ function writeTerminationDiagnostic(reason, details = {}) {
|
|
|
204
379
|
}
|
|
205
380
|
|
|
206
381
|
function stopChild(reason, details) {
|
|
382
|
+
if (settled || stopRequested) return;
|
|
383
|
+
stopRequested = true;
|
|
207
384
|
writeTerminationDiagnostic(reason, details);
|
|
208
385
|
child.kill('SIGTERM');
|
|
209
386
|
forceKillTimer = setTimeout(() => child.kill('SIGKILL'), ${RUNNER_TERMINATION_KILL_GRACE_MS});
|
|
@@ -391,7 +568,10 @@ function armTerminalGrace() {
|
|
|
391
568
|
}
|
|
392
569
|
|
|
393
570
|
const runtimeBoundaryPoll = setInterval(() => {
|
|
394
|
-
if (fs.existsSync(runtimeStartedPath))
|
|
571
|
+
if (fs.existsSync(runtimeStartedPath)) {
|
|
572
|
+
armRuntimeDeadline();
|
|
573
|
+
startRunnerHeartbeatSupervisor();
|
|
574
|
+
}
|
|
395
575
|
if (fs.existsSync(runtimeCompletedPath)) armTerminalGrace();
|
|
396
576
|
}, 25);
|
|
397
577
|
const startupTimer = setTimeout(() => {
|
|
@@ -413,6 +593,7 @@ function finish(code) {
|
|
|
413
593
|
settled = true;
|
|
414
594
|
clearInterval(runtimeBoundaryPoll);
|
|
415
595
|
clearInterval(memoryWatch);
|
|
596
|
+
stopRunnerHeartbeat();
|
|
416
597
|
clearTimeout(startupTimer);
|
|
417
598
|
if (runtimeTimer) clearTimeout(runtimeTimer);
|
|
418
599
|
if (terminalGraceTimer) clearTimeout(terminalGraceTimer);
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts
CHANGED
|
@@ -60,9 +60,13 @@ const CONCURRENCY_OBSERVATION_DEDUP_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
|
60
60
|
const CONCURRENCY_OBSERVER_CLEANUP_BATCH_SIZE = 128;
|
|
61
61
|
const ACTIVE_LEASE_TTL_MS = 5 * 60 * 1000;
|
|
62
62
|
const RATE_RESERVATION_TTL_MS = 5 * 60 * 1000;
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const MIN_ADAPTIVE_RPS_FLOOR =
|
|
63
|
+
// Known provider estimates should not strand later runs below five requests
|
|
64
|
+
// per minute. Explicit adaptive ceilings remain authoritative.
|
|
65
|
+
const MIN_ADAPTIVE_RPS_FLOOR = 5 / 60;
|
|
66
|
+
// Repeated 429s with no successful work back off to at most one probe/minute.
|
|
67
|
+
const MIN_NO_PROGRESS_PROBE_RPS = 1 / 60;
|
|
68
|
+
// Used only to keep wait-time division finite for persisted legacy state.
|
|
69
|
+
const MIN_SCHEDULE_RPS = 1 / 86_400;
|
|
66
70
|
/**
|
|
67
71
|
* Horizon a granted=0 caller is allowed to sleep toward a WHOLE block instead of
|
|
68
72
|
* a single token. Waiting for one token means one permit per store round trip,
|
|
@@ -427,8 +431,8 @@ function normalizeRules(
|
|
|
427
431
|
* per second; `capacity` is one window of burst. The adaptive `[min,max]` range
|
|
428
432
|
* is derived value-first so the same wire shape drives both declared and
|
|
429
433
|
* default-pacing buckets:
|
|
430
|
-
* - min_rps = min(
|
|
431
|
-
* (
|
|
434
|
+
* - min_rps = min(5/60, max)
|
|
435
|
+
* (a lower explicit adaptive maximum remains authoritative)
|
|
432
436
|
* - max_rps = max(base, 100) (catalogued-rate recommendation)
|
|
433
437
|
* - max_rps = explicit adaptiveMaxRps when supplied
|
|
434
438
|
*/
|
|
@@ -446,17 +450,16 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
|
|
|
446
450
|
// Preserve its fractional refill rate, with dispatch times resolved in ms.
|
|
447
451
|
const capacity = Math.max(1, rule.requestsPerWindow);
|
|
448
452
|
const base = (rule.requestsPerWindow / rule.windowMs) * 1000;
|
|
449
|
-
// Undeclared providers use the shared fallback rate as their operating
|
|
450
|
-
// floor. Explicit provider Retry-After deadlines still block dispatch; a
|
|
451
|
-
// prior learned AIMD rate must not leave later runs paced below the fallback.
|
|
452
|
-
const minRps =
|
|
453
|
-
rule.ruleId === 'default:provider-scope'
|
|
454
|
-
? base
|
|
455
|
-
: Math.min(base, MIN_ADAPTIVE_RPS_FLOOR);
|
|
456
453
|
const maxRps =
|
|
457
454
|
rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > 0
|
|
458
455
|
? Math.max(rule.adaptiveMaxRps, base)
|
|
459
456
|
: Math.max(base, DEFAULT_ADVISORY_PROVIDER_MAX_RPS);
|
|
457
|
+
// Undeclared providers keep their existing 10-RPS fallback. Other provider
|
|
458
|
+
// scopes use the five-RPM floor, capped by an explicit adaptive maximum.
|
|
459
|
+
const minRps =
|
|
460
|
+
rule.ruleId === 'default:provider-scope'
|
|
461
|
+
? base
|
|
462
|
+
: Math.min(MIN_ADAPTIVE_RPS_FLOOR, maxRps);
|
|
460
463
|
return {
|
|
461
464
|
rule_id: rule.ruleId,
|
|
462
465
|
capacity,
|
|
@@ -467,10 +470,20 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
|
|
|
467
470
|
};
|
|
468
471
|
}
|
|
469
472
|
|
|
473
|
+
function minimumRateRpsForRules(rules: readonly AcquirePacingRule[]): number {
|
|
474
|
+
if (rules.some((rule) => rule.ruleId === 'default:provider-scope')) {
|
|
475
|
+
return DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS;
|
|
476
|
+
}
|
|
477
|
+
const explicitAdaptiveMaxima = rules.flatMap((rule) => {
|
|
478
|
+
if (rule.adaptiveMaxRps == null || rule.adaptiveMaxRps <= 0) return [];
|
|
479
|
+
const base = (rule.requestsPerWindow / rule.windowMs) * 1_000;
|
|
480
|
+
return [Math.max(rule.adaptiveMaxRps, base)];
|
|
481
|
+
});
|
|
482
|
+
return Math.min(MIN_ADAPTIVE_RPS_FLOOR, ...explicitAdaptiveMaxima);
|
|
483
|
+
}
|
|
484
|
+
|
|
470
485
|
function minimumRateSqlForRules(rules: readonly AcquirePacingRule[]): string {
|
|
471
|
-
return rules
|
|
472
|
-
? `${DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS}::float8`
|
|
473
|
-
: 'NULL::float8';
|
|
486
|
+
return `${minimumRateRpsForRules(rules)}::float8`;
|
|
474
487
|
}
|
|
475
488
|
|
|
476
489
|
function positiveInt(value: number, label: string): number {
|
|
@@ -1053,7 +1066,7 @@ function fairShareSql(
|
|
|
1053
1066
|
r.refill_per_sec,
|
|
1054
1067
|
GREATEST(
|
|
1055
1068
|
${ADAPTIVE_BURST_HORIZON_MS},
|
|
1056
|
-
CEIL(r.effective_capacity / GREATEST(r.refill_per_sec, ${
|
|
1069
|
+
CEIL(r.effective_capacity / GREATEST(r.refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000)
|
|
1057
1070
|
)::bigint AS window_ms
|
|
1058
1071
|
FROM refreshed r
|
|
1059
1072
|
ORDER BY r.refill_per_sec ASC, r.effective_capacity ASC
|
|
@@ -1453,14 +1466,14 @@ async function acquirePureRate(
|
|
|
1453
1466
|
scheduled_capacity
|
|
1454
1467
|
/ GREATEST(
|
|
1455
1468
|
next_refill_per_sec,
|
|
1456
|
-
${
|
|
1469
|
+
${MIN_SCHEDULE_RPS}
|
|
1457
1470
|
)
|
|
1458
1471
|
* 1000
|
|
1459
1472
|
)
|
|
1460
1473
|
)::bigint
|
|
1461
1474
|
)
|
|
1462
1475
|
)::bigint AS first_at_ms,
|
|
1463
|
-
CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${
|
|
1476
|
+
CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_SCHEDULE_RPS})))::bigint AS spacing_ms,
|
|
1464
1477
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
1465
1478
|
FROM refreshed
|
|
1466
1479
|
),
|
|
@@ -1614,7 +1627,7 @@ async function acquirePureRate(
|
|
|
1614
1627
|
FLOOR(new_refill_per_sec * ${BLOCK_WAIT_HORIZON_MS} / 1000.0)
|
|
1615
1628
|
)
|
|
1616
1629
|
) - tokens_after_refill
|
|
1617
|
-
) / GREATEST(new_refill_per_sec, ${
|
|
1630
|
+
) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000
|
|
1618
1631
|
)
|
|
1619
1632
|
)::bigint
|
|
1620
1633
|
FROM updated
|
|
@@ -1973,14 +1986,14 @@ async function acquireWithConcurrency(
|
|
|
1973
1986
|
scheduled_capacity
|
|
1974
1987
|
/ GREATEST(
|
|
1975
1988
|
next_refill_per_sec,
|
|
1976
|
-
${
|
|
1989
|
+
${MIN_SCHEDULE_RPS}
|
|
1977
1990
|
)
|
|
1978
1991
|
* 1000
|
|
1979
1992
|
)
|
|
1980
1993
|
)::bigint
|
|
1981
1994
|
)
|
|
1982
1995
|
)::bigint AS first_at_ms,
|
|
1983
|
-
CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${
|
|
1996
|
+
CEIL(MAX(1000.0 / GREATEST(next_refill_per_sec, ${MIN_SCHEDULE_RPS})))::bigint AS spacing_ms,
|
|
1984
1997
|
GREATEST(1, FLOOR(MIN(next_refill_per_sec) / 100.0))::bigint AS burst_size
|
|
1985
1998
|
FROM refreshed
|
|
1986
1999
|
),
|
|
@@ -2162,7 +2175,7 @@ async function acquireWithConcurrency(
|
|
|
2162
2175
|
WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
|
|
2163
2176
|
ELSE (
|
|
2164
2177
|
SELECT CEIL(
|
|
2165
|
-
MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, ${
|
|
2178
|
+
MAX((1.0 - tokens_after_refill) / GREATEST(new_refill_per_sec, ${MIN_SCHEDULE_RPS}) * 1000)
|
|
2166
2179
|
)::bigint
|
|
2167
2180
|
FROM updated
|
|
2168
2181
|
)
|
|
@@ -3034,11 +3047,11 @@ async function penalizeRateStateStatements(
|
|
|
3034
3047
|
return emptyRateStatePenaltyTelemetry();
|
|
3035
3048
|
}
|
|
3036
3049
|
const rules = normalizeRules((input.rules ?? []) as AcquirePacingRule[]);
|
|
3037
|
-
const minimumRateRps = rules
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3050
|
+
const minimumRateRps = minimumRateRpsForRules(rules);
|
|
3051
|
+
const noProgressProbeRateRps = Math.min(
|
|
3052
|
+
MIN_NO_PROGRESS_PROBE_RPS,
|
|
3053
|
+
minimumRateRps,
|
|
3054
|
+
);
|
|
3042
3055
|
await applyConcurrencyObservations(
|
|
3043
3056
|
client,
|
|
3044
3057
|
input,
|
|
@@ -3047,8 +3060,10 @@ async function penalizeRateStateStatements(
|
|
|
3047
3060
|
concurrencyObservationBatchFrom(input.concurrencyObservationBatch),
|
|
3048
3061
|
);
|
|
3049
3062
|
// Lock/update the provider-scope hold before touching rate-rule rows, matching
|
|
3050
|
-
// acquire's lock order.
|
|
3051
|
-
//
|
|
3063
|
+
// acquire's lock order. Retry-After remains an uncapped provider deadline;
|
|
3064
|
+
// repeated no-success 429s also extend the separate AIMD cooldown to the
|
|
3065
|
+
// one-minute probe interval so the five-RPM floor cannot turn into a retry
|
|
3066
|
+
// loop.
|
|
3052
3067
|
// - refill_per_sec: multiplicative decrease, but ONLY once per active
|
|
3053
3068
|
// cooldown window (halve-once guard: skip if already cooling), floored at
|
|
3054
3069
|
// min_rps.
|
|
@@ -3105,7 +3120,7 @@ async function penalizeRateStateStatements(
|
|
|
3105
3120
|
- COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
|
|
3106
3121
|
END) AS cycle_ms,
|
|
3107
3122
|
CASE
|
|
3108
|
-
WHEN $5::bigint IS NOT NULL
|
|
3123
|
+
WHEN h.rate_successes <= 0 AND ($5::bigint IS NOT NULL OR h.seen_backpressure) THEN
|
|
3109
3124
|
CASE WHEN h.seen_backpressure THEN
|
|
3110
3125
|
LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
|
|
3111
3126
|
ELSE 1000 END
|
|
@@ -3117,20 +3132,25 @@ async function penalizeRateStateStatements(
|
|
|
3117
3132
|
SELECT e.*,
|
|
3118
3133
|
GREATEST(${minimumRateRps},
|
|
3119
3134
|
CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
|
|
3120
|
-
CASE WHEN
|
|
3135
|
+
CASE WHEN e.no_progress_probe_interval_ms IS NOT NULL
|
|
3121
3136
|
-- A fresh provider deadline is not a throughput measurement.
|
|
3122
3137
|
-- Repeated probes with no successful work are evidence that
|
|
3123
|
-
-- the scope is still unavailable,
|
|
3124
|
-
--
|
|
3125
|
-
--
|
|
3126
|
-
THEN
|
|
3138
|
+
-- the scope is still unavailable, even without Retry-After.
|
|
3139
|
+
-- Space them exponentially (up to one per minute) across all
|
|
3140
|
+
-- runs sharing this provider credential.
|
|
3141
|
+
THEN GREATEST(${noProgressProbeRateRps},
|
|
3142
|
+
1000.0 / e.no_progress_probe_interval_ms)
|
|
3127
3143
|
ELSE e.refill_before END
|
|
3128
3144
|
ELSE e.refill_before / 2 END
|
|
3129
3145
|
) AS next_ceiling_rps
|
|
3130
3146
|
FROM evidence e
|
|
3131
3147
|
), learned AS (
|
|
3132
3148
|
SELECT e.*,
|
|
3133
|
-
CASE WHEN e.new_episode THEN GREATEST(
|
|
3149
|
+
CASE WHEN e.new_episode THEN GREATEST(
|
|
3150
|
+
CASE WHEN e.seen_backpressure AND e.rate_successes = 0
|
|
3151
|
+
THEN ${noProgressProbeRateRps}
|
|
3152
|
+
ELSE ${minimumRateRps} END,
|
|
3153
|
+
LEAST(
|
|
3134
3154
|
e.next_ceiling_rps,
|
|
3135
3155
|
GREATEST(1, e.rate_successes) * 1000.0 / e.cycle_ms
|
|
3136
3156
|
)) ELSE e.refill_before END AS refill_after
|
|
@@ -3140,7 +3160,17 @@ async function penalizeRateStateStatements(
|
|
|
3140
3160
|
retry_at_ms = e.deadline,
|
|
3141
3161
|
refill_per_sec = e.refill_after,
|
|
3142
3162
|
seen_backpressure = e.seen_backpressure OR e.new_episode,
|
|
3143
|
-
|
|
3163
|
+
-- A repeated no-success throttle can impose a slower local probe
|
|
3164
|
+
-- interval than the normal adaptive floor. Keep that pause separate
|
|
3165
|
+
-- from retry_at_ms, which remains the provider's absolute deadline.
|
|
3166
|
+
cool_until_ms = GREATEST(e.cool_until_ms, LEAST(
|
|
3167
|
+
e.now_ms + $4::bigint,
|
|
3168
|
+
GREATEST(
|
|
3169
|
+
e.deadline,
|
|
3170
|
+
e.now_ms + LEAST($2::bigint, $4::bigint),
|
|
3171
|
+
e.now_ms + COALESCE(e.no_progress_probe_interval_ms, 0)
|
|
3172
|
+
)
|
|
3173
|
+
)),
|
|
3144
3174
|
tokens = CASE WHEN e.new_episode THEN 0 ELSE e.tokens END,
|
|
3145
3175
|
rate_successes = e.rate_successes,
|
|
3146
3176
|
rate_throttle_ceiling_rps = CASE WHEN e.new_episode
|
|
@@ -13891,24 +13891,33 @@ export async function upsertPostgresSchedulerObservation(
|
|
|
13891
13891
|
/** Read every observation for a run/attempt, ordered for deterministic folds. */
|
|
13892
13892
|
export async function getPostgresSchedulerObservations(
|
|
13893
13893
|
client: PostgresSchedulerQueryClient,
|
|
13894
|
-
input: {
|
|
13894
|
+
input: {
|
|
13895
|
+
runId: string;
|
|
13896
|
+
attempt?: number | null;
|
|
13897
|
+
/** Return only the newest settled invocation for each node. */
|
|
13898
|
+
latestSettledPerNode?: boolean;
|
|
13899
|
+
},
|
|
13895
13900
|
options: PostgresSchedulerReceiptOptions,
|
|
13896
13901
|
): Promise<DocflowObservationRecord[]> {
|
|
13897
13902
|
const hasAttempt = input.attempt != null;
|
|
13903
|
+
const latestSettledPerNode = input.latestSettledPerNode === true;
|
|
13898
13904
|
const params: unknown[] = [receiptOrgId(options), input.runId];
|
|
13899
13905
|
if (hasAttempt) {
|
|
13900
13906
|
params.push(normalizePostgresSchedulerReceiptRunAttempt(input.attempt));
|
|
13901
13907
|
}
|
|
13902
13908
|
const result = await client.query<ObservationRow>(
|
|
13903
13909
|
`
|
|
13904
|
-
SELECT
|
|
13910
|
+
SELECT ${latestSettledPerNode ? 'DISTINCT ON (node_id)' : ''}
|
|
13911
|
+
run_id, attempt, node_id, invocation_seq, invocation_id, status,
|
|
13905
13912
|
inputs_json, outputs_json, inputs_truncated, outputs_truncated,
|
|
13906
13913
|
error, started_at, settled_at
|
|
13907
13914
|
FROM ${tableName('observations', options)}
|
|
13908
13915
|
WHERE org_id = $1
|
|
13909
13916
|
AND run_id = $2
|
|
13910
13917
|
${hasAttempt ? 'AND attempt = $3' : ''}
|
|
13911
|
-
|
|
13918
|
+
${latestSettledPerNode ? "AND status IN ('completed', 'failed')" : ''}
|
|
13919
|
+
ORDER BY node_id, attempt ${latestSettledPerNode ? 'DESC' : ''},
|
|
13920
|
+
invocation_seq ${latestSettledPerNode ? 'DESC' : ''}
|
|
13912
13921
|
`,
|
|
13913
13922
|
params,
|
|
13914
13923
|
);
|
|
@@ -219,11 +219,14 @@ export function runtimeSheetPageTailHoldCompletedEvent(
|
|
|
219
219
|
tableNamespace: string,
|
|
220
220
|
): PlayRunnerEvent | null {
|
|
221
221
|
if (!Number.isInteger(holdMs) || (holdMs ?? 0) <= 0) return null;
|
|
222
|
+
const at = new Date().toISOString();
|
|
222
223
|
return {
|
|
223
224
|
type: 'log',
|
|
224
|
-
at
|
|
225
|
+
at,
|
|
225
226
|
source: 'play',
|
|
226
|
-
line
|
|
227
|
+
// The status projection keeps the line but drops the event envelope's
|
|
228
|
+
// timestamp, so include the emission time for runtime-preview diagnostics.
|
|
229
|
+
line: `[${at}] [runtime.sheet-page-tail-hold] phase=finish table=${tableNamespace} hold_ms=${holdMs}`,
|
|
227
230
|
};
|
|
228
231
|
}
|
|
229
232
|
|