librarian: napraw nieskończoną pętlę w wyszukiwaniu DOI #1

Merged
gitea merged 1 commits from fix-librarian-search-hang into main 2026-07-30 09:26:45 +00:00
Owner

Problem

search_bot używał MAXTHREADS do DWÓCH rzeczy naraz: ile chunków czytać (pliki 0..MAXTHREADS-1) I ile sentineli czekać na koniec — musiały się idealnie zgadzać. Za mało → ciche pomijanie końcowych plików; za dużo albo brakujący/nieczytelny chunk → producent ginął bez sentinela, licznik nigdy nie dobijał do progu, search_for_doi wisiał na join() na zawsze. Failsafe bezczynności był martwym kodem (if empty_counter > 5 ... elif > 10: break>10 implikuje >5, więc break nieosiągalny).

Dla zgłoszonego przypadku (MAXTHREADS=40, pliki 0–43): pliki 40–43 były cicho nieprzeszukiwane, a każdy run wskazujący na brakujący chunk zawieszał się.

Naprawa (3 warstwy)

  • auto-wykrywanie chunków (discover_chunk_files: <n>_chunk.txt numerycznie) zamiast range(0, MAXTHREADS) — czyta wszystkie obecne, nigdy nie wskazuje na nieistniejący;
  • próg = liczba faktycznie wystartowanych producentów (nie env);
  • sentinel w finally — padnięty producent też go emituje; backstop bezczynności przestawiony tak, by faktycznie działał.

MAXTHREADS → deprecated/ignorowany (docs + env zaktualizowane).

Weryfikacja

tests/unit/test_search_bot.py (4 testy, pytest-only): DOI w końcowym chunku znaleziony; nieczytelny chunk i tak kończy; pusty katalog wraca od razu; discovery sortowane numerycznie. Job unit 27 passed.

## Problem `search_bot` używał `MAXTHREADS` do DWÓCH rzeczy naraz: ile chunków czytać (pliki `0..MAXTHREADS-1`) I ile sentineli czekać na koniec — musiały się idealnie zgadzać. Za mało → ciche pomijanie końcowych plików; za dużo albo brakujący/nieczytelny chunk → producent ginął bez sentinela, licznik nigdy nie dobijał do progu, `search_for_doi` wisiał na `join()` **na zawsze**. Failsafe bezczynności był martwym kodem (`if empty_counter > 5 ... elif > 10: break` — `>10` implikuje `>5`, więc `break` nieosiągalny). Dla zgłoszonego przypadku (**MAXTHREADS=40, pliki 0–43**): pliki 40–43 były cicho nieprzeszukiwane, a każdy run wskazujący na brakujący chunk zawieszał się. ## Naprawa (3 warstwy) - **auto-wykrywanie chunków** (`discover_chunk_files`: `<n>_chunk.txt` numerycznie) zamiast `range(0, MAXTHREADS)` — czyta wszystkie obecne, nigdy nie wskazuje na nieistniejący; - **próg = liczba faktycznie wystartowanych producentów** (nie env); - **sentinel w `finally`** — padnięty producent też go emituje; backstop bezczynności przestawiony tak, by faktycznie działał. `MAXTHREADS` → deprecated/ignorowany (docs + env zaktualizowane). ## Weryfikacja `tests/unit/test_search_bot.py` (4 testy, pytest-only): DOI w końcowym chunku znaleziony; nieczytelny chunk i tak kończy; pusty katalog wraca od razu; discovery sortowane numerycznie. Job `unit` 27 passed.
gitea self-assigned this 2026-07-29 22:38:17 +00:00
gitea added 1 commit 2026-07-29 22:38:17 +00:00
librarian: stop the DOI search from hanging on a chunk-count mismatch
CI / compile (pull_request) Successful in 1m26s
CI / unit (pull_request) Successful in 1m8s
CI / integration (pull_request) Failing after 10h21m8s
CI / compile (push) Successful in 12m43s
build / build (push) Failing after 13m30s
CI / unit (push) Successful in 2m32s
CI / integration (push) Failing after 1h54m10s
1a59c9f6c5
search_bot conflated MAXTHREADS into two jobs at once - how many chunk files to
read (files 0..MAXTHREADS-1) AND how many producer sentinels to wait for - so
the two had to match exactly. Set too low it silently skipped trailing chunks;
set too high (or with any chunk missing/unreadable) a producer crashed before
emitting its sentinel, the consumers' count never reached the threshold, and
search_for_doi hung on join() forever. The idle-timeout failsafe that was meant
to break a starved consumer was dead code: `if empty_counter > 5: ... elif
empty_counter > 10: break` - >10 implies >5, so the elif never ran.

Fix, three layers:
* auto-discover the chunk files present (discover_chunk_files: <n>_chunk.txt in
  numeric order) instead of range(0, MAXTHREADS). All files are read regardless
  of count, and no producer is ever pointed at a missing file;
* the sentinel threshold is now the number of producers actually started, so it
  can't drift from what's emitted;
* producers emit their sentinel in a finally, so even a crash (missing/unreadable
  chunk) can't starve the count; and the idle backstop is reordered so it can
  actually fire (>EMPTY_LIMIT seconds) as a last resort.

MAXTHREADS is deprecated and unused (kept only so old env files don't break);
docs/env updated to say chunk files are auto-discovered.

For the reported case (MAXTHREADS=40, files 0..43): before, files 40-43 were
silently never searched, and any run that referenced a missing chunk hung
forever. After, all 44 are searched and it always terminates.

Verified in a pytest-only venv (tests/unit/test_search_bot.py): DOI in a
trailing chunk is found; an unreadable chunk still terminates; empty dir returns
at once; discovery is numeric-sorted. Full unit job 27 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea merged commit 1a59c9f6c5 into main 2026-07-30 09:26:45 +00:00
gitea deleted branch fix-librarian-search-hang 2026-07-30 09:26:49 +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#1