receive: Only wait for write quorum#2621
Conversation
f60b63f to
f6fb934
Compare
pkg/receive/handler.go
Outdated
| level.Error(h.logger).Log("msg", "storing locally", "err", err, "endpoint", endpoint) | ||
| } | ||
| ec <- err | ||
| ec <- errors.Wrapf(err, "storagin locally, endpoint %v", endpoint) |
There was a problem hiding this comment.
| ec <- errors.Wrapf(err, "storagin locally, endpoint %v", endpoint) | |
| ec <- errors.Wrapf(err, "storing locally, endpoint %v", endpoint) |
f6fb934 to
c3cbbd2
Compare
|
@kakkoyun good point! Added! :) |
ce24cc8 to
2258e46
Compare
metalmatze
left a comment
There was a problem hiding this comment.
Love those integration test additions!
For a full-on review I would need to check this out locally start diving into the gritty details. From a reviewing point of this, all LGTM! 😊 👍
There was a problem hiding this comment.
OK I am generally happy with direction, but I am not fan of current parallelizeRequests syntax, we might work on API a bit.
Essentially it confused me, then also confused you (there is a small bug) ;p Let's maybe find something better..
Also you use a lot this errors.Wrap(nil, .... and tsdbError.MultiError.Add(nil) and it just looks too scary to me. It might be just me, but I feel like it should be antipatterns, mentioned in code style ): It's just scares reader a lot (: For me it's just opportunity for easier errors.
pkg/receive/handler.go
Outdated
| h.mtx.RUnlock() | ||
|
|
||
| return h.parallelizeRequests(ctx, tenant, replicas, wreqs) | ||
| n, ec := h.parallelizeRequests(ctx, tenant, replicas, wreqs) |
There was a problem hiding this comment.
why not just for err := range <-ec? without n?
There was a problem hiding this comment.
we need to know the potential number of results so we know when we have reached quorum, it could also return the quorum amount instead, but that would really result in the same code
pkg/receive/handler.go
Outdated
| defer func() { | ||
| go func() { | ||
| for { | ||
| err, more := <-ec |
There was a problem hiding this comment.
defer cancel()
for err := range <-ec {
Would do the work I think.
There was a problem hiding this comment.
Ok because you relied on err != nil on caller side.... and you forgot about it here, I would really recommend my suggestion in comment above =D
There was a problem hiding this comment.
And yes, I understand this is how we know if it was success or not.. but maybe we can come up with cleaner API
There was a problem hiding this comment.
The problem is that we want to drain the channel, and stop when it's actually empty and closed, so we need to have the more returned.
Any suggestions for a better API? The problem is that we need to know in advance how many potential results we would be getting from the channel.
There was a problem hiding this comment.
But for ... <-ec has exactly the same semantics, no?
There was a problem hiding this comment.
It will stop iterating when it's closed. If empty err, more := <-ec will block as well
pkg/receive/handler.go
Outdated
| level.Error(h.logger).Log("msg", "storing locally", "err", err, "endpoint", endpoint) | ||
| } | ||
| ec <- err | ||
| ec <- errors.Wrapf(err, "storing locally, endpoint %v", endpoint) |
There was a problem hiding this comment.
I think we should just return if no error?
There was a problem hiding this comment.
I think you rely on err != nil on caller side, maybe I would do it here for readability, but not a blocker.
There was a problem hiding this comment.
errors.Wrap returns nil if the input err is nil
pkg/receive/handler.go
Outdated
| go func(endpoint string) { | ||
| ec <- h.replicate(ctx, tenant, wreqs[endpoint]) | ||
| defer wg.Done() | ||
| ec <- errors.Wrap(h.replicate(ctx, tenant, wreqs[endpoint]), "could not replicate write request") |
There was a problem hiding this comment.
Should pass error only on error?
There was a problem hiding this comment.
errors.Wrap returns nil if the input err is nil
There was a problem hiding this comment.
I am totally aware of that but IMO it's extremely confusing and prone to error. Plus adds major overhead on critical path.
pkg/receive/handler.go
Outdated
| continue | ||
| } | ||
|
|
||
| if uint64(countCause(errs, isNotReady)) >= (h.options.ReplicationFactor+1)/2 { |
There was a problem hiding this comment.
Maybe worth to keep this important number in some function.. (h.options.ReplicationFactor+1)/2
pkg/receive/handler.go
Outdated
| return nil | ||
| } | ||
| } | ||
| errs.Add(err) |
There was a problem hiding this comment.
I am so confused, why we are passing err nil to multiError?
There was a problem hiding this comment.
nil errors are not actually added
There was a problem hiding this comment.
Again: I am totally aware of that but IMO it's extremely confusing and prone to error.
I don't see anything in the style guide that's violated here. The only other API that I could think of that wouldn't end up in just a rearrangement of the current code is using two channels, one for errors one for successes, but that would complicate draining them a lot. |
3de93f8 to
9c72f1e
Compare
Yea, I am proposing to add that. Plus only this PR is doing so, 100% of Thanos codebase is not putting nil to multierror and does not wrap nils with message. |
|
Let me think about API, I totally see the aim of it, maybe we can find something cleaner |
437fe34 to
28f7407
Compare
| return ctx.Err() | ||
| case err, more := <-ec: | ||
| if !more { | ||
| return errs |
There was a problem hiding this comment.
I was thinking about this case, but I thought that this will never happen (: We either have success or errors quorum kind of in my previous version (:
There was a problem hiding this comment.
The attempt here is to return the best possible error at the possible cost of higher latency, for example in a 3x replication, and 2x return tsdb-not-ready/unavailable, and 1 conflict, would end up with a generalized error when in reality a retry is likely to resolve the error.
It's a trade off, either better error reporting or lower latency. Since the request is failing in this case anyways, I prefer better errors over latency.
| return tsdb.ErrNotReady | ||
| } | ||
| return errors.Wrap(err, "could not replicate write request") | ||
| if countCause(err, isConflict) >= quorum { |
There was a problem hiding this comment.
I still think it should len - quorum
There was a problem hiding this comment.
Let's take the example of replication factor 3, which has a quorum factor of 2, and we get 1 conflict error. 3-2=1, so we would be returning conflict, even though write quorum was met.
There was a problem hiding this comment.
Ok, then it has to be > len(reqs) - quorum, right? (not >=)
I think we are both right if we assume that quorum is always +1 than half. This is however very depending on quorum value.... This algorithm should never assume things like that. Let's say quorum is 1 for some reason, with replication 3, then this logic will not hold true, vs > len(reqs) - quorum is always correct. (:
There was a problem hiding this comment.
This is however very depending on quorum value....
what do you mean by this?
There was a problem hiding this comment.
We talked offline. Not a blocker so merging.
This patch modifies receive replication slightly, in that it doesn't always wait for all requests to complete anymore. If quorum amount of replication requests were successful it now does not wait for the remaining request to finish as it's not necessary to reach quorum anymore. In error cases where quorum is not reached, it still continues to wait for all requests to finish in an attempt to return a quorum error. Additionally this patch moves log lines printed in the parallelize requests function to debug logging. Calling functions already print the resulting error(s), so this was previously just noise, even in cases where requests actually succeeded. Signed-off-by: Frederic Branczyk <fbranczyk@gmail.com>
28f7407 to
85b03e2
Compare
|
Thanks! |
Changes
This patch modifies receive replication slightly, in that it doesn't
always wait for all requests to complete anymore. If quorum amount of
replication requests were successful it now does not wait for the
remaining request to finish as it's not necessary to reach quorum
anymore. In error cases where quorum is not reached, it still continues
to wait for all requests to finish in an attempt to return a quorum
error.
Additionally this patch moves log lines printed in the parallelize
requests function to debug logging. Calling functions already print the
resulting error(s), so this was previously just noise, even in cases
where requests actually succeeded.
Let me know if you think there should be a changelog entry for this, in reality there is not really a user noticable change, other than less noisy logs and lower latency for requests.
Verification
@bwplotka @krasi-georgiev @metalmatze @squat @kakkoyun