Files
Claude def73fa1c4 Add spring-boot-microservices-project: config server, Eureka, gateway and three services on Boot 4.1.1 + Spring Cloud 2025.1.3
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
2026-10-21 17:08:42 +00:00

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