Files
meta-core/internal/leader/election.go
T

139 lines
3.4 KiB
Go

package leader
import (
"fmt"
"net"
"os"
"time"
"github.com/metazla/meta-core/internal/config"
)
// LeaderLockInfo contains all URLs and metadata for the leader
type LeaderLockInfo struct {
Hostname string `json:"hostname"`
BaseUrl string `json:"baseUrl"`
ApiUrl string `json:"apiUrl"` // meta-core API (port 9000)
RedisUrl string `json:"redisUrl"`
WebdavUrl string `json:"webdavUrl"`
Timestamp int64 `json:"timestamp"`
PID int `json:"pid"`
}
// Role represents the current role of this instance
type Role string
const (
RoleUnknown Role = "unknown"
RoleLeader Role = "leader"
RoleFollower Role = "follower"
)
// RoleProvider provides role and leader info
// This interface is used by the API server to get role information
// without needing the full election logic (which is now in bash)
type RoleProvider interface {
Role() Role
LeaderInfo() *LeaderLockInfo
IsLeader() bool
}
// LocalRoleProvider provides role info for this instance
// It reads the role from environment and builds leader info locally
// This is used when running as the leader (started by leader-election.sh)
type LocalRoleProvider struct {
config *config.Config
role Role
leaderInfo *LeaderLockInfo
}
// NewLocalRoleProvider creates a RoleProvider for the local instance
// It reads META_CORE_ROLE from environment (set by leader-election.sh)
func NewLocalRoleProvider(cfg *config.Config) *LocalRoleProvider {
roleStr := os.Getenv("META_CORE_ROLE")
var role Role
switch roleStr {
case "leader":
role = RoleLeader
case "follower":
role = RoleFollower
default:
// Default to leader if not set (for backward compatibility)
role = RoleLeader
}
provider := &LocalRoleProvider{
config: cfg,
role: role,
}
// Build leader info (always, since we're the leader when running)
provider.leaderInfo = provider.buildLeaderInfo()
return provider
}
// Role returns the current role
func (p *LocalRoleProvider) Role() Role {
return p.role
}
// LeaderInfo returns the leader info
func (p *LocalRoleProvider) LeaderInfo() *LeaderLockInfo {
if p.leaderInfo == nil {
return nil
}
// Return a copy
info := *p.leaderInfo
// Update timestamp on each call
info.Timestamp = time.Now().UnixMilli()
return &info
}
// IsLeader returns true if this instance is the leader
func (p *LocalRoleProvider) IsLeader() bool {
return p.role == RoleLeader
}
// buildLeaderInfo creates leader info for this instance
func (p *LocalRoleProvider) buildLeaderInfo() *LeaderLockInfo {
ip := getLocalIP()
hostname, _ := os.Hostname()
// Use BaseURL if configured, otherwise construct from IP
baseUrl := p.config.BaseURL
if baseUrl == "" {
baseUrl = fmt.Sprintf("http://%s:%d", ip, p.config.APIPort)
}
return &LeaderLockInfo{
Hostname: hostname,
BaseUrl: baseUrl,
ApiUrl: fmt.Sprintf("http://%s:%d", ip, p.config.HTTPPort),
RedisUrl: fmt.Sprintf("redis://%s:%d", ip, p.config.RedisPort),
WebdavUrl: fmt.Sprintf("http://%s:%d/webdav", ip, p.config.APIPort),
Timestamp: time.Now().UnixMilli(),
PID: os.Getpid(),
}
}
// getLocalIP returns the local IP address
func getLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
hostname, _ := os.Hostname()
return hostname
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
hostname, _ := os.Hostname()
return hostname
}