fix and improve ProjectionSnapshotRepositoryInRedis

This commit is contained in:
2025-03-26 03:36:49 +01:00
parent 22792a0427
commit 442379dc49
18 changed files with 283 additions and 151 deletions

View File

@@ -4,12 +4,17 @@ import eventDemo.business.entity.GameId
import eventDemo.business.entity.Player
import eventDemo.business.event.GameEventHandler
import eventDemo.business.event.event.NewPlayerEvent
import eventDemo.business.event.projection.gameState.GameState
import eventDemo.business.event.projection.gameState.GameStateRepository
import eventDemo.configuration.injection.Configuration
import eventDemo.configuration.injection.appKoinModule
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.eventuallyConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.comparables.shouldBeGreaterThan
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.ints.shouldBeLessThan
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.joinAll
@@ -17,6 +22,7 @@ import kotlinx.coroutines.launch
import org.koin.core.context.stopKoin
import org.koin.dsl.koinApplication
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class)
class GameStateRepositoryTest :
@@ -32,11 +38,14 @@ class GameStateRepositoryTest :
eventHandler
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also { event ->
assertNotNull(repo.getUntil(event)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
}
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
// Wait until the projection is created
eventually(1.seconds) {
assertNotNull(repo.getUntil(event)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
}
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
}
}
}
}
@@ -48,20 +57,31 @@ class GameStateRepositoryTest :
koinApplication { modules(appKoinModule(Configuration("redis://localhost:6379"))) }.koin.apply {
val repo = get<GameStateRepository>()
val eventHandler = get<GameEventHandler>()
val projectionBus = get<GameProjectionBus>()
var state: GameState? = null
projectionBus.subscribe {
repo.getLast(aggregateId).also {
state = it
}
}
eventHandler
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also {
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
eventually(1.seconds) {
assertNotNull(state).players.isNotEmpty() shouldBeEqual true
assertNotNull(state).players shouldBeEqual setOf(player1)
}
}
eventHandler
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
.also {
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
eventually(1.seconds) {
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
}
}
}
}
@@ -121,9 +141,19 @@ class GameStateRepositoryTest :
}
}.joinAll()
repo.getLast(aggregateId).run {
lastEventVersion shouldBeEqual 1000
players shouldHaveSize 1000
eventually(
eventuallyConfig {
duration = 10.seconds
interval = 1.seconds
includeFirst = false
},
) {
repo.getLast(aggregateId).run {
lastEventVersion shouldBeEqual 1000
players shouldHaveSize 1000
}
repo.count(aggregateId) shouldBeGreaterThan 20
repo.count(aggregateId) shouldBeLessThan 30
}
}
}

View File

@@ -6,14 +6,16 @@ import eventDemo.libs.event.Event
import eventDemo.libs.event.EventStore
import eventDemo.libs.event.EventStoreInMemory
import eventDemo.libs.event.VersionBuilderLocal
import eventDemo.libs.event.projection.DISABLED_CONFIG
import eventDemo.libs.event.projection.Projection
import eventDemo.libs.event.projection.ProjectionSnapshotRepository
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInRedis
import eventDemo.libs.event.projection.SnapshotConfig
import io.kotest.assertions.nondeterministic.continually
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.WithDataTestName
import io.kotest.datatest.withData
import io.kotest.engine.names.WithDataTestName
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
@@ -28,6 +30,7 @@ import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class)
class ProjectionSnapshotRepositoryTest :
@@ -42,7 +45,7 @@ class ProjectionSnapshotRepositoryTest :
val eventStores =
listOf(
EventStoreInMemory<TestEvents, IdTest>(),
{ EventStoreInMemory<TestEvents, IdTest>() },
)
val projectionRepo =
listOf(
@@ -52,17 +55,13 @@ class ProjectionSnapshotRepositoryTest :
val list =
eventStores.flatMap { store ->
projectionRepo.map {
TestData(store, it(store, SnapshotConfig(2000)))
projectionRepo.map { repo ->
store().let { store -> TestData(store, repo(store, DISABLED_CONFIG)) }
}
}
val nameFn: (TestData) -> String = { (eventStore, repo) ->
"${repo::class.simpleName} with ${eventStore::class.simpleName}"
}
context("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
withData(nameFn = nameFn, list) { (eventStore, repo) ->
withData(list) { (eventStore, repo) ->
val aggregateId = IdTest()
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
@@ -97,25 +96,28 @@ class ProjectionSnapshotRepositoryTest :
}
}
context("ProjectionSnapshotRepositoryInMemory should be thread safe") {
withData(list) { (eventStore, repo) ->
val aggregateId = IdTest()
val versionBuilder = VersionBuilderLocal()
val lock = ReentrantLock()
(0..9)
.map {
GlobalScope.launch {
(1..10).map {
val eventX =
lock.withLock {
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
.also { eventStore.publish(it) }
}
repo.applyAndPutToCache(eventX)
context("ProjectionSnapshotRepository should be thread safe") {
continually(1.seconds) {
withData(list) { (eventStore, repo) ->
val aggregateId = IdTest()
val versionBuilder = VersionBuilderLocal()
val lock = ReentrantLock()
(0..9)
.map {
GlobalScope.launch {
(1..10).map {
val eventX =
lock.withLock {
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
.also { eventStore.publish(it) }
}
repo.applyAndPutToCache(eventX)
}
}
}
}.joinAll()
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
}.joinAll()
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
assertNotNull(repo.count(aggregateId)) shouldBeEqual 100
}
}
}
@@ -185,7 +187,7 @@ private data class EventXTest(
private fun getSnapshotRepoInMemoryTest(
eventStore: EventStore<TestEvents, IdTest>,
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
snapshotConfig: SnapshotConfig,
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> =
ProjectionSnapshotRepositoryInMemory(
eventStore = eventStore,
@@ -196,7 +198,7 @@ private fun getSnapshotRepoInMemoryTest(
private fun getSnapshotRepoInRedisTest(
eventStore: EventStore<TestEvents, IdTest>,
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
snapshotConfig: SnapshotConfig,
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> =
ProjectionSnapshotRepositoryInRedis(
eventStore = eventStore,