Product, order and inventory services behind Spring Cloud Gateway (WebMVC), configuration from a Config Server, discovery with Eureka, a Resilience4j circuit breaker on order -> inventory and Brave tracing. Run as six plain jars; output/ holds real transcripts (config, registry, requests, trace propagation, circuit breaker, startup race and the load-balanced RestClient.Builder trap). Dockerfile and docker-compose.yml are included but were not run. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HhW8LD8r3EigoBaCGrQZx5
127 lines
6.3 KiB
Bash
Executable File
127 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Runs the scenarios against a running system (scripts/start.sh) and writes output/NN-*.txt.
|
|
# Every file under output/ is the real response of a real request; nothing is typed by hand.
|
|
set -uo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
GW=http://localhost:8080
|
|
OUT=output
|
|
mkdir -p $OUT run/tmp
|
|
|
|
# curl wrapper: prints the command, then "HTTP <status>" and the body.
|
|
call() { # file method path [json-body]
|
|
local file=$1 method=$2 path=$3 body=${4:-} args=()
|
|
[ -n "$body" ] && args=(-H 'Content-Type: application/json' -d "$body")
|
|
local code; code=$(curl -s -o run/tmp/body -w '%{http_code}' -X "$method" "${args[@]}" "$GW$path")
|
|
{ printf '$ curl -X %s %s%s\n' "$method" "localhost:8080$path" "${body:+ -d '$body'}"
|
|
printf 'HTTP %s\n' "$code"; [ -s run/tmp/body ] && { cat run/tmp/body; echo; }; echo; } >> "$file"
|
|
}
|
|
section() { printf -- '--- %s ---\n' "$2" >> "$1"; }
|
|
trace_lines() { # trace id -> "service | traceId-spanId | message" for every service, in time order
|
|
for s in gateway order-service product-service inventory-service; do
|
|
grep -h "$1" run/logs/$s.log
|
|
done | sort | sed -E 's/^[^ ]+ +[A-Z]+ [0-9]+ --- \[([^]]*)\] \[[^]]*\] \[([^]]*)\] [^ ]+ *: (.*)$/\1 | \2 | \3/'
|
|
}
|
|
|
|
# The gateway and its load balancer refresh their copy of the registry every 30 seconds,
|
|
# so the first requests after startup fail (the gateway answers 500: "Unable to find instance"). Wait until the gateway routes work and the order service can reach both of its dependencies.
|
|
echo "waiting for the gateway to see all services..."
|
|
R=$OUT/00-startup-race.txt; : > $R
|
|
echo "# GET /api/orders/ready through the gateway, every 2 seconds, starting the moment the last service reported healthy." >> $R
|
|
echo "# 200 means the gateway can route to the order service AND the order service can reach product and inventory." >> $R
|
|
echo >> $R
|
|
for path in /api/products/1 /api/inventory/1 /api/orders/ready; do
|
|
start=$(date +%s)
|
|
for _ in $(seq 1 60); do
|
|
code=$(curl -s -o run/tmp/body -w '%{http_code}' $GW$path)
|
|
printf '%-20s t+%2ss HTTP %s %s\n' "$path" "$(( $(date +%s) - start ))" "$code" "$(head -c 90 run/tmp/body | tr -d '\n')" >> $R
|
|
[ "$code" = 200 ] && break
|
|
sleep 2
|
|
done
|
|
done
|
|
|
|
# ---- 01 config server
|
|
F=$OUT/01-config-server.txt; : > $F
|
|
echo "# What the config server hands out (port 8888). The services fetch this at startup." >> $F; echo >> $F
|
|
section $F "order-service: which files were used and the values"
|
|
curl -s localhost:8888/order-service/default | python3 -c '
|
|
import sys, json
|
|
d = json.load(sys.stdin)
|
|
print("name:", d["name"], " profiles:", d["profiles"])
|
|
for ps in d["propertySources"]:
|
|
print("source:", ps["name"].split("/")[-1])
|
|
for k, v in ps["source"].items():
|
|
print(" ", k, "=", v)' >> $F
|
|
echo >> $F
|
|
section $F "the same value as the order service itself reports it (GET /api/orders/config through the gateway)"
|
|
call $F GET /api/orders/config
|
|
|
|
# ---- 02 registry (a service shows up in Eureka after its first registration, which can lag its startup by a few seconds)
|
|
for _ in $(seq 1 60); do
|
|
n=$(curl -s -H 'Accept: application/json' localhost:8761/eureka/apps | python3 -c 'import sys,json; print(sum(len(a["instance"]) for a in json.load(sys.stdin)["applications"]["application"]))' 2>/dev/null || echo 0)
|
|
[ "$n" -ge 4 ] && break
|
|
sleep 2
|
|
done
|
|
F=$OUT/02-eureka-registry.txt; : > $F
|
|
echo "# What is registered in Eureka (port 8761)" >> $F; echo >> $F
|
|
curl -s -H 'Accept: application/json' localhost:8761/eureka/apps | python3 -c '
|
|
import sys, json
|
|
for a in sorted(json.load(sys.stdin)["applications"]["application"], key=lambda a: a["name"]):
|
|
for i in a["instance"]:
|
|
print("%-18s instance=%-22s status=%s" % (a["name"], i["instanceId"], i["status"]))' >> $F
|
|
|
|
# ---- 03 requests through the gateway
|
|
F=$OUT/03-gateway-requests.txt; : > $F
|
|
echo "# Requests through the gateway (port 8080). The client only ever talks to 8080." >> $F; echo >> $F
|
|
section $F "products"
|
|
call $F GET /api/products
|
|
call $F GET /api/products/2
|
|
call $F GET /api/products/99
|
|
section $F "stock"
|
|
call $F GET /api/inventory/3
|
|
section $F "orders"
|
|
call $F POST /api/orders '{"productId":1,"quantity":2}'
|
|
call $F POST /api/orders '{"productId":3,"quantity":4}'
|
|
call $F POST /api/orders '{"productId":2,"quantity":11}'
|
|
call $F POST /api/orders '{"productId":99,"quantity":1}'
|
|
call $F GET /api/orders
|
|
section $F "stock after the first order"
|
|
call $F GET /api/inventory/1
|
|
|
|
# ---- 04 tracing
|
|
F=$OUT/04-trace-propagation.txt; : > $F
|
|
echo "# One request, one trace id, four services. Lines are 'service | traceId-spanId | message' from each service's log." >> $F; echo >> $F
|
|
curl -s -o /dev/null -X POST -H 'Content-Type: application/json' -d '{"productId":2,"quantity":1}' $GW/api/orders
|
|
sleep 1
|
|
TID=$(grep -h "placing order for product 2" run/logs/order-service.log | tail -1 | sed -E 's/.*\[([0-9a-f]{32})-[0-9a-f]{16}\].*/\1/')
|
|
section $F "POST /api/orders sent to the gateway; trace id $TID"
|
|
trace_lines "$TID" >> $F
|
|
echo >> $F
|
|
section $F "a caller that already has a trace id (W3C traceparent header): the same id comes out the other end"
|
|
CALLER=4bf92f3577b34da6a3ce929d0e0e4736
|
|
curl -s -o /dev/null -H "traceparent: 00-$CALLER-00f067aa0ba902b7-01" $GW/api/products/1
|
|
sleep 1
|
|
trace_lines "$CALLER" >> $F
|
|
|
|
# ---- 05 circuit breaker
|
|
F=$OUT/05-circuit-breaker.txt; : > $F
|
|
echo "# order-service -> inventory-service behind a Resilience4j circuit breaker (4-call window, 50% threshold, 5 s open)" >> $F; echo >> $F
|
|
section $F "healthy: circuit state, then one order"
|
|
call $F GET /api/orders/circuit
|
|
call $F POST /api/orders '{"productId":1,"quantity":1}'
|
|
section $F "switch inventory-service to failing (POST /inventory/chaos?down=true), then place 4 orders"
|
|
call $F POST '/api/inventory/chaos?down=true'
|
|
for _ in 1 2 3 4; do call $F POST /api/orders '{"productId":1,"quantity":1}'; done
|
|
call $F GET /api/orders/circuit
|
|
section $F "circuit is OPEN: 2 more orders. inventory reserve calls before and after prove the calls were not even attempted"
|
|
call $F GET /api/inventory/stats
|
|
call $F POST /api/orders '{"productId":1,"quantity":1}'
|
|
call $F POST /api/orders '{"productId":1,"quantity":1}'
|
|
call $F GET /api/inventory/stats
|
|
call $F GET /api/orders/circuit
|
|
section $F "inventory-service recovers; wait 6 seconds (> the 5 s open window), then order again"
|
|
call $F POST '/api/inventory/chaos?down=false'
|
|
sleep 6
|
|
call $F POST /api/orders '{"productId":1,"quantity":1}'
|
|
call $F GET /api/orders/circuit
|
|
echo "demo finished; files:"; ls -1 $OUT
|