localqueue 0.3.0__tar.gz → 0.3.1__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.
Files changed (35) hide show
  1. {localqueue-0.3.0 → localqueue-0.3.1}/.gitignore +2 -0
  2. {localqueue-0.3.0 → localqueue-0.3.1}/CHANGELOG.md +12 -1
  3. {localqueue-0.3.0 → localqueue-0.3.1}/PKG-INFO +1 -1
  4. {localqueue-0.3.0 → localqueue-0.3.1}/docs/api.md +12 -6
  5. {localqueue-0.3.0 → localqueue-0.3.1}/docs/operational-maturity.md +1 -1
  6. {localqueue-0.3.0 → localqueue-0.3.1}/docs/queues.md +13 -7
  7. {localqueue-0.3.0 → localqueue-0.3.1}/docs/retries.md +16 -10
  8. {localqueue-0.3.0 → localqueue-0.3.1}/examples/enqueue_email.py +6 -2
  9. localqueue-0.3.1/examples/retry_demo.py +278 -0
  10. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/__init__.py +2 -0
  11. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/cli.py +92 -80
  12. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/failure.py +0 -12
  13. localqueue-0.3.1/localqueue/paths.py +23 -0
  14. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/queue.py +10 -9
  15. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/retry/__init__.py +2 -0
  16. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/retry/store.py +81 -30
  17. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/retry/tenacity.py +35 -9
  18. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/store.py +211 -95
  19. {localqueue-0.3.0 → localqueue-0.3.1}/pyproject.toml +1 -1
  20. {localqueue-0.3.0 → localqueue-0.3.1}/tests/test_cli.py +79 -0
  21. {localqueue-0.3.0 → localqueue-0.3.1}/tests/test_queue.py +125 -14
  22. {localqueue-0.3.0 → localqueue-0.3.1}/tests/test_retry.py +172 -2
  23. {localqueue-0.3.0 → localqueue-0.3.1}/LICENSE +0 -0
  24. {localqueue-0.3.0 → localqueue-0.3.1}/README.md +0 -0
  25. {localqueue-0.3.0 → localqueue-0.3.1}/docs/compare.md +0 -0
  26. {localqueue-0.3.0 → localqueue-0.3.1}/docs/index.md +0 -0
  27. {localqueue-0.3.0 → localqueue-0.3.1}/docs/release.md +0 -0
  28. {localqueue-0.3.0 → localqueue-0.3.1}/docs/stability.md +0 -0
  29. {localqueue-0.3.0 → localqueue-0.3.1}/examples/email_worker.py +0 -0
  30. {localqueue-0.3.0 → localqueue-0.3.1}/examples/process_webhook.sh +0 -0
  31. {localqueue-0.3.0 → localqueue-0.3.1}/examples/sqlite_concurrency_benchmark.py +0 -0
  32. {localqueue-0.3.0 → localqueue-0.3.1}/examples/sqlite_process_harness.py +0 -0
  33. {localqueue-0.3.0 → localqueue-0.3.1}/localqueue/worker.py +0 -0
  34. {localqueue-0.3.0 → localqueue-0.3.1}/tests/test_process_harness.py +0 -0
  35. {localqueue-0.3.0 → localqueue-0.3.1}/uv.lock +0 -0
@@ -17,4 +17,6 @@ site/
17
17
  /localqueue_queue.sqlite3
18
18
  /localqueue_retries
19
19
  /localqueue_retries.sqlite3
20
+ /examples/*.sqlite3
21
+ /examples/*.sqlite3-*
20
22
  feedback.md
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.1
4
+
5
+ - Use XDG data directories for default SQLite queue and retry store files.
6
+ - Move the exploratory retry demo from `main.py` to `examples/retry_demo.py`.
7
+ - Make `PersistentQueue` unfinished-message tracking O(1) by message id.
8
+ - Add an end-to-end SQLite worker test covering queue ack and retry cleanup.
9
+ - Make `store_path=` for persistent retries open a SQLite attempt store.
10
+ - Tighten permanent-failure classification to avoid dead-lettering common
11
+ transient application errors such as `ValueError` and `KeyError`.
12
+ - Optimize SQLite queue stats and dead-letter retention queries with
13
+ materialized columns and schema migration support.
14
+ - Add indexed SQLite retry-store retention for exhausted records.
4
15
 
5
16
  ## 0.3.0
6
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localqueue
3
- Version: 0.3.0
3
+ Version: 0.3.1
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/
@@ -21,7 +21,7 @@ Constructor options:
21
21
  | --- | --- |
22
22
  | `name` | queue name inside the store |
23
23
  | `store` | queue store instance |
24
- | `store_path` | path for the default SQLite queue store |
24
+ | `store_path` | explicit path for the SQLite queue store |
25
25
  | `lease_timeout` | seconds before an inflight message is redelivered |
26
26
  | `maxsize` | maximum number of ready messages; `0` means unbounded |
27
27
  | `retry_defaults` | Tenacity retry keyword defaults inherited by workers |
@@ -183,8 +183,8 @@ SQLite-backed queue store. This is the default backend. Records are serialized
183
183
  as versioned JSON; values must be JSON-serializable.
184
184
 
185
185
  The SQLite store tracks its on-disk schema version with `PRAGMA user_version`.
186
- Current releases accept older compatible versions and reject future versions
187
- until a migration is defined.
186
+ Current releases migrate older compatible versions and reject future versions
187
+ they do not know how to migrate yet.
188
188
 
189
189
  #### `LMDBQueueStore`
190
190
 
@@ -237,7 +237,7 @@ Both decorators require `key=` or `key_fn=` and accept the persistent options be
237
237
  | Option | Meaning |
238
238
  | --- | --- |
239
239
  | `store` | attempt store instance |
240
- | `store_path` | path for an LMDB attempt-store directory |
240
+ | `store_path` | explicit path for a SQLite attempt-store file |
241
241
  | `key` | fixed retry key |
242
242
  | `key_fn` | function that derives a retry key from the call |
243
243
  | `clear_on_success` | delete the attempt record after success |
@@ -341,6 +341,11 @@ Dataclass stored per retry key.
341
341
  | `first_attempt_at` | `float` | Unix timestamp of the first persisted attempt |
342
342
  | `exhausted` | `bool` | whether the retry budget is exhausted |
343
343
 
344
+ #### `close_default_store(all_threads=False)`
345
+
346
+ Close the default retry store for the current thread. Pass `all_threads=True`
347
+ at process shutdown to close known factory-created default stores.
348
+
344
349
  #### `AttemptStore`
345
350
 
346
351
  Protocol for custom attempt stores.
@@ -365,8 +370,9 @@ store = LMDBAttemptStore("/var/lib/my-worker/retries")
365
370
 
366
371
  #### `SQLiteAttemptStore`
367
372
 
368
- SQLite-backed attempt store. This is the default backend and does not require LMDB's
369
- native dependency.
373
+ SQLite-backed attempt store. This is the default backend and does not require
374
+ LMDB's native dependency. Retention cleanup uses indexed `exhausted` and
375
+ `first_attempt_at` columns instead of scanning every serialized retry record.
370
376
 
371
377
  ```python
372
378
  from localqueue.retry import SQLiteAttemptStore
@@ -107,7 +107,7 @@ inspection tools for that local model, not a distributed liveness protocol.
107
107
  versions.
108
108
 
109
109
  The SQLite store keeps a schema version in `PRAGMA user_version`. Current
110
- releases accept older compatible versions and reject future versions they do
110
+ releases migrate older compatible versions and reject future versions they do
111
111
  not know how to migrate yet. That keeps upgrades explicit and leaves a clear
112
112
  path for future schema changes.
113
113
 
@@ -23,7 +23,6 @@ from localqueue import PersistentQueue
23
23
 
24
24
  queue = PersistentQueue(
25
25
  "emails",
26
- store_path="./localqueue_queue.sqlite3",
27
26
  lease_timeout=30.0,
28
27
  retry_defaults={
29
28
  "max_tries": 3,
@@ -75,10 +74,12 @@ For shared queue-wide retry defaults, attach `retry_defaults` to the
75
74
  `PersistentQueue` and let workers inherit those Tenacity arguments unless the
76
75
  worker passes explicit overrides.
77
76
 
78
- `localqueue` treats validation-style failures such as `ValueError`, `TypeError`,
79
- `KeyError`, `IndexError`, and missing command execution as permanent failures.
80
- Those are dead-lettered even when the worker is configured to release on final
81
- failure.
77
+ `localqueue` keeps built-in permanent-failure classification conservative.
78
+ Import/name-resolution failures and missing command execution are dead-lettered
79
+ even when the worker is configured to release on final failure. Runtime
80
+ validation errors such as `ValueError`, `TypeError`, and `KeyError` follow the
81
+ configured worker policy because they can represent either bad input or a
82
+ recoverable transient condition.
82
83
 
83
84
  Change that behavior with `dead_letter_on_failure=False`. The older
84
85
  `dead_letter_on_exhaustion` name is still accepted as a compatibility alias.
@@ -332,7 +333,12 @@ Negative delays raise `ValueError`.
332
333
 
333
334
  ## Stores
334
335
 
335
- The default queue store is SQLite at `./localqueue_queue.sqlite3`.
336
+ The default queue store is SQLite at
337
+ `$XDG_DATA_HOME/localqueue/queue.sqlite3`. When `XDG_DATA_HOME` is not set,
338
+ the fallback is `~/.local/share/localqueue/queue.sqlite3`.
339
+
340
+ Pass `store_path=` when the queue file must live somewhere explicit, such as a
341
+ service data directory.
336
342
 
337
343
  ```python
338
344
  from localqueue import PersistentQueue, SQLiteQueueStore
@@ -404,7 +410,7 @@ SQLite files can keep running for a long time, but they still need normal file
404
410
  maintenance.
405
411
 
406
412
  The SQLite store keeps a schema version in `PRAGMA user_version`. Current
407
- releases accept older compatible versions and reject future versions they do
413
+ releases migrate older compatible versions and reject future versions they do
408
414
  not know how to migrate yet.
409
415
 
410
416
  - Keep the main `*.sqlite3` file and its retry store together when you back up
@@ -222,29 +222,29 @@ except PersistentRetryExhausted as exc:
222
222
 
223
223
  ## Stores
224
224
 
225
- The default attempt store is SQLite at `./localqueue_retries.sqlite3`.
225
+ The default attempt store is SQLite at
226
+ `$XDG_DATA_HOME/localqueue/retries.sqlite3`. When `XDG_DATA_HOME` is not set,
227
+ the fallback is `~/.local/share/localqueue/retries.sqlite3`.
226
228
 
227
- `store_path=` creates an LMDB attempt-store directory. Install
228
- `localqueue[lmdb]` and use it when you want the retry API to manage an LMDB
229
- backend directly.
229
+ `store_path=` creates a SQLite attempt-store file, matching the default backend.
230
230
 
231
231
  ```python
232
232
  from localqueue.retry import PersistentRetrying, key_from_argument
233
233
 
234
234
  retryer = PersistentRetrying(
235
- store_path="/var/lib/my-worker/retries",
235
+ store_path="/var/lib/my-worker/retries.sqlite3",
236
236
  max_tries=5,
237
237
  key_fn=key_from_argument("job_id"),
238
238
  )
239
239
  ```
240
240
 
241
- Provide a store instance with `store=` when you need SQLite, full control, or
242
- in-memory tests.
241
+ Provide a store instance with `store=` when you need LMDB, full control, or
242
+ in-memory tests. Install `localqueue[lmdb]` when you want the LMDB backend.
243
243
 
244
244
  ```python
245
- from localqueue.retry import SQLiteAttemptStore, key_from_argument, persistent_retry
245
+ from localqueue.retry import LMDBAttemptStore, key_from_argument, persistent_retry
246
246
 
247
- store = SQLiteAttemptStore("/var/lib/my-worker/retries.sqlite3")
247
+ store = LMDBAttemptStore("/var/lib/my-worker/retries")
248
248
 
249
249
 
250
250
  @persistent_retry(
@@ -257,7 +257,8 @@ def flaky(job_id: str) -> str:
257
257
  ```
258
258
 
259
259
  The CLI `retry_store_path` setting and `queue process --retry-store-path` use a
260
- SQLite file path.
260
+ SQLite file path. `queue process` and `queue exec` keep one retry-store
261
+ connection open for the worker loop and close it when the loop exits.
261
262
 
262
263
  ```python
263
264
  from localqueue.retry import MemoryAttemptStore, key_from_argument, persistent_retry
@@ -277,6 +278,11 @@ def flaky(job_id: str) -> str:
277
278
 
278
279
  `store=` and `store_path=` are mutually exclusive.
279
280
 
281
+ Default retry stores are thread-local. Call `close_default_store()` when a
282
+ long-lived thread is shutting down and you want to close its default SQLite
283
+ connection explicitly. At process shutdown, `close_default_store(all_threads=True)`
284
+ closes known factory-created default stores.
285
+
280
286
  ## State and callbacks
281
287
 
282
288
  Tenacity callbacks and strategies receive a state object that behaves like Tenacity's `RetryCallState`, with persistent attempt numbering.
@@ -8,11 +8,15 @@ from localqueue import PersistentQueue
8
8
  def main() -> None:
9
9
  parser = argparse.ArgumentParser()
10
10
  parser.add_argument("address")
11
- parser.add_argument("--store-path", default="./localqueue_queue.sqlite3")
11
+ parser.add_argument("--store-path")
12
12
  parser.add_argument("--fail", action="store_true")
13
13
  args = parser.parse_args()
14
14
 
15
- queue = PersistentQueue("emails", store_path=args.store_path)
15
+ queue = (
16
+ PersistentQueue("emails", store_path=args.store_path)
17
+ if args.store_path is not None
18
+ else PersistentQueue("emails")
19
+ )
16
20
  message = queue.put({"to": args.address, "fail": args.fail})
17
21
  print(message.id)
18
22
 
@@ -0,0 +1,278 @@
1
+ import argparse
2
+ import random
3
+ import time
4
+ from queue import Empty
5
+ from typing import Callable
6
+
7
+ from localqueue.retry import (
8
+ AttemptStoreLockedError,
9
+ PersistentRetrying,
10
+ PersistentRetryExhausted,
11
+ key_from_argument,
12
+ persistent_retry,
13
+ )
14
+ from localqueue import PersistentQueue, QueueStoreLockedError
15
+ from tenacity import wait_exponential, wait_none
16
+
17
+
18
+ SendEmailFn = Callable[[str, str], None]
19
+ ProcessVideoFn = Callable[[str, str], None]
20
+ WorkerJobFn = Callable[[str, dict[str, str]], None]
21
+ DEFAULT_EMAIL_FAILURE_RATE = 0.8
22
+ DEFAULT_VIDEO_FAILURE_RATE = 0.7
23
+ DEFAULT_WORKER_FAILURE_RATE = 0.65
24
+
25
+
26
+ def build_fast_examples(
27
+ db_path: str,
28
+ *,
29
+ email_failure_rate: float = DEFAULT_EMAIL_FAILURE_RATE,
30
+ video_failure_rate: float = DEFAULT_VIDEO_FAILURE_RATE,
31
+ ) -> tuple[SendEmailFn, ProcessVideoFn]:
32
+ @persistent_retry(
33
+ max_tries=3,
34
+ store_path=db_path,
35
+ wait=wait_none(),
36
+ key_fn=key_from_argument("task_id"),
37
+ )
38
+ def send_email_fast(task_id: str, email_address: str) -> None:
39
+ print(f"[send_email_fast] task={task_id} email={email_address}")
40
+ if random.random() < email_failure_rate:
41
+ raise ConnectionError("mail server down")
42
+ print(f"[send_email_fast] task={task_id} sent")
43
+
44
+ @persistent_retry(
45
+ max_tries=5,
46
+ store_path=db_path,
47
+ wait=wait_none(),
48
+ key_fn=key_from_argument("task_id"),
49
+ )
50
+ def process_video_fast(task_id: str, file_path: str) -> None:
51
+ print(f"[process_video_fast] task={task_id} file={file_path}")
52
+ if random.random() < video_failure_rate:
53
+ raise ValueError("transcoder busy")
54
+ print(f"[process_video_fast] task={task_id} processed")
55
+
56
+ return send_email_fast, process_video_fast
57
+
58
+
59
+ def build_slow_examples(
60
+ db_path: str,
61
+ *,
62
+ email_failure_rate: float = DEFAULT_EMAIL_FAILURE_RATE,
63
+ video_failure_rate: float = DEFAULT_VIDEO_FAILURE_RATE,
64
+ ) -> tuple[SendEmailFn, ProcessVideoFn]:
65
+ @persistent_retry(
66
+ max_tries=3,
67
+ store_path=db_path,
68
+ wait=wait_exponential(multiplier=1, min=2, max=10),
69
+ key_fn=key_from_argument("task_id"),
70
+ )
71
+ def send_email_slow(task_id: str, email_address: str) -> None:
72
+ print(f"[send_email_slow] task={task_id} email={email_address}")
73
+ if random.random() < email_failure_rate:
74
+ raise ConnectionError("mail server down")
75
+ print(f"[send_email_slow] task={task_id} sent")
76
+
77
+ @persistent_retry(
78
+ max_tries=5,
79
+ store_path=db_path,
80
+ wait=wait_exponential(multiplier=1, min=2, max=10),
81
+ key_fn=key_from_argument("task_id"),
82
+ )
83
+ def process_video_slow(task_id: str, file_path: str) -> None:
84
+ print(f"[process_video_slow] task={task_id} file={file_path}")
85
+ if random.random() < video_failure_rate:
86
+ raise ValueError("transcoder busy")
87
+ print(f"[process_video_slow] task={task_id} processed")
88
+
89
+ return send_email_slow, process_video_slow
90
+
91
+
92
+ def build_worker_example(
93
+ db_path: str, *, worker_failure_rate: float = DEFAULT_WORKER_FAILURE_RATE
94
+ ) -> WorkerJobFn:
95
+ @persistent_retry(
96
+ max_tries=4,
97
+ store_path=db_path,
98
+ wait=wait_none(),
99
+ key_fn=key_from_argument("task_id"),
100
+ )
101
+ def process_worker_job(task_id: str, payload: dict[str, str]) -> None:
102
+ print(
103
+ f"[worker] job={task_id} kind={payload['kind']} target={payload['target']}"
104
+ )
105
+ if random.random() < worker_failure_rate:
106
+ raise RuntimeError("transient worker failure")
107
+ print(f"[worker] job={task_id} completed")
108
+
109
+ return process_worker_job
110
+
111
+
112
+ def run_pair(
113
+ prefix: str, send_email_fn: SendEmailFn, process_video_fn: ProcessVideoFn
114
+ ) -> None:
115
+ print(f"\n=== {prefix} ===")
116
+ for i in range(1, 4):
117
+ email_task_id = f"{prefix}:send-email:{i}"
118
+ video_task_id = f"{prefix}:process-video:{i}"
119
+
120
+ try:
121
+ _ = send_email_fn(email_task_id, "user@example.com")
122
+ except PersistentRetryExhausted as exc:
123
+ print(f"[{prefix}] {exc.key} exhausted after {exc.attempts} attempts")
124
+ except Exception as exc:
125
+ print(f"[{prefix}] {email_task_id} failed this run: {exc}")
126
+
127
+ try:
128
+ _ = process_video_fn(video_task_id, f"video_{i}.mp4")
129
+ except PersistentRetryExhausted as exc:
130
+ print(f"[{prefix}] {exc.key} exhausted after {exc.attempts} attempts")
131
+ except Exception as exc:
132
+ print(f"[{prefix}] {video_task_id} failed this run: {exc}")
133
+
134
+
135
+ def run_demo(
136
+ db_path: str,
137
+ *,
138
+ email_failure_rate: float = DEFAULT_EMAIL_FAILURE_RATE,
139
+ video_failure_rate: float = DEFAULT_VIDEO_FAILURE_RATE,
140
+ ) -> None:
141
+ send_email_fast, process_video_fast = build_fast_examples(
142
+ db_path,
143
+ email_failure_rate=email_failure_rate,
144
+ video_failure_rate=video_failure_rate,
145
+ )
146
+ send_email_slow, process_video_slow = build_slow_examples(
147
+ db_path,
148
+ email_failure_rate=email_failure_rate,
149
+ video_failure_rate=video_failure_rate,
150
+ )
151
+ run_pair("fast", send_email_fast, process_video_fast)
152
+ run_pair("slow", send_email_slow, process_video_slow)
153
+
154
+
155
+ def run_worker_demo(
156
+ db_path: str, *, worker_failure_rate: float = DEFAULT_WORKER_FAILURE_RATE
157
+ ) -> None:
158
+ print("\n=== persistent queue worker ===")
159
+ queue = PersistentQueue(
160
+ "worker-jobs",
161
+ store_path=f"{db_path}_queue",
162
+ lease_timeout=2,
163
+ )
164
+ retryer = PersistentRetrying(
165
+ store_path=f"{db_path}_queue_retries",
166
+ max_tries=4,
167
+ wait=wait_none(),
168
+ key_fn=key_from_argument("task_id"),
169
+ )
170
+
171
+ if queue.empty():
172
+ print("[queue] enqueue sample jobs")
173
+ _ = queue.put({"kind": "email", "target": "alpha@example.com"})
174
+ _ = queue.put({"kind": "video", "target": "video_2.mp4"})
175
+ _ = queue.put({"kind": "webhook", "target": "customer-3"}, delay=1)
176
+ _ = queue.put({"kind": "invoice", "target": "invoice-4"}, delay=2)
177
+ else:
178
+ print(f"[queue] resuming {queue.qsize()} ready job(s)")
179
+
180
+ def process_worker_job(task_id: str, payload: dict[str, str]) -> None:
181
+ print(
182
+ f"[worker] message={task_id} kind={payload['kind']} "
183
+ + f"target={payload['target']}"
184
+ )
185
+ if random.random() < worker_failure_rate:
186
+ raise RuntimeError("transient worker failure")
187
+ print(f"[worker] message={task_id} completed")
188
+
189
+ for cycle in range(1, 8):
190
+ print(f"\n[worker] poll cycle {cycle}")
191
+ processed = 0
192
+
193
+ while True:
194
+ try:
195
+ message = queue.get_message(block=False)
196
+ except Empty:
197
+ break
198
+
199
+ payload = message.value
200
+ try:
201
+ _ = retryer(process_worker_job, message.id, payload)
202
+ except PersistentRetryExhausted as exc:
203
+ _ = queue.dead_letter(message)
204
+ print(
205
+ f"[queue] message={exc.key} moved to dead letter "
206
+ + f"after {exc.attempts} attempts"
207
+ )
208
+ except Exception as exc:
209
+ _ = queue.release(message, delay=1)
210
+ print(f"[queue] message={message.id} released for retry: {exc}")
211
+ else:
212
+ _ = queue.ack(message)
213
+ print(f"[queue] message={message.id} acked")
214
+ processed += 1
215
+
216
+ if processed == 0:
217
+ print("[worker] no tasks available")
218
+ time.sleep(1)
219
+
220
+
221
+ def parse_args() -> argparse.Namespace:
222
+ parser = argparse.ArgumentParser(
223
+ description="localqueue demo CLI; queue code lives in localqueue/ and retry code in localqueue/retry/"
224
+ )
225
+ _ = parser.add_argument(
226
+ "--worker",
227
+ action="store_true",
228
+ help="run a worker-style example instead of the fast/slow demo",
229
+ )
230
+ _ = parser.add_argument(
231
+ "--db-path",
232
+ default="examples/localqueue_retry_demo.sqlite3",
233
+ help="SQLite file for this demo's persisted retry state",
234
+ )
235
+ _ = parser.add_argument(
236
+ "--email-failure-rate",
237
+ type=float,
238
+ default=DEFAULT_EMAIL_FAILURE_RATE,
239
+ help="demo-only probability that email jobs fail",
240
+ )
241
+ _ = parser.add_argument(
242
+ "--video-failure-rate",
243
+ type=float,
244
+ default=DEFAULT_VIDEO_FAILURE_RATE,
245
+ help="demo-only probability that video jobs fail",
246
+ )
247
+ _ = parser.add_argument(
248
+ "--worker-failure-rate",
249
+ type=float,
250
+ default=DEFAULT_WORKER_FAILURE_RATE,
251
+ help="demo-only probability that worker jobs fail",
252
+ )
253
+ return parser.parse_args()
254
+
255
+
256
+ def main() -> int:
257
+ args = parse_args()
258
+ try:
259
+ if args.worker:
260
+ run_worker_demo(args.db_path, worker_failure_rate=args.worker_failure_rate)
261
+ else:
262
+ run_demo(
263
+ args.db_path,
264
+ email_failure_rate=args.email_failure_rate,
265
+ video_failure_rate=args.video_failure_rate,
266
+ )
267
+ except (AttemptStoreLockedError, QueueStoreLockedError) as exc:
268
+ print(exc)
269
+ print(
270
+ "Tip: rerun with --db-path pointing at a different file, "
271
+ + "for example 'examples/localqueue_retry_demo_2.sqlite3'."
272
+ )
273
+ return 1
274
+ return 0
275
+
276
+
277
+ if __name__ == "__main__":
278
+ raise SystemExit(main())
@@ -16,6 +16,7 @@ from .retry import (
16
16
  PersistentRetrying,
17
17
  RetryRecord,
18
18
  SQLiteAttemptStore,
19
+ close_default_store,
19
20
  configure_default_store,
20
21
  configure_default_store_factory,
21
22
  idempotency_key_from_id,
@@ -55,6 +56,7 @@ __all__ = [
55
56
  "SQLiteAttemptStore",
56
57
  "SQLiteQueueStore",
57
58
  "__version__",
59
+ "close_default_store",
58
60
  "configure_default_store",
59
61
  "configure_default_store_factory",
60
62
  "idempotency_key_from_id",