Skip to content

Commit a5f15df

Browse files
committed
feat: Add critical improvements - idempotency, atomic inventory, circuit breaker, validation, and structured logging
1 parent 5310c03 commit a5f15df

11 files changed

Lines changed: 1321 additions & 54 deletions

File tree

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ WORKDIR /app
33
COPY go.mod go.sum ./
44
RUN go mod download
55
COPY . .
6-
# Build both binaries
7-
RUN go build -o gateway-bin ./gateway/main.go
8-
RUN go build -o processor-bin ./processor/main.go
6+
# Build both binaries (build entire packages, not single files)
7+
RUN go build -o gateway-bin ./gateway
8+
RUN go build -o processor-bin ./processor
99

1010
FROM alpine:latest
1111
WORKDIR /root/

TESTING.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# Comprehensive Testing Guide
2+
3+
This guide covers all features of the Flash Sale Engine and how to test them.
4+
5+
## Quick Start
6+
7+
```powershell
8+
# Start services
9+
docker-compose up -d
10+
11+
# Wait for services to be ready
12+
Start-Sleep -Seconds 10
13+
14+
# Run comprehensive test suite
15+
.\test-all-features.ps1
16+
```
17+
18+
## Feature Test Matrix
19+
20+
### ✅ Feature 1: Input Validation
21+
22+
**What it does**: Validates all input fields before processing.
23+
24+
**How to test**:
25+
```powershell
26+
# Test missing user_id
27+
curl -X POST http://localhost:8080/buy `
28+
-H "Content-Type: application/json" `
29+
-d '{"item_id":"101","amount":1,"request_id":"test"}'
30+
# Expected: 400 Bad Request
31+
32+
# Test negative amount
33+
curl -X POST http://localhost:8080/buy `
34+
-H "Content-Type: application/json" `
35+
-d '{"user_id":"u1","item_id":"101","amount":-1,"request_id":"test"}'
36+
# Expected: 400 Bad Request
37+
38+
# Test amount too large
39+
curl -X POST http://localhost:8080/buy `
40+
-H "Content-Type: application/json" `
41+
-d '{"user_id":"u1","item_id":"101","amount":2000,"request_id":"test"}'
42+
# Expected: 400 Bad Request
43+
```
44+
45+
**Expected Results**:
46+
- Invalid inputs return `400 Bad Request`
47+
- Error messages include field names and validation errors
48+
- Valid requests proceed normally
49+
50+
---
51+
52+
### ✅ Feature 2: Idempotency
53+
54+
**What it does**: Prevents duplicate order processing using Redis SETNX.
55+
56+
**How to test**:
57+
```powershell
58+
# First request
59+
$body = '{"user_id":"u1","item_id":"101","amount":1,"request_id":"unique-123"}'
60+
curl -X POST http://localhost:8080/buy -H "Content-Type: application/json" -d $body
61+
# Expected: 202 Accepted
62+
63+
# Duplicate request (same request_id)
64+
curl -X POST http://localhost:8080/buy -H "Content-Type: application/json" -d $body
65+
# Expected: 409 Conflict
66+
```
67+
68+
**Expected Results**:
69+
- First request: `202 Accepted`
70+
- Duplicate request: `409 Conflict`
71+
- Response includes correlation ID
72+
73+
---
74+
75+
### ✅ Feature 3: Atomic Inventory (Lua Scripts)
76+
77+
**What it does**: Uses Redis Lua scripts for atomic inventory operations.
78+
79+
**How to test**:
80+
```powershell
81+
# Seed inventory
82+
docker exec flash-sale-engine-redis-1 redis-cli SET inventory:101 10
83+
84+
# Send 15 rapid orders
85+
for ($i=1; $i -le 15; $i++) {
86+
$body = "{\"user_id\":\"u$i\",\"item_id\":\"101\",\"amount\":1,\"request_id\":\"atomic-$i\"}"
87+
Invoke-WebRequest -Uri "http://localhost:8080/buy" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
88+
}
89+
90+
# Check inventory (should never go below 0)
91+
docker exec flash-sale-engine-redis-1 redis-cli GET inventory:101
92+
```
93+
94+
**Expected Results**:
95+
- Inventory never goes negative
96+
- All operations are atomic (no race conditions)
97+
- Sold out orders are automatically refunded by Lua script
98+
99+
**Key Code**: `processor/redis_scripts.go` - Lua script ensures DECR and refund are atomic
100+
101+
---
102+
103+
### ✅ Feature 4: Structured Logging with Correlation IDs
104+
105+
**What it does**: All logs include correlation IDs for request tracing.
106+
107+
**How to test**:
108+
```powershell
109+
# Send an order
110+
$body = '{"user_id":"u1","item_id":"101","amount":1,"request_id":"log-test"}'
111+
$response = Invoke-WebRequest -Uri "http://localhost:8080/buy" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
112+
$correlationId = ($response.Content | ConvertFrom-Json).correlation_id
113+
114+
# Check gateway logs
115+
docker logs flash-sale-engine-gateway-1 --tail 20 | Select-String $correlationId
116+
117+
# Check processor logs
118+
docker logs flash-sale-engine-processor-1 --tail 20 | Select-String $correlationId
119+
```
120+
121+
**Expected Results**:
122+
- All logs are JSON formatted
123+
- Correlation ID appears in gateway and processor logs
124+
- Can trace a request across services
125+
126+
**Key Code**:
127+
- `common/logger.go` - Structured JSON logger
128+
- `gateway/main.go` - Generates UUID correlation IDs
129+
- `processor/main.go` - Extracts correlation IDs from Kafka headers
130+
131+
---
132+
133+
### ✅ Feature 5: Health Check Endpoint
134+
135+
**What it does**: Provides health status of Redis, Kafka, and circuit breaker.
136+
137+
**How to test**:
138+
```powershell
139+
# Check health
140+
curl http://localhost:8080/health
141+
142+
# Expected JSON response:
143+
# {
144+
# "status": "healthy",
145+
# "redis": true,
146+
# "kafka": true,
147+
# "circuit_breaker_state": "Closed"
148+
# }
149+
```
150+
151+
**Expected Results**:
152+
- Returns `200 OK` when healthy
153+
- Returns `503 Service Unavailable` when unhealthy
154+
- Shows circuit breaker state
155+
156+
**Key Code**: `gateway/main.go` - `/health` endpoint
157+
158+
---
159+
160+
### ✅ Feature 6: Circuit Breaker
161+
162+
**What it does**: Prevents cascading failures when Kafka is down.
163+
164+
**How to test**:
165+
```powershell
166+
# Stop Kafka/Redpanda
167+
docker-compose stop redpanda
168+
169+
# Send multiple requests (will fail)
170+
for ($i=1; $i -le 6; $i++) {
171+
$body = "{\"user_id\":\"u$i\",\"item_id\":\"101\",\"amount\":1,\"request_id\":\"cb-test-$i\"}"
172+
try {
173+
Invoke-WebRequest -Uri "http://localhost:8080/buy" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
174+
} catch {
175+
Write-Host "Request $i : $($_.Exception.Response.StatusCode.value__)"
176+
}
177+
}
178+
179+
# Check health endpoint (circuit should be Open)
180+
curl http://localhost:8080/health
181+
182+
# Restart Kafka
183+
docker-compose start redpanda
184+
185+
# Wait for circuit to recover (30 seconds timeout)
186+
Start-Sleep -Seconds 35
187+
188+
# Check health again (circuit should be Closed)
189+
curl http://localhost:8080/health
190+
```
191+
192+
**Expected Results**:
193+
- After 5 failures, circuit opens
194+
- Gateway returns `503 Service Unavailable` when circuit is open
195+
- Circuit recovers after timeout period
196+
197+
**Key Code**: `gateway/circuit_breaker.go` - Circuit breaker implementation
198+
199+
---
200+
201+
### ✅ Feature 7: Dead Letter Queue (DLQ)
202+
203+
**What it does**: Failed orders are moved to DLQ for manual processing.
204+
205+
**How to test**:
206+
```powershell
207+
# Send orders (10% will fail due to simulated payment timeout)
208+
for ($i=1; $i -le 20; $i++) {
209+
$body = "{\"user_id\":\"u$i\",\"item_id\":\"101\",\"amount\":1,\"request_id\":\"dlq-test-$i\"}"
210+
Invoke-WebRequest -Uri "http://localhost:8080/buy" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
211+
}
212+
213+
# Check processor logs for DLQ messages
214+
docker logs flash-sale-engine-processor-1 --tail 50 | Select-String "DLQ"
215+
```
216+
217+
**Expected Results**:
218+
- ~10% of orders fail (simulated payment timeout)
219+
- Failed orders moved to `orders-dlq` topic
220+
- Inventory is refunded for failed orders
221+
- Correlation IDs preserved in DLQ messages
222+
223+
**Key Code**: `processor/main.go` - `moveToDLQ()` function
224+
225+
---
226+
227+
## Manual Testing Scenarios
228+
229+
### Scenario 1: High Concurrency Test
230+
231+
```powershell
232+
# Seed inventory
233+
docker exec flash-sale-engine-redis-1 redis-cli SET inventory:101 100
234+
235+
# Send 100 concurrent requests
236+
$jobs = 1..100 | ForEach-Object {
237+
$body = "{\"user_id\":\"u$_\",\"item_id\":\"101\",\"amount\":1,\"request_id\":\"concurrent-$_\"}"
238+
Start-Job -ScriptBlock {
239+
param($uri, $body)
240+
try {
241+
Invoke-WebRequest -Uri $uri -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
242+
return "SUCCESS"
243+
} catch {
244+
return "FAILED"
245+
}
246+
} -ArgumentList "http://localhost:8080/buy", $body
247+
}
248+
249+
# Wait and check results
250+
$results = $jobs | Wait-Job | Receive-Job
251+
$jobs | Remove-Job
252+
$successCount = ($results | Where-Object { $_ -eq "SUCCESS" }).Count
253+
Write-Host "Success: $successCount / 100"
254+
255+
# Verify inventory
256+
docker exec flash-sale-engine-redis-1 redis-cli GET inventory:101
257+
```
258+
259+
### Scenario 2: Correlation ID Tracing
260+
261+
```powershell
262+
# Send order and capture correlation ID
263+
$body = '{"user_id":"u1","item_id":"101","amount":1,"request_id":"trace-test"}'
264+
$response = Invoke-WebRequest -Uri "http://localhost:8080/buy" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing
265+
$correlationId = ($response.Content | ConvertFrom-Json).correlation_id
266+
267+
# Trace through all logs
268+
Write-Host "Gateway logs:"
269+
docker logs flash-sale-engine-gateway-1 --tail 50 | Select-String $correlationId
270+
271+
Write-Host "`nProcessor logs:"
272+
docker logs flash-sale-engine-processor-1 --tail 50 | Select-String $correlationId
273+
```
274+
275+
### Scenario 3: Circuit Breaker Recovery
276+
277+
```powershell
278+
# Monitor circuit breaker state
279+
while ($true) {
280+
$health = (Invoke-WebRequest -Uri "http://localhost:8080/health" -UseBasicParsing).Content | ConvertFrom-Json
281+
Write-Host "$(Get-Date -Format 'HH:mm:ss') - Circuit: $($health.circuit_breaker_state)"
282+
Start-Sleep -Seconds 5
283+
}
284+
```
285+
286+
## Verification Checklist
287+
288+
- [ ] Input validation rejects invalid requests
289+
- [ ] Idempotency prevents duplicate orders
290+
- [ ] Inventory never goes negative (atomic operations)
291+
- [ ] Correlation IDs appear in all logs
292+
- [ ] Health endpoint shows service status
293+
- [ ] Circuit breaker opens after failures
294+
- [ ] DLQ receives failed orders
295+
- [ ] Inventory refunded on failures
296+
- [ ] High concurrency handled correctly
297+
- [ ] No data corruption under load
298+
299+
## Troubleshooting
300+
301+
**Services not starting?**
302+
```powershell
303+
docker-compose logs
304+
docker-compose ps
305+
```
306+
307+
**Can't see correlation IDs in logs?**
308+
```powershell
309+
# Check if JSON logging is enabled
310+
docker logs flash-sale-engine-gateway-1 --tail 5
311+
# Should see JSON formatted logs
312+
```
313+
314+
**Circuit breaker not opening?**
315+
```powershell
316+
# Check Kafka is actually down
317+
docker-compose ps redpanda
318+
# Send 6 requests to trigger circuit open
319+
```
320+
321+
**Inventory mismatch?**
322+
```powershell
323+
# Reset inventory
324+
docker exec flash-sale-engine-redis-1 redis-cli SET inventory:101 100
325+
# Check all keys
326+
docker exec flash-sale-engine-redis-1 redis-cli KEYS "*"
327+
```
328+

common/logger.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package common
2+
3+
import (
4+
"os"
5+
6+
"github.com/sirupsen/logrus"
7+
)
8+
9+
var Logger *logrus.Logger
10+
11+
func InitLogger() *logrus.Logger {
12+
logger := logrus.New()
13+
14+
// Configure JSON formatter for structured logging
15+
// JSON format enables easy parsing by log aggregation tools (ELK, Splunk, etc.)
16+
logger.SetFormatter(&logrus.JSONFormatter{
17+
TimestampFormat: "2006-01-02T15:04:05.000Z07:00", // ISO 8601 format
18+
FieldMap: logrus.FieldMap{
19+
logrus.FieldKeyTime: "timestamp",
20+
logrus.FieldKeyLevel: "level",
21+
logrus.FieldKeyMsg: "message",
22+
},
23+
})
24+
25+
// Set log level from environment variable (LOG_LEVEL) or default to INFO
26+
// Allows runtime log level adjustment without code changes
27+
logLevel := os.Getenv("LOG_LEVEL")
28+
if logLevel == "" {
29+
logLevel = "info"
30+
}
31+
32+
level, err := logrus.ParseLevel(logLevel)
33+
if err != nil {
34+
level = logrus.InfoLevel // Default to INFO if invalid level specified
35+
}
36+
logger.SetLevel(level)
37+
38+
// Output to stdout for containerized environments
39+
// Logs are captured by Docker/Kubernetes logging infrastructure
40+
logger.SetOutput(os.Stdout)
41+
42+
Logger = logger
43+
return logger
44+
}
45+
46+
// WithCorrelationID creates a logger entry with correlation ID for request tracing
47+
// All log entries created from this will include the correlation_id field
48+
// This enables tracing a single request across gateway and processor services
49+
func WithCorrelationID(correlationID string) *logrus.Entry {
50+
if Logger == nil {
51+
InitLogger()
52+
}
53+
return Logger.WithField("correlation_id", correlationID)
54+
}
55+

0 commit comments

Comments
 (0)