136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
// 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
|
|
}
|