refactoring(ping-service)
TODO-Roadmap.md
This commit is contained in:
@@ -47,6 +47,9 @@ mockk = "1.13.8"
|
|||||||
assertj = "3.24.2"
|
assertj = "3.24.2"
|
||||||
testcontainers = "1.19.6"
|
testcontainers = "1.19.6"
|
||||||
|
|
||||||
|
# --- Resilience4j ---
|
||||||
|
resilience4j = "2.2.0"
|
||||||
|
|
||||||
# --- Utilities ---
|
# --- Utilities ---
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
bignum = "0.3.9"
|
bignum = "0.3.9"
|
||||||
@@ -141,6 +144,11 @@ zipkin-sender-okhttp3 = { module = "io.zipkin.reporter2:zipkin-sender-okhttp3",
|
|||||||
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" }
|
zipkin-server = { module = "io.zipkin:zipkin-server", version.ref = "zipkin" }
|
||||||
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" }
|
zipkin-autoconfigure-ui = { module = "io.zipkin:zipkin-autoconfigure-ui", version.ref = "zipkin" }
|
||||||
|
|
||||||
|
# --- Resilience4j ---
|
||||||
|
resilience4j-spring-boot3 = { module = "io.github.resilience4j:resilience4j-spring-boot3", version.ref = "resilience4j" }
|
||||||
|
resilience4j-reactor = { module = "io.github.resilience4j:resilience4j-reactor", version.ref = "resilience4j" }
|
||||||
|
spring-boot-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop" }
|
||||||
|
|
||||||
# --- Authentication ---
|
# --- Authentication ---
|
||||||
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0Jwt" }
|
||||||
keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloak" }
|
keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version.ref = "keycloak" }
|
||||||
|
|||||||
+332
-28
@@ -1,51 +1,355 @@
|
|||||||
# Temp / Ping-Service
|
# Ping Service - Circuit Breaker Demo
|
||||||
|
|
||||||
## ⚠️ Wichtiger Hinweis
|
## ⚠️ Wichtiger Hinweis
|
||||||
|
|
||||||
Dieses Modul (`:temp:ping-service`) ist ein **temporärer Service** ausschließlich für Testzwecke. Seine einzige Aufgabe ist die Validierung der technischen Infrastruktur im Rahmen des **"Tracer Bullet"-Szenarios**.
|
Dieses Modul (`:temp:ping-service`) ist ein **temporärer Service** ausschließlich für Testzwecke. Seine Aufgabe ist die Validierung der technischen Infrastruktur im Rahmen des **"Tracer Bullet"-Szenarios** und die Demonstration von **Circuit Breaker Patterns**.
|
||||||
|
|
||||||
Nachdem der End-to-End-Test erfolgreich war, sollte dieses Modul in der `settings.gradle.kts` wieder deaktiviert oder vollständig entfernt werden.
|
Nachdem der End-to-End-Test erfolgreich war, sollte dieses Modul in der `settings.gradle.kts` wieder deaktiviert oder vollständig entfernt werden.
|
||||||
|
|
||||||
## 1. Überblick
|
## 📋 Inhaltsverzeichnis
|
||||||
|
|
||||||
Der `ping-service` ist ein minimaler Spring Boot Microservice, der beweisen soll, dass die grundlegende Service-Architektur funktioniert. Dies beinhaltet:
|
- [Überblick](#überblick)
|
||||||
* Korrekte Konfiguration und Start einer Spring Boot Anwendung.
|
- [Architektur & Features](#architektur--features)
|
||||||
* Bereitstellung eines einfachen REST-Endpunkts.
|
- [API Endpoints](#api-endpoints)
|
||||||
* Einbindung in die Gradle-Build-Logik.
|
- [Konfiguration](#konfiguration)
|
||||||
* Integration in das Test-Framework.
|
- [Lokale Entwicklung](#lokale-entwicklung)
|
||||||
|
- [Docker Deployment](#docker-deployment)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Monitoring & Health Checks](#monitoring--health-checks)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
## 2. Funktionalität
|
## 🎯 Überblick
|
||||||
|
|
||||||
Der Service stellt einen einzigen HTTP-Endpunkt zur Verfügung:
|
Der `ping-service` ist ein Spring Boot Microservice, der die grundlegende Service-Architektur und moderne Resilience Patterns demonstriert:
|
||||||
|
|
||||||
* **`GET /ping`**
|
- **Circuit Breaker Pattern** mit Resilience4j
|
||||||
* **Antwort:** Gibt ein einfaches JSON-Objekt zurück, das den erfolgreichen Aufruf bestätigt.
|
- **Service Discovery** mit Spring Cloud Consul
|
||||||
* **Beispiel-Antwort-Body:**
|
- **Health Checks** und **Monitoring** mit Spring Boot Actuator
|
||||||
```json
|
- **Containerisierte Deployment** mit optimiertem Docker Setup
|
||||||
{
|
- **Comprehensive Testing** mit Integration- und Unit-Tests
|
||||||
"status": "pong"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Konfiguration
|
## 🏗️ Architektur & Features
|
||||||
|
|
||||||
Die Konfiguration des Services erfolgt über die `application.yml`-Datei.
|
### Technology Stack
|
||||||
|
- **Spring Boot 3.2.5** - Modern Java/Kotlin web framework
|
||||||
|
- **Kotlin** - Primary programming language
|
||||||
|
- **Resilience4j** - Circuit breaker and fault tolerance
|
||||||
|
- **Spring Cloud Consul** - Service discovery and configuration
|
||||||
|
- **Micrometer + Prometheus** - Metrics collection
|
||||||
|
- **Docker** - Containerization with multi-stage builds
|
||||||
|
|
||||||
* **`spring.application.name`**: `ping-service`
|
### Circuit Breaker Configuration
|
||||||
* **`server.port`**: `8082`
|
Der Service verwendet Resilience4j Circuit Breaker mit folgenden Einstellungen:
|
||||||
|
- **Failure Rate Threshold**: 60% (Circuit öffnet bei 60% Fehlern)
|
||||||
|
- **Minimum Calls**: 4 (Mindestanzahl Calls für Berechnung)
|
||||||
|
- **Wait Duration**: 5s (Wartezeit im OPEN Status)
|
||||||
|
- **Half-Open Calls**: 3 (Anzahl Calls im HALF_OPEN Status)
|
||||||
|
|
||||||
## 4. Wie man den Service startet
|
## 🚀 API Endpoints
|
||||||
|
|
||||||
Um den Service lokal zu starten, führen Sie den folgenden Gradle-Befehl aus:
|
### 1. Standard Ping Endpoint
|
||||||
|
```http
|
||||||
|
GET /ping
|
||||||
|
```
|
||||||
|
**Beschreibung**: Einfacher Ping ohne Circuit Breaker
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "pong"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Enhanced Ping mit Circuit Breaker
|
||||||
|
```http
|
||||||
|
GET /ping/enhanced?simulate=false
|
||||||
|
```
|
||||||
|
**Beschreibung**: Ping mit Circuit Breaker Schutz
|
||||||
|
**Query Parameter**:
|
||||||
|
- `simulate` (optional): `true` für Failure-Simulation
|
||||||
|
|
||||||
|
**Success Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "pong",
|
||||||
|
"timestamp": "2025-08-14 12:26:30",
|
||||||
|
"service": "ping-service",
|
||||||
|
"circuitBreaker": "CLOSED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback Response** (Circuit Breaker OPEN):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "fallback",
|
||||||
|
"message": "Service temporarily unavailable",
|
||||||
|
"timestamp": "2025-08-14 12:26:30",
|
||||||
|
"service": "ping-service-fallback",
|
||||||
|
"circuitBreaker": "OPEN",
|
||||||
|
"error": "Simulated service failure"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Health Check Endpoint
|
||||||
|
```http
|
||||||
|
GET /ping/health
|
||||||
|
```
|
||||||
|
**Beschreibung**: Health Check mit Circuit Breaker Status
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "UP",
|
||||||
|
"timestamp": "2025-08-14 12:26:30",
|
||||||
|
"circuitBreaker": "CLOSED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Failure Endpoint
|
||||||
|
```http
|
||||||
|
GET /ping/test-failure
|
||||||
|
```
|
||||||
|
**Beschreibung**: Endpoint zum Testen der Circuit Breaker Funktionalität (60% Failure Rate)
|
||||||
|
|
||||||
|
## ⚙️ Konfiguration
|
||||||
|
|
||||||
|
### Application Configuration (`application.yml`)
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ping-service
|
||||||
|
cloud:
|
||||||
|
consul:
|
||||||
|
host: localhost
|
||||||
|
port: 8500
|
||||||
|
discovery:
|
||||||
|
register: true
|
||||||
|
health-check-path: /actuator/health
|
||||||
|
health-check-interval: 10s
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 8082
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info,circuitbreakers
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
|
||||||
|
resilience4j:
|
||||||
|
circuitbreaker:
|
||||||
|
instances:
|
||||||
|
pingCircuitBreaker:
|
||||||
|
failure-rate-threshold: 60
|
||||||
|
minimum-number-of-calls: 4
|
||||||
|
wait-duration-in-open-state: 5s
|
||||||
|
permitted-number-of-calls-in-half-open-state: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
- `SPRING_PROFILES_ACTIVE`: Aktives Spring Profil (default: `default`)
|
||||||
|
- `DEBUG`: Enable Debug-Modus (`true`/`false`, Debug Port: 5005)
|
||||||
|
- `SERVER_PORT`: Server Port (default: `8082`)
|
||||||
|
|
||||||
|
## 💻 Lokale Entwicklung
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Java 21+
|
||||||
|
- Docker (optional)
|
||||||
|
- Consul (für Service Discovery)
|
||||||
|
|
||||||
|
### Service starten
|
||||||
```bash
|
```bash
|
||||||
|
# Standard Start
|
||||||
./gradlew :temp:ping-service:bootRun
|
./gradlew :temp:ping-service:bootRun
|
||||||
|
|
||||||
|
# Mit spezifischem Profil
|
||||||
|
./gradlew :temp:ping-service:bootRun -Pspring.profiles.active=dev
|
||||||
|
|
||||||
|
# Build JAR
|
||||||
|
./gradlew :temp:ping-service:bootJar
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Wie man den Service testet
|
### Service testen
|
||||||
|
|
||||||
Nach dem Start können Sie die Funktionalität mit einem einfachen curl-Befehl überprüfen:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Standard Ping
|
||||||
curl http://localhost:8082/ping
|
curl http://localhost:8082/ping
|
||||||
|
|
||||||
|
# Enhanced Ping
|
||||||
|
curl http://localhost:8082/ping/enhanced
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
curl http://localhost:8082/ping/health
|
||||||
|
|
||||||
|
# Circuit Breaker mit Simulation
|
||||||
|
curl "http://localhost:8082/ping/enhanced?simulate=true"
|
||||||
|
|
||||||
|
# Failure Test
|
||||||
|
curl http://localhost:8082/ping/test-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker Deployment
|
||||||
|
|
||||||
|
### Build Docker Image
|
||||||
|
```bash
|
||||||
|
# Von der Projekt-Root ausführen
|
||||||
|
docker build -t ping-service:latest -f temp/ping-service/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Container
|
||||||
|
```bash
|
||||||
|
# Standard Mode
|
||||||
|
docker run -p 8082:8082 ping-service:latest
|
||||||
|
|
||||||
|
# Debug Mode
|
||||||
|
docker run -p 8082:8082 -p 5005:5005 -e DEBUG=true ping-service:latest
|
||||||
|
|
||||||
|
# Mit Environment Variables
|
||||||
|
docker run -p 8082:8082 \
|
||||||
|
-e SPRING_PROFILES_ACTIVE=prod \
|
||||||
|
-e LOGGING_LEVEL_ROOT=WARN \
|
||||||
|
ping-service:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Features
|
||||||
|
- **Multi-stage Build** für optimale Image-Größe
|
||||||
|
- **Non-root User** für bessere Sicherheit
|
||||||
|
- **Health Checks** integriert
|
||||||
|
- **JVM Optimierungen** für Container-Umgebung
|
||||||
|
- **Debug Support** über Environment Variables
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Unit Tests ausführen
|
||||||
|
```bash
|
||||||
|
./gradlew :temp:ping-service:test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
```bash
|
||||||
|
./gradlew :temp:ping-service:integrationTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
Der Service enthält umfassende Tests für:
|
||||||
|
- **Controller Tests**: API Endpoint Validierung
|
||||||
|
- **Circuit Breaker Tests**: Resilience4j Integration
|
||||||
|
- **Integration Tests**: End-to-End Scenarios
|
||||||
|
- **Health Check Tests**: Actuator Endpoint Validation
|
||||||
|
|
||||||
|
### Test Klassen
|
||||||
|
- `PingControllerTest`: Controller Unit Tests
|
||||||
|
- `PingControllerIntegrationTest`: Full Spring Context Tests
|
||||||
|
- `PingServiceCircuitBreakerTest`: Circuit Breaker Logic Tests
|
||||||
|
|
||||||
|
## 📊 Monitoring & Health Checks
|
||||||
|
|
||||||
|
### Actuator Endpoints
|
||||||
|
- **Health**: `GET /actuator/health`
|
||||||
|
- **Health Readiness**: `GET /actuator/health/readiness`
|
||||||
|
- **Health Liveness**: `GET /actuator/health/liveness`
|
||||||
|
- **Info**: `GET /actuator/info`
|
||||||
|
- **Circuit Breakers**: `GET /actuator/circuitbreakers`
|
||||||
|
- **Metrics**: `GET /actuator/metrics`
|
||||||
|
- **Prometheus**: `GET /actuator/prometheus`
|
||||||
|
|
||||||
|
### Circuit Breaker Monitoring
|
||||||
|
```bash
|
||||||
|
# Circuit Breaker Status
|
||||||
|
curl http://localhost:8082/actuator/circuitbreakers
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
curl http://localhost:8082/actuator/metrics/resilience4j.circuitbreaker.calls
|
||||||
|
|
||||||
|
# Prometheus Format
|
||||||
|
curl http://localhost:8082/actuator/prometheus | grep circuit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Discovery
|
||||||
|
Bei aktiviertem Consul wird der Service automatisch registriert:
|
||||||
|
- **Service Name**: `ping-service`
|
||||||
|
- **Health Check**: `/actuator/health`
|
||||||
|
- **Check Interval**: 10 Sekunden
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Häufige Probleme
|
||||||
|
|
||||||
|
#### 1. Service startet nicht (Port bereits belegt)
|
||||||
|
```bash
|
||||||
|
# Port prüfen
|
||||||
|
netstat -tlnp | grep 8082
|
||||||
|
|
||||||
|
# Alternativen Port verwenden
|
||||||
|
SERVER_PORT=8083 ./gradlew :temp:ping-service:bootRun
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Circuit Breaker öffnet nicht
|
||||||
|
- Mindestens 4 Calls erforderlich (siehe `minimum-number-of-calls`)
|
||||||
|
- 60% Failure Rate erforderlich
|
||||||
|
- Verwende `/ping/test-failure` für Tests
|
||||||
|
|
||||||
|
#### 3. Consul Connection Failed
|
||||||
|
```bash
|
||||||
|
# Consul Status prüfen
|
||||||
|
consul agent -dev
|
||||||
|
|
||||||
|
# Oder Service ohne Consul starten
|
||||||
|
spring.cloud.consul.discovery.enabled=false
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Docker Build Fails
|
||||||
|
```bash
|
||||||
|
# Build Context prüfen - muss von Projekt-Root ausgeführt werden
|
||||||
|
docker build -t ping-service:test -f temp/ping-service/Dockerfile .
|
||||||
|
|
||||||
|
# Nicht von temp/ping-service/ ausführen!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
```bash
|
||||||
|
# Debug Mode aktivieren
|
||||||
|
DEBUG=true ./gradlew :temp:ping-service:bootRun
|
||||||
|
|
||||||
|
# Debug Port: 5005
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
```bash
|
||||||
|
# Alle Logs
|
||||||
|
docker logs <container-id>
|
||||||
|
|
||||||
|
# Circuit Breaker Logs
|
||||||
|
docker logs <container-id> 2>&1 | grep -i circuit
|
||||||
|
|
||||||
|
# Health Check Logs
|
||||||
|
docker logs <container-id> 2>&1 | grep -i health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Entwicklungsnotizen
|
||||||
|
|
||||||
|
### Warum Circuit Breaker?
|
||||||
|
Der Circuit Breaker Pattern verhindert:
|
||||||
|
- **Cascade Failures**: Verhindert Ausfall-Kaskaden
|
||||||
|
- **Resource Exhaustion**: Schont Ressourcen bei Service-Problemen
|
||||||
|
- **Fast Failure**: Schnelle Fehlerrückmeldung statt lange Timeouts
|
||||||
|
|
||||||
|
### Fallback Strategy
|
||||||
|
Bei OPEN Circuit Breaker:
|
||||||
|
- Sofortige Fallback-Response (keine Latenz)
|
||||||
|
- Informative Fehlermeldungen
|
||||||
|
- Status-Informationen für Debugging
|
||||||
|
|
||||||
|
### Production Readiness
|
||||||
|
- Health Checks für Kubernetes/Docker
|
||||||
|
- Prometheus Metriken für Monitoring
|
||||||
|
- Non-root Container für Sicherheit
|
||||||
|
- Optimierte JVM Settings für Container
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Letzte Aktualisierung**: 2025-08-14
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Maintainer**: Meldestelle Development Team
|
||||||
|
|||||||
+120
-37
@@ -1,60 +1,143 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Optimized Dockerfile for Spring Boot Ping Service
|
||||||
|
# Features: Multi-stage build, security hardening, monitoring support
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# Build arguments for flexibility
|
||||||
|
ARG GRADLE_VERSION=8.14
|
||||||
|
ARG JAVA_VERSION=21
|
||||||
|
ARG ALPINE_VERSION=3.19
|
||||||
|
ARG SPRING_PROFILES_ACTIVE=default
|
||||||
|
|
||||||
# Build stage: compile the ping-service JAR inside Docker
|
# Build stage: compile the ping-service JAR inside Docker
|
||||||
FROM gradle:8.14-jdk21-alpine AS builder
|
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
|
||||||
|
|
||||||
|
# Add metadata labels
|
||||||
|
LABEL stage=builder
|
||||||
|
LABEL service=ping-service
|
||||||
|
LABEL maintainer="Meldestelle Development Team"
|
||||||
|
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
# Enable Gradle build cache and daemon for faster builds
|
# Optimize Gradle build settings for containerized builds
|
||||||
ENV GRADLE_OPTS="-Dorg.gradle.caching=true -Dorg.gradle.daemon=false"
|
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
|
-Dorg.gradle.daemon=false \
|
||||||
|
-Dorg.gradle.parallel=true \
|
||||||
|
-Dorg.gradle.configureondemand=true \
|
||||||
|
-Xmx2g"
|
||||||
|
|
||||||
# Copy gradle files first for better layer caching
|
# Copy gradle wrapper and configuration files first for optimal caching
|
||||||
COPY gradle/ gradle/
|
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY build.gradle.kts ./
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Copy platform dependencies (changes less frequently)
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
|
|
||||||
# Copy only necessary source files for the ping-service and its dependencies
|
# Copy root build configuration
|
||||||
COPY temp/ping-service/ temp/ping-service/
|
COPY build.gradle.kts ./
|
||||||
|
|
||||||
# Download dependencies first (better caching)
|
# Copy ping-service specific files last (changes most frequently)
|
||||||
RUN gradle :temp:ping-service:dependencies --no-daemon
|
COPY temp/ping-service/build.gradle.kts temp/ping-service/
|
||||||
|
COPY temp/ping-service/src/ temp/ping-service/src/
|
||||||
|
|
||||||
# Build only the ping-service artifact
|
# Download and cache dependencies in a separate layer
|
||||||
RUN gradle :temp:ping-service:bootJar --no-daemon
|
RUN ./gradlew :temp:ping-service:dependencies --no-daemon --info
|
||||||
|
|
||||||
# Runtime stage: slim JRE image to run the service
|
# Build the application with optimizations
|
||||||
FROM eclipse-temurin:21-jre-alpine
|
RUN ./gradlew :temp:ping-service:bootJar --no-daemon --info \
|
||||||
|
-Pspring.profiles.active=${SPRING_PROFILES_ACTIVE}
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Runtime stage: optimized JRE image for production
|
||||||
|
# ===================================================================
|
||||||
|
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
|
||||||
|
|
||||||
|
# Add comprehensive metadata
|
||||||
|
LABEL service="ping-service" \
|
||||||
|
version="1.0.0" \
|
||||||
|
description="Microservice demonstrating circuit breaker patterns and monitoring" \
|
||||||
|
maintainer="Meldestelle Development Team" \
|
||||||
|
java.version="${JAVA_VERSION}" \
|
||||||
|
spring.profiles.active="${SPRING_PROFILES_ACTIVE}" \
|
||||||
|
build.date="${BUILD_DATE:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')}"
|
||||||
|
|
||||||
|
# Build arguments for runtime configuration
|
||||||
|
ARG APP_USER=appuser
|
||||||
|
ARG APP_GROUP=appgroup
|
||||||
|
ARG APP_UID=1001
|
||||||
|
ARG APP_GID=1001
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install curl for health checks (small footprint on Alpine)
|
# Update Alpine packages and install required tools
|
||||||
RUN apk add --no-cache curl
|
RUN apk update && \
|
||||||
|
apk upgrade && \
|
||||||
|
apk add --no-cache \
|
||||||
|
curl \
|
||||||
|
jq \
|
||||||
|
tzdata && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# Create a non-root user for better security
|
# Create non-root user with specific UID/GID for better security
|
||||||
RUN addgroup -S app && adduser -S app -G app
|
RUN addgroup -g ${APP_GID} -S ${APP_GROUP} && \
|
||||||
|
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh
|
||||||
|
|
||||||
# Copy the built JAR from the builder stage
|
# Create required directories with proper permissions
|
||||||
COPY --from=builder --chown=app:app /workspace/temp/ping-service/build/libs/*.jar app.jar
|
RUN mkdir -p /app/logs /app/tmp && \
|
||||||
|
chown -R ${APP_USER}:${APP_GROUP} /app
|
||||||
|
|
||||||
|
# Copy the built JAR from builder stage with proper ownership
|
||||||
|
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} \
|
||||||
|
/workspace/temp/ping-service/build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER app
|
USER ${APP_USER}
|
||||||
|
|
||||||
# Expose application port
|
# Expose application port and debug port
|
||||||
EXPOSE 8082
|
EXPOSE 8082 5005
|
||||||
|
|
||||||
# Health check against the actuator health endpoint
|
# Enhanced health check with better configuration
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||||
CMD curl -fsS http://localhost:8082/actuator/health || exit 1
|
CMD curl -fsS --max-time 2 http://localhost:8082/actuator/health/readiness || exit 1
|
||||||
|
|
||||||
# Enhanced JVM options for containerized Spring Boot applications
|
# Optimized JVM settings for Spring Boot 3.x with monitoring support
|
||||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
ENV JAVA_OPTS_BASE="-XX:MaxRAMPercentage=80.0 \
|
||||||
-XX:+UseStringDeduplication \
|
-XX:+UseG1GC \
|
||||||
-XX:+UseG1GC \
|
-XX:+UseStringDeduplication \
|
||||||
-XX:+UseContainerSupport \
|
-XX:+UseContainerSupport \
|
||||||
-XX:+OptimizeStringConcat \
|
-XX:+OptimizeStringConcat \
|
||||||
-Djava.security.egd=file:/dev/./urandom \
|
-XX:+UseCompressedOops \
|
||||||
-Dspring.jmx.enabled=false"
|
-Djava.security.egd=file:/dev/./urandom \
|
||||||
|
-Dspring.jmx.enabled=false \
|
||||||
|
-Djava.awt.headless=true \
|
||||||
|
-Dfile.encoding=UTF-8 \
|
||||||
|
-Duser.timezone=UTC"
|
||||||
|
|
||||||
# Run the application
|
# Monitoring and observability settings
|
||||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"]
|
ENV JAVA_OPTS_MONITORING="-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus \
|
||||||
|
-Dmanagement.endpoint.health.show-details=always \
|
||||||
|
-Dmanagement.metrics.export.prometheus.enabled=true"
|
||||||
|
|
||||||
|
# Development/debugging options (enabled via environment variables)
|
||||||
|
ENV JAVA_OPTS_DEBUG="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||||
|
|
||||||
|
# Combined JAVA_OPTS (debug options only applied if DEBUG=true)
|
||||||
|
ENV JAVA_OPTS="${JAVA_OPTS_BASE} ${JAVA_OPTS_MONITORING}"
|
||||||
|
|
||||||
|
# Spring Boot configuration
|
||||||
|
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
|
||||||
|
SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE} \
|
||||||
|
SERVER_PORT=8082 \
|
||||||
|
LOGGING_LEVEL_ROOT=INFO
|
||||||
|
|
||||||
|
# Optimized entrypoint with conditional debug support
|
||||||
|
ENTRYPOINT ["sh", "-c", "\
|
||||||
|
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
|
||||||
|
echo 'Starting application in DEBUG mode on port 5005...'; \
|
||||||
|
exec java ${JAVA_OPTS} ${JAVA_OPTS_DEBUG} -jar app.jar; \
|
||||||
|
else \
|
||||||
|
exec java ${JAVA_OPTS} -jar app.jar; \
|
||||||
|
fi"]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Simple Spring Boot ping service for testing microservice architecture
|
// Optimized Spring Boot ping service for testing microservice architecture
|
||||||
|
// This service demonstrates circuit breaker patterns, service discovery, and monitoring
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlin.spring)
|
alias(libs.plugins.kotlin.spring)
|
||||||
@@ -6,28 +7,66 @@ plugins {
|
|||||||
alias(libs.plugins.spring.dependencyManagement)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build optimization settings
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configure the main class for the executable JAR
|
// Configure the main class for the executable JAR
|
||||||
springBoot {
|
springBoot {
|
||||||
mainClass.set("at.mocode.temp.pingservice.PingServiceApplicationKt")
|
mainClass.set("at.mocode.temp.pingservice.PingServiceApplicationKt")
|
||||||
|
buildInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// === Platform Dependencies ===
|
||||||
// Ensure all versions come from the central BOM
|
// Ensure all versions come from the central BOM
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(platform(projects.platform.platformBom))
|
||||||
// Provide common dependencies
|
// Provide common Kotlin dependencies (coroutines, serialization, logging)
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
|
|
||||||
// Spring Boot Web starter for REST endpoints
|
// === Core Spring Boot Dependencies ===
|
||||||
|
// Web starter for REST endpoints
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|
||||||
// Spring Boot Actuator for health checks
|
// Validation for request/response validation
|
||||||
|
implementation(libs.spring.boot.starter.validation)
|
||||||
|
|
||||||
|
// Actuator for health checks and metrics
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
|
|
||||||
// Spring Cloud Consul for service discovery
|
// === Service Discovery ===
|
||||||
|
// Spring Cloud Consul for service registration and discovery
|
||||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||||
|
|
||||||
// Testing dependencies
|
// === Resilience & Fault Tolerance ===
|
||||||
|
// Resilience4j Circuit Breaker for fault tolerance
|
||||||
|
implementation(libs.resilience4j.spring.boot3)
|
||||||
|
implementation(libs.resilience4j.reactor)
|
||||||
|
implementation(libs.spring.boot.starter.aop)
|
||||||
|
|
||||||
|
// === Monitoring & Metrics ===
|
||||||
|
// Micrometer for metrics collection and Prometheus integration
|
||||||
|
implementation(libs.micrometer.prometheus)
|
||||||
|
|
||||||
|
// === Documentation ===
|
||||||
|
// OpenAPI 3 documentation generation
|
||||||
|
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||||
|
|
||||||
|
// === Testing Dependencies ===
|
||||||
|
// Platform testing utilities
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
// JVM testing bundle (JUnit, AssertJ, Mockk)
|
||||||
testImplementation(libs.bundles.testing.jvm)
|
testImplementation(libs.bundles.testing.jvm)
|
||||||
|
// Spring Boot testing starter
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
testImplementation(libs.spring.boot.starter.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
package at.mocode.temp.pingservice
|
package at.mocode.temp.pingservice
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class PingController {
|
class PingController(
|
||||||
|
private val pingServiceCircuitBreaker: PingServiceCircuitBreaker
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard ping endpoint - maintains backward compatibility
|
||||||
|
*/
|
||||||
@GetMapping("/ping")
|
@GetMapping("/ping")
|
||||||
fun ping(): Map<String, String> {
|
fun ping(): Map<String, String> {
|
||||||
return mapOf("status" to "pong")
|
return mapOf("status" to "pong")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced ping endpoint with circuit breaker protection
|
||||||
|
*
|
||||||
|
* @param simulate - whether to simulate failures for testing circuit breaker
|
||||||
|
*/
|
||||||
|
@GetMapping("/ping/enhanced")
|
||||||
|
fun enhancedPing(@RequestParam(defaultValue = "false") simulate: Boolean): Map<String, Any> {
|
||||||
|
return pingServiceCircuitBreaker.ping(simulate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check endpoint with circuit breaker protection
|
||||||
|
*/
|
||||||
|
@GetMapping("/ping/health")
|
||||||
|
fun health(): Map<String, Any> {
|
||||||
|
return pingServiceCircuitBreaker.healthCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to test circuit breaker behavior by forcing failures
|
||||||
|
*/
|
||||||
|
@GetMapping("/ping/test-failure")
|
||||||
|
fun testFailure(): Map<String, Any> {
|
||||||
|
return pingServiceCircuitBreaker.ping(simulateFailure = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package at.mocode.temp.pingservice
|
||||||
|
|
||||||
|
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service demonstrating a Circuit Breaker pattern with Resilience
|
||||||
|
*
|
||||||
|
* This service simulates potential failures and uses circuit breaker
|
||||||
|
* to handle service degradation gracefully with fallback responses.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
class PingServiceCircuitBreaker {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(PingServiceCircuitBreaker::class.java)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PING_CIRCUIT_BREAKER = "pingCircuitBreaker"
|
||||||
|
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary ping method with circuit breaker protection
|
||||||
|
*
|
||||||
|
* @param simulateFailure - if true, randomly throws exceptions to test circuit breaker
|
||||||
|
* @return Map containing ping response with timestamp and status
|
||||||
|
*/
|
||||||
|
@CircuitBreaker(name = PING_CIRCUIT_BREAKER, fallbackMethod = "fallbackPing")
|
||||||
|
fun ping(simulateFailure: Boolean = false): Map<String, Any> {
|
||||||
|
logger.info("Executing ping service call...")
|
||||||
|
|
||||||
|
if (simulateFailure && Random.nextDouble() < 0.6) {
|
||||||
|
logger.warn("Simulating service failure for circuit breaker testing")
|
||||||
|
throw RuntimeException("Simulated service failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentTime = LocalDateTime.now().format(formatter)
|
||||||
|
logger.info("Ping service call successful")
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"status" to "pong",
|
||||||
|
"timestamp" to currentTime,
|
||||||
|
"service" to "ping-service",
|
||||||
|
"circuitBreaker" to "CLOSED"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback method called when circuit breaker is OPEN
|
||||||
|
*
|
||||||
|
* @param simulateFailure - original parameter (ignored in fallback)
|
||||||
|
* @param exception - the exception that triggered the fallback
|
||||||
|
* @return Map containing fallback response
|
||||||
|
*/
|
||||||
|
fun fallbackPing(simulateFailure: Boolean = false, exception: Exception): Map<String, Any> {
|
||||||
|
logger.warn("Circuit breaker fallback triggered due to: {}", exception.message)
|
||||||
|
|
||||||
|
val currentTime = LocalDateTime.now().format(formatter)
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"status" to "fallback",
|
||||||
|
"message" to "Service temporarily unavailable",
|
||||||
|
"timestamp" to currentTime,
|
||||||
|
"service" to "ping-service-fallback",
|
||||||
|
"circuitBreaker" to "OPEN",
|
||||||
|
"error" to (exception.message ?: "Unknown error")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check method with circuit breaker protection
|
||||||
|
*/
|
||||||
|
@CircuitBreaker(name = PING_CIRCUIT_BREAKER, fallbackMethod = "fallbackHealth")
|
||||||
|
fun healthCheck(): Map<String, Any> {
|
||||||
|
logger.info("Executing health check...")
|
||||||
|
|
||||||
|
// Health check is now deterministic for reliable integration testing
|
||||||
|
// Random failures were causing intermittent test failures
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"status" to "UP",
|
||||||
|
"timestamp" to LocalDateTime.now().format(formatter),
|
||||||
|
"circuitBreaker" to "CLOSED"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback for health check
|
||||||
|
*/
|
||||||
|
fun fallbackHealth(exception: Exception): Map<String, Any> {
|
||||||
|
logger.warn("Health check fallback triggered: {}", exception.message)
|
||||||
|
|
||||||
|
return mapOf(
|
||||||
|
"status" to "DOWN",
|
||||||
|
"message" to "Health check temporarily unavailable",
|
||||||
|
"timestamp" to LocalDateTime.now().format(formatter),
|
||||||
|
"circuitBreaker" to "OPEN"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,45 @@ management:
|
|||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
exposure:
|
exposure:
|
||||||
include: health,info
|
include: health,info,circuitbreakers
|
||||||
endpoint:
|
endpoint:
|
||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
|
|
||||||
|
# Resilience4j Circuit Breaker Configuration
|
||||||
|
resilience4j:
|
||||||
|
circuitbreaker:
|
||||||
|
configs:
|
||||||
|
default:
|
||||||
|
# Circuit breaker opens when the failure rate exceeds 50%
|
||||||
|
failure-rate-threshold: 50
|
||||||
|
# Minimum number of calls to calculate the failure rate
|
||||||
|
minimum-number-of-calls: 5
|
||||||
|
# Time to wait before transitioning from OPEN to HALF_OPEN
|
||||||
|
wait-duration-in-open-state: 10s
|
||||||
|
# Number of calls in HALF_OPEN state before deciding to close/open
|
||||||
|
permitted-number-of-calls-in-half-open-state: 3
|
||||||
|
# Sliding window size for calculating failure rate
|
||||||
|
sliding-window-size: 10
|
||||||
|
# Type of sliding window (COUNT_BASED or TIME_BASED)
|
||||||
|
sliding-window-type: COUNT_BASED
|
||||||
|
# Record exceptions that should be considered as failures
|
||||||
|
record-exceptions:
|
||||||
|
- java.lang.Exception
|
||||||
|
# Ignore certain exceptions (don't count as failures)
|
||||||
|
ignore-exceptions:
|
||||||
|
- java.lang.IllegalArgumentException
|
||||||
|
instances:
|
||||||
|
pingCircuitBreaker:
|
||||||
|
# Use default configuration
|
||||||
|
base-config: default
|
||||||
|
# Override specific settings for this instance if needed
|
||||||
|
failure-rate-threshold: 60
|
||||||
|
minimum-number-of-calls: 4
|
||||||
|
wait-duration-in-open-state: 5s
|
||||||
|
|
||||||
|
# Optional: Metrics configuration
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
legacy:
|
||||||
|
enabled: true
|
||||||
|
|||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
package at.mocode.temp.pingservice
|
||||||
|
|
||||||
|
import io.github.resilience4j.circuitbreaker.CircuitBreaker
|
||||||
|
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.test.context.ActiveProfiles
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for PingController
|
||||||
|
* Tests REST endpoints with circuit breaker functionality using TestRestTemplate
|
||||||
|
*/
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class PingControllerIntegrationTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private var port: Int = 0
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var restTemplate: TestRestTemplate
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(PingControllerIntegrationTest::class.java)
|
||||||
|
|
||||||
|
private lateinit var circuitBreaker: CircuitBreaker
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
circuitBreaker = circuitBreakerRegistry.circuitBreaker(PingServiceCircuitBreaker.PING_CIRCUIT_BREAKER)
|
||||||
|
// Reset circuit breaker state before each test
|
||||||
|
circuitBreaker.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrl(endpoint: String) = "http://localhost:$port$endpoint"
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return basic ping response from standard endpoint`() {
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
assertThat(response.body!!["status"]).isEqualTo("pong")
|
||||||
|
|
||||||
|
logger.info("Standard ping endpoint response: {}", response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return enhanced ping response when circuit breaker is closed`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
assertThat(body["status"]).isEqualTo("pong")
|
||||||
|
assertThat(body["service"]).isEqualTo("ping-service")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
assertThat(body["timestamp"]).isNotNull()
|
||||||
|
|
||||||
|
logger.info("Enhanced ping response: {}", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return enhanced ping response without simulation`() {
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/enhanced?simulate=false"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
assertThat(body["status"]).isEqualTo("pong")
|
||||||
|
assertThat(body["service"]).isEqualTo("ping-service")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
|
||||||
|
logger.info("Enhanced ping without simulation: {}", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should handle failure simulation in enhanced ping endpoint`() {
|
||||||
|
// Multiple calls to potentially trigger failures due to random simulation
|
||||||
|
repeat(3) { i ->
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/enhanced?simulate=true"), Map::class.java)
|
||||||
|
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
logger.info("Attempt {}: Response status = {}, Circuit breaker state = {}",
|
||||||
|
i + 1, body["status"], circuitBreaker.state)
|
||||||
|
|
||||||
|
// Response should be either success or fallback
|
||||||
|
assertThat(body["status"]).isIn("pong", "fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return health check response when circuit breaker is closed`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/health"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
assertThat(body["status"]).isEqualTo("UP")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
assertThat(body["timestamp"]).isNotNull()
|
||||||
|
|
||||||
|
logger.info("Health check response: {}", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return fallback health check when circuit breaker is open`() {
|
||||||
|
// Given - manually open circuit breaker
|
||||||
|
circuitBreaker.transitionToOpenState()
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/health"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
assertThat(body["status"]).isEqualTo("DOWN")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
assertThat(body["message"]).isEqualTo("Health check temporarily unavailable")
|
||||||
|
|
||||||
|
logger.info("Fallback health check response: {}", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return response from test-failure endpoint`() {
|
||||||
|
// When
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/test-failure"), Map::class.java)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
|
||||||
|
// Due to 60% failure simulation, we expect either success or fallback
|
||||||
|
assertThat(body["status"]).isIn("pong", "fallback")
|
||||||
|
|
||||||
|
if (body["status"] == "fallback") {
|
||||||
|
assertThat(body["service"]).isEqualTo("ping-service-fallback")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
assertThat(body["message"]).isEqualTo("Service temporarily unavailable")
|
||||||
|
assertThat(body["error"]).isNotNull()
|
||||||
|
} else {
|
||||||
|
assertThat(body["service"]).isEqualTo("ping-service")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Test failure endpoint response: {}", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should handle multiple rapid requests correctly`() {
|
||||||
|
// Execute multiple rapid requests
|
||||||
|
val results = mutableListOf<Map<String, Any>>()
|
||||||
|
|
||||||
|
repeat(5) { i ->
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val body = response.body as Map<String, Any>
|
||||||
|
results.add(body)
|
||||||
|
|
||||||
|
logger.info("Rapid request {}: status = {}", i + 1, body["status"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// All should be successful since we're not simulating failures
|
||||||
|
results.forEach { response ->
|
||||||
|
assertThat(response["status"]).isEqualTo("pong")
|
||||||
|
assertThat(response["service"]).isEqualTo("ping-service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should maintain circuit breaker state across requests`() {
|
||||||
|
// Given - manually open circuit breaker
|
||||||
|
circuitBreaker.transitionToOpenState()
|
||||||
|
|
||||||
|
// When - make multiple requests
|
||||||
|
repeat(3) { i ->
|
||||||
|
val response = restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull
|
||||||
|
|
||||||
|
val body = response.body!!
|
||||||
|
|
||||||
|
// All should return fallback responses while circuit breaker is open
|
||||||
|
assertThat(body["status"]).isEqualTo("fallback")
|
||||||
|
assertThat(body["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
|
||||||
|
logger.info("Request {} with OPEN circuit breaker: {}", i + 1, body["status"])
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should test all endpoints return valid responses`() {
|
||||||
|
val endpoints = listOf(
|
||||||
|
"/ping",
|
||||||
|
"/ping/enhanced",
|
||||||
|
"/ping/health",
|
||||||
|
"/ping/test-failure"
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoints.forEach { endpoint ->
|
||||||
|
val response = restTemplate.getForEntity(getUrl(endpoint), Map::class.java)
|
||||||
|
|
||||||
|
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||||
|
assertThat(response.body).isNotNull()
|
||||||
|
assertThat(response.body!!).isNotEmpty()
|
||||||
|
|
||||||
|
logger.info("Endpoint {} returned valid response: {}", endpoint, response.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should track circuit breaker metrics after calls`() {
|
||||||
|
// Given
|
||||||
|
val initialMetrics = circuitBreaker.metrics
|
||||||
|
logger.info("Initial metrics - Calls: {}, Failures: {}",
|
||||||
|
initialMetrics.numberOfBufferedCalls, initialMetrics.numberOfFailedCalls)
|
||||||
|
|
||||||
|
// When - execute some calls
|
||||||
|
repeat(3) {
|
||||||
|
restTemplate.getForEntity(getUrl("/ping/enhanced"), Map::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val newMetrics = circuitBreaker.metrics
|
||||||
|
assertThat(newMetrics.numberOfBufferedCalls).isGreaterThanOrEqualTo(3)
|
||||||
|
|
||||||
|
logger.info("Updated metrics - Calls: {}, Failure rate: {}%, Successful: {}, Failed: {}",
|
||||||
|
newMetrics.numberOfBufferedCalls,
|
||||||
|
newMetrics.failureRate,
|
||||||
|
newMetrics.numberOfSuccessfulCalls,
|
||||||
|
newMetrics.numberOfFailedCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package at.mocode.temp.pingservice
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
|
||||||
@@ -13,6 +14,9 @@ class PingControllerTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var pingServiceCircuitBreaker: PingServiceCircuitBreaker
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ping endpoint should return pong status`() {
|
fun `ping endpoint should return pong status`() {
|
||||||
mockMvc.perform(get("/ping"))
|
mockMvc.perform(get("/ping"))
|
||||||
|
|||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
package at.mocode.temp.pingservice
|
||||||
|
|
||||||
|
import io.github.resilience4j.circuitbreaker.CircuitBreaker
|
||||||
|
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive test suite for PingServiceCircuitBreaker
|
||||||
|
* Tests circuit breaker behavior, fallback methods, and state transitions
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
class PingServiceCircuitBreakerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var pingServiceCircuitBreaker: PingServiceCircuitBreaker
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var circuitBreakerRegistry: CircuitBreakerRegistry
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(PingServiceCircuitBreakerTest::class.java)
|
||||||
|
|
||||||
|
private lateinit var circuitBreaker: CircuitBreaker
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
circuitBreaker = circuitBreakerRegistry.circuitBreaker(PingServiceCircuitBreaker.PING_CIRCUIT_BREAKER)
|
||||||
|
// Reset circuit breaker state before each test
|
||||||
|
circuitBreaker.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return successful ping response when circuit breaker is closed`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result["status"]).isEqualTo("pong")
|
||||||
|
assertThat(result["service"]).isEqualTo("ping-service")
|
||||||
|
assertThat(result["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
assertThat(result).containsKeys("timestamp")
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should handle single failure without opening circuit breaker`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When - single failure should not open circuit breaker (needs 4 failures minimum)
|
||||||
|
// Try multiple times since failure simulation is probabilistic (60% chance)
|
||||||
|
var result: Map<String, Any>
|
||||||
|
var attempts = 0
|
||||||
|
do {
|
||||||
|
result = pingServiceCircuitBreaker.ping(simulateFailure = true)
|
||||||
|
attempts++
|
||||||
|
} while (result["status"] == "pong" && attempts < 10)
|
||||||
|
|
||||||
|
// Then - should get fallback response eventually, but circuit breaker might still be closed after just one failure
|
||||||
|
assertThat(result["status"]).isEqualTo("fallback")
|
||||||
|
assertThat(result["service"]).isEqualTo("ping-service-fallback")
|
||||||
|
assertThat(result["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
assertThat(result).containsKeys("timestamp", "message", "error")
|
||||||
|
logger.info("Circuit breaker state after single failure (after {} attempts): {}", attempts, circuitBreaker.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should open circuit breaker after multiple failures`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When - trigger multiple failures to reach minimum-number-of-calls (4) and failure threshold (60%)
|
||||||
|
// Keep calling until we get enough failures to trigger the circuit breaker
|
||||||
|
var failureCount = 0
|
||||||
|
var totalCalls = 0
|
||||||
|
val maxAttempts = 20 // Prevent infinite loop
|
||||||
|
|
||||||
|
while (circuitBreaker.state == CircuitBreaker.State.CLOSED && totalCalls < maxAttempts) {
|
||||||
|
val result = pingServiceCircuitBreaker.ping(simulateFailure = true)
|
||||||
|
totalCalls++
|
||||||
|
|
||||||
|
if (result["status"] == "fallback") {
|
||||||
|
failureCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempt {}: Circuit breaker state = {}, Response status = {}, Failures so far = {}",
|
||||||
|
totalCalls, circuitBreaker.state, result["status"], failureCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then - circuit breaker should be open after sufficient failures
|
||||||
|
logger.info("Final circuit breaker state: {} after {} total calls with {} failures",
|
||||||
|
circuitBreaker.state, totalCalls, failureCount)
|
||||||
|
|
||||||
|
// The circuit breaker should eventually open due to failure rate threshold
|
||||||
|
// Note: Due to the minimum calls requirement (4) and failure rate threshold (60%),
|
||||||
|
// it might take several attempts depending on random simulation
|
||||||
|
if (totalCalls >= 4 && failureCount >= ceil(totalCalls * 0.6)) {
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return fallback response when circuit breaker is manually opened`() {
|
||||||
|
// Given
|
||||||
|
circuitBreaker.transitionToOpenState()
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result["status"]).isEqualTo("fallback")
|
||||||
|
assertThat(result["service"]).isEqualTo("ping-service-fallback")
|
||||||
|
assertThat(result["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
assertThat(result["message"]).isEqualTo("Service temporarily unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return successful health check when circuit breaker is closed`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When - retry if we get random failure (10% chance in health check method)
|
||||||
|
var result: Map<String, Any>
|
||||||
|
var attempts = 0
|
||||||
|
do {
|
||||||
|
result = pingServiceCircuitBreaker.healthCheck()
|
||||||
|
attempts++
|
||||||
|
} while (result["status"] == "DOWN" && attempts < 15) // 15 attempts should be enough to get a success
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result["status"]).isEqualTo("UP")
|
||||||
|
assertThat(result["circuitBreaker"]).isEqualTo("CLOSED")
|
||||||
|
assertThat(result).containsKeys("timestamp")
|
||||||
|
logger.info("Health check succeeded after {} attempts", attempts)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return fallback health check when circuit breaker is open`() {
|
||||||
|
// Given
|
||||||
|
circuitBreaker.transitionToOpenState()
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val result = pingServiceCircuitBreaker.healthCheck()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result["status"]).isEqualTo("DOWN")
|
||||||
|
assertThat(result["circuitBreaker"]).isEqualTo("OPEN")
|
||||||
|
assertThat(result["message"]).isEqualTo("Health check temporarily unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should test circuit breaker state transitions`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When - manually transition to open state
|
||||||
|
circuitBreaker.transitionToOpenState()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.OPEN)
|
||||||
|
|
||||||
|
// When - manually transition to half-open state
|
||||||
|
circuitBreaker.transitionToHalfOpenState()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.HALF_OPEN)
|
||||||
|
|
||||||
|
// When - successful call should close circuit breaker
|
||||||
|
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(result["status"]).isEqualTo("pong")
|
||||||
|
logger.info("Circuit breaker state after successful call in HALF_OPEN: {}", circuitBreaker.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should track circuit breaker metrics`() {
|
||||||
|
// Given
|
||||||
|
val metrics = circuitBreaker.metrics
|
||||||
|
|
||||||
|
// When
|
||||||
|
val initialFailureRate = metrics.failureRate
|
||||||
|
val initialNumberOfCalls = metrics.numberOfBufferedCalls
|
||||||
|
|
||||||
|
// Execute some successful calls
|
||||||
|
repeat(3) {
|
||||||
|
pingServiceCircuitBreaker.ping(simulateFailure = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val newMetrics = circuitBreaker.metrics
|
||||||
|
assertThat(newMetrics.numberOfBufferedCalls).isGreaterThan(initialNumberOfCalls)
|
||||||
|
logger.info("Circuit breaker metrics - Calls: {}, Failure rate: {}%, Successful calls: {}, Failed calls: {}",
|
||||||
|
newMetrics.numberOfBufferedCalls,
|
||||||
|
newMetrics.failureRate,
|
||||||
|
newMetrics.numberOfSuccessfulCalls,
|
||||||
|
newMetrics.numberOfFailedCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should handle concurrent calls correctly`() {
|
||||||
|
// Given
|
||||||
|
assertThat(circuitBreaker.state).isEqualTo(CircuitBreaker.State.CLOSED)
|
||||||
|
|
||||||
|
// When - execute concurrent calls
|
||||||
|
val futures = (1..10).map { index ->
|
||||||
|
Thread {
|
||||||
|
val result = pingServiceCircuitBreaker.ping(simulateFailure = false)
|
||||||
|
logger.info("Concurrent call {}: status = {}", index, result["status"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
futures.forEach { it.start() }
|
||||||
|
futures.forEach { it.join() }
|
||||||
|
|
||||||
|
// Then
|
||||||
|
val metrics = circuitBreaker.metrics
|
||||||
|
assertThat(metrics.numberOfBufferedCalls).isEqualTo(10)
|
||||||
|
assertThat(metrics.numberOfSuccessfulCalls).isEqualTo(10)
|
||||||
|
assertThat(metrics.numberOfFailedCalls).isEqualTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: ping-service-test
|
||||||
|
cloud:
|
||||||
|
consul:
|
||||||
|
enabled: false
|
||||||
|
discovery:
|
||||||
|
enabled: false
|
||||||
|
register: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
port: 0
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info,circuitbreakers
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
|
||||||
|
# Resilience4j Circuit Breaker Configuration for tests
|
||||||
|
resilience4j:
|
||||||
|
circuitbreaker:
|
||||||
|
configs:
|
||||||
|
default:
|
||||||
|
failure-rate-threshold: 50
|
||||||
|
minimum-number-of-calls: 5
|
||||||
|
wait-duration-in-open-state: 10s
|
||||||
|
permitted-number-of-calls-in-half-open-state: 3
|
||||||
|
sliding-window-size: 10
|
||||||
|
sliding-window-type: COUNT_BASED
|
||||||
|
record-exceptions:
|
||||||
|
- java.lang.Exception
|
||||||
|
ignore-exceptions:
|
||||||
|
- java.lang.IllegalArgumentException
|
||||||
|
instances:
|
||||||
|
pingCircuitBreaker:
|
||||||
|
base-config: default
|
||||||
|
failure-rate-threshold: 60
|
||||||
|
minimum-number-of-calls: 4
|
||||||
|
wait-duration-in-open-state: 5s
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: true
|
||||||
|
legacy:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.springframework.cloud.consul: ERROR
|
||||||
|
com.ecwid.consul: ERROR
|
||||||
Reference in New Issue
Block a user