- brut: _merged/index.json has 586 activities — the count when merge_all last ran. The SSE rebuild bug (already fixed) meant it never re-ran after the full Strava sync

added 3256 more.
  - danilo: _merged/ is 8 KB — basically empty. merge_all likely ran concurrently (multiple file uploads trigger multiple rebuilds without a lock in --no-build mode),
  causing a race where shutil.rmtree(merged_acts) from one run wiped what another run was writing.

  Two fixes: serialize --no-build rebuilds with the same lock, and add a "Rebuild" button to the admin page.

 Root causes fixed:
  1. merge_all race condition — --no-build rebuilds now hold _rebuild_lock, same as full builds
  2. The SSE rebuild-trigger bug (already fixed earlier) was brut's original cause
This commit is contained in:
Davide Scaini
2026-04-13 12:35:05 +02:00
parent 7b37f45180
commit 1587d1cdf3
2 changed files with 66 additions and 14 deletions
+27 -10
View File
@@ -243,16 +243,19 @@ def _trigger_rebuild(handle: str) -> None:
def _run() -> None:
try:
if _webroot is None:
# Fast: only update data, skip Astro build
subprocess.run(
[uv, "run", "bincio", "render",
"--data-dir", _data_dir,
"--site-dir", _site_dir,
"--handle", _handle,
"--no-build"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Fast: only update data, skip Astro build.
# Serialised with the same lock: merge_all wipes and recreates
# _merged/activities/ — concurrent runs would corrupt each other.
with _rebuild_lock:
subprocess.run(
[uv, "run", "bincio", "render",
"--data-dir", _data_dir,
"--site-dir", _site_dir,
"--handle", _handle,
"--no-build"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
# Full build + rsync — serialised so concurrent uploads don't race
with _rebuild_lock:
@@ -500,6 +503,20 @@ async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JS
})
@app.post("/api/admin/users/{handle}/rebuild")
async def admin_rebuild(
handle: str,
bincio_session: Optional[str] = Cookie(default=None),
) -> JSONResponse:
"""Trigger a merge_all + site rebuild for a user. Admin only."""
_require_admin(bincio_session)
user_dir = _get_data_dir() / handle
if not user_dir.is_dir():
raise HTTPException(404, f"No data directory for user '{handle}'")
_trigger_rebuild(handle)
return JSONResponse({"ok": True})
@app.delete("/api/admin/users/{handle}/activities")
async def admin_delete_activities(
handle: str,