refactor: replace Redis references with Valkey in tests and cache modules

Updated test cases in `ValkeyEventStoreTest` and cache implementation in `ValkeyDistributedCache` to fully transition from Redis to Valkey. Adjusted configurations, templates, connection handling, and exception management to reflect Valkey-specific behavior and APIs.
This commit is contained in:
2026-02-12 20:13:35 +01:00
parent 7757684b6e
commit 59f608b553
25 changed files with 1131 additions and 1101 deletions
@@ -28,6 +28,13 @@ dependencies {
// OPTIMIERUNG: Verwendung des `valkey-cache`-Bundles aus libs.versions.toml. // OPTIMIERUNG: Verwendung des `valkey-cache`-Bundles aus libs.versions.toml.
// Dieses Bundle enthält Spring Data Valkey, Lettuce und Jackson-Module. // Dieses Bundle enthält Spring Data Valkey, Lettuce und Jackson-Module.
implementation(libs.bundles.valkey.cache) implementation(libs.bundles.valkey.cache)
// Benötigt für Lettuce-basierten Valkey-Client (LettuceConnectionFactory)
implementation(libs.lettuce.core)
// Für Boot-Autoconfiguration-Annotations wie @ConfigurationProperties,
// @EnableConfigurationProperties und @ConditionalOnMissingBean
implementation("org.springframework.boot:spring-boot-autoconfigure")
// Optional, generiert Metadata f. @ConfigurationProperties (zur IDE-Unterstützung)
compileOnly("org.springframework.boot:spring-boot-configuration-processor")
// Stellt alle Test-Abhängigkeiten gebündelt bereit. // Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm) testImplementation(libs.bundles.testing.jvm)
@@ -3,18 +3,18 @@ package at.mocode.infrastructure.cache.valkey
import at.mocode.infrastructure.cache.api.CacheConfiguration import at.mocode.infrastructure.cache.api.CacheConfiguration
import at.mocode.infrastructure.cache.api.CacheSerializer import at.mocode.infrastructure.cache.api.CacheSerializer
import at.mocode.infrastructure.cache.api.DefaultCacheConfiguration import at.mocode.infrastructure.cache.api.DefaultCacheConfiguration
import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory
import io.valkey.springframework.data.valkey.connection.ValkeyPassword
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.RedisPassword
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
/** /**
* Valkey connection properties. * Valkey connection properties.
@@ -46,11 +46,11 @@ class ValkeyConfiguration {
* @return Valkey connection factory * @return Valkey connection factory
*/ */
@Bean @Bean
fun valkeyConnectionFactory(properties: ValkeyProperties): RedisConnectionFactory { fun valkeyConnectionFactory(properties: ValkeyProperties): ValkeyConnectionFactory {
val config = RedisStandaloneConfiguration().apply { val config = ValkeyStandaloneConfiguration().apply {
hostName = properties.host hostName = properties.host
port = properties.port port = properties.port
properties.password?.let { password = RedisPassword.of(it) } properties.password?.let { password = ValkeyPassword.of(it) }
database = properties.database database = properties.database
} }
@@ -68,11 +68,11 @@ class ValkeyConfiguration {
*/ */
@Bean @Bean
fun valkeyTemplate( fun valkeyTemplate(
@Qualifier("valkeyConnectionFactory") connectionFactory: RedisConnectionFactory @Qualifier("valkeyConnectionFactory") connectionFactory: ValkeyConnectionFactory
): RedisTemplate<String, ByteArray> { ): ValkeyTemplate<String, ByteArray> {
return RedisTemplate<String, ByteArray>().apply { return ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
// Use default serializer for values (byte arrays) // Use default serializer for values (byte arrays)
afterPropertiesSet() afterPropertiesSet()
} }
@@ -5,12 +5,12 @@ import at.mocode.infrastructure.cache.api.DefaultCacheConfiguration
import at.mocode.infrastructure.cache.api.get import at.mocode.infrastructure.cache.api.get
import at.mocode.infrastructure.cache.api.multiGet import at.mocode.infrastructure.cache.api.multiGet
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -37,14 +37,14 @@ class ValkeyDistributedCacheConfigurationTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
@BeforeEach @BeforeEach
@@ -52,13 +52,13 @@ class ValkeyDistributedCacheConfigurationTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -2,12 +2,12 @@ package at.mocode.infrastructure.cache.valkey
import at.mocode.infrastructure.cache.api.* import at.mocode.infrastructure.cache.api.*
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -29,14 +29,14 @@ class ValkeyDistributedCacheEdgeCasesTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
private lateinit var config: CacheConfiguration private lateinit var config: CacheConfiguration
private lateinit var cache: ValkeyDistributedCache private lateinit var cache: ValkeyDistributedCache
@@ -46,13 +46,13 @@ class ValkeyDistributedCacheEdgeCasesTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -2,14 +2,14 @@ package at.mocode.infrastructure.cache.valkey
import at.mocode.infrastructure.cache.api.* import at.mocode.infrastructure.cache.api.*
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -36,14 +36,14 @@ class ValkeyDistributedCacheIntegrationTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
private lateinit var config: CacheConfiguration private lateinit var config: CacheConfiguration
@@ -52,13 +52,13 @@ class ValkeyDistributedCacheIntegrationTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -2,15 +2,15 @@ package at.mocode.infrastructure.cache.valkey
import at.mocode.infrastructure.cache.api.* import at.mocode.infrastructure.cache.api.*
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -32,14 +32,14 @@ class ValkeyDistributedCachePerformanceTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
private lateinit var config: CacheConfiguration private lateinit var config: CacheConfiguration
private lateinit var cache: ValkeyDistributedCache private lateinit var cache: ValkeyDistributedCache
@@ -49,13 +49,13 @@ class ValkeyDistributedCachePerformanceTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -4,16 +4,16 @@ import at.mocode.infrastructure.cache.api.*
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.valkey.springframework.data.valkey.ValkeyConnectionFailureException
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.core.ValueOperations
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.RedisConnectionFailureException
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.core.ValueOperations
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -38,14 +38,14 @@ class ValkeyDistributedCacheResilienceTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
private lateinit var config: CacheConfiguration private lateinit var config: CacheConfiguration
@@ -54,13 +54,13 @@ class ValkeyDistributedCacheResilienceTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -76,7 +76,7 @@ class ValkeyDistributedCacheResilienceTest {
fun `test connection timeout scenarios`() = runBlocking { fun `test connection timeout scenarios`() = runBlocking {
logger.info { "Testing connection timeout scenarios" } logger.info { "Testing connection timeout scenarios" }
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>() val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>()
val mockValueOps = mockk<ValueOperations<String, ByteArray>>() val mockValueOps = mockk<ValueOperations<String, ByteArray>>()
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
@@ -117,7 +117,7 @@ class ValkeyDistributedCacheResilienceTest {
fun `test partial valkey failures`() { fun `test partial valkey failures`() {
logger.info { "Testing partial Valkey failures" } logger.info { "Testing partial Valkey failures" }
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>() val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>()
val mockValueOps = mockk<ValueOperations<String, ByteArray>>() val mockValueOps = mockk<ValueOperations<String, ByteArray>>()
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
@@ -128,14 +128,14 @@ class ValkeyDistributedCacheResilienceTest {
// Simulate intermittent connection failures (fail every 3rd operation) // Simulate intermittent connection failures (fail every 3rd operation)
every { mockValueOps.get(any()) } answers { every { mockValueOps.get(any()) } answers {
if (failureCounter.incrementAndGet() % 3 == 0) { if (failureCounter.incrementAndGet() % 3 == 0) {
throw RedisConnectionFailureException("Intermittent failure") throw ValkeyConnectionFailureException("Intermittent failure") as Throwable
} }
serializer.serializeEntry(CacheEntry("test", "value")) serializer.serializeEntry(CacheEntry("test", "value"))
} }
every { mockValueOps.set(any<String>(), any<ByteArray>(), any<JavaDuration>()) } answers { every { mockValueOps.set(any<String>(), any<ByteArray>(), any<JavaDuration>()) } answers {
if (failureCounter.incrementAndGet() % 3 == 0) { if (failureCounter.incrementAndGet() % 3 == 0) {
throw RedisConnectionFailureException("Intermittent failure") throw ValkeyConnectionFailureException("Intermittent failure")
} }
} }
@@ -197,20 +197,20 @@ class ValkeyDistributedCacheResilienceTest {
// Phase 2: Simulate network partition by creating a new cache with a broken connection // Phase 2: Simulate network partition by creating a new cache with a broken connection
logger.info { "Phase 2: Simulating network partition" } logger.info { "Phase 2: Simulating network partition" }
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>() val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>()
val mockValueOps = mockk<ValueOperations<String, ByteArray>>() val mockValueOps = mockk<ValueOperations<String, ByteArray>>()
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
every { mockValueOps.get(any()) } throws RedisConnectionFailureException("Network partition") every { mockValueOps.get(any()) } throws ValkeyConnectionFailureException("Network partition")
every { every {
mockValueOps.set( mockValueOps.set(
any<String>(), any<String>(),
any<ByteArray>(), any<ByteArray>(),
any<JavaDuration>() any<JavaDuration>()
) )
} throws RedisConnectionFailureException("Network partition") } throws ValkeyConnectionFailureException("Network partition")
every { mockTemplate.delete(any<String>()) } throws RedisConnectionFailureException("Network partition") every { mockTemplate.delete(any<String>()) } throws ValkeyConnectionFailureException("Network partition")
every { mockTemplate.hasKey(any()) } throws RedisConnectionFailureException("Network partition") every { mockTemplate.hasKey(any()) } throws ValkeyConnectionFailureException("Network partition")
val partitionedCache = ValkeyDistributedCache(mockTemplate, serializer, config) val partitionedCache = ValkeyDistributedCache(mockTemplate, serializer, config)
@@ -235,7 +235,7 @@ class ValkeyDistributedCacheResilienceTest {
fun `test reconnection and synchronization after network issues`() { fun `test reconnection and synchronization after network issues`() {
logger.info { "Testing reconnection and synchronization" } logger.info { "Testing reconnection and synchronization" }
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>() val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>()
val mockValueOps = mockk<ValueOperations<String, ByteArray>>() val mockValueOps = mockk<ValueOperations<String, ByteArray>>()
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
@@ -243,15 +243,15 @@ class ValkeyDistributedCacheResilienceTest {
val reconnectingCache = ValkeyDistributedCache(mockTemplate, serializer, config) val reconnectingCache = ValkeyDistributedCache(mockTemplate, serializer, config)
// Phase 1: Simulate disconnection // Phase 1: Simulate disconnection
every { mockValueOps.get(any()) } throws RedisConnectionFailureException("Disconnected") every { mockValueOps.get(any()) } throws ValkeyConnectionFailureException("Disconnected")
every { every {
mockValueOps.set( mockValueOps.set(
any<String>(), any<String>(),
any<ByteArray>(), any<ByteArray>(),
any<JavaDuration>() any<JavaDuration>()
) )
} throws RedisConnectionFailureException("Disconnected") } throws ValkeyConnectionFailureException("Disconnected")
every { mockTemplate.hasKey(any()) } throws RedisConnectionFailureException("Disconnected") every { mockTemplate.hasKey(any()) } throws ValkeyConnectionFailureException("Disconnected")
reconnectingCache.set("reconnect-test-1", "value-1") reconnectingCache.set("reconnect-test-1", "value-1")
reconnectingCache.set("reconnect-test-2", "value-2") reconnectingCache.set("reconnect-test-2", "value-2")
@@ -5,15 +5,15 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import io.valkey.springframework.data.valkey.ValkeyConnectionFailureException
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import io.valkey.springframework.data.valkey.core.ValueOperations
import io.valkey.springframework.data.valkey.serializer.StringValkeySerializer
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.RedisConnectionFailureException
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.core.ValueOperations
import org.springframework.data.redis.serializer.StringRedisSerializer
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -31,14 +31,14 @@ class ValkeyDistributedCacheTest {
@Container @Container
val valkeyContainer = GenericContainer<Nothing>( val valkeyContainer = GenericContainer<Nothing>(
DockerImageName.parse("valkey/valkey:9-alpine") DockerImageName.parse("valkey/valkey:8.0.2-alpine")
.asCompatibleSubstituteFor("valkey") .asCompatibleSubstituteFor("redis")
).apply { ).apply {
withExposedPorts(6379) withExposedPorts(6379)
} }
} }
private lateinit var valkeyTemplate: RedisTemplate<String, ByteArray> private lateinit var valkeyTemplate: ValkeyTemplate<String, ByteArray>
private lateinit var serializer: CacheSerializer private lateinit var serializer: CacheSerializer
private lateinit var config: CacheConfiguration private lateinit var config: CacheConfiguration
private lateinit var cache: ValkeyDistributedCache private lateinit var cache: ValkeyDistributedCache
@@ -48,13 +48,13 @@ class ValkeyDistributedCacheTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = RedisTemplate<String, ByteArray>().apply { valkeyTemplate = ValkeyTemplate<String, ByteArray>().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
keySerializer = StringRedisSerializer() keySerializer = StringValkeySerializer()
afterPropertiesSet() afterPropertiesSet()
} }
@@ -131,7 +131,7 @@ class ValkeyDistributedCacheTest {
@Test @Test
fun `should handle offline mode and synchronize correctly`() { fun `should handle offline mode and synchronize correctly`() {
// Arrange // Arrange
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>(relaxed = true) val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>(relaxed = true)
val mockValueOps = mockk<ValueOperations<String, ByteArray>>(relaxed = true) val mockValueOps = mockk<ValueOperations<String, ByteArray>>(relaxed = true)
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
@@ -157,10 +157,15 @@ class ValkeyDistributedCacheTest {
any<ByteArray>(), any<ByteArray>(),
any<JavaDuration>() any<JavaDuration>()
) )
} throws RedisConnectionFailureException("Valkey is down") } throws ValkeyConnectionFailureException("Valkey is down")
every { mockValueOps.set(any<String>(), any<ByteArray>()) } throws RedisConnectionFailureException("Valkey is down") every {
mockValueOps.set(
any<String>(),
any<ByteArray>()
)
} throws ValkeyConnectionFailureException("Valkey is down")
every { mockTemplate.delete(any<String>()) } throws RedisConnectionFailureException("Valkey is down") every { mockTemplate.delete(any<String>()) } throws ValkeyConnectionFailureException("Valkey is down")
offlineCache.set("key2", "offline-value") offlineCache.set("key2", "offline-value")
offlineCache.delete("key1") offlineCache.delete("key1")
@@ -246,13 +251,13 @@ class ValkeyDistributedCacheTest {
@Test @Test
fun `test handling valkey connection failures`() { fun `test handling valkey connection failures`() {
// Create a mock ValkeyTemplate and ValueOperations // Create a mock ValkeyTemplate and ValueOperations
val mockTemplate = mockk<RedisTemplate<String, ByteArray>>() val mockTemplate = mockk<ValkeyTemplate<String, ByteArray>>()
val mockValueOps = mockk<ValueOperations<String, ByteArray>>() val mockValueOps = mockk<ValueOperations<String, ByteArray>>()
// Configure the mock to throw connection failure // Configure the mock to throw connection failure
every { mockTemplate.opsForValue() } returns mockValueOps every { mockTemplate.opsForValue() } returns mockValueOps
every { mockValueOps.get(any()) } throws RedisConnectionFailureException("Test connection failure") every { mockValueOps.get(any()) } throws ValkeyConnectionFailureException("Test connection failure")
every { mockTemplate.hasKey(any()) } throws RedisConnectionFailureException("Test connection failure") every { mockTemplate.hasKey(any()) } throws ValkeyConnectionFailureException("Test connection failure")
// Create a cache with the mock // Create a cache with the mock
val mockCache = ValkeyDistributedCache(mockTemplate, serializer, config) val mockCache = ValkeyDistributedCache(mockTemplate, serializer, config)
@@ -1,40 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<!-- Console Appender für Test-Ausgaben --> <!-- Console Appender für Test-Ausgaben -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<!-- Cache-spezifische Logger --> <!-- Cache-spezifische Logger -->
<logger name="at.mocode.infrastructure.cache" level="DEBUG" /> <logger name="at.mocode.infrastructure.cache" level="DEBUG"/>
<!-- Performance Test Logger --> <!-- Performance Test Logger -->
<logger name="ValkeyDistributedCachePerformanceTest" level="INFO" /> <logger name="ValkeyDistributedCachePerformanceTest" level="INFO"/>
<!-- Edge Cases Test Logger --> <!-- Edge Cases Test Logger -->
<logger name="ValkeyDistributedCacheEdgeCasesTest" level="INFO" /> <logger name="ValkeyDistributedCacheEdgeCasesTest" level="INFO"/>
<!-- Resilience Test Logger --> <!-- Resilience Test Logger -->
<logger name="ValkeyDistributedCacheResilienceTest" level="INFO" /> <logger name="ValkeyDistributedCacheResilienceTest" level="INFO"/>
<!-- Configuration Test Logger --> <!-- Configuration Test Logger -->
<logger name="ValkeyDistributedCacheConfigurationTest" level="INFO" /> <logger name="ValkeyDistributedCacheConfigurationTest" level="INFO"/>
<!-- Integration Test Logger --> <!-- Integration Test Logger -->
<logger name="ValkeyDistributedCacheIntegrationTest" level="INFO" /> <logger name="ValkeyDistributedCacheIntegrationTest" level="INFO"/>
<!-- Testcontainers Logger (reduziert Verbosity) --> <!-- Testcontainers Logger (reduziert Verbosity) -->
<logger name="org.testcontainers" level="WARN" /> <logger name="org.testcontainers" level="WARN"/>
<logger name="com.github.dockerjava" level="WARN" /> <logger name="com.github.dockerjava" level="WARN"/>
<!-- Valkey/Lettuce Logger (reduziert Verbosity) --> <!-- Valkey/Lettuce Logger (reduziert Verbosity) -->
<logger name="io.lettuce" level="WARN" /> <logger name="io.lettuce" level="WARN"/>
<logger name="org.springframework.data.redis" level="WARN" /> <logger name="io.valkey.springframework.data" level="WARN"/>
<!-- Root Logger --> <!-- Root Logger -->
<root level="INFO"> <root level="INFO">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT"/>
</root> </root>
</configuration> </configuration>
@@ -30,6 +30,11 @@ dependencies {
// OPTIMIERUNG: Wiederverwendung des `valkey-cache`-Bundles, da es die // OPTIMIERUNG: Wiederverwendung des `valkey-cache`-Bundles, da es die
// gleichen Technologien (Spring Data Valkey, Lettuce, Jackson) verwendet // gleichen Technologien (Spring Data Valkey, Lettuce, Jackson) verwendet
implementation(libs.bundles.valkey.cache) implementation(libs.bundles.valkey.cache)
// Benötigt für Lettuce-basierten Valkey-Client (LettuceConnectionFactory)
implementation(libs.lettuce.core)
// Für Boot-Autoconfiguration-Annotations (z. B. @ConditionalOnMissingBean,
// @ConfigurationProperties, @EnableConfigurationProperties)
implementation("org.springframework.boot:spring-boot-autoconfigure")
// Stellt Jakarta Annotations bereit (z. B. @PostConstruct), die von Spring verwendet werden // Stellt Jakarta Annotations bereit (z. B. @PostConstruct), die von Spring verwendet werden
implementation(libs.jakarta.annotation.api) implementation(libs.jakarta.annotation.api)
// Für Kotlin-spezifische Coroutines-Integration mit Spring // Für Kotlin-spezifische Coroutines-Integration mit Spring
@@ -2,12 +2,16 @@ package at.mocode.infrastructure.eventstore.valkey
import at.mocode.core.domain.event.DomainEvent import at.mocode.core.domain.event.DomainEvent
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import io.valkey.springframework.data.valkey.connection.stream.Consumer
import io.valkey.springframework.data.valkey.connection.stream.MapRecord
import io.valkey.springframework.data.valkey.connection.stream.ReadOffset
import io.valkey.springframework.data.valkey.connection.stream.StreamOffset
import io.valkey.springframework.data.valkey.connection.stream.StreamReadOptions
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy import jakarta.annotation.PreDestroy
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.domain.Range import org.springframework.data.domain.Range
import org.springframework.data.redis.connection.stream.*
import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@@ -16,272 +20,272 @@ import java.util.concurrent.CopyOnWriteArrayList
* Consumer for Valkey Streams that processes events using consumer groups. * Consumer for Valkey Streams that processes events using consumer groups.
*/ */
class ValkeyEventConsumer( class ValkeyEventConsumer(
private val valkeyTemplate: StringRedisTemplate, private val valkeyTemplate: StringValkeyTemplate,
private val serializer: EventSerializer, private val serializer: EventSerializer,
private val properties: ValkeyEventStoreProperties private val properties: ValkeyEventStoreProperties
) { ) {
private val logger = LoggerFactory.getLogger(ValkeyEventConsumer::class.java) private val logger = LoggerFactory.getLogger(ValkeyEventConsumer::class.java)
private val eventTypeHandlers = ConcurrentHashMap<String, CopyOnWriteArrayList<(DomainEvent) -> Unit>>() private val eventTypeHandlers = ConcurrentHashMap<String, CopyOnWriteArrayList<(DomainEvent) -> Unit>>()
private val allEventHandlers = CopyOnWriteArrayList<(DomainEvent) -> Unit>() private val allEventHandlers = CopyOnWriteArrayList<(DomainEvent) -> Unit>()
private var running = false private var running = false
/** /**
* Initializes the consumer. * Initializes the consumer.
*/ */
@PostConstruct @PostConstruct
fun init() { fun init() {
if (properties.createConsumerGroupIfNotExists) { if (properties.createConsumerGroupIfNotExists) {
createConsumerGroupsIfNotExist() createConsumerGroupsIfNotExist()
}
}
/**
* Stops the consumer.
*/
@PreDestroy
fun shutdown() {
running = false
}
/**
* Registers a handler for a specific event type.
*
* @param eventType The type of event to handle
* @param handler The handler to call when an event of the specified type is received
*/
fun registerEventHandler(eventType: String, handler: (DomainEvent) -> Unit) {
eventTypeHandlers.computeIfAbsent(eventType) { CopyOnWriteArrayList() }.add(handler)
logger.debug("Registered handler for event type: $eventType")
}
/**
* Registers a handler for all events.
*
* @param handler The handler to call when any event is received
*/
fun registerAllEventsHandler(handler: (DomainEvent) -> Unit) {
allEventHandlers.add(handler)
logger.debug("Registered handler for all events")
}
/**
* Unregisters a handler for a specific event type.
*
* @param eventType The type of event
* @param handler The handler to unregister
*/
fun unregisterEventHandler(eventType: String, handler: (DomainEvent) -> Unit) {
eventTypeHandlers[eventType]?.remove(handler)
logger.debug("Unregistered handler for event type: $eventType")
}
/**
* Unregisters a handler for all events.
*
* @param handler The handler to unregister
*/
fun unregisterAllEventsHandler(handler: (DomainEvent) -> Unit) {
allEventHandlers.remove(handler)
logger.debug("Unregistered handler for all events")
}
/**
* Creates consumer groups for all streams if they don't exist.
*/
private fun createConsumerGroupsIfNotExist() {
try {
val allEventsStreamKey = getAllEventsStreamKey()
try {
valkeyTemplate.opsForStream<String, String>()
.add(allEventsStreamKey, mapOf("init" to "init"))
logger.debug("Ensured all-events stream has messages: $allEventsStreamKey")
} catch (e: Exception) {
logger.debug("All-events stream might already have messages: ${e.message}")
}
createConsumerGroupIfNotExists(allEventsStreamKey)
val streamKeys = valkeyTemplate.keys("${properties.streamPrefix}*")
for (streamKey in streamKeys) {
if (streamKey != allEventsStreamKey) {
createConsumerGroupIfNotExists(streamKey)
} }
}
} catch (e: Exception) {
logger.error("Error creating consumer groups: ${e.message}", e)
}
}
/**
* Creates a consumer group for a stream if it doesn't exist.
*
* @param streamKey The key of the stream
*/
private fun createConsumerGroupIfNotExists(streamKey: String) {
try {
try {
valkeyTemplate.opsForStream<String, String>()
.add(streamKey, mapOf("init" to "init"))
logger.debug("Ensured stream has messages: $streamKey")
} catch (e: Exception) {
logger.debug("Stream $streamKey might already have messages: ${e.message}")
}
try {
valkeyTemplate.opsForStream<String, String>()
.createGroup(streamKey, ReadOffset.latest(), properties.consumerGroup)
logger.debug("Created consumer group ${properties.consumerGroup} for stream: $streamKey")
} catch (e: Exception) {
logger.debug("Could not create consumer group ${properties.consumerGroup} for stream: $streamKey: ${e.message}")
}
} catch (e: Exception) {
logger.error("Error creating consumer group for stream $streamKey: ${e.message}", e)
}
}
/**
* Periodic polls for new events from all streams.
*/
@Scheduled(fixedDelayString = "\${valkey.event-store.poll-interval:100}")
fun pollEvents() {
if (!running) {
running = true
} }
/** try {
* Stops the consumer. pollStream(getAllEventsStreamKey())
*/ claimPendingMessages()
@PreDestroy } catch (e: Exception) {
fun shutdown() { logger.error("Error polling events: ${e.message}", e)
running = false
} }
}
/** /**
* Registers a handler for a specific event type. * Polls a stream for new events.
* *
* @param eventType The type of event to handle * @param streamKey The key of the stream to poll
* @param handler The handler to call when an event of the specified type is received */
*/ private fun pollStream(streamKey: String) {
fun registerEventHandler(eventType: String, handler: (DomainEvent) -> Unit) { try {
eventTypeHandlers.computeIfAbsent(eventType) { CopyOnWriteArrayList() }.add(handler) val options = StreamReadOptions.empty()
logger.debug("Registered handler for event type: $eventType") .count(properties.maxBatchSize.toLong())
} .block(properties.pollTimeout)
/** val records = valkeyTemplate.opsForStream<String, String>()
* Registers a handler for all events. .read(
* Consumer.from(properties.consumerGroup, properties.consumerName),
* @param handler The handler to call when any event is received options,
*/ StreamOffset.create(streamKey, ReadOffset.lastConsumed())
fun registerAllEventsHandler(handler: (DomainEvent) -> Unit) { )
allEventHandlers.add(handler)
logger.debug("Registered handler for all events")
}
/** if (records != null) {
* Unregisters a handler for a specific event type. for (record in records) {
* processRecord(record)
* @param eventType The type of event
* @param handler The handler to unregister
*/
fun unregisterEventHandler(eventType: String, handler: (DomainEvent) -> Unit) {
eventTypeHandlers[eventType]?.remove(handler)
logger.debug("Unregistered handler for event type: $eventType")
}
/**
* Unregisters a handler for all events.
*
* @param handler The handler to unregister
*/
fun unregisterAllEventsHandler(handler: (DomainEvent) -> Unit) {
allEventHandlers.remove(handler)
logger.debug("Unregistered handler for all events")
}
/**
* Creates consumer groups for all streams if they don't exist.
*/
private fun createConsumerGroupsIfNotExist() {
try {
val allEventsStreamKey = getAllEventsStreamKey()
try {
valkeyTemplate.opsForStream<String, String>()
.add(allEventsStreamKey, mapOf("init" to "init"))
logger.debug("Ensured all-events stream has messages: $allEventsStreamKey")
} catch (e: Exception) {
logger.debug("All-events stream might already have messages: ${e.message}")
}
createConsumerGroupIfNotExists(allEventsStreamKey)
val streamKeys = valkeyTemplate.keys("${properties.streamPrefix}*")
for (streamKey in streamKeys) {
if (streamKey != allEventsStreamKey) {
createConsumerGroupIfNotExists(streamKey)
}
}
} catch (e: Exception) {
logger.error("Error creating consumer groups: ${e.message}", e)
} }
}
} catch (e: Exception) {
val message = e.message
if (message == null || !message.contains("NOGROUP")) {
logger.error("Error polling stream $streamKey: ${e.message}", e)
}
} }
}
/** /**
* Creates a consumer group for a stream if it doesn't exist. * Claims pending messages that have been idle for too long.
* */
* @param streamKey The key of the stream private fun claimPendingMessages() {
*/ try {
private fun createConsumerGroupIfNotExists(streamKey: String) { val streamKey = getAllEventsStreamKey()
try {
try {
valkeyTemplate.opsForStream<String, String>()
.add(streamKey, mapOf("init" to "init"))
logger.debug("Ensured stream has messages: $streamKey")
} catch (e: Exception) {
logger.debug("Stream $streamKey might already have messages: ${e.message}")
}
try { val pendingSummary = valkeyTemplate.opsForStream<String, String>()
valkeyTemplate.opsForStream<String, String>() .pending(streamKey, properties.consumerGroup)
.createGroup(streamKey, ReadOffset.latest(), properties.consumerGroup)
logger.debug("Created consumer group ${properties.consumerGroup} for stream: $streamKey")
} catch (e: Exception) {
logger.debug("Could not create consumer group ${properties.consumerGroup} for stream: $streamKey: ${e.message}")
}
} catch (e: Exception) {
logger.error("Error creating consumer group for stream $streamKey: ${e.message}", e)
}
}
/** if (pendingSummary != null && pendingSummary.totalPendingMessages > 0) {
* Periodic polls for new events from all streams. val pendingMessages = valkeyTemplate.opsForStream<String, String>()
*/ .pending(
@Scheduled(fixedDelayString = $$"${valkey.event-store.poll-interval:100}") streamKey,
fun pollEvents() { Consumer.from(properties.consumerGroup, properties.consumerName),
if (!running) { Range.unbounded<String>(),
running = true properties.maxBatchSize.toLong()
} )
try { if (pendingMessages.size() > 0) {
pollStream(getAllEventsStreamKey()) val messageIdsList = pendingMessages.map { it.id }.toList()
claimPendingMessages()
} catch (e: Exception) {
logger.error("Error polling events: ${e.message}", e)
}
}
/** if (messageIdsList.isNotEmpty()) {
* Polls a stream for new events. val messageIds = messageIdsList.toTypedArray()
*
* @param streamKey The key of the stream to poll
*/
private fun pollStream(streamKey: String) {
try {
val options = StreamReadOptions.empty()
.count(properties.maxBatchSize.toLong())
.block(properties.pollTimeout)
val records = valkeyTemplate.opsForStream<String, String>() val records = valkeyTemplate.opsForStream<String, String>()
.read( .claim(
Consumer.from(properties.consumerGroup, properties.consumerName), streamKey,
options, properties.consumerGroup,
StreamOffset.create(streamKey, ReadOffset.lastConsumed()) properties.consumerName,
) properties.claimIdleTimeout,
*messageIds
)
if (records != null) { for (record in records) {
for (record in records) { processRecord(record)
processRecord(record)
}
}
} catch (e: Exception) {
val message = e.message
if (message == null || !message.contains("NOGROUP")) {
logger.error("Error polling stream $streamKey: ${e.message}", e)
} }
}
} }
}
} catch (e: Exception) {
logger.error("Error claiming pending messages: ${e.message}", e)
} }
}
/** /**
* Claims pending messages that have been idle for too long. * Processes a record from a stream.
*/ *
private fun claimPendingMessages() { * @param record The record to process
*/
private fun processRecord(record: MapRecord<String, String, String>) {
try {
val data = record.value
if (data.size == 1 && data.containsKey("init") && data["init"] == "init") {
logger.debug("Skipping init message")
valkeyTemplate.opsForStream<String, String>()
.acknowledge(properties.consumerGroup, record)
return
}
val event = serializer.deserialize(data)
val eventType = serializer.getEventType(data)
eventTypeHandlers[eventType]?.forEach { handler ->
try { try {
val streamKey = getAllEventsStreamKey() handler(event)
val pendingSummary = valkeyTemplate.opsForStream<String, String>()
.pending(streamKey, properties.consumerGroup)
if (pendingSummary != null && pendingSummary.totalPendingMessages > 0) {
val pendingMessages = valkeyTemplate.opsForStream<String, String>()
.pending(
streamKey,
Consumer.from(properties.consumerGroup, properties.consumerName),
Range.unbounded<String>(),
properties.maxBatchSize.toLong()
)
if (pendingMessages.size() > 0) {
val messageIdsList = pendingMessages.map { it.id }.toList()
if (messageIdsList.isNotEmpty()) {
val messageIds = messageIdsList.toTypedArray()
val records = valkeyTemplate.opsForStream<String, String>()
.claim(
streamKey,
properties.consumerGroup,
properties.consumerName,
properties.claimIdleTimeout,
*messageIds
)
for (record in records) {
processRecord(record)
}
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Error claiming pending messages: ${e.message}", e) logger.error("Error handling event of type $eventType: ${e.message}", e)
} }
} }
/** allEventHandlers.forEach { handler ->
* Processes a record from a stream.
*
* @param record The record to process
*/
private fun processRecord(record: MapRecord<String, String, String>) {
try { try {
val data = record.value handler(event)
if (data.size == 1 && data.containsKey("init") && data["init"] == "init") {
logger.debug("Skipping init message")
valkeyTemplate.opsForStream<String, String>()
.acknowledge(properties.consumerGroup, record)
return
}
val event = serializer.deserialize(data)
val eventType = serializer.getEventType(data)
eventTypeHandlers[eventType]?.forEach { handler ->
try {
handler(event)
} catch (e: Exception) {
logger.error("Error handling event of type $eventType: ${e.message}", e)
}
}
allEventHandlers.forEach { handler ->
try {
handler(event)
} catch (e: Exception) {
logger.error("Error handling event: ${e.message}", e)
}
}
valkeyTemplate.opsForStream<String, String>()
.acknowledge(properties.consumerGroup, record)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Error processing record: ${e.message}", e) logger.error("Error handling event: ${e.message}", e)
} }
} }
/** valkeyTemplate.opsForStream<String, String>()
* Gets the Valkey key for the all-events stream. .acknowledge(properties.consumerGroup, record)
*
* @return The Valkey key for the all-events stream } catch (e: Exception) {
*/ logger.error("Error processing record: ${e.message}", e)
private fun getAllEventsStreamKey(): String {
return "${properties.streamPrefix}${properties.allEventsStream}"
} }
}
/**
* Gets the Valkey key for the all-events stream.
*
* @return The Valkey key for the all-events stream
*/
private fun getAllEventsStreamKey(): String {
return "${properties.streamPrefix}${properties.allEventsStream}"
}
} }
@@ -8,16 +8,17 @@ import at.mocode.infrastructure.eventstore.api.ConcurrencyException
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import at.mocode.infrastructure.eventstore.api.Subscription import at.mocode.infrastructure.eventstore.api.Subscription
import io.valkey.springframework.data.valkey.core.SessionCallback
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import io.valkey.springframework.data.valkey.core.ValkeyOperations
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.dao.DataAccessException import org.springframework.dao.DataAccessException
import org.springframework.data.domain.Range import org.springframework.data.domain.Range
import org.springframework.data.redis.core.SessionCallback
import org.springframework.data.redis.core.StringRedisTemplate
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
class ValkeyEventStore( class ValkeyEventStore(
private val valkeyTemplate: StringRedisTemplate, private val valkeyTemplate: StringValkeyTemplate,
private val serializer: EventSerializer, private val serializer: EventSerializer,
private val properties: ValkeyEventStoreProperties private val properties: ValkeyEventStoreProperties
) : EventStore { ) : EventStore {
@@ -131,8 +132,8 @@ class ValkeyEventStore(
try { try {
valkeyTemplate.execute(object : SessionCallback<List<Any>> { valkeyTemplate.execute(object : SessionCallback<List<Any>> {
@Throws(DataAccessException::class) @Throws(DataAccessException::class)
override fun <K, V> execute(operations: org.springframework.data.redis.core.RedisOperations<K, V>): List<Any> { override fun <K, V> execute(operations: ValkeyOperations<K, V>): List<Any> {
val streamOps = (operations as StringRedisTemplate).opsForStream<String, String>() val streamOps = (operations as StringValkeyTemplate).opsForStream<String, String>()
operations.multi() operations.multi()
@@ -178,8 +179,8 @@ class ValkeyEventStore(
try { try {
valkeyTemplate.execute(object : SessionCallback<List<Any>> { valkeyTemplate.execute(object : SessionCallback<List<Any>> {
@Throws(DataAccessException::class) @Throws(DataAccessException::class)
override fun <K, V> execute(operations: org.springframework.data.redis.core.RedisOperations<K, V>): List<Any> { override fun <K, V> execute(operations: ValkeyOperations<K, V>): List<Any> {
val streamOps = (operations as StringRedisTemplate).opsForStream<String, String>() val streamOps = (operations as StringValkeyTemplate).opsForStream<String, String>()
operations.multi() operations.multi()
streamOps.add(streamKey, eventData) streamOps.add(streamKey, eventData)
@@ -2,16 +2,17 @@ package at.mocode.infrastructure.eventstore.valkey
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory
import io.valkey.springframework.data.valkey.connection.ValkeyPassword
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.RedisPassword
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import java.time.Duration import java.time.Duration
/** /**
@@ -53,11 +54,11 @@ class ValkeyEventStoreConfiguration {
*/ */
@Bean @Bean
@ConditionalOnMissingBean(name = ["eventStoreValkeyConnectionFactory"]) @ConditionalOnMissingBean(name = ["eventStoreValkeyConnectionFactory"])
fun eventStoreValkeyConnectionFactory(properties: ValkeyEventStoreProperties): RedisConnectionFactory { fun eventStoreValkeyConnectionFactory(properties: ValkeyEventStoreProperties): ValkeyConnectionFactory {
val config = RedisStandaloneConfiguration().apply { val config = ValkeyStandaloneConfiguration().apply {
hostName = properties.host hostName = properties.host
port = properties.port port = properties.port
properties.password?.let { password = RedisPassword.of(it) } properties.password?.let { password = ValkeyPassword.of(it) }
database = properties.database database = properties.database
} }
@@ -76,10 +77,10 @@ class ValkeyEventStoreConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(name = ["eventStoreValkeyTemplate"]) @ConditionalOnMissingBean(name = ["eventStoreValkeyTemplate"])
fun eventStoreValkeyTemplate( fun eventStoreValkeyTemplate(
@org.springframework.beans.factory.annotation.Qualifier("eventStoreValkeyConnectionFactory") @Qualifier("eventStoreValkeyConnectionFactory")
connectionFactory: RedisConnectionFactory connectionFactory: ValkeyConnectionFactory
): StringRedisTemplate { ): StringValkeyTemplate {
return StringRedisTemplate().apply { return StringValkeyTemplate().apply {
setConnectionFactory(connectionFactory) setConnectionFactory(connectionFactory)
afterPropertiesSet() afterPropertiesSet()
} }
@@ -107,8 +108,8 @@ class ValkeyEventStoreConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
fun eventStore( fun eventStore(
@org.springframework.beans.factory.annotation.Qualifier("eventStoreValkeyTemplate") @Qualifier("eventStoreValkeyTemplate")
valkeyTemplate: StringRedisTemplate, valkeyTemplate: StringValkeyTemplate,
eventSerializer: EventSerializer, eventSerializer: EventSerializer,
properties: ValkeyEventStoreProperties properties: ValkeyEventStoreProperties
): EventStore { ): EventStore {
@@ -126,8 +127,8 @@ class ValkeyEventStoreConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
fun eventConsumer( fun eventConsumer(
@org.springframework.beans.factory.annotation.Qualifier("eventStoreValkeyTemplate") @Qualifier("eventStoreValkeyTemplate")
valkeyTemplate: StringRedisTemplate, valkeyTemplate: StringValkeyTemplate,
eventSerializer: EventSerializer, eventSerializer: EventSerializer,
properties: ValkeyEventStoreProperties properties: ValkeyEventStoreProperties
): ValkeyEventConsumer { ): ValkeyEventConsumer {
@@ -8,6 +8,8 @@ import at.mocode.infrastructure.cache.valkey.JacksonCacheSerializer
import at.mocode.infrastructure.cache.valkey.ValkeyConfiguration import at.mocode.infrastructure.cache.valkey.ValkeyConfiguration
import at.mocode.infrastructure.cache.valkey.ValkeyDistributedCache import at.mocode.infrastructure.cache.valkey.ValkeyDistributedCache
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory
import io.valkey.springframework.data.valkey.core.ValkeyTemplate
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@@ -20,8 +22,6 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import import org.springframework.context.annotation.Import
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.test.context.DynamicPropertyRegistry import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
@@ -96,7 +96,7 @@ class ValkeyCacheAndEventStoreIntegrationTest {
class TestConfig { class TestConfig {
@Bean @Bean
fun distributedCache( fun distributedCache(
@Qualifier("valkeyTemplate") valkeyTemplate: RedisTemplate<String, ByteArray>, @Qualifier("valkeyTemplate") valkeyTemplate: ValkeyTemplate<String, ByteArray>,
cacheConfiguration: CacheConfiguration cacheConfiguration: CacheConfiguration
): DistributedCache { ): DistributedCache {
return ValkeyDistributedCache( return ValkeyDistributedCache(
@@ -116,11 +116,11 @@ class ValkeyCacheAndEventStoreIntegrationTest {
// Verify separate ConnectionFactories // Verify separate ConnectionFactories
@Autowired @Autowired
@Qualifier("valkeyConnectionFactory") @Qualifier("valkeyConnectionFactory")
private lateinit var cacheConnectionFactory: RedisConnectionFactory private lateinit var cacheConnectionFactory: ValkeyConnectionFactory
@Autowired @Autowired
@Qualifier("eventStoreValkeyConnectionFactory") @Qualifier("eventStoreValkeyConnectionFactory")
private lateinit var eventStoreConnectionFactory: RedisConnectionFactory private lateinit var eventStoreConnectionFactory: ValkeyConnectionFactory
@Test @Test
fun `test both modules can be used simultaneously without conflicts`(): Unit = runBlocking { fun `test both modules can be used simultaneously without conflicts`(): Unit = runBlocking {
@@ -6,6 +6,9 @@ import at.mocode.core.domain.model.AggregateId
import at.mocode.core.domain.model.EventType import at.mocode.core.domain.model.EventType
import at.mocode.core.domain.model.EventVersion import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@@ -14,9 +17,6 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -35,11 +35,11 @@ class ValkeyEventConsumerResilienceTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
} }
private lateinit var valkeyTemplate: StringRedisTemplate private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: ValkeyEventStore private lateinit var eventStore: ValkeyEventStore
@@ -51,11 +51,11 @@ class ValkeyEventConsumerResilienceTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory) valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply { serializer = JacksonEventSerializer().apply {
registerEventType(ResilienceTestEvent::class.java, "ResilienceTestEvent") registerEventType(ResilienceTestEvent::class.java, "ResilienceTestEvent")
@@ -2,6 +2,8 @@ package at.mocode.infrastructure.eventstore.valkey
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -11,8 +13,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import java.time.Duration import java.time.Duration
/** /**
@@ -71,8 +71,8 @@ class ValkeyEventStoreConfigurationTest {
assertTrue(context.containsBean("eventConsumer")) assertTrue(context.containsBean("eventConsumer"))
// Verify bean types // Verify bean types
assertNotNull(context.getBean<RedisConnectionFactory>("eventStoreValkeyConnectionFactory")) assertNotNull(context.getBean<ValkeyConnectionFactory>("eventStoreValkeyConnectionFactory"))
assertNotNull(context.getBean<StringRedisTemplate>("eventStoreValkeyTemplate")) assertNotNull(context.getBean<StringValkeyTemplate>("eventStoreValkeyTemplate"))
assertNotNull(context.getBean<EventSerializer>("eventSerializer")) assertNotNull(context.getBean<EventSerializer>("eventSerializer"))
assertNotNull(context.getBean<EventStore>("eventStore")) assertNotNull(context.getBean<EventStore>("eventStore"))
assertNotNull(context.getBean<ValkeyEventConsumer>("eventConsumer")) assertNotNull(context.getBean<ValkeyEventConsumer>("eventConsumer"))
@@ -160,7 +160,7 @@ class ValkeyEventStoreConfigurationTest {
"valkey.event-store.database=1" "valkey.event-store.database=1"
) )
.run { context -> .run { context ->
val connectionFactory = context.getBean<RedisConnectionFactory>("eventStoreValkeyConnectionFactory") val connectionFactory = context.getBean<ValkeyConnectionFactory>("eventStoreValkeyConnectionFactory")
assertNotNull(connectionFactory) assertNotNull(connectionFactory)
// Verify the connection factory is properly configured // Verify the connection factory is properly configured
@@ -176,7 +176,7 @@ class ValkeyEventStoreConfigurationTest {
fun `should handle Valkey template creation correctly`() { fun `should handle Valkey template creation correctly`() {
contextRunner contextRunner
.run { context -> .run { context ->
val valkeyTemplate = context.getBean<StringRedisTemplate>("eventStoreValkeyTemplate") val valkeyTemplate = context.getBean<StringValkeyTemplate>("eventStoreValkeyTemplate")
assertNotNull(valkeyTemplate) assertNotNull(valkeyTemplate)
// Verify the template is properly set up // Verify the template is properly set up
@@ -211,7 +211,7 @@ class ValkeyEventStoreConfigurationTest {
assertTrue(eventStore is ValkeyEventStore) assertTrue(eventStore is ValkeyEventStore)
// Verify dependencies are wired correctly // Verify dependencies are wired correctly
val valkeyTemplate = context.getBean<StringRedisTemplate>("eventStoreValkeyTemplate") val valkeyTemplate = context.getBean<StringValkeyTemplate>("eventStoreValkeyTemplate")
val eventSerializer = context.getBean<EventSerializer>("eventSerializer") val eventSerializer = context.getBean<EventSerializer>("eventSerializer")
val properties = context.getBean<ValkeyEventStoreProperties>() val properties = context.getBean<ValkeyEventStoreProperties>()
@@ -231,7 +231,7 @@ class ValkeyEventStoreConfigurationTest {
assertNotNull(eventConsumer) assertNotNull(eventConsumer)
// Verify dependencies are available // Verify dependencies are available
val valkeyTemplate = context.getBean<StringRedisTemplate>("eventStoreValkeyTemplate") val valkeyTemplate = context.getBean<StringValkeyTemplate>("eventStoreValkeyTemplate")
val eventSerializer = context.getBean<EventSerializer>("eventSerializer") val eventSerializer = context.getBean<EventSerializer>("eventSerializer")
val properties = context.getBean<ValkeyEventStoreProperties>() val properties = context.getBean<ValkeyEventStoreProperties>()
@@ -6,6 +6,9 @@ import at.mocode.core.domain.model.EventType
import at.mocode.core.domain.model.EventVersion import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.ConcurrencyException import at.mocode.infrastructure.eventstore.api.ConcurrencyException
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@@ -13,9 +16,6 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -32,11 +32,11 @@ class ValkeyEventStoreErrorHandlingTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
} }
private lateinit var valkeyTemplate: StringRedisTemplate private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: ValkeyEventStore private lateinit var eventStore: ValkeyEventStore
@@ -46,11 +46,11 @@ class ValkeyEventStoreErrorHandlingTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory) valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply { serializer = JacksonEventSerializer().apply {
registerEventType(TestErrorEvent::class.java, "TestErrorEvent") registerEventType(TestErrorEvent::class.java, "TestErrorEvent")
@@ -7,14 +7,14 @@ import at.mocode.core.domain.event.DomainEvent
import at.mocode.core.domain.model.* import at.mocode.core.domain.model.*
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -30,11 +30,11 @@ class ValkeyEventStoreIntegrationTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
} }
private lateinit var valkeyTemplate: StringRedisTemplate private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: EventStore private lateinit var eventStore: EventStore
@@ -45,11 +45,11 @@ class ValkeyEventStoreIntegrationTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory) valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply { serializer = JacksonEventSerializer().apply {
registerEventType(TestCreatedEvent::class.java, "TestCreated") registerEventType(TestCreatedEvent::class.java, "TestCreated")
@@ -5,6 +5,9 @@ import at.mocode.core.domain.model.AggregateId
import at.mocode.core.domain.model.EventType import at.mocode.core.domain.model.EventType
import at.mocode.core.domain.model.EventVersion import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@@ -12,9 +15,6 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -31,11 +31,11 @@ class ValkeyEventStoreStreamTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
} }
private lateinit var valkeyTemplate: StringRedisTemplate private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: ValkeyEventStore private lateinit var eventStore: ValkeyEventStore
@@ -45,11 +45,11 @@ class ValkeyEventStoreStreamTest {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory) valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply { serializer = JacksonEventSerializer().apply {
registerEventType(StreamTestEvent::class.java, "StreamTestEvent") registerEventType(StreamTestEvent::class.java, "StreamTestEvent")
@@ -6,6 +6,9 @@ import at.mocode.core.domain.model.EventType
import at.mocode.core.domain.model.EventVersion import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.ConcurrencyException import at.mocode.infrastructure.eventstore.api.ConcurrencyException
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@@ -13,9 +16,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -25,93 +25,93 @@ import kotlin.uuid.Uuid
@Testcontainers @Testcontainers
class ValkeyEventStoreTest { class ValkeyEventStoreTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
}
private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: ValkeyEventStore
@BeforeEach
fun setUp() {
val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host
val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet()
valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply {
registerEventType(TestCreatedEvent::class.java, "TestCreated")
registerEventType(TestUpdatedEvent::class.java, "TestUpdated")
} }
private lateinit var valkeyTemplate: StringRedisTemplate properties = ValkeyEventStoreProperties().apply {
private lateinit var serializer: EventSerializer streamPrefix = "test-stream:"
private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: ValkeyEventStore
@BeforeEach
fun setUp() {
val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply {
registerEventType(TestCreatedEvent::class.java, "TestCreated")
registerEventType(TestUpdatedEvent::class.java, "TestUpdated")
}
properties = ValkeyEventStoreProperties().apply {
streamPrefix = "test-stream:"
}
eventStore = ValkeyEventStore(valkeyTemplate, serializer, properties)
cleanupValkey()
} }
eventStore = ValkeyEventStore(valkeyTemplate, serializer, properties)
cleanupValkey()
}
@AfterEach @AfterEach
fun tearDown() = cleanupValkey() fun tearDown() = cleanupValkey()
private fun cleanupValkey() { private fun cleanupValkey() {
val keys = valkeyTemplate.keys("${properties.streamPrefix}*") val keys = valkeyTemplate.keys("${properties.streamPrefix}*")
if (!keys.isNullOrEmpty()) { if (!keys.isNullOrEmpty()) {
valkeyTemplate.delete(keys) valkeyTemplate.delete(keys)
}
} }
}
@Test @Test
fun `append and read events should work correctly for new stream`() { fun `append and read events should work correctly for new stream`() {
val aggregateId = Uuid.random() val aggregateId = Uuid.random()
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity") val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity")
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity") val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity")
eventStore.appendToStream(listOf(event1, event2), aggregateId, 0) eventStore.appendToStream(listOf(event1, event2), aggregateId, 0)
val events = eventStore.readFromStream(aggregateId) val events = eventStore.readFromStream(aggregateId)
assertEquals(2, events.size) assertEquals(2, events.size)
val firstEvent = events[0] as TestCreatedEvent val firstEvent = events[0] as TestCreatedEvent
assertEquals(EventVersion(1L), firstEvent.version) assertEquals(EventVersion(1L), firstEvent.version)
assertEquals("Test Entity", firstEvent.name) assertEquals("Test Entity", firstEvent.name)
val secondEvent = events[1] as TestUpdatedEvent val secondEvent = events[1] as TestUpdatedEvent
assertEquals(EventVersion(2L), secondEvent.version) assertEquals(EventVersion(2L), secondEvent.version)
assertEquals("Updated Test Entity", secondEvent.name) assertEquals("Updated Test Entity", secondEvent.name)
}
@Test
fun `appending with wrong expected version should throw ConcurrencyException`() {
val aggregateId = Uuid.random()
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity")
eventStore.appendToStream(listOf(event1), aggregateId, 0) // Stream is now at version 1
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity")
assertThrows<ConcurrencyException> {
eventStore.appendToStream(listOf(event2), aggregateId, 0)
} }
}
@Test @Serializable
fun `appending with wrong expected version should throw ConcurrencyException`() { data class TestCreatedEvent(
val aggregateId = Uuid.random() @Transient override val aggregateId: AggregateId = AggregateId(Uuid.random()),
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity") @Transient override val version: EventVersion = EventVersion(0),
eventStore.appendToStream(listOf(event1), aggregateId, 0) // Stream is now at version 1 val name: String
) : BaseDomainEvent(aggregateId, EventType("TestCreated"), version)
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity") @Serializable
assertThrows<ConcurrencyException> { data class TestUpdatedEvent(
eventStore.appendToStream(listOf(event2), aggregateId, 0) @Transient override val aggregateId: AggregateId = AggregateId(Uuid.random()),
} @Transient override val version: EventVersion = EventVersion(0),
} val name: String
) : BaseDomainEvent(aggregateId, EventType("TestUpdated"), version)
@Serializable
data class TestCreatedEvent(
@Transient override val aggregateId: AggregateId = AggregateId(Uuid.random()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, EventType("TestCreated"), version)
@Serializable
data class TestUpdatedEvent(
@Transient override val aggregateId: AggregateId = AggregateId(Uuid.random()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, EventType("TestUpdated"), version)
} }
@@ -9,15 +9,15 @@ import at.mocode.core.domain.model.EventType
import at.mocode.core.domain.model.EventVersion import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.EventSerializer import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore import at.mocode.infrastructure.eventstore.api.EventStore
import io.valkey.springframework.data.valkey.connection.ValkeyStandaloneConfiguration
import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory
import io.valkey.springframework.data.valkey.core.StringValkeyTemplate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate
import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.junit.jupiter.Testcontainers
@@ -29,11 +29,11 @@ class ValkeyIntegrationTest {
companion object { companion object {
@Container @Container
val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:9-alpine")) val valkeyContainer: GenericContainer<*> = GenericContainer(DockerImageName.parse("valkey/valkey:8.0.2-alpine"))
.withExposedPorts(6379) .withExposedPorts(6379)
} }
private lateinit var valkeyTemplate: StringRedisTemplate private lateinit var valkeyTemplate: StringValkeyTemplate
private lateinit var serializer: EventSerializer private lateinit var serializer: EventSerializer
private lateinit var properties: ValkeyEventStoreProperties private lateinit var properties: ValkeyEventStoreProperties
private lateinit var eventStore: EventStore private lateinit var eventStore: EventStore
@@ -43,10 +43,10 @@ class ValkeyIntegrationTest {
fun setUp() { fun setUp() {
val valkeyPort = valkeyContainer.getMappedPort(6379) val valkeyPort = valkeyContainer.getMappedPort(6379)
val valkeyHost = valkeyContainer.host val valkeyHost = valkeyContainer.host
val valkeyConfig = RedisStandaloneConfiguration(valkeyHost, valkeyPort) val valkeyConfig = ValkeyStandaloneConfiguration(valkeyHost, valkeyPort)
val connectionFactory = LettuceConnectionFactory(valkeyConfig) val connectionFactory = LettuceConnectionFactory(valkeyConfig)
connectionFactory.afterPropertiesSet() connectionFactory.afterPropertiesSet()
valkeyTemplate = StringRedisTemplate(connectionFactory) valkeyTemplate = StringValkeyTemplate(connectionFactory)
serializer = JacksonEventSerializer().apply { serializer = JacksonEventSerializer().apply {
registerEventType(TestCreatedEvent::class.java, "TestCreated") registerEventType(TestCreatedEvent::class.java, "TestCreated")
registerEventType(TestUpdatedEvent::class.java, "TestUpdated") registerEventType(TestUpdatedEvent::class.java, "TestUpdated")
@@ -31,7 +31,7 @@ dependencies {
// Resilience (Reactive) - WICHTIG: Reactor-Variante für WebFlux! // Resilience (Reactive) - WICHTIG: Reactor-Variante für WebFlux!
implementation(libs.spring.cloud.starter.circuitbreaker.reactor.resilience4j) implementation(libs.spring.cloud.starter.circuitbreaker.reactor.resilience4j)
implementation(libs.spring.boot.starter.data.redis) implementation(libs.spring.data.valkey)
implementation(libs.micrometer.tracing.bridge.brave) implementation(libs.micrometer.tracing.bridge.brave)
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
@@ -1,43 +1,49 @@
// Dieses Modul stellt High-Level-Clients (Producer/Consumer) für die // Dieses Modul stellt High-Level-Clients (Producer/Consumer) für die
// Interaktion mit Apache Kafka bereit. Es baut auf der `messaging-config` auf. // Interaktion mit Apache Kafka bereit. Es baut auf der `messaging-config` auf.
plugins { plugins {
alias(libs.plugins.kotlinJvm) alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring) alias(libs.plugins.kotlinSpring)
alias(libs.plugins.spring.boot) alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement) alias(libs.plugins.spring.dependencyManagement)
} }
// Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul. // Deaktiviert die Erstellung eines ausführbaren Jars für dieses Bibliotheks-Modul.
tasks.bootJar { tasks.bootJar {
enabled = false enabled = false
} }
// Stellt sicher, dass stattdessen ein reguläres Jar gebaut wird // Stellt sicher, dass stattdessen ein reguläres Jar gebaut wird
tasks.jar { tasks.jar {
enabled = true enabled = true
} }
dependencies { dependencies {
// Stellt sicher, dass alle Versionen aus der zentralen BOM kommen. // Stellt sicher, dass alle Versionen aus der zentralen BOM kommen.
implementation(platform(projects.platform.platformBom)) implementation(platform(projects.platform.platformBom))
// Stellt gemeinsame Abhängigkeiten bereit. // Stellt gemeinsame Abhängigkeiten bereit.
implementation(projects.platform.platformDependencies) implementation(projects.platform.platformDependencies)
// Spring Boot / Spring Framework APIs used directly in this module // Spring Boot / Spring Framework APIs used directly in this module
// (e.g. ConditionalOnMissingBean) // (e.g. ConditionalOnMissingBean)
implementation("org.springframework.boot:spring-boot-autoconfigure") implementation("org.springframework.boot:spring-boot-autoconfigure")
implementation("org.springframework.boot:spring-boot") implementation("org.springframework.boot:spring-boot")
// Spring Kafka (ReactiveKafkaProducerTemplate, etc.) // Spring Kafka (ReactiveKafkaProducerTemplate, etc.)
implementation(libs.spring.kafka) implementation(libs.spring.kafka)
// Jakarta annotations used by Spring / configuration classes // Jakarta annotations used by Spring / configuration classes
implementation(libs.jakarta.annotation.api) implementation(libs.jakarta.annotation.api)
// Baut auf der zentralen Kafka-Konfiguration auf und erbt deren Abhängigkeiten. // Baut auf der zentralen Kafka-Konfiguration auf und erbt deren Abhängigkeiten.
implementation(projects.backend.infrastructure.messaging.messagingConfig) implementation(projects.backend.infrastructure.messaging.messagingConfig)
// Fügt die reaktive Kafka-Implementierung hinzu (Project Reactor). // Fügt die reaktive Kafka-Implementierung hinzu (Project Reactor).
implementation(libs.reactor.kafka) implementation(libs.reactor.kafka)
// Stellt alle Test-Abhängigkeiten gebündelt bereit. // Stellt alle Test-Abhängigkeiten gebündelt bereit.
testImplementation(projects.platform.platformTesting) testImplementation(projects.platform.platformTesting)
}
// JVM Native Access (JDK 22+): Unterdrückt Warnung/Blockade für Snappy (org.xerial.snappy)
tasks.withType<Test>().configureEach {
// Erfordert JDK 21+; ab zukünftigen Versionen sonst Fehler statt Warnung
jvmArgs("--enable-native-access=ALL-UNNAMED")
} }
+3 -2
View File
@@ -59,6 +59,7 @@ flyway = "11.19.1"
redisson = "4.0.0" redisson = "4.0.0"
# Spring Boot 3.5.x manages Lettuce 6.6.x; keep aligned to avoid binary/API mismatches. # Spring Boot 3.5.x manages Lettuce 6.6.x; keep aligned to avoid binary/API mismatches.
lettuce = "6.6.0.RELEASE" lettuce = "6.6.0.RELEASE"
springDataValkey = "0.2.0"
# Observability # Observability
micrometer = "1.16.1" micrometer = "1.16.1"
@@ -229,6 +230,7 @@ flyway-postgresql = { module = "org.flywaydb:flyway-database-postgresql", versio
redisson = { module = "org.redisson:redisson", version.ref = "redisson" } redisson = { module = "org.redisson:redisson", version.ref = "redisson" }
lettuce-core = { module = "io.lettuce:lettuce-core", version.ref = "lettuce" } lettuce-core = { module = "io.lettuce:lettuce-core", version.ref = "lettuce" }
spring-data-valkey = { module = "io.valkey.springframework.data:spring-data-valkey", version.ref = "springDataValkey" }
micrometer-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" } micrometer-prometheus = { module = "io.micrometer:micrometer-registry-prometheus", version.ref = "micrometer" }
micrometer-tracing-bridge-brave = { module = "io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometerTracing" } micrometer-tracing-bridge-brave = { module = "io.micrometer:micrometer-tracing-bridge-brave", version.ref = "micrometerTracing" }
@@ -342,8 +344,7 @@ database-complete = [
"flyway-postgresql" "flyway-postgresql"
] ]
valkey-cache = [ valkey-cache = [
"spring-boot-starter-data-redis", "spring-data-valkey",
"lettuce-core",
"jackson-module-kotlin", "jackson-module-kotlin",
"jackson-datatype-jsr310" "jackson-datatype-jsr310"
] ]