Refresh MTP peer identity before validation
This commit is contained in:
parent
4f0a5fee31
commit
4573fe9a42
2 changed files with 88 additions and 5 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -14,6 +15,54 @@ import sync_peer_worker # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class SyncErrorClassificationTest(unittest.TestCase):
|
class SyncErrorClassificationTest(unittest.TestCase):
|
||||||
|
def test_mtp_refreshes_a_retained_peer_advertisement_before_validation(self):
|
||||||
|
expected_peer = "peer_00000000-0000-4000-8000-000000000001"
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
peer_path = root / "android-peer-v1.json"
|
||||||
|
peer_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"format": "trainlog-sync-peer",
|
||||||
|
"version": 1,
|
||||||
|
"peer_id": expected_peer,
|
||||||
|
"capabilities": sorted(
|
||||||
|
sync_peer_worker.CAPS - {"session-preparations-v2"}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pulls = []
|
||||||
|
|
||||||
|
def refresh(_adapter, operation, peer, transport, _deadline, allow_missing):
|
||||||
|
pulls.append((operation, peer, transport, allow_missing))
|
||||||
|
peer_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"format": "trainlog-sync-peer",
|
||||||
|
"version": 1,
|
||||||
|
"peer_id": expected_peer,
|
||||||
|
"capabilities": sorted(sync_peer_worker.CAPS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
with mock.patch.object(sync_peer_worker, "run_adapter", side_effect=refresh):
|
||||||
|
sync_peer_worker.refresh_mtp_peer(
|
||||||
|
Path("/adapter"),
|
||||||
|
expected_peer,
|
||||||
|
root,
|
||||||
|
sync_peer_worker.time.monotonic() + 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
value = sync_peer_worker.load_peer(root, expected_peer)
|
||||||
|
self.assertEqual(value["peer_id"], expected_peer)
|
||||||
|
self.assertEqual(
|
||||||
|
pulls,
|
||||||
|
[("pull", expected_peer, root, True)],
|
||||||
|
)
|
||||||
|
|
||||||
def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self):
|
def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self):
|
||||||
timeout = subprocess.TimeoutExpired(["adapter", "push"], 30)
|
timeout = subprocess.TimeoutExpired(["adapter", "push"], 30)
|
||||||
with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout):
|
with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout):
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,39 @@ def wait_json(path: Path, deadline: float, predicate, pump=None) -> dict:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_mtp_peer(
|
||||||
|
adapter: Path,
|
||||||
|
peer: str,
|
||||||
|
root: Path,
|
||||||
|
deadline: float,
|
||||||
|
) -> None:
|
||||||
|
"""Require a successful device pull before trusting a retained peer file.
|
||||||
|
|
||||||
|
WHY: the durable transport root legitimately retains coordination objects
|
||||||
|
from earlier runs. A local peer advertisement can therefore predate an
|
||||||
|
Android upgrade that adds a required capability.
|
||||||
|
CONTRACT: MTP mode validates only an advertisement obtained after at least
|
||||||
|
one successful pull of the expected physical peer during this run.
|
||||||
|
INVARIANT: an unavailable peer never makes a stale local advertisement
|
||||||
|
authoritative and remains a bounded wait state.
|
||||||
|
"""
|
||||||
|
peer_path = root / "android-peer-v1.json"
|
||||||
|
while True:
|
||||||
|
pulled = run_adapter(
|
||||||
|
adapter,
|
||||||
|
"pull",
|
||||||
|
peer,
|
||||||
|
root,
|
||||||
|
deadline,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
if pulled and peer_path.is_file():
|
||||||
|
return
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise RuntimeError("timeout waiting for android-peer-v1.json")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
def correlated_peer_error(root: Path, run_id: str) -> None:
|
def correlated_peer_error(root: Path, run_id: str) -> None:
|
||||||
path = root / "android-generation-error-v1.json"
|
path = root / "android-generation-error-v1.json"
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
|
|
@ -254,11 +287,12 @@ def main() -> int:
|
||||||
deadline,
|
deadline,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
# Android publishes its durable identity only while participating. A
|
refresh_mtp_peer(
|
||||||
# missing expected advertisement is therefore a bounded wait state;
|
args.mtp_adapter,
|
||||||
# device, transport, ambiguity and malformed-object failures remain
|
args.expected_peer,
|
||||||
# immediate and distinct adapter errors.
|
args.transport_root,
|
||||||
wait_file(args.transport_root / "android-peer-v1.json", deadline, pump)
|
deadline,
|
||||||
|
)
|
||||||
peer = load_peer(args.transport_root, args.expected_peer)
|
peer = load_peer(args.transport_root, args.expected_peer)
|
||||||
with closing(generation.connect_database(args.database)) as db:
|
with closing(generation.connect_database(args.database)) as db:
|
||||||
generation.require_schema(db)
|
generation.require_schema(db)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue