localqueue 0.3.1__tar.gz → 0.3.2__tar.gz
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-0.3.1 → localqueue-0.3.2}/.gitignore +1 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/CHANGELOG.md +5 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/PKG-INFO +9 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/README.md +8 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/release.md +1 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/enqueue_email.py +2 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/retry_demo.py +2 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/sqlite_concurrency_benchmark.py +3 -3
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/sqlite_process_harness.py +4 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/cli.py +318 -107
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/failure.py +6 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/queue.py +9 -7
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/retry/store.py +2 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/retry/tenacity.py +76 -22
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/store.py +3 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/worker.py +24 -16
- {localqueue-0.3.1 → localqueue-0.3.2}/pyproject.toml +42 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/tests/test_cli.py +567 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/tests/test_queue.py +966 -35
- {localqueue-0.3.1 → localqueue-0.3.2}/tests/test_retry.py +451 -2
- {localqueue-0.3.1 → localqueue-0.3.2}/uv.lock +1 -1
- {localqueue-0.3.1 → localqueue-0.3.2}/LICENSE +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/api.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/compare.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/index.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/operational-maturity.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/queues.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/retries.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/docs/stability.md +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/email_worker.py +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/examples/process_webhook.sh +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/__init__.py +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/paths.py +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/localqueue/retry/__init__.py +0 -0
- {localqueue-0.3.1 → localqueue-0.3.2}/tests/test_process_harness.py +0 -0
|
@@ -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
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/brunoportis/localqueue/actions/workflows/tests.yml)
|
|
4
4
|
[](https://github.com/brunoportis/localqueue/actions/workflows/github-code-scanning/codeql)
|
|
5
|
-
](https://pypi.org/project/localqueue/)
|
|
6
|
+

|
|
6
7
|
|
|
7
8
|
`localqueue` is a small durable queue for one machine. It stores work on the local filesystem by default and keeps retry state with Tenacity.
|
|
8
9
|
|
|
@@ -12,6 +13,12 @@ Use it for scripts, CLI tools, cron jobs, and small Python workers that share on
|
|
|
12
13
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
13
14
|
```
|
|
14
15
|
|
|
16
|
+
To run the CLI in a clean container:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
docker run --rm ghcr.io/brunoportis/localqueue:latest --help
|
|
20
|
+
```
|
|
21
|
+
|
|
15
22
|
```python
|
|
16
23
|
from localqueue import PersistentQueue, persistent_worker
|
|
17
24
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from localqueue import PersistentQueue
|
|
6
7
|
|
|
@@ -12,7 +13,7 @@ def main() -> None:
|
|
|
12
13
|
parser.add_argument("--fail", action="store_true")
|
|
13
14
|
args = parser.parse_args()
|
|
14
15
|
|
|
15
|
-
queue = (
|
|
16
|
+
queue: PersistentQueue[dict[str, Any]] = (
|
|
16
17
|
PersistentQueue("emails", store_path=args.store_path)
|
|
17
18
|
if args.store_path is not None
|
|
18
19
|
else PersistentQueue("emails")
|
|
@@ -3,6 +3,7 @@ import random
|
|
|
3
3
|
import time
|
|
4
4
|
from queue import Empty
|
|
5
5
|
from typing import Callable
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
from localqueue.retry import (
|
|
8
9
|
AttemptStoreLockedError,
|
|
@@ -156,7 +157,7 @@ def run_worker_demo(
|
|
|
156
157
|
db_path: str, *, worker_failure_rate: float = DEFAULT_WORKER_FAILURE_RATE
|
|
157
158
|
) -> None:
|
|
158
159
|
print("\n=== persistent queue worker ===")
|
|
159
|
-
queue = PersistentQueue(
|
|
160
|
+
queue: PersistentQueue[dict[str, Any]] = PersistentQueue(
|
|
160
161
|
"worker-jobs",
|
|
161
162
|
store_path=f"{db_path}_queue",
|
|
162
163
|
lease_timeout=2,
|
|
@@ -90,7 +90,7 @@ def _run_benchmark(
|
|
|
90
90
|
|
|
91
91
|
def producer(index: int) -> None:
|
|
92
92
|
try:
|
|
93
|
-
queue = PersistentQueue(
|
|
93
|
+
queue: PersistentQueue[dict[str, Any]] = PersistentQueue(
|
|
94
94
|
queue_name, store_path=store_path, lease_timeout=lease_timeout
|
|
95
95
|
)
|
|
96
96
|
start.wait()
|
|
@@ -105,7 +105,7 @@ def _run_benchmark(
|
|
|
105
105
|
|
|
106
106
|
def consumer(index: int) -> None:
|
|
107
107
|
try:
|
|
108
|
-
queue = PersistentQueue(
|
|
108
|
+
queue: PersistentQueue[dict[str, Any]] = PersistentQueue(
|
|
109
109
|
queue_name, store_path=store_path, lease_timeout=lease_timeout
|
|
110
110
|
)
|
|
111
111
|
start.wait()
|
|
@@ -154,7 +154,7 @@ def _run_benchmark(
|
|
|
154
154
|
record_error(TimeoutError("consumer thread did not finish"))
|
|
155
155
|
|
|
156
156
|
elapsed = time.perf_counter() - started_at
|
|
157
|
-
queue = PersistentQueue(
|
|
157
|
+
queue: PersistentQueue[dict[str, Any]] = PersistentQueue(
|
|
158
158
|
queue_name, store_path=store_path, lease_timeout=lease_timeout
|
|
159
159
|
)
|
|
160
160
|
stats = queue.stats().as_dict()
|
|
@@ -8,13 +8,15 @@ import os
|
|
|
8
8
|
import sqlite3
|
|
9
9
|
import time
|
|
10
10
|
from collections import Counter
|
|
11
|
-
from collections.abc import Sequence
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from queue import Empty
|
|
14
|
-
from typing import Any
|
|
13
|
+
from typing import Any, TYPE_CHECKING
|
|
15
14
|
|
|
16
15
|
from localqueue import PersistentQueue
|
|
17
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def main() -> int:
|
|
20
22
|
args = _parse_args()
|
|
@@ -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
|
*,
|