Skip to content

Commit 60dad3f

Browse files
committed
Add Makefile and tests
1 parent e34c80d commit 60dad3f

35 files changed

+2043
-1429
lines changed

.github/workflows/test-module.yml

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,29 @@ on:
99
jobs:
1010
module-toggle-tests:
1111
runs-on: ubuntu-latest
12-
12+
1313
steps:
1414
- name: Checkout code
1515
uses: actions/checkout@v4
16-
17-
- name: Install dependencies
18-
run: |
19-
sudo apt-get update
20-
sudo apt-get install -y curl jq
21-
16+
2217
- name: Set up Docker Buildx
2318
uses: docker/setup-buildx-action@v3
24-
25-
- name: Build and start services
26-
run: |
27-
docker-compose up -d --build
28-
29-
- name: Wait for services to be ready
30-
run: |
31-
echo "Waiting for services..."
32-
sleep 30
33-
34-
# Health check with retries
35-
for i in {1..10}; do
36-
if curl -f http://localhost:8081/health; then
37-
echo "Services are ready"
38-
break
39-
fi
40-
echo "Waiting for services... (attempt $i/10)"
41-
sleep 10
42-
done
43-
44-
- name: Run module configuration tests
45-
run: |
46-
chmod +x ./tests/test_docker_simple.sh
47-
./tests/test_docker_simple.sh
48-
49-
- name: Run original BBR functionality tests
19+
20+
- name: Run Docker tests
5021
run: |
51-
chmod +x ./tests/test_large_body.sh
52-
# Adapt for Docker environment
53-
sed -i 's/localhost:8081/localhost:8081/g' ./tests/test_large_body.sh
54-
./tests/test_large_body.sh
55-
22+
make test-docker
23+
5624
- name: Show service logs on failure
5725
if: failure()
5826
run: |
5927
echo "=== Nginx logs ==="
60-
docker-compose logs nginx
28+
docker compose -f tests/docker-compose.yml logs nginx
6129
echo "=== Echo server logs ==="
62-
docker-compose logs echo-server
30+
docker compose -f tests/docker-compose.yml logs echo-server
6331
echo "=== Mock EPP logs ==="
64-
docker-compose logs mock-epp
65-
32+
docker compose -f tests/docker-compose.yml logs mock-epp
33+
6634
- name: Cleanup
6735
if: always()
6836
run: |
69-
docker-compose down -v
37+
make clean

README.md

Lines changed: 14 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ flowchart TD
100100
A[Client Request] --> B[NGINX]
101101
subgraph NGINX Pod
102102
subgraph NGINX Container
103-
B --1--> C[Inference Module<br/> with internal BBR]
103+
B --1--> C[Inference Module<br/> with Body-Based Routing]
104104
end
105105
end
106106
C -- 2. gRPC headers --> D[EPP Service<br/>Endpoint Picker]
@@ -112,117 +112,30 @@ flowchart TD
112112
Testing
113113
-------
114114

115-
### Quick Start with Docker Compose
115+
For comprehensive testing information, examples, and troubleshooting guides, see [tests/README.md](tests/README.md).
116116

117-
The project includes a complete testing environment with Docker Compose that sets up:
118-
- NGINX with the ngx-inference module loaded
119-
- Mock external processors for both BBR and EPP
120-
- Echo server as upstream target
117+
### Local Development Setup
121118

122-
1. **Start the test environment:**
123-
```bash
124-
docker-compose up --build
125-
```
119+
For local development and testing without Docker:
126120

127-
2. **Test EPP (Endpoint Picker Processor):**
121+
1. **Setup local environment and build the module:**
128122
```bash
129-
# Headers-only request - EPP selects upstream based on headers
130-
curl -i http://localhost:8081/epp-test \
131-
-H "Content-Type: application/json" \
132-
-H "X-Request-Id: test-epp-123"
133-
```
123+
# Setup local development environment
124+
make setup-local
134125

135-
3. **Test BBR (Body-Based Routing) with JSON model detection:**
136-
```bash
137-
# Request with JSON body - BBR extracts model name from "model" field
138-
curl -i http://localhost:8081/bbr-test \
139-
-H "Content-Type: application/json" \
140-
-d '{"model": "gpt-4", "prompt": "Hello world", "temperature": 0.7}'
126+
# Build the module
127+
make build
141128
```
142129

143-
4. **Test BBR with fallback model:**
130+
2. **Start local services and run tests:**
144131
```bash
145-
# JSON without "model" field - BBR uses configured fallback
146-
curl -i http://localhost:8081/bbr-test \
147-
-H "Content-Type: application/json" \
148-
-d '{"prompt": "Hello world", "temperature": 0.7}'
149-
```
132+
# Start local nginx with the compiled module plus mock services
133+
make start-local
150134

151-
5. **Test combined BBR + EPP pipeline:**
152-
```bash
153-
# JSON with model field - both BBR and EPP process the request
154-
curl -i http://localhost:8081/responses \
155-
-H "Content-Type: application/json" \
156-
-H "X-Client-Id: mobile-app" \
157-
-d '{"model": "claude-3", "messages": [{"role": "user", "content": "Hello"}]}'
135+
# Run configuration tests locally
136+
make test-local
158137
```
159138

160-
### Expected Response Headers
161-
162-
When testing, you should see these headers in the echo server response indicating successful processing:
163-
164-
- `x-gateway-model-name` - Set by BBR based on JSON "model" field
165-
- `x-inference-upstream` - Set by EPP for upstream selection
166-
- Original request headers forwarded to the upstream
167-
168-
### Test Environment Components
169-
170-
The Docker Compose stack includes:
171-
172-
- **nginx** (port 8081) - NGINX with ngx-inference module
173-
- **extproc-epp** (internal port 9001) - Mock EPP external processor
174-
- **echo-server** (internal port 80) - Target upstream that echoes request details
175-
176-
### Manual Testing Setup
177-
178-
If you prefer to test without Docker:
179-
180-
1. **Build the module:**
181-
```bash
182-
cargo build --features "vendored,export-modules" --release
183-
```
184-
185-
2. **Start mock external processors:**
186-
```bash
187-
# Terminal 1: BBR mock (port 9000)
188-
EPP_UPSTREAM=localhost:8080 BBR_MODEL=test-model MOCK_ROLE=BBR \
189-
cargo run --bin extproc_mock --features extproc-mock -- 0.0.0.0:9000
190-
191-
# Terminal 2: EPP mock (port 9001)
192-
EPP_UPSTREAM=localhost:8080 MOCK_ROLE=EPP \
193-
cargo run --bin extproc_mock --features extproc-mock -- 0.0.0.0:9001
194-
195-
# Terminal 3: Simple upstream server
196-
python3 -m http.server 8080
197-
```
198-
199-
3. **Configure and start NGINX:**
200-
```nginx
201-
load_module /path/to/target/release/libngx_inference.so;
202-
# ... rest of configuration similar to docker/nginx.conf
203-
```
204-
205-
### Troubleshooting Tests
206-
207-
- **502 Bad Gateway:** Check if external processors are running and reachable
208-
- Enable fail-open mode: `inference_*_failure_mode_allow on`
209-
- Verify endpoints: `inference_bbr_endpoint` and `inference_epp_endpoint`
210-
211-
- **Headers not set:**
212-
- Check external processor logs for JSON parsing errors
213-
- Verify Content-Type is `application/json` for BBR tests
214-
- Ensure JSON contains valid "model" field for BBR
215-
216-
- **DNS resolution errors:**
217-
- In Docker: Use service names (`extproc-bbr:9000`)
218-
- Local testing: Use `localhost` or `127.0.0.1`
219-
- Check NGINX resolver configuration
220-
221-
- **Module not loading:**
222-
- Verify dynamic library path in `load_module` directive
223-
- Check NGINX error log for loading errors
224-
- Ensure library was built with `export-modules` feature
225-
226139
Troubleshooting
227140
---------------
228141
- If EPP endpoints are unreachable or not listening on gRPC, you may see `BAD_GATEWAY` when failure mode allow is off. Toggle `*_failure_mode_allow on` to fail-open during testing.

bin/extproc_mock.rs

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ struct ExtProcMock {
115115
#[tonic::async_trait]
116116
impl ExternalProcessor for ExtProcMock {
117117
type ProcessStream = ReceiverStream<Result<ProcessingResponse, Status>>;
118-
async fn process(&self, request: Request<tonic::Streaming<ProcessingRequest>>) -> Result<Response<Self::ProcessStream>, Status> {
118+
async fn process(
119+
&self,
120+
request: Request<tonic::Streaming<ProcessingRequest>>,
121+
) -> Result<Response<Self::ProcessStream>, Status> {
119122
let mut inbound = request.into_inner();
120123
let (tx, rx) = mpsc::channel::<Result<ProcessingResponse, Status>>(32);
121124
let epp_upstream = self.epp_upstream.clone();
@@ -129,54 +132,123 @@ impl ExternalProcessor for ExtProcMock {
129132
match msg {
130133
Ok(pr) => match pr.request {
131134
Some(processing_request::Request::RequestHeaders(_)) => {
132-
if role == "EPP" {
133-
eprintln!("extproc_mock: EPP headers received, selecting endpoint: {}", epp_upstream);
134-
let resp = ProcessingResponse { response: Some(processing_response::Response::RequestHeaders(build_headers_response(&epp_upstream, &bbr_model))), dynamic_metadata: None, mode_override: None, override_message_timeout: None }; if tx.send(Ok(resp)).await.is_err() { break; } sent_headers_response = true;
135+
if role == "EPP" {
136+
eprintln!(
137+
"extproc_mock: EPP headers received, selecting endpoint: {}",
138+
epp_upstream
139+
);
140+
let resp = ProcessingResponse {
141+
response: Some(processing_response::Response::RequestHeaders(
142+
build_headers_response(&epp_upstream, &bbr_model),
143+
)),
144+
dynamic_metadata: None,
145+
mode_override: None,
146+
override_message_timeout: None,
147+
};
148+
if tx.send(Ok(resp)).await.is_err() {
149+
break;
150+
}
151+
sent_headers_response = true;
135152
} else {
136-
eprintln!("extproc_mock: BBR headers received, waiting for body...");
153+
eprintln!(
154+
"extproc_mock: BBR headers received, waiting for body..."
155+
);
137156
}
138157
}
139158
Some(processing_request::Request::RequestBody(body)) => {
140159
body_buf.extend_from_slice(&body.body);
141-
if body.end_of_stream {
142-
eprintln!("extproc_mock: end of stream, body size: {} bytes", body_buf.len());
143-
if let Ok(v) = serde_json::from_slice::<Value>(&body_buf) {
144-
if let Some(m) = v.get("model").and_then(|x| x.as_str()) {
160+
if body.end_of_stream {
161+
eprintln!(
162+
"extproc_mock: end of stream, body size: {} bytes",
163+
body_buf.len()
164+
);
165+
if let Ok(v) = serde_json::from_slice::<Value>(&body_buf) {
166+
if let Some(m) = v.get("model").and_then(|x| x.as_str()) {
145167
current_bbr_model = m.to_string();
146-
eprintln!("extproc_mock: detected model in JSON body: {}", current_bbr_model);
147-
}
148-
}
149-
let resp = ProcessingResponse { response: Some(processing_response::Response::RequestBody(build_body_response(&epp_upstream, &current_bbr_model))), dynamic_metadata: None, mode_override: None, override_message_timeout: None };
168+
eprintln!(
169+
"extproc_mock: detected model in JSON body: {}",
170+
current_bbr_model
171+
);
172+
}
173+
}
174+
let resp = ProcessingResponse {
175+
response: Some(processing_response::Response::RequestBody(
176+
build_body_response(&epp_upstream, &current_bbr_model),
177+
)),
178+
dynamic_metadata: None,
179+
mode_override: None,
180+
override_message_timeout: None,
181+
};
150182
if role == "BBR" {
151-
eprintln!("extproc_mock: BBR final response - model: {}", current_bbr_model);
183+
eprintln!(
184+
"extproc_mock: BBR final response - model: {}",
185+
current_bbr_model
186+
);
187+
}
188+
if tx.send(Ok(resp)).await.is_err() {
189+
break;
152190
}
153-
if tx.send(Ok(resp)).await.is_err() { break; }
154191
} else {
155192
eprintln!("extproc_mock: received body chunk, size: {} bytes, total: {} bytes", body.body.len(), body_buf.len());
156193
}
157194
}
158195
_ => {}
159196
},
160-
Err(status) => { let _ = tx.send(Err(status)).await; break; }
197+
Err(status) => {
198+
let _ = tx.send(Err(status)).await;
199+
break;
200+
}
161201
}
162202
}
163-
if !sent_headers_response && role == "EPP" { let resp = ProcessingResponse { response: Some(processing_response::Response::RequestHeaders(build_headers_response(&epp_upstream, &bbr_model))), dynamic_metadata: None, mode_override: None, override_message_timeout: None }; let _ = tx.send(Ok(resp)).await; }
203+
if !sent_headers_response && role == "EPP" {
204+
let resp = ProcessingResponse {
205+
response: Some(processing_response::Response::RequestHeaders(
206+
build_headers_response(&epp_upstream, &bbr_model),
207+
)),
208+
dynamic_metadata: None,
209+
mode_override: None,
210+
override_message_timeout: None,
211+
};
212+
let _ = tx.send(Ok(resp)).await;
213+
}
164214
});
165215
Ok(Response::new(ReceiverStream::new(rx)))
166216
}
167217
}
168218

169219
#[tokio::main(flavor = "multi_thread")]
170220
async fn main() -> Result<(), Box<dyn std::error::Error>> {
171-
let addr: SocketAddr = std::env::args().nth(1).unwrap_or_else(|| "0.0.0.0:9001".to_string()).parse()?;
172-
let epp_upstream = env::var("EPP_UPSTREAM").unwrap_or_else(|_| "host.docker.internal:18080".to_string());
221+
let addr: SocketAddr = std::env::args()
222+
.nth(1)
223+
.unwrap_or_else(|| "0.0.0.0:9001".to_string())
224+
.parse()?;
225+
let epp_upstream =
226+
env::var("EPP_UPSTREAM").unwrap_or_else(|_| "host.docker.internal:18080".to_string());
173227
let bbr_model = env::var("BBR_MODEL").unwrap_or_else(|_| "bbr-chosen-model".to_string());
174-
let default_role = if addr.port() == 9001 { "EPP" } else if addr.port() == 9000 { "BBR" } else { "EPP" };
228+
let default_role = if addr.port() == 9001 {
229+
"EPP"
230+
} else if addr.port() == 9000 {
231+
"BBR"
232+
} else {
233+
"EPP"
234+
};
175235
let role = env::var("MOCK_ROLE").unwrap_or_else(|_| default_role.to_string());
176236

177-
println!("extproc_mock: role={}, configured EPP_UPSTREAM={}, BBR_MODEL={}", role, epp_upstream, bbr_model);
237+
println!(
238+
"extproc_mock: role={}, configured EPP_UPSTREAM={}, BBR_MODEL={}",
239+
role, epp_upstream, bbr_model
240+
);
178241

179-
let svc = ExtProcMock { epp_upstream, bbr_model, role };
242+
let svc = ExtProcMock {
243+
epp_upstream,
244+
bbr_model,
245+
role,
246+
};
180247

181248
println!("extproc_mock listening on {}", addr);
182-
tonic::transport::Server::builder().add_service(ExternalProcessorServer::new(svc)).serve(addr).await?; Ok(()) }
249+
tonic::transport::Server::builder()
250+
.add_service(ExternalProcessorServer::new(svc))
251+
.serve(addr)
252+
.await?;
253+
Ok(())
254+
}

docker/echo-server/custom-echo-server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ app.use('/', (req, res) => {
2929
},
3030
environment: process.env
3131
};
32-
32+
3333
const bodySize = req.body ? JSON.stringify(req.body).length : 0;
3434
console.log(`${new Date().toISOString()} ${req.method} ${req.originalUrl} - Body size: ${bodySize} bytes`);
35-
35+
3636
res.json(response);
3737
});
3838

0 commit comments

Comments
 (0)