You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: named queues with concurrency caps, backpressure, and live config updates
- QueueConfig(concurrency, max_size) for per-queue limits
- queue= on task decorator and add_task() for routing
- QueueFullError when max_size is reached
- REJECTED status for tasks blocked by queue policy
- queue_stats() and update_queue_config() on TaskManager
- Fan-out peer endpoint for multi-instance task aggregation
- Dashboard: Queues tab, Queue column, Queue filter, dark mode
- Docs and README updated for all new features
Copy file name to clipboardExpand all lines: README.md
+49-1Lines changed: 49 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -79,7 +79,7 @@ The route signature does not change. Tasks that fail are retried. If the server
79
79
## Features
80
80
81
81
- Automatic retries with configurable delay and exponential backoff
82
-
- Task IDs and full lifecycle tracking: `PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `INTERRUPTED`
82
+
- Task IDs and full lifecycle tracking: `PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `INTERRUPTED`, `REJECTED`
83
83
- Live admin dashboard over SSE at `/tasks/dashboard`
84
84
- SQLite persistence out of the box; Redis, PostgreSQL, and MySQL as optional extras
85
85
- Pending task requeue: unfinished tasks at shutdown are re-dispatched on startup
@@ -94,6 +94,7 @@ The route signature does not change. Tasks that fail are retried. If the server
94
94
- Trace context propagation: OpenTelemetry spans flow from the request into background execution (Python 3.11+)
95
95
- Process executor: `executor='process'` routes CPU-bound tasks through a `ProcessPoolExecutor`, bypassing the GIL with true parallel workers
96
96
- Concurrency controls: opt-in semaphore for async tasks, dedicated thread pool for sync tasks, and configurable process worker count
97
+
- Named queues: `QueueConfig(concurrency, max_size)` for independent concurrency caps and backpressure per queue, with `QueueFullError` when a queue is full
97
98
- Priority queues: `priority=` on `@task_manager.task()` or `add_task()`, higher-priority tasks run first, equal-priority tasks are FIFO
98
99
- Eager dispatch: `eager=True` starts a task immediately via `asyncio.create_task` before the HTTP response is sent
99
100
- Scheduled tasks: `@task_manager.schedule(every=)` and `cron=` with distributed lock for multi-instance
|`requeue_on_interrupt`|`bool`|`False`| Requeue this task if it was mid-execution at shutdown. Only set for idempotent tasks. |
167
168
|`eager`|`bool`|`False`| Start the task via `asyncio.create_task` immediately when `add_task()` is called, before the response is sent. Per-call `eager` on `add_task()` overrides this. |
168
169
|`priority`|`int \| None`|`None`| Route through the priority queue. Higher integers run first. Conventional range 1 (lowest) to 10 (highest). Per-call `priority` on `add_task()` overrides this. |
170
+
|`queue`|`str \| None`|`None`| Route this task to a named queue. Per-call `queue` on `add_task()` overrides this. |
171
+
|`executor`|`str \| None`|`None`| Force a specific executor: `"async"`, `"thread"`, or `"process"`. When `None`, the executor is inferred from the function signature. |
169
172
170
173
## Idempotency keys
171
174
@@ -332,6 +335,51 @@ task_id = tasks.add_task(process_item, item_id) # use decorator de
332
335
333
336
Tasks with no priority route through Starlette's normal background task list unchanged.
334
337
338
+
## Named queues
339
+
340
+
Define named queues with independent concurrency caps and backpressure limits using `QueueConfig`. Tasks route to a queue via `queue=` on the decorator or per call on `add_task()`.
When a queue is at its `max_size` limit, `add_task()` raises `QueueFullError` so callers can return a 429 rather than silently growing memory. Unknown queue names fall back to `default` with a warning log.
raise HTTPException(status_code=429, detail="Report queue is full")
374
+
```
375
+
376
+
`queue_stats()` returns per-queue pending, running, and finished counts. `update_queue_config()` changes concurrency and max_size at runtime without a restart.
Set `eager=True` to start a task via `asyncio.create_task` the moment `add_task()` is called, before FastAPI sends the response. Useful for batch endpoints where multiple tasks are added in a single request handler and you want them to run concurrently rather than queued sequentially.
Copy file name to clipboardExpand all lines: docs/api/managed-background-tasks.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,6 +40,7 @@ def add_task(
40
40
tags: dict[str, str] |None=None,
41
41
eager: bool|None=None,
42
42
priority: int|None=None,
43
+
queue: str|None=None,
43
44
**kwargs: Any,
44
45
) -> str
45
46
```
@@ -55,7 +56,8 @@ This overrides `BackgroundTasks.add_task()`, which returns `None`. If you are al
55
56
|`idempotency_key`|`str \| None` | `None` | Deduplication key. If a non-failed task with the same key already exists in the store or backend, its `task_id` is returned and `func` is not enqueued again. |
56
57
| `tags` | `dict[str, str] \| None` | `None` | Key/value labels attached to this task. Forwarded to every `LogEvent` and `LifecycleEvent` emitted for the task. |
57
58
|`eager`|`bool \| None` | `None` | When `True`, dispatch via `asyncio.create_task` immediately rather than waiting for the response to be sent. Overrides the decorator-level `eager` setting for this call only. |
58
-
| `priority` | `int \| None` | `None` | Route through the priority queue instead of Starlette's background task list. Higher values run first; the conventional range is 1 (lowest) to 10 (highest). Overrides the decorator-level `priority` for this call only. Mutually exclusive with `eager`: when `priority` is set, `eager` is ignored. |
59
+
| `priority` | `int \| None` | `None` | Route through the priority queue instead of Starlette's background task list. Higher values run first; the conventional range is 1 (lowest) to 10 (highest). Overrides the decorator-level `priority` for this call only. When named queues are active, controls ordering within the target queue's heap. Mutually exclusive with `eager`: when `priority` is set, `eager` is ignored. |
60
+
|`queue`|`str \| None` | `None` | Named queue to route this task into. Overrides the decorator-level `queue` for this call only. Only effective when the named queue system is active (any `queues=` or `max_size=` argument was passed to `TaskManager`). Raises `QueueFullError` if the target queue is at its `max_size` limit. |
Copy file name to clipboardExpand all lines: docs/api/models.md
+47-2Lines changed: 47 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,6 +20,7 @@ class TaskStatus(str, Enum):
20
20
FAILED="failed"
21
21
INTERRUPTED="interrupted"
22
22
CANCELLED="cancelled"
23
+
REJECTED="rejected"
23
24
```
24
25
25
26
| Value | Meaning |
@@ -30,6 +31,7 @@ class TaskStatus(str, Enum):
30
31
|`FAILED`| Task raised an exception on its final attempt after all retries were exhausted. Terminal. |
31
32
|`INTERRUPTED`| Task was mid-execution when the app shut down and `requeue_on_interrupt` was not enabled. Saved to history, visible in the dashboard, and not re-executed automatically. Can be retried via `POST /tasks/{task_id}/retry`. Terminal. |
32
33
|`CANCELLED`| Task was cancelled before execution via `POST /tasks/{task_id}/cancel`. Terminal; the task will not run. |
34
+
|`REJECTED`| The target named queue was at its `max_size` limit when `add_task()` was called. The task record is created with this status and `QueueFullError` is raised. Only occurs when the named queue system is active with a `max_size` limit. Can be retried via `POST /tasks/{task_id}/retry`. Terminal. |
33
35
34
36
---
35
37
@@ -64,6 +66,7 @@ class TaskRecord:
64
66
source: str
65
67
priority: int|None
66
68
executor: str|None
69
+
queue: str
67
70
```
68
71
69
72
### Fields
@@ -88,8 +91,9 @@ Fields marked **auto** are set by the framework. Fields marked **caller** are pr
88
91
|`tags`|`dict[str, str]`| caller | Key/value labels attached at enqueue time. Forwarded to every `LogEvent` and `LifecycleEvent`. Stored as part of the snapshot payload, not as a separate column. |
89
92
|`encrypted_payload`|`bytes \| None`| auto | Fernet-encrypted `(args, kwargs)` when `encrypt_args_key` is configured on `TaskManager`. When present, `args` and `kwargs` are stored empty. Not included in `to_dict()` output or API responses. |
90
93
|`source`|`str`| auto | How the task was created: `"manual"` for tasks enqueued via `add_task()`, `"scheduled"` for tasks fired by the periodic scheduler. |
91
-
|`priority`|`int \| None`| caller | Priority level assigned at enqueue time. `None` when routed through the standard Starlette mechanism. Any integer when routed through the priority queue; higher values run first. |
94
+
|`priority`|`int \| None`| caller | Priority level assigned at enqueue time. `None` when routed through the standard Starlette mechanism. Any integer when routed through the priority queue or named queue heap; higher values run first. |
92
95
|`executor`|`str \| None`| auto | The executor that ran (or will run) this task: `"async"`, `"thread"`, or `"process"`. Reflects the effective executor after auto-detection. Shown in the dashboard detail panel. |
96
+
|`queue`|`str`| auto | The named queue this task was routed into. Always `"default"` when the named queue system is not active. Set from the per-call `queue=` argument, then the decorator-level `queue=`, then `"default"`. |
93
97
94
98
### Properties
95
99
@@ -179,11 +183,52 @@ class TaskConfig:
179
183
|`name`|`str \| None`|`None`| Display name in logs and the dashboard. Defaults to the function's `__name__`. |
180
184
|`requeue_on_interrupt`|`bool`|`False`| When `True`, a task interrupted at shutdown is reset to `PENDING` and re-dispatched on next startup. Only safe for idempotent tasks. |
181
185
|`eager`|`bool`|`False`| Dispatch via `asyncio.create_task` immediately when `add_task()` is called rather than after the response is sent. |
182
-
|`priority`|`int \| None`|`None`| Execution priority. `None` routes through the standard Starlette mechanism. Any integer routes through the priority queue; higher values run first. |
186
+
|`priority`|`int \| None`|`None`| Execution priority. `None` routes through the standard Starlette mechanism. Any integer routes through the priority queue or named queue heap; higher values run first. |
187
+
|`queue`|`str \| None`|`None`| Named queue this function is routed into by default. `None` routes to `"default"` when the named queue system is active. |
183
188
|`executor`|`str \| None`|`None`| Configured executor. `"async"`, `"thread"`, or `"process"`. `None` means auto-detect from the function signature at dispatch time. |
184
189
185
190
---
186
191
192
+
## QueueConfig
193
+
194
+
`QueueConfig` holds the configuration for a single named queue. Pass instances of this in the `queues` dict when constructing `TaskManager`.
195
+
196
+
```python
197
+
from fastapi_taskflow.models import QueueConfig
198
+
```
199
+
200
+
```python
201
+
@dataclass
202
+
classQueueConfig:
203
+
concurrency: int|None=None
204
+
max_size: int|None=None
205
+
```
206
+
207
+
| Field | Type | Default | Description |
208
+
|-------|------|---------|-------------|
209
+
|`concurrency`|`int \| None`|`None`| Maximum number of tasks from this queue that may run concurrently. When the limit is reached, the queue drainer blocks until a slot is released. `None` removes the limit. |
210
+
|`max_size`|`int \| None`|`None`| Maximum number of tasks allowed to wait in this queue's heap. When the heap is full, `add_task()` creates a `REJECTED` task record and raises `QueueFullError`. `None` removes the limit. |
211
+
212
+
`QueueConfig` fields can be updated at runtime via `PATCH /tasks/queues/{name}` or `TaskManager.update_queue_config()`. Changes take effect immediately for new tasks; in-flight tasks are not affected. Updated values are persisted to the backend and restored on the next startup.
Copy file name to clipboardExpand all lines: docs/api/task-manager.md
+33-1Lines changed: 33 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,13 @@ TaskManager(
37
37
max_process_workers: int|None=None,
38
38
process_shutdown_timeout: float=30.0,
39
39
retention_days: float|None=None,
40
+
retry_replaces_original: bool=True,
41
+
queues: dict[str, QueueConfig] |None=None,
42
+
max_size: int|None=None,
43
+
instance_url: str|None=None,
44
+
instance_tasks_prefix: str="",
45
+
registry_ttl: int=90,
46
+
registry_heartbeat: int=30,
40
47
)
41
48
```
42
49
@@ -77,6 +84,28 @@ TaskManager(
77
84
|-----------|------|---------|-------------|
78
85
|`encrypt_args_key`|`bytes \| str \| None`|`None`| A Fernet key for encrypting task args and kwargs at rest. When set, arguments are encrypted at enqueue time and decrypted only when the executor is about to call the function. Accepts a URL-safe base64 string or raw bytes from `Fernet.generate_key()`. Requires `pip install "fastapi-taskflow[encryption]"`. |
79
86
87
+
### Retry parameter
88
+
89
+
| Parameter | Type | Default | Description |
90
+
|-----------|------|---------|-------------|
91
+
|`retry_replaces_original`|`bool`|`True`| When `True`, retrying a task via the API (single retry, bulk retry, or timed retry) removes the original record from the in-memory store and backend after the new task is dispatched. The dashboard and history show only the new run. When `False`, both the original and the new task record are kept. Applies to all retry paths regardless of whether named queues are active. |
92
+
93
+
### Named queue parameters
94
+
95
+
| Parameter | Type | Default | Description |
96
+
|-----------|------|---------|-------------|
97
+
|`queues`|`dict[str, QueueConfig] \| None`|`None`| Named queues with individual concurrency and backpressure settings. When provided, tasks are routed through the named queue system. A `"default"` queue is created automatically if not included. |
98
+
|`max_size`|`int \| None`|`None`| Backpressure limit for the implicit `"default"` queue. When the default queue reaches this many tasks pending, `add_task()` raises `QueueFullError`. Activates the named queue system even when `queues` is not provided. |
99
+
100
+
### Multi-instance parameters
101
+
102
+
| Parameter | Type | Default | Description |
103
+
|-----------|------|---------|-------------|
104
+
|`instance_url`|`str \| None`|`None`| Public base URL of this instance (for example `"http://10.0.0.1:8000"`). When set alongside a backend that supports `save_metadata`/`load_metadata` (SQLite, Redis, Postgres, MySQL), this instance registers itself so peers can fan out to it. No registration or fan-out occurs when `None`. |
105
+
|`instance_tasks_prefix`|`str`|`""`| URL prefix where the tasks router is mounted on this instance (for example `"/api/tasks"`). Peers append this to `instance_url` when building the fan-out URL. Must match the prefix used when mounting `TaskAdmin` or the router. |
106
+
|`registry_ttl`|`int`|`90`| Seconds before a peer registry entry is considered stale and excluded from fan-out. Should be at least `2 * registry_heartbeat`. |
107
+
|`registry_heartbeat`|`int`|`30`| Seconds between heartbeat writes that keep this instance's registry entry fresh. |
108
+
80
109
---
81
110
82
111
## Decorators
@@ -95,6 +124,7 @@ Registers a function as a managed background task. The decorated function is ret
|`name`|`str \| None`| function name | Override the display name in logs and the dashboard. |
111
141
|`requeue_on_interrupt`|`bool`|`False`| When `True` and `requeue_pending=True` on the manager, a task interrupted at shutdown is reset to `PENDING` and re-dispatched on next startup. Only set this for idempotent functions that are safe to restart from scratch. |
112
142
|`eager`|`bool`|`False`| Dispatch via `asyncio.create_task` immediately when `add_task()` is called, before FastAPI sends the response. Per-call `eager` on `add_task()` overrides this value. |
113
-
|`priority`|`int \| None`|`None`| Route through the priority queue instead of Starlette's background task list. Higher values run first. The conventional range is 1 (lowest) to 10 (highest). Per-call `priority` on `add_task()` overrides this value. |
143
+
|`priority`|`int \| None`|`None`| Route through the priority queue instead of Starlette's background task list. Higher values run first. The conventional range is 1 (lowest) to 10 (highest). Per-call `priority` on `add_task()` overrides this value. When named queues are active, controls ordering within the target queue's heap. |
144
+
|`queue`|`str \| None`|`None`| Named queue to route this function into. Requires `queues=` on `TaskManager`. Per-call `queue` on `add_task()` overrides this value. Tasks routed to an unknown queue fall back to `"default"` with a warning. |
114
145
|`executor`|`"async" \| "thread" \| "process" \| None`|`None`| Force a specific executor. `"async"` requires a coroutine. `"thread"` requires a plain function. `"process"` routes to a `ProcessPoolExecutor` and requires a module-level function with picklable arguments. `None` auto-detects from the function signature. |
115
146
116
147
Raises `ValueError` at decoration time if the function is incompatible with the requested executor (for example, `executor='async'` on a sync function, or `executor='process'` on a lambda or nested function).
|`name`|`str \| None`| function name | Override the display name in logs and the dashboard. |
156
187
|`run_on_startup`|`bool`|`False`| When `True`, fire the task on the first scheduler tick immediately after startup, rather than waiting for the first interval or cron slot. |
157
188
|`timezone`|`str`|`"UTC"`| IANA timezone name used when evaluating `cron` expressions (for example `"America/New_York"`). Ignored when `every` is used. |
189
+
|`queue`|`str \| None`|`None`| Named queue to route each periodic firing into. When the named queue system is active, the task is subject to that queue's concurrency limit and backpressure. Defaults to `"default"`. |
158
190
|`executor`|`"async" \| "thread" \| "process" \| None`|`None`| Force a specific executor for each firing. Same constraints as on `@task()`. `None` auto-detects from the function signature. |
159
191
160
192
Raises `ValueError` if neither or both of `every` and `cron` are provided, or if `executor` is incompatible with the function. Raises `ImportError` if `cron` is used and `croniter` is not installed.
0 commit comments