Refactor(coordinator): Split ghost reconstruction

_ghost_from_mapping interleaved defensive reads of corrupt persisted
fields, the physical-name fence, the stay-date fence and the
Reservation construction in one function.

Extract _dict_field and _str_field for the corrupt-tolerant reads
and _ghost_stay for the slot-name and stay-date fence. Each fence
still runs in the same order, logs the same message and returns None
on the same inputs, so ghost reservations are unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
This commit is contained in:
Andrew GrimbergandCopilot committed 2026-11-02 12:19:32 -08:00
1 parent 517af0bdbc
commit 1efab15543
1 file changed
+58 -36
@@ -5,6 +5,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from datetime import tzinfo
import logging import logging
from typing import Any from typing import Any
@@ -104,16 +105,10 @@ def _ghost_from_mapping(
ctx: ReservationBuildContext, ctx: ReservationBuildContext,
) -> _Reservation | None: ) -> _Reservation | None:
"""Return a ghost reservation from a persisted mapping, or None when fenced.""" """Return a ghost reservation from a persisted mapping, or None when fenced."""
identity = mapping.get("identity", {}) identity = _dict_field(mapping, "identity")
if not isinstance(identity, dict): slot_name = _str_field(identity, "slot_name")
identity = {} summary = _str_field(identity, "summary")
slot_name_raw = identity.get("slot_name", "") last_actual = _dict_field(mapping, "last_observed_actual")
slot_name = slot_name_raw if isinstance(slot_name_raw, str) else ""
summary_raw = identity.get("summary", "")
summary = summary_raw if isinstance(summary_raw, str) else ""
last_actual = mapping.get("last_observed_actual", {})
if not isinstance(last_actual, dict):
last_actual = {}
actual_name = last_actual.get("name_state") actual_name = last_actual.get("name_state")
if _ghost_physical_name_mismatch( if _ghost_physical_name_mismatch(
actual_name, slot_name, prefix, observed_mapping_keys, key, ctx actual_name, slot_name, prefix, observed_mapping_keys, key, ctx
@@ -128,33 +123,10 @@ def _ghost_from_mapping(
new_mc, new_mc,
) )
return None return None
start_raw = last_actual.get("start_state") stay = _ghost_stay(key, slot_name, last_actual, new_mc, ctx.timezone)
end_raw = last_actual.get("end_state") if stay is None:
if not slot_name or start_raw is None or end_raw is None:
_LOGGER.debug(
"Ghost reservation %s: missing slot_name or dates; slot remains "
"fenced with missing_count=%d",
key,
new_mc,
)
return None
start_dt = _coerce_persisted_datetime(start_raw)
end_dt = _coerce_persisted_datetime(end_raw)
if start_dt is not None and start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=ctx.timezone)
if end_dt is not None and end_dt.tzinfo is None:
end_dt = end_dt.replace(tzinfo=ctx.timezone)
if start_dt is None or end_dt is None or start_dt >= end_dt:
_LOGGER.debug(
"Ghost reservation %s: invalid dates (start=%s end=%s); "
"slot remains fenced with missing_count=%d",
key,
start_raw,
end_raw,
new_mc,
)
return None return None
start_dt, end_dt = stay
try: try:
ghost = _Reservation( ghost = _Reservation(
@@ -186,6 +158,56 @@ def _ghost_from_mapping(
return ghost return ghost
def _dict_field(container: dict[str, Any], name: str) -> dict[str, Any]:
"""Return a persisted nested mapping, or an empty one when corrupt."""
value = container.get(name, {})
return value if isinstance(value, dict) else {}
def _str_field(container: dict[str, Any], name: str) -> str:
"""Return a persisted string field, or an empty string when corrupt."""
value = container.get(name, "")
return value if isinstance(value, str) else ""
def _ghost_stay(
key: str,
slot_name: str,
last_actual: dict[str, Any],
new_mc: int,
timezone: tzinfo,
) -> tuple[datetime, datetime] | None:
"""Return a ghost's last observed stay, or None when it stays fenced."""
start_raw = last_actual.get("start_state")
end_raw = last_actual.get("end_state")
if not slot_name or start_raw is None or end_raw is None:
_LOGGER.debug(
"Ghost reservation %s: missing slot_name or dates; slot remains "
"fenced with missing_count=%d",
key,
new_mc,
)
return None
start_dt = _coerce_persisted_datetime(start_raw)
end_dt = _coerce_persisted_datetime(end_raw)
if start_dt is not None and start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=timezone)
if end_dt is not None and end_dt.tzinfo is None:
end_dt = end_dt.replace(tzinfo=timezone)
if start_dt is None or end_dt is None or start_dt >= end_dt:
_LOGGER.debug(
"Ghost reservation %s: invalid dates (start=%s end=%s); "
"slot remains fenced with missing_count=%d",
key,
start_raw,
end_raw,
new_mc,
)
return None
return start_dt, end_dt
def _ghost_physical_name_mismatch( def _ghost_physical_name_mismatch(
actual_name: Any, actual_name: Any,
slot_name: str, slot_name: str,