Migrate BAMAS to PostgreSQL + Celery + Persistent Checkpointer for production readiness.
Docker Compose (single server). Kubernetes later.
┌─────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ FastAPI │ │ Celery │ │ Celery Beat │ │
│ │ (port │ │ Worker │ │ (periodic tasks) │ │
│ │ 8000) │ │ (x2) │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────────────┼──────────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Redis │ (broker + events) │
│ │ (port 6379) │ │
│ └───────────────┘ │
│ │
│ ┌───────────────┐ │
│ │ PostgreSQL │ (persistent state) │
│ │ (port 5432) │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────┘
- Add
psycopg[binary]>=3.2.0 - Add
psycopg-pool>=3.2.0 - Add
langgraph-checkpoint-postgres>=3.1.0 - Add
celery[redis]>=5.4.0 - Keep
asyncpg>=0.29.0(audit trail) - Keep
aiosqlite>=0.19.0(fallback)
- Add
database_url(already existed) - Add
db_pool_min_size: int = 2 - Add
db_pool_max_size: int = 10 - Add
celery_broker_url: str | None = None
- Add
get_psycopg_pool()function - Add
_psycopg_poolglobal singleton - Add
close_psycopg_pool()function - Keep existing
asyncpgpool for audit trail
- Remove all
import aiosqliteand directaiosqlite.connect()calls - Add private
_execute()and_fetchall()helper methods - Auto-convert
?params to$1for PostgreSQL - Add separate DDL for PostgreSQL vs SQLite
- Replace all 9
aiosqlite.connect()calls with helpers - Keep file fallback (
rl_policy.json) as optional
- Add
postgresservice (postgres:16-alpine) - Add
postgres_datavolume - Add healthcheck:
pg_isready -U bamas - Update
apiservice to depend onpostgres
- Add
DATABASE_URL=postgresql://bamas:bamas_dev@postgres:5432/bamas
- Import
BaseCheckpointSaverfromlanggraph.checkpoint.base - Replace
checkpointer = MemorySaver()withcheckpointer: BaseCheckpointSaver | None - Add
init_checkpointer()async function (PostgreSQL + MemorySaver fallback) - Add
close_checkpointer()async function - Update
compile_graph()to use new checkpointer
- Call
init_checkpointer()in lifespan startup - Call
close_checkpointer()in lifespan shutdown - Call
close_psycopg_pool()in lifespan shutdown - Call
close_db()in lifespan shutdown
- Already imports
checkpointerfrom builder (same variable name)
- Change
checkpointer: MemorySavertocheckpointer: BaseCheckpointSaver - Import
BaseCheckpointSaverfromlanggraph.checkpoint.base
- Create
celery_app/directory - Create
__init__.pywith Celery app initialization - Configure broker (Redis), backend (Redis), serialization (JSON)
- Add task autodiscovery
- Create
run_task_celerytask - Serialize
budget_usd(not BudgetTracker) - Use
asyncio.run()to bridge async code - Add retry logic (max_retries=3)
- Add task timeout (300s)
- Add result callback to update task store
- Add
worker_process_initsignal (initialize DB connections) - Add
worker_process_shutdownsignal (close DB connections)
- Import Celery task
- Replace
bg.add_task()withcelery_task.delay() - Remove
_tasksdict (move to TaskStore in Phase 4)
- Replace
asyncio.create_task(run_task(...))with Celery dispatch - Add client disconnect detection
- Add
celery_workerservice - Add
celery_beatservice (for periodic tasks) - Add depends on
redisandpostgres
- Define
TaskStoreABC withcreate(),get(),list(),update()methods - Implement
PostgresTaskStoreusingcore/db.py - Implement
InMemoryTaskStorefor testing - Add factory function
get_task_store()
CREATE TABLE IF NOT EXISTS tasks (
task_id UUID PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'pending',
topology TEXT,
task TEXT NOT NULL,
budget_usd FLOAT,
final_result TEXT,
budget_spent_pct FLOAT,
degradation_count INT DEFAULT 0,
logs JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);- Import
get_task_store() - Replace
_tasks[task_id] = ...withtask_store.create(...) - Replace
_evict_if_needed()with DB-level cleanup
- Remove
from api.routes.execute import _tasks - Import
get_task_store() - Replace dict reads with
task_store.list()andtask_store.get()
- Remove
from api.routes.execute import _tasks - Import
get_task_store() - Replace dict reads with
task_store.get()
- Add
await close_db()to lifespan shutdown ✅ (Done in Phase 2.2) - Add
await close_redis()to lifespan shutdown - Add
await close_psycopg_pool()to lifespan shutdown ✅ (Done in Phase 2.2)
- Check PostgreSQL connectivity
- Check Redis connectivity
- Check LLM provider connectivity (optional)
- Return detailed status:
{"status": "ok", "postgres": "ok", "redis": "ok"}
- Make CORS origins configurable via
ALLOWED_ORIGINSenv var - Add request timeout middleware
- Add rate limiting (slowapi or custom)
- Run
pytest tests/unit/ -v— all 358 tests must pass ✅ - Add tests for new TaskStore
- Add tests for PostgreSQL checkpointer
- Add integration test with PostgreSQL
- Commit all Phase 1-5 changes
- Push to origin/master
- Update README.md with new architecture
- Update .env.example with all new settings
- Add deployment guide
| Risk | Impact | Mitigation |
|---|---|---|
rl_policy.py has 9 raw SQLite calls |
✅ Rewritten to use shared DB abstraction | |
| Celery sync workers + async codebase | MEDIUM | Use asyncio.run() bridge |
| Two PostgreSQL drivers (psycopg + asyncpg) | LOW | Keep both — different purposes |
| ✅ Initialize in lifespan handler | ||
_tasks dict imported by 3 modules |
MEDIUM | Extract to TaskStore service |
Phase 1: PostgreSQL Setup (Day 1-2) ✅ COMPLETE
↓
Phase 2: Persistent Checkpointer (Day 2-3) ✅ COMPLETE
↓
Phase 3: Celery Integration (Day 3-5) ← NEXT
↓
Phase 4: Task Store (Day 5-6)
↓
Phase 5: Shutdown & Security (Day 6-7)
Estimated time remaining: 5-7 days for a senior engineer.