Packages
snakepit
0.7.3
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
ARCHITECTURE.md
# Snakepit Architecture
> System Architecture for Snakepit v0.7.2
## Overview
Snakepit is a high-performance process pooler and session manager that bridges Elixir and external language runtimes (primarily Python) via gRPC. The architecture prioritizes:
- **Fault isolation**: Each worker runs in a separate OS process
- **High throughput**: Concurrent worker initialization and request distribution
- **Observability**: Comprehensive telemetry with OpenTelemetry integration
- **Clean lifecycle**: Automatic orphan cleanup and graceful shutdown
## Core Components
### 1. Application Supervisor Tree
```
Snakepit.Supervisor (one_for_one)
├── Snakepit.Bridge.SessionStore # Session management (ETS)
├── Snakepit.Bridge.ToolRegistry # Tool registration (ETS)
├── Snakepit.GRPC.Server.Supervisor # gRPC server for Python callbacks
├── Snakepit.GRPC.Client.Supervisor # gRPC client connections
├── Snakepit.Pool.Registry # Worker registration (ETS)
├── Snakepit.Pool.ProcessRegistry # External PID tracking (ETS/DETS)
├── Snakepit.Pool.Worker.StarterRegistry # Starter supervisor tracking
├── Snakepit.Telemetry.GrpcStream # Bidirectional telemetry streams
├── Snakepit.Pool # Pool GenServer
│ └── Snakepit.Pool.WorkerSupervisor (DynamicSupervisor)
│ └── Snakepit.Pool.Worker.Starter (per worker, permanent)
│ └── Snakepit.GRPCWorker (transient)
└── Snakepit.Pool.ApplicationCleanup # Shutdown guarantees
```
### 2. Pool System (`lib/snakepit/pool/`)
The pool manages worker availability and request distribution.
**Key Files:**
- `pool.ex` - Core GenServer (~1500 lines) handling:
- Worker availability tracking via MapSet
- Request queuing with priority support
- Session affinity cache
- Capacity-aware scheduling (`:pool`, `:profile`, `:hybrid` strategies)
- Statistics and metrics
**Pool State:**
```elixir
%{
name: :default,
size: 10,
workers: MapSet.t(), # All worker IDs
available: MapSet.t(), # Idle worker IDs
worker_loads: %{}, # Per-worker request counts
worker_capacities: %{}, # Per-worker thread capacity
request_queue: %{}, # Priority-keyed pending requests
stats: %{...},
initialized: boolean()
}
```
### 3. Worker Lifecycle
Workers follow a "Permanent Wrapper" supervision pattern:
```
Pool.WorkerSupervisor (DynamicSupervisor)
└── Pool.Worker.Starter (Supervisor, :permanent)
└── GRPCWorker (GenServer, :transient)
```
**Why this pattern?**
- Starter is permanent: always restarts after crash
- GRPCWorker is transient: only restarts on abnormal exit
- Pool tracks availability, not restarts
- Decouples scheduling from lifecycle management
**Worker Startup Sequence:**
1. Pool requests `WorkerSupervisor.start_worker(worker_id, config)`
2. DynamicSupervisor starts a `Worker.Starter` for this worker
3. Starter spawns `GRPCWorker` as its child
4. GRPCWorker opens OS port to spawn Python process
5. Python binds to ephemeral port, prints `GRPC_READY:{port}`
6. GRPCWorker connects to Python's gRPC server
7. Connection verified with ping, worker registered as available
### 4. gRPC Bridge (`lib/snakepit/grpc/`)
Bidirectional communication between Elixir and Python.
**Server (BridgeServer.ex):**
- Receives callbacks from Python workers
- Implements: `ping`, `execute_elixir_tool`, `register_tools`, `heartbeat`
- Routes tool calls to registered Elixir handlers
**Client (ClientImpl.ex):**
- Connects to Python worker's gRPC server
- Methods: `connect`, `ping`, `initialize_session`, `execute_tool`, `execute_streaming_tool`
- Automatic reconnection with exponential backoff
**GRPCWorker (grpc_worker.ex):**
- GenServer managing a single Python process
- Handles request/response and streaming
- Monitors Python process via port
- Emits telemetry events for observability
### 5. Session System (`lib/snakepit/bridge/`)
**SessionStore:**
- ETS-backed storage with TTL expiration
- Automatic cleanup every 60 seconds
- Default TTL: 3600 seconds (1 hour)
- Max sessions: 10,000
**Session Structure:**
```elixir
%Session{
id: "session_123",
created_at: timestamp,
last_accessed: timestamp,
last_worker_id: "worker_1", # Affinity tracking
ttl: 3600,
programs: %{}, # Session-scoped data
metadata: %{}
}
```
**ToolRegistry:**
- Registers both Elixir and Python tools
- Tool types: `:local` (Elixir) or `:remote` (Python)
- Validates tool names and metadata size
### 6. Worker Profiles (`lib/snakepit/worker_profile/`)
Two isolation strategies for different workloads:
**Process Profile (Default):**
- One Python process per worker
- Full process isolation
- Capacity: 1 concurrent request per worker
- Best for: I/O-bound tasks, stability-critical workloads
**Thread Profile (Python 3.13+):**
- ThreadPoolExecutor within each Python process
- Shared memory between threads
- Capacity: `threads_per_worker` concurrent requests
- Best for: CPU-bound tasks with shared data, memory-constrained systems
### 7. Telemetry System (`lib/snakepit/telemetry/`)
Three-layer event architecture:
**Layer 1: Infrastructure (Elixir-originated)**
- Pool: `[:snakepit, :pool, :initialized]`, `[:snakepit, :pool, :worker, :spawned]`
- Queue: `[:snakepit, :pool, :queue, :enqueued]`, `[:snakepit, :pool, :queue, :timeout]`
- Session: `[:snakepit, :session, :created]`, `[:snakepit, :session, :affinity, :assigned]`
**Layer 2: Python Execution (Folded from workers)**
- Calls: `[:snakepit, :python, :call, :start]`, `[:snakepit, :python, :call, :stop]`
- Tools: `[:snakepit, :python, :tool, :execution, :start/stop]`
- Resources: `[:snakepit, :python, :memory, :sampled]`
**Layer 3: gRPC Bridge**
- Calls: `[:snakepit, :grpc, :call, :start/stop]`
- Streams: `[:snakepit, :grpc, :stream, :opened/message/closed]`
- Connections: `[:snakepit, :grpc, :connection, :established/lost]`
**Key Modules:**
- `naming.ex` - Event catalog with atom safety validation
- `safe_metadata.ex` - Metadata sanitization to prevent atom exhaustion
- `grpc_stream.ex` - Bidirectional telemetry stream management
- `open_telemetry.ex` - OpenTelemetry integration
- `control.ex` - Runtime sampling and filtering control
### 8. Adapter System
Adapters define how to spawn external processes.
**Behavior (`lib/snakepit/adapter.ex`):**
```elixir
@callback executable_path() :: String.t() # "python3"
@callback script_path() :: String.t() # Path to gRPC server script
@callback script_args() :: [String.t()] # CLI arguments
@optional_callback command_timeout(cmd, args) :: pos_integer()
```
**GRPCPython Adapter:**
- Default Python adapter
- Discovers Python: config > env > venv > system
- Selects server script: process or threaded
- Configurable per-command timeouts
### 9. Process Management
**ProcessRegistry:**
- Tracks: worker_id ↔ elixir_pid ↔ external_pid mapping
- Persists state to DETS for crash recovery
- Generates unique BEAM run IDs
**ProcessKiller:**
- Finds orphaned Python processes by run ID
- Graceful shutdown: SIGTERM → wait → SIGKILL
- Called during application startup and script cleanup
**ApplicationCleanup:**
- Started last in supervision tree
- Ensures all workers terminate during shutdown
- Provides hard termination guarantees
## Request Flow
### Simple Execution
```
1. Snakepit.execute("cmd", args)
2. Pool.execute via GenServer.call
3. Pool checks :available set for idle worker
4. If none available, enqueue to :request_queue
5. Worker found → GRPCWorker.execute
6. gRPC call to Python worker
7. Python adapter executes tool
8. Result returned through gRPC
9. Worker returned to :available set
10. Pool dequeues pending requests
```
### Streaming Execution
```
1. Snakepit.execute_stream("cmd", args, callback)
2. Pool.checkout_worker → worker_pid
3. GRPCWorker.execute_stream opens bidirectional stream
4. Python sends ToolChunk messages
5. Each chunk passed to callback function
6. Stream closes when is_final=true
7. Pool.checkin_worker returns worker to available
```
### Session-Based Execution
```
1. Snakepit.execute_in_session("session_id", "cmd", args)
2. SessionStore checks affinity cache for last_worker_id
3. If last worker available, use it; otherwise any available
4. Execute as normal
5. Update session.last_worker_id for future affinity
```
## Configuration
### Single Pool (Simple)
```elixir
config :snakepit,
pooling_enabled: true,
adapter_module: Snakepit.Adapters.GRPCPython,
adapter_args: ["--adapter", "my_adapter"],
pool_size: 10
```
### Multi-Pool (Advanced)
```elixir
config :snakepit,
pooling_enabled: true,
pools: [
%{
name: :default,
worker_profile: :process,
pool_size: 10,
adapter_module: Snakepit.Adapters.GRPCPython,
adapter_args: ["--adapter", "general_adapter"],
worker_ttl: {3600, :seconds},
worker_max_requests: 1000
},
%{
name: :compute,
worker_profile: :thread,
pool_size: 4,
threads_per_worker: 8,
adapter_args: ["--adapter", "ml_adapter"],
capacity_strategy: :profile
}
]
```
## Python Side
**gRPC Servers (`priv/python/`):**
- `grpc_server.py` - Single-process server
- `grpc_server_threaded.py` - Multi-threaded server with ThreadPoolExecutor
**Bridge Package (`priv/python/snakepit_bridge/`):**
- `base_adapter.py` - Base class with `@tool` decorator
- `session_context.py` - Session management, Elixir tool calls
- `serialization.py` - JSON/binary serialization with orjson
- `heartbeat.py` - Health check client
- `telemetry/` - Event emission and streaming
## Design Principles
1. **Stateless Python**: All state lives in Elixir; Python workers are replaceable
2. **Layered Supervision**: Crash isolation at every level
3. **ETS for Speed**: O(1) lookups for hot paths (registry, sessions)
4. **Ephemeral Ports**: OS-assigned ports eliminate collision races
5. **Exponential Backoff**: Prevents thundering herd on reconnection
6. **Correlation IDs**: Cross-language request tracing
7. **Graceful Degradation**: Workers can be recycled without service interruption
## Performance Characteristics
- **Worker Startup**: ~500ms per worker (Python + gRPC connection)
- **Request Latency**: ~10-50ms (gRPC overhead minimal)
- **Memory**: ~1KB per OTP process + Python process memory
- **Scalability**: Tested with 250+ workers
- **Telemetry Overhead**: <10μs per event
## Error Handling
**Error Categories:**
- `:pool_error` - No workers, queue full, timeout
- `:worker_error` - Startup, connection, execution failures
- `:validation_error` - Invalid input
- `:timeout` - Operation exceeded time limit
- `:grpc_error` - Communication failures
**Timeout Hierarchy:**
| Operation | Default | Configurable |
|-----------|---------|--------------|
| Command execution | 30s | Per-command via adapter |
| Queue wait | 5s | `queue_timeout` |
| Worker startup | 10s | `worker_startup_timeout` |
| Pool initialization | 15s | `run_as_script` timeout option |
## See Also
- [DIAGRAMS.md](DIAGRAMS.md) - Visual architecture diagrams
- [README_GRPC.md](README_GRPC.md) - gRPC streaming details
- [TELEMETRY.md](TELEMETRY.md) - Complete event catalog
- [README_PROCESS_MANAGEMENT.md](README_PROCESS_MANAGEMENT.md) - Lifecycle management