go.temporal.io/server@v1.23.0/common/dynamicconfig/file_based_client.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. 4 // 5 // Copyright (c) 2020 Uber Technologies, Inc. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 25 //go:generate mockgen -copyright_file ../../LICENSE -package $GOPACKAGE -source $GOFILE -destination file_based_client_mock.go 26 27 package dynamicconfig 28 29 import ( 30 "errors" 31 "fmt" 32 "os" 33 "reflect" 34 "strings" 35 "sync/atomic" 36 "time" 37 38 enumspb "go.temporal.io/api/enums/v1" 39 "gopkg.in/yaml.v3" 40 41 enumsspb "go.temporal.io/server/api/enums/v1" 42 "go.temporal.io/server/common/log" 43 "go.temporal.io/server/common/log/tag" 44 ) 45 46 var _ Client = (*fileBasedClient)(nil) 47 48 const ( 49 minPollInterval = time.Second * 5 50 fileMode = 0644 // used for update config file 51 ) 52 53 type ( 54 fileReader interface { 55 Stat(src string) (os.FileInfo, error) 56 ReadFile(src string) ([]byte, error) 57 } 58 59 // FileBasedClientConfig is the config for the file based dynamic config client. 60 // It specifies where the config file is stored and how often the config should be 61 // updated by checking the config file again. 62 FileBasedClientConfig struct { 63 Filepath string `yaml:"filepath"` 64 PollInterval time.Duration `yaml:"pollInterval"` 65 } 66 67 configValueMap map[string][]ConstrainedValue 68 69 fileBasedClient struct { 70 values atomic.Value // configValueMap 71 logger log.Logger 72 reader fileReader 73 lastUpdatedTime time.Time 74 config *FileBasedClientConfig 75 doneCh <-chan interface{} 76 } 77 78 osReader struct { 79 } 80 ) 81 82 // NewFileBasedClient creates a file based client. 83 func NewFileBasedClient(config *FileBasedClientConfig, logger log.Logger, doneCh <-chan interface{}) (*fileBasedClient, error) { 84 client := &fileBasedClient{ 85 logger: logger, 86 reader: &osReader{}, 87 config: config, 88 doneCh: doneCh, 89 } 90 91 err := client.init() 92 if err != nil { 93 return nil, err 94 } 95 96 return client, nil 97 } 98 99 func NewFileBasedClientWithReader(reader fileReader, config *FileBasedClientConfig, logger log.Logger, doneCh <-chan interface{}) (*fileBasedClient, error) { 100 client := &fileBasedClient{ 101 logger: logger, 102 reader: reader, 103 config: config, 104 doneCh: doneCh, 105 } 106 107 err := client.init() 108 if err != nil { 109 return nil, err 110 } 111 112 return client, nil 113 } 114 115 func (fc *fileBasedClient) GetValue(key Key) []ConstrainedValue { 116 values := fc.values.Load().(configValueMap) 117 return values[strings.ToLower(key.String())] 118 } 119 120 func (fc *fileBasedClient) init() error { 121 if err := fc.validateConfig(fc.config); err != nil { 122 return fmt.Errorf("unable to validate dynamic config: %w", err) 123 } 124 125 if err := fc.update(); err != nil { 126 return fmt.Errorf("unable to read dynamic config: %w", err) 127 } 128 129 go func() { 130 ticker := time.NewTicker(fc.config.PollInterval) 131 for { 132 select { 133 case <-ticker.C: 134 err := fc.update() 135 if err != nil { 136 fc.logger.Error("Unable to update dynamic config.", tag.Error(err)) 137 } 138 case <-fc.doneCh: 139 ticker.Stop() 140 return 141 } 142 } 143 }() 144 145 return nil 146 } 147 148 func (fc *fileBasedClient) update() error { 149 defer func() { 150 fc.lastUpdatedTime = time.Now().UTC() 151 }() 152 153 info, err := fc.reader.Stat(fc.config.Filepath) 154 if err != nil { 155 return fmt.Errorf("dynamic config file: %s: %w", fc.config.Filepath, err) 156 } 157 if !info.ModTime().After(fc.lastUpdatedTime) { 158 return nil 159 } 160 161 confContent, err := fc.reader.ReadFile(fc.config.Filepath) 162 if err != nil { 163 return fmt.Errorf("dynamic config file: %s: %w", fc.config.Filepath, err) 164 } 165 166 var yamlValues map[string][]struct { 167 Constraints map[string]any 168 Value any 169 } 170 if err = yaml.Unmarshal(confContent, &yamlValues); err != nil { 171 return fmt.Errorf("unable to decode dynamic config: %w", err) 172 } 173 174 newValues := make(configValueMap, len(yamlValues)) 175 for key, yamlCV := range yamlValues { 176 cvs := make([]ConstrainedValue, len(yamlCV)) 177 for i, cv := range yamlCV { 178 // yaml will unmarshal map into map[interface{}]interface{} instead of map[string]interface{} 179 // manually convert key type to string for all values here 180 cvs[i].Value, err = convertKeyTypeToString(cv.Value) 181 if err != nil { 182 return err 183 } 184 cvs[i].Constraints, err = convertYamlConstraints(cv.Constraints) 185 if err != nil { 186 return err 187 } 188 } 189 newValues[strings.ToLower(key)] = cvs 190 } 191 192 prev := fc.values.Swap(newValues) 193 oldValues, _ := prev.(configValueMap) 194 fc.logDiff(oldValues, newValues) 195 fc.logger.Info("Updated dynamic config") 196 197 return nil 198 } 199 200 func (fc *fileBasedClient) validateConfig(config *FileBasedClientConfig) error { 201 if config == nil { 202 return errors.New("configuration for dynamic config client is nil") 203 } 204 if _, err := fc.reader.Stat(config.Filepath); err != nil { 205 return fmt.Errorf("dynamic config: %s: %w", config.Filepath, err) 206 } 207 if config.PollInterval < minPollInterval { 208 return fmt.Errorf("poll interval should be at least %v", minPollInterval) 209 } 210 return nil 211 } 212 213 func (fc *fileBasedClient) logDiff(old configValueMap, new configValueMap) { 214 for key, newValues := range new { 215 oldValues, ok := old[key] 216 if !ok { 217 for _, newValue := range newValues { 218 // new key added 219 fc.logValueDiff(key, nil, &newValue) 220 } 221 } else { 222 // compare existing keys 223 fc.logConstraintsDiff(key, oldValues, newValues) 224 } 225 } 226 227 // check for removed values 228 for key, oldValues := range old { 229 if _, ok := new[key]; !ok { 230 for _, oldValue := range oldValues { 231 fc.logValueDiff(key, &oldValue, nil) 232 } 233 } 234 } 235 } 236 237 func (fc *fileBasedClient) logConstraintsDiff(key string, oldValues []ConstrainedValue, newValues []ConstrainedValue) { 238 for _, oldValue := range oldValues { 239 matchFound := false 240 for _, newValue := range newValues { 241 if oldValue.Constraints == newValue.Constraints { 242 matchFound = true 243 if !reflect.DeepEqual(oldValue.Value, newValue.Value) { 244 fc.logValueDiff(key, &oldValue, &newValue) 245 } 246 } 247 } 248 if !matchFound { 249 fc.logValueDiff(key, &oldValue, nil) 250 } 251 } 252 253 for _, newValue := range newValues { 254 matchFound := false 255 for _, oldValue := range oldValues { 256 if oldValue.Constraints == newValue.Constraints { 257 matchFound = true 258 } 259 } 260 if !matchFound { 261 fc.logValueDiff(key, nil, &newValue) 262 } 263 } 264 } 265 266 func (fc *fileBasedClient) logValueDiff(key string, oldValue *ConstrainedValue, newValue *ConstrainedValue) { 267 logLine := &strings.Builder{} 268 logLine.Grow(128) 269 logLine.WriteString("dynamic config changed for the key: ") 270 logLine.WriteString(key) 271 logLine.WriteString(" oldValue: ") 272 fc.appendConstrainedValue(logLine, oldValue) 273 logLine.WriteString(" newValue: ") 274 fc.appendConstrainedValue(logLine, newValue) 275 fc.logger.Info(logLine.String()) 276 } 277 278 func (fc *fileBasedClient) appendConstrainedValue(logLine *strings.Builder, value *ConstrainedValue) { 279 if value == nil { 280 logLine.WriteString("nil") 281 } else { 282 logLine.WriteString("{ constraints: {") 283 if value.Constraints.Namespace != "" { 284 logLine.WriteString(fmt.Sprintf("{Namespace:%s}", value.Constraints.Namespace)) 285 } 286 if value.Constraints.NamespaceID != "" { 287 logLine.WriteString(fmt.Sprintf("{NamespaceID:%s}", value.Constraints.NamespaceID)) 288 } 289 if value.Constraints.TaskQueueName != "" { 290 logLine.WriteString(fmt.Sprintf("{TaskQueueName:%s}", value.Constraints.TaskQueueName)) 291 } 292 if value.Constraints.TaskQueueType != enumspb.TASK_QUEUE_TYPE_UNSPECIFIED { 293 logLine.WriteString(fmt.Sprintf("{TaskQueueType:%s}", value.Constraints.TaskQueueType)) 294 } 295 if value.Constraints.ShardID != 0 { 296 logLine.WriteString(fmt.Sprintf("{ShardID:%d}", value.Constraints.ShardID)) 297 } 298 if value.Constraints.TaskType != enumsspb.TASK_TYPE_UNSPECIFIED { 299 logLine.WriteString(fmt.Sprintf("{HistoryTaskType:%s}", value.Constraints.TaskType)) 300 } 301 logLine.WriteString(fmt.Sprint("} value: ", value.Value, " }")) 302 } 303 } 304 305 func convertKeyTypeToString(v interface{}) (interface{}, error) { 306 switch v := v.(type) { 307 case map[interface{}]interface{}: 308 return convertKeyTypeToStringMap(v) 309 case []interface{}: 310 return convertKeyTypeToStringSlice(v) 311 default: 312 return v, nil 313 } 314 } 315 316 func convertKeyTypeToStringMap(m map[interface{}]interface{}) (map[string]interface{}, error) { 317 stringKeyMap := make(map[string]interface{}) 318 for key, value := range m { 319 stringKey, ok := key.(string) 320 if !ok { 321 return nil, fmt.Errorf("type of key %v is not string", key) 322 } 323 convertedValue, err := convertKeyTypeToString(value) 324 if err != nil { 325 return nil, err 326 } 327 stringKeyMap[stringKey] = convertedValue 328 } 329 return stringKeyMap, nil 330 } 331 332 func convertKeyTypeToStringSlice(s []interface{}) ([]interface{}, error) { 333 stringKeySlice := make([]interface{}, len(s)) 334 for idx, value := range s { 335 convertedValue, err := convertKeyTypeToString(value) 336 if err != nil { 337 return nil, err 338 } 339 stringKeySlice[idx] = convertedValue 340 } 341 return stringKeySlice, nil 342 } 343 344 func convertYamlConstraints(m map[string]any) (Constraints, error) { 345 var cs Constraints 346 for k, v := range m { 347 switch strings.ToLower(k) { 348 case "namespace": 349 if v, ok := v.(string); ok { 350 cs.Namespace = v 351 } else { 352 return cs, fmt.Errorf("namespace constraint must be string") 353 } 354 case "namespaceid": 355 if v, ok := v.(string); ok { 356 cs.NamespaceID = v 357 } else { 358 return cs, fmt.Errorf("namespaceID constraint must be string") 359 } 360 case "taskqueuename": 361 if v, ok := v.(string); ok { 362 cs.TaskQueueName = v 363 } else { 364 return cs, fmt.Errorf("taskQueueName constraint must be string") 365 } 366 case "tasktype": 367 switch v := v.(type) { 368 case string: 369 i, err := enumspb.TaskQueueTypeFromString(v) 370 if err != nil { 371 return cs, fmt.Errorf("invalid value for taskType: %w", err) 372 } else if i <= enumspb.TASK_QUEUE_TYPE_UNSPECIFIED { 373 return cs, fmt.Errorf("taskType constraint must be Workflow/Activity") 374 } 375 cs.TaskQueueType = i 376 case int: 377 if v > int(enumspb.TASK_QUEUE_TYPE_UNSPECIFIED) { 378 cs.TaskQueueType = enumspb.TaskQueueType(v) 379 } else { 380 return cs, fmt.Errorf("taskType constraint must be Workflow/Activity") 381 } 382 default: 383 return cs, fmt.Errorf("taskType constraint must be Workflow/Activity") 384 } 385 case "historytasktype": 386 switch v := v.(type) { 387 case string: 388 tt, err := enumsspb.TaskTypeFromString(v) 389 if err != nil { 390 return cs, fmt.Errorf("invalid value for historytasktype constraint: %w", err) 391 } else if tt <= enumsspb.TASK_TYPE_UNSPECIFIED { 392 return cs, fmt.Errorf("historytasktype %s constraint is not supported", v) 393 } 394 cs.TaskType = tt 395 case int: 396 cs.TaskType = enumsspb.TaskType(v) 397 default: 398 return cs, fmt.Errorf("historytasktype %T constraint is not supported", v) 399 } 400 case "shardid": 401 if v, ok := v.(int); ok { 402 cs.ShardID = int32(v) 403 } else { 404 return cs, fmt.Errorf("shardID constraint must be integer") 405 } 406 default: 407 return cs, fmt.Errorf("unknown constraint type %q", k) 408 } 409 } 410 return cs, nil 411 } 412 413 func (r *osReader) ReadFile(src string) ([]byte, error) { 414 return os.ReadFile(src) 415 } 416 417 func (r *osReader) Stat(src string) (os.FileInfo, error) { 418 return os.Stat(src) 419 }