This commit is contained in:
2025-12-08 20:46:47 +01:00
parent 21ee130ebf
commit f0d85aa4da
16 changed files with 444 additions and 17 deletions
@@ -38,9 +38,10 @@ COPY frontend/ frontend/
COPY core/ core/
COPY backend/ backend/
COPY docs/ docs/
COPY build.gradle.kts ./
COPY entries-service/build.gradle.kts ./
# Copy entries modules
COPY backend/services/entries/entries-api/ backend/services/entries/entries-api/
COPY backend/services/entries/entries-service/ backend/services/entries/entries-service/
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
@@ -127,5 +128,6 @@ ENTRYPOINT ["tini", "--", "sh", "-c", "\
echo 'DEBUG mode enabled'; \
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar; \
else \
echo 'Starting Entries Service in production mode'; \
exec java ${JAVA_OPTS} -jar app.jar; \
fi"]
@@ -0,0 +1,45 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
group = "at.mocode"
version = "1.0.0"
kotlin {
val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
// Align the toolchain with a project (see composeApp uses 21)
jvmToolchain(21)
// JVM target for backend usage
jvm()
// JS target for frontend usage (Compose/Browser)
js {
browser()
// no need for binaries.executable() in a library
}
// Optional Wasm target for browser clients
if (enableWasm) {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
}
sourceSets {
commonMain {
dependencies {
implementation(libs.kotlinx.serialization.json)
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}
@@ -0,0 +1,5 @@
package at.mocode.entries.api
interface EntriesApi {
suspend fun healthCheck(): HealthResponse
}
@@ -0,0 +1,23 @@
package at.mocode.entries.api
import kotlinx.serialization.Serializable
@Serializable
data class EntriesResponse(val status: String, val timestamp: String, val service: String)
@Serializable
data class EnhancedEntriesResponse(
val status: String,
val timestamp: String,
val service: String,
val circuitBreakerState: String,
val responseTime: Long
)
@Serializable
data class HealthResponse(
val status: String,
val timestamp: String,
val service: String,
val healthy: Boolean
)
@@ -13,6 +13,7 @@ springBoot {
dependencies {
implementation(platform(projects.platform.platformBom))
implementation(projects.platform.platformDependencies)
implementation(projects.backend.services.entries.entriesApi)
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
implementation(libs.bundles.spring.boot.service.complete)
@@ -0,0 +1,27 @@
package at.mocode.entries.service
import at.mocode.entries.api.EnhancedEntriesResponse
import at.mocode.entries.api.EntriesApi
import at.mocode.entries.api.HealthResponse
import org.springframework.web.bind.annotation.*
@RestController
@CrossOrigin(
origins = ["http://localhost:8080", "http://localhost:8083", "http://localhost:4000"],
methods = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS],
allowedHeaders = ["*"],
allowCredentials = "true"
)
class EntriesController(
private val entriesService: EntriesServiceCircuitBreaker
) : EntriesApi {
// Contract endpoints
@GetMapping("/entries/enhanced")
suspend fun enhancedEntries(
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
): EnhancedEntriesResponse = entriesService.entries(simulate)
@GetMapping("/entries/health")
override suspend fun healthCheck(): HealthResponse = entriesService.healthCheck()
}
@@ -2,26 +2,34 @@ package at.mocode.entries.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
@SpringBootApplication
class EntriesServiceApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.EnableAspectJAutoProxy
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
fun main(args: Array<String>) {
runApplication<EntriesServiceApplication>(*args)
}
@RestController
class EntriesController {
@GetMapping("/")
fun health(): String = "Entries Service is running"
@SpringBootApplication
@EnableAspectJAutoProxy
class EntriesServiceApplication {
@PostMapping("/entries/conflict-demo")
fun conflictDemo(): ResponseEntity<String> {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Conflict detected (Demo)")
@Bean
fun corsConfigurer(): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*")
.allowedOrigins("http://localhost:8080",
"http://localhost:8083",
"http://localhost:4000"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600)
}
}
}
}
@@ -0,0 +1,110 @@
package at.mocode.entries.service
import at.mocode.entries.api.EnhancedEntriesResponse
import at.mocode.entries.api.HealthResponse
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.ZoneOffset
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 EntriesServiceCircuitBreaker {
private val logger = LoggerFactory.getLogger(EntriesServiceCircuitBreaker::class.java)
companion object {
const val ENTRIES_CIRCUIT_BREAKER = "entriesCircuitBreaker"
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME //.ofPattern("yyyy-MM-dd HH:mm:ss")
}
/**
* Primary entries method with circuit breaker protection returning DTO directly
*
* @param simulateFailure - if true, randomly throws exceptions to test circuit breaker
*/
@CircuitBreaker(name = ENTRIES_CIRCUIT_BREAKER, fallbackMethod = "fallbackEntries")
fun entries(simulateFailure: Boolean = false): EnhancedEntriesResponse {
val start = System.nanoTime()
logger.info("Executing entries 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().atOffset(ZoneOffset.UTC).format(formatter)
val elapsedMs = (System.nanoTime() - start) / 1_000_000
logger.info("Entries service call successful")
return EnhancedEntriesResponse(
status = "entries",
timestamp = currentTime,
service = "entries-service",
circuitBreakerState = "CLOSED",
responseTime = elapsedMs
)
}
/**
* Fallback method called when circuit breaker is OPEN
*
* @param simulateFailure - original parameter (ignored in fallback)
* @param exception - the exception that triggered the fallback
*/
fun fallbackEntries(simulateFailure: Boolean = false, exception: Exception): EnhancedEntriesResponse {
val start = System.nanoTime()
// Die volle Exception nur loggen, nicht an den Client weitergeben.
logger.warn("Circuit breaker fallback triggered due to: {}", exception.toString())
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
val elapsedMs = (System.nanoTime() - start) / 1_000_000
return EnhancedEntriesResponse(
status = "fallback",
timestamp = currentTime,
service = "entries-service-fallback",
circuitBreakerState = "OPEN",
responseTime = elapsedMs
)
}
/**
* Health check method with circuit breaker protection returning DTO directly
*/
@CircuitBreaker(name = ENTRIES_CIRCUIT_BREAKER, fallbackMethod = "fallbackHealth")
fun healthCheck(): HealthResponse {
logger.info("Executing health check...")
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
return HealthResponse(
status = "entries",
timestamp = currentTime,
service = "entries-service",
healthy = true
)
}
/**
* Fallback for health check returning DTO
*/
fun fallbackHealth(exception: Exception): HealthResponse {
logger.warn("Health check fallback triggered: {}", exception.message)
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
return HealthResponse(
status = "down",
timestamp = currentTime,
service = "entries-service",
healthy = false
)
}
}
@@ -0,0 +1,36 @@
package at.mocode.entries.service.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
/**
* Security configuration for the Entries Service.
* Enables method-level security for fine-grained authorization control.
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
class SecurityConfiguration {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf { it.disable() }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests { auth ->
auth
// Allow health check endpoints
.requestMatchers("/actuator/**", "/health/**").permitAll()
// Allow ping endpoints for monitoring (these are typically public)
.requestMatchers("/entries/**").permitAll()
// All other endpoints require authentication (handled by method-level security)
.anyRequest().authenticated()
}
.build()
}
}
@@ -0,0 +1,70 @@
spring:
application:
name: entries-service
cloud:
consul:
host: ${CONSUL_HOST:localhost}
port: ${CONSUL_PORT:8500}
discovery:
enabled: true
register: true
health-check-path: /actuator/health
health-check-interval: 10s
server:
port: ${SERVER_PORT:${ENTRIES_SERVICE_PORT:8083}}
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,circuitbreakers
endpoint:
health:
show-details: always
probes:
enabled: true
tracing:
enabled: ${TRACING_ENABLED:false}
sampling:
probability: ${TRACING_SAMPLING_PROBABILITY:0.1}
zipkin:
tracing:
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
connect-timeout: 1s
read-timeout: 5s
# 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:
entriesCircuitBreaker:
# 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
# Metrics configuration removed to avoid property resolution warnings
# Use micrometer and actuator endpoints for metrics instead
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%d{ISO8601} %-5level [%X{traceId:-}:%X{spanId:-}] %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<logger name="org.springframework" level="INFO"/>
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.springframework.boot.actuate" level="INFO"/>
<logger name="reactor.netty" level="WARN"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
+1 -1
View File
@@ -355,7 +355,7 @@ services:
entries-service:
build:
context: .
dockerfile: backend/services/entries/entries-service/Dockerfile
dockerfile: backend/services/entries/Dockerfile
args:
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.1.0}"
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-21}"
+80
View File
@@ -0,0 +1,80 @@
@startuml
!define table(x) entity x << (T, white) >>
!define primary_key(x) <b><color:#b8861b><&key></color> x</b>
!define foreign_key(x) <color:#aaaaaa><&key></color> x
skinparam linetype ortho
table(Turnier) {
primary_key(turnier_id)
--
name: varchar
start_datum: date
end_datum: date
ort: varchar
kategorie_oeto: varchar
}
table(Bewerb) {
primary_key(bewerb_id)
--
foreign_key(turnier_id)
nummer: varchar
disziplin: varchar
klasse: varchar
nenngeld: decimal
}
table(Person) {
primary_key(person_id)
--
vorname: varchar
nachname: varchar
lizenznummer: varchar
}
table(Pferd) {
primary_key(pferd_id)
--
foreign_key(besitzer_person_id)
name: varchar
lebensnummer: varchar
}
table(Nennung) {
primary_key(nennung_id)
--
foreign_key(bewerb_id)
foreign_key(person_id)
foreign_key(pferd_id)
status: varchar
nennungs_datum: timestamp
}
table(Startplatz) {
primary_key(startplatz_id)
--
foreign_key(nennung_id)
startnummer: int
startzeit: time
status: varchar
}
table(Ergebnis) {
primary_key(ergebnis_id)
--
foreign_key(startplatz_id)
wertnote: decimal
fehler: int
zeit: time
platzierung: int
}
Turnier ||--o{ Bewerb
Bewerb }o--o{ Nennung
Person }o--o{ Nennung
Pferd }o--o{ Nennung
Person }o..o{ Pferd
Nennung ||--|{ Startplatz
Startplatz ||--|{ Ergebnis
@enduml
+1
View File
@@ -65,6 +65,7 @@ include(":backend:infrastructure:monitoring:monitoring-server")
// include(":backend:services:events:events-service")
// --- ENTRIES (Nennungen) ---
include(":backend:services:entries:entries-api")
include(":backend:services:entries:entries-service")
// --- RESULTS (Ergebnisse) ---