fix: fall back to hot code queue reads
This commit is contained in:
@@ -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()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user