[AZ-390] [AZ-392] C8 FC/GCS adapter foundation + covariance projector

Adds the C8 foundation:
- FcAdapter / GcsAdapter / ReplaySink Protocols + contract DTOs in
  _types/fc.py (PortConfig, FcKind, FlightState, GpsStatus, Severity,
  TelemetryKind, FcTelemetryFrame, FlightStateSignal, GpsHealth,
  OperatorCommand, Subscription, Imu/Attitude samples).
- Disjoint FcAdapterError / GcsAdapterError trees with
  SourceSetSwitchNotSupportedError <: SourceSetSwitchError per AC-9.
- FcConfig + GcsConfig cross-cutting Config blocks with config-load
  validation (unknown strategy rejected at __post_init__).
- runtime_root/fc_factory.py: build_fc_adapter / build_gcs_adapter
  with BUILD_FC_*/BUILD_GCS_* flag gating + INFO log on load +
  single-writer outbound-thread binding.
- CovarianceProjector (helper, AZ-392): 6x6 -> 3x3 -> 2x2 ->
  sqrt(lambda_max) reduction; AP returns float m, iNav returns int mm
  with uint16 clamp + WARN + FDR record. Non-SPD / NaN / wrong-shape
  raise FcEmitError and emit an FDR ERROR record carrying frame_id.

Contracts:
- composition_root_protocol.md 1.1.0 -> 1.2.0 (added fc/gcs blocks +
  build_fc_adapter / build_gcs_adapter + outbound-thread binding).
- fc_adapter_protocol.md unchanged (this batch implements v1.0.0).

Tests: 410 pass / 2 skip / 0 fail (+53 new tests in batch 8).

AZ-391 (inbound subscription) deferred to batch 9 — pulls YAMSPy as
a new external dependency (iNav MSP2 decode).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:17:59 +03:00
parent e4ecdaf619
commit 362e93c626
22 changed files with 1909 additions and 59 deletions
@@ -0,0 +1,543 @@
"""Composition root (AZ-270 / E-CC-CONF; ADR-009 interface-first DI).
The single module allowed to import concrete strategy implementations across
components. Per-binary ``compose_*`` functions resolve the ``Config``-selected
strategies, validate them against the registry (ADR-002 gate #3), and assemble
the component graph in dependency order.
Per-binary entrypoints:
* :func:`compose_root` - airborne runtime
* :func:`compose_operator` - operator-side tooling (pre-flight, post-landing)
* :func:`compose_replay` - replay-cli runtime (extension owned by AZ-401)
Public surface frozen by
``_docs/02_document/contracts/shared_config/composition_root_protocol.md`` v1.0.0.
"""
from __future__ import annotations
import os
import sys
from collections.abc import Callable, Iterable, Mapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Final, Literal, get_args
from gps_denied_onboard.config import Config, load_config
from gps_denied_onboard.runtime_root.fc_factory import (
OutboundThreadAlreadyBoundError,
bind_outbound_emit_thread,
build_fc_adapter,
build_gcs_adapter,
clear_outbound_thread_binding,
clear_strategy_registries,
list_registered_fc_strategies,
list_registered_gcs_strategies,
register_fc_adapter,
register_gcs_adapter,
)
if TYPE_CHECKING:
from gps_denied_onboard.components.c13_fdr.headers import FlightHeader
from gps_denied_onboard.components.c13_fdr.writer import FileFdrWriter
__all__ = [
"EXIT_FDR_OPEN_FAILURE",
"EXIT_GENERIC_FAILURE",
"REQUIRED_ENV_VARS",
"ConfigurationError",
"OperatorRoot",
"OutboundThreadAlreadyBoundError",
"RuntimeRoot",
"StrategyNotLinkedError",
"StrategyTier",
"TakeoffResult",
"bind_outbound_emit_thread",
"build_fc_adapter",
"build_gcs_adapter",
"clear_outbound_thread_binding",
"clear_strategy_registries",
"clear_strategy_registry",
"compose_operator",
"compose_replay",
"compose_root",
"list_registered_fc_strategies",
"list_registered_gcs_strategies",
"list_registered_strategies",
"main",
"register_fc_adapter",
"register_gcs_adapter",
"register_strategy",
"take_off",
]
EXIT_GENERIC_FAILURE: Final[int] = 1
EXIT_FDR_OPEN_FAILURE: Final[int] = 2
StrategyTier = Literal["airborne", "operator", "shared"]
_ALL_TIERS: tuple[StrategyTier, ...] = get_args(StrategyTier)
class ConfigurationError(RuntimeError):
"""Raised when a required environment variable is missing.
AC-8 (Bootstrap / AZ-263).
"""
class StrategyNotLinkedError(RuntimeError):
"""Raised when the config selects a strategy that is not linked into this binary.
Carries the offending strategy name, the owning component slug, and the
actually-linked strategies for that component — gives the operator a
clear next step (build with the right flag, or pick a different strategy).
"""
def __init__(
self,
strategy_name: str,
component_slug: str,
available_strategies: Iterable[str],
*,
reason: str = "not linked",
) -> None:
self.strategy_name: str = strategy_name
self.component_slug: str = component_slug
self.available_strategies: list[str] = sorted(set(available_strategies))
self.reason: str = reason
available_text = ", ".join(self.available_strategies) or "<none>"
super().__init__(
f"strategy {strategy_name!r} requested for component {component_slug!r} is "
f"{reason}; available strategies: [{available_text}]"
)
REQUIRED_ENV_VARS: tuple[str, ...] = (
"GPS_DENIED_FC_PROFILE",
"GPS_DENIED_TIER",
"DB_URL",
"CAMERA_CALIBRATION_PATH",
"LOG_LEVEL",
"LOG_SINK",
"INFERENCE_BACKEND",
"FDR_PATH",
"TILE_CACHE_PATH",
)
@dataclass(frozen=True)
class _Registration:
component_slug: str
strategy_name: str
factory: Callable[..., Any]
tier: StrategyTier
depends_on: tuple[str, ...]
_STRATEGY_REGISTRY: dict[tuple[str, str], _Registration] = {}
def register_strategy(
component_slug: str,
strategy_name: str,
factory: Callable[..., Any],
*,
tier: StrategyTier = "shared",
depends_on: Iterable[str] = (),
) -> None:
"""Register a concrete strategy implementation for ``component_slug``.
The single allowed call site is the composition root or a binary-specific
bootstrap module (one module per ``BUILD_*`` flag combination). Calling
this from a component module is an architecture violation; AC-6 catches
that statically.
"""
if tier not in _ALL_TIERS:
raise ValueError(f"tier must be one of {_ALL_TIERS}; got {tier!r}")
key = (component_slug, strategy_name)
existing = _STRATEGY_REGISTRY.get(key)
incoming = _Registration(
component_slug=component_slug,
strategy_name=strategy_name,
factory=factory,
tier=tier,
depends_on=tuple(depends_on),
)
if existing is not None and existing != incoming:
raise StrategyNotLinkedError(
strategy_name=strategy_name,
component_slug=component_slug,
available_strategies=list_registered_strategies(component_slug),
reason="duplicate registration with conflicting attributes",
)
_STRATEGY_REGISTRY[key] = incoming
def clear_strategy_registry() -> None:
"""Reset the global registry; intended for unit-test isolation only."""
_STRATEGY_REGISTRY.clear()
def list_registered_strategies(component_slug: str) -> list[str]:
"""Return the strategy names registered for ``component_slug`` (sorted)."""
return sorted(
reg.strategy_name
for (slug, _name), reg in _STRATEGY_REGISTRY.items()
if slug == component_slug
)
@dataclass(frozen=True)
class RuntimeRoot:
"""Composed airborne runtime graph (every component slot populated)."""
binary: str
profile: str
components: Mapping[str, Any] = field(default_factory=dict)
construction_order: tuple[str, ...] = ()
@dataclass(frozen=True)
class OperatorRoot:
"""Composed operator-side runtime graph (operator-tier components only)."""
binary: str
profile: str
components: Mapping[str, Any] = field(default_factory=dict)
construction_order: tuple[str, ...] = ()
def _check_required_env(extra_required: Iterable[str] = ()) -> None:
missing = [name for name in (*REQUIRED_ENV_VARS, *extra_required) if name not in os.environ]
if missing:
raise ConfigurationError(
"Missing required environment variable(s): "
+ ", ".join(missing)
+ ". See `.env.example` for the full list."
)
def _resolve_strategy(
component_slug: str,
strategy_name: str,
allowed_tiers: frozenset[StrategyTier],
) -> _Registration:
"""Look up ``(component_slug, strategy_name)`` in the registry and gate by tier."""
key = (component_slug, strategy_name)
registration = _STRATEGY_REGISTRY.get(key)
if registration is None:
raise StrategyNotLinkedError(
strategy_name=strategy_name,
component_slug=component_slug,
available_strategies=list_registered_strategies(component_slug),
)
if registration.tier not in allowed_tiers:
raise StrategyNotLinkedError(
strategy_name=strategy_name,
component_slug=component_slug,
available_strategies=list_registered_strategies(component_slug),
reason=(
f"registered under tier={registration.tier!r}; "
f"not allowed in this binary (allowed tiers: {sorted(allowed_tiers)})"
),
)
return registration
def _topo_order(
target_slugs: Iterable[str], registrations: Mapping[str, _Registration]
) -> list[str]:
"""Return ``target_slugs`` in a dependency-respecting order (Kahn's algorithm)."""
ordered: list[str] = []
remaining = set(target_slugs)
visited: set[str] = set()
def visit(slug: str, stack: tuple[str, ...]) -> None:
if slug in visited:
return
if slug in stack:
raise StrategyNotLinkedError(
strategy_name=registrations[slug].strategy_name,
component_slug=slug,
available_strategies=list_registered_strategies(slug),
reason=f"dependency cycle detected: {' -> '.join((*stack, slug))}",
)
if slug not in registrations:
return
for dep in registrations[slug].depends_on:
visit(dep, (*stack, slug))
visited.add(slug)
ordered.append(slug)
for slug in sorted(remaining):
visit(slug, ())
return [slug for slug in ordered if slug in remaining]
def _compose(
config: Config,
*,
binary: str,
allowed_tiers: frozenset[StrategyTier],
extra_required_env: Iterable[str],
) -> tuple[dict[str, Any], tuple[str, ...]]:
"""Shared composition path used by ``compose_root`` / ``compose_operator``."""
_check_required_env(extra_required=extra_required_env)
selections = _resolve_component_strategies(config, allowed_tiers)
resolved: dict[str, _Registration] = {
slug: _resolve_strategy(slug, strategy, allowed_tiers)
for slug, strategy in selections.items()
}
order = _topo_order(resolved.keys(), resolved)
constructed: dict[str, Any] = {}
for slug in order:
registration = resolved[slug]
try:
constructed[slug] = registration.factory(config, constructed)
except Exception:
# All-or-nothing: close anything already built before re-raising.
_close_partial_instances(constructed)
raise
_ = binary # documented but unused beyond labelling the returned root
return constructed, tuple(order)
def _close_partial_instances(instances: Mapping[str, Any]) -> None:
"""Best-effort cleanup of partially-constructed components on failure.
Calls ``.close()`` on each instance that exposes one; swallows individual
close failures so the original exception propagates to the caller.
"""
for inst in instances.values():
close = getattr(inst, "close", None)
if callable(close):
try:
close()
except Exception:
continue
def _resolve_component_strategies(
config: Config, allowed_tiers: frozenset[StrategyTier]
) -> dict[str, str]:
"""Translate ``config.components[slug]`` into ``{slug: strategy_name}``.
Two recognised shapes for a per-component block:
* a dataclass with a ``strategy`` field (preferred);
* a mapping with a ``"strategy"`` key (fallback for raw YAML).
Blocks without a ``strategy`` field are skipped — they configure a
component whose strategy was hard-wired by the binary's bootstrap.
"""
_ = allowed_tiers # tier filtering happens in ``_resolve_strategy``
selections: dict[str, str] = {}
for slug, block in (config.components or {}).items():
strategy = _read_strategy_attr(block)
if strategy is None:
continue
if not isinstance(strategy, str) or not strategy:
raise StrategyNotLinkedError(
strategy_name=str(strategy),
component_slug=slug,
available_strategies=list_registered_strategies(slug),
reason="config.components[slug].strategy must be a non-empty string",
)
selections[slug] = strategy
return selections
def _read_strategy_attr(block: Any) -> Any:
if hasattr(block, "strategy"):
return block.strategy
if isinstance(block, Mapping):
return block.get("strategy")
return None
def compose_root(config: Config) -> RuntimeRoot:
"""Compose the airborne runtime graph (per contract v1.0.0)."""
components, order = _compose(
config,
binary="airborne",
allowed_tiers=frozenset({"airborne", "shared"}),
extra_required_env=("MAVLINK_SIGNING_KEY",),
)
return RuntimeRoot(
binary="airborne",
profile=os.environ["GPS_DENIED_FC_PROFILE"],
components=components,
construction_order=order,
)
def compose_operator(config: Config) -> OperatorRoot:
"""Compose the operator-tooling runtime graph (per contract v1.0.0)."""
components, order = _compose(
config,
binary="operator-tooling",
allowed_tiers=frozenset({"operator", "shared"}),
extra_required_env=("SATELLITE_PROVIDER_URL",),
)
return OperatorRoot(
binary="operator-tooling",
profile=os.environ["GPS_DENIED_FC_PROFILE"],
components=components,
construction_order=order,
)
def compose_replay(config: Config) -> RuntimeRoot:
"""Compose the replay-cli runtime graph. Concrete wiring is owned by AZ-401."""
components, order = _compose(
config,
binary="replay-cli",
allowed_tiers=frozenset({"airborne", "shared"}),
extra_required_env=(),
)
return RuntimeRoot(
binary="replay-cli",
profile=os.environ["GPS_DENIED_FC_PROFILE"],
components=components,
construction_order=order,
)
@dataclass(frozen=True)
class TakeoffResult:
"""Successful takeoff: writer is open, FC adapter is wired, components started.
Returned by :func:`take_off` on the success path. The abort path
never returns — it calls :func:`sys.exit` with
:data:`EXIT_FDR_OPEN_FAILURE`.
"""
writer: Any
fc_adapter: Any
other_components: Mapping[str, Any] = field(default_factory=dict)
def take_off(
config: Config,
*,
writer_factory: Callable[[Config], FileFdrWriter],
flight_header_factory: Callable[[Config], FlightHeader],
fc_adapter_factory: Callable[[Config, Any], Any],
other_components_factory: Callable[[Config, Any, Any], Mapping[str, Any]] | None = None,
flight_root_for_message: str | None = None,
) -> TakeoffResult:
"""Run the strict airborne takeoff sequence (AZ-296).
Order: ``writer_factory`` → ``writer.start()`` →
``writer.open_flight(header)`` → (only on success) ``fc_adapter_factory``
→ ``other_components_factory``.
On :exc:`FdrOpenError` from ``open_flight``, this function logs ONE
structured ERROR, calls ``writer.stop()`` (best-effort), prints the
fixed FATAL line to stderr, and exits the process with
:data:`EXIT_FDR_OPEN_FAILURE`. It never returns on that path.
Other exceptions propagate up unchanged; they reach :func:`main`
which exits with :data:`EXIT_GENERIC_FAILURE`.
Tests inject factories; production wiring builds factories from
:func:`compose_root`.
"""
from gps_denied_onboard.components.c13_fdr.errors import FdrOpenError
writer = writer_factory(config)
writer.start()
try:
writer.open_flight(flight_header_factory(config))
except FdrOpenError as exc:
_abort_takeoff_on_fdr_open_error(
writer=writer,
config=config,
exc=exc,
flight_root=flight_root_for_message,
)
raise AssertionError( # pragma: no cover — abort helper must exit
"unreachable: _abort_takeoff_on_fdr_open_error must exit"
) from None
fc_adapter = fc_adapter_factory(config, writer)
other: Mapping[str, Any] = {}
if other_components_factory is not None:
other = other_components_factory(config, writer, fc_adapter)
return TakeoffResult(writer=writer, fc_adapter=fc_adapter, other_components=other)
def _abort_takeoff_on_fdr_open_error(
*,
writer: Any,
config: Config,
exc: BaseException,
flight_root: str | None,
) -> None:
"""Execute the documented abort path; never returns."""
from gps_denied_onboard.logging import get_logger
resolved_root = flight_root if flight_root is not None else _read_flight_root(config)
underlying = str(exc)
log = get_logger("composition_root")
try:
log.error(
"composition_root.takeoff_aborted",
extra={
"kind": "composition_root.takeoff_aborted",
"kv": {
"reason": "fdr_open_error",
"underlying": underlying,
"flight_root": resolved_root,
},
},
)
except Exception:
# Logging must never block the abort path.
pass
try:
writer.stop()
except Exception as stop_exc:
try:
log.error(
"composition_root.takeoff_abort_stop_failed",
extra={
"kind": "composition_root.takeoff_abort_stop_failed",
"kv": {"error": repr(stop_exc)},
},
)
except Exception:
pass
print(
f"FATAL: cannot open FDR at {resolved_root}: {underlying}; aborting takeoff (exit 2)",
file=sys.stderr,
flush=True,
)
# sys.exit raises SystemExit, which propagates to the process boundary.
# In the unlikely event that some intermediate frame catches SystemExit
# (e.g. a misbehaving test harness), the fallback below ensures the
# process still terminates with the documented exit code.
sys.exit(EXIT_FDR_OPEN_FAILURE)
os._exit(EXIT_FDR_OPEN_FAILURE) # pragma: no cover — only reached if SystemExit is intercepted
def _read_flight_root(config: Config) -> str:
fdr = getattr(config, "fdr", None)
if fdr is None:
return "<unknown>"
path = getattr(fdr, "path", None)
return str(path) if path is not None else "<unknown>"
def main() -> int: # pragma: no cover — guarded entrypoint
try:
config = load_config(env=os.environ, paths=())
compose_root(config)
except (ConfigurationError, StrategyNotLinkedError, RuntimeError) as exc:
print(f"runtime_root: {exc}", file=sys.stderr)
return EXIT_GENERIC_FAILURE
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,216 @@
"""Composition-root factories for C8 (AZ-390 / E-C8).
Lazy-imports the per-variant adapter classes so the ADR-002 build-flag
gate stays honest: the binary's bootstrap (one module per
``BUILD_FC_*`` / ``BUILD_GCS_*`` combination) registers the concrete
strategy via :func:`register_fc_adapter` / :func:`register_gcs_adapter`
ahead of `build_fc_adapter` / `build_gcs_adapter`.
A second binding to the outbound emit thread is rejected (AC-6); the
single-writer invariant for outbound is enforced statically by the
composition root, not by the adapter itself.
"""
from __future__ import annotations
import os
import threading
from collections.abc import Callable
from typing import Any, Final
from gps_denied_onboard.components.c8_fc_adapter.errors import (
FcAdapterConfigError,
GcsAdapterConfigError,
)
from gps_denied_onboard.components.c8_fc_adapter.interface import (
FcAdapter,
GcsAdapter,
)
from gps_denied_onboard.config import Config
from gps_denied_onboard.logging import get_logger
__all__ = [
"OutboundThreadAlreadyBoundError",
"bind_outbound_emit_thread",
"build_fc_adapter",
"build_gcs_adapter",
"clear_outbound_thread_binding",
"clear_strategy_registries",
"list_registered_fc_strategies",
"list_registered_gcs_strategies",
"register_fc_adapter",
"register_gcs_adapter",
]
# ----------------------------------------------------------------------
# Strategy registries (single source of truth; populated by binary
# bootstrap modules per ADR-002).
FcAdapterFactory = Callable[..., FcAdapter]
GcsAdapterFactory = Callable[..., GcsAdapter]
_FC_REGISTRY: dict[str, FcAdapterFactory] = {}
_GCS_REGISTRY: dict[str, GcsAdapterFactory] = {}
# Mapping from strategy slug -> documented BUILD_*_ flag name. The
# build-flag gate (AC-4) checks ``os.environ`` for the canonical name
# because flag state is a build-time artifact, not a config-time
# artifact.
_FC_BUILD_FLAGS: Final[dict[str, str]] = {
"ardupilot_plane": "BUILD_FC_ARDUPILOT_PLANE",
"inav": "BUILD_FC_INAV",
}
_GCS_BUILD_FLAGS: Final[dict[str, str]] = {
"qgc_mavlink": "BUILD_GCS_QGC_MAVLINK",
}
def register_fc_adapter(strategy: str, factory: FcAdapterFactory) -> None:
"""Register a concrete `FcAdapter` strategy.
Called from the per-binary bootstrap module (e.g.
``runtime_root._bootstrap_ap.py``) under the matching
``BUILD_FC_<VARIANT>`` flag. Duplicate registration with a
different factory is a build error.
"""
existing = _FC_REGISTRY.get(strategy)
if existing is not None and existing is not factory:
raise FcAdapterConfigError(f"duplicate FcAdapter registration for strategy {strategy!r}")
_FC_REGISTRY[strategy] = factory
def register_gcs_adapter(strategy: str, factory: GcsAdapterFactory) -> None:
existing = _GCS_REGISTRY.get(strategy)
if existing is not None and existing is not factory:
raise GcsAdapterConfigError(f"duplicate GcsAdapter registration for strategy {strategy!r}")
_GCS_REGISTRY[strategy] = factory
def clear_strategy_registries() -> None:
"""Reset both registries; intended for unit-test isolation only."""
_FC_REGISTRY.clear()
_GCS_REGISTRY.clear()
def list_registered_fc_strategies() -> list[str]:
return sorted(_FC_REGISTRY)
def list_registered_gcs_strategies() -> list[str]:
return sorted(_GCS_REGISTRY)
# ----------------------------------------------------------------------
# Single-writer outbound thread enforcement (Invariant 8 / AC-6).
class OutboundThreadAlreadyBoundError(RuntimeError):
"""Raised on a second :func:`bind_outbound_emit_thread` call."""
_outbound_lock = threading.Lock()
_outbound_bound_thread: int | None = None
def bind_outbound_emit_thread(thread_ident: int | None = None) -> int:
"""Bind ``thread_ident`` (defaults to the caller) as the sole emit thread.
A second call from any thread raises
:class:`OutboundThreadAlreadyBoundError`. The runtime root calls
this once per process before wiring outbound emit; the result is
the canonical thread id the adapter checks on every outbound call.
"""
global _outbound_bound_thread
ident = thread_ident if thread_ident is not None else threading.get_ident()
with _outbound_lock:
if _outbound_bound_thread is not None and _outbound_bound_thread != ident:
raise OutboundThreadAlreadyBoundError(
f"outbound emit thread already bound to {_outbound_bound_thread}; "
f"refused to re-bind to {ident}"
)
_outbound_bound_thread = ident
return ident
def clear_outbound_thread_binding() -> None:
"""Reset the outbound-thread binding; intended for unit-test isolation."""
global _outbound_bound_thread
with _outbound_lock:
_outbound_bound_thread = None
# ----------------------------------------------------------------------
# Build helpers — invoked by `compose_root` after C5 (FC) and after
# the FC adapter (GCS).
def build_fc_adapter(config: Config, **deps: Any) -> FcAdapter:
"""Resolve and build the configured `FcAdapter` strategy.
Validates the build-flag gate (AC-4); raises
:class:`FcAdapterConfigError` with the disabled-flag name when the
requested strategy is not linked into the running binary.
"""
strategy = config.fc.adapter
flag_name = _FC_BUILD_FLAGS.get(strategy)
if flag_name is None:
# config.fc.adapter went through FcConfig validation, so an
# unknown strategy here means we forgot to add it to the
# build-flag table — fail loudly.
raise FcAdapterConfigError(f"FC strategy {strategy!r} has no BUILD_FC_* flag mapping")
if os.environ.get(flag_name, "ON").upper() == "OFF":
raise FcAdapterConfigError(
f"{flag_name} is OFF — strategy {strategy!r} is not linked into this binary"
)
factory = _FC_REGISTRY.get(strategy)
if factory is None:
raise FcAdapterConfigError(
f"FC strategy {strategy!r} is selected by config.fc.adapter but "
f"not registered; registered strategies: "
f"{list_registered_fc_strategies()}"
)
adapter = factory(config=config, **deps)
_log_strategy_loaded(
kind="c8.adapter.strategy_loaded",
strategy=strategy,
port_device=config.fc.port_device,
)
return adapter
def build_gcs_adapter(config: Config, **deps: Any) -> GcsAdapter:
"""Resolve and build the configured `GcsAdapter` strategy (AC-7)."""
strategy = config.gcs.adapter
flag_name = _GCS_BUILD_FLAGS.get(strategy)
if flag_name is None:
raise GcsAdapterConfigError(f"GCS strategy {strategy!r} has no BUILD_GCS_* flag mapping")
if os.environ.get(flag_name, "ON").upper() == "OFF":
raise GcsAdapterConfigError(
f"{flag_name} is OFF — strategy {strategy!r} is not linked into this binary"
)
factory = _GCS_REGISTRY.get(strategy)
if factory is None:
raise GcsAdapterConfigError(
f"GCS strategy {strategy!r} is selected by config.gcs.adapter but "
f"not registered; registered strategies: "
f"{list_registered_gcs_strategies()}"
)
adapter = factory(config=config, **deps)
_log_strategy_loaded(
kind="c8.gcs.strategy_loaded",
strategy=strategy,
port_device=config.gcs.port_device,
)
return adapter
def _log_strategy_loaded(*, kind: str, strategy: str, port_device: str) -> None:
log = get_logger("runtime_root.fc_factory")
log.info(
f"{kind}: strategy={strategy} port_device={port_device}",
extra={
"kind": kind,
"kv": {"strategy": strategy, "port_device": port_device},
},
)