Compare commits

..

3 Commits

Author SHA1 Message Date
c2d86c9880 fix(dbmanager): resolve deadlock in adapter getter methods
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 30s
Build , Vet Test, and Lint / Build (push) Successful in 1m56s
Tests / Unit Tests (push) Failing after 1m5s
Tests / Integration Tests (push) Failing after 1m1s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -23m15s
Build , Vet Test, and Lint / Lint Code (push) Successful in -23m9s
Fixes deadlock caused by calling public methods (Bun, GORM, Native)
while holding a write lock. The public methods attempt to acquire
their own locks, causing the goroutine to freeze.

Solution: Call provider methods directly within the adapter getters
since they already hold the necessary write lock.
2026-01-03 17:26:43 +02:00
70bf0a4be1 fix(dbmanager): add nil checks to connection methods 2026-01-03 17:19:43 +02:00
4964d89158 feat(restheadspec): accept bunrouter.Router and bunrouter.Group in SetupBunRouterRoutes
Replaced *router.StandardBunRouterAdapter parameter with BunRouterHandler
interface to support both bunrouter.Router and bunrouter.Group types,
enabling route registration on router groups with path prefixes.

- Added BunRouterHandler interface
- Updated SetupBunRouterRoutes signature
- Updated ExampleBunRouterWithBunDB to use bunrouter.New() directly
- Added ExampleBunRouterWithGroup demonstrating group usage
2026-01-03 16:10:36 +02:00
2 changed files with 105 additions and 23 deletions

View File

@@ -3,6 +3,7 @@ package dbmanager
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
@@ -159,6 +160,9 @@ func (c *sqlConnection) Close() error {
// HealthCheck verifies the connection is alive
func (c *sqlConnection) HealthCheck(ctx context.Context) error {
if c == nil {
return fmt.Errorf("connection is nil")
}
c.mu.Lock()
defer c.mu.Unlock()
@@ -188,6 +192,9 @@ func (c *sqlConnection) Reconnect(ctx context.Context) error {
// Native returns the native *sql.DB connection
func (c *sqlConnection) Native() (*sql.DB, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.nativeDB != nil {
defer c.mu.RUnlock()
@@ -219,6 +226,9 @@ func (c *sqlConnection) Native() (*sql.DB, error) {
// Bun returns a Bun ORM instance wrapping the native connection
func (c *sqlConnection) Bun() (*bun.DB, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.bunDB != nil {
defer c.mu.RUnlock()
@@ -249,6 +259,9 @@ func (c *sqlConnection) Bun() (*bun.DB, error) {
// GORM returns a GORM instance wrapping the native connection
func (c *sqlConnection) GORM() (*gorm.DB, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.gormDB != nil {
defer c.mu.RUnlock()
@@ -283,6 +296,9 @@ func (c *sqlConnection) GORM() (*gorm.DB, error) {
// Database returns the common.Database interface using the configured default ORM
func (c *sqlConnection) Database() (common.Database, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
defaultORM := c.config.DefaultORM
c.mu.RUnlock()
@@ -307,6 +323,9 @@ func (c *sqlConnection) MongoDB() (*mongo.Client, error) {
// Stats returns connection statistics
func (c *sqlConnection) Stats() *ConnectionStats {
if c == nil {
return nil
}
c.mu.RLock()
defer c.mu.RUnlock()
@@ -336,6 +355,9 @@ func (c *sqlConnection) Stats() *ConnectionStats {
// getBunAdapter returns or creates the Bun adapter
func (c *sqlConnection) getBunAdapter() (common.Database, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.bunAdapter != nil {
defer c.mu.RUnlock()
@@ -350,17 +372,28 @@ func (c *sqlConnection) getBunAdapter() (common.Database, error) {
return c.bunAdapter, nil
}
bunDB, err := c.Bun()
if err != nil {
return nil, err
// Double-check bunDB exists (while already holding write lock)
if c.bunDB == nil {
// Get native connection first
native, err := c.provider.GetNative()
if err != nil {
return nil, NewConnectionError(c.name, "get bun", err)
}
// Create Bun DB wrapping the same sql.DB
dialect := c.getBunDialect()
c.bunDB = bun.NewDB(native, dialect)
}
c.bunAdapter = database.NewBunAdapter(bunDB)
c.bunAdapter = database.NewBunAdapter(c.bunDB)
return c.bunAdapter, nil
}
// getGORMAdapter returns or creates the GORM adapter
func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.gormAdapter != nil {
defer c.mu.RUnlock()
@@ -375,17 +408,33 @@ func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
return c.gormAdapter, nil
}
gormDB, err := c.GORM()
if err != nil {
return nil, err
// Double-check gormDB exists (while already holding write lock)
if c.gormDB == nil {
// Get native connection first
native, err := c.provider.GetNative()
if err != nil {
return nil, NewConnectionError(c.name, "get gorm", err)
}
// Create GORM DB wrapping the same sql.DB
dialector := c.getGORMDialector(native)
db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return nil, NewConnectionError(c.name, "initialize gorm", err)
}
c.gormDB = db
}
c.gormAdapter = database.NewGormAdapter(gormDB)
c.gormAdapter = database.NewGormAdapter(c.gormDB)
return c.gormAdapter, nil
}
// getNativeAdapter returns or creates the native adapter
func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
if c == nil {
return nil, fmt.Errorf("connection is nil")
}
c.mu.RLock()
if c.nativeAdapter != nil {
defer c.mu.RUnlock()
@@ -400,21 +449,31 @@ func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
return c.nativeAdapter, nil
}
native, err := c.Native()
if err != nil {
return nil, err
// Double-check nativeDB exists (while already holding write lock)
if c.nativeDB == nil {
if !c.connected {
return nil, ErrConnectionClosed
}
// Get native connection from provider
db, err := c.provider.GetNative()
if err != nil {
return nil, NewConnectionError(c.name, "get native", err)
}
c.nativeDB = db
}
// Create a native adapter based on database type
switch c.dbType {
case DatabaseTypePostgreSQL:
c.nativeAdapter = database.NewPgSQLAdapter(native)
c.nativeAdapter = database.NewPgSQLAdapter(c.nativeDB)
case DatabaseTypeSQLite:
// For SQLite, we'll use the PgSQL adapter as it works with standard sql.DB
c.nativeAdapter = database.NewPgSQLAdapter(native)
c.nativeAdapter = database.NewPgSQLAdapter(c.nativeDB)
case DatabaseTypeMSSQL:
// For MSSQL, we'll use the PgSQL adapter as it works with standard sql.DB
c.nativeAdapter = database.NewPgSQLAdapter(native)
c.nativeAdapter = database.NewPgSQLAdapter(c.nativeDB)
default:
return nil, ErrUnsupportedDatabase
}
@@ -424,6 +483,7 @@ func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
// getBunDialect returns the appropriate Bun dialect for the database type
func (c *sqlConnection) getBunDialect() schema.Dialect {
switch c.dbType {
case DatabaseTypePostgreSQL:
return database.GetPostgresDialect()

View File

@@ -270,9 +270,14 @@ func ExampleWithBun(bunDB *bun.DB) {
SetupMuxRoutes(muxRouter, handler, nil)
}
// BunRouterHandler is an interface that both bunrouter.Router and bunrouter.Group implement
type BunRouterHandler interface {
Handle(method, path string, handler bunrouter.HandlerFunc)
}
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
func SetupBunRouterRoutes(bunRouter *router.StandardBunRouterAdapter, handler *Handler) {
r := bunRouter.GetBunRouter()
// Accepts bunrouter.Router or bunrouter.Group
func SetupBunRouterRoutes(r BunRouterHandler, handler *Handler) {
// CORS config
corsConfig := common.DefaultCORSConfig()
@@ -450,17 +455,34 @@ func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
// Create handler
handler := NewHandlerWithBun(bunDB)
// Create BunRouter adapter
routerAdapter := NewStandardBunRouter()
// Create bunrouter
bunRouter := bunrouter.New()
// Setup routes
SetupBunRouterRoutes(routerAdapter, handler)
// Get the underlying router for server setup
r := routerAdapter.GetBunRouter()
SetupBunRouterRoutes(bunRouter, handler)
// Start server
if err := http.ListenAndServe(":8080", r); err != nil {
if err := http.ListenAndServe(":8080", bunRouter); err != nil {
logger.Error("Server failed to start: %v", err)
}
}
// ExampleBunRouterWithGroup shows how to use SetupBunRouterRoutes with a bunrouter.Group
func ExampleBunRouterWithGroup(bunDB *bun.DB) {
// Create handler with Bun adapter
handler := NewHandlerWithBun(bunDB)
// Create bunrouter
bunRouter := bunrouter.New()
// Create a route group with a prefix
apiGroup := bunRouter.NewGroup("/api")
// Setup RestHeadSpec routes on the group - routes will be under /api
SetupBunRouterRoutes(apiGroup, handler)
// Start server
if err := http.ListenAndServe(":8080", bunRouter); err != nil {
logger.Error("Server failed to start: %v", err)
}
}