Download apps/api/src/queue/postgres-store.ts from Lazywords/Rl-Auto: direct link, hf CLI and curl.
- Browser
- Download file 8.56 kB
-
https://huggingface.co/spaces/Lazywords/Rl-Auto/resolve/main/apps/api/src/queue/postgres-store.ts
- Command line
-
hf download hf://spaces/Lazywords/Rl-Auto/apps/api/src/queue/postgres-store.ts
-
curl -L -o postgres-store.ts https://huggingface.co/spaces/Lazywords/Rl-Auto/resolve/main/apps/api/src/queue/postgres-store.ts
8.56 kB
| import { randomUUID } from "node:crypto"; | |
| import pg from "pg"; | |
| import { | |
| type DetectedMode, | |
| type ItemRow, | |
| type ItemStatus, | |
| type JobRow, | |
| type JobStatus, | |
| type SetItemStatusMeta, | |
| type Store, | |
| nowMs, | |
| truncateError, | |
| } from "./store.js"; | |
| const { Pool } = pg; | |
| export const POSTGRES_TAKE_NEXT_ITEM_SQL = ` | |
| WITH next_item AS ( | |
| SELECT id | |
| FROM items | |
| WHERE job_id = $1 AND status = 'queued' | |
| ORDER BY ord ASC | |
| LIMIT 1 | |
| FOR UPDATE SKIP LOCKED | |
| ) | |
| UPDATE items | |
| SET status = 'running', | |
| attempt_count = attempt_count + 1, | |
| error_preview = NULL, | |
| last_stage = 'running', | |
| updated_at = $2 | |
| FROM next_item | |
| WHERE items.id = next_item.id | |
| RETURNING items.* | |
| `; | |
| const SCHEMA_SQL = ` | |
| CREATE TABLE IF NOT EXISTS jobs ( | |
| id TEXT PRIMARY KEY, | |
| status TEXT NOT NULL, | |
| options_snapshot TEXT NOT NULL, | |
| created_at BIGINT NOT NULL, | |
| updated_at BIGINT NOT NULL | |
| ); | |
| CREATE TABLE IF NOT EXISTS items ( | |
| id TEXT PRIMARY KEY, | |
| job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE, | |
| ord INTEGER NOT NULL, | |
| filename TEXT NOT NULL, | |
| upload_path TEXT NOT NULL, | |
| detected_mode TEXT NOT NULL, | |
| status TEXT NOT NULL, | |
| evidence_path TEXT, | |
| task_package_path TEXT, | |
| judge_result_path TEXT, | |
| export_zip_path TEXT, | |
| error_preview TEXT, | |
| error_details_path TEXT, | |
| attempt_count INTEGER NOT NULL DEFAULT 0, | |
| last_stage TEXT, | |
| created_at BIGINT NOT NULL, | |
| updated_at BIGINT NOT NULL | |
| ); | |
| ALTER TABLE items ADD COLUMN IF NOT EXISTS error_details_path TEXT; | |
| ALTER TABLE items ADD COLUMN IF NOT EXISTS attempt_count INTEGER NOT NULL DEFAULT 0; | |
| ALTER TABLE items ADD COLUMN IF NOT EXISTS last_stage TEXT; | |
| CREATE INDEX IF NOT EXISTS idx_items_job_ord ON items(job_id, ord); | |
| CREATE INDEX IF NOT EXISTS idx_items_status ON items(status); | |
| CREATE INDEX IF NOT EXISTS idx_items_job_status_ord ON items(job_id, status, ord); | |
| `; | |
| function mapJob(row: any): JobRow { | |
| return { | |
| ...row, | |
| created_at: Number(row.created_at), | |
| updated_at: Number(row.updated_at), | |
| }; | |
| } | |
| function mapItem(row: any): ItemRow { | |
| return { | |
| ...row, | |
| attempt_count: Number(row.attempt_count || 0), | |
| created_at: Number(row.created_at), | |
| updated_at: Number(row.updated_at), | |
| evidence_path: row.evidence_path ?? null, | |
| task_package_path: row.task_package_path ?? null, | |
| judge_result_path: row.judge_result_path ?? null, | |
| export_zip_path: row.export_zip_path ?? null, | |
| error_preview: row.error_preview ?? null, | |
| error_details_path: row.error_details_path ?? null, | |
| last_stage: row.last_stage ?? null, | |
| }; | |
| } | |
| export class PostgresStore implements Store { | |
| private pool: pg.Pool; | |
| constructor(databaseUrl: string) { | |
| if (!databaseUrl) throw new Error("DATABASE_URL is required for Postgres store"); | |
| this.pool = new Pool({ connectionString: databaseUrl }); | |
| } | |
| async init(): Promise<void> { | |
| await this.pool.query(SCHEMA_SQL); | |
| } | |
| async close(): Promise<void> { | |
| await this.pool.end(); | |
| } | |
| async createJob(optionsSnapshot: object): Promise<JobRow> { | |
| const id = randomUUID(); | |
| const t = nowMs(); | |
| const result = await this.pool.query( | |
| `INSERT INTO jobs (id, status, options_snapshot, created_at, updated_at) | |
| VALUES ($1, $2, $3, $4, $5) | |
| RETURNING *`, | |
| [id, "pending", JSON.stringify(optionsSnapshot), t, t], | |
| ); | |
| return mapJob(result.rows[0]); | |
| } | |
| async getJob(id: string): Promise<JobRow | null> { | |
| const result = await this.pool.query("SELECT * FROM jobs WHERE id = $1", [id]); | |
| return result.rows[0] ? mapJob(result.rows[0]) : null; | |
| } | |
| async setJobStatus(id: string, status: JobStatus): Promise<void> { | |
| await this.pool.query("UPDATE jobs SET status = $1, updated_at = $2 WHERE id = $3", [ | |
| status, | |
| nowMs(), | |
| id, | |
| ]); | |
| } | |
| async updateJobOptions(id: string, optionsSnapshot: object): Promise<void> { | |
| await this.pool.query( | |
| "UPDATE jobs SET options_snapshot = $1, updated_at = $2 WHERE id = $3", | |
| [JSON.stringify(optionsSnapshot), nowMs(), id], | |
| ); | |
| } | |
| async listResumableJobs(): Promise<JobRow[]> { | |
| const result = await this.pool.query( | |
| "SELECT * FROM jobs WHERE status = 'running' ORDER BY created_at", | |
| ); | |
| return result.rows.map(mapJob); | |
| } | |
| async recoverInterruptedJob(id: string): Promise<void> { | |
| const t = nowMs(); | |
| const client = await this.pool.connect(); | |
| try { | |
| await client.query("BEGIN"); | |
| await client.query( | |
| `UPDATE items | |
| SET status = 'queued', | |
| error_preview = NULL, | |
| last_stage = 'recovered', | |
| updated_at = $1 | |
| WHERE job_id = $2 AND status IN ('running', 'judging')`, | |
| [t, id], | |
| ); | |
| await client.query("UPDATE jobs SET status = 'running', updated_at = $1 WHERE id = $2", [ | |
| t, | |
| id, | |
| ]); | |
| await client.query("COMMIT"); | |
| } catch (err) { | |
| await client.query("ROLLBACK"); | |
| throw err; | |
| } finally { | |
| client.release(); | |
| } | |
| } | |
| async createItem(input: { | |
| jobId: string; | |
| ord: number; | |
| filename: string; | |
| uploadPath: string; | |
| detectedMode: DetectedMode; | |
| }): Promise<ItemRow> { | |
| const id = randomUUID(); | |
| const t = nowMs(); | |
| const result = await this.pool.query( | |
| `INSERT INTO items | |
| (id, job_id, ord, filename, upload_path, detected_mode, status, | |
| created_at, updated_at) | |
| VALUES ($1, $2, $3, $4, $5, $6, 'queued', $7, $8) | |
| RETURNING *`, | |
| [id, input.jobId, input.ord, input.filename, input.uploadPath, input.detectedMode, t, t], | |
| ); | |
| return mapItem(result.rows[0]); | |
| } | |
| async getItem(id: string): Promise<ItemRow | null> { | |
| const result = await this.pool.query("SELECT * FROM items WHERE id = $1", [id]); | |
| return result.rows[0] ? mapItem(result.rows[0]) : null; | |
| } | |
| async listItemsByJob(jobId: string): Promise<ItemRow[]> { | |
| const result = await this.pool.query( | |
| "SELECT * FROM items WHERE job_id = $1 ORDER BY ord ASC", | |
| [jobId], | |
| ); | |
| return result.rows.map(mapItem); | |
| } | |
| async setItemStatus( | |
| id: string, | |
| status: ItemStatus, | |
| errorPreview?: string, | |
| meta: SetItemStatusMeta = {}, | |
| ): Promise<void> { | |
| await this.pool.query( | |
| `UPDATE items | |
| SET status = $1, | |
| error_preview = $2, | |
| last_stage = COALESCE($3, last_stage), | |
| error_details_path = COALESCE($4, error_details_path), | |
| updated_at = $5 | |
| WHERE id = $6`, | |
| [ | |
| status, | |
| truncateError(errorPreview), | |
| meta.lastStage ?? null, | |
| meta.errorDetailsPath ?? null, | |
| nowMs(), | |
| id, | |
| ], | |
| ); | |
| } | |
| async setItemPaths( | |
| id: string, | |
| paths: Partial<{ | |
| evidencePath: string; | |
| taskPackagePath: string; | |
| judgeResultPath: string; | |
| exportZipPath: string; | |
| }>, | |
| ): Promise<void> { | |
| const fields: string[] = []; | |
| const values: unknown[] = []; | |
| const add = (column: string, value: unknown) => { | |
| values.push(value); | |
| fields.push(`${column} = $${values.length}`); | |
| }; | |
| if (paths.evidencePath !== undefined) add("evidence_path", paths.evidencePath); | |
| if (paths.taskPackagePath !== undefined) add("task_package_path", paths.taskPackagePath); | |
| if (paths.judgeResultPath !== undefined) add("judge_result_path", paths.judgeResultPath); | |
| if (paths.exportZipPath !== undefined) add("export_zip_path", paths.exportZipPath); | |
| if (!fields.length) return; | |
| values.push(nowMs()); | |
| fields.push(`updated_at = $${values.length}`); | |
| values.push(id); | |
| await this.pool.query(`UPDATE items SET ${fields.join(", ")} WHERE id = $${values.length}`, values); | |
| } | |
| async takeNextQueuedItem(jobId: string): Promise<ItemRow | null> { | |
| const client = await this.pool.connect(); | |
| try { | |
| await client.query("BEGIN"); | |
| const result = await client.query(POSTGRES_TAKE_NEXT_ITEM_SQL, [jobId, nowMs()]); | |
| await client.query("COMMIT"); | |
| return result.rows[0] ? mapItem(result.rows[0]) : null; | |
| } catch (err) { | |
| await client.query("ROLLBACK"); | |
| throw err; | |
| } finally { | |
| client.release(); | |
| } | |
| } | |
| async resetItemForRetry(id: string): Promise<void> { | |
| await this.pool.query( | |
| `UPDATE items | |
| SET status = 'queued', | |
| error_preview = NULL, | |
| error_details_path = NULL, | |
| attempt_count = 0, | |
| last_stage = NULL, | |
| evidence_path = NULL, | |
| task_package_path = NULL, | |
| judge_result_path = NULL, | |
| export_zip_path = NULL, | |
| updated_at = $1 | |
| WHERE id = $2`, | |
| [nowMs(), id], | |
| ); | |
| } | |
| } | |