mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 16:08:04 +08:00
feat: sandbox layer for workflow execution
This commit is contained in:
133
api/core/app/layers/sandbox_layer.py
Normal file
133
api/core/app/layers/sandbox_layer.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""
|
||||
Sandbox Layer for managing VirtualEnvironment lifecycle during workflow execution.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
from core.virtual_environment.factory import SandboxFactory, SandboxType
|
||||
from core.workflow.graph_engine.layers.base import GraphEngineLayer
|
||||
from core.workflow.graph_events.base import GraphEngineEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SandboxInitializationError(Exception):
|
||||
"""Raised when sandbox initialization fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SandboxLayer(GraphEngineLayer):
|
||||
"""
|
||||
Manages VirtualEnvironment (sandbox) lifecycle during workflow execution.
|
||||
|
||||
Responsibilities:
|
||||
- on_graph_start: Initialize the sandbox environment
|
||||
- on_graph_end: Release the sandbox environment (cleanup)
|
||||
|
||||
Example:
|
||||
layer = SandboxLayer(
|
||||
sandbox_type=SandboxType.DOCKER,
|
||||
options={"docker_image": "python:3.11-slim"},
|
||||
)
|
||||
graph_engine.layer(layer)
|
||||
|
||||
# During workflow execution, access sandbox via:
|
||||
# layer.sandbox.execute_command(...)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# TODO: read from db table
|
||||
sandbox_type: SandboxType = SandboxType.DOCKER,
|
||||
options: Mapping[str, Any] | None = None,
|
||||
environments: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the SandboxLayer.
|
||||
|
||||
Args:
|
||||
sandbox_type: Type of sandbox to create (default: DOCKER)
|
||||
options: Sandbox-specific configuration options
|
||||
environments: Environment variables to set in the sandbox
|
||||
"""
|
||||
super().__init__()
|
||||
self._sandbox_type = sandbox_type
|
||||
self._options: Mapping[str, Any] = options or {}
|
||||
self._environments: Mapping[str, str] = environments or {}
|
||||
self._sandbox: VirtualEnvironment | None = None
|
||||
|
||||
@property
|
||||
def sandbox(self) -> VirtualEnvironment:
|
||||
"""
|
||||
Get the current sandbox instance.
|
||||
|
||||
Returns:
|
||||
The initialized VirtualEnvironment instance
|
||||
|
||||
Raises:
|
||||
RuntimeError: If sandbox has not been initialized
|
||||
"""
|
||||
if self._sandbox is None:
|
||||
raise RuntimeError("Sandbox not initialized. Ensure on_graph_start() has been called.")
|
||||
return self._sandbox
|
||||
|
||||
def on_graph_start(self) -> None:
|
||||
"""
|
||||
Initialize the sandbox when workflow execution starts.
|
||||
|
||||
Raises:
|
||||
SandboxInitializationError: If sandbox cannot be created
|
||||
"""
|
||||
logger.info("Initializing sandbox, sandbox_type=%s", self._sandbox_type)
|
||||
|
||||
try:
|
||||
self._sandbox = SandboxFactory.create(
|
||||
sandbox_type=self._sandbox_type,
|
||||
options=self._options,
|
||||
environments=self._environments,
|
||||
)
|
||||
logger.info(
|
||||
"Sandbox initialized, sandbox_id=%s, sandbox_arch=%s",
|
||||
self._sandbox.metadata.id,
|
||||
self._sandbox.metadata.arch,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to initialize sandbox")
|
||||
raise SandboxInitializationError(f"Failed to initialize {self._sandbox_type} sandbox: {e}") from e
|
||||
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
"""
|
||||
Handle graph engine events.
|
||||
|
||||
Currently a no-op, but can be extended for sandbox monitoring/health checks.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_graph_end(self, error: Exception | None) -> None:
|
||||
"""
|
||||
Release the sandbox when workflow execution ends.
|
||||
|
||||
This method is idempotent and will not raise exceptions on cleanup failure.
|
||||
|
||||
Args:
|
||||
error: The exception that caused execution to fail, or None if successful
|
||||
"""
|
||||
if self._sandbox is None:
|
||||
logger.debug("No sandbox to release")
|
||||
return
|
||||
|
||||
sandbox_id = self._sandbox.metadata.id
|
||||
logger.info("Releasing sandbox, sandbox_id=%s", sandbox_id)
|
||||
|
||||
try:
|
||||
self._sandbox.release_environment()
|
||||
logger.info("Sandbox released, sandbox_id=%s", sandbox_id)
|
||||
except Exception:
|
||||
# Log but don't raise - cleanup failures should not break workflow completion
|
||||
logger.exception("Failed to release sandbox, sandbox_id=%s", sandbox_id)
|
||||
finally:
|
||||
self._sandbox = None
|
||||
78
api/core/virtual_environment/factory.py
Normal file
78
api/core/virtual_environment/factory.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""
|
||||
Sandbox factory for creating VirtualEnvironment instances.
|
||||
|
||||
Example:
|
||||
sandbox = SandboxFactory.create(
|
||||
SandboxType.DOCKER,
|
||||
options={"docker_image": "python:3.11-slim"},
|
||||
environments={"PATH": "/usr/local/bin"},
|
||||
)
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
|
||||
class SandboxType(StrEnum):
|
||||
"""Supported sandbox types."""
|
||||
|
||||
DOCKER = "docker"
|
||||
E2B = "e2b"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
class SandboxFactory:
|
||||
"""
|
||||
Factory for creating VirtualEnvironment (sandbox) instances.
|
||||
|
||||
Uses lazy imports to avoid loading unused providers.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
sandbox_type: SandboxType,
|
||||
options: Mapping[str, Any] | None = None,
|
||||
environments: Mapping[str, str] | None = None,
|
||||
) -> VirtualEnvironment:
|
||||
"""
|
||||
Create a VirtualEnvironment instance based on the specified type.
|
||||
|
||||
Args:
|
||||
sandbox_type: Type of sandbox to create
|
||||
options: Sandbox-specific configuration options
|
||||
environments: Environment variables to set in the sandbox
|
||||
|
||||
Returns:
|
||||
Configured VirtualEnvironment instance
|
||||
|
||||
Raises:
|
||||
ValueError: If sandbox type is not supported
|
||||
"""
|
||||
options = options or {}
|
||||
environments = environments or {}
|
||||
|
||||
sandbox_class = cls._get_sandbox_class(sandbox_type)
|
||||
return sandbox_class(options=options, environments=environments)
|
||||
|
||||
@classmethod
|
||||
def _get_sandbox_class(cls, sandbox_type: SandboxType) -> type[VirtualEnvironment]:
|
||||
"""Get the sandbox class for the specified type (lazy import)."""
|
||||
match sandbox_type:
|
||||
case SandboxType.DOCKER:
|
||||
from core.virtual_environment.providers.docker_daemon_sandbox import DockerDaemonEnvironment
|
||||
|
||||
return DockerDaemonEnvironment
|
||||
case SandboxType.E2B:
|
||||
from core.virtual_environment.providers.e2b_sandbox import E2BEnvironment
|
||||
|
||||
return E2BEnvironment
|
||||
case SandboxType.LOCAL:
|
||||
from core.virtual_environment.providers.local_without_isolation import LocalVirtualEnvironment
|
||||
|
||||
return LocalVirtualEnvironment
|
||||
case _:
|
||||
raise ValueError(f"Unsupported sandbox type: {sandbox_type}")
|
||||
Reference in New Issue
Block a user