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]>
677 lines
22 KiB
Go
677 lines
22 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NotificationInjector is an interface that can inject maintenance notifications
|
|
// It can be implemented by either a real fault injector or a proxy-based mock
|
|
type NotificationInjector interface {
|
|
// InjectSMIGRATING injects an SMIGRATING notification
|
|
InjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error
|
|
|
|
// InjectSMIGRATED injects an SMIGRATED notification
|
|
InjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error
|
|
|
|
// InjectMOVING injects a MOVING notification (for standalone)
|
|
// Format: ["MOVING", seqID, timeS, endpoint]
|
|
InjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error
|
|
|
|
// InjectMIGRATING injects a MIGRATING notification (for standalone)
|
|
InjectMIGRATING(ctx context.Context, seqID int64, slot int) error
|
|
|
|
// InjectMIGRATED injects a MIGRATED notification (for standalone)
|
|
InjectMIGRATED(ctx context.Context, seqID int64, slot int) error
|
|
|
|
// InjectFAILING_OVER injects a FAILING_OVER notification
|
|
InjectFAILING_OVER(ctx context.Context, seqID int64) error
|
|
|
|
// InjectFAILED_OVER injects a FAILED_OVER notification
|
|
InjectFAILED_OVER(ctx context.Context, seqID int64) error
|
|
|
|
// Start starts the injector (if needed)
|
|
Start() error
|
|
|
|
// Stop stops the injector (if needed)
|
|
Stop() error
|
|
|
|
// GetClusterAddrs returns the cluster addresses to connect to
|
|
GetClusterAddrs() []string
|
|
|
|
// IsReal returns true if this is a real fault injector (not a mock)
|
|
IsReal() bool
|
|
|
|
// GetTestModeConfig returns the test mode configuration for this injector
|
|
GetTestModeConfig() *TestModeConfig
|
|
}
|
|
|
|
// NewNotificationInjector creates a notification injector based on environment
|
|
// If FAULT_INJECTOR_URL is set, it uses the real fault injector
|
|
// Otherwise, it uses the proxy-based mock
|
|
func NewNotificationInjector() (NotificationInjector, error) {
|
|
if faultInjectorURL := os.Getenv("FAULT_INJECTOR_URL"); faultInjectorURL != "" {
|
|
// Use real fault injector
|
|
return NewFaultInjectorNotificationInjector(faultInjectorURL), nil
|
|
}
|
|
|
|
// Use proxy-based mock
|
|
apiPort := 18100 // Default port (updated to avoid macOS Control Center conflict)
|
|
if portStr := os.Getenv("PROXY_API_PORT"); portStr != "" {
|
|
_, _ = fmt.Sscanf(portStr, "%d", &apiPort)
|
|
}
|
|
|
|
return NewProxyNotificationInjector(apiPort), nil
|
|
}
|
|
|
|
// ProxyNotificationInjector implements NotificationInjector using cae-resp-proxy
|
|
type ProxyNotificationInjector struct {
|
|
apiPort int
|
|
apiBaseURL string
|
|
cmd *exec.Cmd
|
|
httpClient *http.Client
|
|
nodes []proxyNode
|
|
visibleNodes []int // Indices of nodes visible in CLUSTER SLOTS (for migration simulation)
|
|
}
|
|
|
|
type proxyNode struct {
|
|
listenPort int
|
|
targetHost string
|
|
targetPort int
|
|
proxyAddr string
|
|
nodeID string
|
|
}
|
|
|
|
// NewProxyNotificationInjector creates a new proxy-based notification injector
|
|
func NewProxyNotificationInjector(apiPort int) *ProxyNotificationInjector {
|
|
return &ProxyNotificationInjector{
|
|
apiPort: apiPort,
|
|
apiBaseURL: fmt.Sprintf("http://localhost:%d", apiPort),
|
|
httpClient: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
nodes: make([]proxyNode, 0),
|
|
}
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) Start() error {
|
|
// Get cluster configuration from environment
|
|
clusterAddrs := os.Getenv("CLUSTER_ADDRS")
|
|
if clusterAddrs == "" {
|
|
// Start with 4 nodes: 17000, 17001, 17002, 17003
|
|
// Initially, CLUSTER SLOTS will only expose 17000, 17001, 17002
|
|
// Node 17003 will be "hidden" until SMIGRATED swaps it in for 17002
|
|
clusterAddrs = "127.0.0.1:17000,127.0.0.1:17001,127.0.0.1:17002,127.0.0.1:17003" // Use 127.0.0.1 to force IPv4
|
|
}
|
|
|
|
targetHost := os.Getenv("REDIS_TARGET_HOST")
|
|
if targetHost == "" {
|
|
targetHost = "127.0.0.1" // Use 127.0.0.1 to force IPv4
|
|
}
|
|
|
|
targetPort := 6379
|
|
if portStr := os.Getenv("REDIS_TARGET_PORT"); portStr != "" {
|
|
_, _ = fmt.Sscanf(portStr, "%d", &targetPort)
|
|
}
|
|
|
|
// 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
|
|
_, _ = fmt.Sscanf(strings.Split(addrs[0], ":")[1], "%d", &initialPort)
|
|
|
|
// Check if proxy is already running (e.g., in Docker)
|
|
proxyAlreadyRunning := false
|
|
resp, err := p.httpClient.Get(p.apiBaseURL + "/stats")
|
|
if err == nil && resp.StatusCode == 200 {
|
|
resp.Body.Close()
|
|
proxyAlreadyRunning = true
|
|
fmt.Printf("✓ Detected existing proxy at %s (e.g., Docker container)\n", p.apiBaseURL)
|
|
}
|
|
|
|
// Only start proxy if not already running
|
|
if !proxyAlreadyRunning {
|
|
// Start proxy with initial node
|
|
p.cmd = exec.Command("cae-resp-proxy",
|
|
"--api-port", fmt.Sprintf("%d", p.apiPort),
|
|
"--listen-port", fmt.Sprintf("%d", initialPort),
|
|
"--target-host", targetHost,
|
|
"--target-port", fmt.Sprintf("%d", targetPort),
|
|
)
|
|
|
|
if err := p.cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start proxy: %w", err)
|
|
}
|
|
|
|
// Wait for proxy to be ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
resp, err := p.httpClient.Get(p.apiBaseURL + "/stats")
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode == 200 {
|
|
// Add all nodes from the cluster addresses
|
|
// For Docker proxy, all nodes are already running
|
|
// For local proxy, we'll add them via API
|
|
for i, addr := range addrs {
|
|
var port int
|
|
_, _ = fmt.Sscanf(strings.Split(addr, ":")[1], "%d", &port)
|
|
|
|
if i == 0 {
|
|
// Add initial node directly
|
|
p.nodes = append(p.nodes, proxyNode{
|
|
listenPort: port,
|
|
targetHost: targetHost,
|
|
targetPort: targetPort,
|
|
proxyAddr: fmt.Sprintf("127.0.0.1:%d", port),
|
|
nodeID: fmt.Sprintf("127.0.0.1:%d:%d", port, targetPort),
|
|
})
|
|
} else if !proxyAlreadyRunning {
|
|
// Add remaining nodes via API (only if we started the proxy ourselves)
|
|
if err := p.addNode(port, targetPort, targetHost); err != nil {
|
|
return fmt.Errorf("failed to add node %d: %w", i, err)
|
|
}
|
|
} else {
|
|
// Docker proxy: nodes are already running, just add to our list
|
|
p.nodes = append(p.nodes, proxyNode{
|
|
listenPort: port,
|
|
targetHost: targetHost,
|
|
targetPort: targetPort,
|
|
proxyAddr: fmt.Sprintf("127.0.0.1:%d", port),
|
|
nodeID: fmt.Sprintf("127.0.0.1:%d:%d", port, targetPort),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Initially, make only the first 3 nodes visible in CLUSTER SLOTS
|
|
// The 4th node (index 3) will be hidden until SMIGRATED swaps it in
|
|
if len(p.nodes) >= 4 {
|
|
p.visibleNodes = []int{0, 1, 2} // Nodes 17000, 17001, 17002
|
|
} else {
|
|
// If we have fewer than 4 nodes, make all visible
|
|
p.visibleNodes = make([]int, len(p.nodes))
|
|
for i := range p.visibleNodes {
|
|
p.visibleNodes[i] = i
|
|
}
|
|
}
|
|
|
|
// Add cluster command interceptors to make standalone Redis appear as a cluster
|
|
if err := p.setupClusterInterceptors(); err != nil {
|
|
return fmt.Errorf("failed to setup cluster interceptors: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
return fmt.Errorf("proxy did not become ready")
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) addNode(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 := p.httpClient.Post(
|
|
p.apiBaseURL+"/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))
|
|
}
|
|
|
|
p.nodes = append(p.nodes, proxyNode{
|
|
listenPort: listenPort,
|
|
targetHost: targetHost,
|
|
targetPort: targetPort,
|
|
proxyAddr: fmt.Sprintf("localhost:%d", listenPort),
|
|
nodeID: fmt.Sprintf("localhost:%d:%d", listenPort, targetPort),
|
|
})
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) buildClusterSlotsResponse() string {
|
|
// Build CLUSTER SLOTS response dynamically based on VISIBLE nodes only
|
|
// Format: Array of slot ranges, each containing:
|
|
// - start slot (integer)
|
|
// - end slot (integer)
|
|
// - master node array: [host, port]
|
|
// - replica arrays (optional)
|
|
|
|
// For simplicity, divide slots equally among visible nodes
|
|
totalSlots := 16384
|
|
visibleCount := len(p.visibleNodes)
|
|
if visibleCount == 0 {
|
|
visibleCount = len(p.nodes)
|
|
}
|
|
slotsPerNode := totalSlots / visibleCount
|
|
|
|
response := fmt.Sprintf("*%d\r\n", visibleCount) // Number of slot ranges
|
|
|
|
for i, nodeIdx := range p.visibleNodes {
|
|
if nodeIdx >= len(p.nodes) {
|
|
continue
|
|
}
|
|
node := p.nodes[nodeIdx]
|
|
|
|
startSlot := i * slotsPerNode
|
|
endSlot := startSlot + slotsPerNode - 1
|
|
if i == visibleCount-1 {
|
|
endSlot = 16383 // Last node gets remaining slots
|
|
}
|
|
|
|
// Extract host and port from proxyAddr
|
|
host, portStr, _ := strings.Cut(node.proxyAddr, ":")
|
|
|
|
response += "*3\r\n" // 3 elements: start, end, master
|
|
response += fmt.Sprintf(":%d\r\n", startSlot)
|
|
response += fmt.Sprintf(":%d\r\n", endSlot)
|
|
response += "*2\r\n" // master info: 2 elements (host, port)
|
|
response += fmt.Sprintf("$%d\r\n%s\r\n", len(host), host)
|
|
response += fmt.Sprintf(":%s\r\n", portStr)
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) addClusterSlotsInterceptor() error {
|
|
clusterSlotsResponse := p.buildClusterSlotsResponse()
|
|
|
|
interceptor := map[string]interface{}{
|
|
"name": "cluster-slots",
|
|
"match": "*2\r\n$7\r\nCLUSTER\r\n$5\r\nSLOTS\r\n",
|
|
"response": clusterSlotsResponse,
|
|
"encoding": "raw",
|
|
}
|
|
|
|
jsonData, err := json.Marshal(interceptor)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal interceptor: %w", err)
|
|
}
|
|
|
|
resp, err := p.httpClient.Post(
|
|
p.apiBaseURL+"/interceptors",
|
|
"application/json",
|
|
bytes.NewReader(jsonData),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add interceptor: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("failed to add interceptor, status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) updateClusterSlotsInterceptor() error {
|
|
// The proxy doesn't support updating existing interceptors
|
|
// As a workaround, we'll send the updated CLUSTER SLOTS response directly to all clients
|
|
// This simulates what would happen when the client calls CLUSTER SLOTS after SMIGRATED
|
|
|
|
clusterSlotsResponse := p.buildClusterSlotsResponse()
|
|
|
|
// Send the updated CLUSTER SLOTS response to all connected clients
|
|
// This uses the /send-to-all-clients endpoint
|
|
payload := map[string]interface{}{
|
|
"data": clusterSlotsResponse,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal payload: %w", err)
|
|
}
|
|
|
|
resp, err := p.httpClient.Post(
|
|
p.apiBaseURL+"/send-to-all-clients",
|
|
"application/json",
|
|
bytes.NewReader(jsonData),
|
|
)
|
|
if err != nil {
|
|
// If this fails, it's not critical - the client will get the updated
|
|
// response when it calls CLUSTER SLOTS after receiving SMIGRATED
|
|
return nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) setupClusterInterceptors() error {
|
|
// Add CLUSTER SLOTS interceptor
|
|
if err := p.addClusterSlotsInterceptor(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Interceptors to add
|
|
interceptors := []map[string]interface{}{
|
|
{
|
|
"name": "client-maint-notifications-on-with-endpoint",
|
|
"match": "*5\r\n$6\r\nclient\r\n$19\r\nmaint_notifications\r\n$2\r\non\r\n$21\r\nmoving-endpoint-type\r\n$4\r\nnone\r\n",
|
|
"response": "+OK\r\n",
|
|
"encoding": "raw",
|
|
},
|
|
{
|
|
"name": "client-maint-notifications-off",
|
|
"match": "*3\r\n$6\r\nclient\r\n$19\r\nmaint_notifications\r\n$3\r\noff\r\n",
|
|
"response": "+OK\r\n",
|
|
"encoding": "raw",
|
|
},
|
|
}
|
|
|
|
// Add all interceptors
|
|
for _, interceptor := range interceptors {
|
|
jsonData, err := json.Marshal(interceptor)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal interceptor %s: %w", interceptor["name"], err)
|
|
}
|
|
|
|
resp, err := p.httpClient.Post(
|
|
p.apiBaseURL+"/interceptors",
|
|
"application/json",
|
|
bytes.NewReader(jsonData),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add interceptor %s: %w", interceptor["name"], err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("failed to add interceptor %s, status %d: %s", interceptor["name"], resp.StatusCode, string(body))
|
|
}
|
|
|
|
fmt.Printf("✓ Added %s interceptor to proxy\n", interceptor["name"])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) Stop() error {
|
|
if p.cmd != nil && p.cmd.Process != nil {
|
|
return p.cmd.Process.Kill()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) GetClusterAddrs() []string {
|
|
addrs := make([]string, len(p.nodes))
|
|
for i, node := range p.nodes {
|
|
addrs[i] = node.proxyAddr
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) IsReal() bool {
|
|
return false
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) GetTestModeConfig() *TestModeConfig {
|
|
return &TestModeConfig{
|
|
Mode: TestModeProxyMock,
|
|
NotificationDelay: 1 * time.Second,
|
|
ActionWaitTimeout: 10 * time.Second,
|
|
ActionPollInterval: 500 * time.Millisecond,
|
|
DatabaseReadyDelay: 1 * time.Second,
|
|
ConnectionEstablishDelay: 500 * time.Millisecond,
|
|
MaxClients: 1,
|
|
SkipMultiClientTests: true,
|
|
}
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error {
|
|
notification := formatSMigratingNotification(seqID, slots...)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error {
|
|
// Simulate topology change by swapping visible nodes
|
|
// If we have 4 nodes and currently showing [0,1,2], swap to [0,1,3]
|
|
// This simulates node 2 (17002) being replaced by node 3 (17003)
|
|
if len(p.nodes) >= 4 && len(p.visibleNodes) == 3 {
|
|
// Check if the hostPort matches node 3 (the "new" node)
|
|
if hostPort == p.nodes[3].proxyAddr {
|
|
// Swap node 2 for node 3 in visible nodes
|
|
p.visibleNodes = []int{0, 1, 3}
|
|
|
|
// Update CLUSTER SLOTS interceptor to reflect new topology
|
|
if err := p.updateClusterSlotsInterceptor(); err != nil {
|
|
return fmt.Errorf("failed to update CLUSTER SLOTS after migration: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format endpoint as "host:port slot1,slot2,range1-range2"
|
|
endpoint := fmt.Sprintf("%s %s", hostPort, strings.Join(slots, ","))
|
|
notification := formatSMigratedNotification(seqID, endpoint)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error {
|
|
notification := formatMovingNotification(seqID, timeS, endpoint)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectMIGRATING(ctx context.Context, seqID int64, slot int) error {
|
|
notification := formatMigratingNotification(seqID, slot)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectMIGRATED(ctx context.Context, seqID int64, slot int) error {
|
|
notification := formatMigratedNotification(seqID, slot)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectFAILING_OVER(ctx context.Context, seqID int64) error {
|
|
notification := formatFailingOverNotification(seqID)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) InjectFAILED_OVER(ctx context.Context, seqID int64) error {
|
|
notification := formatFailedOverNotification(seqID)
|
|
return p.injectNotification(notification)
|
|
}
|
|
|
|
func (p *ProxyNotificationInjector) injectNotification(notification string) error {
|
|
url := p.apiBaseURL + "/send-to-all-clients?encoding=raw"
|
|
resp, err := p.httpClient.Post(url, "application/octet-stream", strings.NewReader(notification))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to inject notification: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("injection failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper functions to format notifications
|
|
|
|
func formatMovingNotification(seqID int64, timeS int64, endpoint string) string {
|
|
// Format: ["MOVING", seqID, timeS, endpoint]
|
|
if endpoint == "" {
|
|
// 3 elements: MOVING, seqID, timeS
|
|
return fmt.Sprintf(">3\r\n$6\r\nMOVING\r\n:%d\r\n:%d\r\n", seqID, timeS)
|
|
}
|
|
// 4 elements: MOVING, seqID, timeS, endpoint
|
|
return 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(endpoint), endpoint)
|
|
}
|
|
|
|
func formatMigratingNotification(seqID int64, slot int) string {
|
|
slotStr := fmt.Sprintf("%d", slot)
|
|
return fmt.Sprintf(">3\r\n$9\r\nMIGRATING\r\n:%d\r\n$%d\r\n%s\r\n", seqID, len(slotStr), slotStr)
|
|
}
|
|
|
|
func formatMigratedNotification(seqID int64, slot int) string {
|
|
slotStr := fmt.Sprintf("%d", slot)
|
|
return fmt.Sprintf(">3\r\n$8\r\nMIGRATED\r\n:%d\r\n$%d\r\n%s\r\n", seqID, len(slotStr), slotStr)
|
|
}
|
|
|
|
func formatFailingOverNotification(seqID int64) string {
|
|
// Format: ["FAILING_OVER", seqID]
|
|
return fmt.Sprintf(">2\r\n$12\r\nFAILING_OVER\r\n:%d\r\n", seqID)
|
|
}
|
|
|
|
func formatFailedOverNotification(seqID int64) string {
|
|
// Format: ["FAILED_OVER", seqID]
|
|
return fmt.Sprintf(">2\r\n$11\r\nFAILED_OVER\r\n:%d\r\n", seqID)
|
|
}
|
|
|
|
// FaultInjectorNotificationInjector implements NotificationInjector using the real fault injector
|
|
type FaultInjectorNotificationInjector struct {
|
|
client *FaultInjectorClient
|
|
clusterAddrs []string
|
|
bdbID int
|
|
}
|
|
|
|
// NewFaultInjectorNotificationInjector creates a new fault injector-based notification injector
|
|
func NewFaultInjectorNotificationInjector(baseURL string) *FaultInjectorNotificationInjector {
|
|
// Get cluster addresses from environment
|
|
clusterAddrs := os.Getenv("CLUSTER_ADDRS")
|
|
if clusterAddrs == "" {
|
|
clusterAddrs = "localhost:6379"
|
|
}
|
|
|
|
bdbID := 1
|
|
if bdbIDStr := os.Getenv("BDB_ID"); bdbIDStr != "" {
|
|
_, _ = fmt.Sscanf(bdbIDStr, "%d", &bdbID)
|
|
}
|
|
|
|
return &FaultInjectorNotificationInjector{
|
|
client: NewFaultInjectorClient(baseURL),
|
|
clusterAddrs: strings.Split(clusterAddrs, ","),
|
|
bdbID: bdbID,
|
|
}
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) Start() error {
|
|
// Fault injector is already running, nothing to start
|
|
return nil
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) Stop() error {
|
|
// Fault injector keeps running, nothing to stop
|
|
return nil
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) GetClusterAddrs() []string {
|
|
return f.clusterAddrs
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) IsReal() bool {
|
|
return true
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) GetTestModeConfig() *TestModeConfig {
|
|
return &TestModeConfig{
|
|
Mode: TestModeRealFaultInjector,
|
|
NotificationDelay: 30 * time.Second,
|
|
ActionWaitTimeout: 5 * time.Minute, // Real fault injector can take up to 5 minutes
|
|
ActionPollInterval: 500 * time.Millisecond,
|
|
DatabaseReadyDelay: 10 * time.Second,
|
|
ConnectionEstablishDelay: 2 * time.Second,
|
|
MaxClients: 3,
|
|
SkipMultiClientTests: false,
|
|
}
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectSMIGRATING(ctx context.Context, seqID int64, slots ...string) error {
|
|
// For real fault injector, we trigger actual slot migration which will generate SMIGRATING
|
|
// Parse slot ranges
|
|
var startSlot, endSlot int
|
|
if len(slots) > 0 {
|
|
if strings.Contains(slots[0], "-") {
|
|
_, _ = fmt.Sscanf(slots[0], "%d-%d", &startSlot, &endSlot)
|
|
} else {
|
|
_, _ = fmt.Sscanf(slots[0], "%d", &startSlot)
|
|
endSlot = startSlot
|
|
}
|
|
}
|
|
|
|
// Trigger slot migration (this will generate SMIGRATING notification)
|
|
resp, err := f.client.TriggerSlotMigration(ctx, startSlot, endSlot, "node-1", "node-2")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to trigger slot migration: %w", err)
|
|
}
|
|
|
|
// Wait for action to start
|
|
_, err = f.client.WaitForAction(ctx, resp.ActionID, WithMaxWaitTime(10*time.Second))
|
|
return err
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectSMIGRATED(ctx context.Context, seqID int64, hostPort string, slots ...string) error {
|
|
// SMIGRATED is generated automatically when migration completes
|
|
// We can't directly inject it with the real fault injector
|
|
// This is a limitation of using the real fault injector
|
|
return fmt.Errorf("SMIGRATED cannot be directly injected with real fault injector - it's generated when migration completes")
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectMOVING(ctx context.Context, seqID int64, timeS int64, endpoint string) error {
|
|
// MOVING notifications are generated during bind action
|
|
return fmt.Errorf("MOVING cannot be directly injected with real fault injector - it's generated during bind action")
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectMIGRATING(ctx context.Context, seqID int64, slot int) error {
|
|
// Trigger slot migration for standalone
|
|
resp, err := f.client.TriggerSlotMigration(ctx, slot, slot, "node-1", "node-2")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to trigger slot migration: %w", err)
|
|
}
|
|
|
|
_, err = f.client.WaitForAction(ctx, resp.ActionID, WithMaxWaitTime(10*time.Second))
|
|
return err
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectMIGRATED(ctx context.Context, seqID int64, slot int) error {
|
|
// MIGRATED is generated automatically when migration completes
|
|
return fmt.Errorf("MIGRATED cannot be directly injected with real fault injector - it's generated when migration completes")
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectFAILING_OVER(ctx context.Context, seqID int64) error {
|
|
// FAILING_OVER is generated automatically when failover starts
|
|
return fmt.Errorf("FAILING_OVER cannot be directly injected with real fault injector - it's generated when failover starts")
|
|
}
|
|
|
|
func (f *FaultInjectorNotificationInjector) InjectFAILED_OVER(ctx context.Context, seqID int64) error {
|
|
// FAILED_OVER is generated automatically when failover completes
|
|
return fmt.Errorf("FAILED_OVER cannot be directly injected with real fault injector - it's generated when failover completes")
|
|
}
|