Go · 4363 bytes Raw Blame History
1 package config
2
3 import (
4 "fmt"
5 "os"
6 "time"
7
8 "gopkg.in/yaml.v3"
9 )
10
11 // Config represents the coordinator configuration
12 type Config struct {
13 Database DatabaseConfig `yaml:"database"`
14 GRPC GRPCConfig `yaml:"grpc"`
15 HTTP HTTPConfig `yaml:"http"`
16 Coordinator CoordinatorConfig `yaml:"coordinator"`
17 Health HealthConfig `yaml:"health"`
18 }
19
20 // DatabaseConfig contains database settings
21 type DatabaseConfig struct {
22 Type string `yaml:"type"` // "bbolt" or "postgres"
23 Path string `yaml:"path"` // For bbolt
24 URL string `yaml:"url"` // For postgres
25 }
26
27 // GRPCConfig contains gRPC server settings
28 type GRPCConfig struct {
29 Port int `yaml:"port"`
30 MaxMessageSize int `yaml:"max_message_size"`
31 EnableReflection bool `yaml:"enable_reflection"`
32 }
33
34 // HTTPConfig contains HTTP server settings
35 type HTTPConfig struct {
36 Enabled bool `yaml:"enabled"`
37 Port int `yaml:"port"`
38 }
39
40 // CoordinatorConfig contains coordinator-specific settings
41 type CoordinatorConfig struct {
42 NodeTimeout time.Duration `yaml:"node_timeout"`
43 HeartbeatInterval time.Duration `yaml:"heartbeat_interval"`
44 ReplicationFactor int `yaml:"replication_factor"`
45 MaxNodesPerChunk int `yaml:"max_nodes_per_chunk"`
46 CleanupInterval time.Duration `yaml:"cleanup_interval"`
47 NodeInactiveAfter time.Duration `yaml:"node_inactive_after"`
48 GeographicSpread bool `yaml:"geographic_spread"`
49 }
50
51 // HealthConfig contains health monitoring settings
52 type HealthConfig struct {
53 CheckInterval time.Duration `yaml:"check_interval"`
54 MetricsEnabled bool `yaml:"metrics_enabled"`
55 MetricsPort int `yaml:"metrics_port"`
56 }
57
58 // DefaultConfig returns a configuration with sensible defaults
59 func DefaultConfig() *Config {
60 return &Config{
61 Database: DatabaseConfig{
62 Type: "bbolt",
63 Path: "coordinator.db",
64 },
65 GRPC: GRPCConfig{
66 Port: 8080,
67 MaxMessageSize: 4 * 1024 * 1024, // 4MB
68 EnableReflection: false,
69 },
70 HTTP: HTTPConfig{
71 Enabled: true,
72 Port: 8090,
73 },
74 Coordinator: CoordinatorConfig{
75 NodeTimeout: 30 * time.Second,
76 HeartbeatInterval: 10 * time.Second,
77 ReplicationFactor: 3,
78 MaxNodesPerChunk: 10,
79 CleanupInterval: 5 * time.Minute,
80 NodeInactiveAfter: 60 * time.Second,
81 GeographicSpread: true,
82 },
83 Health: HealthConfig{
84 CheckInterval: 30 * time.Second,
85 MetricsEnabled: true,
86 MetricsPort: 8091,
87 },
88 }
89 }
90
91 // Load reads configuration from a YAML file, merging with defaults
92 func Load(path string) (*Config, error) {
93 cfg := DefaultConfig()
94
95 if _, err := os.Stat(path); os.IsNotExist(err) {
96 // Config file doesn't exist, use defaults
97 return cfg, nil
98 }
99
100 data, err := os.ReadFile(path)
101 if err != nil {
102 return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
103 }
104
105 if err := yaml.Unmarshal(data, cfg); err != nil {
106 return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
107 }
108
109 // Validate configuration
110 if err := cfg.Validate(); err != nil {
111 return nil, fmt.Errorf("invalid configuration: %w", err)
112 }
113
114 return cfg, nil
115 }
116
117 // Validate checks if the configuration is valid
118 func (c *Config) Validate() error {
119 if c.Database.Type == "" {
120 return fmt.Errorf("database type is required")
121 }
122
123 if c.Database.Type == "bbolt" && c.Database.Path == "" {
124 return fmt.Errorf("database path is required for bbolt")
125 }
126
127 if c.Database.Type == "postgres" && c.Database.URL == "" {
128 return fmt.Errorf("database URL is required for postgres")
129 }
130
131 if c.GRPC.Port <= 0 || c.GRPC.Port > 65535 {
132 return fmt.Errorf("invalid gRPC port: %d", c.GRPC.Port)
133 }
134
135 if c.HTTP.Enabled && (c.HTTP.Port <= 0 || c.HTTP.Port > 65535) {
136 return fmt.Errorf("invalid HTTP port: %d", c.HTTP.Port)
137 }
138
139 if c.Coordinator.ReplicationFactor <= 0 {
140 return fmt.Errorf("replication factor must be positive")
141 }
142
143 if c.Coordinator.MaxNodesPerChunk <= 0 {
144 return fmt.Errorf("max nodes per chunk must be positive")
145 }
146
147 return nil
148 }
149
150 // Save writes the configuration to a YAML file
151 func (c *Config) Save(path string) error {
152 data, err := yaml.Marshal(c)
153 if err != nil {
154 return fmt.Errorf("failed to marshal config: %w", err)
155 }
156
157 if err := os.WriteFile(path, data, 0644); err != nil {
158 return fmt.Errorf("failed to write config file %s: %w", path, err)
159 }
160
161 return nil
162 }