Multi-Thorax Deployment
Multi-Thorax Deployment
Section titled “Multi-Thorax Deployment”Deploy multiple Thorax orchestration instances for high availability and horizontal scaling.
Overview
Section titled “Overview”Multi-Thorax deployment enables:
- High availability - Survive instance failures
- Horizontal scaling - Handle more concurrent deployments
- Load distribution - Spread work across instances
Prerequisites
Section titled “Prerequisites”Required Infrastructure
Section titled “Required Infrastructure”| Component | Purpose | Minimum Version |
|---|---|---|
| Redis | Cluster coordination | 6.0+ |
| RabbitMQ | Command distribution | 3.8+ |
| PostgreSQL | Shared state | 16+ |
Network Requirements
Section titled “Network Requirements”| Source | Destination | Port | Protocol |
|---|---|---|---|
| Thorax | Redis | 6379 | TCP/TLS |
| Thorax | RabbitMQ | 5671 | AMQPS |
| Thorax | PostgreSQL | 5432 | TCP/TLS |
| Thorax | Mandible | 50052 | gRPC/TLS |
| Tarsus | Thorax | 50051 | gRPC/mTLS |
Configuration
Section titled “Configuration”Cluster Configuration
Section titled “Cluster Configuration”Each Thorax instance requires cluster configuration:
[cluster]# Enable cluster coordinationenabled = true
# Unique instance identifier# In Kubernetes, use metadata.name from StatefulSetinstance_id = "thorax-1"
# Redis connection for cluster stateredis_url = "redis://redis:6379"
# Heartbeat frequency (seconds)heartbeat_interval_secs = 10
# Consider instance dead after this (seconds)liveness_timeout_secs = 30
# Redis key namespace (allows multi-tenant Redis)namespace = "mantis-prod"Environment Variables
Section titled “Environment Variables”# Instance identificationTHORAX__CLUSTER__ENABLED=trueTHORAX__CLUSTER__INSTANCE_ID=thorax-1THORAX__CLUSTER__REDIS_URL=redis://redis:6379THORAX__CLUSTER__NAMESPACE=mantis-prod
# TimingTHORAX__CLUSTER__HEARTBEAT_INTERVAL_SECS=10THORAX__CLUSTER__LIVENESS_TIMEOUT_SECS=30Instance ID Generation
Section titled “Instance ID Generation”If instance_id is not configured, it’s auto-generated from the hostname:
Format: thorax-{hostname}Example: thorax-k8s-worker-2Note that when multiple instances share a hostname (e.g. containers on the same node), you must set THORAX__CLUSTER__INSTANCE_ID explicitly to guarantee unique identifiers.
For predictable IDs in Kubernetes, use StatefulSet pod names:
env: - name: THORAX__CLUSTER__INSTANCE_ID valueFrom: fieldRef: fieldPath: metadata.nameRedis Cluster State
Section titled “Redis Cluster State”State Structure
Section titled “State Structure”mantis-prod:instances:index # SET of registered instance IDsmantis-prod:instances:{id} # String (JSON) with instance metadata, TTL = liveness_timeoutmantis-prod:affinity:{target_id} # String (claim info), TTL-basedmantis-prod:affinity:by-instance:{id} # SET of target IDs owned by an instancemantis-prod:deployments:{uuid} # String (JSON) with deployment state, TTL = 1h active / 24h completemantis-prod:deployments:active # SET of UUIDs of running deploymentsmantis-prod:deployments:by-instance:{id} # SET of deployment UUIDs owned by instanceInstance Registration
Section titled “Instance Registration”On startup, each Thorax instance:
-
Registers itself:
Terminal window SADD mantis-prod:instances:index thorax-1SET_EX mantis-prod:instances:thorax-1 30 '{"instance_id":"thorax-1","started_at":1705312200,...}' -
Sends heartbeats (every 10s):
The heartbeat updates the
last_heartbeattimestamp inside the instance JSON and rewrites the key with a refreshed TTL — there is no separate heartbeat key. Check liveness with:Terminal window TTL mantis-prod:instances:thorax-1 -
Loads existing sessions from Mandible
Instance Cleanup
Section titled “Instance Cleanup”When an instance stops sending heartbeats:
- Other instances detect missing heartbeat (TTL expired)
- Dead instance removed from
instancesSET - Target affinities reassigned to live instances
- In-flight deployments marked for retry
Deployment Process
Section titled “Deployment Process”Creating Instances
Section titled “Creating Instances”Docker Compose:
services: thorax-1: image: mantis/thorax:latest environment: - THORAX__CLUSTER__ENABLED=true - THORAX__CLUSTER__INSTANCE_ID=thorax-1 - THORAX__CLUSTER__REDIS_URL=redis://redis:6379 volumes: - ./certs:/certs:ro
thorax-2: image: mantis/thorax:latest environment: - THORAX__CLUSTER__ENABLED=true - THORAX__CLUSTER__INSTANCE_ID=thorax-2 - THORAX__CLUSTER__REDIS_URL=redis://redis:6379 volumes: - ./certs:/certs:roKubernetes StatefulSet:
apiVersion: apps/v1kind: StatefulSetmetadata: name: thoraxspec: replicas: 3 serviceName: thorax selector: matchLabels: app: thorax template: metadata: labels: app: thorax spec: containers: - name: thorax image: mantis/thorax:latest env: - name: THORAX__CLUSTER__ENABLED value: 'true' - name: THORAX__CLUSTER__INSTANCE_ID valueFrom: fieldRef: fieldPath: metadata.name - name: THORAX__CLUSTER__REDIS_URL valueFrom: secretKeyRef: name: thorax-secrets key: redis-url ports: - containerPort: 50051 name: grpc volumeMounts: - name: certs mountPath: /certs readOnly: trueHeadless Service for Target Connections
Section titled “Headless Service for Target Connections”Targets need direct connections to specific Thorax instances:
apiVersion: v1kind: Servicemetadata: name: thoraxspec: clusterIP: None # Headless selector: app: thorax ports: - port: 50051 name: grpcEach pod is addressable as:
thorax-0.thorax.namespace.svc.cluster.localthorax-1.thorax.namespace.svc.cluster.localthorax-2.thorax.namespace.svc.cluster.local
Scaling Operations
Section titled “Scaling Operations”Scale Up
Section titled “Scale Up”# Docker Compose - add instancesdocker-compose up -d --scale thorax=3
# Kubernetes - increase replicaskubectl scale statefulset thorax --replicas=5New instances automatically:
- Register in Redis
- Start consuming from RabbitMQ
- Accept target connections
- Participate in deployment work
Scale Down
Section titled “Scale Down”# Kubernetes - decrease replicaskubectl scale statefulset thorax --replicas=2Scale-down process:
- Instance receives SIGTERM
- Stops accepting new work
- Completes in-flight deployments (graceful period)
- Deregisters from Redis
- Target affinities migrate to remaining instances
Rolling Updates
Section titled “Rolling Updates”# Kubernetes - rolling restartkubectl rollout restart statefulset thorax
# Watch progresskubectl rollout status statefulset thoraxMonitoring
Section titled “Monitoring”Instance Health
Section titled “Instance Health”# Check active instancesredis-cli SMEMBERS mantis-prod:instances:index
# Check instance details (stored as a JSON string)redis-cli GET mantis-prod:instances:thorax-1
# Check instance liveness (TTL on the instance key; 0 or -2 means expired/gone)redis-cli TTL mantis-prod:instances:thorax-1Metrics
Section titled “Metrics”Thorax exposes its Prometheus metrics at :9090/metrics, all under the mantis_ prefix.
There are no dedicated thorax_cluster_* or thorax_heartbeat_latency_ms series; use the
per-instance series below (scrape each instance as a separate target and aggregate):
| Metric | Type | Description |
|---|---|---|
mantis_deployments_active | Gauge | Active deployments on the instance |
mantis_client_sessions_active | Gauge | Connected client sessions on the instance |
mantis_client_heartbeats_total | Counter | Client heartbeats received by the instance |
mantis_commands_dispatched_total | Counter | Commands dispatched by the instance |
Alerts
Section titled “Alerts”# Prometheus AlertManager rulesgroups: - name: thorax-cluster rules: - alert: ThoraxInstanceDown # 'up' is the Prometheus scrape-health series for the thorax job expr: count(up{job="mantis-thorax"} == 1) < 2 for: 1m labels: severity: critical annotations: summary: 'Fewer than 2 Thorax instances are being scraped successfully'
- alert: ThoraxNoActiveDeployments expr: sum(mantis_deployments_active) == 0 for: 10m labels: severity: warning annotations: summary: 'No active deployments across the Thorax cluster'Failover Behavior
Section titled “Failover Behavior”Instance Failure
Section titled “Instance Failure”Target Reconnection
Section titled “Target Reconnection”When an instance fails, targets:
- Detect connection loss
- Re-register with Mandible to obtain a new Thorax assignment
- Reconnect to the newly assigned instance
- Resume operations
Default reconnection timing (exponential backoff with jitter):
- Initial retry delay: 1s
- Backoff multiplier: ×2 per attempt
- Maximum delay cap: 30s
- Maximum retries: Unlimited (retries indefinitely)
Troubleshooting
Section titled “Troubleshooting”Instance Not Joining Cluster
Section titled “Instance Not Joining Cluster”-
Check Redis connectivity:
Terminal window redis-cli -h $REDIS_HOST ping -
Verify configuration:
Terminal window # Check instance ID is uniqueredis-cli SMEMBERS mantis-prod:instances:index -
Check logs:
Terminal window docker logs thorax-1 | grep -i cluster
Targets Not Reconnecting
Section titled “Targets Not Reconnecting”-
Check target affinity:
Terminal window redis-cli GET mantis-prod:affinity:target_id -
Verify instance is healthy:
Terminal window redis-cli TTL mantis-prod:instances:thorax-1 -
Force rebalance (if needed):
Terminal window redis-cli DEL mantis-prod:affinity:target_id
Split-Brain Concerns
Section titled “Split-Brain Concerns”Redis cluster state prevents split-brain:
- Instance registration is atomic
- Heartbeats use TTL for automatic cleanup
- Target assignments are consistent
Best Practices
Section titled “Best Practices”Production Deployment
Section titled “Production Deployment”- Minimum 2 instances for HA
- 3+ instances for production workloads
- Odd number for consensus operations
- Spread across zones for fault tolerance
Configuration
Section titled “Configuration”- Use StatefulSet for stable network identities
- Configure PodDisruptionBudget for safe updates
- Set resource limits to prevent noisy neighbors
- Enable Redis persistence for state durability
Operations
Section titled “Operations”- Monitor heartbeat latency - Early warning of issues
- Test failover regularly - Kill instances intentionally
- Document recovery procedures - Runbooks for incidents
- Automate scaling - HPA based on queue depth
Next Steps
Section titled “Next Steps”- Load Balancing - Route traffic to instances
- Competing Consumers - RabbitMQ distribution
- Instance Affinity - Target binding strategies
