-
Notifications
You must be signed in to change notification settings - Fork 4.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
Option to set "stale" consistency by default for http requests. #3142
Conversation
f49b9ad
to
1f8128b
Compare
agent/http.go
Outdated
query := req.URL.Query() | ||
if _, ok := query["stale"]; ok { | ||
b.AllowStale = true | ||
} | ||
if _, ok := query["consistent"]; ok { | ||
b.RequireConsistent = true | ||
} | ||
if !b.AllowStale && !b.RequireConsistent { |
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.
Suggestion to rewrite to make this boolean logic more readable. You could remove this block and init b.allowStale before line 364 with a single line like
b.allowStale = !b.RequireConsistent && s.agent.config.HttpAPIStaleByDefault
The if block in line 364 after that will check query["stale"] after that to override if necessary. IMO that's easier to follow than what you have now.
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.
done
1f8128b
to
2d3485c
Compare
agent/http_test.go
Outdated
} | ||
|
||
if !query.AllowStale { | ||
t.Fatalf("Bad: %v", query) |
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.
Make the message be a bit more clear, maybe something like "Expected allowStale to be true but was false", rather than printing the whole query 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.
done
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 LGTM after the couple minor changes, but @slackpad might want to do a review as well.
2d3485c
to
8823091
Compare
Should this be refactored as per the 'dns_config' object? "http_config": {
"allow_stale": true,
"max_stale": "5s",
"api_response_headers": {
"Access-Control-Allow-Origin": "*"
}
} |
In general, I would prefer less boolean flags in the config since they make refactoring more difficult. Whenever you need more than two states you're stuck. Also, often you have to use negative logic like But I think @mterron's suggestion is the better approach since it makes the configuration consistent which is more important IMO. Since I'll be touching the configuration soon I can address the boolean flag issue separately. |
@mterron max_stale feature for http endpoint doesn't seem to be implemented yet. At least I can't find it. I can extract http options to http_config struct if you want but at the moment I found only two fields that I can put there: api_response_headers and allow_stale (or default_read_consistency as @magiconair suggested). @magiconair Seems doable but my initial intention was to touch as little consul code as possible because people usually don't accept pull requests with too invasive changes. Let me know if you still want that http_config field. If want me to implement also that max_stale feature for http then probably I would touch a lot more code because at the moment http endpoints are reusing layer that parses requests and then they are making RPC calls directly. That makes implementing max_stale in similar way like dns not very comfortable. Maybe max_stale can be implemented directly in RPC layer to make dns and http code consistent. |
We can do this in multiple steps. Let's get the configuration right first since this will be harder to change later.
I'd suggest one commit which refactor the config without adding the feature. Pls make sure that it is backwards compatible. Then a second commit which adds the feature in the new config. We can then add `max_stale` later.
|
Do you prefer one pull request with multiple commits or separate pull request for every change? |
one pr with multiple commits is fine since then it is easier to track. |
8823091
to
1b28cb8
Compare
@magiconair please take a look |
2f252eb
to
f1bef4c
Compare
agent/config.go
Outdated
@@ -128,6 +128,12 @@ type DNSConfig struct { | |||
RecursorTimeoutRaw string `mapstructure:"recursor_timeout" json:"-"` | |||
} | |||
|
|||
// HttpApiConfig is used to fine tune the Http sub-system. | |||
type HttpApiConfig struct { |
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.
s/HttpApiConfig/HTTPConfig/
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.
done
agent/config.go
Outdated
@@ -364,6 +370,9 @@ type Config struct { | |||
// Domain is the DNS domain for the records. Defaults to "consul." | |||
Domain string `mapstructure:"domain"` | |||
|
|||
// Http api configuration | |||
HttpApiConfig HttpApiConfig `mapstructure:"http_api_config"` |
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.
s/http_api_config/http_config/
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.
done
agent/config.go
Outdated
if len(b.HTTPAPIResponseHeaders) != 0 { | ||
if result.HTTPAPIResponseHeaders == nil { | ||
result.HTTPAPIResponseHeaders = make(map[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.
Can you move that functionality into DecodeConfig
, please? We're doing the other arg mangling there as well.
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.
done
agent/config.go
Outdated
@@ -1167,6 +1174,10 @@ func DecodeConfig(r io.Reader) (*Config, error) { | |||
fmt.Fprintln(os.Stderr, "==> DEPRECATION: atlas_endpoint is deprecated and "+ | |||
"is no longer used. Please remove it from your configuration.") | |||
} | |||
if len(result.DeprecatedHTTPAPIResponseHeaders) > 0 { |
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.
When you move the mangling to DecodeConfig
you can move the log statement as well. I'll take care of the other ones later.
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.
done
agent/config_test.go
Outdated
@@ -327,7 +327,11 @@ func TestDecodeConfig(t *testing.T) { | |||
}, | |||
{ | |||
in: `{"http_api_response_headers":{"a":"b","c":"d"}}`, | |||
c: &Config{HTTPAPIResponseHeaders: map[string]string{"a": "b", "c": "d"}}, | |||
c: &Config{DeprecatedHTTPAPIResponseHeaders: map[string]string{"a": "b", "c": "d"}}, |
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 this should result only in the new field being set. The Atlas
parameters are different since we no longer use them at all but there should be no code using the deprecated field.
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.
done
agent/config.go
Outdated
@@ -132,6 +132,11 @@ type DNSConfig struct { | |||
type HttpApiConfig struct { | |||
// ResponseHeaders are used to add HTTP header response fields to the HTTP API responses. | |||
ResponseHeaders map[string]string `mapstructure:"response_headers"` | |||
|
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.
pls move before ResponseHeaders
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.
done
agent/config.go
Outdated
@@ -914,6 +919,9 @@ func DefaultConfig() *Config { | |||
MaxStale: 10 * 365 * 24 * time.Hour, | |||
RecursorTimeout: 2 * time.Second, | |||
}, | |||
HttpApiConfig: HttpApiConfig{ | |||
AllowStale: false, |
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.
That is the default so you can omit that.
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.
done
agent/http_test.go
Outdated
@@ -433,13 +433,14 @@ func TestParseWait_InvalidIndex(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestParseConsistency(t *testing.T) { | |||
func TestParseConsigistency(t *testing.T) { |
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.
typo
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.
fixed
agent/http.go
Outdated
query := req.URL.Query() | ||
b.AllowStale = !b.RequireConsistent && s.agent.config.HttpApiConfig.AllowStale |
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 doesn't look right since b.RequireConsistent
hasn't been set at this point. I think you can write this better as
if b.AllowStale && b.RequireConsistent { error 400 }
if !b.AllowStale && !b.RequireConsistent {
b.AllowStale = s.agent.config.HTTPConfig.AllowStale
}
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.
@preetapan suggested that it should be this way, original version looked like this
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 rearranged code so RequireConsistent
is initialized before AllowStale
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.
Maybe I'm missing something obvious but I don't think the other version is correct. @preetapan What do you think?
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.
My original suggestion was to simplify that boolean logic because I found it hard to read, but I didn't see the full diff, so missed the fact that RequireConsistent was not yet set. Now that @wojtkiewicz has rearranged code to set RequireConsistent before, this line b.AllowStale = !b.RequireConsistent && s.agent.config.HttpApiConfig.AllowStale
, is doing the right thing. The flow is: First initialize allowStale according to whether the httpApiConfig.allowStale has it set (AND if b.RequireConsistent is not set, since you shouldn't have allowStale/allowConsistent both be true).
Then in the next line, it potentially overrides the value of allowStale if the query param contains "stale"
+1 to your suggestion of failing with a 400 if both are set . It looks like line 371 is already doing that, and in the right place.
agent/http_test.go
Outdated
t.Parallel() | ||
a := NewTestAgent(t.Name(), 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.
defer a.Shutdown()
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.
done
agent/http_test.go
Outdated
@@ -462,15 +463,29 @@ func TestParseConsistency(t *testing.T) { | |||
if !b.RequireConsistent { | |||
t.Fatalf("Bad: %v", b) | |||
} | |||
|
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 I'd prefer if you refactor the two ParseInconsistency
tests as follows to ensure that we cover all cases:
func TestParseConsistency(t *testing.T) {
t.Run("no stale, no consistent, no allowStale", func(t *testing.T) { ... })
t.Run("stale, no consistent, no allowStale", func(t *testing.T) { ... })
... tests for all eight combinations
}
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.
work in progress
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 made a bunch of parametrized tests. I hope that's more readable.
6e0c8ac
to
5ee12b4
Compare
@wojtkiewicz |
Sorry for the delay, I had some other work to do. I will try to get it done today. |
That would be nice. I'd like to merge this. |
5ee12b4
to
8b37ef0
Compare
@magiconair I refactored those tests, let me know if you need anything else. |
agent/http_test.go
Outdated
} | ||
if !b.RequireConsistent { | ||
t.Fatalf("Bad: %v", b) | ||
var tests = []struct { |
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.
two comments:
- formatting
tests := []struct {
url string
allowStale bool
wantAllowStale bool
wantRequireConsistent bool
}{
{"/v1/catalog/nodes?stale", false, true, false},
{"/v1/catalog/nodes?stale", true, true, false},
{"/v1/catalog/nodes?consistent", false, false, true},
{"/v1/catalog/nodes?consistent", true, false, true},
{"/v1/catalog/nodes", false, false, false},
{"/v1/catalog/nodes", true, true, false},
}
for _, tt := range tests {
name := fmt.Sprintf("url=%v, HTTP.AllowStale=%v", tt.url, tt.allowStale)
t.Run(name, func(t *testing.T) {
a.srv.agent.config.HTTPConfig.AllowStale = tt.allowStale
var q structs.QueryOptions
req, _ := http.NewRequest("GET", tt.url, nil)
if d := a.srv.parseConsistency(resp, req, &q); d {
t.Fatalf("Failed to parse consistency.")
}
if got, want := q.AllowStale, tt.wantAllowStale; got != want {
t.Fatalf("got allowStale %v want %v", got, want)
}
if got, want := q.RequireConsistent, tt.wantRequireConsistent; got != want {
t.Fatalf("got requireConsistent %v want %v", got, want)
}
})
}
- I think the fact that we need to change the server config for this test to work shows a wrong design.
parseConsistency
should not depend on the server configuration. This should be a utility function which is independent of the server. Can you just pass theallowStale
parameter from the config into the function?
func parseConsistency(resp http.ResponseWriter, req *http.Request, allowStale bool, opts *structs.QueryOptions) {...}
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.
Done.
8b37ef0
to
3880969
Compare
agent/http.go
Outdated
@@ -359,14 +359,15 @@ func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOpti | |||
|
|||
// parseConsistency is used to parse the ?stale and ?consistent query params. | |||
// Returns true on error | |||
func parseConsistency(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool { | |||
func (s *HTTPServer) parseConsistency(resp http.ResponseWriter, req *http.Request, allowStale bool, b *structs.QueryOptions) bool { |
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.
Then parseConsistency
does not need to be a member of the HTTPServer
anymore. Maybe add a comment what allowStale
is used for.
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.
fixed
3880969
to
5138faf
Compare
5138faf
to
b1a6ffb
Compare
LGTM. Can you please update the documentation in |
Sure but I'm at the conference for a few days. I can push that in the evening though. |
Sounds good. Enjoy the conference :) |
b1a6ffb
to
28ceaa6
Compare
@magiconair I updated documentation. |
@wojtkiewicz I've made some small changes to the docs since it was still referring to the old name. I've also made a small change to the |
@magiconair wonderful, thanks for your help |
This reverts commit 1e0fd27.
@wojtkiewicz we have decided to roll this feature back since |
I explicitly made |
@wojtkiewicz sorry for the back and forth on this one. I think defaulting it to |
@wojtkiewicz That was indeed my mistake to not consider the full implications of the change. DNS is a read-only interface and therefore it is much safer to have a global option to allow stale reads. I think allowing this as an option on the query without providing a global option might be the right compromise but as @slackpad said we want to think this through since the implications for other users could be significant. |
I understand but primary use case for consul is a discovery service. At least from my point of view. Making all services specify some query option so that your cluster can scale horizontally is just not sensible even if it protects user from some specific confusion. I hope that we can somehow address the KV case in the future for instance by having separate configuration. I never really wanted to turn on this feature by default for anybody since it might have some negative consequences but if those do not apply to you: I find that feature to be very useful. |
I agree that the feature is useful but I disagree that consul is merely a discovery service. KV and Locks are important parts of the product which are currently used with a certain expectation in mind. Allowing a global option to change the behavior of the entire API has much wider ranging consequences than it does for DNS where you can only use the service discovery part. |
Obviously I'm not going to argue about which feature is important. My intention was never to touch or break KV or Locks with this feature. I think it is possible to implement this touching only service discovery functionality. Original PR was supposed to implement this using as little code as possible but from what I understood you don't mind refactoring here and there. Let me know if you are still interested. |
@magiconair @slackpad I hope I didn't upset you too much, that wasn't my intention :-) |
Of course we're not upset. Having users who care is important. Please keep pushing! :) |
Hi,
This PR introduces option to set stale consistency by default for http requests when client didn't explicitly asked for any consistency in particular. Such behaviour is possible to accomplish using dns endpoint at the moment but not when client is asking using REST api.
This feature is especially useful when consul cluster is used by few hundred micro-services and without using stale all requests are handled by cluster leader which doesn't help to scale cluster horizontally. When dealing with lots of services asking every team to specify stale consistency explicitly is doable but very impractical.
This new feature has to be explicitly turned on. By default nothing is changed in how default consistency is handled.
Sample configuration:
{"http_api_stale_by_default": true}