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/__init__.py +44 -0
- tikv/_async/__init__.py +1 -0
- tikv/_async/_client.py +584 -0
- tikv/_async/_compat.py +53 -0
- tikv/_async/_pd.py +167 -0
- tikv/_async/_transport.py +52 -0
- tikv/_backoff.py +78 -0
- tikv/_errors.py +53 -0
- tikv/_keys.py +109 -0
- tikv/_proto/__init__.py +2 -0
- tikv/_proto/errorpb_pb2.py +68 -0
- tikv/_proto/errorpb_pb2.pyi +200 -0
- tikv/_proto/kvrpcpb_pb2.py +85 -0
- tikv/_proto/kvrpcpb_pb2.pyi +341 -0
- tikv/_proto/metapb_pb2.py +39 -0
- tikv/_proto/metapb_pb2.pyi +92 -0
- tikv/_proto/pdpb_pb2.py +56 -0
- tikv/_proto/pdpb_pb2.pyi +160 -0
- tikv/_region.py +228 -0
- tikv/_rpc.py +109 -0
- tikv/_sync/__init__.py +2 -0
- tikv/_sync/_client.py +585 -0
- tikv/_sync/_compat.py +43 -0
- tikv/_sync/_pd.py +168 -0
- tikv/_sync/_transport.py +53 -0
- tikv/_types.py +34 -0
- tikv/py.typed +0 -0
- tikv_python-0.1.0.dist-info/METADATA +387 -0
- tikv_python-0.1.0.dist-info/RECORD +31 -0
- tikv_python-0.1.0.dist-info/WHEEL +4 -0
- tikv_python-0.1.0.dist-info/licenses/LICENSE +202 -0
tikv/_async/_pd.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Client for the Placement Driver (PD), TiKV's cluster metadata service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
from google.protobuf.message import Message
|
|
9
|
+
|
|
10
|
+
from .._backoff import Backoff, Wait, describe, log
|
|
11
|
+
from .._errors import PDError
|
|
12
|
+
from .._proto import pdpb_pb2 as pdpb
|
|
13
|
+
from .._region import Region, RegionCache, make_region
|
|
14
|
+
from .._rpc import (
|
|
15
|
+
PD_GET_MEMBERS,
|
|
16
|
+
PD_GET_PREV_REGION,
|
|
17
|
+
PD_GET_REGION,
|
|
18
|
+
PD_GET_STORE,
|
|
19
|
+
PD_SCAN_REGIONS,
|
|
20
|
+
Method,
|
|
21
|
+
Response,
|
|
22
|
+
is_retryable,
|
|
23
|
+
strip_scheme,
|
|
24
|
+
)
|
|
25
|
+
from ._compat import Lock, sleep
|
|
26
|
+
from ._transport import Transport
|
|
27
|
+
|
|
28
|
+
CALLER_ID = "tikv-python"
|
|
29
|
+
|
|
30
|
+
# PD answers metadata queries quickly; a slower reply means a sick leader,
|
|
31
|
+
# so give up on it early and rediscover instead of burning the whole budget.
|
|
32
|
+
RPC_TIMEOUT = 3.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PDClient:
|
|
36
|
+
"""Finds the PD leader and asks it where regions and stores are."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, endpoints: Iterable[str], transport: Transport, cache: RegionCache) -> None:
|
|
39
|
+
self._endpoints = list(dict.fromkeys(map(strip_scheme, endpoints)))
|
|
40
|
+
self._transport = transport
|
|
41
|
+
self._cache = cache
|
|
42
|
+
self._lock = Lock()
|
|
43
|
+
self._leader: str | None = None
|
|
44
|
+
self.cluster_id = 0
|
|
45
|
+
|
|
46
|
+
async def region(self, key: bytes, backoff: Backoff) -> Region:
|
|
47
|
+
"""The region containing *key*."""
|
|
48
|
+
while True:
|
|
49
|
+
region = await self._fetch_region(PD_GET_REGION, key, backoff)
|
|
50
|
+
if region is not None:
|
|
51
|
+
return region
|
|
52
|
+
# PD has no region for this key yet, e.g. right after bootstrap.
|
|
53
|
+
await sleep(backoff.delay(Wait.REGION_MISS, PDError(f"no region found for key {key!r}")))
|
|
54
|
+
|
|
55
|
+
async def region_by_end(self, key: bytes, backoff: Backoff) -> Region:
|
|
56
|
+
"""The region in which *key* is a valid exclusive upper bound."""
|
|
57
|
+
while True:
|
|
58
|
+
region = await self.region(key, backoff)
|
|
59
|
+
if region.start != key or not key:
|
|
60
|
+
return region
|
|
61
|
+
# *key* opens this region, so the keys below it live in the one
|
|
62
|
+
# before. GetPrevRegion returns exactly that (like client-go).
|
|
63
|
+
previous = await self._fetch_region(PD_GET_PREV_REGION, key, backoff)
|
|
64
|
+
if previous is not None:
|
|
65
|
+
return previous
|
|
66
|
+
# The regions merged in between: start over.
|
|
67
|
+
await sleep(backoff.delay(Wait.REGION_MISS, PDError(f"no region found before key {key!r}")))
|
|
68
|
+
|
|
69
|
+
async def _fetch_region(
|
|
70
|
+
self, method: Method[pdpb.GetRegionResponse], key: bytes, backoff: Backoff
|
|
71
|
+
) -> Region | None:
|
|
72
|
+
response = await self._call(method, pdpb.GetRegionRequest(region_key=key), backoff)
|
|
73
|
+
if not response.HasField("region"):
|
|
74
|
+
return None
|
|
75
|
+
region = make_region(response.region, response.leader)
|
|
76
|
+
self._cache.put(region)
|
|
77
|
+
return region
|
|
78
|
+
|
|
79
|
+
async def scan_regions(self, start: bytes, end: bytes, limit: int, backoff: Backoff) -> list[Region]:
|
|
80
|
+
"""Up to *limit* consecutive regions overlapping ``[start, end)``."""
|
|
81
|
+
request = pdpb.ScanRegionsRequest(start_key=start, end_key=end, limit=limit)
|
|
82
|
+
response = await self._call(PD_SCAN_REGIONS, request, backoff)
|
|
83
|
+
if response.regions:
|
|
84
|
+
regions = [make_region(r.region, r.leader) for r in response.regions]
|
|
85
|
+
else: # older PD versions only fill the legacy fields
|
|
86
|
+
regions = [
|
|
87
|
+
make_region(m, leader)
|
|
88
|
+
for m, leader in zip(response.region_metas, response.leaders, strict=False)
|
|
89
|
+
]
|
|
90
|
+
for region in regions:
|
|
91
|
+
self._cache.put(region)
|
|
92
|
+
return regions
|
|
93
|
+
|
|
94
|
+
async def store_address(self, store_id: int, backoff: Backoff) -> str:
|
|
95
|
+
"""The client-facing address of TiKV store *store_id*."""
|
|
96
|
+
address = self._cache.store_address(store_id)
|
|
97
|
+
if address is None:
|
|
98
|
+
response = await self._call(PD_GET_STORE, pdpb.GetStoreRequest(store_id=store_id), backoff)
|
|
99
|
+
address = strip_scheme(response.store.address)
|
|
100
|
+
self._cache.set_store_address(store_id, address)
|
|
101
|
+
return address
|
|
102
|
+
|
|
103
|
+
async def _call(self, method: Method[Response], request: Message, backoff: Backoff) -> Response:
|
|
104
|
+
"""Send *request* to the PD leader, following leader changes."""
|
|
105
|
+
while True:
|
|
106
|
+
leader = await self._leader_address(backoff)
|
|
107
|
+
request.header.cluster_id = self.cluster_id # type: ignore[attr-defined]
|
|
108
|
+
request.header.caller_id = CALLER_ID # type: ignore[attr-defined]
|
|
109
|
+
timeout = min(RPC_TIMEOUT, backoff.remaining())
|
|
110
|
+
try:
|
|
111
|
+
response = await self._transport.call(leader, method, request, timeout)
|
|
112
|
+
except grpc.RpcError as error:
|
|
113
|
+
if not is_retryable(error):
|
|
114
|
+
raise PDError(f"{method.path} failed: {describe(error)}") from error
|
|
115
|
+
self._leader = None # the leader may have moved; rediscover it
|
|
116
|
+
await sleep(backoff.delay(Wait.PD_RPC, error))
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
header_error = response.header.error # type: ignore[attr-defined]
|
|
120
|
+
if header_error.type == pdpb.OK:
|
|
121
|
+
return response
|
|
122
|
+
failure = PDError(
|
|
123
|
+
f"{method.path} failed: {pdpb.ErrorType.Name(header_error.type)}: {header_error.message}"
|
|
124
|
+
)
|
|
125
|
+
if header_error.type != pdpb.REGION_NOT_FOUND:
|
|
126
|
+
raise failure
|
|
127
|
+
await sleep(backoff.delay(Wait.REGION_MISS, failure))
|
|
128
|
+
|
|
129
|
+
async def _leader_address(self, backoff: Backoff) -> str:
|
|
130
|
+
leader = self._leader
|
|
131
|
+
if leader is not None:
|
|
132
|
+
return leader
|
|
133
|
+
async with self._lock:
|
|
134
|
+
while self._leader is None:
|
|
135
|
+
await self._discover(backoff)
|
|
136
|
+
return self._leader
|
|
137
|
+
|
|
138
|
+
async def _discover(self, backoff: Backoff) -> None:
|
|
139
|
+
"""Ask the known PD members who the leader is."""
|
|
140
|
+
cause: Exception = PDError("no PD endpoint answered")
|
|
141
|
+
rejected: list[str] = []
|
|
142
|
+
for endpoint in list(self._endpoints):
|
|
143
|
+
timeout = min(RPC_TIMEOUT, backoff.remaining())
|
|
144
|
+
try:
|
|
145
|
+
response = await self._transport.call(
|
|
146
|
+
endpoint, PD_GET_MEMBERS, pdpb.GetMembersRequest(), timeout
|
|
147
|
+
)
|
|
148
|
+
except grpc.RpcError as error:
|
|
149
|
+
if not is_retryable(error):
|
|
150
|
+
rejected.append(f"{endpoint} is not a PD server: {describe(error)}")
|
|
151
|
+
cause = error
|
|
152
|
+
continue
|
|
153
|
+
if not response.leader.client_urls:
|
|
154
|
+
cause = PDError(f"PD at {endpoint} reports no leader (election in progress?)")
|
|
155
|
+
continue
|
|
156
|
+
# Learn about every member so we survive losing the seed endpoints.
|
|
157
|
+
for member in response.members:
|
|
158
|
+
for url in member.client_urls:
|
|
159
|
+
if strip_scheme(url) not in self._endpoints:
|
|
160
|
+
self._endpoints.append(strip_scheme(url))
|
|
161
|
+
self.cluster_id = response.header.cluster_id
|
|
162
|
+
self._leader = strip_scheme(response.leader.client_urls[0])
|
|
163
|
+
log.debug("PD leader is %s (cluster %d)", self._leader, self.cluster_id)
|
|
164
|
+
return
|
|
165
|
+
if len(rejected) == len(self._endpoints): # retrying cannot help
|
|
166
|
+
raise PDError("; ".join(rejected)) from cause
|
|
167
|
+
await sleep(backoff.delay(Wait.PD_RPC, cause))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""A pool of gRPC channels, one per server address."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
from google.protobuf.message import Message
|
|
10
|
+
|
|
11
|
+
from .._rpc import CHANNEL_OPTIONS, Method, Response
|
|
12
|
+
from ._compat import Channel, open_channel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Transport:
|
|
16
|
+
"""Sends unary requests to PD and TiKV servers over shared channels."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, credentials: grpc.ChannelCredentials | None = None) -> None:
|
|
19
|
+
self._credentials = credentials
|
|
20
|
+
self._lock = threading.Lock() # never held across an await
|
|
21
|
+
self._channels: dict[str, Channel] = {}
|
|
22
|
+
self._stubs: dict[tuple[str, str], Any] = {}
|
|
23
|
+
|
|
24
|
+
async def call(
|
|
25
|
+
self, address: str, method: Method[Response], request: Message, timeout: float
|
|
26
|
+
) -> Response:
|
|
27
|
+
stub = self._stubs.get((address, method.path))
|
|
28
|
+
if stub is None:
|
|
29
|
+
stub = self._channel(address).unary_unary(
|
|
30
|
+
method.path,
|
|
31
|
+
request_serializer=type(request).SerializeToString,
|
|
32
|
+
response_deserializer=method.response.FromString,
|
|
33
|
+
)
|
|
34
|
+
self._stubs[address, method.path] = stub
|
|
35
|
+
response: Response = await stub(request, timeout=timeout)
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
def _channel(self, address: str) -> Channel:
|
|
39
|
+
with self._lock:
|
|
40
|
+
channel = self._channels.get(address)
|
|
41
|
+
if channel is None:
|
|
42
|
+
channel = open_channel(address, self._credentials, CHANNEL_OPTIONS)
|
|
43
|
+
self._channels[address] = channel
|
|
44
|
+
return channel
|
|
45
|
+
|
|
46
|
+
async def close(self) -> None:
|
|
47
|
+
with self._lock:
|
|
48
|
+
channels = list(self._channels.values())
|
|
49
|
+
self._channels.clear()
|
|
50
|
+
self._stubs.clear()
|
|
51
|
+
for channel in channels:
|
|
52
|
+
await channel.close()
|
tikv/_backoff.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Exponential backoff bounded by a per-operation deadline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
import logging
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from ._errors import TimeoutError
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("tikv")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Wait(enum.Enum):
|
|
16
|
+
"""Why an operation is backing off; each reason has its own schedule.
|
|
17
|
+
|
|
18
|
+
Values are ``(base, cap)`` in seconds, following client-go's defaults.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
REGION_MISS = (0.002, 0.5) # stale region cache: refresh and retry soon
|
|
22
|
+
REGION_SCHEDULING = (0.002, 0.5) # leader election, merge, split in progress
|
|
23
|
+
TIKV_RPC = (0.1, 2.0) # store unreachable
|
|
24
|
+
PD_RPC = (0.1, 3.0) # placement driver unreachable or electing a leader
|
|
25
|
+
SERVER_BUSY = (2.0, 10.0) # store is overloaded
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Backoff:
|
|
29
|
+
"""Tracks retries of one logical operation against its deadline."""
|
|
30
|
+
|
|
31
|
+
__slots__ = ("_attempts", "deadline", "timeout")
|
|
32
|
+
|
|
33
|
+
def __init__(self, timeout: float, *, deadline: float | None = None) -> None:
|
|
34
|
+
self.timeout = timeout
|
|
35
|
+
self.deadline = time.monotonic() + timeout if deadline is None else deadline
|
|
36
|
+
self._attempts: dict[Wait, int] = {}
|
|
37
|
+
|
|
38
|
+
def remaining(self) -> float:
|
|
39
|
+
"""Seconds left before the deadline, never negative."""
|
|
40
|
+
return max(0.0, self.deadline - time.monotonic())
|
|
41
|
+
|
|
42
|
+
def fork(self) -> Backoff:
|
|
43
|
+
"""A backoff for a sub-request: same deadline and retry history.
|
|
44
|
+
|
|
45
|
+
The fork escalates independently from here on, so concurrent
|
|
46
|
+
sub-requests do not inflate each other's delays.
|
|
47
|
+
"""
|
|
48
|
+
forked = Backoff(self.timeout, deadline=self.deadline)
|
|
49
|
+
forked._attempts = dict(self._attempts)
|
|
50
|
+
return forked
|
|
51
|
+
|
|
52
|
+
def delay(self, wait: Wait, cause: BaseException, *, hint: float = 0.0) -> float:
|
|
53
|
+
"""Return how long to sleep before the next attempt.
|
|
54
|
+
|
|
55
|
+
Raises :class:`~tikv.TimeoutError` from *cause* when sleeping would
|
|
56
|
+
overrun the deadline, so callers never retry past their budget.
|
|
57
|
+
"""
|
|
58
|
+
base, cap = wait.value
|
|
59
|
+
attempt = self._attempts.get(wait, 0)
|
|
60
|
+
self._attempts[wait] = attempt + 1
|
|
61
|
+
ceiling = min(cap, base * 2.0**attempt)
|
|
62
|
+
# "Equal jitter": at least half the ceiling, so retries stay spread out.
|
|
63
|
+
delay = max(hint, ceiling / 2 + random.uniform(0, ceiling / 2))
|
|
64
|
+
if delay >= self.remaining():
|
|
65
|
+
raise TimeoutError(
|
|
66
|
+
f"operation did not complete within {self.timeout:g}s: {describe(cause)}"
|
|
67
|
+
) from cause
|
|
68
|
+
log.debug("retrying in %.3fs (%s): %s", delay, wait.name.lower(), describe(cause))
|
|
69
|
+
return delay
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def describe(error: BaseException) -> str:
|
|
73
|
+
"""A one-line description of *error*, including gRPC status details."""
|
|
74
|
+
details = getattr(error, "details", None)
|
|
75
|
+
code = getattr(error, "code", None)
|
|
76
|
+
if callable(details) and callable(code):
|
|
77
|
+
return f"gRPC {code().name}: {details()}"
|
|
78
|
+
return str(error) or type(error).__name__
|
tikv/_errors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Exceptions raised by the TiKV client.
|
|
2
|
+
|
|
3
|
+
Every exception derives from :class:`TiKVError`, so ``except tikv.TiKVError``
|
|
4
|
+
catches anything the library raises on purpose. Transient failures (leader
|
|
5
|
+
changes, region splits, busy servers, unreachable stores) are retried
|
|
6
|
+
internally and only surface as :class:`TimeoutError` once the operation's time
|
|
7
|
+
budget runs out.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import builtins
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PDError",
|
|
16
|
+
"RegionError",
|
|
17
|
+
"ServerError",
|
|
18
|
+
"TiKVError",
|
|
19
|
+
"TimeoutError",
|
|
20
|
+
"UndeterminedError",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TiKVError(Exception):
|
|
25
|
+
"""Base class for all errors raised by this library."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TimeoutError(TiKVError, builtins.TimeoutError):
|
|
29
|
+
"""The operation did not complete within the client's ``timeout``.
|
|
30
|
+
|
|
31
|
+
The last transient error, if any, is attached as ``__cause__``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PDError(TiKVError):
|
|
36
|
+
"""The Placement Driver rejected a request."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ServerError(TiKVError):
|
|
40
|
+
"""A TiKV server rejected a request, for example because a key is too large."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RegionError(TiKVError):
|
|
44
|
+
"""A region error that retrying cannot resolve, such as an oversized write."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UndeterminedError(TiKVError):
|
|
48
|
+
"""A non-idempotent request failed in flight; it may or may not have been applied.
|
|
49
|
+
|
|
50
|
+
Raised only by :meth:`RawClient.compare_and_swap`, which is never retried
|
|
51
|
+
blindly: replaying a swap that already succeeded would report a spurious
|
|
52
|
+
failure. Read the key to find out what happened.
|
|
53
|
+
"""
|
tikv/_keys.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Argument validation and key-range arithmetic shared by both clients.
|
|
2
|
+
|
|
3
|
+
Internally an empty ``end`` key means "+inf", as on the wire. The public API
|
|
4
|
+
uses ``None`` for that instead, because ``b""`` is itself a valid key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
11
|
+
from datetime import timedelta
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def as_bytes(value: object, what: str) -> bytes:
|
|
18
|
+
if isinstance(value, bytes):
|
|
19
|
+
return value
|
|
20
|
+
if isinstance(value, (bytearray, memoryview)):
|
|
21
|
+
return bytes(value)
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
raise TypeError(f"{what} must be bytes, not str; encode it first, e.g. {what}.encode()")
|
|
24
|
+
raise TypeError(f"{what} must be bytes, not {type(value).__name__}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def as_key(key: object) -> bytes:
|
|
28
|
+
return as_bytes(key, "key")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def as_value(value: object) -> bytes:
|
|
32
|
+
return as_bytes(value, "value")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ttl_seconds(ttl: float | timedelta | None) -> int:
|
|
36
|
+
"""Convert a TTL to whole seconds, rounding up; 0 means "no expiry"."""
|
|
37
|
+
if ttl is None:
|
|
38
|
+
return 0
|
|
39
|
+
if isinstance(ttl, timedelta):
|
|
40
|
+
seconds = ttl.total_seconds()
|
|
41
|
+
elif isinstance(ttl, (int, float)) and not isinstance(ttl, bool):
|
|
42
|
+
seconds = float(ttl)
|
|
43
|
+
else:
|
|
44
|
+
raise TypeError(f"ttl must be a number of seconds or a timedelta, not {type(ttl).__name__}")
|
|
45
|
+
if not seconds > 0 or math.isinf(seconds):
|
|
46
|
+
raise ValueError(f"ttl must be a positive, finite duration, got {ttl!r}")
|
|
47
|
+
return math.ceil(seconds)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def prefix_end(prefix: bytes) -> bytes:
|
|
51
|
+
"""The smallest key greater than every key starting with *prefix*.
|
|
52
|
+
|
|
53
|
+
Returns ``b""`` (+inf) when no such key exists, e.g. for ``b"\\xff\\xff"``.
|
|
54
|
+
"""
|
|
55
|
+
stripped = prefix.rstrip(b"\xff")
|
|
56
|
+
if not stripped:
|
|
57
|
+
return b""
|
|
58
|
+
return stripped[:-1] + bytes([stripped[-1] + 1])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def key_range(start: object | None, end: object | None, prefix: object | None) -> tuple[bytes, bytes] | None:
|
|
62
|
+
"""Resolve public ``start``/``end``/``prefix`` arguments to ``(start, end)``.
|
|
63
|
+
|
|
64
|
+
Returns ``None`` when the range is empty.
|
|
65
|
+
"""
|
|
66
|
+
if prefix is not None:
|
|
67
|
+
if start is not None or end is not None:
|
|
68
|
+
raise ValueError("pass either prefix or start/end, not both")
|
|
69
|
+
lo = as_bytes(prefix, "prefix")
|
|
70
|
+
return lo, prefix_end(lo)
|
|
71
|
+
lo = b"" if start is None else as_bytes(start, "start")
|
|
72
|
+
if end is None:
|
|
73
|
+
return lo, b""
|
|
74
|
+
hi = as_bytes(end, "end")
|
|
75
|
+
return (lo, hi) if lo < hi else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def min_end(a: bytes, b: bytes) -> bytes:
|
|
79
|
+
"""The smaller of two exclusive upper bounds, where ``b""`` is +inf."""
|
|
80
|
+
if not a:
|
|
81
|
+
return b
|
|
82
|
+
if not b:
|
|
83
|
+
return a
|
|
84
|
+
return min(a, b)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def chunked(
|
|
88
|
+
items: Sequence[T],
|
|
89
|
+
*,
|
|
90
|
+
max_count: int,
|
|
91
|
+
max_bytes: int = 0,
|
|
92
|
+
size: Callable[[T], int] | None = None,
|
|
93
|
+
) -> Iterator[list[T]]:
|
|
94
|
+
"""Split *items* into runs of at most *max_count* items.
|
|
95
|
+
|
|
96
|
+
With a *size* function, runs also stay within *max_bytes*; a single item
|
|
97
|
+
larger than that still gets a run of its own.
|
|
98
|
+
"""
|
|
99
|
+
chunk: list[T] = []
|
|
100
|
+
total = 0
|
|
101
|
+
for item in items:
|
|
102
|
+
item_size = size(item) if size is not None else 0
|
|
103
|
+
if chunk and (len(chunk) >= max_count or (size is not None and total + item_size > max_bytes)):
|
|
104
|
+
yield chunk
|
|
105
|
+
chunk, total = [], 0
|
|
106
|
+
chunk.append(item)
|
|
107
|
+
total += item_size
|
|
108
|
+
if chunk:
|
|
109
|
+
yield chunk
|
tikv/_proto/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Generated by scripts/generate_protos.py. DO NOT EDIT.
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
4
|
+
# source: errorpb.proto
|
|
5
|
+
# Protobuf Python Version: 4.25.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
10
|
+
from google.protobuf.internal import builder as _builder
|
|
11
|
+
# @@protoc_insertion_point(imports)
|
|
12
|
+
|
|
13
|
+
_sym_db = _symbol_database.Default()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from . import metapb_pb2 as metapb__pb2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rerrorpb.proto\x12\x07\x65rrorpb\x1a\x0cmetapb.proto\"<\n\tNotLeader\x12\x11\n\tregion_id\x18\x01 \x01(\x04\x12\x1c\n\x06leader\x18\x02 \x01(\x0b\x32\x0c.metapb.Peer\"\x1e\n\tIsWitness\x12\x11\n\tregion_id\x18\x01 \x01(\x04\",\n\x08\x44iskFull\x12\x10\n\x08store_id\x18\x01 \x03(\x04\x12\x0e\n\x06reason\x18\x02 \x01(\t\"B\n\rStoreNotMatch\x12\x18\n\x10request_store_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x63tual_store_id\x18\x02 \x01(\x04\"#\n\x0eRegionNotFound\x12\x11\n\tregion_id\x18\x01 \x01(\x04\")\n\x14RegionNotInitialized\x12\x11\n\tregion_id\x18\x01 \x01(\x04\"T\n\x0eKeyNotInRegion\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x11\n\tregion_id\x18\x02 \x01(\x04\x12\x11\n\tstart_key\x18\x03 \x01(\x0c\x12\x0f\n\x07\x65nd_key\x18\x04 \x01(\x0c\"8\n\rEpochNotMatch\x12\'\n\x0f\x63urrent_regions\x18\x01 \x03(\x0b\x32\x0e.metapb.Region\"d\n\x0cServerIsBusy\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x12\n\nbackoff_ms\x18\x02 \x01(\x04\x12\x19\n\x11\x65stimated_wait_ms\x18\x03 \x01(\r\x12\x15\n\rapplied_index\x18\x04 \x01(\x04\"\x0e\n\x0cStaleCommand\":\n\x11RaftEntryTooLarge\x12\x11\n\tregion_id\x18\x01 \x01(\x04\x12\x12\n\nentry_size\x18\x02 \x01(\x04\"\x17\n\x15MaxTimestampNotSynced\"6\n\x11ReadIndexNotReady\x12\x0e\n\x06reason\x18\x01 \x01(\t\x12\x11\n\tregion_id\x18\x02 \x01(\x04\"*\n\x15ProposalInMergingMode\x12\x11\n\tregion_id\x18\x01 \x01(\x04\"E\n\x0e\x44\x61taIsNotReady\x12\x11\n\tregion_id\x18\x01 \x01(\x04\x12\x0f\n\x07peer_id\x18\x02 \x01(\x04\x12\x0f\n\x07safe_ts\x18\x03 \x01(\x04\"\'\n\x12RecoveryInProgress\x12\x11\n\tregion_id\x18\x01 \x01(\x04\"D\n\x13\x46lashbackInProgress\x12\x11\n\tregion_id\x18\x01 \x01(\x04\x12\x1a\n\x12\x66lashback_start_ts\x18\x02 \x01(\x04\")\n\x14\x46lashbackNotPrepared\x12\x11\n\tregion_id\x18\x01 \x01(\x04\"@\n\x0eMismatchPeerId\x12\x17\n\x0frequest_peer_id\x18\x01 \x01(\x04\x12\x15\n\rstore_peer_id\x18\x02 \x01(\x04\"%\n\x12UndeterminedResult\x12\x0f\n\x07message\x18\x01 \x01(\t\"\xbd\x08\n\x05\x45rror\x12\x0f\n\x07message\x18\x01 \x01(\t\x12&\n\nnot_leader\x18\x02 \x01(\x0b\x32\x12.errorpb.NotLeader\x12\x31\n\x10region_not_found\x18\x03 \x01(\x0b\x32\x17.errorpb.RegionNotFound\x12\x32\n\x11key_not_in_region\x18\x04 \x01(\x0b\x32\x17.errorpb.KeyNotInRegion\x12/\n\x0f\x65poch_not_match\x18\x05 \x01(\x0b\x32\x16.errorpb.EpochNotMatch\x12-\n\x0eserver_is_busy\x18\x06 \x01(\x0b\x32\x15.errorpb.ServerIsBusy\x12,\n\rstale_command\x18\x07 \x01(\x0b\x32\x15.errorpb.StaleCommand\x12/\n\x0fstore_not_match\x18\x08 \x01(\x0b\x32\x16.errorpb.StoreNotMatch\x12\x38\n\x14raft_entry_too_large\x18\t \x01(\x0b\x32\x1a.errorpb.RaftEntryTooLarge\x12@\n\x18max_timestamp_not_synced\x18\n \x01(\x0b\x32\x1e.errorpb.MaxTimestampNotSynced\x12\x38\n\x14read_index_not_ready\x18\x0b \x01(\x0b\x32\x1a.errorpb.ReadIndexNotReady\x12@\n\x18proposal_in_merging_mode\x18\x0c \x01(\x0b\x32\x1e.errorpb.ProposalInMergingMode\x12\x32\n\x11\x64\x61ta_is_not_ready\x18\r \x01(\x0b\x32\x17.errorpb.DataIsNotReady\x12=\n\x16region_not_initialized\x18\x0e \x01(\x0b\x32\x1d.errorpb.RegionNotInitialized\x12$\n\tdisk_full\x18\x0f \x01(\x0b\x32\x11.errorpb.DiskFull\x12\x37\n\x12RecoveryInProgress\x18\x10 \x01(\x0b\x32\x1b.errorpb.RecoveryInProgress\x12\x39\n\x13\x46lashbackInProgress\x18\x11 \x01(\x0b\x32\x1c.errorpb.FlashbackInProgress\x12;\n\x14\x46lashbackNotPrepared\x18\x12 \x01(\x0b\x32\x1d.errorpb.FlashbackNotPrepared\x12&\n\nis_witness\x18\x13 \x01(\x0b\x32\x12.errorpb.IsWitness\x12\x31\n\x10mismatch_peer_id\x18\x14 \x01(\x0b\x32\x17.errorpb.MismatchPeerId\x12\x38\n\x13undetermined_result\x18\x16 \x01(\x0b\x32\x1b.errorpb.UndeterminedResultb\x06proto3')
|
|
20
|
+
|
|
21
|
+
_globals = globals()
|
|
22
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
23
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'errorpb_pb2', _globals)
|
|
24
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
25
|
+
DESCRIPTOR._options = None
|
|
26
|
+
_globals['_NOTLEADER']._serialized_start=40
|
|
27
|
+
_globals['_NOTLEADER']._serialized_end=100
|
|
28
|
+
_globals['_ISWITNESS']._serialized_start=102
|
|
29
|
+
_globals['_ISWITNESS']._serialized_end=132
|
|
30
|
+
_globals['_DISKFULL']._serialized_start=134
|
|
31
|
+
_globals['_DISKFULL']._serialized_end=178
|
|
32
|
+
_globals['_STORENOTMATCH']._serialized_start=180
|
|
33
|
+
_globals['_STORENOTMATCH']._serialized_end=246
|
|
34
|
+
_globals['_REGIONNOTFOUND']._serialized_start=248
|
|
35
|
+
_globals['_REGIONNOTFOUND']._serialized_end=283
|
|
36
|
+
_globals['_REGIONNOTINITIALIZED']._serialized_start=285
|
|
37
|
+
_globals['_REGIONNOTINITIALIZED']._serialized_end=326
|
|
38
|
+
_globals['_KEYNOTINREGION']._serialized_start=328
|
|
39
|
+
_globals['_KEYNOTINREGION']._serialized_end=412
|
|
40
|
+
_globals['_EPOCHNOTMATCH']._serialized_start=414
|
|
41
|
+
_globals['_EPOCHNOTMATCH']._serialized_end=470
|
|
42
|
+
_globals['_SERVERISBUSY']._serialized_start=472
|
|
43
|
+
_globals['_SERVERISBUSY']._serialized_end=572
|
|
44
|
+
_globals['_STALECOMMAND']._serialized_start=574
|
|
45
|
+
_globals['_STALECOMMAND']._serialized_end=588
|
|
46
|
+
_globals['_RAFTENTRYTOOLARGE']._serialized_start=590
|
|
47
|
+
_globals['_RAFTENTRYTOOLARGE']._serialized_end=648
|
|
48
|
+
_globals['_MAXTIMESTAMPNOTSYNCED']._serialized_start=650
|
|
49
|
+
_globals['_MAXTIMESTAMPNOTSYNCED']._serialized_end=673
|
|
50
|
+
_globals['_READINDEXNOTREADY']._serialized_start=675
|
|
51
|
+
_globals['_READINDEXNOTREADY']._serialized_end=729
|
|
52
|
+
_globals['_PROPOSALINMERGINGMODE']._serialized_start=731
|
|
53
|
+
_globals['_PROPOSALINMERGINGMODE']._serialized_end=773
|
|
54
|
+
_globals['_DATAISNOTREADY']._serialized_start=775
|
|
55
|
+
_globals['_DATAISNOTREADY']._serialized_end=844
|
|
56
|
+
_globals['_RECOVERYINPROGRESS']._serialized_start=846
|
|
57
|
+
_globals['_RECOVERYINPROGRESS']._serialized_end=885
|
|
58
|
+
_globals['_FLASHBACKINPROGRESS']._serialized_start=887
|
|
59
|
+
_globals['_FLASHBACKINPROGRESS']._serialized_end=955
|
|
60
|
+
_globals['_FLASHBACKNOTPREPARED']._serialized_start=957
|
|
61
|
+
_globals['_FLASHBACKNOTPREPARED']._serialized_end=998
|
|
62
|
+
_globals['_MISMATCHPEERID']._serialized_start=1000
|
|
63
|
+
_globals['_MISMATCHPEERID']._serialized_end=1064
|
|
64
|
+
_globals['_UNDETERMINEDRESULT']._serialized_start=1066
|
|
65
|
+
_globals['_UNDETERMINEDRESULT']._serialized_end=1103
|
|
66
|
+
_globals['_ERROR']._serialized_start=1106
|
|
67
|
+
_globals['_ERROR']._serialized_end=2191
|
|
68
|
+
# @@protoc_insertion_point(module_scope)
|