Files
futriix/internal/config/config.go
T

2205 lines
71 KiB
Go
Raw 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.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/config/config.go
// Назначение: Загрузка и парсинг TOML-конфигурации, валидация параметров,
// предоставление доступа к настройкам кластера, хранилища и REPL.
//
// Данный пакет отвечает за:
// - Чтение конфигурационного файла в формате TOML
// - Установку значений по умолчанию для всех параметров
// - Валидацию корректности настроек
// - Предоставление типобезопасного доступа к конфигурации через методы-геттеры
// - Сохранение конфигурации в TOML (SaveConfig) — используется REPL-командой
// "config set" в fallback-режиме, когда Raft недоступен.
//
// ИСПРАВЛЕНО: добавлена секция [metrics] (MetricsConfig) — иначе
// cmd/futriis/main.go не компилируется из-за cfg.Metrics.
//
// ИСПРАВЛЕНО (аудит, кросс-платформенность Linux/OpenIndiana):
// - BackupConfig дополнен полями IncludeSagaState и EnableIncremental.
// - Добавлены геттеры IsIncludeSagaStateEnabled / IsIncrementalEnabled.
// - В LoadConfig добавлены значения по умолчанию для новых полей.
// - В ValidateConfigFull добавлена валидация [backup] и [engines].
//
// ДОБАВЛЕНО (2026-10):
// - SaveConfig — атомарная запись конфигурации в TOML (tempfile + fsync + rename).
// - fsyncDir — синхронизация директории (no-op на Windows).
package config
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/BurntSushi/toml"
)
// =============================================================================
// КОНСТАНТЫ ДЛЯ ВАЛИДАЦИИ
// =============================================================================
const (
// Максимальный размер файла конфигурации (1 MB)
maxConfigFileSize = 1 * 1024 * 1024
// Минимальная длина API-ключа
minAPIKeyLength = 8
)
// allowedLogLevels — допустимые уровни логирования
var allowedLogLevels = map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
"fatal": true,
}
// allowedMigrationModes — допустимые режимы миграции
var allowedMigrationModes = map[string]bool{
"manual": true,
"semi_auto": true,
"auto": true,
}
// allowedCompressionAlgorithms — допустимые алгоритмы сжатия
var allowedCompressionAlgorithms = map[string]bool{
"snappy": true,
"lz4": true,
"zstd": true,
}
// =============================================================================
// ОСНОВНАЯ СТРУКТУРА КОНФИГУРАЦИИ
// =============================================================================
// Config представляет собой корневую структуру конфигурации,
// объединяющую все подсистемы приложения.
// Каждое поле соответствует секции в TOML-файле.
type Config struct {
Cluster ClusterConfig `toml:"cluster"` // Настройки кластеризации (Raft, узлы)
Storage StorageConfig `toml:"storage"` // Настройки хранилища (движки, ограничения)
Repl ReplConfig `toml:"repl"` // Настройки REPL (интерактивной оболочки)
Log LogConfig `toml:"log"` // Настройки логирования
API APIConfig `toml:"api"` // Настройки HTTP API
Replication ReplicationConfig `toml:"replication"` // Настройки репликации
Plugins PluginsConfig `toml:"plugins"` // Настройки системы плагинов (Lua)
Compression CompressionConfig `toml:"compression"` // Настройки сжатия данных
Performance PerformanceConfig `toml:"performance"` // Настройки производительности
Security SecurityConfig `toml:"security"` // Настройки безопасности (TLS)
Monitoring MonitoringConfig `toml:"monitoring"` // Настройки мониторинга (метрики, трассировка)
Recovery RecoveryConfig `toml:"recovery"` // Настройки восстановления после сбоев
WAL WALConfig `toml:"wal"` // Настройки WAL (Write-Ahead Log)
MVCC MVCCConfig `toml:"mvcc"` // Настройки MVCC
Transactions TransactionsConfig `toml:"transactions"` // Настройки транзакций
Saga SagaConfig `toml:"saga"` // Настройки SAGA оркестратора
ACL ACLConfig `toml:"acl"` // Настройки ACL
ClusterTLS TLSConfig `toml:"cluster_tls"` // Настройки TLS для кластерного взаимодействия
Backpressure BackpressureConfig `toml:"backpressure"` // Настройки механизма обратного давления
RuntimeLimits RuntimeLimitsConfig `toml:"runtime_limits"` // Ограничения времени выполнения
Autoscaling AutoscalingConfig `toml:"autoscaling"` // Настройки автоматического масштабирования
SchemaMigration SchemaMigrationConfig `toml:"schema_migration"` // Настройки миграции схемы
Backup BackupConfig `toml:"backup"` // Настройки резервного копирования
Engines EnginesConfig `toml:"engines"` // Настройки движков хранения
Migration MigrationConfig `toml:"migration"` // Настройки кросс-датацентровой миграции
Metrics MetricsConfig `toml:"metrics"` // Настройки Prometheus/Grafana
}
// =============================================================================
// КОНФИГУРАЦИЯ МЕТРИК
// =============================================================================
// MetricsConfig содержит настройки экспорта метрик в формате Prometheus.
type MetricsConfig struct {
Enabled bool `toml:"enabled"`
CollectIntervalSec int `toml:"collect_interval_sec"`
}
// IsMetricsEnabled возвращает флаг включения метрик.
func (m *MetricsConfig) IsMetricsEnabled() bool { return m.Enabled }
// GetCollectInterval возвращает интервал сбора метрик как time.Duration.
func (m *MetricsConfig) GetCollectInterval() time.Duration {
if m.CollectIntervalSec <= 0 {
return 15 * time.Second
}
return time.Duration(m.CollectIntervalSec) * time.Second
}
// =============================================================================
// КОНФИГУРАЦИЯ КЛАСТЕРА
// =============================================================================
// ClusterConfig содержит настройки для работы в кластерном режиме.
type ClusterConfig struct {
Name string `toml:"name"`
NodeIP string `toml:"node_ip"`
NodePort int `toml:"node_port"`
RaftPort int `toml:"raft_port"`
RaftDataDir string `toml:"raft_data_dir"`
Bootstrap bool `toml:"bootstrap"`
Nodes []string `toml:"nodes"`
HeartbeatTimeoutMs int `toml:"heartbeat_timeout_ms"`
ElectionTimeoutMs int `toml:"election_timeout_ms"`
CommitTimeoutMs int `toml:"commit_timeout_ms"`
SnapshotIntervalMin int `toml:"snapshot_interval_min"`
SnapshotThreshold int `toml:"snapshot_threshold"`
SplitBrainPrevention bool `toml:"split_brain_prevention"`
RecoveryTimeoutSec int `toml:"recovery_timeout_sec"`
Region string `toml:"region"`
PriorityZone int `toml:"priority_zone"`
}
// =============================================================================
// КОНФИГУРАЦИЯ SAGA
// =============================================================================
// SagaConfig содержит настройки SAGA оркестратора.
type SagaConfig struct {
Enabled bool `toml:"enabled"`
CoordinatorCount int `toml:"coordinator_count"`
StateDir string `toml:"state_dir"`
MaxRetries int `toml:"max_retries"`
RetryBackoffMs int `toml:"retry_backoff_ms"`
SagaTimeoutSec int `toml:"saga_timeout_sec"`
StuckCheckIntervalSec int `toml:"stuck_check_interval_sec"`
LeaderElectionIntervalSec int `toml:"leader_election_interval_sec"`
RecoveryIntervalSec int `toml:"recovery_interval_sec"`
MetricsIntervalSec int `toml:"metrics_interval_sec"`
CleanupPeriodHours int `toml:"cleanup_period_hours"`
MaxCacheSize int `toml:"max_cache_size"`
OperationRetentionDays int `toml:"operation_retention_days"`
ChannelBufferSize int `toml:"channel_buffer_size"`
AsyncRecoveryWorkers int `toml:"async_recovery_workers"`
AsyncRecoveryTimeoutSec int `toml:"async_recovery_timeout_sec"`
FsyncEnabled bool `toml:"fsync_enabled"`
FsyncMaxRetries int `toml:"fsync_max_retries"`
FsyncRetryDelayMs int `toml:"fsync_retry_delay_ms"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ХРАНИЛИЩА
// =============================================================================
type StorageConfig struct {
PageSizeMB int `toml:"page_size_mb"`
MaxCollections int `toml:"max_collections"`
MaxDocumentsPerCollection int `toml:"max_documents_per_collection"`
DefaultEngine string `toml:"default_engine"`
EnableCustomEngines bool `toml:"enable_custom_engines"`
}
// =============================================================================
// КОНФИГУРАЦИЯ REPL
// =============================================================================
type ReplConfig struct {
PromptColor string `toml:"prompt_color"`
HistorySize int `toml:"history_size"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ЛОГИРОВАНИЯ
// =============================================================================
type LogConfig struct {
LogFile string `toml:"log_file"`
LogLevel string `toml:"log_level"`
}
// =============================================================================
// КОНФИГУРАЦИЯ HTTP API
// =============================================================================
type APIConfig struct {
Port int `toml:"port"`
}
// =============================================================================
// КОНФИГУРАЦИЯ РЕПЛИКАЦИИ
// =============================================================================
type ReplicationConfig struct {
Enabled bool `toml:"enabled"`
SyncReplication bool `toml:"sync_replication"`
ReplicationTimeoutMs int `toml:"replication_timeout_ms"`
MaxReplicaLagMs int `toml:"max_replica_lag_ms"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ПЛАГИНОВ (LUA)
// =============================================================================
type PluginsConfig struct {
Enabled bool `toml:"enabled"`
ScriptDir string `toml:"script_dir"`
AllowList []string `toml:"allow_list"`
MaxCPUTimeMs int `toml:"max_cpu_time_ms"`
MaxMemoryMB int `toml:"max_memory_mb"`
MaxExecutionTimeSec int `toml:"max_execution_time_sec"`
MaxInstructions int64 `toml:"max_instructions"`
HotReloadIntervalSec int `toml:"hot_reload_interval_sec"`
MaxEventLogSize int `toml:"max_event_log_size"`
LoadTimeoutSec int `toml:"load_timeout_sec"`
MaxLuaStates int `toml:"max_lua_states"`
LuaStateTTLSec int `toml:"lua_state_ttl_sec"`
EnginePluginDir string `toml:"engine_plugin_dir"`
}
// =============================================================================
// КОНФИГУРАЦИЯ СЖАТИЯ
// =============================================================================
type CompressionConfig struct {
Enabled bool `toml:"enabled"`
Algorithm string `toml:"algorithm"`
Level int `toml:"level"`
MinSize int `toml:"min_size"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ПРОИЗВОДИТЕЛЬНОСТИ
// =============================================================================
type PerformanceConfig struct {
EnablePipeline bool `toml:"enable_pipeline"`
BatchSize int `toml:"batch_size"`
ReadFromFollower bool `toml:"read_from_follower"`
MaxConnections int `toml:"max_connections"`
ReadReplicaDelayMs int `toml:"read_replica_delay_ms"`
}
// =============================================================================
// КОНФИГУРАЦИЯ БЕЗОПАСНОСТИ
// =============================================================================
type SecurityConfig struct {
EnableTLS bool `toml:"enable_tls"`
CertFile string `toml:"cert_file"`
KeyFile string `toml:"key_file"`
CAFile string `toml:"ca_file"`
MinVersion string `toml:"min_version"`
}
// =============================================================================
// КОНФИГУРАЦИЯ МОНИТОРИНГА
// =============================================================================
type MonitoringConfig struct {
EnableMetrics bool `toml:"enable_metrics"`
MetricsPort int `toml:"metrics_port"`
EnableTracing bool `toml:"enable_tracing"`
TraceSampleRate float64 `toml:"trace_sample_rate"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ВОССТАНОВЛЕНИЯ
// =============================================================================
type RecoveryConfig struct {
AutoRejoin bool `toml:"auto_rejoin"`
MaxRetrySec int `toml:"max_retry_sec"`
DataReplicationTimeoutSec int `toml:"data_replication_timeout_sec"`
StaleReadTimeoutSec int `toml:"stale_read_timeout_sec"`
}
// =============================================================================
// КОНФИГУРАЦИЯ WAL
// =============================================================================
type WALConfig struct {
SegmentSizeMB int `toml:"segment_size_mb"`
SyncIntervalSec int `toml:"sync_interval_sec"`
BatchSize int `toml:"batch_size"`
RecoveryWorkers int `toml:"recovery_workers"`
Enabled bool `toml:"enabled"`
AsyncRecovery bool `toml:"async_recovery"`
AsyncRecoveryWorkers int `toml:"async_recovery_workers"`
AsyncRecoveryBuffer int `toml:"async_recovery_buffer"`
}
// =============================================================================
// КОНФИГУРАЦИЯ MVCC
// =============================================================================
type MVCCConfig struct {
MaxVersionsPerDoc int `toml:"max_versions_per_doc"`
VisibilityMapSize int `toml:"visibility_map_size"`
PruneIntervalMin int `toml:"prune_interval_min"`
RetentionDays int `toml:"retention_days"`
ReadCacheSize int `toml:"read_cache_size"`
ReadCacheTTLSec int `toml:"read_cache_ttl_sec"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ТРАНЗАКЦИЙ
// =============================================================================
type TransactionsConfig struct {
DefaultTimeoutSec int `toml:"default_timeout_sec"`
DeadlockCheckIntervalSec int `toml:"deadlock_check_interval_sec"`
MaxSavepointsPerTx int `toml:"max_savepoints_per_tx"`
Enabled bool `toml:"enabled"`
CheckpointIntervalSec int `toml:"checkpoint_interval_sec"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ACL
// =============================================================================
type ACLConfig struct {
MaxDeniedLogSize int `toml:"max_denied_log_size"`
TemporaryGrantTTLHours int `toml:"temporary_grant_ttl_hours"`
EnableRoleHierarchy bool `toml:"enable_role_hierarchy"`
CacheTTLSec int `toml:"cache_ttl_sec"`
}
// =============================================================================
// КОНФИГУРАЦИЯ TLS ДЛЯ КЛАСТЕРА
// =============================================================================
type TLSConfig struct {
Enabled bool `toml:"enabled"`
CertFile string `toml:"cert_file"`
KeyFile string `toml:"key_file"`
CAFile string `toml:"ca_file"`
MinVersion string `toml:"min_version"`
MutualAuth bool `toml:"mutual_auth"`
KeyRotationDays int `toml:"key_rotation_days"`
AutoGenerate bool `toml:"auto_generate"`
}
// =============================================================================
// КОНФИГУРАЦИЯ BACKPRESSURE
// =============================================================================
type BackpressureConfig struct {
Enabled bool `toml:"enabled"`
CPUThreshold float64 `toml:"cpu_threshold"`
MemoryThreshold float64 `toml:"memory_threshold"`
QueueSizeThreshold int `toml:"queue_size_threshold"`
ConnectionThreshold int `toml:"connection_threshold"`
CheckIntervalMs int `toml:"check_interval_ms"`
LowDelayMs int64 `toml:"low_delay_ms"`
MediumRejectProb uint32 `toml:"medium_reject_prob"`
HighRejectProb uint32 `toml:"high_reject_prob"`
}
// =============================================================================
// КОНФИГУРАЦИЯ RUNTIME LIMITS
// =============================================================================
type RuntimeLimitsConfig struct {
Enabled bool `toml:"enabled"`
GlobalMaxDocSizeMB int `toml:"global_max_doc_size_mb"`
GlobalMaxCollSizeMB int64 `toml:"global_max_coll_size_mb"`
GlobalMaxDocsPerColl int64 `toml:"global_max_docs_per_coll"`
}
// =============================================================================
// КОНФИГУРАЦИЯ AUTOSCALING
// =============================================================================
type AutoscalingConfig struct {
Enabled bool `toml:"enabled"`
MinNodes int `toml:"min_nodes"`
MaxNodes int `toml:"max_nodes"`
ScaleUpThreshold float64 `toml:"scale_up_threshold"`
ScaleDownThreshold float64 `toml:"scale_down_threshold"`
ScaleUpCooldownSec int `toml:"scale_up_cooldown_sec"`
ScaleDownCooldownSec int `toml:"scale_down_cooldown_sec"`
EvaluationIntervalSec int `toml:"evaluation_interval_sec"`
PredictiveEnabled bool `toml:"predictive_enabled"`
MaxScaleUpNodes int `toml:"max_scale_up_nodes"`
MaxScaleDownNodes int `toml:"max_scale_down_nodes"`
}
// =============================================================================
// КОНФИГУРАЦИЯ МИГРАЦИИ СХЕМЫ
// =============================================================================
type SchemaMigrationConfig struct {
Enabled bool `toml:"enabled"`
MigrationDir string `toml:"migration_dir"`
AutoMigrate bool `toml:"auto_migrate"`
TargetVersion string `toml:"target_version"`
}
// =============================================================================
// КОНФИГУРАЦИЯ BACKUP
// =============================================================================
type BackupConfig struct {
Enabled bool `toml:"enabled"`
BackupDir string `toml:"backup_dir"`
MaxConcurrent int `toml:"max_concurrent"`
CompressEnabled bool `toml:"compress_enabled"`
RetentionDays int `toml:"retention_days"`
IncludeSagaState bool `toml:"include_saga_state"`
EnableIncremental bool `toml:"enable_incremental"`
}
// =============================================================================
// КОНФИГУРАЦИЯ ДВИЖКОВ ХРАНЕНИЯ
// =============================================================================
type EnginesConfig struct {
Row EngineConfig `toml:"row"`
Columnar EngineConfig `toml:"columnar"`
Document EngineConfig `toml:"document"`
KV EngineConfig `toml:"kv"`
TS EngineConfig `toml:"ts"`
Graph EngineConfig `toml:"graph"`
}
type EngineConfig struct {
Enabled bool `toml:"enabled"`
Description string `toml:"description"`
Config map[string]interface{} `toml:"config"`
}
// =============================================================================
// КОНФИГУРАЦИЯ КРОСС-ДАТАЦЕНТРОВОЙ МИГРАЦИИ
// =============================================================================
type MigrationConfig struct {
Enabled bool `toml:"enabled"`
Mode string `toml:"mode"`
Source *DatacenterConfig `toml:"source"`
Target *DatacenterConfig `toml:"target"`
Settings *MigrationSettings `toml:"settings"`
Delta *DeltaSyncConfig `toml:"delta"`
Validation *ValidationConfig `toml:"validation"`
}
type DatacenterConfig struct {
Name string `toml:"name"`
Endpoint string `toml:"endpoint"`
APIKey string `toml:"api_key"`
TimeoutSec int `toml:"timeout_sec"`
}
type MigrationSettings struct {
BatchSize int `toml:"batch_size"`
Workers int `toml:"workers"`
Compression string `toml:"compression"`
ResumeEnabled bool `toml:"resume_enabled"`
CheckpointIntervalSec int `toml:"checkpoint_interval_sec"`
MaxRetries int `toml:"max_retries"`
RetryBackoffSec int `toml:"retry_backoff_sec"`
Collections []string `toml:"collections"`
ExcludeCollections []string `toml:"exclude_collections"`
}
type DeltaSyncConfig struct {
Enabled bool `toml:"enabled"`
IntervalSec int `toml:"interval_sec"`
MaxLagSec int `toml:"max_lag_sec"`
}
type ValidationConfig struct {
Enabled bool `toml:"enabled"`
SamplePercent int `toml:"sample_percent"`
MaxErrors int `toml:"max_errors"`
}
// =============================================================================
// ВСПОМОГАТЕЛЬНЫЕ СТРУКТУРЫ
// =============================================================================
type ValidationResult struct {
Valid bool
Errors []error
Warnings []error
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ SagaConfig
// =============================================================================
func (s *SagaConfig) IsSagaEnabled() bool { return s.Enabled }
func (s *SagaConfig) GetCoordinatorCount() int {
if s.CoordinatorCount < 1 {
return 3
}
return s.CoordinatorCount
}
func (s *SagaConfig) GetStateDir() string {
if s.StateDir == "" {
return "saga_states"
}
return s.StateDir
}
func (s *SagaConfig) GetMaxRetries() int {
if s.MaxRetries <= 0 {
return 5
}
return s.MaxRetries
}
func (s *SagaConfig) GetRetryBackoff() time.Duration {
if s.RetryBackoffMs <= 0 {
return 100 * time.Millisecond
}
return time.Duration(s.RetryBackoffMs) * time.Millisecond
}
func (s *SagaConfig) GetSagaTimeout() time.Duration {
if s.SagaTimeoutSec <= 0 {
return 300 * time.Second
}
return time.Duration(s.SagaTimeoutSec) * time.Second
}
func (s *SagaConfig) GetStuckCheckInterval() time.Duration {
if s.StuckCheckIntervalSec <= 0 {
return 10 * time.Second
}
return time.Duration(s.StuckCheckIntervalSec) * time.Second
}
func (s *SagaConfig) GetLeaderElectionInterval() time.Duration {
if s.LeaderElectionIntervalSec <= 0 {
return 5 * time.Second
}
return time.Duration(s.LeaderElectionIntervalSec) * time.Second
}
func (s *SagaConfig) GetRecoveryInterval() time.Duration {
if s.RecoveryIntervalSec <= 0 {
return 30 * time.Second
}
return time.Duration(s.RecoveryIntervalSec) * time.Second
}
func (s *SagaConfig) GetMetricsInterval() time.Duration {
if s.MetricsIntervalSec <= 0 {
return 60 * time.Second
}
return time.Duration(s.MetricsIntervalSec) * time.Second
}
func (s *SagaConfig) GetCleanupPeriod() time.Duration {
if s.CleanupPeriodHours <= 0 {
return 24 * time.Hour
}
return time.Duration(s.CleanupPeriodHours) * time.Hour
}
func (s *SagaConfig) GetMaxCacheSize() int {
if s.MaxCacheSize <= 0 {
return 10000
}
return s.MaxCacheSize
}
func (s *SagaConfig) GetOperationRetentionDuration() time.Duration {
if s.OperationRetentionDays <= 0 {
return 7 * 24 * time.Hour
}
return time.Duration(s.OperationRetentionDays) * 24 * time.Hour
}
func (s *SagaConfig) GetChannelBufferSize() int {
if s.ChannelBufferSize <= 0 {
return 10000
}
return s.ChannelBufferSize
}
func (s *SagaConfig) GetAsyncRecoveryWorkers() int {
if s.AsyncRecoveryWorkers <= 0 {
return 4
}
return s.AsyncRecoveryWorkers
}
func (s *SagaConfig) GetAsyncRecoveryTimeout() time.Duration {
if s.AsyncRecoveryTimeoutSec <= 0 {
return 30 * time.Second
}
return time.Duration(s.AsyncRecoveryTimeoutSec) * time.Second
}
func (s *SagaConfig) IsFsyncEnabled() bool { return s.FsyncEnabled }
func (s *SagaConfig) GetFsyncMaxRetries() int {
if s.FsyncMaxRetries <= 0 {
return 3
}
return s.FsyncMaxRetries
}
func (s *SagaConfig) GetFsyncRetryDelay() time.Duration {
if s.FsyncRetryDelayMs <= 0 {
return 100 * time.Millisecond
}
return time.Duration(s.FsyncRetryDelayMs) * time.Millisecond
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ ReplicationConfig
// =============================================================================
func (r *ReplicationConfig) GetReplicationTimeout() time.Duration {
if r.ReplicationTimeoutMs <= 0 {
return 5 * time.Second
}
return time.Duration(r.ReplicationTimeoutMs) * time.Millisecond
}
func (r *ReplicationConfig) IsReplicationEnabled() bool { return r.Enabled }
func (r *ReplicationConfig) IsSyncReplicationEnabled() bool { return r.SyncReplication }
func (r *ReplicationConfig) GetMaxReplicaLag() time.Duration {
if r.MaxReplicaLagMs <= 0 {
return 5 * time.Second
}
return time.Duration(r.MaxReplicaLagMs) * time.Millisecond
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ ClusterConfig
// =============================================================================
func (c *ClusterConfig) GetHeartbeatTimeout() time.Duration {
if c.HeartbeatTimeoutMs <= 0 {
return 1000 * time.Millisecond
}
return time.Duration(c.HeartbeatTimeoutMs) * time.Millisecond
}
func (c *ClusterConfig) GetElectionTimeout() time.Duration {
if c.ElectionTimeoutMs <= 0 {
return 1000 * time.Millisecond
}
return time.Duration(c.ElectionTimeoutMs) * time.Millisecond
}
func (c *ClusterConfig) GetCommitTimeout() time.Duration {
if c.CommitTimeoutMs <= 0 {
return 500 * time.Millisecond
}
return time.Duration(c.CommitTimeoutMs) * time.Millisecond
}
func (c *ClusterConfig) GetSnapshotInterval() time.Duration {
if c.SnapshotIntervalMin <= 0 {
return 30 * time.Minute
}
return time.Duration(c.SnapshotIntervalMin) * time.Minute
}
func (c *ClusterConfig) GetSnapshotThreshold() uint64 {
if c.SnapshotThreshold <= 0 {
return 1000
}
return uint64(c.SnapshotThreshold)
}
func (c *ClusterConfig) IsSplitBrainPreventionEnabled() bool { return c.SplitBrainPrevention }
func (c *ClusterConfig) GetRecoveryTimeout() time.Duration {
if c.RecoveryTimeoutSec <= 0 {
return 30 * time.Second
}
return time.Duration(c.RecoveryTimeoutSec) * time.Second
}
func (c *ClusterConfig) GetRegion() string {
if c.Region == "" {
return "default"
}
return c.Region
}
func (c *ClusterConfig) GetPriorityZone() int {
if c.PriorityZone < 0 {
return 0
}
if c.PriorityZone > 9 {
return 9
}
return c.PriorityZone
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ PluginsConfig
// =============================================================================
func (p *PluginsConfig) GetMaxCPUTime() time.Duration {
if p.MaxCPUTimeMs <= 0 {
return 100 * time.Millisecond
}
return time.Duration(p.MaxCPUTimeMs) * time.Millisecond
}
func (p *PluginsConfig) GetMaxMemory() int64 {
if p.MaxMemoryMB <= 0 {
return 50 * 1024 * 1024
}
return int64(p.MaxMemoryMB) * 1024 * 1024
}
func (p *PluginsConfig) GetMaxExecutionTime() time.Duration {
if p.MaxExecutionTimeSec <= 0 {
return 5 * time.Second
}
return time.Duration(p.MaxExecutionTimeSec) * time.Second
}
func (p *PluginsConfig) GetMaxInstructions() int64 {
if p.MaxInstructions <= 0 {
return 1000000
}
return p.MaxInstructions
}
func (p *PluginsConfig) GetHotReloadInterval() time.Duration {
if p.HotReloadIntervalSec <= 0 {
return 30 * time.Second
}
return time.Duration(p.HotReloadIntervalSec) * time.Second
}
func (p *PluginsConfig) GetMaxEventLogSize() int {
if p.MaxEventLogSize <= 0 {
return 1000
}
return p.MaxEventLogSize
}
func (p *PluginsConfig) GetLoadTimeout() time.Duration {
if p.LoadTimeoutSec <= 0 {
return 10 * time.Second
}
return time.Duration(p.LoadTimeoutSec) * time.Second
}
func (p *PluginsConfig) GetMaxLuaStates() int {
if p.MaxLuaStates <= 0 {
return 100
}
return p.MaxLuaStates
}
func (p *PluginsConfig) GetLuaStateTTL() time.Duration {
if p.LuaStateTTLSec <= 0 {
return 10 * time.Minute
}
return time.Duration(p.LuaStateTTLSec) * time.Second
}
func (p *PluginsConfig) GetEnginePluginDir() string {
if p.EnginePluginDir == "" {
return "engines"
}
return p.EnginePluginDir
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ WALConfig
// =============================================================================
func (w *WALConfig) GetSegmentSize() int64 {
if w.SegmentSizeMB <= 0 {
return 64 * 1024 * 1024
}
return int64(w.SegmentSizeMB) * 1024 * 1024
}
func (w *WALConfig) GetSyncInterval() time.Duration {
if w.SyncIntervalSec <= 0 {
return 5 * time.Second
}
return time.Duration(w.SyncIntervalSec) * time.Second
}
func (w *WALConfig) GetBatchSize() int {
if w.BatchSize <= 0 {
return 100
}
return w.BatchSize
}
func (w *WALConfig) GetRecoveryWorkers() int {
if w.RecoveryWorkers <= 0 {
return 4
}
return w.RecoveryWorkers
}
func (w *WALConfig) IsWALEnabled() bool { return w.Enabled }
func (w *WALConfig) IsAsyncRecoveryEnabled() bool { return w.AsyncRecovery }
func (w *WALConfig) GetAsyncRecoveryWorkers() int {
if w.AsyncRecoveryWorkers <= 0 {
return 4
}
return w.AsyncRecoveryWorkers
}
func (w *WALConfig) GetAsyncRecoveryBuffer() int {
if w.AsyncRecoveryBuffer <= 0 {
return 10000
}
return w.AsyncRecoveryBuffer
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ MVCCConfig
// =============================================================================
func (m *MVCCConfig) GetMaxVersionsPerDoc() int {
if m.MaxVersionsPerDoc <= 0 {
return 10
}
return m.MaxVersionsPerDoc
}
func (m *MVCCConfig) GetVisibilityMapSize() int {
if m.VisibilityMapSize <= 0 {
return 1024 * 1024
}
return m.VisibilityMapSize
}
func (m *MVCCConfig) GetPruneInterval() time.Duration {
if m.PruneIntervalMin <= 0 {
return 5 * time.Minute
}
return time.Duration(m.PruneIntervalMin) * time.Minute
}
func (m *MVCCConfig) GetRetentionDays() int {
if m.RetentionDays <= 0 {
return 7
}
return m.RetentionDays
}
func (m *MVCCConfig) GetReadCacheSize() int {
if m.ReadCacheSize <= 0 {
return 10000
}
return m.ReadCacheSize
}
func (m *MVCCConfig) GetReadCacheTTL() time.Duration {
if m.ReadCacheTTLSec <= 0 {
return 300 * time.Second
}
return time.Duration(m.ReadCacheTTLSec) * time.Second
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ TransactionsConfig
// =============================================================================
func (t *TransactionsConfig) GetDefaultTimeout() time.Duration {
if t.DefaultTimeoutSec <= 0 {
return 30 * time.Second
}
return time.Duration(t.DefaultTimeoutSec) * time.Second
}
func (t *TransactionsConfig) GetDeadlockCheckInterval() time.Duration {
if t.DeadlockCheckIntervalSec <= 0 {
return 1 * time.Second
}
return time.Duration(t.DeadlockCheckIntervalSec) * time.Second
}
func (t *TransactionsConfig) GetMaxSavepointsPerTx() int {
if t.MaxSavepointsPerTx <= 0 {
return 100
}
return t.MaxSavepointsPerTx
}
func (t *TransactionsConfig) IsTransactionsEnabled() bool { return t.Enabled }
func (t *TransactionsConfig) GetCheckpointInterval() int64 {
if t.CheckpointIntervalSec <= 0 {
return 300
}
return int64(t.CheckpointIntervalSec)
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ ACLConfig
// =============================================================================
func (a *ACLConfig) GetMaxDeniedLogSize() int {
if a.MaxDeniedLogSize <= 0 {
return 10000
}
return a.MaxDeniedLogSize
}
func (a *ACLConfig) GetTemporaryGrantTTL() time.Duration {
if a.TemporaryGrantTTLHours <= 0 {
return 24 * time.Hour
}
return time.Duration(a.TemporaryGrantTTLHours) * time.Hour
}
func (a *ACLConfig) IsRoleHierarchyEnabled() bool { return a.EnableRoleHierarchy }
func (a *ACLConfig) GetCacheTTL() time.Duration {
if a.CacheTTLSec <= 0 {
return 60 * time.Second
}
return time.Duration(a.CacheTTLSec) * time.Second
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ TLSConfig
// =============================================================================
func (t *TLSConfig) GetTLSMinVersion() string {
if t.MinVersion == "" {
return "1.2"
}
return t.MinVersion
}
func (t *TLSConfig) IsTLSEnabled() bool { return t.Enabled }
func (t *TLSConfig) IsMutualAuthEnabled() bool { return t.MutualAuth }
func (t *TLSConfig) GetKeyRotationDays() int {
if t.KeyRotationDays <= 0 {
return 30
}
return t.KeyRotationDays
}
func (t *TLSConfig) IsAutoGenerateEnabled() bool { return t.AutoGenerate }
// =============================================================================
// ГЕТТЕРЫ ДЛЯ BackpressureConfig
// =============================================================================
func (b *BackpressureConfig) IsBackpressureEnabled() bool { return b.Enabled }
func (b *BackpressureConfig) GetCPUThreshold() float64 {
if b.CPUThreshold <= 0 {
return 0.8
}
if b.CPUThreshold > 1.0 {
return 1.0
}
return b.CPUThreshold
}
func (b *BackpressureConfig) GetMemoryThreshold() float64 {
if b.MemoryThreshold <= 0 {
return 0.85
}
if b.MemoryThreshold > 1.0 {
return 1.0
}
return b.MemoryThreshold
}
func (b *BackpressureConfig) GetQueueSizeThreshold() int {
if b.QueueSizeThreshold <= 0 {
return 10000
}
return b.QueueSizeThreshold
}
func (b *BackpressureConfig) GetConnectionThreshold() int {
if b.ConnectionThreshold <= 0 {
return 5000
}
return b.ConnectionThreshold
}
func (b *BackpressureConfig) GetCheckInterval() time.Duration {
if b.CheckIntervalMs <= 0 {
return 1 * time.Second
}
return time.Duration(b.CheckIntervalMs) * time.Millisecond
}
func (b *BackpressureConfig) GetLowDelay() time.Duration {
if b.LowDelayMs <= 0 {
return 100 * time.Millisecond
}
return time.Duration(b.LowDelayMs) * time.Millisecond
}
func (b *BackpressureConfig) GetMediumRejectProb() uint32 {
if b.MediumRejectProb > 100 {
return 100
}
return b.MediumRejectProb
}
func (b *BackpressureConfig) GetHighRejectProb() uint32 {
if b.HighRejectProb > 100 {
return 100
}
return b.HighRejectProb
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ RuntimeLimitsConfig
// =============================================================================
func (r *RuntimeLimitsConfig) IsRuntimeLimitsEnabled() bool { return r.Enabled }
func (r *RuntimeLimitsConfig) GetGlobalMaxDocSizeMB() int {
if r.GlobalMaxDocSizeMB <= 0 {
return 16
}
return r.GlobalMaxDocSizeMB
}
func (r *RuntimeLimitsConfig) GetGlobalMaxCollSizeMB() int64 {
if r.GlobalMaxCollSizeMB <= 0 {
return 10240
}
return r.GlobalMaxCollSizeMB
}
func (r *RuntimeLimitsConfig) GetGlobalMaxDocsPerColl() int64 {
if r.GlobalMaxDocsPerColl <= 0 {
return 10000000
}
return r.GlobalMaxDocsPerColl
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ AutoscalingConfig
// =============================================================================
func (a *AutoscalingConfig) IsAutoscalingEnabled() bool { return a.Enabled }
func (a *AutoscalingConfig) GetMinNodes() int {
if a.MinNodes <= 0 {
return 1
}
return a.MinNodes
}
func (a *AutoscalingConfig) GetMaxNodes() int {
if a.MaxNodes <= 0 {
return 10
}
return a.MaxNodes
}
func (a *AutoscalingConfig) GetScaleUpThreshold() float64 {
if a.ScaleUpThreshold <= 0 {
return 0.75
}
if a.ScaleUpThreshold > 1.0 {
return 1.0
}
return a.ScaleUpThreshold
}
func (a *AutoscalingConfig) GetScaleDownThreshold() float64 {
if a.ScaleDownThreshold <= 0 {
return 0.30
}
return a.ScaleDownThreshold
}
func (a *AutoscalingConfig) GetScaleUpCooldown() time.Duration {
if a.ScaleUpCooldownSec <= 0 {
return 5 * time.Minute
}
return time.Duration(a.ScaleUpCooldownSec) * time.Second
}
func (a *AutoscalingConfig) GetScaleDownCooldown() time.Duration {
if a.ScaleDownCooldownSec <= 0 {
return 10 * time.Minute
}
return time.Duration(a.ScaleDownCooldownSec) * time.Second
}
func (a *AutoscalingConfig) GetEvaluationInterval() time.Duration {
if a.EvaluationIntervalSec <= 0 {
return 30 * time.Second
}
return time.Duration(a.EvaluationIntervalSec) * time.Second
}
func (a *AutoscalingConfig) IsPredictiveEnabled() bool { return a.PredictiveEnabled }
func (a *AutoscalingConfig) GetMaxScaleUpNodes() int {
if a.MaxScaleUpNodes <= 0 {
return 3
}
return a.MaxScaleUpNodes
}
func (a *AutoscalingConfig) GetMaxScaleDownNodes() int {
if a.MaxScaleDownNodes <= 0 {
return 2
}
return a.MaxScaleDownNodes
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ SchemaMigrationConfig
// =============================================================================
func (s *SchemaMigrationConfig) IsSchemaMigrationEnabled() bool { return s.Enabled }
func (s *SchemaMigrationConfig) GetMigrationDir() string {
if s.MigrationDir == "" {
return "migrations"
}
return s.MigrationDir
}
func (s *SchemaMigrationConfig) IsAutoMigrateEnabled() bool { return s.AutoMigrate }
func (s *SchemaMigrationConfig) GetTargetVersion() string {
if s.TargetVersion == "" {
return "latest"
}
return s.TargetVersion
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ BackupConfig
// =============================================================================
func (b *BackupConfig) IsBackupEnabled() bool { return b.Enabled }
func (b *BackupConfig) GetBackupDir() string {
if b.BackupDir == "" {
return "backups"
}
return b.BackupDir
}
func (b *BackupConfig) GetMaxConcurrentBackups() int {
if b.MaxConcurrent <= 0 {
return 1
}
return b.MaxConcurrent
}
func (b *BackupConfig) IsCompressEnabled() bool { return b.CompressEnabled }
func (b *BackupConfig) GetBackupRetentionDays() int {
if b.RetentionDays <= 0 {
return 7
}
return b.RetentionDays
}
func (b *BackupConfig) IsIncludeSagaStateEnabled() bool { return b.IncludeSagaState }
func (b *BackupConfig) IsIncrementalEnabled() bool { return b.EnableIncremental }
// =============================================================================
// ГЕТТЕРЫ ДЛЯ StorageConfig
// =============================================================================
func (s *StorageConfig) GetDefaultEngine() string {
if s.DefaultEngine == "" {
return "row"
}
return s.DefaultEngine
}
func (s *StorageConfig) IsCustomEnginesEnabled() bool { return s.EnableCustomEngines }
// =============================================================================
// МЕТОДЫ ДЛЯ EnginesConfig
// =============================================================================
func (e *EnginesConfig) IsEngineEnabled(name string) bool {
switch name {
case "row":
return e.Row.Enabled
case "columnar":
return e.Columnar.Enabled
case "document":
return e.Document.Enabled
case "kv":
return e.KV.Enabled
case "ts":
return e.TS.Enabled
case "graph":
return e.Graph.Enabled
default:
return false
}
}
func (e *EnginesConfig) GetEngineConfig(name string) map[string]interface{} {
switch name {
case "row":
return e.Row.Config
case "columnar":
return e.Columnar.Config
case "document":
return e.Document.Config
case "kv":
return e.KV.Config
case "ts":
return e.TS.Config
case "graph":
return e.Graph.Config
default:
return make(map[string]interface{})
}
}
func (e *EnginesConfig) GetEngineDescription(name string) string {
switch name {
case "row":
return e.Row.Description
case "columnar":
return e.Columnar.Description
case "document":
return e.Document.Description
case "kv":
return e.KV.Description
case "ts":
return e.TS.Description
case "graph":
return e.Graph.Description
default:
return ""
}
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ RecoveryConfig
// =============================================================================
func (r *RecoveryConfig) GetMaxRetryDuration() time.Duration {
if r.MaxRetrySec <= 0 {
return 300 * time.Second
}
return time.Duration(r.MaxRetrySec) * time.Second
}
func (r *RecoveryConfig) GetDataReplicationTimeout() time.Duration {
if r.DataReplicationTimeoutSec <= 0 {
return 60 * time.Second
}
return time.Duration(r.DataReplicationTimeoutSec) * time.Second
}
func (r *RecoveryConfig) GetStaleReadTimeout() time.Duration {
if r.StaleReadTimeoutSec <= 0 {
return 30 * time.Second
}
return time.Duration(r.StaleReadTimeoutSec) * time.Second
}
func (r *RecoveryConfig) IsAutoRejoinEnabled() bool { return r.AutoRejoin }
// =============================================================================
// ГЕТТЕРЫ ДЛЯ PerformanceConfig
// =============================================================================
func (p *PerformanceConfig) GetBatchSize() int {
if p.BatchSize <= 0 {
return 100
}
return p.BatchSize
}
func (p *PerformanceConfig) GetMaxConnections() int {
if p.MaxConnections <= 0 {
return 1000
}
return p.MaxConnections
}
func (p *PerformanceConfig) IsReadFromFollowerEnabled() bool { return p.ReadFromFollower }
func (p *PerformanceConfig) IsPipelineEnabled() bool { return p.EnablePipeline }
func (p *PerformanceConfig) GetReadReplicaDelay() time.Duration {
if p.ReadReplicaDelayMs <= 0 {
return 100 * time.Millisecond
}
return time.Duration(p.ReadReplicaDelayMs) * time.Millisecond
}
// =============================================================================
// ГЕТТЕРЫ ДЛЯ MonitoringConfig
// =============================================================================
func (m *MonitoringConfig) GetMetricsPort() int {
if m.MetricsPort <= 0 {
return 9090
}
return m.MetricsPort
}
func (m *MonitoringConfig) GetTraceSampleRate() float64 {
if m.TraceSampleRate <= 0 || m.TraceSampleRate > 1 {
return 0.01
}
return m.TraceSampleRate
}
func (m *MonitoringConfig) IsMetricsEnabled() bool { return m.EnableMetrics }
func (m *MonitoringConfig) IsTracingEnabled() bool { return m.EnableTracing }
// =============================================================================
// ГЕТТЕРЫ ДЛЯ SecurityConfig
// =============================================================================
func (s *SecurityConfig) GetTLSMinVersion() uint16 {
switch s.MinVersion {
case "1.0":
return 0x0301
case "1.1":
return 0x0302
case "1.2":
return 0x0303
case "1.3":
return 0x0304
default:
return 0x0303
}
}
func (s *SecurityConfig) IsTLSEnabled() bool {
return s.EnableTLS && s.CertFile != "" && s.KeyFile != ""
}
func (s *SecurityConfig) GetCertFile() string { return s.CertFile }
func (s *SecurityConfig) GetKeyFile() string { return s.KeyFile }
func (s *SecurityConfig) GetCAFile() string { return s.CAFile }
// =============================================================================
// ГЕТТЕРЫ ДЛЯ CompressionConfig
// =============================================================================
func (c *CompressionConfig) IsCompressionEnabled() bool { return c.Enabled }
func (c *CompressionConfig) GetAlgorithm() string {
if c.Algorithm == "" {
return "snappy"
}
return c.Algorithm
}
func (c *CompressionConfig) GetLevel() int {
if c.Level < 1 {
return 3
}
if c.Level > 9 {
return 9
}
return c.Level
}
func (c *CompressionConfig) GetMinSize() int {
if c.MinSize <= 0 {
return 1024
}
return c.MinSize
}
// =============================================================================
// ЗАГРУЗКА И ВАЛИДАЦИЯ КОНФИГУРАЦИИ
// =============================================================================
// LoadConfig загружает и валидирует конфигурацию из TOML-файла.
func LoadConfig(path string) (*Config, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("config file not found: %s", path)
}
return nil, fmt.Errorf("failed to stat config file: %w", err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("config path is not a regular file: %s", path)
}
if info.Size() > maxConfigFileSize {
return nil, fmt.Errorf("config file too large: %d bytes (max %d)", info.Size(), maxConfigFileSize)
}
var cfg Config
if _, err := toml.DecodeFile(path, &cfg); err != nil {
return nil, fmt.Errorf("failed to decode config file: %v", err)
}
// ===== УСТАНОВКА ЗНАЧЕНИЙ ПО УМОЛЧАНИЮ =====
if cfg.Cluster.RaftPort == 0 {
cfg.Cluster.RaftPort = 9878
}
if cfg.Cluster.NodePort == 0 {
cfg.Cluster.NodePort = 9877
}
if cfg.Cluster.NodeIP == "" {
cfg.Cluster.NodeIP = "0.0.0.0"
}
if cfg.Cluster.RaftDataDir == "" {
cfg.Cluster.RaftDataDir = "raft_data"
}
if cfg.Cluster.HeartbeatTimeoutMs == 0 {
cfg.Cluster.HeartbeatTimeoutMs = 1000
}
if cfg.Cluster.ElectionTimeoutMs == 0 {
cfg.Cluster.ElectionTimeoutMs = 1000
}
if cfg.Cluster.CommitTimeoutMs == 0 {
cfg.Cluster.CommitTimeoutMs = 500
}
if cfg.Cluster.SnapshotIntervalMin == 0 {
cfg.Cluster.SnapshotIntervalMin = 30
}
if cfg.Cluster.SnapshotThreshold == 0 {
cfg.Cluster.SnapshotThreshold = 1000
}
if cfg.Cluster.RecoveryTimeoutSec == 0 {
cfg.Cluster.RecoveryTimeoutSec = 30
}
if cfg.Cluster.Region == "" {
cfg.Cluster.Region = "default"
}
if cfg.Saga.CoordinatorCount == 0 {
cfg.Saga.CoordinatorCount = 3
}
if cfg.Saga.StateDir == "" {
cfg.Saga.StateDir = "saga_states"
}
if cfg.Saga.MaxRetries == 0 {
cfg.Saga.MaxRetries = 5
}
if cfg.Saga.RetryBackoffMs == 0 {
cfg.Saga.RetryBackoffMs = 100
}
if cfg.Saga.SagaTimeoutSec == 0 {
cfg.Saga.SagaTimeoutSec = 300
}
if cfg.Saga.StuckCheckIntervalSec == 0 {
cfg.Saga.StuckCheckIntervalSec = 10
}
if cfg.Saga.LeaderElectionIntervalSec == 0 {
cfg.Saga.LeaderElectionIntervalSec = 5
}
if cfg.Saga.RecoveryIntervalSec == 0 {
cfg.Saga.RecoveryIntervalSec = 30
}
if cfg.Saga.MetricsIntervalSec == 0 {
cfg.Saga.MetricsIntervalSec = 60
}
if cfg.Saga.CleanupPeriodHours == 0 {
cfg.Saga.CleanupPeriodHours = 24
}
if cfg.Saga.MaxCacheSize == 0 {
cfg.Saga.MaxCacheSize = 10000
}
if cfg.Saga.OperationRetentionDays == 0 {
cfg.Saga.OperationRetentionDays = 7
}
if cfg.Saga.ChannelBufferSize == 0 {
cfg.Saga.ChannelBufferSize = 10000
}
if cfg.Saga.AsyncRecoveryWorkers == 0 {
cfg.Saga.AsyncRecoveryWorkers = 4
}
if cfg.Saga.AsyncRecoveryTimeoutSec == 0 {
cfg.Saga.AsyncRecoveryTimeoutSec = 30
}
if cfg.Saga.FsyncMaxRetries == 0 {
cfg.Saga.FsyncMaxRetries = 3
}
if cfg.Saga.FsyncRetryDelayMs == 0 {
cfg.Saga.FsyncRetryDelayMs = 100
}
if cfg.Replication.ReplicationTimeoutMs == 0 {
cfg.Replication.ReplicationTimeoutMs = 5000
}
if cfg.Replication.MaxReplicaLagMs == 0 {
cfg.Replication.MaxReplicaLagMs = 5000
}
if cfg.Plugins.ScriptDir == "" {
cfg.Plugins.ScriptDir = "plugins"
}
if cfg.Plugins.EnginePluginDir == "" {
cfg.Plugins.EnginePluginDir = "engines"
}
if cfg.Plugins.MaxCPUTimeMs == 0 {
cfg.Plugins.MaxCPUTimeMs = 100
}
if cfg.Plugins.MaxMemoryMB == 0 {
cfg.Plugins.MaxMemoryMB = 50
}
if cfg.Plugins.MaxExecutionTimeSec == 0 {
cfg.Plugins.MaxExecutionTimeSec = 5
}
if cfg.Plugins.MaxInstructions == 0 {
cfg.Plugins.MaxInstructions = 1000000
}
if cfg.Plugins.HotReloadIntervalSec == 0 {
cfg.Plugins.HotReloadIntervalSec = 30
}
if cfg.Plugins.MaxEventLogSize == 0 {
cfg.Plugins.MaxEventLogSize = 1000
}
if cfg.Plugins.LoadTimeoutSec == 0 {
cfg.Plugins.LoadTimeoutSec = 10
}
if cfg.Plugins.MaxLuaStates == 0 {
cfg.Plugins.MaxLuaStates = 100
}
if cfg.Plugins.LuaStateTTLSec == 0 {
cfg.Plugins.LuaStateTTLSec = 600
}
if cfg.API.Port == 0 {
cfg.API.Port = 8080
}
if cfg.Metrics.CollectIntervalSec == 0 {
cfg.Metrics.CollectIntervalSec = 15
}
if cfg.Compression.Algorithm == "" {
cfg.Compression.Algorithm = "snappy"
}
if cfg.Compression.MinSize == 0 {
cfg.Compression.MinSize = 1024
}
if cfg.Compression.Level == 0 {
cfg.Compression.Level = 3
}
if cfg.Performance.BatchSize == 0 {
cfg.Performance.BatchSize = 100
}
if cfg.Performance.MaxConnections == 0 {
cfg.Performance.MaxConnections = 1000
}
if cfg.Performance.ReadReplicaDelayMs == 0 {
cfg.Performance.ReadReplicaDelayMs = 100
}
if cfg.Monitoring.MetricsPort == 0 {
cfg.Monitoring.MetricsPort = 9090
}
if cfg.Monitoring.TraceSampleRate == 0 {
cfg.Monitoring.TraceSampleRate = 0.01
}
if cfg.Recovery.MaxRetrySec == 0 {
cfg.Recovery.MaxRetrySec = 300
}
if cfg.Recovery.DataReplicationTimeoutSec == 0 {
cfg.Recovery.DataReplicationTimeoutSec = 60
}
if cfg.Recovery.StaleReadTimeoutSec == 0 {
cfg.Recovery.StaleReadTimeoutSec = 30
}
if cfg.Security.MinVersion == "" {
cfg.Security.MinVersion = "1.2"
}
if cfg.Storage.DefaultEngine == "" {
cfg.Storage.DefaultEngine = "row"
}
if cfg.WAL.SegmentSizeMB == 0 {
cfg.WAL.SegmentSizeMB = 64
}
if cfg.WAL.SyncIntervalSec == 0 {
cfg.WAL.SyncIntervalSec = 5
}
if cfg.WAL.BatchSize == 0 {
cfg.WAL.BatchSize = 100
}
if cfg.WAL.RecoveryWorkers == 0 {
cfg.WAL.RecoveryWorkers = 4
}
if cfg.WAL.AsyncRecoveryWorkers == 0 {
cfg.WAL.AsyncRecoveryWorkers = 4
}
if cfg.WAL.AsyncRecoveryBuffer == 0 {
cfg.WAL.AsyncRecoveryBuffer = 10000
}
if cfg.MVCC.MaxVersionsPerDoc == 0 {
cfg.MVCC.MaxVersionsPerDoc = 10
}
if cfg.MVCC.VisibilityMapSize == 0 {
cfg.MVCC.VisibilityMapSize = 1024 * 1024
}
if cfg.MVCC.PruneIntervalMin == 0 {
cfg.MVCC.PruneIntervalMin = 5
}
if cfg.MVCC.RetentionDays == 0 {
cfg.MVCC.RetentionDays = 7
}
if cfg.MVCC.ReadCacheSize == 0 {
cfg.MVCC.ReadCacheSize = 10000
}
if cfg.MVCC.ReadCacheTTLSec == 0 {
cfg.MVCC.ReadCacheTTLSec = 300
}
if cfg.Transactions.DefaultTimeoutSec == 0 {
cfg.Transactions.DefaultTimeoutSec = 30
}
if cfg.Transactions.DeadlockCheckIntervalSec == 0 {
cfg.Transactions.DeadlockCheckIntervalSec = 1
}
if cfg.Transactions.MaxSavepointsPerTx == 0 {
cfg.Transactions.MaxSavepointsPerTx = 100
}
if cfg.Transactions.CheckpointIntervalSec == 0 {
cfg.Transactions.CheckpointIntervalSec = 300
}
if cfg.ACL.MaxDeniedLogSize == 0 {
cfg.ACL.MaxDeniedLogSize = 10000
}
if cfg.ACL.TemporaryGrantTTLHours == 0 {
cfg.ACL.TemporaryGrantTTLHours = 24
}
if cfg.ACL.CacheTTLSec == 0 {
cfg.ACL.CacheTTLSec = 60
}
if cfg.ClusterTLS.MinVersion == "" {
cfg.ClusterTLS.MinVersion = "1.2"
}
if cfg.ClusterTLS.KeyRotationDays == 0 {
cfg.ClusterTLS.KeyRotationDays = 30
}
if cfg.Backpressure.CheckIntervalMs == 0 {
cfg.Backpressure.CheckIntervalMs = 1000
}
if cfg.Backpressure.LowDelayMs == 0 {
cfg.Backpressure.LowDelayMs = 100
}
if cfg.RuntimeLimits.GlobalMaxDocSizeMB == 0 {
cfg.RuntimeLimits.GlobalMaxDocSizeMB = 16
}
if cfg.RuntimeLimits.GlobalMaxCollSizeMB == 0 {
cfg.RuntimeLimits.GlobalMaxCollSizeMB = 10240
}
if cfg.RuntimeLimits.GlobalMaxDocsPerColl == 0 {
cfg.RuntimeLimits.GlobalMaxDocsPerColl = 10000000
}
if cfg.Autoscaling.MinNodes == 0 {
cfg.Autoscaling.MinNodes = 1
}
if cfg.Autoscaling.MaxNodes == 0 {
cfg.Autoscaling.MaxNodes = 10
}
if cfg.Autoscaling.ScaleUpCooldownSec == 0 {
cfg.Autoscaling.ScaleUpCooldownSec = 300
}
if cfg.Autoscaling.ScaleDownCooldownSec == 0 {
cfg.Autoscaling.ScaleDownCooldownSec = 600
}
if cfg.Autoscaling.EvaluationIntervalSec == 0 {
cfg.Autoscaling.EvaluationIntervalSec = 30
}
if cfg.SchemaMigration.MigrationDir == "" {
cfg.SchemaMigration.MigrationDir = "migrations"
}
if cfg.Backup.BackupDir == "" {
cfg.Backup.BackupDir = "backups"
}
if cfg.Backup.RetentionDays == 0 {
cfg.Backup.RetentionDays = 7
}
if !cfg.Backup.IncludeSagaState {
cfg.Backup.IncludeSagaState = true
}
if !cfg.Backup.EnableIncremental {
cfg.Backup.EnableIncremental = true
}
if cfg.Migration.Source == nil {
cfg.Migration.Source = &DatacenterConfig{
Name: "dc-primary",
Endpoint: "localhost:8080",
TimeoutSec: 30,
}
}
if cfg.Migration.Target == nil {
cfg.Migration.Target = &DatacenterConfig{
Name: "dc-secondary",
Endpoint: "localhost:8081",
TimeoutSec: 30,
}
}
if cfg.Migration.Settings == nil {
cfg.Migration.Settings = &MigrationSettings{
BatchSize: 1000,
Workers: 4,
Compression: "snappy",
ResumeEnabled: true,
CheckpointIntervalSec: 30,
MaxRetries: 3,
RetryBackoffSec: 5,
}
}
if cfg.Migration.Delta == nil {
cfg.Migration.Delta = &DeltaSyncConfig{
Enabled: true,
IntervalSec: 60,
MaxLagSec: 300,
}
}
if cfg.Migration.Validation == nil {
cfg.Migration.Validation = &ValidationConfig{
Enabled: true,
SamplePercent: 10,
MaxErrors: 100,
}
}
if cfg.Migration.Mode == "" {
cfg.Migration.Mode = "semi_auto"
}
// ===== ИНИЦИАЛИЗАЦИЯ ДВИЖКОВ =====
if !cfg.Engines.Row.Enabled {
cfg.Engines.Row.Enabled = true
cfg.Engines.Row.Description = "Row-based storage engine (default)"
}
if cfg.Engines.Row.Config == nil {
cfg.Engines.Row.Config = make(map[string]interface{})
}
if cfg.Engines.Columnar.Config == nil {
cfg.Engines.Columnar.Config = make(map[string]interface{})
}
if cfg.Engines.Document.Config == nil {
cfg.Engines.Document.Config = make(map[string]interface{})
}
if cfg.Engines.KV.Config == nil {
cfg.Engines.KV.Config = make(map[string]interface{})
}
if cfg.Engines.TS.Config == nil {
cfg.Engines.TS.Config = make(map[string]interface{})
}
if cfg.Engines.Graph.Config == nil {
cfg.Engines.Graph.Config = make(map[string]interface{})
}
// ===== ВАЛИДАЦИЯ КОНФИГУРАЦИИ =====
validationResult := ValidateConfigFull(&cfg)
if !validationResult.Valid {
var errMsg strings.Builder
errMsg.WriteString("config validation failed:\n")
for _, err := range validationResult.Errors {
errMsg.WriteString(fmt.Sprintf(" - %s\n", err.Error()))
}
if len(validationResult.Warnings) > 0 {
errMsg.WriteString("Warnings:\n")
for _, warn := range validationResult.Warnings {
errMsg.WriteString(fmt.Sprintf(" - %s\n", warn.Error()))
}
}
return nil, fmt.Errorf("%s", errMsg.String())
}
if len(validationResult.Warnings) > 0 {
fmt.Println("Configuration warnings:")
for _, warn := range validationResult.Warnings {
fmt.Printf(" - %s\n", warn.Error())
}
}
return &cfg, nil
}
// ValidateConfig выполняет валидацию конфигурации и возвращает первую ошибку.
func ValidateConfig(cfg *Config) error {
if cfg == nil {
return fmt.Errorf("config is nil")
}
result := ValidateConfigFull(cfg)
if !result.Valid {
if len(result.Errors) > 0 {
return result.Errors[0]
}
return fmt.Errorf("config validation failed")
}
return nil
}
// ValidateConfigFull выполняет полную валидацию конфигурации.
func ValidateConfigFull(cfg *Config) *ValidationResult {
result := &ValidationResult{
Valid: true,
Errors: make([]error, 0),
Warnings: make([]error, 0),
}
if cfg.Cluster.NodePort < 0 || cfg.Cluster.NodePort > 65535 {
result.Errors = append(result.Errors, fmt.Errorf("invalid node_port: %d (must be 0-65535, 0 = auto)", cfg.Cluster.NodePort))
}
if cfg.Cluster.RaftPort <= 0 || cfg.Cluster.RaftPort > 65535 {
result.Errors = append(result.Errors, fmt.Errorf("invalid raft_port: %d (must be 1-65535)", cfg.Cluster.RaftPort))
}
if cfg.Cluster.PriorityZone < 0 || cfg.Cluster.PriorityZone > 9 {
result.Errors = append(result.Errors, fmt.Errorf("priority_zone must be between 0 and 9, got %d", cfg.Cluster.PriorityZone))
}
if cfg.Cluster.NodeIP != "" && cfg.Cluster.NodeIP != "0.0.0.0" && cfg.Cluster.NodeIP != "::" {
if net.ParseIP(cfg.Cluster.NodeIP) == nil {
result.Errors = append(result.Errors, fmt.Errorf("invalid node_ip: %s (must be a valid IP address)", cfg.Cluster.NodeIP))
}
}
if cfg.Saga.Enabled {
if cfg.Saga.CoordinatorCount < 1 {
result.Errors = append(result.Errors, fmt.Errorf("saga.coordinator_count must be at least 1, got %d", cfg.Saga.CoordinatorCount))
}
if cfg.Saga.CoordinatorCount < 3 {
result.Warnings = append(result.Warnings, fmt.Errorf("saga.coordinator_count is less than 3, may not be fully fault-tolerant"))
}
if cfg.Saga.MaxRetries < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.max_retries cannot be negative, got %d", cfg.Saga.MaxRetries))
}
if cfg.Saga.RetryBackoffMs < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.retry_backoff_ms cannot be negative, got %d", cfg.Saga.RetryBackoffMs))
}
if cfg.Saga.SagaTimeoutSec < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.saga_timeout_sec cannot be negative, got %d", cfg.Saga.SagaTimeoutSec))
}
if cfg.Saga.StuckCheckIntervalSec < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.stuck_check_interval_sec cannot be negative, got %d", cfg.Saga.StuckCheckIntervalSec))
}
if cfg.Saga.AsyncRecoveryWorkers < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.async_recovery_workers cannot be negative, got %d", cfg.Saga.AsyncRecoveryWorkers))
}
if cfg.Saga.ChannelBufferSize < 0 {
result.Errors = append(result.Errors, fmt.Errorf("saga.channel_buffer_size cannot be negative, got %d", cfg.Saga.ChannelBufferSize))
}
}
if cfg.Performance.BatchSize < 1 {
result.Errors = append(result.Errors, fmt.Errorf("batch_size must be at least 1, got %d", cfg.Performance.BatchSize))
}
if cfg.Performance.MaxConnections < 1 {
result.Errors = append(result.Errors, fmt.Errorf("max_connections must be at least 1, got %d", cfg.Performance.MaxConnections))
}
if cfg.Monitoring.TraceSampleRate < 0 || cfg.Monitoring.TraceSampleRate > 1 {
result.Errors = append(result.Errors, fmt.Errorf("trace_sample_rate must be between 0 and 1, got %f", cfg.Monitoring.TraceSampleRate))
}
if cfg.Metrics.Enabled {
if cfg.Metrics.CollectIntervalSec <= 0 {
result.Errors = append(result.Errors, fmt.Errorf("metrics.collect_interval_sec must be > 0 when metrics.enabled = true"))
}
}
if cfg.Metrics.CollectIntervalSec < 0 {
result.Errors = append(result.Errors, fmt.Errorf("metrics.collect_interval_sec cannot be negative, got %d", cfg.Metrics.CollectIntervalSec))
}
if cfg.Log.LogLevel != "" {
level := strings.ToLower(cfg.Log.LogLevel)
if !allowedLogLevels[level] {
result.Errors = append(result.Errors, fmt.Errorf("invalid log_level: %s (allowed: debug, info, warn, error, fatal)", cfg.Log.LogLevel))
}
}
if cfg.API.Port == cfg.Cluster.NodePort {
result.Errors = append(result.Errors, fmt.Errorf("API port %d conflicts with cluster node port", cfg.API.Port))
}
if cfg.Monitoring.MetricsPort == cfg.Cluster.NodePort {
result.Errors = append(result.Errors, fmt.Errorf("metrics port %d conflicts with cluster node port", cfg.Monitoring.MetricsPort))
}
if cfg.API.Port == cfg.Monitoring.MetricsPort {
result.Warnings = append(result.Warnings, fmt.Errorf("API port %d conflicts with metrics port", cfg.API.Port))
}
if cfg.Security.IsTLSEnabled() {
if cfg.Security.CertFile == "" {
result.Errors = append(result.Errors, fmt.Errorf("cert_file is required when TLS is enabled"))
}
if cfg.Security.KeyFile == "" {
result.Errors = append(result.Errors, fmt.Errorf("key_file is required when TLS is enabled"))
}
}
if cfg.Compression.Enabled {
if !allowedCompressionAlgorithms[cfg.Compression.Algorithm] {
result.Errors = append(result.Errors, fmt.Errorf("unsupported compression algorithm: %s, supported: snappy, lz4, zstd", cfg.Compression.Algorithm))
}
}
if cfg.WAL.RecoveryWorkers < 1 {
result.Errors = append(result.Errors, fmt.Errorf("wal.recovery_workers must be at least 1, got %d", cfg.WAL.RecoveryWorkers))
}
if cfg.WAL.AsyncRecoveryWorkers < 1 && cfg.WAL.AsyncRecovery {
result.Warnings = append(result.Warnings, fmt.Errorf("wal.async_recovery_workers is less than 1, will use default 4"))
}
if cfg.WAL.AsyncRecoveryBuffer < 0 {
result.Errors = append(result.Errors, fmt.Errorf("wal.async_recovery_buffer cannot be negative, got %d", cfg.WAL.AsyncRecoveryBuffer))
}
if cfg.MVCC.MaxVersionsPerDoc < 0 {
result.Errors = append(result.Errors, fmt.Errorf("mvcc.max_versions_per_doc cannot be negative, got %d", cfg.MVCC.MaxVersionsPerDoc))
}
if cfg.Plugins.MaxCPUTimeMs < 0 {
result.Errors = append(result.Errors, fmt.Errorf("plugins.max_cpu_time_ms cannot be negative, got %d", cfg.Plugins.MaxCPUTimeMs))
}
if cfg.Plugins.MaxMemoryMB < 0 {
result.Errors = append(result.Errors, fmt.Errorf("plugins.max_memory_mb cannot be negative, got %d", cfg.Plugins.MaxMemoryMB))
}
if cfg.Plugins.MaxLuaStates < 1 {
result.Warnings = append(result.Warnings, fmt.Errorf("plugins.max_lua_states is less than 1, will use default 100"))
}
if cfg.Backpressure.Enabled {
if cfg.Backpressure.CPUThreshold < 0 || cfg.Backpressure.CPUThreshold > 1 {
result.Errors = append(result.Errors, fmt.Errorf("backpressure.cpu_threshold must be between 0 and 1, got %f", cfg.Backpressure.CPUThreshold))
}
if cfg.Backpressure.MemoryThreshold < 0 || cfg.Backpressure.MemoryThreshold > 1 {
result.Errors = append(result.Errors, fmt.Errorf("backpressure.memory_threshold must be between 0 and 1, got %f", cfg.Backpressure.MemoryThreshold))
}
if cfg.Backpressure.MediumRejectProb > 100 {
result.Warnings = append(result.Warnings, fmt.Errorf("backpressure.medium_reject_prob is > 100, will be capped to 100"))
}
if cfg.Backpressure.HighRejectProb > 100 {
result.Warnings = append(result.Warnings, fmt.Errorf("backpressure.high_reject_prob is > 100, will be capped to 100"))
}
}
if cfg.RuntimeLimits.Enabled {
if cfg.RuntimeLimits.GlobalMaxDocSizeMB < 0 {
result.Errors = append(result.Errors, fmt.Errorf("runtime_limits.global_max_doc_size_mb cannot be negative, got %d", cfg.RuntimeLimits.GlobalMaxDocSizeMB))
}
if cfg.RuntimeLimits.GlobalMaxCollSizeMB < 0 {
result.Errors = append(result.Errors, fmt.Errorf("runtime_limits.global_max_coll_size_mb cannot be negative, got %d", cfg.RuntimeLimits.GlobalMaxCollSizeMB))
}
if cfg.RuntimeLimits.GlobalMaxDocsPerColl < 0 {
result.Errors = append(result.Errors, fmt.Errorf("runtime_limits.global_max_docs_per_coll cannot be negative, got %d", cfg.RuntimeLimits.GlobalMaxDocsPerColl))
}
}
if cfg.Autoscaling.Enabled {
if cfg.Autoscaling.MinNodes < 1 {
result.Errors = append(result.Errors, fmt.Errorf("autoscaling.min_nodes must be at least 1, got %d", cfg.Autoscaling.MinNodes))
}
if cfg.Autoscaling.MaxNodes < cfg.Autoscaling.MinNodes {
result.Errors = append(result.Errors, fmt.Errorf("autoscaling.max_nodes (%d) must be >= min_nodes (%d)", cfg.Autoscaling.MaxNodes, cfg.Autoscaling.MinNodes))
}
if cfg.Autoscaling.ScaleUpThreshold <= 0 || cfg.Autoscaling.ScaleUpThreshold > 1 {
result.Warnings = append(result.Warnings, fmt.Errorf("autoscaling.scale_up_threshold is invalid, will use default 0.75"))
}
if cfg.Autoscaling.ScaleDownThreshold <= 0 || cfg.Autoscaling.ScaleDownThreshold >= cfg.Autoscaling.ScaleUpThreshold {
result.Warnings = append(result.Warnings, fmt.Errorf("autoscaling.scale_down_threshold should be less than scale_up_threshold"))
}
}
if cfg.Storage.EnableCustomEngines {
defaultEngine := cfg.Storage.GetDefaultEngine()
switch defaultEngine {
case "row", "columnar", "document", "kv", "ts", "graph":
default:
result.Warnings = append(result.Warnings, fmt.Errorf("default_engine '%s' is not a built-in engine, ensure plugin is loaded", defaultEngine))
}
}
if cfg.Storage.EnableCustomEngines {
anyEnabled := cfg.Engines.Row.Enabled ||
cfg.Engines.Columnar.Enabled ||
cfg.Engines.Document.Enabled ||
cfg.Engines.KV.Enabled ||
cfg.Engines.TS.Enabled ||
cfg.Engines.Graph.Enabled
if !anyEnabled {
result.Errors = append(result.Errors, fmt.Errorf("at least one engine must be enabled in [engines]"))
}
}
if cfg.Backup.Enabled {
if cfg.Backup.BackupDir == "" {
result.Errors = append(result.Errors, fmt.Errorf("backup.backup_dir is required when backup.enabled = true"))
}
if cfg.Backup.RetentionDays < 0 {
result.Errors = append(result.Errors, fmt.Errorf("backup.retention_days cannot be negative, got %d", cfg.Backup.RetentionDays))
}
if cfg.Backup.MaxConcurrent < 1 {
result.Warnings = append(result.Warnings, fmt.Errorf("backup.max_concurrent < 1, will use default 1"))
}
}
if cfg.Migration.Enabled {
if cfg.Migration.Source == nil || cfg.Migration.Source.Endpoint == "" {
result.Errors = append(result.Errors, fmt.Errorf("migration.source.endpoint is required when migration is enabled"))
}
if cfg.Migration.Target == nil || cfg.Migration.Target.Endpoint == "" {
result.Errors = append(result.Errors, fmt.Errorf("migration.target.endpoint is required when migration is enabled"))
}
if cfg.Migration.Settings == nil {
result.Warnings = append(result.Warnings, fmt.Errorf("migration.settings not configured, using defaults"))
}
if cfg.Migration.Delta == nil {
result.Warnings = append(result.Warnings, fmt.Errorf("migration.delta not configured, using defaults"))
}
if cfg.Migration.Validation == nil {
result.Warnings = append(result.Warnings, fmt.Errorf("migration.validation not configured, using defaults"))
}
if !allowedMigrationModes[cfg.Migration.Mode] {
result.Errors = append(result.Errors, fmt.Errorf("migration.mode must be one of: manual, semi_auto, auto, got %s", cfg.Migration.Mode))
}
if cfg.Migration.Settings != nil && cfg.Migration.Settings.BatchSize < 1 {
result.Errors = append(result.Errors, fmt.Errorf("migration.settings.batch_size must be at least 1, got %d", cfg.Migration.Settings.BatchSize))
}
if cfg.Migration.Settings != nil && cfg.Migration.Settings.Workers < 1 {
result.Errors = append(result.Errors, fmt.Errorf("migration.settings.workers must be at least 1, got %d", cfg.Migration.Settings.Workers))
}
if cfg.Migration.Settings != nil && cfg.Migration.Settings.MaxRetries < 0 {
result.Errors = append(result.Errors, fmt.Errorf("migration.settings.max_retries cannot be negative, got %d", cfg.Migration.Settings.MaxRetries))
}
if cfg.Migration.Validation != nil && (cfg.Migration.Validation.SamplePercent < 0 || cfg.Migration.Validation.SamplePercent > 100) {
result.Errors = append(result.Errors, fmt.Errorf("migration.validation.sample_percent must be between 0 and 100, got %d", cfg.Migration.Validation.SamplePercent))
}
if cfg.Migration.Delta != nil && cfg.Migration.Delta.IntervalSec < 1 {
result.Errors = append(result.Errors, fmt.Errorf("migration.delta.interval_sec must be at least 1, got %d", cfg.Migration.Delta.IntervalSec))
}
if cfg.Migration.Source != nil && cfg.Migration.Source.APIKey != "" {
if len(cfg.Migration.Source.APIKey) < minAPIKeyLength {
result.Warnings = append(result.Warnings, fmt.Errorf("migration.source.api_key is shorter than %d characters", minAPIKeyLength))
}
}
if cfg.Migration.Target != nil && cfg.Migration.Target.APIKey != "" {
if len(cfg.Migration.Target.APIKey) < minAPIKeyLength {
result.Warnings = append(result.Warnings, fmt.Errorf("migration.target.api_key is shorter than %d characters", minAPIKeyLength))
}
}
}
result.Valid = len(result.Errors) == 0
return result
}
// =============================================================================
// СОХРАНЕНИЕ КОНФИГУРАЦИИ (ДОБАВЛЕНО)
// =============================================================================
// SaveConfig записывает конфигурацию в TOML-файл атомарно:
// сначала во временный файл (с fsync), потом rename.
//
// Используется REPL-командой "config set" в fallback-режиме, когда
// DynamicConfigManager недоступен (Raft выключен или узел не лидер).
//
// Если path == "" — возвращает ошибку.
// Если cfg == nil — возвращает ошибку.
//
// После успешного rename выполняется fsync родительской директории,
// чтобы гарантировать сохранность переименования при крахе.
func SaveConfig(path string, cfg *Config) error {
if cfg == nil {
return fmt.Errorf("config is nil")
}
if path == "" {
return fmt.Errorf("config path is empty")
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory %s: %w", dir, err)
}
tmpPath := path + ".tmp"
f, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to create temp config file: %w", err)
}
encoder := toml.NewEncoder(f)
if err := encoder.Encode(cfg); err != nil {
f.Close()
os.Remove(tmpPath)
return fmt.Errorf("failed to encode config: %w", err)
}
if err := f.Sync(); err != nil {
f.Close()
os.Remove(tmpPath)
return fmt.Errorf("failed to sync config: %w", err)
}
if err := f.Close(); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to close config: %w", err)
}
if err := os.Rename(tmpPath, path); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to rename config: %w", err)
}
// fsync директории — best-effort, на Windows no-op.
if err := fsyncDir(dir); err != nil {
// Не критично: файл уже синхронизирован, rename выполнен.
_ = err
}
return nil
}
// fsyncDir синхронизирует директорию.
// На Linux/macOS/BSD это работает; на Windows — no-op.
func fsyncDir(dir string) error {
d, err := os.Open(dir)
if err != nil {
return err
}
defer d.Close()
return d.Sync()
}