Skip to content

Commit 6cedcc9

Browse files
Merge pull request #19 from PolicyEngine/feat/modal-compute
feat: Move compute to Modal.com for sub-1s cold starts
2 parents 6a23c69 + 49954c2 commit 6cedcc9

12 files changed

Lines changed: 1683 additions & 908 deletions

File tree

.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ DEBUG=true
2020
LOGFIRE_TOKEN=your-logfire-token-here
2121
LOGFIRE_ENVIRONMENT=local
2222

23-
# Worker
24-
WORKER_POLL_INTERVAL=60
23+
# Modal.com (compute backend)
24+
# Authentication is via `modal token set` command, not env vars
25+
# Modal functions need a secret called "policyengine-db" with:
26+
# DATABASE_URL, SUPABASE_URL, SUPABASE_KEY, STORAGE_BUCKET

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install dev format lint test integration-test clean seed up down logs start-supabase stop-supabase reset rebuild create-state-bucket deploy-local init db-reset-prod
1+
.PHONY: install dev format lint test integration-test clean seed up down logs start-supabase stop-supabase reset rebuild create-state-bucket deploy-local init db-reset-prod modal-deploy modal-serve
22

33
# AWS Configuration
44
AWS_REGION ?= us-east-1
@@ -112,3 +112,17 @@ db-reset-prod:
112112
echo "Aborted."; \
113113
exit 1; \
114114
fi
115+
116+
modal-deploy:
117+
@echo "Deploying Modal functions..."
118+
@set -a && . .env.prod && set +a && \
119+
uv run modal secret create policyengine-db \
120+
"DATABASE_URL=$$SUPABASE_POOLER_URL" \
121+
"SUPABASE_URL=$$SUPABASE_URL" \
122+
"SUPABASE_KEY=$$SUPABASE_KEY" \
123+
--force
124+
uv run modal deploy src/policyengine_api/modal_app.py
125+
126+
modal-serve:
127+
@echo "Running Modal functions locally..."
128+
uv run modal serve src/policyengine_api/modal_app.py

docker-compose.yml

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,6 @@ services:
2323
retries: 10
2424
start_period: 10s
2525

26-
worker:
27-
build: .
28-
command: python -m policyengine_api.tasks.worker
29-
ports:
30-
- "${WORKER_PORT:-8080}:${WORKER_PORT:-8080}"
31-
environment:
32-
SUPABASE_URL: http://supabase_kong_policyengine-api-v2:8000
33-
SUPABASE_KEY: ${SUPABASE_KEY}
34-
SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_KEY}
35-
SUPABASE_DB_URL: postgresql://postgres:postgres@supabase_db_policyengine-api-v2:5432/postgres
36-
LOGFIRE_TOKEN: ${LOGFIRE_TOKEN}
37-
WORKER_POLL_INTERVAL: ${WORKER_POLL_INTERVAL:-5}
38-
WORKER_PORT: ${WORKER_PORT:-8080}
39-
volumes:
40-
- ./src:/app/src
41-
- dataset_cache:/tmp/policyengine_dataset_cache
42-
networks:
43-
- supabase_network_policyengine-api-v2
44-
healthcheck:
45-
test: ["CMD", "python", "-c", "import httpx; exit(0 if httpx.get('http://localhost:${WORKER_PORT:-8080}/health', timeout=2).status_code == 200 else 1)"]
46-
interval: 10s
47-
timeout: 5s
48-
retries: 3
49-
start_period: 30s
50-
5126
test:
5227
build: .
5328
command: pytest tests/ -v
@@ -65,14 +40,9 @@ services:
6540
depends_on:
6641
api:
6742
condition: service_healthy
68-
worker:
69-
condition: service_started
7043
profiles:
7144
- test
7245

7346
networks:
7447
supabase_network_policyengine-api-v2:
7548
external: true
76-
77-
volumes:
78-
dataset_cache:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies = [
2222
"fastapi-cache2>=0.2.1",
2323
"boto3>=1.41.1",
2424
"fastapi-mcp>=0.4.0",
25+
"modal>=0.68.0",
2526
]
2627

2728
[project.optional-dependencies]

src/policyengine_api/api/analysis.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from typing import Literal
2020
from uuid import UUID, uuid5
2121

22+
import logfire
2223
from fastapi import APIRouter, Depends, HTTPException
2324
from pydantic import BaseModel, Field
2425
from sqlmodel import Session, select
@@ -285,6 +286,18 @@ def _build_response(
285286
)
286287

287288

289+
def _trigger_modal_report(report_id: str, tax_benefit_model_name: str) -> None:
290+
"""Trigger Modal function for economic impact report."""
291+
import modal
292+
293+
if tax_benefit_model_name == "policyengine_uk":
294+
fn = modal.Function.from_name("policyengine", "run_report_uk")
295+
else:
296+
fn = modal.Function.from_name("policyengine", "run_report_us")
297+
298+
fn.spawn(report_id=report_id)
299+
300+
288301
@router.post("/economic-impact", response_model=EconomicImpactResponse)
289302
def economic_impact(
290303
request: EconomicImpactRequest,
@@ -333,6 +346,11 @@ def economic_impact(
333346

334347
report = _get_or_create_report(baseline_sim.id, reform_sim.id, label, session)
335348

349+
# Trigger Modal if report is pending
350+
if report.status == ReportStatus.PENDING:
351+
with logfire.span("trigger_modal_report", report_id=str(report.id)):
352+
_trigger_modal_report(str(report.id), request.tax_benefit_model_name)
353+
336354
return _build_response(report, baseline_sim, reform_sim, session)
337355

338356

0 commit comments

Comments
 (0)