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

adding retryable to scan #456

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

limbooverlambda
Copy link
Contributor

solve: #455

We were running into an issue with scans where periodically we noticed scans returning empty results for datasets that were present in a cluster. The hypothesis was that the scans were returning empties when the regions are undergoing splits. While trying to reproduce the issue (by issuing splits from pd-ctl), we found out that when there's a region error (epoch_version_mismatch et al), the scan_inner is not triggering any cache invalidations and subsequent retries. The scan simply returns an empty. This PR is fixing the issue by triggering the invalidations and retry for such issues.

@pingyu @ekexium @andylokandy.

Signed-off-by: limbooverlambda <schakra1@gmail.com>
Copy link
Collaborator

@pingyu pingyu left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution !

The PR overall looks good. And I left some minor suggestions.

Besides, If you would like to verify the correctness when region error happens, consider to use failpoint. Please refer to failpoint_test.rs. It's not a must before the PR is accepted.

src/raw/client.rs Outdated Show resolved Hide resolved
src/raw/client.rs Outdated Show resolved Hide resolved
src/raw/client.rs Outdated Show resolved Hide resolved
plan::handle_region_error(self.rpc.clone(), err.clone(), store.clone())
.await?;
return if status {
self.retryable_scan(scan_args.clone()).await
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggest to eliminate the recursion and let caller do the retry, to reduce overhead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the recursion, in the initial implementation, I followed the pattern as outlined in

async fn single_plan_handler(
where a mutual recursion is being performed to retry the flow. The idea was to shield the caller from performing the retries. In subsequent PRs I would have proposed some enhancements to control the retry logic. I can remove the retry if that seems more prudent.

limbooverlambda and others added 4 commits June 25, 2024 13:13
Signed-off-by: limbooverlambda <schakra1@gmail.com>
Signed-off-by: limbooverlambda <schakra1@gmail.com>
Signed-off-by: limbooverlambda <schakra1@gmail.com>
@limbooverlambda
Copy link
Contributor Author

Hi @pingyu, are there any more changes you need me to make before this change can be merged? Thanks for taking the time to look at this.

let scan_args = ScanInnerArgs {
start_key: current_key.clone(),
range: range.clone(),
limit,
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be better to use current_limit here, otherwise it would return kv pairs more than limit. Moreover, the following current_limiit -= kvs.en() would overflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Changed.

current_limit -= kvs.len() as u32;
result.append(&mut kvs);
}
if end_key
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit:

Suggested change
if end_key
if end_key.is_some_and(|ek| ek <= next_key.as_ref())

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Changed. end_key had to be cloned however.

while current_limit > 0 {
let scan_args = ScanInnerArgs {
start_key: current_key.clone(),
range: range.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should scan from start_key. Otherwise if there is region merge during scan, we would get duplicated kv paires, and lose some others if limit is reached.

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 was trying to trace through the logic and from what I understand, we will only be looping if the limit of the scan has not been exhausted. So regardless of a split or merge, won't we be resuming the next scan from the end_key returned by the previous scan call? So if the first scan result returns an end_key "foo", doesn't the system guarantee that if we start the next scan starting from "foo", we are guaranteed to return all results that are lexicographically larger than "foo" and smaller than whatever end_key has been provided. This is regardless of whether the underlying regions are undergoing any churn(due to any splits or merges). There may be gaps in my understanding so will be more than happy to get some more feedback here.

@@ -953,4 +1016,63 @@ mod tests {
);
Ok(())
}

#[tokio::test]
async fn test_raw_scan_retryer() -> Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test case looks a little not necessary to me, as it just check that we call error handler on region error, but introduce more complexity.

The core changes in this PR is the retry on region error, so if we want to verify the correctness, I think we need to simulate such scene.

For example, we first put some little entries, and perform the scan to fill the region cache. Then we put much more (or large) entries to trigger the region split (see tests::common::init()), and scan again. The later scan should meet region errors.

The test case would be a little complex to implement, so it's OK to me if we don't have it in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. The current setup, to your point, is a bit cumbersome. I have removed this test case. I will add the test as you outlined in a subsequent PR.

Signed-off-by: limbooverlambda <schakra1@gmail.com>
@limbooverlambda
Copy link
Contributor Author

@pingyu Thanks for your feedback. It will be great if you could take another look.

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.

None yet

2 participants