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]>
1634 lines
53 KiB
Go
1634 lines
53 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/redis/go-redis/v9/maintnotifications"
|
|
)
|
|
|
|
// debugE2E returns true if E2E_DEBUG environment variable is set to "true"
|
|
// Use this to control verbose debug logging in e2e tests
|
|
func debugE2E() bool {
|
|
return os.Getenv("E2E_DEBUG") == "true"
|
|
}
|
|
|
|
// isPortUnavailableErrorString checks if the error string indicates a port unavailable error
|
|
func isPortUnavailableErrorString(errStr string) bool {
|
|
return strings.Contains(errStr, "port_unavailable") || strings.Contains(errStr, "Unavailable or invalid port")
|
|
}
|
|
|
|
// 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"`
|
|
OSSCluster bool `json:"oss_cluster,omitempty"`
|
|
}
|
|
|
|
// 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
|
|
IsClusterMode bool // Force cluster mode even with single endpoint
|
|
}
|
|
|
|
// 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,
|
|
IsClusterMode: dbConfig.OSSCluster,
|
|
}, 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
|
|
ClusterStateReloadInterval time.Duration // For cluster clients, interval for automatic state reloads
|
|
}
|
|
|
|
// 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,
|
|
ClusterStateReloadInterval: 10 * time.Minute,
|
|
}
|
|
|
|
if options.ReadTimeout > 0 {
|
|
clusterOptions.ReadTimeout = options.ReadTimeout
|
|
}
|
|
if options.WriteTimeout > 0 {
|
|
clusterOptions.WriteTimeout = options.WriteTimeout
|
|
}
|
|
if options.ClusterStateReloadInterval > 0 {
|
|
clusterOptions.ClusterStateReloadInterval = options.ClusterStateReloadInterval
|
|
}
|
|
|
|
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 cluster mode is explicitly set
|
|
if cf.config.IsClusterMode {
|
|
return true
|
|
}
|
|
|
|
// 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
|
|
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it automatically starts a proxy fault injector server
|
|
//
|
|
// Deprecated: Use CreateTestFaultInjectorWithCleanup instead for proper cleanup
|
|
func CreateTestFaultInjector() (*FaultInjectorClient, error) {
|
|
client, _, err := CreateTestFaultInjectorWithCleanup()
|
|
return client, err
|
|
}
|
|
|
|
// CreateTestFaultInjectorWithCleanup creates a fault injector client and returns a cleanup function
|
|
//
|
|
// Decision logic based on environment:
|
|
// 1. If REDIS_ENDPOINTS_CONFIG_PATH is set -> use real fault injector from FAULT_INJECTION_API_URL
|
|
// 2. If REDIS_ENDPOINTS_CONFIG_PATH is NOT set -> use Docker fault injector at http://localhost:15000
|
|
//
|
|
// Both the Docker proxy and fault injector should already be running (started via Docker Compose)
|
|
// This function does NOT start any services - it only connects to existing ones
|
|
//
|
|
// Usage:
|
|
//
|
|
// client, cleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
// if err != nil { ... }
|
|
// defer cleanup()
|
|
func CreateTestFaultInjectorWithCleanup() (*FaultInjectorClient, func(), error) {
|
|
// Try to get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
|
|
// If environment config fails, use Docker fault injector
|
|
// Note: GetEnvConfig() only fails if REDIS_ENDPOINTS_CONFIG_PATH is not set
|
|
if err != nil {
|
|
// Use Docker fault injector at http://localhost:15000 (updated to avoid macOS Control Center conflict)
|
|
// The fault injector should already be running via docker-compose
|
|
faultInjectorURL := "http://localhost:15000"
|
|
|
|
// Check if fault injector is accessible
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
resp, err := client.Get(faultInjectorURL + "/actions")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("Docker fault injector not accessible at %s (did you run 'make docker.e2e.start'?): %w", faultInjectorURL, err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
fmt.Printf("✓ Using Docker fault injector at %s\n", faultInjectorURL)
|
|
|
|
// Return client with no-op cleanup (Docker manages the fault injector lifecycle)
|
|
noopCleanup := func() {}
|
|
return NewFaultInjectorClient(faultInjectorURL), noopCleanup, nil
|
|
}
|
|
|
|
// Using real fault injector - no cleanup needed
|
|
fmt.Printf("✓ Using real fault injector at %s\n", envConfig.FaultInjectorURL)
|
|
noopCleanup := func() {}
|
|
return NewFaultInjectorClient(envConfig.FaultInjectorURL), noopCleanup, 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
|
|
// This creates an OSS Cluster database by default for backward compatibility.
|
|
// Use ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode for explicit control over cluster mode.
|
|
func ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig EnvDatabaseConfig, name string) (DatabaseConfig, error) {
|
|
return ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig, name, true)
|
|
}
|
|
|
|
// ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode converts EnvDatabaseConfig to fault injector DatabaseConfig
|
|
// with explicit control over whether to create a cluster or standalone database.
|
|
// Parameters:
|
|
// - envConfig: The environment database configuration
|
|
// - name: The name for the new database
|
|
// - clusterMode: If true, creates an OSS Cluster database (ClusterClient). If false, creates a standalone database (Client).
|
|
func ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig EnvDatabaseConfig, name string, clusterMode bool) (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(5) + rand.Intn(10) + rand.Intn(10) + rand.Intn(10) // Random port offset to avoid conflicts
|
|
|
|
var dbConfig DatabaseConfig
|
|
|
|
if clusterMode {
|
|
// Build OSS Cluster database config for ClusterClient support
|
|
// Supports cluster-specific features like SMIGRATING notifications
|
|
dbConfig = DatabaseConfig{
|
|
Name: name,
|
|
Port: port + randomPortOffset,
|
|
MemorySize: 268435456, // 256MB default
|
|
Replication: true,
|
|
EvictionPolicy: "noeviction",
|
|
ProxyPolicy: "all-master-shards", // OSS Cluster API requires all-master-shards
|
|
AutoUpgrade: true,
|
|
Sharding: true,
|
|
ShardsCount: 2,
|
|
ShardKeyRegex: []ShardKeyRegexPattern{
|
|
{Regex: ".*\\{(?<tag>.*)\\}.*"},
|
|
{Regex: "(?<tag>.*)"},
|
|
},
|
|
ShardsPlacement: "sparse", // Use sparse placement to distribute shards across nodes (required for slot-shuffle)
|
|
ModuleList: []DatabaseModule{
|
|
{ModuleArgs: "", ModuleName: "ReJSON"},
|
|
{ModuleArgs: "", ModuleName: "search"},
|
|
{ModuleArgs: "", ModuleName: "timeseries"},
|
|
{ModuleArgs: "", ModuleName: "bf"},
|
|
},
|
|
OSSCluster: true, // Enable OSS Cluster API for ClusterClient support
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Note: We ignore endpoint.OSSClusterAPIPreferredIPType here because
|
|
// e2e tests run from outside the cluster network and need external IPs
|
|
}
|
|
|
|
// For OSS Cluster databases, always use external IPs since e2e tests
|
|
// run from outside the cluster network and cannot reach internal IPs
|
|
dbConfig.OSSClusterAPIPreferredIPType = "external"
|
|
} else {
|
|
// Build standalone database config for regular Client support
|
|
// Supports MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications
|
|
dbConfig = DatabaseConfig{
|
|
Name: name,
|
|
Port: port + randomPortOffset,
|
|
MemorySize: 268435456, // 256MB default
|
|
Replication: true, // Enable replication for failover support
|
|
EvictionPolicy: "noeviction",
|
|
ProxyPolicy: "single", // Single proxy for standalone database
|
|
AutoUpgrade: true,
|
|
Sharding: false, // No sharding for standalone
|
|
ShardsCount: 1, // Single shard
|
|
ModuleList: []DatabaseModule{
|
|
{ModuleArgs: "", ModuleName: "ReJSON"},
|
|
{ModuleArgs: "", ModuleName: "search"},
|
|
{ModuleArgs: "", ModuleName: "timeseries"},
|
|
{ModuleArgs: "", ModuleName: "bf"},
|
|
},
|
|
OSSCluster: false, // Disable OSS Cluster API for standalone Client support
|
|
}
|
|
}
|
|
|
|
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
|
|
// If the port is unavailable, it will retry with incremented port numbers
|
|
func (m *TestDatabaseManager) CreateDatabase(ctx context.Context, dbConfig DatabaseConfig) (int, error) {
|
|
resp, finalPort, err := m.faultInjector.CreateDatabaseConfigWithPortRetry(ctx, m.clusterIndex, dbConfig, 100)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to trigger database creation: %w", err)
|
|
}
|
|
|
|
if finalPort != dbConfig.Port && debugE2E() {
|
|
fmt.Printf("[TestDatabaseManager] Database created on port %d (originally requested %d)\n", finalPort, dbConfig.Port)
|
|
}
|
|
|
|
// 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
|
|
// If the port is unavailable, it will retry with incremented port numbers
|
|
func (m *TestDatabaseManager) CreateDatabaseAndGetConfig(ctx context.Context, dbConfig DatabaseConfig) (int, EnvDatabaseConfig, error) {
|
|
const maxPortRetries = 100
|
|
currentPort := dbConfig.Port
|
|
|
|
var status *ActionStatusResponse
|
|
|
|
// Retry loop for port unavailable errors
|
|
for attempt := 0; attempt < maxPortRetries; attempt++ {
|
|
// Update the port in the config
|
|
dbConfig.Port = currentPort
|
|
|
|
resp, err := m.faultInjector.CreateDatabase(ctx, m.clusterIndex, dbConfig)
|
|
if err != nil {
|
|
// Check if it's a port unavailable error at trigger time
|
|
if isPortUnavailableErrorString(err.Error()) {
|
|
if debugE2E() {
|
|
fmt.Printf("[TestDatabaseManager] Port %d unavailable at trigger, trying port %d\n", currentPort, currentPort+1)
|
|
}
|
|
currentPort++
|
|
continue
|
|
}
|
|
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 {
|
|
// Success! Break out of retry loop
|
|
if currentPort != dbConfig.Port && debugE2E() {
|
|
fmt.Printf("[TestDatabaseManager] Database created on port %d (originally requested %d)\n", currentPort, dbConfig.Port)
|
|
}
|
|
break
|
|
}
|
|
|
|
// Check if the error is port unavailable
|
|
errorStr := ""
|
|
if status.Error != nil {
|
|
errorStr = fmt.Sprintf("%v", status.Error)
|
|
}
|
|
if isPortUnavailableErrorString(errorStr) {
|
|
if debugE2E() {
|
|
fmt.Printf("[TestDatabaseManager] Port %d unavailable after action, trying port %d\n", currentPort, currentPort+1)
|
|
}
|
|
currentPort++
|
|
continue
|
|
}
|
|
|
|
// Different error, don't retry
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("database creation failed: %v", status.Error)
|
|
}
|
|
|
|
// Check if we exhausted retries
|
|
if status == nil || status.Status != StatusSuccess {
|
|
return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to create database after %d port retries (last port tried: %d)", maxPortRetries, currentPort)
|
|
}
|
|
|
|
// 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 with cleanup
|
|
faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
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 {
|
|
fiCleanup()
|
|
t.Fatalf("[ERROR] Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Return combined cleanup function
|
|
cleanup = func() {
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
}
|
|
|
|
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 with cleanup
|
|
faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
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 {
|
|
fiCleanup()
|
|
t.Fatalf("[ERROR] Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Return combined cleanup function
|
|
cleanup = func() {
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
}
|
|
|
|
return bdbID, cleanup
|
|
}
|
|
|
|
// SetupTestDatabaseAndFactory creates a database from environment config and returns both bdbID, factory, test mode config, and cleanup function
|
|
// This is the recommended way to setup tests as it ensures the client factory connects to the newly created database
|
|
//
|
|
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.
|
|
// This allows tests to work with either the real fault injector OR the Docker proxy setup.
|
|
//
|
|
// Usage:
|
|
//
|
|
// bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
|
|
// defer cleanup()
|
|
func SetupTestDatabaseAndFactory(t *testing.T, ctx context.Context, databaseName string) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {
|
|
// Get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
// No environment config - use Docker proxy setup
|
|
t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")
|
|
|
|
// Determine cluster mode based on database name
|
|
// "standalone" creates a non-cluster client for testing FAILING_OVER/FAILED_OVER notifications
|
|
isClusterMode := databaseName != "standalone"
|
|
|
|
// Create a simple Redis connection config for Docker proxy
|
|
redisConfig := &RedisConnectionConfig{
|
|
Host: "127.0.0.1", // Use 127.0.0.1 to force IPv4
|
|
Port: 17000,
|
|
Username: "",
|
|
Password: "",
|
|
TLS: false,
|
|
BdbID: 0,
|
|
IsClusterMode: isClusterMode,
|
|
}
|
|
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Get proxy mock test mode config
|
|
testMode = GetTestModeConfig()
|
|
|
|
// No-op cleanup since we're not creating a database
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
}
|
|
|
|
return 0, factory, testMode, cleanup
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Determine cluster mode based on database name
|
|
// "standalone" creates a non-OSS-Cluster database for testing FAILING_OVER/FAILED_OVER notifications
|
|
// Other names create OSS Cluster databases for testing SMIGRATING/SMIGRATED notifications
|
|
clusterMode := databaseName != "standalone"
|
|
|
|
// Convert to DatabaseConfig with appropriate cluster mode
|
|
dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envDbConfig, fmt.Sprintf("e2e-test-%s-%d", databaseName, time.Now().Unix()), clusterMode)
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert config: %v", err)
|
|
}
|
|
|
|
// Create fault injector with cleanup
|
|
faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
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 {
|
|
fiCleanup()
|
|
t.Fatalf("[ERROR] 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
|
|
}
|
|
|
|
// Propagate OSSCluster flag since fault injector response may not include it
|
|
newEnvConfig.OSSCluster = dbConfig.OSSCluster
|
|
|
|
// Convert EnvDatabaseConfig to RedisConnectionConfig
|
|
redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
|
|
if err != nil {
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
t.Fatalf("Failed to convert database config: %v", err)
|
|
}
|
|
|
|
// Create client factory with the actual config from fault injector
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Get real fault injector test mode config
|
|
testMode = GetTestModeConfig()
|
|
|
|
// Combined cleanup function
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
}
|
|
|
|
return bdbID, factory, testMode, cleanup
|
|
}
|
|
|
|
// SetupTestDatabaseAndFactoryWithConfig creates a database with custom config and returns both bdbID, factory, test mode config, and cleanup function
|
|
//
|
|
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.
|
|
// This allows tests to work with either the real fault injector OR the Docker proxy setup.
|
|
//
|
|
// Usage:
|
|
//
|
|
// bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactoryWithConfig(t, ctx, "standalone", dbConfig)
|
|
// defer cleanup()
|
|
func SetupTestDatabaseAndFactoryWithConfig(t *testing.T, ctx context.Context, databaseName string, dbConfig DatabaseConfig) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {
|
|
// Get environment config to use as template for connection details
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
// No environment config - use Docker proxy setup
|
|
t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")
|
|
|
|
// Create a simple Redis connection config for Docker proxy
|
|
// The proxy simulates a cluster, so we set IsClusterMode to true
|
|
redisConfig := &RedisConnectionConfig{
|
|
Host: "127.0.0.1", // Use 127.0.0.1 to force IPv4
|
|
Port: 17000,
|
|
Username: "",
|
|
Password: "",
|
|
TLS: false,
|
|
BdbID: 0,
|
|
IsClusterMode: true, // Docker proxy simulates a cluster
|
|
}
|
|
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Get proxy mock test mode config
|
|
testMode = GetTestModeConfig()
|
|
|
|
// No-op cleanup since we're not creating a database
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
}
|
|
|
|
return 0, factory, testMode, cleanup
|
|
}
|
|
|
|
// 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 with cleanup
|
|
faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
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 {
|
|
fiCleanup()
|
|
t.Fatalf("[ERROR] 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)
|
|
fiCleanup()
|
|
t.Fatalf("Failed to convert database config: %v", err)
|
|
}
|
|
|
|
// Create client factory with the actual config from fault injector
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Get real fault injector test mode config
|
|
testMode = GetTestModeConfig()
|
|
|
|
// Combined cleanup function
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
}
|
|
|
|
return bdbID, factory, testMode, cleanup
|
|
}
|
|
|
|
// GetSlotMigrateRequirementsCount returns the number of database requirements for a given effect/variant.
|
|
// This is useful for iterating over all required databases when running tests.
|
|
// Returns 0 if the effect/variant combination is not found or if in proxy mode (which always returns 1).
|
|
func GetSlotMigrateRequirementsCount(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) int {
|
|
// Get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
// No environment config - use Docker proxy setup (always 1 requirement)
|
|
return 1
|
|
}
|
|
|
|
// Create fault injector client using the URL from environment config
|
|
fiClient := NewFaultInjectorClient(envConfig.FaultInjectorURL)
|
|
|
|
// Query the fault injector for the required database configuration
|
|
triggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)
|
|
if err != nil {
|
|
t.Logf("Warning: Failed to get slot-migrate triggers: %v", err)
|
|
return 1
|
|
}
|
|
|
|
// Find the trigger that matches our variant
|
|
for _, trigger := range triggersResp.Triggers {
|
|
if trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == "migrate") {
|
|
return len(trigger.Requirements)
|
|
}
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
// SetupTestDatabaseForSlotMigrate creates a database configured for a specific slot-migrate effect
|
|
// It queries the fault injector's GET /slot-migrate endpoint to get the required database configuration
|
|
// and creates the database with those settings.
|
|
//
|
|
// Usage:
|
|
//
|
|
// bdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)
|
|
// defer cleanup()
|
|
func SetupTestDatabaseForSlotMigrate(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {
|
|
return SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, 0)
|
|
}
|
|
|
|
// SetupTestDatabaseForSlotMigrateWithRequirementIndex creates a database configured for a specific slot-migrate effect
|
|
// using the requirement at the specified index. This allows testing against all required database configurations.
|
|
//
|
|
// Usage:
|
|
//
|
|
// reqCount := GetSlotMigrateRequirementsCount(t, ctx, effect, variant)
|
|
// for i := 0; i < reqCount; i++ {
|
|
// bdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, i)
|
|
// defer cleanup()
|
|
// // run test...
|
|
// }
|
|
func SetupTestDatabaseForSlotMigrateWithRequirementIndex(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant, requirementIndex int) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {
|
|
// Get environment config
|
|
envConfig, err := GetEnvConfig()
|
|
if err != nil {
|
|
// No environment config - use Docker proxy setup
|
|
t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")
|
|
|
|
redisConfig := &RedisConnectionConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 17000,
|
|
Username: "",
|
|
Password: "",
|
|
TLS: false,
|
|
BdbID: 0,
|
|
IsClusterMode: true,
|
|
}
|
|
|
|
factory = NewClientFactory(redisConfig)
|
|
testMode = GetTestModeConfig()
|
|
|
|
// Create fault injector client for proxy mode using Docker fault injector URL
|
|
fiClient = NewFaultInjectorClient("http://localhost:15000")
|
|
|
|
// Call CreateDatabase to initialize the proxy fault injector with 2 nodes
|
|
// This is required for slot-shuffle, remove, and remove-add effects which need >= 2 nodes
|
|
resp, err := fiClient.CreateDatabase(ctx, 0, DatabaseConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to trigger CreateDatabase on proxy fault injector: %v", err)
|
|
}
|
|
|
|
// Wait for the action to complete
|
|
status, err := fiClient.WaitForAction(ctx, resp.ActionID,
|
|
WithMaxWaitTime(10*time.Second),
|
|
WithPollInterval(100*time.Millisecond))
|
|
if err != nil {
|
|
t.Fatalf("Failed to wait for CreateDatabase action: %v", err)
|
|
}
|
|
if status.Status != StatusSuccess {
|
|
t.Fatalf("CreateDatabase action failed: %v", status.Error)
|
|
}
|
|
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
}
|
|
|
|
return 0, factory, testMode, fiClient, cleanup
|
|
}
|
|
|
|
// Create fault injector client using the URL from environment config
|
|
fiClient = NewFaultInjectorClient(envConfig.FaultInjectorURL)
|
|
|
|
// Query the fault injector for the required database configuration
|
|
t.Logf("Querying fault injector for %s effect requirements...", effect)
|
|
if debugE2E() {
|
|
slotMigrateURL := fmt.Sprintf("%s/slot-migrate?effect=%s&cluster_index=0", envConfig.FaultInjectorURL, effect)
|
|
t.Logf(" URL: %s", slotMigrateURL)
|
|
|
|
// Get raw response for debugging
|
|
rawResp, err := fiClient.GetSlotMigrateTriggersRaw(ctx, effect, 0)
|
|
if err != nil {
|
|
t.Logf("Warning: Failed to get raw slot-migrate triggers: %v", err)
|
|
} else {
|
|
t.Logf(" Raw response: %s", string(rawResp))
|
|
}
|
|
}
|
|
|
|
triggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get slot-migrate triggers: %v", err)
|
|
}
|
|
|
|
// Log the full response (only in debug mode)
|
|
if debugE2E() {
|
|
t.Logf(" Response: Effect=%s, Triggers=%d", triggersResp.Effect, len(triggersResp.Triggers))
|
|
for i, trigger := range triggersResp.Triggers {
|
|
t.Logf(" Trigger[%d]: Name=%s, Description=%s, Requirements=%d", i, trigger.Name, trigger.Description, len(trigger.Requirements))
|
|
for j, req := range trigger.Requirements {
|
|
t.Logf(" Requirement[%d]: DBConfig=%v, Cluster=%v, Description=%s", j, req.DBConfig, req.Cluster, req.Description)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the trigger that matches our variant
|
|
var requiredDBConfig map[string]interface{}
|
|
for _, trigger := range triggersResp.Triggers {
|
|
if trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == "migrate") {
|
|
if requirementIndex < len(trigger.Requirements) {
|
|
requiredDBConfig = trigger.Requirements[requirementIndex].DBConfig
|
|
if debugE2E() {
|
|
t.Logf("Found required DB config for %s/%s (requirement %d/%d): %v",
|
|
effect, variant, requirementIndex+1, len(trigger.Requirements), requiredDBConfig)
|
|
}
|
|
} else if len(trigger.Requirements) > 0 {
|
|
// Fall back to first requirement if index is out of bounds
|
|
requiredDBConfig = trigger.Requirements[0].DBConfig
|
|
t.Logf("Warning: requirement index %d out of bounds (max %d), using first requirement",
|
|
requirementIndex, len(trigger.Requirements)-1)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Get database config from environment
|
|
databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get database config: %v", err)
|
|
}
|
|
|
|
// Get the first database config
|
|
var envDbConfig EnvDatabaseConfig
|
|
for _, config := range databasesConfig {
|
|
envDbConfig = config
|
|
break
|
|
}
|
|
|
|
// Convert to DatabaseConfig
|
|
dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envDbConfig, fmt.Sprintf("e2e-slotmigrate-%d", time.Now().Unix()))
|
|
if err != nil {
|
|
t.Fatalf("Failed to convert config: %v", err)
|
|
}
|
|
|
|
// Apply the required configuration from fault injector
|
|
if requiredDBConfig != nil {
|
|
if shardsCount, ok := requiredDBConfig["shards_count"]; ok {
|
|
if sc, ok := shardsCount.(float64); ok {
|
|
dbConfig.ShardsCount = int(sc)
|
|
if debugE2E() {
|
|
t.Logf("Setting ShardsCount to %d (from fault injector)", dbConfig.ShardsCount)
|
|
}
|
|
}
|
|
}
|
|
if shardsPlacement, ok := requiredDBConfig["shards_placement"]; ok {
|
|
if sp, ok := shardsPlacement.(string); ok {
|
|
dbConfig.ShardsPlacement = sp
|
|
if debugE2E() {
|
|
t.Logf("Setting ShardsPlacement to %s (from fault injector)", dbConfig.ShardsPlacement)
|
|
}
|
|
}
|
|
}
|
|
if replication, ok := requiredDBConfig["replication"]; ok {
|
|
if r, ok := replication.(bool); ok {
|
|
dbConfig.Replication = r
|
|
if debugE2E() {
|
|
t.Logf("Setting Replication to %v (from fault injector)", dbConfig.Replication)
|
|
}
|
|
}
|
|
}
|
|
if ossCluster, ok := requiredDBConfig["oss_cluster"]; ok {
|
|
if oc, ok := ossCluster.(bool); ok {
|
|
dbConfig.OSSCluster = oc
|
|
if debugE2E() {
|
|
t.Logf("Setting OSSCluster to %v (from fault injector)", dbConfig.OSSCluster)
|
|
}
|
|
}
|
|
}
|
|
if sharding, ok := requiredDBConfig["sharding"]; ok {
|
|
if s, ok := sharding.(bool); ok {
|
|
dbConfig.Sharding = s
|
|
if debugE2E() {
|
|
t.Logf("Setting Sharding to %v (from fault injector)", dbConfig.Sharding)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if debugE2E() {
|
|
t.Logf("Creating database with config: ShardsCount=%d, ShardsPlacement=%s, Replication=%v, OSSCluster=%v",
|
|
dbConfig.ShardsCount, dbConfig.ShardsPlacement, dbConfig.Replication, dbConfig.OSSCluster)
|
|
}
|
|
|
|
// Create fault injector with cleanup
|
|
faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
|
|
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 {
|
|
fiCleanup()
|
|
t.Fatalf("[ERROR] 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
|
|
}
|
|
|
|
// Propagate OSSCluster flag since fault injector response may not include it
|
|
newEnvConfig.OSSCluster = dbConfig.OSSCluster
|
|
|
|
// Convert EnvDatabaseConfig to RedisConnectionConfig
|
|
redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
|
|
if err != nil {
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
t.Fatalf("Failed to convert database config: %v", err)
|
|
}
|
|
|
|
// Create client factory with the actual config from fault injector
|
|
factory = NewClientFactory(redisConfig)
|
|
|
|
// Get real fault injector test mode config
|
|
testMode = GetTestModeConfig()
|
|
|
|
// Combined cleanup function
|
|
cleanup = func() {
|
|
factory.DestroyAll()
|
|
dbManager.Cleanup(ctx)
|
|
fiCleanup()
|
|
}
|
|
|
|
return bdbID, factory, testMode, fiClient, cleanup
|
|
}
|