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 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
- WorkerTask,
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.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
- "KVTouchOptions",
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
- "UnsupportedVersionError",
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
- description=opts.description,
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
- priority=opts.priority,
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
- # Response contains run_id as string
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
- # Parse status response - format TBD based on server implementation
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, timeout_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 registered workers.
398
+ """List a page of the namespace's workers, in id order.
406
399
 
407
400
  Args:
408
- options: Optional list 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 list of workers.
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
- value = serialize_worker_list_value(opts.limit)
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
- value,
415
+ encode_worker_list_body(opts.limit, opts.cursor),
423
416
  )
424
417
 
425
- # Parse list response - format TBD based on server implementation
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 .types import HEADER_SIZE, OpCode, StatusCode
19
- from .wire import RawResponse, parse_response_header, serialize_request
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
- if self._writer:
192
- self._writer.close()
193
- with contextlib.suppress(Exception):
194
- await self._writer.wait_closed()
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
- self._reader, self._writer = await asyncio.wait_for(
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
- # Send request
291
- self._writer.write(request)
292
- await self._writer.drain()
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
- header_data = await asyncio.wait_for(
297
- self._reader.readexactly(HEADER_SIZE),
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
- response_data = await asyncio.wait_for(
310
- self._reader.readexactly(data_len),
311
- timeout=self._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("Connection closed while reading response data") from e
315
- else:
316
- response_data = b""
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 = 30000,
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: Timeout for blocking dequeue in milliseconds.
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 = 30000,
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: Timeout for blocking read in milliseconds.
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