Trwała dostawa wyników: OUTBOX + idempotentny INBOX (wynik nie ginie) #12

Merged
gitea merged 1 commits from librarian-durable-delivery into main 2026-08-02 15:31:38 +00:00
Owner

Po co

8-godzinne wyszukanie to realne pieniądze — wynik nie może zginąć przez chwilową niedostępność bota, zły adres/API, ani restart którejkolwiek strony. Robię ścieżkę librarian→bot trwałą, at-least-once, z idempotentnym renderem.

Wspólny rdzeń

durable_queue.DiskQueue — bezzależnościowa, atomowo zapisywana (temp+os.replace) kolejka na dysku, jeden plik JSON na klucz, korumpowane pliki pomijane. Unit-testowana. Dzielona przez oba obrazy (dodane COPY durable_queue.py do Dockerfile.librarian; bot już ma COPY *.py).

Librarian (nadawca) — trwały OUTBOX

  • Gotowy wynik ląduje w OUTBOX na dysku (PVC /lib_temp_files) zanim ruszy wysyłka.
  • Wysyłka z retry + backoff; wpis znika dopiero po ACK 200 od bota.
  • Wątek resendera cyklicznie dowozi zaległe → wynik przeżywa niedostępność bota i restart librariana (OUTBOX jest na woluminie trwałym).

Bot (odbiorca) — idempotentny, trwały INBOX

  • /conjurer idempotentny: każdy wynik trafia do INBOX na dysku (PVC /data) przed ACK; kolejkuje tylko uuid, którego jeszcze nie dostarczono (duplikat → drop) ani nie ma w locie.
  • Po realnym wyrenderowaniu na Discordzie cog woła mark_delivered(uuid) → uuid zapisany w zbiorze "delivered" (przycinany do DELIVERED_MAX), INBOX wyczyszczony → resendy stają się no-opami.
  • Przy starcie bota replay INBOX-u dosyła zaakceptowane-ale-niewyrenderowane wyniki → crash bota w locie nie gubi danych. Pongi zostają ulotne (bez persystencji).

Efekt łączny

Librarian trzyma wynik aż bot potwierdzi; bot trzyma aż wynik jest na ekranie; duplikaty nigdy nie renderują się dwa razy. Razem z poprawką drogi powrotnej w deployu (deploy#8) — drogi wynik przestaje znikać.

Konfiguracja (env, opcjonalne)

CONJURER_RESULT_SEND_ATTEMPTS (3), CONJURER_RESULT_SEND_BACKOFF (2s), CONJURER_OUTBOX_RESEND_SECONDS (60), CONJURER_RESULT_INBOX/CONJURER_DELIVERED_DIR (pod CONJURER_DATA_DIR), CONJURER_DELIVERED_MAX (10000), CONJURER_LIBRARIAN_OUTBOX (pod STATE_DIR).

Testy

  • unit test_durable_queue (put/dedup/prune/atomowość/korupcja/sanityzacja klucza),
  • integration test_librarian_outbox (retry/backoff, przetrwanie outage'u, ACK-only-remove),
  • integration test_result_durable_delivery (persist-przed-ACK, dedup w locie, dedup po dostarczeniu, replay, pong nietrwały).
    Suite: 55 unit + 39 integration zielone.

Uwaga wdrożeniowa

Po restarcie bota awaiting_q jest in-memory — replayowany wynik nie trafi już do oryginalnego pytającego (ctx utracony), więc idzie na kanał zapasowy z notką. Dane są zachowane; ginie tylko routing do konkretnej osoby. To świadomy, bezpieczny kompromis.

🤖 Generated with Claude Code

## Po co 8-godzinne wyszukanie to realne pieniądze — wynik nie może zginąć przez chwilową niedostępność bota, zły adres/API, ani restart którejkolwiek strony. Robię ścieżkę librarian→bot **trwałą, at-least-once, z idempotentnym renderem**. ## Wspólny rdzeń `durable_queue.DiskQueue` — bezzależnościowa, **atomowo zapisywana** (temp+os.replace) kolejka na dysku, jeden plik JSON na klucz, korumpowane pliki pomijane. Unit-testowana. Dzielona przez oba obrazy (dodane `COPY durable_queue.py` do `Dockerfile.librarian`; bot już ma `COPY *.py`). ## Librarian (nadawca) — trwały OUTBOX - Gotowy wynik ląduje w **OUTBOX na dysku** (PVC `/lib_temp_files`) **zanim** ruszy wysyłka. - Wysyłka z **retry + backoff**; wpis znika **dopiero po ACK 200** od bota. - Wątek **resendera** cyklicznie dowozi zaległe → wynik przeżywa niedostępność bota **i** restart librariana (OUTBOX jest na woluminie trwałym). ## Bot (odbiorca) — idempotentny, trwały INBOX - `/conjurer` **idempotentny**: każdy wynik trafia do **INBOX na dysku** (PVC `/data`) **przed** ACK; kolejkuje tylko uuid, którego jeszcze **nie dostarczono** (duplikat → drop) ani nie ma **w locie**. - Po realnym wyrenderowaniu na Discordzie cog woła `mark_delivered(uuid)` → uuid zapisany w zbiorze "delivered" (przycinany do `DELIVERED_MAX`), INBOX wyczyszczony → **resendy stają się no-opami**. - Przy starcie bota **replay INBOX-u** dosyła zaakceptowane-ale-niewyrenderowane wyniki → crash bota w locie nie gubi danych. Pongi zostają ulotne (bez persystencji). ## Efekt łączny Librarian trzyma wynik aż bot potwierdzi; bot trzyma aż wynik jest na ekranie; duplikaty nigdy nie renderują się dwa razy. Razem z poprawką drogi powrotnej w deployu (deploy#8) — drogi wynik przestaje znikać. ## Konfiguracja (env, opcjonalne) `CONJURER_RESULT_SEND_ATTEMPTS` (3), `CONJURER_RESULT_SEND_BACKOFF` (2s), `CONJURER_OUTBOX_RESEND_SECONDS` (60), `CONJURER_RESULT_INBOX`/`CONJURER_DELIVERED_DIR` (pod `CONJURER_DATA_DIR`), `CONJURER_DELIVERED_MAX` (10000), `CONJURER_LIBRARIAN_OUTBOX` (pod STATE_DIR). ## Testy - unit `test_durable_queue` (put/dedup/prune/atomowość/korupcja/sanityzacja klucza), - integration `test_librarian_outbox` (retry/backoff, przetrwanie outage'u, ACK-only-remove), - integration `test_result_durable_delivery` (persist-przed-ACK, dedup w locie, dedup po dostarczeniu, replay, pong nietrwały). Suite: **55 unit + 39 integration** zielone. ## Uwaga wdrożeniowa Po restarcie **bota** awaiting_q jest in-memory — replayowany wynik nie trafi już do oryginalnego pytającego (ctx utracony), więc idzie na kanał zapasowy z notką. Dane są zachowane; ginie tylko routing do konkretnej osoby. To świadomy, bezpieczny kompromis. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea self-assigned this 2026-08-02 15:17:50 +00:00
gitea added 1 commit 2026-08-02 15:17:50 +00:00
Durable result delivery: OUTBOX + idempotent INBOX so results never die
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 18s
CI / integration (pull_request) Successful in 19s
cb2c55b791
An 8h search result must survive a transient bot outage, an api/address
misroute, or a restart of either side. Make the librarian->bot result
path durably at-least-once with idempotent rendering:

Shared: durable_queue.DiskQueue - a dependency-free, atomically-written,
one-file-per-key disk queue (unit-tested), shared by both images
(added to Dockerfile.librarian; the bot already COPYs *.py).

Librarian (sender): finished results go to a persistent OUTBOX before
sending; delivery retries with backoff; an entry is removed only on a
positive ACK; a resender thread keeps flushing the OUTBOX, so a result
survives a bot outage AND a librarian restart (OUTBOX is on the state
volume) - it simply keeps trying until acked.

Bot (receiver): /conjurer is now idempotent and durable - each result is
persisted to an INBOX before acking and only queued if its uuid was not
already delivered (dropped as a duplicate) or already pending. Once the
cog actually renders it, mark_delivered() records the uuid and clears the
inbox, so the librarian's resends become no-ops. On startup the bot
replays any accepted-but-unrendered result from the INBOX, so a bot crash
mid-flight doesn't lose it. Pongs stay ephemeral.

Together: the librarian keeps a result until the bot confirms it; the bot
keeps it until it is on screen; duplicates never double-render. Combined
with the deploy return-path fix, an expensive result no longer vanishes.

Tests: unit test_durable_queue; integration test_librarian_outbox
(retry/backoff, resend survives outage) and test_result_durable_delivery
(persist, dedup pending, dedup delivered, replay, pong not persisted).
Suite: 55 unit + 39 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea merged commit 44b7298a15 into main 2026-08-02 15:31:38 +00:00
gitea deleted branch librarian-durable-delivery 2026-08-02 15:31:38 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea/conjurer#12