-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Add URL option for sampling strategies file #2519
Conversation
go.mod
Outdated
@@ -3,83 +3,57 @@ module github.com/jaegertracing/jaeger | |||
go 1.14 | |||
|
|||
require ( | |||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect |
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.
I do not believe this PR requires changes to dependencies. Please exclude the go.mod/go.sum files.
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.
I've synced my fork with the upstream master. Do I still need to exclude go.mod/go.sum files?
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.
if this PR does not require new dependencies, it should not be updating dependency file. Please do checkout master <file>
to get the originals.
@@ -22,9 +22,11 @@ import ( | |||
) | |||
|
|||
const ( | |||
// SamplingStrategiesFile contains the name of CLI opions for config file. | |||
// SamplingStrategiesFile contains the name of CLI options for config file. |
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.
// SamplingStrategiesFile contains the name of CLI options for config file. | |
// SamplingStrategiesFile contains the name of CLI option for config file. |
SamplingStrategiesFile = "sampling.strategies-file" | ||
samplingStrategiesReloadInterval = "sampling.strategies-reload-interval" | ||
// SamplingStrategiesURL contains the name of CLI option for config file URL. | ||
SamplingStrategiesURL = "sampling.strategies-url" |
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.
I think the intention of the ticket was to allow sampling.strategies-file
to contain a URL. A new flag seems redundant.
if err != nil { | ||
h.logger.Error("failed to load sampling strategies", zap.String("file", filePath), zap.Error(err)) | ||
return lastValue | ||
func (h *strategyStore) reloadSamplingStrategy(reloadFrom string, path string, lastValue string) string { |
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.
The caller of this function already does checks for file vs. url. I would pass a loader function here that abstracts the source of the new config, e.g. loader func () (string, error)
, and only deal with processing the new value.
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.
Yes, that's better. Was going to do the same but changed my mind later.
@@ -140,6 +191,28 @@ func loadStrategies(strategiesFile string) (*strategies, error) { | |||
return &strategies, nil | |||
} | |||
|
|||
func downloadStrategies(strategiesURL string) (*strategies, error) { | |||
resp, err := http.Get(strategiesURL) |
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.
this duplicates code from L112
@yurishkuro Thanks for the suggestions. I've made changes to the pull request |
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.
top-level question: should there be some local file fallback option if the URL is temporarily unavailable? Because if whichever service provides the strategy for download is down the collector would not be able to start.
if lastValue == newValue { | ||
return lastValue | ||
} | ||
if err = h.updateSamplingStrategy(currBytes); err != nil { | ||
h.logger.Error("failed to update sampling strategies from file", zap.Error(err)) | ||
if err := h.updateSamplingStrategy([]byte(newValue)); err != 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.
Consider if ([]byte, error)
would be more appropriate signature for the loader()
, to avoid casting back and forth.
if strategiesFile == "" { | ||
return nil, nil | ||
// Check if it's a URL. | ||
if ok := isURL(strategiesFile); ok { |
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.
there is an existing redundancy in the original source, please consolidate it. You should not need to check isURL twice in this source file.
How about falling back to default strategies similar to when |
Default strategy is fine. The difference with StrategiesFile not found is in that case it's ok to fail because it likely indicates a mis-configuration, whereas in case of HTTP server not available then failing is counterproductive. |
} | ||
|
||
func samplingStrategyLoader(strategiesFile string) strategyLoader { | ||
return func() ([]byte, error) { |
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.
nit: you only need to test strategiesFile once, and return different functors. Otherwise strategyLoader
is pointless, you could've just call the function to read stuff
if strategiesFile == "" {
return func() ([]byte, error) {
return []byte("null"), nil
}
}
if isURL(strategiesFile) {
return func() ([]byte, error) {
return downloadSamplingStrategies(strategiesFile)
}
}
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.
Oops, I was being silly. I get this, thanks.
return func() ([]byte, error) { | ||
if strategiesFile == "" { | ||
// Using "null" so that it un-marshals to nil pointer. | ||
return []byte("null"), 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.
may want to declare this as a constant
case <-h.ctx.Done(): | ||
return | ||
} | ||
} | ||
} | ||
|
||
func (h *strategyStore) reloadSamplingStrategyFile(filePath string, lastValue string) string { | ||
currBytes, err := ioutil.ReadFile(filepath.Clean(filePath)) | ||
func (h *strategyStore) reloadSamplingStrategy(loader strategyLoader, lastValue string) string { |
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.
func (h *strategyStore) reloadSamplingStrategy(loader strategyLoader, lastValue string) string { | |
func (h *strategyStore) reloadSamplingStrategy(loadFn strategyLoader, lastValue string) string { |
please check why the DCO test is failing:
You need to ensure full name in git matches full name in github |
This change will let user to provide a URL to download sampling strategies. Default strategy is the fallback option if the URL is temporarily unavailable. Signed-off-by: Deepak <sah.sslpu@gmail.com>
return func() ([]byte, error) { | ||
currBytes, err := ioutil.ReadFile(strategiesFile) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to open strategies file: %w", err) |
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.
return nil, fmt.Errorf("failed to open strategies file: %w", err) | |
return nil, fmt.Errorf("failed to read strategies file %s: %w", strategiesFile , err) |
@@ -31,6 +32,9 @@ import ( | |||
"github.com/jaegertracing/jaeger/thrift-gen/sampling" | |||
) | |||
|
|||
// null represents "null" JSON value. | |||
const null = "null" |
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.
I was rather thinking this:
const null = "null" | |
var nullJSON = []byte("null") |
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
if resp.StatusCode == http.StatusServiceUnavailable { | ||
return []byte("null"), 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.
return []byte("null"), nil | |
return nullJSON, nil |
|
||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
if resp.StatusCode == http.StatusServiceUnavailable { |
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.
this does not need to be nested inside if != ok, you can do this check first, then if != OK.
|
||
buf := new(bytes.Buffer) | ||
if _, err = buf.ReadFrom(resp.Body); err != nil { | ||
return nil, fmt.Errorf("failed to read sampling strategies from downloaded JSON: %w", err) |
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.
return nil, fmt.Errorf("failed to read sampling strategies from downloaded JSON: %w", err) | |
return nil, fmt.Errorf("failed to read sampling strategies HTTP response body: %w", err) |
// Using null so that it un-marshals to nil pointer. | ||
return []byte(null), 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.
this comment should be on the constant
// Using null so that it un-marshals to nil pointer. | |
return []byte(null), nil | |
return nullJSON, nil |
} | ||
|
||
func samplingStrategyLoader(strategiesFile string) strategyLoader { | ||
if strategiesFile == "" { |
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.
Shouldn't this be an error upon instantiation of strategyStore
?
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.
Didn't quite understand this one. Can you elaborate it further? 😅
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.
There is a constructor function NewStrategyStore. This struct cannot do anything useful if the strategiesFile is empty, correct? So we could just return an error from there if the param is empty. If that's not sufficient (e.g. because it still needs to return the default strategy), then we can still do that in the constructor and just not execute all the loading/reloading functions if the file name is blank. This way we don't have to keep checking if the file path is empty or not.
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.
I get it. Thanks for the explanation.
} | ||
|
||
return func() ([]byte, error) { | ||
currBytes, err := ioutil.ReadFile(strategiesFile) |
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.
you may get go-sec linter error here, wrap the file name:
currBytes, err := ioutil.ReadFile(strategiesFile) | |
currBytes, err := ioutil.ReadFile(filepath.Clean(strategiesFile)) |
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.
While you're at it, can you fix this line too? I.e. remove nolint
comment and use filepath.Clean
./cmd/query/app/static_handler.go:210: bytes, err := ioutil.ReadFile(uiConfig) /* nolint #nosec , this comes from an admin, not user */
This updates the logic for NewStrategyStore constructor to skip any loading/reloading logic to execute when StrategiesFile is empty. Signed-off-by: Deepak <sah.sslpu@gmail.com>
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.
Looks great! I left a couple comments around the tests, otherwise LGTM.
} | ||
|
||
func (h *strategyStore) autoUpdateStrategies(interval time.Duration, loader strategyLoader) { | ||
lastValue := string(nullJSON) |
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.
Technically, we could have already loaded something before calling this function, so lastValue would be different. But since it's a periodic check anyway, I think it's fine.
|
||
// update original strategies file with new probability of 0.9 | ||
newStr = strings.Replace(string(srcBytes), "0.8", "0.9", 1) | ||
require.NoError(t, ioutil.WriteFile(srcFile, []byte(newStr), 0644)) |
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.
I don't think it's a good idea for the test to override the fixture file. In L316 it is already copied to a temp file that can be overwritten.
Also, if you're testing with a mock HTTP server, why use file in the first place? You can simply replace the content returned by the mock server since you can control it.
@@ -293,6 +346,49 @@ func TestAutoUpdateStrategy(t *testing.T) { | |||
time.Sleep(1 * time.Millisecond) | |||
} | |||
assert.EqualValues(t, makeResponse(sampling.SamplingStrategyType_PROBABILISTIC, 0.9), *s) | |||
|
|||
// Test auto update strategy with URL option. |
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.
This seems like a pretty separate testing scenario, any reason why it cannot be moved into a new test function with appropriate name?
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.
Yes, it makes sense to move it to a separate test function.
func TestStrategyStore(t *testing.T) { | ||
_, err := NewStrategyStore(Options{StrategiesFile: "fileNotFound.json"}, zap.NewNop()) | ||
assert.EqualError(t, err, "failed to open strategies file: open fileNotFound.json: no such file or directory") | ||
assert.EqualError(t, err, "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") |
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.
I recommend using assert.Contains (as in L79) to check only for the text produced by our code, not the parts produced by stdlib functions (since they can change the text between Go versions).
This refactors the strategy store tests, moving tests with URL to separate test functions. Signed-off-by: Deepak <sah.sslpu@gmail.com>
Signed-off-by: Deepak <sah.sslpu@gmail.com>
_, err := NewStrategyStore(Options{StrategiesFile: "fileNotFound.json"}, zap.NewNop()) | ||
assert.EqualError(t, err, "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") | ||
assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") |
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.
assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json: open fileNotFound.json: no such file or directory") | |
assert.Contains(t, err.Error(), "failed to read strategies file fileNotFound.json") |
only include the string that we generate, not the stdlib portion
assert.Equal(t, string(strategiesJSON), value) | ||
|
||
// update original strategies with new probability of 0.9 | ||
strategiesJSON = []byte(` |
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.
This is not a good approach to change package variable in-place. Even though it's a var, the general assumption is that package-level vars are constants (in fact I would change its type to string to make a real constant).
An even better approach is to convert it into a function that takes the probability value and returns the full JSON string via Sprintf. Then I would also change makeMockServer to return (strategy *atomic.String, server *httptest.Server)
(using uber-go/atomic), so that you can manipulate the strategy externally, e.g.:
mockStrategy, server := mockStrategyServer()
...
mockStrategy.Store(strategiesJSON(0.9))
...
This updates the mockStrategyServer to return a mock strategy along with a mock server. The returned strategy can be used to update it externally. Also, changed package-level var strategiesJSON to a function to return strategies JSON with a given probability. Signed-off-by: Deepak <sah.sslpu@gmail.com>
please check the linter failures
|
Signed-off-by: Deepak <sah.sslpu@gmail.com>
Fixed the linter issues. |
Codecov Report
@@ Coverage Diff @@
## master #2519 +/- ##
=========================================
Coverage ? 95.31%
=========================================
Files ? 208
Lines ? 9281
Branches ? 0
=========================================
Hits ? 8846
Misses ? 356
Partials ? 79
Continue to review full report at Codecov.
|
Thank you @goku321 ! |
Which problem is this PR solving?
Short description of the changes