Partition workflow execution by tenant
This commit is contained in:
@@ -779,15 +779,19 @@ def reconcile_pending_instances(
|
|||||||
session: Session,
|
session: Session,
|
||||||
*,
|
*,
|
||||||
registry: object | None,
|
registry: object | None,
|
||||||
|
tenant_id: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
|
clauses = [
|
||||||
|
WorkflowInstance.status.in_(("running", "waiting")),
|
||||||
|
WorkflowInstance.current_step_id.is_not(None),
|
||||||
|
]
|
||||||
|
if tenant_id:
|
||||||
|
clauses.append(WorkflowInstance.tenant_id == tenant_id)
|
||||||
instances = list(
|
instances = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
select(WorkflowInstance)
|
select(WorkflowInstance)
|
||||||
.where(
|
.where(*clauses)
|
||||||
WorkflowInstance.status.in_(("running", "waiting")),
|
|
||||||
WorkflowInstance.current_step_id.is_not(None),
|
|
||||||
)
|
|
||||||
.order_by(WorkflowInstance.updated_at, WorkflowInstance.id)
|
.order_by(WorkflowInstance.updated_at, WorkflowInstance.id)
|
||||||
.limit(max(1, min(int(limit), 200)))
|
.limit(max(1, min(int(limit), 200)))
|
||||||
.with_for_update(skip_locked=True)
|
.with_for_update(skip_locked=True)
|
||||||
@@ -2875,19 +2879,20 @@ def _notify_handoff(
|
|||||||
class SqlWorkflowRuntimeWorker:
|
class SqlWorkflowRuntimeWorker:
|
||||||
def __init__(self, *, registry: object | None = None) -> None:
|
def __init__(self, *, registry: object | None = None) -> None:
|
||||||
self._registry = registry
|
self._registry = registry
|
||||||
self._standards_reconciled = False
|
self._standards_reconciled_tenants: set[str | None] = set()
|
||||||
|
|
||||||
def reconcile_pending(
|
def reconcile_pending(
|
||||||
self,
|
self,
|
||||||
session: object,
|
session: object,
|
||||||
*,
|
*,
|
||||||
|
tenant_id: str | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> Mapping[str, object]:
|
) -> Mapping[str, object]:
|
||||||
if not isinstance(session, Session):
|
if not isinstance(session, Session):
|
||||||
raise TypeError("Workflow reconciliation requires a Session.")
|
raise TypeError("Workflow reconciliation requires a Session.")
|
||||||
standards: Mapping[str, object] | None = None
|
standards: Mapping[str, object] | None = None
|
||||||
if not self._standards_reconciled:
|
if tenant_id not in self._standards_reconciled_tenants:
|
||||||
from govoplan_workflow_engine.backend.contributions import (
|
from govoplan_workflow_engine.backend.contributions import (
|
||||||
reconcile_workflow_definition_contributions,
|
reconcile_workflow_definition_contributions,
|
||||||
)
|
)
|
||||||
@@ -2895,11 +2900,14 @@ class SqlWorkflowRuntimeWorker:
|
|||||||
standards = reconcile_workflow_definition_contributions(
|
standards = reconcile_workflow_definition_contributions(
|
||||||
session,
|
session,
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
|
tenant_ids=((tenant_id,) if tenant_id else ()),
|
||||||
)
|
)
|
||||||
self._standards_reconciled = int(standards.get("blocked") or 0) == 0
|
if int(standards.get("blocked") or 0) == 0:
|
||||||
|
self._standards_reconciled_tenants.add(tenant_id)
|
||||||
runtime = reconcile_pending_instances(
|
runtime = reconcile_pending_instances(
|
||||||
session,
|
session,
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
|
tenant_id=tenant_id,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
from govoplan_workflow_engine.backend.triggers import dispatch_due_work
|
from govoplan_workflow_engine.backend.triggers import dispatch_due_work
|
||||||
@@ -2907,6 +2915,7 @@ class SqlWorkflowRuntimeWorker:
|
|||||||
triggers = dispatch_due_work(
|
triggers = dispatch_due_work(
|
||||||
session,
|
session,
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
|
tenant_id=tenant_id,
|
||||||
now=now,
|
now=now,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ manifest = ModuleManifest(
|
|||||||
"outcome and disables Retry. Operators inspect the provider, record "
|
"outcome and disables Retry. Operators inspect the provider, record "
|
||||||
"evidence, and choose Effect confirmed to continue without replay or "
|
"evidence, and choose Effect confirmed to continue without replay or "
|
||||||
"Effect absent to enable a deliberate retry. Instance workers, "
|
"Effect absent to enable a deliberate retry. Instance workers, "
|
||||||
"trigger deliveries, and timers use distributed fences. Linked "
|
"trigger deliveries, and timers use distributed fences and are partitioned by tenant module entitlement before state is claimed. Disabling Workflow Engine preserves accepted instances, waits, and trigger deliveries for operator resolution. Linked "
|
||||||
"Dataflow recovery remains blocked until its result is conclusive."
|
"Dataflow recovery remains blocked until its result is conclusive."
|
||||||
),
|
),
|
||||||
layer="available",
|
layer="available",
|
||||||
|
|||||||
@@ -351,16 +351,25 @@ def dispatch_due_work(
|
|||||||
session: Session,
|
session: Session,
|
||||||
*,
|
*,
|
||||||
registry: object | None,
|
registry: object | None,
|
||||||
|
tenant_id: str | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
current = _as_utc(now or utcnow())
|
current = _as_utc(now or utcnow())
|
||||||
bounded = max(1, min(int(limit), 200))
|
bounded = max(1, min(int(limit), 200))
|
||||||
scheduled = _queue_due_schedules(session, now=current, limit=bounded)
|
scheduled = _queue_due_schedules(
|
||||||
|
session,
|
||||||
|
now=current,
|
||||||
|
limit=bounded,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
)
|
||||||
|
delivery_clauses = [WorkflowTriggerDelivery.status == "queued"]
|
||||||
|
if tenant_id:
|
||||||
|
delivery_clauses.append(WorkflowTriggerDelivery.tenant_id == tenant_id)
|
||||||
deliveries = list(
|
deliveries = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
select(WorkflowTriggerDelivery)
|
select(WorkflowTriggerDelivery)
|
||||||
.where(WorkflowTriggerDelivery.status == "queued")
|
.where(*delivery_clauses)
|
||||||
.order_by(
|
.order_by(
|
||||||
WorkflowTriggerDelivery.created_at,
|
WorkflowTriggerDelivery.created_at,
|
||||||
WorkflowTriggerDelivery.id,
|
WorkflowTriggerDelivery.id,
|
||||||
@@ -396,6 +405,7 @@ def dispatch_due_work(
|
|||||||
registry=registry,
|
registry=registry,
|
||||||
now=current,
|
now=current,
|
||||||
limit=bounded,
|
limit=bounded,
|
||||||
|
tenant_id=tenant_id,
|
||||||
)
|
)
|
||||||
session.flush()
|
session.flush()
|
||||||
return {
|
return {
|
||||||
@@ -416,6 +426,7 @@ class SqlWorkflowTriggerDispatcher:
|
|||||||
self,
|
self,
|
||||||
session: object,
|
session: object,
|
||||||
*,
|
*,
|
||||||
|
tenant_id: str | None = None,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> Mapping[str, object]:
|
) -> Mapping[str, object]:
|
||||||
@@ -424,6 +435,7 @@ class SqlWorkflowTriggerDispatcher:
|
|||||||
return dispatch_due_work(
|
return dispatch_due_work(
|
||||||
session,
|
session,
|
||||||
registry=self._registry,
|
registry=self._registry,
|
||||||
|
tenant_id=tenant_id,
|
||||||
now=now,
|
now=now,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
@@ -444,16 +456,20 @@ def _queue_due_schedules(
|
|||||||
*,
|
*,
|
||||||
now: datetime,
|
now: datetime,
|
||||||
limit: int,
|
limit: int,
|
||||||
|
tenant_id: str | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
|
clauses = [
|
||||||
|
WorkflowTrigger.kind == "schedule",
|
||||||
|
WorkflowTrigger.status == "enabled",
|
||||||
|
WorkflowTrigger.next_fire_at.is_not(None),
|
||||||
|
WorkflowTrigger.next_fire_at <= now,
|
||||||
|
]
|
||||||
|
if tenant_id:
|
||||||
|
clauses.append(WorkflowTrigger.tenant_id == tenant_id)
|
||||||
triggers = list(
|
triggers = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
select(WorkflowTrigger)
|
select(WorkflowTrigger)
|
||||||
.where(
|
.where(*clauses)
|
||||||
WorkflowTrigger.kind == "schedule",
|
|
||||||
WorkflowTrigger.status == "enabled",
|
|
||||||
WorkflowTrigger.next_fire_at.is_not(None),
|
|
||||||
WorkflowTrigger.next_fire_at <= now,
|
|
||||||
)
|
|
||||||
.order_by(WorkflowTrigger.next_fire_at, WorkflowTrigger.id)
|
.order_by(WorkflowTrigger.next_fire_at, WorkflowTrigger.id)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.with_for_update(skip_locked=True)
|
.with_for_update(skip_locked=True)
|
||||||
@@ -606,20 +622,24 @@ def _dispatch_waits(
|
|||||||
registry: object | None,
|
registry: object | None,
|
||||||
now: datetime,
|
now: datetime,
|
||||||
limit: int,
|
limit: int,
|
||||||
|
tenant_id: str | None = None,
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
|
clauses = [
|
||||||
|
or_(
|
||||||
|
WorkflowWaitState.status == "triggered",
|
||||||
|
and_(
|
||||||
|
WorkflowWaitState.status == "waiting",
|
||||||
|
WorkflowWaitState.due_at.is_not(None),
|
||||||
|
WorkflowWaitState.due_at <= now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if tenant_id:
|
||||||
|
clauses.append(WorkflowWaitState.tenant_id == tenant_id)
|
||||||
states = list(
|
states = list(
|
||||||
session.scalars(
|
session.scalars(
|
||||||
select(WorkflowWaitState)
|
select(WorkflowWaitState)
|
||||||
.where(
|
.where(*clauses)
|
||||||
or_(
|
|
||||||
WorkflowWaitState.status == "triggered",
|
|
||||||
and_(
|
|
||||||
WorkflowWaitState.status == "waiting",
|
|
||||||
WorkflowWaitState.due_at.is_not(None),
|
|
||||||
WorkflowWaitState.due_at <= now,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by(WorkflowWaitState.updated_at, WorkflowWaitState.id)
|
.order_by(WorkflowWaitState.updated_at, WorkflowWaitState.id)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.with_for_update(skip_locked=True)
|
.with_for_update(skip_locked=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user