v0.8.00-beta - een kapotte git-cache herstelt zichzelf
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 18m16s
Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 18m16s
"fatal: shallow file has changed since we read it" komt van twee git fetch in dezelfde map. Bij het opstarten synchroniseert Server Up alles, en ondertussen kan iemand op Synchroniseren drukken of een app installeren. Er zat geen slot op, dus een synchronisatie kon over een andere heen lopen en de cache halverwege achterlaten - waarna de repo zich niet meer liet bijwerken en de app store leeg bleef. - een slot per repo, zodat er nog maar een git tegelijk in een cache werkt - lukt bijwerken niet, om welke reden dan ook, dan wordt de cache weggegooid en opnieuw opgehaald. Er staat niets in dat bewaard hoeft te blijven, dus dat is het juiste antwoord op elke fout hier. Dit repareert ook caches die al stuk zijn Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
parent
3fd6c19409
commit
2d85668021
4 changed files with 125 additions and 5 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,3 +1,26 @@
|
|||
# v0.8.00-beta — Een kapotte cache herstelt zichzelf
|
||||
|
||||
`fatal: shallow file has changed since we read it` liet een repo achter die zich
|
||||
niet meer liet bijwerken — en daarmee een app store die leeg bleef.
|
||||
|
||||
Die melding komt van twee `git fetch` in dezelfde map. Bij het opstarten
|
||||
synchroniseert Server Up alles, en ondertussen kan je op **Synchroniseren**
|
||||
drukken of een app installeren die de repo ook aanraakt. Er zat geen slot op:
|
||||
elke synchronisatie kon over een andere heen lopen en de cache halverwege
|
||||
achterlaten, met een `.git/shallow` waar git niets meer mee kan.
|
||||
|
||||
Twee dingen erbij:
|
||||
|
||||
- **Eén git tegelijk per repo.** Botsen kan nu niet meer.
|
||||
- **Zelfherstel.** Lukt het bijwerken niet — om welke reden dan ook — dan wordt
|
||||
de cache weggegooid en opnieuw opgehaald. Er staat niets in dat bewaard hoeft
|
||||
te blijven, dus dat is het juiste antwoord op élke fout hier, en niet alleen
|
||||
op deze. Een repo die zich niet meer laat bijwerken kan daarmee niet meer
|
||||
bestaan.
|
||||
|
||||
Dat repareert ook een cache die al stuk is: de eerstvolgende synchronisatie
|
||||
haalt hem gewoon opnieuw op.
|
||||
|
||||
# v0.7.99-beta — "Gekoppeld" zei niets
|
||||
|
||||
`--doctor` meldde vrolijk dat `/opt/serverup` in de container gekoppeld was,
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.7.99-beta
|
||||
0.8.00-beta
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""Git operations — clone, pull, scan for stacks and modules."""
|
||||
import json, os, re, subprocess, shutil
|
||||
import json, os, re, subprocess, shutil, threading
|
||||
from pathlib import Path
|
||||
from core.docker import COMPOSE_NAMES
|
||||
from core import boilerplates as bp
|
||||
|
|
@ -15,6 +15,18 @@ def guess_logo_url(name: str) -> str:
|
|||
|
||||
CACHE = Path(os.environ.get("SU_GIT_CACHE", "/data/git"))
|
||||
|
||||
# Eén git tegelijk per repo. Bij het opstarten wordt alles gesynchroniseerd, en
|
||||
# ondertussen kan iemand op "Synchroniseren" drukken of een app installeren.
|
||||
# Twee `git fetch` in dezelfde map leveren "fatal: shallow file has changed
|
||||
# since we read it" op — en laten de cache halverwege achter.
|
||||
_repo_sloten: dict[str, threading.Lock] = {}
|
||||
_sloten_lock = threading.Lock()
|
||||
|
||||
|
||||
def _slot(rid: str) -> threading.Lock:
|
||||
with _sloten_lock:
|
||||
return _repo_sloten.setdefault(rid, threading.Lock())
|
||||
|
||||
# Fix git "dubious ownership" — container draait als root, cache kan door andere uid zijn
|
||||
_git_safe_set = False
|
||||
|
||||
|
|
@ -60,7 +72,9 @@ def clone_or_pull(repo: dict, log_fn=None) -> tuple[bool, str]:
|
|||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
|
||||
try:
|
||||
if dest.exists() and (dest / ".git").exists():
|
||||
with _slot(rid):
|
||||
vers_ophalen = not (dest.exists() and (dest / ".git").exists())
|
||||
if not vers_ophalen:
|
||||
if log_fn:
|
||||
log_fn(f"Bijwerken {rid} ({branch})…")
|
||||
# Geen `git pull --rebase`. Dat commando leunt op de
|
||||
|
|
@ -84,12 +98,25 @@ def clone_or_pull(repo: dict, log_fn=None) -> tuple[bool, str]:
|
|||
r = subprocess.run(["git", "reset", "--hard", "FETCH_HEAD"],
|
||||
cwd=str(dest), capture_output=True, text=True,
|
||||
env=env, timeout=60)
|
||||
else:
|
||||
if r.returncode != 0:
|
||||
# Wat er ook misging — een half afgebroken fetch die een kapot
|
||||
# `.git/shallow` achterliet, een verdwenen tak, een lege map —
|
||||
# de cache bevat niets dat bewaard hoeft te blijven. Opnieuw
|
||||
# ophalen is daarom het antwoord op élke fout hier, in plaats
|
||||
# van een repo die zich niet meer laat bijwerken en een app
|
||||
# store die leeg blijft.
|
||||
kort = (r.stderr or r.stdout or "").strip().splitlines()
|
||||
if log_fn:
|
||||
log_fn(f"Bijwerken mislukte ({kort[-1][:90] if kort else '?'});"
|
||||
f" ik haal {rid} opnieuw op")
|
||||
vers_ophalen = True
|
||||
|
||||
if vers_ophalen:
|
||||
if log_fn:
|
||||
log_fn(f"Clone {url}…")
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
r = subprocess.run(["git", "clone", "--branch", branch, "--depth", "1",
|
||||
clone_url, str(dest)],
|
||||
capture_output=True, text=True, env=env, timeout=120)
|
||||
|
|
|
|||
|
|
@ -173,3 +173,73 @@ def test_lokale_rommel_in_de_cache_blokkeert_niets(env, tmp_path, monkeypatch):
|
|||
ok, msg = git.clone_or_pull(repo)
|
||||
assert ok, msg
|
||||
assert (git.cache_dir("proef") / "app.txt").read_text() == "van main"
|
||||
|
||||
|
||||
def test_een_kapotte_cache_haalt_zichzelf_opnieuw_op(env, tmp_path, monkeypatch):
|
||||
""""fatal: shallow file has changed since we read it" liet een repo achter
|
||||
die zich niet meer liet bijwerken, en daarmee een lege app store.
|
||||
|
||||
De cache bevat niets dat bewaard hoeft te blijven, dus opnieuw ophalen is
|
||||
het antwoord op elke fout — niet alleen op deze."""
|
||||
import shutil as _shutil
|
||||
from core import git
|
||||
if not _shutil.which("git"):
|
||||
pytest.skip("git niet aanwezig")
|
||||
bron = _repo_met_twee_takken(tmp_path)
|
||||
monkeypatch.setattr("core.valid_repo_url", lambda u: True)
|
||||
repo = {"id": "proef", "url": f"file://{bron}", "branch": "main"}
|
||||
assert git.clone_or_pull(repo)[0]
|
||||
|
||||
cache = git.cache_dir("proef")
|
||||
# Een `.git/shallow` met onzin erin: precies wat een halverwege afgebroken
|
||||
# fetch achterlaat.
|
||||
(cache / ".git" / "shallow").write_text("dit is geen commit\n", encoding="utf-8")
|
||||
(cache / "app.txt").unlink()
|
||||
|
||||
regels = []
|
||||
ok, msg = git.clone_or_pull(repo, log_fn=regels.append)
|
||||
assert ok, f"de cache herstelt zichzelf niet: {msg}"
|
||||
assert (cache / "app.txt").read_text() == "van main"
|
||||
assert any("opnieuw op" in r for r in regels), regels
|
||||
|
||||
|
||||
def test_twee_synchronisaties_tegelijk_botsen_niet(env, tmp_path, monkeypatch):
|
||||
"""Bij het opstarten wordt alles gesynchroniseerd; ondertussen kan iemand
|
||||
op Synchroniseren drukken. Twee `git fetch` in dezelfde map leveren
|
||||
"shallow file has changed" op en laten de cache halverwege achter."""
|
||||
import shutil as _shutil
|
||||
import threading
|
||||
from core import git
|
||||
if not _shutil.which("git"):
|
||||
pytest.skip("git niet aanwezig")
|
||||
bron = _repo_met_twee_takken(tmp_path)
|
||||
monkeypatch.setattr("core.valid_repo_url", lambda u: True)
|
||||
repo = {"id": "proef", "url": f"file://{bron}", "branch": "main"}
|
||||
assert git.clone_or_pull(repo)[0]
|
||||
|
||||
tegelijk = []
|
||||
bezig = threading.Lock()
|
||||
|
||||
echt = git.subprocess.run
|
||||
|
||||
def merk_op(cmd, **kw):
|
||||
if cmd[:2] == ["git", "fetch"]:
|
||||
if not bezig.acquire(blocking=False):
|
||||
tegelijk.append(cmd)
|
||||
else:
|
||||
try:
|
||||
return echt(cmd, **kw)
|
||||
finally:
|
||||
bezig.release()
|
||||
return echt(cmd, **kw)
|
||||
|
||||
monkeypatch.setattr(git.subprocess, "run", merk_op)
|
||||
draden = [threading.Thread(target=git.clone_or_pull, args=(dict(repo),))
|
||||
for _ in range(4)]
|
||||
for d in draden:
|
||||
d.start()
|
||||
for d in draden:
|
||||
d.join(timeout=60)
|
||||
|
||||
assert not tegelijk, f"{len(tegelijk)} fetches liepen door elkaar heen"
|
||||
assert (cache_na := (git.cache_dir("proef") / "app.txt")).read_text() == "van main", cache_na
|
||||
|
|
|
|||
Loading…
Reference in a new issue