All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m37s
Vier verbeteringen die elk pas opvielen als je ze nodig had. - Een gewijzigd databasewachtwoord wordt vóór de herstart omgezet in de nog draaiende database (ALTER USER / updateUser). Lukt dat niet, dan breekt de wijziging af en gaat de backup terug — 64 sjablonen hebben zo'n veld, en POSTGRES_PASSWORD werkt alleen bij een lege datamap. Wachtwoorden gaan via de omgeving en via stdin, niet via argumenten. - jobs.met_slot serialiseert taken per app; de geplande backup slaat een bezette app over in plaats van een half herschreven map in te pakken. Een taak die klapt laat het slot los en wordt als mislukt afgesloten. - api_stacks en api_connectable gebruiken één docker ps met het projectlabel in plaats van een compose ps per stackmap. - Per service een versietag te kiezen, bewaard in .serverup.json en opnieuw toegepast na het renderen; het containerpaneel toont het draaiende image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
"""Eén taak tegelijk per app.
|
|
|
|
Zonder vergrendeling kan de nachtelijke backup starten terwijl een
|
|
instellingswijziging net `compose down` heeft gedaan en de bestanden
|
|
herschrijft. Het archief bevat dan een halve stack, terwijl de melding erna
|
|
'Backup klaar' is.
|
|
"""
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
WORTEL = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(WORTEL / "server-up"))
|
|
|
|
|
|
def test_tweede_taak_wacht_op_de_eerste(env):
|
|
from core import jobs
|
|
volgorde = []
|
|
eerste_bezig = threading.Event()
|
|
laat_los = threading.Event()
|
|
|
|
def taak_een(jid):
|
|
volgorde.append("een-start")
|
|
eerste_bezig.set()
|
|
laat_los.wait(5)
|
|
volgorde.append("een-klaar")
|
|
jobs.finish(jid, "done")
|
|
jobs.done(jobs.get_queue(jid))
|
|
|
|
def taak_twee(jid):
|
|
volgorde.append("twee-start")
|
|
jobs.finish(jid, "done")
|
|
jobs.done(jobs.get_queue(jid))
|
|
|
|
jid1, _ = jobs.create("update:web")
|
|
jid2, _ = jobs.create("backup:web")
|
|
jobs.run(jobs.met_slot("web", taak_een), jid1)
|
|
assert eerste_bezig.wait(5)
|
|
jobs.run(jobs.met_slot("web", taak_twee), jid2)
|
|
time.sleep(0.2)
|
|
|
|
assert volgorde == ["een-start"], "de tweede taak begon terwijl de eerste liep"
|
|
laat_los.set()
|
|
einde = time.time() + 5
|
|
while time.time() < einde and "twee-start" not in volgorde:
|
|
time.sleep(0.02)
|
|
assert volgorde == ["een-start", "een-klaar", "twee-start"]
|
|
|
|
# En de wachtende taak meldt dat ook in zijn eigen log.
|
|
tekst = " ".join(r["text"] for r in jobs.stream(jid2)["lines"])
|
|
assert "wacht" in tekst
|
|
|
|
|
|
def test_andere_app_hoeft_niet_te_wachten(env):
|
|
"""Twee verschillende apps mogen wél tegelijk."""
|
|
from core import jobs
|
|
bezig = threading.Event()
|
|
los = threading.Event()
|
|
gedraaid = []
|
|
|
|
def blokkeer(jid):
|
|
bezig.set()
|
|
los.wait(5)
|
|
jobs.finish(jid, "done")
|
|
jobs.done(jobs.get_queue(jid))
|
|
|
|
def snel(jid):
|
|
gedraaid.append(True)
|
|
jobs.finish(jid, "done")
|
|
jobs.done(jobs.get_queue(jid))
|
|
|
|
jid1, _ = jobs.create("update:web")
|
|
jid2, _ = jobs.create("update:andere")
|
|
jobs.run(jobs.met_slot("web", blokkeer), jid1)
|
|
assert bezig.wait(5)
|
|
jobs.run(jobs.met_slot("andere", snel), jid2)
|
|
|
|
einde = time.time() + 5
|
|
while time.time() < einde and not gedraaid:
|
|
time.sleep(0.02)
|
|
assert gedraaid, "een taak op een andere app werd onnodig geblokkeerd"
|
|
los.set()
|
|
|
|
|
|
def test_slot_komt_vrij_na_een_fout(env):
|
|
"""Een taak die klapt mag de app niet voor altijd op slot zetten, en mag
|
|
ook niet eeuwig op 'running' blijven staan met een interface die erop
|
|
blijft pollen."""
|
|
from core import jobs
|
|
|
|
def klapt(jid):
|
|
raise RuntimeError("boem")
|
|
|
|
jid, _ = jobs.create("update:web")
|
|
t = threading.Thread(target=jobs.met_slot("web", klapt), args=(jid,))
|
|
t.start(); t.join(5)
|
|
|
|
with jobs.stack_slot("web") as vrij:
|
|
assert vrij, "het slot bleef hangen na een mislukte taak"
|
|
|
|
d = jobs.stream(jid)
|
|
assert d["status"] == "error"
|
|
assert "boem" in " ".join(r["text"] for r in d["lines"])
|
|
|
|
|
|
def test_geplande_backup_slaat_een_bezette_app_over(env, monkeypatch):
|
|
"""Wachten heeft geen zin als er een lange update loopt; dan is overslaan
|
|
met een melding beter dan een archief van een halve stack."""
|
|
import app as app_module
|
|
from core import jobs
|
|
(env["lib"] / "web").mkdir(parents=True, exist_ok=True)
|
|
(env["lib"] / "web" / "docker-compose.yml").write_text(
|
|
"services:\n web:\n image: nginx\n")
|
|
env["core"].patch({"BACKUP_SCHEDULE": "daily"})
|
|
# Het tijdvenster zelf is elders getest; hier gaat het om de vergrendeling.
|
|
monkeypatch.setattr(app_module.scheduler, "binnen_venster",
|
|
lambda *a, **k: True)
|
|
gemaakt = []
|
|
monkeypatch.setattr(app_module.backups, "create",
|
|
lambda *a, **k: gemaakt.append(a) or {"file": "x.tar.gz"})
|
|
|
|
# Niet twee minuten wachten in een test; het gaat om de keuze die daarna
|
|
# gemaakt wordt. De originele functie eerst vastleggen, anders roept de
|
|
# vervanger zichzelf aan.
|
|
origineel = jobs.stack_slot
|
|
monkeypatch.setattr(app_module.jobs, "stack_slot",
|
|
lambda naam, wacht=0.0: origineel(naam))
|
|
|
|
slot = jobs._stackslot("web")
|
|
slot.acquire()
|
|
try:
|
|
app_module._geplande_backups()
|
|
finally:
|
|
slot.release()
|
|
assert gemaakt == [], "er werd een backup gemaakt van een bezette app"
|
|
|
|
# Zonder slot loopt hij gewoon.
|
|
app_module._geplande_backups()
|
|
assert gemaakt, "zonder bezetting hoort de backup gewoon te draaien"
|