Skip to content
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

Merged
merged 9 commits into from
Oct 13, 2020

Conversation

goku321
Copy link
Contributor

@goku321 goku321 commented Sep 29, 2020

Which problem is this PR solving?

Short description of the changes

  • Added a URL option to support downloading a sampling strategies file from a url.

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
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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"
Copy link
Member

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 {
Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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

@goku321
Copy link
Contributor Author

goku321 commented Oct 1, 2020

@yurishkuro Thanks for the suggestions. I've made changes to the pull request

Copy link
Member

@yurishkuro yurishkuro left a 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.

plugin/sampling/strategystore/static/strategy_store.go Outdated Show resolved Hide resolved
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 {
Copy link
Member

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 {
Copy link
Member

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.

@goku321
Copy link
Contributor Author

goku321 commented Oct 4, 2020

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.

How about falling back to default strategies similar to when options.StrategiesFile is empty? Also, we can have a retry mechanism for URL option.

@yurishkuro
Copy link
Member

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) {
Copy link
Member

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)
    }
}

Copy link
Contributor Author

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
Copy link
Member

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (h *strategyStore) reloadSamplingStrategy(loader strategyLoader, lastValue string) string {
func (h *strategyStore) reloadSamplingStrategy(loadFn strategyLoader, lastValue string) string {

@yurishkuro
Copy link
Member

please check why the DCO test is failing:

Commit sha: 932f6e0, Author: Deepak, Committer: Deepak; Expected "Deepak sah.sslpu@gmail.com", but got "Deepak Sah sah.sslpu@gmail.com".

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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"
Copy link
Member

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:

Suggested change
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return []byte("null"), nil
return nullJSON, nil


defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusServiceUnavailable {
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

Comment on lines 125 to 126
// Using null so that it un-marshals to nil pointer.
return []byte(null), nil
Copy link
Member

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

Suggested change
// Using null so that it un-marshals to nil pointer.
return []byte(null), nil
return nullJSON, nil

}

func samplingStrategyLoader(strategiesFile string) strategyLoader {
if strategiesFile == "" {
Copy link
Member

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?

Copy link
Contributor Author

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? 😅

Copy link
Member

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.

Copy link
Contributor Author

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)
Copy link
Member

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:

Suggested change
currBytes, err := ioutil.ReadFile(strategiesFile)
currBytes, err := ioutil.ReadFile(filepath.Clean(strategiesFile))

Copy link
Member

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>
Copy link
Member

@yurishkuro yurishkuro left a 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)
Copy link
Member

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))
Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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")
Copy link
Member

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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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(`
Copy link
Member

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>
@yurishkuro
Copy link
Member

please check the linter failures

plugin/sampling/strategystore/static/strategy_store_test.go:130:9: this value of err is never used (SA4006)
plugin/sampling/strategystore/static/strategy_store_test.go:484:11: this value of err is never used (SA4006)

@goku321
Copy link
Contributor Author

goku321 commented Oct 13, 2020

please check the linter failures

plugin/sampling/strategystore/static/strategy_store_test.go:130:9: this value of err is never used (SA4006)
plugin/sampling/strategystore/static/strategy_store_test.go:484:11: this value of err is never used (SA4006)

Fixed the linter issues.

@codecov
Copy link

codecov bot commented Oct 13, 2020

Codecov Report

❗ No coverage uploaded for pull request base (master@28873d4). Click here to learn what that means.
The diff coverage is 91.48%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master    #2519   +/-   ##
=========================================
  Coverage          ?   95.31%           
=========================================
  Files             ?      208           
  Lines             ?     9281           
  Branches          ?        0           
=========================================
  Hits              ?     8846           
  Misses            ?      356           
  Partials          ?       79           
Impacted Files Coverage Δ
plugin/sampling/strategystore/static/options.go 100.00% <ø> (ø)
...in/sampling/strategystore/static/strategy_store.go 97.53% <91.30%> (ø)
cmd/query/app/static_handler.go 83.03% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 28873d4...c95250b. Read the comment docs.

@yurishkuro yurishkuro merged commit 025d2f5 into jaegertracing:master Oct 13, 2020
@yurishkuro
Copy link
Member

Thank you @goku321 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support url for sampling.strategies file
2 participants