| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 6 | 1 | 0% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 0 | 1 | 1 | 0% |
What each file covers
Sections
0 shared · 6 only in A · 1 only in B- − Background Task Lifecycle
- − Drain vs. cancel on `stop()` (a correctness decision)
- − Loop invariants
- − ✅ wakes immediately on stop
- − ❌ asyncio.sleep(self._interval) # ignores the stop signal
- − Shutdown ordering in `app/main.py` lifespan
- + Claude Code
Commands
neither file has anySection tags
0 shared · 1 only in A · 1 only in B- − monorepo
- + agent-behaviour
Line diff
jleist-clemson/acheron · .cursor/rules/background-tasks.mdc
@@ −1 @@
1---
2description: Background task lifecycle, shutdown ordering, and loop invariants
3globs: app/worker/**/*.py,app/main.py
4alwaysApply: false
5---
6
7# Background Task Lifecycle
8
9Background tasks (`WorkerPool`, `EsIndexer`, `RollupScheduler`) follow a
10`start()` / `stop()` shape with an `asyncio.Event` stop signal and a single
11`asyncio.create_task(..., name=...)`. The skeleton matters less than the
12decisions below — get these wrong and you lose data or hang a deploy.
13
14## Drain vs. cancel on `stop()` (a correctness decision)
15
16- If the task holds **un-acked, un-persisted work**, drain it before cancelling.
17 `WorkerPool.stop()` does `await queue.join()` with a timeout, *then* cancels.
18- If the task's work is **idempotent / replayable** (the outbox re-indexes, the
19 next tick re-aggregates), set the stop event and cancel directly —
20 `EsIndexer` / `RollupScheduler`.
21
22When adding a task, ask: would cancelling mid-flight lose anything? That answer
23picks the variant.
24
25## Loop invariants
26
27- Idle interruptibly — never block shutdown for a full interval:
28
29```python
30# ✅ wakes immediately on stop
31try:
32 await asyncio.wait_for(self._stop_event.wait(), timeout=self._interval)
33except asyncio.TimeoutError:
34 pass # interval elapsed
35# ❌ asyncio.sleep(self._interval) # ignores the stop signal
36```
37
38- The loop body **catches and logs**, never lets an exception kill the task.
39- Consumers must call `task_done()` for every `get()`, even on error.
40
41## Shutdown ordering in `app/main.py` lifespan
42
43`ingestion.stop_accepting()` → `worker.stop()` (drain) → `es_indexer.stop()` →
44`rollup.stop()` → **then** close Mongo/ES/Redis clients. Never close a client a
45running task still uses.
46
jleist-clemson/acheron · CLAUDE.md
@@ +1 @@
1@AGENTS.md
2
3@.claude/rules/python-standards.md
4@.claude/rules/api-routes.md
5@.claude/rules/testing.md
6@.claude/rules/elasticsearch-mapping.md
7@.claude/rules/background-tasks.md
8
9## Claude Code
10
11Shared, vendor-neutral project context lives in `AGENTS.md` (imported above).
12Project conventions live in `.claude/rules/` and are **imported above** so they
13load every session — Claude Code has no glob-scoped rules directory, so each
14file simply names the paths it applies to in an "Applies to" line. They mirror
15the Cursor rules in `.cursor/rules/` (which *are* glob-scoped via `globs:`); if
16you change a convention, update **both** mirrors and the relevant section of
17`ARCHITECTURE.md` so the tools and docs don't drift.
18
@@ −1 +1 @@
1−---
2−description: Background task lifecycle, shutdown ordering, and loop invariants
3−globs: app/worker/**/*.py,app/main.py
4−alwaysApply: false
5−---
1+@AGENTS.md
62
7−# Background Task Lifecycle
3+@.claude/rules/python-standards.md
4+@.claude/rules/api-routes.md
5+@.claude/rules/testing.md
6+@.claude/rules/elasticsearch-mapping.md
7+@.claude/rules/background-tasks.md
88
9−Background tasks (`WorkerPool`, `EsIndexer`, `RollupScheduler`) follow a
10−`start()` / `stop()` shape with an `asyncio.Event` stop signal and a single
11−`asyncio.create_task(..., name=...)`. The skeleton matters less than the
12−decisions below — get these wrong and you lose data or hang a deploy.
9+## Claude Code
1310
14−## Drain vs. cancel on `stop()` (a correctness decision)
15−
16−- If the task holds **un-acked, un-persisted work**, drain it before cancelling.
17− `WorkerPool.stop()` does `await queue.join()` with a timeout, *then* cancels.
18−- If the task's work is **idempotent / replayable** (the outbox re-indexes, the
19− next tick re-aggregates), set the stop event and cancel directly —
20− `EsIndexer` / `RollupScheduler`.
21−
22−When adding a task, ask: would cancelling mid-flight lose anything? That answer
23−picks the variant.
24−
25−## Loop invariants
26−
27−- Idle interruptibly — never block shutdown for a full interval:
28−
29−```python
30−# ✅ wakes immediately on stop
31−try:
32− await asyncio.wait_for(self._stop_event.wait(), timeout=self._interval)
33−except asyncio.TimeoutError:
34− pass # interval elapsed
35−# ❌ asyncio.sleep(self._interval) # ignores the stop signal
36−```
37−
38−- The loop body **catches and logs**, never lets an exception kill the task.
39−- Consumers must call `task_done()` for every `get()`, even on error.
40−
41−## Shutdown ordering in `app/main.py` lifespan
42−
43−`ingestion.stop_accepting()` → `worker.stop()` (drain) → `es_indexer.stop()` →
44−`rollup.stop()` → **then** close Mongo/ES/Redis clients. Never close a client a
45−running task still uses.
11+Shared, vendor-neutral project context lives in `AGENTS.md` (imported above).
12+Project conventions live in `.claude/rules/` and are **imported above** so they
13+load every session — Claude Code has no glob-scoped rules directory, so each
14+file simply names the paths it applies to in an "Applies to" line. They mirror
15+the Cursor rules in `.cursor/rules/` (which *are* glob-scoped via `globs:`); if
16+you change a convention, update **both** mirrors and the relevant section of
17+`ARCHITECTURE.md` so the tools and docs don't drift.
4618
