From 7a5c58051deff6891b45a59a988fe0f4cea745ac Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Mon, 28 Sep 2020 18:51:55 +0200 Subject: [PATCH 01/10] Add data governance related functionality --- LICENSE | 2 +- README.md | 4 +- samples/console/main.go | 2 +- v6/README.md | 4 +- v6/async_result.go | 45 +++-- v6/auto_polling_policy.go | 12 +- v6/auto_polling_policy_test.go | 6 +- v6/config_cache.go | 56 +------ v6/config_fetcher.go | 85 +++++++++- v6/config_fetcher_test.go | 275 ++++++++++++++++++++++++++++++- v6/config_parser.go | 20 ++- v6/config_parser_test.go | 2 +- v6/configcat_client.go | 26 ++- v6/configcat_client_test.go | 10 +- v6/constants.go | 21 +++ v6/go.mod | 2 +- v6/lazy_loading_policy.go | 12 +- v6/lazy_loading_policy_test.go | 6 +- v6/manual_polling_policy.go | 6 +- v6/manual_polling_policy_test.go | 4 +- v6/refresh_policy.go | 55 +++++-- v6/refresh_policy_factory.go | 12 +- v6/version.go | 2 +- 23 files changed, 518 insertions(+), 151 deletions(-) diff --git a/LICENSE b/LICENSE index 645d942..a9b82e3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 ConfigCat +Copyright (c) 2020 ConfigCat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index db1431f..04846e9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ConfigCat is a hosted feature fl ### 1. Install the package with `go` ```bash -go get github.com/configcat/go-sdk/v5 +go get github.com/configcat/go-sdk/v6 ``` ### 2. Go to Connect your application tab to get your *SDK Key*: @@ -26,7 +26,7 @@ go get github.com/configcat/go-sdk/v5 ### 3. Import the *ConfigCat* client package to your application ```go -import "github.com/configcat/go-sdk/v5" +import "github.com/configcat/go-sdk/v6" ``` ### 4. Create a *ConfigCat* client instance: diff --git a/samples/console/main.go b/samples/console/main.go index 333fdcc..ea8cd38 100644 --- a/samples/console/main.go +++ b/samples/console/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/configcat/go-sdk/v5" + "github.com/configcat/go-sdk/v6" ) func main() { diff --git a/v6/README.md b/v6/README.md index db1431f..04846e9 100644 --- a/v6/README.md +++ b/v6/README.md @@ -17,7 +17,7 @@ ConfigCat is a hosted feature fl ### 1. Install the package with `go` ```bash -go get github.com/configcat/go-sdk/v5 +go get github.com/configcat/go-sdk/v6 ``` ### 2. Go to Connect your application tab to get your *SDK Key*: @@ -26,7 +26,7 @@ go get github.com/configcat/go-sdk/v5 ### 3. Import the *ConfigCat* client package to your application ```go -import "github.com/configcat/go-sdk/v5" +import "github.com/configcat/go-sdk/v6" ``` ### 4. Create a *ConfigCat* client instance: diff --git a/v6/async_result.go b/v6/async_result.go index 0eee061..f0d5bd7 100644 --- a/v6/async_result.go +++ b/v6/async_result.go @@ -2,8 +2,6 @@ package configcat import ( "errors" - "sync" - "sync/atomic" "time" ) @@ -19,17 +17,13 @@ import ( // }) // go func() { async.Complete("success") }() type asyncResult struct { - state uint32 - completions []func(result interface{}) - done chan struct{} result interface{} *async - sync.RWMutex } // newAsyncResult initializes a new async object with result. func newAsyncResult() *asyncResult { - return &asyncResult{state: pending, completions: []func(result interface{}){}, done: make(chan struct{}), async: newAsync()} + return &asyncResult{async: newAsync()} } // asCompletedAsyncResult creates an already completed async object. @@ -67,20 +61,35 @@ func (asyncResult *asyncResult) applyThen(completion func(result interface{}) in return newAsyncResult } +// compose allows the chaining of the async operations after each other and subscribes a +// callback function which gets the operation result as argument and returns a new async object. +// Returns an AsyncResult object which returns a different result type. +// For example: +// async.compose(func(result interface{}) { +// newAsyncResult := newAsyncResult() +// +// DoSomethingAsynchronously(func(result interface{}) { +// newAsyncResult.complete(result) +// })) +// +// return newAsyncResult +// }) +func (asyncResult *asyncResult) compose(completion func(result interface{}) *asyncResult) *asyncResult { + newAsyncResult := newAsyncResult() + asyncResult.accept(func(result interface{}) { + newResult := completion(result) + newResult.accept(func(result interface{}) { + newAsyncResult.complete(result) + }) + }) + return newAsyncResult +} + // complete moves the async operation into the completed state. // Gets the result of the operation as argument. func (asyncResult *asyncResult) complete(result interface{}) { - if atomic.CompareAndSwapUint32(&asyncResult.state, pending, completed) { - asyncResult.result = result - asyncResult.async.complete() - close(asyncResult.done) - asyncResult.RLock() - defer asyncResult.RUnlock() - for _, comp := range asyncResult.completions { - comp(result) - } - } - asyncResult.completions = nil + asyncResult.result = result + asyncResult.async.complete() } // get blocks until the async operation is completed, diff --git a/v6/auto_polling_policy.go b/v6/auto_polling_policy.go index 09cadf3..44261ac 100644 --- a/v6/auto_polling_policy.go +++ b/v6/auto_polling_policy.go @@ -47,11 +47,11 @@ func AutoPollWithChangeListener( // newAutoPollingPolicy initializes a new autoPollingPolicy. func newAutoPollingPolicy( configFetcher configProvider, - store *configStore, + cache ConfigCache, logger Logger, autoPollConfig autoPollConfig) *autoPollingPolicy { policy := &autoPollingPolicy{ - configRefresher: configRefresher{configFetcher: configFetcher, store: store, logger: logger}, + configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}, autoPollInterval: autoPollConfig.autoPollInterval, init: newAsync(), initialized: no, @@ -69,7 +69,7 @@ func (policy *autoPollingPolicy) getConfigurationAsync() *asyncResult { } return policy.init.apply(func() interface{} { - return policy.store.get() + return policy.get() }) } @@ -103,9 +103,9 @@ func (policy *autoPollingPolicy) startPolling() { func (policy *autoPollingPolicy) poll() { policy.logger.Debugln("Polling the latest configuration.") response := policy.configFetcher.getConfigurationAsync().get().(fetchResponse) - cached := policy.store.get() + cached := policy.get() if response.isFetched() && cached != response.body { - policy.store.set(response.body) + policy.set(response.body) if policy.configChanged != nil { policy.configChanged() } @@ -118,5 +118,5 @@ func (policy *autoPollingPolicy) poll() { func (policy *autoPollingPolicy) readCache() *asyncResult { policy.logger.Debugln("Reading from cache.") - return asCompletedAsyncResult(policy.store.get()) + return asCompletedAsyncResult(policy.get()) } diff --git a/v6/auto_polling_policy_test.go b/v6/auto_polling_policy_test.go index e64ee03..6407e30 100644 --- a/v6/auto_polling_policy_test.go +++ b/v6/auto_polling_policy_test.go @@ -12,7 +12,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync(t *testing.T) { logger := DefaultLogger(LogLevelWarn) policy := newAutoPollingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, autoPollConfig{time.Second * 2, nil}, ) @@ -46,7 +46,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync_Fail(t *testing.T) { logger := DefaultLogger(LogLevelWarn) policy := newAutoPollingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, autoPollConfig{time.Second * 2, nil}, ) @@ -67,7 +67,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync_WithListener(t *testing.T) { defer close(c) policy := newAutoPollingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, AutoPollWithChangeListener( time.Second*2, diff --git a/v6/config_cache.go b/v6/config_cache.go index 4589c82..023454a 100644 --- a/v6/config_cache.go +++ b/v6/config_cache.go @@ -1,69 +1,29 @@ package configcat -import ( - "sync" -) - // ConfigCache is a cache API used to make custom cache implementations. type ConfigCache interface { // get reads the configuration from the cache. - Get() (string, error) + Get(key string) (string, error) // set writes the configuration into the cache. - Set(value string) error + Set(key string, value string) error } type inMemoryConfigCache struct { - value string -} - -// configStore is used to maintain the cached configuration. -type configStore struct { - cache ConfigCache - logger Logger - inMemoryValue string - sync.RWMutex -} - -func newConfigStore(log Logger, cache ConfigCache) *configStore { - return &configStore{cache: cache, logger: log} + store map[string]string } // newInMemoryConfigCache creates an in-memory cache implementation used to store the fetched configurations. func newInMemoryConfigCache() *inMemoryConfigCache { - return &inMemoryConfigCache{value: ""} + return &inMemoryConfigCache{store: make(map[string]string)} } // get reads the configuration from the cache. -func (cache *inMemoryConfigCache) Get() (string, error) { - return cache.value, nil +func (cache *inMemoryConfigCache) Get(key string) (string, error) { + return cache.store[key], nil } // set writes the configuration into the cache. -func (cache *inMemoryConfigCache) Set(value string) error { - cache.value = value +func (cache *inMemoryConfigCache) Set(key string, value string) error { + cache.store[key] = value return nil } - -// get reads the configuration. -func (store *configStore) get() string { - store.RLock() - defer store.RUnlock() - value, err := store.cache.Get() - if err != nil { - store.logger.Errorf("Reading from the cache failed, %s", err) - return store.inMemoryValue - } - - return value -} - -// set writes the configuration. -func (store *configStore) set(value string) { - store.Lock() - defer store.Unlock() - store.inMemoryValue = value - err := store.cache.Set(value) - if err != nil { - store.logger.Errorf("Saving into the cache failed, %s", err) - } -} diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index 0ba88f4..57dea43 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -11,23 +11,94 @@ type configProvider interface { type configFetcher struct { sdkKey, eTag, mode, baseUrl string + urlIsCustom bool + parser *configParser client *http.Client logger Logger } -func newConfigFetcher(sdkKey string, config ClientConfig) *configFetcher { - return &configFetcher{sdkKey: sdkKey, - mode: config.Mode.getModeIdentifier(), - baseUrl: config.BaseUrl, - logger: config.Logger, - client: &http.Client{Timeout: config.HttpTimeout, Transport: config.Transport}} +func newConfigFetcher(sdkKey string, config ClientConfig, parser *configParser) *configFetcher { + fetcher := &configFetcher{sdkKey: sdkKey, + mode: config.Mode.getModeIdentifier(), + parser: parser, + logger: config.Logger, + client: &http.Client{Timeout: config.HttpTimeout, Transport: config.Transport}} + + if len(config.BaseUrl) == 0 { + fetcher.urlIsCustom = false + fetcher.baseUrl = func() string { + if config.DataGovernance == Global { return globalBaseUrl} else { return euOnlyBaseUrl} + }() + } else { + fetcher.urlIsCustom = true + fetcher.baseUrl = config.BaseUrl + } + + return fetcher } func (fetcher *configFetcher) getConfigurationAsync() *asyncResult { + return fetcher.executeFetchAsync(2) +} + +func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult { + return fetcher.sendFetchRequestAsync().compose(func(result interface{}) *asyncResult { + fetchResponse, ok := result.(fetchResponse) + if !ok || !fetchResponse.isFetched() { + return asCompletedAsyncResult(result) + } + + rootNode, err := fetcher.parser.deserialize(fetchResponse.body) + if err != nil { + return asCompletedAsyncResult(fetchResponse) + } + + preferences, ok := rootNode[preferences].(map[string]interface{}) + if !ok { + return asCompletedAsyncResult(fetchResponse) + } + + newUrl, ok := preferences[preferencesUrl].(string) + if !ok || len(newUrl) == 0 || newUrl == fetcher.baseUrl { + return asCompletedAsyncResult(fetchResponse) + } + + redirect, ok := preferences[preferencesRedirect].(float64) + if !ok { + return asCompletedAsyncResult(fetchResponse) + } + + if fetcher.urlIsCustom && redirect != 2 { + return asCompletedAsyncResult(fetchResponse) + } + + fetcher.baseUrl = newUrl + if redirect == 0 { + return asCompletedAsyncResult(fetchResponse) + } else { + if redirect == 1 { + fetcher.logger.Warnln("Please check the data_governance parameter " + + "in the ConfigCatClient initialization. " + + "It should match the settings provided in " + + "https://app.configcat.com/organization/data-governance. " + + "If you are not allowed to view this page, ask your Organization's Admins " + + "for the correct setting.") + } + + if executionCount > 0 { + return fetcher.executeFetchAsync(executionCount - 1) + } + } + + return asCompletedAsyncResult(fetchResponse) + }) +} + +func (fetcher *configFetcher) sendFetchRequestAsync() *asyncResult { result := newAsyncResult() go func() { - request, requestError := http.NewRequest("GET", fetcher.baseUrl+"/configuration-files/"+fetcher.sdkKey+"/config_v4.json", nil) + request, requestError := http.NewRequest("GET", fetcher.baseUrl+"/configuration-files/"+fetcher.sdkKey+"/config_v5.json", nil) if requestError != nil { result.complete(fetchResponse{status: Failure}) return diff --git a/v6/config_fetcher_test.go b/v6/config_fetcher_test.go index 3114c84..eb035a5 100644 --- a/v6/config_fetcher_test.go +++ b/v6/config_fetcher_test.go @@ -1,12 +1,20 @@ package configcat import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" "testing" ) -func TestConfigFetcher_GetConfigurationJson(t *testing.T) { +const jsonTemplate = "{ \"p\": { \"u\": \"%s\", \"r\": %d }, \"f\": {} }" +const customCdnUrl = "https://custom-cdn.configcat.com" - fetcher := newConfigFetcher("PKDVCLf-Hq-h-kCzMp-L7Q/PaDVCFk9EpmD6sLpGLltTA", defaultConfig()) +func TestConfigFetcher_GetConfigurationJson(t *testing.T) { + fetcher := newConfigFetcher("PKDVCLf-Hq-h-kCzMp-L7Q/PaDVCFk9EpmD6sLpGLltTA", + defaultConfig(), newParser(DefaultLogger(LogLevelError))) response := fetcher.getConfigurationAsync().get().(fetchResponse) if !response.isFetched() { @@ -21,10 +29,271 @@ func TestConfigFetcher_GetConfigurationJson(t *testing.T) { } func TestConfigFetcher_GetConfigurationJson_Fail(t *testing.T) { - fetcher := newConfigFetcher("thisshouldnotexist", defaultConfig()) + fetcher := newConfigFetcher("thisshouldnotexist", defaultConfig(), + newParser(DefaultLogger(LogLevelError))) response := fetcher.getConfigurationAsync().get().(fetchResponse) if !response.isFailed() { t.Error("Expecting failed") } } + +func TestConfigFetcher_ShouldStayOnGivenUrl(t *testing.T) { + // Arrange + body := fmt.Sprintf(jsonTemplate, "https://fakeUrl", 0) + transport := newMockHttpTransport() + transport.enqueue(200, body) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body != result { + t.Error("same result expected") + } + + if len(transport.requests) != 1 { + t.Error("1 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } +} + +func TestConfigFetcher_ShouldStayOnSameUrlWithRedirect(t *testing.T) { + // Arrange + body := fmt.Sprintf(jsonTemplate, globalBaseUrl, 1) + transport := newMockHttpTransport() + transport.enqueue(200, body) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body != result { + t.Error("same result expected") + } + + if len(transport.requests) != 1 { + t.Error("1 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } +} + +func TestConfigFetcher_ShouldStayOnSameUrlEvenWhenForced(t *testing.T) { + // Arrange + body := fmt.Sprintf(jsonTemplate, globalBaseUrl, 2) + transport := newMockHttpTransport() + transport.enqueue(200, body) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body != result { + t.Error("same result expected") + } + + if len(transport.requests) != 1 { + t.Error("1 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } +} + +func TestConfigFetcher_ShouldRedirectToAnotherServer(t *testing.T) { + // Arrange + body1 := fmt.Sprintf(jsonTemplate, euOnlyBaseUrl, 1) + body2 := fmt.Sprintf(jsonTemplate, euOnlyBaseUrl, 0) + transport := newMockHttpTransport() + transport.enqueue(200, body1) + transport.enqueue(200, body2) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body2 != result { + t.Error("same result expected") + } + + if len(transport.requests) != 2 { + t.Error("2 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } + + if !strings.Contains(euOnlyBaseUrl, transport.requests[1].Host) { + t.Error(euOnlyBaseUrl + " does not contain " + transport.requests[1].Host) + } +} + +func TestConfigFetcher_ShouldRedirectToAnotherServerWhenForced(t *testing.T) { + // Arrange + body1 := fmt.Sprintf(jsonTemplate, euOnlyBaseUrl, 2) + body2 := fmt.Sprintf(jsonTemplate, euOnlyBaseUrl, 0) + transport := newMockHttpTransport() + transport.enqueue(200, body1) + transport.enqueue(200, body2) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body2 != result { + t.Error("same result expected") + } + + if len(transport.requests) != 2 { + t.Error("2 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } + + if !strings.Contains(euOnlyBaseUrl, transport.requests[1].Host) { + t.Error(euOnlyBaseUrl + " does not contain " + transport.requests[1].Host) + } +} + +func TestConfigFetcher_ShouldBreakRedirectLoop(t *testing.T) { + // Arrange + body1 := fmt.Sprintf(jsonTemplate, euOnlyBaseUrl, 1) + body2 := fmt.Sprintf(jsonTemplate, globalBaseUrl, 1) + transport := newMockHttpTransport() + transport.enqueue(200, body1) + transport.enqueue(200, body2) + transport.enqueue(200, body1) + + fetcher := createFetcher(transport, "") + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body1 != result { + t.Error("same result expected") + } + + if len(transport.requests) != 3 { + t.Error("3 request expected") + } + + if !strings.Contains(globalBaseUrl, transport.requests[0].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[0].Host) + } + + if !strings.Contains(euOnlyBaseUrl, transport.requests[1].Host) { + t.Error(euOnlyBaseUrl + " does not contain " + transport.requests[1].Host) + } + + if !strings.Contains(globalBaseUrl, transport.requests[2].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[2].Host) + } +} + +func TestConfigFetcher_ShouldRespectCustomUrlWhenNotForced(t *testing.T) { + // Arrange + body := fmt.Sprintf(jsonTemplate, globalBaseUrl, 1) + transport := newMockHttpTransport() + transport.enqueue(200, body) + + fetcher := createFetcher(transport, customCdnUrl) + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body != result { + t.Error("same result expected") + } + + if len(transport.requests) != 1 { + t.Error("1 request expected") + } + + if !strings.Contains(customCdnUrl, transport.requests[0].Host) { + t.Error(customCdnUrl + " does not contain " + transport.requests[0].Host) + } +} + +func TestConfigFetcher_ShouldNotRespectCustomUrlWhenForced(t *testing.T) { + // Arrange + body1 := fmt.Sprintf(jsonTemplate, globalBaseUrl, 2) + body2 := fmt.Sprintf(jsonTemplate, globalBaseUrl, 0) + transport := newMockHttpTransport() + transport.enqueue(200, body1) + transport.enqueue(200, body2) + + fetcher := createFetcher(transport, customCdnUrl) + + // Act + result := fetcher.getConfigurationAsync().get().(fetchResponse).body + + // Assert + if body2 != result { + t.Error("same result expected") + } + + if len(transport.requests) != 2 { + t.Error("1 request expected") + } + + if !strings.Contains(customCdnUrl, transport.requests[0].Host) { + t.Error(customCdnUrl + " does not contain " + transport.requests[0].Host) + } + + if !strings.Contains(globalBaseUrl, transport.requests[1].Host) { + t.Error(globalBaseUrl + " does not contain " + transport.requests[1].Host) + } +} + +func createFetcher(transport http.RoundTripper, url string) *configFetcher { + config := defaultConfig() + config.BaseUrl = url + config.Transport = transport + return newConfigFetcher("fakeKey", config, newParser(DefaultLogger(LogLevelWarn))) +} + +type mockHttpTransport struct { + requests []*http.Request + responses []*http.Response +} + +func newMockHttpTransport() *mockHttpTransport { + return &mockHttpTransport{} +} + +func (m *mockHttpTransport) RoundTrip(req *http.Request) (*http.Response, error) { + m.requests = append(m.requests, req) + + nextResponseInQueue := m.responses[0] + m.responses = m.responses[1:] + return nextResponseInQueue, nil +} + +func (m *mockHttpTransport) enqueue(statusCode int, body string) { + m.responses = append(m.responses, &http.Response{ + StatusCode: statusCode, + Body: ioutil.NopCloser(bytes.NewBufferString(body)), + }) +} diff --git a/v6/config_parser.go b/v6/config_parser.go index db67fed..ccd4f55 100644 --- a/v6/config_parser.go +++ b/v6/config_parser.go @@ -34,7 +34,7 @@ func (parser *configParser) parseVariationId(jsonBody string, key string, user * } func (parser *configParser) getAllKeys(jsonBody string) ([]string, error) { - rootNode, err := parser.deserialize(jsonBody) + rootNode, err := parser.getEntries(jsonBody) if err != nil { return nil, err } @@ -50,7 +50,7 @@ func (parser *configParser) getAllKeys(jsonBody string) ([]string, error) { } func (parser *configParser) parseKeyValue(jsonBody string, variationId string) (string, interface{}, error) { - rootNode, err := parser.deserialize(jsonBody) + rootNode, err := parser.getEntries(jsonBody) if err != nil { return "", nil, &parseError{"JSON parsing failed. " + err.Error() + "."} } @@ -87,7 +87,7 @@ func (parser *configParser) parseInternal(jsonBody string, key string, user *Use panic("Key cannot be empty") } - rootNode, err := parser.deserialize(jsonBody) + rootNode, err := parser.getEntries(jsonBody) if err != nil { return nil, "", &parseError{"JSON parsing failed. " + err.Error() + "."} } @@ -113,6 +113,20 @@ func (parser *configParser) parseInternal(jsonBody string, key string, user *Use return parsed, variationId, nil } +func (parser *configParser) getEntries(jsonBody string) (map[string]interface{}, error) { + rootNode, err := parser.deserialize(jsonBody) + if err != nil { + return nil, err + } + + entries, ok := rootNode[entries].(map[string]interface{}) + if !ok { + return nil, &parseError{"JSON mapping failed, json: " + jsonBody} + } + + return entries, nil +} + func (parser *configParser) deserialize(jsonBody string) (map[string]interface{}, error) { var root interface{} err := json.Unmarshal([]byte(jsonBody), &root) diff --git a/v6/config_parser_test.go b/v6/config_parser_test.go index 661d430..9bf1cd4 100644 --- a/v6/config_parser_test.go +++ b/v6/config_parser_test.go @@ -5,7 +5,7 @@ import ( ) func TestConfigParser_Parse(t *testing.T) { - jsonBody := "{ \"keyDouble\": { \"v\": 120.121238476, \"p\": [], \"r\": [], \"i\":\"\" }}" + jsonBody := "{ \"f\": { \"keyDouble\": { \"v\": 120.121238476, \"p\": [], \"r\": [], \"i\":\"\" }}}" parser := newParser(DefaultLogger(LogLevelWarn)) val, err := parser.parse(jsonBody, "keyDouble", nil) diff --git a/v6/configcat_client.go b/v6/configcat_client.go index 44fc7b9..d744ce7 100644 --- a/v6/configcat_client.go +++ b/v6/configcat_client.go @@ -8,7 +8,6 @@ import ( // Client is an object for handling configurations provided by ConfigCat. type Client struct { - store *configStore parser *configParser refreshPolicy refreshPolicy maxWaitTimeForSyncCalls time.Duration @@ -32,17 +31,20 @@ type ClientConfig struct { Transport http.RoundTripper // The refresh mode of the cached configuration. Mode RefreshMode + // The location from where the sdk gets the ConfigCat configuration. + DataGovernance DataGovernance } func defaultConfig() ClientConfig { return ClientConfig{ Logger: DefaultLogger(LogLevelWarn), - BaseUrl: "https://cdn.configcat.com", + BaseUrl: "", Cache: newInMemoryConfigCache(), MaxWaitTimeForSyncCalls: 0, HttpTimeout: time.Second * 15, Transport: http.DefaultTransport, Mode: AutoPoll(time.Second * 120), + DataGovernance: Global, } } @@ -71,10 +73,6 @@ func newInternal(sdkKey string, config ClientConfig, fetcher configProvider) *Cl config.Cache = defaultConfig.Cache } - if len(config.BaseUrl) == 0 { - config.BaseUrl = defaultConfig.BaseUrl - } - if config.MaxWaitTimeForSyncCalls < 0 { config.MaxWaitTimeForSyncCalls = defaultConfig.MaxWaitTimeForSyncCalls } @@ -91,15 +89,15 @@ func newInternal(sdkKey string, config ClientConfig, fetcher configProvider) *Cl config.Mode = defaultConfig.Mode } + parser := newParser(config.Logger) + if fetcher == nil { - fetcher = newConfigFetcher(sdkKey, config) + fetcher = newConfigFetcher(sdkKey, config, parser) } - store := newConfigStore(config.Logger, config.Cache) - - return &Client{store: store, - parser: newParser(config.Logger), - refreshPolicy: config.Mode.accept(newRefreshPolicyFactory(fetcher, store, config.Logger)), + return &Client{ + parser: parser, + refreshPolicy: config.Mode.accept(newRefreshPolicyFactory(fetcher, config.Cache, config.Logger)), maxWaitTimeForSyncCalls: config.MaxWaitTimeForSyncCalls, logger: config.Logger} } @@ -125,7 +123,7 @@ func (client *Client) GetValueForUser(key string, defaultValue interface{}, user json, err := client.refreshPolicy.getConfigurationAsync().getOrTimeout(client.maxWaitTimeForSyncCalls) if err != nil { client.logger.Errorf("Policy could not provide the configuration: %s", err.Error()) - return client.parseJson(client.store.get(), key, defaultValue, user) + return client.parseJson(client.refreshPolicy.getLastCachedConfig(), key, defaultValue, user) } return client.parseJson(json.(string), key, defaultValue, user) @@ -168,7 +166,7 @@ func (client *Client) GetVariationIdForUser(key string, defaultVariationId strin json, err := client.refreshPolicy.getConfigurationAsync().getOrTimeout(client.maxWaitTimeForSyncCalls) if err != nil { client.logger.Errorf("Policy could not provide the configuration: %s", err.Error()) - return client.parseVariationId(client.store.get(), key, defaultVariationId, user) + return client.parseVariationId(client.refreshPolicy.getLastCachedConfig(), key, defaultVariationId, user) } return client.parseVariationId(json.(string), key, defaultVariationId, user) diff --git a/v6/configcat_client_test.go b/v6/configcat_client_test.go index 2eddddb..d9f64c9 100644 --- a/v6/configcat_client_test.go +++ b/v6/configcat_client_test.go @@ -8,20 +8,20 @@ import ( ) const ( - jsonFormat = "{ \"%s\": { \"v\": %s, \"p\": [], \"r\": [] }}" - variationJsonFormat = "{ \"first\": { \"v\": false, \"p\": [], \"r\": [], \"i\":\"fakeIdFirst\" }, \"second\": { \"v\": true, \"p\": [], \"r\": [], \"i\":\"fakeIdSecond\" }}" + jsonFormat = "{ \"f\": { \"%s\": { \"v\": %s, \"p\": [], \"r\": [] }}}" + variationJsonFormat = "{ \"f\": { \"first\": { \"v\": false, \"p\": [], \"r\": [], \"i\":\"fakeIdFirst\" }, \"second\": { \"v\": true, \"p\": [], \"r\": [], \"i\":\"fakeIdSecond\" }}}" ) type FailingCache struct { } // get reads the configuration from the cache. -func (cache *FailingCache) Get() (string, error) { +func (cache *FailingCache) Get(key string) (string, error) { return "", errors.New("fake failing cache fails to get") } // set writes the configuration into the cache. -func (cache *FailingCache) Set(value string) error { +func (cache *FailingCache) Set(key string, value string) error { return errors.New("fake failing cache fails to set") } @@ -209,7 +209,7 @@ func TestClient_GetAllVariationIds(t *testing.T) { func TestClient_GetAllVariationIds_Empty(t *testing.T) { fetcher, client := getTestClients() - fetcher.SetResponse(fetchResponse{status: Fetched, body: fmt.Sprintf("{}")}) + fetcher.SetResponse(fetchResponse{status: Fetched, body: fmt.Sprintf("{ \"f\": {} }")}) client.Refresh() result, err := client.GetAllVariationIds() diff --git a/v6/constants.go b/v6/constants.go index 39844d7..e3ca024 100644 --- a/v6/constants.go +++ b/v6/constants.go @@ -12,6 +12,21 @@ const ( Failure fetchStatus = 2 ) +// DataGovernance describes the location from where the sdk gets the ConfigCat configuration. +type DataGovernance int + +const ( + // Global indicates that the sdk will use the global cdn servers to get the ConfigCat configuration. + Global DataGovernance = 0 + // EuOnly indicates that the sdk will use the EU cdn servers to get the ConfigCat configuration. + EuOnly DataGovernance = 1 +) + +const ( + globalBaseUrl = "https://cdn-global.configcat.com" + euOnlyBaseUrl = "https://cdn-eu.configcat.com" +) + const ( no = 0 yes = 1 @@ -24,6 +39,12 @@ const ( ) const ( + entries = "f" + preferences = "p" + + preferencesUrl = "u" + preferencesRedirect = "r" + settingValue = "v" settingType = "t" settingRolloutPercentageItems = "p" diff --git a/v6/go.mod b/v6/go.mod index 0cfda5a..beea054 100644 --- a/v6/go.mod +++ b/v6/go.mod @@ -1,4 +1,4 @@ -module github.com/configcat/go-sdk/v5 +module github.com/configcat/go-sdk/v6 go 1.13 diff --git a/v6/lazy_loading_policy.go b/v6/lazy_loading_policy.go index 874fb42..7fcf5e6 100644 --- a/v6/lazy_loading_policy.go +++ b/v6/lazy_loading_policy.go @@ -42,10 +42,10 @@ func LazyLoad(cacheInterval time.Duration, useAsyncRefresh bool) RefreshMode { // newLazyLoadingPolicy initializes a new lazyLoadingPolicy. func newLazyLoadingPolicy( configFetcher configProvider, - store *configStore, + cache ConfigCache, logger Logger, config lazyLoadConfig) *lazyLoadingPolicy { - return &lazyLoadingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, store: store, logger: logger}, + return &lazyLoadingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}, cacheInterval: config.cacheInterval, isFetching: no, initialized: no, @@ -79,7 +79,7 @@ func (policy *lazyLoadingPolicy) getConfigurationAsync() *asyncResult { policy.fetching = policy.fetch() } return policy.init.apply(func() interface{} { - return policy.store.get() + return policy.get() }) } @@ -95,11 +95,11 @@ func (policy *lazyLoadingPolicy) fetch() *asyncResult { defer atomic.StoreUint32(&policy.isFetching, no) response := result.(fetchResponse) - cached := policy.store.get() + cached := policy.get() fetched := response.isFetched() if fetched && response.body != cached { - policy.store.set(response.body) + policy.set(response.body) } if !response.isFailed() { @@ -120,5 +120,5 @@ func (policy *lazyLoadingPolicy) fetch() *asyncResult { func (policy *lazyLoadingPolicy) readCache() *asyncResult { policy.logger.Debugln("Reading from cache.") - return asCompletedAsyncResult(policy.store.get()) + return asCompletedAsyncResult(policy.get()) } diff --git a/v6/lazy_loading_policy_test.go b/v6/lazy_loading_policy_test.go index 3e208c6..835421c 100644 --- a/v6/lazy_loading_policy_test.go +++ b/v6/lazy_loading_policy_test.go @@ -12,7 +12,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_DoNotUseAsync(t *testing.T) { logger := DefaultLogger(LogLevelWarn) policy := newLazyLoadingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, lazyLoadConfig{time.Second * 2, false}) config := policy.getConfigurationAsync().get().(string) @@ -43,7 +43,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_Fail(t *testing.T) { logger := DefaultLogger(LogLevelWarn) policy := newLazyLoadingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, lazyLoadConfig{time.Second * 2, false}) config := policy.getConfigurationAsync().get().(string) @@ -60,7 +60,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_UseAsync(t *testing.T) { logger := DefaultLogger(LogLevelWarn) policy := newLazyLoadingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, lazyLoadConfig{time.Second * 2, true}) config := policy.getConfigurationAsync().get().(string) diff --git a/v6/manual_polling_policy.go b/v6/manual_polling_policy.go index 918cc33..bc5d5e7 100644 --- a/v6/manual_polling_policy.go +++ b/v6/manual_polling_policy.go @@ -24,15 +24,15 @@ func ManualPoll() RefreshMode { // newManualPollingPolicy initializes a new manualPollingPolicy. func newManualPollingPolicy( configFetcher configProvider, - store *configStore, + cache ConfigCache, logger Logger) *manualPollingPolicy { - return &manualPollingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, store: store, logger: logger}} + return &manualPollingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}} } // getConfigurationAsync reads the current configuration value. func (policy *manualPollingPolicy) getConfigurationAsync() *asyncResult { - return asCompletedAsyncResult(policy.store.get()) + return asCompletedAsyncResult(policy.get()) } // close shuts down the policy. diff --git a/v6/manual_polling_policy_test.go b/v6/manual_polling_policy_test.go index 7945595..46c9590 100644 --- a/v6/manual_polling_policy_test.go +++ b/v6/manual_polling_policy_test.go @@ -10,7 +10,7 @@ func TestManualPollingPolicy_GetConfigurationAsync(t *testing.T) { fetcher.SetResponse(fetchResponse{status: Fetched, body: "test"}) policy := newManualPollingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, ) @@ -36,7 +36,7 @@ func TestManualPollingPolicy_GetConfigurationAsync_Fail(t *testing.T) { fetcher.SetResponse(fetchResponse{status: Failure, body: ""}) policy := newManualPollingPolicy( fetcher, - newConfigStore(logger, newInMemoryConfigCache()), + newInMemoryConfigCache(), logger, ) config := policy.getConfigurationAsync().get().(string) diff --git a/v6/refresh_policy.go b/v6/refresh_policy.go index b699dfc..8d92436 100644 --- a/v6/refresh_policy.go +++ b/v6/refresh_policy.go @@ -1,39 +1,64 @@ package configcat -// refreshPolicy is the public interface of a refresh policy which's implementors should describe the configuration update rules. +import "sync" + +const ( + CacheKey = "config-v5" +) + type refreshPolicy interface { - // getConfigurationAsync reads the current configuration value. getConfigurationAsync() *asyncResult - // refreshAsync initiates a force refresh on the cached configuration. + getLastCachedConfig() string refreshAsync() *async - // close shuts down the policy. close() } -// configRefresher describes a configuration refresher, holds a shared implementation of the refreshAsync method on refreshPolicy. type configRefresher struct { - // The configuration provider implementation used to collect the latest configuration. configFetcher configProvider - // The configuration store used to maintain the cached configuration. - store *configStore - // The logger instance. - logger Logger + cache ConfigCache + logger Logger + inMemoryValue string + sync.RWMutex } -// RefreshMode is a base for refresh mode configurations. type RefreshMode interface { - // Returns the identifier sent in User-Agent by the config fetcher. getModeIdentifier() string - // Creates a refresh policy from refresh mode. accept(visitor pollingModeVisitor) refreshPolicy } -// refreshAsync initiates a force refresh on the cached configuration. func (refresher *configRefresher) refreshAsync() *async { return refresher.configFetcher.getConfigurationAsync().accept(func(result interface{}) { response := result.(fetchResponse) if result.(fetchResponse).isFetched() { - refresher.store.set(response.body) + refresher.set(response.body) } }) } + +func (refresher *configRefresher) getLastCachedConfig() string { + return refresher.inMemoryValue +} + +// get reads the configuration. +func (refresher *configRefresher) get() string { + refresher.RLock() + defer refresher.RUnlock() + value, err := refresher.cache.Get(CacheKey) + if err != nil { + refresher.logger.Errorf("Reading from the cache failed, %s", err) + return refresher.inMemoryValue + } + + return value +} + +// set writes the configuration. +func (refresher *configRefresher) set(value string) { + refresher.Lock() + defer refresher.Unlock() + refresher.inMemoryValue = value + err := refresher.cache.Set(CacheKey, value) + if err != nil { + refresher.logger.Errorf("Saving into the cache failed, %s", err) + } +} diff --git a/v6/refresh_policy_factory.go b/v6/refresh_policy_factory.go index a32af63..77ff94d 100644 --- a/v6/refresh_policy_factory.go +++ b/v6/refresh_policy_factory.go @@ -8,22 +8,22 @@ type pollingModeVisitor interface { type refreshPolicyFactory struct { configFetcher configProvider - store *configStore + cache ConfigCache logger Logger } -func newRefreshPolicyFactory(configFetcher configProvider, store *configStore, logger Logger) *refreshPolicyFactory { - return &refreshPolicyFactory{configFetcher: configFetcher, store: store, logger: logger} +func newRefreshPolicyFactory(configFetcher configProvider, cache ConfigCache, logger Logger) *refreshPolicyFactory { + return &refreshPolicyFactory{configFetcher: configFetcher, cache: cache, logger: logger} } func (factory *refreshPolicyFactory) visitAutoPoll(config autoPollConfig) refreshPolicy { - return newAutoPollingPolicy(factory.configFetcher, factory.store, factory.logger, config) + return newAutoPollingPolicy(factory.configFetcher, factory.cache, factory.logger, config) } func (factory *refreshPolicyFactory) visitManualPoll(config manualPollConfig) refreshPolicy { - return newManualPollingPolicy(factory.configFetcher, factory.store, factory.logger) + return newManualPollingPolicy(factory.configFetcher, factory.cache, factory.logger) } func (factory *refreshPolicyFactory) visitLazyLoad(config lazyLoadConfig) refreshPolicy { - return newLazyLoadingPolicy(factory.configFetcher, factory.store, factory.logger, config) + return newLazyLoadingPolicy(factory.configFetcher, factory.cache, factory.logger, config) } diff --git a/v6/version.go b/v6/version.go index ba9d3ee..f717c53 100644 --- a/v6/version.go +++ b/v6/version.go @@ -1,5 +1,5 @@ package configcat const ( - version = "5.1.0" + version = "6.0.0" ) From f1b76dc3cf445f41c4f4e28fc5e3d992048b9e42 Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Mon, 28 Sep 2020 18:55:45 +0200 Subject: [PATCH 02/10] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 918a8e6..1649bc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ go: - master before_script: -- cd v5 +- cd v6 - go vet ./... script: From c2ce95e1fc6e12cfebfc3b0f129b4407ac858e75 Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Mon, 28 Sep 2020 18:56:22 +0200 Subject: [PATCH 03/10] Update main.go --- samples/console/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/console/main.go b/samples/console/main.go index ea8cd38..333fdcc 100644 --- a/samples/console/main.go +++ b/samples/console/main.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/configcat/go-sdk/v6" + "github.com/configcat/go-sdk/v5" ) func main() { From 3330799cc380bb23ffa6b6fdf000ef961122204a Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Mon, 28 Sep 2020 19:00:04 +0200 Subject: [PATCH 04/10] gofmt --- v6/async_result.go | 2 +- v6/config_fetcher.go | 18 +++++++++++------- v6/config_fetcher_test.go | 2 +- v6/config_parser.go | 2 +- v6/configcat_client.go | 2 +- v6/configcat_client_test.go | 4 ++-- v6/constants.go | 24 ++++++++++++------------ v6/rollout_evaluator.go | 2 +- v6/rollout_integration_test.go | 2 +- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/v6/async_result.go b/v6/async_result.go index f0d5bd7..33a40c0 100644 --- a/v6/async_result.go +++ b/v6/async_result.go @@ -17,7 +17,7 @@ import ( // }) // go func() { async.Complete("success") }() type asyncResult struct { - result interface{} + result interface{} *async } diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index 57dea43..11a5fef 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -11,7 +11,7 @@ type configProvider interface { type configFetcher struct { sdkKey, eTag, mode, baseUrl string - urlIsCustom bool + urlIsCustom bool parser *configParser client *http.Client logger Logger @@ -19,15 +19,19 @@ type configFetcher struct { func newConfigFetcher(sdkKey string, config ClientConfig, parser *configParser) *configFetcher { fetcher := &configFetcher{sdkKey: sdkKey, - mode: config.Mode.getModeIdentifier(), - parser: parser, - logger: config.Logger, - client: &http.Client{Timeout: config.HttpTimeout, Transport: config.Transport}} + mode: config.Mode.getModeIdentifier(), + parser: parser, + logger: config.Logger, + client: &http.Client{Timeout: config.HttpTimeout, Transport: config.Transport}} if len(config.BaseUrl) == 0 { fetcher.urlIsCustom = false fetcher.baseUrl = func() string { - if config.DataGovernance == Global { return globalBaseUrl} else { return euOnlyBaseUrl} + if config.DataGovernance == Global { + return globalBaseUrl + } else { + return euOnlyBaseUrl + } }() } else { fetcher.urlIsCustom = true @@ -78,7 +82,7 @@ func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult } else { if redirect == 1 { fetcher.logger.Warnln("Please check the data_governance parameter " + - "in the ConfigCatClient initialization. " + + "in the ConfigCatClient initialization. " + "It should match the settings provided in " + "https://app.configcat.com/organization/data-governance. " + "If you are not allowed to view this page, ask your Organization's Admins " + diff --git a/v6/config_fetcher_test.go b/v6/config_fetcher_test.go index eb035a5..0254676 100644 --- a/v6/config_fetcher_test.go +++ b/v6/config_fetcher_test.go @@ -294,6 +294,6 @@ func (m *mockHttpTransport) RoundTrip(req *http.Request) (*http.Response, error) func (m *mockHttpTransport) enqueue(statusCode int, body string) { m.responses = append(m.responses, &http.Response{ StatusCode: statusCode, - Body: ioutil.NopCloser(bytes.NewBufferString(body)), + Body: ioutil.NopCloser(bytes.NewBufferString(body)), }) } diff --git a/v6/config_parser.go b/v6/config_parser.go index ccd4f55..4aaa044 100644 --- a/v6/config_parser.go +++ b/v6/config_parser.go @@ -79,7 +79,7 @@ func (parser *configParser) parseKeyValue(jsonBody string, variationId string) ( } } - return "", nil, &parseError{"JSON parsing failed." } + return "", nil, &parseError{"JSON parsing failed."} } func (parser *configParser) parseInternal(jsonBody string, key string, user *User) (interface{}, string, error) { diff --git a/v6/configcat_client.go b/v6/configcat_client.go index d744ce7..7d06aac 100644 --- a/v6/configcat_client.go +++ b/v6/configcat_client.go @@ -44,7 +44,7 @@ func defaultConfig() ClientConfig { HttpTimeout: time.Second * 15, Transport: http.DefaultTransport, Mode: AutoPoll(time.Second * 120), - DataGovernance: Global, + DataGovernance: Global, } } diff --git a/v6/configcat_client_test.go b/v6/configcat_client_test.go index d9f64c9..1de3575 100644 --- a/v6/configcat_client_test.go +++ b/v6/configcat_client_test.go @@ -8,7 +8,7 @@ import ( ) const ( - jsonFormat = "{ \"f\": { \"%s\": { \"v\": %s, \"p\": [], \"r\": [] }}}" + jsonFormat = "{ \"f\": { \"%s\": { \"v\": %s, \"p\": [], \"r\": [] }}}" variationJsonFormat = "{ \"f\": { \"first\": { \"v\": false, \"p\": [], \"r\": [], \"i\":\"fakeIdFirst\" }, \"second\": { \"v\": true, \"p\": [], \"r\": [], \"i\":\"fakeIdSecond\" }}}" ) @@ -251,4 +251,4 @@ func TestClient_GetKeyAndValue_Empty(t *testing.T) { if value != nil { t.Error("Expecting nil value") } -} \ No newline at end of file +} diff --git a/v6/constants.go b/v6/constants.go index e3ca024..67cb53f 100644 --- a/v6/constants.go +++ b/v6/constants.go @@ -39,25 +39,25 @@ const ( ) const ( - entries = "f" + entries = "f" preferences = "p" - preferencesUrl = "u" + preferencesUrl = "u" preferencesRedirect = "r" - settingValue = "v" - settingType = "t" + settingValue = "v" + settingType = "t" settingRolloutPercentageItems = "p" - settingRolloutRules = "r" - settingVariationId = "i" + settingRolloutRules = "r" + settingVariationId = "i" - rolloutValue = "v" + rolloutValue = "v" rolloutComparisonAttribute = "a" - rolloutComparator = "t" - rolloutComparisonValue = "c" - rolloutVariationId = "i" + rolloutComparator = "t" + rolloutComparisonValue = "c" + rolloutVariationId = "i" - percentageItemValue = "v" - percentageItemPercentage = "p" + percentageItemValue = "v" + percentageItemPercentage = "p" percentageItemVariationId = "i" ) diff --git a/v6/rollout_evaluator.go b/v6/rollout_evaluator.go index 5007b68..ec707be 100644 --- a/v6/rollout_evaluator.go +++ b/v6/rollout_evaluator.go @@ -288,4 +288,4 @@ func (evaluator *rolloutEvaluator) extractVariationId(variationId interface{}) s return "" } return result -} \ No newline at end of file +} diff --git a/v6/rollout_integration_test.go b/v6/rollout_integration_test.go index 402d078..aadfe6e 100644 --- a/v6/rollout_integration_test.go +++ b/v6/rollout_integration_test.go @@ -13,7 +13,7 @@ import ( ) const ( - valueKind = 0 + valueKind = 0 variationKind = 1 ) From ae1fc05e1e08e01102689242d9252084aafabeb3 Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Mon, 28 Sep 2020 19:32:24 +0200 Subject: [PATCH 05/10] DataGovernance parameter messages --- v6/config_fetcher.go | 10 ++++------ v6/configcat_client.go | 4 +++- v6/constants.go | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index 11a5fef..5038542 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -81,12 +81,10 @@ func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult return asCompletedAsyncResult(fetchResponse) } else { if redirect == 1 { - fetcher.logger.Warnln("Please check the data_governance parameter " + - "in the ConfigCatClient initialization. " + - "It should match the settings provided in " + - "https://app.configcat.com/organization/data-governance. " + - "If you are not allowed to view this page, ask your Organization's Admins " + - "for the correct setting.") + fetcher.logger.Warnln("Your config.DataGovernance parameter at ConfigCatClient " + + "initialization is not in sync with your preferences on the ConfigCat " + + "Dashboard: https://app.configcat.com/organization/data-governance. " + + "Only Organization Admins can set this preference.") } if executionCount > 0 { diff --git a/v6/configcat_client.go b/v6/configcat_client.go index 7d06aac..d0d75f3 100644 --- a/v6/configcat_client.go +++ b/v6/configcat_client.go @@ -31,7 +31,9 @@ type ClientConfig struct { Transport http.RoundTripper // The refresh mode of the cached configuration. Mode RefreshMode - // The location from where the sdk gets the ConfigCat configuration. + // Set this parameter to restrict the location of your feature flag and setting data within the ConfigCat CDN. + // This parameter must be in sync with the preferences on: https://app.configcat.com/organization/data-governance + // (Only Organization Admins can set this preference.) DataGovernance DataGovernance } diff --git a/v6/constants.go b/v6/constants.go index 67cb53f..ccb2587 100644 --- a/v6/constants.go +++ b/v6/constants.go @@ -12,13 +12,13 @@ const ( Failure fetchStatus = 2 ) -// DataGovernance describes the location from where the sdk gets the ConfigCat configuration. +// DataGovernance describes the location of your feature flag and setting data within the ConfigCat CDN. type DataGovernance int const ( - // Global indicates that the sdk will use the global cdn servers to get the ConfigCat configuration. + // Global indicates that your data will be published to all ConfigCat CDN nodes to guarantee lowest response times. Global DataGovernance = 0 - // EuOnly indicates that the sdk will use the EU cdn servers to get the ConfigCat configuration. + // EuOnly indicates that your data will be published to CDN nodes only in the EU. EuOnly DataGovernance = 1 ) From dbe2affb0967b83101900bc189142cdd6ac18d2f Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Tue, 29 Sep 2020 12:46:46 +0200 Subject: [PATCH 06/10] Use hashed sdk key in cache key --- v6/auto_polling_policy.go | 3 ++- v6/auto_polling_policy_test.go | 3 +++ v6/configcat_client.go | 2 +- v6/configcat_client_test.go | 27 ++++++++++++++++++++++++++- v6/go.mod | 1 + v6/go.sum | 2 ++ v6/lazy_loading_policy.go | 3 ++- v6/lazy_loading_policy_test.go | 3 +++ v6/manual_polling_policy.go | 5 +++-- v6/manual_polling_policy_test.go | 2 ++ v6/refresh_policy.go | 21 +++++++++++++++++---- v6/refresh_policy_factory.go | 11 ++++++----- 12 files changed, 68 insertions(+), 15 deletions(-) diff --git a/v6/auto_polling_policy.go b/v6/auto_polling_policy.go index 44261ac..ffd250b 100644 --- a/v6/auto_polling_policy.go +++ b/v6/auto_polling_policy.go @@ -49,9 +49,10 @@ func newAutoPollingPolicy( configFetcher configProvider, cache ConfigCache, logger Logger, + sdkKey string, autoPollConfig autoPollConfig) *autoPollingPolicy { policy := &autoPollingPolicy{ - configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}, + configRefresher: newConfigRefresher(configFetcher, cache, logger, sdkKey), autoPollInterval: autoPollConfig.autoPollInterval, init: newAsync(), initialized: no, diff --git a/v6/auto_polling_policy_test.go b/v6/auto_polling_policy_test.go index 6407e30..4755d3f 100644 --- a/v6/auto_polling_policy_test.go +++ b/v6/auto_polling_policy_test.go @@ -14,6 +14,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", autoPollConfig{time.Second * 2, nil}, ) defer policy.close() @@ -48,6 +49,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync_Fail(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", autoPollConfig{time.Second * 2, nil}, ) defer policy.close() @@ -69,6 +71,7 @@ func TestAutoPollingPolicy_GetConfigurationAsync_WithListener(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", AutoPollWithChangeListener( time.Second*2, func() { c <- true }, diff --git a/v6/configcat_client.go b/v6/configcat_client.go index d0d75f3..9b4fa28 100644 --- a/v6/configcat_client.go +++ b/v6/configcat_client.go @@ -99,7 +99,7 @@ func newInternal(sdkKey string, config ClientConfig, fetcher configProvider) *Cl return &Client{ parser: parser, - refreshPolicy: config.Mode.accept(newRefreshPolicyFactory(fetcher, config.Cache, config.Logger)), + refreshPolicy: config.Mode.accept(newRefreshPolicyFactory(fetcher, config.Cache, config.Logger, sdkKey)), maxWaitTimeForSyncCalls: config.MaxWaitTimeForSyncCalls, logger: config.Logger} } diff --git a/v6/configcat_client_test.go b/v6/configcat_client_test.go index 1de3575..9bc3f2a 100644 --- a/v6/configcat_client_test.go +++ b/v6/configcat_client_test.go @@ -25,6 +25,19 @@ func (cache *FailingCache) Set(key string, value string) error { return errors.New("fake failing cache fails to set") } +type KeyCheckerCache struct { + key string +} + +func (cache *KeyCheckerCache) Get(key string) (string, error) { + return "", nil +} + +func (cache *KeyCheckerCache) Set(key string, value string) error { + cache.key = key + return nil +} + func getTestClients() (*fakeConfigProvider, *Client) { config := ClientConfig{Mode: ManualPoll()} @@ -156,7 +169,6 @@ func TestClient_Get_WithFailingCache(t *testing.T) { } func TestClient_GetAllKeys(t *testing.T) { - client := NewClient("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A") keys, err := client.GetAllKeys() @@ -252,3 +264,16 @@ func TestClient_GetKeyAndValue_Empty(t *testing.T) { t.Error("Expecting nil value") } } + +func TestClient_EnsureCacheKeyHashIsSameOnAllPlatforms(t *testing.T) { + config := defaultConfig() + cache := &KeyCheckerCache{} + config.Cache = cache + client := NewCustomClient("PKDVCLf-Hq-h-kCzMp-L7Q/PaDVCFk9EpmD6sLpGLltTA", config) + + client.Refresh() + + if cache.key != "config-v5-1oi96ci" { + t.Error("config-v5-1oi96ci cache key expected but it's: " + cache.key) + } +} diff --git a/v6/go.mod b/v6/go.mod index beea054..4de6d6b 100644 --- a/v6/go.mod +++ b/v6/go.mod @@ -5,4 +5,5 @@ go 1.13 require ( github.com/blang/semver v3.5.1+incompatible github.com/sirupsen/logrus v1.4.2 + github.com/spaolacci/murmur3 v1.1.0 ) diff --git a/v6/go.sum b/v6/go.sum index d35539e..177ca37 100644 --- a/v6/go.sum +++ b/v6/go.sum @@ -8,6 +8,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/v6/lazy_loading_policy.go b/v6/lazy_loading_policy.go index 7fcf5e6..3b81fea 100644 --- a/v6/lazy_loading_policy.go +++ b/v6/lazy_loading_policy.go @@ -44,8 +44,9 @@ func newLazyLoadingPolicy( configFetcher configProvider, cache ConfigCache, logger Logger, + sdkKey string, config lazyLoadConfig) *lazyLoadingPolicy { - return &lazyLoadingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}, + return &lazyLoadingPolicy{configRefresher: newConfigRefresher(configFetcher, cache, logger, sdkKey), cacheInterval: config.cacheInterval, isFetching: no, initialized: no, diff --git a/v6/lazy_loading_policy_test.go b/v6/lazy_loading_policy_test.go index 835421c..b80fadb 100644 --- a/v6/lazy_loading_policy_test.go +++ b/v6/lazy_loading_policy_test.go @@ -14,6 +14,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_DoNotUseAsync(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", lazyLoadConfig{time.Second * 2, false}) config := policy.getConfigurationAsync().get().(string) @@ -45,6 +46,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_Fail(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", lazyLoadConfig{time.Second * 2, false}) config := policy.getConfigurationAsync().get().(string) @@ -62,6 +64,7 @@ func TestLazyLoadingPolicy_GetConfigurationAsync_UseAsync(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", lazyLoadConfig{time.Second * 2, true}) config := policy.getConfigurationAsync().get().(string) diff --git a/v6/manual_polling_policy.go b/v6/manual_polling_policy.go index bc5d5e7..6adc7e6 100644 --- a/v6/manual_polling_policy.go +++ b/v6/manual_polling_policy.go @@ -25,9 +25,10 @@ func ManualPoll() RefreshMode { func newManualPollingPolicy( configFetcher configProvider, cache ConfigCache, - logger Logger) *manualPollingPolicy { + logger Logger, + sdkKey string) *manualPollingPolicy { - return &manualPollingPolicy{configRefresher: configRefresher{configFetcher: configFetcher, cache: cache, logger: logger}} + return &manualPollingPolicy{configRefresher: newConfigRefresher(configFetcher, cache, logger, sdkKey)} } // getConfigurationAsync reads the current configuration value. diff --git a/v6/manual_polling_policy_test.go b/v6/manual_polling_policy_test.go index 46c9590..894cbed 100644 --- a/v6/manual_polling_policy_test.go +++ b/v6/manual_polling_policy_test.go @@ -12,6 +12,7 @@ func TestManualPollingPolicy_GetConfigurationAsync(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", ) policy.refreshAsync().wait() @@ -38,6 +39,7 @@ func TestManualPollingPolicy_GetConfigurationAsync_Fail(t *testing.T) { fetcher, newInMemoryConfigCache(), logger, + "", ) config := policy.getConfigurationAsync().get().(string) diff --git a/v6/refresh_policy.go b/v6/refresh_policy.go index 8d92436..74a7ad5 100644 --- a/v6/refresh_policy.go +++ b/v6/refresh_policy.go @@ -1,9 +1,14 @@ package configcat -import "sync" +import ( + "fmt" + "strconv" + "sync" +) +import "github.com/spaolacci/murmur3" const ( - CacheKey = "config-v5" + CacheBase = "config-v5-%s" ) type refreshPolicy interface { @@ -18,6 +23,7 @@ type configRefresher struct { cache ConfigCache logger Logger inMemoryValue string + cacheKey string sync.RWMutex } @@ -26,6 +32,13 @@ type RefreshMode interface { accept(visitor pollingModeVisitor) refreshPolicy } +func newConfigRefresher(configFetcher configProvider, cache ConfigCache, logger Logger, sdkKey string) configRefresher { + hasher:= murmur3.New32WithSeed(104729) + _, _ = hasher.Write([]byte(sdkKey)) + cacheKey := fmt.Sprintf(CacheBase, strconv.FormatUint(uint64(hasher.Sum32()), 32)) + return configRefresher{configFetcher: configFetcher, cache: cache, logger: logger, cacheKey: cacheKey} +} + func (refresher *configRefresher) refreshAsync() *async { return refresher.configFetcher.getConfigurationAsync().accept(func(result interface{}) { response := result.(fetchResponse) @@ -43,7 +56,7 @@ func (refresher *configRefresher) getLastCachedConfig() string { func (refresher *configRefresher) get() string { refresher.RLock() defer refresher.RUnlock() - value, err := refresher.cache.Get(CacheKey) + value, err := refresher.cache.Get(refresher.cacheKey) if err != nil { refresher.logger.Errorf("Reading from the cache failed, %s", err) return refresher.inMemoryValue @@ -57,7 +70,7 @@ func (refresher *configRefresher) set(value string) { refresher.Lock() defer refresher.Unlock() refresher.inMemoryValue = value - err := refresher.cache.Set(CacheKey, value) + err := refresher.cache.Set(refresher.cacheKey, value) if err != nil { refresher.logger.Errorf("Saving into the cache failed, %s", err) } diff --git a/v6/refresh_policy_factory.go b/v6/refresh_policy_factory.go index 77ff94d..21deeb1 100644 --- a/v6/refresh_policy_factory.go +++ b/v6/refresh_policy_factory.go @@ -10,20 +10,21 @@ type refreshPolicyFactory struct { configFetcher configProvider cache ConfigCache logger Logger + sdkKey string } -func newRefreshPolicyFactory(configFetcher configProvider, cache ConfigCache, logger Logger) *refreshPolicyFactory { - return &refreshPolicyFactory{configFetcher: configFetcher, cache: cache, logger: logger} +func newRefreshPolicyFactory(configFetcher configProvider, cache ConfigCache, logger Logger, sdkKey string) *refreshPolicyFactory { + return &refreshPolicyFactory{configFetcher: configFetcher, cache: cache, logger: logger, sdkKey: sdkKey} } func (factory *refreshPolicyFactory) visitAutoPoll(config autoPollConfig) refreshPolicy { - return newAutoPollingPolicy(factory.configFetcher, factory.cache, factory.logger, config) + return newAutoPollingPolicy(factory.configFetcher, factory.cache, factory.logger, factory.sdkKey, config) } func (factory *refreshPolicyFactory) visitManualPoll(config manualPollConfig) refreshPolicy { - return newManualPollingPolicy(factory.configFetcher, factory.cache, factory.logger) + return newManualPollingPolicy(factory.configFetcher, factory.cache, factory.logger, factory.sdkKey) } func (factory *refreshPolicyFactory) visitLazyLoad(config lazyLoadConfig) refreshPolicy { - return newLazyLoadingPolicy(factory.configFetcher, factory.cache, factory.logger, config) + return newLazyLoadingPolicy(factory.configFetcher, factory.cache, factory.logger, factory.sdkKey, config) } From dce727eb2dc1dbbb0f34653dfbd12fef1bd69471 Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Tue, 29 Sep 2020 12:58:39 +0200 Subject: [PATCH 07/10] gofmt --- v6/refresh_policy.go | 6 +++--- v6/refresh_policy_factory.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/v6/refresh_policy.go b/v6/refresh_policy.go index 74a7ad5..d87cea1 100644 --- a/v6/refresh_policy.go +++ b/v6/refresh_policy.go @@ -2,10 +2,10 @@ package configcat import ( "fmt" + "github.com/spaolacci/murmur3" "strconv" "sync" ) -import "github.com/spaolacci/murmur3" const ( CacheBase = "config-v5-%s" @@ -23,7 +23,7 @@ type configRefresher struct { cache ConfigCache logger Logger inMemoryValue string - cacheKey string + cacheKey string sync.RWMutex } @@ -33,7 +33,7 @@ type RefreshMode interface { } func newConfigRefresher(configFetcher configProvider, cache ConfigCache, logger Logger, sdkKey string) configRefresher { - hasher:= murmur3.New32WithSeed(104729) + hasher := murmur3.New32WithSeed(104729) _, _ = hasher.Write([]byte(sdkKey)) cacheKey := fmt.Sprintf(CacheBase, strconv.FormatUint(uint64(hasher.Sum32()), 32)) return configRefresher{configFetcher: configFetcher, cache: cache, logger: logger, cacheKey: cacheKey} diff --git a/v6/refresh_policy_factory.go b/v6/refresh_policy_factory.go index 21deeb1..452da3e 100644 --- a/v6/refresh_policy_factory.go +++ b/v6/refresh_policy_factory.go @@ -10,7 +10,7 @@ type refreshPolicyFactory struct { configFetcher configProvider cache ConfigCache logger Logger - sdkKey string + sdkKey string } func newRefreshPolicyFactory(configFetcher configProvider, cache ConfigCache, logger Logger, sdkKey string) *refreshPolicyFactory { From 64d6449a371d9defdd8414cebce8828f99631e66 Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Wed, 30 Sep 2020 14:52:10 +0200 Subject: [PATCH 08/10] cache key, comments --- v6/config_fetcher.go | 6 ++++-- v6/configcat_client.go | 5 ++--- v6/configcat_client_test.go | 13 ------------- v6/constants.go | 4 ++-- v6/go.mod | 1 - v6/refresh_policy.go | 13 +++++++------ 6 files changed, 15 insertions(+), 27 deletions(-) diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index 5038542..288165c 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -5,6 +5,8 @@ import ( "net/http" ) +const ConfigJsonName = "config_v5" + type configProvider interface { getConfigurationAsync() *asyncResult } @@ -84,7 +86,7 @@ func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult fetcher.logger.Warnln("Your config.DataGovernance parameter at ConfigCatClient " + "initialization is not in sync with your preferences on the ConfigCat " + "Dashboard: https://app.configcat.com/organization/data-governance. " + - "Only Organization Admins can set this preference.") + "Only Organization Admins can access this preference.") } if executionCount > 0 { @@ -100,7 +102,7 @@ func (fetcher *configFetcher) sendFetchRequestAsync() *asyncResult { result := newAsyncResult() go func() { - request, requestError := http.NewRequest("GET", fetcher.baseUrl+"/configuration-files/"+fetcher.sdkKey+"/config_v5.json", nil) + request, requestError := http.NewRequest("GET", fetcher.baseUrl+"/configuration-files/"+fetcher.sdkKey+"/"+ConfigJsonName+".json", nil) if requestError != nil { result.complete(fetchResponse{status: Failure}) return diff --git a/v6/configcat_client.go b/v6/configcat_client.go index 9b4fa28..777fa27 100644 --- a/v6/configcat_client.go +++ b/v6/configcat_client.go @@ -31,9 +31,8 @@ type ClientConfig struct { Transport http.RoundTripper // The refresh mode of the cached configuration. Mode RefreshMode - // Set this parameter to restrict the location of your feature flag and setting data within the ConfigCat CDN. - // This parameter must be in sync with the preferences on: https://app.configcat.com/organization/data-governance - // (Only Organization Admins can set this preference.) + // Default: Global. Set this parameter to be in sync with the Data Governance preference on the Dashboard: + // https://app.configcat.com/organization/data-governance (Only Organization Admins have access) DataGovernance DataGovernance } diff --git a/v6/configcat_client_test.go b/v6/configcat_client_test.go index 9bc3f2a..585bba3 100644 --- a/v6/configcat_client_test.go +++ b/v6/configcat_client_test.go @@ -264,16 +264,3 @@ func TestClient_GetKeyAndValue_Empty(t *testing.T) { t.Error("Expecting nil value") } } - -func TestClient_EnsureCacheKeyHashIsSameOnAllPlatforms(t *testing.T) { - config := defaultConfig() - cache := &KeyCheckerCache{} - config.Cache = cache - client := NewCustomClient("PKDVCLf-Hq-h-kCzMp-L7Q/PaDVCFk9EpmD6sLpGLltTA", config) - - client.Refresh() - - if cache.key != "config-v5-1oi96ci" { - t.Error("config-v5-1oi96ci cache key expected but it's: " + cache.key) - } -} diff --git a/v6/constants.go b/v6/constants.go index ccb2587..ef88b8a 100644 --- a/v6/constants.go +++ b/v6/constants.go @@ -16,9 +16,9 @@ const ( type DataGovernance int const ( - // Global indicates that your data will be published to all ConfigCat CDN nodes to guarantee lowest response times. + // Global Select this if your feature flags are published to all global CDN nodes. Global DataGovernance = 0 - // EuOnly indicates that your data will be published to CDN nodes only in the EU. + // EuOnly Select this if your feature flags are published to CDN nodes only in the EU. EuOnly DataGovernance = 1 ) diff --git a/v6/go.mod b/v6/go.mod index 4de6d6b..beea054 100644 --- a/v6/go.mod +++ b/v6/go.mod @@ -5,5 +5,4 @@ go 1.13 require ( github.com/blang/semver v3.5.1+incompatible github.com/sirupsen/logrus v1.4.2 - github.com/spaolacci/murmur3 v1.1.0 ) diff --git a/v6/refresh_policy.go b/v6/refresh_policy.go index d87cea1..0e48822 100644 --- a/v6/refresh_policy.go +++ b/v6/refresh_policy.go @@ -1,14 +1,14 @@ package configcat import ( + "crypto/sha1" + "encoding/hex" "fmt" - "github.com/spaolacci/murmur3" - "strconv" "sync" ) const ( - CacheBase = "config-v5-%s" + CacheBase = "go_"+ ConfigJsonName +"_%s" ) type refreshPolicy interface { @@ -33,9 +33,10 @@ type RefreshMode interface { } func newConfigRefresher(configFetcher configProvider, cache ConfigCache, logger Logger, sdkKey string) configRefresher { - hasher := murmur3.New32WithSeed(104729) - _, _ = hasher.Write([]byte(sdkKey)) - cacheKey := fmt.Sprintf(CacheBase, strconv.FormatUint(uint64(hasher.Sum32()), 32)) + sha := sha1.New() + sha.Write([]byte(sdkKey)) + hash := hex.EncodeToString(sha.Sum(nil)) + cacheKey := fmt.Sprintf(CacheBase, hash) return configRefresher{configFetcher: configFetcher, cache: cache, logger: logger, cacheKey: cacheKey} } From 15dcc0794c78bf4cb8ff8047eb62e574f29162eb Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Wed, 30 Sep 2020 16:42:04 +0200 Subject: [PATCH 09/10] Redirect mode constants --- v6/config_fetcher.go | 14 ++++++++++---- v6/go.sum | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index 288165c..c3b3419 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -5,7 +5,13 @@ import ( "net/http" ) -const ConfigJsonName = "config_v5" +const ( + ConfigJsonName = "config_v5" + + NoRedirect = 0 + ShouldRedirect = 1 + ForceRedirect = 2 +) type configProvider interface { getConfigurationAsync() *asyncResult @@ -74,15 +80,15 @@ func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult return asCompletedAsyncResult(fetchResponse) } - if fetcher.urlIsCustom && redirect != 2 { + if fetcher.urlIsCustom && redirect != ForceRedirect { return asCompletedAsyncResult(fetchResponse) } fetcher.baseUrl = newUrl - if redirect == 0 { + if redirect == NoRedirect { return asCompletedAsyncResult(fetchResponse) } else { - if redirect == 1 { + if redirect == ShouldRedirect { fetcher.logger.Warnln("Your config.DataGovernance parameter at ConfigCatClient " + "initialization is not in sync with your preferences on the ConfigCat " + "Dashboard: https://app.configcat.com/organization/data-governance. " + diff --git a/v6/go.sum b/v6/go.sum index 177ca37..d35539e 100644 --- a/v6/go.sum +++ b/v6/go.sum @@ -8,8 +8,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= From 9d1f85729cb5b96e1e4c88949b786835af8350eb Mon Sep 17 00:00:00 2001 From: Peter Csajtai Date: Thu, 1 Oct 2020 12:17:15 +0200 Subject: [PATCH 10/10] Add missing error msg --- v6/config_fetcher.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v6/config_fetcher.go b/v6/config_fetcher.go index c3b3419..36406ff 100644 --- a/v6/config_fetcher.go +++ b/v6/config_fetcher.go @@ -100,6 +100,7 @@ func (fetcher *configFetcher) executeFetchAsync(executionCount int) *asyncResult } } + fetcher.logger.Errorln("Redirect loop during config.json fetch. Please contact support@configcat.com.") return asCompletedAsyncResult(fetchResponse) }) }