flo-python 0.1.1__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 +32 -17
- flo/actions.py +20 -27
- flo/client.py +160 -45
- flo/exceptions.py +83 -23
- flo/kv.py +248 -365
- flo/kv_body.py +554 -0
- flo/kv_txn.py +55 -158
- flo/processing.py +3 -67
- flo/queue.py +25 -101
- flo/retry.py +124 -0
- flo/streams.py +32 -28
- flo/types.py +278 -231
- flo/wire.py +94 -213
- flo/worker.py +585 -247
- {flo_python-0.1.1.dist-info → flo_python-0.1.2.dist-info}/METADATA +163 -96
- flo_python-0.1.2.dist-info/RECORD +19 -0
- {flo_python-0.1.1.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.1.dist-info/RECORD +0 -17
- flo_python-0.1.1.dist-info/licenses/LICENSE +0 -21
flo/__init__.py
CHANGED
|
@@ -34,6 +34,7 @@ Example:
|
|
|
34
34
|
from .client import FloClient
|
|
35
35
|
from .exceptions import (
|
|
36
36
|
BadRequestError,
|
|
37
|
+
BlockTooLongError,
|
|
37
38
|
ConflictError,
|
|
38
39
|
ConnectionFailedError,
|
|
39
40
|
FloError,
|
|
@@ -52,16 +53,19 @@ from .exceptions import (
|
|
|
52
53
|
PayloadTooLargeError,
|
|
53
54
|
ProtocolError,
|
|
54
55
|
RateLimitedError,
|
|
56
|
+
RequestTimeoutError,
|
|
55
57
|
ServerError,
|
|
58
|
+
TableMismatchError,
|
|
56
59
|
UnauthorizedError,
|
|
60
|
+
UnavailableError,
|
|
57
61
|
UnexpectedEofError,
|
|
58
|
-
UnsupportedVersionError,
|
|
59
62
|
ValidationError,
|
|
60
63
|
ValueTooLargeError,
|
|
61
64
|
is_connection_error,
|
|
62
65
|
)
|
|
63
66
|
from .kv_txn import Transaction, TxnFinishedError, TxnUnsupportedOpError
|
|
64
67
|
from .processing import ProcessingOperations
|
|
68
|
+
from .retry import DEFAULT_RETRY_POLICY, RetryPolicy
|
|
65
69
|
from .types import (
|
|
66
70
|
AckOptions,
|
|
67
71
|
ActionDeleteOptions,
|
|
@@ -74,6 +78,7 @@ from .types import (
|
|
|
74
78
|
ActionRunStatus,
|
|
75
79
|
ActionStatusOptions,
|
|
76
80
|
ActionType,
|
|
81
|
+
Condition,
|
|
77
82
|
DeleteOptions,
|
|
78
83
|
DequeueOptions,
|
|
79
84
|
DequeueResult,
|
|
@@ -84,14 +89,14 @@ from .types import (
|
|
|
84
89
|
GetOptions,
|
|
85
90
|
GetResult,
|
|
86
91
|
HistoryOptions,
|
|
92
|
+
IncrResult,
|
|
87
93
|
KVBeginResult,
|
|
88
94
|
KVCommitResult,
|
|
89
|
-
KVEntry,
|
|
90
95
|
KVExistsOptions,
|
|
96
|
+
KVExpireOptions,
|
|
91
97
|
KVIncrOptions,
|
|
92
98
|
KVJsonOptions,
|
|
93
99
|
KVMGetOptions,
|
|
94
|
-
KVTouchOptions,
|
|
95
100
|
Message,
|
|
96
101
|
MGetEntry,
|
|
97
102
|
NackOptions,
|
|
@@ -102,9 +107,6 @@ from .types import (
|
|
|
102
107
|
ProcessingCancelOptions,
|
|
103
108
|
ProcessingListEntry,
|
|
104
109
|
ProcessingListOptions,
|
|
105
|
-
ProcessingRescaleOptions,
|
|
106
|
-
ProcessingRestoreOptions,
|
|
107
|
-
ProcessingSavepointOptions,
|
|
108
110
|
ProcessingStatusOptions,
|
|
109
111
|
ProcessingStatusResult,
|
|
110
112
|
ProcessingStopOptions,
|
|
@@ -113,6 +115,9 @@ from .types import (
|
|
|
113
115
|
ProcessingSyncResult,
|
|
114
116
|
PutOptions,
|
|
115
117
|
PutResult,
|
|
118
|
+
Ran,
|
|
119
|
+
Reason,
|
|
120
|
+
RunStatus,
|
|
116
121
|
ScanOptions,
|
|
117
122
|
ScanResult,
|
|
118
123
|
StatusCode,
|
|
@@ -131,9 +136,10 @@ from .types import (
|
|
|
131
136
|
StreamReadResult,
|
|
132
137
|
StreamRecord,
|
|
133
138
|
StreamTrimOptions,
|
|
139
|
+
StreamTrimResult,
|
|
134
140
|
TaskAssignment,
|
|
135
|
-
TouchOptions,
|
|
136
141
|
VersionEntry,
|
|
142
|
+
WaitFor,
|
|
137
143
|
WorkerAwaitOptions,
|
|
138
144
|
WorkerAwaitResult,
|
|
139
145
|
WorkerCompleteOptions,
|
|
@@ -142,8 +148,9 @@ from .types import (
|
|
|
142
148
|
WorkerListOptions,
|
|
143
149
|
WorkerListResult,
|
|
144
150
|
WorkerRegisterOptions,
|
|
145
|
-
|
|
151
|
+
WorkerStatus,
|
|
146
152
|
WorkerTouchOptions,
|
|
153
|
+
WorkerType,
|
|
147
154
|
WorkflowCancelOptions,
|
|
148
155
|
WorkflowCreateOptions,
|
|
149
156
|
WorkflowDisableOptions,
|
|
@@ -170,7 +177,7 @@ from .worker import (
|
|
|
170
177
|
)
|
|
171
178
|
from .workflows import WorkflowOperations
|
|
172
179
|
|
|
173
|
-
__version__ = "0.1.
|
|
180
|
+
__version__ = "0.1.2"
|
|
174
181
|
|
|
175
182
|
__all__ = [
|
|
176
183
|
"Transaction",
|
|
@@ -182,13 +189,16 @@ __all__ = [
|
|
|
182
189
|
"KVIncrOptions",
|
|
183
190
|
"KVJsonOptions",
|
|
184
191
|
"KVMGetOptions",
|
|
185
|
-
"
|
|
192
|
+
"KVExpireOptions",
|
|
186
193
|
"GetResult",
|
|
187
194
|
"GetJsonResult",
|
|
188
195
|
"MGetEntry",
|
|
189
196
|
"PutResult",
|
|
197
|
+
"IncrResult",
|
|
190
198
|
# Client
|
|
191
199
|
"FloClient",
|
|
200
|
+
"RetryPolicy",
|
|
201
|
+
"DEFAULT_RETRY_POLICY",
|
|
192
202
|
# High-level Worker API
|
|
193
203
|
"ActionWorker",
|
|
194
204
|
"ActionWorkerOptions",
|
|
@@ -207,9 +217,10 @@ __all__ = [
|
|
|
207
217
|
"ConnectionFailedError",
|
|
208
218
|
"InvalidEndpointError",
|
|
209
219
|
"UnexpectedEofError",
|
|
220
|
+
"RequestTimeoutError",
|
|
210
221
|
"ProtocolError",
|
|
211
222
|
"InvalidMagicError",
|
|
212
|
-
"
|
|
223
|
+
"TableMismatchError",
|
|
213
224
|
"InvalidChecksumError",
|
|
214
225
|
"PayloadTooLargeError",
|
|
215
226
|
"IncompleteResponseError",
|
|
@@ -217,28 +228,33 @@ __all__ = [
|
|
|
217
228
|
"NamespaceTooLargeError",
|
|
218
229
|
"KeyTooLargeError",
|
|
219
230
|
"ValueTooLargeError",
|
|
231
|
+
"BlockTooLongError",
|
|
220
232
|
"ServerError",
|
|
221
233
|
"NotFoundError",
|
|
222
234
|
"BadRequestError",
|
|
223
235
|
"ConflictError",
|
|
224
236
|
"UnauthorizedError",
|
|
225
237
|
"OverloadedError",
|
|
238
|
+
"UnavailableError",
|
|
226
239
|
"RateLimitedError",
|
|
227
240
|
"InternalServerError",
|
|
228
241
|
"GenericServerError",
|
|
229
242
|
"is_connection_error",
|
|
230
243
|
# Types
|
|
231
244
|
"OpCode",
|
|
245
|
+
"Ran",
|
|
246
|
+
"Reason",
|
|
232
247
|
"StatusCode",
|
|
233
248
|
"OptionTag",
|
|
234
249
|
# Result types
|
|
235
|
-
"KVEntry",
|
|
236
250
|
"ScanResult",
|
|
237
251
|
"VersionEntry",
|
|
238
252
|
"Message",
|
|
239
253
|
"DequeueResult",
|
|
240
254
|
# Options
|
|
255
|
+
"Condition",
|
|
241
256
|
"GetOptions",
|
|
257
|
+
"WaitFor",
|
|
242
258
|
"PutOptions",
|
|
243
259
|
"DeleteOptions",
|
|
244
260
|
"ScanOptions",
|
|
@@ -250,7 +266,6 @@ __all__ = [
|
|
|
250
266
|
"DlqListOptions",
|
|
251
267
|
"DlqRequeueOptions",
|
|
252
268
|
"PeekOptions",
|
|
253
|
-
"TouchOptions",
|
|
254
269
|
# Stream types
|
|
255
270
|
"StreamID",
|
|
256
271
|
"StorageTier",
|
|
@@ -264,6 +279,7 @@ __all__ = [
|
|
|
264
279
|
"StreamAppendOptions",
|
|
265
280
|
"StreamReadOptions",
|
|
266
281
|
"StreamTrimOptions",
|
|
282
|
+
"StreamTrimResult",
|
|
267
283
|
"StreamInfoOptions",
|
|
268
284
|
"StreamGroupJoinOptions",
|
|
269
285
|
"StreamGroupReadOptions",
|
|
@@ -271,6 +287,7 @@ __all__ = [
|
|
|
271
287
|
"StreamGroupNackOptions",
|
|
272
288
|
# Action types
|
|
273
289
|
"ActionType",
|
|
290
|
+
"RunStatus",
|
|
274
291
|
"ActionInfo",
|
|
275
292
|
"ActionRunStatus",
|
|
276
293
|
"ActionInvokeResult",
|
|
@@ -283,10 +300,11 @@ __all__ = [
|
|
|
283
300
|
"ActionDeleteOptions",
|
|
284
301
|
# Worker types
|
|
285
302
|
"TaskAssignment",
|
|
286
|
-
"WorkerTask",
|
|
287
303
|
"WorkerAwaitResult",
|
|
288
304
|
"WorkerInfo",
|
|
289
305
|
"WorkerListResult",
|
|
306
|
+
"WorkerStatus",
|
|
307
|
+
"WorkerType",
|
|
290
308
|
# Worker options
|
|
291
309
|
"WorkerRegisterOptions",
|
|
292
310
|
"WorkerAwaitOptions",
|
|
@@ -316,9 +334,6 @@ __all__ = [
|
|
|
316
334
|
"ProcessingListOptions",
|
|
317
335
|
"ProcessingStopOptions",
|
|
318
336
|
"ProcessingCancelOptions",
|
|
319
|
-
"ProcessingSavepointOptions",
|
|
320
|
-
"ProcessingRestoreOptions",
|
|
321
|
-
"ProcessingRescaleOptions",
|
|
322
337
|
"ProcessingSyncOptions",
|
|
323
338
|
"ProcessingStatusResult",
|
|
324
339
|
"ProcessingListEntry",
|
flo/actions.py
CHANGED
|
@@ -9,6 +9,12 @@ For a higher-level API, see `Worker` in worker.py.
|
|
|
9
9
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
|
+
from .kv_body import (
|
|
13
|
+
decode_action_invoke_answer,
|
|
14
|
+
decode_action_status_answer,
|
|
15
|
+
decode_worker_list_answer,
|
|
16
|
+
encode_worker_list_body,
|
|
17
|
+
)
|
|
12
18
|
from .types import (
|
|
13
19
|
ActionDeleteOptions,
|
|
14
20
|
ActionInvokeOptions,
|
|
@@ -39,7 +45,6 @@ from .wire import (
|
|
|
39
45
|
serialize_worker_await_value,
|
|
40
46
|
serialize_worker_complete_value,
|
|
41
47
|
serialize_worker_fail_value,
|
|
42
|
-
serialize_worker_list_value,
|
|
43
48
|
serialize_worker_register_value,
|
|
44
49
|
serialize_worker_touch_value,
|
|
45
50
|
)
|
|
@@ -78,7 +83,7 @@ class ActionOperations:
|
|
|
78
83
|
action_type=action_type,
|
|
79
84
|
timeout_ms=opts.timeout_ms,
|
|
80
85
|
max_retries=opts.max_retries,
|
|
81
|
-
|
|
86
|
+
owner=opts.owner,
|
|
82
87
|
)
|
|
83
88
|
|
|
84
89
|
await self._client._send_and_check(
|
|
@@ -109,8 +114,7 @@ class ActionOperations:
|
|
|
109
114
|
|
|
110
115
|
value = serialize_action_invoke_value(
|
|
111
116
|
input_data=input_data,
|
|
112
|
-
|
|
113
|
-
idempotency_key=opts.idempotency_key,
|
|
117
|
+
labels=opts.labels,
|
|
114
118
|
)
|
|
115
119
|
|
|
116
120
|
response = await self._client._send_and_check(
|
|
@@ -120,9 +124,7 @@ class ActionOperations:
|
|
|
120
124
|
value,
|
|
121
125
|
)
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
run_id = response.data.decode("utf-8") if response.data else ""
|
|
125
|
-
return ActionInvokeResult(run_id=run_id)
|
|
127
|
+
return ActionInvokeResult(run_id=decode_action_invoke_answer(response.data))
|
|
126
128
|
|
|
127
129
|
async def status(
|
|
128
130
|
self,
|
|
@@ -148,13 +150,7 @@ class ActionOperations:
|
|
|
148
150
|
b"",
|
|
149
151
|
)
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
# For now return basic status
|
|
153
|
-
return ActionRunStatus(
|
|
154
|
-
run_id=run_id,
|
|
155
|
-
status="unknown",
|
|
156
|
-
result=response.data if response.data else None,
|
|
157
|
-
)
|
|
153
|
+
return decode_action_status_answer(run_id, response.data)
|
|
158
154
|
|
|
159
155
|
async def list(
|
|
160
156
|
self,
|
|
@@ -280,7 +276,7 @@ class WorkerOperations:
|
|
|
280
276
|
Args:
|
|
281
277
|
worker_id: Worker identifier.
|
|
282
278
|
task_types: Task types to listen for.
|
|
283
|
-
options: Optional await options (block_ms
|
|
279
|
+
options: Optional await options (block_ms).
|
|
284
280
|
|
|
285
281
|
Returns:
|
|
286
282
|
WorkerAwaitResult with task if available.
|
|
@@ -290,12 +286,9 @@ class WorkerOperations:
|
|
|
290
286
|
|
|
291
287
|
value = serialize_worker_await_value(task_types)
|
|
292
288
|
|
|
293
|
-
# Build options for block_ms and timeout_ms
|
|
294
289
|
options_builder = OptionsBuilder()
|
|
295
290
|
if opts.block_ms is not None:
|
|
296
291
|
options_builder.add_u32(OptionTag.BLOCK_MS, opts.block_ms)
|
|
297
|
-
if opts.timeout_ms is not None:
|
|
298
|
-
options_builder.add_u32(OptionTag.TIMEOUT_MS, opts.timeout_ms)
|
|
299
292
|
|
|
300
293
|
response = await self._client._send_and_check(
|
|
301
294
|
OpCode.ACTION_AWAIT,
|
|
@@ -402,25 +395,25 @@ class WorkerOperations:
|
|
|
402
395
|
self,
|
|
403
396
|
options: WorkerListOptions | None = None,
|
|
404
397
|
) -> WorkerListResult:
|
|
405
|
-
"""List
|
|
398
|
+
"""List a page of the namespace's workers, in id order.
|
|
406
399
|
|
|
407
400
|
Args:
|
|
408
|
-
options:
|
|
401
|
+
options: The page's limit and cursor (the last page's ``cursor``;
|
|
402
|
+
None starts).
|
|
409
403
|
|
|
410
404
|
Returns:
|
|
411
|
-
WorkerListResult with
|
|
405
|
+
WorkerListResult with the page's workers and the next page's
|
|
406
|
+
cursor, None on the last page.
|
|
412
407
|
"""
|
|
413
408
|
opts = options or WorkerListOptions()
|
|
414
409
|
namespace = self._client.get_namespace(opts.namespace)
|
|
415
410
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
await self._client._send_and_check(
|
|
411
|
+
response = await self._client._send_and_check(
|
|
419
412
|
OpCode.WORKER_LIST,
|
|
420
413
|
namespace,
|
|
421
414
|
b"",
|
|
422
|
-
|
|
415
|
+
encode_worker_list_body(opts.limit, opts.cursor),
|
|
423
416
|
)
|
|
424
417
|
|
|
425
|
-
|
|
426
|
-
return WorkerListResult(workers=
|
|
418
|
+
workers, cursor = decode_worker_list_answer(response.data)
|
|
419
|
+
return WorkerListResult(workers=workers, cursor=cursor)
|
flo/client.py
CHANGED
|
@@ -10,19 +10,31 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
|
|
11
11
|
from .exceptions import (
|
|
12
12
|
ConnectionFailedError,
|
|
13
|
+
InvalidChecksumError,
|
|
13
14
|
InvalidEndpointError,
|
|
14
15
|
NotConnectedError,
|
|
16
|
+
ProtocolError,
|
|
17
|
+
RequestTimeoutError,
|
|
15
18
|
UnexpectedEofError,
|
|
16
19
|
raise_for_status,
|
|
17
20
|
)
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
21
|
+
from .retry import DEFAULT_RETRY_POLICY, RetryPolicy, _policy_backoff, _refused_unrun
|
|
22
|
+
from .types import _DEFAULT_WORKER_BLOCK_MS, HEADER_SIZE, OpCode, OptionTag, StatusCode
|
|
23
|
+
from .wire import (
|
|
24
|
+
OptionsIterator,
|
|
25
|
+
RawResponse,
|
|
26
|
+
compute_crc32,
|
|
27
|
+
parse_response_header,
|
|
28
|
+
serialize_request,
|
|
29
|
+
)
|
|
20
30
|
|
|
21
31
|
if TYPE_CHECKING:
|
|
22
32
|
from .worker import ActionWorker, StreamRecordHandler, StreamWorker
|
|
23
33
|
|
|
24
34
|
logger = logging.getLogger("flo")
|
|
25
35
|
|
|
36
|
+
_SERVER_AWAIT_DEFAULT_MS = 30_000
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
class FloClient:
|
|
28
40
|
"""Async client for Flo server.
|
|
@@ -40,19 +52,28 @@ class FloClient:
|
|
|
40
52
|
namespace: str = "default",
|
|
41
53
|
timeout_ms: int = 5000,
|
|
42
54
|
debug: bool = False,
|
|
55
|
+
retry: RetryPolicy = DEFAULT_RETRY_POLICY,
|
|
43
56
|
):
|
|
44
57
|
"""Initialize Flo client.
|
|
45
58
|
|
|
46
59
|
Args:
|
|
47
60
|
endpoint: Server endpoint in "host:port" format.
|
|
48
61
|
namespace: Default namespace for operations.
|
|
49
|
-
timeout_ms: Connection and operation timeout in milliseconds.
|
|
62
|
+
timeout_ms: Connection and operation timeout in milliseconds. A
|
|
63
|
+
blocking call (block_ms) is allowed this long on
|
|
64
|
+
top of its wait. A call that times out or is cancelled drops
|
|
65
|
+
the connection; call reconnect() before reusing the client.
|
|
50
66
|
debug: Enable debug logging.
|
|
67
|
+
retry: How a request the server refused without running it
|
|
68
|
+
(overloaded or unavailable, ran = no) is sent again; backoffs
|
|
69
|
+
are in milliseconds. RetryPolicy(max_attempts=1) turns
|
|
70
|
+
retrying off. Workers made from this client use it too.
|
|
51
71
|
"""
|
|
52
72
|
self._endpoint = endpoint
|
|
53
73
|
self._namespace = namespace
|
|
54
74
|
self._timeout = timeout_ms / 1000.0 # Convert to seconds
|
|
55
75
|
self._debug = debug
|
|
76
|
+
self._retry = retry
|
|
56
77
|
|
|
57
78
|
self._reader: asyncio.StreamReader | None = None
|
|
58
79
|
self._writer: asyncio.StreamWriter | None = None
|
|
@@ -187,13 +208,16 @@ class FloClient:
|
|
|
187
208
|
timeout: Maximum total time to keep retrying (default 5 min).
|
|
188
209
|
"""
|
|
189
210
|
async with self._lock:
|
|
190
|
-
# Close existing connection
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
# Close existing connection. It is detached before the await so a
|
|
212
|
+
# concurrent reconnect that installs its connection meanwhile
|
|
213
|
+
# keeps it.
|
|
214
|
+
old = self._writer
|
|
215
|
+
if old:
|
|
195
216
|
self._writer = None
|
|
196
217
|
self._reader = None
|
|
218
|
+
old.close()
|
|
219
|
+
with contextlib.suppress(Exception):
|
|
220
|
+
await old.wait_closed()
|
|
197
221
|
|
|
198
222
|
backoff = 1.0
|
|
199
223
|
max_backoff = 30.0
|
|
@@ -203,10 +227,16 @@ class FloClient:
|
|
|
203
227
|
while True:
|
|
204
228
|
attempt += 1
|
|
205
229
|
try:
|
|
206
|
-
|
|
230
|
+
reader, writer = await asyncio.wait_for(
|
|
207
231
|
asyncio.open_connection(self._host, self._port),
|
|
208
232
|
timeout=self._timeout,
|
|
209
233
|
)
|
|
234
|
+
# A concurrent reconnect may have installed its connection
|
|
235
|
+
# first; close it rather than leak it.
|
|
236
|
+
replaced = self._writer
|
|
237
|
+
self._reader, self._writer = reader, writer
|
|
238
|
+
if replaced is not None:
|
|
239
|
+
replaced.close()
|
|
210
240
|
if self._debug:
|
|
211
241
|
logger.debug(f"[flo] Reconnected to {self._endpoint} (attempt {attempt})")
|
|
212
242
|
return
|
|
@@ -244,6 +274,24 @@ class FloClient:
|
|
|
244
274
|
self._request_id += 1
|
|
245
275
|
return self._request_id
|
|
246
276
|
|
|
277
|
+
@staticmethod
|
|
278
|
+
def _server_wait_ms(op_code: OpCode, value: bytes, options: bytes) -> int:
|
|
279
|
+
"""How long the server may hold this request before answering."""
|
|
280
|
+
if op_code == OpCode.KV_GET:
|
|
281
|
+
# kv.get's body opens with wait_ms: [present:u8][u64].
|
|
282
|
+
if len(value) >= 9 and value[0] == 1:
|
|
283
|
+
return int.from_bytes(value[1:9], "little")
|
|
284
|
+
return 0
|
|
285
|
+
waits = {
|
|
286
|
+
opt.tag: opt.as_u32() or 0
|
|
287
|
+
for opt in OptionsIterator(options)
|
|
288
|
+
if opt.tag == OptionTag.BLOCK_MS
|
|
289
|
+
}
|
|
290
|
+
if op_code == OpCode.ACTION_AWAIT:
|
|
291
|
+
# An await that names no block_ms waits the server's default.
|
|
292
|
+
waits.setdefault(OptionTag.BLOCK_MS, _SERVER_AWAIT_DEFAULT_MS)
|
|
293
|
+
return max(waits.values(), default=0)
|
|
294
|
+
|
|
247
295
|
async def _send_request(
|
|
248
296
|
self,
|
|
249
297
|
op_code: OpCode,
|
|
@@ -252,7 +300,35 @@ class FloClient:
|
|
|
252
300
|
value: bytes,
|
|
253
301
|
options: bytes = b"",
|
|
254
302
|
) -> RawResponse:
|
|
255
|
-
"""Send a request and wait for response
|
|
303
|
+
"""Send a request and wait for its response, sending it again as the
|
|
304
|
+
retry policy allows while the server refuses it without running it.
|
|
305
|
+
|
|
306
|
+
Each try is a new request with a new id. The wait between tries is
|
|
307
|
+
taken outside the connection lock, so other calls proceed meanwhile.
|
|
308
|
+
"""
|
|
309
|
+
wait = _policy_backoff(self._retry)
|
|
310
|
+
attempt = 1
|
|
311
|
+
while True:
|
|
312
|
+
response = await self._send_once(op_code, namespace, key, value, options)
|
|
313
|
+
if attempt >= self._retry.max_attempts or not _refused_unrun(
|
|
314
|
+
response.status, response.data
|
|
315
|
+
):
|
|
316
|
+
return response
|
|
317
|
+
d = wait.next()
|
|
318
|
+
if self._debug:
|
|
319
|
+
logger.debug(f"[flo] {response.status.name}, not run: retrying in {d:.3f}s")
|
|
320
|
+
await asyncio.sleep(d)
|
|
321
|
+
attempt += 1
|
|
322
|
+
|
|
323
|
+
async def _send_once(
|
|
324
|
+
self,
|
|
325
|
+
op_code: OpCode,
|
|
326
|
+
namespace: str,
|
|
327
|
+
key: bytes,
|
|
328
|
+
value: bytes,
|
|
329
|
+
options: bytes = b"",
|
|
330
|
+
) -> RawResponse:
|
|
331
|
+
"""Send a request, under a new request id, and wait for its response.
|
|
256
332
|
|
|
257
333
|
Args:
|
|
258
334
|
op_code: Operation code.
|
|
@@ -272,6 +348,10 @@ class FloClient:
|
|
|
272
348
|
raise NotConnectedError("Not connected to server")
|
|
273
349
|
|
|
274
350
|
async with self._lock:
|
|
351
|
+
# A call that held the lock before this one may have dropped the
|
|
352
|
+
# connection.
|
|
353
|
+
if self._writer is None or self._reader is None:
|
|
354
|
+
raise NotConnectedError("Not connected to server")
|
|
275
355
|
request_id = self._next_request_id()
|
|
276
356
|
|
|
277
357
|
# Serialize request
|
|
@@ -287,46 +367,74 @@ class FloClient:
|
|
|
287
367
|
if self._debug:
|
|
288
368
|
logger.debug(f"[flo] -> {op_code.name} ns={namespace} key={key!r}")
|
|
289
369
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# Read response header
|
|
370
|
+
# The server may hold a blocking request for its whole wait, so it
|
|
371
|
+
# gets the operation timeout on top of that wait.
|
|
372
|
+
read_timeout = self._timeout + self._server_wait_ms(op_code, value, options) / 1000.0
|
|
373
|
+
writer, reader = self._writer, self._reader
|
|
295
374
|
try:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
timeout=self._timeout,
|
|
299
|
-
)
|
|
300
|
-
except asyncio.IncompleteReadError as e:
|
|
301
|
-
raise UnexpectedEofError("Connection closed while reading response header") from e
|
|
302
|
-
|
|
303
|
-
# Parse header to get data length
|
|
304
|
-
status, data_len, resp_request_id, crc = parse_response_header(header_data)
|
|
375
|
+
writer.write(request)
|
|
376
|
+
await writer.drain()
|
|
305
377
|
|
|
306
|
-
# Read response data
|
|
307
|
-
if data_len > 0:
|
|
308
378
|
try:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
timeout=
|
|
379
|
+
header_data = await asyncio.wait_for(
|
|
380
|
+
reader.readexactly(HEADER_SIZE),
|
|
381
|
+
timeout=read_timeout,
|
|
312
382
|
)
|
|
313
383
|
except asyncio.IncompleteReadError as e:
|
|
314
|
-
raise UnexpectedEofError(
|
|
315
|
-
|
|
316
|
-
|
|
384
|
+
raise UnexpectedEofError(
|
|
385
|
+
"Connection closed while reading response header"
|
|
386
|
+
) from e
|
|
387
|
+
except asyncio.TimeoutError as e:
|
|
388
|
+
raise RequestTimeoutError(f"No response to {op_code.name}") from e
|
|
389
|
+
|
|
390
|
+
status, data_len, resp_request_id, crc = parse_response_header(header_data)
|
|
391
|
+
|
|
392
|
+
if data_len > 0:
|
|
393
|
+
try:
|
|
394
|
+
response_data = await asyncio.wait_for(
|
|
395
|
+
reader.readexactly(data_len),
|
|
396
|
+
timeout=self._timeout,
|
|
397
|
+
)
|
|
398
|
+
except asyncio.IncompleteReadError as e:
|
|
399
|
+
raise UnexpectedEofError(
|
|
400
|
+
"Connection closed while reading response data"
|
|
401
|
+
) from e
|
|
402
|
+
except asyncio.TimeoutError as e:
|
|
403
|
+
raise RequestTimeoutError(
|
|
404
|
+
f"Response to {op_code.name} stalled mid-frame"
|
|
405
|
+
) from e
|
|
406
|
+
else:
|
|
407
|
+
response_data = b""
|
|
408
|
+
|
|
409
|
+
computed_crc = compute_crc32(header_data, response_data)
|
|
410
|
+
if computed_crc != crc:
|
|
411
|
+
raise InvalidChecksumError(
|
|
412
|
+
f"CRC32 mismatch: 0x{computed_crc:08X} != 0x{crc:08X}"
|
|
413
|
+
)
|
|
414
|
+
if resp_request_id == 0:
|
|
415
|
+
# The server answers a request it could not parse with
|
|
416
|
+
# id 0 and then closes; surface its error.
|
|
417
|
+
raise_for_status(status, response_data)
|
|
418
|
+
if resp_request_id != request_id:
|
|
419
|
+
raise ProtocolError(
|
|
420
|
+
f"Response is for request {resp_request_id}, expected {request_id}"
|
|
421
|
+
)
|
|
422
|
+
except BaseException:
|
|
423
|
+
# The reply to an abandoned request (timed out, cancelled, cut
|
|
424
|
+
# off mid-frame) may still arrive and would be read as the
|
|
425
|
+
# answer to the next one, and after a corrupt or mismatched
|
|
426
|
+
# frame the stream position is unknown, so the connection is
|
|
427
|
+
# dropped; the caller reconnects.
|
|
428
|
+
writer.close()
|
|
429
|
+
# reconnect() swaps the connection outside the lock.
|
|
430
|
+
if self._writer is writer:
|
|
431
|
+
self._writer = None
|
|
432
|
+
self._reader = None
|
|
433
|
+
raise
|
|
317
434
|
|
|
318
435
|
if self._debug:
|
|
319
436
|
logger.debug(f"[flo] <- {status.name} {len(response_data)} bytes")
|
|
320
437
|
|
|
321
|
-
# Verify CRC32
|
|
322
|
-
from .wire import compute_crc32
|
|
323
|
-
|
|
324
|
-
computed_crc = compute_crc32(header_data, response_data)
|
|
325
|
-
if computed_crc != crc:
|
|
326
|
-
from .exceptions import InvalidChecksumError
|
|
327
|
-
|
|
328
|
-
raise InvalidChecksumError(f"CRC32 mismatch: 0x{computed_crc:08X} != 0x{crc:08X}")
|
|
329
|
-
|
|
330
438
|
return RawResponse(status=status, data=response_data, request_id=resp_request_id)
|
|
331
439
|
|
|
332
440
|
async def _send_and_check(
|
|
@@ -376,7 +484,8 @@ class FloClient:
|
|
|
376
484
|
worker_id: str | None = None,
|
|
377
485
|
concurrency: int = 10,
|
|
378
486
|
action_timeout: float = 300.0,
|
|
379
|
-
block_ms: int =
|
|
487
|
+
block_ms: int = _DEFAULT_WORKER_BLOCK_MS,
|
|
488
|
+
auto_touch: bool = False,
|
|
380
489
|
) -> "ActionWorker":
|
|
381
490
|
"""Create a new ActionWorker from this client.
|
|
382
491
|
|
|
@@ -387,7 +496,11 @@ class FloClient:
|
|
|
387
496
|
worker_id: Unique worker identifier (auto-generated if not provided).
|
|
388
497
|
concurrency: Maximum number of concurrent actions.
|
|
389
498
|
action_timeout: Timeout for action handlers in seconds.
|
|
390
|
-
block_ms:
|
|
499
|
+
block_ms: Long-poll wait per await in milliseconds, at most 300000.
|
|
500
|
+
0 means the 30000 default.
|
|
501
|
+
auto_touch: Touch each run every third of its action's timeout
|
|
502
|
+
while its handler runs. Off by default: a handler that runs
|
|
503
|
+
past the timeout without touching is taken back and run again.
|
|
391
504
|
|
|
392
505
|
Returns:
|
|
393
506
|
A new ActionWorker instance ready to register actions and start.
|
|
@@ -408,6 +521,7 @@ class FloClient:
|
|
|
408
521
|
concurrency=concurrency,
|
|
409
522
|
action_timeout=action_timeout,
|
|
410
523
|
block_ms=block_ms,
|
|
524
|
+
auto_touch=auto_touch,
|
|
411
525
|
)
|
|
412
526
|
|
|
413
527
|
def new_stream_worker(
|
|
@@ -420,7 +534,7 @@ class FloClient:
|
|
|
420
534
|
worker_id: str | None = None,
|
|
421
535
|
concurrency: int = 10,
|
|
422
536
|
batch_size: int = 10,
|
|
423
|
-
block_ms: int =
|
|
537
|
+
block_ms: int = _DEFAULT_WORKER_BLOCK_MS,
|
|
424
538
|
message_timeout: float = 300.0,
|
|
425
539
|
redeliver_pending_on_reconnect: bool = True,
|
|
426
540
|
claim_min_idle_ms: int = 0,
|
|
@@ -439,7 +553,8 @@ class FloClient:
|
|
|
439
553
|
worker_id: Unique worker identifier (auto-generated if not provided).
|
|
440
554
|
concurrency: Maximum number of concurrent record handlers.
|
|
441
555
|
batch_size: Number of records to fetch per poll.
|
|
442
|
-
block_ms:
|
|
556
|
+
block_ms: Long-poll wait per read in milliseconds, at most 300000.
|
|
557
|
+
0 means the 30000 default.
|
|
443
558
|
message_timeout: Timeout for record handlers in seconds.
|
|
444
559
|
redeliver_pending_on_reconnect: Drain this consumer's pending
|
|
445
560
|
(delivered-but-unacked) entries via group_claim after a
|