Mining/pkg/mining/config_manager.go
snider 0c8b2d999b feat: Add rate limiting, race condition fix, and shutdown improvements
- Add rate limiting middleware (10 req/s with burst of 20)
- Add atomic UpdateMinersConfig to fix config race conditions
- Add WebSocket connection limits (max 100 connections)
- Add graceful shutdown timeout (10s max wait for goroutines)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 09:51:48 +00:00

232 lines
6 KiB
Go

package mining
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/adrg/xdg"
)
// configMu protects concurrent access to config file operations
var configMu sync.RWMutex
// MinerAutostartConfig represents the configuration for a single miner's autostart settings.
type MinerAutostartConfig struct {
MinerType string `json:"minerType"`
Autostart bool `json:"autostart"`
Config *Config `json:"config,omitempty"` // Store the last used config
}
// DatabaseConfig holds configuration for SQLite database persistence.
type DatabaseConfig struct {
// Enabled determines if database persistence is active (default: true)
Enabled bool `json:"enabled"`
// RetentionDays is how long to keep historical data (default: 30)
RetentionDays int `json:"retentionDays,omitempty"`
}
// defaultDatabaseConfig returns the default database configuration.
func defaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Enabled: true,
RetentionDays: 30,
}
}
// MinersConfig represents the overall configuration for all miners, including autostart settings.
type MinersConfig struct {
Miners []MinerAutostartConfig `json:"miners"`
Database DatabaseConfig `json:"database"`
}
// getMinersConfigPath returns the path to the miners configuration file.
func getMinersConfigPath() (string, error) {
return xdg.ConfigFile("lethean-desktop/miners/config.json")
}
// LoadMinersConfig loads the miners configuration from the file system.
func LoadMinersConfig() (*MinersConfig, error) {
configMu.RLock()
defer configMu.RUnlock()
configPath, err := getMinersConfigPath()
if err != nil {
return nil, fmt.Errorf("could not determine miners config path: %w", err)
}
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
// Return empty config with defaults if file doesn't exist
return &MinersConfig{
Miners: []MinerAutostartConfig{},
Database: defaultDatabaseConfig(),
}, nil
}
return nil, fmt.Errorf("failed to read miners config file: %w", err)
}
var cfg MinersConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal miners config: %w", err)
}
// Apply default database config if not set (for backwards compatibility)
if cfg.Database.RetentionDays == 0 {
cfg.Database = defaultDatabaseConfig()
}
return &cfg, nil
}
// SaveMinersConfig saves the miners configuration to the file system.
// Uses atomic write pattern: write to temp file, then rename.
func SaveMinersConfig(cfg *MinersConfig) error {
configMu.Lock()
defer configMu.Unlock()
configPath, err := getMinersConfigPath()
if err != nil {
return fmt.Errorf("could not determine miners config path: %w", err)
}
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal miners config: %w", err)
}
// Atomic write: write to temp file, then rename
tmpFile, err := os.CreateTemp(dir, "miners-config-*.tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := tmpFile.Name()
// Clean up temp file on error
success := false
defer func() {
if !success {
os.Remove(tmpPath)
}
}()
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write temp file: %w", err)
}
if err := tmpFile.Sync(); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to sync temp file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
if err := os.Chmod(tmpPath, 0600); err != nil {
return fmt.Errorf("failed to set temp file permissions: %w", err)
}
if err := os.Rename(tmpPath, configPath); err != nil {
return fmt.Errorf("failed to rename temp file: %w", err)
}
success = true
return nil
}
// UpdateMinersConfig atomically loads, modifies, and saves the miners config.
// This prevents race conditions in read-modify-write operations.
func UpdateMinersConfig(fn func(*MinersConfig) error) error {
configMu.Lock()
defer configMu.Unlock()
configPath, err := getMinersConfigPath()
if err != nil {
return fmt.Errorf("could not determine miners config path: %w", err)
}
// Load current config
var cfg MinersConfig
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
cfg = MinersConfig{
Miners: []MinerAutostartConfig{},
Database: defaultDatabaseConfig(),
}
} else {
return fmt.Errorf("failed to read miners config file: %w", err)
}
} else {
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("failed to unmarshal miners config: %w", err)
}
if cfg.Database.RetentionDays == 0 {
cfg.Database = defaultDatabaseConfig()
}
}
// Apply the modification
if err := fn(&cfg); err != nil {
return err
}
// Save atomically
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
newData, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal miners config: %w", err)
}
tmpFile, err := os.CreateTemp(dir, "miners-config-*.tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := tmpFile.Name()
success := false
defer func() {
if !success {
os.Remove(tmpPath)
}
}()
if _, err := tmpFile.Write(newData); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write temp file: %w", err)
}
if err := tmpFile.Sync(); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to sync temp file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
if err := os.Chmod(tmpPath, 0600); err != nil {
return fmt.Errorf("failed to set temp file permissions: %w", err)
}
if err := os.Rename(tmpPath, configPath); err != nil {
return fmt.Errorf("failed to rename temp file: %w", err)
}
success = true
return nil
}