tikv-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tikv/_rpc.py ADDED
@@ -0,0 +1,109 @@
1
+ """gRPC method descriptors and channel settings for PD and TiKV."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from typing import Generic, TypeVar
8
+
9
+ import grpc
10
+ from google.protobuf.message import Message
11
+
12
+ from ._proto import kvrpcpb_pb2 as kvrpcpb
13
+ from ._proto import pdpb_pb2 as pdpb
14
+
15
+ Response = TypeVar("Response", bound=Message)
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Method(Generic[Response]):
20
+ """A unary gRPC method: its full path and response message type."""
21
+
22
+ path: str
23
+ response: type[Response]
24
+
25
+
26
+ def _tikv(name: str, response: type[Response]) -> Method[Response]:
27
+ return Method(f"/tikvpb.Tikv/{name}", response)
28
+
29
+
30
+ def _pd(name: str, response: type[Response]) -> Method[Response]:
31
+ return Method(f"/pdpb.PD/{name}", response)
32
+
33
+
34
+ RAW_GET = _tikv("RawGet", kvrpcpb.RawGetResponse)
35
+ RAW_BATCH_GET = _tikv("RawBatchGet", kvrpcpb.RawBatchGetResponse)
36
+ RAW_PUT = _tikv("RawPut", kvrpcpb.RawPutResponse)
37
+ RAW_BATCH_PUT = _tikv("RawBatchPut", kvrpcpb.RawBatchPutResponse)
38
+ RAW_DELETE = _tikv("RawDelete", kvrpcpb.RawDeleteResponse)
39
+ RAW_BATCH_DELETE = _tikv("RawBatchDelete", kvrpcpb.RawBatchDeleteResponse)
40
+ RAW_DELETE_RANGE = _tikv("RawDeleteRange", kvrpcpb.RawDeleteRangeResponse)
41
+ RAW_SCAN = _tikv("RawScan", kvrpcpb.RawScanResponse)
42
+ RAW_GET_KEY_TTL = _tikv("RawGetKeyTTL", kvrpcpb.RawGetKeyTTLResponse)
43
+ RAW_COMPARE_AND_SWAP = _tikv("RawCompareAndSwap", kvrpcpb.RawCASResponse)
44
+ RAW_CHECKSUM = _tikv("RawChecksum", kvrpcpb.RawChecksumResponse)
45
+
46
+ PD_GET_MEMBERS = _pd("GetMembers", pdpb.GetMembersResponse)
47
+ PD_GET_REGION = _pd("GetRegion", pdpb.GetRegionResponse)
48
+ PD_GET_PREV_REGION = _pd("GetPrevRegion", pdpb.GetRegionResponse)
49
+ PD_GET_STORE = _pd("GetStore", pdpb.GetStoreResponse)
50
+ PD_SCAN_REGIONS = _pd("ScanRegions", pdpb.ScanRegionsResponse)
51
+
52
+
53
+ CHANNEL_OPTIONS = (
54
+ # Scans and batch reads can legitimately exceed gRPC's 4 MiB default.
55
+ ("grpc.max_receive_message_length", -1),
56
+ ("grpc.max_send_message_length", -1),
57
+ # Detect dead peers quickly, like client-go does.
58
+ ("grpc.keepalive_time_ms", 10_000),
59
+ ("grpc.keepalive_timeout_ms", 3_000),
60
+ ("grpc.keepalive_permit_without_calls", 1),
61
+ ("grpc.http2.max_pings_without_data", 0),
62
+ )
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class TLSConfig:
67
+ """Paths to PEM files for mutual TLS with PD and TiKV.
68
+
69
+ >>> TLSConfig(ca="ca.pem", cert="client.pem", key="client-key.pem") # doctest: +SKIP
70
+ """
71
+
72
+ ca: str | os.PathLike[str]
73
+ cert: str | os.PathLike[str] | None = None
74
+ key: str | os.PathLike[str] | None = None
75
+
76
+ def credentials(self) -> grpc.ChannelCredentials:
77
+ def read(path: str | os.PathLike[str] | None) -> bytes | None:
78
+ if path is None:
79
+ return None
80
+ with open(path, "rb") as file:
81
+ return file.read()
82
+
83
+ return grpc.ssl_channel_credentials(
84
+ root_certificates=read(self.ca),
85
+ private_key=read(self.key),
86
+ certificate_chain=read(self.cert),
87
+ )
88
+
89
+
90
+ def strip_scheme(url: str) -> str:
91
+ """``http://host:2379`` -> ``host:2379``; PD advertises URLs with schemes."""
92
+ return url.split("://", 1)[-1].rstrip("/")
93
+
94
+
95
+ # Status codes that say "this will never work", as opposed to transient
96
+ # network or availability problems that a retry can fix.
97
+ _FATAL_CODES = frozenset(
98
+ {
99
+ grpc.StatusCode.INVALID_ARGUMENT,
100
+ grpc.StatusCode.UNIMPLEMENTED,
101
+ grpc.StatusCode.UNAUTHENTICATED,
102
+ grpc.StatusCode.PERMISSION_DENIED,
103
+ }
104
+ )
105
+
106
+
107
+ def is_retryable(error: grpc.RpcError) -> bool:
108
+ code = error.code() if callable(getattr(error, "code", None)) else None
109
+ return code not in _FATAL_CODES
tikv/_sync/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # Generated from _async/__init__.py by scripts/unasync.py. DO NOT EDIT.
2
+ """asyncio implementation; the source of truth for ``_sync``."""
tikv/_sync/_client.py ADDED
@@ -0,0 +1,585 @@
1
+ # Generated from _async/_client.py by scripts/unasync.py. DO NOT EDIT.
2
+ """The raw key-value client."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+ from collections.abc import Iterator, Callable, Iterable, Mapping, Sequence
8
+ from datetime import timedelta
9
+ from types import TracebackType
10
+ from typing import TypeVar
11
+
12
+ import grpc
13
+ from google.protobuf.message import Message
14
+
15
+ from .._backoff import Backoff, Wait, describe
16
+ from .._errors import RegionError, ServerError, TiKVError, UndeterminedError
17
+ from .._keys import as_key, as_value, chunked, key_range, min_end, ttl_seconds
18
+ from .._proto import kvrpcpb_pb2 as kvrpcpb
19
+ from .._region import MAX_KEY, Region, RegionCache, describe_region_error, on_region_error
20
+ from .._rpc import (
21
+ RAW_BATCH_DELETE,
22
+ RAW_BATCH_GET,
23
+ RAW_BATCH_PUT,
24
+ RAW_CHECKSUM,
25
+ RAW_COMPARE_AND_SWAP,
26
+ RAW_DELETE,
27
+ RAW_DELETE_RANGE,
28
+ RAW_GET,
29
+ RAW_GET_KEY_TTL,
30
+ RAW_PUT,
31
+ RAW_SCAN,
32
+ Method,
33
+ Response,
34
+ TLSConfig,
35
+ is_retryable,
36
+ )
37
+ from .._types import CasResult, Checksum
38
+ from ._compat import map_concurrently, sleep
39
+ from ._pd import PDClient
40
+ from ._transport import Transport
41
+
42
+ T = TypeVar("T")
43
+
44
+ # Per-request limits when splitting batches, following client-go.
45
+ BATCH_MAX_KEYS = 512
46
+ BATCH_MAX_BYTES = 1024 * 1024
47
+ # Keys fetched per RawScan request when the caller does not set a limit.
48
+ SCAN_PAGE_SIZE = 512
49
+ # Regions fetched from PD in one go when walking a key range.
50
+ REGION_PREFETCH = 64
51
+
52
+
53
+ class RawClient:
54
+ """A client for TiKV's raw (non-transactional) key-value API.
55
+
56
+ Keys and values are :class:`bytes`. Each single-key operation is atomic;
57
+ batch operations are atomic per region but not across regions.
58
+
59
+ ::
60
+
61
+ with RawClient(["127.0.0.1:2379"]) as client:
62
+ client.put(b"greeting", b"hello")
63
+ assert client.get(b"greeting") == b"hello"
64
+
65
+ :param pd_endpoints: Address of one or more PD servers, e.g.
66
+ ``"pd-0:2379"``. The rest of the cluster is discovered from them.
67
+ :param timeout: Time budget in seconds for each operation, including all
68
+ retries. Scans apply it to each page they fetch.
69
+ :param atomic: Route every write through TiKV's single-row transaction
70
+ path. Required by :meth:`compare_and_swap`; every client writing the
71
+ same keys must agree on this setting.
72
+ :param tls: Certificates for mutual TLS, if the cluster requires it.
73
+
74
+ One client can serve a whole application: it is safe to use from many
75
+ threads (blocking flavor) or tasks (asyncio flavor) at once, and it keeps
76
+ one connection per server. Close it with :meth:`close` or use it as a
77
+ context manager.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ pd_endpoints: str | Iterable[str],
83
+ *,
84
+ timeout: float = 10.0,
85
+ atomic: bool = False,
86
+ tls: TLSConfig | None = None,
87
+ ) -> None:
88
+ endpoints = [pd_endpoints] if isinstance(pd_endpoints, str) else list(pd_endpoints)
89
+ if not endpoints:
90
+ raise ValueError("at least one PD endpoint is required")
91
+ if not timeout > 0:
92
+ raise ValueError(f"timeout must be positive, got {timeout!r}")
93
+ self.timeout = timeout
94
+ self.atomic = atomic
95
+ self._endpoints = endpoints
96
+ self._cache = RegionCache()
97
+ self._transport = Transport(tls.credentials() if tls else None)
98
+ self._pd = PDClient(endpoints, self._transport, self._cache)
99
+ self._closed = False
100
+
101
+ def __repr__(self) -> str:
102
+ return f"{type(self).__name__}({self._endpoints!r}, timeout={self.timeout!r}, atomic={self.atomic!r})"
103
+
104
+ def __enter__(self) -> RawClient:
105
+ return self
106
+
107
+ def __exit__(
108
+ self,
109
+ exc_type: type[BaseException] | None,
110
+ exc: BaseException | None,
111
+ traceback: TracebackType | None,
112
+ ) -> None:
113
+ self.close()
114
+
115
+ def close(self) -> None:
116
+ """Close all connections. The client cannot be used afterwards."""
117
+ if not self._closed:
118
+ self._closed = True
119
+ self._transport.close()
120
+ self._cache.clear()
121
+
122
+ # -- Reads ---------------------------------------------------------------
123
+
124
+ def get(self, key: bytes) -> bytes | None:
125
+ """Return the value of *key*, or ``None`` if it does not exist."""
126
+ request = kvrpcpb.RawGetRequest(key=as_key(key))
127
+ response = self._execute(request.key, RAW_GET, request)
128
+ return None if response.not_found else response.value
129
+
130
+ def get_many(self, keys: Iterable[bytes]) -> dict[bytes, bytes]:
131
+ """Return a dict of the given keys that exist, mapped to their values.
132
+
133
+ Missing keys are left out. The dict follows the order of *keys*.
134
+ """
135
+ wanted = list(dict.fromkeys(map(as_key, keys)))
136
+ responses = self._execute_batch(
137
+ sorted(wanted),
138
+ key=_identity,
139
+ method=RAW_BATCH_GET,
140
+ build=lambda chunk: kvrpcpb.RawBatchGetRequest(keys=chunk),
141
+ )
142
+ found = {pair.key: pair.value for response in responses for pair in _checked(response.pairs)}
143
+ return {key: found[key] for key in wanted if key in found}
144
+
145
+ def ttl(self, key: bytes) -> int | None:
146
+ """Return the seconds until *key* expires, or ``None`` if it never does.
147
+
148
+ :raises KeyError: if *key* does not exist.
149
+ """
150
+ request = kvrpcpb.RawGetKeyTTLRequest(key=as_key(key))
151
+ response = self._execute(request.key, RAW_GET_KEY_TTL, request)
152
+ if response.not_found:
153
+ raise KeyError(key)
154
+ return response.ttl or None
155
+
156
+ def scan(
157
+ self,
158
+ start: bytes | None = None,
159
+ end: bytes | None = None,
160
+ *,
161
+ prefix: bytes | None = None,
162
+ limit: int | None = None,
163
+ reverse: bool = False,
164
+ ) -> Iterator[tuple[bytes, bytes]]:
165
+ """Iterate over ``(key, value)`` pairs in key order.
166
+
167
+ Covers keys in ``[start, end)``, or every key starting with *prefix*.
168
+ ``None`` bounds are open-ended. Results are fetched lazily, a page at
169
+ a time, so scanning a large range uses little memory::
170
+
171
+ for key, value in client.scan(prefix=b"user:"):
172
+ ...
173
+
174
+ :param limit: Stop after this many pairs.
175
+ :param reverse: Iterate from the largest key down.
176
+ """
177
+ for pair in self._scan(start, end, prefix, limit, reverse, key_only=False):
178
+ yield pair.key, pair.value
179
+
180
+ def scan_keys(
181
+ self,
182
+ start: bytes | None = None,
183
+ end: bytes | None = None,
184
+ *,
185
+ prefix: bytes | None = None,
186
+ limit: int | None = None,
187
+ reverse: bool = False,
188
+ ) -> Iterator[bytes]:
189
+ """Like :meth:`scan`, but yields only keys, without fetching values."""
190
+ for pair in self._scan(start, end, prefix, limit, reverse, key_only=True):
191
+ yield pair.key
192
+
193
+ def checksum(
194
+ self,
195
+ start: bytes | None = None,
196
+ end: bytes | None = None,
197
+ *,
198
+ prefix: bytes | None = None,
199
+ ) -> Checksum:
200
+ """Fingerprint the keys in ``[start, end)``, or those starting with *prefix*.
201
+
202
+ The servers compute it in place, so no data crosses the network.
203
+ """
204
+ bounds = key_range(start, end, prefix)
205
+ if bounds is None:
206
+ return Checksum(0, 0, 0)
207
+ responses = self._execute_range(
208
+ *bounds,
209
+ method=RAW_CHECKSUM,
210
+ build=lambda lo, hi: kvrpcpb.RawChecksumRequest(
211
+ ranges=[kvrpcpb.KeyRange(start_key=lo, end_key=hi)]
212
+ ),
213
+ )
214
+ crc64_xor = keys = size = 0
215
+ for response in responses:
216
+ crc64_xor ^= response.checksum
217
+ keys += response.total_kvs
218
+ size += response.total_bytes
219
+ return Checksum(crc64_xor, keys, size)
220
+
221
+ # -- Writes --------------------------------------------------------------
222
+
223
+ def put(self, key: bytes, value: bytes, *, ttl: float | timedelta | None = None) -> None:
224
+ """Set *key* to *value*.
225
+
226
+ :param ttl: Expire the key after this many seconds (rounded up to a
227
+ whole second) or this :class:`~datetime.timedelta`. Requires
228
+ ``storage.enable-ttl`` on the TiKV servers.
229
+ """
230
+ request = kvrpcpb.RawPutRequest(
231
+ key=as_key(key), value=as_value(value), ttl=ttl_seconds(ttl), for_cas=self.atomic
232
+ )
233
+ self._execute(request.key, RAW_PUT, request)
234
+
235
+ def put_many(
236
+ self,
237
+ items: Mapping[bytes, bytes] | Iterable[tuple[bytes, bytes]],
238
+ *,
239
+ ttl: float | timedelta | None = None,
240
+ ) -> None:
241
+ """Set many keys at once, from a mapping or ``(key, value)`` pairs.
242
+
243
+ Writes are atomic per region but not across regions.
244
+
245
+ :param ttl: Expiry applied to every key; see :meth:`put`.
246
+ """
247
+ entries = sorted((as_key(k), as_value(v)) for k, v in dict(items).items())
248
+ ttls = [ttl_seconds(ttl)] if ttl is not None else []
249
+
250
+ def build(chunk: list[tuple[bytes, bytes]]) -> kvrpcpb.RawBatchPutRequest:
251
+ return kvrpcpb.RawBatchPutRequest(
252
+ pairs=[kvrpcpb.KvPair(key=k, value=v) for k, v in chunk], ttls=ttls, for_cas=self.atomic
253
+ )
254
+
255
+ self._execute_batch(entries, key=_first, method=RAW_BATCH_PUT, build=build, size=_pair_size)
256
+
257
+ def delete(self, key: bytes) -> None:
258
+ """Delete *key*. Deleting a missing key is not an error."""
259
+ request = kvrpcpb.RawDeleteRequest(key=as_key(key), for_cas=self.atomic)
260
+ self._execute(request.key, RAW_DELETE, request)
261
+
262
+ def delete_many(self, keys: Iterable[bytes]) -> None:
263
+ """Delete many keys at once. Missing keys are ignored."""
264
+ self._execute_batch(
265
+ sorted(set(map(as_key, keys))),
266
+ key=_identity,
267
+ method=RAW_BATCH_DELETE,
268
+ build=lambda chunk: kvrpcpb.RawBatchDeleteRequest(keys=chunk, for_cas=self.atomic),
269
+ )
270
+
271
+ def delete_range(
272
+ self,
273
+ start: bytes | None = None,
274
+ end: bytes | None = None,
275
+ *,
276
+ prefix: bytes | None = None,
277
+ ) -> None:
278
+ """Delete every key in ``[start, end)``, or every key starting with *prefix*.
279
+
280
+ ``end=None`` deletes through the end of the key space. To guard
281
+ against accidents, *start* or a non-empty *prefix* is required:
282
+ wiping everything takes an explicit ``delete_range(b"")``.
283
+ """
284
+ if self.atomic:
285
+ raise TiKVError("delete_range is not supported by clients in atomic mode")
286
+ if prefix is None and start is None:
287
+ raise TypeError("delete_range() requires start or prefix")
288
+ if prefix is not None and not prefix:
289
+ raise ValueError('an empty prefix matches every key; use delete_range(b"") to wipe them all')
290
+ bounds = key_range(start, end, prefix)
291
+ if bounds is not None:
292
+ self._execute_range(
293
+ *bounds,
294
+ method=RAW_DELETE_RANGE,
295
+ build=lambda lo, hi: kvrpcpb.RawDeleteRangeRequest(start_key=lo, end_key=hi),
296
+ )
297
+
298
+ def compare_and_swap(
299
+ self,
300
+ key: bytes,
301
+ expected: bytes | None,
302
+ new: bytes,
303
+ *,
304
+ ttl: float | timedelta | None = None,
305
+ ) -> CasResult:
306
+ """Set *key* to *new* only if its current value is *expected*.
307
+
308
+ Pass ``expected=None`` to require that *key* does not exist yet.
309
+ Requires a client created with ``atomic=True``::
310
+
311
+ swapped, previous = client.compare_and_swap(b"lock", None, b"owner-1")
312
+
313
+ :raises UndeterminedError: if the request failed in flight, so the
314
+ swap may or may not have happened.
315
+ """
316
+ if not self.atomic:
317
+ raise TiKVError("compare_and_swap requires a client created with atomic=True")
318
+ request = kvrpcpb.RawCASRequest(
319
+ key=as_key(key),
320
+ value=as_value(new),
321
+ previous_not_exist=expected is None,
322
+ previous_value=b"" if expected is None else as_value(expected),
323
+ ttl=ttl_seconds(ttl),
324
+ )
325
+ response = self._execute(request.key, RAW_COMPARE_AND_SWAP, request, idempotent=False)
326
+ return CasResult(response.succeed, None if response.previous_not_exist else response.previous_value)
327
+
328
+ # -- Request routing -----------------------------------------------------
329
+
330
+ def _execute(
331
+ self, key: bytes, method: Method[Response], request: Message, *, idempotent: bool = True
332
+ ) -> Response:
333
+ """Send a single-key request to the region owning *key*."""
334
+ backoff = Backoff(self.timeout)
335
+ while True:
336
+ region = self._locate(key, backoff)
337
+ response = self._send(region, method, request, backoff, idempotent=idempotent)
338
+ if response is not None:
339
+ return response
340
+
341
+ def _execute_batch(
342
+ self,
343
+ items: Sequence[T],
344
+ *,
345
+ key: Callable[[T], bytes],
346
+ method: Method[Response],
347
+ build: Callable[[list[T]], Message],
348
+ size: Callable[[T], int] | None = None,
349
+ backoff: Backoff | None = None,
350
+ ) -> list[Response]:
351
+ """Split key-sorted *items* by region and send one request per chunk."""
352
+ backoff = backoff or Backoff(self.timeout)
353
+ batches = self._plan_batches(items, key, size, backoff)
354
+
355
+ def run(batch: tuple[Region, list[T]]) -> list[Response]:
356
+ region, chunk = batch
357
+ retry = backoff.fork()
358
+ while True:
359
+ response = self._send(region, method, build(chunk), retry)
360
+ if response is not None:
361
+ return [response]
362
+ # The region changed under us: re-route this chunk.
363
+ replanned = self._plan_batches(chunk, key, size, retry)
364
+ if len(replanned) > 1: # it now spans several regions
365
+ return self._execute_batch(
366
+ chunk, key=key, method=method, build=build, size=size, backoff=retry
367
+ )
368
+ region, chunk = replanned[0]
369
+
370
+ results = map_concurrently(run, batches)
371
+ return [response for group in results for response in group]
372
+
373
+ def _plan_batches(
374
+ self, items: Sequence[T], key: Callable[[T], bytes], size: Callable[[T], int] | None, backoff: Backoff
375
+ ) -> list[tuple[Region, list[T]]]:
376
+ """Group key-sorted *items* by region, in chunks small enough for one request."""
377
+ batches: list[tuple[Region, list[T]]] = []
378
+ i = 0
379
+ while i < len(items):
380
+ region = self._locate(key(items[i]), backoff, until=key(items[-1]) + b"\0")
381
+ j = i + 1
382
+ while j < len(items) and region.contains(key(items[j])):
383
+ j += 1
384
+ for chunk in chunked(items[i:j], max_count=BATCH_MAX_KEYS, max_bytes=BATCH_MAX_BYTES, size=size):
385
+ batches.append((region, chunk))
386
+ i = j
387
+ self._resolve_stores([region for region, _ in batches], backoff)
388
+ return batches
389
+
390
+ def _execute_range(
391
+ self,
392
+ start: bytes,
393
+ end: bytes,
394
+ *,
395
+ method: Method[Response],
396
+ build: Callable[[bytes, bytes], Message],
397
+ backoff: Backoff | None = None,
398
+ ) -> list[Response]:
399
+ """Send one request per region overlapping ``[start, end)``."""
400
+ backoff = backoff or Backoff(self.timeout)
401
+ spans = self._plan_spans(start, end, backoff)
402
+
403
+ def run(span: tuple[Region, bytes, bytes]) -> list[Response]:
404
+ region, lo, hi = span
405
+ retry = backoff.fork()
406
+ while True:
407
+ response = self._send(region, method, build(lo, hi), retry)
408
+ if response is not None:
409
+ return [response]
410
+ replanned = self._plan_spans(lo, hi, retry)
411
+ if len(replanned) > 1:
412
+ return self._execute_range(lo, hi, method=method, build=build, backoff=retry)
413
+ region, lo, hi = replanned[0]
414
+
415
+ results = map_concurrently(run, spans)
416
+ return [response for group in results for response in group]
417
+
418
+ def _plan_spans(
419
+ self, start: bytes, end: bytes, backoff: Backoff
420
+ ) -> list[tuple[Region, bytes, bytes]]:
421
+ """Split ``[start, end)`` at region boundaries."""
422
+ spans: list[tuple[Region, bytes, bytes]] = []
423
+ cursor = start
424
+ while True:
425
+ region = self._locate(cursor, backoff, until=end)
426
+ spans.append((region, cursor, min_end(end, region.end)))
427
+ if not region.end or (end and region.end >= end):
428
+ break
429
+ cursor = region.end
430
+ self._resolve_stores([region for region, _, _ in spans], backoff)
431
+ return spans
432
+
433
+ def _scan(
434
+ self,
435
+ start: bytes | None,
436
+ end: bytes | None,
437
+ prefix: bytes | None,
438
+ limit: int | None,
439
+ reverse: bool,
440
+ *,
441
+ key_only: bool,
442
+ ) -> Iterator[kvrpcpb.KvPair]:
443
+ if limit is not None and limit < 0:
444
+ raise ValueError(f"limit must not be negative, got {limit!r}")
445
+ bounds = key_range(start, end, prefix)
446
+ if bounds is None:
447
+ return
448
+ lower, upper = bounds
449
+ remaining = math.inf if limit is None else limit
450
+ # Forward scans move a lower bound up; reverse scans move an upper bound down.
451
+ cursor = (upper or MAX_KEY) if reverse else lower
452
+
453
+ while remaining > 0:
454
+ page = int(min(remaining, SCAN_PAGE_SIZE))
455
+ backoff = Backoff(self.timeout)
456
+ while True:
457
+ if reverse:
458
+ region = self._locate_end(cursor, backoff)
459
+ request = kvrpcpb.RawScanRequest(
460
+ start_key=cursor, end_key=max(lower, region.start), reverse=True
461
+ )
462
+ else:
463
+ region = self._locate(cursor, backoff)
464
+ request = kvrpcpb.RawScanRequest(start_key=cursor, end_key=min_end(upper, region.end))
465
+ request.limit = page
466
+ request.key_only = key_only
467
+ response = self._send(region, RAW_SCAN, request, backoff)
468
+ if response is not None:
469
+ break
470
+
471
+ for pair in _checked(response.kvs):
472
+ yield pair
473
+ remaining -= len(response.kvs)
474
+
475
+ if len(response.kvs) == page: # the region may hold more
476
+ last = response.kvs[-1].key
477
+ if reverse:
478
+ if last <= lower:
479
+ return
480
+ cursor = last
481
+ else:
482
+ cursor = last + b"\0"
483
+ if upper and cursor >= upper:
484
+ return
485
+ elif reverse:
486
+ if region.start <= lower:
487
+ return
488
+ cursor = region.start
489
+ else:
490
+ if not region.end or (upper and region.end >= upper):
491
+ return
492
+ cursor = region.end
493
+
494
+ def _locate(self, key: bytes, backoff: Backoff, *, until: bytes | None = None) -> Region:
495
+ """The region containing *key*.
496
+
497
+ On a cache miss, *until* lets PD return every region up to that key at
498
+ once, which saves round trips when walking a range or a sorted batch.
499
+ """
500
+ region = self._cache.find(key)
501
+ if region is not None:
502
+ return region
503
+ if until is None or (until and until <= key):
504
+ return self._pd.region(key, backoff)
505
+ regions = self._pd.scan_regions(key, until, REGION_PREFETCH, backoff)
506
+ if regions and regions[0].contains(key):
507
+ return regions[0]
508
+ return self._pd.region(key, backoff)
509
+
510
+ def _resolve_stores(self, regions: Iterable[Region], backoff: Backoff) -> None:
511
+ """Look up leader store addresses once, before fanning out requests."""
512
+ for store_id in sorted({r.leader.store_id for r in regions if r.leader is not None}):
513
+ self._pd.store_address(store_id, backoff)
514
+
515
+ def _locate_end(self, key: bytes, backoff: Backoff) -> Region:
516
+ """The region in which *key* is a valid exclusive upper bound."""
517
+ region = self._cache.find_end(key)
518
+ if region is not None:
519
+ return region
520
+ return self._pd.region_by_end(key, backoff)
521
+
522
+ def _send(
523
+ self,
524
+ region: Region,
525
+ method: Method[Response],
526
+ request: Message,
527
+ backoff: Backoff,
528
+ *,
529
+ idempotent: bool = True,
530
+ ) -> Response | None:
531
+ """Send *request* to the leader of *region*.
532
+
533
+ Returns ``None`` when the request must be re-routed, after updating
534
+ the region cache and backing off as needed.
535
+ """
536
+ if self._closed:
537
+ raise TiKVError("the client is closed")
538
+ if region.leader is None: # PD knows no leader: an election is under way
539
+ self._cache.invalidate(region)
540
+ sleep(backoff.delay(Wait.REGION_SCHEDULING, RegionError(f"{region} has no leader")))
541
+ return None
542
+
543
+ store_id = region.leader.store_id
544
+ address = self._pd.store_address(store_id, backoff)
545
+ request.context.CopyFrom(region.context()) # type: ignore[attr-defined]
546
+ try:
547
+ response = self._transport.call(address, method, request, backoff.remaining())
548
+ except grpc.RpcError as error:
549
+ if not is_retryable(error):
550
+ raise ServerError(f"{method.path} failed: {describe(error)}") from error
551
+ if not idempotent:
552
+ raise UndeterminedError(f"{method.path} to {address} failed: {describe(error)}") from error
553
+ self._cache.invalidate(region)
554
+ self._cache.forget_store(store_id)
555
+ sleep(backoff.delay(Wait.TIKV_RPC, error))
556
+ return None
557
+
558
+ if response.HasField("region_error"):
559
+ region_error = response.region_error # type: ignore[attr-defined]
560
+ wait, hint = on_region_error(self._cache, region, region_error, idempotent=idempotent)
561
+ cause = RegionError(describe_region_error(region, region_error))
562
+ sleep(backoff.delay(wait, cause, hint=hint))
563
+ return None
564
+ if getattr(response, "error", ""):
565
+ raise ServerError(response.error) # type: ignore[attr-defined]
566
+ return response
567
+
568
+
569
+ def _checked(pairs: Iterable[kvrpcpb.KvPair]) -> Iterable[kvrpcpb.KvPair]:
570
+ for pair in pairs:
571
+ if pair.HasField("error"):
572
+ raise ServerError(pair.error.abort or pair.error.retryable or str(pair.error))
573
+ return pairs
574
+
575
+
576
+ def _identity(key: bytes) -> bytes:
577
+ return key
578
+
579
+
580
+ def _first(pair: tuple[bytes, bytes]) -> bytes:
581
+ return pair[0]
582
+
583
+
584
+ def _pair_size(pair: tuple[bytes, bytes]) -> int:
585
+ return len(pair[0]) + len(pair[1])