kevo/pkg/config/config.go
Jeremy Tregunna 6fc3be617d
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
feat: Initial release of kevo storage engine.
Adds a complete LSM-based storage engine with these features:
- Single-writer based architecture for the storage engine
- WAL for durability, and hey it's configurable
- MemTable with skip list implementation for fast read/writes
- SSTable with block-based structure for on-disk level-based storage
- Background compaction with tiered strategy
- ACID transactions
- Good documentation (I hope)
2025-04-20 14:06:50 -06:00

203 lines
5.2 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
)
const (
DefaultManifestFileName = "MANIFEST"
CurrentManifestVersion = 1
)
var (
ErrInvalidConfig = errors.New("invalid configuration")
ErrManifestNotFound = errors.New("manifest not found")
ErrInvalidManifest = errors.New("invalid manifest")
)
type SyncMode int
const (
SyncNone SyncMode = iota
SyncBatch
SyncImmediate
)
type Config struct {
Version int `json:"version"`
// WAL configuration
WALDir string `json:"wal_dir"`
WALSyncMode SyncMode `json:"wal_sync_mode"`
WALSyncBytes int64 `json:"wal_sync_bytes"`
WALMaxSize int64 `json:"wal_max_size"`
// MemTable configuration
MemTableSize int64 `json:"memtable_size"`
MaxMemTables int `json:"max_memtables"`
MaxMemTableAge int64 `json:"max_memtable_age"`
MemTablePoolCap int `json:"memtable_pool_cap"`
// SSTable configuration
SSTDir string `json:"sst_dir"`
SSTableBlockSize int `json:"sstable_block_size"`
SSTableIndexSize int `json:"sstable_index_size"`
SSTableMaxSize int64 `json:"sstable_max_size"`
SSTableRestartSize int `json:"sstable_restart_size"`
// Compaction configuration
CompactionLevels int `json:"compaction_levels"`
CompactionRatio float64 `json:"compaction_ratio"`
CompactionThreads int `json:"compaction_threads"`
CompactionInterval int64 `json:"compaction_interval"`
MaxLevelWithTombstones int `json:"max_level_with_tombstones"` // Levels higher than this discard tombstones
mu sync.RWMutex
}
// NewDefaultConfig creates a Config with recommended default values
func NewDefaultConfig(dbPath string) *Config {
walDir := filepath.Join(dbPath, "wal")
sstDir := filepath.Join(dbPath, "sst")
return &Config{
Version: CurrentManifestVersion,
// WAL defaults
WALDir: walDir,
WALSyncMode: SyncBatch,
WALSyncBytes: 1024 * 1024, // 1MB
// MemTable defaults
MemTableSize: 32 * 1024 * 1024, // 32MB
MaxMemTables: 4,
MaxMemTableAge: 600, // 10 minutes
MemTablePoolCap: 4,
// SSTable defaults
SSTDir: sstDir,
SSTableBlockSize: 16 * 1024, // 16KB
SSTableIndexSize: 64 * 1024, // 64KB
SSTableMaxSize: 64 * 1024 * 1024, // 64MB
SSTableRestartSize: 16, // Restart points every 16 keys
// Compaction defaults
CompactionLevels: 7,
CompactionRatio: 10,
CompactionThreads: 2,
CompactionInterval: 30, // 30 seconds
MaxLevelWithTombstones: 1, // Keep tombstones in levels 0 and 1
}
}
// Validate checks if the configuration is valid
func (c *Config) Validate() error {
c.mu.RLock()
defer c.mu.RUnlock()
if c.Version <= 0 {
return fmt.Errorf("%w: invalid version %d", ErrInvalidConfig, c.Version)
}
if c.WALDir == "" {
return fmt.Errorf("%w: WAL directory not specified", ErrInvalidConfig)
}
if c.SSTDir == "" {
return fmt.Errorf("%w: SSTable directory not specified", ErrInvalidConfig)
}
if c.MemTableSize <= 0 {
return fmt.Errorf("%w: MemTable size must be positive", ErrInvalidConfig)
}
if c.MaxMemTables <= 0 {
return fmt.Errorf("%w: Max MemTables must be positive", ErrInvalidConfig)
}
if c.SSTableBlockSize <= 0 {
return fmt.Errorf("%w: SSTable block size must be positive", ErrInvalidConfig)
}
if c.SSTableIndexSize <= 0 {
return fmt.Errorf("%w: SSTable index size must be positive", ErrInvalidConfig)
}
if c.CompactionLevels <= 0 {
return fmt.Errorf("%w: Compaction levels must be positive", ErrInvalidConfig)
}
if c.CompactionRatio <= 1.0 {
return fmt.Errorf("%w: Compaction ratio must be greater than 1.0", ErrInvalidConfig)
}
return nil
}
// LoadConfigFromManifest loads just the configuration portion from the manifest file
func LoadConfigFromManifest(dbPath string) (*Config, error) {
manifestPath := filepath.Join(dbPath, DefaultManifestFileName)
data, err := os.ReadFile(manifestPath)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrManifestNotFound
}
return nil, fmt.Errorf("failed to read manifest: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidManifest, err)
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &cfg, nil
}
// SaveManifest saves the configuration to the manifest file
func (c *Config) SaveManifest(dbPath string) error {
c.mu.RLock()
defer c.mu.RUnlock()
if err := c.Validate(); err != nil {
return err
}
if err := os.MkdirAll(dbPath, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
manifestPath := filepath.Join(dbPath, DefaultManifestFileName)
tempPath := manifestPath + ".tmp"
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(tempPath, data, 0644); err != nil {
return fmt.Errorf("failed to write manifest: %w", err)
}
if err := os.Rename(tempPath, manifestPath); err != nil {
return fmt.Errorf("failed to rename manifest: %w", err)
}
return nil
}
// Update applies the given function to modify the configuration
func (c *Config) Update(fn func(*Config)) {
c.mu.Lock()
defer c.mu.Unlock()
fn(c)
}