monorepo/cloud/maplefile-backend/pkg/leaderelection/interface.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
}