Skip to content

Commit e3c5e84

Browse files
chrisguidryclaude
andcommitted
Expand CLAUDE.md with comprehensive project documentation (#140)
## Summary - Adds detailed development commands (testing, linting, type checking) - Documents core architecture including Docket/Worker classes and dependency injection system - Explains Redis data model and task lifecycle - Provides project structure overview and CLI usage examples This expanded documentation will help future Claude Code instances work more effectively in this codebase by understanding the distributed task queue architecture and development workflow. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent f62d09c commit e3c5e84

File tree

4 files changed

+461
-0
lines changed

4 files changed

+461
-0
lines changed

src/docket/cli.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,32 @@ async def run() -> None:
358358
asyncio.run(run())
359359

360360

361+
@app.command(help="Clear all pending and scheduled tasks from the docket")
362+
def clear(
363+
docket_: Annotated[
364+
str,
365+
typer.Option(
366+
"--docket",
367+
help="The name of the docket",
368+
envvar="DOCKET_NAME",
369+
),
370+
] = "docket",
371+
url: Annotated[
372+
str,
373+
typer.Option(
374+
help="The URL of the Redis server",
375+
envvar="DOCKET_URL",
376+
),
377+
] = "redis://localhost:6379/0",
378+
) -> None:
379+
async def run() -> None:
380+
async with Docket(name=docket_, url=url) as docket:
381+
cleared_count = await docket.clear()
382+
print(f"Cleared {cleared_count} tasks from docket '{docket_}'")
383+
384+
asyncio.run(run())
385+
386+
361387
@app.command(help="Restores a task or parameters to the Docket")
362388
def restore(
363389
function: Annotated[

src/docket/docket.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,3 +743,46 @@ async def task_workers(self, task_name: str) -> Collection[WorkerInfo]:
743743
workers.append(WorkerInfo(worker_name, last_seen, task_names))
744744

745745
return workers
746+
747+
async def clear(self) -> int:
748+
"""Clear all pending and scheduled tasks from the docket.
749+
750+
This removes all tasks from the stream (immediate tasks) and queue
751+
(scheduled tasks), along with their associated parked data. Running
752+
tasks are not affected.
753+
754+
Returns:
755+
The total number of tasks that were cleared.
756+
"""
757+
with tracer.start_as_current_span(
758+
"docket.clear",
759+
attributes=self.labels(),
760+
):
761+
async with self.redis() as redis:
762+
async with redis.pipeline() as pipeline:
763+
# Get counts before clearing
764+
pipeline.xlen(self.stream_key)
765+
pipeline.zcard(self.queue_key)
766+
pipeline.zrange(self.queue_key, 0, -1)
767+
768+
stream_count: int
769+
queue_count: int
770+
scheduled_keys: list[bytes]
771+
stream_count, queue_count, scheduled_keys = await pipeline.execute()
772+
773+
# Clear all data
774+
# Trim stream to 0 messages instead of deleting it to preserve consumer group
775+
if stream_count > 0:
776+
pipeline.xtrim(self.stream_key, maxlen=0, approximate=False)
777+
pipeline.delete(self.queue_key)
778+
779+
# Clear parked task data and known task keys
780+
for key_bytes in scheduled_keys:
781+
key = key_bytes.decode()
782+
pipeline.delete(self.parked_task_key(key))
783+
pipeline.delete(self.known_task_key(key))
784+
785+
await pipeline.execute()
786+
787+
total_cleared = stream_count + queue_count
788+
return total_cleared

tests/cli/test_clear.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import asyncio
2+
from datetime import datetime, timedelta, timezone
3+
from unittest.mock import AsyncMock
4+
5+
import pytest
6+
from typer.testing import CliRunner
7+
8+
from docket.cli import app
9+
from docket.docket import Docket
10+
11+
12+
@pytest.fixture(autouse=True)
13+
async def empty_docket(docket: Docket):
14+
"""Ensure that the docket starts empty"""
15+
await docket.clear()
16+
17+
18+
async def test_clear_command_empty_docket(docket: Docket, runner: CliRunner):
19+
"""Should clear empty docket and report 0 tasks cleared"""
20+
result = await asyncio.get_running_loop().run_in_executor(
21+
None,
22+
runner.invoke,
23+
app,
24+
[
25+
"clear",
26+
"--url",
27+
docket.url,
28+
"--docket",
29+
docket.name,
30+
],
31+
)
32+
assert result.exit_code == 0, result.output
33+
assert "Cleared 0 tasks" in result.output
34+
35+
36+
async def test_clear_command_with_immediate_tasks(
37+
docket: Docket, runner: CliRunner, the_task: AsyncMock
38+
):
39+
"""Should clear immediate tasks and report count"""
40+
docket.register(the_task)
41+
42+
await docket.add(the_task)("arg1")
43+
await docket.add(the_task)("arg2")
44+
await docket.add(the_task)("arg3")
45+
46+
result = await asyncio.get_running_loop().run_in_executor(
47+
None,
48+
runner.invoke,
49+
app,
50+
[
51+
"clear",
52+
"--url",
53+
docket.url,
54+
"--docket",
55+
docket.name,
56+
],
57+
)
58+
assert result.exit_code == 0, result.output
59+
assert "Cleared 3 tasks" in result.output
60+
61+
snapshot = await docket.snapshot()
62+
assert len(snapshot.future) == 0
63+
assert len(snapshot.running) == 0
64+
65+
66+
async def test_clear_command_with_scheduled_tasks(
67+
docket: Docket, runner: CliRunner, the_task: AsyncMock
68+
):
69+
"""Should clear scheduled tasks and report count"""
70+
docket.register(the_task)
71+
72+
future = datetime.now(timezone.utc) + timedelta(seconds=60)
73+
await docket.add(the_task, when=future)("scheduled1")
74+
await docket.add(the_task, when=future + timedelta(seconds=1))("scheduled2")
75+
76+
result = await asyncio.get_running_loop().run_in_executor(
77+
None,
78+
runner.invoke,
79+
app,
80+
[
81+
"clear",
82+
"--url",
83+
docket.url,
84+
"--docket",
85+
docket.name,
86+
],
87+
)
88+
assert result.exit_code == 0, result.output
89+
assert "Cleared 2 tasks" in result.output
90+
91+
snapshot = await docket.snapshot()
92+
assert len(snapshot.future) == 0
93+
assert len(snapshot.running) == 0
94+
95+
96+
async def test_clear_command_with_mixed_tasks(
97+
docket: Docket, runner: CliRunner, the_task: AsyncMock, another_task: AsyncMock
98+
):
99+
"""Should clear both immediate and scheduled tasks"""
100+
docket.register(the_task)
101+
docket.register(another_task)
102+
103+
future = datetime.now(timezone.utc) + timedelta(seconds=60)
104+
105+
await docket.add(the_task)("immediate1")
106+
await docket.add(another_task)("immediate2")
107+
await docket.add(the_task, when=future)("scheduled1")
108+
await docket.add(another_task, when=future + timedelta(seconds=1))("scheduled2")
109+
110+
result = await asyncio.get_running_loop().run_in_executor(
111+
None,
112+
runner.invoke,
113+
app,
114+
[
115+
"clear",
116+
"--url",
117+
docket.url,
118+
"--docket",
119+
docket.name,
120+
],
121+
)
122+
assert result.exit_code == 0, result.output
123+
assert "Cleared 4 tasks" in result.output
124+
125+
snapshot = await docket.snapshot()
126+
assert len(snapshot.future) == 0
127+
assert len(snapshot.running) == 0
128+
129+
130+
async def test_clear_command_with_keyed_tasks(
131+
docket: Docket, runner: CliRunner, the_task: AsyncMock
132+
):
133+
"""Should clear tasks with keys"""
134+
docket.register(the_task)
135+
136+
await docket.add(the_task, key="task1")("arg1")
137+
await docket.add(the_task, key="task2")("arg2")
138+
139+
result = await asyncio.get_running_loop().run_in_executor(
140+
None,
141+
runner.invoke,
142+
app,
143+
[
144+
"clear",
145+
"--url",
146+
docket.url,
147+
"--docket",
148+
docket.name,
149+
],
150+
)
151+
assert result.exit_code == 0, result.output
152+
assert "Cleared 2 tasks" in result.output
153+
154+
snapshot = await docket.snapshot()
155+
assert len(snapshot.future) == 0
156+
157+
158+
async def test_clear_command_basic_functionality(
159+
docket: Docket, runner: CliRunner, the_task: AsyncMock
160+
):
161+
"""Should clear tasks via CLI command"""
162+
docket.register(the_task)
163+
164+
# Add some tasks to clear
165+
await docket.add(the_task)("task1")
166+
future = datetime.now(timezone.utc) + timedelta(seconds=60)
167+
await docket.add(the_task, when=future)("scheduled_task")
168+
169+
result = await asyncio.get_running_loop().run_in_executor(
170+
None,
171+
runner.invoke,
172+
app,
173+
[
174+
"clear",
175+
"--url",
176+
docket.url,
177+
"--docket",
178+
docket.name,
179+
],
180+
)
181+
assert result.exit_code == 0, result.output
182+
assert "Cleared" in result.output
183+
184+
snapshot_after_clear = await docket.snapshot()
185+
assert len(snapshot_after_clear.future) == 0
186+
187+
188+
async def test_clear_command_preserves_strikes(
189+
docket: Docket, runner: CliRunner, the_task: AsyncMock
190+
):
191+
"""Should not affect strikes when clearing"""
192+
docket.register(the_task)
193+
194+
await docket.strike("the_task")
195+
await docket.add(the_task)("arg1")
196+
197+
result = await asyncio.get_running_loop().run_in_executor(
198+
None,
199+
runner.invoke,
200+
app,
201+
[
202+
"clear",
203+
"--url",
204+
docket.url,
205+
"--docket",
206+
docket.name,
207+
],
208+
)
209+
assert result.exit_code == 0, result.output
210+
assert "Cleared" in result.output
211+
212+
# Strikes should still be in effect - clear doesn't affect strikes
213+
214+
215+
async def test_clear_command_with_custom_url(runner: CliRunner):
216+
"""Should handle custom Redis URL"""
217+
result = await asyncio.get_running_loop().run_in_executor(
218+
None,
219+
runner.invoke,
220+
app,
221+
[
222+
"clear",
223+
"--url",
224+
"redis://nonexistent:12345/0",
225+
"--docket",
226+
"test-docket",
227+
],
228+
)
229+
assert result.exit_code != 0
230+
assert result.exit_code != 0
231+
232+
233+
async def test_clear_command_with_custom_docket_name(
234+
docket: Docket, runner: CliRunner, the_task: AsyncMock
235+
):
236+
"""Should handle custom docket name"""
237+
docket.register(the_task)
238+
await docket.add(the_task)("test")
239+
240+
result = await asyncio.get_running_loop().run_in_executor(
241+
None,
242+
runner.invoke,
243+
app,
244+
[
245+
"clear",
246+
"--url",
247+
docket.url,
248+
"--docket",
249+
docket.name,
250+
],
251+
)
252+
assert result.exit_code == 0, result.output
253+
assert "Cleared 1 tasks" in result.output
254+
255+
256+
def test_clear_command_help(runner: CliRunner):
257+
"""Should show help text for clear command"""
258+
result = runner.invoke(app, ["clear", "--help"])
259+
assert result.exit_code == 0
260+
assert "clear" in result.output.lower()
261+
assert "--url" in result.output
262+
assert "--docket" in result.output
263+
264+
265+
def test_clear_command_requires_no_additional_args(runner: CliRunner):
266+
"""Should not require any additional arguments beyond URL and docket name"""
267+
result = runner.invoke(app, ["clear", "--help"])
268+
assert result.exit_code == 0
269+
assert "Usage:" in result.output

0 commit comments

Comments
 (0)