mirror of
https://github.com/redis/go-redis.git
synced 2025-11-26 06:23:09 +03:00
* wip * wip, used and unusable states * polish state machine * correct handling OnPut * better errors for tests, hook should work now * fix linter * improve reauth state management. fix tests * Update internal/pool/conn.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/pool/conn.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * better timeouts * empty endpoint handoff case * fix handoff state when queued for handoff * try to detect the deadlock * try to detect the deadlock x2 * delete should be called * improve tests * fix mark on uninitialized connection * Update internal/pool/conn_state_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/pool/conn_state_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/pool/pool.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/pool/conn_state.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update internal/pool/conn.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix error from copilot * address copilot comment * fix(pool): pool performance (#3565) * perf(pool): replace hookManager RWMutex with atomic.Pointer and add predefined state slices - Replace hookManager RWMutex with atomic.Pointer for lock-free reads in hot paths - Add predefined state slices to avoid allocations (validFromInUse, validFromCreatedOrIdle, etc.) - Add Clone() method to PoolHookManager for atomic updates - Update AddPoolHook/RemovePoolHook to use copy-on-write pattern - Update all hookManager access points to use atomic Load() Performance improvements: - Eliminates RWMutex contention in Get/Put/Remove hot paths - Reduces allocations by reusing predefined state slices - Lock-free reads allow better CPU cache utilization * perf(pool): eliminate mutex overhead in state machine hot path The state machine was calling notifyWaiters() on EVERY Get/Put operation, which acquired a mutex even when no waiters were present (the common case). Fix: Use atomic waiterCount to check for waiters BEFORE acquiring mutex. This eliminates mutex contention in the hot path (Get/Put operations). Implementation: - Added atomic.Int32 waiterCount field to ConnStateMachine - Increment when adding waiter, decrement when removing - Check waiterCount atomically before acquiring mutex in notifyWaiters() Performance impact: - Before: mutex lock/unlock on every Get/Put (even with no waiters) - After: lock-free atomic check, only acquire mutex if waiters exist - Expected improvement: ~30-50% for Get/Put operations * perf(pool): use predefined state slices to eliminate allocations in hot path The pool was creating new slice literals on EVERY Get/Put operation: - popIdle(): []ConnState{StateCreated, StateIdle} - putConn(): []ConnState{StateInUse} - CompareAndSwapUsed(): []ConnState{StateIdle} and []ConnState{StateInUse} - MarkUnusableForHandoff(): []ConnState{StateInUse, StateIdle, StateCreated} These allocations were happening millions of times per second in the hot path. Fix: Use predefined global slices defined in conn_state.go: - validFromInUse - validFromCreatedOrIdle - validFromCreatedInUseOrIdle Performance impact: - Before: 4 slice allocations per Get/Put cycle - After: 0 allocations (use predefined slices) - Expected improvement: ~30-40% reduction in allocations and GC pressure * perf(pool): optimize TryTransition to reduce atomic operations Further optimize the hot path by: 1. Remove redundant GetState() call in the loop 2. Only check waiterCount after successful CAS (not before loop) 3. Inline the waiterCount check to avoid notifyWaiters() call overhead This reduces atomic operations from 4-5 per Get/Put to 2-3: - Before: GetState() + CAS + waiterCount.Load() + notifyWaiters mutex check - After: CAS + waiterCount.Load() (only if CAS succeeds) Performance impact: - Eliminates 1-2 atomic operations per Get/Put - Expected improvement: ~10-15% for Get/Put operations * perf(pool): add fast path for Get/Put to match master performance Introduced TryTransitionFast() for the hot path (Get/Put operations): - Single CAS operation (same as master's atomic bool) - No waiter notification overhead - No loop through valid states - No error allocation Hot path flow: 1. popIdle(): Try IDLE → IN_USE (fast), fallback to CREATED → IN_USE 2. putConn(): Try IN_USE → IDLE (fast) This matches master's performance while preserving state machine for: - Background operations (handoff/reauth use UNUSABLE state) - State validation (TryTransition still available) - Waiter notification (AwaitAndTransition for blocking) Performance comparison per Get/Put cycle: - Master: 2 atomic CAS operations - State machine (before): 5 atomic operations (2.5x slower) - State machine (after): 2 atomic CAS operations (same as master!) Expected improvement: Restore to baseline ~11,373 ops/sec * combine cas * fix linter * try faster approach * fast semaphore * better inlining for hot path * fix linter issues * use new semaphore in auth as well * linter should be happy now * add comments * Update internal/pool/conn_state.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * address comment * slight reordering * try to cache time if for non-critical calculation * fix wrong benchmark * add concurrent test * fix benchmark report * add additional expect to check output * comment and variable rename --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * initConn sets IDLE state - Handle unexpected conn state changes * fix precision of time cache and usedAt * allow e2e tests to run longer * Fix broken initialization of idle connections * optimize push notif * 100ms -> 50ms * use correct timer for last health check * verify pass auth on conn creation * fix assertion * fix unsafe test * fix benchmark test * improve remove conn * re doesn't support requirepass * wait more in e2e test * flaky test * add missed method in interface * fix test assertions * silence logs and faster hooks manager * address linter comment * fix flaky test * use read instad of control * use pool size for semsize * CAS instead of reading the state * preallocate errors and states * preallocate state slices * fix flaky test * fix fast semaphore that could have been starved * try to fix the semaphore * should properly notify the waiters - this way a waiter that timesout at the same time a releaser is releasing, won't throw token. the releaser will fail to notify and will pick another waiter. this hybrid approach should be faster than channels and maintains FIFO * waiter may double-release (if closed/times out) * priority of operations * use simple approach of fifo waiters * use simple channel based semaphores * address linter and tests * remove unused benchs * change log message * address pr comments * address pr comments * fix data race --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1118 lines
33 KiB
Go
1118 lines
33 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/redis/go-redis/v9/maintnotifications"
|
|
)
|
|
|
|
// DatabaseEndpoint represents a single database endpoint configuration
|
|
type DatabaseEndpoint struct {
|
|
Addr []string `json:"addr"`
|
|
AddrType string `json:"addr_type"`
|
|
DNSName string `json:"dns_name"`
|
|
OSSClusterAPIPreferredEndpointType string `json:"oss_cluster_api_preferred_endpoint_type"`
|
|
OSSClusterAPIPreferredIPType string `json:"oss_cluster_api_preferred_ip_type"`
|
|
Port int `json:"port"`
|
|
ProxyPolicy string `json:"proxy_policy"`
|
|
UID string `json:"uid"`
|
|
}
|
|
|
|
// EnvDatabaseConfig represents the configuration for a single database
|
|
type EnvDatabaseConfig struct {
|
|
BdbID interface{} `json:"bdb_id,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
Password string `json:"password,omitempty"`
|
|
TLS bool `json:"tls"`
|
|
CertificatesLocation string `json:"certificatesLocation,omitempty"`
|
|
RawEndpoints []DatabaseEndpoint `json:"raw_endpoints,omitempty"`
|
|
Endpoints []string `json:"endpoints"`
|
|
}
|
|
|
|
// EnvDatabasesConfig represents the complete configuration file structure
|
|
type EnvDatabasesConfig map[string]EnvDatabaseConfig
|
|
|
|
// EnvConfig represents environment configuration for test scenarios
|
|
type EnvConfig struct {
|
|
RedisEndpointsConfigPath string
|
|
FaultInjectorURL string
|
|
}
|
|
|
|
// RedisConnectionConfig represents Redis connection parameters
|
|
type RedisConnectionConfig struct {
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
TLS bool
|
|
BdbID int
|
|
CertificatesLocation string
|
|
Endpoints []string
|
|
}
|
|
|
|
// GetEnvConfig reads environment variables required for the test scenario
|
|
func GetEnvConfig() (*EnvConfig, error) {
|
|
redisConfigPath := os.Getenv("REDIS_ENDPOINTS_CONFIG_PATH")
|
|
if redisConfigPath == "" {
|
|
return nil, fmt.Errorf("REDIS_ENDPOINTS_CONFIG_PATH environment variable must be set")
|
|
}
|
|
|
|
faultInjectorURL := os.Getenv("FAULT_INJECTION_API_URL")
|
|
if faultInjectorURL == "" {
|
|
// Default to localhost if not set
|
|
faultInjectorURL = "http://localhost:8080"
|
|
}
|
|
|
|
return &EnvConfig{
|
|
RedisEndpointsConfigPath: redisConfigPath,
|
|
FaultInjectorURL: faultInjectorURL,
|
|
}, nil
|
|
}
|
|
|
|
// GetDatabaseConfigFromEnv reads database configuration from a file
|
|
func GetDatabaseConfigFromEnv(filePath string) (EnvDatabasesConfig, error) {
|
|
fileContent, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read database config from %s: %w", filePath, err)
|
|
}
|
|
|
|
var config EnvDatabasesConfig
|
|
if err := json.Unmarshal(fileContent, &config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse database config from %s: %w", filePath, err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// GetDatabaseConfig gets Redis connection parameters for a specific database
|
|
func GetDatabaseConfig(databasesConfig EnvDatabasesConfig, databaseName string) (*RedisConnectionConfig, error) {
|
|
var dbConfig EnvDatabaseConfig
|
|
var exists bool
|
|
|
|
if databaseName == "" {
|
|
// Get the first database if no name is provided
|
|
for _, config := range databasesConfig {
|
|
dbConfig = config
|
|
exists = true
|
|
break
|
|
}
|
|
} else {
|
|
dbConfig, exists = databasesConfig[databaseName]
|
|
}
|
|
|
|
if !exists {
|
|
return nil, fmt.Errorf("database %s not found in configuration", databaseName)
|
|
}
|
|
|
|
// Parse connection details from endpoints or raw_endpoints
|
|
var host string
|
|
var port int
|
|
|
|
if len(dbConfig.RawEndpoints) > 0 {
|
|
// Use raw_endpoints if available (for more complex configurations)
|
|
endpoint := dbConfig.RawEndpoints[0] // Use the first endpoint
|
|
host = endpoint.DNSName
|
|
port = endpoint.Port
|
|
} else if len(dbConfig.Endpoints) > 0 {
|
|
// Parse from endpoints URLs
|
|
endpointURL, err := url.Parse(dbConfig.Endpoints[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse endpoint URL %s: %w", dbConfig.Endpoints[0], err)
|
|
}
|
|
|
|
host = endpointURL.Hostname()
|
|
portStr := endpointURL.Port()
|
|
if portStr == "" {
|
|
// Default ports based on scheme
|
|
switch endpointURL.Scheme {
|
|
case "redis":
|
|
port = 6379
|
|
case "rediss":
|
|
port = 6380
|
|
default:
|
|
port = 6379
|
|
}
|
|
} else {
|
|
port, err = strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port in endpoint URL %s: %w", dbConfig.Endpoints[0], err)
|
|
}
|
|
}
|
|
|
|
// Override TLS setting based on scheme if not explicitly set
|
|
if endpointURL.Scheme == "rediss" {
|
|
dbConfig.TLS = true
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("no endpoints found in database configuration")
|
|
}
|
|
|
|
var bdbId int
|
|
switch (dbConfig.BdbID).(type) {
|
|
case int:
|
|
bdbId = dbConfig.BdbID.(int)
|
|
case float64:
|
|
bdbId = int(dbConfig.BdbID.(float64))
|
|
case string:
|
|
bdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))
|
|
}
|
|
|
|
return &RedisConnectionConfig{
|
|
Host: host,
|
|
Port: port,
|
|
Username: dbConfig.Username,
|
|
Password: dbConfig.Password,
|
|
TLS: dbConfig.TLS,
|
|
BdbID: bdbId,
|
|
CertificatesLocation: dbConfig.CertificatesLocation,
|
|
Endpoints: dbConfig.Endpoints,
|
|
}, nil
|
|
}
|
|
|
|
// ConvertEnvDatabaseConfigToRedisConnectionConfig converts EnvDatabaseConfig to RedisConnectionConfig
|
|
func ConvertEnvDatabaseConfigToRedisConnectionConfig(dbConfig EnvDatabaseConfig) (*RedisConnectionConfig, error) {
|
|
// Parse connection details from endpoints or raw_endpoints
|
|
var host string
|
|
var port int
|
|
|
|
if len(dbConfig.RawEndpoints) > 0 {
|
|
// Use raw_endpoints if available (for more complex configurations)
|
|
endpoint := dbConfig.RawEndpoints[0] // Use the first endpoint
|
|
host = endpoint.DNSName
|
|
port = endpoint.Port
|
|
} else if len(dbConfig.Endpoints) > 0 {
|
|
// Parse from endpoints URLs
|
|
endpointURL, err := url.Parse(dbConfig.Endpoints[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse endpoint URL %s: %w", dbConfig.Endpoints[0], err)
|
|
}
|
|
|
|
host = endpointURL.Hostname()
|
|
portStr := endpointURL.Port()
|
|
if portStr == "" {
|
|
// Default ports based on scheme
|
|
switch endpointURL.Scheme {
|
|
case "redis":
|
|
port = 6379
|
|
case "rediss":
|
|
port = 6380
|
|
default:
|
|
port = 6379
|
|
}
|
|
} else {
|
|
port, err = strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port in endpoint URL %s: %w", dbConfig.Endpoints[0], err)
|
|
}
|
|
}
|
|
|
|
// Override TLS setting based on scheme if not explicitly set
|
|
if endpointURL.Scheme == "rediss" {
|
|
dbConfig.TLS = true
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("no endpoints found in database configuration")
|
|
}
|
|
|
|
var bdbId int
|
|
switch dbConfig.BdbID.(type) {
|
|
case int:
|
|
bdbId = dbConfig.BdbID.(int)
|
|
case float64:
|
|
bdbId = int(dbConfig.BdbID.(float64))
|
|
case string:
|
|
bdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))
|
|
}
|
|
|
|
return &RedisConnectionConfig{
|
|
Host: host,
|
|
Port: port,
|
|
Username: dbConfig.Username,
|
|
Password: dbConfig.Password,
|
|
TLS: dbConfig.TLS,
|
|
BdbID: bdbId,
|
|
CertificatesLocation: dbConfig.CertificatesLocation,
|
|
Endpoints: dbConfig.Endpoints,
|
|
}, nil
|
|
}
|
|
|
|
// ClientFactory manages Redis client creation and lifecycle
|
|
type ClientFactory struct {
|
|
config *RedisConnectionConfig
|
|
clients map[string]redis.UniversalClient
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewClientFactory creates a new client factory with the specified configuration
|
|
func NewClientFactory(config *RedisConnectionConfig) *ClientFactory {
|
|
return &ClientFactory{
|
|
config: config,
|
|
clients: make(map[string]redis.UniversalClient),
|
|
}
|
|
}
|
|
|
|
// CreateClientOptions represents options for creating Redis clients
|
|
type CreateClientOptions struct {
|
|
Protocol int
|
|
MaintNotificationsConfig *maintnotifications.Config
|
|
MaxRetries int
|
|
PoolSize int
|
|
MinIdleConns int
|
|
MaxActiveConns int
|
|
ClientName string
|
|
DB int
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
}
|
|
|
|
// DefaultCreateClientOptions returns default options for creating Redis clients
|
|
func DefaultCreateClientOptions() *CreateClientOptions {
|
|
return &CreateClientOptions{
|
|
Protocol: 3, // RESP3 by default for push notifications
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
HandoffTimeout: 30 * time.Second,
|
|
RelaxedTimeout: 10 * time.Second,
|
|
MaxWorkers: 20,
|
|
},
|
|
MaxRetries: 3,
|
|
PoolSize: 10,
|
|
MinIdleConns: 10,
|
|
MaxActiveConns: 10,
|
|
}
|
|
}
|
|
|
|
func (cf *ClientFactory) PrintPoolStats(t *testing.T) {
|
|
cf.mutex.RLock()
|
|
defer cf.mutex.RUnlock()
|
|
|
|
for key, client := range cf.clients {
|
|
stats := client.PoolStats()
|
|
t.Logf("Pool stats for client %s: %+v", key, stats)
|
|
}
|
|
}
|
|
|
|
// Create creates a new Redis client with the specified options and connects it
|
|
func (cf *ClientFactory) Create(key string, options *CreateClientOptions) (redis.UniversalClient, error) {
|
|
if options == nil {
|
|
options = DefaultCreateClientOptions()
|
|
}
|
|
|
|
cf.mutex.Lock()
|
|
defer cf.mutex.Unlock()
|
|
|
|
// Check if client already exists
|
|
if client, exists := cf.clients[key]; exists {
|
|
return client, nil
|
|
}
|
|
|
|
var client redis.UniversalClient
|
|
var opts interface{}
|
|
|
|
// Determine if this is a cluster configuration
|
|
if len(cf.config.Endpoints) > 1 || cf.isClusterEndpoint() {
|
|
// Create cluster client
|
|
clusterOptions := &redis.ClusterOptions{
|
|
Addrs: cf.getAddresses(),
|
|
Username: cf.config.Username,
|
|
Password: cf.config.Password,
|
|
Protocol: options.Protocol,
|
|
MaintNotificationsConfig: options.MaintNotificationsConfig,
|
|
MaxRetries: options.MaxRetries,
|
|
PoolSize: options.PoolSize,
|
|
MinIdleConns: options.MinIdleConns,
|
|
MaxActiveConns: options.MaxActiveConns,
|
|
ClientName: options.ClientName,
|
|
}
|
|
|
|
if options.ReadTimeout > 0 {
|
|
clusterOptions.ReadTimeout = options.ReadTimeout
|
|
}
|
|
if options.WriteTimeout > 0 {
|
|
clusterOptions.WriteTimeout = options.WriteTimeout
|
|
}
|
|
|
|
if cf.config.TLS {
|
|
clusterOptions.TLSConfig = &tls.Config{
|
|
InsecureSkipVerify: true, // For testing purposes
|
|
}
|
|
}
|
|
|
|
opts = clusterOptions
|
|
client = redis.NewClusterClient(clusterOptions)
|
|
} else {
|
|
// Create single client
|
|
clientOptions := &redis.Options{
|
|
Addr: fmt.Sprintf("%s:%d", cf.config.Host, cf.config.Port),
|
|
Username: cf.config.Username,
|
|
Password: cf.config.Password,
|
|
DB: options.DB,
|
|
Protocol: options.Protocol,
|
|
MaintNotificationsConfig: options.MaintNotificationsConfig,
|
|
MaxRetries: options.MaxRetries,
|
|
PoolSize: options.PoolSize,
|
|
MinIdleConns: options.MinIdleConns,
|
|
MaxActiveConns: options.MaxActiveConns,
|
|
ClientName: options.ClientName,
|
|
}
|
|
|
|
if options.ReadTimeout > 0 {
|
|
clientOptions.ReadTimeout = options.ReadTimeout
|
|
}
|
|
if options.WriteTimeout > 0 {
|
|
clientOptions.WriteTimeout = options.WriteTimeout
|
|
}
|
|
|
|
if cf.config.TLS {
|
|
clientOptions.TLSConfig = &tls.Config{
|
|
InsecureSkipVerify: true, // For testing purposes
|
|
}
|
|
}
|
|
|
|
opts = clientOptions
|
|
client = redis.NewClient(clientOptions)
|
|
}
|
|
|
|
if err := client.Ping(context.Background()).Err(); err != nil {
|
|
return nil, fmt.Errorf("failed to connect to Redis: %w\nOptions: %+v", err, opts)
|
|
}
|
|
|
|
// Store the client
|
|
cf.clients[key] = client
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// Get retrieves an existing client by key or the first one if no key is provided
|
|
func (cf *ClientFactory) Get(key string) redis.UniversalClient {
|
|
cf.mutex.RLock()
|
|
defer cf.mutex.RUnlock()
|
|
|
|
if key != "" {
|
|
return cf.clients[key]
|
|
}
|
|
|
|
// Return the first client if no key is provided
|
|
for _, client := range cf.clients {
|
|
return client
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAll returns all created clients
|
|
func (cf *ClientFactory) GetAll() map[string]redis.UniversalClient {
|
|
cf.mutex.RLock()
|
|
defer cf.mutex.RUnlock()
|
|
|
|
result := make(map[string]redis.UniversalClient)
|
|
for key, client := range cf.clients {
|
|
result[key] = client
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// DestroyAll closes and removes all created clients
|
|
func (cf *ClientFactory) DestroyAll() error {
|
|
cf.mutex.Lock()
|
|
defer cf.mutex.Unlock()
|
|
|
|
var lastErr error
|
|
for key, client := range cf.clients {
|
|
if err := client.Close(); err != nil {
|
|
lastErr = err
|
|
}
|
|
delete(cf.clients, key)
|
|
}
|
|
|
|
return lastErr
|
|
}
|
|
|
|
// Destroy closes and removes a specific client
|
|
func (cf *ClientFactory) Destroy(key string) error {
|
|
cf.mutex.Lock()
|
|
defer cf.mutex.Unlock()
|
|
|
|
client, exists := cf.clients[key]
|
|
if !exists {
|
|
return fmt.Errorf("client %s not found", key)
|
|
}
|
|
|
|
err := client.Close()
|
|
delete(cf.clients, key)
|
|
return err
|
|
}
|
|
|
|
// GetConfig returns the connection configuration
|
|
func (cf *ClientFactory) GetConfig() *RedisConnectionConfig {
|
|
return cf.config
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
// isClusterEndpoint determines if the configuration represents a cluster
|
|
func (cf *ClientFactory) isClusterEndpoint() bool {
|
|
// Check if any endpoint contains cluster-related keywords
|
|
for _, endpoint := range cf.config.Endpoints {
|
|
if strings.Contains(strings.ToLower(endpoint), "cluster") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check if we have multiple raw endpoints
|
|
if len(cf.config.Endpoints) > 1 {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// getAddresses returns a list of addresses for cluster configuration
|
|
func (cf *ClientFactory) getAddresses() []string {
|
|
if len(cf.config.Endpoints) > 0 {
|
|
addresses := make([]string, 0, len(cf.config.Endpoints))
|
|
for _, endpoint := range cf.config.Endpoints {
|
|
if parsedURL, err := url.Parse(endpoint); err == nil {
|
|
addr := parsedURL.Host
|
|
if addr != "" {
|
|
addresses = append(addresses, addr)
|
|
}
|
|
}
|
|
}
|
|
if len(addresses) > 0 {
|
|
return addresses
|
|
}
|
|
}
|
|
|
|
// Fallback to single address
|
|
return []string{fmt.Sprintf("%s:%d", cf.config.Host, cf.config.Port)}
|
|
}
|
|
|
|
// Utility functions for common test scenarios
|
|
|
|
// CreateTestClientFactory creates a client factory from environment configuration
|
|
func CreateTestClientFactory(databaseName string) (*ClientFactory, error) {
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get environment config: %w", err)
|
|
}
|
|
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get database config: %w", err)
|
|
}
|
|
|
|
dbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get database config for %s: %w", databaseName, err)
|
|
}
|
|
|
|
return NewClientFactory(dbConfig), nil
|
|
}
|
|
|
|
// CreateTestClientFactoryWithBdbID creates a client factory using a specific bdb_id
|
|
// This is useful when you've created a fresh database and want to connect to it
|
|
func CreateTestClientFactoryWithBdbID(databaseName string, bdbID int) (*ClientFactory, error) {
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get environment config: %w", err)
|
|
}
|
|
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get database config: %w", err)
|
|
}
|
|
|
|
dbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get database config for %s: %w", databaseName, err)
|
|
}
|
|
|
|
// Override the bdb_id with the newly created database ID
|
|
dbConfig.BdbID = bdbID
|
|
|
|
return NewClientFactory(dbConfig), nil
|
|
}
|
|
|
|
// CreateTestFaultInjector creates a fault injector client from environment configuration
|
|
func CreateTestFaultInjector() (*FaultInjectorClient, error) {
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get environment config: %w", err)
|
|
}
|
|
|
|
return NewFaultInjectorClient(envConfig.FaultInjectorURL), nil
|
|
}
|
|
|
|
// GetAvailableDatabases returns a list of available database names from the configuration
|
|
func GetAvailableDatabases(configPath string) ([]string, error) {
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(configPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
databases := make([]string, 0, len(databasesConfig))
|
|
for name := range databasesConfig {
|
|
databases = append(databases, name)
|
|
}
|
|
|
|
return databases, nil
|
|
}
|
|
|
|
// ConvertEnvDatabaseConfigToFaultInjectorConfig converts EnvDatabaseConfig to fault injector DatabaseConfig
|
|
func ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig EnvDatabaseConfig, name string) (DatabaseConfig, error) {
|
|
var port int
|
|
|
|
// Extract port and DNS name from raw_endpoints or endpoints
|
|
if len(envConfig.RawEndpoints) > 0 {
|
|
endpoint := envConfig.RawEndpoints[0]
|
|
port = endpoint.Port
|
|
} else if len(envConfig.Endpoints) > 0 {
|
|
endpointURL, err := url.Parse(envConfig.Endpoints[0])
|
|
if err != nil {
|
|
return DatabaseConfig{}, fmt.Errorf("failed to parse endpoint URL: %w", err)
|
|
}
|
|
portStr := endpointURL.Port()
|
|
if portStr != "" {
|
|
port, err = strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return DatabaseConfig{}, fmt.Errorf("invalid port: %w", err)
|
|
}
|
|
} else {
|
|
port = 6379 * 2 // default*2
|
|
}
|
|
} else {
|
|
return DatabaseConfig{}, fmt.Errorf("no endpoints found in configuration")
|
|
}
|
|
|
|
randomPortOffset := 1 + rand.Intn(10) // Random port offset to avoid conflicts
|
|
|
|
// Build the database config for fault injector
|
|
// TODO: Make this configurable
|
|
// IT is the defaults for a sharded database at the moment
|
|
dbConfig := DatabaseConfig{
|
|
Name: name,
|
|
Port: port + randomPortOffset,
|
|
MemorySize: 268435456, // 256MB default
|
|
Replication: true,
|
|
EvictionPolicy: "noeviction",
|
|
ProxyPolicy: "single",
|
|
AutoUpgrade: true,
|
|
Sharding: true,
|
|
ShardsCount: 2,
|
|
ShardKeyRegex: []ShardKeyRegexPattern{
|
|
{Regex: ".*\\{(?<tag>.*)\\}.*"},
|
|
{Regex: "(?<tag>.*)"},
|
|
},
|
|
ShardsPlacement: "dense",
|
|
ModuleList: []DatabaseModule{
|
|
{ModuleArgs: "", ModuleName: "ReJSON"},
|
|
{ModuleArgs: "", ModuleName: "search"},
|
|
{ModuleArgs: "", ModuleName: "timeseries"},
|
|
{ModuleArgs: "", ModuleName: "bf"},
|
|
},
|
|
OSSCluster: false,
|
|
}
|
|
|
|
// If we have raw_endpoints with cluster info, configure for cluster
|
|
if len(envConfig.RawEndpoints) > 0 {
|
|
endpoint := envConfig.RawEndpoints[0]
|
|
|
|
// Check if this is a cluster configuration
|
|
if endpoint.ProxyPolicy != "" && endpoint.ProxyPolicy != "single" {
|
|
dbConfig.OSSCluster = true
|
|
dbConfig.Sharding = true
|
|
dbConfig.ShardsCount = 3 // default for cluster
|
|
dbConfig.ProxyPolicy = endpoint.ProxyPolicy
|
|
dbConfig.Replication = true
|
|
}
|
|
|
|
if endpoint.OSSClusterAPIPreferredIPType != "" {
|
|
dbConfig.OSSClusterAPIPreferredIPType = endpoint.OSSClusterAPIPreferredIPType
|
|
}
|
|
}
|
|
|
|
return dbConfig, nil
|
|
}
|
|
|
|
// TestDatabaseManager manages database lifecycle for tests
|
|
type TestDatabaseManager struct {
|
|
faultInjector *FaultInjectorClient
|
|
clusterIndex int
|
|
createdBdbID int
|
|
dbConfig DatabaseConfig
|
|
t *testing.T
|
|
}
|
|
|
|
// NewTestDatabaseManager creates a new test database manager
|
|
func NewTestDatabaseManager(t *testing.T, faultInjector *FaultInjectorClient, clusterIndex int) *TestDatabaseManager {
|
|
return &TestDatabaseManager{
|
|
faultInjector: faultInjector,
|
|
clusterIndex: clusterIndex,
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
// CreateDatabaseFromEnvConfig creates a database using EnvDatabaseConfig
|
|
func (m *TestDatabaseManager) CreateDatabaseFromEnvConfig(ctx context.Context, envConfig EnvDatabaseConfig, name string) (int, error) {
|
|
// Convert EnvDatabaseConfig to DatabaseConfig
|
|
dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig, name)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to convert config: %w", err)
|
|
}
|
|
|
|
m.dbConfig = dbConfig
|
|
return m.CreateDatabase(ctx, dbConfig)
|
|
}
|
|
|
|
// CreateDatabase creates a database and waits for it to be ready
|
|
// Returns the bdb_id of the created database
|
|
func (m *TestDatabaseManager) CreateDatabase(ctx context.Context, dbConfig DatabaseConfig) (int, error) {
|
|
resp, err := m.faultInjector.CreateDatabase(ctx, m.clusterIndex, dbConfig)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to trigger database creation: %w", err)
|
|
}
|
|
|
|
// Wait for creation to complete
|
|
status, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,
|
|
WithMaxWaitTime(5*time.Minute),
|
|
WithPollInterval(5*time.Second))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to wait for database creation: %w", err)
|
|
}
|
|
|
|
if status.Status != StatusSuccess {
|
|
return 0, fmt.Errorf("database creation failed: %v", status.Error)
|
|
}
|
|
|
|
// Extract bdb_id from output
|
|
var bdbID int
|
|
if status.Output != nil {
|
|
if id, ok := status.Output["bdb_id"].(float64); ok {
|
|
bdbID = int(id)
|
|
} else if resultMap, ok := status.Output["result"].(map[string]interface{}); ok {
|
|
if id, ok := resultMap["bdb_id"].(float64); ok {
|
|
bdbID = int(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if bdbID == 0 {
|
|
return 0, fmt.Errorf("failed to extract bdb_id from creation output")
|
|
}
|
|
|
|
m.createdBdbID = bdbID
|
|
|
|
return bdbID, nil
|
|
}
|
|
|
|
// CreateDatabaseAndGetConfig creates a database and returns both the bdb_id and the full connection config from the fault injector response
|
|
// This includes endpoints, username, password, TLS settings, and raw_endpoints
|
|
func (m *TestDatabaseManager) CreateDatabaseAndGetConfig(ctx context.Context, dbConfig DatabaseConfig) (int, EnvDatabaseConfig, error) {
|
|
resp, err := m.faultInjector.CreateDatabase(ctx, m.clusterIndex, dbConfig)
|
|
if err != nil {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to trigger database creation: %w", err)
|
|
}
|
|
|
|
// Wait for creation to complete
|
|
status, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,
|
|
WithMaxWaitTime(5*time.Minute),
|
|
WithPollInterval(5*time.Second))
|
|
if err != nil {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to wait for database creation: %w", err)
|
|
}
|
|
|
|
if status.Status != StatusSuccess {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("database creation failed: %v", status.Error)
|
|
}
|
|
|
|
// Extract database configuration from output
|
|
var envConfig EnvDatabaseConfig
|
|
if status.Output == nil {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("no output in creation response")
|
|
}
|
|
|
|
// Extract bdb_id
|
|
var bdbID int
|
|
if id, ok := status.Output["bdb_id"].(float64); ok {
|
|
bdbID = int(id)
|
|
envConfig.BdbID = bdbID
|
|
} else {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to extract bdb_id from creation output")
|
|
}
|
|
|
|
// Extract username
|
|
if username, ok := status.Output["username"].(string); ok {
|
|
envConfig.Username = username
|
|
}
|
|
|
|
// Extract password
|
|
if password, ok := status.Output["password"].(string); ok {
|
|
envConfig.Password = password
|
|
}
|
|
|
|
// Extract TLS setting
|
|
if tls, ok := status.Output["tls"].(bool); ok {
|
|
envConfig.TLS = tls
|
|
}
|
|
|
|
// Extract endpoints
|
|
if endpoints, ok := status.Output["endpoints"].([]interface{}); ok {
|
|
envConfig.Endpoints = make([]string, 0, len(endpoints))
|
|
for _, ep := range endpoints {
|
|
if epStr, ok := ep.(string); ok {
|
|
envConfig.Endpoints = append(envConfig.Endpoints, epStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract raw_endpoints
|
|
if rawEndpoints, ok := status.Output["raw_endpoints"].([]interface{}); ok {
|
|
envConfig.RawEndpoints = make([]DatabaseEndpoint, 0, len(rawEndpoints))
|
|
for _, rawEp := range rawEndpoints {
|
|
if rawEpMap, ok := rawEp.(map[string]interface{}); ok {
|
|
var dbEndpoint DatabaseEndpoint
|
|
|
|
// Extract addr
|
|
if addr, ok := rawEpMap["addr"].([]interface{}); ok {
|
|
dbEndpoint.Addr = make([]string, 0, len(addr))
|
|
for _, a := range addr {
|
|
if aStr, ok := a.(string); ok {
|
|
dbEndpoint.Addr = append(dbEndpoint.Addr, aStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract other fields
|
|
if addrType, ok := rawEpMap["addr_type"].(string); ok {
|
|
dbEndpoint.AddrType = addrType
|
|
}
|
|
if dnsName, ok := rawEpMap["dns_name"].(string); ok {
|
|
dbEndpoint.DNSName = dnsName
|
|
}
|
|
if preferredEndpointType, ok := rawEpMap["oss_cluster_api_preferred_endpoint_type"].(string); ok {
|
|
dbEndpoint.OSSClusterAPIPreferredEndpointType = preferredEndpointType
|
|
}
|
|
if preferredIPType, ok := rawEpMap["oss_cluster_api_preferred_ip_type"].(string); ok {
|
|
dbEndpoint.OSSClusterAPIPreferredIPType = preferredIPType
|
|
}
|
|
if port, ok := rawEpMap["port"].(float64); ok {
|
|
dbEndpoint.Port = int(port)
|
|
}
|
|
if proxyPolicy, ok := rawEpMap["proxy_policy"].(string); ok {
|
|
dbEndpoint.ProxyPolicy = proxyPolicy
|
|
}
|
|
if uid, ok := rawEpMap["uid"].(string); ok {
|
|
dbEndpoint.UID = uid
|
|
}
|
|
|
|
envConfig.RawEndpoints = append(envConfig.RawEndpoints, dbEndpoint)
|
|
}
|
|
}
|
|
}
|
|
|
|
m.createdBdbID = bdbID
|
|
return bdbID, envConfig, nil
|
|
}
|
|
|
|
// DeleteDatabase deletes the created database
|
|
func (m *TestDatabaseManager) DeleteDatabase(ctx context.Context) error {
|
|
if m.createdBdbID == 0 {
|
|
return fmt.Errorf("no database to delete (bdb_id is 0)")
|
|
}
|
|
|
|
resp, err := m.faultInjector.DeleteDatabase(ctx, m.clusterIndex, m.createdBdbID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to trigger database deletion: %w", err)
|
|
}
|
|
|
|
// Wait for deletion to complete
|
|
status, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,
|
|
WithMaxWaitTime(2*time.Minute),
|
|
WithPollInterval(3*time.Second))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to wait for database deletion: %w", err)
|
|
}
|
|
|
|
if status.Status != StatusSuccess {
|
|
return fmt.Errorf("database deletion failed: %v", status.Error)
|
|
}
|
|
|
|
m.createdBdbID = 0
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBdbID returns the created database ID
|
|
func (m *TestDatabaseManager) GetBdbID() int {
|
|
return m.createdBdbID
|
|
}
|
|
|
|
// Cleanup ensures the database is deleted (safe to call multiple times)
|
|
func (m *TestDatabaseManager) Cleanup(ctx context.Context) {
|
|
if m.createdBdbID != 0 {
|
|
if err := m.DeleteDatabase(ctx); err != nil {
|
|
m.t.Logf("Warning: Failed to cleanup database: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetupTestDatabaseFromEnv creates a database from environment config and returns a cleanup function
|
|
// Usage:
|
|
//
|
|
// cleanup := SetupTestDatabaseFromEnv(t, ctx, "my-test-db")
|
|
// defer cleanup()
|
|
func SetupTestDatabaseFromEnv(t *testing.T, ctx context.Context, databaseName string) (bdbID int, cleanup func()) {
|
|
// Get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get environment config: %v", err)
|
|
}
|
|
|
|
// Get database config from environment
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get database config: %v", err)
|
|
}
|
|
|
|
// Get the specific database config
|
|
var envDbConfig EnvDatabaseConfig
|
|
var exists bool
|
|
if databaseName == "" {
|
|
// Get first database if no name provided
|
|
for _, config := range databasesConfig {
|
|
envDbConfig = config
|
|
exists = true
|
|
break
|
|
}
|
|
} else {
|
|
envDbConfig, exists = databasesConfig[databaseName]
|
|
}
|
|
|
|
if !exists {
|
|
t.Fatalf("Database %s not found in configuration", databaseName)
|
|
}
|
|
|
|
// Create fault injector
|
|
faultInjector, err := CreateTestFaultInjector()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create fault injector: %v", err)
|
|
}
|
|
|
|
// Create database manager
|
|
dbManager := NewTestDatabaseManager(t, faultInjector, 0)
|
|
|
|
// Create the database
|
|
testDBName := fmt.Sprintf("e2e-test-%s-%d", databaseName, time.Now().Unix())
|
|
bdbID, err = dbManager.CreateDatabaseFromEnvConfig(ctx, envDbConfig, testDBName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Return cleanup function
|
|
cleanup = func() {
|
|
dbManager.Cleanup(ctx)
|
|
}
|
|
|
|
return bdbID, cleanup
|
|
}
|
|
|
|
// SetupTestDatabaseWithConfig creates a database with custom config and returns a cleanup function
|
|
// Usage:
|
|
//
|
|
// bdbID, cleanup := SetupTestDatabaseWithConfig(t, ctx, dbConfig)
|
|
// defer cleanup()
|
|
func SetupTestDatabaseWithConfig(t *testing.T, ctx context.Context, dbConfig DatabaseConfig) (bdbID int, cleanup func()) {
|
|
// Create fault injector
|
|
faultInjector, err := CreateTestFaultInjector()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create fault injector: %v", err)
|
|
}
|
|
|
|
// Create database manager
|
|
dbManager := NewTestDatabaseManager(t, faultInjector, 0)
|
|
|
|
// Create the database
|
|
bdbID, err = dbManager.CreateDatabase(ctx, dbConfig)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Return cleanup function
|
|
cleanup = func() {
|
|
dbManager.Cleanup(ctx)
|
|
}
|
|
|
|
return bdbID, cleanup
|
|
}
|
|
|
|
// SetupTestDatabaseAndFactory creates a database from environment config and returns both bdbID, factory, and cleanup function
|
|
// This is the recommended way to setup tests as it ensures the client factory connects to the newly created database
|
|
// Usage:
|
|
//
|
|
// bdbID, factory, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
|
|
// defer cleanup()
|
|
func SetupTestDatabaseAndFactory(t *testing.T, ctx context.Context, databaseName string) (bdbID int, factory *ClientFactory, cleanup func()) {
|
|
// Get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get environment config: %v", err)
|
|
}
|
|
|
|
// Get database config from environment
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get database config: %v", err)
|
|
}
|
|
|
|
// Get the specific database config
|
|
var envDbConfig EnvDatabaseConfig
|
|
var exists bool
|
|
if databaseName == "" {
|
|
// Get first database if no name provided
|
|
for _, config := range databasesConfig {
|
|
envDbConfig = config
|
|
exists = true
|
|
break
|
|
}
|
|
} else {
|
|
envDbConfig, exists = databasesConfig[databaseName]
|
|
}
|
|
|
|
if !exists {
|
|
t.Fatalf("Database %s not found in configuration", databaseName)
|
|
}
|
|
|
|
// Convert to DatabaseConfig
|
|
dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envDbConfig, fmt.Sprintf("e2e-test-%s-%d", databaseName, time.Now().Unix()))
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert config: %v", err)
|
|
}
|
|
|
|
// Create fault injector
|
|
faultInjector, err := CreateTestFaultInjector()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create fault injector: %v", err)
|
|
}
|
|
|
|
// Create database manager
|
|
dbManager := NewTestDatabaseManager(t, faultInjector, 0)
|
|
|
|
// Create the database and get the actual connection config from fault injector
|
|
bdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Use certificate location from original config if not provided by fault injector
|
|
if newEnvConfig.CertificatesLocation == "" && envDbConfig.CertificatesLocation != "" {
|
|
newEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation
|
|
}
|
|
|
|
// Convert EnvDatabaseConfig to RedisConnectionConfig
|
|
redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
|
|
if err != nil {
|
|
dbManager.Cleanup(ctx)
|
|
t.Fatalf("Failed to convert database config: %v", err)
|
|
}
|
|
|
|
// Create client factory with the actual config from fault injector
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Combined cleanup function
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
dbManager.Cleanup(ctx)
|
|
}
|
|
|
|
return bdbID, factory, cleanup
|
|
}
|
|
|
|
// SetupTestDatabaseAndFactoryWithConfig creates a database with custom config and returns both bdbID, factory, and cleanup function
|
|
// Usage:
|
|
//
|
|
// bdbID, factory, cleanup := SetupTestDatabaseAndFactoryWithConfig(t, ctx, "standalone", dbConfig)
|
|
// defer cleanup()
|
|
func SetupTestDatabaseAndFactoryWithConfig(t *testing.T, ctx context.Context, databaseName string, dbConfig DatabaseConfig) (bdbID int, factory *ClientFactory, cleanup func()) {
|
|
// Get environment config to use as template for connection details
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get environment config: %v", err)
|
|
}
|
|
|
|
// Get database config from environment
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get database config: %v", err)
|
|
}
|
|
|
|
// Get the specific database config as template
|
|
var envDbConfig EnvDatabaseConfig
|
|
var exists bool
|
|
if databaseName == "" {
|
|
// Get first database if no name provided
|
|
for _, config := range databasesConfig {
|
|
envDbConfig = config
|
|
exists = true
|
|
break
|
|
}
|
|
} else {
|
|
envDbConfig, exists = databasesConfig[databaseName]
|
|
}
|
|
|
|
if !exists {
|
|
t.Fatalf("Database %s not found in configuration", databaseName)
|
|
}
|
|
|
|
// Create fault injector
|
|
faultInjector, err := CreateTestFaultInjector()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create fault injector: %v", err)
|
|
}
|
|
|
|
// Create database manager
|
|
dbManager := NewTestDatabaseManager(t, faultInjector, 0)
|
|
|
|
// Create the database and get the actual connection config from fault injector
|
|
bdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Use certificate location from original config if not provided by fault injector
|
|
if newEnvConfig.CertificatesLocation == "" && envDbConfig.CertificatesLocation != "" {
|
|
newEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation
|
|
}
|
|
|
|
// Convert EnvDatabaseConfig to RedisConnectionConfig
|
|
redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
|
|
if err != nil {
|
|
dbManager.Cleanup(ctx)
|
|
t.Fatalf("Failed to convert database config: %v", err)
|
|
}
|
|
|
|
// Create client factory with the actual config from fault injector
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Combined cleanup function
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
dbManager.Cleanup(ctx)
|
|
}
|
|
|
|
return bdbID, factory, cleanup
|
|
}
|