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
+23
View File
@@ -14,6 +14,7 @@ import {
listJobs,
updateJob,
setJobStatus,
resetJob,
savePushSubscription,
getAllSubscriptions,
deletePushSubscription
@@ -107,6 +108,28 @@ describe('updateJob', () => {
it('does nothing for an unknown id', () => {
expect(() => updateJob({ id: 'no-such-id', title: 'Ghost' })).not.toThrow();
});
it('stores and reads retryCount and nextRetryAt', () => {
const job = createJob('src', 'retry test', 'auto');
// Default retry_count is 0 (DEFAULT 0 in schema)
expect(getJob(job.id)!.retryCount).toBe(0);
expect(getJob(job.id)!.nextRetryAt).toBeNull();
const nextRetry = new Date(Date.now() + 60000).toISOString();
updateJob({ id: job.id, retryCount: 2, nextRetryAt: nextRetry });
const updated = getJob(job.id)!;
expect(updated.retryCount).toBe(2);
expect(updated.nextRetryAt).toBe(nextRetry);
});
it('resetJob clears retry fields', () => {
const job = createJob('src', 'reset retry', 'auto');
updateJob({ id: job.id, retryCount: 3, nextRetryAt: '2026-07-08T12:00:00.000Z' });
resetJob(job.id);
const after = getJob(job.id)!;
expect(after.retryCount).toBe(0);
expect(after.nextRetryAt).toBeNull();
});
});
// ── setJobStatus ──────────────────────────────────────────────────────────────