fix(paperless-worker): celery command bypassed manage.py + missing shared scratch dir
Two config bugs found on the already-deployed split-host OCR worker (module 3): (1) `command: celery ...` was routed through manage.py by the image entrypoint because it didn't start with "/" — fixed with an absolute gosu+celery path. (2) SCRATCH_DIR (/tmp/paperless) was not shared over NFS like data/media/consume, so tasks picked up by worker@SOLARIA instead of worker@PIHA failed with "File not found" — fixed by adding a paperless_scratch NFS volume/bind mount on both sides. Verified live on PIHA + SOLARIA: test PDFs dropped into consume/ were split across both workers, the SOLARIA-picked task completed OCR with zero File not found errors, test documents cleaned up afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
52e412dba3
commit
196a99ffef
|
|
@ -602,3 +602,44 @@ już nie zna → błąd → `set -e` przerywa deploy w połowie.
|
|||
recreate (`docker compose down --remove-orphans` albo jawne usunięcie hash-prefixed
|
||||
kontenerów), ewentualnie jawny `-p` (COMPOSE_PROJECT_NAME) żeby nazwy były deterministyczne.
|
||||
Deploy mózgu NIE MOŻE zostawiać control-plane w stanie zero-kontenerów.
|
||||
|
||||
## Fix: paperless-worker@SOLARIA — dwa bugi configu, naprawione i zweryfikowane na żywo (2026-07-12)
|
||||
|
||||
**Kontekst.** Moduł 3 (`services/paperless-worker/`, split-host OCR worker) był
|
||||
zdeployowany i brał zadania z kolejki, ale miał dwa bugi w compose:
|
||||
|
||||
1. **`command: celery ...` nie odpalał celery.** Obraz paperless-ngx
|
||||
(`/sbin/docker-entrypoint.sh`) routuje każdy argument NIE zaczynający się
|
||||
od `/` do `manage.py` — więc `celery` lądował jako nieznana subkomenda
|
||||
Django, nie jako program. Fix: `command: /usr/sbin/gosu paperless
|
||||
/usr/local/bin/celery ...` (ścieżka absolutna wchodzi w gałąź `exec "$@"`
|
||||
entrypointu; `gosu paperless` z przodu bo ta gałąź nie dostaje automatycznego
|
||||
gosu, inaczej proces poszedłby jako root i zepsuł właściciela plików na NFS).
|
||||
2. **Brak współdzielonego `SCRATCH_DIR`.** Paperless@PIHA staguje wgrywany
|
||||
plik w `/tmp/paperless` (domyślny `SCRATCH_DIR`) i niesie tę ścieżkę w
|
||||
payloadzie zadania celery jako ścieżkę absolutną. Worker@SOLARIA miał
|
||||
własny, lokalny `/tmp/paperless` — gdy odbierał zadanie zamiast workera
|
||||
PIHA, padał `Cannot consume ...: File not found`. Fix: NFS volume
|
||||
`paperless_scratch` (ten sam wzorzec co `data/media/consume`) na
|
||||
`/opt/homelab/data/paperless/scratch` (już istniał na PIHA, `chown 1000:1000`),
|
||||
mount na `/tmp/paperless` po obu stronach.
|
||||
|
||||
Oba fixy + uzasadnienie: `services/paperless/docker-compose.yml`,
|
||||
`services/paperless-worker/docker-compose.yml`, `services/paperless-worker/README.md`.
|
||||
Zweryfikowane end-to-end na żywo (branch `task/paperless-worker-fix`, jeszcze
|
||||
niezmergowany do master w momencie pisania tego wpisu): 3 dokumenty testowe
|
||||
wrzucone do `consume/` na PIHA, jeden odebrany i dokończony przez worker@SOLARIA
|
||||
(log: `ocrmypdf`/`tesseract` → `ConsumeTaskPlugin completed with: Success`),
|
||||
zero `File not found`. Dokumenty testowe usunięte po teście (`document.delete()`
|
||||
+ ręczny cleanup plików) — produkcyjne 6 dokumentów nietknięte.
|
||||
|
||||
**Otwarte (świadomie odłożone, nie blokuje działania):**
|
||||
- `hosts/solaria/services.yaml` i `inventory/topology.yaml` nie mają wpisu
|
||||
`paperless-worker` (SOLARIA ma tam tylko `node-agent`) — było zaplanowane w
|
||||
cutover checkliście README jako krok "przy deployu", ale nigdy nie zrobione.
|
||||
Bez tego wpisu supervisor/observer nie widzą tego serwisu w desired-state —
|
||||
drift (np. worker padnie i nie wstanie) nie zostanie automatycznie wykryty
|
||||
przez agent system, tylko przez brak przetwarzania kolejki.
|
||||
- Test formalnego fallbacku (stop worker@SOLARIA → kolejka mieli na PIHA →
|
||||
start → drenaż) nie był wykonany w tej sesji — mechanizm nie zmienił się
|
||||
tym fixem (był już OK), ale warto zweryfikować przy okazji.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ Ten sam obraz co `services/paperless/`, ale z nadpisanym poleceniem — działa
|
|||
ze storage widzianym przez NFS z PIHA. Realizuje wzorzec kb-02
|
||||
„serwis always-on + compute-worker on-demand z fallback".
|
||||
|
||||
**Status (2026-07-12): ZDEPLOYOWANY I DZIAŁA.** Worker bierze zadania z kolejki
|
||||
razem z workerem PIHA, kończy OCR bez błędów, dokumenty trafiają do Paperless
|
||||
niezależnie od tego, który worker odebrał zadanie. Zweryfikowane end-to-end:
|
||||
kilka dokumentów testowych wrzuconych do `consume/` na PIHA, część odebrana
|
||||
przez worker@SOLARIA (log: `Consuming ...` → `ocrmypdf`/`tesseract` →
|
||||
`ConsumeTaskPlugin completed with: Success` → dokument widoczny w DB), zero
|
||||
`File not found`. Dwa bugi znalezione i naprawione po drodze — patrz sekcje
|
||||
„Gotcha: `command` i entrypoint obrazu" i „SCRATCH_DIR" niżej.
|
||||
|
||||
## Wynik badania: czy split-host worker jest wykonalny?
|
||||
|
||||
**TAK — z zastrzeżeniami.** Ustalenia (GH discussion #3900 + docs):
|
||||
|
|
@ -50,6 +59,55 @@ ze storage widzianym przez NFS z PIHA. Realizuje wzorzec kb-02
|
|||
- **Wersje obrazów muszą być identyczne** (PIHA i SOLARIA) — wspólny schemat
|
||||
DB i sygnatury zadań. Podbijać oba compose naraz.
|
||||
|
||||
## Gotcha: `command` i entrypoint obrazu
|
||||
|
||||
Nadpisanie `command:` w compose na `celery --app paperless worker ...` (jak
|
||||
sugeruje GH discussion #3900 i punkt 1 wyżej) **nie działa** — pada
|
||||
`Unknown command: 'celery'`. Przyczyna w `/sbin/docker-entrypoint.sh` obrazu:
|
||||
|
||||
```bash
|
||||
if [[ "$1" != "/"* ]]; then
|
||||
exec gosu paperless python3 manage.py "$@" # argv[0] bez "/" -> manage.py
|
||||
else
|
||||
exec "$@" # argv[0] zaczyna się od "/" -> exec wprost
|
||||
fi
|
||||
```
|
||||
|
||||
Każdy argument NIE zaczynający się od `/` jest traktowany jako nazwa komendy
|
||||
Django i lądowany w `manage.py <arg>` — stąd `celery` jest interpretowane jako
|
||||
nieznana subkomenda `manage.py`, a nie program do uruchomienia. Fix: ścieżka
|
||||
absolutna, żeby wejść w gałąź `exec "$@"`, plus jawne `gosu paperless` z
|
||||
przodu (bo ten branch NIE dostaje automatycznego gosu jak branch manage.py) —
|
||||
inaczej proces poszedłby jako root, co psuje właściciela plików na NFS
|
||||
(musi być uid 1000, patrz "UID mapping" niżej):
|
||||
|
||||
```yaml
|
||||
command: /usr/sbin/gosu paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --concurrency ${WORKER_CONCURRENCY:-4}
|
||||
```
|
||||
|
||||
Ścieżki zweryfikowane w obrazie `ghcr.io/paperless-ngx/paperless-ngx:2.14`:
|
||||
`gosu` = `/usr/sbin/gosu`, `celery` = `/usr/local/bin/celery`, user `paperless`
|
||||
= uid/gid 1000.
|
||||
|
||||
## SCRATCH_DIR — musi być współdzielony tak samo jak data/media/consume
|
||||
|
||||
Paperless zapisuje wgrywany plik do katalogu roboczego przed konsumpcją —
|
||||
`SCRATCH_DIR`, domyślnie `/tmp/paperless` (paperless-ngx:
|
||||
`tempfile.gettempdir() / "paperless"` w `src/paperless/settings.py`, gdy
|
||||
`PAPERLESS_SCRATCH_DIR` nie jest ustawione — nie trzeba go więc ustawiać
|
||||
jawnie, domyślna wartość już pasuje do mount pointu poniżej). Payload zadania
|
||||
celery niesie ścieżkę do tego pliku jako ścieżkę **absolutną**. Ponieważ
|
||||
worker@PIHA i worker@SOLARIA dzielą jedną kolejkę, ten, który akurat odbierze
|
||||
zadanie, musi widzieć dokładnie ten sam plik pod dokładnie tą samą ścieżką —
|
||||
identyczna zasada jak dla `data/media/consume` (patrz komentarz w
|
||||
`services/paperless/docker-compose.yml`). To zostało pominięte przy
|
||||
pierwszym wdrożeniu modułu 3: dokumenty konsumowane na PIHA, ale odebrane do
|
||||
OCR przez SOLARIĘ, padały z `Cannot consume ...: File not found`, bo SOLARIA
|
||||
miała własny, lokalny, pusty `/tmp/paperless`. Fix: NFS-owy wolumen
|
||||
`paperless_scratch` (ten sam wzorzec co `paperless_data/media/consume`) na
|
||||
katalog `/opt/homelab/data/paperless/scratch` (już istnieje na PIHA,
|
||||
`chown 1000:1000`), montowany na `/tmp/paperless` po obu stronach.
|
||||
|
||||
## NFS: export na PIHA, mount na SOLARIA
|
||||
|
||||
Transfer idzie po **LAN** (PIHA `192.168.31.5` ↔ SOLARIA `192.168.31.70`,
|
||||
|
|
@ -90,11 +148,18 @@ Weryfikacja po deployu: `./healthcheck.sh` robi test zapisu na mount.
|
|||
|
||||
## Cutover checklist (przy deployu modułu 3 — po działającym module 2)
|
||||
|
||||
1. `services/paperless/` działa na PIHA (healthcheck zielony).
|
||||
2. Export NFS na PIHA (wyżej) + `showmount -e 192.168.31.5` z SOLARII.
|
||||
3. `nfs-common` na SOLARII.
|
||||
4. `.env` z `env.example` — sekrety SKOPIOWANE z PIHA, nie nowe.
|
||||
5. `docker compose up -d` + `./healthcheck.sh`.
|
||||
6. Test: wrzucić PDF do consume na PIHA → obciążenie CPU na SOLARII, nie PIHA.
|
||||
7. Test fallbacku: stop workera → zadanie czeka/mieli PIHA → start → drenaż.
|
||||
8. Wpis w `hosts/solaria/services.yaml` + topology (dopiero przy deployu).
|
||||
1. ✅ `services/paperless/` działa na PIHA (healthcheck zielony).
|
||||
2. ✅ Export NFS na PIHA (wyżej) + `showmount -e 192.168.31.5` z SOLARII.
|
||||
3. ✅ `nfs-common` na SOLARII.
|
||||
4. ✅ `.env` z `env.example` — sekrety SKOPIOWANE z PIHA, nie nowe.
|
||||
5. ✅ `docker compose up -d` + `./healthcheck.sh`.
|
||||
6. ✅ Test (2026-07-12): PDF-y wrzucone do consume na PIHA, część odebrana i
|
||||
dokończona przez worker@SOLARIA (dowód w logach, zero File not found).
|
||||
7. ⬜ Test fallbacku: stop workera → zadanie czeka/mieli PIHA → start → drenaż
|
||||
(jeszcze niewykonany formalnie, ale mechanizm nie zmienił się tym fixem —
|
||||
fallback na PIHA działał już wcześniej, patrz sekcja "Fallback" wyżej).
|
||||
8. ⬜ **OTWARTE**: wpis `paperless-worker` w `hosts/solaria/services.yaml` +
|
||||
`inventory/topology.yaml` (obecnie SOLARIA ma tam tylko `node-agent`) —
|
||||
bez tego supervisor/observer nie widzą tego serwisu w desired-state, więc
|
||||
drift między `hosts/solaria/services.yaml` a rzeczywistością nie jest
|
||||
wykrywany. Patrz `docs/backlog.md`.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,19 @@ services:
|
|||
# install, then execs this instead of the full s6 service tree.
|
||||
# OCR (ocrmypdf/tesseract) is CPU-bound — the win here is SOLARIA's
|
||||
# 24 cores, not the GPU (stock paperless OCR does not use CUDA).
|
||||
command: celery --app paperless worker --loglevel INFO --concurrency ${WORKER_CONCURRENCY:-4}
|
||||
#
|
||||
# MUST be an absolute path. /sbin/docker-entrypoint.sh branches on
|
||||
# argv[0]: anything NOT starting with "/" is treated as a Django
|
||||
# management-command name and handed to `manage.py` (`exec gosu paperless
|
||||
# python3 manage.py "$@"`) — so a bare `celery ...` command fails with
|
||||
# "Unknown command: 'celery'". An absolute path takes the `else exec "$@"`
|
||||
# branch instead, running celery directly. `gosu paperless` in front
|
||||
# replaces the implicit gosu the manage.py branch would have applied, so
|
||||
# the process still runs as uid 1000 (paperless), not root — required so
|
||||
# files it writes on the NFS-shared storage keep the correct owner and
|
||||
# match USERMAP_UID/GID below. Verified against the image: gosu is at
|
||||
# /usr/sbin/gosu, celery at /usr/local/bin/celery.
|
||||
command: /usr/sbin/gosu paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --concurrency ${WORKER_CONCURRENCY:-4}
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
|
@ -58,6 +70,18 @@ services:
|
|||
- paperless_data:/usr/src/paperless/data
|
||||
- paperless_media:/usr/src/paperless/media
|
||||
- paperless_consume:/usr/src/paperless/consume
|
||||
# SCRATCH_DIR (default /tmp/paperless — paperless-ngx sets it to
|
||||
# tempfile.gettempdir()/"paperless" when PAPERLESS_SCRATCH_DIR is unset,
|
||||
# see src/paperless/settings.py) is where paperless@PIHA stages the
|
||||
# uploaded file before consuming it. The celery task payload carries
|
||||
# that path as an ABSOLUTE path, and this worker opens the same path on
|
||||
# its own filesystem — without a shared mount here, any task picked up
|
||||
# by SOLARIA (instead of the local PIHA worker) fails with "Cannot
|
||||
# consume ...: File not found". Must be NFS-mounted at the identical
|
||||
# container path, same reasoning as data/media/consume above. No need
|
||||
# to set PAPERLESS_SCRATCH_DIR explicitly: the default already resolves
|
||||
# to /tmp/paperless on both sides.
|
||||
- paperless_scratch:/tmp/paperless
|
||||
healthcheck:
|
||||
# Pings THIS worker through the broker — proves broker connectivity and
|
||||
# a live celery process in one shot.
|
||||
|
|
@ -91,3 +115,9 @@ volumes:
|
|||
type: nfs
|
||||
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
|
||||
device: ":/opt/homelab/data/paperless/consume"
|
||||
paperless_scratch:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: addr=192.168.31.5,rw,nfsvers=4.1,hard,timeo=150
|
||||
device: ":/opt/homelab/data/paperless/scratch"
|
||||
|
|
|
|||
|
|
@ -76,6 +76,19 @@ services:
|
|||
- /opt/homelab/data/paperless/media:/usr/src/paperless/media
|
||||
- /opt/homelab/data/paperless/consume:/usr/src/paperless/consume
|
||||
- /opt/homelab/data/paperless/export:/usr/src/paperless/export
|
||||
# SCRATCH_DIR — where paperless stages an uploaded file before
|
||||
# consuming it (default /tmp/paperless; see paperless-ngx
|
||||
# src/paperless/settings.py, tempfile.gettempdir()/"paperless"). The
|
||||
# celery task payload carries this staged file's path as an ABSOLUTE
|
||||
# path. Since the built-in worker here and the worker@SOLARIA share one
|
||||
# task queue, whichever one picks up the task must be able to open that
|
||||
# exact path — so, same rule as data/media/consume above, this MUST be
|
||||
# NFS-exported and mounted at the identical container path on both
|
||||
# hosts. Was missed when module 3 was first wired up: documents
|
||||
# consumed on PIHA but OCR'd by the SOLARIA worker failed with "Cannot
|
||||
# consume ...: File not found" because SOLARIA had its own local,
|
||||
# empty /tmp/paperless.
|
||||
- /opt/homelab/data/paperless/scratch:/tmp/paperless
|
||||
ports:
|
||||
# LAN-only bind; npm@PIHA proxies paper.kapala.org -> 192.168.31.5:8210.
|
||||
# TODO AT DEPLOY: port 8210 free per the 2026-06-30 inventory —
|
||||
|
|
|
|||
Loading…
Reference in a new issue