feat: split code queue k3s services
This commit is contained in:
@@ -23,6 +23,7 @@ import type {
|
||||
QueueTaskRequest,
|
||||
RunMode,
|
||||
RuntimeConfig,
|
||||
CodeQueueServiceRole,
|
||||
TaskStatus,
|
||||
WorkdirRecord,
|
||||
} from "./types";
|
||||
@@ -266,6 +267,20 @@ function envBool(name: string, fallback: boolean): boolean {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function serviceRoleValue(raw: string): CodeQueueServiceRole {
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (normalized === "read" || normalized === "write" || normalized === "scheduler" || normalized === "combined") return normalized;
|
||||
return "combined";
|
||||
}
|
||||
|
||||
function serviceRoleAllowsWrite(role: CodeQueueServiceRole): boolean {
|
||||
return role === "combined" || role === "write";
|
||||
}
|
||||
|
||||
function serviceRoleAllowsScheduler(role: CodeQueueServiceRole): boolean {
|
||||
return role === "combined" || role === "scheduler";
|
||||
}
|
||||
|
||||
function envList(name: string, fallback: string[]): string[] {
|
||||
const raw = process.env[name];
|
||||
const source = raw === undefined || raw.length === 0 ? fallback.join(",") : raw;
|
||||
@@ -317,6 +332,7 @@ function readConfig(): RuntimeConfig {
|
||||
const defaultWorkdir = envString("CODE_QUEUE_WORKDIR", "/workspace");
|
||||
const devContainerMasterHost = envString("CODE_QUEUE_DEV_CONTAINER_MASTER_HOST", "74.48.78.17");
|
||||
const defaultProviderGatewayProxyHost = `unidesk-provider-gateway-${mainProviderId}`;
|
||||
const serviceRole = serviceRoleValue(envString("CODE_QUEUE_SERVICE_ROLE", "combined"));
|
||||
const executionProviderIds = Array.from(new Set([
|
||||
mainProviderId,
|
||||
...envList("CODE_QUEUE_EXECUTION_PROVIDER_IDS", [devContainerDefaultProviderId]),
|
||||
@@ -328,7 +344,9 @@ function readConfig(): RuntimeConfig {
|
||||
instanceId: envString("CODE_QUEUE_INSTANCE_ID", mainProviderId),
|
||||
deployCommit: envString("CODE_QUEUE_DEPLOY_COMMIT", "unknown"),
|
||||
deployRequestedCommit: envString("CODE_QUEUE_DEPLOY_REQUESTED_COMMIT", ""),
|
||||
schedulerEnabled: envBool("CODE_QUEUE_SCHEDULER_ENABLED", true),
|
||||
serviceRole,
|
||||
schedulerEnabled: envBool("CODE_QUEUE_SCHEDULER_ENABLED", serviceRoleAllowsScheduler(serviceRole)),
|
||||
schedulerPollIntervalMs: Math.max(500, Math.min(30_000, envNumber("CODE_QUEUE_SCHEDULER_POLL_INTERVAL_MS", 2000))),
|
||||
startupOaBackfillEnabled: envBool("CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED", false),
|
||||
outputArchiveDir: envString("CODE_QUEUE_OUTPUT_ARCHIVE_DIR", resolve(dataDir, "output-archive")),
|
||||
logFile: envString("LOG_FILE", "/var/log/unidesk/code-queue.jsonl"),
|
||||
@@ -1298,6 +1316,13 @@ interface DatabaseTaskRow {
|
||||
task_json: unknown;
|
||||
}
|
||||
|
||||
interface DatabaseQueueRow {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: Date | string;
|
||||
updated_at: Date | string;
|
||||
}
|
||||
|
||||
interface DatabaseTaskIdRow {
|
||||
id: string;
|
||||
}
|
||||
@@ -1383,6 +1408,31 @@ async function loadTasksFromDatabase(where: "all" | "hot" = "all"): Promise<Queu
|
||||
return normalizeDatabaseTaskRows(await loadPrunedDatabaseTaskRows(where), where);
|
||||
}
|
||||
|
||||
function databaseQueueRowsToRecords(rows: DatabaseQueueRow[]): QueueRecord[] {
|
||||
const queueMap = new Map<string, QueueRecord>();
|
||||
for (const row of rows) {
|
||||
const id = safeQueueId(row.id);
|
||||
queueMap.set(id, {
|
||||
id,
|
||||
name: safeQueueName(row.name, id),
|
||||
createdAt: taskTimestamp(String(row.created_at)) ?? nowIso(),
|
||||
updatedAt: taskTimestamp(String(row.updated_at)) ?? nowIso(),
|
||||
});
|
||||
}
|
||||
if (!queueMap.has(defaultQueueId)) queueMap.set(defaultQueueId, { id: defaultQueueId, name: defaultQueueId, createdAt: state.updatedAt, updatedAt: state.updatedAt });
|
||||
return Array.from(queueMap.values()).sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
async function loadQueuesFromDatabase(): Promise<QueueRecord[]> {
|
||||
if (!databaseReady) return state.queues;
|
||||
const queueRows = await sql<DatabaseQueueRow[]>`
|
||||
SELECT id, name, created_at, updated_at
|
||||
FROM unidesk_code_queue_queues
|
||||
ORDER BY id ASC
|
||||
`;
|
||||
return databaseQueueRowsToRecords(queueRows);
|
||||
}
|
||||
|
||||
async function loadTasksFromDatabaseByIds(taskIds: string[]): Promise<QueueTask[]> {
|
||||
const ids = Array.from(new Set(taskIds.map((id) => id.trim()).filter(Boolean)));
|
||||
if (ids.length === 0) return [];
|
||||
@@ -1730,22 +1780,13 @@ async function initDatabasePersistence(): Promise<void> {
|
||||
inMemoryOutputRecords: config.maxInMemoryOutputRecords,
|
||||
inMemoryEventRecords: config.maxInMemoryEventRecords,
|
||||
});
|
||||
const queueRows = await sql<Array<{ id: string; name: string; created_at: Date | string; updated_at: Date | string }>>`
|
||||
const queueRows = await sql<DatabaseQueueRow[]>`
|
||||
SELECT id, name, created_at, updated_at
|
||||
FROM unidesk_code_queue_queues
|
||||
ORDER BY id ASC
|
||||
`;
|
||||
runGarbageCollection();
|
||||
const queueMap = new Map<string, QueueRecord>();
|
||||
for (const row of queueRows) {
|
||||
const id = safeQueueId(row.id);
|
||||
queueMap.set(id, {
|
||||
id,
|
||||
name: safeQueueName(row.name, id),
|
||||
createdAt: taskTimestamp(String(row.created_at)) ?? nowIso(),
|
||||
updatedAt: taskTimestamp(String(row.updated_at)) ?? nowIso(),
|
||||
});
|
||||
}
|
||||
const queueMap = new Map(databaseQueueRowsToRecords(queueRows).map((queue) => [queue.id, queue]));
|
||||
if (!queueMap.has(defaultQueueId)) queueMap.set(defaultQueueId, { id: defaultQueueId, name: defaultQueueId, createdAt: state.updatedAt, updatedAt: state.updatedAt });
|
||||
for (const task of state.tasks) {
|
||||
const id = queueIdOf(task);
|
||||
@@ -1773,7 +1814,7 @@ async function initDatabasePersistence(): Promise<void> {
|
||||
ensureDefaultWorkdirRecords();
|
||||
await upsertWorkdirsToDatabase(sortedWorkdirRecords());
|
||||
databaseReady = true;
|
||||
scheduleStartupDatabaseMaintenance();
|
||||
if (config.serviceRole === "combined" || config.serviceRole === "scheduler") scheduleStartupDatabaseMaintenance();
|
||||
runGarbageCollection();
|
||||
logger("info", "database_persistence_init_complete", {
|
||||
databaseTaskCount: Number(countRows[0]?.count ?? hotTasks.length),
|
||||
@@ -2320,6 +2361,7 @@ configureQueueApi({
|
||||
dirtyDatabaseTaskCount: () => dirtyDatabaseTaskIds.size,
|
||||
jsonResponse,
|
||||
judgeFailRetryLimit,
|
||||
loadQueuesFromDatabase,
|
||||
loadTaskFromDatabase,
|
||||
loadTasksFromDatabase,
|
||||
loadTasksFromDatabaseByIds,
|
||||
@@ -3223,7 +3265,7 @@ function queuedStatusReason(task: QueueTask, tasks: QueueTask[] = state.tasks):
|
||||
if (!serviceReady) {
|
||||
return queuedReason("service", "SERVICE", "Code Queue is still starting and has not enabled scheduling yet.");
|
||||
}
|
||||
if (!config.schedulerEnabled) {
|
||||
if (!config.schedulerEnabled && config.serviceRole !== "read") {
|
||||
return queuedReason("scheduler_disabled", "STANDBY", "This Code Queue instance is a k3s-managed standby and does not start queued work.");
|
||||
}
|
||||
if (mergingQueues.has(queueId)) {
|
||||
@@ -3335,6 +3377,57 @@ function hasRunnableTask(): boolean {
|
||||
return state.tasks.some((task) => task.status === "queued" || task.status === "retry_wait");
|
||||
}
|
||||
|
||||
function shouldPollSchedulerDatabase(): boolean {
|
||||
return config.schedulerEnabled && config.serviceRole === "scheduler";
|
||||
}
|
||||
|
||||
function mergeSchedulerDatabaseTasks(tasks: QueueTask[]): number {
|
||||
let changed = 0;
|
||||
for (const task of tasks) {
|
||||
if (task.status !== "queued" && task.status !== "retry_wait" && task.status !== "running" && task.status !== "judging") continue;
|
||||
const existing = findTask(task.id);
|
||||
if (existing === null) {
|
||||
state.tasks.push(task);
|
||||
changed += 1;
|
||||
continue;
|
||||
}
|
||||
const existingUpdatedAt = timestampMs(existing.updatedAt) ?? 0;
|
||||
const taskUpdatedAt = timestampMs(task.updatedAt) ?? 0;
|
||||
if (taskUpdatedAt > existingUpdatedAt && !activeRunForTask(existing)) {
|
||||
Object.assign(existing, task);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
if (changed > 0) {
|
||||
state.tasks.sort((left, right) => (timestampMs(left.createdAt) ?? 0) - (timestampMs(right.createdAt) ?? 0) || left.id.localeCompare(right.id));
|
||||
updateNextSeqFromTasks();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
async function refreshSchedulerTasksFromDatabase(reason: string): Promise<number> {
|
||||
if (!databaseReady || !config.schedulerEnabled) return 0;
|
||||
const tasks = await loadTasksFromDatabase("hot");
|
||||
const changed = mergeSchedulerDatabaseTasks(tasks);
|
||||
if (changed > 0) {
|
||||
logger("info", "scheduler_database_hot_tasks_refreshed", { reason, changed, loaded: tasks.length });
|
||||
scheduleQueue();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function startSchedulerDatabasePoller(): void {
|
||||
if (!shouldPollSchedulerDatabase()) return;
|
||||
const interval = setInterval(() => {
|
||||
if (!serviceReady || shutdownRequested) return;
|
||||
void refreshSchedulerTasksFromDatabase("poll").catch((error) => {
|
||||
databaseLastError = databaseErrorMessage(error);
|
||||
logger("warn", "scheduler_database_poll_failed", { error: errorToJson(error) });
|
||||
});
|
||||
}, config.schedulerPollIntervalMs);
|
||||
interval.unref?.();
|
||||
}
|
||||
|
||||
function activeRunForTask(task: QueueTask): ActiveRun | null {
|
||||
const queueRun = activeRuns.get(queueIdOf(task));
|
||||
if (queueRun?.taskId === task.id) return queueRun;
|
||||
@@ -3440,6 +3533,27 @@ function requestErrorResponse(error: unknown): Response | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function readOnlyRejectResponse(method: string, targetPath: string): Response {
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
error: "Code Queue read service is read-only",
|
||||
serviceRole: config.serviceRole,
|
||||
method,
|
||||
path: targetPath,
|
||||
}, 405);
|
||||
}
|
||||
|
||||
function schedulerOnlyRejectResponse(method: string, targetPath: string): Response {
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
error: "Code Queue write service does not own active scheduler control for this endpoint",
|
||||
serviceRole: config.serviceRole,
|
||||
method,
|
||||
path: targetPath,
|
||||
hint: "Route active-run control to the code-queue-scheduler service.",
|
||||
}, 409);
|
||||
}
|
||||
|
||||
function findTask(id: string): QueueTask | null {
|
||||
return state.tasks.find((task) => task.id === id) ?? null;
|
||||
}
|
||||
@@ -3520,6 +3634,7 @@ async function deleteWorkdir(providerIdValue: string, executionModeValue: string
|
||||
}
|
||||
|
||||
async function createTasks(req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, "/api/tasks");
|
||||
const body = await readJson(req);
|
||||
const batchRecord = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record<string, unknown> : {};
|
||||
const batchQueueId = typeof batchRecord.queueId === "string" && batchRecord.queueId.trim().length > 0 ? normalizeQueueId(batchRecord.queueId) : undefined;
|
||||
@@ -3545,6 +3660,7 @@ async function createTasks(req: Request): Promise<Response> {
|
||||
}
|
||||
|
||||
async function steerTask(task: QueueTask, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsScheduler(config.serviceRole)) return schedulerOnlyRejectResponse(req.method, `/api/tasks/${task.id}/steer`);
|
||||
const body = await readJson(req);
|
||||
const prompt = typeof (body as Record<string, unknown>).prompt === "string" ? String((body as Record<string, unknown>).prompt) : "";
|
||||
if (prompt.trim().length === 0) return jsonResponse({ ok: false, error: "prompt is required" }, 400);
|
||||
@@ -3560,6 +3676,7 @@ async function steerTask(task: QueueTask, req: Request): Promise<Response> {
|
||||
}
|
||||
|
||||
async function editQueuedTaskPrompt(task: QueueTask, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, `/api/tasks/${task.id}/edit`);
|
||||
if (!queuedTaskPromptEditable(task)) {
|
||||
return jsonResponse({
|
||||
ok: false,
|
||||
@@ -3601,7 +3718,8 @@ async function editQueuedTaskPrompt(task: QueueTask, req: Request): Promise<Resp
|
||||
return jsonResponse({ ok: true, changed: true, editable: true, task: taskForResponse(task), queue: await queueSummaryForResponse() });
|
||||
}
|
||||
|
||||
async function interruptTask(task: QueueTask): Promise<Response> {
|
||||
async function interruptTask(task: QueueTask, method = "POST"): Promise<Response> {
|
||||
if (!serviceRoleAllowsScheduler(config.serviceRole)) return schedulerOnlyRejectResponse(method, `/api/tasks/${task.id}/interrupt`);
|
||||
if (task.status === "succeeded" || task.status === "failed" || task.status === "canceled") {
|
||||
return jsonResponse({ ok: false, error: `task is already terminal: ${task.status}`, task: taskForResponse(task) }, 409);
|
||||
}
|
||||
@@ -3631,6 +3749,7 @@ async function interruptTask(task: QueueTask): Promise<Response> {
|
||||
}
|
||||
|
||||
async function manualRetry(task: QueueTask, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, `/api/tasks/${task.id}/retry`);
|
||||
if (task.status !== "failed" && task.status !== "canceled" && task.status !== "succeeded") {
|
||||
return jsonResponse({ ok: false, error: `task is not terminal: ${task.status}`, task: taskForResponse(task) }, 409);
|
||||
}
|
||||
@@ -3657,6 +3776,7 @@ async function manualRetry(task: QueueTask, req: Request): Promise<Response> {
|
||||
}
|
||||
|
||||
async function markTaskRead(task: QueueTask): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse("POST", `/api/tasks/${task.id}/read`);
|
||||
if (!terminalTask(task)) {
|
||||
return jsonResponse({ ok: false, error: `task is not terminal: ${task.status}`, task: taskForResponse(task) }, 409);
|
||||
}
|
||||
@@ -3678,6 +3798,7 @@ function timestampToIso(value: Date | string | null): string | null {
|
||||
}
|
||||
|
||||
async function markTaskReadById(taskId: string): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse("POST", `/api/tasks/${taskId}/read`);
|
||||
if (!databaseReady) {
|
||||
const task = await findTaskForMutation(taskId);
|
||||
return task === null ? jsonResponse({ ok: false, error: "task not found" }, 404) : markTaskRead(task);
|
||||
@@ -3727,6 +3848,7 @@ async function markTaskReadById(taskId: string): Promise<Response> {
|
||||
}
|
||||
|
||||
async function markTerminalTasksRead(url: URL): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse("POST", "/api/tasks/read-all");
|
||||
const queueFilter = url.searchParams.get("queueId");
|
||||
const queueId = queueFilter === null || queueFilter.length === 0 ? null : safeQueueId(queueFilter);
|
||||
const readAt = nowIso();
|
||||
@@ -3780,6 +3902,7 @@ async function markTerminalTasksRead(url: URL): Promise<Response> {
|
||||
}
|
||||
|
||||
async function createQueue(req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, "/api/queues");
|
||||
const body = await readJson(req);
|
||||
const record = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record<string, unknown> : {};
|
||||
const queueId = normalizeQueueId(record.queueId ?? record.id);
|
||||
@@ -3796,6 +3919,7 @@ async function createQueue(req: Request): Promise<Response> {
|
||||
}
|
||||
|
||||
async function updateQueue(queueIdValue: string, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, `/api/queues/${queueIdValue}`);
|
||||
const queueId = normalizeQueueId(queueIdValue);
|
||||
const queue = state.queues.find((item) => item.id === queueId);
|
||||
if (queue === undefined) return jsonResponse({ ok: false, error: "queue not found" }, 404);
|
||||
@@ -3911,6 +4035,7 @@ function deleteQueuesFromState(queueIds: string[]): QueueRecord[] {
|
||||
}
|
||||
|
||||
async function mergeQueues(targetQueueIdValue: string | null, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, targetQueueIdValue === null ? "/api/queues/merge" : `/api/queues/${targetQueueIdValue}/merge`);
|
||||
const body = await readJson(req);
|
||||
const record = typeof body === "object" && body !== null && !Array.isArray(body) ? body as Record<string, unknown> : {};
|
||||
const targetQueueId = normalizeQueueId(targetQueueIdValue ?? record.targetQueueId ?? record.intoQueueId ?? record.into);
|
||||
@@ -3994,6 +4119,7 @@ async function mergeQueues(targetQueueIdValue: string | null, req: Request): Pro
|
||||
}
|
||||
|
||||
async function moveTaskToQueue(task: QueueTask, req: Request): Promise<Response> {
|
||||
if (!serviceRoleAllowsWrite(config.serviceRole)) return readOnlyRejectResponse(req.method, `/api/tasks/${task.id}/move`);
|
||||
if (task.status === "running" || task.status === "judging") {
|
||||
return jsonResponse({ ok: false, error: `cannot move active task ${task.id} while status=${task.status}`, task: taskForResponse(task) }, 409);
|
||||
}
|
||||
@@ -4055,6 +4181,7 @@ async function route(req: Request): Promise<Response> {
|
||||
ok: true,
|
||||
service: "code-queue",
|
||||
instanceId: config.instanceId,
|
||||
role: config.serviceRole,
|
||||
deploy: {
|
||||
commit: config.deployCommit,
|
||||
requestedCommit: config.deployRequestedCommit,
|
||||
@@ -4068,6 +4195,7 @@ async function route(req: Request): Promise<Response> {
|
||||
ok: false,
|
||||
service: "code-queue",
|
||||
instanceId: config.instanceId,
|
||||
role: config.serviceRole,
|
||||
deploy: {
|
||||
commit: config.deployCommit,
|
||||
requestedCommit: config.deployRequestedCommit,
|
||||
@@ -4081,11 +4209,13 @@ async function route(req: Request): Promise<Response> {
|
||||
ok: true,
|
||||
service: "code-queue",
|
||||
instanceId: config.instanceId,
|
||||
role: config.serviceRole,
|
||||
deploy: {
|
||||
commit: config.deployCommit,
|
||||
requestedCommit: config.deployRequestedCommit,
|
||||
},
|
||||
schedulerEnabled: config.schedulerEnabled,
|
||||
schedulerPollIntervalMs: config.schedulerPollIntervalMs,
|
||||
queue: queueSummary(false, state.tasks),
|
||||
egressProxy: await providerGatewayEgressProxyStatus(),
|
||||
oaEventPublisher: oaEventPublisherStatus(),
|
||||
@@ -4145,7 +4275,8 @@ async function route(req: Request): Promise<Response> {
|
||||
}
|
||||
if (url.pathname === "/api/queues" && req.method === "GET") {
|
||||
const tasks = await loadAllTasksForRead();
|
||||
return jsonResponse({ ok: true, queues: perQueueSummaries(tasks), queue: queueSummary(false, tasks) });
|
||||
const queueRecords = await loadQueuesFromDatabase();
|
||||
return jsonResponse({ ok: true, queues: perQueueSummaries(tasks, queueRecords), queue: await queueSummaryForResponse(false, tasks, queueRecords) });
|
||||
}
|
||||
if (url.pathname === "/api/workdirs" && req.method === "GET") return await listWorkdirs(url);
|
||||
if (url.pathname === "/api/workdirs" && req.method === "POST") return await createWorkdir(req);
|
||||
@@ -4269,7 +4400,7 @@ async function route(req: Request): Promise<Response> {
|
||||
if (task === null) return jsonResponse({ ok: false, error: "task not found" }, 404);
|
||||
if (action === "retry" && req.method === "POST") return await manualRetry(task, req);
|
||||
if (action === "steer" && req.method === "POST") return await steerTask(task, req);
|
||||
if (action === "interrupt" && req.method === "POST") return await interruptTask(task);
|
||||
if (action === "interrupt" && req.method === "POST") return await interruptTask(task, req.method);
|
||||
if (action === "move" && req.method === "POST") return await moveTaskToQueue(task, req);
|
||||
if (action === "edit" && (req.method === "POST" || req.method === "PATCH")) return await editQueuedTaskPrompt(task, req);
|
||||
if (action !== undefined) return jsonResponse({ ok: false, error: "not found" }, 404);
|
||||
@@ -4279,7 +4410,7 @@ async function route(req: Request): Promise<Response> {
|
||||
const includeRaw = url.searchParams.get("raw") === "1" || url.searchParams.get("full") === "1";
|
||||
return jsonResponse({ ok: true, task: applyOaTraceStatsToTaskJson(taskForResponse(task, true, includeRaw), traceStats) });
|
||||
}
|
||||
if (req.method === "DELETE") return await interruptTask(task);
|
||||
if (req.method === "DELETE") return await interruptTask(task, req.method);
|
||||
return jsonResponse({ ok: false, error: "method not allowed" }, 405);
|
||||
}
|
||||
return jsonResponse({ ok: false, error: "not found", path: url.pathname }, 404);
|
||||
@@ -4301,7 +4432,7 @@ startCodexSqliteLogExporter();
|
||||
startMemoryWatchdog();
|
||||
await initDatabasePersistenceWithRetry();
|
||||
Bun.serve({ hostname: config.host, port: config.port, idleTimeout: 120, fetch: route });
|
||||
logger("info", "service_started", { port: config.port, instanceId: config.instanceId, schedulerEnabled: config.schedulerEnabled, workdir: config.defaultWorkdir, defaultModel: config.defaultModel, judgeConfigured: config.minimaxApiKey.length > 0, storage: "postgres" });
|
||||
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);
|
||||
@@ -4310,6 +4441,8 @@ const startupRecovered = config.schedulerEnabled ? queueActiveTasksForRestartRet
|
||||
if (startupRecovered > 0) logger("warn", "startup_requeued_active_tasks", { recovered: startupRecovered });
|
||||
persistState();
|
||||
serviceReady = true;
|
||||
await refreshSchedulerTasksFromDatabase("startup");
|
||||
startSchedulerDatabasePoller();
|
||||
if (config.startupOaBackfillEnabled) {
|
||||
setTimeout(() => { void backfillOaTraceStats(new URL("http://code-queue.local/api/oa/backfill?limit=2000")).catch((error) => logger("warn", "oa_trace_stats_startup_backfill_failed", { error: errorToJson(error) })); }, 1000).unref?.();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface QueueApiContext {
|
||||
dirtyDatabaseTaskCount: () => number;
|
||||
jsonResponse: (body: unknown, status?: number) => Response;
|
||||
judgeFailRetryLimit: number;
|
||||
loadQueuesFromDatabase: () => Promise<QueueRecord[]>;
|
||||
loadTaskFromDatabase: (taskId: string) => Promise<QueueTask | null>;
|
||||
loadTasksFromDatabase: (where?: "all" | "hot") => Promise<QueueTask[]>;
|
||||
loadTasksFromDatabaseByIds: (taskIds: string[]) => Promise<QueueTask[]>;
|
||||
@@ -275,7 +276,7 @@ function taskForListResponse(task: QueueTask, lite = false, queueTasks?: QueueTa
|
||||
} as unknown as JsonValue;
|
||||
}
|
||||
|
||||
function perQueueSummaries(tasks: QueueTask[] = ctx().tasks()): JsonValue[] {
|
||||
function perQueueSummaries(tasks: QueueTask[] = ctx().tasks(), queueRecords: QueueRecord[] = ctx().queues()): JsonValue[] {
|
||||
const summaries = new Map<string, {
|
||||
name: string;
|
||||
total: number;
|
||||
@@ -286,10 +287,10 @@ function perQueueSummaries(tasks: QueueTask[] = ctx().tasks()): JsonValue[] {
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}>();
|
||||
for (const queue of ctx().queues()) {
|
||||
queue.name = ctx().safeQueueName(queue.name, queue.id);
|
||||
for (const queue of queueRecords) {
|
||||
const name = ctx().safeQueueName(queue.name, queue.id);
|
||||
summaries.set(queue.id, {
|
||||
name: queue.name,
|
||||
name,
|
||||
total: 0,
|
||||
counts: {},
|
||||
unreadTerminal: 0,
|
||||
@@ -342,7 +343,7 @@ function perQueueSummaries(tasks: QueueTask[] = ctx().tasks()): JsonValue[] {
|
||||
return rows;
|
||||
}
|
||||
|
||||
function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()): JsonValue {
|
||||
function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks(), queueRecords: QueueRecord[] = ctx().queues()): JsonValue {
|
||||
const counts = tasks.reduce<Record<string, number>>((memo, task) => {
|
||||
memo[task.status] = (memo[task.status] ?? 0) + 1;
|
||||
return memo;
|
||||
@@ -355,7 +356,7 @@ function queueSummary(includeDevReady = true, tasks: QueueTask[] = ctx().tasks()
|
||||
}
|
||||
const activeTaskIds = Array.from(activeTaskIdSet).sort();
|
||||
const activeTaskId = activeTaskIds[0] ?? tasks.find((task) => task.status === "running" || task.status === "judging")?.id ?? null;
|
||||
const queues = perQueueSummaries(tasks);
|
||||
const queues = perQueueSummaries(tasks, queueRecords);
|
||||
const summary: Record<string, JsonValue> = {
|
||||
total: tasks.length,
|
||||
defaultQueueId: ctx().defaultQueueId,
|
||||
@@ -449,13 +450,14 @@ async function loadAllTasksForRead(): Promise<QueueTask[]> {
|
||||
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[]): Promise<JsonValue> {
|
||||
if (tasks !== undefined) return queueSummary(includeDevReady, tasks);
|
||||
async function queueSummaryForResponse(includeDevReady = true, tasks?: QueueTask[], queueRecords?: QueueRecord[]): Promise<JsonValue> {
|
||||
if (tasks !== undefined || queueRecords !== undefined) return queueSummary(includeDevReady, tasks ?? ctx().tasks(), queueRecords ?? ctx().queues());
|
||||
return queueSummaryForHealth(includeDevReady);
|
||||
}
|
||||
|
||||
async function queueSummaryForHealth(includeDevReady = true): Promise<JsonValue> {
|
||||
const summary = queueSummary(includeDevReady, ctx().tasks()) as Record<string, JsonValue>;
|
||||
const queueRecords = ctx().databaseReady() ? await ctx().loadQueuesFromDatabase() : ctx().queues();
|
||||
const summary = queueSummary(includeDevReady, ctx().tasks(), queueRecords) as Record<string, JsonValue>;
|
||||
if (!ctx().databaseReady()) return summary;
|
||||
const aggregateRows = await ctx().sql<QueueSummaryAggregateRow[]>`
|
||||
SELECT
|
||||
@@ -501,7 +503,7 @@ async function queueSummaryForHealth(includeDevReady = true): Promise<JsonValue>
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}>();
|
||||
for (const queue of ctx().queues()) {
|
||||
for (const queue of queueRecords) {
|
||||
summaries.set(queue.id, {
|
||||
name: ctx().safeQueueName(queue.name, queue.id),
|
||||
total: 0,
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface QueuedStatusReason {
|
||||
|
||||
export type RunMode = "initial" | "retry";
|
||||
|
||||
export type CodeQueueServiceRole = "combined" | "read" | "write" | "scheduler";
|
||||
|
||||
export type JudgeDecision = "complete" | "retry" | "fail";
|
||||
|
||||
export type CodeExecutionMode = "default" | "windows-native";
|
||||
@@ -35,7 +37,9 @@ export interface RuntimeConfig {
|
||||
instanceId: string;
|
||||
deployCommit: string;
|
||||
deployRequestedCommit: string;
|
||||
serviceRole: CodeQueueServiceRole;
|
||||
schedulerEnabled: boolean;
|
||||
schedulerPollIntervalMs: number;
|
||||
startupOaBackfillEnabled: boolean;
|
||||
outputArchiveDir: string;
|
||||
logFile: string;
|
||||
|
||||
@@ -1,46 +1,159 @@
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
[
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "code-queue",
|
||||
"servicePort": 4222
|
||||
},
|
||||
"activeInstanceId": "D601",
|
||||
"singleWriter": true,
|
||||
"expectedNodeIds": [
|
||||
"D601",
|
||||
"D518"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601",
|
||||
"nodeId": "D601",
|
||||
"role": "primary",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "service-proxy"
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
{
|
||||
"id": "D518",
|
||||
"nodeId": "D518",
|
||||
"role": "standby",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-d518:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "pod-ready"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": false
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "code-queue-scheduler",
|
||||
"servicePort": 4222
|
||||
},
|
||||
"activeInstanceId": "D601",
|
||||
"singleWriter": true,
|
||||
"expectedNodeIds": [
|
||||
"D601",
|
||||
"D518"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601",
|
||||
"nodeId": "D601",
|
||||
"role": "primary",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-scheduler:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "service-proxy"
|
||||
},
|
||||
{
|
||||
"id": "D518",
|
||||
"nodeId": "D518",
|
||||
"role": "standby",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-d518:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "pod-ready"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue-read",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "code-queue-read",
|
||||
"servicePort": 4222
|
||||
},
|
||||
"activeInstanceId": "D601-read",
|
||||
"singleWriter": false,
|
||||
"expectedNodeIds": [
|
||||
"D601"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601-read",
|
||||
"nodeId": "D601",
|
||||
"role": "standby",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-read:4222",
|
||||
"healthPath": "/live",
|
||||
"healthMode": "service-proxy"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue-write",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "code-queue-write",
|
||||
"servicePort": 4222
|
||||
},
|
||||
"activeInstanceId": "D601-write",
|
||||
"singleWriter": true,
|
||||
"expectedNodeIds": [
|
||||
"D601"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601-write",
|
||||
"nodeId": "D601",
|
||||
"role": "primary",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-write:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "service-proxy"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "unidesk.ai/k3s/v1",
|
||||
"kind": "ManagedKubernetesService",
|
||||
"metadata": {
|
||||
"name": "code-queue-scheduler",
|
||||
"namespace": "unidesk"
|
||||
},
|
||||
"spec": {
|
||||
"adapterServiceId": "k3sctl-adapter",
|
||||
"controlPlane": {
|
||||
"type": "kubernetes",
|
||||
"cluster": "unidesk-k3s",
|
||||
"context": "unidesk-k3s"
|
||||
},
|
||||
"route": {
|
||||
"kind": "kubernetes-service",
|
||||
"serviceName": "code-queue-scheduler",
|
||||
"servicePort": 4222
|
||||
},
|
||||
"activeInstanceId": "D601-scheduler",
|
||||
"singleWriter": true,
|
||||
"expectedNodeIds": [
|
||||
"D601"
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "D601-scheduler",
|
||||
"nodeId": "D601",
|
||||
"role": "primary",
|
||||
"baseUrl": "kubernetes://unidesk/services/code-queue-scheduler:4222",
|
||||
"healthPath": "/health",
|
||||
"healthMode": "service-proxy"
|
||||
}
|
||||
],
|
||||
"requireAllInstancesHealthy": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -6,6 +6,544 @@ metadata:
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/k3s-cluster: unidesk-k3s
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: code-queue-read
|
||||
namespace: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: read
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601-read
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: read
|
||||
unidesk.ai/instance-id: D601-read
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: read
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601-read
|
||||
unidesk.ai/node-id: D601
|
||||
spec:
|
||||
nodeSelector:
|
||||
unidesk.ai/node-id: D601
|
||||
terminationGracePeriodSeconds: 30
|
||||
containers:
|
||||
- name: code-queue
|
||||
image: unidesk-code-queue:d601
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4222
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: code-queue-env
|
||||
optional: true
|
||||
env:
|
||||
- name: HOST
|
||||
value: "0.0.0.0"
|
||||
- name: PORT
|
||||
value: "4222"
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk"
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
value: "read"
|
||||
- name: CODE_QUEUE_SCHEDULER_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_DATA_DIR
|
||||
value: "/var/lib/unidesk/code-queue"
|
||||
- name: CODE_QUEUE_WORKDIR
|
||||
value: "/workspace"
|
||||
- name: CODE_QUEUE_CODEX_HOME
|
||||
value: "/var/lib/unidesk/code-queue/codex-home"
|
||||
- name: CODE_QUEUE_OPENCODE_XDG_DIR
|
||||
value: "/var/lib/unidesk/code-queue/opencode-xdg"
|
||||
- name: CODE_QUEUE_SOURCE_CODEX_CONFIG
|
||||
value: "/root/.codex/config.toml"
|
||||
- name: CODE_QUEUE_DEFAULT_MODEL
|
||||
value: "gpt-5.5"
|
||||
- name: CODE_QUEUE_MODELS
|
||||
value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7"
|
||||
- name: CODE_QUEUE_MODEL_REASONING_EFFORTS
|
||||
value: "gpt-5.5=xhigh"
|
||||
- name: CODE_QUEUE_SANDBOX
|
||||
value: "danger-full-access"
|
||||
- name: CODE_QUEUE_APPROVAL_POLICY
|
||||
value: "never"
|
||||
- name: CODE_QUEUE_MAX_ACTIVE_QUEUES
|
||||
value: "0"
|
||||
- name: CODE_QUEUE_DATABASE_POOL_MAX
|
||||
value: "2"
|
||||
- name: NODE_OPTIONS
|
||||
value: "--max-old-space-size=512"
|
||||
- name: GIT_CONFIG_COUNT
|
||||
value: "1"
|
||||
- name: GIT_CONFIG_KEY_0
|
||||
value: "safe.directory"
|
||||
- name: GIT_CONFIG_VALUE_0
|
||||
value: "*"
|
||||
- name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS
|
||||
value: "10"
|
||||
- name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS
|
||||
value: "10"
|
||||
- name: CODE_QUEUE_MAIN_PROVIDER_ID
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_REMOTE_WORKDIR
|
||||
value: "/home/ubuntu"
|
||||
- name: CODE_QUEUE_EXECUTION_PROVIDER_IDS
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_MASTER_HOST
|
||||
value: "74.48.78.17"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_WORKDIR
|
||||
value: "/home/ubuntu"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_URL
|
||||
value: ""
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: HTTP_PROXY
|
||||
value: ""
|
||||
- name: HTTPS_PROXY
|
||||
value: ""
|
||||
- name: ALL_PROXY
|
||||
value: ""
|
||||
- name: http_proxy
|
||||
value: ""
|
||||
- name: https_proxy
|
||||
value: ""
|
||||
- name: all_proxy
|
||||
value: ""
|
||||
- name: NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: no_proxy
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: OA_EVENT_FLOW_BASE_URL
|
||||
value: "http://d601-tcp-egress-gateway.unidesk.svc.cluster.local:4255"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL
|
||||
value: ""
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE
|
||||
value: "private"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID
|
||||
value: "645275593"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS
|
||||
value: "12000"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS
|
||||
value: "15000"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS
|
||||
value: "3"
|
||||
- name: LOG_FILE
|
||||
value: "/var/log/unidesk/code-queue-read.jsonl"
|
||||
- name: UNIDESK_LOG_RETENTION_BYTES
|
||||
value: "1GiB"
|
||||
volumeMounts:
|
||||
- name: docker-sock
|
||||
mountPath: /var/run/docker.sock
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: workspace
|
||||
mountPath: /home/ubuntu
|
||||
- name: repo
|
||||
mountPath: /root/unidesk
|
||||
- name: repo
|
||||
mountPath: /app
|
||||
- name: codex-config
|
||||
mountPath: /root/.codex/config.toml
|
||||
readOnly: true
|
||||
- name: codex-auth
|
||||
mountPath: /root/.codex/auth.json
|
||||
readOnly: true
|
||||
- name: ssh-dir
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
- name: state
|
||||
mountPath: /var/lib/unidesk/code-queue
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 20
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 60
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
type: Socket
|
||||
- name: workspace
|
||||
hostPath:
|
||||
path: /home/ubuntu
|
||||
type: Directory
|
||||
- name: repo
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy
|
||||
type: Directory
|
||||
- name: codex-config
|
||||
hostPath:
|
||||
path: /home/ubuntu/.codex/config.toml
|
||||
type: File
|
||||
- name: codex-auth
|
||||
hostPath:
|
||||
path: /home/ubuntu/.codex/auth.json
|
||||
type: File
|
||||
- name: ssh-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.ssh
|
||||
type: Directory
|
||||
- name: logs
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy/.state/code-queue/logs
|
||||
type: DirectoryOrCreate
|
||||
- name: state
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy/.state/code-queue
|
||||
type: DirectoryOrCreate
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: code-queue-write
|
||||
namespace: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: write
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601-write
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: write
|
||||
unidesk.ai/instance-id: D601-write
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: write
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
unidesk.ai/instance-id: D601-write
|
||||
unidesk.ai/node-id: D601
|
||||
spec:
|
||||
nodeSelector:
|
||||
unidesk.ai/node-id: D601
|
||||
terminationGracePeriodSeconds: 30
|
||||
containers:
|
||||
- name: code-queue
|
||||
image: unidesk-code-queue:d601
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 4222
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: code-queue-env
|
||||
optional: true
|
||||
env:
|
||||
- name: HOST
|
||||
value: "0.0.0.0"
|
||||
- name: PORT
|
||||
value: "4222"
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk"
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: "D601-write"
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
value: "write"
|
||||
- name: CODE_QUEUE_SCHEDULER_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_DATA_DIR
|
||||
value: "/var/lib/unidesk/code-queue"
|
||||
- name: CODE_QUEUE_WORKDIR
|
||||
value: "/workspace"
|
||||
- name: CODE_QUEUE_CODEX_HOME
|
||||
value: "/var/lib/unidesk/code-queue/codex-home"
|
||||
- name: CODE_QUEUE_OPENCODE_XDG_DIR
|
||||
value: "/var/lib/unidesk/code-queue/opencode-xdg"
|
||||
- name: CODE_QUEUE_SOURCE_CODEX_CONFIG
|
||||
value: "/root/.codex/config.toml"
|
||||
- name: CODE_QUEUE_DEFAULT_MODEL
|
||||
value: "gpt-5.5"
|
||||
- name: CODE_QUEUE_MODELS
|
||||
value: "gpt-5.5,gpt-5.4-mini,gpt-5.4,minimax-m2.7"
|
||||
- name: CODE_QUEUE_MODEL_REASONING_EFFORTS
|
||||
value: "gpt-5.5=xhigh"
|
||||
- name: CODE_QUEUE_SANDBOX
|
||||
value: "danger-full-access"
|
||||
- name: CODE_QUEUE_APPROVAL_POLICY
|
||||
value: "never"
|
||||
- name: CODE_QUEUE_MAX_ACTIVE_QUEUES
|
||||
value: "0"
|
||||
- name: CODE_QUEUE_DATABASE_POOL_MAX
|
||||
value: "2"
|
||||
- name: NODE_OPTIONS
|
||||
value: "--max-old-space-size=768"
|
||||
- name: GIT_CONFIG_COUNT
|
||||
value: "1"
|
||||
- name: GIT_CONFIG_KEY_0
|
||||
value: "safe.directory"
|
||||
- name: GIT_CONFIG_VALUE_0
|
||||
value: "*"
|
||||
- name: CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS
|
||||
value: "10"
|
||||
- name: CODE_QUEUE_IN_MEMORY_EVENT_RECORDS
|
||||
value: "10"
|
||||
- name: CODE_QUEUE_MAIN_PROVIDER_ID
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_REMOTE_WORKDIR
|
||||
value: "/home/ubuntu"
|
||||
- name: CODE_QUEUE_EXECUTION_PROVIDER_IDS
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_MASTER_HOST
|
||||
value: "74.48.78.17"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_DEV_CONTAINER_WORKDIR
|
||||
value: "/home/ubuntu"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_URL
|
||||
value: ""
|
||||
- name: CODE_QUEUE_EGRESS_PROXY_NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: HTTP_PROXY
|
||||
value: ""
|
||||
- name: HTTPS_PROXY
|
||||
value: ""
|
||||
- name: ALL_PROXY
|
||||
value: ""
|
||||
- name: http_proxy
|
||||
value: ""
|
||||
- name: https_proxy
|
||||
value: ""
|
||||
- name: all_proxy
|
||||
value: ""
|
||||
- name: NO_PROXY
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: no_proxy
|
||||
value: "localhost,127.0.0.1,::1,host.docker.internal,d601-tcp-egress-gateway,d601-tcp-egress-gateway.unidesk,d601-tcp-egress-gateway.unidesk.svc,d601-tcp-egress-gateway.unidesk.svc.cluster.local,backend-core,oa-event-flow,database"
|
||||
- name: OA_EVENT_FLOW_BASE_URL
|
||||
value: "http://d601-tcp-egress-gateway.unidesk.svc.cluster.local:4255"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL
|
||||
value: ""
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE
|
||||
value: "private"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID
|
||||
value: "645275593"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS
|
||||
value: "12000"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS
|
||||
value: "15000"
|
||||
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS
|
||||
value: "3"
|
||||
- name: LOG_FILE
|
||||
value: "/var/log/unidesk/code-queue-write.jsonl"
|
||||
- name: UNIDESK_LOG_RETENTION_BYTES
|
||||
value: "1GiB"
|
||||
volumeMounts:
|
||||
- name: docker-sock
|
||||
mountPath: /var/run/docker.sock
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: workspace
|
||||
mountPath: /home/ubuntu
|
||||
- name: repo
|
||||
mountPath: /root/unidesk
|
||||
- name: repo
|
||||
mountPath: /app
|
||||
- name: codex-config
|
||||
mountPath: /root/.codex/config.toml
|
||||
readOnly: true
|
||||
- name: codex-auth
|
||||
mountPath: /root/.codex/auth.json
|
||||
readOnly: true
|
||||
- name: ssh-dir
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
- name: logs
|
||||
mountPath: /var/log/unidesk
|
||||
- name: state
|
||||
mountPath: /var/lib/unidesk/code-queue
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 20
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /live
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 60
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
hostPath:
|
||||
path: /var/run/docker.sock
|
||||
type: Socket
|
||||
- name: workspace
|
||||
hostPath:
|
||||
path: /home/ubuntu
|
||||
type: Directory
|
||||
- name: repo
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy
|
||||
type: Directory
|
||||
- name: codex-config
|
||||
hostPath:
|
||||
path: /home/ubuntu/.codex/config.toml
|
||||
type: File
|
||||
- name: codex-auth
|
||||
hostPath:
|
||||
path: /home/ubuntu/.codex/auth.json
|
||||
type: File
|
||||
- name: ssh-dir
|
||||
hostPath:
|
||||
path: /home/ubuntu/.ssh
|
||||
type: Directory
|
||||
- name: logs
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy/.state/code-queue/logs
|
||||
type: DirectoryOrCreate
|
||||
- name: state
|
||||
hostPath:
|
||||
path: /home/ubuntu/cq-deploy/.state/code-queue
|
||||
type: DirectoryOrCreate
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: code-queue-scheduler
|
||||
namespace: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: scheduler
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: code-queue
|
||||
unidesk.ai/instance-id: D601
|
||||
ports:
|
||||
- name: http
|
||||
port: 4222
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: code-queue-read
|
||||
namespace: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: read
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: read
|
||||
unidesk.ai/instance-id: D601-read
|
||||
ports:
|
||||
- name: http
|
||||
port: 4222
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: code-queue-write
|
||||
namespace: unidesk
|
||||
labels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: write
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/deployment-mode: k3sctl-managed
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: code-queue
|
||||
app.kubernetes.io/component: write
|
||||
unidesk.ai/instance-id: D601-write
|
||||
ports:
|
||||
- name: http
|
||||
port: 4222
|
||||
targetPort: http
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@@ -348,6 +886,11 @@ metadata:
|
||||
unidesk.ai/instance-id: D601
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: code-queue
|
||||
@@ -384,8 +927,12 @@ spec:
|
||||
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk"
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: "D601"
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
value: "scheduler"
|
||||
- name: CODE_QUEUE_SCHEDULER_ENABLED
|
||||
value: "true"
|
||||
- name: CODE_QUEUE_SCHEDULER_POLL_INTERVAL_MS
|
||||
value: "2000"
|
||||
- name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_DATA_DIR
|
||||
@@ -631,6 +1178,8 @@ spec:
|
||||
value: "postgres://unidesk:unidesk_dev_password@d601-tcp-egress-gateway.unidesk.svc.cluster.local:15432/unidesk"
|
||||
- name: CODE_QUEUE_INSTANCE_ID
|
||||
value: "D518"
|
||||
- name: CODE_QUEUE_SERVICE_ROLE
|
||||
value: "read"
|
||||
- name: CODE_QUEUE_SCHEDULER_ENABLED
|
||||
value: "false"
|
||||
- name: CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED
|
||||
|
||||
Reference in New Issue
Block a user