* wait I forgot to comit locally * cp the entire core directory and then rm the .git subdir
243 lines
8.9 KiB
Python
243 lines
8.9 KiB
Python
"""
|
|
Modal Deployment Manager - Handles deployment orchestration with optional locking.
|
|
|
|
This module separates deployment logic from the main sandbox execution,
|
|
making it easier to understand and optionally disable locking/version tracking.
|
|
"""
|
|
|
|
import hashlib
|
|
from typing import Tuple
|
|
|
|
import modal
|
|
|
|
from letta.log import get_logger
|
|
from letta.schemas.sandbox_config import SandboxConfig
|
|
from letta.schemas.tool import Tool
|
|
from letta.services.tool_sandbox.modal_constants import VERSION_HASH_LENGTH
|
|
from letta.services.tool_sandbox.modal_version_manager import ModalVersionManager, get_version_manager
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class ModalDeploymentManager:
|
|
"""Manages Modal app deployments with optional locking and version tracking."""
|
|
|
|
def __init__(
|
|
self,
|
|
tool: Tool,
|
|
version_manager: ModalVersionManager | None = None,
|
|
use_locking: bool = True,
|
|
use_version_tracking: bool = True,
|
|
):
|
|
"""
|
|
Initialize deployment manager.
|
|
|
|
Args:
|
|
tool: The tool to deploy
|
|
version_manager: Version manager for tracking deployments (optional)
|
|
use_locking: Whether to use locking for coordinated deployments
|
|
use_version_tracking: Whether to track and reuse existing deployments
|
|
"""
|
|
self.tool = tool
|
|
self.version_manager = version_manager or get_version_manager() if (use_locking or use_version_tracking) else None
|
|
self.use_locking = use_locking
|
|
self.use_version_tracking = use_version_tracking
|
|
self._app_name = self._generate_app_name()
|
|
|
|
def _generate_app_name(self) -> str:
|
|
"""Generate app name based on tool ID."""
|
|
return self.tool.id[:40]
|
|
|
|
def calculate_version_hash(self, sbx_config: SandboxConfig) -> str:
|
|
"""Calculate version hash for the current configuration."""
|
|
components = (
|
|
self.tool.source_code,
|
|
str(self.tool.pip_requirements) if self.tool.pip_requirements else "",
|
|
str(self.tool.npm_requirements) if self.tool.npm_requirements else "",
|
|
sbx_config.fingerprint(),
|
|
)
|
|
combined = "|".join(components)
|
|
return hashlib.sha256(combined.encode()).hexdigest()[:VERSION_HASH_LENGTH]
|
|
|
|
def get_full_app_name(self, version_hash: str) -> str:
|
|
"""Get the full app name including version."""
|
|
app_full_name = f"{self._app_name}-{version_hash}"
|
|
# Ensure total length is under 64 characters
|
|
if len(app_full_name) > 63:
|
|
max_id_len = 63 - len(version_hash) - 1
|
|
app_full_name = f"{self._app_name[:max_id_len]}-{version_hash}"
|
|
return app_full_name
|
|
|
|
async def get_or_deploy_app(
|
|
self,
|
|
sbx_config: SandboxConfig,
|
|
user,
|
|
create_app_func,
|
|
) -> Tuple[modal.App, str]:
|
|
"""
|
|
Get existing app or deploy new one.
|
|
|
|
Args:
|
|
sbx_config: Sandbox configuration
|
|
user: User/actor for permissions
|
|
create_app_func: Function to create and deploy the app
|
|
|
|
Returns:
|
|
Tuple of (Modal app, version hash)
|
|
"""
|
|
version_hash = self.calculate_version_hash(sbx_config)
|
|
|
|
# Simple path: no version tracking or locking
|
|
if not self.use_version_tracking:
|
|
logger.info(f"Deploying Modal app {self._app_name} (version tracking disabled)")
|
|
app = await create_app_func(sbx_config, version_hash)
|
|
return app, version_hash
|
|
|
|
# Try to use existing deployment
|
|
if self.use_version_tracking:
|
|
existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
|
|
if existing_app:
|
|
return existing_app, version_hash
|
|
|
|
# Need to deploy - with or without locking
|
|
if self.use_locking:
|
|
return await self._deploy_with_locking(sbx_config, version_hash, user, create_app_func)
|
|
else:
|
|
return await self._deploy_without_locking(sbx_config, version_hash, user, create_app_func)
|
|
|
|
async def _try_get_existing_app(
|
|
self,
|
|
sbx_config: SandboxConfig,
|
|
version_hash: str,
|
|
user,
|
|
) -> modal.App | None:
|
|
"""Try to get an existing deployed app."""
|
|
if not self.version_manager:
|
|
return None
|
|
|
|
deployment = await self.version_manager.get_deployment(
|
|
tool_id=self.tool.id, sandbox_config_id=sbx_config.id if sbx_config else None, actor=user
|
|
)
|
|
|
|
if deployment and deployment.version_hash == version_hash:
|
|
app_full_name = self.get_full_app_name(version_hash)
|
|
logger.info(f"Checking for existing Modal app {app_full_name}")
|
|
|
|
try:
|
|
app = await modal.App.lookup.aio(app_full_name)
|
|
logger.info(f"Found existing Modal app {app_full_name}")
|
|
return app
|
|
except Exception:
|
|
logger.info(f"Modal app {app_full_name} not found in Modal, will redeploy")
|
|
return None
|
|
|
|
return None
|
|
|
|
async def _deploy_without_locking(
|
|
self,
|
|
sbx_config: SandboxConfig,
|
|
version_hash: str,
|
|
user,
|
|
create_app_func,
|
|
) -> Tuple[modal.App, str]:
|
|
"""Deploy without locking - simpler but may have race conditions."""
|
|
app_full_name = self.get_full_app_name(version_hash)
|
|
logger.info(f"Deploying Modal app {app_full_name} (no locking)")
|
|
|
|
# Deploy the app
|
|
app = await create_app_func(sbx_config, version_hash)
|
|
|
|
# Register deployment if tracking is enabled
|
|
if self.use_version_tracking and self.version_manager:
|
|
await self._register_deployment(sbx_config, version_hash, app, user)
|
|
|
|
return app, version_hash
|
|
|
|
async def _deploy_with_locking(
|
|
self,
|
|
sbx_config: SandboxConfig,
|
|
version_hash: str,
|
|
user,
|
|
create_app_func,
|
|
) -> Tuple[modal.App, str]:
|
|
"""Deploy with locking to prevent concurrent deployments."""
|
|
cache_key = f"{self.tool.id}:{sbx_config.id if sbx_config else 'default'}"
|
|
deployment_lock = self.version_manager.get_deployment_lock(cache_key)
|
|
|
|
async with deployment_lock:
|
|
# Double-check after acquiring lock
|
|
existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
|
|
if existing_app:
|
|
return existing_app, version_hash
|
|
|
|
# Check if another process is deploying
|
|
if self.version_manager.is_deployment_in_progress(cache_key, version_hash):
|
|
logger.info(f"Another process is deploying {self._app_name} v{version_hash}, waiting...")
|
|
# Release lock and wait
|
|
deployment_lock = None
|
|
|
|
# Wait for other deployment if needed
|
|
if deployment_lock is None:
|
|
success = await self.version_manager.wait_for_deployment(cache_key, version_hash, timeout=120)
|
|
if success:
|
|
existing_app = await self._try_get_existing_app(sbx_config, version_hash, user)
|
|
if existing_app:
|
|
return existing_app, version_hash
|
|
raise RuntimeError("Deployment completed but app not found")
|
|
else:
|
|
raise RuntimeError("Timeout waiting for deployment")
|
|
|
|
# We're deploying - mark as in progress
|
|
deployment_key = None
|
|
async with deployment_lock:
|
|
deployment_key = self.version_manager.mark_deployment_in_progress(cache_key, version_hash)
|
|
|
|
try:
|
|
app_full_name = self.get_full_app_name(version_hash)
|
|
logger.info(f"Deploying Modal app {app_full_name} with locking")
|
|
|
|
# Deploy the app
|
|
app = await create_app_func(sbx_config, version_hash)
|
|
|
|
# Mark deployment complete
|
|
if deployment_key:
|
|
self.version_manager.complete_deployment(deployment_key)
|
|
|
|
# Register deployment
|
|
if self.use_version_tracking:
|
|
await self._register_deployment(sbx_config, version_hash, app, user)
|
|
|
|
return app, version_hash
|
|
|
|
except Exception:
|
|
if deployment_key:
|
|
self.version_manager.complete_deployment(deployment_key)
|
|
raise
|
|
|
|
async def _register_deployment(
|
|
self,
|
|
sbx_config: SandboxConfig,
|
|
version_hash: str,
|
|
app: modal.App,
|
|
user,
|
|
):
|
|
if not self.version_manager:
|
|
return
|
|
|
|
dependencies = set()
|
|
if self.tool.pip_requirements:
|
|
dependencies.update(str(req) for req in self.tool.pip_requirements)
|
|
modal_config = sbx_config.get_modal_config()
|
|
if modal_config.pip_requirements:
|
|
dependencies.update(str(req) for req in modal_config.pip_requirements)
|
|
|
|
await self.version_manager.register_deployment(
|
|
tool_id=self.tool.id,
|
|
app_name=self._app_name,
|
|
version_hash=version_hash,
|
|
app=app,
|
|
dependencies=dependencies,
|
|
sandbox_config_id=sbx_config.id if sbx_config else None,
|
|
actor=user,
|
|
)
|