Serialize concurrent datasource publications
This commit is contained in:
@@ -416,7 +416,7 @@ manifest = ModuleManifest(
|
||||
"promotion. Updates compare the detected schema with the current target and classify each change as compatible, warning, or "
|
||||
"breaking. Diagnostics expose counts and bounded row numbers, never field values. The policy version and hash, diagnostics, and "
|
||||
"schema diff are copied into immutable materialization provenance when promotion succeeds. Producer publication applies the same "
|
||||
"gate before any catalogue effect, retains validation evidence on the immutable output revision, and emits a transactional terminal event. Approval and retention execution "
|
||||
"gate before any catalogue effect, retains validation evidence on the immutable output revision, serializes a publication identity across PostgreSQL worker nodes before replay lookup, and emits a transactional terminal event. Approval and retention execution "
|
||||
"are not inferred from arbitrary JSON flags and remain separate governed lifecycle work."
|
||||
),
|
||||
layer="available",
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any, cast
|
||||
|
||||
from sqlalchemy import exists, func, or_, select
|
||||
from sqlalchemy import exists, func, or_, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
@@ -98,6 +98,12 @@ class SqlDatasourceProvider:
|
||||
) -> DatasourcePublicationResult:
|
||||
db, api_principal = _publication_context(session, principal)
|
||||
prepared = _prepare_publication(request)
|
||||
_lock_publication_identity(
|
||||
db,
|
||||
tenant_id=api_principal.tenant_id,
|
||||
producer_module=prepared.producer_module,
|
||||
idempotency_key=prepared.idempotency_key,
|
||||
)
|
||||
existing = _existing_publication_result(
|
||||
db,
|
||||
tenant_id=api_principal.tenant_id,
|
||||
@@ -1649,6 +1655,44 @@ def _validate_publication_identity(
|
||||
)
|
||||
|
||||
|
||||
def _publication_lock_key(
|
||||
*,
|
||||
tenant_id: str,
|
||||
producer_module: str,
|
||||
idempotency_key: str,
|
||||
) -> int:
|
||||
payload = (
|
||||
f"govoplan.datasources.publication\0{tenant_id}\0"
|
||||
f"{producer_module}\0{idempotency_key}"
|
||||
).encode("utf-8")
|
||||
return int.from_bytes(
|
||||
hashlib.sha256(payload).digest()[:8],
|
||||
"big",
|
||||
signed=True,
|
||||
)
|
||||
|
||||
|
||||
def _lock_publication_identity(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
producer_module: str,
|
||||
idempotency_key: str,
|
||||
) -> None:
|
||||
if session.get_bind().dialect.name != "postgresql":
|
||||
return
|
||||
session.execute(
|
||||
text("SELECT pg_advisory_xact_lock(:key)"),
|
||||
{
|
||||
"key": _publication_lock_key(
|
||||
tenant_id=tenant_id,
|
||||
producer_module=producer_module,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _existing_publication_result(
|
||||
session: Session,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user