mirror of
https://github.com/juanfont/headscale.git
synced 2025-08-11 19:07:33 +00:00
db: add sqlite "source of truth" schema
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:

committed by
Kristoffer Dalby

parent
855c48aec2
commit
c6736dd6d6
345
hscontrol/db/sqliteconfig/config.go
Normal file
345
hscontrol/db/sqliteconfig/config.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// Package sqliteconfig provides type-safe configuration for SQLite databases
|
||||
// with proper enum validation and URL generation for modernc.org/sqlite driver.
|
||||
package sqliteconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Errors returned by config validation.
|
||||
var (
|
||||
ErrPathEmpty = errors.New("path cannot be empty")
|
||||
ErrBusyTimeoutNegative = errors.New("busy_timeout must be >= 0")
|
||||
ErrInvalidJournalMode = errors.New("invalid journal_mode")
|
||||
ErrInvalidAutoVacuum = errors.New("invalid auto_vacuum")
|
||||
ErrWALAutocheckpoint = errors.New("wal_autocheckpoint must be >= -1")
|
||||
ErrInvalidSynchronous = errors.New("invalid synchronous")
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultBusyTimeout is the default busy timeout in milliseconds.
|
||||
DefaultBusyTimeout = 10000
|
||||
)
|
||||
|
||||
// JournalMode represents SQLite journal_mode pragma values.
|
||||
// Journal modes control how SQLite handles write transactions and crash recovery.
|
||||
//
|
||||
// Performance vs Durability Tradeoffs:
|
||||
//
|
||||
// WAL (Write-Ahead Logging) - Recommended for production:
|
||||
// - Best performance for concurrent reads/writes
|
||||
// - Readers don't block writers, writers don't block readers
|
||||
// - Excellent crash recovery with minimal data loss risk
|
||||
// - Uses additional .wal and .shm files
|
||||
// - Default choice for Headscale production deployments
|
||||
//
|
||||
// DELETE - Traditional rollback journal:
|
||||
// - Good performance for single-threaded access
|
||||
// - Readers block writers and vice versa
|
||||
// - Reliable crash recovery but with exclusive locking
|
||||
// - Creates temporary journal files during transactions
|
||||
// - Suitable for low-concurrency scenarios
|
||||
//
|
||||
// TRUNCATE - Similar to DELETE but faster cleanup:
|
||||
// - Slightly better performance than DELETE
|
||||
// - Same concurrency limitations as DELETE
|
||||
// - Faster transaction commit by truncating instead of deleting journal
|
||||
//
|
||||
// PERSIST - Journal file remains between transactions:
|
||||
// - Avoids file creation/deletion overhead
|
||||
// - Same concurrency limitations as DELETE
|
||||
// - Good for frequent small transactions
|
||||
//
|
||||
// MEMORY - Journal kept in memory:
|
||||
// - Fastest performance but NO crash recovery
|
||||
// - Data loss risk on power failure or crash
|
||||
// - Only suitable for temporary or non-critical data
|
||||
//
|
||||
// OFF - No journaling:
|
||||
// - Maximum performance but NO transaction safety
|
||||
// - High risk of database corruption on crash
|
||||
// - Should only be used for read-only or disposable databases
|
||||
type JournalMode string
|
||||
|
||||
const (
|
||||
// JournalModeWAL enables Write-Ahead Logging (RECOMMENDED for production).
|
||||
// Best concurrent performance + crash recovery. Uses additional .wal/.shm files.
|
||||
JournalModeWAL JournalMode = "WAL"
|
||||
|
||||
// JournalModeDelete uses traditional rollback journaling.
|
||||
// Good single-threaded performance, readers block writers. Creates temp journal files.
|
||||
JournalModeDelete JournalMode = "DELETE"
|
||||
|
||||
// JournalModeTruncate is like DELETE but with faster cleanup.
|
||||
// Slightly better performance than DELETE, same safety with exclusive locking.
|
||||
JournalModeTruncate JournalMode = "TRUNCATE"
|
||||
|
||||
// JournalModePersist keeps journal file between transactions.
|
||||
// Good for frequent transactions, avoids file creation/deletion overhead.
|
||||
JournalModePersist JournalMode = "PERSIST"
|
||||
|
||||
// JournalModeMemory keeps journal in memory (DANGEROUS).
|
||||
// Fastest performance but NO crash recovery - data loss on power failure.
|
||||
JournalModeMemory JournalMode = "MEMORY"
|
||||
|
||||
// JournalModeOff disables journaling entirely (EXTREMELY DANGEROUS).
|
||||
// Maximum performance but high corruption risk. Only for disposable databases.
|
||||
JournalModeOff JournalMode = "OFF"
|
||||
)
|
||||
|
||||
// IsValid returns true if the JournalMode is valid.
|
||||
func (j JournalMode) IsValid() bool {
|
||||
switch j {
|
||||
case JournalModeWAL, JournalModeDelete, JournalModeTruncate,
|
||||
JournalModePersist, JournalModeMemory, JournalModeOff:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation.
|
||||
func (j JournalMode) String() string {
|
||||
return string(j)
|
||||
}
|
||||
|
||||
// AutoVacuum represents SQLite auto_vacuum pragma values.
|
||||
// Auto-vacuum controls how SQLite reclaims space from deleted data.
|
||||
//
|
||||
// Performance vs Storage Tradeoffs:
|
||||
//
|
||||
// INCREMENTAL - Recommended for production:
|
||||
// - Reclaims space gradually during normal operations
|
||||
// - Minimal performance impact on writes
|
||||
// - Database size shrinks automatically over time
|
||||
// - Can manually trigger with PRAGMA incremental_vacuum
|
||||
// - Good balance of space efficiency and performance
|
||||
//
|
||||
// FULL - Automatic space reclamation:
|
||||
// - Immediately reclaims space on every DELETE/DROP
|
||||
// - Higher write overhead due to page reorganization
|
||||
// - Keeps database file size minimal
|
||||
// - Can cause significant slowdowns on large deletions
|
||||
// - Best for applications with frequent deletes and limited storage
|
||||
//
|
||||
// NONE - No automatic space reclamation:
|
||||
// - Fastest write performance (no vacuum overhead)
|
||||
// - Database file only grows, never shrinks
|
||||
// - Deleted space is reused but file size remains large
|
||||
// - Requires manual VACUUM to reclaim space
|
||||
// - Best for write-heavy workloads where storage isn't constrained
|
||||
type AutoVacuum string
|
||||
|
||||
const (
|
||||
// AutoVacuumNone disables automatic space reclamation.
|
||||
// Fastest writes, file only grows. Requires manual VACUUM to reclaim space.
|
||||
AutoVacuumNone AutoVacuum = "NONE"
|
||||
|
||||
// AutoVacuumFull immediately reclaims space on every DELETE/DROP.
|
||||
// Minimal file size but slower writes. Can impact performance on large deletions.
|
||||
AutoVacuumFull AutoVacuum = "FULL"
|
||||
|
||||
// AutoVacuumIncremental reclaims space gradually (RECOMMENDED for production).
|
||||
// Good balance: minimal write impact, automatic space management over time.
|
||||
AutoVacuumIncremental AutoVacuum = "INCREMENTAL"
|
||||
)
|
||||
|
||||
// IsValid returns true if the AutoVacuum is valid.
|
||||
func (a AutoVacuum) IsValid() bool {
|
||||
switch a {
|
||||
case AutoVacuumNone, AutoVacuumFull, AutoVacuumIncremental:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation.
|
||||
func (a AutoVacuum) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// Synchronous represents SQLite synchronous pragma values.
|
||||
// Synchronous mode controls how aggressively SQLite flushes data to disk.
|
||||
//
|
||||
// Performance vs Durability Tradeoffs:
|
||||
//
|
||||
// NORMAL - Recommended for production:
|
||||
// - Good balance of performance and safety
|
||||
// - Syncs at critical moments (transaction commits in WAL mode)
|
||||
// - Very low risk of corruption, minimal performance impact
|
||||
// - Safe with WAL mode even with power loss
|
||||
// - Default choice for most production applications
|
||||
//
|
||||
// FULL - Maximum durability:
|
||||
// - Syncs to disk after every write operation
|
||||
// - Highest data safety, virtually no corruption risk
|
||||
// - Significant performance penalty (up to 50% slower)
|
||||
// - Recommended for critical data where corruption is unacceptable
|
||||
//
|
||||
// EXTRA - Paranoid mode:
|
||||
// - Even more aggressive syncing than FULL
|
||||
// - Maximum possible data safety
|
||||
// - Severe performance impact
|
||||
// - Only for extremely critical scenarios
|
||||
//
|
||||
// OFF - Maximum performance, minimum safety:
|
||||
// - No syncing, relies on OS to flush data
|
||||
// - Fastest possible performance
|
||||
// - High risk of corruption on power failure or crash
|
||||
// - Only suitable for non-critical or easily recreatable data
|
||||
type Synchronous string
|
||||
|
||||
const (
|
||||
// SynchronousOff disables syncing (DANGEROUS).
|
||||
// Fastest performance but high corruption risk on power failure. Avoid in production.
|
||||
SynchronousOff Synchronous = "OFF"
|
||||
|
||||
// SynchronousNormal provides balanced performance and safety (RECOMMENDED).
|
||||
// Good performance with low corruption risk. Safe with WAL mode on power loss.
|
||||
SynchronousNormal Synchronous = "NORMAL"
|
||||
|
||||
// SynchronousFull provides maximum durability with performance cost.
|
||||
// Syncs after every write. Up to 50% slower but virtually no corruption risk.
|
||||
SynchronousFull Synchronous = "FULL"
|
||||
|
||||
// SynchronousExtra provides paranoid-level data safety (EXTREME).
|
||||
// Maximum safety with severe performance impact. Rarely needed in practice.
|
||||
SynchronousExtra Synchronous = "EXTRA"
|
||||
)
|
||||
|
||||
// IsValid returns true if the Synchronous is valid.
|
||||
func (s Synchronous) IsValid() bool {
|
||||
switch s {
|
||||
case SynchronousOff, SynchronousNormal, SynchronousFull, SynchronousExtra:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation.
|
||||
func (s Synchronous) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Config holds SQLite database configuration with type-safe enums.
|
||||
// This configuration balances performance, durability, and operational requirements
|
||||
// for Headscale's SQLite database usage patterns.
|
||||
type Config struct {
|
||||
Path string // file path or ":memory:"
|
||||
BusyTimeout int // milliseconds (0 = default/disabled)
|
||||
JournalMode JournalMode // journal mode (affects concurrency and crash recovery)
|
||||
AutoVacuum AutoVacuum // auto vacuum mode (affects storage efficiency)
|
||||
WALAutocheckpoint int // pages (-1 = default/not set, 0 = disabled, >0 = enabled)
|
||||
Synchronous Synchronous // synchronous mode (affects durability vs performance)
|
||||
ForeignKeys bool // enable foreign key constraints (data integrity)
|
||||
}
|
||||
|
||||
// Default returns the production configuration optimized for Headscale's usage patterns.
|
||||
// This configuration prioritizes:
|
||||
// - Concurrent access (WAL mode for multiple readers/writers)
|
||||
// - Data durability with good performance (NORMAL synchronous)
|
||||
// - Automatic space management (INCREMENTAL auto-vacuum)
|
||||
// - Data integrity (foreign key constraints enabled)
|
||||
// - Reasonable timeout for busy database scenarios (10s)
|
||||
func Default(path string) *Config {
|
||||
return &Config{
|
||||
Path: path,
|
||||
BusyTimeout: DefaultBusyTimeout,
|
||||
JournalMode: JournalModeWAL,
|
||||
AutoVacuum: AutoVacuumIncremental,
|
||||
WALAutocheckpoint: 1000,
|
||||
Synchronous: SynchronousNormal,
|
||||
ForeignKeys: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Memory returns a configuration for in-memory databases.
|
||||
func Memory() *Config {
|
||||
return &Config{
|
||||
Path: ":memory:",
|
||||
WALAutocheckpoint: -1, // not set, use driver default
|
||||
ForeignKeys: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if all configuration values are valid.
|
||||
func (c *Config) Validate() error {
|
||||
if c.Path == "" {
|
||||
return ErrPathEmpty
|
||||
}
|
||||
|
||||
if c.BusyTimeout < 0 {
|
||||
return fmt.Errorf("%w, got %d", ErrBusyTimeoutNegative, c.BusyTimeout)
|
||||
}
|
||||
|
||||
if c.JournalMode != "" && !c.JournalMode.IsValid() {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidJournalMode, c.JournalMode)
|
||||
}
|
||||
|
||||
if c.AutoVacuum != "" && !c.AutoVacuum.IsValid() {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidAutoVacuum, c.AutoVacuum)
|
||||
}
|
||||
|
||||
if c.WALAutocheckpoint < -1 {
|
||||
return fmt.Errorf("%w, got %d", ErrWALAutocheckpoint, c.WALAutocheckpoint)
|
||||
}
|
||||
|
||||
if c.Synchronous != "" && !c.Synchronous.IsValid() {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidSynchronous, c.Synchronous)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToURL builds a properly encoded SQLite connection string using _pragma parameters
|
||||
// compatible with modernc.org/sqlite driver.
|
||||
func (c *Config) ToURL() (string, error) {
|
||||
if err := c.Validate(); err != nil {
|
||||
return "", fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
var pragmas []string
|
||||
|
||||
// Add pragma parameters only if they're set (non-zero/non-empty)
|
||||
if c.BusyTimeout > 0 {
|
||||
pragmas = append(pragmas, fmt.Sprintf("busy_timeout=%d", c.BusyTimeout))
|
||||
}
|
||||
if c.JournalMode != "" {
|
||||
pragmas = append(pragmas, fmt.Sprintf("journal_mode=%s", c.JournalMode))
|
||||
}
|
||||
if c.AutoVacuum != "" {
|
||||
pragmas = append(pragmas, fmt.Sprintf("auto_vacuum=%s", c.AutoVacuum))
|
||||
}
|
||||
if c.WALAutocheckpoint >= 0 {
|
||||
pragmas = append(pragmas, fmt.Sprintf("wal_autocheckpoint=%d", c.WALAutocheckpoint))
|
||||
}
|
||||
if c.Synchronous != "" {
|
||||
pragmas = append(pragmas, fmt.Sprintf("synchronous=%s", c.Synchronous))
|
||||
}
|
||||
if c.ForeignKeys {
|
||||
pragmas = append(pragmas, "foreign_keys=ON")
|
||||
}
|
||||
|
||||
// Handle different database types
|
||||
var baseURL string
|
||||
if c.Path == ":memory:" {
|
||||
baseURL = ":memory:"
|
||||
} else {
|
||||
baseURL = "file:" + c.Path
|
||||
}
|
||||
|
||||
// Add parameters without encoding = signs
|
||||
if len(pragmas) > 0 {
|
||||
var queryParts []string
|
||||
for _, pragma := range pragmas {
|
||||
queryParts = append(queryParts, "_pragma="+pragma)
|
||||
}
|
||||
baseURL += "?" + strings.Join(queryParts, "&")
|
||||
}
|
||||
|
||||
return baseURL, nil
|
||||
}
|
Reference in New Issue
Block a user