deepline 0.3.105 → 0.3.107

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.
@@ -0,0 +1,718 @@
1
+ import type { PostgresSchedulerQueryClient } from './postgres';
2
+ import {
3
+ deriveQueueHealth,
4
+ exact,
5
+ lowerBound,
6
+ notApplicable,
7
+ unavailable,
8
+ type QueueDefinition,
9
+ type QueueHealth,
10
+ type QueueObservation,
11
+ } from '../../observability/queue-health';
12
+
13
+ /**
14
+ * Bounded, read-only health projection for the durable runtime queues.
15
+ *
16
+ * This intentionally lives beside the scheduler adapters rather than in the
17
+ * metrics package: Absurd and Runtime Postgres are the sources of truth for
18
+ * these rows. A caller receives one coherent snapshot (the queries run on a
19
+ * single connection) and can publish it to any transport or dashboard.
20
+ */
21
+
22
+ export const RUNTIME_QUEUE_HEALTH_MAX_ABSURD_QUEUES = 128;
23
+ export const RUNTIME_QUEUE_HEALTH_DEFAULT_STALE_AFTER_MS = 90_000;
24
+ export const RUNTIME_QUEUE_HEALTH_DEFAULT_WARN_AGE_MS = 60_000;
25
+ export const RUNTIME_QUEUE_HEALTH_DEFAULT_FAIL_AGE_MS = 300_000;
26
+ /**
27
+ * Queue health is a cheap control-plane read. A bucket may contain more
28
+ * work than this cap; in that case its depth is explicitly reported as a
29
+ * lower bound instead of making the observer scan the whole queue.
30
+ */
31
+ export const RUNTIME_QUEUE_HEALTH_BUCKET_SAMPLE_LIMIT = 1_000;
32
+
33
+ export type RuntimeQueueHealthStatus = 'green' | 'amber' | 'red' | 'unknown';
34
+
35
+ export type RuntimeQueueDepth = {
36
+ ready: number;
37
+ delayed: number;
38
+ inFlight: number;
39
+ blocked: number;
40
+ deadLettered: number;
41
+ };
42
+
43
+ export type RuntimeQueueOldestAge = {
44
+ ready: number | null;
45
+ delayed: number | null;
46
+ inFlight: number | null;
47
+ blocked: number | null;
48
+ deadLettered: number | null;
49
+ };
50
+
51
+ export type RuntimeQueueFlow = {
52
+ admitted: number;
53
+ started: number;
54
+ completed: number;
55
+ failed: number;
56
+ retried: number;
57
+ poisoned: number;
58
+ };
59
+
60
+ export type RuntimeQueueHealthProjection = {
61
+ id: string;
62
+ kind: 'absurd' | 'run_ledger_outbox' | 'sandbox_cleanup';
63
+ source: 'absurd' | 'runtime_postgres';
64
+ observedAt: string;
65
+ status: RuntimeQueueHealthStatus;
66
+ statusReason: string;
67
+ /** Shared scheduler-neutral contract consumed by transports and dashboards. */
68
+ observation: QueueObservation;
69
+ health: QueueHealth;
70
+ depth: RuntimeQueueDepth;
71
+ oldestAgeMs: RuntimeQueueOldestAge;
72
+ flow: RuntimeQueueFlow;
73
+ lifecycle: {
74
+ started: number;
75
+ completed: number;
76
+ unresolved: number;
77
+ triggerGap: number;
78
+ };
79
+ thresholds: {
80
+ warnAgeMs: number;
81
+ failAgeMs: number;
82
+ staleAfterMs: number;
83
+ };
84
+ };
85
+
86
+ export type RuntimeQueueHealthSnapshot = {
87
+ observedAt: string;
88
+ staleAfterMs: number;
89
+ queues: RuntimeQueueHealthProjection[];
90
+ summary: {
91
+ green: number;
92
+ amber: number;
93
+ red: number;
94
+ unknown: number;
95
+ };
96
+ };
97
+
98
+ export type RuntimeQueueHealthOptions = {
99
+ schema?: string;
100
+ now?: Date;
101
+ staleAfterMs?: number;
102
+ warnAgeMs?: number;
103
+ failAgeMs?: number;
104
+ /** Restrict Absurd rows to these queue names. Registry values are validated. */
105
+ absurdQueueNames?: string[];
106
+ maxAbsurdQueues?: number;
107
+ };
108
+
109
+ export type RuntimeQueueHealthThresholds = Required<
110
+ Pick<
111
+ RuntimeQueueHealthOptions,
112
+ 'now' | 'staleAfterMs' | 'warnAgeMs' | 'failAgeMs'
113
+ >
114
+ >;
115
+
116
+ type CountRow = {
117
+ ready: string | number;
118
+ delayed: string | number;
119
+ in_flight: string | number;
120
+ blocked: string | number;
121
+ dead_lettered: string | number;
122
+ oldest_ready_ms: string | number | null;
123
+ oldest_delayed_ms: string | number | null;
124
+ oldest_in_flight_ms: string | number | null;
125
+ oldest_blocked_ms: string | number | null;
126
+ oldest_dead_lettered_ms: string | number | null;
127
+ admitted: string | number;
128
+ started: string | number;
129
+ completed: string | number;
130
+ failed: string | number;
131
+ retried: string | number;
132
+ ready_truncated?: boolean;
133
+ delayed_truncated?: boolean;
134
+ in_flight_truncated?: boolean;
135
+ blocked_truncated?: boolean;
136
+ dead_lettered_truncated?: boolean;
137
+ flow_available?: boolean;
138
+ flow_unavailable_reason?: string | null;
139
+ };
140
+
141
+ function quoteIdentifier(value: string, label: string): string {
142
+ if (!value || Buffer.byteLength(value, 'utf8') > 63) {
143
+ throw new Error(
144
+ `${label} must be a non-empty SQL identifier of at most 63 bytes.`,
145
+ );
146
+ }
147
+ return `"${value.replaceAll('"', '""')}"`;
148
+ }
149
+
150
+ function schemaName(schema?: string): string {
151
+ const value = schema?.trim() || 'runtime_scheduler';
152
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(value)) {
153
+ throw new Error('schema must be a simple SQL identifier.');
154
+ }
155
+ return quoteIdentifier(value, 'schema');
156
+ }
157
+
158
+ function numberValue(value: string | number | null | undefined): number {
159
+ if (value === null || value === undefined) return 0;
160
+ const parsed = typeof value === 'number' ? value : Number(value);
161
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
162
+ }
163
+
164
+ function ageValue(value: string | number | null | undefined): number | null {
165
+ if (value === null || value === undefined) return null;
166
+ const parsed = numberValue(value);
167
+ return parsed > 0 ? Math.floor(parsed) : null;
168
+ }
169
+
170
+ function optionsWithDefaults(
171
+ input: RuntimeQueueHealthOptions = {},
172
+ ): RuntimeQueueHealthThresholds {
173
+ const now = input.now ?? new Date();
174
+ const staleAfterMs =
175
+ input.staleAfterMs ?? RUNTIME_QUEUE_HEALTH_DEFAULT_STALE_AFTER_MS;
176
+ const warnAgeMs = input.warnAgeMs ?? RUNTIME_QUEUE_HEALTH_DEFAULT_WARN_AGE_MS;
177
+ const failAgeMs = input.failAgeMs ?? RUNTIME_QUEUE_HEALTH_DEFAULT_FAIL_AGE_MS;
178
+ if (
179
+ !Number.isFinite(staleAfterMs) ||
180
+ staleAfterMs <= 0 ||
181
+ !Number.isFinite(warnAgeMs) ||
182
+ warnAgeMs <= 0 ||
183
+ !Number.isFinite(failAgeMs) ||
184
+ failAgeMs < warnAgeMs
185
+ ) {
186
+ throw new Error('Queue health age thresholds are invalid.');
187
+ }
188
+ return { now, staleAfterMs, warnAgeMs, failAgeMs };
189
+ }
190
+
191
+ export function deriveRuntimeQueueHealthStatus(input: {
192
+ depth: RuntimeQueueDepth;
193
+ oldestAgeMs: RuntimeQueueOldestAge;
194
+ options: RuntimeQueueHealthThresholds;
195
+ observedAt: Date;
196
+ statusReason?: string;
197
+ }): { status: RuntimeQueueHealthStatus; statusReason: string } {
198
+ if (input.statusReason)
199
+ return { status: 'unknown', statusReason: input.statusReason };
200
+ const age =
201
+ input.oldestAgeMs.ready ??
202
+ input.oldestAgeMs.inFlight ??
203
+ input.oldestAgeMs.delayed;
204
+ const ageForFailure = [
205
+ input.oldestAgeMs.ready,
206
+ input.oldestAgeMs.inFlight,
207
+ input.oldestAgeMs.blocked,
208
+ input.oldestAgeMs.deadLettered,
209
+ ].reduce<number | null>(
210
+ (max, current) =>
211
+ current === null ? max : max === null ? current : Math.max(max, current),
212
+ null,
213
+ );
214
+ const ageForWarning =
215
+ age ?? input.oldestAgeMs.blocked ?? input.oldestAgeMs.deadLettered;
216
+ if (
217
+ input.options.now.getTime() - input.observedAt.getTime() >
218
+ input.options.staleAfterMs
219
+ ) {
220
+ return { status: 'unknown', statusReason: 'observation_stale' };
221
+ }
222
+ if (input.depth.deadLettered > 0) {
223
+ return { status: 'red', statusReason: 'dead_lettered_work' };
224
+ }
225
+ if (input.depth.blocked > 0) {
226
+ return { status: 'red', statusReason: 'blocked_work' };
227
+ }
228
+ if (ageForFailure !== null && ageForFailure >= input.options.failAgeMs) {
229
+ return { status: 'red', statusReason: 'oldest_work_exceeded_failure_slo' };
230
+ }
231
+ if (ageForWarning !== null && ageForWarning >= input.options.warnAgeMs) {
232
+ return { status: 'amber', statusReason: 'oldest_work_approaching_slo' };
233
+ }
234
+ return {
235
+ status: 'green',
236
+ statusReason:
237
+ input.depth.ready + input.depth.delayed + input.depth.inFlight === 0
238
+ ? 'idle'
239
+ : 'within_slo',
240
+ };
241
+ }
242
+
243
+ function projectionFromRow(input: {
244
+ id: string;
245
+ kind: RuntimeQueueHealthProjection['kind'];
246
+ source: RuntimeQueueHealthProjection['source'];
247
+ row: CountRow;
248
+ observedAt: Date;
249
+ options: RuntimeQueueHealthThresholds;
250
+ }): RuntimeQueueHealthProjection {
251
+ const depth: RuntimeQueueDepth = {
252
+ ready: numberValue(input.row.ready),
253
+ delayed: numberValue(input.row.delayed),
254
+ inFlight: numberValue(input.row.in_flight),
255
+ blocked: numberValue(input.row.blocked),
256
+ deadLettered: numberValue(input.row.dead_lettered),
257
+ };
258
+ const oldestAgeMs: RuntimeQueueOldestAge = {
259
+ ready: ageValue(input.row.oldest_ready_ms),
260
+ delayed: ageValue(input.row.oldest_delayed_ms),
261
+ inFlight: ageValue(input.row.oldest_in_flight_ms),
262
+ blocked: ageValue(input.row.oldest_blocked_ms),
263
+ deadLettered: ageValue(input.row.oldest_dead_lettered_ms),
264
+ };
265
+ const lifecycle = {
266
+ started: numberValue(input.row.started),
267
+ completed: numberValue(input.row.completed),
268
+ unresolved: depth.ready + depth.delayed + depth.inFlight + depth.blocked,
269
+ // A trigger gap is the durable work that has not started yet. It is a
270
+ // signal, not a failure: an expected-but-not-started schedule occurrence
271
+ // needs the scheduler's own occurrence ledger to become actionable.
272
+ triggerGap: depth.ready + depth.delayed,
273
+ };
274
+ const derivedStatus = deriveRuntimeQueueHealthStatus({
275
+ depth,
276
+ oldestAgeMs,
277
+ options: input.options,
278
+ observedAt: input.observedAt,
279
+ });
280
+ const hasTruncatedDepth = [
281
+ input.row.ready_truncated,
282
+ input.row.delayed_truncated,
283
+ input.row.in_flight_truncated,
284
+ input.row.blocked_truncated,
285
+ input.row.dead_lettered_truncated,
286
+ ].some(Boolean);
287
+ const derived =
288
+ hasTruncatedDepth && derivedStatus.status === 'green'
289
+ ? {
290
+ status: 'amber' as const,
291
+ statusReason: 'bounded_depth_sample',
292
+ }
293
+ : derivedStatus;
294
+ const definition: QueueDefinition = {
295
+ id: input.id,
296
+ owner: 'runtime',
297
+ purpose:
298
+ input.kind === 'absurd'
299
+ ? 'Run admitted Play runtime tasks to terminal state'
300
+ : input.kind === 'run_ledger_outbox'
301
+ ? 'Deliver causal Run Ledger events to control-plane projections'
302
+ : 'Complete durable sandbox deletion obligations',
303
+ unit: 'items',
304
+ kind: input.kind === 'absurd' ? 'fifo' : 'leased_workset',
305
+ ageSlo: {
306
+ warnMs: input.options.warnAgeMs,
307
+ failMs: input.options.failAgeMs,
308
+ },
309
+ staleAfterMs: input.options.staleAfterMs,
310
+ };
311
+ const measureAge = (value: number | null, truncated?: boolean) =>
312
+ value === null
313
+ ? notApplicable<number>('no work in this bucket')
314
+ : truncated
315
+ ? lowerBound(value)
316
+ : exact(value);
317
+ const measureDepth = (value: number, truncated?: boolean) =>
318
+ truncated ? lowerBound(value) : exact(value);
319
+ const flowAvailable = input.row.flow_available !== false;
320
+ const flowUnavailableReason =
321
+ input.row.flow_unavailable_reason ??
322
+ 'runtime queue health does not include an exact rolling lifecycle projection';
323
+ const observation: QueueObservation = {
324
+ observedAt: input.observedAt.getTime(),
325
+ adapter: `runtime-queue-health/${input.source}`,
326
+ depth: {
327
+ ready: measureDepth(depth.ready, input.row.ready_truncated),
328
+ delayed: measureDepth(depth.delayed, input.row.delayed_truncated),
329
+ inFlight: measureDepth(depth.inFlight, input.row.in_flight_truncated),
330
+ blocked: measureDepth(depth.blocked, input.row.blocked_truncated),
331
+ deadLettered: measureDepth(
332
+ depth.deadLettered,
333
+ input.row.dead_lettered_truncated,
334
+ ),
335
+ },
336
+ oldestAgeMs: {
337
+ ready: measureAge(oldestAgeMs.ready, input.row.ready_truncated),
338
+ delayed: measureAge(oldestAgeMs.delayed, input.row.delayed_truncated),
339
+ inFlight: measureAge(oldestAgeMs.inFlight, input.row.in_flight_truncated),
340
+ blocked: measureAge(oldestAgeMs.blocked, input.row.blocked_truncated),
341
+ deadLettered: measureAge(
342
+ oldestAgeMs.deadLettered,
343
+ input.row.dead_lettered_truncated,
344
+ ),
345
+ },
346
+ flow: {
347
+ admitted: flowAvailable
348
+ ? exact(numberValue(input.row.admitted))
349
+ : unavailable(flowUnavailableReason),
350
+ started: flowAvailable
351
+ ? exact(lifecycle.started)
352
+ : unavailable(flowUnavailableReason),
353
+ completed: flowAvailable
354
+ ? exact(lifecycle.completed)
355
+ : unavailable(flowUnavailableReason),
356
+ failed: flowAvailable
357
+ ? exact(numberValue(input.row.failed))
358
+ : unavailable(flowUnavailableReason),
359
+ retried: flowAvailable
360
+ ? exact(numberValue(input.row.retried))
361
+ : unavailable(flowUnavailableReason),
362
+ poisoned: flowAvailable
363
+ ? exact(depth.deadLettered)
364
+ : unavailable(flowUnavailableReason),
365
+ },
366
+ };
367
+ const health = deriveQueueHealth(
368
+ definition,
369
+ observation,
370
+ input.options.now.getTime(),
371
+ );
372
+ return {
373
+ id: input.id,
374
+ kind: input.kind,
375
+ source: input.source,
376
+ observedAt: input.observedAt.toISOString(),
377
+ status: derived.status,
378
+ statusReason: derived.statusReason,
379
+ observation,
380
+ health,
381
+ depth,
382
+ oldestAgeMs,
383
+ flow: {
384
+ admitted: numberValue(input.row.admitted),
385
+ started: lifecycle.started,
386
+ completed: lifecycle.completed,
387
+ failed: numberValue(input.row.failed),
388
+ retried: numberValue(input.row.retried),
389
+ poisoned: depth.deadLettered,
390
+ },
391
+ lifecycle,
392
+ thresholds: {
393
+ warnAgeMs: input.options.warnAgeMs,
394
+ failAgeMs: input.options.failAgeMs,
395
+ staleAfterMs: input.options.staleAfterMs,
396
+ },
397
+ };
398
+ }
399
+
400
+ function intervalExpr(column: string, nowParameter = '$1'): string {
401
+ return `GREATEST(0, floor(extract(epoch FROM (${nowParameter}::timestamptz - ${column})) * 1000))`;
402
+ }
403
+
404
+ function absurdProjectionQuery(queueTable: string): string {
405
+ const table = `absurd.${quoteIdentifier(queueTable, 'queue table')}`;
406
+ return `
407
+ WITH sampled AS (
408
+ (SELECT 'ready'::text AS bucket, available_at AS age_at
409
+ FROM ${table}
410
+ WHERE state = 'pending' AND available_at <= $1::timestamptz
411
+ ORDER BY available_at, run_id
412
+ LIMIT $3)
413
+ UNION ALL
414
+ (SELECT 'delayed'::text, available_at
415
+ FROM ${table}
416
+ WHERE state = 'sleeping'
417
+ ORDER BY available_at, run_id
418
+ LIMIT $3)
419
+ UNION ALL
420
+ (SELECT 'delayed'::text, available_at
421
+ FROM ${table}
422
+ WHERE state = 'pending' AND available_at > $1::timestamptz
423
+ ORDER BY available_at, run_id
424
+ LIMIT $3)
425
+ UNION ALL
426
+ (SELECT 'in_flight'::text, coalesce(started_at, created_at)
427
+ FROM ${table}
428
+ WHERE state = 'running'
429
+ AND (claim_expires_at IS NULL OR claim_expires_at > $1::timestamptz)
430
+ ORDER BY available_at, run_id
431
+ LIMIT $3)
432
+ UNION ALL
433
+ (SELECT 'blocked'::text, coalesce(claim_expires_at, started_at, created_at)
434
+ FROM ${table}
435
+ WHERE state = 'running'
436
+ AND claim_expires_at IS NOT NULL
437
+ AND claim_expires_at <= $1::timestamptz
438
+ ORDER BY claim_expires_at, run_id
439
+ LIMIT $3)
440
+ UNION ALL
441
+ (SELECT 'dead_lettered'::text, coalesce(failed_at, created_at)
442
+ FROM ${table}
443
+ WHERE state = 'failed'
444
+ ORDER BY available_at, run_id
445
+ LIMIT $3)
446
+ )
447
+ SELECT
448
+ least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
449
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
450
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
451
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
452
+ least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $4) AS dead_lettered,
453
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
454
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
455
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
456
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
457
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'dead_lettered') AS oldest_dead_lettered_ms,
458
+ count(*) FILTER (WHERE bucket = 'ready') > $4 AS ready_truncated,
459
+ count(*) FILTER (WHERE bucket = 'delayed') > $4 AS delayed_truncated,
460
+ count(*) FILTER (WHERE bucket = 'in_flight') > $4 AS in_flight_truncated,
461
+ count(*) FILTER (WHERE bucket = 'blocked') > $4 AS blocked_truncated,
462
+ count(*) FILTER (WHERE bucket = 'dead_lettered') > $4 AS dead_lettered_truncated,
463
+ 0::bigint AS admitted,
464
+ 0::bigint AS started,
465
+ 0::bigint AS completed,
466
+ 0::bigint AS failed,
467
+ 0::bigint AS retried,
468
+ false AS flow_available,
469
+ 'Absurd run history has no bounded exact rolling flow projection'::text AS flow_unavailable_reason
470
+ FROM sampled
471
+ `;
472
+ }
473
+
474
+ function outboxProjectionQuery(schema: string): string {
475
+ const table = `${schema}."outbox"`;
476
+ return `
477
+ WITH pending_sample AS (
478
+ SELECT
479
+ CASE
480
+ WHEN lease_owner IS NULL AND coalesce(next_attempt_at, created_at) <= $1::timestamptz THEN 'ready'
481
+ WHEN lease_owner IS NULL THEN 'delayed'
482
+ WHEN lease_expires_at > $1::timestamptz THEN 'in_flight'
483
+ ELSE 'blocked'
484
+ END AS bucket,
485
+ CASE
486
+ WHEN lease_owner IS NULL AND coalesce(next_attempt_at, created_at) > $1::timestamptz
487
+ THEN coalesce(next_attempt_at, created_at)
488
+ WHEN lease_owner IS NULL THEN created_at
489
+ WHEN lease_expires_at > $1::timestamptz
490
+ THEN coalesce(delivery_batch_started_at, created_at)
491
+ ELSE coalesce(lease_expires_at, created_at)
492
+ END AS age_at
493
+ FROM ${table}
494
+ WHERE delivered_at IS NULL AND failed_at IS NULL
495
+ ORDER BY coalesce(next_attempt_at, created_at), created_at, event_id
496
+ LIMIT $3
497
+ ), dead_sample AS (
498
+ SELECT 'dead_lettered'::text AS bucket, coalesce(failed_at, created_at) AS age_at
499
+ FROM ${table}
500
+ WHERE delivered_at IS NULL AND failed_at IS NOT NULL
501
+ ORDER BY failed_at, event_id
502
+ LIMIT $3
503
+ ), sampled AS (
504
+ SELECT * FROM pending_sample
505
+ UNION ALL
506
+ SELECT * FROM dead_sample
507
+ )
508
+ SELECT
509
+ least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
510
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
511
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
512
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
513
+ least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $4) AS dead_lettered,
514
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
515
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
516
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
517
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
518
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'dead_lettered') AS oldest_dead_lettered_ms,
519
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS ready_truncated,
520
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS delayed_truncated,
521
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS in_flight_truncated,
522
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS blocked_truncated,
523
+ count(*) FILTER (WHERE bucket = 'dead_lettered') > $4 AS dead_lettered_truncated,
524
+ 0::bigint AS admitted,
525
+ 0::bigint AS started,
526
+ 0::bigint AS completed,
527
+ 0::bigint AS failed,
528
+ 0::bigint AS retried,
529
+ false AS flow_available,
530
+ 'Outbox health has no bounded exact rolling flow projection'::text AS flow_unavailable_reason
531
+ FROM sampled
532
+ `;
533
+ }
534
+
535
+ function cleanupProjectionQuery(schema: string): string {
536
+ const table = `${schema}."sandbox_cleanup_jobs_v2"`;
537
+ return `
538
+ WITH sampled AS (
539
+ SELECT
540
+ CASE
541
+ WHEN blocked_at IS NOT NULL THEN 'blocked'
542
+ WHEN lease_owner IS NOT NULL AND lease_expires_at > $1::timestamptz THEN 'in_flight'
543
+ WHEN lease_owner IS NOT NULL OR status = 'deleting' THEN 'blocked'
544
+ WHEN available_at > $1::timestamptz THEN 'delayed'
545
+ ELSE 'ready'
546
+ END AS bucket,
547
+ CASE
548
+ WHEN blocked_at IS NOT NULL THEN blocked_at
549
+ WHEN lease_owner IS NOT NULL AND lease_expires_at > $1::timestamptz
550
+ THEN coalesce(updated_at, created_at)
551
+ WHEN lease_owner IS NOT NULL OR status = 'deleting'
552
+ THEN coalesce(lease_expires_at, created_at)
553
+ WHEN available_at > $1::timestamptz THEN available_at
554
+ ELSE created_at
555
+ END AS age_at
556
+ FROM ${table}
557
+ WHERE status <> 'deleted'
558
+ ORDER BY available_at, created_at, provider, sandbox_id
559
+ LIMIT $3
560
+ )
561
+ SELECT
562
+ least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
563
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
564
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
565
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
566
+ 0::bigint AS dead_lettered,
567
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
568
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
569
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
570
+ max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
571
+ NULL::bigint AS oldest_dead_lettered_ms,
572
+ count(*) > $4 AS ready_truncated,
573
+ count(*) > $4 AS delayed_truncated,
574
+ count(*) > $4 AS in_flight_truncated,
575
+ count(*) > $4 AS blocked_truncated,
576
+ false AS dead_lettered_truncated,
577
+ 0::bigint AS admitted,
578
+ 0::bigint AS started,
579
+ 0::bigint AS completed,
580
+ 0::bigint AS failed,
581
+ 0::bigint AS retried,
582
+ false AS flow_available,
583
+ 'Sandbox cleanup health has no bounded exact rolling flow projection'::text AS flow_unavailable_reason
584
+ FROM sampled
585
+ `;
586
+ }
587
+
588
+ function rowFromResult(result: { rows: CountRow[] }): CountRow {
589
+ return (
590
+ result.rows[0] ?? {
591
+ ready: 0,
592
+ delayed: 0,
593
+ in_flight: 0,
594
+ blocked: 0,
595
+ dead_lettered: 0,
596
+ oldest_ready_ms: null,
597
+ oldest_delayed_ms: null,
598
+ oldest_in_flight_ms: null,
599
+ oldest_blocked_ms: null,
600
+ oldest_dead_lettered_ms: null,
601
+ admitted: 0,
602
+ started: 0,
603
+ completed: 0,
604
+ failed: 0,
605
+ retried: 0,
606
+ }
607
+ );
608
+ }
609
+
610
+ /** Read all bounded runtime queue projections on one scheduler connection. */
611
+ export async function readRuntimeQueueHealthFromClient(
612
+ client: PostgresSchedulerQueryClient,
613
+ input: RuntimeQueueHealthOptions = {},
614
+ ): Promise<RuntimeQueueHealthSnapshot> {
615
+ const options = optionsWithDefaults(input);
616
+ const now = options.now;
617
+ const schema = schemaName(input.schema);
618
+ const sampleLimit = RUNTIME_QUEUE_HEALTH_BUCKET_SAMPLE_LIMIT;
619
+ const maxQueues = Math.max(
620
+ 1,
621
+ Math.min(
622
+ input.maxAbsurdQueues ?? RUNTIME_QUEUE_HEALTH_MAX_ABSURD_QUEUES,
623
+ RUNTIME_QUEUE_HEALTH_MAX_ABSURD_QUEUES,
624
+ ),
625
+ );
626
+ const requestedNames = input.absurdQueueNames?.length
627
+ ? [
628
+ ...new Set(
629
+ input.absurdQueueNames.map((name) => name.trim()).filter(Boolean),
630
+ ),
631
+ ]
632
+ : undefined;
633
+ if (requestedNames && requestedNames.length > maxQueues) {
634
+ throw new Error(
635
+ `Requested ${requestedNames.length} Absurd queues, exceeding the ${maxQueues} queue health limit.`,
636
+ );
637
+ }
638
+ const registry = requestedNames
639
+ ? await client.query<{ queue_name: string }>(
640
+ `SELECT queue_name FROM absurd.queues WHERE queue_name = ANY($1::text[]) ORDER BY queue_name LIMIT $2`,
641
+ [requestedNames, maxQueues],
642
+ )
643
+ : await client.query<{ queue_name: string }>(
644
+ `SELECT queue_name FROM absurd.queues ORDER BY queue_name LIMIT $1`,
645
+ [maxQueues],
646
+ );
647
+ const queueNames = registry.rows.map((row) => row.queue_name);
648
+ if (requestedNames) {
649
+ const found = new Set(queueNames);
650
+ const missing = requestedNames.filter((name) => !found.has(name));
651
+ if (missing.length > 0) {
652
+ throw new Error(
653
+ `Requested Absurd queues are not registered: ${missing.join(', ')}`,
654
+ );
655
+ }
656
+ }
657
+ const projections: RuntimeQueueHealthProjection[] = [];
658
+ for (const queueName of queueNames) {
659
+ const result = await client.query<CountRow>(
660
+ absurdProjectionQuery(`r_${queueName}`),
661
+ [now, now, sampleLimit + 1, sampleLimit],
662
+ );
663
+ projections.push(
664
+ projectionFromRow({
665
+ id: `absurd:${queueName}`,
666
+ kind: 'absurd',
667
+ source: 'absurd',
668
+ row: rowFromResult(result),
669
+ observedAt: now,
670
+ options,
671
+ }),
672
+ );
673
+ }
674
+ const [outbox, cleanup] = await Promise.all([
675
+ client.query<CountRow>(outboxProjectionQuery(schema), [
676
+ now,
677
+ now,
678
+ sampleLimit + 1,
679
+ sampleLimit,
680
+ ]),
681
+ client.query<CountRow>(cleanupProjectionQuery(schema), [
682
+ now,
683
+ now,
684
+ sampleLimit + 1,
685
+ sampleLimit,
686
+ ]),
687
+ ]);
688
+ projections.push(
689
+ projectionFromRow({
690
+ id: 'runtime:run-ledger-outbox',
691
+ kind: 'run_ledger_outbox',
692
+ source: 'runtime_postgres',
693
+ row: rowFromResult(outbox),
694
+ observedAt: now,
695
+ options,
696
+ }),
697
+ projectionFromRow({
698
+ id: 'runtime:sandbox-cleanup-jobs-v2',
699
+ kind: 'sandbox_cleanup',
700
+ source: 'runtime_postgres',
701
+ row: rowFromResult(cleanup),
702
+ observedAt: now,
703
+ options,
704
+ }),
705
+ );
706
+ const summary = {
707
+ green: projections.filter((p) => p.status === 'green').length,
708
+ amber: projections.filter((p) => p.status === 'amber').length,
709
+ red: projections.filter((p) => p.status === 'red').length,
710
+ unknown: projections.filter((p) => p.status === 'unknown').length,
711
+ };
712
+ return {
713
+ observedAt: now.toISOString(),
714
+ staleAfterMs: options.staleAfterMs,
715
+ queues: projections,
716
+ summary,
717
+ };
718
+ }