Стабилизация восстановления Bridge и Quick Connect
This commit is contained in:
@@ -12885,6 +12885,8 @@ class XgridsK1CompatibilityService:
|
||||
|
||||
def _validate_active_acquisition_checkpoint_lineage(
|
||||
self,
|
||||
*,
|
||||
allow_untrusted_terminal_settlement: bool = False,
|
||||
) -> _ActiveAcquisitionCheckpointTrustToken | None:
|
||||
"""Recompute the current unceased checkpoint/ledger lineage locally.
|
||||
|
||||
@@ -12892,13 +12894,23 @@ class XgridsK1CompatibilityService:
|
||||
ledger and immutable archive retain its proof material. This method is
|
||||
deliberately read-only: a mismatch revokes local START/resume admission
|
||||
for the process lifetime and never repairs either durable document.
|
||||
|
||||
``allow_untrusted_terminal_settlement`` is reserved for one narrower
|
||||
caller which has already durably committed an exact, non-retained
|
||||
READY/SCAN_OVER reconciliation. It may recompute a token only to cease
|
||||
the old checkpoint; process-local START/resume trust stays revoked until
|
||||
that terminal CAS has succeeded.
|
||||
"""
|
||||
|
||||
if getattr(
|
||||
current_trust = getattr(
|
||||
self,
|
||||
"_active_acquisition_checkpoint_trust",
|
||||
"trusted",
|
||||
) != "trusted":
|
||||
)
|
||||
if current_trust != "trusted" and not (
|
||||
allow_untrusted_terminal_settlement
|
||||
and current_trust == "unavailable"
|
||||
):
|
||||
return None
|
||||
store = self._active_acquisition_checkpoint
|
||||
if store is None:
|
||||
@@ -13023,6 +13035,110 @@ class XgridsK1CompatibilityService:
|
||||
)
|
||||
return None
|
||||
|
||||
def _settle_untrusted_checkpoint_after_verified_standby(
|
||||
self,
|
||||
*,
|
||||
reconciliation_id: str,
|
||||
reconciled_record: Mapping[str, Any],
|
||||
) -> bool:
|
||||
"""Consume a new durable standby proof without reviving START authority.
|
||||
|
||||
A process can reject an ACTIVE checkpoint while an older STOP is still
|
||||
unresolved. If the same process later appends an exact read-only
|
||||
READY/SCAN_OVER reconciliation, that durable change is new evidence.
|
||||
Recompute the lineage only for terminal checkpoint cessation; no K1
|
||||
command, network mutation, runtime rehydration, or acquisition authority
|
||||
is permitted by this path.
|
||||
"""
|
||||
|
||||
if getattr(
|
||||
self,
|
||||
"_active_acquisition_checkpoint_trust",
|
||||
"trusted",
|
||||
) != "unavailable":
|
||||
return False
|
||||
store = self._active_acquisition_checkpoint
|
||||
if store is None:
|
||||
return False
|
||||
try:
|
||||
checkpoint_snapshot = store.snapshot()
|
||||
ledger_snapshot = self._physical_command_ledger.snapshot()
|
||||
record = ledger_snapshot.record
|
||||
reconciliation = (
|
||||
record.reconciliations[-1]
|
||||
if record is not None and record.reconciliations
|
||||
else None
|
||||
)
|
||||
if not (
|
||||
checkpoint_snapshot.status in {"prepared", "active"}
|
||||
and checkpoint_snapshot.checkpoint is not None
|
||||
and ledger_snapshot.status == "resolved"
|
||||
and record is not None
|
||||
and record.as_dict() == dict(reconciled_record)
|
||||
and reconciliation is not None
|
||||
and reconciliation.reconciliation_id == reconciliation_id
|
||||
and reconciliation.resolution == "physical-standby-observed"
|
||||
and reconciliation.kind
|
||||
in {
|
||||
"ambiguous-outcome",
|
||||
"prepared-stop-classification",
|
||||
"resolved-active-cessation",
|
||||
}
|
||||
and reconciliation.observation.source
|
||||
== "explicit-read-only-reconciliation"
|
||||
and reconciliation.observation.session_state
|
||||
in {"ready", "scan_over"}
|
||||
and not reconciliation.observation.project_bound
|
||||
and not reconciliation.observation.init_ready
|
||||
and not reconciliation.observation.mqtt_retained
|
||||
):
|
||||
return False
|
||||
token = self._validate_active_acquisition_checkpoint_lineage(
|
||||
allow_untrusted_terminal_settlement=True,
|
||||
)
|
||||
if token is None:
|
||||
return False
|
||||
settled = self._settle_restart_checkpoint_after_verified_standby(
|
||||
token=token,
|
||||
reconciliation_id=reconciliation_id,
|
||||
reconciled_record=reconciled_record,
|
||||
)
|
||||
if not settled:
|
||||
return False
|
||||
committed = store.snapshot().checkpoint
|
||||
if not (
|
||||
committed is not None
|
||||
and committed.state == "ceased"
|
||||
and committed.acquisition_id == token.acquisition_id
|
||||
and committed.original_start_operation_id
|
||||
== token.root_start_operation_id
|
||||
):
|
||||
return False
|
||||
with self._lock:
|
||||
if self._active_acquisition_checkpoint_trust == "unavailable":
|
||||
self._active_acquisition_checkpoint_trust = "trusted"
|
||||
self._active_acquisition_checkpoint_reason = None
|
||||
logger.info(
|
||||
"untrusted K1 checkpoint closed from new durable standby proof",
|
||||
extra={
|
||||
"event_code": (
|
||||
"active_acquisition_checkpoint_untrusted_standby_settled"
|
||||
),
|
||||
"acquisition_id": token.acquisition_id,
|
||||
"reconciliation_id": reconciliation_id,
|
||||
"device_write_performed": False,
|
||||
"automatic_retry": False,
|
||||
},
|
||||
)
|
||||
return True
|
||||
except (
|
||||
ActiveAcquisitionRecoveryCheckpointError,
|
||||
PhysicalCommandLedgerError,
|
||||
OSError,
|
||||
ValueError,
|
||||
):
|
||||
return False
|
||||
|
||||
def _require_active_acquisition_checkpoint_store(
|
||||
self,
|
||||
) -> ActiveAcquisitionRecoveryCheckpointStore:
|
||||
@@ -16105,8 +16221,7 @@ class XgridsK1CompatibilityService:
|
||||
else None
|
||||
)
|
||||
checkpoint_settlement_required = bool(
|
||||
checkpoint_trust_token is not None
|
||||
and ledger_snapshot.status == "resolved"
|
||||
ledger_snapshot.status == "resolved"
|
||||
and record is not None
|
||||
and latest_reconciliation is not None
|
||||
and latest_reconciliation.resolution
|
||||
@@ -16128,14 +16243,24 @@ class XgridsK1CompatibilityService:
|
||||
if checkpoint_settlement_required:
|
||||
assert record is not None
|
||||
assert latest_reconciliation is not None
|
||||
settled = self._settle_restart_checkpoint_after_verified_standby(
|
||||
token=checkpoint_trust_token,
|
||||
reconciliation_id=(
|
||||
latest_reconciliation.reconciliation_id
|
||||
),
|
||||
reconciled_record=record.as_dict(),
|
||||
)
|
||||
if not settled:
|
||||
if checkpoint_trust_token is not None:
|
||||
settled = self._settle_restart_checkpoint_after_verified_standby(
|
||||
token=checkpoint_trust_token,
|
||||
reconciliation_id=(
|
||||
latest_reconciliation.reconciliation_id
|
||||
),
|
||||
reconciled_record=record.as_dict(),
|
||||
)
|
||||
else:
|
||||
settled = (
|
||||
self._settle_untrusted_checkpoint_after_verified_standby(
|
||||
reconciliation_id=(
|
||||
latest_reconciliation.reconciliation_id
|
||||
),
|
||||
reconciled_record=record.as_dict(),
|
||||
)
|
||||
)
|
||||
if checkpoint_trust_token is not None and not settled:
|
||||
raise ConnectionVerificationError(
|
||||
"Подтверждённый READY не закрыл старую локальную START-сессию",
|
||||
reason_code=(
|
||||
@@ -16369,11 +16494,19 @@ class XgridsK1CompatibilityService:
|
||||
checkpoint_lineage=checkpoint_lineage,
|
||||
)
|
||||
elif reconciled_record is not None:
|
||||
settled = self._settle_restart_checkpoint_after_verified_standby(
|
||||
token=checkpoint_trust_token,
|
||||
reconciliation_id=reconciliation_id,
|
||||
reconciled_record=reconciled_record,
|
||||
)
|
||||
if checkpoint_trust_token is not None:
|
||||
settled = self._settle_restart_checkpoint_after_verified_standby(
|
||||
token=checkpoint_trust_token,
|
||||
reconciliation_id=reconciliation_id,
|
||||
reconciled_record=reconciled_record,
|
||||
)
|
||||
else:
|
||||
settled = (
|
||||
self._settle_untrusted_checkpoint_after_verified_standby(
|
||||
reconciliation_id=reconciliation_id,
|
||||
reconciled_record=reconciled_record,
|
||||
)
|
||||
)
|
||||
if checkpoint_trust_token is not None and not settled:
|
||||
raise ActiveAcquisitionRecoveryCheckpointError(
|
||||
"restart standby checkpoint settlement failed closed"
|
||||
|
||||
@@ -535,6 +535,7 @@ _CLOSED_WEBSOCKET_SEND_ERRORS = frozenset(
|
||||
"Unexpected ASGI message 'websocket.send', after sending "
|
||||
"'websocket.close' or response already completed."
|
||||
),
|
||||
"Unexpected ASGI message 'websocket.send', after sending 'websocket.close'.",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user