# Master Election Service Specification - Distributed Process Coordination ## Overview The MasterElection service implements a Redis-based distributed leadership election and process coordination system for the CMS backend cluster. This service provides robust master-slave coordination with automatic failover, process registration, and TTL-based cleanup for multi-process backend deployments. **Key Architectural Principle**: Redis-based coordination with atomic Lua scripts ensures consistency and prevents split-brain scenarios while providing automatic cleanup through per-entry TTL expiration. ## Architecture Components ### Two-Tier Process Coordination The system manages two distinct coordination layers: 1. **Master Election Layer**: Single leader election across all backend processes 2. **Process Registry Layer**: Individual process registration and heartbeat management ### Leadership Election Pattern - **Single Master**: Only one backend process holds master lock at any time - **Automatic Failover**: Master election triggers immediately when current master fails - **Heartbeat-Based**: Master must renew lock every 10 seconds or lose leadership - **Lua Script Atomicity**: All Redis operations use atomic Lua scripts to prevent race conditions - **Event-Driven Transitions**: Role changes emit events for dependent services integration ## Core Components ### MasterElection Class `cms-backend/services/MasterElection.ts` Primary coordination service that handles distributed leadership election and process lifecycle management. **Key Responsibilities:** - Manages master lock acquisition and renewal using atomic Redis operations - Provides process registration with automatic TTL-based expiration (45 seconds) - Emits role transition events for dependent service coordination - Handles slave registration and heartbeat management - Maintains process-to-channel mapping for message routing ### Process Management System **Process Registration:** - Each backend process registers with unique UUID-based identifier - Process metadata includes role, channel name, and capabilities - TTL-based expiration (45 seconds) with heartbeat renewal - Automatic cleanup of stale process entries without manual intervention **Channel Assignment:** - Each process gets assigned a unique Redis pub/sub channel - Channel mapping stored persistently for message routing - Master process maintains channel-to-process mapping ## Data Structures ### MasterElectionEvents ```typescript interface MasterElectionEvents { 'master-acquired': () => void; // This process became master 'master-lost': () => void; // This process lost master status 'election-started': () => void; // Election process initiated 'election-completed': (isMaster: boolean) => void; // Election finished 'slave-registered': (slave: SlaveNode) => void; // New slave joined 'slave-removed': (nodeId: string) => void; // Slave left/expired 'error': (error: Error) => void; // Election/coordination errors } ``` ### ProcessInfo ```typescript interface ProcessInfo { processId: string; // Unique process identifier (UUID) nodeId: string; // Node identifier (same as processId) role: 'master' | 'slave'; // Current process role lastSeen: string; // Last heartbeat timestamp (ISO string) capabilities: ProcessCapabilities; // Process feature capabilities } // Channel name derived as: `worker:slave:${processInfo.processId}` ``` ### ProcessCapabilities ```typescript interface ProcessCapabilities { canProcessDetections: boolean; // Can handle AI detection processing maxSubscriptions: number; // Maximum camera subscriptions supported preferredWorkload: number; // Preferred subscription load (0-100) } ``` ### SlaveNode ```typescript interface SlaveNode { nodeId: string; // Unique slave node identifier identifier: string; // Human-readable process identifier registeredAt: string; // Initial registration timestamp lastSeen: string; // Last heartbeat timestamp metadata?: Record; // Optional process metadata } ``` ## Redis Data Architecture ### Master Election Keys - `master-election:master` - Current master process identifier with TTL lock - `master-election:heartbeat` - Master heartbeat timestamp for liveness detection - `master-election:master_process` - Detailed master process information (JSON) ### Process Registry Keys (TTL-Enabled) - `master-election:processes` - Hash map of all active processes with per-entry TTL (45s) - Channel names derived directly from process ID: `worker:slave:{processId}` - no separate mapping needed ### TTL Configuration ```typescript // Per-entry TTL using hSetEx for automatic cleanup PROCESS_TTL = 45; // Process registration expires after 45 seconds HEARTBEAT_RENEWAL_INTERVAL = 10; // Process heartbeats renew TTL every 10 seconds MASTER_LOCK_TTL = 30; // Master lock expires after 30 seconds ``` ### Data Persistence Strategy Uses **per-entry TTL with hSetEx** for automatic cleanup: - Process entries automatically expire if heartbeats stop - No manual cleanup processes required - Prevents memory leaks from crashed processes - Self-healing system that maintains only active processes - Slave information derived from processes with role='slave' - no separate storage needed - Channel names derived directly from process ID - no mapping table required ## Master Election Algorithm ### Election Flow Diagram ```mermaid graph TB subgraph "Election Process" START[Process Starts] --> ATTEMPT[attemptElection] ATTEMPT --> ACQUIRE{acquireMasterLock} ACQUIRE -->|Success| MASTER[becomeMaster] ACQUIRE -->|Failed| SLAVE[becomeSlave] MASTER --> HEARTBEAT[startHeartbeat] SLAVE --> REGISTER[registerAsSlave] HEARTBEAT --> RENEW{renewMasterLock} RENEW -->|Success| CONTINUE[Continue as Master] RENEW -->|Failed| STEPDOWN[Step Down → SLAVE] REGISTER --> MONITOR[Monitor Master] MONITOR --> CHECK{Master Exists?} CHECK -->|Yes| WAIT[Wait and Monitor] CHECK -->|No| ATTEMPT STEPDOWN --> SLAVE WAIT --> MONITOR CONTINUE --> RENEW end subgraph "Atomic Operations" ACQUIRE --> LUA1[Lua Script: SET master NX + SET heartbeat] RENEW --> LUA2[Lua Script: Check owner + PEXPIRE + SET heartbeat] STEPDOWN --> LUA3[Lua Script: Check owner + DEL master + DEL heartbeat] end ``` ### Atomic Lock Operations #### Master Lock Acquisition ```lua -- Atomic master lock acquisition with heartbeat if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then redis.call("SET", KEYS[2], ARGV[3], "PX", ARGV[2]) return 1 else return 0 end ``` #### Master Lock Renewal ```lua -- Atomic master lock renewal with heartbeat update if redis.call("GET", KEYS[1]) == ARGV[1] then redis.call("PEXPIRE", KEYS[1], ARGV[2]) redis.call("SET", KEYS[2], ARGV[3], "PX", ARGV[2]) return 1 else return 0 end ``` #### Master Lock Release ```lua -- Atomic master lock release if redis.call("GET", KEYS[1]) == ARGV[1] then redis.call("DEL", KEYS[1], KEYS[2]) return 1 else return 0 end ``` ## Process Lifecycle Management ### Process Registration Flow ```mermaid sequenceDiagram participant P as Process participant R as Redis participant M as Master Process Note over P,M: Process Registration with TTL P->>+P: Generate UUID processId P->>+P: Determine role (master/slave) P->>+P: Assign channel name P->>+R: hSetEx(processes, processId, processInfo, {EX: 45}) R-->>-P: Registration confirmed P->>+R: hSet(channels, processId, channelName) R-->>-P: Channel mapping stored alt Process becomes master P->>+R: set(master_process, processInfo) R-->>-P: Master process registered P->>+M: emit('master-acquired') else Process becomes slave P->>+R: hSet(slaves, nodeId, slaveInfo) R-->>-P: Slave registered P->>+M: emit('slave-registered', slaveInfo) end Note over P,M: Heartbeat Loop (Every 10s) loop Every 10 seconds P->>+P: updateProcessHeartbeat(processId) P->>+R: hSetEx(processes, processId, updatedInfo, {EX: 45}) Note over R: TTL renewed for 45 seconds R-->>-P: Heartbeat recorded end Note over P,M: Automatic Expiration (No heartbeat) R->>R: 45 seconds pass without heartbeat R->>R: Process entry automatically expires Note over R: No manual cleanup needed ``` ### Master Election Scenarios #### Scenario 1: Initial Startup ```mermaid sequenceDiagram participant P1 as Process 1 participant P2 as Process 2 participant R as Redis Note over P1,R: First Process Startup P1->>+P1: attemptElection() P1->>+R: Lua Script: SET master NX R-->>-P1: Success (no existing master) P1->>+P1: becomeMaster() P1->>+P1: emit('master-acquired') P1->>+P1: startHeartbeat() every 10s Note over P1,R: Second Process Startup P2->>+P2: attemptElection() P2->>+R: Lua Script: SET master NX R-->>-P2: Failed (master exists) P2->>+P2: becomeSlave() P2->>+R: hSet(slaves, nodeId, slaveInfo) P2->>+P2: emit('election-completed', false) ``` #### Scenario 2: Master Failure and Failover ```mermaid sequenceDiagram participant P1 as Master Process participant P2 as Slave Process 1 participant P3 as Slave Process 2 participant R as Redis Note over P1,R: Normal Operation P1->>+R: Heartbeat renewal every 10s P2->>+P2: Monitor master existence every 5s P3->>+P3: Monitor master existence every 5s Note over P1,R: Master Failure P1--XP1: Process crashes/network failure Note over R: Master lock expires after 30s R->>R: Master lock TTL expires Note over P2,R: Slave Detects Missing Master P2->>+R: checkMasterExists() Lua Script R-->>-P2: Master not found or stale P2->>+P2: Random delay (0-2s) to reduce collisions P2->>+R: attemptElection() - Lua Script: SET master NX R-->>-P2: Success - became new master P2->>+P2: becomeMaster() P2->>+P2: emit('master-acquired') Note over P3,R: Other Slave Detects New Master P3->>+R: checkMasterExists() R-->>-P3: New master found P3->>+P3: Continue as slave - no election needed ``` ## TTL-Based Cleanup System ### Per-Entry TTL Implementation ```typescript // Process registration with automatic TTL expiration public async registerProcess(processInfo: ProcessInfo): Promise { // Set process registration with 45 second TTL per entry await redisClient.hSetEx( this.processesKey, { [processInfo.processId]: JSON.stringify(processInfo) }, { expiration: { type: 'EX', value: 45 // 45 second TTL per process entry } } ); // Map process to channel (no TTL - cleaned up manually) await redisClient.hSet( this.processChannelsKey, processInfo.processId, processInfo.channelName ); } // Heartbeat renewal extends TTL automatically public async updateProcessHeartbeat(processId: string): Promise { const processData = await redisClient.hGet(this.processesKey, processId); if (processData) { const processInfo: ProcessInfo = JSON.parse(processData); processInfo.lastSeen = new Date().toISOString(); // Update process and renew TTL on heartbeat (per-entry TTL) await redisClient.hSetEx( this.processesKey, { [processId]: JSON.stringify(processInfo) }, { expiration: { type: 'EX', value: 45 // Renew 45 second TTL for this specific process entry } } ); } } ``` ### Cleanup Behavior ```mermaid graph TB subgraph "TTL Cleanup Process" REG[Process Registration] --> TTL[45s TTL Set] TTL --> HB{Heartbeat Within 45s?} HB -->|Yes| RENEW[TTL Renewed to 45s] HB -->|No| EXPIRE[Entry Automatically Expires] RENEW --> HB EXPIRE --> GONE[Process Removed from Redis] GONE --> DETECT[Other Processes Detect Absence] DETECT --> REBALANCE[Automatic Rebalancing] end subgraph "Manual vs TTL Cleanup" MANUAL[Manual Cleanup Process] AUTOMATIC[TTL-Based Cleanup] MANUAL -.->|"❌ Complex"| ISSUES[Race Conditions
Memory Leaks
Stale Data] AUTOMATIC -.->|"✅ Simple"| BENEFITS[Self-Healing
No Race Conditions
Guaranteed Cleanup] end ``` ## Event System Architecture ### Event Emission Flow ```mermaid graph TD subgraph "Election Events" START[Election Started] --> ATTEMPT[Attempt Lock Acquisition] ATTEMPT --> SUCCESS{Lock Acquired?} SUCCESS -->|Yes| MASTER[Become Master] SUCCESS -->|No| SLAVE[Become Slave] MASTER --> MASTER_EVENT[emit('master-acquired')] SLAVE --> SLAVE_EVENT[emit('election-completed', false)] MASTER_EVENT --> HEARTBEAT[Start Heartbeat Loop] SLAVE_EVENT --> MONITOR[Start Master Monitoring] end subgraph "Heartbeat Events" HEARTBEAT --> RENEW{Renew Lock?} RENEW -->|Success| CONTINUE[Continue as Master] RENEW -->|Failed| LOST[emit('master-lost')] LOST --> STEPDOWN[Step Down to Slave] STEPDOWN --> TRIGGER[Trigger New Election] CONTINUE --> HEARTBEAT end subgraph "Slave Management Events" SLAVE_JOIN[New Slave Joins] --> SLAVE_REG[emit('slave-registered')] SLAVE_TIMEOUT[Slave Heartbeat Timeout] --> SLAVE_REM[emit('slave-removed')] SLAVE_REG --> NOTIFY[Notify Dependent Services] SLAVE_REM --> CLEANUP[Cleanup Assignments] end ``` ### Event Handler Integration ```typescript // Example: Camera module integration with MasterElection events const masterElection = getMasterElection(); masterElection.on('master-acquired', () => { // This process became master - start managing workers masterSlaveWorkerCluster.becomeMaster(); logger.info('Camera cluster: Became master, connecting to workers'); }); masterElection.on('master-lost', () => { // This process lost master status - become slave masterSlaveWorkerCluster.becomeSlave(); logger.info('Camera cluster: Became slave, disconnecting workers'); }); masterElection.on('slave-registered', (slave: SlaveNode) => { // New backend process joined - rebalance workload masterSlaveWorkerCluster.handleSlaveJoined(slave); logger.info(`Camera cluster: Slave ${slave.nodeId} joined`); }); masterElection.on('slave-removed', (nodeId: string) => { // Backend process left - reassign its workload masterSlaveWorkerCluster.handleSlaveLeft(nodeId); logger.info(`Camera cluster: Slave ${nodeId} removed`); }); ``` ## Process Coordination Patterns ### Master Role Responsibilities ```mermaid graph TB subgraph "Master Process Duties" LOCK[Maintain Master Lock] --> HEARTBEAT[Send Heartbeats Every 10s] HEARTBEAT --> MONITOR[Monitor All Slave Processes] MONITOR --> CLEANUP[Cleanup Stale Slave Entries] CLEANUP --> BALANCE[Coordinate Resource Balancing] BALANCE --> WORKERS[Manage Worker Connections] WORKERS --> ROUTE[Route Messages to Slaves] ROUTE --> STATUS[Provide Cluster Status] STATUS --> LOCK end subgraph "Master Failure Scenarios" NETWORK[Network Partition] --> TIMEOUT[Lock Renewal Timeout] CRASH[Process Crash] --> TIMEOUT OVERLOAD[Resource Overload] --> TIMEOUT TIMEOUT --> EXPIRE[Master Lock Expires] EXPIRE --> ELECTION[New Election Triggered] ELECTION --> RECOVER[New Master Elected] end ``` ### Slave Role Responsibilities ```mermaid graph TB subgraph "Slave Process Duties" REGISTER[Register with Master Election] --> HEARTBEAT[Send Heartbeats Every 5s] HEARTBEAT --> MONITOR[Monitor Master Existence] MONITOR --> PROCESS[Process Assigned Messages] PROCESS --> REPORT[Report Status to Master] REPORT --> DETECT{Master Missing?} DETECT -->|No| MONITOR DETECT -->|Yes| ELECTION[Trigger Election] ELECTION --> ATTEMPT{Win Election?} ATTEMPT -->|Yes| PROMOTE[Become Master] ATTEMPT -->|No| CONTINUE[Continue as Slave] PROMOTE --> MASTER[Master Role Duties] CONTINUE --> REGISTER end ``` ## Class Responsibilities Overview ### Core Class Functions | Class | Primary Responsibility | Key Methods | Process Type | |-------|----------------------|-------------|--------------| | **MasterElection** | Distributed coordination and leadership election | • `start()` - Initialize election process
• `attemptElection()` - Try to acquire master lock
• `becomeMaster()` - Transition to master role
• `becomeSlave()` - Transition to slave role
• `waitForElectionComplete()` - Synchronous election waiting | Both Master & Slave | | **Process Registry** | Process lifecycle management | • `registerProcess()` - Register with TTL
• `updateProcessHeartbeat()` - Renew TTL
• `getAllProcesses()` - Get active processes
• `getProcessesByRole()` - Filter by master/slave
• `unregisterProcess()` - Manual cleanup | Both Master & Slave | | **Master Lock Manager** | Atomic lock operations | • `acquireMasterLock()` - Lua script lock acquisition
• `renewMasterLock()` - Lua script lock renewal
• `releaseMasterLock()` - Lua script lock release
• `checkMasterExists()` - Lua script master validation | Both Master & Slave | | **Slave Management** | Slave registration and monitoring | • `registerAsSlave()` - Register as slave node
• `updateSlaveHeartbeat()` - Update slave status
• `cleanupStaleSlaves()` - Remove expired slaves
• `getSlaves()` - Get all registered slaves | Both Master & Slave | ## Object Relationship Diagrams ### Core Class Structure and Dependencies ```mermaid classDiagram class MasterElection { -nodeId: string -identifier: string -isMaster: boolean -lockTtl: number -heartbeatInterval: number +start() +stop() +getIsMaster(): boolean +getNodeId(): string +waitForElectionComplete(): Promise~boolean~ -attemptElection() -acquireMasterLock(): Promise~boolean~ -renewMasterLock(): Promise~boolean~ -releaseMasterLock() -becomeMaster() -becomeSlave() -checkMasterExists(): Promise~boolean~ } class ProcessRegistry { +registerProcess(processInfo) +updateProcessHeartbeat(processId) +getAllProcesses(): Promise~ProcessInfo[]~ +getMasterProcess(): Promise~ProcessInfo~ +getProcessesByRole(role): Promise~ProcessInfo[]~ +unregisterProcess(processId) +getProcessChannel(processId): Promise~string~ } class SlaveManagement { +registerAsSlave() +unregisterFromSlaves() +updateSlaveHeartbeat() +getSlaves(): Promise~SlaveNode[]~ +getSlave(nodeId): Promise~SlaveNode~ +getSlaveCount(): Promise~number~ -cleanupStaleSlaves() -startSlaveManagement() -stopSlaveManagement() } class EventEmitter { +on(event, listener) +emit(event, ...args) +once(event, listener) +off(event, listener) } MasterElection --|> EventEmitter : extends MasterElection --* ProcessRegistry : contains MasterElection --* SlaveManagement : contains MasterElection --> Redis : uses for coordination ProcessRegistry --> Redis : uses hSetEx for TTL SlaveManagement --> Redis : uses for slave state ``` ### Redis Operations and Key Management ```mermaid graph TB subgraph "Redis Key Structure" MASTER[master-election:master
String - Current master ID with TTL] HEARTBEAT[master-election:heartbeat
String - Master heartbeat timestamp] MASTER_PROC[master-election:master_process
String - Master ProcessInfo JSON] PROCESSES[master-election:processes
Hash - ProcessInfo with per-entry TTL] CHANNELS[master-election:channels
Hash - ProcessID → Channel mapping] SLAVES[master-election:slaves
Hash - SlaveNode data] end subgraph "Atomic Operations" LUA1[Master Acquisition
SET master NX + SET heartbeat] LUA2[Master Renewal
Check owner + PEXPIRE + SET heartbeat] LUA3[Master Release
Check owner + DEL master + heartbeat] LUA4[Master Check
GET master + GET heartbeat + validate TTL] end subgraph "TTL Operations" HSETEX1[Process Registration
hSetEx with 45s TTL per entry] HSETEX2[Heartbeat Renewal
hSetEx renews TTL to 45s] AUTO[Automatic Expiration
Redis removes expired entries] end MASTER --> LUA1 MASTER --> LUA2 MASTER --> LUA3 HEARTBEAT --> LUA1 HEARTBEAT --> LUA2 HEARTBEAT --> LUA4 PROCESSES --> HSETEX1 PROCESSES --> HSETEX2 PROCESSES --> AUTO ``` ## Method Call Flow Analysis ### Election and Role Transition Flow ```mermaid sequenceDiagram participant App as Application participant ME as MasterElection participant R as Redis participant Dep as Dependent Services Note over App,Dep: Election Initialization App->>+ME: start() ME->>+ME: attemptElection() ME->>+ME: emit('election-started') ME->>+R: Lua Script: acquireMasterLock() alt Lock acquired successfully R-->>-ME: Success (1) ME->>+ME: becomeMaster() ME->>+ME: startHeartbeat() - every 10s ME->>+ME: startSlaveManagement() ME->>+Dep: emit('master-acquired') ME->>+ME: emit('election-completed', true) else Lock acquisition failed R-->>-ME: Failed (0) ME->>+ME: becomeSlave() ME->>+R: hSet(slaves, nodeId, slaveInfo) ME->>+ME: startPeriodicCheck() - every 5s ME->>+Dep: emit('election-completed', false) end Note over App,Dep: Heartbeat and Monitoring Loop loop Every 10 seconds (Master) / 5 seconds (Slave) alt Process is Master ME->>+R: Lua Script: renewMasterLock() alt Renewal successful R-->>-ME: Success (1) ME->>+ME: Continue as master else Renewal failed R-->>-ME: Failed (0) ME->>+ME: becomeSlave() ME->>+Dep: emit('master-lost') ME->>+ME: attemptElection() after delay end else Process is Slave ME->>+R: Lua Script: checkMasterExists() alt Master exists and healthy R-->>-ME: Master found (1) ME->>+ME: Continue monitoring else No master or stale R-->>-ME: No master (0) ME->>+ME: attemptElection() with random delay end end end ``` ### Process Registration and TTL Management Flow ```mermaid sequenceDiagram participant P as Process participant ME as MasterElection participant R as Redis participant Auto as Redis TTL Note over P,Auto: Process Registration with TTL P->>+ME: registerProcess(processInfo) ME->>+R: hSetEx(processes, processId, processInfo, {EX: 45}) Note over R: Entry set with 45 second TTL R-->>-ME: Registration confirmed ME->>+R: hSet(channels, processId, channelName) R-->>-ME: Channel mapping stored alt Process is master ME->>+R: set(master_process, processInfo) R-->>-ME: Master process info stored end ME-->>-P: Registration complete Note over P,Auto: Heartbeat Loop (Every 10s) loop Every 10 seconds P->>+ME: updateProcessHeartbeat(processId) ME->>+R: hGet(processes, processId) R-->>-ME: Current process data ME->>+ME: Update lastSeen timestamp ME->>+R: hSetEx(processes, processId, updatedInfo, {EX: 45}) Note over R: TTL renewed to 45 seconds R-->>-ME: Heartbeat recorded ME-->>-P: Heartbeat updated end Note over P,Auto: Automatic TTL Expiration (No heartbeat) Note over Auto: 45 seconds pass without heartbeat Auto->>Auto: Process entry automatically expires Auto->>R: Remove expired entry from hash Note over P,Auto: Other processes detect absence P->>+ME: getAllProcesses() ME->>+R: hGetAll(processes) R-->>-ME: Only active processes returned Note over ME: Expired process not included ME-->>-P: Updated process list ``` ## System Architecture Diagrams ### Master Election Cluster Architecture ```mermaid graph TB subgraph "Backend Process Cluster" M[Master Process
Elected Leader
🏆] S1[Slave Process 1
Follower] S2[Slave Process 2
Follower] S3[Slave Process N
Follower] end subgraph "Redis Coordination Layer" R[(Redis Server)] subgraph "Election Keys" MK[master-election:master
Lock with TTL] HK[master-election:heartbeat
Timestamp] end subgraph "Process Registry (TTL)" PK[master-election:processes
Hash with per-entry TTL] CK[master-election:channels
Process→Channel mapping] end subgraph "Slave Management" SK[master-election:slaves
Slave registration data] end end subgraph "Dependent Services" CAM[Camera Module
MasterSlaveWorkerCluster] DS[Display Service
WebSocket Cluster] OTHER[Other Services
...] end M ===|Master Lock
Heartbeat Every 10s| MK M ===|Timestamp Update| HK M ===|TTL Registration
Heartbeat Renewal| PK S1 <-->|Monitor Master
Every 5s| R S2 <-->|Monitor Master
Every 5s| R S3 <-->|Monitor Master
Every 5s| R S1 ===|Slave Registration
Heartbeat Every 5s| SK S2 ===|Slave Registration
Heartbeat Every 5s| SK S3 ===|Slave Registration
Heartbeat Every 5s| SK M -.->|master-acquired
slave-registered
slave-removed| CAM M -.->|Role transition events| DS M -.->|Coordination events| OTHER S1 -.->|election-completed
master-lost| CAM S2 -.->|Election events| DS S3 -.->|Status events| OTHER ``` ### TTL-Based Cleanup Architecture ```mermaid graph TB subgraph "Process Lifecycle with TTL" START[Process Starts] --> REG[Register with 45s TTL] REG --> ACTIVE[Process Active] ACTIVE --> HB{Heartbeat?} HB -->|Every 10s| RENEW[Renew TTL to 45s] HB -->|Missed| COUNT[Count down TTL] RENEW --> ACTIVE COUNT --> EXPIRE{TTL = 0?} EXPIRE -->|No| COUNT EXPIRE -->|Yes| CLEANUP[Redis Auto-Remove] CLEANUP --> DETECT[Other Processes Detect] DETECT --> REBALANCE[Trigger Rebalancing] end subgraph "Traditional Manual Cleanup vs TTL" subgraph "❌ Manual Cleanup Problems" RACE[Race Conditions] LEAK[Memory Leaks] STALE[Stale Data] COMPLEX[Complex Logic] end subgraph "✅ TTL-Based Benefits" AUTO[Automatic Cleanup] RELIABLE[Reliable Expiration] SIMPLE[Simple Implementation] SELF[Self-Healing] end end subgraph "TTL Management Operations" HSETEX[hSetEx(key, field, value, {EX: 45})] RENEWAL[Heartbeat renews TTL automatically] EXPIRY[Redis removes expired entries] HSETEX --> RENEWAL RENEWAL --> EXPIRY EXPIRY --> HSETEX end ``` ### Election Timing and Coordination ```mermaid gantt title Master Election Timeline dateFormat X axisFormat %s section Master Lock Master Lock TTL (30s) :milestone, m1, 0, 0s Lock Renewal (10s) :10, 20s Lock Renewal (10s) :20, 30s Lock Expires :milestone, m2, 30, 30s section Process TTL Process Registration (45s) :milestone, p1, 0, 0s Heartbeat Renewal (10s) :10, 20s Heartbeat Renewal (10s) :20, 30s Heartbeat Renewal (10s) :30, 40s Process Expires :milestone, p2, 45, 45s section Election Events Initial Election :milestone, e1, 0, 0s Slave Monitoring (5s) :5, 10s Slave Monitoring (5s) :10, 15s Master Failure Detected :milestone, e2, 30, 30s New Election Started :32, 35s New Master Elected :milestone, e3, 35, 35s ``` ## Event System Architecture ### Event Flow and Dependencies ```mermaid graph TD subgraph "MasterElection Events" ES[election-started] --> EA{Election Attempt} EA -->|Success| MA[master-acquired] EA -->|Failed| EC[election-completed(false)] MA --> HB[Start Heartbeat Loop] EC --> MON[Start Master Monitoring] HB --> RENEW{Heartbeat Success?} RENEW -->|Success| CONT[Continue as Master] RENEW -->|Failed| ML[master-lost] ML --> STEP[Step Down to Slave] STEP --> MON CONT --> HB MON --> CHECK{Master Missing?} CHECK -->|Yes| ES CHECK -->|No| MON end subgraph "Slave Management Events" SR[slave-registered] --> UP[Update Assignments] SREM[slave-removed] --> CLEAN[Cleanup Assignments] UP --> NOTIFY[Notify Services] CLEAN --> REBAL[Rebalance Load] end subgraph "Error Handling Events" ERR[error] --> LOG[Log Error Details] LOG --> RECOVER[Attempt Recovery] RECOVER --> ES end subgraph "External Service Integration" MA -.->|becomeMaster()| CAMERA[Camera Module] ML -.->|becomeSlave()| CAMERA SR -.->|slaveJoined()| CAMERA SREM -.->|slaveLeft()| CAMERA MA -.->|Master role| DISPLAY[Display Service] ML -.->|Slave role| DISPLAY MA -.->|Coordinate| OTHER[Other Services] ML -.->|Follow| OTHER end ``` ### Event Sequence Patterns #### Master Failure and Recovery Pattern ```mermaid sequenceDiagram participant M as Master Process participant S1 as Slave 1 participant S2 as Slave 2 participant R as Redis participant Svc as Dependent Services Note over M,Svc: Normal Operation M->>R: Heartbeat renewal every 10s S1->>R: Monitor master every 5s S2->>R: Monitor master every 5s Note over M,Svc: Master Failure M--XM: Process crashes Note over R: Master lock expires (30s) R->>R: Lock TTL expires Note over S1,S2: Slaves detect master failure S1->>R: checkMasterExists() → false S2->>R: checkMasterExists() → false Note over S1,S2: Election race with random delay S1->>S1: Random delay 1.2s S2->>S2: Random delay 0.8s S2->>R: attemptElection() first R->>S2: Success - became master S2->>S2: emit('master-acquired') S2->>Svc: becomeMaster() event S1->>R: attemptElection() second R->>S1: Failed - master exists S1->>S1: Continue as slave Note over S2,Svc: New master operational S2->>R: Start heartbeat renewal Svc->>S2: Acknowledge new master ``` ## Configuration and Tuning ### Timing Configuration ```typescript // MasterElection constructor parameters interface MasterElectionConfig { lockName: string = 'master-election'; // Redis key prefix lockTtl: number = 30000; // Master lock TTL (30 seconds) heartbeatInterval: number = 10000; // Master heartbeat interval (10 seconds) checkInterval: number = 5000; // Slave monitoring interval (5 seconds) identifier: string = 'cms-backend'; // Human-readable process identifier } // TTL Configuration const PROCESS_TTL_SECONDS = 45; // Process registration TTL const SLAVE_TIMEOUT_MS = 15000; // Slave cleanup threshold (3x heartbeat) const ELECTION_RANDOM_DELAY_MAX = 2000; // Max random delay to prevent collisions ``` ### Redis Key Structure ```typescript // Election and coordination keys const REDIS_KEYS = { // Master election coordination master: `${lockName}:master`, // Current master ID with TTL heartbeat: `${lockName}:heartbeat`, // Master heartbeat timestamp masterProcess: `${lockName}:master_process`, // Master ProcessInfo JSON // Process registry with TTL processes: `${lockName}:processes`, // Hash: processId → ProcessInfo (TTL per entry) channels: `${lockName}:channels`, // Hash: processId → channelName // Slave management slaves: `${lockName}:slaves`, // Hash: nodeId → SlaveNode }; // TTL settings const TTL_CONFIG = { masterLock: 30, // seconds - Master lock expiration processEntry: 45, // seconds - Process registration TTL heartbeatRenewal: 10, // seconds - How often to renew heartbeats slaveMonitoring: 5, // seconds - How often slaves check master }; ``` ### Performance Characteristics #### Scalability Metrics - **Election Speed**: < 100ms for uncontested election - **Failover Time**: < 5 seconds from master failure to new election - **Process Registration**: < 10ms per process registration - **TTL Cleanup**: Automatic, no performance impact on application #### Resource Usage - **Memory**: O(n) where n = number of backend processes - **Redis Operations**: Atomic Lua scripts prevent race conditions - **Network**: Minimal - only heartbeats and election attempts - **CPU**: Negligible overhead for coordination operations #### Reliability Guarantees - **Split-Brain Prevention**: Atomic Lua scripts ensure single master - **Automatic Recovery**: TTL-based cleanup handles all failure scenarios - **Event Consistency**: All role transitions emit events for service coordination - **State Persistence**: Process registry survives Redis restarts ## Public Interface Specification The MasterElection service provides a clean, event-driven interface for distributed coordination across backend processes. ### Primary Interface: MasterElection Class #### Core Lifecycle Methods ```typescript /** * Initialize and start the master election process * @returns Promise - Resolves when election completes */ public async start(): Promise /** * Stop master election and cleanup resources * @returns Promise - Resolves when cleanup completes */ public async stop(): Promise /** * Wait for election to complete with timeout * @param timeoutMs - Maximum time to wait (default: 30000) * @returns Promise - true if became master, false if slave */ public async waitForElectionComplete(timeoutMs: number = 30000): Promise ``` #### Status and Information Methods ```typescript /** * Check if this process is currently the master * @returns boolean - true if master, false if slave */ public getIsMaster(): boolean /** * Get this process's unique node identifier * @returns string - UUID-based node identifier */ public getNodeId(): string /** * Get this process's human-readable identifier * @returns string - Process identifier (e.g., 'cms-backend') */ public getIdentifier(): string /** * Get or set process metadata for coordination * @param metadata - Optional metadata to set * @returns Record - Current metadata */ public setMetadata(metadata: Record): void public getMetadata(): Record ``` #### Process Registry Methods ```typescript /** * Register a process in the distributed registry with TTL * @param processInfo - Process information including role and capabilities * @returns Promise */ public async registerProcess(processInfo: ProcessInfo): Promise /** * Update process heartbeat to renew TTL (45 seconds) * @param processId - Process identifier to update * @returns Promise */ public async updateProcessHeartbeat(processId: string): Promise /** * Get all currently registered processes (auto-filtered by TTL) * @returns Promise - Array of active processes */ public async getAllProcesses(): Promise /** * Get current master process information * @returns Promise - Master process or null if none */ public async getMasterProcess(): Promise /** * Get processes filtered by role * @param role - 'master' or 'slave' * @returns Promise - Processes with specified role */ public async getProcessesByRole(role: 'master' | 'slave'): Promise ``` #### Slave Management Methods ```typescript /** * Get all registered slave nodes * @returns Promise - Array of active slaves */ public async getSlaves(): Promise /** * Get specific slave node information * @param nodeId - Slave node identifier * @returns Promise - Slave info or null if not found */ public async getSlave(nodeId: string): Promise /** * Get count of registered slave nodes * @returns Promise - Number of active slaves */ public async getSlaveCount(): Promise ``` ### Event System Interface #### Event Registration ```typescript // Type-safe event registration masterElection.on('master-acquired', () => { // This process became the master console.log('Became master - start coordinating resources'); }); masterElection.on('master-lost', () => { // This process lost master status console.log('Lost master status - step down to slave role'); }); masterElection.on('election-completed', (isMaster: boolean) => { // Election finished - role determined console.log(`Election completed - role: ${isMaster ? 'MASTER' : 'SLAVE'}`); }); masterElection.on('slave-registered', (slave: SlaveNode) => { // New backend process joined cluster console.log(`New slave joined: ${slave.nodeId}`); }); masterElection.on('slave-removed', (nodeId: string) => { // Backend process left cluster (TTL expired) console.log(`Slave removed: ${nodeId}`); }); masterElection.on('error', (error: Error) => { // Election or coordination error occurred console.error('Master election error:', error); }); ``` #### Event Timing Guarantees - **master-acquired**: Emitted immediately after successful lock acquisition - **master-lost**: Emitted immediately after failed lock renewal - **election-completed**: Emitted after initial election resolves (master or slave) - **slave-registered**: Emitted when new slave joins (master only) - **slave-removed**: Emitted when slave TTL expires (master only) - **error**: Emitted on Redis connection issues or election failures ### Usage Patterns #### Basic Initialization and Coordination ```typescript import { initialize, getMasterElection } from '~/services/MasterElection'; // Initialize master election with custom settings await initialize( 'cms-cluster', // lockName - Redis key prefix 30000, // lockTtl - Master lock TTL (30s) 10000, // heartbeatInterval - Master heartbeat (10s) 5000, // checkInterval - Slave monitoring (5s) 'cms-backend-prod' // identifier - Human-readable name ); // Get election instance for event handling const masterElection = getMasterElection(); // Wait for initial election to complete const isMaster = await masterElection.waitForElectionComplete(); console.log(`Process started as: ${isMaster ? 'MASTER' : 'SLAVE'}`); ``` #### Service Integration Pattern ```typescript // Camera module integration example class CameraClusterService { private masterElection: MasterElection; constructor() { this.masterElection = getMasterElection(); this.setupElectionHandlers(); } private setupElectionHandlers() { // Handle master role transitions this.masterElection.on('master-acquired', () => { this.becomeMaster(); }); this.masterElection.on('master-lost', () => { this.becomeSlave(); }); // Handle cluster membership changes this.masterElection.on('slave-registered', (slave) => { this.handleSlaveJoined(slave); }); this.masterElection.on('slave-removed', (nodeId) => { this.handleSlaveLeft(nodeId); }); } private async becomeMaster() { console.log('Camera service: Becoming master'); // Connect to all Python ML workers await this.connectToAllWorkers(); // Start managing cluster assignments this.startClusterManagement(); // Begin rebalancing subscriptions this.startRebalancing(); } private async becomeSlave() { console.log('Camera service: Becoming slave'); // Disconnect from Python workers (master-only) await this.disconnectFromWorkers(); // Stop cluster management this.stopClusterManagement(); // Start listening for routed messages this.startSlaveMessageHandling(); } } ``` #### Process Registration with Custom Capabilities ```typescript // Register this process with specific capabilities await masterElection.registerProcess({ processId: masterElection.getNodeId(), nodeId: masterElection.getNodeId(), role: masterElection.getIsMaster() ? 'master' : 'slave', channelName: `worker:slave:${masterElection.getNodeId()}`, lastSeen: new Date().toISOString(), capabilities: { canProcessDetections: true, // Can handle AI detection callbacks maxSubscriptions: 100, // Maximum camera subscriptions preferredWorkload: 80 // Preferred load percentage (0-100) } }); // Start heartbeat loop to maintain registration setInterval(async () => { await masterElection.updateProcessHeartbeat(masterElection.getNodeId()); }, 10000); // Every 10 seconds ``` #### Cluster Monitoring and Status ```typescript // Monitor cluster status and health async function monitorClusterHealth() { // Get all active processes (TTL-filtered automatically) const allProcesses = await masterElection.getAllProcesses(); console.log(`Active processes: ${allProcesses.length}`); // Get current master const masterProcess = await masterElection.getMasterProcess(); if (masterProcess) { console.log(`Master: ${masterProcess.processId} (${masterProcess.capabilities.maxSubscriptions} max subscriptions)`); } // Get all slaves const slaves = await masterElection.getSlaves(); console.log(`Slaves: ${slaves.length}`); slaves.forEach(slave => { console.log(` Slave ${slave.nodeId}: last seen ${slave.lastSeen}`); }); // Check if this process is master if (masterElection.getIsMaster()) { console.log('This process is the master - coordinating cluster'); } else { console.log('This process is a slave - following master'); } } // Run monitoring every 30 seconds setInterval(monitorClusterHealth, 30000); ``` #### Graceful Shutdown Pattern ```typescript // Graceful shutdown with proper cleanup process.on('SIGTERM', async () => { console.log('Shutting down master election...'); try { // Stop election and cleanup resources await masterElection.stop(); // Master automatically releases lock // Process TTL will expire naturally // Slaves will detect and trigger new election console.log('Master election shutdown complete'); } catch (error) { console.error('Error during election shutdown:', error); } process.exit(0); }); ``` ### Error Handling and Recovery #### Election Failure Scenarios ```typescript // Handle various failure modes masterElection.on('error', (error) => { console.error('Master election error:', error.message); // Common error types: if (error.message.includes('Redis')) { // Redis connection issues console.log('Redis connectivity problem - will retry automatically'); } else if (error.message.includes('timeout')) { // Election timeout console.log('Election timeout - may indicate network issues'); } else if (error.message.includes('lock')) { // Lock acquisition issues console.log('Lock contention - normal during elections'); } // Service continues running - election will retry automatically }); // Handle network partitions masterElection.on('master-lost', () => { console.log('Lost master status - likely network partition or overload'); // Dependent services should gracefully step down // New election will start automatically after random delay }); ``` #### Recovery Guarantees - **Split-Brain Prevention**: Atomic Lua scripts ensure only one master exists - **Automatic Failover**: New elections triggered immediately when master fails - **TTL-Based Cleanup**: Processes automatically removed when heartbeats stop - **State Recovery**: Process registry rebuilds automatically from active heartbeats - **Event Consistency**: All role changes emit events for service coordination ### Integration with Dependent Services The MasterElection service is designed to coordinate multiple backend services that need distributed leadership: #### Camera Module Integration - Master: Connects to Python ML workers, manages subscriptions - Slaves: Process routed detection messages, forward commands #### Display WebSocket Cluster - Master: Manages WebSocket connection assignments across processes - Slaves: Handle assigned display connections, route messages #### Database Migration Coordination - Master: Executes database migrations and schema changes - Slaves: Wait for master to complete before proceeding This specification provides a comprehensive understanding of the MasterElection service's distributed coordination capabilities and integration patterns for multi-process backend systems.