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-gitKolejność jest tu najważniejsza
git pull— nowe pliki Compose i skrypty, nigdy nowe wersje obrazów.docker compose pull— najdłuższy krok, i nie kosztuje przerwy.- Nic nowego? Koniec. Nic nie zostaje zamknięte.
- Zrób kopię zapasową.
- Zamknij i poczekaj na drenowanie.
up -d, poczekaj, aż stos będzie zdrowy.- Zdrowy: zapisz digesty. Niezdrowy: wycofaj.
- 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.shPrzywraca 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 -dInstalacja, 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.