From c75842ebb05f63e35ee597411fc54747ad14716c Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 14 Jan 2026 15:04:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(dbmanager):=20=E2=9C=A8=20update=20health?= =?UTF-8?q?=20check=20interval=20and=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change default health check interval from 30s to 15s. * Always start background health checks regardless of auto-reconnect setting. * Add tests for health checker functionality and default configurations. --- pkg/dbmanager/config.go | 7 +- pkg/dbmanager/manager.go | 5 +- pkg/dbmanager/manager_test.go | 226 ++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 pkg/dbmanager/manager_test.go diff --git a/pkg/dbmanager/config.go b/pkg/dbmanager/config.go index 8f81259..7213eb6 100644 --- a/pkg/dbmanager/config.go +++ b/pkg/dbmanager/config.go @@ -128,7 +128,7 @@ func DefaultManagerConfig() ManagerConfig { RetryAttempts: 3, RetryDelay: 1 * time.Second, RetryMaxDelay: 10 * time.Second, - HealthCheckInterval: 30 * time.Second, + HealthCheckInterval: 15 * time.Second, EnableAutoReconnect: true, } } @@ -161,6 +161,11 @@ func (c *ManagerConfig) ApplyDefaults() { if c.HealthCheckInterval == 0 { c.HealthCheckInterval = defaults.HealthCheckInterval } + // EnableAutoReconnect defaults to true - apply if not explicitly set + // Since this is a boolean, we apply the default unconditionally when it's false + if !c.EnableAutoReconnect { + c.EnableAutoReconnect = defaults.EnableAutoReconnect + } } // Validate validates the manager configuration diff --git a/pkg/dbmanager/manager.go b/pkg/dbmanager/manager.go index 2b68cc2..7bcab48 100644 --- a/pkg/dbmanager/manager.go +++ b/pkg/dbmanager/manager.go @@ -219,9 +219,10 @@ func (m *connectionManager) Connect(ctx context.Context) error { logger.Info("Database connection established: name=%s, type=%s", name, connCfg.Type) } - // Start background health checks if enabled - if m.config.EnableAutoReconnect && m.config.HealthCheckInterval > 0 { + // Always start background health checks + if m.config.HealthCheckInterval > 0 { m.startHealthChecker() + logger.Info("Background health checker started: interval=%v", m.config.HealthCheckInterval) } logger.Info("Database manager initialized: connections=%d", len(m.connections)) diff --git a/pkg/dbmanager/manager_test.go b/pkg/dbmanager/manager_test.go new file mode 100644 index 0000000..3497690 --- /dev/null +++ b/pkg/dbmanager/manager_test.go @@ -0,0 +1,226 @@ +package dbmanager + +import ( + "context" + "database/sql" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +func TestBackgroundHealthChecker(t *testing.T) { + // Create a SQLite in-memory database + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Failed to open database: %v", err) + } + defer db.Close() + + // Create manager config with a short health check interval for testing + cfg := ManagerConfig{ + DefaultConnection: "test", + Connections: map[string]ConnectionConfig{ + "test": { + Name: "test", + Type: DatabaseTypeSQLite, + FilePath: ":memory:", + }, + }, + HealthCheckInterval: 1 * time.Second, // Short interval for testing + EnableAutoReconnect: true, + } + + // Create manager + mgr, err := NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create manager: %v", err) + } + + // Connect - this should start the background health checker + ctx := context.Background() + err = mgr.Connect(ctx) + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + defer mgr.Close() + + // Get the connection to verify it's healthy + conn, err := mgr.Get("test") + if err != nil { + t.Fatalf("Failed to get connection: %v", err) + } + + // Verify initial health check + err = conn.HealthCheck(ctx) + if err != nil { + t.Errorf("Initial health check failed: %v", err) + } + + // Wait for a few health check cycles + time.Sleep(3 * time.Second) + + // Get stats to verify the connection is still healthy + stats := conn.Stats() + if stats == nil { + t.Fatal("Expected stats to be returned") + } + + if !stats.Connected { + t.Error("Expected connection to still be connected") + } + + if stats.HealthCheckStatus == "" { + t.Error("Expected health check status to be set") + } + + // Verify the manager has started the health checker + if cm, ok := mgr.(*connectionManager); ok { + if cm.healthTicker == nil { + t.Error("Expected health ticker to be running") + } + } +} + +func TestDefaultHealthCheckInterval(t *testing.T) { + // Verify the default health check interval is 15 seconds + defaults := DefaultManagerConfig() + + expectedInterval := 15 * time.Second + if defaults.HealthCheckInterval != expectedInterval { + t.Errorf("Expected default health check interval to be %v, got %v", + expectedInterval, defaults.HealthCheckInterval) + } + + if !defaults.EnableAutoReconnect { + t.Error("Expected EnableAutoReconnect to be true by default") + } +} + +func TestApplyDefaultsEnablesAutoReconnect(t *testing.T) { + // Create a config without setting EnableAutoReconnect + cfg := ManagerConfig{ + Connections: map[string]ConnectionConfig{ + "test": { + Name: "test", + Type: DatabaseTypeSQLite, + FilePath: ":memory:", + }, + }, + } + + // Verify it's false initially (Go's zero value for bool) + if cfg.EnableAutoReconnect { + t.Error("Expected EnableAutoReconnect to be false before ApplyDefaults") + } + + // Apply defaults + cfg.ApplyDefaults() + + // Verify it's now true + if !cfg.EnableAutoReconnect { + t.Error("Expected EnableAutoReconnect to be true after ApplyDefaults") + } + + // Verify health check interval is also set + if cfg.HealthCheckInterval != 15*time.Second { + t.Errorf("Expected health check interval to be 15s, got %v", cfg.HealthCheckInterval) + } +} + +func TestManagerHealthCheck(t *testing.T) { + // Create a SQLite in-memory database + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("Failed to open database: %v", err) + } + defer db.Close() + + // Create manager config + cfg := ManagerConfig{ + DefaultConnection: "test", + Connections: map[string]ConnectionConfig{ + "test": { + Name: "test", + Type: DatabaseTypeSQLite, + FilePath: ":memory:", + }, + }, + HealthCheckInterval: 15 * time.Second, + EnableAutoReconnect: true, + } + + // Create and connect manager + mgr, err := NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create manager: %v", err) + } + + ctx := context.Background() + err = mgr.Connect(ctx) + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + defer mgr.Close() + + // Perform health check on all connections + err = mgr.HealthCheck(ctx) + if err != nil { + t.Errorf("Health check failed: %v", err) + } + + // Get stats + stats := mgr.Stats() + if stats == nil { + t.Fatal("Expected stats to be returned") + } + + if stats.TotalConnections != 1 { + t.Errorf("Expected 1 total connection, got %d", stats.TotalConnections) + } + + if stats.HealthyCount != 1 { + t.Errorf("Expected 1 healthy connection, got %d", stats.HealthyCount) + } + + if stats.UnhealthyCount != 0 { + t.Errorf("Expected 0 unhealthy connections, got %d", stats.UnhealthyCount) + } +} + +func TestManagerStatsAfterClose(t *testing.T) { + cfg := ManagerConfig{ + DefaultConnection: "test", + Connections: map[string]ConnectionConfig{ + "test": { + Name: "test", + Type: DatabaseTypeSQLite, + FilePath: ":memory:", + }, + }, + HealthCheckInterval: 15 * time.Second, + } + + mgr, err := NewManager(cfg) + if err != nil { + t.Fatalf("Failed to create manager: %v", err) + } + + ctx := context.Background() + err = mgr.Connect(ctx) + if err != nil { + t.Fatalf("Failed to connect: %v", err) + } + + // Close the manager + err = mgr.Close() + if err != nil { + t.Errorf("Failed to close manager: %v", err) + } + + // Stats should show no connections + stats := mgr.Stats() + if stats.TotalConnections != 0 { + t.Errorf("Expected 0 total connections after close, got %d", stats.TotalConnections) + } +}