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  }