fix(sync): bound slow MTP rollout and retained generations

This commit is contained in:
fy59 2026-10-01 08:20:38 +02:00
parent df41300353
commit 3a4ec34eb9
8 changed files with 31 additions and 13 deletions

View file

@ -5,9 +5,13 @@
The isolated `trainlog-program-recalage-v1` candidate adds an explicit
preview-and-confirm cascade for unfinished Program sessions. It retains source
and actual dates, stores absolute current dates and ceded-slot state, and
selects a separate Programs V2 Android projection by capability. This candidate
is not merged into the stable installation or deployed to Android.
Rollout packaging now rejects a native build without the embedded Web frontend.
selects a separate Programs V2 Android projection by capability. The source
candidate remains isolated from the stable checkout while its private desktop
and compatible Android build are installed for the coordinated rollout.
Rollout packaging rejects a native build without the embedded Web frontend.
The private rollout preserves parsed V1 Program number types during Android V2
upgrade and raises the bounded local MTP operation budget to 90 seconds and
retained outgoing generation capacity to 16 after interrupted exchanges.
Development opened after the stable v0.1.6 release. This line is restricted to
observed bug fixes, regression fixes, directly related small UI corrections,

View file

@ -59,7 +59,7 @@ callbacks complete before the mutable generation reference is published; the
reference and Android ACK use the same confirmation. Bounded diagnostic logs
contain only run/generation identities, artifact names, phases and results.
The desktop worker performs lightweight local correlation checks continuously
but opens a full MTP pull no more than once every three seconds. A 30-second
but opens a full MTP pull no more than once every three seconds. A 90-second
individual adapter budget is distinct from the overall conversation deadline,
so an absent correlated generation reports a protocol wait timeout while an
actually blocked adapter reports `transport_timeout`.

View file

@ -1112,7 +1112,7 @@ named by that validated reference. It does not recursively mirror retained
Android generations, desktop publications, or unrelated exchange history.
Local correlation is checked every 50 ms without USB work; complete MTP pulls
are throttled to a three-second cadence. Each MTP operation has its own
30-second ceiling while the conversation retains its configured global
90-second ceiling while the conversation retains its configured global
deadline. Exhausting the global deadline while waiting for a correlated
reference reports `timeout waiting for correlated android-generation-v1.json`;
only an adapter that exceeds its independent operation budget reports

View file

@ -110,7 +110,7 @@ These are unsigned development evidence, not deployment authorization.
Do not delete an unacknowledged generation, the last acknowledged generation,
its previous valid generation, or causal evidence. An ACK timeout is unresolved,
not rollback. Eight retained outgoing generations exhaust admission and require
not rollback. Sixteen retained outgoing generations exhaust admission and require
operator diagnosis; this tranche adds no tombstone or evidence garbage
collection. A migrated database is recovered from its verified backup, never by
assuming an older binary reverses its migration.

View file

@ -128,21 +128,25 @@ class SyncErrorClassificationTest(unittest.TestCase):
)
def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self):
timeout = subprocess.TimeoutExpired(["adapter", "push"], 30)
operation_budget = sync_peer_worker.MTP_OPERATION_TIMEOUT_SECONDS
timeout = subprocess.TimeoutExpired(
["adapter", "push"], operation_budget,
)
with tempfile.TemporaryDirectory() as directory:
transport = Path(directory) / "transport"
transport.mkdir()
with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout):
with self.assertRaisesRegex(
RuntimeError,
r"^transport_timeout: MTP push did not finish within 30 seconds$",
rf"^transport_timeout: MTP push did not finish within "
rf"{operation_budget:g} seconds$",
) as caught:
sync_peer_worker.run_adapter(
Path("/adapter"),
"push",
"peer_00000000-0000-4000-8000-000000000001",
transport,
sync_peer_worker.time.monotonic() + 60,
sync_peer_worker.time.monotonic() + operation_budget + 10,
)
self.assertEqual(sync_orchestrator.failure_code(caught.exception), "transport_timeout")

View file

@ -58,7 +58,8 @@ class SyncPeerWorkerTest(unittest.TestCase):
with mock.patch.object(worker.subprocess, "run", return_value=completed) as run:
worker.run_adapter(
Path("/fixed/adapter"), "pull", "peer_fixed",
self.transport, 100.0, clock=lambda: 50.0,
self.transport, 50.0 + worker.MTP_OPERATION_TIMEOUT_SECONDS + 10.0,
clock=lambda: 50.0,
)
self.assertEqual(worker.MTP_OPERATION_TIMEOUT_SECONDS, run.call_args.kwargs["timeout"])
@ -112,7 +113,8 @@ class SyncPeerWorkerTest(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "transport_timeout"):
worker.run_adapter(
Path("/fixed/adapter"), "pull", "peer_fixed",
self.transport, 100.0, clock=lambda: 50.0,
self.transport, 50.0 + worker.MTP_OPERATION_TIMEOUT_SECONDS + 10.0,
clock=lambda: 50.0,
)
def test_delayed_correlated_generation_survives_multiple_polls(self):

View file

@ -51,7 +51,11 @@ MAX_GENERATION = 256 * 1024 * 1024
MAX_NAME = 64
MAX_PATH = 240
MAX_DIAGNOSTIC = 1024
MAX_RETAINED_PER_PEER = 8
# WHY: interrupted MTP publications can leave immutable generations awaiting
# an ACK alongside the two acknowledged lineage members kept for recovery.
# CONTRACT: admission remains bounded and never erases unresolved generations.
# INVARIANT: captured, published and waiting generations still consume a slot.
MAX_RETAINED_PER_PEER = 16
ID = re.compile(r"^(?:gen|peer|sy)_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
HEX = re.compile(r"^[0-9a-f]{64}$")

View file

@ -37,7 +37,11 @@ CAPS = {
ACK_RECOVERY_CAP = "generation-ack-recovery-v1"
MTP_OPERATION_TIMEOUT_SECONDS = 30.0
# WHY: a verified personal phone can need more than 30 seconds to publish an
# immutable Programs generation over MTP. CONTRACT: this bounds each adapter
# call independently of the configured overall conversation deadline.
# INVARIANT: an adapter overrun remains an ambiguous transport failure.
MTP_OPERATION_TIMEOUT_SECONDS = 90.0
MTP_POLL_INTERVAL_SECONDS = 3.0