Kiedy coś nie działa
Awarie, które naprawdę się zdarzyły, a większość z nich wygląda na inny problem, niż jest.
Server nie startuje
The database has N pending migration(s)
Schemat jest za obrazem, a temu Serverowi nie powiedziano, że wolno mu to
naprawić. Ustaw MIGRATE_ON_START=true w .env — po zrobieniu kopii — albo
zastosuj migracje samodzielnie przed startem.
To mówi też świeża instalacja, jeśli przełącznik jest wyłączony: pusta baza ma wszystkie migracje oczekujące.
Kończy pracę, narzekając na Forwarded
TRUSTED_PROXY_NETWORKS jest nieustawione. Server odmawia startu, dopóki nie
wskażesz mu, czyim informacjom o adresie odwiedzającego ma wierzyć. none to
pełna odpowiedź dla Servera osiąganego bezpośrednio.
Nazywa sieć, której nie napisałeś
Napisałeś CIDR z ustawionymi bitami hosta — 172.28.0.5/24. .NET normalizuje to
do 172.28.0.0/24 bez słowa, co zamienia „jedną maszynę” w „całą pracownię”,
więc Server odmawia i nazywa adres, który powinieneś był napisać.
preflight.sh łapie to wcześniej.
No storage is configured
STORAGE_KIND jest puste albo z literówką. Wartości to postgres, filesystem
albo s3.
Nazywa ustawienie magazynu, które jest puste
filesystem potrzebuje STORAGE_PATH; s3 potrzebuje STORAGE_ENDPOINT,
STORAGE_BUCKET, STORAGE_ACCESS_KEY i STORAGE_SECRET_KEY — wszystkich
czterech. Server odmawia startu, nazywając to, którego mu zabrakło, a
preflight.sh odmawia jeszcze wcześniej. Przy STORAGE_KIND=postgres żadne
z nich nie jest czytane i dlatego wszystkie są puste.
API odpowiada 404, a jesteś pewien ścieżki
/health nie jest endpointem zdrowia. Jest nim /api/v1/health.
Każda instalacja serwuje API pod /api/v1, a Server odpowiada 404 w gołym
korzeniu celowo — żeby Client wskazany na złą ścieżkę został poprawiony,
a nie obsłużony. Monitor odpytujący /health zgłasza instalację jako
niedostępną, choć ta serwuje bez zarzutu.
Przez dołączony nginx jest gorzej
/health nie jest tam nawet 404. location / kieruje na Client, który dla
każdej nieznanej ścieżki serwuje index.html, więc curl https://twoja.domena/health zwraca 200 i stronę HTML. To router aplikacji,
nie API.
/admin odpowiada 404 mimo poprawnego tokenu
I tak ma być. /api/v1/admin/** wymaga, żeby żądanie dotarło na własny
interfejs pętli zwrotnej Servera, a:
- przez nginx dociera jako brama mostka — 404;
- przez opublikowane
127.0.0.1:8080też dociera jako brama mostka, bo tak działa opublikowany port — 404.
Każda odmowa to celowo to samo 404, więc nic nie odróżnia złego tokenu od złej trasy. Nie 403: 403 potwierdziłoby, że endpoint tam jest i że wywołujący trafił jedną z dwóch połówek.
Wchodzi się tak:
docker compose exec -T server aj-admin statusJeśli odpowie AJ_Admin__Token is not set in this container, token był pusty,
gdy kontener startował. Ustaw go w .env i odtwórz Server.
Nikt nie może administrować instalacją
Zrestartuj Server. Przy każdym starcie sprawdza on, czy w instalacji jest
jeszcze aktywne nadanie systemowe z kluczem system:administrator —
czyjekolwiek, nie tylko na koncie zakładanym przy pierwszym starcie. Jeśli nie
ma żadnego, przywraca kontu admin uprawnienia z szablonu admin:
docker compose restart serverW logu pojawia się wtedy Nobody administered this installation, so admin has been granted system:administrator again. Its password is unchanged. W tym
ostatnim zdaniu mieści się całe zastrzeżenie: restart przywraca uprawnienia, a
nie hasło. Jeśli hasła też nikt nie zna, ustaw je poleceniem
aj-admin password.
Nadania zrobionego ręcznie Server nigdy nie nadpisze. Jeżeli konto admin
ma już własne nadanie systemowe, które nie daje praw administratora — przycięte
do krótszej listy albo przestawione na zaproszone — zostaje ono bez zmian, a
log mówi grant system:administrator by hand. To jedyny przypadek, którego
restart nie naprawia.
Dziś dojście do tego stanu wymaga już bazy starszej niż sama odmowa albo edycji
wprost w bazie: odebranie instalacji ostatniego nadania z
system:administrator — przez jego usunięcie, przepisanie bez tego klucza albo
przestawienie na zaproszone — kończy się odpowiedzią 403 z kodem
grant.administrator.last. aj-admin nie ma żadnego polecenia do nadań i
dlatego jedyną drogą jest restart.
Nic nie jest oceniane
Od najczęstszej przyczyny:
- Runner nie został zatwierdzony. Jego log mówi
waiting: this Runner has not been approved yeti będzie to mówił zawsze. Zatwierdź go w panelu pod Runnery. - Obrazy sandboxa nie istnieją. Runner szuka wkompilowanych domyślnych
algojudge/lang-*:local, które ma tylko host deweloperski.compose.yamlustawia wszystkie cztery zmienneAJ_Sandbox__Image__*zREGISTRYiRUNNER_TAG; jeśli je nadpisałeś, sprawdźdocker images. RUNNER_WORK_DIRjest zły. To najpaskudniejszy przypadek, bo zawodzi po cichu: demon Dockera dostaje tę ścieżkę wprost, a ścieżka, której nie potrafi otworzyć, daje pusty katalog, a nie błąd. Każde zlecenie działa wtedy na niczym. Musi być bezwzględna i musi być ścieżką, którą potrafi otworzyć demon, a nie tylko Twoja powłoka.- Tagi. Runner z ustawionym
RUNNER_TAGSwypada z puli ogólnej, a praca bez tagów idzie do puli ogólnej. Puste znaczy pulę ogólną po obu stronach. Tagi są czytane raz, przy pierwszej rejestracji — późniejsza zmiana zmiennej nie robi nic, a zmienia je ponowna rejestracja.
Każde zlecenie zawodzi z carries no /usr/local/bin/aj-shim
Runner mówi to o jednym z czterech obrazów językowych i ma rację: aj-shim jest
tym, co czyni oceniany przebieg mierzalnym, więc obraz sprzed jego powstania nie
oceni niczego.
Host ma starą kopię. Milczą o tym dwie rzeczy, obie zamierzone: docker compose pull nie pobiera obrazów językowych — to wartości przekazywane
Runnerowi, a nie usługi — a Runner pobiera obraz wyłącznie wtedy, gdy hosta nie
ma go wcale, nigdy dlatego, że istnieje nowszy.
docker pull ghcr.io/algojudge/lang-gcc:0 # oraz clang, python, pypy
docker compose restart runner-1 # każdy runner-N, który uruchamiaszRestart nie jest opcjonalny
Runner bada każdy obraz raz, przy pierwszym użyciu, i pamięta odpowiedź przez całe życie procesu. Ten, który już uznał, że obraz nie ma shima, będzie zawodził na każdym zleceniu także wobec poprawionej kopii — dopóki nie powstanie na nowo.
scripts/update.sh pobiera wszystkie cztery, więc instalacja utrzymywana nim
tego nie spotka.
Nic zewnętrznego nie jest oceniane
Lista jest inna, bo zwykłe przyczyny tutaj nie występują. Dwie pierwsze opisuje Zewnętrzny sędzia i obie zostawiają pustą kolejkę przy kontenerze, który wygląda zdrowo: ocenianie zewnętrzne jest w tej instalacji wyłączone oraz zewnętrzny Runner nie został zatwierdzony.
Pozostałe trzy:
- Zadanie nie powstało jako zewnętrzne. Musi mieć typ archiwum i znacznik zewnętrznego; zadanie typowane lokalnie nigdy nie trafia do Runnera, który przekazuje dalej.
propswersji nie podaje numeru zadania w archiwum. To zostaje odrzucone po nazwie, zanim cokolwiek opuści tę instalację.- Tagi, dokładnie jak przy każdym Runnerze, czytane raz przy pierwszej rejestracji.
Runner restartuje się co kilka sekund
restart: unless-stopped zamienia każdą odmowę przy starcie w pętlę,
a preflight.sh większości z nich nie zobaczy, bo z konfiguracją wszystko jest
w porządku. Czytaj log:
docker compose logs external-runner | tail -20
docker compose logs runner-1 | tail -5Konto w systemie oceniającym jest puste albo błędne. Zewnętrzny Runner
odmawia już przy czytaniu konfiguracji — przed kluczem tożsamości, przed
rejestracją — i nazywa ustawienie. Przy włączonym profilu preflight.sh odmawia
wcześniej, więc stos podnoszony skryptami nigdy tu nie dochodzi. Spacja na
początku albo na końcu hasła nie jest przyczyną: hasło idzie dalej dokładnie
tak, jak je zapisano, i to celowo, bo obcięcie spacji raz już zepsuło logowanie.
the Server refused with 403: runner.nonce.unknown to inna usterka o tym
samym objawie i dotyczy obu Runnerów: ten obraz jest starszy niż Server, u
którego się rejestruje. Pojawia się dopiero od drugiego startu, bo to wolumen
tożsamości sprawia, że Server rozpoznaje klucz, a odmowy rejestracji Runner nie
ponawia. Pobieraj oba obrazy z tego samego wydania, zamiast pozwalać ruchomym
tagom się rozjechać — dlaczego muszą chodzić razem, mówi Aktualizacja i
wycofanie.
Nazwane ustawienie odpytywania albo dzierżawy to trzecia przyczyna: Runner
sprawdza swoje odstępy nawzajem, zanim cokolwiek zrobi. Żadnej z nich nie da się
osiągnąć wartościami, które dostarcza ten stos — biorą się z własnego .env.
AJ_Poll__WaitSeconds powyżej 300 jest odrzucane, bo 300 to własny sufit
Servera, a Runner odróżnia trzymane pobranie pracy od natychmiastowej odpowiedzi
po czasie, jaki minął — czytałby więc każde trzymane jako puste. Żeby odpytywać
rzadziej, służy AJ_Poll__MaxSeconds.
Archiwum twierdzi, że takie konto nie istnieje
Sprawdź nazwę użytkownika w samym archiwum — to ta sama, którą podaje się przy
logowaniu. Wyszukiwanie odmawia wprost, gdy identyfikator użytkownika wynosi 0,
bo dla nieznanej nazwy archiwum odpowiada właśnie 0, a 0 daje się sparsować
jako liczbę: zanim ta odmowa powstała, każde odpytanie po cichu nie zwracało
niczego.
Permission denied (os error 13) w Runnerze
Dwie różne przyczyny dają to jedno zdanie, a ono nie nazywa żadnej z nich:
komunikat przychodzi z głębi klienta HTTP albo z warstwy sandboxa, bez ścieżki
i bez numeru. preflight.sh sprawdza obie.
Pierwsza: gniazdo demona. DOCKER_GID nie zgadza się z grupą, do której
należy gniazdo.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock alpine stat -c '%g' /var/run/docker.sockNa Docker Desktop to 0; na hoście linuksowym to grupa docker. preflight.sh
uruchamia dokładnie to i ostrzega przy niezgodności — pytając kontener, a nie
Twoją powłokę, bo pod Docker Desktop i WSL gniazdo widziane przez tę powłokę nie
jest tym, które obsługuje demon. Usługa Runnera działa jako root, więc jest to
przyczyną tylko tam, gdzie ktoś usunął to z compose.yaml — wtedy Runner
zawodzi przy starcie i nigdy nie bierze żadnego zlecenia.
Druga: kontener zadania nie potrafi odczytać RUNNER_WORK_DIR. Runner
zapisuje tam pliki zgłoszenia jako root, a każdy kontener zadania montuje ten
katalog tylko do odczytu i czyta go jako uid 65534. Katalog ręcznie zamknięty
przed innymi przechodzi próbę Runnera i nie przechodzi próby zadania, więc
gniazdo działa bez zarzutu, Runner rejestruje się, bierze zlecenie — i każde bez
wyjątku zawodzi natychmiast, co właśnie odróżnia te dwie przyczyny.
sudo chmod 755 /srv/algojudge/runner-workKażde zlecenie zawodzi, a Runner napisał coś o cgroup
O limicie czasu rozstrzyga czas procesora, odczytany z cgroup, w której uruchamiany jest sandbox. Runner, który nie potrafi jej odczytać, nie ocenia bez niej — to komunikat ze startu, a nie z pojedynczego zlecenia:
docker compose logs runner-1 | head -40| Co mówi | Co zrobić |
|---|---|
| cgroup w wersji 1 | host wstaje z hierarchią hybrydową. Dopisz systemd.unified_cgroup_hierarchy=1 do wiersza poleceń jądra i zrestartuj maszynę |
| sterownik cgroup, którego nie zna | docker info --format '{{.CgroupDriver}}' musi wypisać cgroupfs albo systemd. Cokolwiek innego znaczy, że cgroup są wyłączone |
| nie potrafi odczytać hierarchii | ktoś zmienił compose.yaml: brakuje montażu /sys/fs/cgroup, cgroup: host albo user: "0:0" w usłudze runnera |
Oba sterowniki cgroup działają i żaden nie wymaga przestawiania demona. Wpis
native.cgroupdriver w /etc/docker/daemon.json nie szkodzi i niczego nie
daje.
Werdykty są poprawne, ale nie ma pomiaru pamięci
Zdarza się to wyłącznie na hoście ze sterownikiem cgroup systemd i Runner
mówi o tym na poziomie ERROR przy każdym starcie. Jedna slice obsługuje tam
wszystkie przebiegi, więc szczyt bierze się z wyzerowania memory.peak —
interfejsu jądra, który pojawił się w Linuksie 6.12, a więc już nie
w Ubuntu 24.04, za to w Debianie 13. O limicie czasu rozstrzyga czas procesora,
więc każdy werdykt pozostaje w mocy, a szczyt samego ocenianego zgłoszenia też
nie cierpi: ten czyta się z cgroup założonej świeżo na ten przebieg. Brakuje
szczytu przebiegów, które nie są niczyim zgłoszeniem — kompilacji, programu
weryfikującego, interaktora.
uname -rNa starszym jądrze albo się z tym pogódź, albo daj demonowi sterownik
cgroupfs, gdzie każdy szczyt pochodzi ze świeżej cgroup i nie wymaga niczego
poza Linuksem 5.19:
// /etc/docker/daemon.json
{ "exec-opts": ["native.cgroupdriver=cgroupfs"] }nginx nie startuje
Uruchom ten sam test co CI:
docker run --rm --add-host server:127.0.0.1 --add-host client:127.0.0.1 \
-v "$PWD/nginx/algojudge.conf:/etc/nginx/conf.d/default.conf:ro" \
-v "$PWD/nginx/snippets:/etc/nginx/snippets:ro" \
-v "$PWD/certs:/etc/nginx/certs:ro" \
nginx:1.30-alpine nginx -t--add-host jest potrzebne, bo nginx rozwiązuje każdy upstream przy parsowaniu
konfiguracji, więc poza siecią Compose zawodzi z host not found in upstream "server:8080" na konfiguracji, która jest zupełnie poprawna.
Zdarzyły się tu naprawdę dwa takie przypadki: zduplikowany
proxy_read_timeout, bo include wstawia dyrektywę do bloku włączającego,
a nginx odrzuca duplikaty wprost, zamiast nadpisywać; oraz brakujący certyfikat,
bo certs/ było puste.
PostgreSQL nie startuje po aktualizacji
Wersja 18 przeniosła miejsce danych. PGDATA to
/var/lib/postgresql/18/docker, a wolumen należy o poziom wyżej, do
/var/lib/postgresql. Każdy poradnik napisany przed 18 mówi
/var/lib/postgresql/data, a zamontowanie tego sprawia, że kontener odmawia
startu.
compose.yaml robi to poprawnie. Wersja główna jest przypięta celowo —
nieprzypięty postgres:latest przechodzący na 18 to sposób, w jaki to
znaleziono.
docker compose ps nie pokazuje zdrowia Runnera
Oba Runnery mają pustą kolumnę zdrowia i mają ją zawsze; w żadnym z nich nie
jest to usterka. Obraz Runnera nie deklaruje testu zdrowia, a obraz zewnętrznego nie może
go mieć — nie ma powłoki, nie ma portu, nie ma o co zapytać. Dlatego
docker compose up --wait uznaje obu za gotowych, gdy tylko działają, a działają
także wtedy, gdy jeden z nich nie potrafi się zalogować. Czytaj ich logi.
Stos stoi, a przeglądarka nic nie pokazuje
- Ostrzeżenie o certyfikacie jest oczekiwane, jeśli uruchomiłeś
render-tls.sh: jest samopodpisany. - Pusta strona i 404 na
/assets/…: obraz Client wymieniono bez Servera albo proxy cache'ujeindex.html. Jest serwowany zno-storedokładnie dlatego. - Strona przerwy technicznej: Server jest w oknie. Uruchom
./scripts/maintenance.sh status, a jeśli tak być nie powinno —off.
Skrypt mówi, że działa inny
another AlgoJudge maintenance script is running (pid 1234, lock: …/state/algojudge.lock.d)Jeśli pid 1234 naprawdę działa, poczekaj. Jeśli nie, następne uruchomienie samo
przejmie blokadę i to powie — blokada nigdy nie zostaje zakleszczona na stałe.
Żeby wyczyścić ją ręcznie, usuń state/algojudge.lock.d.
Kopia nie jest kompletną kopią
backup.sh ostrzega przy każdym uruchomieniu, gdy STORAGE_KIND nie jest
postgres, a .meta obok każdego zrzutu zapisuje to jako
coverage=INCOMPLETE. Jeśli odtworzysz sam taki zrzut, dostaniesz instalację,
której wiersze wskazują na bajty, których nie ma — co objawia się jako 503 przy
każdym pobraniu, bez niczego, co nazwałoby przyczynę.
Odtwarzanie wyglądało na udane i nic nie zmieniło
Przeczytaj state/restore.log.
Ta awaria zdarzyła się naprawdę, przy pisaniu repozytorium wdrożeniowego:
wywołanie Compose zawiodło, jego jednolinijkowy błąd zaraportowano jako
ostrzeżenie pg_restore, a odtwarzanie nie zrobiło zupełnie nic, podczas gdy
każda kontrola po nim przechodziła na danych, których nikt nawet nie tknął.
restore.sh bierze tożsamość każdej tabeli przed i porównuje ją po:
--clean --if-exists nadaje każdej odtworzonej tabeli nową, a odtwarzanie, które
nic nie zrobiło, zostawia stare tam, gdzie były. Ale prawdziwym testem jest ten,
który to wyłapał: najpierw coś zmień, potem odtwórz, potem sprawdź, że zmiana
zniknęła.