Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/server/internal/database/db.go
Fimeg 82018bfb80 taught the rocks to stop tripping over each other under load
server was sized for a campfire, not a fleet. 25 db connections, every agent
report flinging goroutines into the void, the syncer plodding one repo at a
time while clutching a lock nobody needed. loosened the choke points:

- db pool 25 -> 100 + connection lifetime, all env-tunable
- bounded pool for the report-path fire-and-forget work; /health/tasks to
  watch it breathe. no more unbounded goroutine spray per report
- upstream syncer runs concurrent now, dropped the dead mutex around repology
  fetches, reconciler single-flights instead of locking through the whole crawl
- scheduler caps jobs per tick so an aligned fleet can't stampede the db
- swatted a context-cancel bug that was quietly killing immediate syncs

builds clean, race detector's calm.
2026-06-07 19:17:08 -04:00

222 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package database
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
// Connection-pool defaults. Sized for a single well-tuned server fronting 50200
// agents (SCALE-001 S1). The old 25/5 ceiling starved around ~30 concurrent agents
// once the scheduler workers, sweeps, syncer, reconciler, and request handlers all
// contended for the pool. All three are overridable via env for smaller/larger hosts.
const (
defaultMaxOpenConns = 100
defaultMaxIdleConns = 25
defaultConnMaxLifetimeMins = 30
)
// DB wraps the database connection
type DB struct {
*sqlx.DB
}
// Connect establishes a connection to the PostgreSQL database
func Connect(databaseURL string) (*DB, error) {
db, err := sqlx.Connect("postgres", databaseURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Configure connection pool (SCALE-001 S1). ConnMaxLifetime caps how long a
// connection is reused so the pool recycles cleanly behind poolers/restarts
// instead of holding stale handles indefinitely.
maxOpen := envInt("REDFLAG_DB_MAX_OPEN_CONNS", defaultMaxOpenConns)
maxIdle := envInt("REDFLAG_DB_MAX_IDLE_CONNS", defaultMaxIdleConns)
lifetimeMins := envInt("REDFLAG_DB_CONN_MAX_LIFETIME_MINUTES", defaultConnMaxLifetimeMins)
db.SetMaxOpenConns(maxOpen)
db.SetMaxIdleConns(maxIdle)
db.SetConnMaxLifetime(time.Duration(lifetimeMins) * time.Minute)
// Test the connection
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
log.Printf("[INFO] [server] [database] pool_configured max_open=%d max_idle=%d conn_max_lifetime_min=%d", maxOpen, maxIdle, lifetimeMins)
return &DB{db}, nil
}
// envInt reads a positive integer from env, falling back to def when unset, empty,
// unparseable, or non-positive. A non-positive override is ignored so a stray
// "0"/"-1" can't silently disable the pool ceiling.
func envInt(key string, def int) int {
v := os.Getenv(key)
if v == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
log.Printf("[WARN] [server] [database] invalid_env key=%s value=%q falling_back=%d", key, v, def)
return def
}
return n
}
// Migrate runs database migrations with proper tracking and safety
func (db *DB) Migrate(migrationsPath string) error {
// Create migrations table if it doesn't exist
createTableSQL := `
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)`
if _, err := db.Exec(createTableSQL); err != nil {
return fmt.Errorf("failed to create migrations table: %w", err)
}
// ISSUE-001: Create pre-migration backup point for recovery
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS schema_migrations_backup AS SELECT * FROM schema_migrations WHERE false"); err != nil {
log.Printf("[WARN] [server] [database] backup_table_creation_failed: %v", err)
// Non-fatal: continue without backup capability
}
// Read migration files
files, err := os.ReadDir(migrationsPath)
if err != nil {
return fmt.Errorf("failed to read migrations directory: %w", err)
}
// Filter and sort .up.sql files
var migrationFiles []string
for _, file := range files {
if strings.HasSuffix(file.Name(), ".up.sql") {
migrationFiles = append(migrationFiles, file.Name())
}
}
sort.Strings(migrationFiles)
// Execute migrations that haven't been applied yet
for _, filename := range migrationFiles {
// Check if migration has already been applied
var count int
err := db.Get(&count, "SELECT COUNT(*) FROM schema_migrations WHERE version = $1", filename)
if err != nil {
return fmt.Errorf("failed to check migration status for %s: %w", filename, err)
}
if count > 0 {
log.Printf("[INFO] [server] [database] migration_skipped version=%s already_applied=true", filename)
continue
}
// Read migration file
path := filepath.Join(migrationsPath, filename)
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read migration %s: %w", filename, err)
}
// Execute migration in a transaction
tx, err := db.Beginx()
if err != nil {
return fmt.Errorf("failed to begin transaction for migration %s: %w", filename, err)
}
// Execute the migration SQL
if _, err := tx.Exec(string(content)); err != nil {
// Rollback the failed transaction
tx.Rollback()
// Check if it's an "already exists" error - this indicates the migration
// may have been partially applied. We need to verify state before deciding.
if strings.Contains(err.Error(), "already exists") ||
strings.Contains(err.Error(), "duplicate key") {
// Check if this migration was already recorded as applied
var count int
checkErr := db.Get(&count, "SELECT COUNT(*) FROM schema_migrations WHERE version = $1", filename)
if checkErr == nil && count > 0 {
// Migration was recorded but execution failed - this is a CONSISTENCY ERROR
// The database state may not match what the migration intended
log.Printf("[ERROR] [server] [database] migration_inconsistent version=%s error=%q", filename, err)
return fmt.Errorf("migration %s recorded as applied but execution failed with 'already exists': database may be in inconsistent state. Manual intervention required: %w", filename, err)
}
// Migration not recorded and failed - this is a normal error
return fmt.Errorf("migration %s failed: %w", filename, err)
}
// For any other error, fail
return fmt.Errorf("failed to execute migration %s: %w", filename, err)
}
// Record the migration as applied (normal success path)
if _, err := tx.Exec("INSERT INTO schema_migrations (version) VALUES ($1)", filename); err != nil {
tx.Rollback()
return fmt.Errorf("failed to record migration %s: %w", filename, err)
}
// Commit the transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit migration %s: %w", filename, err)
}
log.Printf("[INFO] [server] [database] migration_applied version=%s", filename)
}
return nil
}
// MigrationHealth checks for inconsistent migration state (ISSUE-001)
func (db *DB) MigrationHealth() (map[string]interface{}, error) {
health := map[string]interface{}{
"status": "healthy",
"issues": []string{},
}
// Check for duplicate migration versions (should never happen with PK, but verify)
var duplicates []string
err := db.Select(&duplicates, `
SELECT version FROM schema_migrations
GROUP BY version HAVING COUNT(*) > 1
`)
if err != nil {
health["status"] = "error"
health["issues"] = append(health["issues"].([]string), fmt.Sprintf("duplicate_check_failed: %v", err))
} else if len(duplicates) > 0 {
health["status"] = "critical"
health["issues"] = append(health["issues"].([]string), fmt.Sprintf("duplicate_versions: %v", duplicates))
}
// Check for migrations applied out of order
var versions []string
err = db.Select(&versions, `SELECT version FROM schema_migrations ORDER BY applied_at`)
if err != nil {
health["status"] = "error"
health["issues"] = append(health["issues"].([]string), fmt.Sprintf("version_check_failed: %v", err))
} else {
// Verify versions are sorted lexicographically (migration naming convention)
for i := 1; i < len(versions); i++ {
if versions[i] < versions[i-1] {
health["status"] = "warning"
health["issues"] = append(health["issues"].([]string),
fmt.Sprintf("out_of_order: %s applied before %s", versions[i], versions[i-1]))
}
}
}
return health, nil
}
// Close closes the database connection
func (db *DB) Close() error {
return db.DB.Close()
}