Files
siloqy/prod/clean_arch/dita_v2/launcher.py

360 lines
13 KiB
Python
Raw Normal View History

"""Operator-facing bootstrap helpers for DITAv2.
This module keeps the wiring explicit:
- control plane selection
- Zinc plane selection
- projection sink selection
- venue adapter selection
The defaults stay safe and testable. Real shared-memory or live BingX wiring
is only enabled when the caller opts in via arguments or environment.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import asyncio
import inspect
import os
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.config import BingxInstrumentProviderConfig
from prod.bingx.enums import BingxEnvironment
from .bingx_venue import BingxVenueAdapter
from .control import BackendMode
from .control import ControlPlane
from .control import ControlUpdate
from .control import KernelControlSnapshot
from .control import KernelMode
from .control import KernelVerbosity
from .control import build_control_plane
from .mock_venue import MockVenueAdapter
from .mock_venue import MockVenueScenario
from .projection import HazelcastProjection
from .projection import build_projection
from .real_control_plane import RealZincControlPlane
from .real_control_plane import RealZincUnavailable
from .real_zinc_plane import RealZincPlane
from .real_zinc_plane import RealZincUnavailable as RealZincPlaneUnavailable
from .rust_backend import ExecutionKernel
from .venue import VenueAdapter
from .zinc_plane import InMemoryZincPlane
from .zinc_plane import ZincPlane
PROJECT_ROOT = Path(__file__).resolve().parents[3]
load_dotenv(PROJECT_ROOT / ".env")
class LauncherVenueMode(str, Enum):
MOCK = "MOCK"
BINGX = "BINGX"
class LauncherZincMode(str, Enum):
IN_MEMORY = "IN_MEMORY"
REAL = "REAL"
@dataclass
class DITAv2LauncherBundle:
"""Concrete runtime components assembled by the launcher."""
kernel: ExecutionKernel
control_plane: ControlPlane
projection: HazelcastProjection
zinc_plane: ZincPlane
venue: VenueAdapter
def close(self) -> None:
_maybe_close(self.venue)
_maybe_close(self.zinc_plane)
_maybe_close(self.control_plane)
def _env_upper(name: str, default: str = "") -> str:
return str(os.environ.get(name, default)).strip().upper()
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.environ.get(name)
if raw is None:
return default
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
def _resolve_control_mode() -> KernelMode | None:
raw = _env_upper("DITA_V2_MODE", "")
if raw == KernelMode.DEBUG.value:
return KernelMode.DEBUG
if raw == KernelMode.NORMAL.value:
return KernelMode.NORMAL
return None
def _resolve_control_verbosity() -> KernelVerbosity | None:
raw = _env_upper("DITA_V2_VERBOSITY", "")
if raw == KernelVerbosity.TRACE.value:
return KernelVerbosity.TRACE
if raw == KernelVerbosity.VERBOSE.value:
return KernelVerbosity.VERBOSE
if raw == KernelVerbosity.QUIET.value:
return KernelVerbosity.QUIET
return None
def _resolve_backend_mode() -> BackendMode | None:
raw = _env_upper("DITA_V2_BACKEND_MODE", "")
if raw == BackendMode.BINGX.value:
return BackendMode.BINGX
if raw == BackendMode.MOCK.value:
return BackendMode.MOCK
return None
def _control_update_from_env() -> ControlUpdate | None:
fields: dict[str, Any] = {}
mode = _resolve_control_mode()
if mode is not None:
fields["mode"] = mode
verbosity = _resolve_control_verbosity()
if verbosity is not None:
fields["verbosity"] = verbosity
backend_mode = _resolve_backend_mode()
if backend_mode is not None:
fields["backend_mode"] = backend_mode
raw = os.environ.get("DITA_V2_DEBUG_CLICKHOUSE")
if raw is not None:
fields["debug_clickhouse_enabled"] = _env_bool("DITA_V2_DEBUG_CLICKHOUSE", True)
raw = os.environ.get("DITA_V2_TRACE_TRANSITIONS")
if raw is not None:
fields["trace_transitions"] = _env_bool("DITA_V2_TRACE_TRANSITIONS", False)
raw = os.environ.get("DITA_V2_MIRROR_TO_HAZELCAST")
if raw is not None:
fields["mirror_to_hazelcast"] = _env_bool("DITA_V2_MIRROR_TO_HAZELCAST", True)
raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT")
if raw is not None:
try:
fields["active_slot_limit"] = max(1, int(str(raw).strip()))
except Exception:
pass
raw = os.environ.get("DITA_V2_RECONCILE_ON_RESTART")
if raw is not None:
fields["reconcile_on_restart"] = _env_bool("DITA_V2_RECONCILE_ON_RESTART", True)
return ControlUpdate(**fields) if fields else None
def _resolve_venue_mode(venue_mode: Optional[str] = None) -> LauncherVenueMode:
raw = _env_upper("DITA_V2_VENUE", venue_mode or LauncherVenueMode.MOCK.value)
if raw == LauncherVenueMode.BINGX.value:
return LauncherVenueMode.BINGX
return LauncherVenueMode.MOCK
def _resolve_zinc_mode(zinc_mode: Optional[str] = None) -> LauncherZincMode:
raw = _env_upper("DITA_V2_ZINC", zinc_mode or LauncherZincMode.IN_MEMORY.value)
if raw == LauncherZincMode.REAL.value:
return LauncherZincMode.REAL
return LauncherZincMode.IN_MEMORY
def _resolve_hazelcast_real(prefer_real_hazelcast: Optional[bool] = None) -> bool:
if prefer_real_hazelcast is not None:
return bool(prefer_real_hazelcast)
raw = _env_upper("DITA_V2_HAZELCAST", "")
return raw in {"REAL", "REAL_HZ", "HAZELCAST"}
def build_bingx_exec_client_config(
*,
environment: Optional[BingxEnvironment] = None,
allow_mainnet: Optional[bool] = None,
recv_window_ms: Optional[int] = None,
default_leverage: Optional[int] = None,
exchange_leverage_cap: Optional[int] = None,
prefer_websocket: Optional[bool] = None,
sizing_mode: Optional[str] = None,
) -> BingxExecClientConfig:
"""Build the direct BingX config used by the DITAv2 launcher."""
resolved_environment = environment or (
BingxEnvironment.LIVE if _env_upper("DOLPHIN_BINGX_ENV", "VST") == "LIVE" else BingxEnvironment.VST
)
resolved_allow_mainnet = _env_bool("DOLPHIN_BINGX_ALLOW_MAINNET", False) if allow_mainnet is None else bool(allow_mainnet)
resolved_recv_window = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000")) if recv_window_ms is None else int(recv_window_ms)
resolved_default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")) if default_leverage is None else int(default_leverage)
resolved_exchange_cap = int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")) if exchange_leverage_cap is None else int(exchange_leverage_cap)
resolved_prefer_ws = _env_bool("DOLPHIN_BINGX_PREFER_WEBSOCKET", False) if prefer_websocket is None else bool(prefer_websocket)
resolved_sizing_mode = sizing_mode or os.environ.get("DOLPHIN_BINGX_SIZING_MODE", "testnet")
return BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY"),
secret_key=os.environ.get("BINGX_SECRET_KEY"),
environment=resolved_environment,
allow_mainnet=resolved_allow_mainnet,
recv_window_ms=max(1, resolved_recv_window),
default_leverage=max(1, resolved_default_leverage),
exchange_leverage_cap=max(1, resolved_exchange_cap),
prefer_websocket=resolved_prefer_ws,
sizing_mode=resolved_sizing_mode,
journal_strategy=os.environ.get("DOLPHIN_BINGX_JOURNAL_STRATEGY", "dita_v2"),
journal_db=os.environ.get("DOLPHIN_BINGX_JOURNAL_DB", "dolphin_pink"),
instrument_provider=BingxInstrumentProviderConfig(load_all=True),
)
def _build_control_plane(
*,
prefix: str,
control_plane: Optional[ControlPlane] = None,
) -> ControlPlane:
plane = control_plane or build_control_plane(prefix=prefix)
update = _control_update_from_env()
if update is not None:
plane.update(update)
return plane
def _build_zinc_plane(
*,
prefix: str,
slot_count: int,
zinc_mode: Optional[LauncherZincMode] = None,
zinc_plane: Optional[ZincPlane] = None,
) -> ZincPlane:
if zinc_plane is not None:
return zinc_plane
resolved_mode = zinc_mode or _resolve_zinc_mode()
if resolved_mode is LauncherZincMode.REAL:
try:
return RealZincPlane(prefix=prefix, slot_count=slot_count, create=True)
except (RealZincPlaneUnavailable, RealZincUnavailable, Exception):
pass
return InMemoryZincPlane()
def _build_venue(
*,
venue_mode: Optional[LauncherVenueMode] = None,
mock_scenario: Optional[MockVenueScenario] = None,
bingx_config: Optional[BingxExecClientConfig] = None,
bingx_backend: Optional[Any] = None,
venue: Optional[VenueAdapter] = None,
) -> VenueAdapter:
if venue is not None:
return venue
resolved_mode = venue_mode or _resolve_venue_mode()
if resolved_mode is LauncherVenueMode.BINGX:
backend = bingx_backend
if backend is None:
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
backend = BingxDirectExecutionAdapter(bingx_config or build_bingx_exec_client_config())
return BingxVenueAdapter(backend=backend)
return MockVenueAdapter(mock_scenario)
def _maybe_close(obj: Any) -> None:
for method_name in ("close", "disconnect"):
method = getattr(obj, method_name, None)
if method is None:
continue
try:
result = method()
except TypeError:
continue
if inspect.isawaitable(result):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None and loop.is_running():
# Called from within an async context — schedule on the
# shared executor so asyncio.run() can create its own loop
# without conflicting with the caller's loop (O1).
import concurrent.futures as _cf
with _cf.ThreadPoolExecutor(max_workers=1) as _pool:
_pool.submit(asyncio.run, result).result(timeout=10.0)
else:
asyncio.run(result)
break
def build_launcher_bundle(
*,
max_slots: int = 10,
prefix: Optional[str] = None,
control_plane: Optional[ControlPlane] = None,
projection: Optional[HazelcastProjection] = None,
projection_client: Optional[Any] = None,
zinc_plane: Optional[ZincPlane] = None,
venue: Optional[VenueAdapter] = None,
venue_mode: Optional[LauncherVenueMode | str] = None,
zinc_mode: Optional[LauncherZincMode | str] = None,
bingx_config: Optional[BingxExecClientConfig] = None,
bingx_backend: Optional[Any] = None,
mock_scenario: Optional[MockVenueScenario] = None,
) -> DITAv2LauncherBundle:
"""Build a fully wired DITAv2 runtime bundle.
Defaults stay non-destructive:
- in-memory Zinc plane
- in-process control plane
- mock venue
- callback projection unless a Hazelcast client is supplied
"""
resolved_prefix = (prefix or os.environ.get("DITA_V2_PREFIX", "dita_v2")).strip() or "dita_v2"
if isinstance(venue_mode, LauncherVenueMode):
resolved_venue_mode = venue_mode
elif isinstance(venue_mode, str):
resolved_venue_mode = LauncherVenueMode(venue_mode.strip().upper())
else:
resolved_venue_mode = None
if isinstance(zinc_mode, LauncherZincMode):
resolved_zinc_mode = zinc_mode
elif isinstance(zinc_mode, str):
resolved_zinc_mode = LauncherZincMode(zinc_mode.strip().upper())
else:
resolved_zinc_mode = None
active_control_plane = _build_control_plane(prefix=resolved_prefix, control_plane=control_plane)
control_snapshot = active_control_plane.read()
active_projection = projection or build_projection(
client=projection_client,
prefer_real_hazelcast=_resolve_hazelcast_real(),
control_snapshot=control_snapshot,
)
active_zinc_plane = _build_zinc_plane(
prefix=resolved_prefix,
slot_count=int(max_slots),
zinc_mode=resolved_zinc_mode,
zinc_plane=zinc_plane,
)
active_venue = _build_venue(
venue_mode=resolved_venue_mode,
mock_scenario=mock_scenario,
bingx_config=bingx_config,
bingx_backend=bingx_backend,
venue=venue,
)
kernel = ExecutionKernel(
max_slots=int(max_slots),
control_plane=active_control_plane,
venue=active_venue,
projection=active_projection,
projection_client=projection_client,
zinc_plane=active_zinc_plane,
)
return DITAv2LauncherBundle(
kernel=kernel,
control_plane=active_control_plane,
projection=active_projection,
zinc_plane=active_zinc_plane,
venue=active_venue,
)