Skip to content

Commit c06bc8f

Browse files
committed
Add inspector TUI
1 parent b135558 commit c06bc8f

19 files changed

Lines changed: 1965 additions & 57 deletions

File tree

‎AGENTS.md‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ uv run pytest -m "not benchmark" # what CI runs by default
1616
uv run pytest -m integration # integration tests only
1717
uv run pytest -m "integration and benchmark"
1818
uv run pytest --benchmark-compare # compare vs main baseline (run main first)
19-
uv run ruff check --fix # lint (auto-fix)
20-
uv run ruff format # format
21-
uv run manage.py threadmill # run the worker pool (the app's CLI)
19+
uvx prek run --all-files
20+
uv run manage.py threadmill worker # run the worker pool
21+
uv run manage.py threadmill inspector # launch the textual TUI inspector
2222
```
2323

2424
CI additionally pins Django per matrix step: `uv run --with django~=6.1a1 pytest -m "not benchmark"`.
@@ -37,11 +37,7 @@ Run a single test by node ID, e.g. `uv run pytest tests/test_command.py::TestCom
3737
```bash
3838
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md
3939
```
40-
- Ruff config: `select` includes E,W,F,I,S,D,UP,B,C; `ignore` includes `B904`, `D1`, `E501`, `S101` (so `assert` in tests is fine, and missing docstrings on private members are allowed).
41-
- Docstrings use `pep257` convention: present-tense imperative, start with capital, end with period, describe external behavior only. Class docstrings must not repeat the class name or start with a verb.
42-
- Prefer generators over building lists/arrays, `match`/`switch` over `if` chains, EAFP over LBYL, dataclasses for plain data, context managers for resources, walrus `:=` when it aids readability.
43-
- Avoid code comments except for third-party behavior or complex algorithms. Avoid nested functions and loops-in-loops where a generator/recursion works.
44-
- Do not add dependencies unless widely adopted and well-maintained; prefer BSD/MIT-style permissive licenses.
40+
- NEVER format code, tests, or docs manually. MUST USE `prek run --all-files`.
4541

4642
## Coverage
4743

@@ -54,7 +50,7 @@ Codecov requires 100% patch coverage on PRs (`pyproject.toml`, `.codecov.yml`).
5450

5551
## Architecture notes
5652

57-
- Entry point for end users: `threadmill/management/commands/threadmill.py` (Django management command, Gunicorn-style CLI).
53+
- Entry point for end users: `threadmill/management/commands/threadmill.py` (Django management command with `worker` and `inspector` subcommands).
5854
- Core runtime: `threadmill/executor.py` (`TaskExecutor`) — process/thread pool, graceful shutdown, worker recycling, task timeout/backlog.
5955
- Integration point for queue authors: `threadmill/backends.py` (`AcknowledgeableTaskBackend`) — subclasses implement `acquire` (lock-without-remove) and `acknowledge` (remove + publish). Requires late-ack support from the underlying queue.
6056
- Test app backend `tests/testapp/backends.py` (`GeneratingTaskBackend`) generates tasks in-process for benchmarks; reset between runs via `default_task_backend.reset()`.

‎README.md‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
<p align="center">
44
<picture>
5-
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/images/logo-dark.svg">
6-
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/images/logo-light.svg">
7-
<img alt="Threadmill: A queue agnostic worker for Django's task framework." src="https://github.com/codingjoe/threadmill/raw/main/images/logo-light.svg">
5+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-dark.svg">
6+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
7+
<img alt="Threadmill: A queue agnostic worker for Django's task framework." src="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
88
</picture>
99
<br>
1010
<a href="https://github.com/codingjoe/threadmill/">Documentation</a> |
@@ -61,10 +61,16 @@ TASKS = {
6161
}
6262
```
6363

64-
Finally, you launch the worker pool:
64+
Optionally, install the inspector dependency if you want the TUI:
6565

6666
```console
67-
uv run manage.py threadmill
67+
uv add threadmill[inspector]
68+
```
69+
70+
Then launch the worker pool:
71+
72+
```console
73+
uv run manage.py threadmill worker
6874
```
6975

7076
## Usage
@@ -79,15 +85,15 @@ Depending on your workload, you can tweak the number of processes and threads.
7985
Processes allow for parallel compute (no GIL) while threads are great for low-memory concurrent IO.
8086

8187
```console
82-
uv run manage.py threadmill --processes 4 --threads 2
88+
uv run manage.py threadmill worker --processes 4 --threads 2
8389
```
8490

8591
#### Health
8692

8793
If your tasks leak memory, you can recycle (restart) the workers after a certain number of tasks have been processed:
8894

8995
```console
90-
uv run manage.py threadmill --max-tasks 1000 --max-tasks-jitter 100
96+
uv run manage.py threadmill worker --max-tasks 1000 --max-tasks-jitter 100
9197
```
9298

9399
This will restart the workers after 1000 tasks have been processed, with a random jitter of up to 100 tasks to avoid all workers restarting at the same time.
@@ -102,6 +108,18 @@ All workers will finish the tasks they acquired and acknowledge them.
102108
You can use `--exit-empty` to exit immediately after all tasks have been processed,
103109
which might be useful for draining a one-off queue.
104110

111+
### Inspector
112+
113+
![Inspector TUI screenshot](https://github.com/codingjoe/threadmill/raw/main/docs/images/TUI-screenshot.svg)
114+
115+
The optional TUI inspector lets you watch queues, tasks, and task details in real-time.
116+
Install it with the `inspector` extra and launch it from a separate terminal:
117+
118+
```console
119+
uv add threadmill[inspector]
120+
uv run manage.py threadmill inspector
121+
```
122+
105123
### Redis Backend Options
106124

107125
The `RedisTaskBackend` accepts the following options under `OPTIONS` in your

‎docs/images/TUI-screenshot.svg‎

Lines changed: 263 additions & 0 deletions
Loading

‎pyproject.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ dependencies = ["django>=6.1a1"]
3737

3838
[project.optional-dependencies]
3939
redis = ["redis>=5.0"]
40+
inspector = [
41+
"textual>=8.2.7",
42+
]
4043

4144
[project.urls]
4245
# https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels

‎tests/backends/test_base.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ def test_peek__raise_not_implemented_error(self) -> None:
4949
with pytest.raises(NotImplementedError):
5050
list(BackendDouble(alias="default", params={}).peek("default"))
5151

52+
def test_queue_telemetry__raise_not_implemented_error(self) -> None:
53+
"""Raise NotImplementedError for backend telemetry API."""
54+
with pytest.raises(NotImplementedError):
55+
BackendDouble(alias="default", params={}).queue_telemetry()
56+
5257

5358
class TestAcknowledgementTimeout:
5459
"""Tests for the AcknowledgementTimeout exception."""

0 commit comments

Comments
 (0)