Files
Oleksandr Bezdieniezhnykh 610e8a743c [AZ-319] C11 HttpTileUploader (post-landing upload path)
Lands the production HttpTileUploader composing AZ-317's gate, AZ-318's
per-flight signing, and consumer-side cuts over c6 storage. Implements
the full upload flow: gate ON_GROUND -> start_session -> enumerate
pending -> per-batch multipart POST with Ed25519 signing -> mark_uploaded
on ack -> end_session in finally. Honours Retry-After (RFC 7231 int +
HTTP-date), exponential backoff on 5xx, fail-fast on TLS/401/403.

Adds C11Config block, three FDR kinds (tile.queued, tile.rejected,
batch.complete), and the build_tile_uploader composition-root factory.
Cross-component access to c6 stays Protocol-cut (AZ-507 / AZ-270).

Tests: 17 new unit tests covering AC-1..AC-14 plus throughput NFR; AZ-272
schema fixtures for the three new FDR kinds. Full unit suite: 1404 passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 06:13:36 +03:00

6.2 KiB

Batch 39 — Cycle 1 Report

Date: 2026-05-13 Batch: 39 (single-task batch — C11 upload orchestrator) Tasks:

  • AZ-319 (C11 TileUploader, 5pt)

Total complexity: 5pt Status: complete; pending transition to "In Testing".

Scope

Batch 39 lands the production HttpTileUploader — the operator-side post-landing path that completes the C11 upload story (gate + signing key were Batch 38). It composes AZ-317's FlightStateGate, AZ-318's PerFlightKeyManager, and consumer-side cuts over c6's TileStore / TileMetadataStore into a single class that:

  1. Gates on ON_GROUND BEFORE any C6 read or HTTP egress
  2. Starts an AZ-318 signing session (deterministic per-flight Ed25519)
  3. Enumerates pending tiles from c6 (source = onboard_ingest, voting_status = pending), batched to request.batch_size
  4. Per batch: reads pixel bytes, computes the canonical signing payload (SHA-256 over tile_blob || zoom || lat || lon || capture_ts_ns || flight_id || companion_id || quality_json), signs it, packages a multipart POST per the D-PROJ-2 contract sketch, and submits to /api/satellite/tiles/ingest
  5. Honours Retry-After on 429s (RFC 7231 integer-seconds AND HTTP-date forms), backs off exponentially on 5xx (1s/2s/4s/8s), fails fast on TLS / 401 / 403
  6. Marks acked tiles uploaded in c6 (one stamp per acknowledged tile, with the per-batch batch_uuid as the audit correlation key)
  7. Surfaces per-tile signature rejections through key_manager.record_signature_rejection AND a dedicated c11.upload.tile.rejected FDR record
  8. Always calls key_manager.end_session() in finally — guaranteed zeroisation regardless of success / failure / KeyboardInterrupt

Architectural decisions

AZ-507 — consumer-side cuts for c6

The task spec lists tile_store: TileStore and tile_metadata_store: TileMetadataStore as constructor parameters. A direct from gps_denied_onboard.components.c6_tile_cache import … would violate AZ-507 (cross-component imports forbidden) and trip the AZ-270 lint. Instead, tile_uploader.py declares three local Protocol cuts that duck-type the c6 surfaces it actually uses:

  • _TilePixelHandleLike — c6's TilePixelHandle context manager
  • _TileBytesReader — c6's TileStore.read_tile_pixels(tile_id)
  • _PendingMetadataReader — c6's TileMetadataStore.pending_uploads()
    • mark_uploaded(tile_id, uploaded_at)

The composition root (build_tile_uploader) is the single layer that may bind concrete c6 implementations into the constructor. This pattern is documented in _docs/02_document/module-layout.md Rule 9 and was already used for FlightStateSource in AZ-317.

Sleep injection vs. full Clock injection

The task spec lists clock: Clock as a constructor parameter. The uploader only ever needs a sleep primitive (for 429 / 5xx backoff), never monotonic_ns or time_ns. Threading the full Clock Protocol through would carry payload the class never reads. Implementation accepts a sleep: Callable[[float], None] defaulting to a WallClock-routed helper, which preserves the AZ-398 invariant that components/ never calls time.sleep directly. Documented in the batch review as F2 (Low).

FDR key naming

The three new KNOWN_PAYLOAD_KEYS entries (c11.upload.tile.queued, c11.upload.tile.rejected, c11.upload.batch.complete) carry consistent correlation keys (flight_id, fingerprint, batch_uuid, observed_at_iso) across all three records, so an auditor can join per-tile events to the batch summary and back to the c11.upload.session.key.public record from Batch 38. Per-tile records also carry the IngestStatus enum value as status for fast filtering.

Failure paths raise vs. return FAILURE

The spec text describes outcome = failure as a return value for gate-blocked / auth-failed / persistent-5xx scenarios. The implementation raises (FlightStateNotOnGroundError, SatelliteProviderError, RateLimitedError) instead and the finally emitter writes outcome = failure into the FDR c11.upload.batch.complete record. AC-2, AC-9, AC-10 all assert the raise behaviour, so the spec text drift is documented in the batch review (F1, Low) without code change.

Files touched

Production:

  • src/gps_denied_onboard/components/c11_tile_manager/_types.py (added IngestStatus, UploadOutcome, UploadRequest, PerTileStatus, UploadBatchReport)
  • src/gps_denied_onboard/components/c11_tile_manager/errors.py (added SatelliteProviderError, RateLimitedError)
  • src/gps_denied_onboard/components/c11_tile_manager/config.py (new)
  • src/gps_denied_onboard/components/c11_tile_manager/interface.py (TileUploader Protocol now has the real signature)
  • src/gps_denied_onboard/components/c11_tile_manager/tile_uploader.py (new)
  • src/gps_denied_onboard/components/c11_tile_manager/__init__.py (re-exports + register_component_block)
  • src/gps_denied_onboard/runtime_root/c11_factory.py (added build_tile_uploader)
  • src/gps_denied_onboard/fdr_client/records.py (3 new KNOWN_PAYLOAD_KEYS entries)

Tests:

  • tests/unit/c11_tile_manager/test_tile_uploader.py (new — 15 tests)
  • tests/unit/c11_tile_manager/test_protocol_conformance.py (new — 2 tests)
  • tests/unit/test_az272_fdr_record_schema.py (3 fixture additions in _kind_payload)

Test results

pytest tests/unit -q:

  • 1404 passed, 80 skipped, 0 failed
  • Skips are environment-gated (Docker compose, CUDA, TensorRT, Tier-2 hardware, actionlint); none are AZ-319-related

pytest tests/unit/c11_tile_manager/:

  • 41 passed (Batch 38 + Batch 39 combined)
  • AC-1 .. AC-11, AC-13, AC-14, plus rate-limit budget exhaustion, plus AC-12 conformance (positive + negative), plus the throughput NFR

ReadLints: clean across all touched files.

Code review verdict

PASS_WITH_WARNINGS — see _docs/03_implementation/reviews/batch_39_review.md. Four Low findings, all documentation-level (spec text drift, constructor signature deviation, test-double honesty caveat, documented Risk-5 race window).

Cumulative review

This batch closes the C11 upload-side trio (AZ-317, AZ-318, AZ-319). The next cumulative review window covers batches 37-39; that report will land before Batch 41 starts.