-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf: [do not deserialize json objects or update local cache if remote response has not changed from the previous one] (FF-2576) #56
base: main
Are you sure you want to change the base?
Changes from all commits
3bf13d7
2dff525
af3484f
9a73b70
7f62ae0
a166cd9
b6a2a76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,10 +5,11 @@ import "time" | |
const default_base_url = "https://fscdn.eppo.cloud/api" | ||
|
||
type Config struct { | ||
BaseUrl string | ||
SdkKey string | ||
AssignmentLogger IAssignmentLogger | ||
PollerInterval time.Duration | ||
BaseUrl string | ||
SdkKey string | ||
AssignmentLogger IAssignmentLogger | ||
PollerInterval time.Duration | ||
SkipDeserializeAndUpdateFlagConfigIfUnchanged bool // default false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want it enabled by default? |
||
} | ||
|
||
func (cfg *Config) validate() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,21 +9,22 @@ const CONFIG_ENDPOINT = "/flag-config/v1/config" | |
const BANDIT_ENDPOINT = "/flag-config/v1/bandits" | ||
|
||
type configurationRequestor struct { | ||
httpClient httpClient | ||
configStore *configurationStore | ||
httpClient HttpClientInterface | ||
configStore *configurationStore | ||
storedUFCResponseETag string | ||
deserializeCount int | ||
skipDeserializeAndUpdateFlagConfigIfUnchanged bool | ||
} | ||
|
||
func newConfigurationRequestor(httpClient httpClient, configStore *configurationStore) *configurationRequestor { | ||
func newConfigurationRequestor(httpClient HttpClientInterface, configStore *configurationStore, skipDeserializeAndUpdateFlagConfigIfUnchanged bool) *configurationRequestor { | ||
return &configurationRequestor{ | ||
httpClient: httpClient, | ||
configStore: configStore, | ||
httpClient: httpClient, | ||
configStore: configStore, | ||
deserializeCount: 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only for testing validation |
||
skipDeserializeAndUpdateFlagConfigIfUnchanged: skipDeserializeAndUpdateFlagConfigIfUnchanged, | ||
} | ||
} | ||
|
||
func (cr *configurationRequestor) IsAuthorized() bool { | ||
return !cr.httpClient.isUnauthorized | ||
} | ||
|
||
func (cr *configurationRequestor) FetchAndStoreConfigurations() { | ||
configuration, err := cr.fetchConfiguration() | ||
if err != nil { | ||
|
@@ -59,8 +60,19 @@ func (cr *configurationRequestor) fetchConfig() (configResponse, error) { | |
return configResponse{}, err | ||
} | ||
|
||
if cr.skipDeserializeAndUpdateFlagConfigIfUnchanged { | ||
if result.ETag == cr.storedUFCResponseETag { | ||
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use |
||
fmt.Println("[EppoSDK] Response has not changed, skipping deserialization and cache update.") | ||
// Returning an empty configResponse and an error to indicate that the response has not changed | ||
// which prevents the flag config from being updated with an empty configResponse | ||
return configResponse{}, fmt.Errorf("config response has not changed") | ||
} | ||
cr.storedUFCResponseETag = result.ETag | ||
} | ||
|
||
var response configResponse | ||
err = json.Unmarshal(result, &response) | ||
cr.deserializeCount++ | ||
err = json.Unmarshal([]byte(result.Body), &response) | ||
if err != nil { | ||
fmt.Println("Failed to unmarshal config response JSON", result) | ||
fmt.Println(err) | ||
|
@@ -83,7 +95,7 @@ func (cr *configurationRequestor) fetchBandits() (banditResponse, error) { | |
} | ||
|
||
var response banditResponse | ||
err = json.Unmarshal(result, &response) | ||
err = json.Unmarshal([]byte(result.Body), &response) | ||
if err != nil { | ||
fmt.Println("Failed to unmarshal bandit response JSON", result) | ||
fmt.Println(err) | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -10,11 +10,14 @@ import ( | |||||
|
||||||
const REQUEST_TIMEOUT_SECONDS = time.Duration(10 * time.Second) | ||||||
|
||||||
type HttpClientInterface interface { | ||||||
get(resource string) (HttpResponse, error) | ||||||
} | ||||||
|
||||||
type httpClient struct { | ||||||
baseUrl string | ||||||
sdkParams SDKParams | ||||||
isUnauthorized bool | ||||||
client *http.Client | ||||||
baseUrl string | ||||||
sdkParams SDKParams | ||||||
client *http.Client | ||||||
} | ||||||
|
||||||
type SDKParams struct { | ||||||
|
@@ -23,22 +26,26 @@ type SDKParams struct { | |||||
sdkVersion string | ||||||
} | ||||||
|
||||||
func newHttpClient(baseUrl string, client *http.Client, sdkParams SDKParams) *httpClient { | ||||||
type HttpResponse struct { | ||||||
Body string | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be
Suggested change
|
||||||
ETag string | ||||||
} | ||||||
|
||||||
func newHttpClient(baseUrl string, client *http.Client, sdkParams SDKParams) HttpClientInterface { | ||||||
var hc = &httpClient{ | ||||||
baseUrl: baseUrl, | ||||||
sdkParams: sdkParams, | ||||||
isUnauthorized: false, | ||||||
client: client, | ||||||
baseUrl: baseUrl, | ||||||
sdkParams: sdkParams, | ||||||
client: client, | ||||||
} | ||||||
return hc | ||||||
} | ||||||
|
||||||
func (hc *httpClient) get(resource string) ([]byte, error) { | ||||||
func (hc *httpClient) get(resource string) (HttpResponse, error) { | ||||||
url := hc.baseUrl + resource | ||||||
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
return HttpResponse{}, err // Return empty strings and the error | ||||||
} | ||||||
req.Header.Set("Content-Type", "application/json; charset=UTF-8") | ||||||
|
||||||
|
@@ -59,22 +66,25 @@ func (hc *httpClient) get(resource string) ([]byte, error) { | |||||
// error. | ||||||
// | ||||||
// We should almost never expect to see this condition be executed. | ||||||
return nil, err | ||||||
return HttpResponse{}, err // Return empty strings and the error | ||||||
} | ||||||
defer resp.Body.Close() | ||||||
|
||||||
if resp.StatusCode == 401 { | ||||||
hc.isUnauthorized = true | ||||||
return nil, fmt.Errorf("unauthorized access") | ||||||
return HttpResponse{}, fmt.Errorf("unauthorized access") // Return an error indicating unauthorized access | ||||||
} | ||||||
|
||||||
if resp.StatusCode >= 500 { | ||||||
return nil, fmt.Errorf("server error: %d", resp.StatusCode) | ||||||
return HttpResponse{}, fmt.Errorf("server error: %d", resp.StatusCode) // Handle server errors (status code > 500) | ||||||
} | ||||||
|
||||||
b, err := io.ReadAll(resp.Body) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("server error: unreadable body") | ||||||
return HttpResponse{}, fmt.Errorf("server error: unreadable body") // Return empty strings and the error | ||||||
} | ||||||
return b, nil | ||||||
|
||||||
return HttpResponse{ | ||||||
Body: string(b), | ||||||
ETag: resp.Header.Get("ETag"), // Capture the ETag header | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am learning that there are two types but The Eppo Fastly CDN seems to return Before we release this, Eppo's web server might need to be updated to return "strong" etags. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weak ETag is fine for our purposes as we don't care about byte-by-byte identity—we only need the json value to match. |
||||||
}, nil | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we want it configurable? Is there any reason for a user to ever disable it?