fix(sync): bound slow MTP rollout and retained generations
This commit is contained in:
parent
df41300353
commit
3a4ec34eb9
8 changed files with 31 additions and 13 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}$")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue