Skip to content

Commit 2bfa214

Browse files
committed
feat(transaction): add Transaction::start_ts and scan_unbounded
Signed-off-by: Yijun Zhao <ariesdevil77@gmail.com>
1 parent 2c0f2f3 commit 2bfa214

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

src/transaction/snapshot.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ impl Snapshot {
5454
self.transaction.scan(range, limit).await
5555
}
5656

57+
/// Scan a range, return ALL key-value pairs that lying in the range.
58+
///
59+
/// Equivalent of client-go's `KVSnapshot.Iter(start, end)`; see
60+
/// [`Transaction::scan_unbounded`].
61+
pub async fn scan_unbounded(
62+
&mut self,
63+
range: impl Into<BoundRange>,
64+
) -> Result<impl Iterator<Item = KvPair>> {
65+
debug!("invoking scan_unbounded request on snapshot");
66+
self.transaction.scan_unbounded(range).await
67+
}
68+
5769
/// Scan a range, return at most `limit` keys that lying in the range.
5870
pub async fn scan_keys(
5971
&mut self,

src/transaction/transaction.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,19 @@ impl<PdC: PdClient> Transaction<PdC> {
119119
}
120120
}
121121

122+
/// Get the start timestamp of the transaction.
123+
///
124+
/// This is the TSO timestamp at which the transaction reads. The
125+
/// transaction's writes become visible at its *commit* timestamp
126+
/// (returned by [`commit`](Transaction::commit)), not at `start_ts`.
127+
///
128+
/// The `physical` component is a cluster-wide wall clock in
129+
/// milliseconds, useful as a consistent clock reading across processes
130+
/// (e.g. checking lease or deadline expiry).
131+
pub fn start_ts(&self) -> Timestamp {
132+
self.timestamp.clone()
133+
}
134+
122135
/// Create a new 'get' request
123136
///
124137
/// Once resolved this request will result in the fetching of the value associated with the
@@ -448,6 +461,70 @@ impl<PdC: PdClient> Transaction<PdC> {
448461
.map(KvPair::into_key))
449462
}
450463

464+
/// Create a 'scan' request without a limit.
465+
///
466+
/// Once resolved this request will result in a `Vec` of ALL key-value pairs
467+
/// that lie in the specified range, ordered by key.
468+
///
469+
/// Internally the range is fetched in batches of `SCAN_UNBOUNDED_BATCH_SIZE`
470+
/// pairs, so no single RPC carries an unbounded limit and each response
471+
/// message stays bounded in size. This is the equivalent of client-go's
472+
/// `KVSnapshot.Iter(start, end)`.
473+
///
474+
/// # Examples
475+
///
476+
/// ```rust,no_run
477+
/// # use tikv_client::{Key, KvPair, Value, Config, TransactionClient};
478+
/// # use futures::prelude::*;
479+
/// # futures::executor::block_on(async {
480+
/// # let client = TransactionClient::new(vec!["192.168.0.100", "192.168.0.101"]).await.unwrap();
481+
/// let mut txn = client.begin_optimistic().await.unwrap();
482+
/// let key1: Key = b"foo".to_vec().into();
483+
/// let key2: Key = b"bar".to_vec().into();
484+
/// let result: Vec<KvPair> = txn
485+
/// .scan_unbounded(key1..key2)
486+
/// .await
487+
/// .unwrap()
488+
/// .collect();
489+
/// // Finish the transaction...
490+
/// txn.commit().await.unwrap();
491+
/// # });
492+
/// ```
493+
pub async fn scan_unbounded(
494+
&mut self,
495+
range: impl Into<BoundRange>,
496+
) -> Result<impl Iterator<Item = KvPair>> {
497+
debug!("invoking transactional scan_unbounded request");
498+
let (start, end) = range.into().into_keys();
499+
let mut start = start;
500+
let mut out: Vec<KvPair> = Vec::new();
501+
loop {
502+
let page: Vec<KvPair> = self
503+
.scan((start.clone(), end.clone()), SCAN_UNBOUNDED_BATCH_SIZE)
504+
.await?
505+
.collect();
506+
// Termination follows client-go's Scanner (txnsnapshot/scan.go):
507+
// a short page means the range is exhausted — the plan layer fans
508+
// a scan out to every region in the range and each region returns
509+
// up to `limit` pairs, so a merged page smaller than the batch can
510+
// only happen when no region has more data. A full page means more
511+
// data may remain; advance past the last returned key with
512+
// `next_key()` (the same key+'\x00' successor trick client-go
513+
// uses) and fetch again. If the total is an exact multiple of the
514+
// batch size, this costs one extra empty page fetch, same as
515+
// client-go discovering EOF on its next getData.
516+
let full_page = page.len() as u32 == SCAN_UNBOUNDED_BATCH_SIZE;
517+
if let Some(last) = page.last() {
518+
start = last.key().clone().next_key();
519+
}
520+
out.extend(page);
521+
if !full_page {
522+
break;
523+
}
524+
}
525+
Ok(out.into_iter())
526+
}
527+
451528
/// Sets the value associated with the given key.
452529
///
453530
/// # Examples
@@ -1128,6 +1205,9 @@ const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_millis(MAX_TTL / 2);
11281205
/// TiKV recommends each RPC packet should be less than around 1MB. We keep KV size of
11291206
/// each request below 16KB.
11301207
pub const TXN_COMMIT_BATCH_SIZE: u64 = 16 * 1024;
1208+
1209+
/// Batch size used internally by `scan_unbounded` to paginate through a range.
1210+
const SCAN_UNBOUNDED_BATCH_SIZE: u32 = 1024;
11311211
const TTL_FACTOR: f64 = 6000.0;
11321212

11331213
/// Optimistic or pessimistic transaction.
@@ -1701,6 +1781,7 @@ impl From<u8> for TransactionStatus {
17011781
#[cfg(test)]
17021782
mod tests {
17031783
use std::any::Any;
1784+
use std::collections::BTreeMap;
17041785
use std::io;
17051786
use std::sync::atomic::AtomicUsize;
17061787
use std::sync::atomic::Ordering;
@@ -1717,6 +1798,7 @@ mod tests {
17171798
use crate::proto::pdpb::Timestamp;
17181799
use crate::request::Keyspace;
17191800
use crate::transaction::HeartbeatOption;
1801+
use crate::KvPair;
17201802
use crate::TimestampExt;
17211803
use crate::Transaction;
17221804
use crate::TransactionOptions;
@@ -1870,4 +1952,85 @@ mod tests {
18701952
"expected a lifecycle log carrying start_ts {start_ts}; captured: {logs:?}"
18711953
);
18721954
}
1955+
1956+
#[tokio::test]
1957+
async fn start_ts_returns_transaction_timestamp() {
1958+
let ts = Timestamp {
1959+
physical: 1_700_000_000_123,
1960+
logical: 42,
1961+
..Default::default()
1962+
};
1963+
let txn = Transaction::new(
1964+
ts.clone(),
1965+
Arc::new(MockPdClient::default()),
1966+
TransactionOptions::new_optimistic().read_only(),
1967+
Keyspace::Disable,
1968+
);
1969+
assert_eq!(txn.start_ts(), ts);
1970+
}
1971+
1972+
#[tokio::test]
1973+
async fn scan_unbounded_paginates_through_range() {
1974+
// 2500 pairs force 3 pages at the internal batch size of 1024.
1975+
let data: BTreeMap<Vec<u8>, Vec<u8>> = (0..2500u32)
1976+
.map(|i| {
1977+
(
1978+
format!("k{i:04}").into_bytes(),
1979+
format!("v{i}").into_bytes(),
1980+
)
1981+
})
1982+
.collect();
1983+
let scan_calls = Arc::new(AtomicUsize::new(0));
1984+
let scan_calls_cloned = scan_calls.clone();
1985+
let pd_client = Arc::new(MockPdClient::new(MockKvClient::with_dispatch_hook(
1986+
move |req: &dyn Any| {
1987+
let scan = req
1988+
.downcast_ref::<kvrpcpb::ScanRequest>()
1989+
.expect("only scan requests are expected");
1990+
scan_calls_cloned.fetch_add(1, Ordering::SeqCst);
1991+
assert_eq!(scan.limit, super::SCAN_UNBOUNDED_BATCH_SIZE);
1992+
let mut pairs = Vec::new();
1993+
for (k, v) in data.range(scan.start_key.clone()..) {
1994+
if !scan.end_key.is_empty() && k.as_slice() >= scan.end_key.as_slice() {
1995+
break;
1996+
}
1997+
if pairs.len() >= scan.limit as usize {
1998+
break;
1999+
}
2000+
pairs.push(kvrpcpb::KvPair {
2001+
key: k.clone(),
2002+
value: v.clone(),
2003+
..Default::default()
2004+
});
2005+
}
2006+
Ok(Box::new(kvrpcpb::ScanResponse {
2007+
pairs,
2008+
..Default::default()
2009+
}) as Box<dyn Any>)
2010+
},
2011+
)));
2012+
2013+
let mut txn = Transaction::new(
2014+
Timestamp::default(),
2015+
pd_client,
2016+
TransactionOptions::new_optimistic().read_only(),
2017+
Keyspace::Disable,
2018+
);
2019+
let result: Vec<KvPair> = txn
2020+
.scan_unbounded("k0000".to_owned()..="k9999".to_owned())
2021+
.await
2022+
.unwrap()
2023+
.collect();
2024+
2025+
assert_eq!(result.len(), 2500);
2026+
assert_eq!(scan_calls.load(Ordering::SeqCst), 3);
2027+
let keys: Vec<&[u8]> = result.iter().map(|p| p.key().into()).collect();
2028+
let mut sorted = keys.clone();
2029+
sorted.sort_unstable();
2030+
assert_eq!(keys, sorted, "pairs must come back in key order");
2031+
assert_eq!(keys.first().unwrap(), &b"k0000");
2032+
assert_eq!(keys.last().unwrap(), &b"k2499");
2033+
assert_eq!(result[0].value(), &b"v0".to_vec());
2034+
assert_eq!(result[2499].value(), &b"v2499".to_vec());
2035+
}
18732036
}

0 commit comments

Comments
 (0)