181 lines
3.9 KiB
Go
181 lines
3.9 KiB
Go
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",
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|