Cluster Coordination
Cluster Coordination
Section titled “Cluster Coordination”Coordinate multiple Thorax instances using Redis for instance registry, target affinity, and deployment state.
Overview
Section titled “Overview”Instance Registry
Section titled “Instance Registry”Purpose
Section titled “Purpose”The instance registry tracks all live Thorax instances in the cluster:
- Discover other instances for routing
- Detect failed instances for cleanup
- Coordinate target ownership handoff
Redis Keys
Section titled “Redis Keys”| Key Pattern | Type | TTL | Description |
|---|---|---|---|
{ns}:instances:{id} | String (JSON) | liveness_timeout | Instance info |
{ns}:instances:index | Set | None | All instance IDs |
Instance Info Structure
Section titled “Instance Info Structure”{ "instance_id": "thorax-node-1", "hostname": "thorax-1.mantis.local", "grpc_address": "10.0.0.1:50051", "started_at": 1704067200, "last_heartbeat": 1704067260, "version": "0.1.0"}Registration Flow
Section titled “Registration Flow”Configuration
Section titled “Configuration”[cluster]enabled = trueredis_url = "redis://:password@redis:6379"namespace = "thorax"
# Heartbeat interval (default: 10s)heartbeat_interval_secs = 10
# TTL for instance keys (default: 30s)# Should be > 2x heartbeat_intervalliveness_timeout_secs = 30
# Instance ID (default: auto-generated)# instance_id = "thorax-node-1"Automatic Instance ID
Section titled “Automatic Instance ID”If not configured, instance ID is generated from:
THORAX__CLUSTER__INSTANCE_IDenvironment variable- System hostname prefixed with
thorax-(e.g.thorax-myhost) - The literal string
thorax(last-resort fallback)
For predictable IDs in Kubernetes:
env: - name: THORAX__CLUSTER__INSTANCE_ID valueFrom: fieldRef: fieldPath: metadata.nameTarget Affinity
Section titled “Target Affinity”Purpose
Section titled “Purpose”Target affinity ensures each listen-mode target is owned by exactly one Thorax instance:
- Prevents duplicate command dispatch
- Enables efficient direct routing
- Handles graceful ownership transfer
Redis Keys
Section titled “Redis Keys”| Key Pattern | Type | TTL | Description |
|---|---|---|---|
{ns}:affinity:{target_id} | String | liveness_timeout | Claim info |
{ns}:affinity:by-instance:{id} | Set | liveness_timeout | Targets owned by instance |
Claim Protocol
Section titled “Claim Protocol”Claim Value Format
Section titled “Claim Value Format”{instance_id}:{claim_id}The claim_id is a UUID that acts as a fencing token to detect stale claims.
Ownership Transfer
Section titled “Ownership Transfer”When a Thorax instance fails:
- Redis TTL expires on affinity keys
- Target reconnects to any available Thorax
- New Thorax claims the target
- Old affinity is cleaned up
Query Target Owner
Section titled “Query Target Owner”# Find which instance owns a targetredis-cli GET "thorax:affinity:42"# Result: thorax-1:550e8400-e29b-41d4-a716-446655440000
# List all targets owned by an instanceredis-cli SMEMBERS "thorax:affinity:by-instance:thorax-1"# Result: 42, 43, 44Deployment State
Section titled “Deployment State”Purpose
Section titled “Purpose”Track deployment progress across Thorax instances:
- Resume deployments after failover
- Coordinate multi-target deployments
- Share step completion status
Redis Keys
Section titled “Redis Keys”| Key Pattern | Type | TTL | Description |
|---|---|---|---|
{ns}:deployment:{id}:state | Hash | 24h | Deployment progress |
{ns}:deployment:{id}:targets | Set | 24h | Involved targets |
{ns}:deployment:{id}:step:{n} | Hash | 24h | Step results |
Deployment State Structure
Section titled “Deployment State Structure”HSET thorax:deployment:abc123:state status "in_progress" current_step 2 total_steps 5 started_at 1704067200 last_update 1704067260 owning_instance "thorax-1"Step Results
Section titled “Step Results”HSET thorax:deployment:abc123:step:1 target_42 "completed" target_43 "completed" target_44 "failed"Deployment Handoff
Section titled “Deployment Handoff”When the owning Thorax fails:
- Another instance detects stale deployment
- Takes ownership via atomic compare-and-set
- Resumes from last checkpoint
# Atomically claim deployment ownershipredis-cli WATCH "thorax:deployment:abc123:state"# Check owning_instance is staleredis-cli MULTIredis-cli HSET "thorax:deployment:abc123:state" owning_instance "thorax-2"redis-cli EXECHealth Monitoring
Section titled “Health Monitoring”Cluster Health Check
Section titled “Cluster Health Check”Thorax monitors cluster health and reports via health endpoint:
The HTTP health endpoint is served on port 9090 (port 50051 is the gRPC port):
curl -s http://localhost:9090/health | jq{ "status": "healthy", "instance_id": "thorax-node-1", "version": "0.1.0", "cluster": { "healthy": true, "active_instances": 3, "active_deployments": 5 }}Detecting Failed Instances
Section titled “Detecting Failed Instances”# List all registered instancesredis-cli SMEMBERS "thorax:instances:index"
# Check if instance is still alive (key exists)redis-cli EXISTS "thorax:instances:thorax-node-1"
# List instances with expired keys (stale index entries)for id in $(redis-cli SMEMBERS "thorax:instances:index"); do if [ "$(redis-cli EXISTS "thorax:instances:$id")" = "0" ]; then echo "Stale: $id" fidoneCleanup Script
Section titled “Cleanup Script”#!/bin/bashNAMESPACE="thorax"
# Get all instance IDsINSTANCES=$(redis-cli SMEMBERS "${NAMESPACE}:instances:index")
for id in $INSTANCES; do # Check if instance key exists EXISTS=$(redis-cli EXISTS "${NAMESPACE}:instances:${id}")
if [ "$EXISTS" = "0" ]; then echo "Removing stale instance: $id"
# Remove from index redis-cli SREM "${NAMESPACE}:instances:index" "$id"
# Clean up affinity keys TARGETS=$(redis-cli SMEMBERS "${NAMESPACE}:affinity:by-instance:${id}") for target in $TARGETS; do redis-cli DEL "${NAMESPACE}:affinity:${target}" done redis-cli DEL "${NAMESPACE}:affinity:by-instance:${id}" fidoneGraceful Shutdown
Section titled “Graceful Shutdown”Deregistration
Section titled “Deregistration”On shutdown, Thorax:
- Stops accepting new connections
- Releases target affinity claims
- Deregisters from instance registry
- Allows in-flight operations to complete
Split-Brain Prevention
Section titled “Split-Brain Prevention”Fencing Tokens
Section titled “Fencing Tokens”Each affinity claim includes a unique claim_id:
thorax-1:550e8400-e29b-41d4-a716-446655440000This prevents split-brain scenarios where:
- Thorax 1 claims target
- Thorax 1 loses Redis connectivity
- TTL expires
- Thorax 2 claims target
- Thorax 1 reconnects (stale claim)
The claim_id ensures only the current owner can release the claim.
Network Partition Handling
Section titled “Network Partition Handling”If a Thorax instance is partitioned from Redis:
- Heartbeats fail
- Instance key TTL expires
- Affinity claims expire
- Instance switches to degraded mode
- Stops accepting new listen-mode connections
- Continues processing existing work
Monitoring
Section titled “Monitoring”Key Metrics
Section titled “Key Metrics”| Metric | Query | Alert Threshold |
|---|---|---|
| Active instances | SCARD thorax:instances:index | < expected |
| Owned targets | SCARD thorax:affinity:by-instance:* | Varies |
| Orphaned targets | Affinity keys with dead owners | > 0 |
Prometheus Integration
Section titled “Prometheus Integration”Thorax does not currently expose cluster-specific Prometheus metrics (no mantis_cluster_*
series are registered). Observe cluster state via the health endpoint
(GET :9090/health, shown above) and the component logs.
Troubleshooting
Section titled “Troubleshooting”Instance Not Appearing in Registry
Section titled “Instance Not Appearing in Registry”-
Check cluster coordination is active (the
clusterobject is present only when clustering is enabled):Terminal window curl -s http://localhost:9090/health | jq '.cluster' -
Verify Redis connectivity:
Terminal window redis-cli -a password ping -
Check for registration errors in logs:
Terminal window docker logs thorax 2>&1 | grep -i "registry\|cluster"
Target Not Being Claimed
Section titled “Target Not Being Claimed”-
Check existing claim:
Terminal window redis-cli GET "thorax:affinity:42" -
Check claim TTL:
Terminal window redis-cli TTL "thorax:affinity:42" -
Force release (emergency only):
Terminal window redis-cli DEL "thorax:affinity:42"
Stale Instance Entries
Section titled “Stale Instance Entries”-
List stale entries:
Terminal window for id in $(redis-cli SMEMBERS "thorax:instances:index"); doif [ "$(redis-cli EXISTS "thorax:instances:$id")" = "0" ]; thenecho "Stale: $id"fidone -
Clean up manually:
Terminal window redis-cli SREM "thorax:instances:index" "stale-instance-id"
Next Steps
Section titled “Next Steps”- Session Management - SSE session handling
- Redis Setup - Redis installation
- Instance Affinity - Scaling considerations
