(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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user