Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/voidrunnerhq/voidrunner/internal/api/routes"
"github.com/voidrunnerhq/voidrunner/internal/config"
"github.com/voidrunnerhq/voidrunner/internal/database"
"github.com/voidrunnerhq/voidrunner/pkg/logger"
)

Expand All @@ -25,12 +26,46 @@ func main() {

log := logger.New(cfg.Logger.Level, cfg.Logger.Format)

// Initialize database connection
dbConn, err := database.NewConnection(&cfg.Database, log.Logger)
if err != nil {
log.Error("failed to initialize database connection", "error", err)
os.Exit(1)
}
defer dbConn.Close()

// Run database migrations
migrateConfig := &database.MigrateConfig{
DatabaseConfig: &cfg.Database,
MigrationsPath: "file://migrations",
Logger: log.Logger,
}

if err := database.MigrateUp(migrateConfig); err != nil {
log.Error("failed to run database migrations", "error", err)
os.Exit(1)
}

// Initialize repositories
repos := database.NewRepositories(dbConn)

// Perform database health check
healthCtx, healthCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer healthCancel()

if err := dbConn.HealthCheck(healthCtx); err != nil {
log.Error("database health check failed", "error", err)
os.Exit(1)
}

log.Info("database initialized successfully")

if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}

router := gin.New()
routes.Setup(router, cfg, log)
routes.Setup(router, cfg, log, repos)

srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port),
Expand Down
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,29 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-migrate/migrate/v4 v4.18.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
Copy link

Copilot AI Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lib/pq module appears unused since you rely on pgx for database connections. Removing it will reduce your dependency footprint.

Suggested change
github.com/lib/pq v1.10.9 // indirect

Copilot uses AI. Check for mistakes.
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
Expand Down
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,24 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.6.0-pre.2 h1:SCkYm/XGeCcXItAv0Xofqsa4JPdDDkyNcG1Ush5cBLQ=
Expand All @@ -40,6 +55,8 @@ github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQe
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -65,12 +82,16 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
Expand Down
19 changes: 16 additions & 3 deletions internal/api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"github.com/voidrunnerhq/voidrunner/internal/api/handlers"
"github.com/voidrunnerhq/voidrunner/internal/api/middleware"
"github.com/voidrunnerhq/voidrunner/internal/config"
"github.com/voidrunnerhq/voidrunner/internal/database"
"github.com/voidrunnerhq/voidrunner/pkg/logger"
)

func Setup(router *gin.Engine, cfg *config.Config, log *logger.Logger) {
func Setup(router *gin.Engine, cfg *config.Config, log *logger.Logger, repos *database.Repositories) {
setupMiddleware(router, cfg, log)
setupRoutes(router)
setupRoutes(router, repos)
}

func setupMiddleware(router *gin.Engine, cfg *config.Config, log *logger.Logger) {
Expand All @@ -22,7 +23,7 @@ func setupMiddleware(router *gin.Engine, cfg *config.Config, log *logger.Logger)
router.Use(middleware.ErrorHandler())
}

func setupRoutes(router *gin.Engine) {
func setupRoutes(router *gin.Engine, repos *database.Repositories) {
healthHandler := handlers.NewHealthHandler()

router.GET("/health", healthHandler.Health)
Expand All @@ -35,5 +36,17 @@ func setupRoutes(router *gin.Engine) {
"message": "pong",
})
})

// Future API routes will use repos here
// userHandler := handlers.NewUserHandler(repos.Users)
// taskHandler := handlers.NewTaskHandler(repos.Tasks)
// executionHandler := handlers.NewTaskExecutionHandler(repos.TaskExecutions)

// v1.POST("/users", userHandler.Create)
// v1.GET("/users/:id", userHandler.GetByID)
// v1.POST("/tasks", taskHandler.Create)
// v1.GET("/tasks/:id", taskHandler.GetByID)
// v1.POST("/tasks/:id/executions", executionHandler.Create)
// v1.GET("/executions/:id", executionHandler.GetByID)
}
}
150 changes: 150 additions & 0 deletions internal/database/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package database

import (
"context"
"fmt"
"log/slog"
"time"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/voidrunnerhq/voidrunner/internal/config"
)

// Connection represents a database connection pool
type Connection struct {
Pool *pgxpool.Pool
logger *slog.Logger
}

// NewConnection creates a new database connection pool
func NewConnection(cfg *config.DatabaseConfig, logger *slog.Logger) (*Connection, error) {
if cfg == nil {
return nil, fmt.Errorf("database configuration is required")
}

if logger == nil {
logger = slog.Default()
}

connStr := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
cfg.User,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.Database,
cfg.SSLMode,
)

poolConfig, err := pgxpool.ParseConfig(connStr)
if err != nil {
return nil, fmt.Errorf("failed to parse database connection string: %w", err)
}

// Configure connection pool settings for optimal performance
poolConfig.MaxConns = 25 // Maximum number of connections
poolConfig.MinConns = 5 // Minimum number of connections
poolConfig.MaxConnLifetime = time.Hour * 1 // Maximum connection lifetime
poolConfig.MaxConnIdleTime = time.Minute * 30 // Maximum connection idle time
poolConfig.HealthCheckPeriod = time.Minute * 5 // Health check frequency

// Connection timeout settings
poolConfig.ConnConfig.ConnectTimeout = time.Second * 10
poolConfig.ConnConfig.RuntimeParams["statement_timeout"] = "30s"
poolConfig.ConnConfig.RuntimeParams["idle_in_transaction_session_timeout"] = "60s"

ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, fmt.Errorf("failed to create database pool: %w", err)
}

// Test the connection
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}

logger.Info("database connection pool created successfully",
"host", cfg.Host,
"port", cfg.Port,
"database", cfg.Database,
"max_conns", poolConfig.MaxConns,
"min_conns", poolConfig.MinConns,
)

return &Connection{
Pool: pool,
logger: logger,
}, nil
}

// Close closes the database connection pool
func (c *Connection) Close() {
if c.Pool != nil {
c.logger.Info("closing database connection pool")
c.Pool.Close()
}
}

// Ping checks if the database connection is alive
func (c *Connection) Ping(ctx context.Context) error {
return c.Pool.Ping(ctx)
}

// Stats returns connection pool statistics
func (c *Connection) Stats() *pgxpool.Stat {
return c.Pool.Stat()
}

// LogStats logs connection pool statistics
func (c *Connection) LogStats() {
stats := c.Stats()
c.logger.Info("database connection pool stats",
"total_conns", stats.TotalConns(),
"idle_conns", stats.IdleConns(),
"acquired_conns", stats.AcquiredConns(),
"constructing_conns", stats.ConstructingConns(),
"acquire_count", stats.AcquireCount(),
"acquire_duration", stats.AcquireDuration(),
"acquired_conns_duration", stats.AcquiredConns(),
Copy link

Copilot AI Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log key acquired_conns_duration is using stats.AcquiredConns() (an integer). To report connection acquisition time, consider using stats.AcquireDuration() instead.

Suggested change
"acquired_conns_duration", stats.AcquiredConns(),
"acquired_conns_duration", stats.AcquireDuration(),

Copilot uses AI. Check for mistakes.
"canceled_acquire_count", stats.CanceledAcquireCount(),
"empty_acquire_count", stats.EmptyAcquireCount(),
"max_conns", stats.MaxConns(),
"new_conns_count", stats.NewConnsCount(),
)
}

// HealthCheck performs a comprehensive health check of the database connection
func (c *Connection) HealthCheck(ctx context.Context) error {
// Check if pool is available
if c.Pool == nil {
return fmt.Errorf("database pool is not initialized")
}

// Ping the database
if err := c.Pool.Ping(ctx); err != nil {
return fmt.Errorf("database ping failed: %w", err)
}

// Check pool statistics
stats := c.Stats()
if stats.TotalConns() == 0 {
return fmt.Errorf("no database connections available")
}

// Execute a simple query to ensure the database is responsive
var result int
err := c.Pool.QueryRow(ctx, "SELECT 1").Scan(&result)
if err != nil {
return fmt.Errorf("database query test failed: %w", err)
}

if result != 1 {
return fmt.Errorf("unexpected database query result: %d", result)
}

return nil
}
Loading