// Package leaderelection provides distributed leader election for multiple application instances. // It ensures only one instance acts as the leader at any given time, with automatic failover. package leaderelection import ( "context" "time" ) // LeaderElection provides distributed leader election across multiple application instances. // It uses Redis to coordinate which instance is the current leader, with automatic failover // if the leader crashes or becomes unavailable. type LeaderElection interface { // Start begins participating in leader election. // This method blocks and runs the election loop until ctx is cancelled or an error occurs. // The instance will automatically attempt to become leader and maintain leadership. Start(ctx context.Context) error // IsLeader returns true if this instance is currently the leader. // This is a local check and does not require network communication. IsLeader() bool // GetLeaderID returns the unique identifier of the current leader instance. // Returns empty string if no leader exists (should be rare). GetLeaderID() (string, error) // GetLeaderInfo returns detailed information about the current leader. GetLeaderInfo() (*LeaderInfo, error) // OnBecomeLeader registers a callback function that will be executed when // this instance becomes the leader. Multiple callbacks can be registered. OnBecomeLeader(callback func()) // OnLoseLeadership registers a callback function that will be executed when // this instance loses leadership (either voluntarily or due to failure). // Multiple callbacks can be registered. OnLoseLeadership(callback func()) // Stop gracefully stops leader election participation. // If this instance is the leader, it releases leadership allowing another instance to take over. // This should be called during application shutdown. Stop() error // GetInstanceID returns the unique identifier for this instance. GetInstanceID() string } // LeaderInfo contains information about the current leader. type LeaderInfo struct { // InstanceID is the unique identifier of the leader instance InstanceID string `json:"instance_id"` // Hostname is the hostname of the leader instance Hostname string `json:"hostname"` // StartedAt is when this instance became the leader StartedAt time.Time `json:"started_at"` // LastHeartbeat is the last time the leader renewed its lock LastHeartbeat time.Time `json:"last_heartbeat"` } // Config contains configuration for leader election. type Config struct { // RedisKeyName is the Redis key used for leader election. // Default: "maplefile:leader:lock" RedisKeyName string // RedisInfoKeyName is the Redis key used to store leader information. // Default: "maplefile:leader:info" RedisInfoKeyName string // LockTTL is how long the leader lock lasts before expiring. // The leader must renew the lock before this time expires. // Default: 10 seconds // Recommended: 10-30 seconds LockTTL time.Duration // HeartbeatInterval is how often the leader renews its lock. // This should be significantly less than LockTTL (e.g., LockTTL / 3). // Default: 3 seconds // Recommended: LockTTL / 3 HeartbeatInterval time.Duration // RetryInterval is how often followers check for leadership opportunity. // Default: 2 seconds // Recommended: 1-5 seconds RetryInterval time.Duration // InstanceID uniquely identifies this application instance. // If empty, will be auto-generated from hostname + random suffix. // Default: auto-generated InstanceID string // Hostname is the hostname of this instance. // If empty, will be auto-detected. // Default: os.Hostname() Hostname string } // DefaultConfig returns a Config with sensible defaults. func DefaultConfig() *Config { return &Config{ RedisKeyName: "maplefile:leader:lock", RedisInfoKeyName: "maplefile:leader:info", LockTTL: 10 * time.Second, HeartbeatInterval: 3 * time.Second, RetryInterval: 2 * time.Second, } } // Validate checks if the configuration is valid and returns an error if not. func (c *Config) Validate() error { if c.RedisKeyName == "" { c.RedisKeyName = "maplefile:leader:lock" } if c.RedisInfoKeyName == "" { c.RedisInfoKeyName = "maplefile:leader:info" } if c.LockTTL <= 0 { c.LockTTL = 10 * time.Second } if c.HeartbeatInterval <= 0 { c.HeartbeatInterval = 3 * time.Second } if c.RetryInterval <= 0 { c.RetryInterval = 2 * time.Second } // HeartbeatInterval should be less than LockTTL if c.HeartbeatInterval >= c.LockTTL { c.HeartbeatInterval = c.LockTTL / 3 } return nil }