mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
Track backend integration, Ruff cleanup and migration reconciliation in ENG-1271. Retain required copy contracts without implementing frontend interactions.
344 lines
15 KiB
Python
344 lines
15 KiB
Python
"""Transaction-local native admission, acknowledged fences and unknown quarantine."""
|
|
|
|
from sqlalchemy import select
|
|
|
|
from models.dify_builder import DifyBuilderExecutionRequest
|
|
from models.dify_builder_native_tests import (
|
|
NativeContinuation,
|
|
NativeDispatch,
|
|
NativeInvocationState,
|
|
NativePendingAction,
|
|
NativeTestInvocation,
|
|
NativeTestScope,
|
|
)
|
|
from runtime_contracts.builder.execution import BuilderExecutionPolicyError
|
|
|
|
|
|
def validate_pending_fence(rows, *, state, ref, tenant_id, pending_ids):
|
|
"""Validate exact current local pending facts, preserving the represented order."""
|
|
if (
|
|
not isinstance(pending_ids, (tuple, list))
|
|
or not pending_ids
|
|
or any(type(identity) is not str or not identity for identity in pending_ids)
|
|
or len(set(pending_ids)) != len(pending_ids)
|
|
or len(rows) != len(pending_ids)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
by_id = {row.id: row for row in rows}
|
|
if set(by_id) != set(pending_ids) or any(
|
|
(row.tenant_id, row.request_id, row.invocation_id, row.generation)
|
|
!= (tenant_id, ref.request_id, ref.invocation_id, state.generation)
|
|
or row.category not in {"approval", "configuration", "interaction"}
|
|
for row in rows
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
return tuple(by_id[identity] for identity in pending_ids)
|
|
|
|
|
|
def pending_reason(rows):
|
|
"""The actual private reason uses the existing waiting-category precedence."""
|
|
categories = {row.category for row in rows}
|
|
if not categories or not categories <= {"configuration", "interaction", "approval"}:
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
return next(reason for reason in ("configuration", "interaction", "approval") if reason in categories)
|
|
|
|
|
|
def validate_pending_history(session, *, ref, tenant_id, state, states=None, dispatch_states=("claimed", "completed")):
|
|
"""Retained IDs prove a past acknowledged claim, never a current pause fence.
|
|
|
|
Root's preservation ruling uses real wait_reason=None to open admission.
|
|
Each retained owner must still have its exact saved checkpoint and older
|
|
pending generation. Never reconstruct a lost set by scanning a generation.
|
|
"""
|
|
if states is None:
|
|
states = {
|
|
row.invocation_id: row
|
|
for row in session.scalars(
|
|
select(NativeInvocationState).where(
|
|
NativeInvocationState.request_id == ref.request_id, NativeInvocationState.tenant_id == tenant_id
|
|
)
|
|
)
|
|
}
|
|
ids = state.pending_ids
|
|
if (
|
|
not state.checkpoint_id
|
|
or type(state.generation) is not int
|
|
or state.generation <= 0
|
|
or type(ids) is not list
|
|
or not ids
|
|
or any(type(identity) is not str or not identity for identity in ids)
|
|
or len(set(ids)) != len(ids)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
continuations = {
|
|
(row.invocation_id, row.checkpoint_id): row
|
|
for row in session.scalars(
|
|
select(NativeContinuation).where(
|
|
NativeContinuation.request_id == ref.request_id, NativeContinuation.tenant_id == tenant_id
|
|
)
|
|
)
|
|
}
|
|
path, selected = set(), state.invocation_id
|
|
while selected is not None:
|
|
owner = states.get(selected)
|
|
if selected in path or owner is None or (owner.tenant_id, owner.request_id) != (tenant_id, ref.request_id):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
path.add(selected)
|
|
selected = owner.parent_invocation_id
|
|
claims = session.scalars(
|
|
select(NativeDispatch).where(NativeDispatch.request_id == ref.request_id, NativeDispatch.tenant_id == tenant_id)
|
|
).all()
|
|
if not any(
|
|
row.invocation_id in path
|
|
and row.generation == state.generation
|
|
and row.state in dispatch_states
|
|
and (row.tenant_id, row.request_id) == (tenant_id, ref.request_id)
|
|
and (row.invocation_id, row.checkpoint_id) in continuations
|
|
and continuations[row.invocation_id, row.checkpoint_id].generation < row.generation
|
|
and (row.invocation_id != state.invocation_id or row.checkpoint_id == state.checkpoint_id)
|
|
for row in claims
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
selected_checkpoint = continuations.get((state.invocation_id, state.checkpoint_id))
|
|
if (
|
|
selected_checkpoint is None
|
|
or selected_checkpoint.generation >= state.generation
|
|
or (selected_checkpoint.tenant_id, selected_checkpoint.request_id, selected_checkpoint.invocation_id)
|
|
!= (tenant_id, ref.request_id, state.invocation_id)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
rows = session.scalars(
|
|
select(NativePendingAction).where(
|
|
NativePendingAction.request_id == ref.request_id,
|
|
NativePendingAction.tenant_id == tenant_id,
|
|
NativePendingAction.id.in_(ids),
|
|
)
|
|
).all()
|
|
by_id = {row.id: row for row in rows}
|
|
if set(by_id) != set(ids) or len(rows) != len(ids):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
for row in rows:
|
|
owner = states.get(row.invocation_id)
|
|
saved = continuations.get((row.invocation_id, owner.checkpoint_id)) if owner is not None else None
|
|
if (
|
|
owner is None
|
|
or saved is None
|
|
or row.id not in owner.pending_ids
|
|
or (owner.tenant_id, owner.request_id) != (tenant_id, ref.request_id)
|
|
or (row.tenant_id, row.request_id, saved.tenant_id, saved.request_id)
|
|
!= (tenant_id, ref.request_id, tenant_id, ref.request_id)
|
|
or row.category not in {"approval", "configuration", "interaction"}
|
|
or row.generation != saved.generation
|
|
or saved.generation >= owner.generation
|
|
):
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
descendant, seen = row.invocation_id, set()
|
|
while descendant != state.invocation_id:
|
|
if descendant in seen or descendant not in states:
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
seen.add(descendant)
|
|
descendant = states[descendant].parent_invocation_id
|
|
if descendant is None:
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
return tuple(by_id[identity] for identity in ids)
|
|
|
|
|
|
def dispatch_readiness(session, *, ref, tenant_id, now, coalesce_self=False):
|
|
"""SQL-only physical admission; caller's root lock serializes any write.
|
|
|
|
Recovery/private reads retain their separate authority. Global generation
|
|
changes do not invalidate another branch's current local generation.
|
|
Owned wait boundaries may coalesce a validated self fence into an owned wait.
|
|
"""
|
|
request = session.scalar(
|
|
select(DifyBuilderExecutionRequest).where(
|
|
DifyBuilderExecutionRequest.id == ref.request_id, DifyBuilderExecutionRequest.tenant_id == tenant_id
|
|
)
|
|
)
|
|
scope = session.scalar(
|
|
select(NativeTestScope).where(
|
|
NativeTestScope.request_id == ref.request_id, NativeTestScope.tenant_id == tenant_id
|
|
)
|
|
)
|
|
if (
|
|
request is None
|
|
or (request.id, request.tenant_id, request.context_digest) != (ref.request_id, tenant_id, ref.context_digest)
|
|
or scope is None
|
|
or (scope.request_id, scope.tenant_id) != (ref.request_id, tenant_id)
|
|
or scope.cancelled
|
|
or scope.expires_at <= now
|
|
):
|
|
raise BuilderExecutionPolicyError("native_dispatch_closed")
|
|
states = {
|
|
state.invocation_id: state
|
|
for state in session.scalars(
|
|
select(NativeInvocationState).where(
|
|
NativeInvocationState.request_id == ref.request_id, NativeInvocationState.tenant_id == tenant_id
|
|
)
|
|
)
|
|
}
|
|
active = {
|
|
row.invocation_id
|
|
for row in session.scalars(
|
|
select(NativeTestInvocation).where(
|
|
NativeTestInvocation.request_id == ref.request_id, NativeTestInvocation.tenant_id == tenant_id
|
|
)
|
|
)
|
|
if row.active and (row.request_id, row.tenant_id) == (ref.request_id, tenant_id)
|
|
}
|
|
roots = [state for state in states.values() if state.parent_invocation_id is None]
|
|
selected, seen, fence = ref.invocation_id, set(), None
|
|
while True:
|
|
state = states.get(selected)
|
|
if (
|
|
selected in seen
|
|
or state is None
|
|
or selected not in active
|
|
or (state.request_id, state.tenant_id) != (ref.request_id, tenant_id)
|
|
or state.state != "running"
|
|
):
|
|
raise BuilderExecutionPolicyError("native_dispatch_closed")
|
|
seen.add(selected)
|
|
if type(state.pending_ids) is not list or state.wait_reason not in {
|
|
None,
|
|
"approval",
|
|
"configuration",
|
|
"interaction",
|
|
}:
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
if state.wait_reason is not None:
|
|
if selected != ref.invocation_id or not coalesce_self:
|
|
raise BuilderExecutionPolicyError("native_dispatch_fenced")
|
|
rows = session.scalars(
|
|
select(NativePendingAction).where(
|
|
NativePendingAction.tenant_id == tenant_id,
|
|
NativePendingAction.request_id == ref.request_id,
|
|
NativePendingAction.id.in_(state.pending_ids),
|
|
)
|
|
).all()
|
|
validated = validate_pending_fence(
|
|
rows, state=state, ref=ref, tenant_id=tenant_id, pending_ids=state.pending_ids
|
|
)
|
|
if pending_reason(validated) != state.wait_reason:
|
|
raise BuilderExecutionPolicyError("native_pending_mismatch")
|
|
fence = next(row for row in validated if row.category == state.wait_reason)
|
|
elif state.pending_ids:
|
|
validate_pending_history(
|
|
session,
|
|
ref=ref.model_copy(update={"invocation_id": selected}),
|
|
tenant_id=tenant_id,
|
|
state=state,
|
|
states=states,
|
|
)
|
|
if state.parent_invocation_id is None:
|
|
if len(roots) != 1 or (state.app_id, state.workflow_id) != (request.app_id, request.workflow_id):
|
|
raise BuilderExecutionPolicyError("native_dispatch_closed")
|
|
return fence
|
|
selected = state.parent_invocation_id
|
|
|
|
|
|
def invocation_subtree(session, ref, tenant_id):
|
|
states = session.scalars(
|
|
select(NativeInvocationState).where(
|
|
NativeInvocationState.request_id == ref.request_id, NativeInvocationState.tenant_id == tenant_id
|
|
)
|
|
).all()
|
|
ids = {ref.invocation_id}
|
|
while True:
|
|
expanded = ids | {state.invocation_id for state in states if state.parent_invocation_id in ids}
|
|
if expanded == ids:
|
|
return [state for state in states if state.invocation_id in ids]
|
|
ids = expanded
|
|
|
|
|
|
def quarantine_unknown(session, *, ref, tenant_id, outcome=None):
|
|
"""Caller holds the root request lock. Preserve descendants' saved execution facts.
|
|
|
|
An unknown descendant blocks ancestor resume separately; only trusted whole
|
|
subtree native fencing evidence may restore execution authority.
|
|
"""
|
|
states = invocation_subtree(session, ref, tenant_id)
|
|
ids = {state.invocation_id for state in states}
|
|
for state in states:
|
|
if state.invocation_id == ref.invocation_id and state.state not in {"completed", "stopped", "cancelled"}:
|
|
state.state = "unknown"
|
|
if outcome is not None:
|
|
state.outcome = outcome
|
|
for invocation in session.scalars(
|
|
select(NativeTestInvocation).where(
|
|
NativeTestInvocation.request_id == ref.request_id,
|
|
NativeTestInvocation.tenant_id == tenant_id,
|
|
NativeTestInvocation.invocation_id.in_(ids),
|
|
)
|
|
):
|
|
invocation.active = False
|
|
for dispatch in session.scalars(
|
|
select(NativeDispatch).where(
|
|
NativeDispatch.request_id == ref.request_id,
|
|
NativeDispatch.tenant_id == tenant_id,
|
|
NativeDispatch.invocation_id.in_(ids),
|
|
NativeDispatch.state.in_(("queued", "published", "claimed")),
|
|
)
|
|
):
|
|
dispatch.state = "unknown"
|
|
|
|
|
|
def has_unknown_descendant(session, ref, tenant_id):
|
|
return any(state.state == "unknown" for state in invocation_subtree(session, ref, tenant_id))
|
|
|
|
|
|
def require_ancestor_authority(session, ref, tenant_id):
|
|
"""An invocation's own proof cannot discharge a controlling ancestor quarantine.
|
|
|
|
Call before external recovery proof and again under the root request lock.
|
|
The selected invocation may itself be unknown/inactive during recovery;
|
|
every ancestor must still own execution authority. Missing/cyclic lineage
|
|
fails closed instead of treating a disconnected child as another root.
|
|
"""
|
|
states = {
|
|
state.invocation_id: state
|
|
for state in session.scalars(
|
|
select(NativeInvocationState).where(
|
|
NativeInvocationState.request_id == ref.request_id, NativeInvocationState.tenant_id == tenant_id
|
|
)
|
|
)
|
|
}
|
|
active_ids = set(
|
|
session.scalars(
|
|
select(NativeTestInvocation.invocation_id).where(
|
|
NativeTestInvocation.request_id == ref.request_id,
|
|
NativeTestInvocation.tenant_id == tenant_id,
|
|
NativeTestInvocation.active.is_(True),
|
|
)
|
|
)
|
|
)
|
|
state = states.get(ref.invocation_id)
|
|
seen = {ref.invocation_id}
|
|
if state is None:
|
|
raise BuilderExecutionPolicyError("native_ancestor_quarantined")
|
|
while state.parent_invocation_id is not None:
|
|
parent_id = state.parent_invocation_id
|
|
if parent_id in seen or parent_id not in active_ids:
|
|
raise BuilderExecutionPolicyError("native_ancestor_quarantined")
|
|
seen.add(parent_id)
|
|
state = states.get(parent_id)
|
|
if state is None or state.state not in {"running", "waiting", "queued"}:
|
|
raise BuilderExecutionPolicyError("native_ancestor_quarantined")
|
|
|
|
|
|
def recoverable_invocation_ids(states, root_invocation_id):
|
|
"""Restore only paths whose complete lineage belongs to the recovered subtree.
|
|
|
|
An independently unknown or completed branch remains inactive, including its
|
|
descendants, even when the controlling root's physical fence is proven.
|
|
"""
|
|
eligible = {state.invocation_id: state for state in states if state.state in {"running", "waiting", "queued"}}
|
|
recoverable = {root_invocation_id} if root_invocation_id in eligible else set()
|
|
while True:
|
|
expanded = recoverable | {
|
|
state.invocation_id for state in eligible.values() if state.parent_invocation_id in recoverable
|
|
}
|
|
if expanded == recoverable:
|
|
return recoverable
|
|
recoverable = expanded
|