fix: recover orphaned code queue runs
This commit is contained in:
@@ -14,6 +14,10 @@ interface RuntimeConfig {
|
||||
port: number;
|
||||
databaseUrl: string;
|
||||
logFile: string;
|
||||
deployServiceId: string;
|
||||
deployRepo: string;
|
||||
deployCommit: string;
|
||||
deployRequestedCommit: string;
|
||||
mgrDatabasePoolMax: number;
|
||||
traceDatabasePoolMax: number;
|
||||
defaultProviderId: string;
|
||||
@@ -284,6 +288,10 @@ function configFromEnv(): RuntimeConfig {
|
||||
port: envNumber("PORT", 4278),
|
||||
databaseUrl,
|
||||
logFile: envString("LOG_FILE", "/var/log/unidesk/code-queue-mgr.jsonl"),
|
||||
deployServiceId: envString("UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID", "code-queue-mgr"),
|
||||
deployRepo: envString("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO", ""),
|
||||
deployCommit: envString("UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT", ""),
|
||||
deployRequestedCommit: envString("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT", ""),
|
||||
mgrDatabasePoolMax: Math.max(1, Math.min(2, envNumber("CODE_QUEUE_MGR_DATABASE_POOL_MAX", 2))),
|
||||
traceDatabasePoolMax: Math.max(1, Math.min(2, envNumber("CODE_QUEUE_TRACE_DATABASE_POOL_MAX", 1))),
|
||||
defaultProviderId: envString("CODE_QUEUE_MAIN_PROVIDER_ID", "D601"),
|
||||
@@ -1240,26 +1248,27 @@ async function queueSummary(tasks?: QueueTask[], queueRecords?: QueueRecord[]):
|
||||
counts[task.status] = Number(counts[task.status] ?? 0) + 1;
|
||||
summary.counts = counts;
|
||||
if (terminalTaskUnread(task)) summary.unreadTerminal = Number(summary.unreadTerminal) + 1;
|
||||
if ((task.status === "running" || task.status === "judging") && summary.activeTaskId === null) summary.activeTaskId = task.id;
|
||||
if ((task.status === "queued" || task.status === "retry_wait") && summary.runnableTaskId === null) summary.runnableTaskId = task.id;
|
||||
}
|
||||
const counts = taskRows.reduce<Record<string, number>>((memo, task) => {
|
||||
memo[task.status] = (memo[task.status] ?? 0) + 1;
|
||||
return memo;
|
||||
}, {});
|
||||
const activeTaskIds = taskRows.filter((task) => task.status === "running" || task.status === "judging").map((task) => task.id).sort();
|
||||
const orphanedActiveTaskIds = taskRows.filter((task) => task.status === "running" || task.status === "judging").map((task) => task.id).sort();
|
||||
const queueList = Array.from(summaries.values()).sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
||||
return {
|
||||
total: taskRows.length,
|
||||
defaultQueueId: config.defaultQueueId,
|
||||
queueCount: queueList.length,
|
||||
queues: queueList,
|
||||
activeQueueIds: queueList.filter((queue) => typeof queue.activeTaskId === "string").map((queue) => String(queue.id)),
|
||||
activeQueueIds: [],
|
||||
processingQueueIds: [],
|
||||
activeRunSlotCount: activeTaskIds.length,
|
||||
activeRunSlotCount: 0,
|
||||
activeRunSlotWaiters: [],
|
||||
activeTaskIds,
|
||||
activeTaskId: activeTaskIds[0] ?? null,
|
||||
activeTaskIds: [],
|
||||
activeTaskId: null,
|
||||
orphanedActiveTaskIds,
|
||||
orphanedActiveTaskCount: orphanedActiveTaskIds.length,
|
||||
processing: false,
|
||||
counts,
|
||||
unreadTerminal: taskRows.reduce((total, task) => total + (terminalTaskUnread(task) ? 1 : 0), 0),
|
||||
@@ -2059,6 +2068,12 @@ async function route(req: Request): Promise<Response> {
|
||||
ok: schemaReady,
|
||||
service: "code-queue-mgr",
|
||||
role: "master-control-plane",
|
||||
deploy: {
|
||||
serviceId: config.deployServiceId,
|
||||
repo: config.deployRepo,
|
||||
commit: config.deployCommit,
|
||||
requestedCommit: config.deployRequestedCommit,
|
||||
},
|
||||
startedAt: serviceStartedAt,
|
||||
schemaReady,
|
||||
schemaLastError,
|
||||
|
||||
@@ -3201,6 +3201,7 @@ function taskHasLocalExecutionRecoveryClaim(task: QueueTask): boolean {
|
||||
function queueActiveTasksForRestartRetry(reason: string, method: string, options: { onlyActiveRuns?: boolean } = {}): number {
|
||||
if (!config.schedulerEnabled || !serviceRoleAllowsScheduler(config.serviceRole)) return 0;
|
||||
let recovered = 0;
|
||||
const recoveredTaskIds: string[] = [];
|
||||
for (const task of state.tasks) {
|
||||
if (task.status !== "running" && task.status !== "judging") continue;
|
||||
if (options.onlyActiveRuns === true && !taskHasLocalExecutionRecoveryClaim(task)) continue;
|
||||
@@ -3218,13 +3219,16 @@ function queueActiveTasksForRestartRetry(reason: string, method: string, options
|
||||
task.nextPrompt = null;
|
||||
task.lastError = `Max attempts reached (${maxTaskAttempts}) before restart recovery. ${reason}`;
|
||||
appendOutput(task, "error", `${task.lastError}\n`, method);
|
||||
recoveredTaskIds.push(task.id);
|
||||
continue;
|
||||
}
|
||||
setAttemptFeedbackPrompt(task.attempts.at(-1), task.nextPrompt, "queue-recovery-retry", task.attempts.length + 1);
|
||||
task.updatedAt = nowIso();
|
||||
appendOutput(task, "system", `${reason}; task queued for retry\n`, method);
|
||||
recoveredTaskIds.push(task.id);
|
||||
recovered += 1;
|
||||
}
|
||||
for (const taskId of recoveredTaskIds) markTaskDirty(taskId);
|
||||
if (recovered > 0) armIdleNotification();
|
||||
return recovered;
|
||||
}
|
||||
@@ -4929,13 +4933,18 @@ startMemoryWatchdog();
|
||||
Bun.serve({ hostname: config.host, port: config.port, idleTimeout: 120, fetch: route });
|
||||
logger("info", "service_listening", { port: config.port, instanceId: config.instanceId, role: config.serviceRole, schedulerEnabled: config.schedulerEnabled, schedulerPollIntervalMs: config.schedulerPollIntervalMs, workdir: config.defaultWorkdir, defaultModel: config.defaultModel, judgeConfigured: config.minimaxApiKey.length > 0, storage: "postgres", databaseReady });
|
||||
|
||||
function startDatabaseBackedRuntime(): void {
|
||||
async function startDatabaseBackedRuntime(): Promise<void> {
|
||||
if (serviceReady) return;
|
||||
logger("info", "service_started", { port: config.port, instanceId: config.instanceId, role: config.serviceRole, schedulerEnabled: config.schedulerEnabled, schedulerPollIntervalMs: config.schedulerPollIntervalMs, workdir: config.defaultWorkdir, defaultModel: config.defaultModel, judgeConfigured: config.minimaxApiKey.length > 0, storage: "postgres" });
|
||||
const devReady = collectDevReady() as Record<string, JsonValue>;
|
||||
logger(devReady.ok === true ? "info" : "warn", "dev_ready_check", devReady);
|
||||
const startupRecovered = config.schedulerEnabled ? queueActiveTasksForRestartRetry("Service restarted while task was active", "startup") : 0;
|
||||
if (startupRecovered > 0) logger("warn", "startup_requeued_active_tasks", { recovered: startupRecovered });
|
||||
const startupRecoveryDirtyTaskCount = dirtyDatabaseTaskIds.size;
|
||||
if (startupRecovered > 0 || startupRecoveryDirtyTaskCount > 0) {
|
||||
logger("warn", "startup_requeued_active_tasks", { recovered: startupRecovered, dirtyTaskCount: startupRecoveryDirtyTaskCount });
|
||||
await flushDirtyTasksToDatabase(true);
|
||||
logger("info", "startup_requeued_active_tasks_flushed", { recovered: startupRecovered, dirtyTaskCount: startupRecoveryDirtyTaskCount });
|
||||
}
|
||||
persistState();
|
||||
serviceReady = true;
|
||||
void refreshSchedulerTasksFromDatabase("startup").catch((error) => {
|
||||
|
||||
@@ -375,7 +375,7 @@ function perQueueSummaries(tasks: QueueTask[] = ctx().tasks(), queueRecords: Que
|
||||
for (const [queueId, summary] of summaries) {
|
||||
const activeRun = ctx().activeRuns.get(queueId);
|
||||
const head = ctx().queueHeadTask(queueId, tasks);
|
||||
summary.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null);
|
||||
summary.activeTaskId = activeRun?.taskId ?? null;
|
||||
summary.runnableTaskId = head !== null && ctx().queueTaskIsRunnable(head) ? head.id : null;
|
||||
}
|
||||
const rows = Array.from(summaries.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([queueId, summary]) => {
|
||||
@@ -403,11 +403,12 @@ function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()
|
||||
const unreadTerminal = tasks.reduce((total, task) => total + (terminalTaskUnread(task) ? 1 : 0), 0);
|
||||
const activeRunSlots = ctx().activeRunSlotQueueIds();
|
||||
const activeTaskIdSet = new Set<string>(Array.from(ctx().activeRuns.values()).map((run) => run.taskId));
|
||||
for (const task of tasks) {
|
||||
if (task.status === "running" || task.status === "judging") activeTaskIdSet.add(task.id);
|
||||
}
|
||||
const orphanedActiveTaskIds = tasks
|
||||
.filter((task) => (task.status === "running" || task.status === "judging") && !activeTaskIdSet.has(task.id))
|
||||
.map((task) => task.id)
|
||||
.sort();
|
||||
const activeTaskIds = Array.from(activeTaskIdSet).sort();
|
||||
const activeTaskId = activeTaskIds[0] ?? tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null;
|
||||
const activeTaskId = activeTaskIds[0] ?? null;
|
||||
const queues = perQueueSummaries(tasks, queueRecords);
|
||||
const summary: Record<string, JsonValue> = {
|
||||
total: tasks.length,
|
||||
@@ -420,6 +421,8 @@ function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()
|
||||
activeRunSlotWaiters: ctx().activeRunSlotWaiterSummaries(),
|
||||
activeTaskIds,
|
||||
activeTaskId,
|
||||
orphanedActiveTaskIds,
|
||||
orphanedActiveTaskCount: orphanedActiveTaskIds.length,
|
||||
processing: ctx().processing(),
|
||||
counts,
|
||||
unreadTerminal,
|
||||
@@ -611,7 +614,7 @@ async function queueSummaryForHealth(includeDevReady = true): Promise<JsonValue>
|
||||
for (const [queueId, queue] of summaries) {
|
||||
const activeRun = ctx().activeRuns.get(queueId);
|
||||
const head = ctx().queueHeadTask(queueId, hotTasks);
|
||||
queue.activeTaskId = activeRun?.taskId ?? (head !== null && (head.status === "running" || head.status === "judging") ? head.id : null);
|
||||
queue.activeTaskId = activeRun?.taskId ?? null;
|
||||
queue.runnableTaskId = head !== null && ctx().queueTaskIsRunnable(head) ? head.id : null;
|
||||
}
|
||||
const queues = Array.from(summaries.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([id, queue]) => ({
|
||||
|
||||
Reference in New Issue
Block a user