localqueue 0.3.1__py3-none-any.whl → 0.3.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.
- localqueue/cli.py +318 -107
- localqueue/failure.py +6 -2
- localqueue/queue.py +9 -7
- localqueue/retry/store.py +2 -1
- localqueue/retry/tenacity.py +76 -22
- localqueue/store.py +3 -2
- localqueue/worker.py +24 -16
- {localqueue-0.3.1.dist-info → localqueue-0.3.2.dist-info}/METADATA +9 -2
- localqueue-0.3.2.dist-info/RECORD +15 -0
- localqueue-0.3.1.dist-info/RECORD +0 -15
- {localqueue-0.3.1.dist-info → localqueue-0.3.2.dist-info}/WHEEL +0 -0
- {localqueue-0.3.1.dist-info → localqueue-0.3.2.dist-info}/entry_points.txt +0 -0
- {localqueue-0.3.1.dist-info → localqueue-0.3.2.dist-info}/licenses/LICENSE +0 -0
localqueue/cli.py
CHANGED
|
@@ -8,13 +8,11 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
import time
|
|
10
10
|
from collections import Counter
|
|
11
|
-
from collections.abc import Callable
|
|
12
11
|
from contextlib import contextmanager
|
|
13
12
|
from dataclasses import dataclass
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from queue import Empty
|
|
16
|
-
from
|
|
17
|
-
from typing import Any, Iterator
|
|
15
|
+
from typing import Any, Iterator, TYPE_CHECKING
|
|
18
16
|
|
|
19
17
|
from tenacity import wait_none
|
|
20
18
|
|
|
@@ -35,6 +33,10 @@ from .worker import (
|
|
|
35
33
|
_sleep_for_policy,
|
|
36
34
|
)
|
|
37
35
|
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from types import FrameType
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
|
|
38
40
|
DEFAULT_STORE_PATH = str(default_queue_store_path())
|
|
39
41
|
DEFAULT_RETRY_STORE_PATH = str(default_retry_store_path())
|
|
40
42
|
CONFIG_FILENAME = "config.yaml"
|
|
@@ -114,7 +116,20 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
114
116
|
app.add_typer(queue_app, name="queue")
|
|
115
117
|
app.add_typer(retry_app, name="retry")
|
|
116
118
|
app.add_typer(config_app, name="config")
|
|
119
|
+
_register_config_commands(config_app, typer, yaml, console, err_console, config)
|
|
120
|
+
_register_queue_commands(queue_app, typer, console, err_console, config)
|
|
121
|
+
_register_retry_commands(retry_app, typer, console, err_console, config)
|
|
122
|
+
return app
|
|
117
123
|
|
|
124
|
+
|
|
125
|
+
def _register_config_commands(
|
|
126
|
+
config_app: Any,
|
|
127
|
+
typer: Any,
|
|
128
|
+
yaml: Any,
|
|
129
|
+
console: Any,
|
|
130
|
+
err_console: Any,
|
|
131
|
+
config: dict[str, Any],
|
|
132
|
+
) -> None:
|
|
118
133
|
@config_app.command("path")
|
|
119
134
|
def config_path() -> None:
|
|
120
135
|
console.print(str(_config_path()))
|
|
@@ -181,6 +196,26 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
181
196
|
config.update(updated)
|
|
182
197
|
_print_json(console, config)
|
|
183
198
|
|
|
199
|
+
|
|
200
|
+
def _register_queue_commands(
|
|
201
|
+
queue_app: Any,
|
|
202
|
+
typer: Any,
|
|
203
|
+
console: Any,
|
|
204
|
+
err_console: Any,
|
|
205
|
+
config: dict[str, Any],
|
|
206
|
+
) -> None:
|
|
207
|
+
_register_queue_basic_commands(queue_app, typer, console, err_console, config)
|
|
208
|
+
_register_queue_admin_commands(queue_app, typer, console, err_console, config)
|
|
209
|
+
_register_queue_worker_commands(queue_app, typer, console, err_console, config)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _register_queue_basic_commands(
|
|
213
|
+
queue_app: Any,
|
|
214
|
+
typer: Any,
|
|
215
|
+
console: Any,
|
|
216
|
+
err_console: Any,
|
|
217
|
+
config: dict[str, Any],
|
|
218
|
+
) -> None:
|
|
184
219
|
@queue_app.command("add")
|
|
185
220
|
def queue_add(
|
|
186
221
|
queue: str,
|
|
@@ -210,6 +245,17 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
210
245
|
)
|
|
211
246
|
_print_json(console, _message_payload(message))
|
|
212
247
|
|
|
248
|
+
_register_queue_message_commands(queue_app, typer, console, err_console, config)
|
|
249
|
+
_register_queue_maintenance_commands(queue_app, typer, console, err_console, config)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _register_queue_message_commands(
|
|
253
|
+
queue_app: Any,
|
|
254
|
+
typer: Any,
|
|
255
|
+
console: Any,
|
|
256
|
+
err_console: Any,
|
|
257
|
+
config: dict[str, Any],
|
|
258
|
+
) -> None:
|
|
213
259
|
@queue_app.command("pop")
|
|
214
260
|
def queue_pop(
|
|
215
261
|
queue: str,
|
|
@@ -319,6 +365,27 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
319
365
|
) -> None:
|
|
320
366
|
console.print(_queue(queue, _resolve_store_path(store_path, config)).qsize())
|
|
321
367
|
|
|
368
|
+
|
|
369
|
+
def _register_queue_maintenance_commands(
|
|
370
|
+
queue_app: Any,
|
|
371
|
+
typer: Any,
|
|
372
|
+
console: Any,
|
|
373
|
+
err_console: Any,
|
|
374
|
+
config: dict[str, Any],
|
|
375
|
+
) -> None:
|
|
376
|
+
_register_queue_stats_commands(queue_app, typer, console, err_console, config)
|
|
377
|
+
_register_queue_dead_commands(queue_app, typer, console, err_console, config)
|
|
378
|
+
_register_queue_requeue_commands(queue_app, typer, console, err_console, config)
|
|
379
|
+
_register_queue_purge_commands(queue_app, typer, console, err_console, config)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _register_queue_stats_commands(
|
|
383
|
+
queue_app: Any,
|
|
384
|
+
typer: Any,
|
|
385
|
+
console: Any,
|
|
386
|
+
err_console: Any,
|
|
387
|
+
config: dict[str, Any],
|
|
388
|
+
) -> None:
|
|
322
389
|
@queue_app.command("stats")
|
|
323
390
|
def queue_stats(
|
|
324
391
|
queue: str,
|
|
@@ -362,6 +429,14 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
362
429
|
},
|
|
363
430
|
)
|
|
364
431
|
|
|
432
|
+
|
|
433
|
+
def _register_queue_dead_commands(
|
|
434
|
+
queue_app: Any,
|
|
435
|
+
typer: Any,
|
|
436
|
+
console: Any,
|
|
437
|
+
err_console: Any,
|
|
438
|
+
config: dict[str, Any],
|
|
439
|
+
) -> None:
|
|
365
440
|
@queue_app.command("dead")
|
|
366
441
|
def queue_dead(
|
|
367
442
|
queue: str,
|
|
@@ -434,39 +509,14 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
434
509
|
failed_within=failed_within,
|
|
435
510
|
)
|
|
436
511
|
|
|
437
|
-
@retry_app.command("prune")
|
|
438
|
-
def retry_prune(
|
|
439
|
-
older_than: float | None = typer.Option(None, "--older-than", min=0.0),
|
|
440
|
-
retry_store_path: str | None = typer.Option(None, "--retry-store-path"),
|
|
441
|
-
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
442
|
-
) -> None:
|
|
443
|
-
store = SQLiteAttemptStore(_resolve_retry_store_path(retry_store_path, config))
|
|
444
|
-
try:
|
|
445
|
-
resolved_older_than = _resolve_retry_record_ttl(older_than, config)
|
|
446
|
-
if resolved_older_than is None:
|
|
447
|
-
err_console.print(
|
|
448
|
-
"[red]pass --older-than or configure retry_record_ttl_seconds[/red]"
|
|
449
|
-
)
|
|
450
|
-
raise typer.Exit(1)
|
|
451
|
-
now = time.time()
|
|
452
|
-
deleted = (
|
|
453
|
-
store.count_exhausted_older_than(
|
|
454
|
-
older_than=resolved_older_than, now=now
|
|
455
|
-
)
|
|
456
|
-
if dry_run
|
|
457
|
-
else store.prune_exhausted(older_than=resolved_older_than, now=now)
|
|
458
|
-
)
|
|
459
|
-
finally:
|
|
460
|
-
store.close()
|
|
461
|
-
_print_json(
|
|
462
|
-
console,
|
|
463
|
-
{
|
|
464
|
-
"dry_run": dry_run,
|
|
465
|
-
"older_than": resolved_older_than,
|
|
466
|
-
"would_delete" if dry_run else "deleted": deleted,
|
|
467
|
-
},
|
|
468
|
-
)
|
|
469
512
|
|
|
513
|
+
def _register_queue_requeue_commands(
|
|
514
|
+
queue_app: Any,
|
|
515
|
+
typer: Any,
|
|
516
|
+
console: Any,
|
|
517
|
+
err_console: Any,
|
|
518
|
+
config: dict[str, Any],
|
|
519
|
+
) -> None:
|
|
470
520
|
@queue_app.command("requeue-dead")
|
|
471
521
|
def queue_requeue_dead(
|
|
472
522
|
queue: str,
|
|
@@ -518,6 +568,14 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
518
568
|
)
|
|
519
569
|
_print_json(console, {"id": message_id, "state": "requeued"})
|
|
520
570
|
|
|
571
|
+
|
|
572
|
+
def _register_queue_purge_commands(
|
|
573
|
+
queue_app: Any,
|
|
574
|
+
typer: Any,
|
|
575
|
+
console: Any,
|
|
576
|
+
err_console: Any,
|
|
577
|
+
config: dict[str, Any],
|
|
578
|
+
) -> None:
|
|
521
579
|
@queue_app.command("purge")
|
|
522
580
|
def queue_purge(
|
|
523
581
|
queue: str,
|
|
@@ -529,6 +587,55 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
529
587
|
_emit_event(err_console, "queue.purge", queue=queue, deleted=deleted)
|
|
530
588
|
console.print(deleted)
|
|
531
589
|
|
|
590
|
+
|
|
591
|
+
def _register_retry_commands(
|
|
592
|
+
retry_app: Any,
|
|
593
|
+
typer: Any,
|
|
594
|
+
console: Any,
|
|
595
|
+
err_console: Any,
|
|
596
|
+
config: dict[str, Any],
|
|
597
|
+
) -> None:
|
|
598
|
+
@retry_app.command("prune")
|
|
599
|
+
def retry_prune(
|
|
600
|
+
older_than: float | None = typer.Option(None, "--older-than", min=0.0),
|
|
601
|
+
retry_store_path: str | None = typer.Option(None, "--retry-store-path"),
|
|
602
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
603
|
+
) -> None:
|
|
604
|
+
store = SQLiteAttemptStore(_resolve_retry_store_path(retry_store_path, config))
|
|
605
|
+
try:
|
|
606
|
+
resolved_older_than = _resolve_retry_record_ttl(older_than, config)
|
|
607
|
+
if resolved_older_than is None:
|
|
608
|
+
err_console.print(
|
|
609
|
+
"[red]pass --older-than or configure retry_record_ttl_seconds[/red]"
|
|
610
|
+
)
|
|
611
|
+
raise typer.Exit(1)
|
|
612
|
+
now = time.time()
|
|
613
|
+
deleted = (
|
|
614
|
+
store.count_exhausted_older_than(
|
|
615
|
+
older_than=resolved_older_than, now=now
|
|
616
|
+
)
|
|
617
|
+
if dry_run
|
|
618
|
+
else store.prune_exhausted(older_than=resolved_older_than, now=now)
|
|
619
|
+
)
|
|
620
|
+
finally:
|
|
621
|
+
store.close()
|
|
622
|
+
_print_json(
|
|
623
|
+
console,
|
|
624
|
+
{
|
|
625
|
+
"dry_run": dry_run,
|
|
626
|
+
"older_than": resolved_older_than,
|
|
627
|
+
"would_delete" if dry_run else "deleted": deleted,
|
|
628
|
+
},
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _register_queue_admin_commands(
|
|
633
|
+
queue_app: Any,
|
|
634
|
+
typer: Any,
|
|
635
|
+
console: Any,
|
|
636
|
+
err_console: Any,
|
|
637
|
+
config: dict[str, Any],
|
|
638
|
+
) -> None:
|
|
532
639
|
@queue_app.command("process")
|
|
533
640
|
def queue_process(
|
|
534
641
|
queue: str,
|
|
@@ -606,6 +713,14 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
606
713
|
if exit_code:
|
|
607
714
|
raise typer.Exit(exit_code)
|
|
608
715
|
|
|
716
|
+
|
|
717
|
+
def _register_queue_worker_commands(
|
|
718
|
+
queue_app: Any,
|
|
719
|
+
typer: Any,
|
|
720
|
+
console: Any,
|
|
721
|
+
err_console: Any,
|
|
722
|
+
config: dict[str, Any],
|
|
723
|
+
) -> None:
|
|
609
724
|
@queue_app.command("exec")
|
|
610
725
|
def queue_exec(
|
|
611
726
|
queue: str,
|
|
@@ -681,8 +796,6 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
681
796
|
if exit_code:
|
|
682
797
|
raise typer.Exit(exit_code)
|
|
683
798
|
|
|
684
|
-
return app
|
|
685
|
-
|
|
686
799
|
|
|
687
800
|
def _queue(
|
|
688
801
|
name: str, store_path: str, *, lease_timeout: float = 30.0
|
|
@@ -869,86 +982,53 @@ def _process_queue_messages(
|
|
|
869
982
|
_print_json(console, {"state": "stopped", "processed": processed})
|
|
870
983
|
return 0
|
|
871
984
|
|
|
872
|
-
|
|
873
|
-
queue.record_worker_heartbeat(worker_id)
|
|
874
|
-
_sleep_for_policy(policy_state, resolved_policy)
|
|
875
|
-
|
|
876
|
-
try:
|
|
877
|
-
message = queue.get_message(
|
|
878
|
-
block=block,
|
|
879
|
-
timeout=_poll_timeout(forever, block, timeout),
|
|
880
|
-
leased_by=worker_id,
|
|
881
|
-
)
|
|
882
|
-
except Empty:
|
|
883
|
-
if forever:
|
|
884
|
-
if not block:
|
|
885
|
-
time.sleep(idle_sleep)
|
|
886
|
-
continue
|
|
887
|
-
if processed == 0:
|
|
888
|
-
if err_console is not None:
|
|
889
|
-
err_console.print("[yellow]queue is empty[/yellow]")
|
|
890
|
-
return 1
|
|
891
|
-
break
|
|
892
|
-
|
|
893
|
-
if log_events and err_console is not None:
|
|
894
|
-
_emit_event(
|
|
895
|
-
err_console,
|
|
896
|
-
f"{mode}.lease",
|
|
897
|
-
queue=message.queue,
|
|
898
|
-
message_id=message.id,
|
|
899
|
-
leased_by=message.leased_by,
|
|
900
|
-
attempts=message.attempts,
|
|
901
|
-
leased_until=message.leased_until,
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
if retry_store_path is not None and owned_retry_store is None:
|
|
905
|
-
owned_retry_store = SQLiteAttemptStore(retry_store_path)
|
|
906
|
-
|
|
907
|
-
result = _process_message(
|
|
985
|
+
iteration, owned_retry_store, empty_queue = _process_queue_iteration(
|
|
908
986
|
queue,
|
|
909
|
-
message,
|
|
910
987
|
handler,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
988
|
+
console=console,
|
|
989
|
+
err_console=err_console,
|
|
990
|
+
policy_state=policy_state,
|
|
991
|
+
resolved_policy=resolved_policy,
|
|
992
|
+
retry_store_path=retry_store_path,
|
|
993
|
+
owned_retry_store=owned_retry_store,
|
|
915
994
|
max_tries=max_tries,
|
|
995
|
+
worker_id=worker_id,
|
|
996
|
+
block=block,
|
|
997
|
+
timeout=timeout,
|
|
998
|
+
idle_sleep=idle_sleep,
|
|
916
999
|
release_delay=release_delay,
|
|
917
1000
|
dead_letter_on_exhaustion=dead_letter_on_exhaustion,
|
|
918
1001
|
log_events=log_events,
|
|
919
1002
|
mode=mode,
|
|
920
|
-
|
|
1003
|
+
forever=forever,
|
|
921
1004
|
)
|
|
922
|
-
if
|
|
1005
|
+
if empty_queue:
|
|
1006
|
+
if processed == 0 and err_console is not None:
|
|
1007
|
+
err_console.print("[yellow]queue is empty[/yellow]")
|
|
1008
|
+
if processed == 0:
|
|
1009
|
+
return 1
|
|
1010
|
+
break
|
|
1011
|
+
assert iteration is not None
|
|
1012
|
+
if _handle_processed_queue_result(
|
|
1013
|
+
queue,
|
|
1014
|
+
iteration.message,
|
|
1015
|
+
iteration.process_result,
|
|
1016
|
+
console=console,
|
|
1017
|
+
worker_id=worker_id,
|
|
1018
|
+
policy_state=policy_state,
|
|
1019
|
+
):
|
|
923
1020
|
processed += 1
|
|
924
|
-
_record_success(policy_state)
|
|
925
|
-
if worker_id is not None:
|
|
926
|
-
queue.record_worker_heartbeat(worker_id)
|
|
927
|
-
_print_json(console, {"id": message.id, "state": "acked"})
|
|
928
1021
|
continue
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
else {"id": message.id, "queue": message.queue}
|
|
940
|
-
)
|
|
941
|
-
_print_json(
|
|
942
|
-
console,
|
|
943
|
-
{
|
|
944
|
-
**payload,
|
|
945
|
-
"state": "failed",
|
|
946
|
-
"last_error": result.last_error,
|
|
947
|
-
},
|
|
948
|
-
)
|
|
949
|
-
if worker_id is not None:
|
|
950
|
-
queue.record_worker_heartbeat(worker_id)
|
|
951
|
-
if forever:
|
|
1022
|
+
if _handle_failed_queue_result(
|
|
1023
|
+
queue,
|
|
1024
|
+
iteration.message,
|
|
1025
|
+
iteration.process_result,
|
|
1026
|
+
console=console,
|
|
1027
|
+
worker_id=worker_id,
|
|
1028
|
+
policy_state=policy_state,
|
|
1029
|
+
resolved_policy=resolved_policy,
|
|
1030
|
+
forever=forever,
|
|
1031
|
+
):
|
|
952
1032
|
continue
|
|
953
1033
|
return 1
|
|
954
1034
|
|
|
@@ -958,6 +1038,137 @@ def _process_queue_messages(
|
|
|
958
1038
|
owned_retry_store.close()
|
|
959
1039
|
|
|
960
1040
|
|
|
1041
|
+
@dataclass(slots=True)
|
|
1042
|
+
class _QueueIterationResult:
|
|
1043
|
+
message: QueueMessage
|
|
1044
|
+
process_result: _ProcessResult
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _process_queue_iteration(
|
|
1048
|
+
queue: PersistentQueue,
|
|
1049
|
+
handler: Callable[[Any], Any],
|
|
1050
|
+
*,
|
|
1051
|
+
console: Any,
|
|
1052
|
+
err_console: Any,
|
|
1053
|
+
policy_state: WorkerPolicyState,
|
|
1054
|
+
resolved_policy: PersistentWorkerConfig,
|
|
1055
|
+
retry_store_path: str | None,
|
|
1056
|
+
owned_retry_store: SQLiteAttemptStore | None,
|
|
1057
|
+
max_tries: int,
|
|
1058
|
+
worker_id: str | None,
|
|
1059
|
+
block: bool,
|
|
1060
|
+
timeout: float | None,
|
|
1061
|
+
idle_sleep: float,
|
|
1062
|
+
release_delay: float,
|
|
1063
|
+
dead_letter_on_exhaustion: bool,
|
|
1064
|
+
log_events: bool,
|
|
1065
|
+
mode: str,
|
|
1066
|
+
forever: bool,
|
|
1067
|
+
) -> tuple[_QueueIterationResult | None, SQLiteAttemptStore | None, bool]:
|
|
1068
|
+
if worker_id is not None:
|
|
1069
|
+
queue.record_worker_heartbeat(worker_id)
|
|
1070
|
+
_sleep_for_policy(policy_state, resolved_policy)
|
|
1071
|
+
|
|
1072
|
+
try:
|
|
1073
|
+
message = queue.get_message(
|
|
1074
|
+
block=block,
|
|
1075
|
+
timeout=_poll_timeout(forever, block, timeout),
|
|
1076
|
+
leased_by=worker_id,
|
|
1077
|
+
)
|
|
1078
|
+
except Empty:
|
|
1079
|
+
if forever:
|
|
1080
|
+
if not block:
|
|
1081
|
+
time.sleep(idle_sleep)
|
|
1082
|
+
return None, owned_retry_store, True
|
|
1083
|
+
return None, owned_retry_store, True
|
|
1084
|
+
|
|
1085
|
+
if log_events and err_console is not None:
|
|
1086
|
+
_emit_event(
|
|
1087
|
+
err_console,
|
|
1088
|
+
f"{mode}.lease",
|
|
1089
|
+
queue=message.queue,
|
|
1090
|
+
message_id=message.id,
|
|
1091
|
+
leased_by=message.leased_by,
|
|
1092
|
+
attempts=message.attempts,
|
|
1093
|
+
leased_until=message.leased_until,
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
if retry_store_path is not None and owned_retry_store is None:
|
|
1097
|
+
owned_retry_store = SQLiteAttemptStore(retry_store_path)
|
|
1098
|
+
|
|
1099
|
+
result = _process_message(
|
|
1100
|
+
queue,
|
|
1101
|
+
message,
|
|
1102
|
+
handler,
|
|
1103
|
+
retry_store=owned_retry_store,
|
|
1104
|
+
retry_store_path=None if owned_retry_store is not None else retry_store_path,
|
|
1105
|
+
max_tries=max_tries,
|
|
1106
|
+
release_delay=release_delay,
|
|
1107
|
+
dead_letter_on_exhaustion=dead_letter_on_exhaustion,
|
|
1108
|
+
log_events=log_events,
|
|
1109
|
+
mode=mode,
|
|
1110
|
+
err_console=err_console,
|
|
1111
|
+
)
|
|
1112
|
+
return (
|
|
1113
|
+
_QueueIterationResult(message=message, process_result=result),
|
|
1114
|
+
owned_retry_store,
|
|
1115
|
+
False,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def _handle_processed_queue_result(
|
|
1120
|
+
queue: PersistentQueue,
|
|
1121
|
+
message: QueueMessage,
|
|
1122
|
+
result: _ProcessResult,
|
|
1123
|
+
*,
|
|
1124
|
+
console: Any,
|
|
1125
|
+
worker_id: str | None,
|
|
1126
|
+
policy_state: WorkerPolicyState,
|
|
1127
|
+
) -> bool:
|
|
1128
|
+
if not result.processed:
|
|
1129
|
+
return False
|
|
1130
|
+
_record_success(policy_state)
|
|
1131
|
+
if worker_id is not None:
|
|
1132
|
+
queue.record_worker_heartbeat(worker_id)
|
|
1133
|
+
_print_json(console, {"id": message.id, "state": "acked"})
|
|
1134
|
+
return True
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _handle_failed_queue_result(
|
|
1138
|
+
queue: PersistentQueue,
|
|
1139
|
+
message: QueueMessage,
|
|
1140
|
+
result: _ProcessResult,
|
|
1141
|
+
*,
|
|
1142
|
+
console: Any,
|
|
1143
|
+
worker_id: str | None,
|
|
1144
|
+
policy_state: WorkerPolicyState,
|
|
1145
|
+
resolved_policy: PersistentWorkerConfig,
|
|
1146
|
+
forever: bool,
|
|
1147
|
+
) -> bool:
|
|
1148
|
+
_record_failure(
|
|
1149
|
+
policy_state,
|
|
1150
|
+
resolved_policy,
|
|
1151
|
+
permanent=result.permanent_failure,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
payload = (
|
|
1155
|
+
_message_payload(current_message)
|
|
1156
|
+
if (current_message := queue.inspect(message.id)) is not None
|
|
1157
|
+
else {"id": message.id, "queue": message.queue}
|
|
1158
|
+
)
|
|
1159
|
+
_print_json(
|
|
1160
|
+
console,
|
|
1161
|
+
{
|
|
1162
|
+
**payload,
|
|
1163
|
+
"state": "failed",
|
|
1164
|
+
"last_error": result.last_error,
|
|
1165
|
+
},
|
|
1166
|
+
)
|
|
1167
|
+
if worker_id is not None:
|
|
1168
|
+
queue.record_worker_heartbeat(worker_id)
|
|
1169
|
+
return forever
|
|
1170
|
+
|
|
1171
|
+
|
|
961
1172
|
def _print_dead_letters(
|
|
962
1173
|
queue: PersistentQueue,
|
|
963
1174
|
*,
|
localqueue/failure.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Protocol
|
|
4
4
|
|
|
5
5
|
_PERMANENT_FAILURE_TYPES = (
|
|
6
6
|
ImportError,
|
|
@@ -9,7 +9,11 @@ _PERMANENT_FAILURE_TYPES = (
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
class _ExitCodeCarrier(Protocol):
|
|
13
|
+
exit_code: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_permanent_failure(error: BaseException | _ExitCodeCarrier) -> bool:
|
|
13
17
|
if getattr(error, "exit_code", None) == 127:
|
|
14
18
|
return True
|
|
15
19
|
return isinstance(error, _PERMANENT_FAILURE_TYPES)
|
localqueue/queue.py
CHANGED
|
@@ -5,13 +5,15 @@ from pathlib import Path
|
|
|
5
5
|
from collections.abc import Mapping
|
|
6
6
|
from queue import Empty, Full
|
|
7
7
|
from threading import Condition
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Generic, TypeVar, cast
|
|
9
9
|
|
|
10
10
|
from .paths import default_queue_store_path
|
|
11
11
|
from .store import QueueMessage, QueueStats, QueueStore, SQLiteQueueStore
|
|
12
12
|
|
|
13
|
+
T = TypeVar("T")
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
class PersistentQueue(Generic[T]):
|
|
15
17
|
name: str
|
|
16
18
|
lease_timeout: float
|
|
17
19
|
maxsize: int
|
|
@@ -52,7 +54,7 @@ class PersistentQueue:
|
|
|
52
54
|
|
|
53
55
|
def put(
|
|
54
56
|
self,
|
|
55
|
-
item:
|
|
57
|
+
item: T,
|
|
56
58
|
block: bool = True,
|
|
57
59
|
timeout: float | None = None,
|
|
58
60
|
*,
|
|
@@ -81,7 +83,7 @@ class PersistentQueue:
|
|
|
81
83
|
_ = self._condition.notify_all()
|
|
82
84
|
return message
|
|
83
85
|
|
|
84
|
-
def put_nowait(self, item:
|
|
86
|
+
def put_nowait(self, item: T) -> QueueMessage:
|
|
85
87
|
return self.put(item, block=False)
|
|
86
88
|
|
|
87
89
|
def get(
|
|
@@ -90,13 +92,13 @@ class PersistentQueue:
|
|
|
90
92
|
timeout: float | None = None,
|
|
91
93
|
*,
|
|
92
94
|
leased_by: str | None = None,
|
|
93
|
-
) ->
|
|
95
|
+
) -> T:
|
|
94
96
|
message = self.get_message(block=block, timeout=timeout, leased_by=leased_by)
|
|
95
97
|
with self._condition:
|
|
96
98
|
self._unfinished[message.id] = message
|
|
97
|
-
return message.value
|
|
99
|
+
return cast("T", message.value)
|
|
98
100
|
|
|
99
|
-
def get_nowait(self) ->
|
|
101
|
+
def get_nowait(self) -> T:
|
|
100
102
|
return self.get(block=False)
|
|
101
103
|
|
|
102
104
|
def get_message(
|
localqueue/retry/store.py
CHANGED
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
+
from types import ModuleType
|
|
13
14
|
import lmdb
|
|
14
15
|
|
|
15
16
|
_ENVS: dict[tuple[str, int], Any] = {}
|
|
@@ -17,7 +18,7 @@ _ENVS_LOCK = threading.Lock()
|
|
|
17
18
|
_SQLITE_RETRY_SCHEMA_VERSION = 1
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def _import_lmdb() ->
|
|
21
|
+
def _import_lmdb() -> ModuleType:
|
|
21
22
|
try:
|
|
22
23
|
import lmdb
|
|
23
24
|
except ModuleNotFoundError as exc:
|
localqueue/retry/tenacity.py
CHANGED
|
@@ -9,6 +9,7 @@ import weakref
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Callable, TypeVar, cast
|
|
12
|
+
import asyncio
|
|
12
13
|
|
|
13
14
|
from tenacity import DoAttempt, DoSleep
|
|
14
15
|
from tenacity import AsyncRetrying, RetryCallState, Retrying
|
|
@@ -100,7 +101,7 @@ def _get_default_store() -> AttemptStore:
|
|
|
100
101
|
_default_owned_stores.add(store)
|
|
101
102
|
except TypeError:
|
|
102
103
|
pass
|
|
103
|
-
return cast(AttemptStore, store)
|
|
104
|
+
return cast("AttemptStore", store)
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
def configure_default_store(store: AttemptStore | None = None) -> None:
|
|
@@ -321,7 +322,7 @@ class _PersistentMixin:
|
|
|
321
322
|
context = getattr(self._local, "persistent_context", None)
|
|
322
323
|
if context is None:
|
|
323
324
|
raise RuntimeError("persistent retry context not initialized")
|
|
324
|
-
return cast(PersistentCallContext, context)
|
|
325
|
+
return cast("PersistentCallContext", context)
|
|
325
326
|
|
|
326
327
|
def _translate_state(self, retry_state: RetryCallState) -> PersistentRetryState:
|
|
327
328
|
context = self._current_context()
|
|
@@ -411,7 +412,7 @@ class _PersistentMixin:
|
|
|
411
412
|
|
|
412
413
|
@property
|
|
413
414
|
def statistics(self) -> dict[str, Any]:
|
|
414
|
-
return cast(dict[str, Any], self._retrying.statistics)
|
|
415
|
+
return cast("dict[str, Any]", self._retrying.statistics)
|
|
415
416
|
|
|
416
417
|
def begin(self) -> None:
|
|
417
418
|
self._retrying.begin()
|
|
@@ -426,7 +427,7 @@ class _PersistentMixin:
|
|
|
426
427
|
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
|
|
427
428
|
copy = self.copy()
|
|
428
429
|
wrapped_f.statistics = copy.statistics # type: ignore[attr-defined]
|
|
429
|
-
return cast(Any, copy)(f, *args, **kwargs)
|
|
430
|
+
return cast("Any", copy)(f, *args, **kwargs)
|
|
430
431
|
|
|
431
432
|
def retry_with(*args: Any, **kwargs: Any) -> WrappedFn:
|
|
432
433
|
return self.copy(*args, **kwargs).wraps(f)
|
|
@@ -434,7 +435,7 @@ class _PersistentMixin:
|
|
|
434
435
|
wrapped_f.retry = self # type: ignore[attr-defined]
|
|
435
436
|
wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
|
|
436
437
|
wrapped_f.statistics = {} # type: ignore[attr-defined]
|
|
437
|
-
return cast(WrappedFn, wrapped_f)
|
|
438
|
+
return cast("WrappedFn", wrapped_f)
|
|
438
439
|
|
|
439
440
|
def copy(self, **kwargs: Any) -> "_PersistentMixin":
|
|
440
441
|
store = kwargs.pop("store", self._store)
|
|
@@ -449,17 +450,16 @@ class _PersistentMixin:
|
|
|
449
450
|
_ = tenacity_kwargs.pop("stop", None)
|
|
450
451
|
|
|
451
452
|
return self.__class__(
|
|
452
|
-
store=cast(AttemptStore | None, store),
|
|
453
|
+
store=cast("AttemptStore | None", store),
|
|
453
454
|
store_path=self._store_path
|
|
454
455
|
if store_path is _UNSET
|
|
455
|
-
else cast(str | Path | None, store_path),
|
|
456
|
-
key=cast(str | None, key),
|
|
456
|
+
else cast("str | Path | None", store_path),
|
|
457
|
+
key=cast("str | None", key),
|
|
457
458
|
key_fn=cast(
|
|
458
|
-
Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
459
|
-
| None,
|
|
459
|
+
"Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str] | None",
|
|
460
460
|
key_fn,
|
|
461
461
|
),
|
|
462
|
-
clear_on_success=cast(bool, clear_on_success),
|
|
462
|
+
clear_on_success=cast("bool", clear_on_success),
|
|
463
463
|
**({} if max_tries is _UNSET else {"max_tries": max_tries}),
|
|
464
464
|
**tenacity_kwargs,
|
|
465
465
|
)
|
|
@@ -506,7 +506,10 @@ class PersistentRetrying(_PersistentMixin):
|
|
|
506
506
|
self._local.persistent_context = context
|
|
507
507
|
try:
|
|
508
508
|
retry_state = RetryCallState(
|
|
509
|
-
retry_object=cast(Any, self._retrying),
|
|
509
|
+
retry_object=cast("Any", self._retrying),
|
|
510
|
+
fn=fn,
|
|
511
|
+
args=args,
|
|
512
|
+
kwargs=kwargs,
|
|
510
513
|
)
|
|
511
514
|
while True:
|
|
512
515
|
do = self.iter(retry_state=retry_state)
|
|
@@ -527,7 +530,7 @@ class PersistentRetrying(_PersistentMixin):
|
|
|
527
530
|
retry_state.set_result(result)
|
|
528
531
|
elif isinstance(do, DoSleep):
|
|
529
532
|
retry_state.prepare_for_next_attempt()
|
|
530
|
-
cast(Any, self._retrying).sleep(do)
|
|
533
|
+
cast("Any", self._retrying).sleep(do)
|
|
531
534
|
else:
|
|
532
535
|
self._clear_if_success(retry_state)
|
|
533
536
|
return do
|
|
@@ -605,10 +608,13 @@ class PersistentAsyncRetrying(_PersistentMixin):
|
|
|
605
608
|
if _utils.is_coroutine_callable(self._user_stop):
|
|
606
609
|
|
|
607
610
|
async def async_wrapped_stop(retry_state: RetryCallState) -> bool:
|
|
611
|
+
context = self._current_context()
|
|
608
612
|
should_stop = bool(
|
|
609
613
|
await self._user_stop(self._translate_state(retry_state))
|
|
610
614
|
)
|
|
611
|
-
self.
|
|
615
|
+
await self._persist_attempt_async(
|
|
616
|
+
context, retry_state, exhausted=should_stop
|
|
617
|
+
)
|
|
612
618
|
return should_stop
|
|
613
619
|
|
|
614
620
|
return async_wrapped_stop
|
|
@@ -645,7 +651,8 @@ class PersistentAsyncRetrying(_PersistentMixin):
|
|
|
645
651
|
async def async_wrapped_retry_error_callback(
|
|
646
652
|
retry_state: RetryCallState,
|
|
647
653
|
) -> Any:
|
|
648
|
-
self.
|
|
654
|
+
context = self._current_context()
|
|
655
|
+
await self._persist_attempt_async(context, retry_state, exhausted=True)
|
|
649
656
|
return await self._user_retry_error_callback(
|
|
650
657
|
self._translate_state(retry_state)
|
|
651
658
|
)
|
|
@@ -658,19 +665,66 @@ class PersistentAsyncRetrying(_PersistentMixin):
|
|
|
658
665
|
|
|
659
666
|
return wrapped_retry_error_callback
|
|
660
667
|
|
|
668
|
+
async def _load_context_async(
|
|
669
|
+
self, fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
670
|
+
) -> PersistentCallContext:
|
|
671
|
+
return await asyncio.to_thread(self._load_context, fn, args, kwargs)
|
|
672
|
+
|
|
673
|
+
async def _persist_attempt_async(
|
|
674
|
+
self,
|
|
675
|
+
context: PersistentCallContext,
|
|
676
|
+
retry_state: RetryCallState,
|
|
677
|
+
*,
|
|
678
|
+
exhausted: bool,
|
|
679
|
+
) -> None:
|
|
680
|
+
await asyncio.to_thread(
|
|
681
|
+
self._persist_attempt_for_context,
|
|
682
|
+
context,
|
|
683
|
+
retry_state,
|
|
684
|
+
exhausted=exhausted,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def _persist_attempt_for_context(
|
|
688
|
+
self,
|
|
689
|
+
context: PersistentCallContext,
|
|
690
|
+
retry_state: RetryCallState,
|
|
691
|
+
*,
|
|
692
|
+
exhausted: bool,
|
|
693
|
+
) -> None:
|
|
694
|
+
context.record.attempts = context.starting_attempts + retry_state.attempt_number
|
|
695
|
+
context.record.exhausted = exhausted
|
|
696
|
+
self._get_store().save(context.key, context.record)
|
|
697
|
+
|
|
698
|
+
async def _clear_if_success_async(
|
|
699
|
+
self, context: PersistentCallContext, retry_state: RetryCallState
|
|
700
|
+
) -> None:
|
|
701
|
+
await asyncio.to_thread(
|
|
702
|
+
self._clear_if_success_for_context, context, retry_state
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def _clear_if_success_for_context(
|
|
706
|
+
self, context: PersistentCallContext, retry_state: RetryCallState
|
|
707
|
+
) -> None:
|
|
708
|
+
outcome = retry_state.outcome
|
|
709
|
+
if outcome is not None and not outcome.failed:
|
|
710
|
+
self._get_store().delete(context.key)
|
|
711
|
+
|
|
661
712
|
async def __call__(self, fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
662
713
|
self.begin()
|
|
663
|
-
context = self.
|
|
714
|
+
context = await self._load_context_async(fn, args, kwargs)
|
|
664
715
|
if context.record.exhausted:
|
|
665
716
|
raise PersistentRetryExhausted(context.key, context.record.attempts)
|
|
666
717
|
|
|
667
718
|
self._local.persistent_context = context
|
|
668
719
|
try:
|
|
669
720
|
retry_state = RetryCallState(
|
|
670
|
-
retry_object=cast(Any, self._retrying),
|
|
721
|
+
retry_object=cast("Any", self._retrying),
|
|
722
|
+
fn=fn,
|
|
723
|
+
args=args,
|
|
724
|
+
kwargs=kwargs,
|
|
671
725
|
)
|
|
672
726
|
while True:
|
|
673
|
-
do = await cast(AsyncRetrying, self._retrying).iter(
|
|
727
|
+
do = await cast("AsyncRetrying", self._retrying).iter(
|
|
674
728
|
retry_state=retry_state
|
|
675
729
|
)
|
|
676
730
|
if isinstance(do, DoAttempt):
|
|
@@ -692,9 +746,9 @@ class PersistentAsyncRetrying(_PersistentMixin):
|
|
|
692
746
|
retry_state.set_result(result)
|
|
693
747
|
elif isinstance(do, DoSleep):
|
|
694
748
|
retry_state.prepare_for_next_attempt()
|
|
695
|
-
await cast(Any, self._retrying).sleep(do)
|
|
749
|
+
await cast("Any", self._retrying).sleep(do)
|
|
696
750
|
else:
|
|
697
|
-
self.
|
|
751
|
+
await self._clear_if_success_async(context, retry_state)
|
|
698
752
|
return do
|
|
699
753
|
finally:
|
|
700
754
|
if hasattr(self._local, "persistent_context"):
|
|
@@ -702,10 +756,10 @@ class PersistentAsyncRetrying(_PersistentMixin):
|
|
|
702
756
|
|
|
703
757
|
|
|
704
758
|
def persistent_retry(**kwargs: Any) -> Callable[[WrappedFn], WrappedFn]:
|
|
705
|
-
return cast(Callable[[WrappedFn], WrappedFn], PersistentRetrying(**kwargs).wraps)
|
|
759
|
+
return cast("Callable[[WrappedFn], WrappedFn]", PersistentRetrying(**kwargs).wraps)
|
|
706
760
|
|
|
707
761
|
|
|
708
762
|
def persistent_async_retry(**kwargs: Any) -> Callable[[WrappedFn], WrappedFn]:
|
|
709
763
|
return cast(
|
|
710
|
-
Callable[[WrappedFn], WrappedFn], PersistentAsyncRetrying(**kwargs).wraps
|
|
764
|
+
"Callable[[WrappedFn], WrappedFn]", PersistentAsyncRetrying(**kwargs).wraps
|
|
711
765
|
)
|
localqueue/store.py
CHANGED
|
@@ -9,10 +9,11 @@ from collections import Counter
|
|
|
9
9
|
from contextlib import contextmanager, suppress
|
|
10
10
|
from dataclasses import dataclass, field, replace
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from collections.abc import Iterable
|
|
13
12
|
from typing import TYPE_CHECKING, Any, Iterator, Protocol
|
|
14
13
|
|
|
15
14
|
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from types import ModuleType
|
|
16
17
|
import lmdb
|
|
17
18
|
|
|
18
19
|
_ENVS: dict[tuple[str, int], Any] = {}
|
|
@@ -24,7 +25,7 @@ _QUEUE_RECORD_VERSION = 4
|
|
|
24
25
|
_SQLITE_SCHEMA_VERSION = 2
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def _import_lmdb() ->
|
|
28
|
+
def _import_lmdb() -> ModuleType:
|
|
28
29
|
try:
|
|
29
30
|
import lmdb
|
|
30
31
|
except ModuleNotFoundError as exc:
|
localqueue/worker.py
CHANGED
|
@@ -5,13 +5,15 @@ import time
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
from queue import Empty
|
|
8
|
-
from typing import Any, TypeVar, cast
|
|
8
|
+
from typing import Any, TypeVar, cast, TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
from .retry import PersistentAsyncRetrying, PersistentRetrying
|
|
11
11
|
|
|
12
12
|
from .failure import is_permanent_failure
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .queue import PersistentQueue
|
|
16
|
+
from .store import QueueMessage
|
|
15
17
|
|
|
16
18
|
WrappedFn = TypeVar("WrappedFn", bound=Callable[..., Any])
|
|
17
19
|
_UNSET = object()
|
|
@@ -32,9 +34,9 @@ def _resolve_dead_letter_on_failure(
|
|
|
32
34
|
"dead_letter_on_exhaustion=, not conflicting values"
|
|
33
35
|
)
|
|
34
36
|
if dead_letter_on_failure is not _UNSET:
|
|
35
|
-
return cast(bool, dead_letter_on_failure)
|
|
37
|
+
return cast("bool", dead_letter_on_failure)
|
|
36
38
|
if dead_letter_on_exhaustion is not _UNSET:
|
|
37
|
-
return cast(bool, dead_letter_on_exhaustion)
|
|
39
|
+
return cast("bool", dead_letter_on_exhaustion)
|
|
38
40
|
return True
|
|
39
41
|
|
|
40
42
|
|
|
@@ -130,9 +132,9 @@ class PersistentWorkerConfig:
|
|
|
130
132
|
dead_letter_on_exhaustion=dead_letter_on_exhaustion,
|
|
131
133
|
)
|
|
132
134
|
if release_delay is not _UNSET:
|
|
133
|
-
_validate_release_delay(cast(float, release_delay))
|
|
135
|
+
_validate_release_delay(cast("float", release_delay))
|
|
134
136
|
if min_interval is not _UNSET:
|
|
135
|
-
_validate_min_interval(cast(float, min_interval))
|
|
137
|
+
_validate_min_interval(cast("float", min_interval))
|
|
136
138
|
if (
|
|
137
139
|
circuit_breaker_failures is not _UNSET
|
|
138
140
|
or circuit_breaker_cooldown is not _UNSET
|
|
@@ -140,12 +142,12 @@ class PersistentWorkerConfig:
|
|
|
140
142
|
resolved_breaker_failures = (
|
|
141
143
|
self.circuit_breaker_failures
|
|
142
144
|
if circuit_breaker_failures is _UNSET
|
|
143
|
-
else cast(int, circuit_breaker_failures)
|
|
145
|
+
else cast("int", circuit_breaker_failures)
|
|
144
146
|
)
|
|
145
147
|
resolved_breaker_cooldown = (
|
|
146
148
|
self.circuit_breaker_cooldown
|
|
147
149
|
if circuit_breaker_cooldown is _UNSET
|
|
148
|
-
else cast(float, circuit_breaker_cooldown)
|
|
150
|
+
else cast("float", circuit_breaker_cooldown)
|
|
149
151
|
)
|
|
150
152
|
_validate_circuit_breaker(
|
|
151
153
|
resolved_breaker_failures, resolved_breaker_cooldown
|
|
@@ -160,22 +162,22 @@ class PersistentWorkerConfig:
|
|
|
160
162
|
release_delay=(
|
|
161
163
|
self.release_delay
|
|
162
164
|
if release_delay is _UNSET
|
|
163
|
-
else cast(float, release_delay)
|
|
165
|
+
else cast("float", release_delay)
|
|
164
166
|
),
|
|
165
167
|
min_interval=(
|
|
166
168
|
self.min_interval
|
|
167
169
|
if min_interval is _UNSET
|
|
168
|
-
else cast(float, min_interval)
|
|
170
|
+
else cast("float", min_interval)
|
|
169
171
|
),
|
|
170
172
|
circuit_breaker_failures=(
|
|
171
173
|
self.circuit_breaker_failures
|
|
172
174
|
if circuit_breaker_failures is _UNSET
|
|
173
|
-
else cast(int, circuit_breaker_failures)
|
|
175
|
+
else cast("int", circuit_breaker_failures)
|
|
174
176
|
),
|
|
175
177
|
circuit_breaker_cooldown=(
|
|
176
178
|
self.circuit_breaker_cooldown
|
|
177
179
|
if circuit_breaker_cooldown is _UNSET
|
|
178
|
-
else cast(float, circuit_breaker_cooldown)
|
|
180
|
+
else cast("float", circuit_breaker_cooldown)
|
|
179
181
|
),
|
|
180
182
|
**merged_retry_kwargs,
|
|
181
183
|
)
|
|
@@ -313,7 +315,10 @@ def persistent_worker(
|
|
|
313
315
|
if worker_id is not None:
|
|
314
316
|
queue.record_worker_heartbeat(worker_id)
|
|
315
317
|
_record_success(policy_state)
|
|
316
|
-
|
|
318
|
+
try:
|
|
319
|
+
queue.ack(message)
|
|
320
|
+
except Exception:
|
|
321
|
+
raise
|
|
317
322
|
return result
|
|
318
323
|
|
|
319
324
|
return wrapped
|
|
@@ -372,7 +377,10 @@ def persistent_async_worker(
|
|
|
372
377
|
if worker_id is not None:
|
|
373
378
|
queue.record_worker_heartbeat(worker_id)
|
|
374
379
|
_record_success(policy_state)
|
|
375
|
-
|
|
380
|
+
try:
|
|
381
|
+
queue.ack(message)
|
|
382
|
+
except Exception:
|
|
383
|
+
raise
|
|
376
384
|
return result
|
|
377
385
|
|
|
378
386
|
return wrapped
|
|
@@ -383,6 +391,6 @@ def persistent_async_worker(
|
|
|
383
391
|
async def _get_message_async(queue: PersistentQueue) -> QueueMessage:
|
|
384
392
|
while True:
|
|
385
393
|
try:
|
|
386
|
-
return queue.get_message
|
|
394
|
+
return await asyncio.to_thread(queue.get_message, False)
|
|
387
395
|
except Empty:
|
|
388
396
|
await asyncio.sleep(0.05)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localqueue
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Durable local queues for Python, with persistent retry state powered by Tenacity.
|
|
5
5
|
Project-URL: Homepage, https://brunoportis.github.io/localqueue/
|
|
6
6
|
Project-URL: Documentation, https://brunoportis.github.io/localqueue/
|
|
@@ -37,7 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
[](https://github.com/brunoportis/localqueue/actions/workflows/tests.yml)
|
|
39
39
|
[](https://github.com/brunoportis/localqueue/actions/workflows/github-code-scanning/codeql)
|
|
40
|
-
](https://pypi.org/project/localqueue/)
|
|
41
|
+

|
|
41
42
|
|
|
42
43
|
`localqueue` is a small durable queue for one machine. It stores work on the local filesystem by default and keeps retry state with Tenacity.
|
|
43
44
|
|
|
@@ -47,6 +48,12 @@ Use it for scripts, CLI tools, cron jobs, and small Python workers that share on
|
|
|
47
48
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
To run the CLI in a clean container:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docker run --rm ghcr.io/brunoportis/localqueue:latest --help
|
|
55
|
+
```
|
|
56
|
+
|
|
50
57
|
```python
|
|
51
58
|
from localqueue import PersistentQueue, persistent_worker
|
|
52
59
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
localqueue/__init__.py,sha256=76PTXzQOzD_VWK8sXiPxoyidN3TlxYtg3oJhIDqGBSU,1667
|
|
2
|
+
localqueue/cli.py,sha256=jKNjl45VwjZEzH0r0AI9OvV5SOBLjNhN4x8choZBMSQ,52762
|
|
3
|
+
localqueue/failure.py,sha256=8pBorAaEGoTlij-HDFOTJmNRROsPKY7evMr2rJVeI6w,409
|
|
4
|
+
localqueue/paths.py,sha256=0IRbSMhu35nB5N-HGYltJ0NFYj3I4iagSzUvR15LgWs,555
|
|
5
|
+
localqueue/queue.py,sha256=8M2LnQixnwtD07Oetgugprrn_7q9nVXqhCOAWu6K6JM,10660
|
|
6
|
+
localqueue/store.py,sha256=vxujfXad86gU6SaRSBEvjWXiFJ0KVHESRqUoRgV85ik,72759
|
|
7
|
+
localqueue/worker.py,sha256=AxpVHJciX2bh569kdwqA7oSb_xNQLwg_P-wkQKt4IK8,14010
|
|
8
|
+
localqueue/retry/__init__.py,sha256=kxDYtiksSd-JPlTzmd1esJLBUSrvm2fJD-TN9sMsh0Q,956
|
|
9
|
+
localqueue/retry/store.py,sha256=B9OsmcST_HhGyGYLKgt94_sofBMhAx976sPoX_aykmM,11128
|
|
10
|
+
localqueue/retry/tenacity.py,sha256=s0BmSWUuTZe8Sy5hw_3e46Us94NO8lvuOnrFX-_KJa8,27763
|
|
11
|
+
localqueue-0.3.2.dist-info/METADATA,sha256=i4h_5xrdS9VLLrJrBmyNdPZc_vI8h4kSSrZPOzHp_6Q,3313
|
|
12
|
+
localqueue-0.3.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
localqueue-0.3.2.dist-info/entry_points.txt,sha256=UtW4aZEN4Xi_yb3sFeiAd6k8jQ--1A5acmPs6FAs2JQ,51
|
|
14
|
+
localqueue-0.3.2.dist-info/licenses/LICENSE,sha256=WaGG8H1cJIusbf6T862P7oDXMchv9Gy32I--7WwIdsI,1069
|
|
15
|
+
localqueue-0.3.2.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
localqueue/__init__.py,sha256=76PTXzQOzD_VWK8sXiPxoyidN3TlxYtg3oJhIDqGBSU,1667
|
|
2
|
-
localqueue/cli.py,sha256=we4znzvSI55pobSqhXs1TMm5Yd8k-Y-SRQ71sR03AsA,47464
|
|
3
|
-
localqueue/failure.py,sha256=s9s9q3PqAtsuNn2Huxsd0cYqdery0kHiF0hg92BqEvc,336
|
|
4
|
-
localqueue/paths.py,sha256=0IRbSMhu35nB5N-HGYltJ0NFYj3I4iagSzUvR15LgWs,555
|
|
5
|
-
localqueue/queue.py,sha256=UiATgC8MXtcnQoKNVE9n90eZvWFw-B7vVQ5fBeQQG2o,10603
|
|
6
|
-
localqueue/store.py,sha256=zusZwArzF4aH8N6MuiM-2mwLpvHgaVy8JIQVWZqYfyM,72715
|
|
7
|
-
localqueue/worker.py,sha256=BRdb2d8BrRfKFuhI1ggBHeKOQFkuMvZYy5HuWNOYIXQ,13783
|
|
8
|
-
localqueue/retry/__init__.py,sha256=kxDYtiksSd-JPlTzmd1esJLBUSrvm2fJD-TN9sMsh0Q,956
|
|
9
|
-
localqueue/retry/store.py,sha256=USV1G8RGbThWLwuFpwnr9qntJyctZdEfNKMv21vjTTg,11088
|
|
10
|
-
localqueue/retry/tenacity.py,sha256=cBAoXUesg8aFgRqQtDyL0_j-buKlzdavXSNQ-yFwAdg,25952
|
|
11
|
-
localqueue-0.3.1.dist-info/METADATA,sha256=VRUmcdLZ1ZUnQfWKPSTtX-Eh_cXk9kjm0htD6aWI68s,3107
|
|
12
|
-
localqueue-0.3.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
-
localqueue-0.3.1.dist-info/entry_points.txt,sha256=UtW4aZEN4Xi_yb3sFeiAd6k8jQ--1A5acmPs6FAs2JQ,51
|
|
14
|
-
localqueue-0.3.1.dist-info/licenses/LICENSE,sha256=WaGG8H1cJIusbf6T862P7oDXMchv9Gy32I--7WwIdsI,1069
|
|
15
|
-
localqueue-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|