librarian: graceful shutdown + wznawialny stan wyszukania (przeżywa restart) #17

Merged
gitea merged 1 commits from librarian-resumable-search into main 2026-08-03 15:41:58 +00:00
Owner

Stackowane na #16 (librarian-log-hygiene) — zmerguj #16 najpierw, wtedy ten PR pokaże już tylko swój commit. Gałąź odbita od #16, więc rebase będzie trywialny.

Po co

Restart librariana (redeploy, OOM, cokolwiek) wyrzucał w kosz trwające wyszukanie i te czekające w kolejce. Teraz stan przeżywa restart — jak w Twoim daemonie do Jiry: przejęcie sygnału, ładne złożenie z zapisem stanu, wznowienie po podniesieniu.

Mechanizmy

  1. Wznawialny skan bazy (search_bot): każdy producent zapisuje watermark (tell()-cookie) per plik-chunk w miarę czytania — bezpieczny, bo search_for_doi drenuje kolejkę przed zwrotem, więc offset = to-co-przetworzone. search_for_doi bierze teraz stop_event + resume i zwraca (result_list, positions, interrupted). Przy wznowieniu producent seek-uje do zapisanego offsetu → żadna linia nie jest czytana dwa razy ani pominięta ("dotychczas znalezione" + "w pliku x jestem na offsecie y", dokładnie jak sugerowałeś).
  2. Trwała kolejka żądań: /query zapisuje przyjęte żądanie na dysk przed enqueue; replay_requests re-enqueue'uje niedokończone przy starcie. Więc nawet wyszukanie czekające w kolejce przeżywa restart (to jest ta "kolejka wyszukań", o którą pytałeś — wcześniej NIE była trwała, tylko wyniki były).
  3. Checkpointy: gdy graceful shutdown przerywa skan, zapisujemy {dois, found-so-far, per-file offsets}. Po restarcie answer_query ładuje checkpoint, pomija (już zrobione) Crossref+refine i kontynuuje skan od offsetów z pre-markiem znalezionych. Skończone/scrashowane wyszukanie zapomina request+checkpoint (żadnych poison-pill retry).
  4. Graceful shutdown z TIMEOUTEM: SIGTERM/SIGINT ustawia event → skan się checkpointuje, worker staje. Main wychodzi w ograniczonym oknie (CONJURER_LIBRARIAN_GRACEFUL_TIMEOUT, domyślnie 45s) — żadnych nieśmiertelnych zombie podów. Wymaga terminationGracePeriodSeconds >= 45 w deployu (osobny PR).

Testy

  • search_bot (unit): poprawność wznawiania — seek pomija zeskanowane, nie gubi, nie skanuje ponownie; stop_eventinterrupted.
  • test_librarian_resume_state (integration): replay żądań, forget (request+checkpoint), round-trip checkpointu, drop niereplayowalnego (anty-poison-pill).
    Suite: 58 unit + 49 integration zielone.

Uwagi

  • Faza Crossref nie jest checkpointowana (krótsza niż skan bazy); przerwanie w jej trakcie = ponowienie od Crossref (request trwały). Checkpointujemy długi skan bazy — o to chodziło.
  • Po restarcie bota ctx pytającego ginie (osobny, znany kompromis z #12) — to dotyczy dostarczenia, nie tego PR-a.

🤖 Generated with Claude Code

> **Stackowane na #16** (`librarian-log-hygiene`) — zmerguj #16 najpierw, wtedy ten PR pokaże już tylko swój commit. Gałąź odbita od #16, więc rebase będzie trywialny. ## Po co Restart librariana (redeploy, OOM, cokolwiek) wyrzucał w kosz trwające wyszukanie **i** te czekające w kolejce. Teraz stan przeżywa restart — jak w Twoim daemonie do Jiry: przejęcie sygnału, ładne złożenie z zapisem stanu, wznowienie po podniesieniu. ## Mechanizmy 1. **Wznawialny skan bazy** (`search_bot`): każdy producent zapisuje watermark (`tell()`-cookie) per plik-chunk w miarę czytania — bezpieczny, bo `search_for_doi` **drenuje kolejkę przed zwrotem**, więc offset = to-co-przetworzone. `search_for_doi` bierze teraz `stop_event` + `resume` i zwraca `(result_list, positions, interrupted)`. Przy wznowieniu producent **seek-uje** do zapisanego offsetu → żadna linia nie jest czytana dwa razy ani pominięta ("dotychczas znalezione" + "w pliku x jestem na offsecie y", dokładnie jak sugerowałeś). 2. **Trwała kolejka żądań**: `/query` zapisuje przyjęte żądanie na dysk **przed** enqueue; `replay_requests` re-enqueue'uje niedokończone przy starcie. Więc nawet wyszukanie **czekające w kolejce** przeżywa restart (to jest ta "kolejka wyszukań", o którą pytałeś — wcześniej NIE była trwała, tylko wyniki były). 3. **Checkpointy**: gdy graceful shutdown przerywa skan, zapisujemy `{dois, found-so-far, per-file offsets}`. Po restarcie `answer_query` ładuje checkpoint, **pomija** (już zrobione) Crossref+refine i kontynuuje skan od offsetów z pre-markiem znalezionych. Skończone/scrashowane wyszukanie **zapomina** request+checkpoint (żadnych poison-pill retry). 4. **Graceful shutdown z TIMEOUTEM**: SIGTERM/SIGINT ustawia event → skan się checkpointuje, worker staje. Main wychodzi w **ograniczonym oknie** (`CONJURER_LIBRARIAN_GRACEFUL_TIMEOUT`, domyślnie 45s) — **żadnych nieśmiertelnych zombie podów**. Wymaga `terminationGracePeriodSeconds >= 45` w deployu (osobny PR). ## Testy - `search_bot` (unit): poprawność wznawiania — seek pomija zeskanowane, nie gubi, nie skanuje ponownie; `stop_event` → `interrupted`. - `test_librarian_resume_state` (integration): replay żądań, forget (request+checkpoint), round-trip checkpointu, drop niereplayowalnego (anty-poison-pill). Suite: **58 unit + 49 integration** zielone. ## Uwagi - Faza Crossref nie jest checkpointowana (krótsza niż skan bazy); przerwanie w jej trakcie = ponowienie od Crossref (request trwały). Checkpointujemy długi skan bazy — o to chodziło. - Po restarcie **bota** ctx pytającego ginie (osobny, znany kompromis z #12) — to dotyczy dostarczenia, nie tego PR-a. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
gitea self-assigned this 2026-08-02 20:10:28 +00:00
gitea added 2 commits 2026-08-02 20:10:29 +00:00
Librarian: drop write-only result dumps + tame search logging
CI / compile (pull_request) Successful in 9s
CI / unit (pull_request) Successful in 23s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 33s
CI / compile (push) Successful in 13s
CI / unit (push) Successful in 33s
CI / integration (push) Successful in 27s
c5643aa28f
Two hygiene fixes on top of the work-queue OOM bound:

Result dumps: cr_results / rr_results / s_results.json were write-only
(nothing reads them) yet accumulated EVERY search forever and json.load'd
the whole growing file on each write - unbounded RAM and PVC growth, and
for a deep search the raw cr_results dump is hundreds of MB. They are now
off by default (CONJURER_LIBRARIAN_DEBUG_DUMPS) and, when enabled, are
overwritten with just the latest search - never loaded or accumulated.
not_in_db.json is untouched: it's a real queue the scraper drains.

Search logging: search_bot logged via print(), including a per-line
carriage-return progress line that flooded stdout / the log file with
millions of entries - fine for a desktop app, unreadable and bloating in
a container. All of it is now proper logging at DEBUG (with coarse
per-500k-line progress), so a normal run is quiet. The librarian log
level is configurable (CONJURER_LIBRARIAN_LOG_LEVEL, default INFO) and a
stdout handler is added so  stays useful now that the
search no longer prints straight to stdout. Set DEBUG for full verbosity.

Also: make test_result_delivery_contract hermetic (point the durable
spool at a temp dir so it can't pollute or be poisoned by the real
result_inbox/ between runs) and gitignore the runtime spool dirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Librarian: graceful shutdown with resumable search state
CI / compile (pull_request) Successful in 10s
CI / unit (pull_request) Successful in 28s
CI / integration (pull_request) Successful in 27s
build / build (push) Successful in 42s
CI / compile (push) Successful in 10s
CI / unit (push) Successful in 26s
CI / integration (push) Successful in 25s
ac16b77f56
A restart of the librarian used to throw away an in-flight search (and any
searches still queued). Now search state survives a restart:

* Resumable DB scan (search_bot): each producer records a tell()-cookie
  watermark per chunk file as it goes (safe because search_for_doi drains
  the work queue before returning), and can seek back to it. search_for_doi
  now takes stop_event + resume and returns (result_list, positions,
  interrupted).

* Persisted requests: /query writes the accepted request to a disk queue
  before enqueuing; replay_requests re-enqueues unfinished ones on startup.
  So even a search still waiting in the queue survives a restart.

* Checkpoints: when a graceful shutdown interrupts a scan, the librarian
  writes {dois, found-so-far, per-file offsets}. On restart answer_query
  loads it, skips the (already done) Crossref+refine, and continues the
  scan from the saved offsets with the found DOIs pre-marked - no line is
  read twice and none is missed. A finished or crashed search forgets its
  request+checkpoint (no poison-pill replay).

* Graceful shutdown: SIGTERM/SIGINT set a shutdown event; the running scan
  checkpoints and the worker stops. The main thread then exits within a
  BOUNDED window (CONJURER_LIBRARIAN_GRACEFUL_TIMEOUT, default 45s) so the
  pod can never become an un-killable zombie. Needs terminationGracePeriod
  >= that in the deploy (separate PR).

Tests: search_bot resume correctness (seek past scanned, don't miss/re-scan;
stop_event -> interrupted) and librarian state mechanics (request replay,
forget, checkpoint round-trip, poison-pill drop). Suite: 58 unit + 49
integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea merged commit ac16b77f56 into main 2026-08-03 15:41:58 +00:00
gitea deleted branch librarian-resumable-search 2026-08-03 15:41:58 +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#17