Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 78 additions & 2 deletions packages/daemon/src/lib/external-events/external-event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TERMINAL_DELIVERY_STATES,
TERMINAL_EVENT_STATES,
} from './types';
import type { DeliveryTerminalEvent, QueueAgeStats } from './queue-health-metrics';
import { validateLiteralTopic, validateSource } from './topic-validator';

interface ExternalEventRow {
Expand Down Expand Up @@ -90,6 +91,20 @@ export class ExternalEventValidationError extends Error {
export class ExternalEventStore {
constructor(private readonly db: BunDatabase) {}

/**
* Optional hook fired when a delivery row transitions to a terminal state
* (`delivered`, or `failed` via `failure.terminal=true`). Set by the space
* runtime so queue-health metrics can count every delivery outcome from a
* single observation point, regardless of which call path reached the
* transition. Only fired on an actual transition (`changes > 0`).
*/
private deliveryTerminalHook?: (event: DeliveryTerminalEvent) => void;

/** Install the delivery-terminal observation hook. */
setDeliveryTerminalHook(hook: (event: DeliveryTerminalEvent) => void): void {
this.deliveryTerminalHook = hook;
}

// ---------------------------------------------------------------------------
// Source event lifecycle
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -430,14 +445,17 @@ export class ExternalEventStore {
/** Mark the delivery row terminal `delivered`. No-op if already terminal. */
markDeliveryDelivered(eventId: string, deliveryKey: string): void {
const now = Date.now();
this.db
const result = this.db
.prepare(
`UPDATE space_external_event_deliveries
SET state = 'delivered', failure_reason = NULL, delivered_at = ?, updated_at = ?
WHERE event_id = ? AND delivery_key = ?
AND state NOT IN ('delivered', 'failed')`
)
.run(now, now, eventId, deliveryKey);
if (result.changes > 0 && this.deliveryTerminalHook) {
this.deliveryTerminalHook({ eventId, deliveryKey, outcome: 'delivered', reason: null });
}
}

/**
Expand All @@ -452,14 +470,22 @@ export class ExternalEventStore {
markDeliveryFailed(eventId: string, deliveryKey: string, failure: DeliveryFailure): void {
const now = Date.now();
const newState: ExternalEventDeliveryState = failure.terminal ? 'failed' : 'pending';
this.db
const result = this.db
.prepare(
`UPDATE space_external_event_deliveries
SET state = ?, failure_reason = ?, updated_at = ?
WHERE event_id = ? AND delivery_key = ?
AND state NOT IN ('delivered', 'failed')`
)
.run(newState, failure.reason, now, eventId, deliveryKey);
if (failure.terminal && result.changes > 0 && this.deliveryTerminalHook) {
this.deliveryTerminalHook({
eventId,
deliveryKey,
outcome: 'failed',
reason: failure.reason,
});
}
}

/** List delivery rows for an event (for diagnostics and tests). */
Expand Down Expand Up @@ -555,6 +581,56 @@ export class ExternalEventStore {
return rows.map(deliveryRowToRecord);
}

/**
* Summarize DB-persisted `pending` deliveries for the queue-health snapshot
* without materializing every row: count + min/max/avg via SQL aggregates, and
* p95 via a single `LIMIT 1 OFFSET k` lookup over a sorted scan (no per-row JS
* allocation). The anchor is the source event's ingestion time
* (`space_external_events.created_at`), matching the runtime's event-age TTL
* semantics — see `EXTERNAL_EVENT_QUEUE_TTL_MS`. Returns `null` when there are
* no pending deliveries.
*/
summarizePendingDeliveries(now: number = Date.now()): QueueAgeStats | null {
const agg = this.db
.prepare(
`SELECT
COUNT(*) AS count,
MIN(? - e.created_at) AS minMs,
MAX(? - e.created_at) AS maxMs,
AVG(? - e.created_at) AS avgMs
FROM space_external_event_deliveries d
INNER JOIN space_external_events e ON e.id = d.event_id
WHERE d.state = 'pending'`
)
.get(now, now, now) as {
count: number;
minMs: number | null;
maxMs: number | null;
avgMs: number | null;
};
const count = agg?.count ?? 0;
if (count === 0) return null;
// Nearest-rank p95 (no interpolation): the ceil(0.95 * n)th value, clamped.
const p95Offset = Math.min(count - 1, Math.ceil(count * 0.95) - 1);
const p95 = this.db
.prepare(
`SELECT (? - e.created_at) AS age
FROM space_external_event_deliveries d
INNER JOIN space_external_events e ON e.id = d.event_id
WHERE d.state = 'pending'
ORDER BY age
LIMIT 1 OFFSET ?`
)
.get(now, p95Offset) as { age: number } | undefined;
return {
count,
minMs: agg.minMs ?? 0,
maxMs: agg.maxMs ?? 0,
avgMs: Math.round(agg.avgMs ?? 0),
p95Ms: p95?.age ?? agg.maxMs ?? 0,
};
}

getDelivery(eventId: string, deliveryKey: string): ExternalEventDeliveryRecord | null {
const row = this.db
.prepare(
Expand Down
Loading
Loading