Add resilience: Spring Framework 7 @Retryable and @ConcurrencyLimit

Companion code for "Spring Framework 7's Built-in Resilience: @Retryable,
@ConcurrencyLimit, and What's Left for Resilience4j". Every retry counted
by recording real invocations: defaults, backoff and jitter, timeout,
reactive and CompletableFuture returns, the concurrency limit's BLOCK and
REJECT policies, retries around transactions, composition with
Resilience4j 2.4.0, and the annotation API across 7.0.0-7.0.9.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
This commit is contained in:
2026-09-11 17:12:25 +00:00
co-authored by Claude Opus 5
parent 926250e1a9
commit 7e1676c763
61 changed files with 2155 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Which attributes exist in which 7.0.x release, read by reflection from the jars themselves.
# Needs spring-context/spring-core 7.0.0-7.0.3 in the local repository, e.g.
# mvn dependency:get -Dartifact=org.springframework:spring-context:7.0.0 (and 7.0.1, 7.0.2, 7.0.3,
# plus the matching spring-core)
# -> docs/output/api-surface.txt
set -uo pipefail
source "$(dirname "$0")/env.sh"
M2="${M2:-$HOME/.m2/repository}"
S="$M2/org/springframework"
cat > /tmp/api-surface.jsh <<'JSH'
String attrs(Class<?> c) { var s = new java.util.TreeSet<String>(); for (var m : c.getDeclaredMethods()) if (java.lang.reflect.Modifier.isPublic(m.getModifiers())) s.add(m.getName()); return s.toString(); }
System.out.println(" @Retryable " + attrs(org.springframework.resilience.annotation.Retryable.class));
System.out.println(" @ConcurrencyLimit " + attrs(org.springframework.resilience.annotation.ConcurrencyLimit.class));
System.out.println(" RetryPolicy.Builder " + attrs(org.springframework.core.retry.RetryPolicy.Builder.class));
System.out.println(" RetryListener " + attrs(org.springframework.core.retry.RetryListener.class));
/exit
JSH
{
echo "# Public API of the resilience support, per Spring Framework release"
for v in 7.0.0 7.0.1 7.0.2 7.0.3 7.0.9; do
echo; echo "## $v"
CP="$S/spring-context/$v/spring-context-$v.jar:$S/spring-core/$v/spring-core-$v.jar:$S/spring-aop/7.0.9/spring-aop-7.0.9.jar:$S/spring-beans/7.0.9/spring-beans-7.0.9.jar:$(ls "$M2"/org/jspecify/jspecify/*/jspecify-*.jar | head -1)"
jshell --class-path "$CP" -q /tmp/api-surface.jsh 2>&1 | grep -v 'Picked up' | sed 's/^jshell> //'
done
} > "$OUT/api-surface.txt"
cat "$OUT/api-surface.txt"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Run demo scenarios against a running application and commit each report.
# ./scripts/demo.sh retry defaults exhausted ... -> docs/output/retry-<scenario>.txt
set -uo pipefail
source "$(dirname "$0")/env.sh"
group="$1"; shift
for scenario in "$@"; do
file="$OUT/$group-$scenario.txt"
{
echo "# GET /demo/$group/$scenario (Spring Framework 7.0.9, Resilience4j 2.4.0, JDK 25)"
echo "# Source: src/main/java/com/ankurm/resilience/web/DemoController.java"
echo
curl -s "$BASE/demo/$group/$scenario" | python3 -m json.tool
} > "$file"
echo "wrote $file"
done
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Shared settings for every script in this module.
MODULE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="$MODULE_DIR/docs/output"
JAR="$MODULE_DIR/target/resilience-1.0.0.jar"
PORT="${PORT:-8081}"
BASE="http://localhost:$PORT"
PIDFILE="$MODULE_DIR/target/app.pid"
LOG="$MODULE_DIR/target/app.log"
mkdir -p "$OUT"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Regenerate every transcript under docs/output/. About a minute and a half - several scenarios
# sleep through Spring's one-second default delay on purpose.
set -euo pipefail
cd "$(dirname "$0")/.."
source scripts/env.sh
mvn -q -DskipTests package
./scripts/run.sh
./scripts/demo.sh retry defaults exhausted exponential jitter includes cause future mono timeout hang self-invocation
./scripts/demo.sh tx standalone joined
./scripts/demo.sh limit none block reject limit-and-retry r4j-bulkhead
./scripts/demo.sh r4j retry breaker combo timelimiter
./scripts/demo.sh proxy payments stock-writer reports gateway
{
echo "# Spring retry has no metrics of its own; the only meter is the one RetryEvents registers."
echo "\$ curl /actuator/metrics | grep -iE 'retry|resilience4j'"
curl -s "$BASE/actuator/metrics" | python3 -c 'import json,sys
print("\n".join(n for n in json.load(sys.stdin)["names"] if "retry" in n or "resilience4j" in n))'
} > "$OUT/metrics-names.txt"
./scripts/stop.sh
EXTRA_ARGS=--demo.resilience.enabled=false ./scripts/run.sh
{
echo "# The same @Retryable method with @EnableResilientMethods switched off (demo.resilience.enabled=false)"
echo
curl -s "$BASE/demo/retry/defaults" | python3 -m json.tool
echo
echo "# Startup log lines mentioning retry or resilience:"
grep -E 'Retr|Resilient|ConcurrencyLimit' "$LOG" | grep -v 'Picked up' || echo "(none)"
} > "$OUT/retry-disabled.txt"
./scripts/stop.sh
./scripts/demo-api-surface.sh
echo "Regenerated: $(ls docs/output | wc -l) files in docs/output/"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Start the application (optional comma-separated profiles) and wait until it
# answers. Any instance started by a previous run is stopped first, by PID file - never by
# pattern-matching the process list, which can match (and kill) the calling shell.
# ./scripts/run.sh
# EXTRA_ARGS=--demo.resilience.enabled=false ./scripts/run.sh
set -euo pipefail
source "$(dirname "$0")/env.sh"
"$MODULE_DIR/scripts/stop.sh"
PROFILES="${1:-}"
[ -f "$JAR" ] || (cd "$MODULE_DIR" && mvn -q -DskipTests package)
nohup java -jar "$JAR" --server.port="$PORT" ${PROFILES:+--spring.profiles.active=$PROFILES} ${EXTRA_ARGS:-} \
> "$LOG" 2>&1 < /dev/null &
echo $! > "$PIDFILE"
for _ in $(seq 1 60); do
if curl -s -o /dev/null "$BASE/actuator/health"; then exit 0; fi
if ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "application exited - see $LOG" >&2; exit 1; fi
sleep 0.5
done
echo "application did not start within 30s - see $LOG" >&2; exit 1
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
source "$(dirname "$0")/env.sh"
if [ -f "$PIDFILE" ]; then
PID="$(cat "$PIDFILE")"
kill "$PID" 2>/dev/null || true
for _ in $(seq 1 40); do kill -0 "$PID" 2>/dev/null || break; sleep 0.25; done
kill -9 "$PID" 2>/dev/null || true
rm -f "$PIDFILE"
fi