feat: D3 foundation — retry schema, shared types, health client, backoff helper

- Add retry_count/next_retry_at to jobs table via guarded idempotent ALTER TABLE
  (PRAGMA table_info existence check, try/catch for safety on restart)
- Add retryCount/nextRetryAt to Job type, rowToJob(), updateJob(), resetJob()
- Add getHealth() returning structured WhisperHealth (status, gpu_name,
  vram_total_mb, model, queue_depth, model_state) distinct from
  existing checkHealth() boolean helper
- Create retryWithBackoff() pure helper in src/lib/server/retry.ts with
  exponential backoff + jitter and onAttempt callback — no persistence owned
- Tests: retry column read/write/reset, getHealth() smoke tests,
  retryWithBackoff() edge cases (success, failure, backoff timing, onAttempt)
This commit is contained in:
Giancarmine Salucci
2026-07-08 00:40:01 +02:00
parent 35a2d86dbb
commit 9962a0fc0f
7 changed files with 319 additions and 3 deletions
+28 -1
View File
@@ -4,6 +4,18 @@ import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import type { Job, JobStatus, AudioMode, PushSubscription } from '$lib/types.js';
/**
* Add a column to a table if it does not already exist.
* Uses PRAGMA table_info for an existence check, consistent with the
* existing CREATE TABLE IF NOT EXISTS idiom — no migration tool introduced.
*/
function addColumnIfMissing(table: string, column: string, typeDef: string): void {
const columns = db.pragma(`table_info(${table})`) as { name: string }[];
if (!columns.some((c) => c.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDef}`);
}
}
const DATA_DIR = process.env.DATA_DIR ?? join(process.env.HOME ?? '/tmp', '.whisper-pwa');
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
@@ -37,6 +49,14 @@ db.exec(`
);
`);
// Add retry columns idempotently (may already exist on restart)
try {
addColumnIfMissing('jobs', 'retry_count', 'INTEGER DEFAULT 0');
addColumnIfMissing('jobs', 'next_retry_at', 'TEXT');
} catch (e) {
console.warn('[db] failed to add retry columns:', e);
}
const stmts = {
insertJob: db.prepare(`
INSERT INTO jobs (id, status, title, source, audio_mode)
@@ -52,6 +72,8 @@ const stmts = {
output_dir = @outputDir,
segments_json = @segmentsJson,
error = @error,
retry_count = @retryCount,
next_retry_at = @nextRetryAt,
updated_at = datetime('now')
WHERE id = @id
`),
@@ -64,6 +86,7 @@ const stmts = {
status = 'pending', progress = 0, error = NULL,
mean_volume = NULL, whisper_job_id = NULL,
output_dir = NULL, segments_json = NULL,
retry_count = 0, next_retry_at = NULL,
updated_at = datetime('now')
WHERE id = @id
`),
@@ -92,6 +115,8 @@ function rowToJob(row: Record<string, unknown>): Job {
outputDir: row.output_dir as string | null,
segmentsJson: row.segments_json as string | null,
error: row.error as string | null,
retryCount: row.retry_count as number | null,
nextRetryAt: row.next_retry_at as string | null,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string
};
@@ -125,7 +150,9 @@ export function updateJob(job: Partial<Job> & { id: string }): void {
progress: merged.progress,
outputDir: merged.outputDir,
segmentsJson: merged.segmentsJson,
error: merged.error
error: merged.error,
retryCount: merged.retryCount ?? null,
nextRetryAt: merged.nextRetryAt ?? null
});
}