Skip to content

Commit 6b3f67f

Browse files
committed
Resolve #18 -- Add worker pool telemetry to inspector
1 parent a481394 commit 6b3f67f

21 files changed

Lines changed: 2110 additions & 424 deletions

.github/agents/superjoe.agent.md

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,43 @@ description: CodingJoe's digital clone following his coding guidelines and best
1010

1111
## Planning
1212

13-
You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
13+
MUST follow the `naming-things` guidelines:
1414

1515
```console
1616
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | cat
1717
```
1818

19-
You MUST ALWAYS read the `CONTRIBUTING.md` file before planning or writing any code.
20-
You MUST ALWAYS search the documentation and update it as necessary.
21-
You MUST ALWAYS check for pre-commit hooks and run them before committing code.
22-
You MUST ALWAYS ensure that all new code is fully tested with 100% coverage. Unreachable code branches MUST be removed.
19+
MUST read `CONTRIBUTING.md` before planning or writing code.
20+
MUST search the documentation and update it as necessary.
21+
MUST check pre-commit hooks and run them before committing.
22+
MUST cover all new code with 100% coverage; remove unreachable branches.
2323

2424
## Writing Code
2525

26-
Less code is more! Use the latest language features and libraries to achieve more with less code.
26+
Less code is more. Use the latest language features and libraries to do more with less.
2727

28-
Do not add new dependencies, but if you do, they must be widely adopted and well-maintained in the open-source community.
28+
Do not add new dependencies; if unavoidable, prefer widely adopted, well-maintained FOSS with permissive licenses (BSD or MIT).
2929

30-
You are a strong FOSS advocate with a preference for permissive licenses like BSD or MIT.
30+
Use class syntax for object-oriented code.
31+
Use named functions over anonymous functions.
32+
Break complex functions into smaller ones.
33+
Use generators over building lists or arrays, and prefer them over loops for large data sets.
34+
Avoid nested functions.
35+
Prefer `match`/`switch` or polymorphism over `if` chains.
36+
Do not name values returned on the next line.
3137

32-
Use generators instead of adding items to lists or arrays.
38+
### Docstrings
3339

34-
Use class syntax for all object-oriented code.
35-
Use named functions instead of anonymous functions whenever possible.
36-
Avoid overly complex functions. Break them into smaller functions if necessary.
37-
Docstrings should be written in present tense imperative mood.
38-
They must start with a capital letter and end with a period.
39-
Docstrings must describe the external behavior of the function, class, or method.
40-
Docstrings should avoid redundant phrases like "This function" or "This method".
41-
Class docstrings must not repeat the class name or start with a verb since they don't do anything themselves.
42-
Avoid code comments unless they describe behavior of 3rd party code or complex algorithms.
43-
Avoid loops in favor of recursive functions or generator functions.
44-
Avoid functions or other code inside functions.
45-
Avoid if-statements in favor of switch/match-statements or polymorphism.
46-
Do not assign names to objects which are returned in the next line.
40+
Present tense, imperative mood, capital letter start, period end.
41+
Describe external behavior; avoid "This function" or "This method".
42+
Class docstrings must not repeat the class name or start with a verb.
43+
Avoid code comments unless they describe third-party behavior or complex algorithms.
4744

4845
## Python
4946

50-
Follow PEP 8 guidelines for code style.
51-
EAFP (Easier to Ask Forgiveness than Permission) is preferred over LBYL (Look Before You Leap).
52-
Use type hints for all public functions, classes, and methods.
47+
Follow PEP 8. Prefer EAFP over LBYL.
48+
Use type hints on all public functions, classes, and methods.
5349
Use dataclasses for simple data structures.
5450
Use context managers for resource management.
55-
Use list/set/dict comprehensions instead of loops for creating collections.
56-
Use generators for large data sets to save memory.
57-
Use the walrus operator (`:=`) for inline assignments when it improves readability.
51+
Use comprehensions over loops for building collections.
52+
Use the walrus operator (`:=`) when it improves readability.

.github/workflows/release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ jobs:
88
runs-on: ubuntu-latest
99
steps:
1010
- uses: actions/checkout@v7
11-
with:
12-
fetch-depth: 0
1311
- uses: astral-sh/setup-uv@v7
1412
- run: uvx --from build pyproject-build --sdist --wheel
1513
- uses: actions/upload-artifact@v7

AGENTS.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ uv run pytest -m "integration and benchmark"
1818
uv run pytest --benchmark-compare # compare vs main baseline (run main first)
1919
uvx prek run --all-files
2020
uv run manage.py threadmill worker # run the worker pool
21-
uv run manage.py threadmill inspector # launch the textual TUI inspector
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"`.
@@ -28,8 +28,8 @@ Run a single test by node ID, e.g. `uv run pytest tests/test_command.py::TestCom
2828
## Setup
2929

3030
- Install pre-commit hooks before first commit: `uvx prek install` (not `pre-commit install`).
31-
- `DJANGO_SETTINGS_MODULE=tests.testapp.settings` is already set in `.env` and in `pyproject.toml`. Pytest auto-loads it.
32-
- CI's Linux job starts a Redis service and sets `REDIS_URL`; some integration tests may rely on it. Local runs of `-m integration` may need Redis if a backend test targets it.
31+
- `DJANGO_SETTINGS_MODULE=tests.testapp.settings` is set in `.env` and `pyproject.toml`; pytest auto-loads it.
32+
- CI's Linux job starts Redis and sets `REDIS_URL`; some integration tests rely on it. Local `-m integration` runs may need Redis.
3333

3434
## Code & style (repo-specific, beyond PEP 8)
3535

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

5151
## Architecture notes
5252

53-
- Entry point for end users: `threadmill/management/commands/threadmill.py` (Django management command with `worker` and `inspector` subcommands).
53+
- Entry point: `threadmill/management/commands/threadmill.py` (management command with `worker` and `inspector` subcommands).
5454
- Core runtime: `threadmill/executor.py` (`TaskExecutor`) — process/thread pool, graceful shutdown, worker recycling, task timeout/backlog.
55-
- 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.
55+
- Queue author integration point: `threadmill/backends/base.py` (`ThreadmillTaskBackend`) — subclasses implement `acquire` (lock-without-remove) and `acknowledge` (remove + publish); requires late-ack support from the underlying queue.
5656
- Test app backend `tests/testapp/backends.py` (`GeneratingTaskBackend`) generates tasks in-process for benchmarks; reset between runs via `default_task_backend.reset()`.
5757

5858
## Pre-commit
5959

60-
`.pre-commit-config.yaml` runs ruff (check + format), django-upgrade, pyupgrade, mdformat (excludes `.github/agents/`), yamlfmt, and `no-commit-to-branch` (protects `main`). Hooks auto-fix; ruff is configured `--exit-non-zero-on-fix`, so commit any fixes before pushing.
60+
`.pre-commit-config.yaml` runs ruff (check + format), django-upgrade, pyupgrade, mdformat (excludes `.github/agents/`), yamlfmt, and `no-commit-to-branch` (protects `main`). Hooks auto-fix; ruff uses `--exit-non-zero-on-fix`, so commit fixes before pushing.
6161

6262
## PR / release
6363

64-
CI runs on `main` pushes and PRs. Releases are published to PyPI via `.github/workflows/release.yml` on GitHub release. Commits to `main` are blocked by `no-commit-to-branch`; work on a branch.
64+
CI runs on `main` pushes and PRs. Releases publish to PyPI via `.github/workflows/release.yml` on GitHub release. `no-commit-to-branch` blocks commits to `main`; work on a branch.

CONTRIBUTING.md

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,21 @@ curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/m
1515

1616
## Testing
1717

18-
We have unit tests, integration tests, and benchmarks. Avoid mocking if possible.
19-
20-
To run the tests, use the following command:
21-
22-
```bash
23-
uv run pytest
24-
```
25-
26-
To run only integration tests:
27-
28-
```bash
29-
uv run pytest -m integration
30-
```
31-
32-
To run only integration benchmarks:
18+
The suite has unit tests, integration tests, and benchmarks. Avoid mocking where possible.
3319

3420
```bash
21+
uv run pytest # full suite
22+
uv run pytest -m integration # integration tests only
3523
uv run pytest -m "integration and benchmark"
3624
```
3725

38-
Benchmarking snapshots are created automatically.
39-
To compare your feature branch against the main branch,
40-
run the test suite on main, followed by:
26+
Benchmark snapshots are created automatically. To compare a feature branch against main, run the suite on main first, then:
4127

4228
```
4329
uv run pytest --benchmark-compare
4430
```
4531

46-
Before your first commit, ensure that the pre-commit hooks are installed by running:
32+
Install pre-commit hooks before your first commit:
4733

4834
```bash
4935
uvx prek install

README.md

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
# Threadmill
2+
13
<p align="center">
24
<picture>
35
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-dark.svg">
46
<source media="(prefers-color-scheme: light)" srcset="https://github.com/codingjoe/threadmill/raw/main/docs/images/logo-light.svg">
5-
<img alt="Threadmill: Durable high-performance backend for Django's task framework." src="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">
68
</picture>
79
<br>
810
<a href="https://github.com/codingjoe/threadmill/">Documentation</a> |
@@ -11,15 +13,24 @@
1113
<a href="https://github.com/sponsors/codingjoe">Funding</a> 💚
1214
</p>
1315

14-
# Threadmill [![PyPi Version](https://img.shields.io/pypi/v/threadmill.svg)](https://pypi.python.org/pypi/threadmill/) [![Test Coverage](https://codecov.io/gh/codingjoe/threadmill/branch/main/graph/badge.svg)](https://codecov.io/gh/codingjoe/threadmill) [![GitHub License](https://img.shields.io/github/license/codingjoe/threadmill)](https://raw.githubusercontent.com/codingjoe/threadmill/master/LICENSE)
15-
16-
**Durable high-performance backend for Django's task framework.**
16+
**A queue agnostic worker for Django's task framework.**
1717

1818
## Design Principles
1919

20-
- **Durability** – Recover from any failures, even poorly written tasks.
21-
- **Consistency** – Never lose data, even if someone unplugs the power or network.
22-
- **Utilization** – Keep the CPU saturated with tasks, not with idle time or waiting for locks.
20+
- **Durability** – We recover from any failures, even poorly written tasks.
21+
- **Consistency** – We never lose data, even if someone unplugs the power or network.
22+
- **Utilization** – We keep the CPU saturated with tasks, not with idle time or waiting for locks.
23+
24+
> [!WARNING]
25+
> Threadmill requires a development version of Django and is in a preview stage.
26+
27+
[![PyPi Version](https://img.shields.io/pypi/v/threadmill.svg)](https://pypi.python.org/pypi/threadmill/)
28+
[![Test Coverage](https://codecov.io/gh/codingjoe/threadmill/branch/main/graph/badge.svg)](https://codecov.io/gh/codingjoe/threadmill)
29+
[![GitHub License](https://img.shields.io/github/license/codingjoe/threadmill)](https://raw.githubusercontent.com/codingjoe/threadmill/master/LICENSE)
30+
31+
## Sponsors
32+
33+
[![Sponsors](https://django.the-box.sh/sponsors/codingjoe/threadmill.svg)](https://github.com/sponsors/codingjoe)
2334

2435
## Setup
2536

@@ -50,7 +61,7 @@ TASKS = {
5061
}
5162
```
5263

53-
Optionally, install the inspector dependency if you want the TUI:
64+
Optionally, add the inspector extra for the TUI:
5465

5566
```console
5667
uv add threadmill[inspector]
@@ -70,39 +81,33 @@ The workers are inspired by Gunicorn, and the CLI is very similar.
7081

7182
#### Utilization
7283

73-
Depending on your workload, you can tweak the number of processes and threads.
74-
Processes allow for parallel compute (no GIL) while threads are great for low-memory concurrent IO.
84+
Tune processes and threads to your workload. Processes bypass the GIL for parallel compute; threads suit low-memory concurrent IO.
7585

7686
```console
7787
uv run manage.py threadmill worker --processes 4 --threads 2
7888
```
7989

8090
#### Health
8191

82-
If your tasks leak memory, you can recycle (restart) the workers after a certain number of tasks have been processed:
92+
Recycle workers after a number of tasks to reclaim leaked memory:
8393

8494
```console
8595
uv run manage.py threadmill worker --max-tasks 1000 --max-tasks-jitter 100
8696
```
8797

88-
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.
89-
90-
Should a worker crash or be killed, the pool will automatically restart it.
98+
Jitter spreads restarts so workers do not recycle all at once. Crashed or killed workers are restarted automatically.
9199

92100
#### Shutdown
93101

94-
A graceful shutdown is possible with the `SIGTERM` or a keyboard interrupt.
95-
All workers will finish the tasks they acquired and acknowledge them.
102+
`SIGTERM` or a keyboard interrupt triggers a graceful shutdown: workers finish and acknowledge their in-flight tasks.
96103

97-
You can use `--exit-empty` to exit immediately after all tasks have been processed,
98-
which might be useful for draining a one-off queue.
104+
Use `--exit-empty` to exit once the queue is drained, useful for one-off queue processing.
99105

100106
### Inspector
101107

102108
![Inspector TUI screenshot](https://github.com/codingjoe/threadmill/raw/main/docs/images/TUI-screenshot.svg)
103109

104-
The optional TUI inspector lets you watch queues, tasks, and task details in real-time.
105-
Install it with the `inspector` extra and launch it from a separate terminal:
110+
The optional TUI inspector shows queues, tasks, and task details in real-time. Install the `inspector` extra and launch it from a separate terminal:
106111

107112
```console
108113
uv add threadmill[inspector]
@@ -111,8 +116,7 @@ uv run manage.py threadmill inspector
111116

112117
### Redis Backend Options
113118

114-
The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
115-
`TASKS` configuration:
119+
The `RedisTaskBackend` accepts the following options under `OPTIONS` in your `TASKS` configuration:
116120

117121
| Option | Default | Description |
118122
| ----------------- | ---------------------- | ------------------------------------------------------------ |
@@ -121,17 +125,8 @@ The `RedisTaskBackend` accepts the following options under `OPTIONS` in your
121125
| `broker_interval` | `timedelta(seconds=1)` | Interval between background broker maintenance passes. |
122126
| `batch_size` | `100` | Max tasks to move or requeue per broker pass. |
123127

124-
A task that is started but never acknowledged (lease expired) is marked FAILED
125-
with an `AcknowledgementTimeout` error. Set `lease_ttl` comfortably above your
126-
worst-case task runtime.
127-
128-
All keys for one backend alias share a Redis Cluster hash tag (`{alias}`), so
129-
every multi-key operation — including the cross-queue acquire — runs on a single
130-
shard. Scale horizontally by running additional backend aliases, not by relying
131-
on cross-slot operations.
128+
A started task that is never acknowledged (lease expired) is marked FAILED with an `AcknowledgementTimeout` error. Set `lease_ttl` above your worst-case task runtime.
132129

133-
## Sponsors
134-
135-
[![Sponsors](https://django.the-box.sh/sponsors/codingjoe/threadmill.svg)](https://github.com/sponsors/codingjoe)
130+
All keys for one backend alias share a Redis Cluster hash tag (`{alias}`), so every multi-key operation — including cross-queue acquire — runs on a single shard. Scale horizontally with additional backend aliases, not cross-slot operations.
136131

137132
[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/

docs/images/logo-dark.svg

Lines changed: 1 addition & 1 deletion
Loading

docs/images/logo-light.svg

Lines changed: 1 addition & 1 deletion
Loading

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ redis = ["redis>=5.0"]
4141
inspector = [
4242
"textual>=8.2.7",
4343
]
44+
worker = [
45+
"psutil>=7.2.2",
46+
]
4447

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

0 commit comments

Comments
 (0)