fix(k1): allow explicit reset after terminal start fault
This commit is contained in:
@@ -8477,6 +8477,7 @@ class XgridsK1CompatibilityService:
|
|||||||
acquisition_id=(
|
acquisition_id=(
|
||||||
acquisition.acquisition_id if acquisition is not None else None
|
acquisition.acquisition_id if acquisition is not None else None
|
||||||
),
|
),
|
||||||
|
allow_terminal_failure_retirement=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._supersede_control_bootstrap_continuation()
|
self._supersede_control_bootstrap_continuation()
|
||||||
@@ -27415,6 +27416,7 @@ class XgridsK1CompatibilityService:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
acquisition_id: str | None,
|
acquisition_id: str | None,
|
||||||
|
allow_terminal_failure_retirement: bool = False,
|
||||||
) -> tuple[_LocalStopRetirementDisposition, str | None]:
|
) -> tuple[_LocalStopRetirementDisposition, str | None]:
|
||||||
"""Cancel the local worker under the exact publish fence.
|
"""Cancel the local worker under the exact publish fence.
|
||||||
|
|
||||||
@@ -27450,6 +27452,24 @@ class XgridsK1CompatibilityService:
|
|||||||
)
|
)
|
||||||
self._application_control_session.close()
|
self._application_control_session.close()
|
||||||
retired_control = dict(self._application_control_session.snapshot())
|
retired_control = dict(self._application_control_session.snapshot())
|
||||||
|
if (
|
||||||
|
allow_terminal_failure_retirement
|
||||||
|
and retired_control.get("state") == "failed"
|
||||||
|
):
|
||||||
|
# ``can_open`` deliberately remains false after an ambiguous
|
||||||
|
# START/STOP failure even when the socket worker has already
|
||||||
|
# exited. That is the correct admission policy for an
|
||||||
|
# ordinary retry, but an explicit scenario reset is a
|
||||||
|
# different operation: retire only the dead local owner while
|
||||||
|
# the durable physical ledger below preserves the ambiguous
|
||||||
|
# device outcome. ``retire_for_network_change`` still checks
|
||||||
|
# that the thread and transport are actually gone, so a live
|
||||||
|
# publisher cannot be detached by this path.
|
||||||
|
with suppress(AttributeError, RuntimeError):
|
||||||
|
self._retire_application_control_for_network_change(
|
||||||
|
allow_terminal_failure=True,
|
||||||
|
)
|
||||||
|
retired_control = dict(self._application_control_session.snapshot())
|
||||||
if (
|
if (
|
||||||
retired_control.get("state")
|
retired_control.get("state")
|
||||||
not in {"idle", "completed", "closed", "failed"}
|
not in {"idle", "completed", "closed", "failed"}
|
||||||
|
|||||||
@@ -1722,6 +1722,72 @@ def test_terminal_cleanup_pending_is_locally_sealed_by_mode_reset(
|
|||||||
assert reset["connection_scenario_reset"]["network_write_performed"] is False
|
assert reset["connection_scenario_reset"]["network_write_performed"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_terminal_ambiguous_start_failure_can_be_locally_reset_after_worker_exit(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
service = _service(monkeypatch, tmp_path)
|
||||||
|
_seed_resettable_physical_record(service)
|
||||||
|
acquisition = AcquisitionRecord(
|
||||||
|
acquisition_id="reset-preflight-acquisition-0001",
|
||||||
|
device_id="device-reset-terminal-start-fault",
|
||||||
|
device_session_id="device-session-reset-terminal-start-fault",
|
||||||
|
compatibility_profile_id=("xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"),
|
||||||
|
control_mode="plugin-commanded",
|
||||||
|
requested_streams=("points",),
|
||||||
|
target_host="192.168.68.51",
|
||||||
|
duration_seconds=None,
|
||||||
|
evidence_policy="required",
|
||||||
|
)
|
||||||
|
acquisition.transition(
|
||||||
|
"failed",
|
||||||
|
message_code="acquisition.start.device_outcome_unknown",
|
||||||
|
result={
|
||||||
|
"receiver_stopped": False,
|
||||||
|
"device_start": "outcome-unknown",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
control = service._application_control_session # noqa: SLF001
|
||||||
|
with control._lock: # noqa: SLF001
|
||||||
|
control._set_phase_locked("failed") # noqa: SLF001
|
||||||
|
control._failure = { # noqa: SLF001
|
||||||
|
"reason_code": "device_status_fault",
|
||||||
|
"safe_to_retry": False,
|
||||||
|
"modeling_command_attempted": True,
|
||||||
|
"outcome_unknown": True,
|
||||||
|
}
|
||||||
|
control._thread = None # noqa: SLF001
|
||||||
|
control._transport = None # noqa: SLF001
|
||||||
|
with service._lock: # noqa: SLF001
|
||||||
|
service._acquisition = acquisition # noqa: SLF001
|
||||||
|
|
||||||
|
before = control.snapshot()
|
||||||
|
assert before["state"] == "failed"
|
||||||
|
assert before["can_open"] is False
|
||||||
|
|
||||||
|
reset = service.select_connection_mode(
|
||||||
|
_reset(
|
||||||
|
mode="bridge",
|
||||||
|
revision=0,
|
||||||
|
reset_id="op-reset-terminal-ambiguous-start-01",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert reset["desired_connection_mode_revision"] == 1
|
||||||
|
assert reset["connection_scenario_reset"]["local_session_closed"] is True
|
||||||
|
assert reset["connection_scenario_reset"]["physical_disposition"] == (
|
||||||
|
"operator-retired-outcome-unknown"
|
||||||
|
)
|
||||||
|
assert reset["connection_scenario_reset"]["device_command_performed"] is False
|
||||||
|
assert reset["connection_scenario_reset"]["network_write_performed"] is False
|
||||||
|
assert reset["application_control_session"]["state"] == "idle"
|
||||||
|
assert reset["acquisition"]["state"] == "failed"
|
||||||
|
assert reset["acquisition"]["cleanup_pending"] is False
|
||||||
|
physical = service._physical_command_ledger.snapshot().record # noqa: SLF001
|
||||||
|
assert physical is not None
|
||||||
|
assert physical.resolution == "operator-retired-outcome-unknown"
|
||||||
|
|
||||||
|
|
||||||
def test_terminal_cleanup_failure_stays_visible_and_exact_reset_can_retry(
|
def test_terminal_cleanup_failure_stays_visible_and_exact_reset_can_retry(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user