localqueue 0.1.1__tar.gz → 0.2.0__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.1.1 → localqueue-0.2.0}/CHANGELOG.md +10 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/PKG-INFO +9 -1
- {localqueue-0.1.1 → localqueue-0.2.0}/README.md +8 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/index.md +21 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/queues.md +26 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/release.md +1 -1
- localqueue-0.2.0/examples/process_webhook.sh +10 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/cli.py +104 -14
- {localqueue-0.1.1 → localqueue-0.2.0}/pyproject.toml +1 -1
- {localqueue-0.1.1 → localqueue-0.2.0}/tests/test_cli.py +125 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/uv.lock +1 -1
- {localqueue-0.1.1 → localqueue-0.2.0}/.gitignore +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/LICENSE +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/api.md +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/operational-maturity.md +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/docs/retries.md +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/examples/email_worker.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/examples/enqueue_email.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/__init__.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/queue.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/retry/__init__.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/retry/store.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/retry/tenacity.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/store.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/localqueue/worker.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/tests/test_queue.py +0 -0
- {localqueue-0.1.1 → localqueue-0.2.0}/tests/test_retry.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
- Add `queue stats --watch` for monitoring queue counts while workers run.
|
|
8
|
+
- Add `queue dead --watch` for repeatedly listing dead letters.
|
|
9
|
+
- Add `queue requeue-dead --all` for bulk recovery after a fix.
|
|
10
|
+
- Add structured `command not found` handling for `queue exec`.
|
|
11
|
+
- Add `examples/process_webhook.sh` as a shell/curl worker example.
|
|
12
|
+
|
|
3
13
|
## 0.1.1
|
|
4
14
|
|
|
5
15
|
- Add project URLs for PyPI metadata.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localqueue
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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/
|
|
@@ -125,9 +125,12 @@ localqueue queue add emails --value '{"to":"user@example.com"}'
|
|
|
125
125
|
echo '{"to":"user@example.com"}' | localqueue queue add emails
|
|
126
126
|
localqueue queue size emails
|
|
127
127
|
localqueue queue stats emails
|
|
128
|
+
localqueue queue stats emails --watch --interval 1
|
|
128
129
|
localqueue queue inspect emails <message-id>
|
|
129
130
|
localqueue queue dead emails
|
|
131
|
+
localqueue queue dead emails --watch --interval 1
|
|
130
132
|
localqueue queue requeue-dead emails <message-id>
|
|
133
|
+
localqueue queue requeue-dead emails --all
|
|
131
134
|
localqueue queue pop emails --worker-id worker-1
|
|
132
135
|
localqueue queue ack emails <message-id>
|
|
133
136
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
@@ -161,6 +164,7 @@ retry, release, and dead-letter rules as `queue process`.
|
|
|
161
164
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
162
165
|
localqueue queue exec webhooks -- curl -X POST https://example.com/hook -d @-
|
|
163
166
|
localqueue queue exec emails -- sh -c 'jq -r .to | xargs -I{} curl https://example.com/{}'
|
|
167
|
+
localqueue queue exec webhooks -- sh examples/process_webhook.sh
|
|
164
168
|
```
|
|
165
169
|
|
|
166
170
|
Command output is captured so the CLI can keep printing its own JSON status.
|
|
@@ -284,6 +288,10 @@ Use `queue stats` when you need ready, delayed, inflight, dead-letter, and total
|
|
|
284
288
|
counts instead of only ready messages. Use `queue inspect` to inspect one
|
|
285
289
|
message by id, `queue dead` to list dead-letter messages, and
|
|
286
290
|
`queue requeue-dead` to retry one after the failure cause is fixed.
|
|
291
|
+
Use `queue stats --watch` to print those counts repeatedly while local workers
|
|
292
|
+
are running.
|
|
293
|
+
Use `queue dead --watch` to keep an eye on newly failed jobs, and
|
|
294
|
+
`queue requeue-dead --all` once you have fixed the underlying issue.
|
|
287
295
|
|
|
288
296
|
## Operational boundaries
|
|
289
297
|
|
|
@@ -90,9 +90,12 @@ localqueue queue add emails --value '{"to":"user@example.com"}'
|
|
|
90
90
|
echo '{"to":"user@example.com"}' | localqueue queue add emails
|
|
91
91
|
localqueue queue size emails
|
|
92
92
|
localqueue queue stats emails
|
|
93
|
+
localqueue queue stats emails --watch --interval 1
|
|
93
94
|
localqueue queue inspect emails <message-id>
|
|
94
95
|
localqueue queue dead emails
|
|
96
|
+
localqueue queue dead emails --watch --interval 1
|
|
95
97
|
localqueue queue requeue-dead emails <message-id>
|
|
98
|
+
localqueue queue requeue-dead emails --all
|
|
96
99
|
localqueue queue pop emails --worker-id worker-1
|
|
97
100
|
localqueue queue ack emails <message-id>
|
|
98
101
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
@@ -126,6 +129,7 @@ retry, release, and dead-letter rules as `queue process`.
|
|
|
126
129
|
localqueue queue exec emails -- python scripts/send_email.py
|
|
127
130
|
localqueue queue exec webhooks -- curl -X POST https://example.com/hook -d @-
|
|
128
131
|
localqueue queue exec emails -- sh -c 'jq -r .to | xargs -I{} curl https://example.com/{}'
|
|
132
|
+
localqueue queue exec webhooks -- sh examples/process_webhook.sh
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
Command output is captured so the CLI can keep printing its own JSON status.
|
|
@@ -249,6 +253,10 @@ Use `queue stats` when you need ready, delayed, inflight, dead-letter, and total
|
|
|
249
253
|
counts instead of only ready messages. Use `queue inspect` to inspect one
|
|
250
254
|
message by id, `queue dead` to list dead-letter messages, and
|
|
251
255
|
`queue requeue-dead` to retry one after the failure cause is fixed.
|
|
256
|
+
Use `queue stats --watch` to print those counts repeatedly while local workers
|
|
257
|
+
are running.
|
|
258
|
+
Use `queue dead --watch` to keep an eye on newly failed jobs, and
|
|
259
|
+
`queue requeue-dead --all` once you have fixed the underlying issue.
|
|
252
260
|
|
|
253
261
|
## Operational boundaries
|
|
254
262
|
|
|
@@ -64,6 +64,27 @@ localqueue queue process emails myapp.workers:send_email \
|
|
|
64
64
|
--max-tries 5
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Watch queue counts while workers run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
localqueue queue stats emails --watch --interval 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For shell workers, `examples/process_webhook.sh` shows the same pattern with
|
|
74
|
+
`jq` and `curl`.
|
|
75
|
+
|
|
76
|
+
Inspect dead letters while workers are running:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
localqueue queue dead emails --watch --interval 2
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
After fixing a problem, requeue all dead letters with:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
localqueue queue requeue-dead emails --all
|
|
86
|
+
```
|
|
87
|
+
|
|
67
88
|
For a local smoke test, enqueue one job and process it with the bundled example
|
|
68
89
|
handler:
|
|
69
90
|
|
|
@@ -91,6 +91,7 @@ external command instead of an importable Python function.
|
|
|
91
91
|
localqueue queue exec jobs -- python scripts/process_job.py
|
|
92
92
|
localqueue queue exec webhooks -- curl -X POST https://example.com/hook -d @-
|
|
93
93
|
localqueue queue exec jobs -- sh -c 'jq -r .id | xargs -I{} ./process-job {}'
|
|
94
|
+
localqueue queue exec webhooks -- sh examples/process_webhook.sh
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
The command receives `message.value` on stdin as JSON. `localqueue` captures the
|
|
@@ -111,6 +112,8 @@ Command failures are stored in `last_error` with structured fields:
|
|
|
111
112
|
|
|
112
113
|
Commands are executed without an implicit shell. Use `sh -c` explicitly when you
|
|
113
114
|
want pipes, redirects, shell expansion, or multiple commands.
|
|
115
|
+
See `examples/process_webhook.sh` for a shell worker that reads JSON, extracts
|
|
116
|
+
fields with `jq`, and posts them with `curl`.
|
|
114
117
|
|
|
115
118
|
## Queue-style usage
|
|
116
119
|
|
|
@@ -315,3 +318,26 @@ by id without changing state. `dead_letters()` lists dead-letter messages for
|
|
|
315
318
|
inspection. `requeue_dead()` moves a dead-letter message back to the ready queue.
|
|
316
319
|
`purge()` removes all records for that queue, including ready, inflight, and
|
|
317
320
|
dead-letter records.
|
|
321
|
+
|
|
322
|
+
From the CLI, use `queue stats --watch` to monitor those counts while workers
|
|
323
|
+
are running:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
localqueue queue stats jobs --watch --interval 1
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Each sample is printed as JSON with `ready`, `delayed`, `inflight`, `dead`, and
|
|
330
|
+
`total` counts. Stop the watch with `Ctrl-C`.
|
|
331
|
+
|
|
332
|
+
Use `queue dead --watch` to keep an eye on recent failures:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
localqueue queue dead jobs --watch --interval 2
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
When the underlying problem is fixed, `queue requeue-dead --all` moves every
|
|
339
|
+
dead letter back to ready delivery:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
localqueue queue requeue-dead jobs --all
|
|
343
|
+
```
|
|
@@ -67,6 +67,17 @@ class _CommandExecutionError(RuntimeError):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class _CommandNotFoundError(_CommandExecutionError):
|
|
71
|
+
def __init__(self, command: list[str]) -> None:
|
|
72
|
+
super().__init__(
|
|
73
|
+
command,
|
|
74
|
+
exit_code=127,
|
|
75
|
+
stdout="",
|
|
76
|
+
stderr="command not found",
|
|
77
|
+
)
|
|
78
|
+
self.args = (f"command not found: {_format_command(command)}",)
|
|
79
|
+
|
|
80
|
+
|
|
70
81
|
def main(args: list[str] | None = None) -> None:
|
|
71
82
|
try:
|
|
72
83
|
from rich.console import Console
|
|
@@ -251,29 +262,65 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
|
|
|
251
262
|
def queue_stats(
|
|
252
263
|
queue: str,
|
|
253
264
|
store_path: str | None = typer.Option(None, "--store-path"),
|
|
265
|
+
watch: bool = typer.Option(False, "--watch"),
|
|
266
|
+
interval: float = typer.Option(1.0, "--interval", min=0.001),
|
|
254
267
|
) -> None:
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
persistent_queue = _queue(queue, _resolve_store_path(store_path, config))
|
|
269
|
+
with _shutdown_state() as shutdown:
|
|
270
|
+
_print_queue_stats(
|
|
271
|
+
persistent_queue,
|
|
272
|
+
console=console,
|
|
273
|
+
watch=watch,
|
|
274
|
+
interval=interval,
|
|
275
|
+
shutdown=shutdown,
|
|
276
|
+
)
|
|
257
277
|
|
|
258
278
|
@queue_app.command("dead")
|
|
259
279
|
def queue_dead(
|
|
260
280
|
queue: str,
|
|
261
281
|
store_path: str | None = typer.Option(None, "--store-path"),
|
|
262
282
|
limit: int | None = typer.Option(None, "--limit", min=0),
|
|
283
|
+
watch: bool = typer.Option(False, "--watch"),
|
|
284
|
+
interval: float = typer.Option(1.0, "--interval", min=0.001),
|
|
263
285
|
) -> None:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
persistent_queue = _queue(queue, _resolve_store_path(store_path, config))
|
|
287
|
+
with _shutdown_state() as shutdown:
|
|
288
|
+
_print_dead_letters(
|
|
289
|
+
persistent_queue,
|
|
290
|
+
console=console,
|
|
291
|
+
limit=limit,
|
|
292
|
+
watch=watch,
|
|
293
|
+
interval=interval,
|
|
294
|
+
shutdown=shutdown,
|
|
295
|
+
)
|
|
268
296
|
|
|
269
297
|
@queue_app.command("requeue-dead")
|
|
270
298
|
def queue_requeue_dead(
|
|
271
299
|
queue: str,
|
|
272
|
-
message_id: str,
|
|
300
|
+
message_id: str | None = typer.Argument(None),
|
|
273
301
|
store_path: str | None = typer.Option(None, "--store-path"),
|
|
274
302
|
delay: float = typer.Option(0.0, "--delay", min=0.0),
|
|
303
|
+
all_: bool = typer.Option(False, "--all"),
|
|
275
304
|
) -> None:
|
|
276
305
|
persistent_queue = _queue(queue, _resolve_store_path(store_path, config))
|
|
306
|
+
if all_:
|
|
307
|
+
if message_id is not None:
|
|
308
|
+
err_console.print(
|
|
309
|
+
"[red]pass either a message id or --all, not both[/red]"
|
|
310
|
+
)
|
|
311
|
+
raise typer.Exit(1)
|
|
312
|
+
messages = persistent_queue.dead_letters()
|
|
313
|
+
requeued = 0
|
|
314
|
+
for message in messages:
|
|
315
|
+
if persistent_queue.requeue_dead(message, delay=delay):
|
|
316
|
+
requeued += 1
|
|
317
|
+
_print_json(console, {"requeued": requeued})
|
|
318
|
+
return
|
|
319
|
+
if message_id is None:
|
|
320
|
+
err_console.print(
|
|
321
|
+
"[red]pass a message id or use --all to requeue dead letters[/red]"
|
|
322
|
+
)
|
|
323
|
+
raise typer.Exit(1)
|
|
277
324
|
message = QueueMessage(id=message_id, value=None, queue=queue)
|
|
278
325
|
if not persistent_queue.requeue_dead(message, delay=delay):
|
|
279
326
|
err_console.print(f"[red]dead-letter message not found:[/red] {message_id}")
|
|
@@ -550,6 +597,46 @@ def _process_queue_messages(
|
|
|
550
597
|
return 0
|
|
551
598
|
|
|
552
599
|
|
|
600
|
+
def _print_dead_letters(
|
|
601
|
+
queue: PersistentQueue,
|
|
602
|
+
*,
|
|
603
|
+
console: Any,
|
|
604
|
+
limit: int | None,
|
|
605
|
+
watch: bool,
|
|
606
|
+
interval: float,
|
|
607
|
+
shutdown: _ShutdownState,
|
|
608
|
+
) -> None:
|
|
609
|
+
while True:
|
|
610
|
+
messages = queue.dead_letters(limit=limit)
|
|
611
|
+
_print_json(console, [_message_payload(message) for message in messages])
|
|
612
|
+
if not watch:
|
|
613
|
+
return
|
|
614
|
+
if shutdown.requested:
|
|
615
|
+
return
|
|
616
|
+
time.sleep(interval)
|
|
617
|
+
if shutdown.requested:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _print_queue_stats(
|
|
622
|
+
queue: PersistentQueue,
|
|
623
|
+
*,
|
|
624
|
+
console: Any,
|
|
625
|
+
watch: bool,
|
|
626
|
+
interval: float,
|
|
627
|
+
shutdown: _ShutdownState,
|
|
628
|
+
) -> None:
|
|
629
|
+
while True:
|
|
630
|
+
_print_json(console, queue.stats().as_dict())
|
|
631
|
+
if not watch:
|
|
632
|
+
return
|
|
633
|
+
if shutdown.requested:
|
|
634
|
+
return
|
|
635
|
+
time.sleep(interval)
|
|
636
|
+
if shutdown.requested:
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
|
|
553
640
|
def _poll_timeout(forever: bool, block: bool, timeout: float | None) -> float | None:
|
|
554
641
|
if forever and block and timeout is None:
|
|
555
642
|
return 0.5
|
|
@@ -692,13 +779,16 @@ def _command_handler(command: list[str]) -> Callable[[Any], None]:
|
|
|
692
779
|
raise ValueError("command cannot be empty")
|
|
693
780
|
|
|
694
781
|
def run_command(value: Any) -> None:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
782
|
+
try:
|
|
783
|
+
completed = subprocess.run(
|
|
784
|
+
command,
|
|
785
|
+
input=_json_dumps(value) + "\n",
|
|
786
|
+
capture_output=True,
|
|
787
|
+
text=True,
|
|
788
|
+
check=False,
|
|
789
|
+
)
|
|
790
|
+
except FileNotFoundError as exc:
|
|
791
|
+
raise _CommandNotFoundError(command) from exc
|
|
702
792
|
if completed.returncode != 0:
|
|
703
793
|
raise _CommandExecutionError(
|
|
704
794
|
command,
|
|
@@ -28,6 +28,8 @@ from localqueue.cli import (
|
|
|
28
28
|
_load_callable,
|
|
29
29
|
_load_config,
|
|
30
30
|
_parse_json,
|
|
31
|
+
_print_dead_letters,
|
|
32
|
+
_print_queue_stats,
|
|
31
33
|
_process_message,
|
|
32
34
|
_process_queue_messages,
|
|
33
35
|
_read_value,
|
|
@@ -380,6 +382,30 @@ class CliTests(unittest.TestCase):
|
|
|
380
382
|
self.assertEqual(missing_requeue.exit_code, 1)
|
|
381
383
|
self.assertIn("dead-letter message not found", missing_requeue.output)
|
|
382
384
|
|
|
385
|
+
def test_print_dead_letters_watch_stops_on_shutdown(self) -> None:
|
|
386
|
+
queue = PersistentQueue("emails", store=MemoryQueueStore())
|
|
387
|
+
_ = queue.put({"to": "user@example.com"})
|
|
388
|
+
message = queue.get_message()
|
|
389
|
+
self.assertTrue(queue.dead_letter(message, error=RuntimeError("bad")))
|
|
390
|
+
console = _JsonConsole()
|
|
391
|
+
shutdown = _ShutdownState()
|
|
392
|
+
|
|
393
|
+
def request_shutdown(_interval: float) -> None:
|
|
394
|
+
shutdown.requested = True
|
|
395
|
+
|
|
396
|
+
with mock.patch("time.sleep", side_effect=request_shutdown):
|
|
397
|
+
_print_dead_letters(
|
|
398
|
+
queue,
|
|
399
|
+
console=console,
|
|
400
|
+
limit=None,
|
|
401
|
+
watch=True,
|
|
402
|
+
interval=0.001,
|
|
403
|
+
shutdown=shutdown,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
self.assertEqual(len(console.values), 1)
|
|
407
|
+
self.assertEqual(console.values[0][0]["id"], message.id)
|
|
408
|
+
|
|
383
409
|
def test_queue_process_command_acks_successful_handler(self) -> None:
|
|
384
410
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
385
411
|
tmp_path = Path(tmpdir)
|
|
@@ -496,6 +522,40 @@ class CliTests(unittest.TestCase):
|
|
|
496
522
|
self.assertEqual(result.exit_code, 1)
|
|
497
523
|
self.assertIn("module:function", result.output)
|
|
498
524
|
|
|
525
|
+
def test_queue_exec_reports_missing_command(self) -> None:
|
|
526
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
527
|
+
store_path = str(Path(tmpdir) / "queues")
|
|
528
|
+
retry_store_path = str(Path(tmpdir) / "retries.sqlite3")
|
|
529
|
+
queue = PersistentQueue("emails", store_path=store_path)
|
|
530
|
+
message = queue.put({"to": "user@example.com"})
|
|
531
|
+
|
|
532
|
+
result = self._invoke(
|
|
533
|
+
[
|
|
534
|
+
"queue",
|
|
535
|
+
"exec",
|
|
536
|
+
"emails",
|
|
537
|
+
"--store-path",
|
|
538
|
+
store_path,
|
|
539
|
+
"--retry-store-path",
|
|
540
|
+
retry_store_path,
|
|
541
|
+
"--max-tries",
|
|
542
|
+
"1",
|
|
543
|
+
"--",
|
|
544
|
+
"definitely-not-a-real-command-123",
|
|
545
|
+
]
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
self.assertEqual(result.exit_code, 1)
|
|
549
|
+
payload = json.loads(result.stdout)
|
|
550
|
+
self.assertEqual(payload["id"], message.id)
|
|
551
|
+
self.assertEqual(payload["state"], "failed")
|
|
552
|
+
self.assertEqual(payload["last_error"]["type"], "_CommandNotFoundError")
|
|
553
|
+
self.assertEqual(payload["last_error"]["exit_code"], 127)
|
|
554
|
+
self.assertEqual(
|
|
555
|
+
payload["last_error"]["command"], ["definitely-not-a-real-command-123"]
|
|
556
|
+
)
|
|
557
|
+
self.assertIn("command not found", payload["last_error"]["message"])
|
|
558
|
+
|
|
499
559
|
def test_command_handler_passes_message_value_as_json_stdin(self) -> None:
|
|
500
560
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
501
561
|
tmp_path = Path(tmpdir)
|
|
@@ -610,6 +670,48 @@ class CliTests(unittest.TestCase):
|
|
|
610
670
|
self.assertEqual(dead_letters[0].last_error["exit_code"], 7)
|
|
611
671
|
self.assertEqual(dead_letters[0].last_error["stderr"], "cannot deliver")
|
|
612
672
|
|
|
673
|
+
def test_queue_requeue_dead_all_moves_every_dead_message(self) -> None:
|
|
674
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
675
|
+
store_path = str(Path(tmpdir) / "queues")
|
|
676
|
+
persistent_queue = PersistentQueue("emails", store_path=store_path)
|
|
677
|
+
first = persistent_queue.put({"to": "one@example.com"})
|
|
678
|
+
second = persistent_queue.put({"to": "two@example.com"})
|
|
679
|
+
self.assertTrue(
|
|
680
|
+
persistent_queue.dead_letter(
|
|
681
|
+
persistent_queue.get_message(), error=RuntimeError("bad 1")
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
self.assertTrue(
|
|
685
|
+
persistent_queue.dead_letter(
|
|
686
|
+
persistent_queue.get_message(), error=RuntimeError("bad 2")
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
result = self._invoke(
|
|
691
|
+
[
|
|
692
|
+
"queue",
|
|
693
|
+
"requeue-dead",
|
|
694
|
+
"emails",
|
|
695
|
+
"--all",
|
|
696
|
+
"--store-path",
|
|
697
|
+
store_path,
|
|
698
|
+
]
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
self.assertEqual(result.exit_code, 0, result.output)
|
|
702
|
+
self.assertEqual(json.loads(result.stdout), {"requeued": 2})
|
|
703
|
+
fresh_queue = PersistentQueue("emails", store_path=store_path)
|
|
704
|
+
self.assertEqual(fresh_queue.dead_letters(), [])
|
|
705
|
+
self.assertEqual(fresh_queue.qsize(), 2)
|
|
706
|
+
first_message = fresh_queue.inspect(first.id)
|
|
707
|
+
second_message = fresh_queue.inspect(second.id)
|
|
708
|
+
self.assertIsNotNone(first_message)
|
|
709
|
+
self.assertIsNotNone(second_message)
|
|
710
|
+
assert first_message is not None
|
|
711
|
+
assert second_message is not None
|
|
712
|
+
self.assertEqual(first_message.state, "ready")
|
|
713
|
+
self.assertEqual(second_message.state, "ready")
|
|
714
|
+
|
|
613
715
|
def test_process_message_acks_successful_handler(self) -> None:
|
|
614
716
|
queue = PersistentQueue("emails", store=MemoryQueueStore())
|
|
615
717
|
_ = queue.put({"to": "user@example.com"})
|
|
@@ -724,6 +826,29 @@ class CliTests(unittest.TestCase):
|
|
|
724
826
|
self.assertEqual(console.values, [])
|
|
725
827
|
self.assertEqual(err_console.messages, ["[yellow]queue is empty[/yellow]"])
|
|
726
828
|
|
|
829
|
+
def test_print_queue_stats_watch_stops_on_shutdown(self) -> None:
|
|
830
|
+
queue = PersistentQueue("emails", store=MemoryQueueStore())
|
|
831
|
+
_ = queue.put({"to": "user@example.com"})
|
|
832
|
+
console = _JsonConsole()
|
|
833
|
+
shutdown = _ShutdownState()
|
|
834
|
+
|
|
835
|
+
def request_shutdown(_interval: float) -> None:
|
|
836
|
+
shutdown.requested = True
|
|
837
|
+
|
|
838
|
+
with mock.patch("time.sleep", side_effect=request_shutdown):
|
|
839
|
+
_print_queue_stats(
|
|
840
|
+
queue,
|
|
841
|
+
console=console,
|
|
842
|
+
watch=True,
|
|
843
|
+
interval=0.001,
|
|
844
|
+
shutdown=shutdown,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
self.assertEqual(
|
|
848
|
+
console.values,
|
|
849
|
+
[{"ready": 1, "delayed": 0, "inflight": 0, "dead": 0, "total": 1}],
|
|
850
|
+
)
|
|
851
|
+
|
|
727
852
|
def test_process_queue_messages_forever_stops_after_current_message(self) -> None:
|
|
728
853
|
queue = PersistentQueue("emails", store=MemoryQueueStore())
|
|
729
854
|
_ = queue.put({"to": "first@example.com"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|