mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-22 16:06:50 +00:00
8b697245eb
* fix(sandbox): offload async sandbox acquisition Run blocking sandbox provider acquisition through the async provider hook so eager sandbox setup does not stall the event loop. * fix(sandbox): add async readiness polling Introduce an async sandbox readiness poller using httpx and asyncio.sleep while preserving the existing synchronous API. * test(sandbox): cover async readiness polling Lock in non-blocking readiness behavior so the async helper does not regress to requests.get or time.sleep. * fix(sandbox): allow anonymous backend creation * fix(sandbox): use async readiness in provider acquisition * fix(sandbox): use async acquisition for lazy tools * test(sandbox): cover anonymous remote creation * fix(sandbox): clamp async readiness timeout budget * fix(sandbox): offload async lock file handling * fix(sandbox): delegate async middleware fallthrough * docs(sandbox): document async acquisition path * fix(sandbox): offload async sandbox release * docs(sandbox): mention async release hook * fix(sandbox): address async lock review Reduce duplicate sync/async sandbox acquisition state handling and move async thread-lock waits onto a dedicated executor with cancellation-safe cleanup. * chore: retrigger ci Retrigger GitHub Actions after upstream main fixed the stale PR merge lint failure. * test(sandbox): sync backend unit fixtures --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
import asyncio
|
|
from abc import ABC, abstractmethod
|
|
|
|
from deerflow.config import get_app_config
|
|
from deerflow.reflection import resolve_class
|
|
from deerflow.sandbox.sandbox import Sandbox
|
|
|
|
|
|
class SandboxProvider(ABC):
|
|
"""Abstract base class for sandbox providers"""
|
|
|
|
uses_thread_data_mounts: bool = False
|
|
|
|
@abstractmethod
|
|
def acquire(self, thread_id: str | None = None) -> str:
|
|
"""Acquire a sandbox environment and return its ID.
|
|
|
|
Returns:
|
|
The ID of the acquired sandbox environment.
|
|
"""
|
|
pass
|
|
|
|
async def acquire_async(self, thread_id: str | None = None) -> str:
|
|
"""Acquire a sandbox without blocking the event loop.
|
|
|
|
Most sandbox providers expose a synchronous lifecycle API because local
|
|
Docker/provisioner operations are blocking. Async runtimes should call
|
|
this method so those blocking operations run in a worker thread instead
|
|
of stalling the event loop.
|
|
"""
|
|
return await asyncio.to_thread(self.acquire, thread_id)
|
|
|
|
@abstractmethod
|
|
def get(self, sandbox_id: str) -> Sandbox | None:
|
|
"""Get a sandbox environment by ID.
|
|
|
|
Args:
|
|
sandbox_id: The ID of the sandbox environment to retain.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def release(self, sandbox_id: str) -> None:
|
|
"""Release a sandbox environment.
|
|
|
|
Args:
|
|
sandbox_id: The ID of the sandbox environment to destroy.
|
|
"""
|
|
pass
|
|
|
|
def reset(self) -> None:
|
|
"""Clear cached state that survives provider instance replacement."""
|
|
pass
|
|
|
|
|
|
_default_sandbox_provider: SandboxProvider | None = None
|
|
|
|
|
|
def get_sandbox_provider(**kwargs) -> SandboxProvider:
|
|
"""Get the sandbox provider singleton.
|
|
|
|
Returns a cached singleton instance. Use `reset_sandbox_provider()` to clear
|
|
the cache, or `shutdown_sandbox_provider()` to properly shutdown and clear.
|
|
|
|
Returns:
|
|
A sandbox provider instance.
|
|
"""
|
|
global _default_sandbox_provider
|
|
if _default_sandbox_provider is None:
|
|
config = get_app_config()
|
|
cls = resolve_class(config.sandbox.use, SandboxProvider)
|
|
_default_sandbox_provider = cls(**kwargs)
|
|
return _default_sandbox_provider
|
|
|
|
|
|
def reset_sandbox_provider() -> None:
|
|
"""Reset the sandbox provider singleton.
|
|
|
|
This clears the cached instance without calling shutdown.
|
|
The next call to `get_sandbox_provider()` will create a new instance.
|
|
Useful for testing or when switching configurations.
|
|
|
|
Providers can override `reset()` to clear any module-level state they keep
|
|
alive across instances (for example, `LocalSandboxProvider`'s cached
|
|
`LocalSandbox` singleton). Without it, config/mount changes would not take
|
|
effect on the next acquire().
|
|
|
|
Note: If the provider has active sandboxes, they will be orphaned.
|
|
Use `shutdown_sandbox_provider()` for proper cleanup.
|
|
"""
|
|
global _default_sandbox_provider
|
|
if _default_sandbox_provider is not None:
|
|
_default_sandbox_provider.reset()
|
|
_default_sandbox_provider = None
|
|
|
|
|
|
def shutdown_sandbox_provider() -> None:
|
|
"""Shutdown and reset the sandbox provider.
|
|
|
|
This properly shuts down the provider (releasing all sandboxes)
|
|
before clearing the singleton. Call this when the application
|
|
is shutting down or when you need to completely reset the sandbox system.
|
|
"""
|
|
global _default_sandbox_provider
|
|
if _default_sandbox_provider is not None:
|
|
if hasattr(_default_sandbox_provider, "shutdown"):
|
|
_default_sandbox_provider.shutdown()
|
|
_default_sandbox_provider = None
|
|
|
|
|
|
def set_sandbox_provider(provider: SandboxProvider) -> None:
|
|
"""Set a custom sandbox provider instance.
|
|
|
|
This allows injecting a custom or mock provider for testing purposes.
|
|
|
|
Args:
|
|
provider: The SandboxProvider instance to use.
|
|
"""
|
|
global _default_sandbox_provider
|
|
_default_sandbox_provider = provider
|