queue-max 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.
- queue_max-0.1.0/LICENSE +21 -0
- queue_max-0.1.0/MANIFEST.in +7 -0
- queue_max-0.1.0/PKG-INFO +233 -0
- queue_max-0.1.0/README.md +193 -0
- queue_max-0.1.0/examples/ai_rate_limited.py +78 -0
- queue_max-0.1.0/examples/basic_usage.py +43 -0
- queue_max-0.1.0/examples/webhook_processor.py +65 -0
- queue_max-0.1.0/pyproject.toml +78 -0
- queue_max-0.1.0/setup.cfg +4 -0
- queue_max-0.1.0/src/queue_max/__init__.py +62 -0
- queue_max-0.1.0/src/queue_max/cli.py +373 -0
- queue_max-0.1.0/src/queue_max/contrib/__init__.py +7 -0
- queue_max-0.1.0/src/queue_max/contrib/django/__init__.py +61 -0
- queue_max-0.1.0/src/queue_max/contrib/django/management/__init__.py +0 -0
- queue_max-0.1.0/src/queue_max/contrib/django/management/commands/__init__.py +0 -0
- queue_max-0.1.0/src/queue_max/contrib/django/management/commands/queue_purge.py +19 -0
- queue_max-0.1.0/src/queue_max/contrib/django/management/commands/queue_stats.py +39 -0
- queue_max-0.1.0/src/queue_max/contrib/django/management/commands/queue_worker.py +69 -0
- queue_max-0.1.0/src/queue_max/contrib/fastapi/__init__.py +117 -0
- queue_max-0.1.0/src/queue_max/contrib/flask/__init__.py +99 -0
- queue_max-0.1.0/src/queue_max/core/__init__.py +16 -0
- queue_max-0.1.0/src/queue_max/core/circuit_breaker.py +162 -0
- queue_max-0.1.0/src/queue_max/core/database.py +253 -0
- queue_max-0.1.0/src/queue_max/core/decorator.py +346 -0
- queue_max-0.1.0/src/queue_max/core/queue.py +420 -0
- queue_max-0.1.0/src/queue_max/core/rate_limiter.py +214 -0
- queue_max-0.1.0/src/queue_max/core/worker.py +426 -0
- queue_max-0.1.0/src/queue_max/exceptions.py +25 -0
- queue_max-0.1.0/src/queue_max/models/__init__.py +5 -0
- queue_max-0.1.0/src/queue_max/models/job.py +340 -0
- queue_max-0.1.0/src/queue_max/py.typed +0 -0
- queue_max-0.1.0/src/queue_max/utils/__init__.py +23 -0
- queue_max-0.1.0/src/queue_max/utils/helpers.py +156 -0
- queue_max-0.1.0/src/queue_max.egg-info/PKG-INFO +233 -0
- queue_max-0.1.0/src/queue_max.egg-info/SOURCES.txt +46 -0
- queue_max-0.1.0/src/queue_max.egg-info/dependency_links.txt +1 -0
- queue_max-0.1.0/src/queue_max.egg-info/entry_points.txt +2 -0
- queue_max-0.1.0/src/queue_max.egg-info/requires.txt +19 -0
- queue_max-0.1.0/src/queue_max.egg-info/top_level.txt +1 -0
- queue_max-0.1.0/tests/__init__.py +0 -0
- queue_max-0.1.0/tests/conftest.py +51 -0
- queue_max-0.1.0/tests/test_circuit_breaker.py +150 -0
- queue_max-0.1.0/tests/test_concurrency.py +130 -0
- queue_max-0.1.0/tests/test_helpers.py +84 -0
- queue_max-0.1.0/tests/test_integration.py +172 -0
- queue_max-0.1.0/tests/test_queue.py +246 -0
- queue_max-0.1.0/tests/test_rate_limiter.py +99 -0
- queue_max-0.1.0/tests/test_worker.py +91 -0
queue_max-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Robusta Queue Contributors
|
|
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.
|
queue_max-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: queue-max
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Task queue with SQLite sharding, rate limiting, and circuit breaker
|
|
5
|
+
Author: Alexandre All
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/all451/queue-max
|
|
8
|
+
Project-URL: repository, https://github.com/all451/queue-max
|
|
9
|
+
Keywords: queue,task-queue,sqlite,background-tasks,worker,sharding
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: typing-extensions>=4.5.0
|
|
26
|
+
Provides-Extra: webhook
|
|
27
|
+
Requires-Dist: requests>=2.31.0; extra == "webhook"
|
|
28
|
+
Provides-Extra: django
|
|
29
|
+
Requires-Dist: Django>=3.2; extra == "django"
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
|
|
32
|
+
Provides-Extra: flask
|
|
33
|
+
Requires-Dist: Flask>=2.0; extra == "flask"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: requests>=2.31.0; extra == "all"
|
|
36
|
+
Requires-Dist: Django>=3.2; extra == "all"
|
|
37
|
+
Requires-Dist: fastapi>=0.100.0; extra == "all"
|
|
38
|
+
Requires-Dist: Flask>=2.0; extra == "all"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# Queue Max
|
|
42
|
+
|
|
43
|
+
Task queue library with SQLite persistence, sharding, rate limiting, and circuit breaker.
|
|
44
|
+
|
|
45
|
+
No Redis or RabbitMQ required. Zero external dependencies (except typing-extensions).
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install queue-max
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With framework integrations:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install queue-max[django]
|
|
57
|
+
pip install queue-max[fastapi]
|
|
58
|
+
pip install queue-max[flask]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from queue_max import Queue, Worker
|
|
65
|
+
|
|
66
|
+
# Create queue
|
|
67
|
+
queue = Queue()
|
|
68
|
+
|
|
69
|
+
# Enqueue a job
|
|
70
|
+
queue.enqueue({"task": "send_email", "to": "user@example.com"}, priority=2)
|
|
71
|
+
|
|
72
|
+
# Define processor
|
|
73
|
+
def process(payload):
|
|
74
|
+
print(f"Processing: {payload}")
|
|
75
|
+
|
|
76
|
+
# Start worker
|
|
77
|
+
worker = Worker("worker-1", process, queue)
|
|
78
|
+
worker.start()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
**SQLite Persistence** -- Jobs are stored in SQLite with WAL mode. No external services needed.
|
|
84
|
+
|
|
85
|
+
**Physical Sharding** -- Multiple .db files allow concurrent read/write across workers.
|
|
86
|
+
|
|
87
|
+
**Rate Limiting** -- Token bucket algorithm shared across all workers. Configurable per minute.
|
|
88
|
+
|
|
89
|
+
**Circuit Breaker** -- Stops calling failing services after N consecutive errors. Recovers automatically.
|
|
90
|
+
|
|
91
|
+
**Retry with Backoff** -- Exponential backoff with jitter for failed jobs. Configurable max retries.
|
|
92
|
+
|
|
93
|
+
**Heartbeat and Recovery** -- Workers send periodic heartbeats. Orphaned jobs are recovered automatically.
|
|
94
|
+
|
|
95
|
+
**Priority Queues** -- Three levels: low (0), medium (1), high (2).
|
|
96
|
+
|
|
97
|
+
**CLI** -- Built-in command line for stats, workers, and queue management.
|
|
98
|
+
|
|
99
|
+
## Configuration via Environment Variables
|
|
100
|
+
|
|
101
|
+
| Variable | Default | Description |
|
|
102
|
+
|----------|---------|-------------|
|
|
103
|
+
| NUM_SHARDS | 6 | Number of shard databases |
|
|
104
|
+
| RATE_LIMIT_MAX | 160 | Max requests per minute |
|
|
105
|
+
| QUEUE_MAX_RETRIES | 3 | Default max retry attempts |
|
|
106
|
+
| DB_BUSY_TIMEOUT | 30000 | SQLite busy timeout (ms) |
|
|
107
|
+
| HEARTBEAT_INTERVAL | 5000 | Worker heartbeat interval (ms) |
|
|
108
|
+
| STUCK_TIMEOUT | 30000 | Orphan job timeout (ms) |
|
|
109
|
+
| DATA_DIR | ./data | Directory for shard files |
|
|
110
|
+
|
|
111
|
+
## API Overview
|
|
112
|
+
|
|
113
|
+
### Queue
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from queue_max import Queue
|
|
117
|
+
|
|
118
|
+
queue = Queue(shards=6, rate_limit=160, max_retries=3)
|
|
119
|
+
|
|
120
|
+
# Enqueue jobs
|
|
121
|
+
queue.enqueue(payload, pagina_id=None, priority=0, max_retries=None)
|
|
122
|
+
queue.enqueue_batch([{"payload": {...}}, ...])
|
|
123
|
+
|
|
124
|
+
# Process jobs
|
|
125
|
+
job = queue.pop_job(worker_id)
|
|
126
|
+
queue.complete_job(job_id, shard_id)
|
|
127
|
+
queue.fail_job(job_id, shard_id, error, permanent=False)
|
|
128
|
+
|
|
129
|
+
# Management
|
|
130
|
+
queue.retry_failed_jobs()
|
|
131
|
+
queue.cleanup_old_jobs(days=7)
|
|
132
|
+
queue.recover_orphans()
|
|
133
|
+
stats = queue.get_stats()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Worker
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from queue_max import Worker, WorkerPool
|
|
140
|
+
|
|
141
|
+
worker = Worker("worker-1", process_function, queue)
|
|
142
|
+
worker.start()
|
|
143
|
+
worker.stop()
|
|
144
|
+
|
|
145
|
+
pool = WorkerPool([worker1, worker2])
|
|
146
|
+
pool.start_all()
|
|
147
|
+
pool.stop_all()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Decorator
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from queue_max import task
|
|
154
|
+
|
|
155
|
+
@task(priority=2, max_retries=3)
|
|
156
|
+
def send_email(to: str, subject: str):
|
|
157
|
+
return send(to, subject)
|
|
158
|
+
|
|
159
|
+
send_email.delay("user@example.com", "Hello")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### CLI
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
queue-max stats
|
|
166
|
+
queue-max worker --function mymodule:myfunction --workers 4
|
|
167
|
+
queue-max enqueue --payload '{"task": "test"}' --priority 2
|
|
168
|
+
queue-max list --status failed --limit 20
|
|
169
|
+
queue-max retry
|
|
170
|
+
queue-max purge --days 7
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Framework Integrations
|
|
174
|
+
|
|
175
|
+
### Django
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
# settings.py
|
|
179
|
+
INSTALLED_APPS = ["queue_max.contrib.django", ...]
|
|
180
|
+
QUEUE_MAX = {"SHARDS": 4, "RATE_LIMIT": 160}
|
|
181
|
+
|
|
182
|
+
# tasks.py
|
|
183
|
+
from queue_max.contrib.django import task
|
|
184
|
+
@task
|
|
185
|
+
def my_task(user_id): ...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Management commands: `python manage.py queue_worker`, `queue_stats`, `queue_purge`.
|
|
189
|
+
|
|
190
|
+
### FastAPI
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from fastapi import FastAPI
|
|
194
|
+
from queue_max.contrib.fastapi import QueueMiddleware
|
|
195
|
+
|
|
196
|
+
app = FastAPI()
|
|
197
|
+
app.add_middleware(QueueMiddleware, max_workers=4)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Flask
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from flask import Flask
|
|
204
|
+
from queue_max.contrib.flask import QueueExtension
|
|
205
|
+
|
|
206
|
+
app = Flask(__name__)
|
|
207
|
+
queue = QueueExtension(app)
|
|
208
|
+
|
|
209
|
+
@queue.task
|
|
210
|
+
def my_task(): ...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Performance
|
|
214
|
+
|
|
215
|
+
| Cenário | Throughput |
|
|
216
|
+
|---------|-----------|
|
|
217
|
+
| Burst (20 workers, 10 shards) | **~3.300 jobs/sec** |
|
|
218
|
+
| Contenção (10 workers, 1 shard) | **~1.660 jobs/sec** |
|
|
219
|
+
| Com 30% de falhas (8 workers) | **Estável** — circuit breaker não trip |
|
|
220
|
+
|
|
221
|
+
- Max queue size: 1M+ jobs per shard
|
|
222
|
+
- [Resultados detalhados dos stress tests](docs/stress-test.md)
|
|
223
|
+
|
|
224
|
+
## Running Tests
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pip install -e ".[dev]"
|
|
228
|
+
pytest tests/ -v
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Queue Max
|
|
2
|
+
|
|
3
|
+
Task queue library with SQLite persistence, sharding, rate limiting, and circuit breaker.
|
|
4
|
+
|
|
5
|
+
No Redis or RabbitMQ required. Zero external dependencies (except typing-extensions).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install queue-max
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With framework integrations:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install queue-max[django]
|
|
17
|
+
pip install queue-max[fastapi]
|
|
18
|
+
pip install queue-max[flask]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from queue_max import Queue, Worker
|
|
25
|
+
|
|
26
|
+
# Create queue
|
|
27
|
+
queue = Queue()
|
|
28
|
+
|
|
29
|
+
# Enqueue a job
|
|
30
|
+
queue.enqueue({"task": "send_email", "to": "user@example.com"}, priority=2)
|
|
31
|
+
|
|
32
|
+
# Define processor
|
|
33
|
+
def process(payload):
|
|
34
|
+
print(f"Processing: {payload}")
|
|
35
|
+
|
|
36
|
+
# Start worker
|
|
37
|
+
worker = Worker("worker-1", process, queue)
|
|
38
|
+
worker.start()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
**SQLite Persistence** -- Jobs are stored in SQLite with WAL mode. No external services needed.
|
|
44
|
+
|
|
45
|
+
**Physical Sharding** -- Multiple .db files allow concurrent read/write across workers.
|
|
46
|
+
|
|
47
|
+
**Rate Limiting** -- Token bucket algorithm shared across all workers. Configurable per minute.
|
|
48
|
+
|
|
49
|
+
**Circuit Breaker** -- Stops calling failing services after N consecutive errors. Recovers automatically.
|
|
50
|
+
|
|
51
|
+
**Retry with Backoff** -- Exponential backoff with jitter for failed jobs. Configurable max retries.
|
|
52
|
+
|
|
53
|
+
**Heartbeat and Recovery** -- Workers send periodic heartbeats. Orphaned jobs are recovered automatically.
|
|
54
|
+
|
|
55
|
+
**Priority Queues** -- Three levels: low (0), medium (1), high (2).
|
|
56
|
+
|
|
57
|
+
**CLI** -- Built-in command line for stats, workers, and queue management.
|
|
58
|
+
|
|
59
|
+
## Configuration via Environment Variables
|
|
60
|
+
|
|
61
|
+
| Variable | Default | Description |
|
|
62
|
+
|----------|---------|-------------|
|
|
63
|
+
| NUM_SHARDS | 6 | Number of shard databases |
|
|
64
|
+
| RATE_LIMIT_MAX | 160 | Max requests per minute |
|
|
65
|
+
| QUEUE_MAX_RETRIES | 3 | Default max retry attempts |
|
|
66
|
+
| DB_BUSY_TIMEOUT | 30000 | SQLite busy timeout (ms) |
|
|
67
|
+
| HEARTBEAT_INTERVAL | 5000 | Worker heartbeat interval (ms) |
|
|
68
|
+
| STUCK_TIMEOUT | 30000 | Orphan job timeout (ms) |
|
|
69
|
+
| DATA_DIR | ./data | Directory for shard files |
|
|
70
|
+
|
|
71
|
+
## API Overview
|
|
72
|
+
|
|
73
|
+
### Queue
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from queue_max import Queue
|
|
77
|
+
|
|
78
|
+
queue = Queue(shards=6, rate_limit=160, max_retries=3)
|
|
79
|
+
|
|
80
|
+
# Enqueue jobs
|
|
81
|
+
queue.enqueue(payload, pagina_id=None, priority=0, max_retries=None)
|
|
82
|
+
queue.enqueue_batch([{"payload": {...}}, ...])
|
|
83
|
+
|
|
84
|
+
# Process jobs
|
|
85
|
+
job = queue.pop_job(worker_id)
|
|
86
|
+
queue.complete_job(job_id, shard_id)
|
|
87
|
+
queue.fail_job(job_id, shard_id, error, permanent=False)
|
|
88
|
+
|
|
89
|
+
# Management
|
|
90
|
+
queue.retry_failed_jobs()
|
|
91
|
+
queue.cleanup_old_jobs(days=7)
|
|
92
|
+
queue.recover_orphans()
|
|
93
|
+
stats = queue.get_stats()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Worker
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from queue_max import Worker, WorkerPool
|
|
100
|
+
|
|
101
|
+
worker = Worker("worker-1", process_function, queue)
|
|
102
|
+
worker.start()
|
|
103
|
+
worker.stop()
|
|
104
|
+
|
|
105
|
+
pool = WorkerPool([worker1, worker2])
|
|
106
|
+
pool.start_all()
|
|
107
|
+
pool.stop_all()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Decorator
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from queue_max import task
|
|
114
|
+
|
|
115
|
+
@task(priority=2, max_retries=3)
|
|
116
|
+
def send_email(to: str, subject: str):
|
|
117
|
+
return send(to, subject)
|
|
118
|
+
|
|
119
|
+
send_email.delay("user@example.com", "Hello")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### CLI
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
queue-max stats
|
|
126
|
+
queue-max worker --function mymodule:myfunction --workers 4
|
|
127
|
+
queue-max enqueue --payload '{"task": "test"}' --priority 2
|
|
128
|
+
queue-max list --status failed --limit 20
|
|
129
|
+
queue-max retry
|
|
130
|
+
queue-max purge --days 7
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Framework Integrations
|
|
134
|
+
|
|
135
|
+
### Django
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# settings.py
|
|
139
|
+
INSTALLED_APPS = ["queue_max.contrib.django", ...]
|
|
140
|
+
QUEUE_MAX = {"SHARDS": 4, "RATE_LIMIT": 160}
|
|
141
|
+
|
|
142
|
+
# tasks.py
|
|
143
|
+
from queue_max.contrib.django import task
|
|
144
|
+
@task
|
|
145
|
+
def my_task(user_id): ...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Management commands: `python manage.py queue_worker`, `queue_stats`, `queue_purge`.
|
|
149
|
+
|
|
150
|
+
### FastAPI
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from fastapi import FastAPI
|
|
154
|
+
from queue_max.contrib.fastapi import QueueMiddleware
|
|
155
|
+
|
|
156
|
+
app = FastAPI()
|
|
157
|
+
app.add_middleware(QueueMiddleware, max_workers=4)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Flask
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from flask import Flask
|
|
164
|
+
from queue_max.contrib.flask import QueueExtension
|
|
165
|
+
|
|
166
|
+
app = Flask(__name__)
|
|
167
|
+
queue = QueueExtension(app)
|
|
168
|
+
|
|
169
|
+
@queue.task
|
|
170
|
+
def my_task(): ...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Performance
|
|
174
|
+
|
|
175
|
+
| Cenário | Throughput |
|
|
176
|
+
|---------|-----------|
|
|
177
|
+
| Burst (20 workers, 10 shards) | **~3.300 jobs/sec** |
|
|
178
|
+
| Contenção (10 workers, 1 shard) | **~1.660 jobs/sec** |
|
|
179
|
+
| Com 30% de falhas (8 workers) | **Estável** — circuit breaker não trip |
|
|
180
|
+
|
|
181
|
+
- Max queue size: 1M+ jobs per shard
|
|
182
|
+
- [Resultados detalhados dos stress tests](docs/stress-test.md)
|
|
183
|
+
|
|
184
|
+
## Running Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install -e ".[dev]"
|
|
188
|
+
pytest tests/ -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""AI/LLM API usage example with rate limiting.
|
|
2
|
+
|
|
3
|
+
Shows how to use Robusta Queue to handle rate-limited API calls
|
|
4
|
+
for AI/LLM services like OpenAI, Anthropic, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from queue_max import Queue, Worker
|
|
10
|
+
|
|
11
|
+
# Configure queue with rate limit matching your API tier
|
|
12
|
+
# e.g., OpenAI free tier: 3 RPM, Tier 1: 500 RPM
|
|
13
|
+
queue = Queue(shards=2, rate_limit=10, max_retries=3)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def call_llm_api(payload: dict) -> dict:
|
|
17
|
+
"""Simulate calling an LLM API with rate limiting.
|
|
18
|
+
|
|
19
|
+
Replace with actual API call:
|
|
20
|
+
import openai
|
|
21
|
+
response = openai.ChatCompletion.create(
|
|
22
|
+
model="gpt-4",
|
|
23
|
+
messages=[{"role": "user", "content": payload['prompt']}]
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
prompt = payload.get("prompt", "")
|
|
27
|
+
print(f" Calling LLM API with prompt: {prompt[:50]}...")
|
|
28
|
+
|
|
29
|
+
# Simulate API latency
|
|
30
|
+
time.sleep(1.0)
|
|
31
|
+
|
|
32
|
+
# Simulate rate limit error
|
|
33
|
+
import random
|
|
34
|
+
|
|
35
|
+
if random.random() < 0.1:
|
|
36
|
+
raise Exception("429 Too Many Requests")
|
|
37
|
+
|
|
38
|
+
return {"response": f"Response to: {prompt}", "tokens": len(prompt)}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main():
|
|
42
|
+
prompts = [
|
|
43
|
+
"Explain quantum computing in simple terms",
|
|
44
|
+
"Write a Python decorator for logging",
|
|
45
|
+
"Summarize the theory of relativity",
|
|
46
|
+
"Create a recipe for vegan chocolate cake",
|
|
47
|
+
"Translate 'hello world' to Japanese",
|
|
48
|
+
"Write a haiku about programming",
|
|
49
|
+
"Explain how databases work",
|
|
50
|
+
"Describe the water cycle",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
print(" Enqueuing LLM API calls...")
|
|
54
|
+
for prompt in prompts:
|
|
55
|
+
result = queue.enqueue(
|
|
56
|
+
payload={"prompt": prompt, "model": "gpt-4"},
|
|
57
|
+
priority=1,
|
|
58
|
+
)
|
|
59
|
+
print(f" Enqueued: {prompt[:30]}... (job {result['id']})")
|
|
60
|
+
|
|
61
|
+
# Start worker with rate-limited processing
|
|
62
|
+
worker = Worker("ai-worker", call_llm_api, queue)
|
|
63
|
+
worker.start()
|
|
64
|
+
|
|
65
|
+
print("\n Processing with rate limiting (10 req/min)...")
|
|
66
|
+
time.sleep(15)
|
|
67
|
+
|
|
68
|
+
worker_stats = worker.get_stats()
|
|
69
|
+
print(f"\n Processed: {worker_stats['processed']}")
|
|
70
|
+
print(f" Failed: {worker_stats['failed']}")
|
|
71
|
+
print(f" Retried: {worker_stats['retried']}")
|
|
72
|
+
|
|
73
|
+
worker.stop()
|
|
74
|
+
print(" Done!")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Basic usage example for Robusta Queue."""
|
|
2
|
+
|
|
3
|
+
from queue_max import Queue, Worker
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def process_task(payload: dict) -> str:
|
|
7
|
+
"""Process a task payload."""
|
|
8
|
+
print(f" Processing: {payload}")
|
|
9
|
+
return f"Done: {payload.get('task', 'unknown')}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
# 1. Create a queue (3 shards, 100 req/min)
|
|
14
|
+
queue = Queue(shards=3, rate_limit=100)
|
|
15
|
+
|
|
16
|
+
# 2. Enqueue some jobs
|
|
17
|
+
for i in range(5):
|
|
18
|
+
result = queue.enqueue(
|
|
19
|
+
payload={"task": f"job-{i}", "data": f"payload-{i}"},
|
|
20
|
+
priority=i % 3, # Mix of priorities
|
|
21
|
+
pagina_id=i % 3, # Spread across shards
|
|
22
|
+
)
|
|
23
|
+
print(f" Enqueued job {result['id']} in shard {result['shard_id']}")
|
|
24
|
+
|
|
25
|
+
# 3. Process with a worker
|
|
26
|
+
worker = Worker("example-worker", process_task, queue)
|
|
27
|
+
worker.start()
|
|
28
|
+
|
|
29
|
+
# 4. Let it process for a bit
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
time.sleep(3)
|
|
33
|
+
|
|
34
|
+
# 5. Check stats
|
|
35
|
+
stats = queue.get_stats()
|
|
36
|
+
print(f"\n Queue stats: {stats}")
|
|
37
|
+
|
|
38
|
+
worker.stop()
|
|
39
|
+
print(" Done!")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Webhook processing example with Robusta Queue."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from queue_max import Queue, Worker
|
|
7
|
+
|
|
8
|
+
# Queue configured for webhook processing
|
|
9
|
+
queue = Queue(shards=4, rate_limit=160, max_retries=3)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def process_webhook(payload: dict) -> dict:
|
|
13
|
+
"""Process a webhook payload.
|
|
14
|
+
|
|
15
|
+
In real usage, this would call an external API, send emails, etc.
|
|
16
|
+
"""
|
|
17
|
+
print(f" Processing webhook: {payload.get('event', 'unknown')}")
|
|
18
|
+
print(f" Account: {payload.get('account_id', 'N/A')}")
|
|
19
|
+
|
|
20
|
+
# Simulate API call
|
|
21
|
+
time.sleep(0.5)
|
|
22
|
+
|
|
23
|
+
# Simulate occasional failure
|
|
24
|
+
if payload.get("simulate_error"):
|
|
25
|
+
raise ConnectionError("External API timeout")
|
|
26
|
+
|
|
27
|
+
return {"status": "processed"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
# Enqueue sample webhooks
|
|
32
|
+
webhooks = [
|
|
33
|
+
{"event": "user.created", "account_id": 101, "user": "alice"},
|
|
34
|
+
{"event": "payment.received", "account_id": 102, "amount": 99.90},
|
|
35
|
+
{"event": "order.shipped", "account_id": 101, "order_id": "ORD-123"},
|
|
36
|
+
{"event": "user.deleted", "account_id": 103, "user": "bob"},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for wh in webhooks:
|
|
40
|
+
result = queue.enqueue(
|
|
41
|
+
payload=wh,
|
|
42
|
+
priority=2, # Webhooks are high priority
|
|
43
|
+
pagina_id=wh.get("account_id"), # Consistent sharding per account
|
|
44
|
+
)
|
|
45
|
+
print(f" Enqueued webhook: {wh['event']} (job {result['id']})")
|
|
46
|
+
|
|
47
|
+
# Start worker
|
|
48
|
+
worker = Worker("webhook-worker", process_webhook, queue)
|
|
49
|
+
worker.start()
|
|
50
|
+
|
|
51
|
+
print("\n Processing webhooks...")
|
|
52
|
+
time.sleep(5)
|
|
53
|
+
|
|
54
|
+
# Stats
|
|
55
|
+
stats = queue.get_stats()
|
|
56
|
+
worker_stats = worker.get_stats()
|
|
57
|
+
print(f"\n Queue: {stats['pending']} pending, {stats['failed']} failed")
|
|
58
|
+
print(f" Worker: {worker_stats['processed']} processed")
|
|
59
|
+
|
|
60
|
+
worker.stop()
|
|
61
|
+
print(" Done!")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|