Fence and reconcile workflow runtime effects
This commit is contained in:
@@ -4,7 +4,7 @@ from dataclasses import replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import create_engine, func, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
@@ -32,7 +32,19 @@ from govoplan_core.core.institutional import (
|
||||
ServiceLaunchRequest,
|
||||
TemporalRevision,
|
||||
)
|
||||
from govoplan_core.core.recovery import (
|
||||
RecoveryCheckpoint,
|
||||
RecoveryOperation,
|
||||
RecoveryStatus,
|
||||
verify_recovery_evidence_chain,
|
||||
)
|
||||
from govoplan_core.core.runtime_coordination import (
|
||||
DistributedLease,
|
||||
RuntimeIdentity,
|
||||
bind_process_runtime_identity,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.db.base import utcnow
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionRevision,
|
||||
@@ -45,12 +57,17 @@ from govoplan_workflow_engine.backend.db.models import (
|
||||
)
|
||||
from govoplan_workflow_engine.backend.instance_service import (
|
||||
SqlWorkflowRuntimeWorker,
|
||||
_action_preview_payload,
|
||||
cancel_instance,
|
||||
instance_response,
|
||||
reconcile_instance,
|
||||
resolve_step,
|
||||
start_instance,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.recovery import (
|
||||
acquire_workflow_state_fence,
|
||||
begin_workflow_action_recovery,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.schemas import (
|
||||
BpmnRevisionInput,
|
||||
WorkflowDefinitionCreateRequest,
|
||||
@@ -97,6 +114,17 @@ def principal() -> ApiPrincipal:
|
||||
)
|
||||
|
||||
|
||||
def runtime_identity() -> RuntimeIdentity:
|
||||
return RuntimeIdentity(
|
||||
installation_id="workflow-engine-tests",
|
||||
node_id="workflow-worker",
|
||||
incarnation="workflow-worker-incarnation",
|
||||
role="worker",
|
||||
software_version="test",
|
||||
composition_hash="c" * 64,
|
||||
)
|
||||
|
||||
|
||||
def runtime_graph() -> WorkflowGraph:
|
||||
return WorkflowGraph(
|
||||
nodes=[
|
||||
@@ -278,6 +306,20 @@ class FakeDataflowLifecycle:
|
||||
error="Data quality gate failed.",
|
||||
)
|
||||
|
||||
def mark_outcome_unknown(self, run_ref: str) -> None:
|
||||
self.runs[run_ref] = replace(
|
||||
self.runs[run_ref],
|
||||
status="outcome_unknown",
|
||||
error="Output acknowledgement was lost.",
|
||||
metadata={
|
||||
"recovery": {
|
||||
"operation_id": "recovery:dataflow:1",
|
||||
"status": "outcome_unknown",
|
||||
"requires_attention": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class FakeAutomationProvider:
|
||||
def __init__(self) -> None:
|
||||
@@ -337,6 +379,8 @@ class FakeActionProvider:
|
||||
def execute_action(self, _session, _principal, *, request):
|
||||
self.requests.append(request)
|
||||
state = self.states.pop(0) if self.states else "completed"
|
||||
if state == "exception":
|
||||
raise RuntimeError("Provider acknowledgement was lost.")
|
||||
if state != "completed":
|
||||
return ActionExecutionResult(
|
||||
state=state,
|
||||
@@ -396,6 +440,9 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
WorkflowDefinition.__table__,
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
WorkflowInstance.__table__,
|
||||
@@ -408,6 +455,7 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
self.session: Session = self.Session()
|
||||
bind_process_runtime_identity(runtime_identity())
|
||||
self.dataflow = FakeDataflowLifecycle()
|
||||
self.registry = Registry(self.dataflow)
|
||||
self.definition = create_definition(
|
||||
@@ -432,6 +480,7 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
self.session.commit()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
bind_process_runtime_identity(None)
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(
|
||||
self.engine,
|
||||
@@ -444,6 +493,9 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
WorkflowInstance.__table__,
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
WorkflowDefinition.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
DistributedLease.__table__,
|
||||
],
|
||||
)
|
||||
self.engine.dispose()
|
||||
@@ -687,6 +739,51 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
"workflow.action.completed",
|
||||
{event.kind for event in response.events},
|
||||
)
|
||||
operation = self.session.scalar(
|
||||
select(RecoveryOperation).where(
|
||||
RecoveryOperation.resource_id == response.steps[1].id
|
||||
)
|
||||
)
|
||||
assert operation is not None
|
||||
self.assertEqual(RecoveryStatus.SUCCEEDED.value, operation.status)
|
||||
self.assertTrue(verify_recovery_evidence_chain(self.session, operation.id))
|
||||
|
||||
def test_missing_optional_action_provider_blocks_without_recovery_effect(
|
||||
self,
|
||||
) -> None:
|
||||
definition = create_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Unavailable action provider",
|
||||
graph=action_graph(),
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(WorkflowConflictError, "not available"):
|
||||
start_instance(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=self.registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="missing-action-provider",
|
||||
input={"case_id": "case-missing"},
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
0,
|
||||
self.session.scalar(select(func.count(RecoveryOperation.id))),
|
||||
)
|
||||
|
||||
def test_retryable_module_action_reuses_the_idempotency_key(self) -> None:
|
||||
action = FakeActionProvider("retryable", "completed")
|
||||
@@ -739,6 +836,383 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
|
||||
action.requests[1].idempotency_key,
|
||||
)
|
||||
|
||||
def test_unknown_module_action_requires_evidence_before_retry(self) -> None:
|
||||
action = FakeActionProvider("exception", "completed")
|
||||
registry = Registry(self.dataflow, action=action)
|
||||
definition = create_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Uncertain action",
|
||||
graph=action_graph(),
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
instance, _replayed = start_instance(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="uncertain-action",
|
||||
input={"case_id": "case-unknown"},
|
||||
),
|
||||
)
|
||||
waiting = instance_response(self.session, instance)
|
||||
current = waiting.steps[-1]
|
||||
self.assertEqual("outcome_unknown", current.handoff["state"])
|
||||
self.assertNotIn("retry", current.handoff["allowed_actions"])
|
||||
self.assertEqual(
|
||||
{"confirm_effect", "confirm_absent", "cancel"},
|
||||
set(current.handoff["allowed_actions"]),
|
||||
)
|
||||
operation = self.session.get(
|
||||
RecoveryOperation,
|
||||
current.handoff["recovery"]["operation_id"],
|
||||
)
|
||||
assert operation is not None
|
||||
self.assertEqual(RecoveryStatus.OUTCOME_UNKNOWN.value, operation.status)
|
||||
|
||||
with self.assertRaisesRegex(WorkflowConflictError, "evidence reference"):
|
||||
resolve_step(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance.id,
|
||||
step_id=current.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(action="confirm_absent"),
|
||||
)
|
||||
resolved = resolve_step(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance.id,
|
||||
step_id=current.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(
|
||||
action="confirm_absent",
|
||||
evidence=["provider-search:case-unknown"],
|
||||
),
|
||||
)
|
||||
current = next(step for step in resolved.steps if step.id == current.id)
|
||||
self.assertEqual("retryable", current.handoff["state"])
|
||||
self.session.refresh(operation)
|
||||
self.assertEqual(RecoveryStatus.RECOVERED.value, operation.status)
|
||||
|
||||
completed = resolve_step(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance.id,
|
||||
step_id=current.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(action="retry"),
|
||||
)
|
||||
self.assertEqual("completed", completed.status)
|
||||
self.assertEqual(2, len(action.requests))
|
||||
self.assertEqual(
|
||||
action.requests[0].idempotency_key,
|
||||
action.requests[1].idempotency_key,
|
||||
)
|
||||
|
||||
def test_confirmed_unknown_effect_advances_without_provider_replay(self) -> None:
|
||||
action = FakeActionProvider("exception")
|
||||
registry = Registry(self.dataflow, action=action)
|
||||
definition = create_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Confirmed uncertain action",
|
||||
graph=action_graph(),
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
instance, _replayed = start_instance(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="confirmed-uncertain-action",
|
||||
input={"case_id": "case-confirmed"},
|
||||
),
|
||||
)
|
||||
current = instance_response(self.session, instance).steps[-1]
|
||||
operation_id = current.handoff["recovery"]["operation_id"]
|
||||
|
||||
resolved = resolve_step(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance.id,
|
||||
step_id=current.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(
|
||||
action="confirm_effect",
|
||||
output={"case_ref": "case:confirmed"},
|
||||
evidence=["provider-receipt:case-confirmed"],
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual("completed", resolved.status)
|
||||
self.assertEqual(1, len(action.requests))
|
||||
operation = self.session.get(RecoveryOperation, operation_id)
|
||||
assert operation is not None
|
||||
self.assertEqual(RecoveryStatus.SUCCEEDED.value, operation.status)
|
||||
|
||||
def test_tampered_action_evidence_prevents_workflow_completion(self) -> None:
|
||||
class TamperingProvider(FakeActionProvider):
|
||||
def execute_action(self, session, principal, *, request):
|
||||
result = super().execute_action(
|
||||
session,
|
||||
principal,
|
||||
request=request,
|
||||
)
|
||||
checkpoint = session.scalar(
|
||||
select(RecoveryCheckpoint).order_by(
|
||||
RecoveryCheckpoint.sequence
|
||||
)
|
||||
)
|
||||
assert checkpoint is not None
|
||||
checkpoint.summary = "tampered provider evidence"
|
||||
session.commit()
|
||||
return result
|
||||
|
||||
action = TamperingProvider()
|
||||
registry = Registry(self.dataflow, action=action)
|
||||
definition = create_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Tampered action",
|
||||
graph=action_graph(),
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "chain verification failed"):
|
||||
start_instance(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="tampered-action",
|
||||
input={"case_id": "case-tampered"},
|
||||
),
|
||||
)
|
||||
|
||||
operation = self.session.scalar(
|
||||
select(RecoveryOperation).where(
|
||||
RecoveryOperation.module_id == "workflow_engine"
|
||||
)
|
||||
)
|
||||
assert operation is not None
|
||||
self.assertEqual(RecoveryStatus.RUNNING.value, operation.status)
|
||||
self.assertFalse(verify_recovery_evidence_chain(self.session, operation.id))
|
||||
|
||||
def test_dataflow_unknown_outcome_blocks_workflow_until_resolved(self) -> None:
|
||||
instance = self._start("dataflow-unknown")
|
||||
self.dataflow.mark_outcome_unknown("run:1")
|
||||
|
||||
changed = reconcile_instance(
|
||||
self.session,
|
||||
instance=instance,
|
||||
principal=principal(),
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual("waiting", instance.status)
|
||||
current = next(
|
||||
step for step in instance.steps if step.id == instance.current_step_id
|
||||
)
|
||||
self.assertEqual("dataflow_recovery", current.handoff["kind"])
|
||||
self.assertEqual(["cancel"], current.handoff["allowed_actions"])
|
||||
self.assertTrue(current.handoff["recovery"]["requires_attention"])
|
||||
|
||||
self.dataflow.finish("run:1")
|
||||
changed = reconcile_instance(
|
||||
self.session,
|
||||
instance=instance,
|
||||
principal=principal(),
|
||||
registry=self.registry,
|
||||
)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual("completed", instance.status)
|
||||
|
||||
def test_worker_does_not_advance_an_instance_owned_by_another_runtime(
|
||||
self,
|
||||
) -> None:
|
||||
instance = self._start("fenced-instance")
|
||||
self.session.commit()
|
||||
self.dataflow.finish("run:1")
|
||||
fence = acquire_workflow_state_fence(
|
||||
self.session,
|
||||
resource_key=f"workflow:instance:{instance.id}",
|
||||
)
|
||||
assert fence is not None
|
||||
self.session.commit()
|
||||
bind_process_runtime_identity(
|
||||
RuntimeIdentity(
|
||||
installation_id="workflow-engine-tests",
|
||||
node_id="other-worker",
|
||||
incarnation="other-worker-incarnation",
|
||||
role="worker",
|
||||
software_version="test",
|
||||
composition_hash="e" * 64,
|
||||
)
|
||||
)
|
||||
worker = SqlWorkflowRuntimeWorker(
|
||||
registry=Registry(self.dataflow, FakeAutomationProvider()),
|
||||
)
|
||||
|
||||
summary = worker.reconcile_pending(self.session)
|
||||
|
||||
self.assertEqual(0, summary["advanced"])
|
||||
self.assertEqual(1, summary["skipped"])
|
||||
self.session.refresh(instance)
|
||||
self.assertEqual("waiting", instance.status)
|
||||
lease = self.session.scalar(
|
||||
select(DistributedLease).where(
|
||||
DistributedLease.resource_key == f"workflow:instance:{instance.id}"
|
||||
)
|
||||
)
|
||||
assert lease is not None
|
||||
lease.expires_at = utcnow() - timedelta(seconds=1)
|
||||
self.session.commit()
|
||||
|
||||
summary = worker.reconcile_pending(self.session)
|
||||
|
||||
self.assertEqual(1, summary["advanced"])
|
||||
self.session.refresh(instance)
|
||||
self.assertEqual("completed", instance.status)
|
||||
bind_process_runtime_identity(runtime_identity())
|
||||
|
||||
def test_stale_action_attempt_becomes_unknown_instead_of_replaying(
|
||||
self,
|
||||
) -> None:
|
||||
action = FakeActionProvider("retryable", "completed")
|
||||
registry = Registry(self.dataflow, action=action)
|
||||
definition = create_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionCreateRequest(
|
||||
name="Stale action attempt",
|
||||
graph=action_graph(),
|
||||
),
|
||||
)
|
||||
activate_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
)
|
||||
instance, _replayed = start_instance(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=definition.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowInstanceStartRequest(
|
||||
idempotency_key="stale-action-attempt",
|
||||
input={"case_id": "case-stale"},
|
||||
),
|
||||
)
|
||||
current = instance_response(self.session, instance).steps[-1]
|
||||
revision = self.session.get(
|
||||
WorkflowDefinitionRevision,
|
||||
instance.definition_revision_id,
|
||||
)
|
||||
assert revision is not None
|
||||
request = action.requests[0]
|
||||
preview = action.preview_action(
|
||||
self.session,
|
||||
principal(),
|
||||
request=request,
|
||||
)
|
||||
started = begin_workflow_action_recovery(
|
||||
self.session,
|
||||
instance=instance,
|
||||
step=current,
|
||||
revision=revision,
|
||||
definition=action.action,
|
||||
capability_name="test.actions",
|
||||
request_idempotency_key=request.idempotency_key,
|
||||
action_input={"case_id": "case-stale"},
|
||||
preview_payload=_action_preview_payload(preview),
|
||||
)
|
||||
self.assertFalse(started.replayed)
|
||||
lease = self.session.scalar(
|
||||
select(DistributedLease).where(
|
||||
DistributedLease.resource_key == f"workflow:step:{current.id}"
|
||||
)
|
||||
)
|
||||
assert lease is not None
|
||||
lease.expires_at = utcnow() - timedelta(seconds=1)
|
||||
self.session.commit()
|
||||
bind_process_runtime_identity(
|
||||
RuntimeIdentity(
|
||||
installation_id="workflow-engine-tests",
|
||||
node_id="takeover-worker",
|
||||
incarnation="takeover-worker-incarnation",
|
||||
role="worker",
|
||||
software_version="test",
|
||||
composition_hash="f" * 64,
|
||||
)
|
||||
)
|
||||
|
||||
resolved = resolve_step(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
instance_id=instance.id,
|
||||
step_id=current.id,
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=registry,
|
||||
payload=WorkflowStepActionRequest(action="retry"),
|
||||
)
|
||||
|
||||
current = next(step for step in resolved.steps if step.id == current.id)
|
||||
self.assertEqual("outcome_unknown", current.handoff["state"])
|
||||
self.assertNotIn("retry", current.handoff["allowed_actions"])
|
||||
self.assertEqual(1, len(action.requests))
|
||||
operation = self.session.get(RecoveryOperation, started.operation_id)
|
||||
assert operation is not None
|
||||
self.assertEqual(RecoveryStatus.OUTCOME_UNKNOWN.value, operation.status)
|
||||
bind_process_runtime_identity(runtime_identity())
|
||||
|
||||
def test_automated_dataflow_failure_policy_fails_without_handoff(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
@@ -13,6 +13,12 @@ from govoplan_core.core.access import (
|
||||
)
|
||||
from govoplan_core.core.automation import AutomationPrincipalResolution
|
||||
from govoplan_core.core.events import EventTenantRef, PlatformEvent
|
||||
from govoplan_core.core.recovery import RecoveryCheckpoint, RecoveryOperation
|
||||
from govoplan_core.core.runtime_coordination import (
|
||||
DistributedLease,
|
||||
RuntimeIdentity,
|
||||
bind_process_runtime_identity,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
@@ -55,6 +61,17 @@ def principal() -> ApiPrincipal:
|
||||
)
|
||||
|
||||
|
||||
def runtime_identity() -> RuntimeIdentity:
|
||||
return RuntimeIdentity(
|
||||
installation_id="workflow-trigger-tests",
|
||||
node_id="workflow-trigger-worker",
|
||||
incarnation="workflow-trigger-incarnation",
|
||||
role="worker",
|
||||
software_version="test",
|
||||
composition_hash="d" * 64,
|
||||
)
|
||||
|
||||
|
||||
class AutomationProvider:
|
||||
def resolve_automation_principal(self, _session, *, request):
|
||||
return AutomationPrincipalResolution(
|
||||
@@ -110,6 +127,9 @@ class WorkflowTriggerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
self.tables = [
|
||||
DistributedLease.__table__,
|
||||
RecoveryOperation.__table__,
|
||||
RecoveryCheckpoint.__table__,
|
||||
WorkflowDefinition.__table__,
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
WorkflowInstance.__table__,
|
||||
@@ -122,9 +142,11 @@ class WorkflowTriggerTests(unittest.TestCase):
|
||||
Base.metadata.create_all(self.engine, tables=self.tables)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
self.session: Session = self.Session()
|
||||
bind_process_runtime_identity(runtime_identity())
|
||||
self.registry = Registry()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
bind_process_runtime_identity(None)
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(self.engine, tables=list(reversed(self.tables)))
|
||||
self.engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user