broccoli-workers 0.1.0__tar.gz → 0.1.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.
- broccoli_workers-0.1.2/PKG-INFO +539 -0
- broccoli_workers-0.1.2/README.md +491 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/cli.py +263 -40
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/core/__init__.py +15 -15
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/core/chain/chain.py +17 -3
- broccoli_workers-0.1.2/broccoli/core/chain/chain_queue.py +21 -0
- broccoli_workers-0.1.2/broccoli/core/chain/task_chain.py +169 -0
- broccoli_workers-0.1.2/broccoli/core/gpu/gpu_queue.py +12 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/core/health.py +20 -20
- broccoli_workers-0.1.2/broccoli/core/redis_controller.py +66 -0
- broccoli_workers-0.1.2/broccoli/core/result.py +60 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/core/task/task.py +36 -6
- broccoli_workers-0.1.2/broccoli/core/task/task_queue.py +517 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/core/task/task_registry.py +47 -47
- broccoli_workers-0.1.2/broccoli/logging_config.py +42 -0
- broccoli_workers-0.1.2/broccoli/workers/GPUAutoScalingPool.py +68 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/__init__.py +2 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/async_worker.py +21 -2
- broccoli_workers-0.1.2/broccoli/workers/auto_scale_worker.py +114 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/base_worker.py +98 -25
- broccoli_workers-0.1.2/broccoli/workers/chain_worker.py +131 -0
- broccoli_workers-0.1.2/broccoli/workers/gpu_worker.py +60 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/hybrid_worker.py +29 -10
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/threaded_worker.py +20 -8
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/workers/worker_pool.py +144 -99
- broccoli_workers-0.1.2/broccoli_workers.egg-info/PKG-INFO +539 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli_workers.egg-info/SOURCES.txt +5 -1
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/pyproject.toml +1 -1
- broccoli_workers-0.1.0/PKG-INFO +0 -594
- broccoli_workers-0.1.0/README.md +0 -546
- broccoli_workers-0.1.0/broccoli/core/chain/chain_mixin.py +0 -49
- broccoli_workers-0.1.0/broccoli/core/chain/chain_queue.py +0 -299
- broccoli_workers-0.1.0/broccoli/core/chain/task_chain.py +0 -201
- broccoli_workers-0.1.0/broccoli/core/redis_controller.py +0 -35
- broccoli_workers-0.1.0/broccoli/core/result.py +0 -78
- broccoli_workers-0.1.0/broccoli/core/task/task_queue.py +0 -300
- broccoli_workers-0.1.0/broccoli/workers/chain_worker.py +0 -94
- broccoli_workers-0.1.0/broccoli_workers.egg-info/PKG-INFO +0 -594
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/LICENSE +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli/__init__.py +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli_workers.egg-info/dependency_links.txt +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli_workers.egg-info/entry_points.txt +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli_workers.egg-info/requires.txt +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/broccoli_workers.egg-info/top_level.txt +0 -0
- {broccoli_workers-0.1.0 → broccoli_workers-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: broccoli-workers
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Distributed task queue with priorities, dependencies, and chains, backed by Redis
|
|
5
|
+
Author-email: Success Oguntuyi <oguntuyisuccess@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Broccoli Contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Classifier: Development Status :: 4 - Beta
|
|
29
|
+
Classifier: Intended Audience :: Developers
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Requires-Python: >=3.11
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: redis>=4.0.0
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
43
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
44
|
+
Requires-Dist: isort>=5.0; extra == "dev"
|
|
45
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
46
|
+
Requires-Dist: flake8; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Broccoli
|
|
50
|
+
|
|
51
|
+
Broccoli is a Redis-backed Python task queue for running background work with:
|
|
52
|
+
|
|
53
|
+
- **Priority scheduling** (strict priority tiers + FIFO ordering inside each tier)
|
|
54
|
+
- **Dependency-aware tasks** (`depends_on`)
|
|
55
|
+
- **Retries + dead-letter handling**
|
|
56
|
+
- **Crash/stall recovery**
|
|
57
|
+
- **Multiple worker runtimes** (base, threaded, async, hybrid, chain, gpu)
|
|
58
|
+
- **CLI tooling** for operational inspection and control
|
|
59
|
+
|
|
60
|
+
It is designed for teams that want Celery-like queue behavior with a smaller, explicit codebase.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Table of contents
|
|
65
|
+
|
|
66
|
+
- [Features](#features)
|
|
67
|
+
- [Architecture and Redis data model](#architecture-and-redis-data-model)
|
|
68
|
+
- [Requirements](#requirements)
|
|
69
|
+
- [Installation](#installation)
|
|
70
|
+
- [Quick start](#quick-start)
|
|
71
|
+
- [Core concepts](#core-concepts)
|
|
72
|
+
- [Task lifecycle](#task-lifecycle)
|
|
73
|
+
- [Worker types](#worker-types)
|
|
74
|
+
- [Task dependencies](#task-dependencies)
|
|
75
|
+
- [Dead-letter and recovery](#dead-letter-and-recovery)
|
|
76
|
+
- [CLI reference](#cli-reference)
|
|
77
|
+
- [Environment variables](#environment-variables)
|
|
78
|
+
- [Programmatic API reference](#programmatic-api-reference)
|
|
79
|
+
- [Operational guidance](#operational-guidance)
|
|
80
|
+
- [Troubleshooting](#troubleshooting)
|
|
81
|
+
- [Development](#development)
|
|
82
|
+
- [License](#license)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Features
|
|
87
|
+
|
|
88
|
+
### Queueing and scheduling
|
|
89
|
+
|
|
90
|
+
- Push tasks with explicit priorities (`0` is highest priority)
|
|
91
|
+
- FIFO ordering is preserved within each priority band
|
|
92
|
+
- Queue operations are persisted in Redis for durability
|
|
93
|
+
|
|
94
|
+
### Dependency-aware execution
|
|
95
|
+
|
|
96
|
+
- A task can declare `depends_on=[<task_id>, ...]` for fan-out/fan-in style waits
|
|
97
|
+
- Dependent tasks are marked `waiting` until all listed parents complete
|
|
98
|
+
- Dependency release is handled automatically on parent completion
|
|
99
|
+
|
|
100
|
+
### Worker execution models
|
|
101
|
+
|
|
102
|
+
- Single-threaded worker (`BaseWorker`)
|
|
103
|
+
- Thread pool worker (`ThreadedWorker`)
|
|
104
|
+
- Asyncio worker (`AsyncWorker`)
|
|
105
|
+
- Hybrid worker (`HybridWorker`: async dispatch + threaded execution)
|
|
106
|
+
- Chain-specific worker (`ChainWorker`)
|
|
107
|
+
- GPU worker (`GPUWorker`: hybrid worker pinned to a GPU queue/device)
|
|
108
|
+
|
|
109
|
+
### Reliability and failure handling
|
|
110
|
+
|
|
111
|
+
- Built-in retry handling (`max_retries`, default `3`)
|
|
112
|
+
- Dead-letter capture for permanently failed tasks
|
|
113
|
+
- Requeue dead-letter tasks for replay
|
|
114
|
+
- Stalled task recovery (manual and startup auto-recovery)
|
|
115
|
+
|
|
116
|
+
### Operations and observability
|
|
117
|
+
|
|
118
|
+
- Queue depth and processing stats
|
|
119
|
+
- Dead-letter inspection CLI commands
|
|
120
|
+
- Health checks for Redis reachability
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Architecture and Redis data model
|
|
125
|
+
|
|
126
|
+
Broccoli primarily uses Redis sorted sets and hashes:
|
|
127
|
+
|
|
128
|
+
- `<base>:queue` — runnable tasks, scored by `(priority tier + FIFO sequence)`
|
|
129
|
+
- `<base>:processing` — in-flight tasks, scored by processing timestamp
|
|
130
|
+
- `<base>:sequence` — monotonic sequence for FIFO ordering
|
|
131
|
+
- `<task_prefix>:<task_id>` — task metadata hash
|
|
132
|
+
- `dependency:<task_id>` — set of blocked dependents waiting for parent
|
|
133
|
+
- `<task_prefix>:dead_letter` — dead-letter task IDs with failure timestamps
|
|
134
|
+
- `dl:<task_id>` — dead-letter task snapshot
|
|
135
|
+
- `result:<task_id>` / `result:<chain_id>` — result records with TTL
|
|
136
|
+
|
|
137
|
+
For chains, additional keys like `chain:<chain_id>` and `chain:<chain_id>:tasks` are used.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Requirements
|
|
142
|
+
|
|
143
|
+
- Python **3.11+**
|
|
144
|
+
- Redis **6+**
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Installation
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install broccoli-workers
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Local development install:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pip install -e .
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Quick start
|
|
163
|
+
|
|
164
|
+
### 1) Create a producer
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from broccoli.core.task.task import Task
|
|
168
|
+
from broccoli.core.task.task_queue import TaskQueue
|
|
169
|
+
|
|
170
|
+
queue = TaskQueue(
|
|
171
|
+
redis_url="redis://localhost:6379",
|
|
172
|
+
decode_responses=True, # set False to work with raw Redis bytes
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
task = Task(task_type="send_email", payload={"to": "user@example.com"})
|
|
176
|
+
queue.push(task, priority=1)
|
|
177
|
+
print(task.task_id)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2) Create a worker and register handlers
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from broccoli.workers.threaded_worker import ThreadedWorker
|
|
184
|
+
|
|
185
|
+
worker = ThreadedWorker(redis_url="redis://localhost:6379", max_workers=4)
|
|
186
|
+
|
|
187
|
+
@worker.registry.register("send_email")
|
|
188
|
+
def send_email(payload):
|
|
189
|
+
print(f"Sending email to {payload['to']}")
|
|
190
|
+
return {"status": "sent", "to": payload["to"]}
|
|
191
|
+
|
|
192
|
+
worker.start()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Core concepts
|
|
198
|
+
|
|
199
|
+
### Task model
|
|
200
|
+
|
|
201
|
+
`Task` fields include:
|
|
202
|
+
|
|
203
|
+
- `task_id`: UUID (auto-generated unless provided)
|
|
204
|
+
- `task_type`: handler key in registry
|
|
205
|
+
- `payload`: arbitrary JSON-serializable dictionary
|
|
206
|
+
- `status`: `pending | waiting | in_progress | completed | failed`
|
|
207
|
+
- `retries`: current retry count
|
|
208
|
+
- `max_retries`: retry limit
|
|
209
|
+
- `depends_on`: optional list of parent task IDs
|
|
210
|
+
- `result`: handler output
|
|
211
|
+
- `error`: error message for failed attempts
|
|
212
|
+
|
|
213
|
+
### Registry model
|
|
214
|
+
|
|
215
|
+
`TaskRegistry` is singleton-backed, so handlers are globally visible in-process.
|
|
216
|
+
|
|
217
|
+
You can register with a decorator:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
@worker.registry.register("my_task")
|
|
221
|
+
def my_task(payload):
|
|
222
|
+
return {"ok": True}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or manually:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
worker.registry.register_manually("my_task", my_task_handler)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Task lifecycle
|
|
234
|
+
|
|
235
|
+
1. **Push**: task metadata hash is saved.
|
|
236
|
+
2. **Queue placement**:
|
|
237
|
+
- no dependency -> `pending` in runnable queue
|
|
238
|
+
- with unresolved dependency -> `waiting`
|
|
239
|
+
3. **Pop**: worker moves task to `in_progress` and processing set.
|
|
240
|
+
4. **Execution**:
|
|
241
|
+
- success -> `completed`
|
|
242
|
+
- failure + retries left -> `pending` (requeued)
|
|
243
|
+
- failure + retries exhausted -> `failed` (dead-letter eligible)
|
|
244
|
+
5. **Post-processing**:
|
|
245
|
+
- results persisted
|
|
246
|
+
- task hash cleanup for terminal states
|
|
247
|
+
- handlers invoked
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Worker types
|
|
252
|
+
|
|
253
|
+
### BaseWorker
|
|
254
|
+
|
|
255
|
+
- Sequential task execution
|
|
256
|
+
- Simple and predictable behavior
|
|
257
|
+
- Useful for debugging and low-throughput workloads
|
|
258
|
+
|
|
259
|
+
### ThreadedWorker
|
|
260
|
+
|
|
261
|
+
- Executes tasks concurrently using `ThreadPoolExecutor`
|
|
262
|
+
- Good for mixed and blocking workloads
|
|
263
|
+
- Config: `max_workers`
|
|
264
|
+
|
|
265
|
+
### AsyncWorker
|
|
266
|
+
|
|
267
|
+
- Uses `asyncio` loop with bounded concurrency
|
|
268
|
+
- Runs task handlers via executor with timeout enforcement
|
|
269
|
+
- Config: `max_concurrent`
|
|
270
|
+
|
|
271
|
+
### HybridWorker
|
|
272
|
+
|
|
273
|
+
- Async dispatch + thread pool execution
|
|
274
|
+
- Suitable for high-throughput mixed workloads
|
|
275
|
+
- Config: `thread_workers`, `async_tasks`
|
|
276
|
+
|
|
277
|
+
### ChainWorker
|
|
278
|
+
|
|
279
|
+
- Specialized for chain orchestration
|
|
280
|
+
- Updates chain progress and completion state
|
|
281
|
+
- Works with `TaskChain`
|
|
282
|
+
|
|
283
|
+
### GPUWorker
|
|
284
|
+
|
|
285
|
+
- Dedicated worker for GPU workloads (`gpu_tasks:queue`)
|
|
286
|
+
- Pins execution to a selected GPU via `--gpu-id`
|
|
287
|
+
- Uses `HybridWorker` execution model with GPU cache cleanup
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Task dependencies
|
|
292
|
+
|
|
293
|
+
When pushing dependent tasks:
|
|
294
|
+
|
|
295
|
+
- If all parents already completed, dependent is enqueued immediately.
|
|
296
|
+
- If one or more parents are incomplete, dependent enters `waiting` and is linked under each unresolved `dependency:<parent_id>` set.
|
|
297
|
+
|
|
298
|
+
On parent completion, waiting tasks decrement their remaining dependency count and enqueue only when all dependencies are satisfied.
|
|
299
|
+
|
|
300
|
+
Helpful APIs:
|
|
301
|
+
|
|
302
|
+
- `queue.get_waiting_for(parent_id)`
|
|
303
|
+
- `queue.get_waiting_tasks()`
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Dead-letter and recovery
|
|
308
|
+
|
|
309
|
+
### Dead-letter behavior
|
|
310
|
+
|
|
311
|
+
On permanent failure, task IDs are stored in `<task_prefix>:dead_letter` and a snapshot is persisted at `dl:<task_id>`.
|
|
312
|
+
|
|
313
|
+
### Requeue dead-letter task
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
queue.requeue_dead(task_id)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
CLI:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
broccoli dead list
|
|
323
|
+
broccoli dead requeue <task_id>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Recover stalled processing tasks
|
|
327
|
+
|
|
328
|
+
Manual recovery before startup:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
broccoli worker start --type threaded --recover-stalled 600
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Automatic startup recovery (default enabled):
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
broccoli worker start --type threaded --recover-on-startup
|
|
338
|
+
broccoli worker start --type threaded --no-recover-on-startup
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## CLI reference
|
|
344
|
+
|
|
345
|
+
### Global
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
broccoli -v ...
|
|
349
|
+
broccoli -vv ...
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Worker startup
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
broccoli worker start --type threaded
|
|
356
|
+
broccoli worker start --type async --concurrency 20
|
|
357
|
+
broccoli worker start --type hybrid --thread-workers 8 --async-tasks 50
|
|
358
|
+
broccoli worker start --type chain --chain-queue-name chain_tasks:queue
|
|
359
|
+
broccoli worker start --type gpu --gpu-id 0
|
|
360
|
+
broccoli worker start --type threaded --pool --num-workers 4
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Common worker flags:
|
|
364
|
+
|
|
365
|
+
- `--redis-url`
|
|
366
|
+
- `--queue-name`
|
|
367
|
+
- `--chain-queue-name`
|
|
368
|
+
- `--task-prefix`
|
|
369
|
+
- `--worker-id`
|
|
370
|
+
- `--recover-stalled`
|
|
371
|
+
- `--recover-stalled-timeout`
|
|
372
|
+
- `--recover-on-startup` / `--no-recover-on-startup`
|
|
373
|
+
- `--decode-responses` / `--no-decode-responses`
|
|
374
|
+
- `--redis-socket-timeout`
|
|
375
|
+
- `--redis-socket-connect-timeout`
|
|
376
|
+
- `--redis-health-check-interval`
|
|
377
|
+
- `--redis-retry-on-timeout` / `--no-redis-retry-on-timeout`
|
|
378
|
+
- `--redis-max-connections`
|
|
379
|
+
|
|
380
|
+
### Queue inspection
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
broccoli queue stats --format table
|
|
384
|
+
broccoli queue stats --format json
|
|
385
|
+
broccoli queue list --status pending --limit 20
|
|
386
|
+
broccoli queue get <task_id>
|
|
387
|
+
broccoli queue waiting <parent_id>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Dead-letter operations
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
broccoli dead list
|
|
394
|
+
broccoli dead list --format json
|
|
395
|
+
broccoli dead requeue <task_id>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Chain inspection
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
broccoli chain status <chain_id>
|
|
402
|
+
broccoli chain tasks <chain_id>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Health
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
broccoli health
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Environment variables
|
|
414
|
+
|
|
415
|
+
Broccoli CLI defaults can come from environment variables:
|
|
416
|
+
|
|
417
|
+
- `BROCCOLI_REDIS_URL` (default: `redis://localhost:6379`)
|
|
418
|
+
- `BROCCOLI_QUEUE_NAME` (default: `tasks:queue`)
|
|
419
|
+
- `BROCCOLI_CHAIN_QUEUE_NAME` (default: `chain_tasks:queue`)
|
|
420
|
+
- `BROCCOLI_TASK_PREFIX` (default: `task`)
|
|
421
|
+
- `BROCCOLI_REDIS_DECODE_RESPONSES` (default: `true`)
|
|
422
|
+
- `BROCCOLI_REDIS_SOCKET_TIMEOUT` (optional, seconds)
|
|
423
|
+
- `BROCCOLI_REDIS_SOCKET_CONNECT_TIMEOUT` (optional, seconds)
|
|
424
|
+
- `BROCCOLI_REDIS_HEALTH_CHECK_INTERVAL` (default: `30`)
|
|
425
|
+
- `BROCCOLI_REDIS_RETRY_ON_TIMEOUT` (default: `true`)
|
|
426
|
+
- `BROCCOLI_REDIS_MAX_CONNECTIONS` (optional)
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Programmatic API reference
|
|
431
|
+
|
|
432
|
+
### TaskQueue
|
|
433
|
+
|
|
434
|
+
- constructor: `TaskQueue(redis_url=..., queue_name=..., task_prefix=..., decode_responses=True, redis_config={...})`
|
|
435
|
+
- `push(task, priority=0) -> task_id`
|
|
436
|
+
- `pop() -> Task | None`
|
|
437
|
+
- `complete(task)`
|
|
438
|
+
- `fail(task)`
|
|
439
|
+
- `requeue(task_id, priority=None)`
|
|
440
|
+
- `requeue_dead(task_id) -> bool`
|
|
441
|
+
- `recover_stalled(timeout_seconds=3600) -> int`
|
|
442
|
+
- `get_task(task_id) -> Task | None`
|
|
443
|
+
- `stats() -> dict`
|
|
444
|
+
- `processing_stats() -> dict`
|
|
445
|
+
- `get_waiting_for(task_id) -> list[str]`
|
|
446
|
+
- `get_waiting_tasks() -> list[str]`
|
|
447
|
+
- `is_fully_drained() -> bool`
|
|
448
|
+
|
|
449
|
+
### TaskChain
|
|
450
|
+
|
|
451
|
+
- `chain(tasks, shared_payload=None, completion_task=None, completion_payload=None) -> chain_id`
|
|
452
|
+
- `get_chain_status(chain_id) -> dict`
|
|
453
|
+
|
|
454
|
+
### Worker hooks
|
|
455
|
+
|
|
456
|
+
All workers support lifecycle hooks:
|
|
457
|
+
|
|
458
|
+
- `add_completion_handler(handler)`
|
|
459
|
+
- `add_failure_handler(handler)`
|
|
460
|
+
- `add_pre_process_handler(handler)`
|
|
461
|
+
- `add_post_process_handler(handler)`
|
|
462
|
+
- decorator aliases: `on_complete`, `on_failure`, `on_pre_process`, `on_post_process`
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Operational guidance
|
|
467
|
+
|
|
468
|
+
### 1. Use shared registration modules
|
|
469
|
+
|
|
470
|
+
Keep task registrations in one importable module and register into each worker instance at startup.
|
|
471
|
+
|
|
472
|
+
### 2. Prefer idempotent handlers
|
|
473
|
+
|
|
474
|
+
Retries can execute handlers multiple times. Side effects should be safe to repeat.
|
|
475
|
+
|
|
476
|
+
### 3. Set realistic timeout and recovery values
|
|
477
|
+
|
|
478
|
+
Choose `recover_stalled_timeout` high enough to avoid recovering long-running but valid tasks.
|
|
479
|
+
|
|
480
|
+
### 4. Monitor dead-letter growth
|
|
481
|
+
|
|
482
|
+
A growing dead-letter set often indicates handler bugs or dependency/data issues.
|
|
483
|
+
|
|
484
|
+
### 5. Separate queues by workload profile
|
|
485
|
+
|
|
486
|
+
Use distinct queue names/prefixes for high-latency jobs, chain jobs, or tenant isolation.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Troubleshooting
|
|
491
|
+
|
|
492
|
+
### Worker starts but does nothing
|
|
493
|
+
|
|
494
|
+
- Ensure tasks are pushed to the same `queue_name` the worker consumes.
|
|
495
|
+
- Verify `task_prefix` alignment between producer and worker.
|
|
496
|
+
- Confirm handlers are registered for every `task_type`.
|
|
497
|
+
|
|
498
|
+
### `No handler registered for task type`
|
|
499
|
+
|
|
500
|
+
- Register the handler in the same process that runs the worker.
|
|
501
|
+
- Import your registration module before calling `worker.start()`.
|
|
502
|
+
|
|
503
|
+
### Tasks remain `waiting`
|
|
504
|
+
|
|
505
|
+
- Check parent task status with `broccoli queue get <parent_id>`.
|
|
506
|
+
- Confirm parent was completed (not failed/deleted).
|
|
507
|
+
|
|
508
|
+
### Stuck processing tasks after crash
|
|
509
|
+
|
|
510
|
+
- Enable startup recovery or run `--recover-stalled` manually.
|
|
511
|
+
|
|
512
|
+
### Redis connectivity errors
|
|
513
|
+
|
|
514
|
+
- Run `broccoli health`.
|
|
515
|
+
- Verify Redis URL/auth/network.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Development
|
|
520
|
+
|
|
521
|
+
Install development dependencies:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
pip install -e .[dev]
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Run tests:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
pytest
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Format/lint/type-check tools are configured in `pyproject.toml` (`black`, `isort`, `flake8`, `mypy`).
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## License
|
|
538
|
+
|
|
539
|
+
MIT License. See `/home/runner/work/Broccoli/Broccoli/LICENSE`.
|