- Omorganiser docs/: konsepter, features, infra og proposals i egne mapper - Ny docs/erfaringer/ med lærdommer fra chat-implementering (Svelte 5, SpacetimeDB, adapter-mønster) - Oppdater ARCHITECTURE.md: Lag 1 status, ny §10 Erfaringslogg, SpacetimeDB i lokal dev - Oppdater synkronisering.md med implementeringsstatus og designvalg - Oppdater lokal.md med SpacetimeDB og AI Gateway - Utvid PG-skjema med channels, messages, media_files, message_revisions - Legg til seed_dev.sql, migration_safety.md, .env.example - Nye feature-specs: chat, kanban, whiteboard, live_ai, lydmeldinger m.fl. - Nye konsept-specs: studioet, møterommet, redaksjonen, den asynkrone gjesten m.fl. - SpacetimeDB og AI Gateway i docker-compose.dev.yml - collect-docs.sh inkluderer erfaringer/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
5.6 KiB
Markdown
101 lines
5.6 KiB
Markdown
# Infrastruktur: Jobbkø (PostgreSQL-basert)
|
||
**Filsti:** `docs/infra/jobbkø.md`
|
||
|
||
## 1. Konsept
|
||
Et felles, sentralisert køsystem for alle asynkrone bakgrunnsjobber i Sidelinja. Bygget som en enkel tabell i PostgreSQL med Rust-workers som konsumerer jobber. Ingen ekstern message broker — PostgreSQL er køen.
|
||
|
||
## 2. Hvorfor PostgreSQL?
|
||
- Allerede i stacken, ingen ny infrastruktur å drifte
|
||
- Transaksjonell garanti: jobben og resultatet kan committes sammen med dataendringer
|
||
- Lavt volum (titalls jobber/time) gjør polling neglisjerbart
|
||
- Enkel feilsøking via SQL (`SELECT * FROM job_queue WHERE status = 'error'`)
|
||
- `SELECT ... FOR UPDATE SKIP LOCKED` gir trygg concurrent polling uten låsekonflikt
|
||
|
||
## 3. Datastruktur
|
||
|
||
```sql
|
||
CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry');
|
||
|
||
CREATE TABLE job_queue (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||
job_type TEXT NOT NULL, -- 'whisper_transcribe', 'openrouter_analyze', 'stats_parse', 'research_clip'
|
||
payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.)
|
||
status job_status NOT NULL DEFAULT 'pending',
|
||
priority SMALLINT NOT NULL DEFAULT 0, -- Høyere = viktigere
|
||
result JSONB, -- Resultatet ved fullført jobb
|
||
error_msg TEXT, -- Feilmelding ved error
|
||
attempts SMALLINT NOT NULL DEFAULT 0,
|
||
max_attempts SMALLINT NOT NULL DEFAULT 3,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
started_at TIMESTAMPTZ,
|
||
completed_at TIMESTAMPTZ,
|
||
scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now() -- For utsatte jobber / retry med backoff
|
||
);
|
||
|
||
CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC)
|
||
WHERE status IN ('pending', 'retry');
|
||
```
|
||
|
||
## 4. Worker-arkitektur (Rust)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Rust Worker-prosess (én per jobbtype) │
|
||
│ │
|
||
│ Loop: │
|
||
│ 1. SELECT ... FOR UPDATE SKIP LOCKED │
|
||
│ WHERE status IN ('pending','retry') │
|
||
│ AND job_type = $type │
|
||
│ AND scheduled_for <= now() │
|
||
│ ORDER BY priority DESC, scheduled_for │
|
||
│ LIMIT 1 │
|
||
│ │
|
||
│ 2. UPDATE status = 'running' │
|
||
│ 3. Utfør jobben │
|
||
│ 4a. OK: UPDATE status = 'completed' │
|
||
│ 4b. Feil: attempts += 1 │
|
||
│ Hvis attempts < max_attempts: │
|
||
│ status = 'retry' │
|
||
│ scheduled_for = now() │
|
||
│ + backoff(attempts) │
|
||
│ Ellers: status = 'error' │
|
||
│ │
|
||
│ Poll-intervall: 1 sekund (konfigurerbart) │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Backoff-strategi:** Eksponentiell: `30s × 2^(attempts-1)` (30s, 60s, 120s).
|
||
|
||
## 5. Jobbtyper
|
||
|
||
| `job_type` | Konsument | Beskrivelse |
|
||
|---|---|---|
|
||
| `whisper_transcribe` | Podcastfabrikken | Transkriber MP3 via faster-whisper |
|
||
| `openrouter_analyze` | Podcastfabrikken | Metadata-uttrekk fra transkripsjon |
|
||
| `research_clip` | AI Research-Klipper | Rens og strukturer innlimt tekst |
|
||
| `stats_parse` | Podcast-Statistikk | Batch-prosesser Caddy-logger |
|
||
| `meeting_summarize` | Møterommet | Generer møtereferat og action points fra transkripsjon |
|
||
| `valgomat_generate_profile` | Valgomat | Generer syntetiske kandidatprofiler fra partiprogrammer |
|
||
| `valgomat_moderation` | Valgomat | Semantisk deduplisering og nøytralitetsvask av brukerspørsmål |
|
||
| `dictation_cleanup` | Lydmeldinger | AI-opprydding av diktert transkripsjon til strukturert notat |
|
||
| `generate_embeddings` | Kunnskaps-Bridge | Generer vector embeddings for noder (pgvector) |
|
||
| `prompt_eval` | Prompt-Laboratorium | Batch-evaluering av testsett mot valgte modeller |
|
||
|
||
## 6. Workspace-isolasjon
|
||
Alle jobber merkes med `workspace_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode:
|
||
* Worker leser `workspace_id` fra jobben og bruker det til å lagre resultater tilbake i riktig silo
|
||
* Workspace-spesifikk config (AI-prompts, navnelister) hentes fra `workspaces.settings`
|
||
* Feilede jobber vises kun for brukere i riktig workspace i admin-visningen
|
||
|
||
## 7. Observabilitet
|
||
- Jobber med `status = 'error'` skal være synlige i admin-visningen (SvelteKit `/admin/jobs`)
|
||
- Valgfritt: SpacetimeDB-event ved statusendring slik at UI kan vise fremdrift i sanntid (f.eks. "Transkriberer... 2/3 forsøk")
|
||
|
||
## 7. Instruks for Claude Code
|
||
- Implementer worker-logikken som et Rust-bibliotek (`sidelinja-jobs`) som de ulike binærene kan bruke
|
||
- Hver jobbtype får sin egen handler-funksjon, men deler polling-loopen
|
||
- Unngå å spinne opp mange tråder — én tokio-task per jobbtype er tilstrekkelig
|
||
- Aldri lagre lydfiler i `payload` — bruk filstier
|
||
- Opprett alltid jobber med riktig `workspace_id` — hent fra konteksten (innlogget bruker, webhook, etc.)
|
||
- Ved `stats_parse`: denne erstatter den frittstående cronjobben beskrevet i podcast_statistikk.md — bruk jobbkøen med `scheduled_for` for periodisk kjøring
|