homelab-codex-ws/kb/runbooks/mail-body-ingest-run.md
oskar 241ce6aecd feat(kb-mail-batching): retry + izolacja trujacego chunka w torze embed + benchmark
Batching /api/embed juz istnial (Krok 1 fazy mailowej, batch 64). Recon przed
Etapem B wykazal w torze backfillu blad blokujacy i dwie luki.

BUG (blokujacy dla Etapu B): flush_embed_buffer lapal wylacznie
aiohttp.ClientError, a wyczerpanie ClientTimeout(total=...) rzuca goly
builtins.TimeoutError, ktory NIE jest jego podklasa (zweryfikowane empirycznie
na aiohttp 3.14.3). Zawieszona Ollama — czyli jej udokumentowany failure mode,
"przyjmuje polaczenie i milczy" — wywalala caly run nieobsluzonym wyjatkiem,
bez breakera i bez flushu threadingu. Na plastrze 50k = utrata zarobionej pracy.
Klasy przejsciowe nazwane teraz jawnie w TRANSIENT_EMBED_ERRORS.

kb-retrieval:
- embed_batch(timeout_s=...) — bound per zadanie, skalowalny z batch size
- embed_batch_resilient() — retry z backoffem wykladniczym, a po ich wyczerpaniu
  probe /api/tags rozstrzyga: backend zywy -> bisekcja izolujaca trujacy chunk
  (jeden zly tekst kosztowal caly batch 64, bo /api/embed jest all-or-nothing);
  backend martwy -> natychmiastowe gave_up bez bisekcji, ktora spalilaby 2n-1
  zadan i opoznila breaker. EmbeddingDimensionError nigdy nie jest retry'owane.
- failed_indices wyprowadzane z wyniku, nie akumulowane per span — przy gave_up
  w srodku bisekcji porzucone poddrzewo nigdy nie dochodzi do liscia.

mail-body-ingest:
- breaker liczy give-upy (backend padl), nie dowolne nieudane batche; porazka
  czesciowa przy zywym backendzie nie przesuwa licznika, bo te chunki i tak
  zlapie kolejny run przez idempotencje
- wiersze zembedowane w umierajacym batchu sa commitowane przed abortem
- parametryzacja: --batch-size/--embed-retries/--embed-backoff/--embed-timeout,
  kazdy z odpowiednikiem env MAIL_INGEST_*; bledna wartosc env = glosny SystemExit
- metryka embed_ms_per_chunk (porownywalna miedzy runami, w odroznieniu od
  sredniej per batch) + embed_requests_total/embed_calls jako sygnal zdrowia

mail-body-ingest-bench: nowy entry point, sweep batch size na realnych chunkach.
Read-only (SELECT + inferencja, zero sciezki zapisu), warmup przed pomiarem, ten
sam zbior chunkow dla kazdego rozmiaru. Czyni liczby z planu §1.4 odtwarzalnymi.

Fallback SOLARIA->PIHA dla backfillu SWIADOMIE nie powstaje (potwierdzone przez
operatora): 271k chunkow x 790 ms CPU ~ 60 h na 8 GB PIHA dzielonym z HA i
Paperlessem. Wlasciwa odpowiedzia na martwy backend jest exit 2 i wznowienie
plastra. Tor online (kb-query -> embed_router) zachowuje fallback — rozdzial
torow udokumentowany w docstringu embed.py i w kb/services/.

Testy: 117 zielonych (62 job + 22 klient embed + reszta pakietow), w tym
regresja na TimeoutError, bisekcja, ograniczony koszt przy martwym backendzie
i porazka czesciowa nieprzesuwajaca breakera. Bez uruchamiania backfillu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 12:02:41 +02:00

4.1 KiB

okf type visibility status updated links
0.1 runbook private active 2026-08-05
../services/job-mail-body-ingest.md

mail-body-ingest — uruchomienie i testy

Usage

# Dry run (default) — parse, quote-strip, classify, chunk, count. Zero Ollama calls, zero
# DB writes (including the threading UPDATE):
mail-body-ingest --dsn postgresql://kb:<pw>@piha:5433/kb --archive-root /home/oskar/kb/mail/archive

# Etap A pilot — last 12 months only (plan Decyzja 9):
mail-body-ingest --dsn ... --since 2025-07-01 --apply > mail-ingest-etapA.log 2>&1

# Etap B — full archive in 50k slices (plan §9; ORDER BY id is stable, so slices are
# reproducible, and idempotency covers their boundaries):
nice -n 10 ionice -c2 -n7 mail-body-ingest --dsn ... --apply \
    --limit 50000 --offset 0 > mail-ingest-etapB-0.log 2>&1

# Smoke-test slice:
mail-body-ingest --dsn ... --apply --limit 10

DSN can also come from KB_DSN, Ollama URL from OLLAMA_URL (default http://localhost:11434 — this job is meant to run where Ollama lives).

Embed tuning

Flag Env Default Notes
--batch-size MAIL_INGEST_BATCH_SIZE 64 /api/embed batch. 64 measured best across the corpus's length mix; short-text-heavy slices favour 128 (plan §1.4)
--embed-retries MAIL_INGEST_EMBED_RETRIES 2 Retries per batch on transport errors; 0 disables
--embed-backoff MAIL_INGEST_EMBED_BACKOFF 1.0 s Base backoff, doubled each attempt
--embed-timeout MAIL_INGEST_EMBED_TIMEOUT 120 s Per-request hard timeout — scale it with --batch-size
--max-embed-failures 5 Consecutive give-ups before aborting with exit 2; 0 disables

A malformed env value is a startup failure, not a silent fallback — a typo'd MAIL_INGEST_BATCH_SIZE must not quietly produce a multi-hour run at the wrong batch size.

Batch-size benchmark

Read-only (SELECTs + inference, no write path at all), so it is safe against the live DB:

mail-body-ingest-bench --dsn postgresql://kb:<pw>@piha:5433/kb \
    --archive-root /home/oskar/kb/mail/archive --sample-envelopes 200

# Wider sweep, capped so batch=1 doesn't dominate the wall clock:
mail-body-ingest-bench --dsn ... --sizes 1,8,32,64,128 --max-chunks 300

Prints ms/chunk, chunks/s and a projected full-corpus wall clock per batch size. Re-run it after an Ollama upgrade or on a different GPU before trusting the default --batch-size.

Tests

pip install -e "jobs/mail-body-ingest[dev]"
cd jobs/mail-body-ingest && pytest

Pure unit tests (62), no DB/Ollama — run() is tested by monkeypatching asyncpg.connect and aiohttp.ClientSession with in-memory fakes, .eml bytes written to tmp_path. Covers: quote-strip (EN/PL/Outlook markers, bare > lines), HTML->text (style/script/blockquote/ gmail_quote skipping), newsletter classification, threading extraction, prefix building, body extraction (plain-preferred, HTML fallback, attachment-only), the typed/compat32 parse fallback, stats balance, idempotency (second run inserts nothing new), newsletter chunks never reaching Ollama, dimension-mismatch abort, and the circuit breaker (trips on N consecutive give-ups, resets on a success, disabled by 0, flushes pending threading on abort). Batch-specific: a poison chunk isolated out of a full batch, a partial failure not advancing the breaker, and an Ollama hang degrading to the breaker instead of crashing the run.

The batch client itself is tested in packages/kb-retrieval/tests/test_embed.py (22) — retry with backoff, bisection, dead-backend give-up bounds, and the TimeoutError-is-not-a- ClientError regression. The benchmark's pure logic (batch splitting, derived metrics, table formatting, failure counting) is in jobs/mail-body-ingest/tests/test_benchmark.py.

In a worktree without a preinstalled venv:

python3 -m venv /tmp/kbvenv && /tmp/kbvenv/bin/pip install -q pytest pytest-asyncio \
    -e packages/kb-mail/ -e packages/kb-retrieval/ -e jobs/mail-body-ingest/
/tmp/kbvenv/bin/python -m pytest jobs/mail-body-ingest/tests/ packages/kb-retrieval/tests/ -q