oxia 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.
oxia/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Oxia Python Client SDK
17
+ """
18
+
19
+ from oxia.client import (
20
+ Client,
21
+ )
22
+ from oxia.defs import (
23
+ ComparisonType,
24
+ NotificationType,
25
+ Notification,
26
+ Version,
27
+ EXPECTED_RECORD_DOES_NOT_EXIST,
28
+ )
29
+
30
+ __all__ = [
31
+ 'ex',
32
+ 'ComparisonType',
33
+ 'Client',
34
+ 'Version',
35
+ 'EXPECTED_RECORD_DOES_NOT_EXIST',
36
+ 'NotificationType',
37
+ 'Notification',
38
+ ]
oxia/client.py ADDED
@@ -0,0 +1,300 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import functools
15
+ import heapq
16
+ from typing import Iterator
17
+
18
+ from oxia.internal.compare import compare_tuple_with_slash, compare_with_slash
19
+ from oxia.internal.connection_pool import ConnectionPool
20
+ from oxia.internal.notifications import Notifications
21
+ from oxia.internal.sequence_updates import SequenceUpdatesImpl
22
+ from oxia.internal.sessions import SessionManager
23
+ from oxia.internal.service_discovery import ServiceDiscovery
24
+ from oxia.internal.proto.io.streamnative.oxia import proto as pb
25
+ import oxia.ex
26
+
27
+ import datetime
28
+
29
+ def _check_status(status: pb.Status):
30
+ if status == pb.Status.OK:
31
+ pass
32
+ elif status == pb.Status.KEY_NOT_FOUND:
33
+ raise oxia.ex.KeyNotFound()
34
+ elif status == pb.Status.UNEXPECTED_VERSION_ID:
35
+ raise oxia.ex.UnexpectedVersionId()
36
+ elif status == pb.Status.SESSION_DOES_NOT_EXIST:
37
+ raise oxia.ex.SessionNotFound()
38
+
39
+
40
+ def _get_version(pbv : pb.Version):
41
+ v = oxia.defs.Version()
42
+ v._version_id = pbv.version_id
43
+ v._modifications_count = pbv.modifications_count
44
+ v._created_timestamp = datetime.datetime.fromtimestamp(pbv.created_timestamp/1000.0)
45
+ v._modified_timestamp = datetime.datetime.fromtimestamp(pbv.modified_timestamp/1000.0)
46
+ v._session_id = pbv.session_id
47
+ v._client_identity = pbv.client_identity
48
+ return v
49
+
50
+ class Client:
51
+ def __init__(self, service_address: str,
52
+ namespace: str = "default",
53
+ session_timeout_ms: int = 30_000,
54
+ client_identifier: str = None,
55
+ ):
56
+ self._closed = False
57
+ self._connections = ConnectionPool()
58
+ self._service_discovery = ServiceDiscovery(service_address, self._connections, namespace)
59
+ self._session_manager = SessionManager(self._service_discovery, session_timeout_ms, client_identifier)
60
+
61
+ def put(self, key: str, value: object,
62
+ partition_key: str = None,
63
+ expected_version_id: int = None,
64
+ ephemeral: bool = False,
65
+ sequence_keys_deltas: list[int] = None,
66
+ secondary_indexes: dict[str, str] = None,
67
+ ) -> (str, oxia.defs.Version):
68
+ """
69
+ Put Associates a value with a key
70
+
71
+ There are few options that can be passed to the Put operation:
72
+ - The Put operation can be made conditional on that the record hasn't changed from
73
+ a specific existing version by passing the [ExpectedVersionId] option.
74
+ - Client can assert that the record does not exist by passing [ExpectedRecordNotExists]
75
+ - Client can create an ephemeral record with [Ephemeral]
76
+
77
+ Returns the actual key of the inserted record
78
+ Returns a [Version] object that contains information about the newly updated record
79
+ Throw [ErrorUnexpectedVersionId] if the expected version id does not match the
80
+ current version id of the record
81
+
82
+ :param key:
83
+ :param value:
84
+ :param partition_key:
85
+ :param expected_version_id:
86
+ :param ephemeral:
87
+ :return:
88
+ """
89
+ shard, stub = self._service_discovery.get_leader(key, partition_key)
90
+
91
+ if sequence_keys_deltas:
92
+ if not partition_key:
93
+ raise oxia.ex.InvalidOptions("sequence_keys_deltas can only be used with partition_key")
94
+ if expected_version_id is not None:
95
+ raise oxia.ex.InvalidOptions("sequence_keys_deltas cannot be used with expected_version_id")
96
+
97
+ if type(value) is str:
98
+ value = bytes(str(value), encoding='utf-8')
99
+
100
+ pr = pb.PutRequest(key=key, value=value,
101
+ partition_key=partition_key,
102
+ expected_version_id=expected_version_id,
103
+ sequence_key_delta=sequence_keys_deltas,
104
+ )
105
+ if ephemeral:
106
+ session = self._session_manager.get_session(shard)
107
+ pr.session_id = session.session_id()
108
+ pr.client_identity = session.client_identifier()
109
+
110
+ if secondary_indexes:
111
+ indexes = []
112
+ for k, v in secondary_indexes.items():
113
+ indexes.append(pb.SecondaryIndex(k, v))
114
+ pr.secondary_indexes = indexes
115
+
116
+ res = stub.write(pb.WriteRequest(shard=shard, puts=[pr]))
117
+
118
+ put_res = res.puts[0] # We only have 1 request
119
+ _check_status(put_res.status)
120
+
121
+ if put_res.key:
122
+ key = put_res.key
123
+ return key, _get_version(put_res.version)
124
+
125
+ def delete(self, key: str,
126
+ partition_key: str = None,
127
+ expected_version_id: int = None, ) -> bool:
128
+ # // Delete removes the key and its associated value from the data store.
129
+ # //
130
+ # // The Delete operation can be made conditional on that the record hasn't changed from
131
+ # // a specific existing version by passing the [ExpectedVersionId] option.
132
+ # // Returns [ErrorUnexpectedVersionId] if the expected version id does not match the
133
+ # // current version id of the record
134
+ shard, stub = self._service_discovery.get_leader(key, partition_key)
135
+
136
+ dr = pb.DeleteRequest(key=key,
137
+ expected_version_id=expected_version_id)
138
+ res = stub.write(pb.WriteRequest(shard=shard, deletes=[dr]))
139
+
140
+ status = res.deletes[0].status # We only have 1 request
141
+ if status == pb.Status.KEY_NOT_FOUND:
142
+ return False
143
+ else:
144
+ _check_status(status)
145
+ return True
146
+
147
+ def delete_range(self, min_key_inclusive: str, max_key_exclusive: str, partition_key: str = None):
148
+ # // DeleteRange deletes any records with keys within the specified range.
149
+ # // Note: Oxia uses a custom sorting order that treats `/` characters in special way.
150
+ # // Refer to this documentation for the specifics:
151
+ # // https://github.com/streamnative/oxia/blob/main/docs/oxia-key-sorting.md
152
+ if partition_key is None:
153
+ for shard, stub in self._service_discovery.get_all_shards():
154
+ self._delete_range_single_shard(min_key_inclusive, max_key_exclusive, shard, stub)
155
+ else:
156
+ shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
157
+ self._delete_range_single_shard(min_key_inclusive, max_key_exclusive, shard, stub)
158
+
159
+ @staticmethod
160
+ def _delete_range_single_shard(min_key_inclusive: str, max_key_exclusive: str, shard: int, stub):
161
+ dr = pb.DeleteRangeRequest(start_inclusive=min_key_inclusive,
162
+ end_exclusive=max_key_exclusive)
163
+ res = stub.write(pb.WriteRequest(shard=shard, delete_ranges=[dr]))
164
+
165
+ status = res.delete_ranges[0].status # We only have 1 request
166
+ _check_status(status)
167
+
168
+ def get(self, key: str,
169
+ partition_key: str = None,
170
+ comparison_type: oxia.defs.ComparisonType = oxia.defs.ComparisonType.EQUAL,
171
+ include_value: bool = True,
172
+ use_index: str = None,
173
+ ) -> (str, str, oxia.defs.Version):
174
+ # // Get returns the value associated with the specified key.
175
+ # // In addition to the value, a version object is also returned, with information
176
+ # // about the record state.
177
+ # // Returns ErrorKeyNotFound if the record does not exist
178
+ if partition_key is None and \
179
+ (comparison_type != oxia.defs.ComparisonType.EQUAL
180
+ or use_index is not None):
181
+
182
+ results = []
183
+ for shard, stub in self._service_discovery.get_all_shards():
184
+ try:
185
+ k, val, version = self._get_single_shard(shard, stub, key, comparison_type, include_value, use_index)
186
+ results.append((k, val, version))
187
+ except oxia.ex.KeyNotFound:
188
+ pass
189
+ if not results: raise oxia.ex.KeyNotFound
190
+ results.sort(key=functools.cmp_to_key(compare_tuple_with_slash))
191
+
192
+ if comparison_type == oxia.defs.ComparisonType.EQUAL or \
193
+ comparison_type == oxia.defs.ComparisonType.CEILING or \
194
+ comparison_type == oxia.defs.ComparisonType.HIGHER:
195
+ return results[0]
196
+ elif comparison_type == oxia.defs.ComparisonType.FLOOR or \
197
+ comparison_type == oxia.defs.ComparisonType.LOWER:
198
+ return results[-1]
199
+
200
+ else:
201
+ # Single shard get operation
202
+ shard, stub = self._service_discovery.get_leader(key, partition_key)
203
+ return self._get_single_shard(shard, stub, key, comparison_type, include_value, use_index)
204
+
205
+ @staticmethod
206
+ def _get_single_shard(shard: int, stub,
207
+ key: str,
208
+ comparison_type: oxia.defs.ComparisonType,
209
+ include_value: bool,
210
+ use_index: str) -> (str, str, oxia.defs.Version):
211
+ gr = pb.GetRequest(key=key,
212
+ comparison_type=comparison_type,
213
+ include_value=include_value,
214
+ secondary_index_name=use_index)
215
+ res = next(stub.read(pb.ReadRequest(shard=shard, gets=[gr])))
216
+ get_res = res.gets[0] # We only have 1 request
217
+ _check_status(get_res.status)
218
+
219
+ res_key = get_res.key if get_res.key is not None else key
220
+ return res_key, get_res.value, _get_version(get_res.version)
221
+
222
+ def list(self, min_key_inclusive: str, max_key_exclusive: str,
223
+ partition_key: str = None,
224
+ use_index: str = None,
225
+ ):
226
+ # // List any existing keys within the specified range.
227
+ # // Note: Oxia uses a custom sorting order that treats `/` characters in special way.
228
+ # // Refer to this documentation for the specifics:
229
+ # // https://github.com/streamnative/oxia/blob/main/docs/oxia-key-sorting.md
230
+ if partition_key is None:
231
+ all_res = []
232
+ for shard, stub in self._service_discovery.get_all_shards():
233
+ all_res.extend(self._list_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index))
234
+ all_res.sort(key=functools.cmp_to_key(compare_with_slash))
235
+ return all_res
236
+ else:
237
+ shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
238
+ return self._list_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index)
239
+
240
+ @staticmethod
241
+ def _list_single_shard(shard, stub, min_key_inclusive: str, max_key_exclusive: str, use_index: str) -> list:
242
+ res = stub.list(pb.ListRequest(shard=shard,
243
+ start_inclusive=min_key_inclusive,
244
+ end_exclusive=max_key_exclusive,
245
+ secondary_index_name=use_index))
246
+
247
+ keys = []
248
+ for i in res:
249
+ keys.extend(i.keys)
250
+ return keys
251
+
252
+ def range_scan(self,
253
+ min_key_inclusive: str,
254
+ max_key_exclusive: str,
255
+ partition_key: str = None,
256
+ use_index: str = None,
257
+ ) -> Iterator[tuple[str, str, oxia.defs.Version]]:
258
+ # // RangeScan perform a scan for existing records with any keys within the specified range.
259
+ # // Ordering in results channel is respected only if a [PartitionKey] option is passed (and the keys were
260
+ # // inserted with that partition key).
261
+ if partition_key is None:
262
+ its = []
263
+ for shard, stub in self._service_discovery.get_all_shards():
264
+ its.append(self._range_scan_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index))
265
+ return heapq.merge(*its, key=functools.cmp_to_key(compare_tuple_with_slash))
266
+ else:
267
+ shard, stub = self._service_discovery.get_leader(partition_key, partition_key)
268
+ return self._range_scan_single_shard(shard, stub, min_key_inclusive, max_key_exclusive, use_index)
269
+
270
+ @staticmethod
271
+ def _range_scan_single_shard(shard, stub, min_key_inclusive: str, max_key_exclusive: str, use_index: str) \
272
+ -> Iterator[tuple[str, str, oxia.defs.Version]]:
273
+ it = stub.range_scan(pb.RangeScanRequest(shard=shard,
274
+ start_inclusive=min_key_inclusive,
275
+ end_exclusive=max_key_exclusive,
276
+ secondary_index_name=use_index))
277
+
278
+ for res in it:
279
+ for x in res.records:
280
+ yield x.key, x.value, _get_version(x.version)
281
+
282
+ def get_sequence_updates(self, prefix_key: str, partition_key: str = None) -> oxia.defs.SequenceUpdates:
283
+ # // GetSequenceUpdates allows to subscribe to the updates happening on a sequential key
284
+ # // The channel will report the current latest sequence for a given key.
285
+ # // Multiple updates can be collapsed into one single event with the
286
+ # // highest sequence.
287
+ if partition_key is None:
288
+ raise oxia.ex.InvalidOptions("get_sequence_updates requires a partition_key")
289
+ return SequenceUpdatesImpl(self._service_discovery, prefix_key, partition_key, lambda : self._closed)
290
+
291
+ def get_notifications(self):
292
+ """GetNotifications creates a new subscription to receive the notifications
293
+ from Oxia for any change that is applied to the database"""
294
+ return Notifications(self._service_discovery)
295
+
296
+ def close(self):
297
+ self._closed = True
298
+ self._session_manager.close()
299
+ self._connections.close()
300
+ self._service_discovery.close()
oxia/defs.py ADDED
@@ -0,0 +1,165 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from enum import Enum, IntEnum
16
+ from abc import ABC
17
+ from oxia.internal.proto.io.streamnative.oxia import proto as pb
18
+ import datetime
19
+ from typing import Iterator
20
+
21
+ class NotificationType(Enum):
22
+ """NotificationType represents the type of the notification event."""
23
+
24
+ """KeyCreated A record that didn't exist was created."""
25
+ KEY_CREATED = 0
26
+
27
+ """KeyModified An existing record was modified."""
28
+ KEY_MODIFIED = 1
29
+
30
+ """KeyDeleted A record was deleted."""
31
+ KEY_DELETED = 2
32
+
33
+ """KeyRangeDeleted A range of keys was deleted."""
34
+ KEY_RANGE_DELETED = 3
35
+
36
+
37
+ class Notification:
38
+ """Notification represents one change in the Oxia database."""
39
+
40
+ def notification_type(self) -> NotificationType:
41
+ """The type of the modification"""
42
+ return self._type
43
+
44
+ def key(self) -> str:
45
+ """The Key of the record to which the notification is referring"""
46
+ return self._key
47
+
48
+ def version_id(self) -> int:
49
+ """The current VersionId of the record, or -1 for a KeyDeleted event"""
50
+ return self._version_id
51
+
52
+ def key_range_end(self) -> str:
53
+ """In case of a KeyRangeRangeDeleted notification, this would represent
54
+ the end (excluded) of the range of keys"""
55
+ return self._key_range_end
56
+
57
+ def __str__(self):
58
+ return f"Notification(key: {self.key()}, type: {self.notification_type()}, version_id: {self.version_id()}, key_range_end: {self.key_range_end()})"
59
+
60
+ class ComparisonType(IntEnum):
61
+ """ComparisonType is an enumeration of the possible comparison types for the `get()` operation."""
62
+
63
+ """Equal sets the Get() operation to compare the stored key for equality."""
64
+ EQUAL = pb.KeyComparisonType.EQUAL
65
+
66
+ """Floor option will make the get operation to search for the record whose key is the
67
+ highest key <= to the supplied key."""
68
+ FLOOR = pb.KeyComparisonType.FLOOR
69
+
70
+ """Ceiling option will make the get operation to search for the record whose key is the
71
+ lowest key >= to the supplied key."""
72
+ CEILING = pb.KeyComparisonType.CEILING
73
+
74
+ """Lower option will make the get operation to search for the record whose key is strictly < to
75
+ the supplied key."""
76
+ LOWER = pb.KeyComparisonType.LOWER
77
+
78
+ """Higher option will make the get operation to search for the record whose key is
79
+ strictly > to the supplied key."""
80
+ HIGHER = pb.KeyComparisonType.HIGHER
81
+
82
+
83
+ class Version:
84
+ """
85
+ Version includes some information regarding the state of a record.
86
+ """
87
+
88
+ def version_id(self) -> int:
89
+ """
90
+ Retrieve the version ID.
91
+
92
+ This method returns the version ID, which is an integer value representing
93
+ the current version. It does not accept any parameters and simply outputs
94
+ the version ID as an integer.
95
+
96
+ :return: The version ID.
97
+ :rtype: int
98
+ """
99
+ return self._version_id
100
+
101
+ def created_timestamp(self) -> datetime.datetime:
102
+ """
103
+ The time when the record was last created
104
+ (If the record gets deleted and recreated, it will have a new CreatedTimestamp value)
105
+ """
106
+ return self._created_timestamp
107
+
108
+ def modified_timestamp(self) -> datetime.datetime:
109
+ """
110
+ Get the time when the record was last modified.
111
+
112
+ :return: The last modification timestamp.
113
+ :rtype: int
114
+ """
115
+ return self._modified_timestamp
116
+
117
+ def modifications_count(self) -> int:
118
+ """
119
+ Get the number of modifications to the record since it was last created.
120
+ If the record gets deleted and recreated, the ModificationsCount will restart at 0.
121
+
122
+ :return: The number of modifications.
123
+ :rtype: int
124
+ """
125
+ return self._modifications_count
126
+
127
+ def is_ephemeral(self) -> bool:
128
+ """
129
+ Check if the record is ephemeral.
130
+
131
+ :return: True if the record is ephemeral, False otherwise.
132
+ :rtype: bool
133
+ """
134
+ return self._session_id is not None
135
+
136
+ def session_id(self) -> int:
137
+ """
138
+ Get the session identifier for ephemeral records.
139
+ For ephemeral records, this is the identifier of the session to which this record lifecycle
140
+ is attached to. Non-ephemeral records will always report 0.
141
+
142
+ :return: The session ID.
143
+ :rtype: int
144
+ """
145
+ return self._session_id
146
+
147
+ def client_identity(self) -> str:
148
+ """
149
+ Get the client identity for ephemeral records.
150
+ For ephemeral records, this is the unique identity of the Oxia client that did last modify it.
151
+ It will be empty for all non-ephemeral records.
152
+
153
+ :return: The client identity.
154
+ :rtype: str
155
+ """
156
+ return self._client_identity
157
+
158
+ def __str__(self):
159
+ return f"Version(version_id: {self.version_id()}, session_id: {self.session_id()}, modifications_count: {self.modifications_count()}, created_timestamp: {self.created_timestamp()}, modified_timestamp: {self.modified_timestamp()}, client_identity: {self.client_identity()})"
160
+
161
+ EXPECTED_RECORD_DOES_NOT_EXIST = -1
162
+
163
+ class SequenceUpdates(Iterator[str], ABC):
164
+ def close(self):
165
+ pass
oxia/ex.py ADDED
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ class OxiaException(Exception):
16
+ pass
17
+
18
+ class InvalidOptions(OxiaException):
19
+ pass
20
+
21
+ class KeyNotFound(OxiaException):
22
+ pass
23
+
24
+ class UnexpectedVersionId(OxiaException):
25
+ pass
26
+
27
+ class SessionNotFound(OxiaException):
28
+ pass
@@ -0,0 +1,14 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import time
16
+
17
+ class Backoff:
18
+ def __init__(self, start_delay=0.1):
19
+ self.start_delay = start_delay
20
+ self.delay = start_delay
21
+ self.max_delay = 60.0
22
+
23
+ def reset(self):
24
+ self.delay = self.start_delay
25
+
26
+ def wait_next(self):
27
+ time.sleep(self.delay)
28
+ self.delay = min(self.delay * 2, self.max_delay)
@@ -0,0 +1,53 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ def _cmp(a: str, b: str) -> int:
16
+ if a < b:
17
+ return -1
18
+ elif a > b:
19
+ return +1
20
+ else:
21
+ return 0
22
+
23
+ def compare_tuple_with_slash(a, b) -> int:
24
+ return compare_with_slash(a[0], b[0])
25
+
26
+ def compare_with_slash(a : str, b : str) -> int:
27
+ while a and b:
28
+ idx_a = a.find('/')
29
+ idx_b = b.find('/')
30
+ if idx_a < 0 and idx_b < 0:
31
+ return _cmp(a, b)
32
+ elif idx_a < 0:
33
+ return -1
34
+ elif idx_b < 0:
35
+ return +1
36
+
37
+
38
+ # At this point, both slices have '/'
39
+ span_a = a[:idx_a]
40
+ span_b = b[:idx_b]
41
+ span_cmp = _cmp(span_a, span_b)
42
+ if span_cmp != 0:
43
+ return span_cmp
44
+
45
+ a = a[idx_a+1:]
46
+ b = b[idx_b+1:]
47
+
48
+ if len(a) < len(b):
49
+ return -1
50
+ elif len(a) > 0:
51
+ return +1
52
+ else:
53
+ return 0
@@ -0,0 +1,36 @@
1
+ # Copyright 2025 StreamNative, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import grpc
16
+ from oxia.internal.proto.io.streamnative.oxia.proto import OxiaClientStub
17
+
18
+ class ConnectionPool:
19
+
20
+ def __init__(self):
21
+ self.connections = {}
22
+
23
+ def get(self, address) -> OxiaClientStub:
24
+ x = self.connections.get(address)
25
+ if x is None:
26
+ channel = grpc.insecure_channel(address)
27
+ stub = OxiaClientStub(channel)
28
+ x = (channel, stub)
29
+ self.connections[address] = x
30
+
31
+ return x[1]
32
+
33
+
34
+ def close(self):
35
+ for c in self.connections.values():
36
+ c[0].close()