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/_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
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])
|