| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 6 | 6 | 0% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 0 | 6 | 1 | 0% |
What each file covers
Sections
0 shared · 6 only in A · 6 only in B- − Acheron — AI agent & contributor guide
- − What this is
- − Layering (keep these boundaries)
- − Store roles (do not blur)
- − Conventions in brief (see the scoped rules for detail)
- − Docs are a first-class deliverable
- + 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
Commands
neither file has anySection tags
0 shared · 6 only in A · 1 only in B- − test
- − lint-format
- − code-style
- − do-not
- − agent-behaviour
- − docs
- + monorepo
Line diff
jleist-clemson/acheron · AGENTS.md
@@ −1 @@
1# Acheron — AI agent & contributor guide
2
3Vendor-neutral project context, shared across AI coding tools. Cursor reads this
4file natively; Claude Code reads it via the `@AGENTS.md` import in `CLAUDE.md`.
5Tool-specific rules live in `.cursor/rules/` (Cursor, glob-scoped via `globs:`)
6and `.claude/rules/` (Claude Code, imported from `CLAUDE.md` so they load every
7session) — keep those mirrors in sync with each other and with this file.
8`ARCHITECTURE.md` is the authoritative design document.
9
10## What this is
11
12`acheron` is a Distributed Event Processing Platform. Write path:
13`POST /events` → bounded in-process `asyncio.Queue` → async worker →
14**MongoDB (source of truth)**. Elasticsearch is a **derived mirror**, populated
15strictly downstream from a Mongo outbox (`es_indexed` marker) by the `EsIndexer`.
16Redis caches the realtime stats summary.
17
18## Layering (keep these boundaries)
19
20- `app/api/` — HTTP only: translate request ↔ domain, map errors to status codes.
21- `app/ingestion/`, `app/worker/` — pipeline logic (enqueue, consume, index, rollup).
22- `app/storage/`, `app/cache/`, `app/queue/` — one backend per module.
23
24Business logic lives in services/stores, never in route handlers.
25
26## Store roles (do not blur)
27
28- **Mongo is authoritative.** If Mongo and ES disagree, Mongo wins.
29- **ES is best-effort and rebuildable** — never fail an authoritative write on it.
30- **Redis is a cache** — a Redis outage must degrade, never lose data.
31- The queue is **in-process and non-durable**; that constraint drives most
32 failure-mode and scaling reasoning. Don't design as if it were durable.
33
34## Conventions in brief (see the scoped rules for detail)
35
36- `from __future__ import annotations` atop every module; Google-style docstrings
37 (ruff `D`, pydocstyle google). Tunables live in `Settings`, not magic numbers.
38- Routes are thin; the events routes declare a Pydantic `response_model`
39 (`/health` and `/metrics` are intentionally exempt). Errors map to status
40 codes (Mongo→503, ES→502, queue full→429, shutdown→503).
41- Tests: pytest with `asyncio_mode=auto`; unit tests are hermetic, Docker-backed
42 tests are marked `integration` and deselected by default.
43
44## Docs are a first-class deliverable
45
46When you change behavior, update `ARCHITECTURE.md` and `README.md` in the same
47change. Code and docs must not drift — the architecture doc is graded.
48
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
@@ −1 +1 @@
1−# Acheron — AI agent & contributor guide
1+---
2+description: Background task lifecycle, shutdown ordering, and loop invariants
3+globs: app/worker/**/*.py,app/main.py
4+alwaysApply: false
5+---
26
3−Vendor-neutral project context, shared across AI coding tools. Cursor reads this
4−file natively; Claude Code reads it via the `@AGENTS.md` import in `CLAUDE.md`.
5−Tool-specific rules live in `.cursor/rules/` (Cursor, glob-scoped via `globs:`)
6−and `.claude/rules/` (Claude Code, imported from `CLAUDE.md` so they load every
7−session) — keep those mirrors in sync with each other and with this file.
8−`ARCHITECTURE.md` is the authoritative design document.
7+# Background Task Lifecycle
98
10−## What this is
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.
1113
12−`acheron` is a Distributed Event Processing Platform. Write path:
13−`POST /events` → bounded in-process `asyncio.Queue` → async worker →
14−**MongoDB (source of truth)**. Elasticsearch is a **derived mirror**, populated
15−strictly downstream from a Mongo outbox (`es_indexed` marker) by the `EsIndexer`.
16−Redis caches the realtime stats summary.
14+## Drain vs. cancel on `stop()` (a correctness decision)
1715
18−## Layering (keep these boundaries)
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`.
1921
20−- `app/api/` — HTTP only: translate request ↔ domain, map errors to status codes.
21−- `app/ingestion/`, `app/worker/` — pipeline logic (enqueue, consume, index, rollup).
22−- `app/storage/`, `app/cache/`, `app/queue/` — one backend per module.
22+When adding a task, ask: would cancelling mid-flight lose anything? That answer
23+picks the variant.
2324
24−Business logic lives in services/stores, never in route handlers.
25+## Loop invariants
2526
26−## Store roles (do not blur)
27+- Idle interruptibly — never block shutdown for a full interval:
2728
28−- **Mongo is authoritative.** If Mongo and ES disagree, Mongo wins.
29−- **ES is best-effort and rebuildable** — never fail an authoritative write on it.
30−- **Redis is a cache** — a Redis outage must degrade, never lose data.
31−- The queue is **in-process and non-durable**; that constraint drives most
32− failure-mode and scaling reasoning. Don't design as if it were durable.
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+```
3337
34−## Conventions in brief (see the scoped rules for detail)
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.
3540
36−- `from __future__ import annotations` atop every module; Google-style docstrings
37− (ruff `D`, pydocstyle google). Tunables live in `Settings`, not magic numbers.
38−- Routes are thin; the events routes declare a Pydantic `response_model`
39− (`/health` and `/metrics` are intentionally exempt). Errors map to status
40− codes (Mongo→503, ES→502, queue full→429, shutdown→503).
41−- Tests: pytest with `asyncio_mode=auto`; unit tests are hermetic, Docker-backed
42− tests are marked `integration` and deselected by default.
41+## Shutdown ordering in `app/main.py` lifespan
4342
44−## Docs are a first-class deliverable
45−
46−When you change behavior, update `ARCHITECTURE.md` and `README.md` in the same
47−change. Code and docs must not drift — the architecture doc is graded.
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.
4846
