server/docs/infra/jobbkø.md
vegard a5985ef3f8 Dokumentasjon, erfaringslogg, migrasjoner og infra-oppdateringer
- 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>
2026-03-15 01:40:14 +01:00

5.6 KiB
Raw Blame History

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

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