pydocket 0.9.2__tar.gz → 0.13.0b1__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.
Potentially problematic release.
This version of pydocket might be problematic. Click here for more details.
- pydocket-0.13.0b1/.coveragerc-memory +10 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.github/workflows/chaos.yml +2 -0
- pydocket-0.13.0b1/.github/workflows/ci.yml +97 -0
- pydocket-0.13.0b1/.github/workflows/claude-code-review.yml +40 -0
- pydocket-0.13.0b1/.github/workflows/claude.yml +42 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.gitignore +2 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/CLAUDE.md +5 -7
- {pydocket-0.9.2 → pydocket-0.13.0b1}/PKG-INFO +24 -4
- {pydocket-0.9.2 → pydocket-0.13.0b1}/README.md +14 -1
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/driver.py +36 -6
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/advanced-patterns.md +132 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/dependencies.md +135 -40
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/getting-started.md +1 -1
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/testing.md +70 -0
- pydocket-0.13.0b1/examples/agenda_scatter.py +128 -0
- pydocket-0.13.0b1/examples/fastapi_background_tasks.py +204 -0
- pydocket-0.13.0b1/examples/local_development.py +98 -0
- pydocket-0.13.0b1/examples/task_progress.py +111 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/pyproject.toml +27 -6
- pydocket-0.13.0b1/sitecustomize.py +7 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/__init__.py +6 -1
- pydocket-0.13.0b1/src/docket/agenda.py +202 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/annotations.py +3 -1
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/cli.py +293 -22
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/dependencies.py +158 -57
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/docket.py +130 -149
- pydocket-0.13.0b1/src/docket/execution.py +1212 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/instrumentation.py +14 -1
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/tasks.py +2 -2
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/worker.py +127 -50
- pydocket-0.13.0b1/tests/cli/run.py +53 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_clear.py +71 -122
- pydocket-0.13.0b1/tests/cli/test_module.py +10 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_parsing.py +7 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_snapshot.py +137 -123
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_striking.py +78 -108
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_tasks.py +32 -45
- pydocket-0.13.0b1/tests/cli/test_url_validation.py +59 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_version.py +5 -6
- pydocket-0.13.0b1/tests/cli/test_watch.py +404 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_worker.py +45 -68
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/test_workers.py +27 -29
- pydocket-0.13.0b1/tests/cli/waiting.py +133 -0
- pydocket-0.13.0b1/tests/conftest.py +210 -0
- pydocket-0.13.0b1/tests/test_agenda.py +404 -0
- pydocket-0.13.0b1/tests/test_dependencies.py +666 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_docket.py +42 -0
- pydocket-0.13.0b1/tests/test_execution_progress.py +835 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_fundamentals.py +393 -4
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_instrumentation.py +115 -4
- pydocket-0.13.0b1/tests/test_memory_backend.py +113 -0
- pydocket-0.13.0b1/tests/test_results.py +429 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_striking.py +2 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_worker.py +26 -4
- pydocket-0.13.0b1/uv.lock +2243 -0
- pydocket-0.9.2/.github/workflows/ci.yml +0 -65
- pydocket-0.9.2/src/docket/execution.py +0 -438
- pydocket-0.9.2/tests/cli/conftest.py +0 -8
- pydocket-0.9.2/tests/cli/test_module.py +0 -22
- pydocket-0.9.2/tests/conftest.py +0 -180
- pydocket-0.9.2/tests/test_dependencies.py +0 -262
- pydocket-0.9.2/uv.lock +0 -1444
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.cursor/rules/general.mdc +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.cursor/rules/python-style.mdc +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.github/codecov.yml +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.github/workflows/docs.yml +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.github/workflows/publish.yml +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/.pre-commit-config.yaml +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/LICENSE +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/README.md +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/__init__.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/producer.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/run +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/chaos/tasks.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/api-reference.md +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/index.md +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/docs/production.md +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/examples/__init__.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/examples/common.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/examples/concurrency_control.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/examples/find_and_flood.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/examples/self_perpetuating.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/mkdocs.yml +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/__main__.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/src/docket/py.typed +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/telemetry/.gitignore +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/telemetry/start +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/telemetry/stop +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/__init__.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/cli/__init__.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_concurrency_basic.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_concurrency_control.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_concurrency_refresh.py +0 -0
- {pydocket-0.9.2 → pydocket-0.13.0b1}/tests/test_execution.py +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: Docket CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test Python ${{ matrix.python-version }}, ${{ matrix.backend.name }}
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
backend:
|
|
18
|
+
- name: "Redis 6.2, redis-py <5"
|
|
19
|
+
redis-version: "6.2"
|
|
20
|
+
redis-py-version: ">=5,<6"
|
|
21
|
+
- name: "Redis 7.4, redis-py >=5"
|
|
22
|
+
redis-version: "7.4"
|
|
23
|
+
redis-py-version: ">=5"
|
|
24
|
+
- name: "Valkey 8.0, redis-py >=5"
|
|
25
|
+
redis-version: "valkey-8.0"
|
|
26
|
+
redis-py-version: ">=5"
|
|
27
|
+
- name: "Memory (in-memory backend)"
|
|
28
|
+
redis-version: "memory"
|
|
29
|
+
redis-py-version: ">=5"
|
|
30
|
+
include:
|
|
31
|
+
- python-version: "3.10"
|
|
32
|
+
cov-threshold: 100
|
|
33
|
+
pytest-args: ""
|
|
34
|
+
# Python 3.11 coverage reporting is unstable, so use 98% threshold
|
|
35
|
+
- python-version: "3.11"
|
|
36
|
+
cov-threshold: 98
|
|
37
|
+
pytest-args: ""
|
|
38
|
+
- python-version: "3.12"
|
|
39
|
+
cov-threshold: 100
|
|
40
|
+
pytest-args: ""
|
|
41
|
+
- python-version: "3.13"
|
|
42
|
+
cov-threshold: 100
|
|
43
|
+
pytest-args: ""
|
|
44
|
+
- python-version: "3.14"
|
|
45
|
+
cov-threshold: 100
|
|
46
|
+
pytest-args: ""
|
|
47
|
+
# Memory backend: CLI tests are skipped via pytest skip markers because
|
|
48
|
+
# CLI rejects memory:// URLs. Use separate coverage config to exclude CLI.
|
|
49
|
+
- backend:
|
|
50
|
+
name: "Memory (in-memory backend)"
|
|
51
|
+
redis-version: "memory"
|
|
52
|
+
redis-py-version: ">=5"
|
|
53
|
+
cov-threshold: 98 # CLI tests are excluded from coverage and some lines are only covered by CLI tests
|
|
54
|
+
pytest-args: "--cov-config=.coveragerc-memory"
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- name: Install uv and set Python version
|
|
60
|
+
uses: astral-sh/setup-uv@v5
|
|
61
|
+
with:
|
|
62
|
+
python-version: ${{ matrix.python-version }}
|
|
63
|
+
enable-cache: true
|
|
64
|
+
cache-dependency-glob: "pyproject.toml"
|
|
65
|
+
|
|
66
|
+
- name: Install dependencies
|
|
67
|
+
run: uv sync --upgrade-package 'redis${{ matrix.backend.redis-py-version }}'
|
|
68
|
+
|
|
69
|
+
- name: Run tests
|
|
70
|
+
env:
|
|
71
|
+
REDIS_VERSION: ${{ matrix.backend.redis-version }}
|
|
72
|
+
run: uv run pytest --cov-branch --cov-fail-under=${{ matrix.cov-threshold }} --cov-report=xml --cov-report=term-missing:skip-covered ${{ matrix.pytest-args }}
|
|
73
|
+
|
|
74
|
+
- name: Upload coverage reports to Codecov
|
|
75
|
+
uses: codecov/codecov-action@v5
|
|
76
|
+
with:
|
|
77
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
78
|
+
flags: python-${{ matrix.python-version }}
|
|
79
|
+
|
|
80
|
+
prek:
|
|
81
|
+
name: Prek checks
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
|
|
86
|
+
- name: Install uv and set Python version
|
|
87
|
+
uses: astral-sh/setup-uv@v5
|
|
88
|
+
with:
|
|
89
|
+
python-version: "3.10"
|
|
90
|
+
enable-cache: true
|
|
91
|
+
cache-dependency-glob: "pyproject.toml"
|
|
92
|
+
|
|
93
|
+
- name: Install dependencies
|
|
94
|
+
run: uv sync
|
|
95
|
+
|
|
96
|
+
- name: Run prek
|
|
97
|
+
uses: j178/prek-action@v1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Claude Code Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
claude-review:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pull-requests: read
|
|
13
|
+
issues: read
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 1
|
|
21
|
+
|
|
22
|
+
- name: Run Claude Code Review
|
|
23
|
+
id: claude-review
|
|
24
|
+
uses: anthropics/claude-code-action@beta
|
|
25
|
+
with:
|
|
26
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
27
|
+
model: "claude-opus-4-1-20250805"
|
|
28
|
+
|
|
29
|
+
# Direct prompt for automated review (no @claude mention needed)
|
|
30
|
+
direct_prompt: |
|
|
31
|
+
Please review this pull request and provide feedback on:
|
|
32
|
+
- Code quality and best practices
|
|
33
|
+
- Potential bugs or issues
|
|
34
|
+
- Performance considerations
|
|
35
|
+
- Security concerns
|
|
36
|
+
- Test coverage, which must be maintained at 100% for this project
|
|
37
|
+
|
|
38
|
+
Be constructive and helpful in your feedback.
|
|
39
|
+
|
|
40
|
+
use_sticky_comment: true
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Claude Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
issues:
|
|
9
|
+
types: [opened, assigned]
|
|
10
|
+
pull_request_review:
|
|
11
|
+
types: [submitted]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude:
|
|
15
|
+
if: |
|
|
16
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
17
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
18
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
19
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
pull-requests: read
|
|
24
|
+
issues: read
|
|
25
|
+
id-token: write
|
|
26
|
+
actions: read # Required for Claude to read CI results on PRs
|
|
27
|
+
steps:
|
|
28
|
+
- name: Checkout repository
|
|
29
|
+
uses: actions/checkout@v4
|
|
30
|
+
with:
|
|
31
|
+
fetch-depth: 1
|
|
32
|
+
|
|
33
|
+
- name: Run Claude Code
|
|
34
|
+
id: claude
|
|
35
|
+
uses: anthropics/claude-code-action@beta
|
|
36
|
+
with:
|
|
37
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
38
|
+
|
|
39
|
+
additional_permissions: |
|
|
40
|
+
actions: read
|
|
41
|
+
|
|
42
|
+
model: "claude-opus-4-1-20250805"
|
|
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
6
6
|
|
|
7
7
|
**Docket** (`pydocket` on PyPI) is a distributed background task system for Python functions with Redis-backed persistence. It enables scheduling both immediate and future work with comprehensive dependency injection, retry mechanisms, and fault tolerance.
|
|
8
8
|
|
|
9
|
-
**Key Requirements**: Python 3.
|
|
9
|
+
**Key Requirements**: Python 3.10+, Redis 6.2+ or Valkey 8.0+
|
|
10
10
|
|
|
11
11
|
## Development Commands
|
|
12
12
|
|
|
@@ -34,8 +34,8 @@ ruff format
|
|
|
34
34
|
pyright
|
|
35
35
|
pyright tests
|
|
36
36
|
|
|
37
|
-
# Run all
|
|
38
|
-
|
|
37
|
+
# Run all prek hooks
|
|
38
|
+
uv run prek run --all-files
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
### Development Setup
|
|
@@ -44,8 +44,8 @@ pre-commit run --all-files
|
|
|
44
44
|
# Install development dependencies
|
|
45
45
|
uv sync --group dev
|
|
46
46
|
|
|
47
|
-
# Install
|
|
48
|
-
|
|
47
|
+
# Install prek hooks
|
|
48
|
+
uv run prek install
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
### Git Workflow
|
|
@@ -58,7 +58,6 @@ pre-commit install
|
|
|
58
58
|
### Key Classes
|
|
59
59
|
|
|
60
60
|
- **`Docket`** (`src/docket/docket.py`): Central task registry and scheduler
|
|
61
|
-
|
|
62
61
|
- `add()`: Schedule tasks for execution
|
|
63
62
|
- `replace()`: Replace existing scheduled tasks
|
|
64
63
|
- `cancel()`: Cancel pending tasks
|
|
@@ -66,7 +65,6 @@ pre-commit install
|
|
|
66
65
|
- `snapshot()`: Get current state for observability
|
|
67
66
|
|
|
68
67
|
- **`Worker`** (`src/docket/worker.py`): Task execution engine
|
|
69
|
-
|
|
70
68
|
- `run_forever()`/`run_until_finished()`: Main execution loops
|
|
71
69
|
- Handles concurrency, retries, and dependency injection
|
|
72
70
|
- Maintains heartbeat for liveness tracking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydocket
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0b1
|
|
4
4
|
Summary: A distributed background task system for Python functions
|
|
5
5
|
Project-URL: Homepage, https://github.com/chrisguidry/docket
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/chrisguidry/docket/issues
|
|
@@ -19,18 +19,25 @@ Classifier: Development Status :: 4 - Beta
|
|
|
19
19
|
Classifier: License :: OSI Approved :: MIT License
|
|
20
20
|
Classifier: Operating System :: OS Independent
|
|
21
21
|
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
25
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
27
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Python: >=3.
|
|
28
|
+
Requires-Python: >=3.10
|
|
26
29
|
Requires-Dist: cloudpickle>=3.1.1
|
|
30
|
+
Requires-Dist: exceptiongroup>=1.2.0; python_version < '3.11'
|
|
31
|
+
Requires-Dist: fakeredis[lua]>=2.32.1
|
|
27
32
|
Requires-Dist: opentelemetry-api>=1.30.0
|
|
28
33
|
Requires-Dist: opentelemetry-exporter-prometheus>=0.51b0
|
|
29
34
|
Requires-Dist: prometheus-client>=0.21.1
|
|
35
|
+
Requires-Dist: py-key-value-aio[memory,redis]>=0.2.8
|
|
30
36
|
Requires-Dist: python-json-logger>=3.2.1
|
|
31
|
-
Requires-Dist: redis>=
|
|
37
|
+
Requires-Dist: redis>=5
|
|
32
38
|
Requires-Dist: rich>=13.9.4
|
|
33
39
|
Requires-Dist: typer>=0.15.1
|
|
40
|
+
Requires-Dist: typing-extensions>=4.12.0
|
|
34
41
|
Requires-Dist: uuid7>=0.1.0
|
|
35
42
|
Description-Content-Type: text/markdown
|
|
36
43
|
|
|
@@ -69,6 +76,7 @@ from docket import Docket, Worker
|
|
|
69
76
|
|
|
70
77
|
async with Docket() as docket:
|
|
71
78
|
async with Worker(docket) as worker:
|
|
79
|
+
worker.register(greet)
|
|
72
80
|
await worker.run_until_finished()
|
|
73
81
|
```
|
|
74
82
|
|
|
@@ -98,7 +106,7 @@ reference](https://chrisguidry.github.io/docket/api-reference/).
|
|
|
98
106
|
## Installing `docket`
|
|
99
107
|
|
|
100
108
|
Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
|
|
101
|
-
`pydocket`. It targets Python 3.
|
|
109
|
+
`pydocket`. It targets Python 3.10 or above.
|
|
102
110
|
|
|
103
111
|
With [`uv`](https://docs.astral.sh/uv/):
|
|
104
112
|
|
|
@@ -119,6 +127,18 @@ pip install pydocket
|
|
|
119
127
|
Docket requires a [Redis](http://redis.io/) server with Streams support (which was
|
|
120
128
|
introduced in Redis 5.0.0). Docket is tested with Redis 6 and 7.
|
|
121
129
|
|
|
130
|
+
For testing without Redis, Docket includes [fakeredis](https://github.com/cunla/fakeredis-py) for in-memory operation:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from docket import Docket
|
|
134
|
+
|
|
135
|
+
async with Docket(name="my-docket", url="memory://my-docket") as docket:
|
|
136
|
+
# Use docket normally - all operations are in-memory
|
|
137
|
+
...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
See [Testing with Docket](https://chrisguidry.github.io/docket/testing/#using-in-memory-backend-no-redis-required) for more details.
|
|
141
|
+
|
|
122
142
|
# Hacking on `docket`
|
|
123
143
|
|
|
124
144
|
We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
|
|
@@ -33,6 +33,7 @@ from docket import Docket, Worker
|
|
|
33
33
|
|
|
34
34
|
async with Docket() as docket:
|
|
35
35
|
async with Worker(docket) as worker:
|
|
36
|
+
worker.register(greet)
|
|
36
37
|
await worker.run_until_finished()
|
|
37
38
|
```
|
|
38
39
|
|
|
@@ -62,7 +63,7 @@ reference](https://chrisguidry.github.io/docket/api-reference/).
|
|
|
62
63
|
## Installing `docket`
|
|
63
64
|
|
|
64
65
|
Docket is [available on PyPI](https://pypi.org/project/pydocket/) under the package name
|
|
65
|
-
`pydocket`. It targets Python 3.
|
|
66
|
+
`pydocket`. It targets Python 3.10 or above.
|
|
66
67
|
|
|
67
68
|
With [`uv`](https://docs.astral.sh/uv/):
|
|
68
69
|
|
|
@@ -83,6 +84,18 @@ pip install pydocket
|
|
|
83
84
|
Docket requires a [Redis](http://redis.io/) server with Streams support (which was
|
|
84
85
|
introduced in Redis 5.0.0). Docket is tested with Redis 6 and 7.
|
|
85
86
|
|
|
87
|
+
For testing without Redis, Docket includes [fakeredis](https://github.com/cunla/fakeredis-py) for in-memory operation:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from docket import Docket
|
|
91
|
+
|
|
92
|
+
async with Docket(name="my-docket", url="memory://my-docket") as docket:
|
|
93
|
+
# Use docket normally - all operations are in-memory
|
|
94
|
+
...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [Testing with Docket](https://chrisguidry.github.io/docket/testing/#using-in-memory-backend-no-redis-required) for more details.
|
|
98
|
+
|
|
86
99
|
# Hacking on `docket`
|
|
87
100
|
|
|
88
101
|
We use [`uv`](https://docs.astral.sh/uv/) for project management, so getting set up
|
|
@@ -73,7 +73,20 @@ async def main(
|
|
|
73
73
|
tasks: int = 20000,
|
|
74
74
|
producers: int = 5,
|
|
75
75
|
workers: int = 10,
|
|
76
|
+
base_version: str | None = None,
|
|
76
77
|
):
|
|
78
|
+
if base_version is None:
|
|
79
|
+
process = await asyncio.create_subprocess_exec(
|
|
80
|
+
"git",
|
|
81
|
+
"describe",
|
|
82
|
+
"--tags",
|
|
83
|
+
"--abbrev=0",
|
|
84
|
+
stdout=subprocess.PIPE,
|
|
85
|
+
stderr=subprocess.PIPE,
|
|
86
|
+
)
|
|
87
|
+
stdout, _ = await process.communicate()
|
|
88
|
+
base_version = stdout.decode("utf-8").strip()
|
|
89
|
+
|
|
77
90
|
async with (
|
|
78
91
|
run_redis("7.4.2") as (redis_url, redis_container),
|
|
79
92
|
Docket(
|
|
@@ -105,11 +118,17 @@ async def main(
|
|
|
105
118
|
)
|
|
106
119
|
|
|
107
120
|
async def spawn_producer() -> Process:
|
|
121
|
+
docket_version = base_version if random.random() < 0.5 else "main"
|
|
122
|
+
base_command = ["uv", "run"]
|
|
123
|
+
if docket_version != "main":
|
|
124
|
+
logger.info("Using pydocket %s for producer", docket_version)
|
|
125
|
+
base_command.extend(["--with", f"pydocket=={docket_version}"])
|
|
126
|
+
else:
|
|
127
|
+
logger.info("Using main pydocket for producer")
|
|
128
|
+
|
|
129
|
+
command = [*base_command, "-m", "chaos.producer", str(tasks_per_producer)]
|
|
108
130
|
return await asyncio.create_subprocess_exec(
|
|
109
|
-
*
|
|
110
|
-
"-m",
|
|
111
|
-
"chaos.producer",
|
|
112
|
-
str(tasks_per_producer),
|
|
131
|
+
*command,
|
|
113
132
|
env=environment | {"OTEL_SERVICE_NAME": "chaos-producer"},
|
|
114
133
|
stdout=subprocess.DEVNULL,
|
|
115
134
|
stderr=subprocess.DEVNULL,
|
|
@@ -122,8 +141,16 @@ async def main(
|
|
|
122
141
|
logger.info("Spawning %d workers...", workers)
|
|
123
142
|
|
|
124
143
|
async def spawn_worker() -> Process:
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
docket_version = base_version if random.random() < 0.5 else "main"
|
|
145
|
+
base_command = ["uv", "run"]
|
|
146
|
+
if docket_version != "main":
|
|
147
|
+
logger.info("Using pydocket %s for worker", docket_version)
|
|
148
|
+
base_command.extend(["--with", f"pydocket=={docket_version}"])
|
|
149
|
+
else:
|
|
150
|
+
logger.info("Using main pydocket for worker")
|
|
151
|
+
|
|
152
|
+
command = [
|
|
153
|
+
*base_command,
|
|
127
154
|
"-m",
|
|
128
155
|
"docket",
|
|
129
156
|
"worker",
|
|
@@ -135,6 +162,9 @@ async def main(
|
|
|
135
162
|
"chaos.tasks:chaos_tasks",
|
|
136
163
|
"--redelivery-timeout",
|
|
137
164
|
"5s",
|
|
165
|
+
]
|
|
166
|
+
return await asyncio.create_subprocess_exec(
|
|
167
|
+
*command,
|
|
138
168
|
env=environment | {"OTEL_SERVICE_NAME": "chaos-worker"},
|
|
139
169
|
stdout=subprocess.DEVNULL,
|
|
140
170
|
stderr=subprocess.DEVNULL,
|
|
@@ -140,6 +140,138 @@ async def process_single_order(order_id: int) -> None:
|
|
|
140
140
|
|
|
141
141
|
This pattern separates discovery (finding work) from execution (doing work), allowing for better load distribution and fault isolation. The perpetual task stays lightweight and fast, while the actual work is distributed across many workers.
|
|
142
142
|
|
|
143
|
+
## Task Scattering with Agenda
|
|
144
|
+
|
|
145
|
+
For "find-and-flood" workloads, you often want to distribute a batch of tasks over time rather than scheduling them all immediately. The `Agenda` class collects related tasks and scatters them evenly across a time window.
|
|
146
|
+
|
|
147
|
+
### Basic Scattering
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from datetime import timedelta
|
|
151
|
+
from docket import Agenda, Docket
|
|
152
|
+
|
|
153
|
+
async def process_item(item_id: int) -> None:
|
|
154
|
+
await perform_expensive_operation(item_id)
|
|
155
|
+
await update_database(item_id)
|
|
156
|
+
|
|
157
|
+
async with Docket() as docket:
|
|
158
|
+
# Build an agenda of tasks
|
|
159
|
+
agenda = Agenda()
|
|
160
|
+
for item_id in range(1, 101): # 100 items to process
|
|
161
|
+
agenda.add(process_item)(item_id)
|
|
162
|
+
|
|
163
|
+
# Scatter them evenly over 50 minutes to avoid overwhelming the system
|
|
164
|
+
executions = await agenda.scatter(docket, over=timedelta(minutes=50))
|
|
165
|
+
print(f"Scheduled {len(executions)} tasks over 50 minutes")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Tasks are distributed evenly across the time window. For 100 tasks over 50 minutes, they'll be scheduled approximately 30 seconds apart.
|
|
169
|
+
|
|
170
|
+
### Jitter for Thundering Herd Prevention
|
|
171
|
+
|
|
172
|
+
Add random jitter to prevent multiple processes from scheduling identical work at exactly the same times:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# Scatter with ±30 second jitter around each scheduled time
|
|
176
|
+
await agenda.scatter(
|
|
177
|
+
docket,
|
|
178
|
+
over=timedelta(minutes=50),
|
|
179
|
+
jitter=timedelta(seconds=30)
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Future Scatter Windows
|
|
184
|
+
|
|
185
|
+
Schedule the entire batch to start at a specific time in the future:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from datetime import datetime, timezone
|
|
189
|
+
|
|
190
|
+
# Start scattering in 2 hours, spread over 30 minutes
|
|
191
|
+
start_time = datetime.now(timezone.utc) + timedelta(hours=2)
|
|
192
|
+
await agenda.scatter(
|
|
193
|
+
docket,
|
|
194
|
+
start=start_time,
|
|
195
|
+
over=timedelta(minutes=30)
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Mixed Task Types
|
|
200
|
+
|
|
201
|
+
Agendas can contain different types of tasks:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
async def send_email(user_id: str, template: str) -> None:
|
|
205
|
+
await email_service.send(user_id, template)
|
|
206
|
+
|
|
207
|
+
async def update_analytics(event_data: dict[str, str]) -> None:
|
|
208
|
+
await analytics_service.track(event_data)
|
|
209
|
+
|
|
210
|
+
# Create a mixed agenda
|
|
211
|
+
agenda = Agenda()
|
|
212
|
+
agenda.add(process_item)(item_id=1001)
|
|
213
|
+
agenda.add(send_email)("user123", "welcome")
|
|
214
|
+
agenda.add(update_analytics)({"event": "signup", "user": "user123"})
|
|
215
|
+
agenda.add(process_item)(item_id=1002)
|
|
216
|
+
|
|
217
|
+
# All tasks will be scattered in the order they were added
|
|
218
|
+
await agenda.scatter(docket, over=timedelta(minutes=10))
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Single Task Positioning
|
|
222
|
+
|
|
223
|
+
When scattering a single task, it's positioned at the midpoint of the time window:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
agenda = Agenda()
|
|
227
|
+
agenda.add(process_item)(item_id=42)
|
|
228
|
+
|
|
229
|
+
# This task will be scheduled 5 minutes from now (middle of 10-minute window)
|
|
230
|
+
await agenda.scatter(docket, over=timedelta(minutes=10))
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Agenda Reusability
|
|
234
|
+
|
|
235
|
+
Agendas can be reused for multiple scatter operations:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Create a reusable template
|
|
239
|
+
daily_cleanup_agenda = Agenda()
|
|
240
|
+
daily_cleanup_agenda.add(cleanup_temp_files)()
|
|
241
|
+
daily_cleanup_agenda.add(compress_old_logs)()
|
|
242
|
+
daily_cleanup_agenda.add(update_metrics)()
|
|
243
|
+
|
|
244
|
+
# Use it multiple times with different timing
|
|
245
|
+
await daily_cleanup_agenda.scatter(docket, over=timedelta(hours=1))
|
|
246
|
+
|
|
247
|
+
# Later, scatter the same tasks over a different window
|
|
248
|
+
tomorrow = datetime.now(timezone.utc) + timedelta(days=1)
|
|
249
|
+
await daily_cleanup_agenda.scatter(
|
|
250
|
+
docket,
|
|
251
|
+
start=tomorrow,
|
|
252
|
+
over=timedelta(minutes=30)
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Failure Behavior
|
|
257
|
+
|
|
258
|
+
Keep in mind that, if an error occurs during scheduling, some tasks may have already been scheduled successfully:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
agenda = Agenda()
|
|
262
|
+
agenda.add(valid_task)("arg1")
|
|
263
|
+
agenda.add(valid_task)("arg2")
|
|
264
|
+
agenda.add("nonexistent_task")("arg3") # This will cause an error
|
|
265
|
+
agenda.add(valid_task)("arg4")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
await agenda.scatter(docket, over=timedelta(minutes=10))
|
|
269
|
+
except KeyError:
|
|
270
|
+
# The first two tasks were scheduled successfully
|
|
271
|
+
# The error prevented the fourth task from being scheduled
|
|
272
|
+
pass
|
|
273
|
+
```
|
|
274
|
+
|
|
143
275
|
## Striking and Restoring Tasks
|
|
144
276
|
|
|
145
277
|
Striking allows you to temporarily disable tasks without redeploying code. This is invaluable for incident response, gradual rollouts, or handling problematic customers.
|