mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-03 03:56:04 +00:00
Full module path rebrand from Temporal to Hanzo Tasks: - go.mod module: github.com/hanzoai/tasks - All internal Go imports updated (1906 files) - cmd/server renamed to cmd/tasksd - Binary name: tasksd (was temporal-server) - Docker images: hanzoai/tasks (was temporalio/server) - README rewritten with Hanzo Tasks branding - LLM.md created with integration notes - Makefile, goreleaser, CI workflows updated - go.temporal.io/sdk and go.temporal.io/api imports preserved (separate repos) Fork of Temporal.io (MIT license).
1015 lines
32 KiB
Go
1015 lines
32 KiB
Go
package dynamicconfig_test
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
enumsspb "github.com/hanzoai/tasks/api/enums/v1"
|
|
"github.com/hanzoai/tasks/common/dynamicconfig"
|
|
"github.com/hanzoai/tasks/common/log"
|
|
"github.com/hanzoai/tasks/common/metrics"
|
|
"github.com/hanzoai/tasks/common/metrics/metricstest"
|
|
"github.com/hanzoai/tasks/common/retrypolicy"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
// Note: fileBasedClientSuite also heavily tests Collection, since some tests are easier with data
|
|
// provided from a file.
|
|
type fileBasedClientSuite struct {
|
|
suite.Suite
|
|
*require.Assertions
|
|
client dynamicconfig.Client
|
|
collection *dynamicconfig.Collection
|
|
doneCh chan any
|
|
}
|
|
|
|
func TestFileBasedClientSuite(t *testing.T) {
|
|
s := new(fileBasedClientSuite)
|
|
suite.Run(t, s)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) SetupSuite() {
|
|
var err error
|
|
s.doneCh = make(chan any)
|
|
logger := log.NewNoopLogger()
|
|
s.client, err = dynamicconfig.NewFileBasedClientWithMetrics(&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "config/testConfig.yaml",
|
|
PollInterval: time.Second * 5,
|
|
}, logger, s.doneCh, metrics.NoopMetricsHandler)
|
|
s.Require().NoError(err)
|
|
s.collection = dynamicconfig.NewCollection(s.client, logger)
|
|
s.collection.Start()
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TearDownSuite() {
|
|
s.collection.Stop()
|
|
close(s.doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) SetupTest() {
|
|
s.Assertions = require.New(s.T())
|
|
dynamicconfig.ResetRegistryForTest()
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetValue() {
|
|
cvs := s.client.GetValue(dynamicconfig.MakeKey(testGetBoolPropertyKey))
|
|
s.Equal(3, len(cvs))
|
|
s.ElementsMatch([]dynamicconfig.ConstrainedValue{
|
|
{Constraints: dynamicconfig.Constraints{}, Value: false},
|
|
{Constraints: dynamicconfig.Constraints{Namespace: "global-samples-namespace"}, Value: true},
|
|
{Constraints: dynamicconfig.Constraints{Namespace: "samples-namespace"}, Value: true},
|
|
}, cvs)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetValue_NonExistKey() {
|
|
cvs := s.client.GetValue(dynamicconfig.MakeKey(unknownKey))
|
|
s.Nil(cvs)
|
|
|
|
defaultValue := true
|
|
v := dynamicconfig.NewGlobalBoolSetting(unknownKey, defaultValue, "").Get(s.collection)()
|
|
s.Equal(defaultValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestNewFileBasedClientWithoutMetrics() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
logger := log.NewNoopLogger()
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
|
|
_, err := dynamicconfig.NewFileBasedClient(
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "config/testConfig.yaml",
|
|
PollInterval: time.Minute * 5,
|
|
}, logger, doneCh)
|
|
s.NoError(err)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetValue_CaseInsensitie() {
|
|
cvs := s.client.GetValue(dynamicconfig.MakeKey(testCaseInsensitivePropertyKey))
|
|
s.Equal(1, len(cvs))
|
|
|
|
v := dynamicconfig.NewGlobalBoolSetting(testCaseInsensitivePropertyKey, false, "").Get(s.collection)()
|
|
s.Equal(true, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue() {
|
|
v := dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 1, "").Get(s.collection)()
|
|
s.Equal(1000, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilterNotMatch() {
|
|
v := dynamicconfig.NewNamespaceIntSetting(testGetIntPropertyKey, 500, "").Get(s.collection)("samples-namespace")
|
|
s.Equal(1000, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_WrongType() {
|
|
defaultValue := 2000
|
|
v := dynamicconfig.NewNamespaceIntSetting(testGetIntPropertyKey, defaultValue, "").Get(s.collection)("global-samples-namespace")
|
|
s.Equal(defaultValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilteredByWorkflowTaskQueueInfo() {
|
|
expectedValue := 1001
|
|
v := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 0, "").Get(s.collection)(
|
|
"global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilteredByNoTaskTypeQueueInfo() {
|
|
expectedValue := 1003
|
|
v := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 0, "").Get(s.collection)(
|
|
// this is contrived, but simulates something that doesn't match workflow or activity
|
|
"global-samples-namespace", "test-tq", enumspb.TaskQueueType(3),
|
|
)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilteredByActivityTaskQueueInfo() {
|
|
expectedValue := 1002
|
|
v := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 0, "").Get(s.collection)(
|
|
"global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_ACTIVITY)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilteredByTaskQueueNameOnly() {
|
|
expectedValue := 1005
|
|
v := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 0, "").Get(s.collection)(
|
|
"some-other-namespace", "other-test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilterByTQ_NamespaceOnly() {
|
|
expectedValue := 1004
|
|
setting := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 0, "")
|
|
v := setting.Get(s.collection)("another-namespace", "test-tq", 0)
|
|
s.Equal(expectedValue, v)
|
|
expectedValue = 1005
|
|
v = setting.Get(s.collection)("another-namespace", "other-test-tq", 0)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilterByTQ_MatchFallback() {
|
|
// should return 1001 as the most specific match
|
|
setting := dynamicconfig.NewTaskQueueIntSetting(testGetIntPropertyKey, 1001, "")
|
|
v1 := setting.Get(s.collection)(
|
|
"global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
v2 := setting.WithDefault(0).Get(s.collection)(
|
|
"global-samples-namespace", "test-tq", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
s.Equal(v1, v2)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetIntValue_FilterByDestination() {
|
|
dc := dynamicconfig.NewDestinationIntSetting(testGetIntPropertyFilteredByDestinationKey, 5, "").Get(s.collection)
|
|
s.Equal(10, dc("foo", "bar"))
|
|
s.Equal(20, dc("test-namespace", "test-destination-1"))
|
|
s.Equal(30, dc("test-namespace", "random-destination"))
|
|
s.Equal(40, dc("random-namespace", "test-destination-1"))
|
|
s.Equal(50, dc("test-namespace", "test-destination-2"))
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetFloatValue() {
|
|
v := dynamicconfig.NewGlobalFloatSetting(testGetFloat64PropertyKey, 1, "").Get(s.collection)()
|
|
s.Equal(12.0, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetFloatValue_WrongType() {
|
|
defaultValue := 1.0
|
|
v := dynamicconfig.NewNamespaceFloatSetting(testGetFloat64PropertyKey, defaultValue, "").Get(s.collection)("samples-namespace")
|
|
s.Equal(defaultValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetBoolValue() {
|
|
v := dynamicconfig.NewGlobalBoolSetting(testGetBoolPropertyKey, true, "").Get(s.collection)()
|
|
s.Equal(false, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetStringValue() {
|
|
v := dynamicconfig.NewNamespaceStringSetting(testGetStringPropertyKey, "defaultString", "").Get(s.collection)("random-namespace")
|
|
s.Equal("constrained-string", v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetMapValue() {
|
|
var defaultVal map[string]any
|
|
v := dynamicconfig.NewGlobalMapSetting(testGetMapPropertyKey, defaultVal, "").Get(s.collection)()
|
|
expectedVal := map[string]any{
|
|
"key1": "1",
|
|
"key2": 1,
|
|
"key3": []any{
|
|
false,
|
|
map[string]any{
|
|
"key4": true,
|
|
"key5": 2.1,
|
|
},
|
|
},
|
|
}
|
|
s.Equal(expectedVal, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetTypedValue() {
|
|
type myStruct struct {
|
|
Number int
|
|
Days time.Duration
|
|
Inner struct {
|
|
Key1 float64
|
|
Key2 bool
|
|
}
|
|
UnsetInFile string
|
|
}
|
|
v := dynamicconfig.NewGlobalTypedSetting(testGetTypedPropertyKey, myStruct{UnsetInFile: "unset"}, "").Get(s.collection)()
|
|
expectedVal := myStruct{
|
|
Number: 23,
|
|
Days: 6 * 24 * time.Hour,
|
|
Inner: struct {
|
|
Key1 float64
|
|
Key2 bool
|
|
}{
|
|
Key1: 12345.0,
|
|
Key2: true,
|
|
},
|
|
UnsetInFile: "unset", // note this value comes from the default
|
|
}
|
|
s.Equal(expectedVal, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetMapValue_WrongType() {
|
|
var defaultVal map[string]any
|
|
v := dynamicconfig.NewNamespaceMapSetting(testGetMapPropertyKey, defaultVal, "").Get(s.collection)("random-namespace")
|
|
s.Equal(defaultVal, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue() {
|
|
v := dynamicconfig.NewGlobalDurationSetting(testGetDurationPropertyKey, time.Second, "").Get(s.collection)()
|
|
s.Equal(time.Minute, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue_DefaultSeconds() {
|
|
v := dynamicconfig.NewNamespaceDurationSetting(testGetDurationPropertyKey, time.Second, "").Get(s.collection)("samples-namespace")
|
|
s.Equal(2*time.Second, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue_NotStringRepresentation() {
|
|
v := dynamicconfig.NewNamespaceDurationSetting(testGetDurationPropertyKey, time.Second, "").Get(s.collection)("broken-namespace")
|
|
s.Equal(time.Second, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue_ParseFailed() {
|
|
v := dynamicconfig.NewTaskQueueDurationSetting(testGetDurationPropertyKey, time.Second, "").Get(s.collection)(
|
|
"samples-namespace", "longIdleTimeTaskqueue", enumspb.TASK_QUEUE_TYPE_WORKFLOW)
|
|
s.Equal(time.Second, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue_FilteredByTaskTypeQueue() {
|
|
expectedValue := time.Second * 10
|
|
setting := dynamicconfig.NewTaskTypeDurationSetting(testGetDurationPropertyFilteredByTaskTypeKey, 0, "")
|
|
v := setting.Get(s.collection)(enumsspb.TASK_TYPE_ACTIVITY_RETRY_TIMER)
|
|
s.Equal(expectedValue, v)
|
|
v = setting.Get(s.collection)(enumsspb.TASK_TYPE_REPLICATION_HISTORY)
|
|
s.Equal(expectedValue, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetDurationValue_FilteredByChasmTaskType() {
|
|
setting := dynamicconfig.NewChasmTaskTypeDurationSetting(testGetDurationPropertyFilteredByChasmTaskTypeKey, 0, "")
|
|
v := setting.Get(s.collection)("activity.dispatch")
|
|
s.Equal(30*time.Second, v)
|
|
v = setting.Get(s.collection)("callback.invoke")
|
|
s.Equal(24*time.Hour, v)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestValidateConfig_NilLogger() {
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(gomock.NewController(s.T()))
|
|
_, err := dynamicconfig.NewFileBasedClientWithReader(reader, &dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "config/testConfig.yaml",
|
|
PollInterval: time.Second * 5,
|
|
}, nil, doneCh, metrics.NoopMetricsHandler)
|
|
s.Error(err, "logger for dynamic config client is nil")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestValidateConfig_NilConfig() {
|
|
logger := log.NewNoopLogger()
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
_, err := dynamicconfig.NewFileBasedClientWithMetrics(nil, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.Error(err, "configuration for dynamic config client is nil")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestValidateConfig_NilReader() {
|
|
logger := log.NewNoopLogger()
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
_, err := dynamicconfig.NewFileBasedClientWithReader(nil, &dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "config/testConfig.yaml",
|
|
PollInterval: time.Second * 5,
|
|
}, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.Error(err, "file reader for dynamic config client is nil")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestValidateConfig_ShortPollInterval() {
|
|
logger := log.NewNoopLogger()
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
_, err := dynamicconfig.NewFileBasedClientWithMetrics(&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "config/testConfig.yaml",
|
|
PollInterval: time.Second,
|
|
}, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.Contains(err.Error(), "poll interval should be at least")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_ChangedValue() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
dynamicconfig.NewNamespaceFloatSetting(testGetFloat64PropertyKey, 0, "")
|
|
setting := dynamicconfig.NewNamespaceBoolSetting(testGetBoolPropertyKey, false, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
mockLogger := log.NewMockLogger(ctrl)
|
|
|
|
updateInterval := time.Minute * 5
|
|
originModTime := time.Now()
|
|
updatedModTime := originModTime.Add(updateInterval + time.Second)
|
|
|
|
originFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 12
|
|
constraints: {}
|
|
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
|
|
testGetBoolPropertyKey:
|
|
- value: false
|
|
constraints: {}
|
|
- value: true
|
|
constraints:
|
|
namespace: global-samples-namespace
|
|
- value: true
|
|
constraints:
|
|
namespace: samples-namespace
|
|
`)
|
|
updatedFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 13
|
|
constraints: {}
|
|
|
|
testGetIntPropertyKey:
|
|
- value: 2000
|
|
constraints: {}
|
|
|
|
testGetBoolPropertyKey:
|
|
- value: true
|
|
constraints: {}
|
|
- value: false
|
|
constraints:
|
|
namespace: global-samples-namespace
|
|
- value: true
|
|
constraints:
|
|
namespace: samples-namespace
|
|
`)
|
|
|
|
reader.EXPECT().GetModTime().Return(originModTime, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(originFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info(gomock.Any()).Times(6)
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, mockLogger, doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
c := dynamicconfig.NewCollection(client, mockLogger)
|
|
c.Start()
|
|
sub := setting.Subscribe(c)
|
|
|
|
val1 := make(chan bool, 1)
|
|
initial1, cancel1 := sub("global-samples-namespace", func(n bool) { val1 <- n })
|
|
s.True(initial1)
|
|
val2 := make(chan bool, 1)
|
|
initial2, cancel2 := sub("other-namespace", func(n bool) { val2 <- n })
|
|
s.False(initial2)
|
|
|
|
reader.EXPECT().GetModTime().Return(updatedModTime, nil)
|
|
reader.EXPECT().ReadFile().Return(updatedFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: { constraints: {} value: 12 } newValue: { constraints: {} value: 13 }", gomock.Any())
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetintpropertykey oldValue: { constraints: {} value: 1000 } newValue: { constraints: {} value: 2000 }", gomock.Any())
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetboolpropertykey oldValue: { constraints: {} value: false } newValue: { constraints: {} value: true }", gomock.Any())
|
|
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())
|
|
mockLogger.EXPECT().Info(gomock.Any())
|
|
s.NoError(client.Update())
|
|
|
|
s.False(<-val1)
|
|
s.True(<-val2)
|
|
cancel1()
|
|
cancel2()
|
|
c.Stop()
|
|
|
|
close(doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestNewFileBasedClientWithNilMetricsHandler() {
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
originFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 12.22
|
|
constraints: {}
|
|
`)
|
|
|
|
doneCh := make(chan any)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
mockLogger := log.NewMockLogger(ctrl)
|
|
fileModTime := time.Now().Add(-time.Minute * 5)
|
|
pollInterval := time.Minute * 5
|
|
mockLogger.EXPECT().Info(gomock.Any()).Times(2)
|
|
// warnings expected with nil metrics and unregistered key
|
|
mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
|
|
|
|
reader.EXPECT().GetModTime().Return(fileModTime, nil).Times(3)
|
|
reader.EXPECT().ReadFile().Return([]byte(originFileData), nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: pollInterval,
|
|
}, mockLogger, doneCh, nil)
|
|
s.NoError(err)
|
|
|
|
// set the metrics handler and shoul expect no warnings
|
|
captureHandler := metricstest.NewCaptureHandler()
|
|
capture := captureHandler.StartCapture()
|
|
defer captureHandler.StopCapture(capture)
|
|
client.SetMetricsHandler(captureHandler)
|
|
s.NoError(client.Update())
|
|
snapshot := capture.Snapshot()
|
|
s.Len(snapshot["dynamic_config_update_failure"], 1)
|
|
s.InDelta(float64(0), snapshot["dynamic_config_update_failure"][0].Value, 0)
|
|
close(doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_ChangedTypedValue() {
|
|
dynamicconfig.NewNamespaceTypedSetting("history.fakeRetryPolicy", retrypolicy.DefaultDefaultRetrySettings, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
mockLogger := log.NewMockLogger(ctrl)
|
|
|
|
updateInterval := time.Minute * 5
|
|
originModTime := time.Now()
|
|
updatedModTime := originModTime.Add(updateInterval + time.Second)
|
|
|
|
originFileData := []byte(`
|
|
history.fakeRetryPolicy:
|
|
- value:
|
|
InitialIntervalInSeconds: 1
|
|
MaximumIntervalCoefficient: 100.0
|
|
BackoffCoefficient: 3.0
|
|
MaximumAttempts: 0
|
|
`)
|
|
updatedFileData := []byte(`
|
|
history.fakeRetryPolicy:
|
|
- value:
|
|
InitialIntervalInSeconds: 3
|
|
MaximumIntervalCoefficient: 100.0
|
|
BackoffCoefficient: 2.0
|
|
MaximumAttempts: 0
|
|
`)
|
|
|
|
reader.EXPECT().GetModTime().Return(originModTime, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(originFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info(gomock.Any()).Times(2)
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, mockLogger, s.doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
reader.EXPECT().GetModTime().Return(updatedModTime, nil)
|
|
reader.EXPECT().ReadFile().Return(updatedFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: history.fakeretrypolicy 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())
|
|
mockLogger.EXPECT().Info(gomock.Any())
|
|
s.NoError(client.Update())
|
|
s.NoError(err)
|
|
close(doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_NewEntry() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
dynamicconfig.NewNamespaceFloatSetting(testGetFloat64PropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
mockLogger := log.NewMockLogger(ctrl)
|
|
|
|
updateInterval := time.Minute * 5
|
|
originModTime := time.Now()
|
|
updatedModTime := originModTime.Add(updateInterval + time.Second)
|
|
|
|
originFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 12
|
|
constraints: {}
|
|
`)
|
|
updatedFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 12
|
|
constraints: {}
|
|
- value: 22
|
|
constraints:
|
|
namespace: samples-namespace
|
|
|
|
testGetIntPropertyKey:
|
|
- value: 2000
|
|
constraints: {}
|
|
`)
|
|
|
|
reader.EXPECT().GetModTime().Return(originModTime, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(originFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: nil newValue: { constraints: {} value: 12 }", gomock.Any())
|
|
mockLogger.EXPECT().Info(gomock.Any())
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, mockLogger, s.doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
reader.EXPECT().GetModTime().Return(updatedModTime, nil)
|
|
reader.EXPECT().ReadFile().Return(updatedFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetfloat64propertykey oldValue: nil newValue: { constraints: {{Namespace:samples-namespace}} value: 22 }", gomock.Any())
|
|
mockLogger.EXPECT().Info("dynamic config changed for the key: testgetintpropertykey oldValue: nil newValue: { constraints: {} value: 2000 }", gomock.Any())
|
|
mockLogger.EXPECT().Info(gomock.Any())
|
|
s.NoError(client.Update())
|
|
s.NoError(err)
|
|
close(doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_ChangeOrder_ShouldNotWriteLog() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
dynamicconfig.NewNamespaceFloatSetting(testGetFloat64PropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
mockLogger := log.NewMockLogger(ctrl)
|
|
|
|
updateInterval := time.Minute * 5
|
|
originModTime := time.Now()
|
|
updatedModTime := originModTime.Add(updateInterval + time.Second)
|
|
|
|
originFileData := []byte(`
|
|
testGetFloat64PropertyKey:
|
|
- value: 12
|
|
constraints: {}
|
|
- value: 22
|
|
constraints:
|
|
namespace: samples-namespace
|
|
|
|
testGetIntPropertyKey:
|
|
- value: 2000
|
|
constraints: {}
|
|
`)
|
|
updatedFileData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 2000
|
|
constraints: {}
|
|
|
|
testGetFloat64PropertyKey:
|
|
- value: 22
|
|
constraints:
|
|
namespace: samples-namespace
|
|
- value: 12
|
|
constraints: {}
|
|
`)
|
|
|
|
reader.EXPECT().GetModTime().Return(originModTime, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(originFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info(gomock.Any()).Times(4)
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, mockLogger, s.doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
reader.EXPECT().GetModTime().Return(updatedModTime, nil)
|
|
reader.EXPECT().ReadFile().Return(updatedFileData, nil)
|
|
|
|
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
|
|
s.NoError(client.Update())
|
|
s.NoError(err)
|
|
close(doneCh)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_ReadFileFailShouldRetry() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
|
|
updateInterval := time.Minute * 5
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
|
|
fileData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
`)
|
|
|
|
// init: GetModTime called twice (validateStaticConfig + Update), ReadFile once
|
|
reader.EXPECT().GetModTime().Return(t1, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
// Second update: mod time advanced to t2, but ReadFile fails transiently
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error"))
|
|
s.Error(client.Update())
|
|
|
|
// Third update: same mod time t2.
|
|
// Since retryOnErr=true for ReadFile failures, lastCheckedTime is not advanced.
|
|
// t2.After(t1) is true → ReadFile is called again.
|
|
// If bug: t2.After(t2) is false → update silently skipped → mock expectation fails.
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
s.NoError(client.Update())
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_YamlParseErrorDoesNotRetry() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
|
|
// update interval is set to 5 minutes to postpone the goroutine from updating the config file
|
|
// and leave room for manual calls of update()
|
|
updateInterval := time.Minute * 5
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
|
|
goodData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
`)
|
|
// Invalid YAML that produces lr.Errors (decode error)
|
|
badData := []byte(`{
|
|
"matching.numTaskqueueReadPartitions": [
|
|
{
|
|
"value": 4
|
|
},
|
|
{
|
|
"constraints": {
|
|
"namespace": "cp-snapjoinerv2-staging.27ece"
|
|
},
|
|
"value": 16
|
|
},
|
|
{
|
|
"constraints": {
|
|
"namespace": "ejp-automation-api-prod.2bf53",
|
|
"taskQueueName": "Interpreter_DEFAULT",
|
|
"taskType": "Activity, Workflow"
|
|
},
|
|
"value": 8
|
|
}
|
|
]
|
|
}`)
|
|
|
|
// init: GetModTime called twice (validateStaticConfig + Update), ReadFile once
|
|
reader.EXPECT().GetModTime().Return(t1, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(goodData, nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
// Second update: mod time advanced to t2, ReadFile returns bad YAML (parse error)
|
|
// retryOnErr=false → lastCheckedTime advances to t2
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(badData, nil)
|
|
s.Error(client.Update())
|
|
|
|
// Third update: same mod time t2.
|
|
// Since retryOnErr=false for parse errors, lastCheckedTime was already set to t2.
|
|
// t2.After(t2) is false → update skipped, ReadFile is NOT called.
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
s.NoError(client.Update())
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_EmitsMetricOnFailure() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
captureHandler := metricstest.NewCaptureHandler()
|
|
|
|
updateInterval := time.Minute * 5
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
|
|
fileData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
`)
|
|
|
|
// init: GetModTime x2, ReadFile x1
|
|
reader.EXPECT().GetModTime().Return(t1, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, logger, doneCh, captureHandler)
|
|
s.NoError(err)
|
|
|
|
capture := captureHandler.StartCapture()
|
|
defer captureHandler.StopCapture(capture)
|
|
|
|
// Trigger a failing update
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error"))
|
|
s.Error(client.Update())
|
|
|
|
snapshot := capture.Snapshot()
|
|
s.Len(snapshot["dynamic_config_update_failure"], 1)
|
|
s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_EmitsGaugeMetricOnFailure() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
captureHandler := metricstest.NewCaptureHandler()
|
|
|
|
updateInterval := time.Minute * 5
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
t3 := t2.Add(time.Second)
|
|
|
|
fileData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
`)
|
|
|
|
// init: GetModTime x2, ReadFile x1
|
|
reader.EXPECT().GetModTime().Return(t1, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, logger, doneCh, captureHandler)
|
|
s.NoError(err)
|
|
|
|
capture := captureHandler.StartCapture()
|
|
defer captureHandler.StopCapture(capture)
|
|
|
|
// Trigger a failing update: gauge should be 1
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error"))
|
|
s.Error(client.Update())
|
|
|
|
snapshot := capture.Snapshot()
|
|
s.Len(snapshot["dynamic_config_update_failure"], 1)
|
|
s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0)
|
|
|
|
// Fix the problem and trigger a successful update: gauge should be 0
|
|
// lastCheckedTime is still t1 (retryOnErr=true on transient error), so t2 will trigger a read
|
|
reader.EXPECT().GetModTime().Return(t3, nil)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
s.NoError(client.Update())
|
|
|
|
snapshot = capture.Snapshot()
|
|
s.Len(snapshot["dynamic_config_update_failure"], 2)
|
|
s.InDelta(float64(0), snapshot["dynamic_config_update_failure"][1].Value, 0)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestUpdate_SetMetricsHandlerRecordsMetrics() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
captureHandler := metricstest.NewCaptureHandler()
|
|
|
|
updateInterval := time.Minute * 5
|
|
t1 := time.Now()
|
|
t2 := t1.Add(time.Second)
|
|
|
|
fileData := []byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 1000
|
|
constraints: {}
|
|
`)
|
|
|
|
// init: GetModTime x2, ReadFile x1; starts with NoopMetricsHandler so no captured metrics
|
|
reader.EXPECT().GetModTime().Return(t1, nil).Times(2)
|
|
reader.EXPECT().ReadFile().Return(fileData, nil)
|
|
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader,
|
|
&dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: updateInterval,
|
|
}, logger, doneCh, metrics.NoopMetricsHandler)
|
|
s.NoError(err)
|
|
|
|
// Inject the real metrics handler after construction (deferred injection pattern)
|
|
client.SetMetricsHandler(captureHandler)
|
|
|
|
capture := captureHandler.StartCapture()
|
|
defer captureHandler.StopCapture(capture)
|
|
|
|
// Trigger a failing update — metric should now be recorded via the injected handler
|
|
reader.EXPECT().GetModTime().Return(t2, nil)
|
|
reader.EXPECT().ReadFile().Return(nil, errors.New("transient read error"))
|
|
s.Error(client.Update())
|
|
|
|
snapshot := capture.Snapshot()
|
|
s.Len(snapshot["dynamic_config_update_failure"], 1)
|
|
s.InDelta(float64(1), snapshot["dynamic_config_update_failure"][0].Value, 0)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestWarnUnregisteredKey() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 2000
|
|
testGetFloat64PropertyKey:
|
|
- value: 22.222
|
|
`))
|
|
s.Empty(lr.Errors)
|
|
s.Equal(1, len(lr.Warnings))
|
|
s.ErrorContains(lr.Warnings[0], `unregistered key "testgetfloat64propertykey"`)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestWarnValidationInt() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
testGetIntPropertyKey:
|
|
- value: not a number
|
|
`))
|
|
s.Empty(lr.Errors)
|
|
s.Equal(1, len(lr.Warnings))
|
|
s.ErrorContains(lr.Warnings[0], `validation failed: key "testgetintpropertykey" value not a number: value type is not int`)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestWarnConstraint() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
testGetIntPropertyKey:
|
|
- value: 5005
|
|
constraints:
|
|
namespace: samples-namespace
|
|
`))
|
|
s.Empty(lr.Errors)
|
|
s.Equal(1, len(lr.Warnings))
|
|
s.ErrorContains(lr.Warnings[0], `constraint "namespace" isn't valid for dynamic config key "testgetintpropertykey"`)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestWarnMultiple() {
|
|
dynamicconfig.NewGlobalIntSetting(testGetIntPropertyKey, 0, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
unknownKey:
|
|
- value: "5d"
|
|
testGetIntPropertyKey:
|
|
- value: not a number
|
|
constraints:
|
|
namespace: samples-namespace
|
|
`))
|
|
s.Empty(lr.Errors)
|
|
s.Equal(3, len(lr.Warnings))
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestErrorYamlDecode() {
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`}}}}}}}}}`))
|
|
s.Equal(1, len(lr.Errors))
|
|
s.ErrorContains(lr.Errors[0], "decode error")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestErrorBadConstraint() {
|
|
dynamicconfig.NewNamespaceBoolSetting(testGetBoolPropertyKey, true, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
testGetBoolPropertyKey:
|
|
- value: false
|
|
constraints:
|
|
namespace: 35
|
|
`))
|
|
s.Equal(1, len(lr.Errors))
|
|
s.ErrorContains(lr.Errors[0], "namespace constraint must be string")
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestErrorBadConstraints() {
|
|
dynamicconfig.NewTaskQueueBoolSetting(testGetBoolPropertyKey, true, "")
|
|
|
|
lr := dynamicconfig.LoadYamlFile([]byte(`
|
|
testGetBoolPropertyKey:
|
|
- value: false
|
|
constraints:
|
|
taskQueueName: 22
|
|
taskType: invalid
|
|
- value: true
|
|
constraints:
|
|
color: blue
|
|
`))
|
|
found := 0
|
|
for _, err := range lr.Errors {
|
|
e := err.Error()
|
|
switch {
|
|
case strings.Contains(e, "taskQueueName constraint must be string"):
|
|
found++
|
|
case strings.Contains(e, "invalid is not a valid TaskQueueType"):
|
|
found++
|
|
case strings.Contains(e, `unknown constraint type "color"`):
|
|
found++
|
|
}
|
|
}
|
|
s.Equal(3, found)
|
|
}
|
|
|
|
func (s *fileBasedClientSuite) TestGetMetricsHandler_SetNilHandler() {
|
|
ctrl := gomock.NewController(s.T())
|
|
defer ctrl.Finish()
|
|
reader := dynamicconfig.NewMockFileReader(ctrl)
|
|
logger := log.NewNoopLogger()
|
|
doneCh := make(chan any)
|
|
defer close(doneCh)
|
|
reader.EXPECT().GetModTime().Return(time.Now(), nil).AnyTimes()
|
|
// Use a minimal valid YAML config to avoid parse errors
|
|
reader.EXPECT().ReadFile().Return([]byte("testGetIntPropertyKey:\n- value: 1000\n"), nil).AnyTimes()
|
|
client, err := dynamicconfig.NewFileBasedClientWithReader(reader, &dynamicconfig.FileBasedClientConfig{
|
|
Filepath: "anyValue",
|
|
PollInterval: time.Minute * 5,
|
|
}, logger, doneCh, nil)
|
|
s.NoError(err)
|
|
client.SetMetricsHandler(nil)
|
|
s.NoError(client.Update())
|
|
}
|