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>
4.1 KiB
| okf | type | visibility | status | updated | links | |
|---|---|---|---|---|---|---|
| 0.1 | runbook | private | active | 2026-08-05 |
|
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