mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 18:21:16 +00:00
[AZ-316] Implement C11 HttpTileDownloader (batch 40)
Lands the operator-side pre-flight download path: authenticated httpx GETs against satellite-provider, RESTRICT-SAT-4 (>= 0.5 m/px) enforcement at the C11 boundary, c6 writes via consumer-side cuts (_TileWriterLike, _BudgetEnforcerLike), per-(flight_id, request_hash) journal under cache_root/.c11/journal/ for idempotent re-runs (AC-8, AC-12), 429 Retry-After + 5xx exponential backoff handling, fail-fast on TLS / 401 / 403, and a redacted-bearer auth-header policy. Architecture: - AZ-507 cross-component rule held: tile_downloader.py imports zero c6 symbols; the composition-root _C6DownloadAdapter in runtime_root/c11_factory.py absorbs c6's TileMetadata / TileSource / FreshnessLabel / VotingStatus enum assembly. - Sleep-callable injection (not full Clock) per Batch 39 precedent; default routes through WallClock.sleep_until_ns to keep the AZ-398 invariant intact. - No FDR records on the download path; spec mandates structured logs only (8 log kinds wired: session.start/end, resolution_rejected, freshness_rejected_summary, freshness_downgraded, batch.retry, provider.failed, budget.exceeded, idempotent_no_op). Tests: 14 new downloader unit tests covering AC-1..AC-9, AC-11, AC-12 plus throughput NFR + 429 HTTP-date + 429 budget exhaustion; 2 new TileDownloader Protocol conformance tests (AC-10). Full unit suite: 1420 passed, 80 skipped (env-gated), 0 failed. Code review: PASS_WITH_WARNINGS (5 Low findings, all documentation or downstream-blocked). See _docs/03_implementation/reviews/ batch_40_review.md and batch_40_cycle1_report.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
"""C11 TileManager config block (AZ-319).
|
||||
"""C11 TileManager config block (AZ-316, AZ-319).
|
||||
|
||||
Registered into ``config.components['c11_tile_manager']`` by the
|
||||
package ``__init__.py``. The composition-root factory
|
||||
:func:`gps_denied_onboard.runtime_root.c11_factory.build_tile_uploader`
|
||||
reads this block to drive the upload path's HTTP behaviour and to
|
||||
identify the producing companion against the parent suite's voting
|
||||
layer.
|
||||
package ``__init__.py``. Two composition-root factories read this
|
||||
block:
|
||||
|
||||
The four fields below match the AZ-319 task spec § ``Outcome`` —
|
||||
``config.c11.satellite_provider_ingest_url``,
|
||||
``config.c11.upload_batch_size``, ``config.c11.upload_http_timeout_s``,
|
||||
``config.c11.companion_id``. The ``upload_max_retry_after_s`` cap is
|
||||
the Risk-3 ceiling on cumulative ``Retry-After`` budget for 429
|
||||
responses (see :class:`RateLimitedError`).
|
||||
* :func:`gps_denied_onboard.runtime_root.c11_factory.build_tile_uploader`
|
||||
reads the ``upload_*`` fields and ``companion_id`` to drive AZ-319.
|
||||
* :func:`gps_denied_onboard.runtime_root.c11_factory.build_tile_downloader`
|
||||
reads the ``satellite_provider_url``, ``service_api_key``, and
|
||||
``download_*`` fields to drive AZ-316.
|
||||
|
||||
All defaults are conservative no-op values so unit tests / replay
|
||||
runs that do not exercise C11 keep working without YAML; the factory
|
||||
raises :class:`ConfigError` when an empty production-required field
|
||||
(``service_api_key``, ``companion_id``, etc.) is observed in operator
|
||||
wiring.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,25 +30,42 @@ _DEFAULT_BATCH_SIZE: int = 25
|
||||
_DEFAULT_HTTP_TIMEOUT_S: float = 30.0
|
||||
_DEFAULT_MAX_RETRY_AFTER_S: int = 600
|
||||
_MAX_BATCH_SIZE: int = 200
|
||||
_DEFAULT_DOWNLOAD_RESOLUTION_FLOOR: float = 0.5
|
||||
_DEFAULT_DOWNLOAD_MAX_5XX_RETRIES: int = 4
|
||||
_MIN_DOWNLOAD_RETRIES: int = 1
|
||||
_MAX_DOWNLOAD_RETRIES: int = 16
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class C11Config:
|
||||
"""Per-component config for C11 tile manager (upload path).
|
||||
"""Per-component config for C11 tile manager (upload + download paths).
|
||||
|
||||
``satellite_provider_ingest_url`` is the parent-suite ingest base
|
||||
URL (e.g. ``https://satellite-provider.example.com``); the
|
||||
uploader appends ``/api/satellite/tiles/ingest`` to it. Defaulted
|
||||
to empty so unit tests / replay runs that do not exercise the
|
||||
upload path stay no-op; production configuration MUST set this
|
||||
via YAML / env override or :class:`HttpTileUploader` raises
|
||||
:class:`SatelliteProviderError` on the first attempt.
|
||||
Upload-side fields (AZ-319):
|
||||
|
||||
``companion_id`` is the stable per-companion identifier the
|
||||
parent suite's voting layer uses to attribute uploads to one
|
||||
physical airframe. Defaulted to empty so test runs without a
|
||||
paired companion stay valid; the factory raises ``ConfigError``
|
||||
when the empty default is used in operator / production wiring.
|
||||
* ``satellite_provider_ingest_url`` — base URL for the upload
|
||||
endpoint; ``HttpTileUploader`` appends
|
||||
``/api/satellite/tiles/ingest``. Empty → upload factory raises
|
||||
:class:`ConfigError`.
|
||||
* ``upload_batch_size`` — tiles per multipart POST.
|
||||
* ``upload_http_timeout_s`` — per-request timeout (seconds).
|
||||
* ``upload_max_retry_after_s`` — cumulative 429 ``Retry-After``
|
||||
cap before :class:`RateLimitedError`.
|
||||
* ``companion_id`` — stable per-companion id for D-PROJ-2 voting.
|
||||
|
||||
Download-side fields (AZ-316):
|
||||
|
||||
* ``satellite_provider_url`` — base URL for the GET surface;
|
||||
``HttpTileDownloader`` appends per-tile / list paths.
|
||||
* ``service_api_key`` — bearer token for authenticated GETs;
|
||||
logged ONLY redacted (``Bearer ***``). Empty → download factory
|
||||
raises :class:`ConfigError`.
|
||||
* ``download_http_timeout_s`` — per-request timeout (seconds).
|
||||
* ``download_max_5xx_retries`` — exponential-backoff cap before
|
||||
:class:`SatelliteProviderError`.
|
||||
* ``download_max_retry_after_s`` — cumulative 429 ``Retry-After``
|
||||
cap before :class:`RateLimitedError`.
|
||||
* ``download_resolution_floor_m_per_px`` — RESTRICT-SAT-4 lower
|
||||
bound for the C11 boundary check; defaults to 0.5 m/px.
|
||||
"""
|
||||
|
||||
satellite_provider_ingest_url: str = ""
|
||||
@@ -55,6 +74,13 @@ class C11Config:
|
||||
upload_max_retry_after_s: int = _DEFAULT_MAX_RETRY_AFTER_S
|
||||
companion_id: str = ""
|
||||
|
||||
satellite_provider_url: str = ""
|
||||
service_api_key: str = ""
|
||||
download_http_timeout_s: float = _DEFAULT_HTTP_TIMEOUT_S
|
||||
download_max_5xx_retries: int = _DEFAULT_DOWNLOAD_MAX_5XX_RETRIES
|
||||
download_max_retry_after_s: int = _DEFAULT_MAX_RETRY_AFTER_S
|
||||
download_resolution_floor_m_per_px: float = _DEFAULT_DOWNLOAD_RESOLUTION_FLOOR
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 1 <= self.upload_batch_size <= _MAX_BATCH_SIZE:
|
||||
raise ConfigError(
|
||||
@@ -71,3 +97,24 @@ class C11Config:
|
||||
"C11Config.upload_max_retry_after_s must be > 0; "
|
||||
f"got {self.upload_max_retry_after_s}"
|
||||
)
|
||||
if self.download_http_timeout_s <= 0:
|
||||
raise ConfigError(
|
||||
"C11Config.download_http_timeout_s must be > 0; "
|
||||
f"got {self.download_http_timeout_s}"
|
||||
)
|
||||
if not _MIN_DOWNLOAD_RETRIES <= self.download_max_5xx_retries <= _MAX_DOWNLOAD_RETRIES:
|
||||
raise ConfigError(
|
||||
"C11Config.download_max_5xx_retries must be in "
|
||||
f"[{_MIN_DOWNLOAD_RETRIES}, {_MAX_DOWNLOAD_RETRIES}]; "
|
||||
f"got {self.download_max_5xx_retries}"
|
||||
)
|
||||
if self.download_max_retry_after_s <= 0:
|
||||
raise ConfigError(
|
||||
"C11Config.download_max_retry_after_s must be > 0; "
|
||||
f"got {self.download_max_retry_after_s}"
|
||||
)
|
||||
if self.download_resolution_floor_m_per_px <= 0:
|
||||
raise ConfigError(
|
||||
"C11Config.download_resolution_floor_m_per_px must be > 0; "
|
||||
f"got {self.download_resolution_floor_m_per_px}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user