tigerbeetle 0.16.16__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.
- tigerbeetle/__init__.py +3 -0
- tigerbeetle/bindings.py +775 -0
- tigerbeetle/client.py +236 -0
- tigerbeetle/lib/aarch64-linux-gnu.2.27/libtb_client.so +0 -0
- tigerbeetle/lib/aarch64-linux-musl/libtb_client.so +0 -0
- tigerbeetle/lib/aarch64-macos/libtb_client.dylib +0 -0
- tigerbeetle/lib/x86_64-linux-gnu.2.27/libtb_client.so +0 -0
- tigerbeetle/lib/x86_64-linux-musl/libtb_client.so +0 -0
- tigerbeetle/lib/x86_64-macos/libtb_client.dylib +0 -0
- tigerbeetle/lib/x86_64-windows/tb_client.dll +0 -0
- tigerbeetle/lib.py +88 -0
- tigerbeetle-0.16.16.dist-info/METADATA +798 -0
- tigerbeetle-0.16.16.dist-info/RECORD +14 -0
- tigerbeetle-0.16.16.dist-info/WHEEL +4 -0
tigerbeetle/client.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import ctypes
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable # noqa: TCH003
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import bindings
|
|
14
|
+
from .lib import tb_assert
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("tigerbeetle")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AtomicInteger:
|
|
20
|
+
def __init__(self, value=0):
|
|
21
|
+
self._value = value
|
|
22
|
+
self._lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
def increment(self):
|
|
25
|
+
with self._lock:
|
|
26
|
+
self._value += 1
|
|
27
|
+
return self._value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CompletionContextSync:
|
|
32
|
+
event: threading.Event
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CompletionContextAsync:
|
|
37
|
+
loop: asyncio.AbstractEventLoop
|
|
38
|
+
event: asyncio.Event
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class InflightPacket:
|
|
43
|
+
packet: bindings.CPacket
|
|
44
|
+
response: Any
|
|
45
|
+
operation: bindings.Operation
|
|
46
|
+
c_event_type: Any
|
|
47
|
+
c_result_type: Any
|
|
48
|
+
on_completion: Callable | None
|
|
49
|
+
on_completion_context: CompletionContextSync | CompletionContextAsync | None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def id() -> int:
|
|
54
|
+
"""
|
|
55
|
+
Generates a Universally Unique and Sortable Identifier as a 128-bit integer. Based on ULIDs.
|
|
56
|
+
"""
|
|
57
|
+
time_ms = time.time_ns() // (1000 * 1000)
|
|
58
|
+
|
|
59
|
+
# Ensure time_ms monotonically increases.
|
|
60
|
+
time_ms_last = getattr(id, "_time_ms_last", 0)
|
|
61
|
+
if time_ms <= time_ms_last:
|
|
62
|
+
time_ms = time_ms_last
|
|
63
|
+
else:
|
|
64
|
+
id._time_ms_last = time_ms
|
|
65
|
+
|
|
66
|
+
randomness = os.urandom(10)
|
|
67
|
+
|
|
68
|
+
return int.from_bytes(
|
|
69
|
+
time_ms.to_bytes(6, "big") + randomness,
|
|
70
|
+
"big",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
amount_max = (2 ** 128) - 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InitError(Exception):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
class PacketError(Exception):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Client:
|
|
85
|
+
_clients: dict[int, Any] = {}
|
|
86
|
+
_counter = AtomicInteger()
|
|
87
|
+
|
|
88
|
+
def __init__(self, cluster_id: int, replica_addresses: str):
|
|
89
|
+
self._client_key = Client._counter.increment()
|
|
90
|
+
self._client = bindings.Client()
|
|
91
|
+
|
|
92
|
+
self._inflight_packets: dict[int, InflightPacket] = {}
|
|
93
|
+
|
|
94
|
+
init_status = bindings.tb_client_init(
|
|
95
|
+
ctypes.byref(self._client),
|
|
96
|
+
cluster_id,
|
|
97
|
+
replica_addresses.encode("ascii"),
|
|
98
|
+
len(replica_addresses),
|
|
99
|
+
self._client_key,
|
|
100
|
+
self._c_on_completion
|
|
101
|
+
)
|
|
102
|
+
if init_status != bindings.Status.SUCCESS:
|
|
103
|
+
raise InitError(init_status)
|
|
104
|
+
|
|
105
|
+
Client._clients[self._client_key] = self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _acquire_packet(self, operation: bindings.Operation, operations: Any,
|
|
109
|
+
c_event_type: Any, c_result_type: Any) -> InflightPacket:
|
|
110
|
+
packet = bindings.CPacket()
|
|
111
|
+
packet.next = None
|
|
112
|
+
packet.user_data = Client._counter.increment()
|
|
113
|
+
packet.operation = operation
|
|
114
|
+
packet.status = bindings.PacketStatus.OK
|
|
115
|
+
|
|
116
|
+
operations_array_type = c_event_type * len(operations)
|
|
117
|
+
operations_array = operations_array_type(*map(c_event_type.from_param, operations))
|
|
118
|
+
|
|
119
|
+
packet.data_size = ctypes.sizeof(operations_array)
|
|
120
|
+
packet.data = ctypes.cast(operations_array, ctypes.c_void_p)
|
|
121
|
+
|
|
122
|
+
return InflightPacket(
|
|
123
|
+
packet=packet,
|
|
124
|
+
response=None,
|
|
125
|
+
on_completion=None,
|
|
126
|
+
on_completion_context=None,
|
|
127
|
+
operation=operation,
|
|
128
|
+
c_event_type=c_event_type,
|
|
129
|
+
c_result_type=c_result_type)
|
|
130
|
+
|
|
131
|
+
def close(self):
|
|
132
|
+
bindings.tb_client_deinit(self._client)
|
|
133
|
+
tb_assert(self._client is not None)
|
|
134
|
+
tb_assert(len(self._inflight_packets) == 0)
|
|
135
|
+
del Client._clients[self._client_key]
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
@bindings.OnCompletion
|
|
139
|
+
def _c_on_completion(completion_ctx, tb_client, packet, timestamp, bytes_ptr, len_):
|
|
140
|
+
"""
|
|
141
|
+
Invoked in a separate thread
|
|
142
|
+
"""
|
|
143
|
+
self: Client = Client._clients[completion_ctx]
|
|
144
|
+
tb_assert(self._client.value == tb_client)
|
|
145
|
+
|
|
146
|
+
packet = ctypes.cast(packet, ctypes.POINTER(bindings.CPacket))
|
|
147
|
+
inflight_packet = self._inflight_packets[packet[0].user_data]
|
|
148
|
+
|
|
149
|
+
if packet[0].status != bindings.PacketStatus.OK.value:
|
|
150
|
+
inflight_packet.response = PacketError(repr(bindings.PacketStatus(packet[0].status)))
|
|
151
|
+
tb_assert(inflight_packet.on_completion is not None)
|
|
152
|
+
inflight_packet.on_completion(inflight_packet)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
c_result_type = inflight_packet.c_result_type
|
|
156
|
+
tb_assert(len_ % ctypes.sizeof(c_result_type) == 0)
|
|
157
|
+
|
|
158
|
+
# The memory referenced in bytes_ptr is only valid for the duration of this callback. Copy
|
|
159
|
+
# it to a fresh, Python owned buffer and do the conversion from the raw C type to the Python
|
|
160
|
+
# dataclass.
|
|
161
|
+
results_slice = ctypes.cast(
|
|
162
|
+
bytes_ptr,
|
|
163
|
+
ctypes.POINTER(c_result_type)
|
|
164
|
+
)[0:(len_ // ctypes.sizeof(c_result_type))]
|
|
165
|
+
results = [result.to_python() for result in results_slice]
|
|
166
|
+
|
|
167
|
+
inflight_packet.response = results
|
|
168
|
+
|
|
169
|
+
tb_assert(inflight_packet.on_completion is not None)
|
|
170
|
+
inflight_packet.on_completion(inflight_packet)
|
|
171
|
+
|
|
172
|
+
def __enter__(self):
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
176
|
+
self.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ClientSync(Client, bindings.StateMachineMixin):
|
|
180
|
+
def _on_completion(self, inflight_packet):
|
|
181
|
+
inflight_packet.on_completion_context.event.set()
|
|
182
|
+
|
|
183
|
+
def _submit(self, operation: bindings.Operation, operations: list[Any],
|
|
184
|
+
c_event_type: Any, c_result_type: Any):
|
|
185
|
+
inflight_packet = self._acquire_packet(operation, operations, c_event_type, c_result_type)
|
|
186
|
+
self._inflight_packets[inflight_packet.packet.user_data] = inflight_packet
|
|
187
|
+
|
|
188
|
+
inflight_packet.on_completion = self._on_completion
|
|
189
|
+
inflight_packet.on_completion_context = CompletionContextSync(event=threading.Event())
|
|
190
|
+
|
|
191
|
+
bindings.tb_client_submit(self._client, ctypes.byref(inflight_packet.packet))
|
|
192
|
+
inflight_packet.on_completion_context.event.wait()
|
|
193
|
+
|
|
194
|
+
del self._inflight_packets[inflight_packet.packet.user_data]
|
|
195
|
+
|
|
196
|
+
if isinstance(inflight_packet.response, Exception):
|
|
197
|
+
raise inflight_packet.response
|
|
198
|
+
|
|
199
|
+
return inflight_packet.response
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ClientAsync(Client, bindings.AsyncStateMachineMixin):
|
|
203
|
+
def _on_completion(self, inflight_packet):
|
|
204
|
+
"""
|
|
205
|
+
Called by Client._c_on_completion, which itself is called from a different thread. Use
|
|
206
|
+
`call_soon_threadsafe` to return to the thread of the event loop the request was invoked
|
|
207
|
+
from, so _trigger_event() can trigger the async event and allow the client to progress.
|
|
208
|
+
"""
|
|
209
|
+
inflight_packet.on_completion_context.loop.call_soon_threadsafe(
|
|
210
|
+
self._trigger_event,
|
|
211
|
+
inflight_packet
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _trigger_event(self, inflight_packet):
|
|
215
|
+
inflight_packet.on_completion_context.event.set()
|
|
216
|
+
|
|
217
|
+
async def _submit(self, operation: bindings.Operation, operations: Any,
|
|
218
|
+
c_event_type: Any, c_result_type: Any):
|
|
219
|
+
inflight_packet = self._acquire_packet(operation, operations, c_event_type, c_result_type)
|
|
220
|
+
self._inflight_packets[inflight_packet.packet.user_data] = inflight_packet
|
|
221
|
+
|
|
222
|
+
inflight_packet.on_completion = self._on_completion
|
|
223
|
+
inflight_packet.on_completion_context = CompletionContextAsync(
|
|
224
|
+
loop=asyncio.get_event_loop(),
|
|
225
|
+
event=asyncio.Event()
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
bindings.tb_client_submit(self._client, ctypes.byref(inflight_packet.packet))
|
|
229
|
+
await inflight_packet.on_completion_context.event.wait()
|
|
230
|
+
|
|
231
|
+
del self._inflight_packets[inflight_packet.packet.user_data]
|
|
232
|
+
|
|
233
|
+
if isinstance(inflight_packet.response, Exception):
|
|
234
|
+
raise inflight_packet.response
|
|
235
|
+
|
|
236
|
+
return inflight_packet.response
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
tigerbeetle/lib.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import dataclasses
|
|
3
|
+
import platform
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NativeError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class IntegerOverflowError(ValueError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_tbclient():
|
|
15
|
+
prefix = ""
|
|
16
|
+
arch = ""
|
|
17
|
+
system = ""
|
|
18
|
+
linux_libc = ""
|
|
19
|
+
suffix = ""
|
|
20
|
+
|
|
21
|
+
platform_machine = platform.machine().lower()
|
|
22
|
+
|
|
23
|
+
if platform_machine == "x86_64" or platform_machine == "amd64":
|
|
24
|
+
arch = "x86_64"
|
|
25
|
+
elif platform_machine == "aarch64" or platform_machine == "arm64":
|
|
26
|
+
arch = "aarch64"
|
|
27
|
+
else:
|
|
28
|
+
raise NativeError("Unsupported machine: " + platform.machine())
|
|
29
|
+
|
|
30
|
+
if platform.system() == "Linux":
|
|
31
|
+
prefix = "lib"
|
|
32
|
+
system = "linux"
|
|
33
|
+
suffix = ".so"
|
|
34
|
+
libc = platform.libc_ver()[0]
|
|
35
|
+
if libc == "glibc":
|
|
36
|
+
linux_libc = "-gnu.2.27"
|
|
37
|
+
elif libc == "musl":
|
|
38
|
+
linux_libc = "-musl"
|
|
39
|
+
else:
|
|
40
|
+
raise NativeError("Unsupported libc: " + libc)
|
|
41
|
+
elif platform.system() == "Darwin":
|
|
42
|
+
prefix = "lib"
|
|
43
|
+
system = "macos"
|
|
44
|
+
suffix = ".dylib"
|
|
45
|
+
elif platform.system() == "Windows":
|
|
46
|
+
system = "windows"
|
|
47
|
+
suffix = ".dll"
|
|
48
|
+
else:
|
|
49
|
+
raise NativeError("Unsupported system: " + platform.system())
|
|
50
|
+
|
|
51
|
+
source_path = Path(__file__)
|
|
52
|
+
source_dir = source_path.parent
|
|
53
|
+
library_path = source_dir / "lib" / f"{arch}-{system}{linux_libc}" / f"{prefix}tb_client{suffix}"
|
|
54
|
+
return ctypes.CDLL(str(library_path))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_uint(*, bits: int, name: str, number: int):
|
|
58
|
+
if number > 2**bits - 1:
|
|
59
|
+
raise IntegerOverflowError(f"{name}=={number} is too large to fit in {bits} bits")
|
|
60
|
+
if number < 0:
|
|
61
|
+
raise IntegerOverflowError(f"{name}=={number} cannot be negative")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class c_uint128(ctypes.Structure): # noqa: N801
|
|
65
|
+
_fields_ = [("_low", ctypes.c_uint64), ("_high", ctypes.c_uint64)] # noqa: RUF012
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_param(cls, obj):
|
|
69
|
+
return cls(_high=obj >> 64, _low=obj & 0xffffffffffffffff)
|
|
70
|
+
|
|
71
|
+
def to_python(self):
|
|
72
|
+
return self._high << 64 | self._low
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Use slots=True if the version of Python is new enough (3.10+) to support it.
|
|
76
|
+
try:
|
|
77
|
+
dataclass = dataclasses.dataclass(slots=True)
|
|
78
|
+
except TypeError:
|
|
79
|
+
dataclass = dataclasses.dataclass()
|
|
80
|
+
|
|
81
|
+
def tb_assert(value):
|
|
82
|
+
"""
|
|
83
|
+
Python's built-in assert can be silently disabled if Python is run with -O.
|
|
84
|
+
"""
|
|
85
|
+
if not value:
|
|
86
|
+
raise AssertionError()
|
|
87
|
+
|
|
88
|
+
tbclient = _load_tbclient()
|