[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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 06:13:36 +03:00
parent cde237e236
commit 610e8a743c
15 changed files with 2461 additions and 24 deletions
@@ -1,4 +1,4 @@
"""C11 TileManager composition-root factories (AZ-317, AZ-318).
"""C11 TileManager composition-root factories (AZ-317, AZ-318, AZ-319).
Wires the upload-side services that have landed:
@@ -8,6 +8,10 @@ Wires the upload-side services that have landed:
* :func:`build_per_flight_key_manager` (AZ-318) — wires the AZ-273
:class:`FdrClient` and the project ``Clock`` strategy into the
ephemeral signing-key manager.
* :func:`build_tile_uploader` (AZ-319) — composes the gate, the
key manager, the c6 storage cuts, an :class:`httpx.Client`, and
the :class:`C11Config` block into the production
:class:`HttpTileUploader`.
Composition root is the ONLY layer permitted to import from
``components.c11_tile_manager`` (per ``module-layout.md`` Rule 9 +
@@ -16,13 +20,18 @@ the AZ-270 lint).
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import httpx
from gps_denied_onboard.components.c11_tile_manager import (
C11Config,
FlightStateGate,
FlightStateSource,
HttpTileUploader,
PerFlightKeyManager,
)
from gps_denied_onboard.config.schema import ConfigError
from gps_denied_onboard.fdr_client import FdrClient, make_fdr_client
from gps_denied_onboard.logging import get_logger
@@ -33,12 +42,15 @@ if TYPE_CHECKING:
__all__ = [
"build_flight_state_gate",
"build_per_flight_key_manager",
"build_tile_uploader",
]
_C11_GATE_LOGGER = "c11_tile_manager.flight_state_gate"
_C11_SIGNING_LOGGER = "c11_tile_manager.signing_key"
_C11_SIGNING_PRODUCER_ID = "c11_tile_manager.signing_key"
_C11_UPLOADER_LOGGER = "c11_tile_manager.tile_uploader"
_C11_UPLOADER_PRODUCER_ID = "c11_tile_manager.tile_uploader"
def build_flight_state_gate(*, source: FlightStateSource) -> FlightStateGate:
@@ -76,3 +88,60 @@ def build_per_flight_key_manager(
logger=logger,
clock=clock,
)
def build_tile_uploader(
config: Config,
*,
http_client: httpx.Client,
tile_store: Any,
tile_metadata_store: Any,
flight_state_gate: FlightStateGate,
key_manager: PerFlightKeyManager,
fdr_client: FdrClient | None = None,
) -> HttpTileUploader:
"""Construct a wired :class:`HttpTileUploader` (AZ-319).
The c6 surfaces (``tile_store``, ``tile_metadata_store``) are
consumer-side cuts injected here by the operator-binary
composition root; C11 NEVER imports c6 directly. The ``http_client``
is also caller-owned: production wiring uses one long-lived
:class:`httpx.Client` per process; tests inject
``httpx.Client(transport=httpx.MockTransport(...))``.
"""
block = config.components.get("c11_tile_manager")
if block is None:
raise ConfigError(
"build_tile_uploader: config.components['c11_tile_manager'] "
"block is missing — register C11Config and supply YAML"
)
if not isinstance(block, C11Config):
raise ConfigError(
"build_tile_uploader: config.components['c11_tile_manager'] "
f"must be a C11Config, got {type(block).__name__}"
)
if not block.satellite_provider_ingest_url:
raise ConfigError(
"build_tile_uploader: C11Config.satellite_provider_ingest_url "
"must be configured for production / operator wiring"
)
if not block.companion_id:
raise ConfigError(
"build_tile_uploader: C11Config.companion_id must be set "
"(stable per-companion identifier for the parent-suite "
"voting layer)"
)
if fdr_client is None:
fdr_client = make_fdr_client(_C11_UPLOADER_PRODUCER_ID, config)
logger = get_logger(_C11_UPLOADER_LOGGER)
return HttpTileUploader(
http_client=http_client,
tile_store=tile_store,
tile_metadata_store=tile_metadata_store,
flight_state_gate=flight_state_gate,
key_manager=key_manager,
fdr_client=fdr_client,
logger=logger,
config=block,
)