go.temporal.io/server@v1.23.0/common/dynamicconfig/collection.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 package dynamicconfig 26 27 import ( 28 "errors" 29 "fmt" 30 "sync/atomic" 31 "time" 32 33 enumspb "go.temporal.io/api/enums/v1" 34 35 enumsspb "go.temporal.io/server/api/enums/v1" 36 "go.temporal.io/server/common/log" 37 "go.temporal.io/server/common/log/tag" 38 "go.temporal.io/server/common/primitives/timestamp" 39 ) 40 41 type ( 42 // Collection implements lookup and constraint logic on top of a Client. 43 // The rest of the server code should use Collection as the interface to dynamic config, 44 // instead of the low-level Client. 45 Collection struct { 46 client Client 47 logger log.Logger 48 errCount int64 49 } 50 51 // These function types follow a similar pattern: 52 // {X}PropertyFn - returns a value of type X that is global (no filters) 53 // {X}PropertyFnWith{Y}Filter - returns a value of type X with the given filters 54 // Available value types: 55 // Bool: bool 56 // Duration: time.Duration 57 // Float: float64 58 // Int: int 59 // Map: map[string]any 60 // String: string 61 // Available filters: 62 // Namespace func(namespace string) 63 // NamespaceID func(namespaceID string) 64 // TaskQueueInfo func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) 65 // ShardID func(shardID int32) 66 BoolPropertyFn func() bool 67 BoolPropertyFnWithNamespaceFilter func(namespace string) bool 68 BoolPropertyFnWithNamespaceIDFilter func(namespaceID string) bool 69 BoolPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool 70 DurationPropertyFn func() time.Duration 71 DurationPropertyFnWithNamespaceFilter func(namespace string) time.Duration 72 DurationPropertyFnWithNamespaceIDFilter func(namespaceID string) time.Duration 73 DurationPropertyFnWithShardIDFilter func(shardID int32) time.Duration 74 DurationPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) time.Duration 75 DurationPropertyFnWithTaskTypeFilter func(task enumsspb.TaskType) time.Duration 76 FloatPropertyFn func() float64 77 FloatPropertyFnWithNamespaceFilter func(namespace string) float64 78 FloatPropertyFnWithShardIDFilter func(shardID int32) float64 79 FloatPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) float64 80 IntPropertyFn func() int 81 IntPropertyFnWithNamespaceFilter func(namespace string) int 82 IntPropertyFnWithShardIDFilter func(shardID int32) int 83 IntPropertyFnWithTaskQueueInfoFilters func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) int 84 MapPropertyFn func() map[string]any 85 MapPropertyFnWithNamespaceFilter func(namespace string) map[string]any 86 StringPropertyFn func() string 87 StringPropertyFnWithNamespaceFilter func(namespace string) string 88 StringPropertyFnWithNamespaceIDFilter func(namespaceID string) string 89 ) 90 91 const ( 92 errCountLogThreshold = 1000 93 ) 94 95 var ( 96 errKeyNotPresent = errors.New("key not present") 97 errNoMatchingConstraint = errors.New("no matching constraint in key") 98 ) 99 100 // NewCollection creates a new collection 101 func NewCollection(client Client, logger log.Logger) *Collection { 102 return &Collection{ 103 client: client, 104 logger: logger, 105 errCount: -1, 106 } 107 } 108 109 func (c *Collection) throttleLog() bool { 110 // TODO: This is a lot of unnecessary contention with little benefit. Consider using 111 // https://github.com/cespare/percpu here. 112 errCount := atomic.AddInt64(&c.errCount, 1) 113 // log only the first x errors and then one every x after that to reduce log noise 114 return errCount < errCountLogThreshold || errCount%errCountLogThreshold == 0 115 } 116 117 // GetIntProperty gets property and asserts that it's an integer 118 func (c *Collection) GetIntProperty(key Key, defaultValue any) IntPropertyFn { 119 return func() int { 120 return matchAndConvert( 121 c, 122 key, 123 defaultValue, 124 globalPrecedence(), 125 convertInt, 126 ) 127 } 128 } 129 130 // GetIntPropertyFilteredByNamespace gets property with namespace filter and asserts that it's an integer 131 func (c *Collection) GetIntPropertyFilteredByNamespace(key Key, defaultValue any) IntPropertyFnWithNamespaceFilter { 132 return func(namespace string) int { 133 return matchAndConvert( 134 c, 135 key, 136 defaultValue, 137 namespacePrecedence(namespace), 138 convertInt, 139 ) 140 } 141 } 142 143 // GetIntPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's an integer 144 func (c *Collection) GetIntPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) IntPropertyFnWithTaskQueueInfoFilters { 145 return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) int { 146 return matchAndConvert( 147 c, 148 key, 149 defaultValue, 150 taskQueuePrecedence(namespace, taskQueue, taskType), 151 convertInt, 152 ) 153 } 154 } 155 156 // GetIntPropertyFilteredByShardID gets property with shardID as filter and asserts that it's an integer 157 func (c *Collection) GetIntPropertyFilteredByShardID(key Key, defaultValue any) IntPropertyFnWithShardIDFilter { 158 return func(shardID int32) int { 159 return matchAndConvert( 160 c, 161 key, 162 defaultValue, 163 shardIDPrecedence(shardID), 164 convertInt, 165 ) 166 } 167 } 168 169 // GetFloat64Property gets property and asserts that it's a float64 170 func (c *Collection) GetFloat64Property(key Key, defaultValue any) FloatPropertyFn { 171 return func() float64 { 172 return matchAndConvert( 173 c, 174 key, 175 defaultValue, 176 globalPrecedence(), 177 convertFloat, 178 ) 179 } 180 } 181 182 // GetFloat64PropertyFilteredByShardID gets property with shardID filter and asserts that it's a float64 183 func (c *Collection) GetFloat64PropertyFilteredByShardID(key Key, defaultValue any) FloatPropertyFnWithShardIDFilter { 184 return func(shardID int32) float64 { 185 return matchAndConvert( 186 c, 187 key, 188 defaultValue, 189 shardIDPrecedence(shardID), 190 convertFloat, 191 ) 192 } 193 } 194 195 // GetFloatPropertyFilteredByNamespace gets property with namespace filter and asserts that it's a float64 196 func (c *Collection) GetFloatPropertyFilteredByNamespace(key Key, defaultValue any) FloatPropertyFnWithNamespaceFilter { 197 return func(namespace string) float64 { 198 return matchAndConvert( 199 c, 200 key, 201 defaultValue, 202 namespacePrecedence(namespace), 203 convertFloat, 204 ) 205 } 206 } 207 208 // GetFloatPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a float64 209 func (c *Collection) GetFloatPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) FloatPropertyFnWithTaskQueueInfoFilters { 210 return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) float64 { 211 return matchAndConvert( 212 c, 213 key, 214 defaultValue, 215 taskQueuePrecedence(namespace, taskQueue, taskType), 216 convertFloat, 217 ) 218 } 219 } 220 221 // GetDurationProperty gets property and asserts that it's a duration 222 func (c *Collection) GetDurationProperty(key Key, defaultValue any) DurationPropertyFn { 223 return func() time.Duration { 224 return matchAndConvert( 225 c, 226 key, 227 defaultValue, 228 globalPrecedence(), 229 convertDuration, 230 ) 231 } 232 } 233 234 // GetDurationPropertyFilteredByNamespace gets property with namespace filter and asserts that it's a duration 235 func (c *Collection) GetDurationPropertyFilteredByNamespace(key Key, defaultValue any) DurationPropertyFnWithNamespaceFilter { 236 return func(namespace string) time.Duration { 237 return matchAndConvert( 238 c, 239 key, 240 defaultValue, 241 namespacePrecedence(namespace), 242 convertDuration, 243 ) 244 } 245 } 246 247 // GetDurationPropertyFilteredByNamespaceID gets property with namespaceID filter and asserts that it's a duration 248 func (c *Collection) GetDurationPropertyFilteredByNamespaceID(key Key, defaultValue any) DurationPropertyFnWithNamespaceIDFilter { 249 return func(namespaceID string) time.Duration { 250 return matchAndConvert( 251 c, 252 key, 253 defaultValue, 254 namespaceIDPrecedence(namespaceID), 255 convertDuration, 256 ) 257 } 258 } 259 260 // GetDurationPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a duration 261 func (c *Collection) GetDurationPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) DurationPropertyFnWithTaskQueueInfoFilters { 262 return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) time.Duration { 263 return matchAndConvert( 264 c, 265 key, 266 defaultValue, 267 taskQueuePrecedence(namespace, taskQueue, taskType), 268 convertDuration, 269 ) 270 } 271 } 272 273 // GetDurationPropertyFilteredByShardID gets property with shardID id as filter and asserts that it's a duration 274 func (c *Collection) GetDurationPropertyFilteredByShardID(key Key, defaultValue any) DurationPropertyFnWithShardIDFilter { 275 return func(shardID int32) time.Duration { 276 return matchAndConvert( 277 c, 278 key, 279 defaultValue, 280 shardIDPrecedence(shardID), 281 convertDuration, 282 ) 283 } 284 } 285 286 // GetDurationPropertyFilteredByTaskType gets property with task type as filters and asserts that it's a duration 287 func (c *Collection) GetDurationPropertyFilteredByTaskType(key Key, defaultValue any) DurationPropertyFnWithTaskTypeFilter { 288 return func(taskType enumsspb.TaskType) time.Duration { 289 return matchAndConvert( 290 c, 291 key, 292 defaultValue, 293 taskTypePrecedence(taskType), 294 convertDuration, 295 ) 296 } 297 } 298 299 // GetBoolProperty gets property and asserts that it's a bool 300 func (c *Collection) GetBoolProperty(key Key, defaultValue any) BoolPropertyFn { 301 return func() bool { 302 return matchAndConvert( 303 c, 304 key, 305 defaultValue, 306 globalPrecedence(), 307 convertBool, 308 ) 309 } 310 } 311 312 // GetStringProperty gets property and asserts that it's a string 313 func (c *Collection) GetStringProperty(key Key, defaultValue any) StringPropertyFn { 314 return func() string { 315 return matchAndConvert( 316 c, 317 key, 318 defaultValue, 319 globalPrecedence(), 320 convertString, 321 ) 322 } 323 } 324 325 // GetMapProperty gets property and asserts that it's a map 326 func (c *Collection) GetMapProperty(key Key, defaultValue any) MapPropertyFn { 327 return func() map[string]interface{} { 328 return matchAndConvert( 329 c, 330 key, 331 defaultValue, 332 globalPrecedence(), 333 convertMap, 334 ) 335 } 336 } 337 338 // GetStringPropertyFnWithNamespaceFilter gets property with namespace filter and asserts that it's a string 339 func (c *Collection) GetStringPropertyFnWithNamespaceFilter(key Key, defaultValue any) StringPropertyFnWithNamespaceFilter { 340 return func(namespace string) string { 341 return matchAndConvert( 342 c, 343 key, 344 defaultValue, 345 namespacePrecedence(namespace), 346 convertString, 347 ) 348 } 349 } 350 351 // GetStringPropertyFnWithNamespaceIDFilter gets property with namespace ID filter and asserts that it's a string 352 func (c *Collection) GetStringPropertyFnWithNamespaceIDFilter(key Key, defaultValue any) StringPropertyFnWithNamespaceIDFilter { 353 return func(namespaceID string) string { 354 return matchAndConvert( 355 c, 356 key, 357 defaultValue, 358 namespaceIDPrecedence(namespaceID), 359 convertString, 360 ) 361 } 362 } 363 364 // GetMapPropertyFnWithNamespaceFilter gets property and asserts that it's a map 365 func (c *Collection) GetMapPropertyFnWithNamespaceFilter(key Key, defaultValue any) MapPropertyFnWithNamespaceFilter { 366 return func(namespace string) map[string]interface{} { 367 return matchAndConvert( 368 c, 369 key, 370 defaultValue, 371 namespacePrecedence(namespace), 372 convertMap, 373 ) 374 } 375 } 376 377 // GetBoolPropertyFnWithNamespaceFilter gets property with namespace filter and asserts that it's a bool 378 func (c *Collection) GetBoolPropertyFnWithNamespaceFilter(key Key, defaultValue any) BoolPropertyFnWithNamespaceFilter { 379 return func(namespace string) bool { 380 return matchAndConvert( 381 c, 382 key, 383 defaultValue, 384 namespacePrecedence(namespace), 385 convertBool, 386 ) 387 } 388 } 389 390 // GetBoolPropertyFnWithNamespaceIDFilter gets property with namespaceID filter and asserts that it's a bool 391 func (c *Collection) GetBoolPropertyFnWithNamespaceIDFilter(key Key, defaultValue any) BoolPropertyFnWithNamespaceIDFilter { 392 return func(namespaceID string) bool { 393 return matchAndConvert( 394 c, 395 key, 396 defaultValue, 397 namespaceIDPrecedence(namespaceID), 398 convertBool, 399 ) 400 } 401 } 402 403 // GetBoolPropertyFilteredByTaskQueueInfo gets property with taskQueueInfo as filters and asserts that it's a bool 404 func (c *Collection) GetBoolPropertyFilteredByTaskQueueInfo(key Key, defaultValue any) BoolPropertyFnWithTaskQueueInfoFilters { 405 return func(namespace string, taskQueue string, taskType enumspb.TaskQueueType) bool { 406 return matchAndConvert( 407 c, 408 key, 409 defaultValue, 410 taskQueuePrecedence(namespace, taskQueue, taskType), 411 convertBool, 412 ) 413 } 414 } 415 416 // Task queue partitions use a dedicated function to handle defaults. 417 func (c *Collection) GetTaskQueuePartitionsProperty(key Key) IntPropertyFnWithTaskQueueInfoFilters { 418 return c.GetIntPropertyFilteredByTaskQueueInfo(key, defaultNumTaskQueuePartitions) 419 } 420 421 func (c *Collection) HasKey(key Key) bool { 422 cvs := c.client.GetValue(key) 423 return len(cvs) > 0 424 } 425 426 func findMatch(cvs, defaultCVs []ConstrainedValue, precedence []Constraints) (any, error) { 427 if len(cvs)+len(defaultCVs) == 0 { 428 return nil, errKeyNotPresent 429 } 430 for _, m := range precedence { 431 // duplicate the code so that we don't have to allocate a new slice to hold the 432 // concatenation of cvs and defaultCVs 433 for _, cv := range cvs { 434 if m == cv.Constraints { 435 return cv.Value, nil 436 } 437 } 438 for _, cv := range defaultCVs { 439 if m == cv.Constraints { 440 return cv.Value, nil 441 } 442 } 443 } 444 // key is present but no constraint section matches 445 return nil, errNoMatchingConstraint 446 } 447 448 // matchAndConvert can't be a method of Collection because methods can't be generic, but we can 449 // take a *Collection as an argument. 450 func matchAndConvert[T any]( 451 c *Collection, 452 key Key, 453 defaultValue any, 454 precedence []Constraints, 455 converter func(value any) (T, error), 456 ) T { 457 cvs := c.client.GetValue(key) 458 459 // defaultValue may be a list of constrained values. In that case, one of them must have an 460 // empty constraint set to be the fallback default. Otherwise we'll return the zero value 461 // and log an error (since []ConstrainedValue can't be converted to the desired type). 462 defaultCVs, _ := defaultValue.([]ConstrainedValue) 463 464 val, matchErr := findMatch(cvs, defaultCVs, precedence) 465 if matchErr != nil { 466 if c.throttleLog() { 467 c.logger.Debug("No such key in dynamic config, using default", tag.Key(key.String()), tag.Error(matchErr)) 468 } 469 // couldn't find a constrained match, use default 470 val = defaultValue 471 } 472 473 typedVal, convertErr := converter(val) 474 if convertErr != nil && matchErr == nil { 475 // We failed to convert the value to the desired type. Try converting the default. note 476 // that if matchErr != nil then val _is_ defaultValue and we don't have to try this again. 477 if c.throttleLog() { 478 c.logger.Warn("Failed to convert value, using default", tag.Key(key.String()), tag.IgnoredValue(val), tag.Error(convertErr)) 479 } 480 typedVal, convertErr = converter(defaultValue) 481 } 482 if convertErr != nil { 483 // If we can't convert the default, that's a bug in our code, use Warn level. 484 c.logger.Warn("Can't convert default value (this is a bug; fix server code)", tag.Key(key.String()), tag.IgnoredValue(defaultValue), tag.Error(convertErr)) 485 // Return typedVal anyway since we have to return something. 486 } 487 return typedVal 488 } 489 490 func globalPrecedence() []Constraints { 491 return []Constraints{ 492 {}, 493 } 494 } 495 496 func namespacePrecedence(namespace string) []Constraints { 497 return []Constraints{ 498 {Namespace: namespace}, 499 {}, 500 } 501 } 502 503 func namespaceIDPrecedence(namespaceID string) []Constraints { 504 return []Constraints{ 505 {NamespaceID: namespaceID}, 506 {}, 507 } 508 } 509 510 func taskQueuePrecedence(namespace string, taskQueue string, taskType enumspb.TaskQueueType) []Constraints { 511 return []Constraints{ 512 {Namespace: namespace, TaskQueueName: taskQueue, TaskQueueType: taskType}, 513 {Namespace: namespace, TaskQueueName: taskQueue}, 514 // A task-queue-name-only filter applies to a single task queue name across all 515 // namespaces, with higher precedence than a namespace-only filter. This is intended to 516 // be used by defaultNumTaskQueuePartitions and is probably not useful otherwise. 517 {TaskQueueName: taskQueue}, 518 {Namespace: namespace}, 519 {}, 520 } 521 } 522 523 func shardIDPrecedence(shardID int32) []Constraints { 524 return []Constraints{ 525 {ShardID: shardID}, 526 {}, 527 } 528 } 529 530 func taskTypePrecedence(taskType enumsspb.TaskType) []Constraints { 531 return []Constraints{ 532 {TaskType: taskType}, 533 {}, 534 } 535 } 536 537 func convertInt(val any) (int, error) { 538 if intVal, ok := val.(int); ok { 539 return intVal, nil 540 } 541 return 0, errors.New("value type is not int") 542 } 543 544 func convertFloat(val any) (float64, error) { 545 if floatVal, ok := val.(float64); ok { 546 return floatVal, nil 547 } else if intVal, ok := val.(int); ok { 548 return float64(intVal), nil 549 } 550 return 0, errors.New("value type is not float64") 551 } 552 553 func convertDuration(val any) (time.Duration, error) { 554 switch v := val.(type) { 555 case time.Duration: 556 return v, nil 557 case int: 558 // treat plain int as seconds 559 return time.Duration(v) * time.Second, nil 560 case string: 561 d, err := timestamp.ParseDurationDefaultSeconds(v) 562 if err != nil { 563 return 0, fmt.Errorf("failed to parse duration: %v", err) 564 } 565 return d, nil 566 } 567 return 0, errors.New("value not convertible to Duration") 568 } 569 570 func convertString(val any) (string, error) { 571 if stringVal, ok := val.(string); ok { 572 return stringVal, nil 573 } 574 return "", errors.New("value type is not string") 575 } 576 577 func convertBool(val any) (bool, error) { 578 if boolVal, ok := val.(bool); ok { 579 return boolVal, nil 580 } 581 return false, errors.New("value type is not bool") 582 } 583 584 func convertMap(val any) (map[string]any, error) { 585 if mapVal, ok := val.(map[string]any); ok { 586 return mapVal, nil 587 } 588 return nil, errors.New("value type is not map") 589 }