fix: fall back to hot code queue reads

This commit is contained in:
Codex
2026-05-17 08:34:45 +00:00
parent c93039bf95
commit 48db823e52
2 changed files with 116 additions and 56 deletions
@@ -1634,10 +1634,16 @@ function rememberHotTask(task: QueueTask): QueueTask {
async function findTaskForRead(taskId: string): Promise<QueueTask | null> { async function findTaskForRead(taskId: string): Promise<QueueTask | null> {
const hotTask = findTask(taskId); const hotTask = findTask(taskId);
if (!databaseReady) return hotTask; if (!databaseReady) return hotTask;
const databaseTask = await loadTaskFromDatabase(taskId); try {
if (databaseTask === null) return hotTask; const databaseTask = await loadTaskFromDatabase(taskId);
if (hotTask !== null && (timestampMs(hotTask.updatedAt) ?? 0) > (timestampMs(databaseTask.updatedAt) ?? 0)) return hotTask; if (databaseTask === null) return hotTask;
return databaseTask; if (hotTask !== null && (timestampMs(hotTask.updatedAt) ?? 0) > (timestampMs(databaseTask.updatedAt) ?? 0)) return hotTask;
return databaseTask;
} catch (error) {
databaseLastError = databaseErrorMessage(error);
logger("warn", "read_database_fallback", { taskId, error: errorToJson(error) });
return hotTask;
}
} }
async function findTaskForMutation(taskId: string): Promise<QueueTask | null> { async function findTaskForMutation(taskId: string): Promise<QueueTask | null> {
@@ -82,22 +82,43 @@ function mergeReadTaskByFreshness(target: Map<string, QueueTask>, task: QueueTas
if (existing === undefined || memoryTaskIsNewerThanDatabase(task, existing)) target.set(task.id, task); if (existing === undefined || memoryTaskIsNewerThanDatabase(task, existing)) target.set(task.id, task);
} }
function rememberReadDatabaseError(error: unknown): void {
const message = error instanceof Error ? error.message : String(error);
console.warn(JSON.stringify({
ts: new Date().toISOString(),
service: "code-queue",
level: "warn",
message: "read_database_fallback",
data: { error: message },
}));
}
async function loadFreshMemoryTasksForRead(): Promise<QueueTask[]> { async function loadFreshMemoryTasksForRead(): Promise<QueueTask[]> {
const memoryTasks = ctx().tasks(); const memoryTasks = ctx().tasks();
if (!ctx().databaseReady() || memoryTasks.length === 0) return memoryTasks; if (!ctx().databaseReady() || memoryTasks.length === 0) return memoryTasks;
const databaseTasks = await ctx().loadTasksFromDatabaseByIds(memoryTasks.map((task) => task.id)); try {
const byId = new Map(databaseTasks.map((task) => [task.id, task])); const databaseTasks = await ctx().loadTasksFromDatabaseByIds(memoryTasks.map((task) => task.id));
for (const task of memoryTasks) mergeReadTaskByFreshness(byId, task); const byId = new Map(databaseTasks.map((task) => [task.id, task]));
return Array.from(byId.values()); for (const task of memoryTasks) mergeReadTaskByFreshness(byId, task);
return Array.from(byId.values());
} catch (error) {
rememberReadDatabaseError(error);
return memoryTasks;
}
} }
async function loadTaskForReadByFreshness(taskId: string): Promise<QueueTask | null> { async function loadTaskForReadByFreshness(taskId: string): Promise<QueueTask | null> {
const memoryTask = ctx().tasks().find((task) => task.id === taskId) ?? null; const memoryTask = ctx().tasks().find((task) => task.id === taskId) ?? null;
if (!ctx().databaseReady()) return memoryTask; if (!ctx().databaseReady()) return memoryTask;
const databaseTask = await ctx().loadTaskFromDatabase(taskId); try {
if (databaseTask === null) return memoryTask; const databaseTask = await ctx().loadTaskFromDatabase(taskId);
if (memoryTask !== null && memoryTaskIsNewerThanDatabase(memoryTask, databaseTask)) return memoryTask; if (databaseTask === null) return memoryTask;
return databaseTask; if (memoryTask !== null && memoryTaskIsNewerThanDatabase(memoryTask, databaseTask)) return memoryTask;
return databaseTask;
} catch (error) {
rememberReadDatabaseError(error);
return memoryTask;
}
} }
function queuedStatusPayload(task: QueueTask, tasks?: QueueTask[]): { queuedReason: QueuedStatusReason | null; queuedReasonLabel: string | null } { function queuedStatusPayload(task: QueueTask, tasks?: QueueTask[]): { queuedReason: QueuedStatusReason | null; queuedReasonLabel: string | null } {
@@ -472,13 +493,18 @@ function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()
async function loadAllTasksForRead(): Promise<QueueTask[]> { async function loadAllTasksForRead(): Promise<QueueTask[]> {
if (!ctx().databaseReady()) return ctx().tasks(); if (!ctx().databaseReady()) return ctx().tasks();
const tasks = await ctx().loadTasksFromDatabase("all"); try {
const byId = new Map(tasks.map((task) => [task.id, task])); const tasks = await ctx().loadTasksFromDatabase("all");
for (const active of ctx().tasks()) { const byId = new Map(tasks.map((task) => [task.id, task]));
mergeReadTaskByFreshness(byId, active); for (const active of ctx().tasks()) {
mergeReadTaskByFreshness(byId, active);
}
ctx().runGarbageCollection();
return Array.from(byId.values()).sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id));
} catch (error) {
rememberReadDatabaseError(error);
return ctx().tasks();
} }
ctx().runGarbageCollection();
return Array.from(byId.values()).sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id));
} }
async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask[], queueRecords?: QueueRecord[]): Promise<JsonValue> { async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask[], queueRecords?: QueueRecord[]): Promise<JsonValue> {
@@ -487,40 +513,53 @@ async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask
} }
async function queueSummaryForHealth(includeDevReady = true): Promise<JsonValue> { async function queueSummaryForHealth(includeDevReady = true): Promise<JsonValue> {
const queueRecords = ctx().databaseReady() ? await ctx().loadQueuesFromDatabase() : ctx().queues(); let queueRecords = ctx().queues();
if (ctx().databaseReady()) {
try {
queueRecords = await ctx().loadQueuesFromDatabase();
} catch (error) {
rememberReadDatabaseError(error);
}
}
const hotTasks = await loadFreshMemoryTasksForRead(); const hotTasks = await loadFreshMemoryTasksForRead();
const summary = queueSummary(includeDevReady, hotTasks, queueRecords) as Record<string, JsonValue>; const summary = queueSummary(includeDevReady, hotTasks, queueRecords) as Record<string, JsonValue>;
if (!ctx().databaseReady()) return summary; if (!ctx().databaseReady()) return summary;
const aggregateRows = await ctx().sql<QueueSummaryAggregateRow[]>` let aggregateRows: QueueSummaryAggregateRow[];
SELECT try {
(SELECT COUNT(*) FROM unidesk_code_queue_tasks) AS total, aggregateRows = await ctx().sql<QueueSummaryAggregateRow[]>`
COALESCE(( SELECT
SELECT jsonb_object_agg(status, count) (SELECT COUNT(*) FROM unidesk_code_queue_tasks) AS total,
FROM ( COALESCE((
SELECT status, COUNT(*) AS count SELECT jsonb_object_agg(status, count)
FROM unidesk_code_queue_tasks FROM (
GROUP BY status SELECT status, COUNT(*) AS count
) AS status_counts FROM unidesk_code_queue_tasks
), '{}'::jsonb) AS status_counts, GROUP BY status
COALESCE(( ) AS status_counts
SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'status', status, 'count', count)) ), '{}'::jsonb) AS status_counts,
FROM ( COALESCE((
SELECT queue_id, status, COUNT(*) AS count SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'status', status, 'count', count))
FROM unidesk_code_queue_tasks FROM (
GROUP BY queue_id, status SELECT queue_id, status, COUNT(*) AS count
) AS queue_status_counts FROM unidesk_code_queue_tasks
), '[]'::jsonb) AS queue_status_counts, GROUP BY queue_id, status
COALESCE(( ) AS queue_status_counts
SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'count', count)) ), '[]'::jsonb) AS queue_status_counts,
FROM ( COALESCE((
SELECT queue_id, COUNT(*) AS count SELECT jsonb_agg(jsonb_build_object('queueId', queue_id, 'count', count))
FROM unidesk_code_queue_tasks FROM (
WHERE status IN ('succeeded', 'failed', 'canceled') SELECT queue_id, COUNT(*) AS count
AND read_at IS NULL FROM unidesk_code_queue_tasks
GROUP BY queue_id WHERE status IN ('succeeded', 'failed', 'canceled')
) AS unread_counts AND read_at IS NULL
), '[]'::jsonb) AS unread_counts GROUP BY queue_id
`; ) AS unread_counts
), '[]'::jsonb) AS unread_counts
`;
} catch (error) {
rememberReadDatabaseError(error);
return summary as JsonValue;
}
const aggregate = aggregateRows[0] ?? { total: ctx().tasks().length, status_counts: {}, queue_status_counts: [], unread_counts: [] }; const aggregate = aggregateRows[0] ?? { total: ctx().tasks().length, status_counts: {}, queue_status_counts: [], unread_counts: [] };
const counts: Record<string, number> = {}; const counts: Record<string, number> = {};
for (const [status, count] of Object.entries(aggregate.status_counts ?? {})) counts[status] = Number(count); for (const [status, count] of Object.entries(aggregate.status_counts ?? {})) counts[status] = Number(count);
@@ -1065,12 +1104,21 @@ async function databaseTasksOverviewResponse(url: URL): Promise<Response | null>
const includeStatistics = url.searchParams.get("stats") !== "0"; const includeStatistics = url.searchParams.get("stats") !== "0";
const priorityLimit = parseNamedLimit(url, "priorityLimit", Math.max(100, limit)); const priorityLimit = parseNamedLimit(url, "priorityLimit", Math.max(100, limit));
const beforeId = url.searchParams.get("beforeId"); const beforeId = url.searchParams.get("beforeId");
const [queue, statistics, page, priorityIds] = await Promise.all([ let queue: JsonValue;
queueSummaryForResponse(false), let statistics: JsonValue | null;
includeStatistics ? databaseTaskStatisticsSummary(queueId, url) : Promise.resolve(null), let page: { ids: string[]; total: number; hasMore: boolean; nextBeforeId: string | null };
databasePageTaskIds(queueId, beforeId, limit), let priorityIds: string[];
includeActive ? databasePriorityTaskIds(queueId, priorityLimit) : Promise.resolve([]), try {
]); [queue, statistics, page, priorityIds] = await Promise.all([
queueSummaryForResponse(false),
includeStatistics ? databaseTaskStatisticsSummary(queueId, url) : Promise.resolve(null),
databasePageTaskIds(queueId, beforeId, limit),
includeActive ? databasePriorityTaskIds(queueId, priorityLimit) : Promise.resolve([]),
]);
} catch (error) {
rememberReadDatabaseError(error);
return null;
}
const orderedIds: string[] = []; const orderedIds: string[] = [];
const seenIds = new Set<string>(); const seenIds = new Set<string>();
for (const id of priorityIds) pushUniqueId(orderedIds, seenIds, id); for (const id of priorityIds) pushUniqueId(orderedIds, seenIds, id);
@@ -1079,7 +1127,13 @@ async function databaseTasksOverviewResponse(url: URL): Promise<Response | null>
if (!includeActive || !taskMatchesQueueFilter(task, queueId)) continue; if (!includeActive || !taskMatchesQueueFilter(task, queueId)) continue;
if (terminalTaskUnread(task) || activePriority(task) < 3) pushUniqueId(orderedIds, seenIds, task.id); if (terminalTaskUnread(task) || activePriority(task) < 3) pushUniqueId(orderedIds, seenIds, task.id);
} }
const loadedTasks = await ctx().loadTasksFromDatabaseByIds(orderedIds); let loadedTasks: QueueTask[];
try {
loadedTasks = await ctx().loadTasksFromDatabaseByIds(orderedIds);
} catch (error) {
rememberReadDatabaseError(error);
return null;
}
const byId = new Map<string, QueueTask>(); const byId = new Map<string, QueueTask>();
for (const task of loadedTasks) byId.set(task.id, task); for (const task of loadedTasks) byId.set(task.id, task);
for (const task of ctx().tasks()) { for (const task of ctx().tasks()) {