http4s and dispatch thoughts

This commit is contained in:
simonredfern 2025-12-05 08:14:45 +01:00
parent 14b26fc667
commit 6ba32f562e

View File

@ -5,26 +5,139 @@ Can http4s and Lift coexist in the same project to convert endpoints one by one?
## Answer: Yes, on Different Ports
### Architecture Overview
## OBP-API-Dispatch
OBP-API-Dispatch already exists:
- **Location:** `workspace_2024/OBP-API-Dispatch`
- **GitHub:** https://github.com/OpenBankProject/OBP-API-Dispatch
- **Technology:** http4s (Cats Effect 3, Ember server)
- **Purpose:** Routes requests between different OBP-API backends
- **Current routing:** Based on API version (v1.3.0 → backend 2, others → backend 1)
It needs minor enhancements to support version-based routing for the Lift → http4s migration.
## Answers to Your Three Questions
### Q1: Could we use OBP-API-Dispatch to route between two ports?
**YES - it already exists**
OBP-API-Dispatch can be used for this:
- Single entry point for clients
- Route by API version: v4/v5 → Lift, v6/v7 → http4s
- No client configuration changes needed
- Rollback by changing routing config
### Q2: Running http4s in Jetty until migration complete?
**Possible but not recommended**
Running http4s in Jetty (servlet mode) loses:
- True non-blocking I/O
- HTTP/2, WebSockets, efficient streaming
- Would need to refactor again later to standalone
Use standalone http4s on port 8081 from the start.
### Q3: How would the developer experience be?
**IDE and Project Setup:**
You'll work in **one IDE window** with the OBP-API codebase:
- Same project structure
- Same database (Lift Boot continues to handle DB creation/migrations)
- Both Lift and http4s code in the same `obp-api` module
- Edit both Lift endpoints and http4s endpoints in the same IDE
**Running the Servers:**
You'll run **three separate terminal processes**:
**Terminal 1: Lift Server (existing)**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" run
```
- Runs Lift Boot (handles DB initialization)
- Starts on port 8080
- Keep this running as long as you have Lift endpoints
**Terminal 2: http4s Server (new)**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" "runMain code.api.http4s.Http4sMain"
```
- Starts on port 8081
- Separate process from Lift
- Uses same database connection pool
**Terminal 3: OBP-API-Dispatch (separate project)**
```bash
cd workspace_2024/OBP-API-Dispatch
mvn clean package
java -jar target/OBP-API-Dispatch-1.0-SNAPSHOT-jar-with-dependencies.jar
```
- Separate IDE window or just a terminal
- Routes requests between Lift (8080) and http4s (8081)
- Runs on port 8088
**Editing Workflow:**
1. **Adding new http4s endpoint:**
- Create endpoint in `obp-api/src/main/scala/code/api/http4s/`
- Edit in same IDE as Lift code
- Restart Terminal 2 only (http4s server)
2. **Fixing Lift endpoint:**
- Edit existing Lift code in `obp-api/src/main/scala/code/api/`
- Restart Terminal 1 only (Lift server)
3. **Updating routing (which endpoints go where):**
- Edit `OBP-API-Dispatch/src/main/resources/application.conf`
- Restart Terminal 3 only (Dispatch)
**Database:**
Lift Boot continues to handle:
- Database connection setup
- Schema migrations
- Table creation
Both Lift and http4s use the same database connection pool and Mapper classes.
### Architecture with OBP-API-Dispatch
```
┌─────────────────────────────────────────┐
│ OBP-API Application │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Lift/Jetty │ │ http4s Server │ │
│ │ Port 8080 │ │ Port 8081 │ │
│ └──────────────┘ └─────────────────┘ │
│ │ │ │
│ └──────┬───────────┘ │
│ │ │
│ Shared Resources: │
│ - Database │
│ - Business Logic │
│ - Authentication │
│ - ResourceDocs │
└─────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ API Clients │
└────────────────────────────────────────────┘
Port 8088/443
┌────────────────────────────────────────────┐
│ OBP-API-Dispatch (http4s) │
│ │
│ Routing Rules: │
│ • /obp/v4.0.0/* → Lift (8080) │
│ • /obp/v5.0.0/* → Lift (8080) │
│ • /obp/v5.1.0/* → Lift (8080) │
│ • /obp/v6.0.0/* → http4s (8081) ✨ │
│ • /obp/v7.0.0/* → http4s (8081) ✨ │
└────────────────────────────────────────────┘
↓ ↓
┌─────────┐ ┌─────────┐
│ Lift │ │ http4s │
│ :8080 │ │ :8081 │
└─────────┘ └─────────┘
↓ ↓
┌────────────────────────────────┐
│ Shared Resources: │
│ - Database │
│ - Business Logic │
│ - Authentication │
│ - ResourceDocs │
└────────────────────────────────┘
```
### How It Works
@ -46,6 +159,122 @@ Can http4s and Lift coexist in the same project to convert endpoints one by one?
- During: Some on Lift, some on http4s
- End: All on http4s (port 8081), Lift removed
## Using OBP-API-Dispatch for Routing
### Current Status
OBP-API-Dispatch already exists and is functional:
- **Location:** `workspace_2024/OBP-API-Dispatch`
- **GitHub:** https://github.com/OpenBankProject/OBP-API-Dispatch
- **Build:** Maven-based
- **Technology:** http4s with Ember server
- **Current routing:** Routes v1.3.0 to backend 2, others to backend 1
### Current Configuration
```hocon
# application.conf
app {
dispatch_host = "127.0.0.1"
dispatch_dev_port = 8088
obp_api_1_base_uri = "http://localhost:8080"
obp_api_2_base_uri = "http://localhost:8086"
}
```
### Enhancement for Migration
Update configuration to support version-based routing:
```hocon
# application.conf
app {
dispatch_host = "0.0.0.0"
dispatch_port = 8088
# Lift backend (legacy endpoints)
lift_backend_uri = "http://localhost:8080"
lift_backend_uri = ${?LIFT_BACKEND_URI}
# http4s backend (modern endpoints)
http4s_backend_uri = "http://localhost:8081"
http4s_backend_uri = ${?HTTP4S_BACKEND_URI}
# Routing strategy
routing {
# API versions that go to http4s backend
http4s_versions = ["v6.0.0", "v7.0.0"]
# Specific endpoint overrides (optional)
overrides = [
# Example: migrate specific v5.1.0 endpoints early
# { path = "/obp/v5.1.0/banks", target = "http4s" }
]
}
}
```
### Enhanced Routing Logic
Update `ObpApiDispatch.scala` to support version-based routing:
```scala
private def selectBackend(path: String, method: Method): String = {
// Extract API version from path: /obp/v{version}/...
val versionPattern = """/obp/(v\d+\.\d+\.\d+)/.*""".r
path match {
case versionPattern(version) =>
if (http4sVersions.contains(version)) {
logger.info(s"Version $version routed to http4s")
"http4s"
} else {
logger.info(s"Version $version routed to Lift")
"lift"
}
case _ =>
logger.debug(s"Path $path routing to Lift (default)")
"lift"
}
}
```
### Local Development Workflow
**Terminal 1: Start Lift**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" run
# Starts on port 8080
```
**Terminal 2: Start http4s**
```bash
cd workspace_2024/OBP-API-C/OBP-API
sbt "project obp-api" "runMain code.api.http4s.Http4sMain"
# Starts on port 8081
```
**Terminal 3: Start OBP-API-Dispatch**
```bash
cd workspace_2024/OBP-API-Dispatch
mvn clean package
java -jar target/OBP-API-Dispatch-1.0-SNAPSHOT-jar-with-dependencies.jar
# Starts on port 8088
```
**Terminal 4: Test**
```bash
# Old endpoint (goes to Lift)
curl http://localhost:8088/obp/v5.1.0/banks
# New endpoint (goes to http4s)
curl http://localhost:8088/obp/v6.0.0/banks
# Health checks
curl http://localhost:8088/health
```
## Implementation Approach
### Step 1: Add http4s to Project
@ -88,14 +317,14 @@ object Http4sServer {
### Step 3: THE JETTY PROBLEM
**IMPORTANT:** If you start http4s from Lift's Bootstrap, it runs INSIDE Jetty's servlet container. This defeats the purpose of using http4s!
If you start http4s from Lift's Bootstrap, it runs INSIDE Jetty's servlet container. This defeats the purpose of using http4s.
```
❌ WRONG APPROACH:
┌─────────────────────────────┐
│ Jetty Servlet Container │
│ ├─ Lift (port 8080) │
│ └─ http4s (port 8081) │ ← Still requires Jetty!
│ └─ http4s (port 8081) │ ← Still requires Jetty
└─────────────────────────────┘
```
@ -220,9 +449,9 @@ Access via:
- Can't remove servlet container
- Only for development/testing
### RECOMMENDED APPROACH
### Option A: Two Separate Processes
**For actual migration, use Option A (Two Separate Processes):**
For actual migration:
1. **Keep Lift/Jetty running as-is** on port 8080
2. **Create standalone http4s server** on port 8081 with its own Main class
@ -247,7 +476,7 @@ Phase 4 (Complete):
└──────┬──────┘
└──→ http4s standalone Port 8080 ← All endpoints
(Jetty removed!)
(Jetty removed)
```
**This way http4s is NEVER dependent on Jetty.**
@ -267,7 +496,7 @@ Thread-per-request model:
│ Request 3 → Thread 3 (busy) │ Blocks waiting for file I/O
│ Request 4 → Thread 4 (busy) │
│ ... │
│ Request N → Thread pool full │ ← New requests wait!
│ Request N → Thread pool full │ ← New requests wait
└─────────────────────────────┘
Problem:
@ -282,7 +511,7 @@ Problem:
Async/Effect model:
┌─────────────────────────────┐
│ Thread 1: │
│ Request 1 → DB call (IO) │ ← Doesn't block! Fiber suspended
│ Request 1 → DB call (IO) │ ← Doesn't block - Fiber suspended
│ Request 2 → API call (IO) │ ← Continues processing
│ Request 3 → Processing │
│ Request N → ... │
@ -466,18 +695,18 @@ class Http4sEndpoints[F[_]: Concurrent] {
## Migration Strategy
### Phase 1: Setup (Week 1-2)
### Phase 1: Setup
- Add http4s dependencies
- Create http4s server infrastructure
- Start http4s on port 8081
- Keep all endpoints on Lift
### Phase 2: Convert New Endpoints (Week 3-8)
### Phase 2: Convert New Endpoints
- All NEW endpoints go to http4s only
- Existing endpoints stay on Lift
- Share business logic between both
### Phase 3: Migrate Existing Endpoints (Month 3-6)
### Phase 3: Migrate Existing Endpoints
Priority order:
1. Simple GET endpoints (read-only, no sessions)
2. POST endpoints with simple authentication
@ -485,13 +714,13 @@ Priority order:
4. Admin/management endpoints
5. OAuth/authentication endpoints (last)
### Phase 4: Deprecation (Month 7-9)
### Phase 4: Deprecation
- Announce Lift endpoints deprecated
- Run both servers (port 8080 and 8081)
- Redirect/proxy 8080 -> 8081
- Update documentation
### Phase 5: Removal (Month 10-12)
### Phase 5: Removal
- Remove Lift dependencies
- Remove Jetty dependency
- Single http4s server on port 8080
@ -499,54 +728,68 @@ Priority order:
## Request Routing During Migration
### Option A: Two Separate Ports
### OBP-API-Dispatch
```
Clients → OBP-API-Dispatch (8088/443)
├─→ Lift (8080) - v4, v5, v5.1
└─→ http4s (8081) - v6, v7
```
**OBP-API-Dispatch:**
- Already exists in workspace
- Already http4s-based
- Designed for routing between backends
- Has error handling, logging
- Needs routing logic updates
- Single entry point
- Route by version or endpoint
**Migration Phases:**
**Phase 1: Setup**
```hocon
routing {
http4s_versions = [] # All traffic to Lift
}
```
**Phase 2: First Migration**
```hocon
routing {
http4s_versions = ["v6.0.0"] # v6 to http4s
}
```
**Phase 3: Progressive**
```hocon
routing {
http4s_versions = ["v5.1.0", "v6.0.0", "v7.0.0"]
}
```
**Phase 4: Complete**
```hocon
routing {
http4s_versions = ["v4.0.0", "v5.0.0", "v5.1.0", "v6.0.0", "v7.0.0"]
}
```
### Alternative Options
#### Option A: Two Separate Ports
```
Clients → Load Balancer
├─→ Port 8080 (Lift) - Old endpoints
└─→ Port 8081 (http4s) - New endpoints
├─→ Port 8080 (Lift)
└─→ Port 8081 (http4s)
```
Clients need to know which port to use
**Pros:**
- Simple, clear separation
- Easy to monitor which endpoints are migrated
- No risk of conflicts
**Cons:**
- Clients need to know which port to use
- Load balancer configuration needed
### Option B: Proxy Pattern
#### Option B: Nginx/HAProxy
```
Clients → Port 8080 (Lift)
├─→ Handle locally (Lift endpoints)
└─→ Proxy to 8081 (http4s endpoints)
Clients → Nginx (443) → Backends
```
**Pros:**
- Single port for clients
- Transparent migration
- No client changes needed
**Cons:**
- Additional latency for proxied requests
- More complex routing logic
### Option C: Reverse Proxy (Nginx/HAProxy)
```
Clients → Nginx (Port 443)
├─→ Port 8080 (Lift) - /api/v4.0.0/*
└─→ Port 8081 (http4s) - /api/v6.0.0/*
```
**Pros:**
- Professional solution
- Fine-grained routing rules
- SSL termination
- Load balancing
**Cons:**
- Additional infrastructure component
- Configuration overhead
Additional infrastructure when OBP-API-Dispatch already exists
## Database Access Migration
@ -684,10 +927,7 @@ def createUserHttp4s[F[_]: Concurrent]: HttpRoutes[F] = {
- Same business logic
- Same configuration
5. **Flexible Timeline**
- Migrate at your own pace
- Pause if needed
- No hard deadlines
## Challenges and Solutions
@ -798,22 +1038,6 @@ scenario("Get bank - http4s version") {
- Delete Lift endpoint code
- All traffic to http4s
## Timeline Estimate
### Conservative Approach (12-18 months)
- **Month 1-2:** Setup and infrastructure
- **Month 3-6:** Migrate 25% of endpoints
- **Month 7-10:** Migrate 50% more (75% total)
- **Month 11-14:** Migrate remaining 25%
- **Month 15-16:** Testing and stabilization
- **Month 17-18:** Remove Lift, cleanup
### Aggressive Approach (6-9 months)
- **Month 1:** Setup
- **Month 2-5:** Migrate 80% of endpoints
- **Month 6-7:** Migrate remaining 20%
- **Month 8-9:** Remove Lift
## Conclusion
**Yes, Lift and http4s can coexist** by running on different ports (8080 and 8081) within the same application. This allows for:
@ -822,7 +1046,7 @@ scenario("Get bank - http4s version") {
- Endpoint-by-endpoint conversion
- Shared business logic and resources
- Zero downtime
- Flexible timeline
- Flexible migration pace
The key is to **keep HTTP layer separate from business logic** so both frameworks can call the same underlying functions.