Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/sandbox_proxy_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import asyncio
from contextlib import asynccontextmanager

from rock.actions import CreateBashSessionRequest
from rock.sdk.sandbox.client import Sandbox
from rock.sdk.sandbox.config import SandboxConfig

# =============================================================================
# Local Read/Write Separation Architecture Demo
# =============================================================================
#
# This script demonstrates how to use the proxy + admin read/write separation
# architecture locally:
#
# - Admin server (port 8080): Handles write operations (start/stop sandbox)
# - Proxy server (port 8081): Handles read/runtime operations (create session,
# execute commands, etc.)
#
# Prerequisites:
# 1. Start Redis:
# docker run -d --name redis-local -p 6379:6379 redis:latest
# 2. Start admin server:
# rock admin start --env local-proxy --role admin --port 8080
# 3. Start proxy server:
# rock admin start --env local-proxy --role proxy --port 8081
#
# NOTE: For production environments, it is recommended to add a gateway layer
# (e.g. Nginx, Kong, or a custom API gateway) to route requests
# transparently, so that clients don't need to manage URL switching
# between admin and proxy manually.
# =============================================================================


@asynccontextmanager
async def managed_sandbox(
image: str = "python:3.11",
memory: str = "2g",
cpus: float = 0.5,
):
"""Context manager that handles sandbox lifecycle with read/write separation.

Lifecycle:
1. start() -> routed to admin server (write operation)
2. yield -> url switched to proxy (read/runtime operations)
3. stop() -> url restored to admin (write operation)

Args:
image: Docker image to use for the sandbox.
memory: Memory limit for the sandbox container.
cpus: CPU limit for the sandbox container.
"""
base_url = "http://127.0.0.1:8080"
proxy_url = "http://127.0.0.1:8081/apis/envs/sandbox/v1"
admin_url = f"{base_url}/apis/envs/sandbox/v1"

config = SandboxConfig(base_url=base_url, image=image, memory=memory, cpus=cpus)
sandbox = Sandbox(config)

try:
# Write operation: create sandbox via admin server
await sandbox.start()
# Switch to proxy server for runtime operations
sandbox.url = proxy_url
yield sandbox
finally:
# Switch back to admin server for cleanup
sandbox.url = admin_url
await sandbox.stop()


async def run_sandbox():
async with managed_sandbox() as sandbox:
await sandbox.create_session(CreateBashSessionRequest(session="bash-1"))

result = await sandbox.arun(cmd="echo Hello ROCK", session="bash-1")
print(f"\n{'*' * 50}\n{result.output}\n{'*' * 50}\n")


if __name__ == "__main__":
asyncio.run(run_sandbox())
15 changes: 15 additions & 0 deletions rock-conf/rock-local-proxy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
ray:
runtime_env:
working_dir: ./
pip: ./requirements_sandbox_actor.txt
namespace: "rock-sandbox-local"

warmup:
images:
- "python:3.11"

# to start admin proxy in local, redis is required
# quickest way to start redis local: "docker run -d --name redis-dev -p 6379:6379 redis:latest"
redis:
host: localhost
port: 6379
10 changes: 8 additions & 2 deletions rock/cli/command/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ async def arun(self, args: argparse.Namespace):

async def _admin_start(self, args: argparse.Namespace):
"""Start admin service"""
env = getattr(args, "env", None)
env = getattr(args, "env", "local")
role = getattr(args, "role", "admin")
port = getattr(args, "port", 8080)

subprocess.Popen(["admin", "--env", env])
subprocess.Popen(["admin", "--env", env, "--role", role, "--port", str(port)])

async def _admin_stop(self, args: argparse.Namespace):
"""Stop admin service"""
Expand Down Expand Up @@ -112,6 +114,10 @@ async def add_parser_to(subparsers: argparse._SubParsersAction):
# admin start
admin_start_parser = admin_subparsers.add_parser("start", help="Start admin service")
admin_start_parser.add_argument("--env", default="local", help="admin service env")
admin_start_parser.add_argument(
"--role", default="admin", choices=["admin", "proxy"], help="admin service role (admin or proxy)"
)
admin_start_parser.add_argument("--port", type=int, default=8080, help="admin service port")

# admin stop
admin_subparsers.add_parser("stop", help="Stop admin service")
8 changes: 8 additions & 0 deletions rock/sdk/sandbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ def host_ip(self) -> str:
def cluster(self) -> str:
return self._cluster

@property
def url(self) -> str:
return self._url

@url.setter
def url(self, value: str):
self._url = value

def _build_headers(self) -> dict[str, str]:
"""Build basic request headers."""
headers = {
Expand Down
Loading