mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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
 | 
						|
}
 |