flo-python 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev3__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 +80 -8
- flo/actions.py +44 -15
- flo/client.py +137 -16
- flo/exceptions.py +21 -0
- flo/kv.py +6 -6
- flo/processing.py +341 -0
- flo/streams.py +17 -16
- flo/types.py +440 -190
- flo/wire.py +107 -49
- flo/worker.py +610 -42
- flo/workflows.py +463 -0
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/METADATA +29 -4
- flo_python-0.1.0.dev3.dist-info/RECORD +16 -0
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/WHEEL +1 -1
- flo_python-0.1.0.dev2.dist-info/RECORD +0 -14
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/licenses/LICENSE +0 -0
flo/__init__.py
CHANGED
|
@@ -24,7 +24,7 @@ Example:
|
|
|
24
24
|
await client.action.invoke("process", b'{}')
|
|
25
25
|
|
|
26
26
|
# Worker (created from client)
|
|
27
|
-
worker = client.
|
|
27
|
+
worker = client.new_action_worker(concurrency=5)
|
|
28
28
|
worker.register_action("my-action", handler)
|
|
29
29
|
await worker.start()
|
|
30
30
|
|
|
@@ -45,6 +45,7 @@ from .exceptions import (
|
|
|
45
45
|
InvalidMagicError,
|
|
46
46
|
KeyTooLargeError,
|
|
47
47
|
NamespaceTooLargeError,
|
|
48
|
+
NonRetryableError,
|
|
48
49
|
NotConnectedError,
|
|
49
50
|
NotFoundError,
|
|
50
51
|
OverloadedError,
|
|
@@ -57,11 +58,11 @@ from .exceptions import (
|
|
|
57
58
|
UnsupportedVersionError,
|
|
58
59
|
ValidationError,
|
|
59
60
|
ValueTooLargeError,
|
|
61
|
+
is_connection_error,
|
|
60
62
|
)
|
|
63
|
+
from .processing import ProcessingOperations
|
|
61
64
|
from .types import (
|
|
62
|
-
# KV types
|
|
63
65
|
AckOptions,
|
|
64
|
-
# Action types
|
|
65
66
|
ActionDeleteOptions,
|
|
66
67
|
ActionInfo,
|
|
67
68
|
ActionInvokeOptions,
|
|
@@ -86,11 +87,22 @@ from .types import (
|
|
|
86
87
|
OpCode,
|
|
87
88
|
OptionTag,
|
|
88
89
|
PeekOptions,
|
|
90
|
+
ProcessingCancelOptions,
|
|
91
|
+
ProcessingListEntry,
|
|
92
|
+
ProcessingListOptions,
|
|
93
|
+
ProcessingRescaleOptions,
|
|
94
|
+
ProcessingRestoreOptions,
|
|
95
|
+
ProcessingSavepointOptions,
|
|
96
|
+
ProcessingStatusOptions,
|
|
97
|
+
ProcessingStatusResult,
|
|
98
|
+
ProcessingStopOptions,
|
|
99
|
+
ProcessingSubmitOptions,
|
|
100
|
+
ProcessingSyncOptions,
|
|
101
|
+
ProcessingSyncResult,
|
|
89
102
|
PutOptions,
|
|
90
103
|
ScanOptions,
|
|
91
104
|
ScanResult,
|
|
92
105
|
StatusCode,
|
|
93
|
-
# Stream types
|
|
94
106
|
StorageTier,
|
|
95
107
|
StreamAppendOptions,
|
|
96
108
|
StreamAppendResult,
|
|
@@ -105,7 +117,6 @@ from .types import (
|
|
|
105
117
|
StreamReadResult,
|
|
106
118
|
StreamRecord,
|
|
107
119
|
StreamTrimOptions,
|
|
108
|
-
# Worker types
|
|
109
120
|
TaskAssignment,
|
|
110
121
|
TouchOptions,
|
|
111
122
|
VersionEntry,
|
|
@@ -119,8 +130,31 @@ from .types import (
|
|
|
119
130
|
WorkerRegisterOptions,
|
|
120
131
|
WorkerTask,
|
|
121
132
|
WorkerTouchOptions,
|
|
133
|
+
WorkflowCancelOptions,
|
|
134
|
+
WorkflowCreateOptions,
|
|
135
|
+
WorkflowDisableOptions,
|
|
136
|
+
WorkflowEnableOptions,
|
|
137
|
+
WorkflowGetDefinitionOptions,
|
|
138
|
+
WorkflowHistoryOptions,
|
|
139
|
+
WorkflowListDefinitionsOptions,
|
|
140
|
+
WorkflowListRunsOptions,
|
|
141
|
+
WorkflowSignalOptions,
|
|
142
|
+
WorkflowStartOptions,
|
|
143
|
+
WorkflowStatusOptions,
|
|
144
|
+
WorkflowSyncOptions,
|
|
145
|
+
WorkflowSyncResult,
|
|
146
|
+
)
|
|
147
|
+
from .worker import (
|
|
148
|
+
ActionContext,
|
|
149
|
+
ActionResult,
|
|
150
|
+
ActionWorker,
|
|
151
|
+
ActionWorkerOptions,
|
|
152
|
+
StreamContext,
|
|
153
|
+
StreamRecordHandler,
|
|
154
|
+
StreamWorker,
|
|
155
|
+
StreamWorkerOptions,
|
|
122
156
|
)
|
|
123
|
-
from .
|
|
157
|
+
from .workflows import WorkflowOperations
|
|
124
158
|
|
|
125
159
|
__version__ = "0.1.0"
|
|
126
160
|
|
|
@@ -128,11 +162,19 @@ __all__ = [
|
|
|
128
162
|
# Client
|
|
129
163
|
"FloClient",
|
|
130
164
|
# High-level Worker API
|
|
131
|
-
"
|
|
132
|
-
"
|
|
165
|
+
"ActionWorker",
|
|
166
|
+
"ActionWorkerOptions",
|
|
133
167
|
"ActionContext",
|
|
168
|
+
"ActionResult",
|
|
169
|
+
"StreamWorker",
|
|
170
|
+
"StreamWorkerOptions",
|
|
171
|
+
"StreamContext",
|
|
172
|
+
"StreamRecordHandler",
|
|
173
|
+
"WorkflowOperations",
|
|
174
|
+
"ProcessingOperations",
|
|
134
175
|
# Exceptions
|
|
135
176
|
"FloError",
|
|
177
|
+
"NonRetryableError",
|
|
136
178
|
"NotConnectedError",
|
|
137
179
|
"ConnectionFailedError",
|
|
138
180
|
"InvalidEndpointError",
|
|
@@ -156,6 +198,7 @@ __all__ = [
|
|
|
156
198
|
"RateLimitedError",
|
|
157
199
|
"InternalServerError",
|
|
158
200
|
"GenericServerError",
|
|
201
|
+
"is_connection_error",
|
|
159
202
|
# Types
|
|
160
203
|
"OpCode",
|
|
161
204
|
"StatusCode",
|
|
@@ -221,4 +264,33 @@ __all__ = [
|
|
|
221
264
|
"WorkerCompleteOptions",
|
|
222
265
|
"WorkerFailOptions",
|
|
223
266
|
"WorkerListOptions",
|
|
267
|
+
# Workflow types
|
|
268
|
+
"WorkflowOperations",
|
|
269
|
+
"WorkflowCreateOptions",
|
|
270
|
+
"WorkflowGetDefinitionOptions",
|
|
271
|
+
"WorkflowStartOptions",
|
|
272
|
+
"WorkflowStatusOptions",
|
|
273
|
+
"WorkflowSignalOptions",
|
|
274
|
+
"WorkflowCancelOptions",
|
|
275
|
+
"WorkflowHistoryOptions",
|
|
276
|
+
"WorkflowListRunsOptions",
|
|
277
|
+
"WorkflowListDefinitionsOptions",
|
|
278
|
+
"WorkflowDisableOptions",
|
|
279
|
+
"WorkflowEnableOptions",
|
|
280
|
+
"WorkflowSyncOptions",
|
|
281
|
+
"WorkflowSyncResult",
|
|
282
|
+
# Processing types
|
|
283
|
+
"ProcessingOperations",
|
|
284
|
+
"ProcessingSubmitOptions",
|
|
285
|
+
"ProcessingStatusOptions",
|
|
286
|
+
"ProcessingListOptions",
|
|
287
|
+
"ProcessingStopOptions",
|
|
288
|
+
"ProcessingCancelOptions",
|
|
289
|
+
"ProcessingSavepointOptions",
|
|
290
|
+
"ProcessingRestoreOptions",
|
|
291
|
+
"ProcessingRescaleOptions",
|
|
292
|
+
"ProcessingSyncOptions",
|
|
293
|
+
"ProcessingStatusResult",
|
|
294
|
+
"ProcessingListEntry",
|
|
295
|
+
"ProcessingSyncResult",
|
|
224
296
|
]
|
flo/actions.py
CHANGED
|
@@ -68,7 +68,7 @@ class ActionOperations:
|
|
|
68
68
|
|
|
69
69
|
Args:
|
|
70
70
|
name: Action name.
|
|
71
|
-
action_type: Type of action (USER
|
|
71
|
+
action_type: Type of action (USER only).
|
|
72
72
|
options: Optional registration options.
|
|
73
73
|
"""
|
|
74
74
|
opts = options or ActionRegisterOptions()
|
|
@@ -231,7 +231,12 @@ class WorkerOperations:
|
|
|
231
231
|
opts = options or WorkerRegisterOptions()
|
|
232
232
|
namespace = self._client.get_namespace(opts.namespace)
|
|
233
233
|
|
|
234
|
-
value = serialize_worker_register_value(
|
|
234
|
+
value = serialize_worker_register_value(
|
|
235
|
+
task_types,
|
|
236
|
+
max_concurrency=opts.concurrency,
|
|
237
|
+
metadata=opts.metadata,
|
|
238
|
+
machine_id=opts.machine_id,
|
|
239
|
+
)
|
|
235
240
|
|
|
236
241
|
await self._client._send_and_check(
|
|
237
242
|
OpCode.WORKER_REGISTER,
|
|
@@ -240,6 +245,30 @@ class WorkerOperations:
|
|
|
240
245
|
value,
|
|
241
246
|
)
|
|
242
247
|
|
|
248
|
+
async def heartbeat(
|
|
249
|
+
self,
|
|
250
|
+
worker_id: str,
|
|
251
|
+
current_load: int = 0,
|
|
252
|
+
namespace: str | None = None,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Send a worker heartbeat to keep the registration alive.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
worker_id: Worker identifier.
|
|
258
|
+
current_load: Current number of active tasks.
|
|
259
|
+
namespace: Optional namespace override.
|
|
260
|
+
"""
|
|
261
|
+
import struct
|
|
262
|
+
|
|
263
|
+
ns = self._client.get_namespace(namespace)
|
|
264
|
+
value = struct.pack("<I", current_load)
|
|
265
|
+
await self._client._send_and_check(
|
|
266
|
+
OpCode.WORKER_HEARTBEAT,
|
|
267
|
+
ns,
|
|
268
|
+
worker_id.encode("utf-8"),
|
|
269
|
+
value,
|
|
270
|
+
)
|
|
271
|
+
|
|
243
272
|
async def await_task(
|
|
244
273
|
self,
|
|
245
274
|
worker_id: str,
|
|
@@ -269,7 +298,7 @@ class WorkerOperations:
|
|
|
269
298
|
options_builder.add_u32(OptionTag.TIMEOUT_MS, opts.timeout_ms)
|
|
270
299
|
|
|
271
300
|
response = await self._client._send_and_check(
|
|
272
|
-
OpCode.
|
|
301
|
+
OpCode.ACTION_AWAIT,
|
|
273
302
|
namespace,
|
|
274
303
|
worker_id.encode("utf-8"),
|
|
275
304
|
value,
|
|
@@ -287,6 +316,7 @@ class WorkerOperations:
|
|
|
287
316
|
async def touch(
|
|
288
317
|
self,
|
|
289
318
|
worker_id: str,
|
|
319
|
+
action_name: str,
|
|
290
320
|
task_id: str,
|
|
291
321
|
options: WorkerTouchOptions | None = None,
|
|
292
322
|
) -> None:
|
|
@@ -294,16 +324,17 @@ class WorkerOperations:
|
|
|
294
324
|
|
|
295
325
|
Args:
|
|
296
326
|
worker_id: Worker identifier.
|
|
327
|
+
action_name: Action name for routing.
|
|
297
328
|
task_id: Task identifier to extend.
|
|
298
329
|
options: Optional touch options (extend_ms).
|
|
299
330
|
"""
|
|
300
331
|
opts = options or WorkerTouchOptions()
|
|
301
332
|
namespace = self._client.get_namespace(opts.namespace)
|
|
302
333
|
|
|
303
|
-
value = serialize_worker_touch_value(task_id, opts.extend_ms)
|
|
334
|
+
value = serialize_worker_touch_value(action_name, task_id, opts.extend_ms)
|
|
304
335
|
|
|
305
336
|
await self._client._send_and_check(
|
|
306
|
-
OpCode.
|
|
337
|
+
OpCode.ACTION_TOUCH,
|
|
307
338
|
namespace,
|
|
308
339
|
worker_id.encode("utf-8"),
|
|
309
340
|
value,
|
|
@@ -312,6 +343,7 @@ class WorkerOperations:
|
|
|
312
343
|
async def complete(
|
|
313
344
|
self,
|
|
314
345
|
worker_id: str,
|
|
346
|
+
action_name: str,
|
|
315
347
|
task_id: str,
|
|
316
348
|
result: bytes,
|
|
317
349
|
options: WorkerCompleteOptions | None = None,
|
|
@@ -320,6 +352,7 @@ class WorkerOperations:
|
|
|
320
352
|
|
|
321
353
|
Args:
|
|
322
354
|
worker_id: Worker identifier.
|
|
355
|
+
action_name: Action name for routing.
|
|
323
356
|
task_id: Task identifier to complete.
|
|
324
357
|
result: Result data from the task.
|
|
325
358
|
options: Optional complete options.
|
|
@@ -327,10 +360,10 @@ class WorkerOperations:
|
|
|
327
360
|
opts = options or WorkerCompleteOptions()
|
|
328
361
|
namespace = self._client.get_namespace(opts.namespace)
|
|
329
362
|
|
|
330
|
-
value = serialize_worker_complete_value(task_id, result)
|
|
363
|
+
value = serialize_worker_complete_value(action_name, task_id, result, opts.outcome)
|
|
331
364
|
|
|
332
365
|
await self._client._send_and_check(
|
|
333
|
-
OpCode.
|
|
366
|
+
OpCode.ACTION_COMPLETE,
|
|
334
367
|
namespace,
|
|
335
368
|
worker_id.encode("utf-8"),
|
|
336
369
|
value,
|
|
@@ -339,6 +372,7 @@ class WorkerOperations:
|
|
|
339
372
|
async def fail(
|
|
340
373
|
self,
|
|
341
374
|
worker_id: str,
|
|
375
|
+
action_name: str,
|
|
342
376
|
task_id: str,
|
|
343
377
|
error_message: str,
|
|
344
378
|
options: WorkerFailOptions | None = None,
|
|
@@ -347,6 +381,7 @@ class WorkerOperations:
|
|
|
347
381
|
|
|
348
382
|
Args:
|
|
349
383
|
worker_id: Worker identifier.
|
|
384
|
+
action_name: Action name for routing.
|
|
350
385
|
task_id: Task identifier that failed.
|
|
351
386
|
error_message: Error message describing the failure.
|
|
352
387
|
options: Optional fail options (retry flag).
|
|
@@ -354,19 +389,13 @@ class WorkerOperations:
|
|
|
354
389
|
opts = options or WorkerFailOptions()
|
|
355
390
|
namespace = self._client.get_namespace(opts.namespace)
|
|
356
391
|
|
|
357
|
-
value = serialize_worker_fail_value(task_id, error_message)
|
|
358
|
-
|
|
359
|
-
# Retry flag goes in TLV options (matches Go SDK)
|
|
360
|
-
options_builder = OptionsBuilder()
|
|
361
|
-
if opts.retry:
|
|
362
|
-
options_builder.add_flag(OptionTag.RETRY)
|
|
392
|
+
value = serialize_worker_fail_value(action_name, task_id, error_message, opts.retry)
|
|
363
393
|
|
|
364
394
|
await self._client._send_and_check(
|
|
365
|
-
OpCode.
|
|
395
|
+
OpCode.ACTION_FAIL,
|
|
366
396
|
namespace,
|
|
367
397
|
worker_id.encode("utf-8"),
|
|
368
398
|
value,
|
|
369
|
-
options_builder.build(),
|
|
370
399
|
)
|
|
371
400
|
|
|
372
401
|
async def list(
|
flo/client.py
CHANGED
|
@@ -19,7 +19,7 @@ from .types import HEADER_SIZE, OpCode, StatusCode
|
|
|
19
19
|
from .wire import RawResponse, parse_response_header, serialize_request
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from .worker import
|
|
22
|
+
from .worker import ActionWorker, StreamRecordHandler, StreamWorker
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger("flo")
|
|
25
25
|
|
|
@@ -65,14 +65,18 @@ class FloClient:
|
|
|
65
65
|
# Initialize operation mixins (will be set after import to avoid circular deps)
|
|
66
66
|
from .actions import ActionOperations, WorkerOperations
|
|
67
67
|
from .kv import KVOperations
|
|
68
|
+
from .processing import ProcessingOperations
|
|
68
69
|
from .queue import QueueOperations
|
|
69
70
|
from .streams import StreamOperations
|
|
71
|
+
from .workflows import WorkflowOperations
|
|
70
72
|
|
|
71
73
|
self.kv = KVOperations(self)
|
|
72
74
|
self.queue = QueueOperations(self)
|
|
73
75
|
self.stream = StreamOperations(self)
|
|
74
76
|
self.action = ActionOperations(self)
|
|
75
77
|
self.worker = WorkerOperations(self)
|
|
78
|
+
self.workflow = WorkflowOperations(self)
|
|
79
|
+
self.processing = ProcessingOperations(self)
|
|
76
80
|
|
|
77
81
|
if debug:
|
|
78
82
|
logging.basicConfig(level=logging.DEBUG)
|
|
@@ -133,14 +137,73 @@ class FloClient:
|
|
|
133
137
|
|
|
134
138
|
async def close(self) -> None:
|
|
135
139
|
"""Close the connection."""
|
|
136
|
-
|
|
137
|
-
self._writer
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
async with self._lock:
|
|
141
|
+
if self._writer:
|
|
142
|
+
self._writer.close()
|
|
143
|
+
with contextlib.suppress(Exception):
|
|
144
|
+
await self._writer.wait_closed()
|
|
145
|
+
self._writer = None
|
|
146
|
+
self._reader = None
|
|
147
|
+
if self._debug:
|
|
148
|
+
logger.debug("[flo] Disconnected")
|
|
149
|
+
|
|
150
|
+
def interrupt(self) -> None:
|
|
151
|
+
"""Forcibly close the underlying connection without acquiring the lock.
|
|
152
|
+
|
|
153
|
+
This unblocks any in-flight ``_send_request`` that is holding the lock
|
|
154
|
+
(e.g. a blocking Await). The interrupted call will raise an error, and
|
|
155
|
+
the caller should reconnect before reusing the client.
|
|
156
|
+
"""
|
|
157
|
+
writer = self._writer
|
|
158
|
+
if writer is not None:
|
|
159
|
+
writer.close()
|
|
160
|
+
|
|
161
|
+
async def reconnect(self, *, timeout: float = 300.0) -> None:
|
|
162
|
+
"""Close and re-establish the connection with exponential back-off.
|
|
163
|
+
|
|
164
|
+
Retries from 1 s up to 30 s intervals until *timeout* seconds elapse.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
timeout: Maximum total time to keep retrying (default 5 min).
|
|
168
|
+
"""
|
|
169
|
+
async with self._lock:
|
|
170
|
+
# Close existing connection
|
|
171
|
+
if self._writer:
|
|
172
|
+
self._writer.close()
|
|
173
|
+
with contextlib.suppress(Exception):
|
|
174
|
+
await self._writer.wait_closed()
|
|
175
|
+
self._writer = None
|
|
176
|
+
self._reader = None
|
|
177
|
+
|
|
178
|
+
backoff = 1.0
|
|
179
|
+
max_backoff = 30.0
|
|
180
|
+
deadline = asyncio.get_event_loop().time() + timeout
|
|
181
|
+
attempt = 0
|
|
182
|
+
|
|
183
|
+
while True:
|
|
184
|
+
attempt += 1
|
|
185
|
+
try:
|
|
186
|
+
self._reader, self._writer = await asyncio.wait_for(
|
|
187
|
+
asyncio.open_connection(self._host, self._port),
|
|
188
|
+
timeout=self._timeout,
|
|
189
|
+
)
|
|
190
|
+
if self._debug:
|
|
191
|
+
logger.debug(f"[flo] Reconnected to {self._endpoint} (attempt {attempt})")
|
|
192
|
+
return
|
|
193
|
+
except (asyncio.TimeoutError, OSError) as exc:
|
|
194
|
+
now = asyncio.get_event_loop().time()
|
|
195
|
+
if now >= deadline:
|
|
196
|
+
raise ConnectionFailedError(
|
|
197
|
+
f"Failed to reconnect to {self._endpoint} after {attempt} attempts"
|
|
198
|
+
) from exc
|
|
199
|
+
logger.warning(
|
|
200
|
+
"[flo] Reconnect attempt %d failed: %s, retrying in %.0fs...",
|
|
201
|
+
attempt,
|
|
202
|
+
exc,
|
|
203
|
+
backoff,
|
|
204
|
+
)
|
|
205
|
+
await asyncio.sleep(backoff)
|
|
206
|
+
backoff = min(backoff * 2, max_backoff)
|
|
144
207
|
|
|
145
208
|
async def __aenter__(self) -> "FloClient":
|
|
146
209
|
"""Async context manager entry."""
|
|
@@ -287,15 +350,15 @@ class FloClient:
|
|
|
287
350
|
"""Get effective namespace, using override if provided."""
|
|
288
351
|
return override if override is not None else self._namespace
|
|
289
352
|
|
|
290
|
-
def
|
|
353
|
+
def new_action_worker(
|
|
291
354
|
self,
|
|
292
355
|
*,
|
|
293
356
|
worker_id: str | None = None,
|
|
294
357
|
concurrency: int = 10,
|
|
295
358
|
action_timeout: float = 300.0,
|
|
296
359
|
block_ms: int = 30000,
|
|
297
|
-
) -> "
|
|
298
|
-
"""Create a new
|
|
360
|
+
) -> "ActionWorker":
|
|
361
|
+
"""Create a new ActionWorker from this client.
|
|
299
362
|
|
|
300
363
|
The worker inherits the client's endpoint and namespace, and creates
|
|
301
364
|
a dedicated connection for polling tasks.
|
|
@@ -307,20 +370,78 @@ class FloClient:
|
|
|
307
370
|
block_ms: Timeout for blocking dequeue in milliseconds.
|
|
308
371
|
|
|
309
372
|
Returns:
|
|
310
|
-
A new
|
|
373
|
+
A new ActionWorker instance ready to register actions and start.
|
|
311
374
|
|
|
312
375
|
Example:
|
|
313
376
|
async with FloClient("localhost:3000", namespace="myapp") as client:
|
|
314
|
-
worker = client.
|
|
377
|
+
worker = client.new_action_worker(concurrency=5)
|
|
315
378
|
worker.register_action("process-order", process_order)
|
|
316
379
|
await worker.start()
|
|
317
380
|
"""
|
|
318
|
-
from .worker import
|
|
381
|
+
from .worker import ActionWorker
|
|
319
382
|
|
|
320
|
-
return
|
|
383
|
+
return ActionWorker(
|
|
321
384
|
self,
|
|
322
385
|
worker_id=worker_id,
|
|
323
386
|
concurrency=concurrency,
|
|
324
387
|
action_timeout=action_timeout,
|
|
325
388
|
block_ms=block_ms,
|
|
326
389
|
)
|
|
390
|
+
|
|
391
|
+
def new_stream_worker(
|
|
392
|
+
self,
|
|
393
|
+
*,
|
|
394
|
+
stream: str,
|
|
395
|
+
group: str,
|
|
396
|
+
handler: "StreamRecordHandler",
|
|
397
|
+
consumer: str | None = None,
|
|
398
|
+
worker_id: str | None = None,
|
|
399
|
+
concurrency: int = 10,
|
|
400
|
+
batch_size: int = 10,
|
|
401
|
+
block_ms: int = 30000,
|
|
402
|
+
message_timeout: float = 300.0,
|
|
403
|
+
) -> "StreamWorker":
|
|
404
|
+
"""Create a new StreamWorker from this client.
|
|
405
|
+
|
|
406
|
+
The worker joins a consumer group on the specified stream, polls for
|
|
407
|
+
records, and dispatches them to the handler. Records are auto-acked
|
|
408
|
+
on success and auto-nacked on error.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
stream: Stream name to consume from.
|
|
412
|
+
group: Consumer group name.
|
|
413
|
+
handler: Async callable that processes each StreamContext.
|
|
414
|
+
consumer: Consumer ID (auto-generated if not provided).
|
|
415
|
+
worker_id: Unique worker identifier (auto-generated if not provided).
|
|
416
|
+
concurrency: Maximum number of concurrent record handlers.
|
|
417
|
+
batch_size: Number of records to fetch per poll.
|
|
418
|
+
block_ms: Timeout for blocking read in milliseconds.
|
|
419
|
+
message_timeout: Timeout for record handlers in seconds.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
A new StreamWorker instance ready to start.
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
async def process_event(ctx):
|
|
426
|
+
event = ctx.json()
|
|
427
|
+
print(f"Got: {event}")
|
|
428
|
+
|
|
429
|
+
worker = client.new_stream_worker(
|
|
430
|
+
stream="events", group="processors", handler=process_event
|
|
431
|
+
)
|
|
432
|
+
await worker.start()
|
|
433
|
+
"""
|
|
434
|
+
from .worker import StreamWorker
|
|
435
|
+
|
|
436
|
+
return StreamWorker(
|
|
437
|
+
self,
|
|
438
|
+
handler,
|
|
439
|
+
stream=stream,
|
|
440
|
+
group=group,
|
|
441
|
+
consumer=consumer,
|
|
442
|
+
worker_id=worker_id,
|
|
443
|
+
concurrency=concurrency,
|
|
444
|
+
batch_size=batch_size,
|
|
445
|
+
block_ms=block_ms,
|
|
446
|
+
message_timeout=message_timeout,
|
|
447
|
+
)
|
flo/exceptions.py
CHANGED
|
@@ -14,6 +14,19 @@ class FloError(Exception):
|
|
|
14
14
|
pass
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class NonRetryableError(FloError):
|
|
18
|
+
"""Marks an action error as non-retryable.
|
|
19
|
+
|
|
20
|
+
When raised from an action handler, the task will be failed with
|
|
21
|
+
retry=False, so the server will not re-queue it.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
raise NonRetryableError("invalid input, will never succeed")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
17
30
|
# =============================================================================
|
|
18
31
|
# Connection Errors
|
|
19
32
|
# =============================================================================
|
|
@@ -190,6 +203,14 @@ class GenericServerError(ServerError):
|
|
|
190
203
|
super().__init__(message, StatusCode.ERROR_GENERIC)
|
|
191
204
|
|
|
192
205
|
|
|
206
|
+
def is_connection_error(exc: BaseException) -> bool:
|
|
207
|
+
"""Return True if the exception indicates a broken connection.
|
|
208
|
+
|
|
209
|
+
Connection errors may be resolved by reconnecting.
|
|
210
|
+
"""
|
|
211
|
+
return isinstance(exc, (UnexpectedEofError, NotConnectedError))
|
|
212
|
+
|
|
213
|
+
|
|
193
214
|
def raise_for_status(status: StatusCode, data: bytes = b"") -> None:
|
|
194
215
|
"""Raise appropriate exception for non-OK status codes.
|
|
195
216
|
|
flo/kv.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Key-value store operations for Flo client.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import struct
|
|
6
7
|
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from .types import (
|
|
@@ -194,17 +195,16 @@ class KVOperations:
|
|
|
194
195
|
|
|
195
196
|
prefix_bytes = prefix.encode("utf-8") if isinstance(prefix, str) else prefix
|
|
196
197
|
|
|
197
|
-
# Build TLV options
|
|
198
|
+
# Build TLV options (keys_only only — limit is in value now)
|
|
198
199
|
builder = OptionsBuilder()
|
|
199
200
|
|
|
200
|
-
if opts.limit is not None:
|
|
201
|
-
builder.add_u32(OptionTag.LIMIT, opts.limit)
|
|
202
|
-
|
|
203
201
|
if opts.keys_only:
|
|
204
202
|
builder.add_u8(OptionTag.KEYS_ONLY, 1)
|
|
205
203
|
|
|
206
|
-
#
|
|
207
|
-
|
|
204
|
+
# Value: [limit:u32][cursor...]
|
|
205
|
+
limit = opts.limit if opts.limit is not None else 0 # 0 = server default
|
|
206
|
+
cursor = opts.cursor if opts.cursor is not None else b""
|
|
207
|
+
value = struct.pack("<I", limit) + cursor
|
|
208
208
|
|
|
209
209
|
response = await self._client._send_and_check(
|
|
210
210
|
OpCode.KV_SCAN,
|