Dokumentacja AlgoJudge0.1

Aktualizacja i wycofanie

Najpierw pobierz, zamykaj późno, i to jedno, czego wycofanie nie cofa.

./scripts/update.sh
./scripts/update.sh --dry-run
./scripts/update.sh --no-git

Kolejność jest tu najważniejsza

  1. git pull — nowe pliki Compose i skrypty, nigdy nowe wersje obrazów.
  2. docker compose pull — najdłuższy krok, i nie kosztuje przerwy.
  3. Nic nowego? Koniec. Nic nie zostaje zamknięte.
  4. Zrób kopię zapasową.
  5. Zamknij i poczekaj na drenowanie.
  6. up -d, poczekaj, aż stos będzie zdrowy.
  7. Zdrowy: zapisz digesty. Niezdrowy: wycofaj.
  8. Otwórz z powrotem i usuń tylko te obrazy, które należą do tego projektu.

Przerwa to kroki od 5 do 7 — zmierzone na około osiemnaście sekund na małej instalacji, w większości drenowanie.

Krok 4 nie jest opcjonalny i skrypt go nie pominie: aktualizacja bez kopii przed nią nie jest aktualizacją, którą ten skrypt zrobi. Jeśli kopia przed aktualizacją zawiedzie, nic nie zostaje zaktualizowane.

Co znaczy „coś nowego”

Nie sam tag. SERVER_TAG=0 wskazuje po każdym wydaniu inny obraz, więc „czy tag jest ten sam” odpowiadałoby „tak” w nieskończoność. Skrypt pyta Compose, jaki obraz uruchomiłby teraz dla każdej usługi, i zestawia go z identyfikatorem obrazu, na którym kontener naprawdę działa. Różnica między nimi to dokładnie „coś nowego”.

Pytany jest Compose, a nie kontener, bo kontener zna wyłącznie referencję, z której powstał — więc operator, który przypiął 0.1.0, a później wpisał 0.1.1, usłyszałby, że nic się nie zmieniło, podczas gdy pobrany przed chwilą obraz leżałby nieużywany.

Cztery obrazy językowe porównuje się osobno

lang-gcc, lang-clang, lang-python i lang-pypy nie są usługami, więc docker compose pull nigdy ich nie pobiera i żadne porównanie usług ich nie widzi. Aktualizacja pobiera wszystkie cztery po nazwie i zestawia je z tym, co zapisał state/current.lock. Nowy obraz liczy się jako „coś nowego”, bo Runner bada każdy obraz raz i to zapamiętuje — podmieniony zauważy dopiero wtedy, gdy jego kontener powstanie na nowo.

Wersje to tagi; to, co działa, to digest

Tagi leżą w .env. Digesty leżą w state/current.lock. Tag mówi, o co poproszono; digest mówi, co działa, i to jego przywraca wycofanie.

Digesty nie mogą leżeć w repozytorium. To produkt, który wiele organizacji wdraża niezależnie, i żadna z nich nie może do niego commitować.

Domyślny SERVER_TAG=0 to ruchoma wersja główna i nie ma w nim v: wydanie otagowane v0.4.2 publikuje tagi obrazów 0.4.2, 0.4, 0 i latest. Kto zostanie na 0, dostanie przy następnej aktualizacji każde późniejsze wydanie.

Poniżej 1.0 to nie tylko poprawki. Wersja minor może tam zmienić to, co robiła poprzednia, a 0 bierze także takie wydania. Kto chce wyłącznie poprawek, wpisuje 0.1; kto chce decydować o każdej wersji — 0.1.0. latest nie jest oferowany — instalacja, która zmieniła wersję, bo ktoś zrobił pull, nie jest wdrożeniem.

Wycofanie

./scripts/rollback.sh

Przywraca dokładnie te obrazy, które są w state/current.lock, niezależnie od tego, na co wskazują dziś tagi, przez nakładkę zapisaną do state/rollback.compose.yaml. Dotyczy to także czterech obrazów językowych: wracają jako AJ_Sandbox__Image__* przy każdym Runnerze, bo stary Runner obok dzisiejszych sandboksów to jedyne zestawienie, w którym Runnera się nie testuje.

Do czasu usunięcia przyczyny podnoś stos z obu plików

Zwykłe docker compose up nie czyta nakładki, więc wstawia nowe obrazy z powrotem.

docker compose -f compose.yaml -f state/rollback.compose.yaml up -d

Instalacja, która nigdy się nie aktualizowała, nie ma dokąd wracać: state/current.lock zapisuje udana aktualizacja.

Wycofanie nie cofa migracji

Jeśli aktualizacja przesunęła schemat, state/last-migration to zapisuje, a rollback.sh tego nie przemilczy. Starszy Server wobec nowszego schematu nie wystartuje, a jedyna droga powrotu to odtworzyć zrzut zrobiony bezpośrednio przed — a to gubi wszystko, co zapisano od tamtej pory. Skrypt nazywa ten zrzut.

Aktualizuj wszystkie hosty w tym samym oknie

A obrazy językowe razem z Serverem. Server i Runner rozmawiają protokołem, który zmienia się między wersjami, a tagi, które dostarcza ten stos, to ruchome wersje główne pobierane niezależnie — nic więc nie powstrzyma update.sh przed postawieniem nowego Servera obok obrazu Runnera sprzed miesiąca. Runner zbyt stary wobec Servera, u którego się rejestruje, dostaje odmowę, a odmowy rejestracji się nie ponawia: proces kończy pracę, restart: unless-stopped zamienia to w pętlę, a preflight.sh tego nie zobaczy, bo z konfiguracją wszystko jest w porządku. W logu stoi the Server refused with 403: runner.nonce.unknown.

Najdotkliwiej dotyczy to układu T2, gdzie Runnery stoją na własnych hostach. Te hosty mają własny update.sh, a ten, którego nie zaktualizowano razem z resztą, cichnie dopiero przy kolejnym restarcie — czyli w sposób, którego nikt nie zauważy aż do zawodów.

Runner w trakcie aktualizacji

Po SIGTERM Runner oddaje swoje zlecenie. Zatrzymuje ocenianie, mówi Serverowi, że zlecenie jest wolne, sprząta uruchomione przez siebie kontenery i kończy pracę. Inny Runner może wziąć to zlecenie w tej samej chwili, a nie dopiero po dziesięciu minutach, gdy wygaśnie dzierżawa — i doręczenie nie liczy się zgłoszeniu do puli prób, więc operator restartujący flotę nie zużywa prób uczestnika.

Trzy razy, a potem już się liczy. Trzy pierwsze oddania są darmowe, tak samo jak doręczenie, o którym nikt się nigdy nie odezwał; każde następne zabiera jedną z pięciu prób, bo dalej Runner w pętli restartów pod nadzorcą i operator restartujący flotę wyglądają tak samo i rozróżnia je tylko licznik.

Praca już wykonana na tym zgłoszeniu idzie do kosza i robi ją od nowa ten, kto weźmie zlecenie następny. Zmienia się tylko to, że dzieje się to w sekundach, a nie w dziesięć minut.

RUNNER_STOP_GRACE jest właśnie na te wywołania i 30s to z zapasem wystarczająco na kilka żądań HTTP i usunięcie paru kontenerów. Skrócenie go poniżej tego, ile one trwają, zamienia zatrzymanie z powrotem w zabicie, a zabity Runner zostawia swoje zlecenie dzierżawie. Przy domyślnych dla Compose 300s docker compose down spędza za to pięć minut w ciszy: zmierzone na tym stosie, 302 s wobec 32 s.

O tym, jaki limit naprawdę trzyma kontener, decydują dwie rzeczy

Docker zapisuje ten limit na kontenerze w chwili jego utworzenia, więc down odczekuje starą wartość, dopóki kontenery nie zostaną utworzone na nowo — przy pierwszym up po pobraniu obrazów albo przez up --force-recreate. docker inspect -f '{{.Config.StopTimeout}}' mówi, którą wartość trzyma dany kontener.

O samej wartości decyduje Twój .env, a nie wartość zapasowa z compose.yaml. Zmieniaj ją tam.

Czyste drenowanie to nadal maintenance.sh on --wait-closed najpierw na Serverze — i tak właśnie robi update.sh: wtedy do Runnera w ogóle nie trafia nowa praca, co jest schludniejsze niż każdy Runner oddający to, co przed chwilą dostał.

Zewnętrzny Runner też obsługuje SIGTERM, a jego margines to EXTERNAL_RUNNER_STOP_GRACE — sześćdziesiąt sekund, dwa razy tyle co u Runnera oceniającego, bo oddaje do dwudziestu zgłoszeń w toku, po jednym wywołaniu na każde. Kiedy zamiast tego zostaje zabity, traci nie ocenianie w toku, tylko listę tego, na co archiwum jeszcze nie odpowiedziało — a każde takie zgłoszenie zostaje wysłane drugi raz, kiedy zlecenie wróci do kolejki. Rachunek jest w Przerwie technicznej.

Gdy zawiodą obie połowy

Jeśli nowe obrazy nie wstaną zdrowe w 120 sekund, skrypt sam się wycofuje i zapisuje ostatnie pięćdziesiąt linii logu Servera do state/failed-update.log. Jeśli wycofanie też zawiedzie, skrypt przestaje próbować: instalacja zostaje zamknięta, a log i zrzut sprzed aktualizacji są nazwane. To nie jest coś, co skrypt powinien powtarzać.

Aktualizacja nie jest za Ciebie zaplanowana

cron/algojudge.cron dostarcza wpis aktualizacji zakomentowany. Odkomentuj go dopiero wtedy, gdy raz odtworzyłeś kopię i wiesz, że działa — automatyczna aktualizacja z nieprzetestowaną kopią za plecami to nocna okazja do utraty instalacji.

Na tej stronie