mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
* [maintnotif] Cluster specific handlers (#3613) * maint notification handlers for cluster messages * unrelax all conns * trigger ci on feature branches * feat(maintnotif): lazy cluster topology reload (#3614) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> * feat(e2e): mock maintnotif e2e tests (#3639) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> * wip fault with mock proxy * make lint happy * fix linter issues * faster tests with mocks * linter once again * add complex node test * add ci e2e * use correct redis container * e2e fix --------- Co-authored-by: Copilot <[email protected]> * fix(retry): Add retry mechanism for NOREPLICAS error (#3647) * fix(queuedNewConn): protect against nil context (#3649) * fix(maintnotif): fix smigrated parser and add cluster state reload interval option (#3663) * fix SMIGRATED parsing * fix smigrated parser * add ClusterStateReloadInterval to ClusterOptions * fix tests * set default cluster reload interval to 10s * chore(lint): format * feat(smigrated): new format & remember original host:port (#3697) * lazy cluster topology reload * fix discrepancies between options structs * Update osscluster_lazy_reload_test.go Co-authored-by: Copilot <[email protected]> * Update osscluster.go Co-authored-by: Copilot <[email protected]> * wip fault with mock proxy * make lint happy * fix linter issues * faster tests with mocks * linter once again * add complex node test * add ci e2e * use correct redis container * e2e fix * additional e2e tests * fix data race * fix random shard picker * fix e2e tests * fix for empty endpoint * fix case when semaphore is full, but still need to check idle * scenario tests * create database from config * wip * feat(client): store original addrs for later use * fix(notif): change smigrated notification * fix(lint): fix linter * fix(smigrated): use array * fix(e2e): wip * Update options.go Co-authored-by: Copilot <[email protected]> * Update redis.go Co-authored-by: Copilot <[email protected]> * fix(notif): if the conn has no original addr, trigger reload with first target * chore(e2e): wip cluster e2e * chore(e2e): fix nil pointer from e2e tests * chore(e2e): fix tests and reports * chore(e2e): proper logging in e2e * chore(e2e): add pubsub in the tests as well * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes * chore(e2e): mockproxy fixes v3 * stop background routines * fix(e2e): tests --------- Co-authored-by: Copilot <[email protected]> * chore(docs): add example app (#3651) * chore(lint): fmt * chore(docs): improve docs * chore(docs): update features.md * chore(github): remove example * chore(maintnotif): rename option and address pr comments * fix(e2e): command runner should use client timeout * chore(e2e): refactor tests * fix(e2e): set default timeout to 90m * fix(e2e): skip tests if proxy cannot be started --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: ofekshenawa <[email protected]> Co-authored-by: Elena Kolevska <[email protected]>
1481 lines
47 KiB
Go
1481 lines
47 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// ProxyFaultInjectorServer mimics the fault injector server using cae-resp-proxy
|
|
// This allows existing e2e tests to work unchanged - they just point to this server
|
|
// instead of the real fault injector
|
|
type ProxyFaultInjectorServer struct {
|
|
// HTTP server for fault injector API
|
|
httpServer *http.Server
|
|
listenAddr string
|
|
|
|
// Proxy management
|
|
proxyCmd *exec.Cmd
|
|
proxyAPIPort int
|
|
proxyAPIURL string
|
|
proxyHTTP *http.Client
|
|
|
|
// Cluster node tracking
|
|
nodes []proxyNodeInfo
|
|
nodesMutex sync.RWMutex
|
|
|
|
// Action tracking
|
|
actions map[string]*actionState
|
|
actionsMutex sync.RWMutex
|
|
actionCounter atomic.Int64
|
|
seqIDCounter atomic.Int64 // Counter for generating sequence IDs (starts at 1001)
|
|
|
|
// Track if this instance started the server
|
|
startedServer bool
|
|
|
|
// Track active notifications for new connections
|
|
activeNotifications map[string]string // map[notificationType]notification (RESP format)
|
|
activeNotificationsMutex sync.RWMutex
|
|
knownConnections map[string]bool // map[connectionID]bool
|
|
knownConnectionsMutex sync.RWMutex
|
|
monitoringActive atomic.Bool
|
|
}
|
|
|
|
type proxyNodeInfo struct {
|
|
listenPort int
|
|
targetHost string
|
|
targetPort int
|
|
proxyAddr string
|
|
nodeID string
|
|
}
|
|
|
|
type actionState struct {
|
|
ID string
|
|
Type ActionType
|
|
Status ActionStatus
|
|
Parameters map[string]interface{}
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
Error error
|
|
Output map[string]interface{}
|
|
}
|
|
|
|
// NewProxyFaultInjectorServer creates a new proxy-based fault injector server
|
|
func NewProxyFaultInjectorServer(listenAddr string, proxyAPIPort int) *ProxyFaultInjectorServer {
|
|
s := &ProxyFaultInjectorServer{
|
|
listenAddr: listenAddr,
|
|
proxyAPIPort: proxyAPIPort,
|
|
proxyAPIURL: fmt.Sprintf("http://localhost:%d", proxyAPIPort),
|
|
proxyHTTP: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
nodes: make([]proxyNodeInfo, 0),
|
|
actions: make(map[string]*actionState),
|
|
activeNotifications: make(map[string]string),
|
|
knownConnections: make(map[string]bool),
|
|
}
|
|
s.seqIDCounter.Store(1000) // Start at 1001 (will be incremented before use)
|
|
return s
|
|
}
|
|
|
|
// NewProxyFaultInjectorServerWithURL creates a new proxy-based fault injector server with a custom proxy API URL
|
|
func NewProxyFaultInjectorServerWithURL(listenAddr string, proxyAPIURL string) *ProxyFaultInjectorServer {
|
|
s := &ProxyFaultInjectorServer{
|
|
listenAddr: listenAddr,
|
|
proxyAPIURL: proxyAPIURL,
|
|
proxyHTTP: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
nodes: make([]proxyNodeInfo, 0),
|
|
actions: make(map[string]*actionState),
|
|
activeNotifications: make(map[string]string),
|
|
knownConnections: make(map[string]bool),
|
|
}
|
|
s.seqIDCounter.Store(1000) // Start at 1001 (will be incremented before use)
|
|
return s
|
|
}
|
|
|
|
// Start starts both the proxy and the fault injector API server
|
|
func (s *ProxyFaultInjectorServer) Start() error {
|
|
// Get cluster configuration from environment
|
|
clusterAddrs := os.Getenv("CLUSTER_ADDRS")
|
|
if clusterAddrs == "" {
|
|
clusterAddrs = "127.0.0.1:17000,127.0.0.1:17001,127.0.0.1:17002" // Use 127.0.0.1 to force IPv4
|
|
}
|
|
|
|
targetHost := os.Getenv("REDIS_TARGET_HOST")
|
|
if targetHost == "" {
|
|
targetHost = "localhost"
|
|
}
|
|
|
|
targetPort := 6379
|
|
if portStr := os.Getenv("REDIS_TARGET_PORT"); portStr != "" {
|
|
if _, err := fmt.Sscanf(portStr, "%d", &targetPort); err != nil {
|
|
return fmt.Errorf("invalid REDIS_TARGET_PORT: %w", err)
|
|
}
|
|
}
|
|
|
|
// Parse cluster addresses
|
|
addrs := strings.Split(clusterAddrs, ",")
|
|
if len(addrs) == 0 {
|
|
return fmt.Errorf("no cluster addresses specified")
|
|
}
|
|
|
|
// Extract first port for initial node
|
|
var initialPort int
|
|
if _, err := fmt.Sscanf(strings.Split(addrs[0], ":")[1], "%d", &initialPort); err != nil {
|
|
return fmt.Errorf("invalid port in cluster address %s: %w", addrs[0], err)
|
|
}
|
|
|
|
// Check if proxy is already running (e.g., in Docker)
|
|
proxyAlreadyRunning := false
|
|
resp, err := s.proxyHTTP.Get(s.proxyAPIURL + "/stats")
|
|
if err == nil && resp.StatusCode == 200 {
|
|
resp.Body.Close()
|
|
proxyAlreadyRunning = true
|
|
fmt.Printf("✓ Detected existing proxy at %s (e.g., Docker container)\n", s.proxyAPIURL)
|
|
}
|
|
|
|
// Only start proxy if not already running
|
|
if !proxyAlreadyRunning {
|
|
// Start cae-resp-proxy with initial node
|
|
s.proxyCmd = exec.Command("cae-resp-proxy",
|
|
"--api-port", fmt.Sprintf("%d", s.proxyAPIPort),
|
|
"--listen-port", fmt.Sprintf("%d", initialPort),
|
|
"--target-host", targetHost,
|
|
"--target-port", fmt.Sprintf("%d", targetPort),
|
|
)
|
|
|
|
if err := s.proxyCmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start proxy: %w", err)
|
|
}
|
|
|
|
// Wait for proxy to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
// Wait for proxy to be ready and configure nodes
|
|
for i := 0; i < 10; i++ {
|
|
resp, err := s.proxyHTTP.Get(s.proxyAPIURL + "/stats")
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode == 200 {
|
|
// Add initial node
|
|
s.nodesMutex.Lock()
|
|
s.nodes = append(s.nodes, proxyNodeInfo{
|
|
listenPort: initialPort,
|
|
targetHost: targetHost,
|
|
targetPort: targetPort,
|
|
proxyAddr: fmt.Sprintf("localhost:%d", initialPort),
|
|
nodeID: fmt.Sprintf("node-%d", initialPort),
|
|
})
|
|
s.nodesMutex.Unlock()
|
|
|
|
// Add remaining nodes (only if we started the proxy ourselves)
|
|
// If using Docker proxy, it's already configured
|
|
if !proxyAlreadyRunning {
|
|
for i := 1; i < len(addrs); i++ {
|
|
var port int
|
|
if _, err := fmt.Sscanf(strings.Split(addrs[i], ":")[1], "%d", &port); err != nil {
|
|
return fmt.Errorf("invalid port in cluster address %s: %w", addrs[i], err)
|
|
}
|
|
if err := s.addProxyNode(port, targetPort, targetHost); err != nil {
|
|
return fmt.Errorf("failed to add node %d: %w", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
// Check if HTTP server is already running on this address
|
|
testClient := &http.Client{Timeout: 1 * time.Second}
|
|
testResp, err := testClient.Get("http://" + s.listenAddr + "/actions")
|
|
if err == nil {
|
|
testResp.Body.Close()
|
|
if testResp.StatusCode == http.StatusOK {
|
|
fmt.Printf("✓ Fault injector server already running at %s\n", s.listenAddr)
|
|
fmt.Printf("[ProxyFI] Proxy API at %s\n", s.proxyAPIURL)
|
|
fmt.Printf("[ProxyFI] Cluster nodes: %d\n", len(s.nodes))
|
|
s.startedServer = false // This instance didn't start the server
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Start HTTP server for fault injector API
|
|
mux := http.NewServeMux()
|
|
|
|
// Add logging middleware
|
|
loggingMux := http.NewServeMux()
|
|
loggingMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Printf("[ProxyFI HTTP] %s %s from %s\n", r.Method, r.URL.Path, r.RemoteAddr)
|
|
mux.ServeHTTP(w, r)
|
|
})
|
|
|
|
// IMPORTANT: The real fault injector uses /action (singular) for both listing and triggering
|
|
// - GET /action -> list all actions
|
|
// - POST /action -> trigger a new action
|
|
// - GET /action/{action_id} -> get status of a specific action
|
|
mux.HandleFunc("/action", s.handleAction)
|
|
mux.HandleFunc("/action/", s.handleActionStatus)
|
|
mux.HandleFunc("/slot-migrate", s.handleSlotMigrate)
|
|
|
|
s.httpServer = &http.Server{
|
|
Addr: s.listenAddr,
|
|
Handler: loggingMux,
|
|
}
|
|
|
|
go func() {
|
|
fmt.Printf("[ProxyFI] HTTP server starting on %s\n", s.listenAddr)
|
|
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
fmt.Printf("[ProxyFI] HTTP server error: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for HTTP server to be ready
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Start monitoring for new connections
|
|
s.startConnectionMonitoring()
|
|
|
|
fmt.Printf("[ProxyFI] Started fault injector server at %s\n", s.listenAddr)
|
|
fmt.Printf("[ProxyFI] Proxy API at %s\n", s.proxyAPIURL)
|
|
fmt.Printf("[ProxyFI] Cluster nodes: %d\n", len(s.nodes))
|
|
|
|
s.startedServer = true // This instance started the server
|
|
return nil
|
|
}
|
|
|
|
// Stop stops both the proxy and the HTTP server
|
|
// Only stops the server if this instance started it
|
|
func (s *ProxyFaultInjectorServer) Stop() error {
|
|
// Only stop if this instance started the server
|
|
if !s.startedServer {
|
|
return nil
|
|
}
|
|
|
|
// Stop connection monitoring
|
|
s.stopConnectionMonitoring()
|
|
|
|
if s.httpServer != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = s.httpServer.Shutdown(ctx)
|
|
}
|
|
|
|
if s.proxyCmd != nil && s.proxyCmd.Process != nil {
|
|
return s.proxyCmd.Process.Kill()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) addProxyNode(listenPort, targetPort int, targetHost string) error {
|
|
nodeConfig := map[string]interface{}{
|
|
"listenPort": listenPort,
|
|
"targetHost": targetHost,
|
|
"targetPort": targetPort,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(nodeConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal node config: %w", err)
|
|
}
|
|
|
|
resp, err := s.proxyHTTP.Post(
|
|
s.proxyAPIURL+"/nodes",
|
|
"application/json",
|
|
bytes.NewReader(jsonData),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add node: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("failed to add node, status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
s.nodesMutex.Lock()
|
|
s.nodes = append(s.nodes, proxyNodeInfo{
|
|
listenPort: listenPort,
|
|
targetHost: targetHost,
|
|
targetPort: targetPort,
|
|
proxyAddr: fmt.Sprintf("localhost:%d", listenPort),
|
|
nodeID: fmt.Sprintf("node-%d", listenPort),
|
|
})
|
|
s.nodesMutex.Unlock()
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
return nil
|
|
}
|
|
|
|
// HTTP Handlers - mimic fault injector API
|
|
|
|
// handleAction handles both GET (list actions) and POST (trigger action) requests to /action
|
|
// This matches the real fault injector API which uses /action (singular) for both operations
|
|
func (s *ProxyFaultInjectorServer) handleAction(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
s.handleListActions(w, r)
|
|
case http.MethodPost:
|
|
s.handleTriggerAction(w, r)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) handleListActions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Return list of actions in the same format as the real fault injector API
|
|
// The real API returns an array of objects with job_id, action_type, status, and submitted_at
|
|
actions := []ActionListItem{
|
|
{
|
|
JobID: "mock-job-1",
|
|
ActionType: string(ActionSlotMigration),
|
|
Status: "completed",
|
|
SubmittedAt: "2026-01-26T00:00:00+00:00",
|
|
},
|
|
{
|
|
JobID: "mock-job-2",
|
|
ActionType: string(ActionClusterMigrate),
|
|
Status: "completed",
|
|
SubmittedAt: "2026-01-26T00:00:00+00:00",
|
|
},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(actions)
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) handleTriggerAction(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req ActionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("[ProxyFI] handleTriggerAction received: Type='%s', Parameters=%+v\n", req.Type, req.Parameters)
|
|
|
|
// Create action
|
|
actionID := fmt.Sprintf("action-%d", s.actionCounter.Add(1))
|
|
action := &actionState{
|
|
ID: actionID,
|
|
Type: req.Type,
|
|
Status: StatusRunning,
|
|
Parameters: req.Parameters,
|
|
StartTime: time.Now(),
|
|
Output: make(map[string]interface{}),
|
|
}
|
|
|
|
s.actionsMutex.Lock()
|
|
s.actions[actionID] = action
|
|
s.actionsMutex.Unlock()
|
|
|
|
fmt.Printf("[ProxyFI] Starting executeAction goroutine for action %s\n", actionID)
|
|
|
|
// Execute action asynchronously
|
|
go s.executeAction(action)
|
|
|
|
// Return response
|
|
resp := ActionResponse{
|
|
ActionID: actionID,
|
|
Status: string(StatusRunning),
|
|
Message: fmt.Sprintf("Action %s started", req.Type),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) handleActionStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Extract action ID from path
|
|
actionID := strings.TrimPrefix(r.URL.Path, "/action/")
|
|
|
|
s.actionsMutex.RLock()
|
|
action, exists := s.actions[actionID]
|
|
s.actionsMutex.RUnlock()
|
|
|
|
if !exists {
|
|
http.Error(w, "Action not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
resp := ActionStatusResponse{
|
|
ActionID: action.ID,
|
|
Status: action.Status,
|
|
StartTime: action.StartTime,
|
|
EndTime: action.EndTime,
|
|
Output: action.Output,
|
|
}
|
|
|
|
if action.Error != nil {
|
|
resp.Error = action.Error.Error()
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// executeAction executes an action and injects appropriate notifications
|
|
func (s *ProxyFaultInjectorServer) executeAction(action *actionState) {
|
|
defer func() {
|
|
action.EndTime = time.Now()
|
|
if action.Status == StatusRunning {
|
|
action.Status = StatusSuccess
|
|
}
|
|
}()
|
|
|
|
fmt.Printf("[ProxyFI] executeAction called with type: '%s' (ActionBind='%s')\n", action.Type, ActionBind)
|
|
|
|
switch action.Type {
|
|
case ActionSlotMigration:
|
|
s.executeSlotMigration(action)
|
|
case ActionClusterReshard:
|
|
s.executeClusterReshard(action)
|
|
case ActionClusterMigrate:
|
|
s.executeClusterMigrate(action)
|
|
case ActionFailover:
|
|
s.executeFailover(action)
|
|
case ActionMigrate:
|
|
s.executeMigrate(action)
|
|
case ActionBind:
|
|
fmt.Printf("[ProxyFI] Matched ActionBind case\n")
|
|
s.executeBind(action)
|
|
case ActionCreateDatabase:
|
|
fmt.Printf("[ProxyFI] Executing CreateDatabase\n")
|
|
s.executeCreateDatabase(action)
|
|
case ActionDeleteDatabase:
|
|
fmt.Printf("[ProxyFI] Executing DeleteDatabase\n")
|
|
s.executeDeleteDatabase(action)
|
|
default:
|
|
fmt.Printf("[ProxyFI] No match, using default case\n")
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("unsupported action type: %s", action.Type)
|
|
}
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigration(action *actionState) {
|
|
// Extract parameters
|
|
startSlot, _ := action.Parameters["start_slot"].(float64)
|
|
endSlot, _ := action.Parameters["end_slot"].(float64)
|
|
sourceNode, _ := action.Parameters["source_node"].(string)
|
|
targetNode, _ := action.Parameters["target_node"].(string)
|
|
|
|
fmt.Printf("[ProxyFI] Executing slot migration: slots %d-%d from %s to %s\n",
|
|
int(startSlot), int(endSlot), sourceNode, targetNode)
|
|
|
|
// Generate sequence ID using counter (starts at 1001)
|
|
seqID := s.generateSeqID()
|
|
|
|
// Step 1: Inject SMIGRATING notification
|
|
slotRange := fmt.Sprintf("%d-%d", int(startSlot), int(endSlot))
|
|
notification := formatSMigratingNotification(seqID, slotRange)
|
|
|
|
// Track this as an active notification for new connections
|
|
s.setActiveNotification("SMIGRATING", notification)
|
|
|
|
if err := s.injectNotification(notification); err != nil {
|
|
s.clearActiveNotification("SMIGRATING")
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject SMIGRATING: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["smigrating_injected"] = true
|
|
action.Output["seqID"] = seqID
|
|
|
|
// Wait a bit to simulate migration in progress
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Step 2: Inject SMIGRATED notification
|
|
// Get source and target addresses from nodes
|
|
s.nodesMutex.RLock()
|
|
sourceAddr := "localhost:7000" // Default source
|
|
targetAddr := "localhost:7001" // Default target
|
|
if len(s.nodes) > 0 {
|
|
sourceAddr = s.nodes[0].proxyAddr
|
|
}
|
|
if len(s.nodes) > 1 {
|
|
targetAddr = s.nodes[1].proxyAddr
|
|
}
|
|
s.nodesMutex.RUnlock()
|
|
|
|
// Format as triplet: "source target slots"
|
|
triplet := fmt.Sprintf("%s %s %s", sourceAddr, targetAddr, slotRange)
|
|
migratedNotif := formatSMigratedNotification(seqID+1, triplet)
|
|
|
|
// Clear SMIGRATING from active notifications before sending SMIGRATED
|
|
s.clearActiveNotification("SMIGRATING")
|
|
|
|
if err := s.injectNotification(migratedNotif); err != nil {
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject SMIGRATED: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["smigrated_injected"] = true
|
|
action.Output["source_endpoint"] = sourceAddr
|
|
action.Output["target_endpoint"] = targetAddr
|
|
|
|
fmt.Printf("[ProxyFI] Slot migration completed: %s\n", slotRange)
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeClusterReshard(action *actionState) {
|
|
// Similar to slot migration but for multiple slots
|
|
slots, _ := action.Parameters["slots"].([]interface{})
|
|
sourceNode, _ := action.Parameters["source_node"].(string)
|
|
targetNode, _ := action.Parameters["target_node"].(string)
|
|
|
|
fmt.Printf("[ProxyFI] Executing cluster reshard: %d slots from %s to %s\n",
|
|
len(slots), sourceNode, targetNode)
|
|
|
|
// Generate sequence ID using counter (starts at 1001)
|
|
seqID := s.generateSeqID()
|
|
|
|
// Convert slots to string ranges
|
|
slotStrs := make([]string, len(slots))
|
|
for i, slot := range slots {
|
|
slotStrs[i] = fmt.Sprintf("%d", int(slot.(float64)))
|
|
}
|
|
|
|
// Inject SMIGRATING
|
|
notification := formatSMigratingNotification(seqID, slotStrs...)
|
|
s.setActiveNotification("SMIGRATING", notification)
|
|
|
|
if err := s.injectNotification(notification); err != nil {
|
|
s.clearActiveNotification("SMIGRATING")
|
|
action.Status = StatusFailed
|
|
action.Error = err
|
|
return
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Inject SMIGRATED with triplet format: "source target slots"
|
|
s.nodesMutex.RLock()
|
|
sourceAddr := "localhost:7000" // Default source
|
|
targetAddr := "localhost:7001" // Default target
|
|
if len(s.nodes) > 0 {
|
|
sourceAddr = s.nodes[0].proxyAddr
|
|
}
|
|
if len(s.nodes) > 1 {
|
|
targetAddr = s.nodes[1].proxyAddr
|
|
}
|
|
s.nodesMutex.RUnlock()
|
|
|
|
// Format as triplet: "source target slots"
|
|
triplet := fmt.Sprintf("%s %s %s", sourceAddr, targetAddr, strings.Join(slotStrs, ","))
|
|
migratedNotif := formatSMigratedNotification(seqID+1, triplet)
|
|
|
|
// Clear SMIGRATING from active notifications before sending SMIGRATED
|
|
s.clearActiveNotification("SMIGRATING")
|
|
|
|
if err := s.injectNotification(migratedNotif); err != nil {
|
|
action.Status = StatusFailed
|
|
action.Error = err
|
|
return
|
|
}
|
|
|
|
action.Output["slots_migrated"] = len(slots)
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeClusterMigrate(action *actionState) {
|
|
// Similar to slot migration
|
|
s.executeSlotMigration(action)
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) injectNotification(notification string) error {
|
|
url := s.proxyAPIURL + "/send-to-all-clients?encoding=raw"
|
|
|
|
fmt.Printf("[ProxyFI] Injecting notification to %s\n", url)
|
|
fmt.Printf("[ProxyFI] Notification (first 100 chars): %s\n", notification[:min(100, len(notification))])
|
|
|
|
resp, err := s.proxyHTTP.Post(url, "application/octet-stream", strings.NewReader(notification))
|
|
if err != nil {
|
|
fmt.Printf("[ProxyFI] Failed to inject notification: %v\n", err)
|
|
return fmt.Errorf("failed to inject notification: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("[ProxyFI] Injection failed with status %d: %s\n", resp.StatusCode, string(body))
|
|
return fmt.Errorf("injection failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
fmt.Printf("[ProxyFI] Notification injected successfully\n")
|
|
return nil
|
|
}
|
|
|
|
// Helper function for min
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeFailover(action *actionState) {
|
|
fmt.Printf("[ProxyFI] Executing failover\n")
|
|
|
|
// Generate sequence ID using counter (starts at 1001)
|
|
seqID := s.generateSeqID()
|
|
|
|
// Step 1: Inject FAILING_OVER notification
|
|
// Format: ["FAILING_OVER", SeqID]
|
|
failingOverNotif := fmt.Sprintf(">2\r\n$12\r\nFAILING_OVER\r\n:%d\r\n", seqID)
|
|
|
|
s.setActiveNotification("FAILING_OVER", failingOverNotif)
|
|
|
|
if err := s.injectNotification(failingOverNotif); err != nil {
|
|
s.clearActiveNotification("FAILING_OVER")
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject FAILING_OVER: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["failing_over_injected"] = true
|
|
action.Output["seqID"] = seqID
|
|
|
|
// Wait to simulate failover in progress
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Step 2: Inject FAILED_OVER notification
|
|
// Format: ["FAILED_OVER", SeqID]
|
|
failedOverNotif := fmt.Sprintf(">2\r\n$11\r\nFAILED_OVER\r\n:%d\r\n", seqID+1)
|
|
|
|
s.clearActiveNotification("FAILING_OVER")
|
|
|
|
if err := s.injectNotification(failedOverNotif); err != nil {
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject FAILED_OVER: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["failed_over_injected"] = true
|
|
|
|
fmt.Printf("[ProxyFI] Failover completed\n")
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeMigrate(action *actionState) {
|
|
fmt.Printf("[ProxyFI] Executing migrate\n")
|
|
|
|
// Generate sequence ID using counter (starts at 1001)
|
|
seqID := s.generateSeqID()
|
|
slot := 1000 // Default slot for migration
|
|
|
|
// Step 1: Inject MIGRATING notification (no MOVING for migrate action)
|
|
// Format: ["MIGRATING", seqID, slot]
|
|
slotStr := fmt.Sprintf("%d", slot)
|
|
migratingNotif := fmt.Sprintf(">3\r\n$9\r\nMIGRATING\r\n:%d\r\n$%d\r\n%s\r\n",
|
|
seqID, len(slotStr), slotStr)
|
|
|
|
s.setActiveNotification("MIGRATING", migratingNotif)
|
|
|
|
if err := s.injectNotification(migratingNotif); err != nil {
|
|
s.clearActiveNotification("MIGRATING")
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject MIGRATING: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["migrating_injected"] = true
|
|
action.Output["seqID"] = seqID
|
|
|
|
// Wait to simulate migration in progress
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Step 2: Inject MIGRATED notification
|
|
// Format: ["MIGRATED", seqID, slot]
|
|
migratedNotif := fmt.Sprintf(">3\r\n$8\r\nMIGRATED\r\n:%d\r\n$%d\r\n%s\r\n",
|
|
seqID+1, len(slotStr), slotStr)
|
|
|
|
s.clearActiveNotification("MIGRATING")
|
|
|
|
if err := s.injectNotification(migratedNotif); err != nil {
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject MIGRATED: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["migrated_injected"] = true
|
|
|
|
fmt.Printf("[ProxyFI] Migrate completed\n")
|
|
}
|
|
|
|
func (s *ProxyFaultInjectorServer) executeBind(action *actionState) {
|
|
fmt.Printf("[ProxyFI] Executing bind\n")
|
|
|
|
// Generate sequence ID using counter (starts at 1001)
|
|
seqID := s.generateSeqID()
|
|
timeS := int64(5) // Time in seconds for handoff
|
|
|
|
// Get endpoint type from parameters (if provided)
|
|
endpointType, _ := action.Parameters["endpoint_type"].(string)
|
|
fmt.Printf("[ProxyFI] Bind action - endpoint_type parameter: '%s'\n", endpointType)
|
|
|
|
// Determine target endpoint based on endpoint type
|
|
var targetEndpoint string
|
|
s.nodesMutex.RLock()
|
|
defaultAddr := "localhost:7000"
|
|
if len(s.nodes) > 0 {
|
|
defaultAddr = s.nodes[0].proxyAddr
|
|
}
|
|
s.nodesMutex.RUnlock()
|
|
|
|
switch endpointType {
|
|
case "external-ip":
|
|
// Return IP address (use 127.0.0.1 for localhost)
|
|
host := strings.Split(defaultAddr, ":")[0]
|
|
port := strings.Split(defaultAddr, ":")[1]
|
|
if host == "localhost" {
|
|
host = "127.0.0.1"
|
|
}
|
|
targetEndpoint = fmt.Sprintf("%s:%s", host, port)
|
|
fmt.Printf("[ProxyFI] Using external-ip format: %s\n", targetEndpoint)
|
|
|
|
case "external-fqdn":
|
|
// Return FQDN format (e.g., node-1.localhost:7000)
|
|
// Extract host and port from defaultAddr
|
|
parts := strings.Split(defaultAddr, ":")
|
|
host := parts[0]
|
|
port := parts[1]
|
|
|
|
// Create FQDN by prepending "node-1." to the host
|
|
// This ensures the domain suffix matches the endpointConfig.Host
|
|
targetEndpoint = fmt.Sprintf("node-1.%s:%s", host, port)
|
|
fmt.Printf("[ProxyFI] Using external-fqdn format: %s\n", targetEndpoint)
|
|
|
|
case "none":
|
|
// Return null for "none" endpoint type
|
|
// For RESP3, null is represented as "_\r\n"
|
|
// But in array context, we use bulk string "$-1\r\n"
|
|
// Actually, for "none" we should send the special null value
|
|
// Let's use the internal.RedisNull constant which is "-"
|
|
targetEndpoint = "-"
|
|
fmt.Printf("[ProxyFI] Using none format: null\n")
|
|
|
|
case "":
|
|
// Empty endpoint type - use default
|
|
targetEndpoint = defaultAddr
|
|
fmt.Printf("[ProxyFI] Empty endpoint_type, using default: %s\n", targetEndpoint)
|
|
|
|
default:
|
|
// Default to localhost address
|
|
targetEndpoint = defaultAddr
|
|
fmt.Printf("[ProxyFI] Unknown endpoint_type '%s', using default: %s\n", endpointType, targetEndpoint)
|
|
}
|
|
|
|
// Inject MOVING notification
|
|
// Format: ["MOVING", seqID, timeS, endpoint]
|
|
var movingNotif string
|
|
if targetEndpoint == "-" {
|
|
// Special case for null endpoint (EndpointTypeNone)
|
|
// Use RESP3 null: "_\r\n" in array context
|
|
movingNotif = fmt.Sprintf(">4\r\n$6\r\nMOVING\r\n:%d\r\n:%d\r\n_\r\n",
|
|
seqID, timeS)
|
|
} else {
|
|
movingNotif = fmt.Sprintf(">4\r\n$6\r\nMOVING\r\n:%d\r\n:%d\r\n$%d\r\n%s\r\n",
|
|
seqID, timeS, len(targetEndpoint), targetEndpoint)
|
|
}
|
|
|
|
s.setActiveNotification("MOVING", movingNotif)
|
|
|
|
if err := s.injectNotification(movingNotif); err != nil {
|
|
s.clearActiveNotification("MOVING")
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("failed to inject MOVING: %w", err)
|
|
return
|
|
}
|
|
|
|
action.Output["moving_injected"] = true
|
|
action.Output["seqID"] = seqID
|
|
action.Output["target_endpoint"] = targetEndpoint
|
|
action.Output["endpoint_type"] = endpointType
|
|
|
|
fmt.Printf("[ProxyFI] Bind completed - MOVING notification sent (endpoint_type=%s, endpoint=%s)\n", endpointType, targetEndpoint)
|
|
}
|
|
|
|
// GetClusterAddrs returns the cluster addresses for connecting
|
|
func (s *ProxyFaultInjectorServer) GetClusterAddrs() []string {
|
|
s.nodesMutex.RLock()
|
|
defer s.nodesMutex.RUnlock()
|
|
|
|
addrs := make([]string, len(s.nodes))
|
|
for i, node := range s.nodes {
|
|
addrs[i] = node.proxyAddr
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// generateSeqID generates a new sequence ID starting from 1001
|
|
func (s *ProxyFaultInjectorServer) generateSeqID() int64 {
|
|
return s.seqIDCounter.Add(1)
|
|
}
|
|
|
|
// setActiveNotification stores a notification that should be sent to new connections
|
|
func (s *ProxyFaultInjectorServer) setActiveNotification(notifType string, notification string) {
|
|
s.activeNotificationsMutex.Lock()
|
|
defer s.activeNotificationsMutex.Unlock()
|
|
s.activeNotifications[notifType] = notification
|
|
fmt.Printf("[ProxyFI] Set active notification: %s\n", notifType)
|
|
}
|
|
|
|
// clearActiveNotification removes an active notification
|
|
func (s *ProxyFaultInjectorServer) clearActiveNotification(notifType string) {
|
|
s.activeNotificationsMutex.Lock()
|
|
defer s.activeNotificationsMutex.Unlock()
|
|
delete(s.activeNotifications, notifType)
|
|
fmt.Printf("[ProxyFI] Cleared active notification: %s\n", notifType)
|
|
}
|
|
|
|
// getActiveNotifications returns a copy of all active notifications
|
|
func (s *ProxyFaultInjectorServer) getActiveNotifications() map[string]string {
|
|
s.activeNotificationsMutex.RLock()
|
|
defer s.activeNotificationsMutex.RUnlock()
|
|
|
|
notifications := make(map[string]string, len(s.activeNotifications))
|
|
for k, v := range s.activeNotifications {
|
|
notifications[k] = v
|
|
}
|
|
return notifications
|
|
}
|
|
|
|
// startConnectionMonitoring starts monitoring for new connections
|
|
func (s *ProxyFaultInjectorServer) startConnectionMonitoring() {
|
|
if s.monitoringActive.Swap(true) {
|
|
return // Already monitoring
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(100 * time.Millisecond) // Poll every 100ms for faster detection
|
|
defer ticker.Stop()
|
|
|
|
for s.monitoringActive.Load() {
|
|
<-ticker.C
|
|
s.checkForNewConnections()
|
|
}
|
|
}()
|
|
fmt.Printf("[ProxyFI] Started connection monitoring (polling every 100ms)\n")
|
|
}
|
|
|
|
// stopConnectionMonitoring stops monitoring for new connections
|
|
func (s *ProxyFaultInjectorServer) stopConnectionMonitoring() {
|
|
s.monitoringActive.Store(false)
|
|
fmt.Printf("[ProxyFI] Stopped connection monitoring\n")
|
|
}
|
|
|
|
// checkForNewConnections checks for new connections and sends active notifications
|
|
func (s *ProxyFaultInjectorServer) checkForNewConnections() {
|
|
// Debug: Log that we're checking
|
|
activeNotifs := s.getActiveNotifications()
|
|
fmt.Printf("[ProxyFI] Connection monitoring: checking (active notifications: %d)...\n", len(activeNotifs))
|
|
|
|
// Get current connections from proxy stats endpoint
|
|
resp, err := s.proxyHTTP.Get(s.proxyAPIURL + "/stats")
|
|
if err != nil {
|
|
fmt.Printf("[ProxyFI] Connection monitoring: failed to get stats: %v\n", err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
fmt.Printf("[ProxyFI] Connection monitoring: stats returned status %d\n", resp.StatusCode)
|
|
return
|
|
}
|
|
|
|
// Parse stats response - format is map[backend]stats
|
|
// connections is an array of connection objects with "id" field
|
|
var stats map[string]struct {
|
|
Connections []struct {
|
|
ID string `json:"id"`
|
|
} `json:"connections"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
|
fmt.Printf("[ProxyFI] Connection monitoring: failed to decode stats: %v\n", err)
|
|
return
|
|
}
|
|
|
|
// Collect all connection IDs from all backends
|
|
allConnIDs := make([]string, 0)
|
|
for backend, backendStats := range stats {
|
|
for _, conn := range backendStats.Connections {
|
|
allConnIDs = append(allConnIDs, conn.ID)
|
|
}
|
|
fmt.Printf("[ProxyFI] Connection monitoring: backend %s has %d connections\n", backend, len(backendStats.Connections))
|
|
}
|
|
|
|
// Check for new connections
|
|
s.knownConnectionsMutex.Lock()
|
|
newConnections := make([]string, 0)
|
|
for _, connID := range allConnIDs {
|
|
if !s.knownConnections[connID] {
|
|
s.knownConnections[connID] = true
|
|
newConnections = append(newConnections, connID)
|
|
}
|
|
}
|
|
totalKnown := len(s.knownConnections)
|
|
s.knownConnectionsMutex.Unlock()
|
|
|
|
fmt.Printf("[ProxyFI] Connection monitoring: total=%d, known=%d, new=%d\n", len(allConnIDs), totalKnown, len(newConnections))
|
|
|
|
// Send active notifications to new connections
|
|
if len(newConnections) > 0 {
|
|
activeNotifs := s.getActiveNotifications()
|
|
fmt.Printf("[ProxyFI] Found %d new connection(s), have %d active notification(s)\n",
|
|
len(newConnections), len(activeNotifs))
|
|
|
|
if len(activeNotifs) > 0 {
|
|
for _, connID := range newConnections {
|
|
for notifType, notification := range activeNotifs {
|
|
if err := s.sendToConnection(connID, notification); err != nil {
|
|
fmt.Printf("[ProxyFI] Failed to send %s to new connection %s: %v\n",
|
|
notifType, connID, err)
|
|
} else {
|
|
fmt.Printf("[ProxyFI] Sent %s to new connection %s\n", notifType, connID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendToConnection sends a notification to a specific connection
|
|
func (s *ProxyFaultInjectorServer) sendToConnection(connID string, notification string) error {
|
|
url := fmt.Sprintf("%s/send-to-client/%s?encoding=raw", s.proxyAPIURL, connID)
|
|
|
|
resp, err := s.proxyHTTP.Post(url, "application/octet-stream", strings.NewReader(notification))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send to connection: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("failed to send to connection, status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleSlotMigrate handles the /slot-migrate endpoint
|
|
// GET: Returns available triggers for a slot migration effect
|
|
// POST: Triggers a slot migration action
|
|
func (s *ProxyFaultInjectorServer) handleSlotMigrate(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
s.handleSlotMigrateGetTriggers(w, r)
|
|
case http.MethodPost:
|
|
s.handleSlotMigrateTrigger(w, r)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// handleSlotMigrateGetTriggers returns available triggers for a slot migration effect
|
|
func (s *ProxyFaultInjectorServer) handleSlotMigrateGetTriggers(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query()
|
|
effect := SlotMigrateEffect(query.Get("effect"))
|
|
if effect == "" {
|
|
http.Error(w, "Missing required parameter: effect", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Parse cluster_index (optional, default: 0)
|
|
clusterIndex := 0
|
|
if clusterIndexStr := query.Get("cluster_index"); clusterIndexStr != "" {
|
|
if _, err := fmt.Sscanf(clusterIndexStr, "%d", &clusterIndex); err != nil {
|
|
http.Error(w, "Invalid cluster_index parameter", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Build cluster field with proper structure: {index, nodes: [{host, port}, ...]}
|
|
s.nodesMutex.RLock()
|
|
nodesArray := make([]map[string]interface{}, len(s.nodes))
|
|
for i, node := range s.nodes {
|
|
// Parse host and port from proxyAddr (format: "localhost:7001")
|
|
parts := strings.Split(node.proxyAddr, ":")
|
|
host := parts[0]
|
|
port := node.listenPort
|
|
nodesArray[i] = map[string]interface{}{
|
|
"host": host,
|
|
"port": port,
|
|
}
|
|
}
|
|
s.nodesMutex.RUnlock()
|
|
|
|
clusterInfo := map[string]interface{}{
|
|
"index": clusterIndex,
|
|
"nodes": nodesArray,
|
|
}
|
|
|
|
// Return mock triggers based on effect
|
|
var triggers []SlotMigrateTrigger
|
|
|
|
// Helper function to create requirements with dbconfig and cluster
|
|
makeRequirements := func(dbconfig map[string]interface{}, description string) []SlotMigrateTriggerRequirement {
|
|
return []SlotMigrateTriggerRequirement{
|
|
{
|
|
DBConfig: dbconfig,
|
|
Cluster: clusterInfo,
|
|
Description: description,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Default dbconfig for migrate (sharded cluster)
|
|
defaultDBConfig := map[string]interface{}{
|
|
"shards_count": 3,
|
|
"shards_placement": "sparse",
|
|
}
|
|
|
|
// Failover requirements (requires replication)
|
|
failoverDBConfig := map[string]interface{}{
|
|
"shards_count": 3,
|
|
"shards_placement": "sparse",
|
|
"replication": true,
|
|
}
|
|
|
|
switch effect {
|
|
case SlotMigrateEffectRemoveAdd:
|
|
triggers = []SlotMigrateTrigger{
|
|
{
|
|
Name: "migrate",
|
|
Description: "Migrate all shards from source node to empty node",
|
|
Requirements: makeRequirements(defaultDBConfig, "Requires sharded cluster"),
|
|
},
|
|
{
|
|
Name: "failover",
|
|
Description: "Trigger failover (requires replication)",
|
|
Requirements: makeRequirements(failoverDBConfig, "Requires replication enabled for failover"),
|
|
},
|
|
}
|
|
case SlotMigrateEffectRemove:
|
|
triggers = []SlotMigrateTrigger{
|
|
{
|
|
Name: "migrate",
|
|
Description: "Migrate all shards from source node to existing node",
|
|
Requirements: makeRequirements(defaultDBConfig, "Requires sharded cluster"),
|
|
},
|
|
}
|
|
case SlotMigrateEffectAdd:
|
|
triggers = []SlotMigrateTrigger{
|
|
{
|
|
Name: "migrate",
|
|
Description: "Migrate one shard to empty node",
|
|
Requirements: makeRequirements(defaultDBConfig, "Requires sharded cluster"),
|
|
},
|
|
{
|
|
Name: "failover",
|
|
Description: "Trigger failover (requires replication)",
|
|
Requirements: makeRequirements(failoverDBConfig, "Requires replication enabled for failover"),
|
|
},
|
|
}
|
|
case SlotMigrateEffectSlotShuffle:
|
|
triggers = []SlotMigrateTrigger{
|
|
{
|
|
Name: "migrate",
|
|
Description: "Migrate one shard between existing nodes",
|
|
Requirements: makeRequirements(defaultDBConfig, "Requires sharded cluster"),
|
|
},
|
|
{
|
|
Name: "failover",
|
|
Description: "Trigger failover (requires replication)",
|
|
Requirements: makeRequirements(failoverDBConfig, "Requires replication enabled for failover"),
|
|
},
|
|
}
|
|
default:
|
|
http.Error(w, fmt.Sprintf("Unknown effect: %s", effect), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
response := SlotMigrateTriggersResponse{
|
|
Effect: effect,
|
|
Cluster: clusterInfo,
|
|
Triggers: triggers,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// handleSlotMigrateTrigger triggers a slot migration action
|
|
func (s *ProxyFaultInjectorServer) handleSlotMigrateTrigger(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query()
|
|
effect := SlotMigrateEffect(query.Get("effect"))
|
|
bdbID := query.Get("bdb_id")
|
|
// IMPORTANT: The real fault injector API uses "variant" as the query parameter name (rest_api.py line 301)
|
|
// We use "trigger" as the variable name internally, but must read from "variant" parameter
|
|
trigger := SlotMigrateVariant(query.Get("variant"))
|
|
sourceNode := query.Get("source_node")
|
|
targetNode := query.Get("target_node")
|
|
clusterIndexStr := query.Get("cluster_index")
|
|
|
|
// Parse cluster_index with default of 0
|
|
clusterIndex := 0
|
|
if clusterIndexStr != "" {
|
|
if idx, err := strconv.Atoi(clusterIndexStr); err == nil {
|
|
clusterIndex = idx
|
|
}
|
|
}
|
|
|
|
if effect == "" {
|
|
http.Error(w, "Missing required parameter: effect", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if bdbID == "" {
|
|
http.Error(w, "Missing required parameter: bdb_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Default trigger is "migrate"
|
|
if trigger == "" || trigger == SlotMigrateVariantDefault {
|
|
trigger = SlotMigrateVariantMigrate
|
|
}
|
|
|
|
fmt.Printf("[ProxyFI] Slot-migrate: effect=%s, bdb_id=%s, trigger=%s, cluster_index=%d, source_node=%s, target_node=%s\n",
|
|
effect, bdbID, trigger, clusterIndex, sourceNode, targetNode)
|
|
|
|
// Create action
|
|
actionID := fmt.Sprintf("slot-migrate-%d", s.actionCounter.Add(1))
|
|
action := &actionState{
|
|
ID: actionID,
|
|
Type: ActionSlotMigrate,
|
|
Status: StatusRunning,
|
|
Parameters: map[string]interface{}{
|
|
"effect": string(effect),
|
|
"bdb_id": bdbID,
|
|
"variant": string(trigger),
|
|
"cluster_index": clusterIndex,
|
|
"source_node": sourceNode,
|
|
"target_node": targetNode,
|
|
},
|
|
StartTime: time.Now(),
|
|
Output: make(map[string]interface{}),
|
|
}
|
|
|
|
s.actionsMutex.Lock()
|
|
s.actions[actionID] = action
|
|
s.actionsMutex.Unlock()
|
|
|
|
// Execute action asynchronously
|
|
go s.executeSlotMigrateAction(action, effect, trigger)
|
|
|
|
// Return response per spec: only {"action_id": "..."}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"action_id": actionID})
|
|
}
|
|
|
|
// executeSlotMigrateAction executes a slot-migrate action with the specified effect and trigger
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigrateAction(action *actionState, effect SlotMigrateEffect, trigger SlotMigrateVariant) {
|
|
defer func() {
|
|
action.EndTime = time.Now()
|
|
if action.Status == StatusRunning {
|
|
action.Status = StatusSuccess
|
|
}
|
|
}()
|
|
|
|
fmt.Printf("[ProxyFI] Executing slot-migrate: effect=%s, trigger=%s\n", effect, trigger)
|
|
|
|
switch effect {
|
|
case SlotMigrateEffectRemoveAdd:
|
|
s.executeSlotMigrateRemoveAdd(action, trigger)
|
|
case SlotMigrateEffectRemove:
|
|
s.executeSlotMigrateRemove(action, trigger)
|
|
case SlotMigrateEffectAdd:
|
|
s.executeSlotMigrateAdd(action, trigger)
|
|
case SlotMigrateEffectSlotShuffle:
|
|
s.executeSlotMigrateSlotShuffle(action, trigger)
|
|
default:
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("unsupported slot-migrate effect: %s", effect)
|
|
}
|
|
}
|
|
|
|
// executeSlotMigrateRemoveAdd simulates migrating all shards from one node to a new node
|
|
// Effect: One endpoint removed, one endpoint added
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigrateRemoveAdd(action *actionState, trigger SlotMigrateVariant) {
|
|
s.nodesMutex.RLock()
|
|
if len(s.nodes) < 2 {
|
|
s.nodesMutex.RUnlock()
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("need at least 2 nodes for remove-add effect")
|
|
return
|
|
}
|
|
|
|
// Pick a source node (the one that will be "removed")
|
|
sourceNode := s.nodes[0]
|
|
sourceAddr := sourceNode.proxyAddr
|
|
numNodes := len(s.nodes)
|
|
s.nodesMutex.RUnlock()
|
|
|
|
// Calculate simulated slot range for this node (16384 slots total, distributed evenly)
|
|
slotsPerNode := 16384 / numNodes
|
|
slotStart := 0 // First node starts at slot 0
|
|
slotEnd := slotsPerNode - 1
|
|
|
|
// Generate a new endpoint address (simulating a new node)
|
|
newAddr := fmt.Sprintf("127.0.0.1:%d", 7000+numNodes+1)
|
|
|
|
fmt.Printf("[ProxyFI] remove-add: source=%s (slots %d-%d) -> new=%s\n", sourceAddr, slotStart, slotEnd, newAddr)
|
|
|
|
// Generate sequence ID for notifications
|
|
seqID := s.generateSeqID()
|
|
|
|
// Send SMIGRATING notification for all slots on source node
|
|
smigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf("%d-%d", slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratingMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATING: %v\n", err)
|
|
}
|
|
|
|
// Brief delay to simulate migration in progress
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Send SMIGRATED notification with the new endpoint
|
|
smigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf("%s %d-%d", newAddr, slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratedMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATED: %v\n", err)
|
|
}
|
|
|
|
action.Output["source_addr"] = sourceAddr
|
|
action.Output["new_addr"] = newAddr
|
|
action.Output["slots"] = fmt.Sprintf("%d-%d", slotStart, slotEnd)
|
|
action.Output["variant"] = string(trigger)
|
|
}
|
|
|
|
// executeSlotMigrateRemove simulates migrating all shards from one node to an existing node
|
|
// Effect: One endpoint removed (node count decreases by 1)
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigrateRemove(action *actionState, trigger SlotMigrateVariant) {
|
|
s.nodesMutex.Lock()
|
|
if len(s.nodes) < 2 {
|
|
s.nodesMutex.Unlock()
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("need at least 2 nodes for remove effect")
|
|
return
|
|
}
|
|
|
|
// Pick source and destination nodes
|
|
sourceNode := s.nodes[0]
|
|
destNode := s.nodes[1]
|
|
sourceAddr := sourceNode.proxyAddr
|
|
destAddr := destNode.proxyAddr
|
|
numNodes := len(s.nodes)
|
|
|
|
// Remove the source node from the node list (topology change)
|
|
s.nodes = s.nodes[1:]
|
|
s.nodesMutex.Unlock()
|
|
|
|
// Calculate simulated slot range for source node
|
|
slotsPerNode := 16384 / numNodes
|
|
slotStart := 0
|
|
slotEnd := slotsPerNode - 1
|
|
|
|
fmt.Printf("[ProxyFI] remove: source=%s (slots %d-%d) -> dest=%s (nodes: %d -> %d)\n", sourceAddr, slotStart, slotEnd, destAddr, numNodes, numNodes-1)
|
|
|
|
// Generate sequence ID for notifications
|
|
seqID := s.generateSeqID()
|
|
|
|
// Send SMIGRATING notification
|
|
smigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf("%d-%d", slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratingMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATING: %v\n", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Send SMIGRATED notification with existing endpoint
|
|
smigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf("%s %d-%d", destAddr, slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratedMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATED: %v\n", err)
|
|
}
|
|
|
|
action.Output["source_addr"] = sourceAddr
|
|
action.Output["dest_addr"] = destAddr
|
|
action.Output["slots"] = fmt.Sprintf("%d-%d", slotStart, slotEnd)
|
|
action.Output["variant"] = string(trigger)
|
|
action.Output["nodes_before"] = numNodes
|
|
action.Output["nodes_after"] = numNodes - 1
|
|
}
|
|
|
|
// executeSlotMigrateAdd simulates migrating one shard from an existing node to a new node
|
|
// Effect: One endpoint added (net +1 node)
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigrateAdd(action *actionState, trigger SlotMigrateVariant) {
|
|
s.nodesMutex.Lock()
|
|
if len(s.nodes) < 1 {
|
|
s.nodesMutex.Unlock()
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("need at least 1 node for add effect")
|
|
return
|
|
}
|
|
|
|
// Pick a source node (an existing node that will give up some slots)
|
|
sourceNode := s.nodes[0]
|
|
sourceAddr := sourceNode.proxyAddr
|
|
nodesBefore := len(s.nodes)
|
|
|
|
// Calculate partial slot range (one shard worth - roughly 1/3 of the node's slots)
|
|
slotsPerNode := 16384 / nodesBefore
|
|
slotStart := 0
|
|
slotRange := slotsPerNode / 3
|
|
slotEnd := slotStart + slotRange
|
|
|
|
// Generate a new endpoint address (simulating a new node)
|
|
newAddr := fmt.Sprintf("127.0.0.1:%d", 7000+nodesBefore+1)
|
|
|
|
// Add the new node to the cluster
|
|
newNode := proxyNodeInfo{
|
|
listenPort: 7000 + nodesBefore + 1,
|
|
proxyAddr: newAddr,
|
|
nodeID: fmt.Sprintf("node-%d", nodesBefore+1),
|
|
}
|
|
s.nodes = append(s.nodes, newNode)
|
|
nodesAfter := len(s.nodes)
|
|
s.nodesMutex.Unlock()
|
|
|
|
fmt.Printf("[ProxyFI] add: source=%s (slots %d-%d) -> new=%s, nodes: %d -> %d\n",
|
|
sourceAddr, slotStart, slotEnd, newAddr, nodesBefore, nodesAfter)
|
|
|
|
// Generate sequence ID for notifications
|
|
seqID := s.generateSeqID()
|
|
|
|
// Send SMIGRATING notification for partial slot range
|
|
smigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf("%d-%d", slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratingMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATING: %v\n", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Send SMIGRATED notification with the new endpoint
|
|
smigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf("%s %d-%d", newAddr, slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratedMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATED: %v\n", err)
|
|
}
|
|
|
|
action.Output["source_addr"] = sourceAddr
|
|
action.Output["new_addr"] = newAddr
|
|
action.Output["slots"] = fmt.Sprintf("%d-%d", slotStart, slotEnd)
|
|
action.Output["variant"] = string(trigger)
|
|
action.Output["nodes_before"] = nodesBefore
|
|
action.Output["nodes_after"] = nodesAfter
|
|
}
|
|
|
|
// executeSlotMigrateSlotShuffle simulates moving slots between existing nodes
|
|
// Effect: Slots move, no endpoint changes (same nodes, different slot distribution)
|
|
func (s *ProxyFaultInjectorServer) executeSlotMigrateSlotShuffle(action *actionState, trigger SlotMigrateVariant) {
|
|
s.nodesMutex.RLock()
|
|
if len(s.nodes) < 2 {
|
|
s.nodesMutex.RUnlock()
|
|
action.Status = StatusFailed
|
|
action.Error = fmt.Errorf("need at least 2 nodes for slot-shuffle effect")
|
|
return
|
|
}
|
|
|
|
// Pick source and destination nodes (both existing)
|
|
sourceNode := s.nodes[0]
|
|
sourceAddr := sourceNode.proxyAddr
|
|
destNode := s.nodes[1]
|
|
destAddr := destNode.proxyAddr
|
|
numNodes := len(s.nodes)
|
|
|
|
// Calculate partial slot range (one shard worth)
|
|
slotsPerNode := 16384 / numNodes
|
|
slotStart := 0
|
|
slotRange := slotsPerNode / 3
|
|
slotEnd := slotStart + slotRange
|
|
s.nodesMutex.RUnlock()
|
|
|
|
fmt.Printf("[ProxyFI] slot-shuffle: source=%s (slots %d-%d) -> dest=%s\n", sourceAddr, slotStart, slotEnd, destAddr)
|
|
|
|
// Generate sequence ID for notifications
|
|
seqID := s.generateSeqID()
|
|
|
|
// Send SMIGRATING notification
|
|
smigratingMsg := formatSMigratingNotification(seqID, fmt.Sprintf("%d-%d", slotStart, slotEnd))
|
|
|
|
// Track this as an active notification for new connections
|
|
s.setActiveNotification("SMIGRATING", smigratingMsg)
|
|
|
|
if err := s.injectNotification(smigratingMsg); err != nil {
|
|
s.clearActiveNotification("SMIGRATING")
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATING: %v\n", err)
|
|
}
|
|
|
|
// Wait 500ms to simulate migration in progress - allows tests to create new connections
|
|
// that should receive the active SMIGRATING notification
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Clear SMIGRATING from active notifications before sending SMIGRATED
|
|
s.clearActiveNotification("SMIGRATING")
|
|
|
|
// Send SMIGRATED notification with existing destination endpoint
|
|
smigratedMsg := formatSMigratedNotification(seqID+1, fmt.Sprintf("%s %d-%d", destAddr, slotStart, slotEnd))
|
|
if err := s.injectNotification(smigratedMsg); err != nil {
|
|
fmt.Printf("[ProxyFI] Error sending SMIGRATED: %v\n", err)
|
|
}
|
|
|
|
action.Output["source_addr"] = sourceAddr
|
|
action.Output["dest_addr"] = destAddr
|
|
action.Output["slots"] = fmt.Sprintf("%d-%d", slotStart, slotEnd)
|
|
action.Output["variant"] = string(trigger)
|
|
}
|
|
|
|
// executeDeleteDatabase clears all nodes to simulate database deletion
|
|
func (s *ProxyFaultInjectorServer) executeDeleteDatabase(action *actionState) {
|
|
s.nodesMutex.Lock()
|
|
s.nodes = make([]proxyNodeInfo, 0)
|
|
s.nodesMutex.Unlock()
|
|
|
|
fmt.Printf("[ProxyFI] DeleteDatabase: cleared all nodes\n")
|
|
action.Status = StatusSuccess
|
|
}
|
|
|
|
// executeCreateDatabase resets nodes to initial state with 2 nodes
|
|
// so that all slot-migrate effects can work (remove, remove-add, slot-shuffle need >= 2 nodes)
|
|
func (s *ProxyFaultInjectorServer) executeCreateDatabase(action *actionState) {
|
|
s.nodesMutex.Lock()
|
|
// Get target host/port from the existing proxy or use defaults
|
|
targetHost := "cae-resp-proxy"
|
|
targetPort := 6379
|
|
|
|
// Create 2 nodes so all effects can work
|
|
s.nodes = []proxyNodeInfo{
|
|
{listenPort: 7001, targetHost: targetHost, targetPort: targetPort, proxyAddr: "localhost:7001", nodeID: "node-7001"},
|
|
{listenPort: 7002, targetHost: targetHost, targetPort: targetPort, proxyAddr: "localhost:7002", nodeID: "node-7002"},
|
|
}
|
|
s.nodesMutex.Unlock()
|
|
|
|
fmt.Printf("[ProxyFI] CreateDatabase: initialized with 2 nodes\n")
|
|
action.Status = StatusSuccess
|
|
}
|