change election leader mechanism
This commit is contained in:
+49
-413
@@ -1,18 +1,9 @@
|
||||
package leader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/metazla/meta-core/internal/config"
|
||||
@@ -38,449 +29,94 @@ const (
|
||||
RoleFollower Role = "follower"
|
||||
)
|
||||
|
||||
// Election handles leader election using flock on shared filesystem
|
||||
type Election struct {
|
||||
config *config.Config
|
||||
// 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
|
||||
}
|
||||
|
||||
lockFile *os.File
|
||||
// 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
|
||||
mu sync.RWMutex
|
||||
|
||||
redisManager *RedisManager
|
||||
storage StorageConnector
|
||||
|
||||
// Callbacks
|
||||
onBecomeLeader func()
|
||||
onBecomeFollower func(info *LeaderLockInfo)
|
||||
onLeaderLost func()
|
||||
|
||||
// Lifecycle
|
||||
stopChan chan struct{}
|
||||
isShuttingDown bool
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// StorageConnector interface for connecting to Redis
|
||||
type StorageConnector interface {
|
||||
Connect(url string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// NewElection creates a new leader election instance
|
||||
func NewElection(cfg *config.Config) *Election {
|
||||
return &Election{
|
||||
config: cfg,
|
||||
role: RoleUnknown,
|
||||
redisManager: NewRedisManager(cfg),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SetStorageConnector sets the storage connector for Redis connections
|
||||
func (e *Election) SetStorageConnector(s StorageConnector) {
|
||||
e.storage = s
|
||||
}
|
||||
|
||||
// OnBecomeLeader sets callback for when this instance becomes leader
|
||||
func (e *Election) OnBecomeLeader(fn func()) {
|
||||
e.onBecomeLeader = fn
|
||||
}
|
||||
|
||||
// OnBecomeFollower sets callback for when this instance becomes follower
|
||||
func (e *Election) OnBecomeFollower(fn func(info *LeaderLockInfo)) {
|
||||
e.onBecomeFollower = fn
|
||||
}
|
||||
|
||||
// OnLeaderLost sets callback for when leadership is lost
|
||||
func (e *Election) OnLeaderLost(fn func()) {
|
||||
e.onLeaderLost = fn
|
||||
}
|
||||
|
||||
// Start begins the leader election process
|
||||
func (e *Election) Start() error {
|
||||
log.Println("[Election] Starting leader election...")
|
||||
|
||||
// Ensure lock directory exists
|
||||
lockDir := filepath.Dir(e.config.LockFilePath())
|
||||
if err := os.MkdirAll(lockDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create lock directory: %w", err)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Try to acquire the lock
|
||||
acquired, err := e.tryAcquireLock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to acquire lock: %w", err)
|
||||
provider := &LocalRoleProvider{
|
||||
config: cfg,
|
||||
role: role,
|
||||
}
|
||||
|
||||
if acquired {
|
||||
if err := e.transitionToLeader(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := e.transitionToFollower(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Build leader info (always, since we're the leader when running)
|
||||
provider.leaderInfo = provider.buildLeaderInfo()
|
||||
|
||||
// Start health check loop
|
||||
e.wg.Add(1)
|
||||
go e.healthCheckLoop()
|
||||
|
||||
return nil
|
||||
return provider
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the election
|
||||
func (e *Election) Stop() error {
|
||||
log.Println("[Election] Stopping leader election...")
|
||||
e.isShuttingDown = true
|
||||
close(e.stopChan)
|
||||
|
||||
e.wg.Wait()
|
||||
|
||||
// Stop Redis if we were leader
|
||||
if e.Role() == RoleLeader {
|
||||
if err := e.redisManager.Stop(); err != nil {
|
||||
log.Printf("[Election] Error stopping Redis: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Release the lock
|
||||
e.releaseLock()
|
||||
|
||||
return nil
|
||||
// Role returns the current role
|
||||
func (p *LocalRoleProvider) Role() Role {
|
||||
return p.role
|
||||
}
|
||||
|
||||
// Role returns the current role (thread-safe)
|
||||
func (e *Election) Role() Role {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
return e.role
|
||||
}
|
||||
|
||||
// LeaderInfo returns the current leader info (thread-safe)
|
||||
func (e *Election) LeaderInfo() *LeaderLockInfo {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
if e.leaderInfo == nil {
|
||||
// LeaderInfo returns the leader info
|
||||
func (p *LocalRoleProvider) LeaderInfo() *LeaderLockInfo {
|
||||
if p.leaderInfo == nil {
|
||||
return nil
|
||||
}
|
||||
// Return a copy
|
||||
info := *e.leaderInfo
|
||||
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 (e *Election) IsLeader() bool {
|
||||
return e.Role() == RoleLeader
|
||||
}
|
||||
|
||||
// tryAcquireLock attempts to acquire exclusive flock on the lock file
|
||||
func (e *Election) tryAcquireLock() (bool, error) {
|
||||
lockPath := e.config.LockFilePath()
|
||||
|
||||
// Open or create the lock file
|
||||
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open lock file: %w", err)
|
||||
}
|
||||
|
||||
// Try non-blocking exclusive flock
|
||||
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
if err == syscall.EWOULDBLOCK {
|
||||
log.Println("[Election] Lock is held by another process")
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("flock failed: %w", err)
|
||||
}
|
||||
|
||||
e.lockFile = f
|
||||
log.Printf("[Election] Acquired flock on %s", lockPath)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// releaseLock releases the flock
|
||||
func (e *Election) releaseLock() {
|
||||
if e.lockFile != nil {
|
||||
syscall.Flock(int(e.lockFile.Fd()), syscall.LOCK_UN)
|
||||
e.lockFile.Close()
|
||||
e.lockFile = nil
|
||||
log.Println("[Election] Released flock")
|
||||
}
|
||||
}
|
||||
|
||||
// transitionToLeader handles becoming the leader
|
||||
func (e *Election) transitionToLeader() error {
|
||||
log.Println("[Election] Transitioning to LEADER role")
|
||||
|
||||
e.mu.Lock()
|
||||
e.role = RoleLeader
|
||||
e.mu.Unlock()
|
||||
|
||||
// Start Redis
|
||||
if err := e.redisManager.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start Redis: %w", err)
|
||||
}
|
||||
|
||||
// Wait for Redis to be ready
|
||||
if err := e.redisManager.WaitForReady(30 * time.Second); err != nil {
|
||||
return fmt.Errorf("Redis not ready: %w", err)
|
||||
}
|
||||
|
||||
// Build and write leader info
|
||||
info := e.buildLeaderInfo()
|
||||
e.mu.Lock()
|
||||
e.leaderInfo = info
|
||||
e.mu.Unlock()
|
||||
|
||||
if err := e.writeLeaderInfo(info); err != nil {
|
||||
return fmt.Errorf("failed to write leader info: %w", err)
|
||||
}
|
||||
|
||||
// Connect storage to local Redis
|
||||
if e.storage != nil {
|
||||
if err := e.storage.Connect(info.RedisUrl); err != nil {
|
||||
log.Printf("[Election] Warning: failed to connect storage: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify callback
|
||||
if e.onBecomeLeader != nil {
|
||||
e.onBecomeLeader()
|
||||
}
|
||||
|
||||
log.Println("[Election] Now acting as LEADER")
|
||||
return nil
|
||||
}
|
||||
|
||||
// transitionToFollower handles becoming a follower
|
||||
func (e *Election) transitionToFollower() error {
|
||||
log.Println("[Election] Transitioning to FOLLOWER role")
|
||||
|
||||
e.mu.Lock()
|
||||
e.role = RoleFollower
|
||||
e.mu.Unlock()
|
||||
|
||||
// Read leader info
|
||||
info, err := e.readLeaderInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read leader info: %w", err)
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.leaderInfo = info
|
||||
e.mu.Unlock()
|
||||
|
||||
if info != nil {
|
||||
log.Printf("[Election] Leader is at %s (Redis: %s)", info.ApiUrl, info.RedisUrl)
|
||||
|
||||
// Connect storage to leader's Redis
|
||||
if e.storage != nil {
|
||||
if err := e.storage.Connect(info.RedisUrl); err != nil {
|
||||
log.Printf("[Election] Warning: failed to connect to leader: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify callback
|
||||
if e.onBecomeFollower != nil {
|
||||
e.onBecomeFollower(info)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[Election] Now acting as FOLLOWER")
|
||||
return nil
|
||||
func (p *LocalRoleProvider) IsLeader() bool {
|
||||
return p.role == RoleLeader
|
||||
}
|
||||
|
||||
// buildLeaderInfo creates leader info for this instance
|
||||
func (e *Election) buildLeaderInfo() *LeaderLockInfo {
|
||||
func (p *LocalRoleProvider) buildLeaderInfo() *LeaderLockInfo {
|
||||
ip := getLocalIP()
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
// Use BaseURL if configured, otherwise construct from IP
|
||||
baseUrl := e.config.BaseURL
|
||||
baseUrl := p.config.BaseURL
|
||||
if baseUrl == "" {
|
||||
baseUrl = fmt.Sprintf("http://%s:%d", ip, e.config.APIPort)
|
||||
baseUrl = fmt.Sprintf("http://%s:%d", ip, p.config.APIPort)
|
||||
}
|
||||
|
||||
return &LeaderLockInfo{
|
||||
Hostname: hostname,
|
||||
BaseUrl: baseUrl,
|
||||
ApiUrl: fmt.Sprintf("http://%s:%d", ip, e.config.HTTPPort),
|
||||
RedisUrl: fmt.Sprintf("redis://%s:%d", ip, e.config.RedisPort),
|
||||
WebdavUrl: fmt.Sprintf("http://%s:%d/webdav", ip, e.config.APIPort),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
// writeLeaderInfo atomically writes leader API URL to file (plain text)
|
||||
// Only writes if the content has changed to avoid triggering file watchers unnecessarily
|
||||
func (e *Election) writeLeaderInfo(info *LeaderLockInfo) error {
|
||||
infoPath := e.config.InfoFilePath()
|
||||
|
||||
// Write just the API URL as plain text
|
||||
newData := []byte(info.ApiUrl)
|
||||
|
||||
// Check if file already has the same content
|
||||
existingData, err := os.ReadFile(infoPath)
|
||||
if err == nil && string(existingData) == string(newData) {
|
||||
// Content unchanged, skip write to avoid triggering file watchers
|
||||
return nil
|
||||
}
|
||||
|
||||
// Content changed or file doesn't exist, write atomically
|
||||
tempPath := infoPath + ".tmp"
|
||||
if err := os.WriteFile(tempPath, newData, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tempPath, infoPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[Election] Wrote leader API URL to %s: %s", infoPath, info.ApiUrl)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLeaderInfo reads leader API URL from file and fetches full info via /urls API
|
||||
func (e *Election) readLeaderInfo() (*LeaderLockInfo, error) {
|
||||
infoPath := e.config.InfoFilePath()
|
||||
|
||||
data, err := os.ReadFile(infoPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// File contains plain text API URL
|
||||
apiUrl := strings.TrimSpace(string(data))
|
||||
if apiUrl == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Fetch full info from /urls API
|
||||
return e.fetchLeaderInfoFromAPI(apiUrl)
|
||||
}
|
||||
|
||||
// fetchLeaderInfoFromAPI calls the /urls API to get full leader info
|
||||
func (e *Election) fetchLeaderInfoFromAPI(apiUrl string) (*LeaderLockInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl+"/urls", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call /urls API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("/urls API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Parse URLsResponse format
|
||||
var urlsResp struct {
|
||||
Hostname string `json:"hostname"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
ApiUrl string `json:"apiUrl"`
|
||||
RedisUrl string `json:"redisUrl"`
|
||||
WebdavUrl string `json:"webdavUrl"`
|
||||
IsLeader bool `json:"isLeader"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &urlsResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse /urls response: %w", err)
|
||||
}
|
||||
|
||||
// Convert to LeaderLockInfo
|
||||
return &LeaderLockInfo{
|
||||
Hostname: urlsResp.Hostname,
|
||||
BaseUrl: urlsResp.BaseUrl,
|
||||
ApiUrl: urlsResp.ApiUrl,
|
||||
RedisUrl: urlsResp.RedisUrl,
|
||||
WebdavUrl: urlsResp.WebdavUrl,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
PID: 0, // Unknown for remote leader
|
||||
}, nil
|
||||
}
|
||||
|
||||
// healthCheckLoop periodically checks health and updates timestamps
|
||||
func (e *Election) healthCheckLoop() {
|
||||
defer e.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(time.Duration(e.config.HealthCheckIntervalMS) * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-e.stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
e.performHealthCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performHealthCheck checks health based on current role
|
||||
func (e *Election) performHealthCheck() {
|
||||
switch e.Role() {
|
||||
case RoleLeader:
|
||||
// Update timestamp in leader info
|
||||
e.updateLeaderTimestamp()
|
||||
|
||||
// Check if Redis is still running
|
||||
if !e.redisManager.IsRunning() {
|
||||
log.Println("[Election] Redis not running, attempting restart...")
|
||||
if err := e.redisManager.Start(); err != nil {
|
||||
log.Printf("[Election] Failed to restart Redis: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case RoleFollower:
|
||||
// Re-read leader info in case it changed
|
||||
info, err := e.readLeaderInfo()
|
||||
if err != nil {
|
||||
log.Printf("[Election] Failed to read leader info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if info != nil {
|
||||
e.mu.Lock()
|
||||
e.leaderInfo = info
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateLeaderTimestamp updates the timestamp in leader info
|
||||
func (e *Election) updateLeaderTimestamp() {
|
||||
e.mu.Lock()
|
||||
if e.leaderInfo != nil {
|
||||
e.leaderInfo.Timestamp = time.Now().UnixMilli()
|
||||
info := *e.leaderInfo
|
||||
e.mu.Unlock()
|
||||
|
||||
if err := e.writeLeaderInfo(&info); err != nil {
|
||||
log.Printf("[Election] Failed to update timestamp: %v", err)
|
||||
}
|
||||
} else {
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// getLocalIP returns the local IP address
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
package leader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/metazla/meta-core/internal/config"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisManager handles spawning and managing the Redis process
|
||||
type RedisManager struct {
|
||||
config *config.Config
|
||||
cmd *exec.Cmd
|
||||
running bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRedisManager creates a new Redis manager
|
||||
func NewRedisManager(cfg *config.Config) *RedisManager {
|
||||
return &RedisManager{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Start spawns the Redis server process
|
||||
func (rm *RedisManager) Start() error {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
if rm.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure Redis data directory exists
|
||||
dataDir := rm.config.RedisDataDir()
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create Redis data directory: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[Redis] Spawning Redis on port %d...", rm.config.RedisPort)
|
||||
|
||||
// Build Redis arguments matching TypeScript implementation
|
||||
args := []string{
|
||||
"--port", fmt.Sprintf("%d", rm.config.RedisPort),
|
||||
"--bind", "0.0.0.0",
|
||||
"--protected-mode", "no",
|
||||
"--dir", dataDir,
|
||||
"--appendonly", "yes",
|
||||
"--appendfilename", "appendonly.aof",
|
||||
"--dbfilename", "dump.rdb",
|
||||
"--save", "60", "1", // Save after 60 seconds if at least 1 key changed
|
||||
"--loglevel", "warning",
|
||||
// Enable keyspace notifications for meta:events stream
|
||||
// K = Keyspace events (publish to __keyspace@0__:keyname)
|
||||
// g = Generic commands (DEL, EXPIRE, RENAME)
|
||||
// $ = String commands (SET, MSET, etc.)
|
||||
"--notify-keyspace-events", "Kg$",
|
||||
}
|
||||
|
||||
rm.cmd = exec.Command("redis-server", args...)
|
||||
rm.cmd.Stdout = os.Stdout
|
||||
rm.cmd.Stderr = os.Stderr
|
||||
|
||||
if err := rm.cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start Redis: %w", err)
|
||||
}
|
||||
|
||||
rm.running = true
|
||||
log.Printf("[Redis] Started with PID %d", rm.cmd.Process.Pid)
|
||||
|
||||
// Monitor the process in background
|
||||
go rm.monitorProcess()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the Redis server
|
||||
func (rm *RedisManager) Stop() error {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
if !rm.running || rm.cmd == nil || rm.cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("[Redis] Stopping Redis...")
|
||||
|
||||
// Try graceful shutdown first (SIGTERM)
|
||||
if err := rm.cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||
log.Printf("[Redis] Failed to send SIGTERM: %v", err)
|
||||
}
|
||||
|
||||
// Wait up to 10 seconds for graceful shutdown
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- rm.cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
log.Println("[Redis] Stopped gracefully")
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Println("[Redis] Timeout, sending SIGKILL...")
|
||||
rm.cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
|
||||
rm.running = false
|
||||
rm.cmd = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns true if Redis is currently running
|
||||
func (rm *RedisManager) IsRunning() bool {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
return rm.running
|
||||
}
|
||||
|
||||
// WaitForReady waits for Redis to be ready to accept connections
|
||||
func (rm *RedisManager) WaitForReady(timeout time.Duration) error {
|
||||
log.Println("[Redis] Waiting for Redis to be ready...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("localhost:%d", rm.config.RedisPort),
|
||||
})
|
||||
defer client.Close()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timeout waiting for Redis")
|
||||
case <-ticker.C:
|
||||
if err := client.Ping(ctx).Err(); err == nil {
|
||||
log.Println("[Redis] Ready to accept connections")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ping checks if Redis is responding
|
||||
func (rm *RedisManager) Ping() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("localhost:%d", rm.config.RedisPort),
|
||||
})
|
||||
defer client.Close()
|
||||
|
||||
return client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
// monitorProcess monitors the Redis process and updates state when it exits
|
||||
func (rm *RedisManager) monitorProcess() {
|
||||
if rm.cmd == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := rm.cmd.Wait()
|
||||
|
||||
rm.mu.Lock()
|
||||
rm.running = false
|
||||
rm.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[Redis] Process exited with error: %v", err)
|
||||
} else {
|
||||
log.Println("[Redis] Process exited normally")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user