mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
138 lines
5.6 KiB
Python
138 lines
5.6 KiB
Python
"""Bounded native diagnostic context through Dify and public Graphon ports.
|
|
|
|
No logger is installed or reconfigured here. Real application handlers must
|
|
already protect these factual native emitters. Captured Dify contexts restore
|
|
Flask but do not reset ContextVar writes; this adapter owns the missing tokens
|
|
and uses a fresh enter manager for nested/parallel/reused-thread isolation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
from contextlib import AbstractContextManager, contextmanager
|
|
from contextvars import Context, ContextVar, Token
|
|
from threading import local
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
from flask import Flask, has_app_context
|
|
|
|
from context import capture_current_context
|
|
from context.execution_context import ExecutionContext
|
|
from context.flask_app_context import FlaskExecutionContext, capture_flask_context
|
|
from graphon.runtime import RuntimeState
|
|
from libs.flask_utils import preserve_flask_contexts
|
|
from libs.sensitive_http_telemetry import sensitive_http_telemetry
|
|
from services.dify_builder.native_testing.control import NativeApprovalWait, NativePolicyStop
|
|
from services.dify_builder.native_testing.observation_adapters import NativeObservationError, NativeObservationGuard
|
|
|
|
if TYPE_CHECKING:
|
|
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
|
|
|
|
NATIVE_LOG_EMITTERS = (
|
|
"core.app.apps.workflow",
|
|
"core.app.apps.base_app_queue_manager",
|
|
"services.workflow_runtime",
|
|
"core.app.workflow.layers.persistence",
|
|
"services.workflow_persistence",
|
|
"services.workflow_run_agg",
|
|
"core.workflow",
|
|
"graphon",
|
|
"services.dify_builder.native_testing",
|
|
)
|
|
_native_observation: ContextVar[NativeObservationGuard | None] = ContextVar("dify_native_observation", default=None)
|
|
|
|
|
|
def current_native_observation() -> NativeObservationGuard | None:
|
|
"""Context-local association only; host/state establish trusted ownership."""
|
|
return _native_observation.get()
|
|
|
|
|
|
@contextmanager
|
|
def native_observation_scope(*, guard: NativeObservationGuard) -> Generator[None]:
|
|
if type(guard) is not NativeObservationGuard:
|
|
raise NativeObservationError() from None
|
|
guard.require_healthy()
|
|
try:
|
|
with sensitive_http_telemetry(additional_emitters=NATIVE_LOG_EMITTERS):
|
|
token = _native_observation.set(guard)
|
|
try:
|
|
yield
|
|
finally:
|
|
_native_observation.reset(token)
|
|
except (NativeApprovalWait, NativePolicyStop):
|
|
raise # Existing marked native boundary owns typed control semantics.
|
|
except BaseException:
|
|
guard.fail()
|
|
|
|
|
|
@contextmanager
|
|
def native_flask_worker_scope(*, host: NativeHostLaunch, flask_app: Flask, context_vars: Context) -> Generator[None]:
|
|
"""Retain Dify's actual user/Flask adapter while bounding all captured writes."""
|
|
tokens: list[tuple[ContextVar[Any], Token[Any]]] = []
|
|
try:
|
|
for variable, value in context_vars.items():
|
|
tokens.append((variable, variable.set(value)))
|
|
with preserve_flask_contexts(flask_app, context_vars=context_vars), host.observation_scope():
|
|
yield
|
|
finally:
|
|
for variable, token in reversed(tokens):
|
|
variable.reset(token)
|
|
|
|
|
|
class _NativeContextManagers(local):
|
|
def __init__(self) -> None:
|
|
self.stack: list[AbstractContextManager[None]] = []
|
|
|
|
|
|
class NativeExecutionContext:
|
|
"""Reentrant public execution context; every entry restores prior tokens."""
|
|
|
|
def __init__(self, *, guard: NativeObservationGuard) -> None:
|
|
captured = capture_flask_context() if has_app_context() else capture_current_context()
|
|
if type(guard) is not NativeObservationGuard or type(captured) not in (ExecutionContext, FlaskExecutionContext):
|
|
raise NativeObservationError() from None
|
|
self._captured = cast(ExecutionContext | FlaskExecutionContext, captured)
|
|
self._guard = guard
|
|
self._local = _NativeContextManagers()
|
|
|
|
@contextmanager
|
|
def enter(self) -> Generator[None]:
|
|
tokens: list[tuple[ContextVar[Any], Token[Any]]] = []
|
|
try:
|
|
if self._captured.context_vars is not None:
|
|
for variable, value in self._captured.context_vars.items():
|
|
tokens.append((variable, variable.set(value)))
|
|
# .enter() allocates a fresh manager; existing __enter__ stores only
|
|
# one local manager and is therefore not safe for nested reuse.
|
|
with self._captured.enter(), native_observation_scope(guard=self._guard):
|
|
yield
|
|
finally:
|
|
for variable, token in reversed(tokens):
|
|
variable.reset(token)
|
|
|
|
def __enter__(self) -> NativeExecutionContext:
|
|
manager = self.enter()
|
|
manager.__enter__()
|
|
self._local.stack.append(manager)
|
|
return self
|
|
|
|
def __exit__(self, *args: Any) -> None:
|
|
manager: AbstractContextManager[None] = self._local.stack.pop()
|
|
manager.__exit__(*args)
|
|
|
|
|
|
class NativeGraphRuntimeState(RuntimeState):
|
|
"""Validate marked state construction, including inherited snapshot restore.
|
|
|
|
This establishes no resume provenance; all existing marked resume guards
|
|
stay closed. Upstream restoration calls cls(...) without process context,
|
|
so our constructor checks the current explicitly entered native guard.
|
|
Graphon 0.8 worker context is installed through the host layer, not state.
|
|
"""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
guard = current_native_observation()
|
|
if guard is None or "execution_context" in kwargs:
|
|
raise NativeObservationError() from None
|
|
super().__init__(**kwargs)
|