Fence and reconcile Dataflow runs
This commit is contained in:
@@ -25,6 +25,13 @@ from govoplan_dataflow.backend.db.models import (
|
||||
DataflowTrigger,
|
||||
)
|
||||
from govoplan_dataflow.backend.governance import require_definition_action
|
||||
from govoplan_dataflow.backend.recovery import (
|
||||
DataflowRecoveryError,
|
||||
DataflowRunRecovery,
|
||||
begin_dataflow_run_recovery,
|
||||
claim_stale_dataflow_recovery,
|
||||
dataflow_run_recovery_state,
|
||||
)
|
||||
from govoplan_dataflow.backend.service import (
|
||||
_execute_pipeline_run,
|
||||
_require_deployed_revision,
|
||||
@@ -55,7 +62,10 @@ def dispatch_pending_runs(
|
||||
if worker_id and str(worker_id).strip()
|
||||
else socket.gethostname()
|
||||
)
|
||||
recovered = _recover_expired_leases(session, now=current)
|
||||
recovered, recovered_outcome_unknown = _recover_expired_leases(
|
||||
session,
|
||||
now=current,
|
||||
)
|
||||
session.commit()
|
||||
summary: dict[str, object] = {
|
||||
"claimed": 0,
|
||||
@@ -63,6 +73,7 @@ def dispatch_pending_runs(
|
||||
"retrying": 0,
|
||||
"failed": 0,
|
||||
"cancelled": 0,
|
||||
"outcome_unknown": recovered_outcome_unknown,
|
||||
"recovered": recovered,
|
||||
"runs": [],
|
||||
}
|
||||
@@ -193,7 +204,7 @@ def _recover_expired_leases(
|
||||
session: Session,
|
||||
*,
|
||||
now: datetime,
|
||||
) -> int:
|
||||
) -> tuple[int, int]:
|
||||
runs = list(
|
||||
session.scalars(
|
||||
select(DataflowRun)
|
||||
@@ -205,13 +216,46 @@ def _recover_expired_leases(
|
||||
.with_for_update(skip_locked=True)
|
||||
)
|
||||
)
|
||||
recovered = 0
|
||||
outcome_unknown = 0
|
||||
for run in runs:
|
||||
recovery = dataflow_run_recovery_state(session, run_id=run.id)
|
||||
recovery_status: str | None = None
|
||||
recovery_metadata = dict(run.authorization_).get("recovery")
|
||||
effect_started = bool(
|
||||
isinstance(recovery_metadata, Mapping)
|
||||
and recovery_metadata.get("boundary") == "output-publication"
|
||||
)
|
||||
if recovery is not None and recovery.get("operation_id"):
|
||||
try:
|
||||
recovery_status = claim_stale_dataflow_recovery(
|
||||
session,
|
||||
operation_id=str(recovery["operation_id"]),
|
||||
lease_ttl_seconds=DEFAULT_LEASE_SECONDS,
|
||||
effect_started=effect_started,
|
||||
)
|
||||
except DataflowRecoveryError:
|
||||
logger.warning(
|
||||
"Could not claim stale Dataflow recovery operation %s",
|
||||
recovery["operation_id"],
|
||||
exc_info=True,
|
||||
)
|
||||
continue
|
||||
run.worker_id = None
|
||||
run.claimed_at = None
|
||||
run.lease_expires_at = None
|
||||
run.heartbeat_at = None
|
||||
if run.cancellation_requested_at is not None:
|
||||
_cancel_run(run, now=now)
|
||||
elif recovery_status == "outcome_unknown" or effect_started:
|
||||
run.status = "outcome_unknown"
|
||||
run.finished_at = now
|
||||
run.progress_phase = "outcome_unknown"
|
||||
run.error = (
|
||||
"The worker lease expired after output publication may have "
|
||||
"started. Verify the sink before retrying."
|
||||
)
|
||||
outcome_unknown += 1
|
||||
elif run.attempts < run.max_attempts:
|
||||
run.status = "retrying"
|
||||
run.available_at = now
|
||||
@@ -223,7 +267,8 @@ def _recover_expired_leases(
|
||||
now=now,
|
||||
message="The worker lease expired after the final attempt.",
|
||||
)
|
||||
return len(runs)
|
||||
recovered += 1
|
||||
return recovered, outcome_unknown
|
||||
|
||||
|
||||
def _claim_next_run(
|
||||
@@ -291,6 +336,7 @@ def _execute_claimed_run(
|
||||
if run.cancellation_requested_at is not None:
|
||||
_cancel_run(run, now=now)
|
||||
return "cancelled"
|
||||
recovery: DataflowRunRecovery | None = None
|
||||
try:
|
||||
principal, provenance = _resolve_principal(
|
||||
session,
|
||||
@@ -329,6 +375,16 @@ def _execute_claimed_run(
|
||||
revision=revision,
|
||||
environment=run.environment,
|
||||
)
|
||||
session.commit()
|
||||
recovery = begin_dataflow_run_recovery(
|
||||
session,
|
||||
run=run,
|
||||
lease_ttl_seconds=_lease_seconds(run),
|
||||
)
|
||||
if recovery.replayed:
|
||||
session.rollback()
|
||||
run = session.get(DataflowRun, run_id)
|
||||
return run.status if run is not None else "failed"
|
||||
_notify_run(
|
||||
session,
|
||||
registry=registry,
|
||||
@@ -344,6 +400,7 @@ def _execute_claimed_run(
|
||||
request=pipeline_run_request(run),
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
recovery=recovery,
|
||||
)
|
||||
if (
|
||||
run.status == "failed"
|
||||
@@ -359,7 +416,47 @@ def _execute_claimed_run(
|
||||
run=run,
|
||||
pipeline_name=pipeline.name,
|
||||
)
|
||||
recovery.finish(session, run=run)
|
||||
return run.status
|
||||
except DataflowRecoveryError:
|
||||
logger.exception(
|
||||
"Dataflow recovery finalization failed for run %s",
|
||||
run_id,
|
||||
)
|
||||
session.rollback()
|
||||
run = session.get(DataflowRun, run_id)
|
||||
if run is not None:
|
||||
if recovery is not None and recovery.publication_started:
|
||||
run.status = "outcome_unknown"
|
||||
run.finished_at = now
|
||||
run.progress_phase = "outcome_unknown"
|
||||
run.error = (
|
||||
"Output publication completed without verifiable terminal "
|
||||
"recovery evidence; reconcile the sink."
|
||||
)
|
||||
_finish_claim(run)
|
||||
return "outcome_unknown"
|
||||
if recovery is None:
|
||||
_fail_run(
|
||||
run,
|
||||
now=now,
|
||||
message=(
|
||||
"The recovery ledger was unavailable; no Dataflow "
|
||||
"execution started."
|
||||
),
|
||||
)
|
||||
_finish_claim(run)
|
||||
else:
|
||||
_fail_run(
|
||||
run,
|
||||
now=now,
|
||||
message=(
|
||||
"The database-only run could not commit verifiable "
|
||||
"recovery evidence."
|
||||
),
|
||||
)
|
||||
_finish_claim(run)
|
||||
return "failed"
|
||||
except (DataflowWorkerError, PermissionError, ValueError) as exc:
|
||||
_fail_run(run, now=now, message=str(exc))
|
||||
_finish_claim(run)
|
||||
@@ -370,6 +467,8 @@ def _execute_claimed_run(
|
||||
run=run,
|
||||
pipeline_name="Dataflow",
|
||||
)
|
||||
if recovery is not None:
|
||||
recovery.finish(session, run=run)
|
||||
return "failed"
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
@@ -380,6 +479,24 @@ def _execute_claimed_run(
|
||||
run = session.get(DataflowRun, run_id)
|
||||
if run is None:
|
||||
return "failed"
|
||||
if recovery is not None and recovery.publication_started:
|
||||
run.status = "outcome_unknown"
|
||||
run.finished_at = now
|
||||
run.error = (
|
||||
"The output provider failed after dispatch began; verify the "
|
||||
"sink before retrying."
|
||||
)
|
||||
run.progress_phase = "outcome_unknown"
|
||||
_finish_claim(run)
|
||||
try:
|
||||
recovery.finish(session, run=run)
|
||||
except DataflowRecoveryError:
|
||||
logger.exception(
|
||||
"Could not persist the uncertain output state for run %s",
|
||||
run_id,
|
||||
)
|
||||
session.rollback()
|
||||
return "outcome_unknown"
|
||||
_fail_run(
|
||||
run,
|
||||
now=now,
|
||||
@@ -389,6 +506,15 @@ def _execute_claimed_run(
|
||||
),
|
||||
)
|
||||
_finish_claim(run)
|
||||
if recovery is not None:
|
||||
try:
|
||||
recovery.finish(session, run=run)
|
||||
except DataflowRecoveryError:
|
||||
logger.exception(
|
||||
"Could not finalize failed Dataflow recovery for run %s",
|
||||
run_id,
|
||||
)
|
||||
session.rollback()
|
||||
return "failed"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user