chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
from typing import Iterable
|
||||
|
||||
from mirage import Workspace, WorkspaceRunner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def new_workspace_id() -> str:
|
||||
"""Mint a fresh workspace id of the form ``ws_<16 hex chars>``.
|
||||
|
||||
Returns:
|
||||
str: opaque, URL-safe, collision-resistant id.
|
||||
"""
|
||||
return f"ws_{secrets.token_hex(8)}"
|
||||
|
||||
|
||||
class WorkspaceEntry:
|
||||
|
||||
def __init__(self, workspace_id: str, runner: WorkspaceRunner) -> None:
|
||||
self.id = workspace_id
|
||||
self.runner = runner
|
||||
self.created_at = time.time()
|
||||
|
||||
|
||||
class WorkspaceRegistry:
|
||||
"""In-memory map of workspace_id -> WorkspaceRunner.
|
||||
|
||||
Owns the lifecycle for each workspace inside the daemon process:
|
||||
register on create, drop on delete, and trip an idle-shutdown
|
||||
event when the registry empties for ``idle_grace_seconds``.
|
||||
|
||||
Threading: the underlying ``dict`` is mutated only from the FastAPI
|
||||
server loop (the same loop the registry is constructed on), so no
|
||||
external lock is required.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
idle_grace_seconds: float = 30.0,
|
||||
exit_event: asyncio.Event | None = None) -> None:
|
||||
"""Construct an empty registry.
|
||||
|
||||
Args:
|
||||
idle_grace_seconds (float): seconds to wait after the last
|
||||
workspace is removed before signalling exit. ``0``
|
||||
means exit immediately on empty.
|
||||
exit_event (asyncio.Event | None): event to set when the
|
||||
idle timer fires. Defaults to a fresh event.
|
||||
"""
|
||||
self._entries: dict[str, WorkspaceEntry] = {}
|
||||
self.idle_grace_seconds = idle_grace_seconds
|
||||
self.exit_event = exit_event or asyncio.Event()
|
||||
self._idle_task: asyncio.Task | None = None
|
||||
|
||||
def __contains__(self, workspace_id: str) -> bool:
|
||||
return workspace_id in self._entries
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._entries)
|
||||
|
||||
def get(self, workspace_id: str) -> WorkspaceEntry:
|
||||
if workspace_id not in self._entries:
|
||||
raise KeyError(workspace_id)
|
||||
return self._entries[workspace_id]
|
||||
|
||||
def list(self) -> list[WorkspaceEntry]:
|
||||
return list(self._entries.values())
|
||||
|
||||
def items(self) -> Iterable[tuple[str, WorkspaceEntry]]:
|
||||
return self._entries.items()
|
||||
|
||||
def add(self,
|
||||
workspace: Workspace,
|
||||
workspace_id: str | None = None) -> WorkspaceEntry:
|
||||
"""Wrap ``workspace`` in a runner and register it.
|
||||
|
||||
Args:
|
||||
workspace (Workspace): freshly-constructed workspace.
|
||||
workspace_id (str | None): explicit id, or None to auto-mint.
|
||||
|
||||
Returns:
|
||||
WorkspaceEntry: the registered entry.
|
||||
|
||||
Raises:
|
||||
ValueError: ``workspace_id`` is already registered.
|
||||
"""
|
||||
wid = workspace_id or new_workspace_id()
|
||||
if wid in self._entries:
|
||||
raise ValueError(f"workspace id already exists: {wid!r}")
|
||||
runner = WorkspaceRunner(workspace)
|
||||
entry = WorkspaceEntry(wid, runner)
|
||||
self._entries[wid] = entry
|
||||
self._cancel_idle_timer()
|
||||
return entry
|
||||
|
||||
async def remove(self, workspace_id: str) -> WorkspaceEntry:
|
||||
"""Stop the runner for ``workspace_id`` and drop it.
|
||||
|
||||
Args:
|
||||
workspace_id (str): id to remove.
|
||||
|
||||
Returns:
|
||||
WorkspaceEntry: the removed entry (after its runner is
|
||||
stopped).
|
||||
|
||||
Raises:
|
||||
KeyError: ``workspace_id`` is not registered.
|
||||
"""
|
||||
if workspace_id not in self._entries:
|
||||
raise KeyError(workspace_id)
|
||||
entry = self._entries.pop(workspace_id)
|
||||
await entry.runner.stop()
|
||||
if not self._entries:
|
||||
self._start_idle_timer()
|
||||
return entry
|
||||
|
||||
async def close_all(self) -> None:
|
||||
"""Stop every runner. Used at daemon shutdown."""
|
||||
self._cancel_idle_timer()
|
||||
ids = list(self._entries)
|
||||
for wid in ids:
|
||||
entry = self._entries.pop(wid)
|
||||
try:
|
||||
await entry.runner.stop()
|
||||
except Exception:
|
||||
logger.exception("error stopping runner for %s", wid)
|
||||
|
||||
def _start_idle_timer(self) -> None:
|
||||
if self.idle_grace_seconds <= 0:
|
||||
self.exit_event.set()
|
||||
return
|
||||
if self._idle_task is not None and not self._idle_task.done():
|
||||
return
|
||||
self._idle_task = asyncio.create_task(self._idle_wait())
|
||||
|
||||
def _cancel_idle_timer(self) -> None:
|
||||
if self._idle_task is not None and not self._idle_task.done():
|
||||
self._idle_task.cancel()
|
||||
self._idle_task = None
|
||||
|
||||
async def _idle_wait(self) -> None:
|
||||
try:
|
||||
await asyncio.sleep(self.idle_grace_seconds)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
if not self._entries:
|
||||
self.exit_event.set()
|
||||
Reference in New Issue
Block a user