kgo-verifier: add an option to commit offsets right after fetch#27480
Conversation
| if grw.config.commitAfterFetch { | ||
| if err := client.CommitUncommittedOffsets(ctx); err != nil { | ||
| return err | ||
| } | ||
| } |
There was a problem hiding this comment.
should this come after the EachRecord loop below? It seems like the docs say something like
First PollFetches, then process every record, then call CommitUncommittedOffsets.
There was a problem hiding this comment.
Processing a record involves validating, which, in turn, bumps the counters. This way we may end up in a situation where counters have been bumped but the offsets cannot be committed, as the consumer group on the cluster may not be available for writing into it any more. It's specifically the case I'm testing, as the cluster is migrating away the consumer group while we are using it.
There was a problem hiding this comment.
maybe we should handle the error here. It is always possible that we won't be able to commit. In this case we should either:
- give up and fetch the data once again, then trying to commit them (at least once semantics)
- move on with what we have processed and commit with the next batch, risking reprocessing more data if group rebalance happens
There was a problem hiding this comment.
give up and fetch the data once again, then trying to commit them (at least once semantics)
isn't it what happens when the error is handled in the caller (GroupReadWorker::Wait)?
There was a problem hiding this comment.
@bashtanov the way I read @dotnwat comment is this: "are you sure franz-go internally bumped the "consumed up to" offsets after fetch but before EachRecord loop executed?" Otherwise you are just committing offsets from the previous consume round rather than the current one and you haven't solved the problem the PR claims to.
There was a problem hiding this comment.
From my glancing at the code looks like the answer is yes.
But now I have a different question. What happens if commit succeeds but we fail to get the response and error out?
On that error, we'll bubble up the error and the caller will retry.
restarting consumer group reader for error
Now the problem with this is that the next round will restart from "higher up" than was previously recorded. This way the number of valid reads reported by the status endpoint might end up being lower than number of records in topic/partition.
Grep the repo for "valid_reads >=". We allow in a couple of places that to happen. With the current PR logic this will be broken.
There was a problem hiding this comment.
I would move this to below any logic that is important to be done before for at-least-once logic.
If a crash happens after the commit goes through right after this line but before the EachRecord below, then ValidateRecord will not be called.
| if grw.config.commitAfterFetch { | ||
| if err := client.CommitUncommittedOffsets(ctx); err != nil { | ||
| return err | ||
| } | ||
| } |
There was a problem hiding this comment.
From my glancing at the code looks like the answer is yes.
But now I have a different question. What happens if commit succeeds but we fail to get the response and error out?
On that error, we'll bubble up the error and the caller will retry.
restarting consumer group reader for error
Now the problem with this is that the next round will restart from "higher up" than was previously recorded. This way the number of valid reads reported by the status endpoint might end up being lower than number of records in topic/partition.
Grep the repo for "valid_reads >=". We allow in a couple of places that to happen. With the current PR logic this will be broken.
|
Didn't mean to approve just yet. |
6b1cbcd to
bb72535
Compare
|
Thanks everyone for explaining me why proposed logic was incorrect. Please could you have a look at the new version? |
|
/dt |
CI test resultstest results on build#72075
test results on build#72179
test results on build#72244
test results on build#72257
|
bb72535 to
e9cb50f
Compare
|
I've changed it according to @nvartolomei's suggestion (commit once every N messages, N configurable), as only having N=1 option may kill performance. |
| @@ -0,0 +1,94 @@ | |||
| @rem | |||
There was a problem hiding this comment.
some IDE plugin added it? I'll remove, thanks
| batchMaxBytes = flag.Int("batch_max_bytes", 1048576, "the maximum batch size to allow per-partition (must be less than Kafka's max.message.bytes, producing)") | ||
| cgReaders = flag.Int("consumer_group_readers", 0, "Number of parallel readers in the consumer group") | ||
| cgName = flag.String("consumer_group_name", "", "The name of the consumer group. Generated randomly if not set.") | ||
| cgCommitFrequency = flag.Int("commit-frequency", 0, "if non-zero, commit offsets after fetching this many records rather than relying on auto-commit") |
There was a problem hiding this comment.
nit: cgMaxUncommitted and consumer_group_max_uncommitted
-1— auto0— at most once at we are not really interested in it now so be lazy: throw/do not implement it>0— what you want
commit-frequency is too vague imho and makes me think about rates rather than "lag" which it actually implements
also the value of 0 if you don't have the implementation context and you don't read the doc deep down here (i.e. someone else on our team is reading test code) could think it means don't commit at all
maxUncommitted seems more descriptive
Also, add a note that this is per topic rather than per partition.
e06b7ec to
f45cad3
Compare
| cmd += " --tolerate-data-loss" | ||
| if self._group_name is not None: | ||
| cmd += f" --consumer_group_name {self._group_name}" | ||
| if self._max_uncommitted != 0: |
There was a problem hiding this comment.
this should have been if self._max_uncommitted != -1? I.e. pass the flag if default has been altered. This way if someone passes 0 it will crash as not supported. Current behavior is to continue with auto-commit which is confusing imho.
Maybe even make it None by default (not customized) and pass the flag only if not None. -1 as default makes sense in go flag parsing as it simplifies the flag parsing slightly.
|
|
||
| fetches.EachRecord(func(r *kgo.Record) { | ||
| // Will cancel the context if we have read everything | ||
| cgOffsets.AddRecord(ctx, r) |
There was a problem hiding this comment.
Is this reordering of code relevant for the change?
| fetches := client.PollFetches(ctx) | ||
| fetchBatchSize := 0 | ||
| if grw.config.maxUncommitted > 0 { | ||
| fetchBatchSize = grw.config.maxUncommitted |
There was a problem hiding this comment.
you can pass it as-is to PollRecords. It already implements the semantics you need for negative values.
The only problem is value of 0 which it also interprets as -1 so this is where I would add the util.Die check. This is where validation is important and not in the main.
| batchMaxBytes = flag.Int("batch_max_bytes", 1048576, "the maximum batch size to allow per-partition (must be less than Kafka's max.message.bytes, producing)") | ||
| cgReaders = flag.Int("consumer_group_readers", 0, "Number of parallel readers in the consumer group") | ||
| cgName = flag.String("consumer_group_name", "", "The name of the consumer group. Generated randomly if not set.") | ||
| cgMaxUncommitted = flag.Int("max-uncommitted", -1, "Negative means rely on auto-commit. For positive, commit offsets after fetching this many records. Restricts the number of records shown as reads in statistics but not yet committed.") |
There was a problem hiding this comment.
nit: Mentioning "consumer group" at least in help message should be helpful for the reader to understand what "commit" refers to.
Commit messages after each N records to control how many messages may be uncommitted but processed in each partition.
f45cad3 to
e07ba08
Compare
For kgo-verifier status thread to minimize the number of uncommitted messages reported as valid reads to the status thread.
Based on redpanda-data/kgo-verifier#54.
Backports Required
Release Notes