(vision) SCS/DDD

Service Discovery einführen
Consul als Service-Registry implementieren
Services für automatische Registrierung konfigurieren
Dynamisches Service-Routing im API-Gateway einrichten
Health-Checks für jeden Service implementieren
This commit is contained in:
2025-07-21 23:54:13 +02:00
parent 3371b241df
commit 1ecac43d72
36 changed files with 4181 additions and 123 deletions
+8
View File
@@ -30,6 +30,14 @@ kotlin {
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlinDatetime)
implementation(libs.postgresql.driver)
// Service Discovery dependencies
implementation("com.orbitz.consul:consul-client:1.5.3")
implementation("com.ecwid.consul:consul-api:1.4.5") // Downgraded from 2.2.10 to 1.4.5 which is available on Maven Central
implementation("io.ktor:ktor-client-core:${libs.versions.ktor.get()}")
implementation("io.ktor:ktor-client-cio:${libs.versions.ktor.get()}")
implementation("io.ktor:ktor-client-content-negotiation:${libs.versions.ktor.get()}")
implementation("io.ktor:ktor-serialization-kotlinx-json:${libs.versions.ktor.get()}")
}
jvmTest.dependencies {
@@ -27,6 +27,9 @@ object AppConfig {
// Rate Limiting-Konfiguration
val rateLimit = RateLimitConfig()
// Service Discovery-Konfiguration
val serviceDiscovery = ServiceDiscoveryConfig()
// Datenbank-Konfiguration (wird nach dem Laden der Properties initialisiert)
val database: DatabaseConfig
@@ -40,6 +43,7 @@ object AppConfig {
security.configure(props)
logging.configure(props)
rateLimit.configure(props)
serviceDiscovery.configure(props)
// Datenbank-Konfiguration mit Properties initialisieren
database = DatabaseConfig.fromEnv(props)
@@ -303,4 +307,29 @@ class RateLimitConfig {
val limit: Int,
val periodMinutes: Int
)
}
}
/**
* Konfiguration für Service Discovery.
*/
class ServiceDiscoveryConfig {
// Consul Konfiguration
var enabled: Boolean = true
var consulHost: String = System.getenv("CONSUL_HOST") ?: "consul"
var consulPort: Int = System.getenv("CONSUL_PORT")?.toIntOrNull() ?: 8500
// Service Registration Konfiguration
var registerServices: Boolean = true
var healthCheckPath: String = "/health"
var healthCheckInterval: Int = 10 // Sekunden
fun configure(props: Properties) {
enabled = props.getProperty("service-discovery.enabled")?.toBoolean() ?: enabled
consulHost = props.getProperty("service-discovery.consul.host") ?: consulHost
consulPort = props.getProperty("service-discovery.consul.port")?.toIntOrNull() ?: consulPort
registerServices = props.getProperty("service-discovery.register-services")?.toBoolean() ?: registerServices
healthCheckPath = props.getProperty("service-discovery.health-check.path") ?: healthCheckPath
healthCheckInterval = props.getProperty("service-discovery.health-check.interval")?.toIntOrNull() ?: healthCheckInterval
}
}
@@ -12,6 +12,7 @@ data class DatabaseConfig(
val password: String,
val driverClassName: String = "org.postgresql.Driver",
val maxPoolSize: Int = 10,
val minPoolSize: Int = 5,
val autoMigrate: Boolean = true
) {
companion object {
@@ -29,6 +30,9 @@ data class DatabaseConfig(
val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull()
?: props.getProperty("database.maxPoolSize")?.toIntOrNull()
?: 10
val minPoolSize = System.getenv("DB_MIN_POOL_SIZE")?.toIntOrNull()
?: props.getProperty("database.minPoolSize")?.toIntOrNull()
?: 5
val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean()
?: props.getProperty("database.autoMigrate")?.toBoolean()
?: true
@@ -39,6 +43,7 @@ data class DatabaseConfig(
password = password,
driverClassName = "org.postgresql.Driver",
maxPoolSize = maxPoolSize,
minPoolSize = minPoolSize,
autoMigrate = autoMigrate
)
}
@@ -28,8 +28,37 @@ object DatabaseFactory {
username = config.username
password = config.password
maximumPoolSize = config.maxPoolSize
minimumIdle = config.minPoolSize // Use the minPoolSize from config
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"
// Use READ_COMMITTED for better performance while maintaining data integrity
// REPEATABLE_READ is more strict and can lead to more contention
transactionIsolation = "TRANSACTION_READ_COMMITTED"
// Connection validation
connectionTestQuery = "SELECT 1"
validationTimeout = 5000 // 5 seconds
// Connection timeouts
connectionTimeout = 30000 // 30 seconds
idleTimeout = 600000 // 10 minutes
maxLifetime = 1800000 // 30 minutes
// Leak detection
leakDetectionThreshold = 60000 // 1 minute
// Statement cache for better performance
dataSourceProperties["cachePrepStmts"] = "true"
dataSourceProperties["prepStmtCacheSize"] = "250"
dataSourceProperties["prepStmtCacheSqlLimit"] = "2048"
dataSourceProperties["useServerPrepStmts"] = "true"
// Connection initialization - run a simple query to warm up connections
connectionInitSql = "SELECT 1"
// Pool name for better identification in metrics
poolName = "MeldestelleDbPool"
validate()
}
@@ -52,4 +81,28 @@ object DatabaseFactory {
dataSource?.close()
dataSource = null
}
/**
* Gets the number of active connections in the pool.
* @return The number of active connections, or 0 if the pool is not initialized
*/
fun getActiveConnections(): Int {
return dataSource?.hikariPoolMXBean?.activeConnections ?: 0
}
/**
* Gets the number of idle connections in the pool.
* @return The number of idle connections, or 0 if the pool is not initialized
*/
fun getIdleConnections(): Int {
return dataSource?.hikariPoolMXBean?.idleConnections ?: 0
}
/**
* Gets the total number of connections in the pool.
* @return The total number of connections, or 0 if the pool is not initialized
*/
fun getTotalConnections(): Int {
return dataSource?.hikariPoolMXBean?.totalConnections ?: 0
}
}
@@ -0,0 +1,165 @@
package at.mocode.shared.discovery
import at.mocode.shared.config.AppConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.net.InetAddress
import java.util.*
import kotlin.time.Duration.Companion.seconds
import com.orbitz.consul.Consul
import com.orbitz.consul.model.agent.ImmutableRegistration
import com.orbitz.consul.model.agent.Registration
/**
* Service registration configuration.
*
* @property serviceName The name of the service to register
* @property serviceId A unique ID for this service instance (defaults to serviceName + random UUID)
* @property servicePort The port the service is running on
* @property healthCheckPath The path for the health check endpoint (defaults to "/health")
* @property healthCheckInterval The interval between health checks in seconds (defaults to 10 seconds)
* @property tags Optional tags to associate with the service
* @property meta Optional metadata to associate with the service
*/
data class ServiceRegistrationConfig(
val serviceName: String,
val serviceId: String = "$serviceName-${UUID.randomUUID()}",
val servicePort: Int,
val healthCheckPath: String = "/health",
val healthCheckInterval: Int = 10,
val tags: List<String> = emptyList(),
val meta: Map<String, String> = emptyMap()
)
/**
* Service registration component for registering services with Consul.
*/
class ServiceRegistration(
private val config: ServiceRegistrationConfig,
private val consulHost: String = "consul",
private val consulPort: Int = 8500
) {
private val consul: Consul by lazy {
try {
Consul.builder()
.withUrl("http://$consulHost:$consulPort")
.build()
} catch (e: Exception) {
println("Failed to connect to Consul: ${e.message}")
throw e
}
}
private val serviceId = config.serviceId
private var registered = false
/**
* Register the service with Consul.
*/
fun register() {
try {
val hostAddress = InetAddress.getLocalHost().hostAddress
// Create health check
val healthCheck = Registration.RegCheck.http(
"http://$hostAddress:${config.servicePort}${config.healthCheckPath}",
config.healthCheckInterval.toLong()
)
// Create service registration
val registration = ImmutableRegistration.builder()
.id(serviceId)
.name(config.serviceName)
.address(hostAddress)
.port(config.servicePort)
.tags(config.tags)
.meta(config.meta)
.check(healthCheck)
.build()
// Register service with Consul
consul.agentClient().register(registration)
registered = true
println("Service $serviceId registered with Consul at $consulHost:$consulPort")
// Start heartbeat to keep service registration active
startHeartbeat()
} catch (e: Exception) {
println("Failed to register service with Consul: ${e.message}")
e.printStackTrace()
}
}
/**
* Deregister the service from Consul.
*/
fun deregister() {
try {
if (registered) {
consul.agentClient().deregister(serviceId)
registered = false
println("Service $serviceId deregistered from Consul")
}
} catch (e: Exception) {
println("Failed to deregister service from Consul: ${e.message}")
e.printStackTrace()
}
}
/**
* Start a heartbeat to keep the service registration active.
*/
private fun startHeartbeat() {
CoroutineScope(Dispatchers.IO).launch {
while (registered) {
try {
// Send heartbeat to Consul
consul.agentClient().pass(serviceId)
delay(config.healthCheckInterval.seconds)
} catch (e: Exception) {
println("Failed to send heartbeat to Consul: ${e.message}")
delay(5.seconds)
}
}
}
}
}
/**
* Factory for creating ServiceRegistration instances.
*/
object ServiceRegistrationFactory {
/**
* Create a ServiceRegistration instance for a service.
*
* @param serviceName The name of the service to register
* @param servicePort The port the service is running on
* @param healthCheckPath The path for the health check endpoint (defaults to "/health")
* @param tags Optional tags to associate with the service
* @param meta Optional metadata to associate with the service
* @return A ServiceRegistration instance
*/
fun createServiceRegistration(
serviceName: String,
servicePort: Int,
healthCheckPath: String = "/health",
tags: List<String> = emptyList(),
meta: Map<String, String> = emptyMap()
): ServiceRegistration {
val config = ServiceRegistrationConfig(
serviceName = serviceName,
servicePort = servicePort,
healthCheckPath = healthCheckPath,
tags = tags,
meta = meta
)
// Get Consul host and port from configuration if available
val consulHost = AppConfig.serviceDiscovery.consulHost
val consulPort = AppConfig.serviceDiscovery.consulPort
return ServiceRegistration(config, consulHost, consulPort)
}
}