From a2430de94d9416b4409a95adedf17b13a8582d46 Mon Sep 17 00:00:00 2001 From: Grant Whitmer Date: Tue, 11 Aug 2026 16:16:05 -0400 Subject: [PATCH] I-4: distinguish a mirror that never ran from one that is behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea reports the epoch for 'not yet synced', which arithmetic turns into a 56-year lag and a confident 'degraded'. Collapsing those two states is how a backup that was never made gets read as a backup that is merely stale — which is the more dangerous direction, because 'behind' sounds survivable. Co-Authored-By: Claude Opus 5 --- api/app/routes/repos.py | 1 + api/app/services/mirror.py | 14 ++++++++++++-- api/tests/test_invariants.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/api/app/routes/repos.py b/api/app/routes/repos.py index 53f950e..11fe3a1 100644 --- a/api/app/routes/repos.py +++ b/api/app/routes/repos.py @@ -514,6 +514,7 @@ async def mirror_status( "healthy": "A second copy of this project is up to date.", "degraded": "The second copy is behind. Your work here is safe.", "absent": "There is no second copy of this project yet.", + "pending": "The second copy is set up and hasn't run yet.", "unconfigured": "Off-site copies aren't switched on yet.", "unknown": "We can't tell how the second copy is doing right now.", }[status["state"]] diff --git a/api/app/services/mirror.py b/api/app/services/mirror.py index 8f80b37..811846f 100644 --- a/api/app/services/mirror.py +++ b/api/app/services/mirror.py @@ -154,8 +154,18 @@ class MirrorService: except ValueError: lag = None + # A mirror that has NEVER run is not the same thing as one that is + # behind, and collapsing the two is how a backup that was never made + # gets read as a backup that is merely stale. Gitea reports the epoch + # for "not yet", which arithmetic turns into a 56-year lag and a + # confident "degraded". + never_synced = not last or last.startswith("1970-01-01") + # I-4: lag over the threshold is a P2, not a shrug. - if lag is None: + if never_synced: + state = "pending" + lag = None + elif lag is None: state = "unknown" elif lag > self._s.mirror_lag_p2_seconds: state = "degraded" @@ -164,6 +174,6 @@ class MirrorService: return { "state": state, "lag_seconds": lag, - "last_success_at": last, + "last_success_at": None if never_synced else last, "remote": m.get("remote_address"), } diff --git a/api/tests/test_invariants.py b/api/tests/test_invariants.py index ebe8e97..e62683c 100644 --- a/api/tests/test_invariants.py +++ b/api/tests/test_invariants.py @@ -383,3 +383,13 @@ def test_owner_namespace_is_derived_from_the_repo_not_the_caller(): body = src[src.index("async def list_versions") : src.index("async def create_grant")] assert "_repo_owner_login(repo)" in body assert "_owner_login(caller)" not in body + + +def test_i04_never_synced_is_not_reported_as_merely_behind(): + """Collapsing 'never ran' into 'behind' is how a backup that was never made + gets read as a backup that is merely stale. Gitea reports the epoch for + 'not yet', which arithmetic turns into a 56-year lag and a confident + 'degraded'.""" + src = (ROOT / "api" / "app" / "services" / "mirror.py").read_text() + assert "never_synced" in src + assert '"pending"' in src