chore: erweitere Resilience4j-Bundle um Kotlin-Support, aktualisiere PingController um Fallback-Logik, füge Fehlerhandler hinzu, verbessere PingControllerTest, synchronisiere .env und dc-infra.yaml
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
plugins {
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
@@ -37,8 +37,7 @@ dependencies {
|
||||
implementation(libs.bundles.database.complete)
|
||||
|
||||
// === Resilience ===
|
||||
implementation(libs.resilience4j.spring.boot3)
|
||||
implementation(libs.resilience4j.reactor)
|
||||
implementation(libs.bundles.resilience)
|
||||
implementation(libs.spring.boot.starter.aop)
|
||||
|
||||
// === Testing ===
|
||||
|
||||
+17
-8
@@ -2,6 +2,7 @@ package at.mocode.ping.infrastructure.web
|
||||
|
||||
import at.mocode.ping.api.*
|
||||
import at.mocode.ping.application.PingUseCase
|
||||
import at.mocode.ping.domain.Ping
|
||||
import at.mocode.ping.infrastructure.PingProperties
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -20,7 +21,7 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
*/
|
||||
@RestController
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingController(
|
||||
open class PingController(
|
||||
private val pingUseCase: PingUseCase,
|
||||
private val properties: PingProperties
|
||||
) : PingApi {
|
||||
@@ -43,10 +44,16 @@ class PingController(
|
||||
override suspend fun enhancedPing(
|
||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||
): EnhancedPingResponse {
|
||||
logger.info("Enhanced ping requested, simulate: {}", simulate)
|
||||
val start = System.nanoTime()
|
||||
|
||||
if (simulate && Random.nextDouble() < 0.6) {
|
||||
throw RuntimeException("Simulated service failure")
|
||||
if (simulate) {
|
||||
if (Random.nextDouble() < 0.6) {
|
||||
logger.info("Simulating service failure now...")
|
||||
throw SimulatedException("Simulated service failure")
|
||||
} else {
|
||||
logger.info("Simulation mode ACTIVE, but this time lucky: Request passed!")
|
||||
}
|
||||
}
|
||||
|
||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||
@@ -61,6 +68,8 @@ class PingController(
|
||||
)
|
||||
}
|
||||
|
||||
class SimulatedException(message: String) : RuntimeException(message)
|
||||
|
||||
// Neue Endpunkte
|
||||
|
||||
@GetMapping("/ping/public")
|
||||
@@ -70,7 +79,7 @@ class PingController(
|
||||
}
|
||||
|
||||
@GetMapping("/ping/secure")
|
||||
@PreAuthorize("hasRole('MELD_USER') or hasRole('MELD_ADMIN')") // Beispiel-Rollen
|
||||
@PreAuthorize("hasRole('ROLE_MELD_USER') or hasRole('ROLE_MELD_ADMIN')") // Beispiel-Rollen
|
||||
override suspend fun securePing(): PingResponse {
|
||||
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||
return createResponse(domainPing, "secure-pong")
|
||||
@@ -79,7 +88,7 @@ class PingController(
|
||||
@GetMapping("/ping/sync")
|
||||
override suspend fun syncPings(
|
||||
// Changed the parameter name to 'since' to match SyncManager convention
|
||||
@RequestParam(required = false, defaultValue = "0") since: Long
|
||||
@RequestParam(name = "lastSyncTimestamp", required = false, defaultValue = "0") since: Long
|
||||
): List<PingEvent> {
|
||||
return pingUseCase.getPingsSince(since).map {
|
||||
PingEvent(
|
||||
@@ -91,7 +100,7 @@ class PingController(
|
||||
}
|
||||
|
||||
// Helper
|
||||
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
||||
private fun createResponse(domainPing: Ping, status: String) = PingResponse(
|
||||
status = status,
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = properties.serviceName
|
||||
@@ -99,8 +108,8 @@ class PingController(
|
||||
|
||||
// Fallback
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
||||
open fun fallbackPing(simulate: Boolean, ex: Throwable): EnhancedPingResponse {
|
||||
logger.error("CIRCUIT BREAKER FALLBACK TRIGGERED! Reason: {}", ex.message, ex)
|
||||
return EnhancedPingResponse(
|
||||
status = "fallback",
|
||||
timestamp = java.time.OffsetDateTime.now().format(formatter),
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package at.mocode.ping.infrastructure.web
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ProblemDetail
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
|
||||
@RestControllerAdvice
|
||||
class PingExceptionHandler {
|
||||
private val log = LoggerFactory.getLogger(PingExceptionHandler::class.java)
|
||||
|
||||
@ExceptionHandler(AccessDeniedException::class)
|
||||
fun handleAccessDenied(ex: AccessDeniedException): ProblemDetail {
|
||||
log.warn("Zugriff verweigert: ${ex.message}")
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, "Nicht berechtigt: ${ex.message}")
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleAll(ex: Exception): ProblemDetail {
|
||||
log.error("Unerwarteter Fehler: ", ex)
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Ein interner Fehler ist aufgetreten")
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException::class)
|
||||
fun handleRuntime(ex: RuntimeException): ProblemDetail {
|
||||
log.error("Interner Fehler: ", ex)
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.message ?: "Unbekannter Fehler")
|
||||
}
|
||||
|
||||
}
|
||||
+23
-5
@@ -40,12 +40,17 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||
controllers = [PingController::class],
|
||||
properties = ["spring.aop.proxy-target-class=true"]
|
||||
)
|
||||
@Import(
|
||||
PingControllerTest.PingControllerTestConfig::class,
|
||||
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerAutoConfiguration::class,
|
||||
io.github.resilience4j.springboot3.circuitbreaker.autoconfigure.CircuitBreakerMetricsAutoConfiguration::class,
|
||||
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration::class
|
||||
)
|
||||
@ContextConfiguration(classes = [TestPingServiceApplication::class])
|
||||
@ActiveProfiles("test")
|
||||
@Import(PingControllerTest.PingControllerTestConfig::class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class PingControllerTest {
|
||||
open class PingControllerTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var mockMvc: MockMvc
|
||||
@@ -125,11 +130,24 @@ class PingControllerTest {
|
||||
|
||||
// Then
|
||||
val json = objectMapper.readTree(result.response.contentAsString)
|
||||
assertThat(json["status"].asText()).isEqualTo("pong")
|
||||
assertThat(json["service"].asText()).isEqualTo(properties.serviceName)
|
||||
verify { pingUseCase.executePing("Enhanced Ping") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return fallback when simulation failure occurs`() {
|
||||
// Given
|
||||
val controller = PingController(pingUseCase, properties)
|
||||
|
||||
// When
|
||||
val response = controller.fallbackPing(simulate = true, ex = PingController.SimulatedException("test"))
|
||||
|
||||
// Then
|
||||
assertThat(response.status).isEqualTo("fallback")
|
||||
assertThat(response.service).isEqualTo(properties.serviceNameFallback)
|
||||
assertThat(response.circuitBreakerState).isEqualTo("OPEN")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return health check response with status up`() {
|
||||
// When
|
||||
@@ -159,7 +177,7 @@ class PingControllerTest {
|
||||
)
|
||||
|
||||
// When
|
||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||
.andExpect(request().asyncStarted())
|
||||
.andReturn()
|
||||
|
||||
@@ -183,7 +201,7 @@ class PingControllerTest {
|
||||
every { pingUseCase.getPingsSince(timestamp) } returns emptyList()
|
||||
|
||||
// When
|
||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("since", timestamp.toString()))
|
||||
val mvcResult: MvcResult = mockMvc.perform(get("/ping/sync").param("lastSyncTimestamp", timestamp.toString()))
|
||||
.andExpect(request().asyncStarted())
|
||||
.andReturn()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user