go.temporal.io/server@v1.23.0/common/dynamicconfig/file_based_client_test.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 "os" 29 "testing" 30 "time" 31 32 "github.com/golang/mock/gomock" 33 "github.com/stretchr/testify/require" 34 "github.com/stretchr/testify/suite" 35 36 enumspb "go.temporal.io/api/enums/v1" 37 38 enumsspb "go.temporal.io/server/api/enums/v1" 39 "go.temporal.io/server/common/log" 40 ) 41 42 // Note: fileBasedClientSuite also heavily tests Collection, since some tests are easier with data 43 // provided from a file. 44 type fileBasedClientSuite struct { 45 suite.Suite 46 *require.Assertions 47 client Client 48 collection *Collection 49 doneCh chan interface{} 50 } 51 52 func TestFileBasedClientSuite(t *testing.T) { 53 s := new(fileBasedClientSuite) 54 suite.Run(t, s) 55 } 56 57 func (s *fileBasedClientSuite) SetupSuite() { 58 var err error 59 s.doneCh = make(chan interface{}) 60 logger := log.NewNoopLogger() 61 s.client, err = NewFileBasedClient(&FileBasedClientConfig{ 62 Filepath: "config/testConfig.yaml", 63 PollInterval: time.Second * 5, 64 }, logger, s.doneCh) 65 s.collection = NewCollection(s.client, logger) 66 s.Require().NoError(err) 67 } 68 69 func (s *fileBasedClientSuite) TearDownSuite() { 70 close(s.doneCh) 71 } 72 73 func (s *fileBasedClientSuite) SetupTest() { 74 s.Assertions = require.New(s.T()) 75 } 76 77 func (s *fileBasedClientSuite) TestGetValue() { 78 cvs := s.client.GetValue(testGetBoolPropertyKey) 79 s.Equal(3, len(cvs)) 80 s.ElementsMatch([]ConstrainedValue{ 81 {Constraints: Constraints{}, Value: false}, 82 {Constraints: Constraints{Namespace: "global-samples-namespace"}, Value: true}, 83 {Constraints: Constraints{Namespace: "samples-namespace"}, Value: true}, 84 }, cvs) 85 } 86 87 func (s *fileBasedClientSuite) TestGetValue_NonExistKey() { 88 cvs := s.client.GetValue(unknownKey) 89 s.Nil(cvs) 90 91 defaultValue := true 92 v := s.collection.GetBoolProperty(unknownKey, defaultValue)() 93 s.Equal(defaultValue, v) 94 } 95 96 func (s *fileBasedClientSuite) TestGetValue_CaseInsensitie() { 97 cvs := s.client.GetValue(testCaseInsensitivePropertyKey) 98 s.Equal(1, len(cvs)) 99 100 v := s.collection.GetBoolProperty(testCaseInsensitivePropertyKey, false)() 101 s.Equal(true, v) 102 } 103 104 func (s *fileBasedClientSuite) TestGetIntValue() { 105 v := s.collection.GetIntProperty(testGetIntPropertyKey, 1)() 106 s.Equal(1000, v) 107 } 108 109 func (s *fileBasedClientSuite) TestGetIntValue_FilterNotMatch() { 110 v := s.collection.GetIntPropertyFilteredByNamespace(testGetIntPropertyKey, 500)("samples-namespace") 111 s.Equal(1000, v) 112 } 113 114 func (s *fileBasedClientSuite) TestGetIntValue_WrongType() { 115 defaultValue := 2000 116 v := s.collection.GetIntPropertyFilteredByNamespace(testGetIntPropertyKey, defaultValue)("global-samples-namespace") 117 s.Equal(defaultValue, v) 118 } 119 120 func (s *fileBasedClientSuite) TestGetIntValue_FilteredByWorkflowTaskQueueInfo() { 121 expectedValue := 1001 122 v := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 123 "global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW) 124 s.Equal(expectedValue, v) 125 } 126 127 func (s *fileBasedClientSuite) TestGetIntValue_FilteredByNoTaskTypeQueueInfo() { 128 expectedValue := 1003 129 v := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 130 // this is contrived, but simulates something that doesn't match workflow or activity 131 "global-samples-namespace", "test-tq", enumspb.TaskQueueType(3), 132 ) 133 s.Equal(expectedValue, v) 134 } 135 136 func (s *fileBasedClientSuite) TestGetIntValue_FilteredByActivityTaskQueueInfo() { 137 expectedValue := 1002 138 v := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 139 "global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_ACTIVITY) 140 s.Equal(expectedValue, v) 141 } 142 143 func (s *fileBasedClientSuite) TestGetIntValue_FilteredByTaskQueueNameOnly() { 144 expectedValue := 1005 145 v := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 146 "some-other-namespace", "other-test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW) 147 s.Equal(expectedValue, v) 148 } 149 150 func (s *fileBasedClientSuite) TestGetIntValue_FilterByTQ_NamespaceOnly() { 151 expectedValue := 1004 152 v := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 153 "another-namespace", "test-tq", 0) 154 s.Equal(expectedValue, v) 155 expectedValue = 1005 156 v = s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 157 "another-namespace", "other-test-tq", 0) 158 s.Equal(expectedValue, v) 159 } 160 161 func (s *fileBasedClientSuite) TestGetIntValue_FilterByTQ_MatchFallback() { 162 // should return 1001 as the most specific match 163 v1 := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 1001)( 164 "global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW) 165 v2 := s.collection.GetIntPropertyFilteredByTaskQueueInfo(testGetIntPropertyKey, 0)( 166 "global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW) 167 s.Equal(v1, v2) 168 } 169 170 func (s *fileBasedClientSuite) TestGetFloatValue() { 171 v := s.collection.GetFloat64Property(testGetFloat64PropertyKey, 1)() 172 s.Equal(12.0, v) 173 } 174 175 func (s *fileBasedClientSuite) TestGetFloatValue_WrongType() { 176 defaultValue := 1.0 177 v := s.collection.GetFloatPropertyFilteredByNamespace(testGetFloat64PropertyKey, defaultValue)("samples-namespace") 178 s.Equal(defaultValue, v) 179 } 180 181 func (s *fileBasedClientSuite) TestGetBoolValue() { 182 v := s.collection.GetBoolProperty(testGetBoolPropertyKey, true)() 183 s.Equal(false, v) 184 } 185 186 func (s *fileBasedClientSuite) TestGetStringValue() { 187 v := s.collection.GetStringPropertyFnWithNamespaceFilter(testGetStringPropertyKey, "defaultString")("random-namespace") 188 s.Equal("constrained-string", v) 189 } 190 191 func (s *fileBasedClientSuite) TestGetMapValue() { 192 var defaultVal map[string]interface{} 193 v := s.collection.GetMapProperty(testGetMapPropertyKey, defaultVal)() 194 expectedVal := map[string]interface{}{ 195 "key1": "1", 196 "key2": 1, 197 "key3": []interface{}{ 198 false, 199 map[string]interface{}{ 200 "key4": true, 201 "key5": 2.1, 202 }, 203 }, 204 } 205 s.Equal(expectedVal, v) 206 } 207 208 func (s *fileBasedClientSuite) TestGetMapValue_WrongType() { 209 var defaultVal map[string]interface{} 210 v := s.collection.GetMapPropertyFnWithNamespaceFilter(testGetMapPropertyKey, defaultVal)("random-namespace") 211 s.Equal(defaultVal, v) 212 } 213 214 func (s *fileBasedClientSuite) TestGetDurationValue() { 215 v := s.collection.GetDurationProperty(testGetDurationPropertyKey, time.Second)() 216 s.Equal(time.Minute, v) 217 } 218 219 func (s *fileBasedClientSuite) TestGetDurationValue_DefaultSeconds() { 220 v := s.collection.GetDurationPropertyFilteredByNamespace(testGetDurationPropertyKey, time.Second)("samples-namespace") 221 s.Equal(2*time.Second, v) 222 } 223 224 func (s *fileBasedClientSuite) TestGetDurationValue_NotStringRepresentation() { 225 v := s.collection.GetDurationPropertyFilteredByNamespace(testGetDurationPropertyKey, time.Second)("broken-namespace") 226 s.Equal(time.Second, v) 227 } 228 229 func (s *fileBasedClientSuite) TestGetDurationValue_ParseFailed() { 230 v := s.collection.GetDurationPropertyFilteredByTaskQueueInfo(testGetDurationPropertyKey, time.Second)( 231 "samples-namespace", "longIdleTimeTaskqueue", enumspb.TASK_QUEUE_TYPE_WORKFLOW) 232 s.Equal(time.Second, v) 233 } 234 235 func (s *fileBasedClientSuite) TestGetDurationValue_FilteredByTaskTypeQueue() { 236 expectedValue := time.Second * 10 237 v := s.collection.GetDurationPropertyFilteredByTaskType(testGetDurationPropertyFilteredByTaskTypeKey, 0)( 238 enumsspb.TASK_TYPE_ACTIVITY_RETRY_TIMER, 239 ) 240 s.Equal(expectedValue, v) 241 v = s.collection.GetDurationPropertyFilteredByTaskType(testGetDurationPropertyFilteredByTaskTypeKey, 0)( 242 enumsspb.TASK_TYPE_REPLICATION_HISTORY, 243 ) 244 s.Equal(expectedValue, v) 245 } 246 247 func (s *fileBasedClientSuite) TestValidateConfig_ConfigNotExist() { 248 _, err := NewFileBasedClient(nil, nil, nil) 249 s.Error(err) 250 } 251 252 func (s *fileBasedClientSuite) TestValidateConfig_FileNotExist() { 253 _, err := NewFileBasedClient(&FileBasedClientConfig{ 254 Filepath: "file/not/exist.yaml", 255 PollInterval: time.Second * 10, 256 }, nil, nil) 257 s.Error(err) 258 } 259 260 func (s *fileBasedClientSuite) TestValidateConfig_ShortPollInterval() { 261 _, err := NewFileBasedClient(&FileBasedClientConfig{ 262 Filepath: "config/testConfig.yaml", 263 PollInterval: time.Second, 264 }, nil, nil) 265 s.Error(err) 266 } 267 268 type MockFileInfo struct { 269 FileName string 270 IsDirectory bool 271 ModTimeValue time.Time 272 } 273 274 func (mfi MockFileInfo) Name() string { return mfi.FileName } 275 func (mfi MockFileInfo) Size() int64 { return int64(8) } 276 func (mfi MockFileInfo) Mode() os.FileMode { return os.ModePerm } 277 func (mfi MockFileInfo) ModTime() time.Time { return mfi.ModTimeValue } 278 func (mfi MockFileInfo) IsDir() bool { return mfi.IsDirectory } 279 func (mfi MockFileInfo) Sys() interface{} { return nil } 280 281 func (s *fileBasedClientSuite) TestUpdate_ChangedValue() { 282 ctrl := gomock.NewController(s.T()) 283 defer ctrl.Finish() 284 285 doneCh := make(chan interface{}) 286 reader := NewMockfileReader(ctrl) 287 mockLogger := log.NewMockLogger(ctrl) 288 289 updateInterval := time.Minute * 5 290 originFileInfo := &MockFileInfo{ModTimeValue: time.Now()} 291 updatedFileInfo := &MockFileInfo{ModTimeValue: originFileInfo.ModTimeValue.Add(updateInterval + time.Second)} 292 293 originFileData := []byte(` 294 testGetFloat64PropertyKey: 295 - value: 12 296 constraints: {} 297 298 testGetIntPropertyKey: 299 - value: 1000 300 constraints: {} 301 302 testGetBoolPropertyKey: 303 - value: false 304 constraints: {} 305 - value: true 306 constraints: 307 namespace: global-samples-namespace 308 - value: true 309 constraints: 310 namespace: samples-namespace 311 `) 312 updatedFileData := []byte(` 313 testGetFloat64PropertyKey: 314 - value: 13 315 constraints: {} 316 317 testGetIntPropertyKey: 318 - value: 2000 319 constraints: {} 320 321 testGetBoolPropertyKey: 322 - value: true 323 constraints: {} 324 - value: false 325 constraints: 326 namespace: global-samples-namespace 327 - value: true 328 constraints: 329 namespace: samples-namespace 330 `) 331 332 reader.EXPECT().Stat(gomock.Any()).Return(originFileInfo, nil).Times(2) 333 reader.EXPECT().ReadFile(gomock.Any()).Return(originFileData, nil) 334 335 mockLogger.EXPECT().Info(gomock.Any()).Times(6) 336 client, err := NewFileBasedClientWithReader(reader, 337 &FileBasedClientConfig{ 338 Filepath: "anyValue", 339 PollInterval: updateInterval, 340 }, mockLogger, s.doneCh) 341 s.NoError(err) 342 343 reader.EXPECT().Stat(gomock.Any()).Return(updatedFileInfo, nil) 344 reader.EXPECT().ReadFile(gomock.Any()).Return(updatedFileData, nil) 345 346 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: { constraints: {} value: 12 } newValue: { constraints: {} value: 13 }", gomock.Any()) 347 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetintpropertykey oldValue: { constraints: {} value: 1000 } newValue: { constraints: {} value: 2000 }", gomock.Any()) 348 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetboolpropertykey oldValue: { constraints: {} value: false } newValue: { constraints: {} value: true }", gomock.Any()) 349 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetboolpropertykey oldValue: { constraints: {{Namespace:global-samples-namespace}} value: true } newValue: { constraints: {{Namespace:global-samples-namespace}} value: false }", gomock.Any()) 350 mockLogger.EXPECT().Info(gomock.Any()) 351 s.NoError(client.update()) 352 s.NoError(err) 353 close(doneCh) 354 } 355 356 func (s *fileBasedClientSuite) TestUpdate_ChangedMapValue() { 357 ctrl := gomock.NewController(s.T()) 358 defer ctrl.Finish() 359 360 doneCh := make(chan interface{}) 361 reader := NewMockfileReader(ctrl) 362 mockLogger := log.NewMockLogger(ctrl) 363 364 updateInterval := time.Minute * 5 365 originFileInfo := &MockFileInfo{ModTimeValue: time.Now()} 366 updatedFileInfo := &MockFileInfo{ModTimeValue: originFileInfo.ModTimeValue.Add(updateInterval + time.Second)} 367 368 originFileData := []byte(` 369 history.defaultActivityRetryPolicy: 370 - value: 371 InitialIntervalInSeconds: 1 372 MaximumIntervalCoefficient: 100.0 373 BackoffCoefficient: 3.0 374 MaximumAttempts: 0 375 `) 376 updatedFileData := []byte(` 377 history.defaultActivityRetryPolicy: 378 - value: 379 InitialIntervalInSeconds: 3 380 MaximumIntervalCoefficient: 100.0 381 BackoffCoefficient: 2.0 382 MaximumAttempts: 0 383 `) 384 385 reader.EXPECT().Stat(gomock.Any()).Return(originFileInfo, nil).Times(2) 386 reader.EXPECT().ReadFile(gomock.Any()).Return(originFileData, nil) 387 388 mockLogger.EXPECT().Info(gomock.Any()).Times(2) 389 client, err := NewFileBasedClientWithReader(reader, 390 &FileBasedClientConfig{ 391 Filepath: "anyValue", 392 PollInterval: updateInterval, 393 }, mockLogger, s.doneCh) 394 s.NoError(err) 395 396 reader.EXPECT().Stat(gomock.Any()).Return(updatedFileInfo, nil) 397 reader.EXPECT().ReadFile(gomock.Any()).Return(updatedFileData, nil) 398 399 mockLogger.EXPECT().Info("dynamic config changed for the key: history.defaultactivityretrypolicy oldValue: { constraints: {} value: map[BackoffCoefficient:3 InitialIntervalInSeconds:1 MaximumAttempts:0 MaximumIntervalCoefficient:100] } newValue: { constraints: {} value: map[BackoffCoefficient:2 InitialIntervalInSeconds:3 MaximumAttempts:0 MaximumIntervalCoefficient:100] }", gomock.Any()) 400 mockLogger.EXPECT().Info(gomock.Any()) 401 s.NoError(client.update()) 402 s.NoError(err) 403 close(doneCh) 404 } 405 406 func (s *fileBasedClientSuite) TestUpdate_NewEntry() { 407 ctrl := gomock.NewController(s.T()) 408 defer ctrl.Finish() 409 410 doneCh := make(chan interface{}) 411 reader := NewMockfileReader(ctrl) 412 mockLogger := log.NewMockLogger(ctrl) 413 414 updateInterval := time.Minute * 5 415 originFileInfo := &MockFileInfo{ModTimeValue: time.Now()} 416 updatedFileInfo := &MockFileInfo{ModTimeValue: originFileInfo.ModTimeValue.Add(updateInterval + time.Second)} 417 418 originFileData := []byte(` 419 testGetFloat64PropertyKey: 420 - value: 12 421 constraints: {} 422 `) 423 updatedFileData := []byte(` 424 testGetFloat64PropertyKey: 425 - value: 12 426 constraints: {} 427 - value: 22 428 constraints: 429 namespace: samples-namespace 430 431 testGetIntPropertyKey: 432 - value: 2000 433 constraints: {} 434 `) 435 436 reader.EXPECT().Stat(gomock.Any()).Return(originFileInfo, nil).Times(2) 437 reader.EXPECT().ReadFile(gomock.Any()).Return(originFileData, nil) 438 439 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: nil newValue: { constraints: {} value: 12 }", gomock.Any()) 440 mockLogger.EXPECT().Info(gomock.Any()) 441 client, err := NewFileBasedClientWithReader(reader, 442 &FileBasedClientConfig{ 443 Filepath: "anyValue", 444 PollInterval: updateInterval, 445 }, mockLogger, s.doneCh) 446 s.NoError(err) 447 448 reader.EXPECT().Stat(gomock.Any()).Return(updatedFileInfo, nil) 449 reader.EXPECT().ReadFile(gomock.Any()).Return(updatedFileData, nil) 450 451 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: nil newValue: { constraints: {{Namespace:samples-namespace}} value: 22 }", gomock.Any()) 452 mockLogger.EXPECT().Info("dynamic config changed for the key: testgetintpropertykey oldValue: nil newValue: { constraints: {} value: 2000 }", gomock.Any()) 453 mockLogger.EXPECT().Info(gomock.Any()) 454 s.NoError(client.update()) 455 s.NoError(err) 456 close(doneCh) 457 } 458 459 func (s *fileBasedClientSuite) TestUpdate_ChangeOrder_ShouldNotWriteLog() { 460 ctrl := gomock.NewController(s.T()) 461 defer ctrl.Finish() 462 463 doneCh := make(chan interface{}) 464 reader := NewMockfileReader(ctrl) 465 mockLogger := log.NewMockLogger(ctrl) 466 467 updateInterval := time.Minute * 5 468 originFileInfo := &MockFileInfo{ModTimeValue: time.Now()} 469 updatedFileInfo := &MockFileInfo{ModTimeValue: originFileInfo.ModTimeValue.Add(updateInterval + time.Second)} 470 471 originFileData := []byte(` 472 testGetFloat64PropertyKey: 473 - value: 12 474 constraints: {} 475 - value: 22 476 constraints: 477 namespace: samples-namespace 478 479 testGetIntPropertyKey: 480 - value: 2000 481 constraints: {} 482 `) 483 updatedFileData := []byte(` 484 testGetIntPropertyKey: 485 - value: 2000 486 constraints: {} 487 488 testGetFloat64PropertyKey: 489 - value: 22 490 constraints: 491 namespace: samples-namespace 492 - value: 12 493 constraints: {} 494 `) 495 496 reader.EXPECT().Stat(gomock.Any()).Return(originFileInfo, nil).Times(2) 497 reader.EXPECT().ReadFile(gomock.Any()).Return(originFileData, nil) 498 499 mockLogger.EXPECT().Info(gomock.Any()).Times(4) 500 client, err := NewFileBasedClientWithReader(reader, 501 &FileBasedClientConfig{ 502 Filepath: "anyValue", 503 PollInterval: updateInterval, 504 }, mockLogger, s.doneCh) 505 s.NoError(err) 506 507 reader.EXPECT().Stat(gomock.Any()).Return(updatedFileInfo, nil) 508 reader.EXPECT().ReadFile(gomock.Any()).Return(updatedFileData, nil) 509 510 mockLogger.EXPECT().Info(gomock.Any()).Times(1) 511 s.NoError(client.update()) 512 s.NoError(err) 513 close(doneCh) 514 }