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  }