mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
322 lines
16 KiB
Python
322 lines
16 KiB
Python
"""Trusted native root capacity; App lock precedes the existing request lock.
|
|
|
|
No ordinary workflow limiter, physical host, provider I/O or default cleanup proof.
|
|
"""
|
|
|
|
from contextlib import contextmanager
|
|
from typing import Protocol, runtime_checkable
|
|
|
|
from sqlalchemy import and_, select
|
|
|
|
from models.dify_builder import DifyBuilderExecutionRequest, DifyBuilderSession, DifyBuilderTestInput
|
|
from models.dify_builder_native_tests import (
|
|
NativeContinuation,
|
|
NativeDispatch,
|
|
NativeEffectAttempt,
|
|
NativeEffectDispatchClaim,
|
|
NativeEffectOperation,
|
|
NativeEffectOutcome,
|
|
NativeInvocationState,
|
|
NativePayloadObject,
|
|
NativeTestInvocation,
|
|
NativeTestScope,
|
|
)
|
|
from models.model import App
|
|
from runtime_contracts.builder.execution import BuilderExecutionPolicyError, canonical_digest
|
|
from runtime_contracts.builder.native.contracts import NativeTestContextRef
|
|
from services.dify_builder.native_testing.lifecycle import NativeTestLifecycleService
|
|
from services.dify_builder.native_testing.repository import native_observation_transaction
|
|
from services.dify_builder.native_testing.state_store import invocation_state, owner_query
|
|
|
|
|
|
@runtime_checkable
|
|
class NativeHostCleanupVerifier(Protocol):
|
|
def consume_cleanup(self, *, ref: NativeTestContextRef, generation: int, dispatch_id: str) -> None:
|
|
"""Consume exact retained acquisition's genuine complete cleanup once.
|
|
|
|
Immutable per-lifetime binding; reject if this lifetime is not CURRENT,
|
|
even after SAME dispatch ID/generation reacquisition. No handle absence,
|
|
caller DTO/boolean, lease or runtime.cancel list substitutes for proof.
|
|
Invoked outside the writer. Missing/old/replayed/uncertain proof raises.
|
|
"""
|
|
...
|
|
|
|
|
|
class NativeHostCapacity:
|
|
def __init__(
|
|
self, lifecycle: NativeTestLifecycleService, *, cleanup_verifier: NativeHostCleanupVerifier, code_cleanup=None
|
|
):
|
|
if not isinstance(cleanup_verifier, NativeHostCleanupVerifier) or not callable(
|
|
cleanup_verifier.consume_cleanup
|
|
):
|
|
raise BuilderExecutionPolicyError("native_cleanup_verifier_required")
|
|
if lifecycle._capacity is not None:
|
|
raise BuilderExecutionPolicyError("native_capacity_already_bound")
|
|
self.lifecycle, self.loader, self._verifier = lifecycle, lifecycle.loader, cleanup_verifier
|
|
# Absent for the existing HTTP-only runtime: do not query Code storage.
|
|
self._code_cleanup = code_cleanup
|
|
self._used = False
|
|
lifecycle._capacity = self
|
|
|
|
@contextmanager
|
|
def locked(self, *, ref: NativeTestContextRef, settlement: bool = False):
|
|
with locked_native_capacity(self.lifecycle, ref=ref, settlement=settlement) as result:
|
|
yield result
|
|
|
|
def _occupants(self, session, owned):
|
|
request_model, dispatch_model, state_model, invocation_model, scope_model = (
|
|
DifyBuilderExecutionRequest,
|
|
NativeDispatch,
|
|
NativeInvocationState,
|
|
NativeTestInvocation,
|
|
NativeTestScope,
|
|
)
|
|
rows = session.execute(
|
|
select(
|
|
request_model,
|
|
dispatch_model,
|
|
state_model,
|
|
invocation_model,
|
|
scope_model,
|
|
DifyBuilderSession,
|
|
DifyBuilderTestInput,
|
|
)
|
|
.select_from(request_model)
|
|
.outerjoin(dispatch_model, dispatch_model.request_id == request_model.id)
|
|
.outerjoin(
|
|
state_model,
|
|
and_(
|
|
state_model.request_id == request_model.id,
|
|
state_model.invocation_id == dispatch_model.invocation_id,
|
|
),
|
|
)
|
|
.outerjoin(
|
|
invocation_model,
|
|
and_(
|
|
invocation_model.request_id == request_model.id,
|
|
invocation_model.invocation_id == dispatch_model.invocation_id,
|
|
),
|
|
)
|
|
.outerjoin(scope_model, scope_model.request_id == request_model.id)
|
|
.outerjoin(DifyBuilderSession, DifyBuilderSession.id == request_model.session_id)
|
|
.outerjoin(DifyBuilderTestInput, DifyBuilderTestInput.id == request_model.test_input_id)
|
|
.where(request_model.app_id == owned.app_id, request_model.tenant_id == owned.tenant_id)
|
|
).all()
|
|
roots = set()
|
|
for request, dispatch, state, invocation, scope, builder, inputs in rows:
|
|
if dispatch is None or dispatch.state not in {"claimed", "unknown", "completed"}:
|
|
continue
|
|
if (
|
|
(dispatch.request_id, dispatch.tenant_id) != (request.id, request.tenant_id)
|
|
or state is None
|
|
or invocation is None
|
|
or scope is None
|
|
or (state.request_id, state.tenant_id, state.invocation_id)
|
|
!= (request.id, request.tenant_id, dispatch.invocation_id)
|
|
or (invocation.request_id, invocation.tenant_id, invocation.invocation_id)
|
|
!= (request.id, request.tenant_id, dispatch.invocation_id)
|
|
or (scope.request_id, scope.tenant_id) != (request.id, request.tenant_id)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_capacity_owner_mismatch")
|
|
if state.parent_invocation_id is not None:
|
|
continue # Actual host children are a separately gated physical owner.
|
|
if (
|
|
(state.request_id, state.tenant_id, state.app_id, state.workflow_id)
|
|
!= (request.id, request.tenant_id, request.app_id, request.workflow_id)
|
|
or not isinstance(request.context, dict)
|
|
or request.context.get("policy_version") != "builder-native-test-v1"
|
|
or canonical_digest(request.context) != request.context_digest
|
|
):
|
|
raise BuilderExecutionPolicyError("native_capacity_owner_mismatch")
|
|
if state.cleanup_complete:
|
|
continue
|
|
if (
|
|
invocation is None
|
|
or scope is None
|
|
or builder is None
|
|
or inputs is None
|
|
or (builder.tenant_id, builder.app_id, builder.owner_account_id, inputs.session_id)
|
|
!= (request.tenant_id, request.app_id, request.actor_id, request.session_id)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_capacity_owner_mismatch")
|
|
roots.add((request.id, state.invocation_id))
|
|
return roots
|
|
|
|
def claim_dispatch(self, *, ref: NativeTestContextRef, generation: int, dispatch_id: str) -> bool:
|
|
if self._used:
|
|
return False
|
|
prepared = self.lifecycle.prepare_dispatch_claim(ref=ref, generation=generation, dispatch_id=dispatch_id)
|
|
if prepared is None:
|
|
return False
|
|
with self.locked(ref=ref) as (session, owned, scope):
|
|
if self._used:
|
|
return False
|
|
current = session.scalar(
|
|
owner_query(NativeDispatch, ref, owned.tenant_id).where(NativeDispatch.id == dispatch_id)
|
|
)
|
|
if current is None or type(generation) is not int or current.generation != generation:
|
|
raise BuilderExecutionPolicyError("native_dispatch_mismatch")
|
|
if current.state not in {"queued", "published"}:
|
|
return False
|
|
if self._occupants(session, owned):
|
|
raise BuilderExecutionPolicyError("native_capacity_busy")
|
|
self._used = True # An ambiguous claim cannot reuse this physical lifetime.
|
|
return self.lifecycle.consume_dispatch_claim(session=session, owned=owned, scope=scope, prepared=prepared)
|
|
|
|
@staticmethod
|
|
def _latest_acquired(session, ref, tenant_id):
|
|
rows = session.scalars(
|
|
owner_query(NativeDispatch, ref, tenant_id).where(
|
|
NativeDispatch.state.in_(("claimed", "unknown", "completed"))
|
|
)
|
|
).all()
|
|
return max(rows, key=lambda row: row.generation) if rows else None
|
|
|
|
def record_cleanup(self, *, ref: NativeTestContextRef, generation: int, dispatch_id: str) -> None:
|
|
with self.loader.session_factory() as session:
|
|
owned, _ = self.loader._load(session, ref=ref, lock=False, settlement=True)
|
|
state = invocation_state(session, ref, owned.tenant_id)
|
|
selected = self._latest_acquired(session, ref, owned.tenant_id)
|
|
self._validate_cleanup(state, selected, generation, dispatch_id)
|
|
if self._code_cleanup is not None:
|
|
self._code_cleanup.validate_cleanup_rows(
|
|
session=session, ref=ref, tenant_id=owned.tenant_id, dispatch_id=dispatch_id, generation=generation
|
|
)
|
|
# This verifier is immutable and bound to ONE actual physical lifetime,
|
|
# never a registry lookup that selects a newer lifetime by these IDs.
|
|
if self._verifier.consume_cleanup(ref=ref, generation=generation, dispatch_id=dispatch_id) is not None:
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
with self.locked(ref=ref, settlement=True) as (session, owned, _):
|
|
state = invocation_state(session, ref, owned.tenant_id)
|
|
selected = self._latest_acquired(session, ref, owned.tenant_id)
|
|
self._validate_cleanup(state, selected, generation, dispatch_id)
|
|
if self._code_cleanup is not None:
|
|
self._code_cleanup.validate_cleanup_rows(
|
|
session=session, ref=ref, tenant_id=owned.tenant_id, dispatch_id=dispatch_id, generation=generation
|
|
)
|
|
for current in self.lifecycle._subtree(session, ref, owned.tenant_id):
|
|
current.cleanup_complete = True
|
|
|
|
@staticmethod
|
|
def _validate_cleanup(state, selected, generation, dispatch_id):
|
|
if (
|
|
type(generation) is not int
|
|
or state.parent_invocation_id is not None
|
|
or state.generation < generation
|
|
or state.cleanup_complete
|
|
or selected is None
|
|
or (selected.id, selected.generation) != (dispatch_id, generation)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
|
|
def _finish_unacquired_cancellation(self, *, ref: NativeTestContextRef) -> bool:
|
|
return finish_unacquired_cancellation(self.lifecycle, ref=ref)
|
|
|
|
def record_cancelled_cleanup(self, *, ref: NativeTestContextRef) -> None:
|
|
if self._finish_unacquired_cancellation(ref=ref):
|
|
return
|
|
with self.loader.session_factory() as session:
|
|
owned, _ = self.loader._load(session, ref=ref, lock=False, settlement=True)
|
|
selected = self._latest_acquired(session, ref, owned.tenant_id)
|
|
if selected is None:
|
|
return # No physical claim is inferred from a cancelled queue row.
|
|
identity, generation = selected.id, selected.generation
|
|
self.record_cleanup(ref=ref, generation=generation, dispatch_id=identity)
|
|
|
|
|
|
@contextmanager
|
|
def locked_native_capacity(lifecycle, *, ref: NativeTestContextRef, settlement: bool = False):
|
|
# Read the persisted App identity before the Session→App→Request writer.
|
|
with lifecycle.loader.session_factory() as session:
|
|
owned, _ = lifecycle.loader._load(session, ref=ref, lock=False, settlement=settlement)
|
|
app_id, tenant_id = owned.app_id, owned.tenant_id
|
|
with native_observation_transaction(
|
|
lifecycle.loader.session_factory, session_id=lambda: owned.session_id
|
|
) as session:
|
|
if not settlement:
|
|
lifecycle.loader.lock_task_admission(session, ref=ref)
|
|
app = session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant_id).with_for_update())
|
|
if app is None:
|
|
raise BuilderExecutionPolicyError("native_owner_mismatch")
|
|
owned, scope = lifecycle.loader._load(session, ref=ref, lock=True, settlement=settlement)
|
|
state = invocation_state(session, ref, owned.tenant_id)
|
|
if (
|
|
state.parent_invocation_id is not None
|
|
or (owned.app_id, owned.tenant_id) != (app_id, tenant_id)
|
|
or (state.app_id, state.workflow_id) != (owned.app_id, owned.workflow_id)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_capacity_root_required")
|
|
yield session, owned, scope
|
|
|
|
|
|
def finish_unacquired_cancellation(lifecycle, *, ref: NativeTestContextRef) -> bool:
|
|
# Authoritative SQL history under App→request, never local handle absence.
|
|
with locked_native_capacity(lifecycle, ref=ref, settlement=True) as (session, owned, scope):
|
|
states = session.scalars(
|
|
select(NativeInvocationState).where(NativeInvocationState.request_id == ref.request_id)
|
|
).all()
|
|
invocations = session.scalars(
|
|
select(NativeTestInvocation).where(NativeTestInvocation.request_id == ref.request_id)
|
|
).all()
|
|
dispatches = session.scalars(select(NativeDispatch).where(NativeDispatch.request_id == ref.request_id)).all()
|
|
identities = {row.invocation_id for row in states}
|
|
if (
|
|
not scope.cancelled
|
|
or not states
|
|
or len(identities) != len(states)
|
|
or identities != {row.invocation_id for row in invocations}
|
|
or len(invocations) != len(states)
|
|
or any(
|
|
(row.request_id, row.tenant_id) != (ref.request_id, owned.tenant_id)
|
|
for row in (*states, *invocations, *dispatches)
|
|
)
|
|
or any(row.state != "cancelled" or not row.cancellation_key for row in states)
|
|
or any(row.active for row in invocations)
|
|
or any(row.invocation_id not in identities for row in dispatches)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
if any(row.state in {"claimed", "unknown", "completed"} for row in dispatches):
|
|
return False # The exact retained-lifetime verifier remains mandatory.
|
|
# Only a complete genuine initial outbox proves never acquired here.
|
|
# Missing history, children, resumed/effectful work remain uncertain.
|
|
state = states[0]
|
|
if (
|
|
len(states) != 1
|
|
or state.invocation_id != ref.invocation_id
|
|
or state.parent_invocation_id is not None
|
|
or type(state.generation) is not int
|
|
or state.generation != 0
|
|
or scope.generation != 0
|
|
or state.checkpoint_id is not None
|
|
or state.pending_ids != []
|
|
or state.wait_reason is not None
|
|
or state.resolution != []
|
|
or state.outcome is not None
|
|
or len(dispatches) != 1
|
|
or dispatches[0].state != "cancelled"
|
|
or dispatches[0].generation != 0
|
|
or dispatches[0].checkpoint_id is not None
|
|
or dispatches[0].claim_expires_at is not None
|
|
or dispatches[0].idempotency_digest != canonical_digest({"initial": ref.model_dump(mode="json")})
|
|
):
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
context = session.get(NativePayloadObject, state.context_object_id)
|
|
if context is None or (context.request_id, context.tenant_id, context.invocation_id, context.kind) != (
|
|
ref.request_id,
|
|
owned.tenant_id,
|
|
ref.invocation_id,
|
|
"context",
|
|
):
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
for model in (
|
|
NativeContinuation,
|
|
NativeEffectOperation,
|
|
NativeEffectAttempt,
|
|
NativeEffectDispatchClaim,
|
|
NativeEffectOutcome,
|
|
):
|
|
if session.scalar(select(model.id).where(model.request_id == ref.request_id)) is not None:
|
|
raise BuilderExecutionPolicyError("native_cleanup_unproven")
|
|
state.cleanup_complete = True # Logical cancellation; no physical acquisition was inferred.
|
|
return True
|