perf(observatory): pack camera epoch source transfer
This commit is contained in:
@@ -4,6 +4,7 @@ import asyncio
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import tarfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
@@ -484,6 +485,51 @@ def test_source_member_rejects_tampering_and_stale_generation(tmp_path: Path) ->
|
||||
)
|
||||
|
||||
|
||||
def test_camera_epoch_archive_is_sealed_deterministic_and_cached(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fixture = _fixture(tmp_path)
|
||||
|
||||
archive, archive_path = fixture.service.materialize_camera_epoch_archive(
|
||||
job_id=fixture.job.job_id,
|
||||
claim_token=fixture.claim_token,
|
||||
claim_generation=fixture.job.claim_generation,
|
||||
claimant_id="worker-006",
|
||||
)
|
||||
|
||||
assert archive_path.name == archive.sha256
|
||||
assert archive_path.stat().st_size == archive.byte_length
|
||||
assert hashlib.sha256(archive_path.read_bytes()).hexdigest() == archive.sha256
|
||||
with tarfile.open(archive_path, mode="r:") as packed:
|
||||
members = packed.getmembers()
|
||||
assert [member.name for member in members] == ["init.mp4", "segments/1.m4s"]
|
||||
assert all(
|
||||
member.type == tarfile.REGTYPE
|
||||
and member.mode == 0o600
|
||||
and member.mtime == 0
|
||||
and member.uid == 0
|
||||
and member.gid == 0
|
||||
and member.uname == ""
|
||||
and member.gname == ""
|
||||
for member in members
|
||||
)
|
||||
|
||||
def reject_rebuild(*_args: object, **_kwargs: object) -> object:
|
||||
pytest.fail("immutable camera epoch archive must be reused")
|
||||
|
||||
monkeypatch.setattr(tarfile, "open", reject_rebuild)
|
||||
repeated, repeated_path = fixture.service.materialize_camera_epoch_archive(
|
||||
job_id=fixture.job.job_id,
|
||||
claim_token=fixture.claim_token,
|
||||
claim_generation=fixture.job.claim_generation,
|
||||
claimant_id="worker-006",
|
||||
)
|
||||
|
||||
assert repeated == archive
|
||||
assert repeated_path == archive_path
|
||||
|
||||
|
||||
def test_result_upload_is_atomic_resumable_and_required_before_success(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import tarfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,9 +11,12 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from k1link.observatory.portable_artifact_transport import (
|
||||
PORTABLE_CAMERA_EPOCH_ARCHIVE_ID_HEADER,
|
||||
PORTABLE_CAMERA_EPOCH_ARCHIVE_MEDIA_TYPE,
|
||||
PORTABLE_RESULT_UPLOAD_PLAN_SCHEMA,
|
||||
PORTABLE_RESULT_UPLOAD_RECEIPT_SCHEMA,
|
||||
PORTABLE_SOURCE_MATERIALIZATION_SCHEMA,
|
||||
portable_camera_epoch_archive_id,
|
||||
)
|
||||
from k1link.observatory.portable_result_contract import (
|
||||
OBSERVATION_ONLY_AUTHORITY,
|
||||
@@ -29,6 +34,8 @@ from k1link.observatory.worker_agent import (
|
||||
SealedObservatoryRecordedJob,
|
||||
)
|
||||
from k1link.observatory.worker_http_transport import (
|
||||
WORKER_HTTP_CAMERA_ARCHIVE_READ_TIMEOUT_SECONDS,
|
||||
WORKER_HTTP_DEFAULT_TIMEOUT_SECONDS,
|
||||
ObservatoryWorkerHttpError,
|
||||
ObservatoryWorkerHttpGateway,
|
||||
)
|
||||
@@ -138,7 +145,10 @@ def _source_contract(
|
||||
job: SealedObservatoryRecordedJob,
|
||||
*,
|
||||
inject_path: bool = False,
|
||||
camera_segment_count: int = 1,
|
||||
) -> tuple[dict[str, object], dict[str, bytes]]:
|
||||
if camera_segment_count < 1:
|
||||
raise ValueError("camera segment count must be positive")
|
||||
rows = [
|
||||
_source_member(
|
||||
job,
|
||||
@@ -175,16 +185,23 @@ def _source_contract(
|
||||
camera_epoch=1,
|
||||
media_type='video/mp4; codecs="avc1.641028"',
|
||||
),
|
||||
]
|
||||
rows.extend(
|
||||
_source_member(
|
||||
job,
|
||||
kind="camera-segment",
|
||||
payload=b"sealed-camera-segment",
|
||||
payload=(
|
||||
b"sealed-camera-segment"
|
||||
if sequence == 1
|
||||
else f"sealed-camera-segment-{sequence}".encode("ascii")
|
||||
),
|
||||
artifact_id="recorded-camera-right",
|
||||
camera_epoch=1,
|
||||
camera_sequence=1,
|
||||
camera_sequence=sequence,
|
||||
media_type="video/iso.segment",
|
||||
),
|
||||
]
|
||||
)
|
||||
for sequence in range(1, camera_segment_count + 1)
|
||||
)
|
||||
members = [row for row, _payload in rows]
|
||||
members.sort(key=lambda row: str(row["member_id"]))
|
||||
if inject_path:
|
||||
@@ -210,6 +227,86 @@ def _source_contract(
|
||||
)
|
||||
|
||||
|
||||
def _camera_archive_response(
|
||||
job: SealedObservatoryRecordedJob,
|
||||
manifest: dict[str, object],
|
||||
payloads: dict[str, bytes],
|
||||
*,
|
||||
mutation: str | None = None,
|
||||
) -> tuple[bytes, dict[str, str]]:
|
||||
rows = manifest["members"]
|
||||
assert isinstance(rows, list)
|
||||
camera_rows = [
|
||||
row
|
||||
for row in rows
|
||||
if isinstance(row, dict)
|
||||
and row.get("kind") in {"camera-init", "camera-segment"}
|
||||
]
|
||||
camera_rows.sort(
|
||||
key=lambda row: (
|
||||
0 if row["kind"] == "camera-init" else 1,
|
||||
int(row["camera_sequence"] or 0),
|
||||
)
|
||||
)
|
||||
packed = io.BytesIO()
|
||||
with tarfile.open(
|
||||
fileobj=packed,
|
||||
mode="w",
|
||||
format=tarfile.USTAR_FORMAT,
|
||||
) as archive:
|
||||
for index, row in enumerate(camera_rows):
|
||||
sequence = row["camera_sequence"]
|
||||
name = (
|
||||
"init.mp4"
|
||||
if row["kind"] == "camera-init"
|
||||
else f"segments/{sequence}.m4s"
|
||||
)
|
||||
if mutation == "traversal" and index == len(camera_rows) - 1:
|
||||
name = "../../operator-secret"
|
||||
member_id = str(row["member_id"])
|
||||
payload = payloads[member_id]
|
||||
info = tarfile.TarInfo(name)
|
||||
info.mode = 0o600
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.uname = ""
|
||||
info.gname = ""
|
||||
if mutation == "symlink" and index == len(camera_rows) - 1:
|
||||
info.type = tarfile.SYMTYPE
|
||||
info.linkname = "../../operator-secret"
|
||||
info.size = 0
|
||||
archive.addfile(info)
|
||||
else:
|
||||
info.size = len(payload)
|
||||
archive.addfile(info, io.BytesIO(payload))
|
||||
archive_payload = packed.getvalue()
|
||||
sha256 = hashlib.sha256(archive_payload).hexdigest()
|
||||
init = camera_rows[0]
|
||||
archive_id = portable_camera_epoch_archive_id(
|
||||
job_identity_sha256=job.identity_sha256,
|
||||
source_bundle_sha256=job.source_bundle_sha256,
|
||||
artifact_id=str(init["artifact_id"]),
|
||||
camera_epoch=int(init["camera_epoch"]),
|
||||
member_ids=tuple(str(row["member_id"]) for row in camera_rows),
|
||||
byte_length=len(archive_payload),
|
||||
sha256=sha256,
|
||||
)
|
||||
headers = {
|
||||
"Content-Type": PORTABLE_CAMERA_EPOCH_ARCHIVE_MEDIA_TYPE,
|
||||
"Content-Length": str(len(archive_payload)),
|
||||
"X-Mission-Core-Content-Sha256": sha256,
|
||||
PORTABLE_CAMERA_EPOCH_ARCHIVE_ID_HEADER: archive_id,
|
||||
}
|
||||
if mutation == "corruption":
|
||||
changed = bytearray(archive_payload)
|
||||
offset = archive_payload.find(b"sealed-camera-init")
|
||||
assert offset >= 0
|
||||
changed[offset] ^= 1
|
||||
archive_payload = bytes(changed)
|
||||
return archive_payload, headers
|
||||
|
||||
|
||||
def test_http_gateway_materializes_only_exact_claim_bound_members(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -232,6 +329,8 @@ def test_http_gateway_materializes_only_exact_claim_bound_members(
|
||||
artifact_requests.append(request)
|
||||
if request.url.path.endswith("/source-materialization"):
|
||||
return httpx.Response(200, json=manifest)
|
||||
if request.url.path.endswith("/source-camera-epoch-archive"):
|
||||
return httpx.Response(404)
|
||||
member_id = request.url.path.rsplit("/", 1)[-1]
|
||||
payload = payloads[member_id]
|
||||
return httpx.Response(
|
||||
@@ -266,7 +365,148 @@ def test_http_gateway_materializes_only_exact_claim_bound_members(
|
||||
)
|
||||
assert persisted == manifest
|
||||
assert "path" not in json.dumps(persisted, sort_keys=True)
|
||||
assert len(artifact_requests) == 1 + len(payloads)
|
||||
assert len(artifact_requests) == 2 + len(payloads)
|
||||
|
||||
|
||||
def test_http_gateway_packed_camera_epoch_bounds_requests_and_header_timeout(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
job = _job(
|
||||
bundle_sha256=hashlib.sha256(b"source-bundle").hexdigest(),
|
||||
capability_sha256=hashlib.sha256(b"source-capability").hexdigest(),
|
||||
)
|
||||
manifest, payloads = _source_contract(job, camera_segment_count=101)
|
||||
archive_payload, archive_headers = _camera_archive_response(
|
||||
job,
|
||||
manifest,
|
||||
payloads,
|
||||
)
|
||||
artifact_requests: list[httpx.Request] = []
|
||||
camera_member_requests: list[str] = []
|
||||
archive_read_timeouts: list[float] = []
|
||||
rows = manifest["members"]
|
||||
assert isinstance(rows, list)
|
||||
camera_member_ids = {
|
||||
str(row["member_id"])
|
||||
for row in rows
|
||||
if isinstance(row, dict)
|
||||
and row.get("kind") in {"camera-init", "camera-segment"}
|
||||
}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.url.path.endswith("/claims"):
|
||||
return _claim_response()
|
||||
artifact_requests.append(request)
|
||||
if request.url.path.endswith("/source-materialization"):
|
||||
return httpx.Response(200, json=manifest)
|
||||
if request.url.path.endswith("/source-camera-epoch-archive"):
|
||||
timeout = request.extensions.get("timeout")
|
||||
assert isinstance(timeout, dict)
|
||||
read_timeout = timeout.get("read")
|
||||
assert isinstance(read_timeout, float)
|
||||
assert read_timeout > WORKER_HTTP_DEFAULT_TIMEOUT_SECONDS
|
||||
archive_read_timeouts.append(read_timeout)
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=archive_payload,
|
||||
headers=archive_headers,
|
||||
)
|
||||
member_id = request.url.path.rsplit("/", 1)[-1]
|
||||
if member_id in camera_member_ids:
|
||||
camera_member_requests.append(member_id)
|
||||
payload = payloads[member_id]
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=payload,
|
||||
headers={
|
||||
"X-Mission-Core-Content-Sha256": hashlib.sha256(
|
||||
payload
|
||||
).hexdigest()
|
||||
},
|
||||
)
|
||||
|
||||
with ObservatoryWorkerHttpGateway(
|
||||
base_url="http://127.0.0.1:18080",
|
||||
bearer_token=BEARER_TOKEN,
|
||||
work_root=tmp_path / "worker",
|
||||
transport=httpx.MockTransport(handler),
|
||||
) as gateway:
|
||||
_cache_claim(gateway)
|
||||
stage = gateway.materialize(job)
|
||||
|
||||
assert archive_read_timeouts == [
|
||||
WORKER_HTTP_CAMERA_ARCHIVE_READ_TIMEOUT_SECONDS
|
||||
]
|
||||
assert camera_member_requests == []
|
||||
assert len(artifact_requests) == 6
|
||||
assert (
|
||||
stage.root / "camera/epoch-1/segments/101.m4s"
|
||||
).read_bytes() == b"sealed-camera-segment-101"
|
||||
expected_files = {
|
||||
"source-bundle.json",
|
||||
"source-capability.json",
|
||||
"mqtt.raw.k1mqtt",
|
||||
"mqtt.metadata.jsonl",
|
||||
"materialization-manifest.json",
|
||||
"camera/epoch-1/init.mp4",
|
||||
*(f"camera/epoch-1/segments/{sequence}.m4s" for sequence in range(1, 102)),
|
||||
}
|
||||
assert {
|
||||
path.relative_to(stage.root).as_posix()
|
||||
for path in stage.root.rglob("*")
|
||||
if path.is_file()
|
||||
} == expected_files
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mutation", "message"),
|
||||
[
|
||||
("corruption", "content differs"),
|
||||
("traversal", "unsafe member"),
|
||||
("symlink", "unsafe member"),
|
||||
],
|
||||
)
|
||||
def test_http_gateway_rejects_corrupt_or_unsafe_camera_archive(
|
||||
tmp_path: Path,
|
||||
mutation: str,
|
||||
message: str,
|
||||
) -> None:
|
||||
job = _job(
|
||||
bundle_sha256=hashlib.sha256(b"source-bundle").hexdigest(),
|
||||
capability_sha256=hashlib.sha256(b"source-capability").hexdigest(),
|
||||
)
|
||||
manifest, payloads = _source_contract(job)
|
||||
archive_payload, archive_headers = _camera_archive_response(
|
||||
job,
|
||||
manifest,
|
||||
payloads,
|
||||
mutation=mutation,
|
||||
)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.url.path.endswith("/claims"):
|
||||
return _claim_response()
|
||||
if request.url.path.endswith("/source-materialization"):
|
||||
return httpx.Response(200, json=manifest)
|
||||
if request.url.path.endswith("/source-camera-epoch-archive"):
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=archive_payload,
|
||||
headers=archive_headers,
|
||||
)
|
||||
pytest.fail("unsafe camera archive must fail before member fallback")
|
||||
|
||||
with ObservatoryWorkerHttpGateway(
|
||||
base_url="http://127.0.0.1:18080",
|
||||
bearer_token=BEARER_TOKEN,
|
||||
work_root=tmp_path / "worker",
|
||||
transport=httpx.MockTransport(handler),
|
||||
) as gateway:
|
||||
_cache_claim(gateway)
|
||||
with pytest.raises(ObservatoryWorkerHttpError, match=message):
|
||||
gateway.materialize(job)
|
||||
|
||||
assert not list(tmp_path.rglob("operator-secret"))
|
||||
|
||||
|
||||
def test_http_gateway_rejects_server_selected_source_path(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user