Skip to content

Conversation

@HueCodes
Copy link

Summary

Fix reverse_scan returning key_not_in_region error when the scan range spans multiple TiKV regions.

Root cause: retryable_scan selected the region based on start_key (lower bound), but TiKV reverse scan starts from the upper bound and goes backward. When bounds are in different regions, TiKV returns an error because the request context specifies the wrong region.

Fix:

  • Select region based on end_key (upper bound) for reverse scans
  • Return region.start_key() as continuation point (moving toward lower keys)
  • Handle edge case when iteration lands exactly on a region boundary
  • Add safety check to prevent infinite loops

Fixes #512

Test plan

  • cargo fmt --check passes
  • cargo clippy --lib --tests passes with no warnings
  • cargo test --lib passes (53 tests)
  • Integration test with multi-region TiKV cluster (requires TiUP setup)

@ti-chi-bot ti-chi-bot bot added the dco-signoff: yes Indicates the PR's author has signed the dco. label Jan 25, 2026
@ti-chi-bot
Copy link

ti-chi-bot bot commented Jan 25, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign overvenus for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot bot added contribution This PR is from a community contributor. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jan 25, 2026
@ti-chi-bot
Copy link

ti-chi-bot bot commented Jan 25, 2026

Welcome @HueCodes!

It looks like this is your first PR to tikv/client-rust 🎉.

I'm the bot to help you request reviewers, add labels and more, See available commands.

We want to make sure your contribution gets all the attention it needs!



Thank you, and welcome to tikv/client-rust. 😃

@ti-chi-bot ti-chi-bot bot added the first-time-contributor Indicates that the PR was contributed by an external member and is a first-time contributor. label Jan 25, 2026
@pingyu pingyu requested a review from Copilot January 26, 2026 02:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where reverse_scan fails with a key_not_in_region error when the scan range spans multiple TiKV regions. The root cause was that the region selection used start_key (lower bound) for both forward and reverse scans, but TiKV reverse scans start from the upper bound.

Changes:

  • Modified region selection to use end_key for reverse scans instead of start_key
  • Updated scan iteration to track the moving boundary (upper for reverse, lower for forward)
  • Added boundary handling logic to select the previous region when landing exactly on a region boundary
  • Implemented safety checks to prevent infinite loops during reverse scan iteration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 769 to 771
let mut current_key: Key = if reverse {
// Start from the upper bound for reverse scan
end_key.clone().unwrap_or_default()
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

When end_key is None (indicating an unbounded upper range like start_key..), using unwrap_or_default() sets current_key to an empty key []. For a reverse scan, this is incorrect because the scan should start from the end of the keyspace, not from the beginning. An empty key represents the start of the keyspace. This will cause reverse scans with unbounded upper bounds (e.g., scan_reverse("k2".., limit)) to fail or return incorrect results. The test at line 611-617 in tests/integration_tests.rs appears to test this scenario.

Copilot uses AI. Check for mistakes.
Comment on lines 861 to 863
// If no upper bound, fall back to start_key (though this case
// is documented as unsupported for reverse scan)
_ => &start_key,
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The comment indicates that reverse scans with no upper bound are documented as unsupported (line 520), but the code attempts to handle it by falling back to start_key. This fallback will lead to incorrect behavior. It would be clearer and safer to return an error for this unsupported case rather than silently proceeding with incorrect logic. Consider adding an explicit error check for reverse && end_key.is_none() before entering the scan loop.

Copilot uses AI. Check for mistakes.
@ti-chi-bot ti-chi-bot bot added dco-signoff: no Indicates the PR's author has not signed dco. and removed dco-signoff: yes Indicates the PR's author has signed the dco. labels Jan 26, 2026
@HueCodes HueCodes force-pushed the fix/reverse-scan-multi-region branch from f893fa6 to 7e6311d Compare January 26, 2026 03:45
@ti-chi-bot ti-chi-bot bot added dco-signoff: yes Indicates the PR's author has signed the dco. and removed dco-signoff: no Indicates the PR's author has not signed dco. labels Jan 26, 2026
@pingyu pingyu requested a review from Copilot January 26, 2026 06:36
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// if we've reached/passed the upper bound of our scan range
if next_key.is_empty() || end_key.clone().is_some_and(|ek| ek <= next_key) {
break;
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Add a safety check similar to the reverse scan case to prevent infinite loops if next_key doesn't advance. The forward scan path should verify that next_key is strictly greater than current_key before continuing, otherwise break to avoid an infinite loop.

Suggested change
}
}
// Safety: ensure progress is made; if next_key does not advance,
// break to avoid an infinite loop
if next_key <= current_key {
break;
}

Copilot uses AI. Check for mistakes.
Comment on lines 810 to 900
loop {
let region = self.rpc.clone().region_for_key(&start_key).await?;
// For forward scan: select region containing start_key (lower bound)
// For reverse scan: select region containing end_key (upper bound)
// because TiKV reverse scan starts from the upper bound and goes backward.
//
// When reverse=true, new_raw_scan_request swaps the keys so that TiKV receives
// end_key as its start_key. We must select the region containing that key.
let region_lookup_key: &Key = if reverse {
match &end_key {
Some(ek) if !ek.is_empty() => ek,
// If no upper bound, fall back to start_key (though this case
// is documented as unsupported for reverse scan)
_ => &start_key,
}
} else {
&start_key
};
let mut region = self.rpc.clone().region_for_key(region_lookup_key).await?;

// For reverse scan: if the lookup key equals the region's start_key exactly,
// we're at a boundary and need the previous region. This happens when iterating
// backward and current_key lands on a region boundary.
if reverse {
let region_start = region.start_key();
if !region_start.is_empty() && *region_lookup_key == region_start {
// Find the previous region by computing the key immediately before
// this boundary. Decrement the last byte with borrow propagation.
let prev_key = {
let mut key: Vec<u8> = region_start.into();
while let Some(last) = key.last_mut() {
if *last > 0 {
*last -= 1;
break;
}
key.pop(); // byte was 0, borrow from previous
}
key
};
// Always look up - empty prev_key correctly returns the first region
region = self.rpc.clone().region_for_key(&prev_key.into()).await?;
}
}
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

The fix for multi-region reverse scans lacks automated test coverage. While integration tests exist for scan_reverse, they don't validate the specific multi-region scenario that this PR fixes. Consider adding a unit test with a mocked multi-region setup to verify the region selection and boundary handling logic, or document how to manually test this scenario.

Copilot uses AI. Check for mistakes.
@HueCodes
Copy link
Author

Thanks for the feedback. Addressing these issues, I will fix and post and update today.

@ti-chi-bot ti-chi-bot bot added dco-signoff: no Indicates the PR's author has not signed dco. and removed dco-signoff: yes Indicates the PR's author has signed the dco. labels Jan 27, 2026
When reverse scanning a range spanning multiple regions, `retryable_scan`
selected the region based on `start_key` (lower bound). However, TiKV
reverse scan starts from the upper bound, causing `key_not_in_region`
errors when bounds are in different regions.

Fix by selecting the region containing `end_key` for reverse scans and
adjusting continuation logic to move backward through regions. Also
handle the edge case when iteration lands exactly on a region boundary.

Fixes tikv#512

Signed-off-by: Hugh <[email protected]>
The previous boundary handling used pop() to get a "previous" key,
which truncates instead of decrementing. This could select the wrong
region when:
1. The region boundary key is a single byte (pop makes it empty)
2. The truncated key falls in a much earlier region

Fix by properly decrementing the last byte with borrow propagation.
Also remove the !prev_key.is_empty() guard that skipped the region
lookup entirely for single-byte boundaries.

Signed-off-by: Hugh <[email protected]>
- Add explicit error for reverse scan without upper bound
- Add forward scan safety check to prevent infinite loops
- Simplify region lookup now that end_key is validated

Signed-off-by: Hugh <[email protected]>
@HueCodes HueCodes force-pushed the fix/reverse-scan-multi-region branch from 879fb1d to 7e7fe5e Compare January 27, 2026 04:27
@ti-chi-bot ti-chi-bot bot added dco-signoff: yes Indicates the PR's author has signed the dco. and removed dco-signoff: no Indicates the PR's author has not signed dco. labels Jan 27, 2026
@HueCodes
Copy link
Author

Let me know if theres any other changes you would like me to make.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution This PR is from a community contributor. dco-signoff: yes Indicates the PR's author has signed the dco. first-time-contributor Indicates that the PR was contributed by an external member and is a first-time contributor. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reverse scan fail to find key when range spans multi region

1 participant