kickdown 0.4.0a1__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.
- kickdown-0.4.0a1/.claude/settings.local.json +7 -0
- kickdown-0.4.0a1/.github/CODEOWNERS +1 -0
- kickdown-0.4.0a1/.github/workflows/ci.yml +35 -0
- kickdown-0.4.0a1/.gitignore +19 -0
- kickdown-0.4.0a1/.python-version +1 -0
- kickdown-0.4.0a1/.vscode/settings.json +11 -0
- kickdown-0.4.0a1/CHANGELOG.md +138 -0
- kickdown-0.4.0a1/LICENSE +21 -0
- kickdown-0.4.0a1/PKG-INFO +273 -0
- kickdown-0.4.0a1/README.md +244 -0
- kickdown-0.4.0a1/docker-compose.yml +12 -0
- kickdown-0.4.0a1/kickdown/__init__.py +13 -0
- kickdown-0.4.0a1/kickdown/client.py +32 -0
- kickdown-0.4.0a1/kickdown/consumer.py +144 -0
- kickdown-0.4.0a1/kickdown/log.py +14 -0
- kickdown-0.4.0a1/kickdown/models.py +26 -0
- kickdown-0.4.0a1/kickdown/py.typed +0 -0
- kickdown-0.4.0a1/kickdown/queue.py +60 -0
- kickdown-0.4.0a1/kickdown/scheduler.py +47 -0
- kickdown-0.4.0a1/kickdown/server.py +151 -0
- kickdown-0.4.0a1/kickdown/store.py +162 -0
- kickdown-0.4.0a1/kickdown/web/__init__.py +98 -0
- kickdown-0.4.0a1/kickdown/web/admin.html +45 -0
- kickdown-0.4.0a1/kickdown/web/assets/admin.css +95 -0
- kickdown-0.4.0a1/kickdown/web/assets/admin.js +1 -0
- kickdown-0.4.0a1/pyproject.toml +55 -0
- kickdown-0.4.0a1/tests/test_client.py +78 -0
- kickdown-0.4.0a1/tests/test_consumer.py +322 -0
- kickdown-0.4.0a1/tests/test_queue.py +81 -0
- kickdown-0.4.0a1/tests/test_scheduler.py +99 -0
- kickdown-0.4.0a1/tests/test_server.py +228 -0
- kickdown-0.4.0a1/tests/test_web.py +136 -0
- kickdown-0.4.0a1/uv.lock +371 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @vklokov
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: PR Verify
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
types:
|
|
5
|
+
- opened
|
|
6
|
+
- reopened
|
|
7
|
+
- synchronize
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
|
|
19
|
+
- name: Run Ruff
|
|
20
|
+
uses: astral-sh/ruff-action@4919ec5cf1f49eff0871dbcea0da843445b837e6 #v3.6.1
|
|
21
|
+
|
|
22
|
+
- name: Run ty
|
|
23
|
+
run: uv run ty check
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
- name: Run Pytest
|
|
35
|
+
run: uv run pytest -q
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.tabSize": 2,
|
|
3
|
+
"editor.insertSpaces": true,
|
|
4
|
+
"editor.detectIndentation": true,
|
|
5
|
+
"editor.renderWhitespace": "all",
|
|
6
|
+
"editor.formatOnSave": true,
|
|
7
|
+
"python.defaultInterpreterPath": "./.venv/bin/python",
|
|
8
|
+
"[python]": {
|
|
9
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
10
|
+
},
|
|
11
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 - 2026-09-17
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Rewritten around an arbitrary number of queues instead of a single configured queue
|
|
7
|
+
- Workers (`Performable`) now declare their own `queue` and `operation`; the server derives the set of polled queues from the registered workers automatically
|
|
8
|
+
- Queues are polled in round-robin order with equal frequency — no priority between queues at this stage
|
|
9
|
+
- Workers are keyed by `(queue, operation)`, so the same `operation` name can be reused safely across different queues
|
|
10
|
+
- `Client.enqueue` now takes a single `Task` (carrying `queue`, `operation`, `params`, `jid`, `retry_count`) instead of a worker class and payload dict
|
|
11
|
+
- `Client.pending(queue)` / `Client.stats(queue)` moved onto the queue handle: `client.queue(name).pending()` / `.stats()`
|
|
12
|
+
- `Client` is now fully async (`enqueue`, `pending`, `close`), and supports `async with` instead of a sync context manager
|
|
13
|
+
- `Server` no longer takes a `Config` object; it's constructed directly with `redis_url` and `concurrency`
|
|
14
|
+
- Retries use a hardcoded exponential backoff (`1s * 1.5 ** attempt`) instead of a per-task `backoff_coefficient` — a failed task is retried with `retry_count` decremented and `attempt` incremented until `retry_count` reaches `0`
|
|
15
|
+
- Retries are no longer awaited in memory: the failed task is stored in a scheduled sorted set in Redis and picked up when due, so it survives a process restart and does not hold a concurrency slot for the duration of the delay
|
|
16
|
+
- On shutdown, the server now waits for in-flight tasks (including ones mid-retry) to finish before closing the Redis connection
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- A default logger (plain `logging.Logger`, text output to stdout) on `Client` and `Server` — no custom logger interface/protocol; swap it by assigning `server.logger = ...` / `client.logger = ...`
|
|
20
|
+
- `Client` logs when a task is accepted (enqueued)
|
|
21
|
+
- `Server` logs process start/shutdown (with the polled queues and concurrency), each task's start/completion, retries, permanent failures, and unrecognized operations — all messages include the task's `jid`
|
|
22
|
+
- `Server` always starts a small HTTP server (`web_port`, default `3030`) with `/live` and `/ready` for liveness/readiness probes; `/ready` checks Redis connectivity
|
|
23
|
+
- Per-queue `processed`/`failed` counters (`Stats` model) via `client.queue(name).stats()`; the consumer increments them on task completion, permanent failure (retries exhausted), and unrecognized operations
|
|
24
|
+
- `GET /admin` renders an HTML dashboard: total processed/failed/scheduled counters, and a per-queue table of pending/scheduled/in-flight counts (in-flight is a placeholder until tasks are tracked while they run); optionally gated behind HTTP Basic Auth via `Server(admin_username=..., admin_password=...)` (no auth if either is left unset)
|
|
25
|
+
- `Queue.purge()` drops every pending task in a queue and returns how many were dropped (scheduled tasks and counters are untouched); not exposed over HTTP
|
|
26
|
+
- `Server.enqueue(task)` pushes a task using the server's own Redis connection, so workers/hooks can schedule tasks without a separate `Client`
|
|
27
|
+
- A `Scheduler` loop runs inside every `Server`, moving due tasks from each queue's `kickdown:scheduled:{name}` set back into the queue (atomically, via a Lua script); a server only sweeps the queues it has workers for
|
|
28
|
+
- `Queue`, a handle bound to one queue name, carrying every per-queue operation (`push`, `pending`, `length`, `scheduled`, `stats`, `purge`, counters); obtained via `Client.queue(name)` / `Server.queue(name)`, and `Server.queues` now returns `Queue` objects instead of names
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
- The old `Loggable` protocol and the custom `Logger` wrapper class (replaced by the stdlib logger above)
|
|
32
|
+
- `Server.uptime()` / `Status` and the `humanize` dependency
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 0.3.3 - 2026-06-19
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Pinned `redis` to the 7.x line (`~=7.4`) to avoid an unintended upgrade to `redis` 8.x; previously `>=7.4.0` allowed any newer major version
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 0.3.2 - 2026-06-03
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- Job execution duration is now tracked and logged (in seconds) on job completion
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 0.3.1 - 2026-05-26
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- Lifecycle hooks: `Server.on_startup` and `Server.on_shutdown` register async callbacks for server startup and shutdown
|
|
54
|
+
- Usable as decorators or called directly with a callable argument
|
|
55
|
+
- Startup hooks run after the Redis connection is verified, before the consumer starts
|
|
56
|
+
- Shutdown hooks run after the consumer is stopped, before the Redis connection is closed
|
|
57
|
+
- Exceptions in hooks are logged and do not crash the server
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 0.3.0 - 2026-05-21
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- Retry mechanism for failed jobs
|
|
65
|
+
- `Client.enqueue` accepts `retry_count` (default: `1`) and `backoff_coefficient` (default: `1.5`)
|
|
66
|
+
- Both values are stored on the `Job` model and travel with the job through the queue
|
|
67
|
+
- On failure, the consumer re-enqueues the job with an exponential backoff delay (`backoff_coefficient ** attempt` seconds) until retries are exhausted
|
|
68
|
+
- Once all retries are exhausted the job is marked failed as before
|
|
69
|
+
- Jobs log a warning on each retry attempt and an error only when permanently failed
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 0.2.8 - 2026-05-20
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- `Server.add_worker` now accepts a worker **instance** (`Performable`) — the same instance is reused across all jobs of that type
|
|
77
|
+
- `Client.enqueue` accepts a worker **class** (`type[Performable]`) — no instantiation cost at enqueue time, only the class name is needed to build the job
|
|
78
|
+
- Healthcheck rewritten with FastAPI + uvicorn, replacing the manual raw TCP/HTTP server
|
|
79
|
+
- `GET /live` — always returns 200, used as a liveness probe
|
|
80
|
+
- `GET /ready` — checks consumer heartbeat and Redis connectivity (`store.ping`); returns 503 with reason on failure
|
|
81
|
+
- Job IDs switched from `uuid4` (stdlib) to `uuid7` (`uuid_extensions`), providing time-sortable identifiers
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
- `config.queue` was logged as a bound method object; now called correctly as `config.queue()`
|
|
85
|
+
|
|
86
|
+
### Dependencies
|
|
87
|
+
- Added `fastapi>=0.136.1`
|
|
88
|
+
- Added `uvicorn>=0.34.0`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 0.2.6 - 2026-05-20
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- All Redis operations extracted into a new internal `Store` class (`kickdown/store.py`)
|
|
96
|
+
- Redis key names (`kickdown:queue:*`, `kickdown:stats:*`) are now centralized in `Store`
|
|
97
|
+
- `Consumer` and `Client` no longer interact with Redis directly — all calls go through `Store`
|
|
98
|
+
- `asyncio.to_thread` wrapping for blocking Redis calls moved from `Consumer` into `Store`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 0.2.5 - 2026-05-20
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
- Workers are now registered on `Server` via `add_worker()` instead of being passed to `Config`
|
|
106
|
+
- `Server` lazily initializes the consumer on `start()`, decoupling construction from Redis connection
|
|
107
|
+
- `Consumer` receives workers directly rather than reading them from `Config`
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
- `Server.uptime()` returns a human-readable duration (e.g. `"1 day, 4 hours"`) via the `humanize` library
|
|
111
|
+
- `Status` response includes an `uptime` field
|
|
112
|
+
- `Server.start()` raises `RuntimeError` if no workers have been registered
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
- `Consumer.is_ok` converted from a method to a property, fixing incorrect truthy evaluation in `status()`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 0.2.4 — 2026-05-19
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
- Queue names are now raw identifiers (e.g. `"default"`, `"emails"`); the full Redis key is constructed internally as `kickdown:queue:{name}`
|
|
123
|
+
- Stats counters moved from a Redis hash to separate keys (`kickdown:stats:processed`, `kickdown:stats:failed`), incremented with `INCR`
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 0.2.3 — 2026-05-19
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
- `Client.stats()` returns a `Stats` model with cumulative `processed` and `failed` job counts
|
|
131
|
+
- Consumer increments `kickdown.stats.processed` / `kickdown.stats.failed` in Redis after each job completes or raises
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 0.2.2 — 2026-05-19
|
|
136
|
+
|
|
137
|
+
### Added
|
|
138
|
+
- `Client.pending()` returns a list of jobs currently waiting in the queue (non-destructive)
|
kickdown-0.4.0a1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladimir Klokov
|
|
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,273 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kickdown
|
|
3
|
+
Version: 0.4.0a1
|
|
4
|
+
Summary: A Redis-backed background job queue for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/vklokov/kickdown
|
|
6
|
+
Project-URL: Repository, https://github.com/vklokov/kickdown
|
|
7
|
+
Project-URL: Issues, https://github.com/vklokov/kickdown/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/vklokov/kickdown/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Vladimir Klokov <klokov.dev@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asyncio,background,jobs,queue,redis,tasks,worker
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: fastapi>=0.136.1
|
|
24
|
+
Requires-Dist: pydantic>=2.13.4
|
|
25
|
+
Requires-Dist: redis~=7.4
|
|
26
|
+
Requires-Dist: uuid7>=0.1.0
|
|
27
|
+
Requires-Dist: uvicorn>=0.34.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# kickdown
|
|
31
|
+
|
|
32
|
+
A Redis-backed background job queue for Python.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Defining a worker
|
|
37
|
+
|
|
38
|
+
Implement the `Performable` protocol: declare which queue a worker consumes
|
|
39
|
+
from and which `operation` name identifies it, plus the async `perform` method.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
class SendEmailWorker:
|
|
43
|
+
queue = "emails"
|
|
44
|
+
operation = "send_email"
|
|
45
|
+
|
|
46
|
+
async def perform(self, payload: dict) -> None:
|
|
47
|
+
recipient = payload["to"]
|
|
48
|
+
# ... send email
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Any number of queues is supported — a worker's `queue` attribute is what
|
|
52
|
+
determines which Redis list it consumes from. The server automatically polls
|
|
53
|
+
every queue that has at least one registered worker.
|
|
54
|
+
|
|
55
|
+
### Running the server
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import asyncio
|
|
59
|
+
from kickdown import Server
|
|
60
|
+
from your_workers import SendEmailWorker, ExportReportWorker
|
|
61
|
+
|
|
62
|
+
server = Server(
|
|
63
|
+
redis_url="redis://localhost:6379",
|
|
64
|
+
concurrency=5, # optional, default: 1 - max tasks processed concurrently
|
|
65
|
+
)
|
|
66
|
+
server.add_workers(SendEmailWorker(), ExportReportWorker())
|
|
67
|
+
|
|
68
|
+
asyncio.run(server.run())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`add_workers` accepts any number of `Performable` instances. Workers are
|
|
72
|
+
resolved by their `(queue, operation)` pair, so the same `operation` name can
|
|
73
|
+
be reused safely across different queues.
|
|
74
|
+
|
|
75
|
+
Queues are polled with equal frequency in round-robin order — there is
|
|
76
|
+
currently no notion of priority between queues.
|
|
77
|
+
|
|
78
|
+
### Enqueueing jobs
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from kickdown import Client, Task
|
|
82
|
+
|
|
83
|
+
client = Client(redis_url="redis://localhost:6379")
|
|
84
|
+
|
|
85
|
+
task = Task(
|
|
86
|
+
queue="emails",
|
|
87
|
+
operation="send_email",
|
|
88
|
+
params={"to": "user@example.com"},
|
|
89
|
+
)
|
|
90
|
+
jid = await client.enqueue(task)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`enqueue` returns the job ID (`jid`) that can be used for tracing. It is
|
|
94
|
+
generated automatically (a time-sortable `uuid7`) if not set explicitly on
|
|
95
|
+
the `Task`.
|
|
96
|
+
|
|
97
|
+
`Client` can also be used as an async context manager, which closes the
|
|
98
|
+
underlying Redis connection on exit:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
async with Client(redis_url="redis://localhost:6379") as client:
|
|
102
|
+
await client.enqueue(task)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`Server` can enqueue tasks too, using the same Redis connection — handy for
|
|
106
|
+
a worker that needs to schedule a follow-up task, or for enqueueing from a
|
|
107
|
+
startup hook, without opening a separate `Client`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
jid = await server.enqueue(task)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Retries
|
|
114
|
+
|
|
115
|
+
`retry_count` (default `1`) on `Task` sets how many times a failed task is
|
|
116
|
+
retried before being dropped. On failure, the consumer re-enqueues the task
|
|
117
|
+
with `retry_count` decremented by one and `attempt` incremented by one, after
|
|
118
|
+
an exponentially growing delay. Once `retry_count` reaches `0` the task is
|
|
119
|
+
dropped.
|
|
120
|
+
|
|
121
|
+
The delay is `1s * 1.5 ** attempt` — both the base delay and the backoff
|
|
122
|
+
coefficient are fixed in the library and cannot be configured per task:
|
|
123
|
+
|
|
124
|
+
| Retry | Delay |
|
|
125
|
+
| ----- | ----- |
|
|
126
|
+
| 1st | 1.0s |
|
|
127
|
+
| 2nd | 1.5s |
|
|
128
|
+
| 3rd | 2.3s |
|
|
129
|
+
|
|
130
|
+
A retried task is not held in memory while it waits: it goes into the queue's
|
|
131
|
+
scheduled sorted set in Redis (`kickdown:scheduled:{name}`, scored by its due
|
|
132
|
+
timestamp), and a scheduler loop running inside every server moves due tasks
|
|
133
|
+
back into the queue. So a retry survives a process restart, and the
|
|
134
|
+
concurrency slot is freed immediately instead of being blocked for the whole
|
|
135
|
+
delay.
|
|
136
|
+
|
|
137
|
+
A server only sweeps the queues it has workers for, which is also the only
|
|
138
|
+
place its own retries can land.
|
|
139
|
+
|
|
140
|
+
Note that a task is still lost if the process dies *while its worker is
|
|
141
|
+
running* — closing that window is the next step (an in-flight list per
|
|
142
|
+
consumer plus a reaper).
|
|
143
|
+
|
|
144
|
+
Queue names are raw identifiers (e.g. `"default"`, `"emails"`). The client
|
|
145
|
+
constructs the full Redis key internally as `kickdown:queue:{name}`.
|
|
146
|
+
|
|
147
|
+
### Inspecting a queue
|
|
148
|
+
|
|
149
|
+
`client.queue(name)` returns a `Queue` handle — every per-queue operation
|
|
150
|
+
lives on it, so the queue name is given once instead of on every call:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
emails = client.queue("emails")
|
|
154
|
+
|
|
155
|
+
# Tasks waiting to be processed (non-destructive)
|
|
156
|
+
tasks = await emails.pending()
|
|
157
|
+
count = await emails.length()
|
|
158
|
+
|
|
159
|
+
# Tasks waiting for their retry delay to elapse
|
|
160
|
+
retries = await emails.scheduled()
|
|
161
|
+
|
|
162
|
+
# Cumulative processed/failed counters
|
|
163
|
+
stats = await emails.stats()
|
|
164
|
+
print(stats.processed, stats.failed)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Enqueueing stays on the client (`client.enqueue(task)`): a `Task` already
|
|
168
|
+
carries its own `queue`, and that field remains the single source of truth
|
|
169
|
+
for routing.
|
|
170
|
+
|
|
171
|
+
`purge()` drops every task waiting in the queue and returns how many were
|
|
172
|
+
dropped. It does not touch scheduled tasks or the counters, and there is no
|
|
173
|
+
undo:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
dropped = await emails.purge()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`processed` counts tasks whose worker completed successfully; `failed`
|
|
180
|
+
counts tasks that were permanently dropped (retries exhausted, or no
|
|
181
|
+
worker registered for the task's `operation`).
|
|
182
|
+
|
|
183
|
+
### Lifecycle hooks
|
|
184
|
+
|
|
185
|
+
Register async callbacks to run on server startup and shutdown — useful for
|
|
186
|
+
initialising shared resources like database pools.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
server = Server(redis_url=...)
|
|
190
|
+
|
|
191
|
+
@server.on_startup
|
|
192
|
+
async def init_db():
|
|
193
|
+
app.db = await asyncpg.create_pool(DATABASE_URL)
|
|
194
|
+
|
|
195
|
+
@server.on_shutdown
|
|
196
|
+
async def close_db():
|
|
197
|
+
await app.db.close()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Both methods can also be called directly instead of used as decorators:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
server.on_startup(init_db)
|
|
204
|
+
server.on_shutdown(close_db)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Startup hooks run after the Redis connection is verified, before the
|
|
208
|
+
consumer starts. Shutdown hooks run after the consumer stops (whether by
|
|
209
|
+
`SIGTERM`/`SIGINT` or an unexpected error), before the Redis connection is
|
|
210
|
+
closed.
|
|
211
|
+
|
|
212
|
+
### Logging
|
|
213
|
+
|
|
214
|
+
`Client` and `Server` each expose a plain `logging.Logger` as `.logger`,
|
|
215
|
+
pre-configured to write text to stdout — there's no custom logger
|
|
216
|
+
interface to implement. To use your own logger (different format, handler,
|
|
217
|
+
sink, etc.), just assign it:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
import logging
|
|
221
|
+
|
|
222
|
+
server.logger = logging.getLogger("myapp.kickdown")
|
|
223
|
+
client.logger = logging.getLogger("myapp.kickdown")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`Client` logs when a task is accepted. `Server` logs process start/shutdown
|
|
227
|
+
(with the polled queues and concurrency), and each task's start,
|
|
228
|
+
completion, retries and permanent failures — every task-related message
|
|
229
|
+
includes the task's `jid`.
|
|
230
|
+
|
|
231
|
+
### Healthcheck
|
|
232
|
+
|
|
233
|
+
`Server` always starts a small HTTP server for liveness/readiness probes,
|
|
234
|
+
on the port given by `web_port` (default `3030`):
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
server = Server(redis_url=..., web_port=3030)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
GET /live -> 200 {"status": "ok"} # process is up
|
|
242
|
+
GET /ready -> 200 {"status": "ok"} # Redis reachable
|
|
243
|
+
-> 503 {"status": "redis unavailable"} # Redis unreachable
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Admin page
|
|
247
|
+
|
|
248
|
+
`GET /admin` renders an HTML dashboard: a summary block with total
|
|
249
|
+
processed/failed counters (aggregated across all queues, from the same
|
|
250
|
+
counters as `client.stats()`), and a table of every polled queue with its
|
|
251
|
+
current pending count (how many tasks are physically waiting in it).
|
|
252
|
+
|
|
253
|
+
By default it's open to anyone who can reach the port. To require HTTP
|
|
254
|
+
Basic Auth, set both `admin_username` and `admin_password`:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
server = Server(
|
|
258
|
+
redis_url=...,
|
|
259
|
+
admin_username="alice",
|
|
260
|
+
admin_password="secret",
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If either is left unset, `/admin` requires no credentials.
|
|
265
|
+
|
|
266
|
+
## Scaling
|
|
267
|
+
|
|
268
|
+
`Server.run()` uses a single asyncio event loop with an `asyncio.Semaphore`
|
|
269
|
+
to cap concurrent task execution within the process — this is a good fit
|
|
270
|
+
since `Performable.perform` is a coroutine. To scale across CPU cores or
|
|
271
|
+
machines, run multiple `Server` processes against the same Redis instance;
|
|
272
|
+
each process independently pops from the shared queues, so Redis balances
|
|
273
|
+
the work between them without any extra coordination.
|