|
|
@@ -1,5 +1,6 @@
|
|
|
from __future__ import annotations
|
|
|
|
|
|
+import hashlib
|
|
|
import json
|
|
|
import re
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
@@ -119,6 +120,7 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
failed_receipt_correlation_id = new_governance_uid()
|
|
|
no_run_crash_correlation_id = new_governance_uid()
|
|
|
evidence_crash_correlation_id = new_governance_uid()
|
|
|
+ active_retry_correlation_id = new_governance_uid()
|
|
|
sql_binding_id = new_governance_uid()
|
|
|
prefix = f"rules/{correlation_id}/"
|
|
|
customer_table = "task5_polars_customers"
|
|
|
@@ -138,6 +140,7 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
retry_ledger_jti = None
|
|
|
no_run_crash_jti = None
|
|
|
evidence_crash_jti = None
|
|
|
+ active_retry_jti = None
|
|
|
gateway_candidate_id = None
|
|
|
|
|
|
try:
|
|
|
@@ -952,6 +955,21 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
node=node,
|
|
|
)
|
|
|
evidence_crash_jti = evidence_crash_claims.jti
|
|
|
+ active_retry_token = TaskTokenIssuer(task_secret).issue(
|
|
|
+ task_uid=new_governance_uid(),
|
|
|
+ dataflow_uid=dataflow_uid,
|
|
|
+ deployment_id=deployment_id,
|
|
|
+ environment="test",
|
|
|
+ workflow_version=1,
|
|
|
+ correlation_id=active_retry_correlation_id,
|
|
|
+ node=node,
|
|
|
+ write_authorized=True,
|
|
|
+ )
|
|
|
+ active_retry_claims = verifier.verify(
|
|
|
+ active_retry_token,
|
|
|
+ node=node,
|
|
|
+ )
|
|
|
+ active_retry_jti = active_retry_claims.jti
|
|
|
|
|
|
def task_binding(claims):
|
|
|
return {
|
|
|
@@ -977,6 +995,11 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
task_binding(evidence_crash_claims),
|
|
|
expires_at=evidence_crash_claims.expires_at,
|
|
|
)
|
|
|
+ assert real_ledger.claim(
|
|
|
+ active_retry_jti,
|
|
|
+ task_binding(active_retry_claims),
|
|
|
+ expires_at=active_retry_claims.expires_at,
|
|
|
+ )
|
|
|
real_evidence_writer.start(
|
|
|
component_binding_id=component_binding_id,
|
|
|
rule_version_id=rule_id,
|
|
|
@@ -989,6 +1012,18 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
node_id=node["id"],
|
|
|
lease_owner=evidence_crash_jti,
|
|
|
)
|
|
|
+ real_evidence_writer.start(
|
|
|
+ component_binding_id=component_binding_id,
|
|
|
+ rule_version_id=rule_id,
|
|
|
+ plan_hash=compiled["plan_hash"],
|
|
|
+ correlation_id=active_retry_correlation_id,
|
|
|
+ dataflow_uid=dataflow_uid,
|
|
|
+ deployment_id=deployment_id,
|
|
|
+ environment="test",
|
|
|
+ workflow_version=1,
|
|
|
+ node_id=node["id"],
|
|
|
+ lease_owner=active_retry_jti,
|
|
|
+ )
|
|
|
with platform.begin() as connection:
|
|
|
connection.execute(
|
|
|
text(
|
|
|
@@ -1059,6 +1094,14 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
"parameters": {},
|
|
|
},
|
|
|
)
|
|
|
+ active_retry = client.post(
|
|
|
+ "/v1/tasks/execute",
|
|
|
+ json={
|
|
|
+ "task_token": active_retry_token,
|
|
|
+ "node": node,
|
|
|
+ "parameters": {},
|
|
|
+ },
|
|
|
+ )
|
|
|
assert http_result.status_code == 200, http_result.get_json()
|
|
|
assert http_result.get_json()["output_artifact"] == result[
|
|
|
"artifact_ref"
|
|
|
@@ -1081,6 +1124,10 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
assert evidence_crash.get_json() == {
|
|
|
"error": "task execution outcome is unknown"
|
|
|
}
|
|
|
+ assert active_retry.status_code == 202
|
|
|
+ assert active_retry.headers["Retry-After"] == "2"
|
|
|
+ assert real_ledger.get(active_retry_jti).status == "running"
|
|
|
+ assert real_ledger.get(active_retry_jti).replay_body is None
|
|
|
assert real_ledger.get(no_run_crash_jti).status == "unknown"
|
|
|
assert real_ledger.get(evidence_crash_jti).status == "unknown"
|
|
|
with platform.connect() as connection:
|
|
|
@@ -1110,6 +1157,28 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
"status": "unknown",
|
|
|
"commit_outcome": "unknown",
|
|
|
}
|
|
|
+ terminal_replay = real_evidence_writer.reconcile_expired_lease(
|
|
|
+ lease_owner=ledger_jti,
|
|
|
+ deployment_id=deployment_id,
|
|
|
+ correlation_id=correlation_id,
|
|
|
+ component_binding_id=component_binding_id,
|
|
|
+ rule_version_id=rule_id,
|
|
|
+ plan_hash=compiled["plan_hash"],
|
|
|
+ )
|
|
|
+ assert terminal_replay["state"] == "terminal"
|
|
|
+ assert terminal_replay["status"] == "success"
|
|
|
+ assert terminal_replay["result_digest"] == hashlib.sha256(
|
|
|
+ json.dumps(
|
|
|
+ terminal_replay["result"],
|
|
|
+ sort_keys=True,
|
|
|
+ separators=(",", ":"),
|
|
|
+ ensure_ascii=False,
|
|
|
+ ).encode("utf-8")
|
|
|
+ ).hexdigest()
|
|
|
+ assert re.fullmatch(
|
|
|
+ r"[0-9a-f]{64}",
|
|
|
+ terminal_replay["evidence_digest"],
|
|
|
+ )
|
|
|
ledger_record = PostgresTaskLedger(platform).get(ledger_jti)
|
|
|
assert ledger_record is not None
|
|
|
assert ledger_record.status == "success"
|
|
|
@@ -1523,7 +1592,29 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
text(
|
|
|
"""
|
|
|
UPDATE public.rule_violation_samples
|
|
|
- SET handoff_status = 'unknown'
|
|
|
+ SET handoff_status = 'unknown',
|
|
|
+ cleanup_claim = CAST(:cleanup_claim AS uuid),
|
|
|
+ cleanup_claim_expires_at =
|
|
|
+ CURRENT_TIMESTAMP + INTERVAL '5 minutes'
|
|
|
+ WHERE rule_run_id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "id": sample_crash_run_id,
|
|
|
+ "cleanup_claim": new_governance_uid(),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ before_claim_expiry = evidence_writer.reconcile_samples(
|
|
|
+ limit=10
|
|
|
+ )
|
|
|
+ assert before_claim_expiry["claimed"] == 0
|
|
|
+ with platform.begin() as connection:
|
|
|
+ connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_violation_samples
|
|
|
+ SET cleanup_claim_expires_at =
|
|
|
+ CURRENT_TIMESTAMP - INTERVAL '1 second'
|
|
|
WHERE rule_run_id = CAST(:id AS uuid)
|
|
|
"""
|
|
|
),
|
|
|
@@ -1532,6 +1623,7 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
sample_reconciliation = evidence_writer.reconcile_samples(
|
|
|
limit=10
|
|
|
)
|
|
|
+ assert sample_reconciliation["claimed"] == 1
|
|
|
assert sample_reconciliation["ready"] >= 1
|
|
|
evidence_writer.finish(
|
|
|
sample_crash_run_id,
|
|
|
@@ -1540,6 +1632,102 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
assert evidence_writer.replay(sample_crash_run_id)[
|
|
|
"status"
|
|
|
] == "success"
|
|
|
+ with platform.connect() as connection:
|
|
|
+ missing_sample_ref = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT artifact_ref
|
|
|
+ FROM public.rule_violation_samples
|
|
|
+ WHERE rule_run_id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": sample_crash_run_id},
|
|
|
+ ).scalar_one()
|
|
|
+ store.delete(missing_sample_ref)
|
|
|
+ with platform.begin() as connection:
|
|
|
+ connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_violation_samples
|
|
|
+ SET handoff_status = 'unknown',
|
|
|
+ cleanup_claim = NULL,
|
|
|
+ cleanup_claim_expires_at = NULL
|
|
|
+ WHERE rule_run_id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": sample_crash_run_id},
|
|
|
+ )
|
|
|
+ missing_reconciliation = evidence_writer.reconcile_samples(
|
|
|
+ limit=10
|
|
|
+ )
|
|
|
+ assert missing_reconciliation["failed"] == 1
|
|
|
+ with platform.connect() as connection:
|
|
|
+ assert connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT handoff_status
|
|
|
+ FROM public.rule_violation_samples
|
|
|
+ WHERE rule_run_id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": sample_crash_run_id},
|
|
|
+ ).scalar_one() == "failed"
|
|
|
+
|
|
|
+ class TransientArtifactStore:
|
|
|
+ def __init__(self, wrapped):
|
|
|
+ self.wrapped = wrapped
|
|
|
+
|
|
|
+ def __getattr__(self, name):
|
|
|
+ return getattr(self.wrapped, name)
|
|
|
+
|
|
|
+ def describe_optional(self, _ref):
|
|
|
+ raise TimeoutError("temporary MinIO timeout")
|
|
|
+
|
|
|
+ with platform.begin() as connection:
|
|
|
+ transient_sample_id = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_violation_samples s
|
|
|
+ SET handoff_status = 'unknown',
|
|
|
+ cleanup_claim = NULL,
|
|
|
+ cleanup_claim_expires_at = NULL
|
|
|
+ FROM public.rule_runs r
|
|
|
+ WHERE s.rule_run_id = r.id
|
|
|
+ AND r.correlation_id =
|
|
|
+ CAST(:correlation_id AS uuid)
|
|
|
+ RETURNING s.id::text
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"correlation_id": correlation_id},
|
|
|
+ ).scalar_one()
|
|
|
+ transient_writer = PostgresRuleEvidenceWriter(
|
|
|
+ platform,
|
|
|
+ TransientArtifactStore(store),
|
|
|
+ sample_ttl_seconds=900,
|
|
|
+ )
|
|
|
+ transient_result = transient_writer.reconcile_samples(
|
|
|
+ limit=10
|
|
|
+ )
|
|
|
+ assert transient_result["claimed"] == 1
|
|
|
+ assert transient_result["ready"] == 0
|
|
|
+ assert transient_result["failed"] == 0
|
|
|
+ with platform.connect() as connection:
|
|
|
+ transient_state = connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT handoff_status,
|
|
|
+ cleanup_claim,
|
|
|
+ cleanup_claim_expires_at
|
|
|
+ FROM public.rule_violation_samples
|
|
|
+ WHERE id = CAST(:id AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"id": transient_sample_id},
|
|
|
+ ).mappings().one()
|
|
|
+ assert transient_state["handoff_status"] == "unknown"
|
|
|
+ assert transient_state["cleanup_claim"] is None
|
|
|
+ assert transient_state["cleanup_claim_expires_at"] is None
|
|
|
+ assert evidence_writer.reconcile_samples(limit=10)["ready"] == 1
|
|
|
|
|
|
with platform.begin() as connection:
|
|
|
connection.execute(
|
|
|
@@ -1681,7 +1869,20 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
"""
|
|
|
UPDATE public.rule_sql_staging_receipts
|
|
|
SET expires_at =
|
|
|
- CURRENT_TIMESTAMP - INTERVAL '1 second'
|
|
|
+ CURRENT_TIMESTAMP - INTERVAL '1 second',
|
|
|
+ cleanup_claim = CASE
|
|
|
+ WHEN correlation_id =
|
|
|
+ CAST(:ready AS uuid)
|
|
|
+ THEN CAST(:cleanup_claim AS uuid)
|
|
|
+ ELSE NULL
|
|
|
+ END,
|
|
|
+ cleanup_claim_expires_at = CASE
|
|
|
+ WHEN correlation_id =
|
|
|
+ CAST(:ready AS uuid)
|
|
|
+ THEN CURRENT_TIMESTAMP
|
|
|
+ + INTERVAL '5 minutes'
|
|
|
+ ELSE NULL
|
|
|
+ END
|
|
|
WHERE correlation_id IN (
|
|
|
CAST(:ready AS uuid),
|
|
|
CAST(:failed AS uuid)
|
|
|
@@ -1691,9 +1892,34 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
{
|
|
|
"ready": receipt_correlation_id,
|
|
|
"failed": failed_receipt_correlation_id,
|
|
|
+ "cleanup_claim": new_governance_uid(),
|
|
|
},
|
|
|
)
|
|
|
- assert evidence_writer.cleanup_sql_staging(limit=10) == 2
|
|
|
+ assert evidence_writer.cleanup_sql_staging(limit=10) == 1
|
|
|
+ with platform.connect() as connection:
|
|
|
+ assert connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ SELECT status
|
|
|
+ FROM public.rule_sql_staging_receipts
|
|
|
+ WHERE correlation_id = CAST(:ready AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"ready": receipt_correlation_id},
|
|
|
+ ).scalar_one() == "ready"
|
|
|
+ with platform.begin() as connection:
|
|
|
+ connection.execute(
|
|
|
+ text(
|
|
|
+ """
|
|
|
+ UPDATE public.rule_sql_staging_receipts
|
|
|
+ SET cleanup_claim_expires_at =
|
|
|
+ CURRENT_TIMESTAMP - INTERVAL '1 second'
|
|
|
+ WHERE correlation_id = CAST(:ready AS uuid)
|
|
|
+ """
|
|
|
+ ),
|
|
|
+ {"ready": receipt_correlation_id},
|
|
|
+ )
|
|
|
+ assert evidence_writer.cleanup_sql_staging(limit=10) == 1
|
|
|
with platform.connect() as connection:
|
|
|
assert connection.execute(
|
|
|
text(
|
|
|
@@ -1803,6 +2029,7 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
for crash_jti in (
|
|
|
no_run_crash_jti,
|
|
|
evidence_crash_jti,
|
|
|
+ active_retry_jti,
|
|
|
):
|
|
|
if crash_jti is not None:
|
|
|
connection.execute(
|
|
|
@@ -1860,7 +2087,8 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
"CAST(:receipt_correlation_id AS uuid), "
|
|
|
"CAST(:failed_receipt_correlation_id AS uuid), "
|
|
|
"CAST(:no_run_crash_correlation_id AS uuid), "
|
|
|
- "CAST(:evidence_crash_correlation_id AS uuid))"
|
|
|
+ "CAST(:evidence_crash_correlation_id AS uuid), "
|
|
|
+ "CAST(:active_retry_correlation_id AS uuid))"
|
|
|
),
|
|
|
{
|
|
|
"correlation_id": correlation_id,
|
|
|
@@ -1880,6 +2108,9 @@ def test_real_postgres_mysql_minio_polars_cross_source_execution(tmp_path):
|
|
|
"evidence_crash_correlation_id": (
|
|
|
evidence_crash_correlation_id
|
|
|
),
|
|
|
+ "active_retry_correlation_id": (
|
|
|
+ active_retry_correlation_id
|
|
|
+ ),
|
|
|
},
|
|
|
)
|
|
|
connection.execute(
|