flo-python 0.1.0.dev4__py3-none-any.whl → 0.1.2__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.
- flo/__init__.py +60 -15
- flo/actions.py +20 -27
- flo/client.py +193 -46
- flo/exceptions.py +83 -23
- flo/kv.py +373 -165
- flo/kv_body.py +554 -0
- flo/kv_txn.py +237 -0
- flo/processing.py +3 -67
- flo/queue.py +25 -101
- flo/retry.py +124 -0
- flo/streams.py +146 -31
- flo/types.py +431 -221
- flo/wire.py +263 -208
- flo/worker.py +684 -250
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/METADATA +165 -98
- flo_python-0.1.2.dist-info/RECORD +19 -0
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/WHEEL +1 -1
- flo_python-0.1.2.dist-info/licenses/LICENSE +202 -0
- flo_python-0.1.0.dev4.dist-info/RECORD +0 -16
- flo_python-0.1.0.dev4.dist-info/licenses/LICENSE +0 -21
flo/kv_txn.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Flo KV Per-Shard Transactions
|
|
2
|
+
|
|
3
|
+
Transactions are pinned to a single partition, chosen by the routing key
|
|
4
|
+
passed to ``begin``; every op inside the transaction is routed by it.
|
|
5
|
+
|
|
6
|
+
Caps (server-enforced):
|
|
7
|
+
* 256 ops per transaction
|
|
8
|
+
* 1 MiB total payload across buffered writes
|
|
9
|
+
|
|
10
|
+
The following operations are NOT supported inside a transaction and raise
|
|
11
|
+
:class:`TxnUnsupportedOpError` without a server round-trip: ``scan``,
|
|
12
|
+
``mget``, ``json_get``, ``json_set``, ``json_del``, ``history``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import dataclasses
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from .exceptions import ValidationError
|
|
22
|
+
from .kv import kv_delete, kv_exists, kv_expire, kv_get, kv_incr, kv_set
|
|
23
|
+
from .kv_body import (
|
|
24
|
+
decode_empty_answer,
|
|
25
|
+
decode_txn_begin_answer,
|
|
26
|
+
decode_txn_commit_answer,
|
|
27
|
+
encode_txn_body,
|
|
28
|
+
)
|
|
29
|
+
from .types import (
|
|
30
|
+
DeleteOptions,
|
|
31
|
+
GetOptions,
|
|
32
|
+
GetResult,
|
|
33
|
+
KVCommitResult,
|
|
34
|
+
KVExistsOptions,
|
|
35
|
+
KVExpireOptions,
|
|
36
|
+
KVIncrOptions,
|
|
37
|
+
OpCode,
|
|
38
|
+
PutOptions,
|
|
39
|
+
PutResult,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from .client import FloClient
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TxnUnsupportedOpError(Exception):
|
|
47
|
+
"""Raised when an operation is not allowed inside a KV transaction."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TxnFinishedError(Exception):
|
|
51
|
+
"""Raised when an operation is attempted on a closed transaction."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Transaction:
|
|
55
|
+
"""Per-shard KV transaction handle.
|
|
56
|
+
|
|
57
|
+
Operations are buffered on the server's pinned shard until :meth:`commit`
|
|
58
|
+
or :meth:`rollback` is called. Use as an async context manager for
|
|
59
|
+
automatic rollback on exception::
|
|
60
|
+
|
|
61
|
+
async with await client.kv.begin("user:123") as txn:
|
|
62
|
+
await txn.put("user:123:name", b"Jane")
|
|
63
|
+
await txn.incr("user:123:visits", 1)
|
|
64
|
+
result = await txn.commit()
|
|
65
|
+
print(result.commit_index)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
client: FloClient,
|
|
71
|
+
namespace: str,
|
|
72
|
+
routing_key: str,
|
|
73
|
+
txn_id: int,
|
|
74
|
+
pinned_hash: int,
|
|
75
|
+
):
|
|
76
|
+
self._client = client
|
|
77
|
+
self._namespace = namespace
|
|
78
|
+
self._routing_key = routing_key
|
|
79
|
+
self._txn_id = txn_id
|
|
80
|
+
self._pinned_hash = pinned_hash
|
|
81
|
+
self._done = False
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def id(self) -> int:
|
|
85
|
+
"""The server-assigned transaction id."""
|
|
86
|
+
return self._txn_id
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def pinned_hash(self) -> int:
|
|
90
|
+
"""The partition hash this transaction is bound to."""
|
|
91
|
+
return self._pinned_hash
|
|
92
|
+
|
|
93
|
+
def _check_alive(self) -> None:
|
|
94
|
+
if self._done:
|
|
95
|
+
raise TxnFinishedError("transaction already committed or rolled back")
|
|
96
|
+
|
|
97
|
+
async def put(
|
|
98
|
+
self,
|
|
99
|
+
key: str | bytes,
|
|
100
|
+
value: bytes,
|
|
101
|
+
options: PutOptions | None = None,
|
|
102
|
+
) -> PutResult:
|
|
103
|
+
"""Buffer a put inside the transaction; it has no version until commit (0)."""
|
|
104
|
+
self._check_alive()
|
|
105
|
+
opts = dataclasses.replace(options or PutOptions(), routing_key=self._routing_key)
|
|
106
|
+
return await kv_set(self._client, self._namespace, key, value, opts, self._txn_id)
|
|
107
|
+
|
|
108
|
+
async def get(self, key: str | bytes) -> GetResult | None:
|
|
109
|
+
"""Read a key inside the transaction (sees buffered writes)."""
|
|
110
|
+
self._check_alive()
|
|
111
|
+
return await kv_get(
|
|
112
|
+
self._client,
|
|
113
|
+
self._namespace,
|
|
114
|
+
key,
|
|
115
|
+
GetOptions(routing_key=self._routing_key),
|
|
116
|
+
self._txn_id,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def delete(
|
|
120
|
+
self,
|
|
121
|
+
key: str | bytes,
|
|
122
|
+
options: DeleteOptions | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Buffer a delete inside the transaction; whether the key existed is unknown."""
|
|
125
|
+
self._check_alive()
|
|
126
|
+
opts = dataclasses.replace(options or DeleteOptions(), routing_key=self._routing_key)
|
|
127
|
+
await kv_delete(self._client, self._namespace, key, opts, self._txn_id)
|
|
128
|
+
|
|
129
|
+
async def incr(self, key: str | bytes, delta: int = 1) -> None:
|
|
130
|
+
"""Buffer an atomic counter increment; its value is unknown until commit."""
|
|
131
|
+
self._check_alive()
|
|
132
|
+
opts = KVIncrOptions(delta=delta, routing_key=self._routing_key)
|
|
133
|
+
await kv_incr(self._client, self._namespace, key, opts, self._txn_id)
|
|
134
|
+
|
|
135
|
+
async def expire(
|
|
136
|
+
self,
|
|
137
|
+
key: str | bytes,
|
|
138
|
+
ttl_ms: int,
|
|
139
|
+
options: KVExpireOptions | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Buffer setting the TTL on a key, in milliseconds; ``ttl_ms=0`` clears it."""
|
|
142
|
+
self._check_alive()
|
|
143
|
+
opts = dataclasses.replace(options or KVExpireOptions(), routing_key=self._routing_key)
|
|
144
|
+
await kv_expire(self._client, self._namespace, key, ttl_ms, opts, self._txn_id)
|
|
145
|
+
|
|
146
|
+
async def exists(self, key: str | bytes) -> bool:
|
|
147
|
+
"""Check key existence inside the transaction (sees buffered writes)."""
|
|
148
|
+
self._check_alive()
|
|
149
|
+
opts = KVExistsOptions(routing_key=self._routing_key)
|
|
150
|
+
return await kv_exists(self._client, self._namespace, key, opts, self._txn_id)
|
|
151
|
+
|
|
152
|
+
# ── Disallowed inside a transaction ───────────────────────────────
|
|
153
|
+
|
|
154
|
+
async def scan(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
155
|
+
raise TxnUnsupportedOpError("scan is not supported inside a KV transaction")
|
|
156
|
+
|
|
157
|
+
async def mget(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
158
|
+
raise TxnUnsupportedOpError("mget is not supported inside a KV transaction")
|
|
159
|
+
|
|
160
|
+
async def json_get(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
161
|
+
raise TxnUnsupportedOpError("json_get is not supported inside a KV transaction")
|
|
162
|
+
|
|
163
|
+
async def json_set(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
164
|
+
raise TxnUnsupportedOpError("json_set is not supported inside a KV transaction")
|
|
165
|
+
|
|
166
|
+
async def json_del(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
167
|
+
raise TxnUnsupportedOpError("json_del is not supported inside a KV transaction")
|
|
168
|
+
|
|
169
|
+
async def history(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover
|
|
170
|
+
raise TxnUnsupportedOpError("history is not supported inside a KV transaction")
|
|
171
|
+
|
|
172
|
+
# ── Lifecycle ──────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async def commit(self) -> KVCommitResult:
|
|
175
|
+
"""Atomically apply all buffered operations.
|
|
176
|
+
|
|
177
|
+
After commit returns the transaction is closed and further operations
|
|
178
|
+
raise :class:`TxnFinishedError`.
|
|
179
|
+
"""
|
|
180
|
+
self._check_alive()
|
|
181
|
+
self._done = True
|
|
182
|
+
response = await self._client._send_and_check(
|
|
183
|
+
OpCode.KV_COMMIT_TXN, self._namespace, b"", encode_txn_body(self._txn_id)
|
|
184
|
+
)
|
|
185
|
+
commit_index, op_count = decode_txn_commit_answer(response.data)
|
|
186
|
+
return KVCommitResult(commit_index=commit_index, op_count=op_count)
|
|
187
|
+
|
|
188
|
+
async def rollback(self) -> None:
|
|
189
|
+
"""Discard the buffered operations without committing.
|
|
190
|
+
|
|
191
|
+
Idempotent: calling rollback after commit (or vice versa) is a no-op.
|
|
192
|
+
"""
|
|
193
|
+
if self._done:
|
|
194
|
+
return
|
|
195
|
+
self._done = True
|
|
196
|
+
response = await self._client._send_and_check(
|
|
197
|
+
OpCode.KV_ROLLBACK_TXN, self._namespace, b"", encode_txn_body(self._txn_id)
|
|
198
|
+
)
|
|
199
|
+
decode_empty_answer("kv.txn_rollback", response.data)
|
|
200
|
+
|
|
201
|
+
async def __aenter__(self) -> Transaction:
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
205
|
+
if not self._done:
|
|
206
|
+
# Best-effort cleanup; surface the original exception.
|
|
207
|
+
with contextlib.suppress(Exception):
|
|
208
|
+
await self.rollback()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def begin(
|
|
212
|
+
client: FloClient,
|
|
213
|
+
namespace: str,
|
|
214
|
+
routing_key: str,
|
|
215
|
+
) -> Transaction:
|
|
216
|
+
"""Open a new per-shard KV transaction: its routing key is the request's key."""
|
|
217
|
+
if not routing_key:
|
|
218
|
+
raise ValidationError("a transaction needs a routing key")
|
|
219
|
+
response = await client._send_and_check(
|
|
220
|
+
OpCode.KV_BEGIN_TXN, namespace, routing_key.encode("utf-8"), b""
|
|
221
|
+
)
|
|
222
|
+
txn_id, pinned_hash = decode_txn_begin_answer(response.data)
|
|
223
|
+
return Transaction(
|
|
224
|
+
client=client,
|
|
225
|
+
namespace=namespace,
|
|
226
|
+
routing_key=routing_key,
|
|
227
|
+
txn_id=txn_id,
|
|
228
|
+
pinned_hash=pinned_hash,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
__all__ = [
|
|
233
|
+
"Transaction",
|
|
234
|
+
"TxnUnsupportedOpError",
|
|
235
|
+
"TxnFinishedError",
|
|
236
|
+
"begin",
|
|
237
|
+
]
|
flo/processing.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Flo Processing Operations
|
|
2
2
|
|
|
3
|
-
Stream processing operations: submit, status, list, stop, cancel,
|
|
4
|
-
savepoint, restore, rescale, sync.
|
|
3
|
+
Stream processing operations: submit, status, list, stop, cancel, sync.
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -17,9 +16,6 @@ from .types import (
|
|
|
17
16
|
ProcessingCancelOptions,
|
|
18
17
|
ProcessingListEntry,
|
|
19
18
|
ProcessingListOptions,
|
|
20
|
-
ProcessingRescaleOptions,
|
|
21
|
-
ProcessingRestoreOptions,
|
|
22
|
-
ProcessingSavepointOptions,
|
|
23
19
|
ProcessingStatusOptions,
|
|
24
20
|
ProcessingStatusResult,
|
|
25
21
|
ProcessingStopOptions,
|
|
@@ -126,58 +122,6 @@ class ProcessingOperations:
|
|
|
126
122
|
b"",
|
|
127
123
|
)
|
|
128
124
|
|
|
129
|
-
async def savepoint(
|
|
130
|
-
self, job_id: str, options: ProcessingSavepointOptions | None = None
|
|
131
|
-
) -> str:
|
|
132
|
-
"""Trigger a savepoint for a processing job. Returns the savepoint ID."""
|
|
133
|
-
opts = options or ProcessingSavepointOptions()
|
|
134
|
-
namespace = self._client.get_namespace(opts.namespace)
|
|
135
|
-
|
|
136
|
-
resp = await self._client._send_and_check(
|
|
137
|
-
OpCode.PROCESSING_SAVEPOINT,
|
|
138
|
-
namespace,
|
|
139
|
-
job_id.encode("utf-8"),
|
|
140
|
-
b"",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
return resp.data.decode("utf-8")
|
|
144
|
-
|
|
145
|
-
async def restore(
|
|
146
|
-
self,
|
|
147
|
-
job_id: str,
|
|
148
|
-
savepoint_id: str,
|
|
149
|
-
options: ProcessingRestoreOptions | None = None,
|
|
150
|
-
) -> None:
|
|
151
|
-
"""Restore a processing job from a savepoint."""
|
|
152
|
-
opts = options or ProcessingRestoreOptions()
|
|
153
|
-
namespace = self._client.get_namespace(opts.namespace)
|
|
154
|
-
|
|
155
|
-
await self._client._send_and_check(
|
|
156
|
-
OpCode.PROCESSING_RESTORE,
|
|
157
|
-
namespace,
|
|
158
|
-
job_id.encode("utf-8"),
|
|
159
|
-
savepoint_id.encode("utf-8"),
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
async def rescale(
|
|
163
|
-
self,
|
|
164
|
-
job_id: str,
|
|
165
|
-
parallelism: int,
|
|
166
|
-
options: ProcessingRescaleOptions | None = None,
|
|
167
|
-
) -> None:
|
|
168
|
-
"""Change the parallelism of a processing job."""
|
|
169
|
-
opts = options or ProcessingRescaleOptions()
|
|
170
|
-
namespace = self._client.get_namespace(opts.namespace)
|
|
171
|
-
|
|
172
|
-
value = struct.pack("<I", parallelism)
|
|
173
|
-
|
|
174
|
-
await self._client._send_and_check(
|
|
175
|
-
OpCode.PROCESSING_RESCALE,
|
|
176
|
-
namespace,
|
|
177
|
-
job_id.encode("utf-8"),
|
|
178
|
-
value,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
125
|
# =========================================================================
|
|
182
126
|
# Declarative Sync
|
|
183
127
|
# =========================================================================
|
|
@@ -224,7 +168,7 @@ def _parse_processing_status(data: bytes) -> ProcessingStatusResult:
|
|
|
224
168
|
"""Parse binary wire format for processing job status.
|
|
225
169
|
|
|
226
170
|
Wire format: [job_id_len:u16][job_id][name_len:u16][name][status:u8]
|
|
227
|
-
[
|
|
171
|
+
[batch_size:u32][records_processed:u64][created_at:i64]
|
|
228
172
|
"""
|
|
229
173
|
pos = 0
|
|
230
174
|
|
|
@@ -252,9 +196,6 @@ def _parse_processing_status(data: bytes) -> ProcessingStatusResult:
|
|
|
252
196
|
else f"unknown({status_byte})"
|
|
253
197
|
)
|
|
254
198
|
|
|
255
|
-
(parallelism,) = struct.unpack_from("<I", data, pos)
|
|
256
|
-
pos += 4
|
|
257
|
-
|
|
258
199
|
(batch_size,) = struct.unpack_from("<I", data, pos)
|
|
259
200
|
pos += 4
|
|
260
201
|
|
|
@@ -267,7 +208,6 @@ def _parse_processing_status(data: bytes) -> ProcessingStatusResult:
|
|
|
267
208
|
job_id=job_id,
|
|
268
209
|
name=name,
|
|
269
210
|
status=status_str,
|
|
270
|
-
parallelism=parallelism,
|
|
271
211
|
batch_size=batch_size,
|
|
272
212
|
records_processed=records_processed,
|
|
273
213
|
created_at=created_at,
|
|
@@ -278,7 +218,7 @@ def _parse_processing_list(data: bytes) -> list[ProcessingListEntry]:
|
|
|
278
218
|
"""Parse binary wire format for processing job list.
|
|
279
219
|
|
|
280
220
|
Wire format: [count:u32]([name_len:u16][name][job_id_len:u16][job_id]
|
|
281
|
-
[status_len:u16][status][
|
|
221
|
+
[status_len:u16][status][created_at:i64])*
|
|
282
222
|
"""
|
|
283
223
|
if len(data) < 4:
|
|
284
224
|
return []
|
|
@@ -305,9 +245,6 @@ def _parse_processing_list(data: bytes) -> list[ProcessingListEntry]:
|
|
|
305
245
|
job_id = read_str()
|
|
306
246
|
status = read_str()
|
|
307
247
|
|
|
308
|
-
(parallelism,) = struct.unpack_from("<I", data, pos)
|
|
309
|
-
pos += 4
|
|
310
|
-
|
|
311
248
|
(created_at,) = struct.unpack_from("<q", data, pos)
|
|
312
249
|
pos += 8
|
|
313
250
|
|
|
@@ -316,7 +253,6 @@ def _parse_processing_list(data: bytes) -> list[ProcessingListEntry]:
|
|
|
316
253
|
name=name,
|
|
317
254
|
job_id=job_id,
|
|
318
255
|
status=status,
|
|
319
|
-
parallelism=parallelism,
|
|
320
256
|
created_at=created_at,
|
|
321
257
|
)
|
|
322
258
|
)
|
flo/queue.py
CHANGED
|
@@ -16,7 +16,6 @@ from .types import (
|
|
|
16
16
|
OpCode,
|
|
17
17
|
OptionTag,
|
|
18
18
|
PeekOptions,
|
|
19
|
-
TouchOptions,
|
|
20
19
|
)
|
|
21
20
|
from .wire import OptionsBuilder, parse_dequeue_response, parse_enqueue_response, serialize_seqs
|
|
22
21
|
|
|
@@ -41,7 +40,7 @@ class QueueOperations:
|
|
|
41
40
|
Args:
|
|
42
41
|
queue: Queue name.
|
|
43
42
|
payload: Message payload.
|
|
44
|
-
options: Optional enqueue options (priority
|
|
43
|
+
options: Optional enqueue options (priority).
|
|
45
44
|
|
|
46
45
|
Returns:
|
|
47
46
|
Sequence number of the enqueued message.
|
|
@@ -50,14 +49,8 @@ class QueueOperations:
|
|
|
50
49
|
# Simple enqueue
|
|
51
50
|
seq = await client.queue.enqueue("tasks", b'{"task": "process"}')
|
|
52
51
|
|
|
53
|
-
# With priority (
|
|
52
|
+
# With priority (0-255, lower is taken first; default 0)
|
|
54
53
|
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(priority=10))
|
|
55
|
-
|
|
56
|
-
# With delay
|
|
57
|
-
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(delay_ms=60000))
|
|
58
|
-
|
|
59
|
-
# With deduplication
|
|
60
|
-
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(dedup_key="task-123"))
|
|
61
54
|
"""
|
|
62
55
|
opts = options or EnqueueOptions()
|
|
63
56
|
namespace = self._client.get_namespace(opts.namespace)
|
|
@@ -68,12 +61,6 @@ class QueueOperations:
|
|
|
68
61
|
if opts.priority != 0:
|
|
69
62
|
builder.add_u8(OptionTag.PRIORITY, opts.priority)
|
|
70
63
|
|
|
71
|
-
if opts.delay_ms is not None:
|
|
72
|
-
builder.add_u64(OptionTag.DELAY_MS, opts.delay_ms)
|
|
73
|
-
|
|
74
|
-
if opts.dedup_key is not None:
|
|
75
|
-
builder.add_bytes(OptionTag.DEDUP_KEY, opts.dedup_key.encode("utf-8"))
|
|
76
|
-
|
|
77
64
|
response = await self._client._send_and_check(
|
|
78
65
|
OpCode.QUEUE_ENQUEUE,
|
|
79
66
|
namespace,
|
|
@@ -92,10 +79,14 @@ class QueueOperations:
|
|
|
92
79
|
) -> DequeueResult:
|
|
93
80
|
"""Dequeue messages from a queue.
|
|
94
81
|
|
|
82
|
+
Queues are currently at-most-once: the server acknowledges each
|
|
83
|
+
message as it hands it out, so it is not redelivered in normal
|
|
84
|
+
operation, even if the consumer crashes.
|
|
85
|
+
|
|
95
86
|
Args:
|
|
96
87
|
queue: Queue name.
|
|
97
88
|
count: Maximum number of messages to dequeue.
|
|
98
|
-
options: Optional dequeue options (
|
|
89
|
+
options: Optional dequeue options (block_ms).
|
|
99
90
|
|
|
100
91
|
Returns:
|
|
101
92
|
DequeueResult containing list of messages.
|
|
@@ -105,19 +96,12 @@ class QueueOperations:
|
|
|
105
96
|
result = await client.queue.dequeue("tasks", 10)
|
|
106
97
|
for msg in result.messages:
|
|
107
98
|
process(msg.payload)
|
|
108
|
-
await client.queue.ack("tasks", [msg.seq])
|
|
109
99
|
|
|
110
100
|
# With long polling (wait up to 30s for messages)
|
|
111
101
|
result = await client.queue.dequeue(
|
|
112
102
|
"tasks", 10,
|
|
113
103
|
DequeueOptions(block_ms=30000)
|
|
114
104
|
)
|
|
115
|
-
|
|
116
|
-
# With custom visibility timeout
|
|
117
|
-
result = await client.queue.dequeue(
|
|
118
|
-
"tasks", 10,
|
|
119
|
-
DequeueOptions(visibility_timeout_ms=60000)
|
|
120
|
-
)
|
|
121
105
|
"""
|
|
122
106
|
opts = options or DequeueOptions()
|
|
123
107
|
namespace = self._client.get_namespace(opts.namespace)
|
|
@@ -126,9 +110,6 @@ class QueueOperations:
|
|
|
126
110
|
builder = OptionsBuilder()
|
|
127
111
|
builder.add_u32(OptionTag.COUNT, count)
|
|
128
112
|
|
|
129
|
-
if opts.visibility_timeout_ms is not None:
|
|
130
|
-
builder.add_u32(OptionTag.VISIBILITY_TIMEOUT_MS, opts.visibility_timeout_ms)
|
|
131
|
-
|
|
132
113
|
if opts.block_ms is not None:
|
|
133
114
|
builder.add_u32(OptionTag.BLOCK_MS, opts.block_ms)
|
|
134
115
|
|
|
@@ -148,21 +129,15 @@ class QueueOperations:
|
|
|
148
129
|
seqs: list[int],
|
|
149
130
|
options: AckOptions | None = None,
|
|
150
131
|
) -> None:
|
|
151
|
-
"""Acknowledge messages
|
|
132
|
+
"""Acknowledge messages.
|
|
133
|
+
|
|
134
|
+
Queues are currently at-most-once: dequeue already acknowledges each
|
|
135
|
+
message it hands out, so this has no effect on a dequeued message.
|
|
152
136
|
|
|
153
137
|
Args:
|
|
154
138
|
queue: Queue name.
|
|
155
139
|
seqs: Sequence numbers of messages to acknowledge.
|
|
156
140
|
options: Optional ack options.
|
|
157
|
-
|
|
158
|
-
Example:
|
|
159
|
-
result = await client.queue.dequeue("tasks", 10)
|
|
160
|
-
for msg in result.messages:
|
|
161
|
-
try:
|
|
162
|
-
process(msg.payload)
|
|
163
|
-
await client.queue.ack("tasks", [msg.seq])
|
|
164
|
-
except Exception:
|
|
165
|
-
await client.queue.nack("tasks", [msg.seq])
|
|
166
141
|
"""
|
|
167
142
|
if not seqs:
|
|
168
143
|
return
|
|
@@ -185,19 +160,16 @@ class QueueOperations:
|
|
|
185
160
|
seqs: list[int],
|
|
186
161
|
options: NackOptions | None = None,
|
|
187
162
|
) -> None:
|
|
188
|
-
"""
|
|
163
|
+
"""Negatively acknowledge messages.
|
|
164
|
+
|
|
165
|
+
Queues are currently at-most-once: dequeue already acknowledges each
|
|
166
|
+
message it hands out, so this has no effect on a dequeued message and
|
|
167
|
+
does not retry it.
|
|
189
168
|
|
|
190
169
|
Args:
|
|
191
170
|
queue: Queue name.
|
|
192
171
|
seqs: Sequence numbers of messages to nack.
|
|
193
|
-
options: Optional nack options
|
|
194
|
-
|
|
195
|
-
Example:
|
|
196
|
-
# Retry the message
|
|
197
|
-
await client.queue.nack("tasks", [msg.seq])
|
|
198
|
-
|
|
199
|
-
# Send to DLQ (don't retry)
|
|
200
|
-
await client.queue.nack("tasks", [msg.seq], NackOptions(to_dlq=True))
|
|
172
|
+
options: Optional nack options.
|
|
201
173
|
"""
|
|
202
174
|
if not seqs:
|
|
203
175
|
return
|
|
@@ -205,12 +177,6 @@ class QueueOperations:
|
|
|
205
177
|
opts = options or NackOptions()
|
|
206
178
|
namespace = self._client.get_namespace(opts.namespace)
|
|
207
179
|
|
|
208
|
-
# Build TLV options
|
|
209
|
-
builder = OptionsBuilder()
|
|
210
|
-
|
|
211
|
-
if opts.to_dlq:
|
|
212
|
-
builder.add_u8(OptionTag.SEND_TO_DLQ, 1)
|
|
213
|
-
|
|
214
180
|
value = serialize_seqs(seqs)
|
|
215
181
|
|
|
216
182
|
await self._client._send_and_check(
|
|
@@ -218,7 +184,6 @@ class QueueOperations:
|
|
|
218
184
|
namespace,
|
|
219
185
|
queue.encode("utf-8"),
|
|
220
186
|
value,
|
|
221
|
-
builder.build(),
|
|
222
187
|
)
|
|
223
188
|
|
|
224
189
|
async def dlq_list(
|
|
@@ -228,31 +193,29 @@ class QueueOperations:
|
|
|
228
193
|
) -> DequeueResult:
|
|
229
194
|
"""List messages in the Dead Letter Queue.
|
|
230
195
|
|
|
196
|
+
Queues are currently at-most-once, so messages don't reach the DLQ in
|
|
197
|
+
normal use.
|
|
198
|
+
|
|
231
199
|
Args:
|
|
232
200
|
queue: Queue name.
|
|
233
|
-
options: Optional DLQ list options (
|
|
201
|
+
options: Optional DLQ list options (namespace).
|
|
234
202
|
|
|
235
203
|
Returns:
|
|
236
204
|
DequeueResult containing list of DLQ messages.
|
|
237
205
|
|
|
238
206
|
Example:
|
|
239
|
-
result = await client.queue.dlq_list("tasks"
|
|
207
|
+
result = await client.queue.dlq_list("tasks")
|
|
240
208
|
for msg in result.messages:
|
|
241
209
|
print(f"Failed message {msg.seq}: {msg.payload}")
|
|
242
210
|
"""
|
|
243
211
|
opts = options or DlqListOptions()
|
|
244
212
|
namespace = self._client.get_namespace(opts.namespace)
|
|
245
213
|
|
|
246
|
-
# Build TLV options
|
|
247
|
-
builder = OptionsBuilder()
|
|
248
|
-
builder.add_u32(OptionTag.LIMIT, opts.limit)
|
|
249
|
-
|
|
250
214
|
response = await self._client._send_and_check(
|
|
251
215
|
OpCode.QUEUE_DLQ_LIST,
|
|
252
216
|
namespace,
|
|
253
217
|
queue.encode("utf-8"),
|
|
254
218
|
b"",
|
|
255
|
-
builder.build(),
|
|
256
219
|
)
|
|
257
220
|
|
|
258
221
|
return parse_dequeue_response(response.data)
|
|
@@ -265,6 +228,9 @@ class QueueOperations:
|
|
|
265
228
|
) -> None:
|
|
266
229
|
"""Move messages from DLQ back to the main queue.
|
|
267
230
|
|
|
231
|
+
Queues are currently at-most-once, so messages don't reach the DLQ in
|
|
232
|
+
normal use.
|
|
233
|
+
|
|
268
234
|
Args:
|
|
269
235
|
queue: Queue name.
|
|
270
236
|
seqs: Sequence numbers of DLQ messages to requeue.
|
|
@@ -332,45 +298,3 @@ class QueueOperations:
|
|
|
332
298
|
)
|
|
333
299
|
|
|
334
300
|
return parse_dequeue_response(response.data)
|
|
335
|
-
|
|
336
|
-
async def touch(
|
|
337
|
-
self,
|
|
338
|
-
queue: str,
|
|
339
|
-
seqs: list[int],
|
|
340
|
-
options: TouchOptions | None = None,
|
|
341
|
-
) -> None:
|
|
342
|
-
"""Renew lease on messages to prevent visibility timeout.
|
|
343
|
-
|
|
344
|
-
Call this periodically during long-running processing to prevent
|
|
345
|
-
the message from becoming visible again before you're done.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
queue: Queue name.
|
|
349
|
-
seqs: Sequence numbers of messages to touch.
|
|
350
|
-
options: Optional touch options.
|
|
351
|
-
|
|
352
|
-
Example:
|
|
353
|
-
result = await client.queue.dequeue("tasks", 1)
|
|
354
|
-
msg = result.messages[0]
|
|
355
|
-
|
|
356
|
-
# Long running task - renew lease periodically
|
|
357
|
-
for chunk in process_in_chunks(msg.payload):
|
|
358
|
-
await process_chunk(chunk)
|
|
359
|
-
await client.queue.touch("tasks", [msg.seq])
|
|
360
|
-
|
|
361
|
-
await client.queue.ack("tasks", [msg.seq])
|
|
362
|
-
"""
|
|
363
|
-
if not seqs:
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
opts = options or TouchOptions()
|
|
367
|
-
namespace = self._client.get_namespace(opts.namespace)
|
|
368
|
-
|
|
369
|
-
value = serialize_seqs(seqs)
|
|
370
|
-
|
|
371
|
-
await self._client._send_and_check(
|
|
372
|
-
OpCode.QUEUE_TOUCH,
|
|
373
|
-
namespace,
|
|
374
|
-
queue.encode("utf-8"),
|
|
375
|
-
value,
|
|
376
|
-
)
|