nodus-queue 0.1.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.
- nodus_queue-0.1.0/LICENSE +21 -0
- nodus_queue-0.1.0/PKG-INFO +136 -0
- nodus_queue-0.1.0/README.md +110 -0
- nodus_queue-0.1.0/nodus_queue/__init__.py +58 -0
- nodus_queue-0.1.0/nodus_queue/backends.py +776 -0
- nodus_queue-0.1.0/nodus_queue/metrics.py +52 -0
- nodus_queue-0.1.0/nodus_queue/payload.py +58 -0
- nodus_queue-0.1.0/nodus_queue/queue.py +242 -0
- nodus_queue-0.1.0/nodus_queue.egg-info/PKG-INFO +136 -0
- nodus_queue-0.1.0/nodus_queue.egg-info/SOURCES.txt +17 -0
- nodus_queue-0.1.0/nodus_queue.egg-info/dependency_links.txt +1 -0
- nodus_queue-0.1.0/nodus_queue.egg-info/requires.txt +9 -0
- nodus_queue-0.1.0/nodus_queue.egg-info/top_level.txt +1 -0
- nodus_queue-0.1.0/pyproject.toml +36 -0
- nodus_queue-0.1.0/setup.cfg +4 -0
- nodus_queue-0.1.0/tests/test_factory.py +129 -0
- nodus_queue-0.1.0/tests/test_memory_backend.py +214 -0
- nodus_queue-0.1.0/tests/test_payload.py +63 -0
- nodus_queue-0.1.0/tests/test_redis_backend.py +209 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shawn Knight
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodus-queue
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distributed job queue with DLQ, delayed jobs, in-flight tracking, Redis backend, and in-memory fallback
|
|
5
|
+
Author: Shawn Knight
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Masterplanner25/nodus-queue
|
|
8
|
+
Project-URL: Repository, https://github.com/Masterplanner25/nodus-queue
|
|
9
|
+
Keywords: queue,redis,distributed,jobs,dlq,nodus
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: tenacity>=8.0.0
|
|
19
|
+
Provides-Extra: redis
|
|
20
|
+
Requires-Dist: redis>=4.0.0; extra == "redis"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
23
|
+
Requires-Dist: fakeredis>=2.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: redis>=4.0.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# nodus-queue
|
|
28
|
+
|
|
29
|
+
Distributed job queue with Dead Letter Queue, delayed jobs, in-flight tracking, and visibility-timeout recovery. Redis backend for multi-instance production; in-memory fallback for dev and tests. Zero hard dependencies beyond `tenacity`.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install nodus-queue # core + in-memory backend
|
|
35
|
+
pip install "nodus-queue[redis]" # + Redis backend
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from nodus_queue import QueueJobPayload, get_queue, reset_queue
|
|
42
|
+
|
|
43
|
+
# In dev/test — in-memory backend (automatic when REDIS_URL is unset)
|
|
44
|
+
q = get_queue()
|
|
45
|
+
|
|
46
|
+
job = QueueJobPayload(job_id="run-123", task_name="agent.run")
|
|
47
|
+
q.enqueue(job)
|
|
48
|
+
|
|
49
|
+
# Worker side
|
|
50
|
+
job = q.dequeue(timeout=5) # blocks up to 5 seconds
|
|
51
|
+
if job:
|
|
52
|
+
try:
|
|
53
|
+
# ... process job ...
|
|
54
|
+
q.ack(job.job_id) # remove from in-flight
|
|
55
|
+
except Exception as e:
|
|
56
|
+
q.fail(job.job_id, str(e)) # move to DLQ
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Redis backend
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
REDIS_URL=redis://localhost:6379/0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from nodus_queue import get_queue
|
|
67
|
+
|
|
68
|
+
q = get_queue() # picks up REDIS_URL automatically
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Delayed jobs
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Schedule a job to run after 30 seconds
|
|
75
|
+
q.enqueue_delayed(job, delay_seconds=30)
|
|
76
|
+
|
|
77
|
+
# Promote ready jobs (call periodically in Redis mode)
|
|
78
|
+
count = q.process_delayed_jobs()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Crash recovery
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# On worker startup — re-enqueue jobs stuck in-flight for > 5 minutes
|
|
85
|
+
q.requeue_stale_jobs(timeout_seconds=300)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Dead Letter Queue
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
depth = q.get_dlq_depth()
|
|
92
|
+
q.drain_dead_letters() # clear all
|
|
93
|
+
q.remove_dead_letter("job-id-123") # remove one
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Optional Prometheus metrics
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from prometheus_client import CollectorRegistry, Counter, Gauge
|
|
100
|
+
from nodus_queue import QueueMetrics, get_queue
|
|
101
|
+
|
|
102
|
+
REGISTRY = CollectorRegistry()
|
|
103
|
+
enq = Counter("queue_enqueue_total", "...", ["backend", "outcome"], registry=REGISTRY)
|
|
104
|
+
|
|
105
|
+
class MyMetrics(QueueMetrics):
|
|
106
|
+
def on_enqueue(self, backend, outcome):
|
|
107
|
+
enq.labels(backend=backend, outcome=outcome).inc()
|
|
108
|
+
# override other hooks as needed
|
|
109
|
+
|
|
110
|
+
q = get_queue(metrics=MyMetrics())
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Backend change callback
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
def on_change(event: str, payload: dict) -> None:
|
|
117
|
+
print(f"Queue backend changed: {event} {payload}")
|
|
118
|
+
|
|
119
|
+
q = get_queue(on_backend_change=on_change)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Environment variables
|
|
123
|
+
|
|
124
|
+
| Variable | Default | Purpose |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `REDIS_URL` | — | Redis connection URL |
|
|
127
|
+
| `NODUS_QUEUE_NAME` | `nodus:jobs` | Key prefix for all queue Redis keys |
|
|
128
|
+
| `NODUS_QUEUE_MAXSIZE` | `100` | Hard capacity limit |
|
|
129
|
+
| `NODUS_REQUIRE_REDIS` | `false` | Fail on startup if Redis is unavailable |
|
|
130
|
+
| `EXECUTION_MODE` | `thread` | `distributed` requires `REDIS_URL` |
|
|
131
|
+
| `ENV` | — | `production`/`prod` requires `REDIS_URL` |
|
|
132
|
+
| `TESTING` / `TEST_MODE` | — | Auto-select in-memory backend |
|
|
133
|
+
|
|
134
|
+
## Extracted from
|
|
135
|
+
|
|
136
|
+
`AINDY/core/distributed_queue.py` in the A.I.N.D.Y. runtime.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# nodus-queue
|
|
2
|
+
|
|
3
|
+
Distributed job queue with Dead Letter Queue, delayed jobs, in-flight tracking, and visibility-timeout recovery. Redis backend for multi-instance production; in-memory fallback for dev and tests. Zero hard dependencies beyond `tenacity`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nodus-queue # core + in-memory backend
|
|
9
|
+
pip install "nodus-queue[redis]" # + Redis backend
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from nodus_queue import QueueJobPayload, get_queue, reset_queue
|
|
16
|
+
|
|
17
|
+
# In dev/test — in-memory backend (automatic when REDIS_URL is unset)
|
|
18
|
+
q = get_queue()
|
|
19
|
+
|
|
20
|
+
job = QueueJobPayload(job_id="run-123", task_name="agent.run")
|
|
21
|
+
q.enqueue(job)
|
|
22
|
+
|
|
23
|
+
# Worker side
|
|
24
|
+
job = q.dequeue(timeout=5) # blocks up to 5 seconds
|
|
25
|
+
if job:
|
|
26
|
+
try:
|
|
27
|
+
# ... process job ...
|
|
28
|
+
q.ack(job.job_id) # remove from in-flight
|
|
29
|
+
except Exception as e:
|
|
30
|
+
q.fail(job.job_id, str(e)) # move to DLQ
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Redis backend
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
REDIS_URL=redis://localhost:6379/0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from nodus_queue import get_queue
|
|
41
|
+
|
|
42
|
+
q = get_queue() # picks up REDIS_URL automatically
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Delayed jobs
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Schedule a job to run after 30 seconds
|
|
49
|
+
q.enqueue_delayed(job, delay_seconds=30)
|
|
50
|
+
|
|
51
|
+
# Promote ready jobs (call periodically in Redis mode)
|
|
52
|
+
count = q.process_delayed_jobs()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Crash recovery
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# On worker startup — re-enqueue jobs stuck in-flight for > 5 minutes
|
|
59
|
+
q.requeue_stale_jobs(timeout_seconds=300)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Dead Letter Queue
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
depth = q.get_dlq_depth()
|
|
66
|
+
q.drain_dead_letters() # clear all
|
|
67
|
+
q.remove_dead_letter("job-id-123") # remove one
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Optional Prometheus metrics
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from prometheus_client import CollectorRegistry, Counter, Gauge
|
|
74
|
+
from nodus_queue import QueueMetrics, get_queue
|
|
75
|
+
|
|
76
|
+
REGISTRY = CollectorRegistry()
|
|
77
|
+
enq = Counter("queue_enqueue_total", "...", ["backend", "outcome"], registry=REGISTRY)
|
|
78
|
+
|
|
79
|
+
class MyMetrics(QueueMetrics):
|
|
80
|
+
def on_enqueue(self, backend, outcome):
|
|
81
|
+
enq.labels(backend=backend, outcome=outcome).inc()
|
|
82
|
+
# override other hooks as needed
|
|
83
|
+
|
|
84
|
+
q = get_queue(metrics=MyMetrics())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Backend change callback
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def on_change(event: str, payload: dict) -> None:
|
|
91
|
+
print(f"Queue backend changed: {event} {payload}")
|
|
92
|
+
|
|
93
|
+
q = get_queue(on_backend_change=on_change)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Environment variables
|
|
97
|
+
|
|
98
|
+
| Variable | Default | Purpose |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `REDIS_URL` | — | Redis connection URL |
|
|
101
|
+
| `NODUS_QUEUE_NAME` | `nodus:jobs` | Key prefix for all queue Redis keys |
|
|
102
|
+
| `NODUS_QUEUE_MAXSIZE` | `100` | Hard capacity limit |
|
|
103
|
+
| `NODUS_REQUIRE_REDIS` | `false` | Fail on startup if Redis is unavailable |
|
|
104
|
+
| `EXECUTION_MODE` | `thread` | `distributed` requires `REDIS_URL` |
|
|
105
|
+
| `ENV` | — | `production`/`prod` requires `REDIS_URL` |
|
|
106
|
+
| `TESTING` / `TEST_MODE` | — | Auto-select in-memory backend |
|
|
107
|
+
|
|
108
|
+
## Extracted from
|
|
109
|
+
|
|
110
|
+
`AINDY/core/distributed_queue.py` in the A.I.N.D.Y. runtime.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""nodus-queue — distributed job queue with DLQ, delayed jobs, and in-flight tracking.
|
|
2
|
+
|
|
3
|
+
Backends:
|
|
4
|
+
RedisQueueBackend — LPUSH/BRPOP; single-consumer atomic; Lua-script capacity guard
|
|
5
|
+
InMemoryQueueBackend — thread-safe; Timer-based delayed enqueue; for tests and dev
|
|
6
|
+
|
|
7
|
+
Payload:
|
|
8
|
+
QueueJobPayload — serialisable job envelope with idempotency key
|
|
9
|
+
|
|
10
|
+
Metrics hook:
|
|
11
|
+
QueueMetrics — optional noop base class; subclass to wire Prometheus
|
|
12
|
+
|
|
13
|
+
Errors:
|
|
14
|
+
QueueSaturatedError — raised when the queue rejects work at capacity
|
|
15
|
+
|
|
16
|
+
Factory:
|
|
17
|
+
get_queue() — return the singleton backend (Redis or in-memory fallback)
|
|
18
|
+
reset_queue() — reset singleton for test isolation
|
|
19
|
+
validate_queue_backend() — fail fast if backend is unavailable
|
|
20
|
+
get_queue_health_snapshot() — health dict for monitoring
|
|
21
|
+
attempt_queue_backend_reconnect() — try to restore Redis after degraded fallback
|
|
22
|
+
"""
|
|
23
|
+
from .backends import (
|
|
24
|
+
QUEUE_NAME_DEFAULT,
|
|
25
|
+
DistributedQueueBackend,
|
|
26
|
+
InMemoryQueueBackend,
|
|
27
|
+
QueueSaturatedError,
|
|
28
|
+
RedisQueueBackend,
|
|
29
|
+
)
|
|
30
|
+
from .metrics import QueueMetrics
|
|
31
|
+
from .payload import QueueJobPayload
|
|
32
|
+
from .queue import (
|
|
33
|
+
attempt_queue_backend_reconnect,
|
|
34
|
+
get_queue,
|
|
35
|
+
get_queue_health_snapshot,
|
|
36
|
+
reset_queue,
|
|
37
|
+
validate_queue_backend,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Backends
|
|
42
|
+
"DistributedQueueBackend",
|
|
43
|
+
"InMemoryQueueBackend",
|
|
44
|
+
"RedisQueueBackend",
|
|
45
|
+
"QUEUE_NAME_DEFAULT",
|
|
46
|
+
# Payload
|
|
47
|
+
"QueueJobPayload",
|
|
48
|
+
# Metrics
|
|
49
|
+
"QueueMetrics",
|
|
50
|
+
# Errors
|
|
51
|
+
"QueueSaturatedError",
|
|
52
|
+
# Factory
|
|
53
|
+
"get_queue",
|
|
54
|
+
"reset_queue",
|
|
55
|
+
"validate_queue_backend",
|
|
56
|
+
"get_queue_health_snapshot",
|
|
57
|
+
"attempt_queue_backend_reconnect",
|
|
58
|
+
]
|