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
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: flo-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Python SDK for the Flo distributed systems platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/floruntime/flo-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/floruntime/flo-python#readme
|
|
7
7
|
Project-URL: Repository, https://github.com/floruntime/flo-python
|
|
8
8
|
Author: Flo Team
|
|
9
|
-
License-Expression:
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: async,distributed-systems,flo,kv-store,queue
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -24,6 +23,7 @@ Requires-Python: >=3.10
|
|
|
24
23
|
Provides-Extra: dev
|
|
25
24
|
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
26
25
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-timeout>=2.1; extra == 'dev'
|
|
27
27
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
28
|
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
@@ -35,7 +35,7 @@ Python SDK for the [Flo](https://github.com/floruntime) distributed systems plat
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pip install flo
|
|
38
|
+
pip install flo-python
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Quick Start
|
|
@@ -62,9 +62,9 @@ asyncio.run(main())
|
|
|
62
62
|
## Features
|
|
63
63
|
|
|
64
64
|
- **KV Store**: Versioned key-value storage with MVCC, TTL, and optimistic locking
|
|
65
|
-
- **Queues**: Priority
|
|
65
|
+
- **Queues**: Priority task queues (currently at-most-once: dequeue hands each message out once)
|
|
66
66
|
- **Streams**: Append-only logs with consumer groups for distributed processing
|
|
67
|
-
- **Actions**: Registered tasks with configurable timeouts
|
|
67
|
+
- **Actions**: Registered tasks with configurable timeouts and retries
|
|
68
68
|
- **Workers**: Distributed task execution with lease management and heartbeats
|
|
69
69
|
- **Async/await**: Native asyncio support for high-performance applications
|
|
70
70
|
- **Type hints**: Full type annotations for IDE support
|
|
@@ -74,18 +74,20 @@ asyncio.run(main())
|
|
|
74
74
|
### Get
|
|
75
75
|
|
|
76
76
|
```python
|
|
77
|
-
from flo import GetOptions
|
|
77
|
+
from flo import GetOptions, WaitFor
|
|
78
78
|
|
|
79
79
|
# Simple get
|
|
80
80
|
value = await client.kv.get("key")
|
|
81
81
|
if value is not None:
|
|
82
82
|
print(value.decode())
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
|
|
84
|
+
# Wait up to 2 seconds for the key to exist (answers at once if it does)
|
|
85
|
+
# (wait_ms is at most 300000; the client waits timeout_ms on top of it;
|
|
86
|
+
# 0 means don't wait)
|
|
87
|
+
value = await client.kv.get("key", GetOptions(wait_ms=2000))
|
|
86
88
|
|
|
87
|
-
#
|
|
88
|
-
value = await client.kv.get("key", GetOptions(
|
|
89
|
+
# Wait up to 2 seconds for the key's next version
|
|
90
|
+
value = await client.kv.get("key", GetOptions(wait_ms=2000, wait_for=WaitFor.CHANGE))
|
|
89
91
|
|
|
90
92
|
# Get with namespace override
|
|
91
93
|
value = await client.kv.get("key", GetOptions(namespace="other"))
|
|
@@ -94,28 +96,34 @@ value = await client.kv.get("key", GetOptions(namespace="other"))
|
|
|
94
96
|
### Put
|
|
95
97
|
|
|
96
98
|
```python
|
|
97
|
-
from flo import PutOptions
|
|
99
|
+
from flo import Condition, PutOptions
|
|
98
100
|
|
|
99
101
|
# Simple put
|
|
100
102
|
await client.kv.put("key", b"value")
|
|
101
103
|
|
|
102
104
|
# Put with TTL (expires in 1 hour)
|
|
103
|
-
await client.kv.put("session:abc", b"data", PutOptions(
|
|
105
|
+
await client.kv.put("session:abc", b"data", PutOptions(ttl_ms=3_600_000))
|
|
104
106
|
|
|
105
107
|
# Put with CAS (optimistic locking)
|
|
106
108
|
await client.kv.put("counter", b"2", PutOptions(cas_version=1))
|
|
107
109
|
|
|
108
110
|
# Put only if key doesn't exist
|
|
109
|
-
await client.kv.put("key", b"value", PutOptions(
|
|
111
|
+
await client.kv.put("key", b"value", PutOptions(condition=Condition.IF_ABSENT))
|
|
110
112
|
|
|
111
113
|
# Put only if key exists
|
|
112
|
-
await client.kv.put("key", b"value", PutOptions(
|
|
114
|
+
await client.kv.put("key", b"value", PutOptions(condition=Condition.IF_PRESENT))
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
### Delete
|
|
116
118
|
|
|
117
119
|
```python
|
|
118
|
-
|
|
120
|
+
from flo import DeleteOptions
|
|
121
|
+
|
|
122
|
+
# Returns whether the key existed; deleting a missing key is not an error
|
|
123
|
+
existed = await client.kv.delete("key")
|
|
124
|
+
|
|
125
|
+
# Delete only at a known version
|
|
126
|
+
await client.kv.delete("lock:resource", DeleteOptions(cas_version=held_version))
|
|
119
127
|
```
|
|
120
128
|
|
|
121
129
|
### Scan
|
|
@@ -123,19 +131,29 @@ await client.kv.delete("key")
|
|
|
123
131
|
```python
|
|
124
132
|
from flo import ScanOptions
|
|
125
133
|
|
|
126
|
-
#
|
|
134
|
+
# Names of keys with a prefix ("" for all), up to 1024 per page by default
|
|
127
135
|
result = await client.kv.scan("user:")
|
|
128
|
-
for
|
|
129
|
-
print(
|
|
136
|
+
for name in result.names:
|
|
137
|
+
print(name)
|
|
130
138
|
|
|
131
|
-
#
|
|
132
|
-
result
|
|
133
|
-
while result.has_more:
|
|
134
|
-
# Process entries...
|
|
139
|
+
# Next pages: the cursor is None on the last page (limit is 1..1024)
|
|
140
|
+
while result.cursor is not None:
|
|
135
141
|
result = await client.kv.scan("user:", ScanOptions(cursor=result.cursor, limit=100))
|
|
142
|
+
```
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
### MGet
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# One entry per key, in order (at most 256 keys of at most 4096 bytes each)
|
|
148
|
+
for e in await client.kv.mget(["user:1", "user:2"]):
|
|
149
|
+
if e.found:
|
|
150
|
+
print(e.key, e.version, e.value)
|
|
151
|
+
|
|
152
|
+
# Keys all written with one routing key: look them all up on its shard
|
|
153
|
+
# (without it, each key is looked up on its own shard)
|
|
154
|
+
from flo import KVMGetOptions
|
|
155
|
+
|
|
156
|
+
await client.kv.mget(["user:1:name", "user:1:email"], KVMGetOptions(routing_key="user:1"))
|
|
139
157
|
```
|
|
140
158
|
|
|
141
159
|
### History
|
|
@@ -143,10 +161,51 @@ result = await client.kv.scan("user:", ScanOptions(keys_only=True))
|
|
|
143
161
|
```python
|
|
144
162
|
from flo import HistoryOptions
|
|
145
163
|
|
|
146
|
-
#
|
|
164
|
+
# Versions newest first (limit 1..65, default 10), each with its write time in ms;
|
|
165
|
+
# empty for a missing key
|
|
147
166
|
history = await client.kv.history("user:123", HistoryOptions(limit=10))
|
|
148
167
|
for entry in history:
|
|
149
|
-
print(f"v{entry.version} at {entry.
|
|
168
|
+
print(f"v{entry.version} at {entry.at_ms}: {entry.value.decode()}")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Counters, TTL and exists
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from flo import KVExpireOptions, KVIncrOptions
|
|
175
|
+
|
|
176
|
+
res = await client.kv.incr("visits") # +1
|
|
177
|
+
res = await client.kv.incr("visits", KVIncrOptions(delta=-5))
|
|
178
|
+
print(res.value, res.version)
|
|
179
|
+
|
|
180
|
+
# Set a key's TTL in ms; 0 clears it. cas_version makes lease renewal race-free.
|
|
181
|
+
await client.kv.expire("session:abc", 60_000)
|
|
182
|
+
await client.kv.expire("lease", 30_000, KVExpireOptions(cas_version=lease_version))
|
|
183
|
+
await client.kv.expire("session:abc", 0)
|
|
184
|
+
|
|
185
|
+
if await client.kv.exists("session:abc"):
|
|
186
|
+
...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### JSON documents
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
# Path defaults to "$" (the whole document)
|
|
193
|
+
await client.kv.json_set("user:1", "$", b'{"name": "Ada", "tags": []}')
|
|
194
|
+
doc = await client.kv.json_get("user:1", "$.name") # GetJsonResult or None
|
|
195
|
+
await client.kv.json_del("user:1", "$.tags") # returns the new version
|
|
196
|
+
await client.kv.json_del("user:1") # "$" deletes the key: returns None
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Transactions
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# Every op inside is routed by the transaction's routing key
|
|
203
|
+
async with await client.kv.begin("user:123") as txn:
|
|
204
|
+
await txn.put("user:123:name", b"Jane")
|
|
205
|
+
await txn.incr("user:123:visits")
|
|
206
|
+
await txn.expire("user:123:session", 60_000)
|
|
207
|
+
result = await txn.commit()
|
|
208
|
+
print(result.commit_index, result.op_count)
|
|
150
209
|
```
|
|
151
210
|
|
|
152
211
|
## Queue Operations
|
|
@@ -159,18 +218,16 @@ from flo import EnqueueOptions
|
|
|
159
218
|
# Simple enqueue
|
|
160
219
|
seq = await client.queue.enqueue("tasks", b'{"task": "process"}')
|
|
161
220
|
|
|
162
|
-
# With priority (
|
|
221
|
+
# With priority (0-255, lower is taken first; default 0)
|
|
163
222
|
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(priority=10))
|
|
164
|
-
|
|
165
|
-
# With delay (message invisible for 60 seconds)
|
|
166
|
-
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(delay_ms=60000))
|
|
167
|
-
|
|
168
|
-
# With deduplication key
|
|
169
|
-
seq = await client.queue.enqueue("tasks", payload, EnqueueOptions(dedup_key="task-123"))
|
|
170
223
|
```
|
|
171
224
|
|
|
172
225
|
### Dequeue
|
|
173
226
|
|
|
227
|
+
Queues are currently at-most-once: a dequeue acknowledges each message as it
|
|
228
|
+
hands it out. A message is not redelivered in normal operation, even if the
|
|
229
|
+
consumer crashes while handling it, so handle failures in your own code.
|
|
230
|
+
|
|
174
231
|
```python
|
|
175
232
|
from flo import DequeueOptions
|
|
176
233
|
|
|
@@ -181,69 +238,25 @@ for msg in result.messages:
|
|
|
181
238
|
|
|
182
239
|
# Long polling (wait up to 30s for messages)
|
|
183
240
|
result = await client.queue.dequeue("tasks", 10, DequeueOptions(block_ms=30000))
|
|
184
|
-
|
|
185
|
-
# Custom visibility timeout (message invisible for 60s)
|
|
186
|
-
result = await client.queue.dequeue("tasks", 10, DequeueOptions(visibility_timeout_ms=60000))
|
|
187
241
|
```
|
|
188
242
|
|
|
189
|
-
### Ack/Nack
|
|
243
|
+
### Ack/Nack and the Dead Letter Queue
|
|
190
244
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
for msg in result.messages:
|
|
196
|
-
try:
|
|
197
|
-
process(msg.payload)
|
|
198
|
-
# Acknowledge successful processing
|
|
199
|
-
await client.queue.ack("tasks", [msg.seq])
|
|
200
|
-
except Exception:
|
|
201
|
-
# Retry the message
|
|
202
|
-
await client.queue.nack("tasks", [msg.seq])
|
|
203
|
-
# Or send to DLQ
|
|
204
|
-
await client.queue.nack("tasks", [msg.seq], NackOptions(to_dlq=True))
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Dead Letter Queue
|
|
208
|
-
|
|
209
|
-
```python
|
|
210
|
-
from flo import DlqListOptions
|
|
211
|
-
|
|
212
|
-
# List DLQ messages
|
|
213
|
-
result = await client.queue.dlq_list("tasks", DlqListOptions(limit=100))
|
|
214
|
-
for msg in result.messages:
|
|
215
|
-
print(f"Failed message {msg.seq}: {msg.payload}")
|
|
216
|
-
|
|
217
|
-
# Requeue DLQ messages
|
|
218
|
-
seqs = [msg.seq for msg in result.messages]
|
|
219
|
-
await client.queue.dlq_requeue("tasks", seqs)
|
|
220
|
-
```
|
|
245
|
+
`queue.ack`, `queue.nack`, `queue.dlq_list` and `queue.dlq_requeue` exist, but
|
|
246
|
+
while queues are at-most-once they do nothing useful: a dequeued message is
|
|
247
|
+
already acknowledged, so ack and nack have no effect on it, nack does not
|
|
248
|
+
retry it, and messages don't reach the DLQ in normal use.
|
|
221
249
|
|
|
222
250
|
### Peek
|
|
223
251
|
|
|
224
252
|
```python
|
|
225
|
-
# Peek at messages without
|
|
253
|
+
# Peek at messages without removing them
|
|
226
254
|
result = await client.queue.peek("tasks", 5)
|
|
227
255
|
for msg in result.messages:
|
|
228
256
|
print(f"Message {msg.seq}: {msg.payload}")
|
|
229
257
|
# Messages remain visible to other consumers
|
|
230
258
|
```
|
|
231
259
|
|
|
232
|
-
### Touch (Lease Renewal)
|
|
233
|
-
|
|
234
|
-
```python
|
|
235
|
-
# Renew lease on messages during long-running processing
|
|
236
|
-
result = await client.queue.dequeue("tasks", 1)
|
|
237
|
-
msg = result.messages[0]
|
|
238
|
-
|
|
239
|
-
# Long running task - periodically touch to prevent visibility timeout
|
|
240
|
-
for chunk in process_in_chunks(msg.payload):
|
|
241
|
-
await process_chunk(chunk)
|
|
242
|
-
await client.queue.touch("tasks", [msg.seq]) # Renew lease
|
|
243
|
-
|
|
244
|
-
await client.queue.ack("tasks", [msg.seq])
|
|
245
|
-
```
|
|
246
|
-
|
|
247
260
|
## Stream Operations
|
|
248
261
|
|
|
249
262
|
Streams are append-only logs ideal for event sourcing, activity feeds, and real-time data pipelines.
|
|
@@ -359,15 +372,14 @@ from flo import ActionInvokeOptions
|
|
|
359
372
|
result = await client.action.invoke(
|
|
360
373
|
"process-image",
|
|
361
374
|
b'{"image_url": "https://example.com/image.jpg"}',
|
|
362
|
-
ActionInvokeOptions(priority=10)
|
|
363
375
|
)
|
|
364
376
|
print(f"Run ID: {result.run_id}")
|
|
365
377
|
|
|
366
|
-
#
|
|
378
|
+
# Route the run only to workers registered with matching labels
|
|
367
379
|
result = await client.action.invoke(
|
|
368
380
|
"process-image",
|
|
369
|
-
|
|
370
|
-
ActionInvokeOptions(
|
|
381
|
+
b'{"image_url": "https://example.com/image.jpg"}',
|
|
382
|
+
ActionInvokeOptions(labels={"gpu": True}),
|
|
371
383
|
)
|
|
372
384
|
```
|
|
373
385
|
|
|
@@ -375,9 +387,11 @@ result = await client.action.invoke(
|
|
|
375
387
|
|
|
376
388
|
```python
|
|
377
389
|
status = await client.action.status(result.run_id)
|
|
378
|
-
print(f"Status: {status.status}")
|
|
379
|
-
if status.
|
|
380
|
-
print(f"
|
|
390
|
+
print(f"Status: {status.status.name}, attempt {status.attempt}")
|
|
391
|
+
if status.output is not None:
|
|
392
|
+
print(f"Output: {status.output!r}")
|
|
393
|
+
if status.error_message is not None:
|
|
394
|
+
print(f"Error: {status.error_message}")
|
|
381
395
|
```
|
|
382
396
|
|
|
383
397
|
### List and Delete Actions
|
|
@@ -453,14 +467,14 @@ await client.worker.touch(
|
|
|
453
467
|
|
|
454
468
|
```python
|
|
455
469
|
import asyncio
|
|
456
|
-
from flo import FloClient
|
|
470
|
+
from flo import ActionRegisterOptions, FloClient
|
|
457
471
|
|
|
458
472
|
async def run_worker():
|
|
459
473
|
async with FloClient("localhost:9000", namespace="myapp") as client:
|
|
460
474
|
# Create an action worker from the client
|
|
461
|
-
worker = client.new_action_worker(concurrency=5)
|
|
475
|
+
worker = client.new_action_worker(concurrency=5, auto_touch=True)
|
|
462
476
|
|
|
463
|
-
@worker.action("process-image")
|
|
477
|
+
@worker.action("process-image", ActionRegisterOptions(timeout_ms=60000))
|
|
464
478
|
async def process_image(ctx):
|
|
465
479
|
data = ctx.json()
|
|
466
480
|
result = await do_processing(data)
|
|
@@ -471,6 +485,14 @@ async def run_worker():
|
|
|
471
485
|
asyncio.run(run_worker())
|
|
472
486
|
```
|
|
473
487
|
|
|
488
|
+
The worker registers each action with the server when it starts, with exactly the options given (the server's defaults when none: a 30 s timeout, 3 attempts). Registering replaces the action's settings, so workers that register one action with different options leave it with whichever registered last.
|
|
489
|
+
|
|
490
|
+
A run whose handler goes past the action's `timeout_ms` without touching it is taken back and run again, by this worker or another. `auto_touch=True` touches each run every third of its action's timeout while the handler runs; it's off by default. Without it, call `await ctx.touch()` from long handlers. The worker also stops any handler after its own `action_timeout` (300 s by default) whatever the action's `timeout_ms`, so raise it for actions that run longer.
|
|
491
|
+
|
|
492
|
+
Delivery is at-least-once: a result lost to a dropped connection is resent, and one that never arrives lets the lease run out, so a handler may see the same task or record again. `worker.stop()` waits for running handlers, and their results are still sent.
|
|
493
|
+
|
|
494
|
+
An action worker heartbeats every 30 s, reporting the handlers it is running. One the server removed after missed heartbeats registers again, with the same options, when its next heartbeat is answered not found. One the server has drained stops taking work, and `start()` returns once its running handlers finish.
|
|
495
|
+
|
|
474
496
|
### StreamWorker Example
|
|
475
497
|
|
|
476
498
|
```python
|
|
@@ -506,9 +528,12 @@ client = FloClient(
|
|
|
506
528
|
namespace="myapp", # Default namespace for all operations
|
|
507
529
|
timeout_ms=5000, # Connection and operation timeout
|
|
508
530
|
debug=True, # Enable debug logging
|
|
531
|
+
retry=RetryPolicy(), # Retries of refused, not-run requests (the default)
|
|
509
532
|
)
|
|
510
533
|
```
|
|
511
534
|
|
|
535
|
+
A call that times out or is cancelled drops the connection; call `reconnect()` before reusing the client (workers do this themselves).
|
|
536
|
+
|
|
512
537
|
### Namespaces
|
|
513
538
|
|
|
514
539
|
Each operation can override the default namespace:
|
|
@@ -545,6 +570,8 @@ except FloError as e:
|
|
|
545
570
|
print(f"Flo error: {e}")
|
|
546
571
|
```
|
|
547
572
|
|
|
573
|
+
A call that times out (`RequestTimeoutError`, also an `asyncio.TimeoutError`) or is cancelled drops the connection; call `reconnect()` before reusing the client (workers do this themselves).
|
|
574
|
+
|
|
548
575
|
### Error Types
|
|
549
576
|
|
|
550
577
|
| Error | Description |
|
|
@@ -553,11 +580,51 @@ except FloError as e:
|
|
|
553
580
|
| `BadRequestError` | Invalid request parameters |
|
|
554
581
|
| `ConflictError` | CAS version mismatch |
|
|
555
582
|
| `UnauthorizedError` | Authentication failed |
|
|
556
|
-
| `OverloadedError` | Server is overloaded |
|
|
557
|
-
| `
|
|
583
|
+
| `OverloadedError` | Server is overloaded, and the client's retries ran out |
|
|
584
|
+
| `UnavailableError` | No leader, or the shard isn't taking writes or is offline, and the client's retries ran out (the message says why, and whether an operator must act first) |
|
|
585
|
+
| `InternalServerError` | Server internal error; not retryable (may mean committed but not applied) |
|
|
586
|
+
| `GenericServerError` | Any other error status, including one this SDK doesn't know (`status_code` carries it) |
|
|
558
587
|
| `ConnectionFailedError` | Failed to connect |
|
|
588
|
+
| `RequestTimeoutError` | No response within the timeout |
|
|
559
589
|
| `InvalidChecksumError` | Response CRC32 mismatch |
|
|
560
590
|
|
|
591
|
+
### Retries
|
|
592
|
+
|
|
593
|
+
When the server refuses a request as `overloaded` or `unavailable` and says it
|
|
594
|
+
didn't run it (`error.ran == Ran.NO`), the client sends it again itself, as a
|
|
595
|
+
new request, after a jittered wait. Because the server never ran it, this is
|
|
596
|
+
safe for writes too. Any other refusal, or one that may have run, comes
|
|
597
|
+
straight back.
|
|
598
|
+
|
|
599
|
+
| `RetryPolicy` field | Default | |
|
|
600
|
+
|---|---|---|
|
|
601
|
+
| `max_attempts` | 4 | Tries in all, the first included; 1 turns retrying off |
|
|
602
|
+
| `initial_backoff_ms` | 50 | Ceiling of the first wait, in ms; it doubles each retry |
|
|
603
|
+
| `max_backoff_ms` | 1000 | Largest ceiling, in ms |
|
|
604
|
+
|
|
605
|
+
Each wait is uniform between 0 and its ceiling, so with the defaults a refused
|
|
606
|
+
request is given back after at most about 350 ms of waiting. A negative backoff
|
|
607
|
+
counts as 0, and a `max_backoff_ms` below `initial_backoff_ms` as
|
|
608
|
+
`initial_backoff_ms`. The request goes again on the same connection; the server
|
|
609
|
+
routes it afresh. Other calls on the client are not held up by the wait.
|
|
610
|
+
|
|
611
|
+
The action and stream workers use their client's policy, and wait the same way
|
|
612
|
+
between failed polls, from 100 ms up to 5 s, starting again from 100 ms after a
|
|
613
|
+
poll that succeeds; `stop()` cuts the wait short. A stream worker retries a
|
|
614
|
+
group join refused this way instead of stopping.
|
|
615
|
+
|
|
616
|
+
```python
|
|
617
|
+
from flo import FloClient, RetryPolicy
|
|
618
|
+
|
|
619
|
+
# Up to 8 tries, waiting at most 2 s between them
|
|
620
|
+
client = FloClient("localhost:9000", retry=RetryPolicy(
|
|
621
|
+
max_attempts=8, initial_backoff_ms=100, max_backoff_ms=2000,
|
|
622
|
+
))
|
|
623
|
+
|
|
624
|
+
# No retries
|
|
625
|
+
client = FloClient("localhost:9000", retry=RetryPolicy(max_attempts=1))
|
|
626
|
+
```
|
|
627
|
+
|
|
561
628
|
## Requirements
|
|
562
629
|
|
|
563
630
|
- Python 3.10+
|
|
@@ -565,4 +632,4 @@ except FloError as e:
|
|
|
565
632
|
|
|
566
633
|
## License
|
|
567
634
|
|
|
568
|
-
|
|
635
|
+
Apache-2.0 - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
flo/__init__.py,sha256=4zwv03hg6JTNTvy8n7p3k2KTS8Y2dW1UoLSR9Mrjb4c,7932
|
|
2
|
+
flo/actions.py,sha256=hHtfcmaDRHt0rm2-DNRPT2r3Xm6FmV8XKqSvCa7tHW4,12009
|
|
3
|
+
flo/client.py,sha256=0JxnT_dgiJIgpsmykQiCtnjMQLTcRlzA2sd4BZ_Y6ns,22309
|
|
4
|
+
flo/exceptions.py,sha256=Ab71lT63i9s_0Q55IE7IIjp-ejAcuUPD-7QcMs2dLmM,8509
|
|
5
|
+
flo/kv.py,sha256=4Y0PT9XK_AHouRlxFAPWZOtMYpwN6rN8Ny7HwP1Li8o,15755
|
|
6
|
+
flo/kv_body.py,sha256=k-Xeq_9LjqyuXCdqo7I65RfDyBJKc0SROHc887AS2YE,19073
|
|
7
|
+
flo/kv_txn.py,sha256=MkEoONiZz092IbFPHNScxEl_Ltr2TIwHwczche4lx9c,8399
|
|
8
|
+
flo/processing.py,sha256=jN8PhUahsTdYleNptMovE7kE9WLLrOqUu6UVbzULjns,8536
|
|
9
|
+
flo/queue.py,sha256=BoYekudiTAxB6PLpEft1j1sdW4A6rGB8A_MZZ15qjlw,8483
|
|
10
|
+
flo/retry.py,sha256=L8dawQa2qaOKbKm4WBGmo9ERs3krARTOZJ2u9OHUhV4,4479
|
|
11
|
+
flo/streams.py,sha256=RIdZIyww-1FXjJpDNfw_sBhjXjgScqt1EZEsXmE6TXc,17444
|
|
12
|
+
flo/types.py,sha256=LqsVbcWrhPp7FzE8LURwIy2LBQem9hIMhr6SgZ8f_gg,33469
|
|
13
|
+
flo/wire.py,sha256=zkBWTkFIdjuXFJ76RtecKe_69CKi5IDqvulvtg_p-JQ,33550
|
|
14
|
+
flo/worker.py,sha256=uboWk9bWsVrcnrRcTJgPaEQNc_EThYEeFqTUtnl_-90,56330
|
|
15
|
+
flo/workflows.py,sha256=8Z5mhq2UOAnjV_Ef261mcWlj4Z5yGCwrFbxjey0ICoU,15050
|
|
16
|
+
flo_python-0.1.2.dist-info/METADATA,sha256=4DsaVXkbzi5QweSmnYZ_rn9rJVdNPRERRfJp3MPm_1s,19611
|
|
17
|
+
flo_python-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
18
|
+
flo_python-0.1.2.dist-info/licenses/LICENSE,sha256=ti77D_daMo24gEINF1J7388qriJ5r5GjuJUo0b0k27o,11344
|
|
19
|
+
flo_python-0.1.2.dist-info/RECORD,,
|