Dokumentacja AlgoJudge0.2

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 .envpo 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:8080 też 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 status

Jeś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.

Drugą przyczynę zamknięcia tej powierzchni widać w innym komunikacie. Server wypisuje go w logu przy starcie:

AJ_Admin__Token is the well-known development token, so /admin is closed.

To instalacja z wartością, którą dostarcza stos deweloperski, admin-token-development-only — jest opublikowana, więc nie jest żadnym tokenem. Wpisz do .env własny sekret i odtwórz Server.

preflight.sh odrzuca dokładnie tę wartość, więc stos podnoszony przez make up nigdy nie dochodzi do tego komunikatu. Wymienienie jej wprost to jedyny sposób, żeby ją wyłapać: jest dość długa, żeby przejść kontrolę długości. Konsekwencje sięgają zresztą dalej niż samo hasło: scripts/maintenance.sh nie potrafi ani otworzyć, ani zamknąć instalacji, której /admin jest zamknięty, co zatrzymuje na tym kroku backup.sh --quiesce, update.sh i restore.sh.

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 roli admin:

docker compose restart server

W 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:

  1. Runner nie został zatwierdzony. Jego log mówi waiting: this Runner has not been approved yet i będzie to mówił zawsze. Zatwierdź go w panelu pod Runnery.
  2. Obrazy sandboxa nie istnieją. Runner szuka wkompilowanych domyślnych algojudge/lang-*:local, które ma tylko host deweloperski. compose.yaml ustawia wszystkie cztery zmienne AJ_Sandbox__Image__* z REGISTRY i RUNNER_TAG; jeśli je nadpisałeś, sprawdź docker images.
  3. Demon jest starszy niż Docker Engine 26. Paczka jest rozpakowywana we wspólnym wolumenie Runnerów, a każdy kontener sędziego montuje stamtąd jej podkatalog — montowaniem subpath, które pojawiło się w Engine 26 / API 1.45 (kwiecień 2024) i w Podmanie 5. Runner odmawia oceniania i mówi o tym, zamiast oceniać na niczym. ./scripts/preflight.sh odmawia wcześniej niż pierwsze zgłoszenie, a docker version --format '{{.Server.APIVersion}}' podaje tę liczbę. Zaktualizuj demona; poniżej tego progu nie ma rozwiązania zastępczego.
  4. Wolumen pamięci podręcznej nie jest tym, o którym powiedziano Runnerowi. Każdy Runner dostaje nazwę wolumenu, o który ma poprosić demona, i kończy pracę przy starcie, gdy demon takiego wolumenu nie ma — jest to więc Runner, który nigdy się nie rejestruje, a nie taki, który źle ocenia. Dwa polecenia z wpisu Checker zawodzi na każdym zgłoszeniu poniżej wypisują to, co Runner dostał, i to, czym dysponuje demon.
  5. Tagi. Runner z ustawionym RUNNER_TAGS wypada 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ę, a docker compose pull jej nie wymieni: obrazy językowe to wartości przekazywane Runnerowi, a nie usługi, więc Compose ich nie dotyka.

./scripts/pull.sh
docker compose restart runner-1              # każdy runner-N, który uruchamiasz

Restart to prosta odpowiedź i nigdy nie jest błędem

Runner pobiera swoje obrazy językowe przy starcie, więc restart jest też tym, co je przynosi.

Pobranie ręczne też działa: działający Runner przyjmuje podmieniony obraz bez restartu, bo to, co o obrazie pamięta, jest zapisane pod jego identyfikatorem, a pobrany obraz ma inny identyfikator.

Do 2026-09-16 ta strona mówiła coś przeciwnego — że restart jest konieczny, bo Runner bada każdy obraz raz i pamięta odpowiedź przez całe życie procesu. To było zmierzone i było prawdą o Runnerze, który zapisywał to pod nazwą obrazu. Klucz się od tego czasu zmienił.

Runner z 2026-09-16 lub nowszy w ogóle tego nie spotka: pobiera wszystkie cztery przy starcie, bezwarunkowo, a jeśli nie może — kończy pracę zamiast się zarejestrować. make up i scripts/update.sh też je pobierają, więc zwykle są już właściwe, zanim Runner spojrzy.

Każde zlecenie zawodzi z predates the input arriving as a descriptor

Przyczyna i lekarstwo są te same co wyżej: pobierz cztery obrazy, a potem zrestartuj każdy Runner. Wejście testu nie jest montowane do kontenera — Runner trzyma je w pamięci i podaje shimowi deskryptor przez gniazdo — więc shim sprzed tej zmiany otworzyłby gniazdo tak, jakby było plikiem, i zgłosiłby przebieg bez pomiaru. Runner woli odmówić, bo to zdanie, na które operator może zareagować.

Test kończy się na nothing opened the run's output channel

test 03: nothing opened the run's output channel within 30s

Kontener, który miał ocenić ten test, nie wystartował. Runner czeka trzydzieści sekund, aż ktokolwiek otworzy wyjście przebiegu, i wtedy mówi o tym wprost, zamiast zgłosić pustą odpowiedź — a ta byłaby błędnym werdyktem przypisanym zgłoszeniu, które nigdy nie ruszyło. To awaria infrastruktury, więc zlecenie wraca do kolejki, zamiast upaść, aż wyczerpią się jego doręczenia.

Własny komunikat demona o kontenerze, którego nie dało się uruchomić, stoi w logu Runnera linię wyżej i zwykle nazywa przyczynę wprost:

docker compose logs runner-1 | tail -40

Dwie przyczyny, od najczęstszej:

  1. Obraz językowy nie startuje na tym hoście. docker images pokazuje, czy wszystkie cztery są na miejscu; kontener, który ginie na OCI runtime create failed, nie otworzy niczego.
  2. Katalog roboczy nie potrafi trzymać nazwanego potoku. Kanał to FIFO i gniazdo uniksowe, a nie bajty, więc system plików, który nie tworzy ani jednego, ani drugiego, kończy się tutaj. Zwykłą drogą do tego jest wpis tmpfs: przy usłudze runnera: należy on do przestrzeni montowań samego Runnera, a demon robi obok własny pusty katalog.

Na Dockerze Desktop pamięć podręczna nie ma dobrego miejsca pod /var

/var w jego maszynie wirtualnej jest zamontowany jako noexec, więc checker zbudowany do pamięci podręcznej w tym miejscu nie da się uruchomić, a jego kontener ginie przy starcie — i to jest jedna z dróg, którymi test trafia tutaj. Trzymaj pamięć podręczną w nazwanym wolumenie, bo to właśnie dostarcza ten stos.

Checker albo interactor, którego własnego kanału nikt nie otworzył, czyta się inaczej: wraca jako zepsuty checker, a nie z powyższym zdaniem. Przyczyna jest ta sama, a zaczynać należy od wpisu poniżej.

Checker zawodzi na każdym zgłoszeniu albo ocenia wobec pustki

Zgłoszenia do zadania, którego paczka niesie checker, wracają jako awarie infrastruktury — często opisane tak, jakby to checker autora się nie zbudował.

Pamięć podręczna nie jest tą, do której Runner zapisuje. Paczka jest rozpakowywana, a jej checker budowany raz, w pamięci podręcznej, i kontener sędziego montuje jedno i drugie — więc demon musi sięgnąć po dokładnie te bajty, które przygotował Runner.

Pamięcią podręczną jest wolumen algojudge_runner-cache, do którego demon sięga po nazwie, więc nie ma tu czego pomylić; ten wpis dotyczy wtedy prawie zawsze Runnera wskazanego ręcznie gdzie indziej.

Oba obrazy Runnerów są distroless, więc docker compose exec nie ma ani env, ani powłoki, w której dałoby się je uruchomić. Odczytaj kontener z zewnątrz:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$(docker compose ps -q runner-1)" | grep AJ_Cache__
docker volume inspect algojudge_runner-cache

Runner, który w ogóle nie widzi swojej pamięci podręcznej, odmawia startu i mówi o AJ_Cache__Volume. Ten wpis dotyczy przypadku węższego: pamięć podręczna istnieje, ale nie jest tą, do której Runner zapisuje.

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:

  1. 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.
  2. props wersji nie podaje numeru zadania w archiwum. To zostaje odrzucone po nazwie, zanim cokolwiek opuści tę instalację.
  3. 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 -5

Konto 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.

AJ_Runner__TestsAtOnce w treści odmowy należy do Runnera oceniającego: nie wystartuje z większą liczbą torów, niż jego zbiór ma procesorów. Kryją się tu dwie pułapki.

Szerokość to jedna wartość dla wszystkich Runnerów. RUNNER_TESTS_AT_ONCE trafia do obu usług z tej samej kotwicy, więc jeden wąski RUNNER_2_CPUSET to pętla na runner-2, podczas gdy runner-1 ocenia bez zarzutu — czytaj log tego, który się zapętlił, a nie tego, który działa.

Runner bez cpusetu czyta całą maszynę, a oba zbiory dostarczamy puste. Zostawienie pustego zbioru nie rozluźnia sprawdzenia: szerokość cztery na maszynie o dwóch procesorach zostanie odrzucona, czy zbiór jest wąski, czy go nie ma. preflight.sh dochodzi do obu odpowiedzi, zanim cokolwiek wystartuje: odmawia szerokości większej niż liczba procesorów hosta, a w każdym zapisanym cpusecie liczy procesory i zestawia je z szerokością.

Nazwane ustawienie odpytywania albo dzierżawy to czwarta 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.

Zewnętrzny Runner odnawia trzymane dzierżawy na własnym zegarze, co ćwiartkę dzierżawy przyznanej przez Server, więc z tego, jak często pyta archiwum, nie wynika nic o tym, czy dzierżawa wygaśnie. Przy starcie odmawia dzierżawy, która nie przeżywa AJ_External__PendingTimeoutSeconds, dzierżawy powyżej sufitu Servera wynoszącego 3600 sekund oraz AJ_Poll__WaitSeconds powyżej 300.

Requested CPUs are not available

Demon odmawia utworzenia kontenera, którego cpuset nazywa procesor nieistniejący na tym hoście, więc ta przyczyna zatrzymuje docker compose up -d --wait z częścią stosu w ruchu, zamiast zostawić Runnera w pętli restartów.

Oba cpusety dostarczamy puste, co oznacza wszystkie procesory hosta, więc taki zbiór ktoś zapisał ręcznie. lscpu -p=CPU,CORE wypisuje procesory, które istnieją, a preflight.sh nazywa te, o które prosi RUNNER_n_CPUSET, a których host nie ma — osobno dla każdego Runnera, zanim cokolwiek wystartuje.

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.sock

Na 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ć przestrzeni roboczej — co przy wolumenach jest mało prawdopodobne: demon zakłada wolumen z właścicielem root i prawami 0755, a to jest już poprawne. 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. Wolumen podmieniony ręcznie potrafi nie spełnić tej drugiej połowy, 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.

docker volume inspect -f '{{.Mountpoint}}' algojudge_runner-1-work

Pamięć podręczna ma te same dwie połowy, z tego samego powodu: Runner rozpakowuje tam każdą paczkę jako root, a kontener sędziego czyta ją jako 65534. scripts/preflight.sh sprawdza obie — zapisując jako root i odczytując z powrotem jako 65534.

Każ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ówiCo zrobić
cgroup w wersji 1host wstaje z hierarchią hybrydową. Dopisz systemd.unified_cgroup_hierarchy=1 do wiersza poleceń jądra i zrestartuj maszynę
sterownik cgroup, którego nie znadocker info --format '{{.CgroupDriver}}' musi wypisać cgroupfs albo systemd. Cokolwiek innego znaczy, że cgroup są wyłączone
nie potrafi odczytać hierarchiiktoś 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 jednego toru, 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 -r

Na 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'uje index.html. Jest serwowany z no-store dokł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.

Na tej stronie