Improve concurrence of ProjectionSnapshotRepositoryInMemory and GameEventHandler

This commit is contained in:
2025-03-13 23:57:20 +01:00
parent 286dedac76
commit 91767e3747
21 changed files with 358 additions and 154 deletions

View File

@@ -29,7 +29,7 @@ class GameStateBuilderTest :
NewPlayerEvent(
aggregateId = gameId,
player = player1,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -41,7 +41,7 @@ class GameStateBuilderTest :
NewPlayerEvent(
aggregateId = gameId,
player = player2,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -52,7 +52,7 @@ class GameStateBuilderTest :
PlayerReadyEvent(
aggregateId = gameId,
player = player1,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -63,7 +63,7 @@ class GameStateBuilderTest :
PlayerReadyEvent(
aggregateId = gameId,
player = player2,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -77,7 +77,7 @@ class GameStateBuilderTest :
id = gameId,
players = setOf(player1, player2),
shuffleIsDisabled = true,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -94,7 +94,7 @@ class GameStateBuilderTest :
aggregateId = gameId,
card = playedCard,
player = player1,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
@@ -111,7 +111,7 @@ class GameStateBuilderTest :
aggregateId = gameId,
card = playedCard,
player = player2,
version = versionBuilder.buildNextVersion(),
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId

View File

@@ -28,7 +28,7 @@ class GameStateRepositoryTest :
val repo = get<GameStateRepository>()
val eventHandler = get<GameEventHandler>()
eventHandler
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also { event ->
assertNotNull(repo.getUntil(event)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
@@ -48,7 +48,7 @@ class GameStateRepositoryTest :
val eventHandler = get<GameEventHandler>()
eventHandler
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also {
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
@@ -56,7 +56,7 @@ class GameStateRepositoryTest :
}
eventHandler
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
.also {
assertNotNull(repo.getLast(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
@@ -74,7 +74,7 @@ class GameStateRepositoryTest :
val event1 =
eventHandler
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also { event1 ->
assertNotNull(repo.getUntil(event1)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
@@ -82,7 +82,7 @@ class GameStateRepositoryTest :
}
eventHandler
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
.also { event2 ->
assertNotNull(repo.getUntil(event2)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
@@ -108,7 +108,7 @@ class GameStateRepositoryTest :
repeat(100) { r2 ->
val playerX = Player("player$r$r2")
eventHandler
.handle {
.handle(aggregateId) {
NewPlayerEvent(
aggregateId = aggregateId,
player = playerX,
@@ -119,8 +119,10 @@ class GameStateRepositoryTest :
}
}.joinAll()
repo.getLast(aggregateId).players shouldHaveSize 1000
repo.getLast(aggregateId).lastEventVersion shouldBeEqual 1000
repo.getLast(aggregateId).run {
lastEventVersion shouldBeEqual 1000
players shouldHaveSize 1000
}
}
}

View File

@@ -2,6 +2,9 @@ package eventDemo.app.event.projection
import eventDemo.libs.event.AggregateId
import eventDemo.libs.event.Event
import eventDemo.libs.event.EventStream
import eventDemo.libs.event.EventStreamInMemory
import eventDemo.libs.event.VersionBuilderLocal
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -11,6 +14,8 @@ import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.test.assertNotNull
@OptIn(DelicateCoroutinesApi::class)
@@ -18,53 +23,84 @@ class ProjectionSnapshotRepositoryInMemoryTest :
FunSpec({
test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
repeat(10) {
val repo = getRepoTest()
val aggregateId = IdTest()
val eventStream: EventStream<TestEvents, IdTest> = EventStreamInMemory()
val repo = getSnapshotRepoTest(eventStream)
val aggregateId = IdTest()
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
repo.applyAndPutToCache(eventOther)
assertNotNull(repo.getUntil(eventOther)).also {
assertNotNull(it.value) shouldBeEqual "valOther"
}
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
eventStream.publish(eventOther)
repo.applyAndPutToCache(eventOther)
assertNotNull(repo.getUntil(eventOther)).also {
assertNotNull(it.value) shouldBeEqual "valOther"
}
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
repo.applyAndPutToCache(event1)
assertNotNull(repo.getLast(event1.aggregateId)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
assertNotNull(repo.getUntil(event1)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
eventStream.publish(event1)
repo.applyAndPutToCache(event1)
assertNotNull(repo.getLast(event1.aggregateId)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
assertNotNull(repo.getUntil(event1)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
repo.applyAndPutToCache(event2)
assertNotNull(repo.getLast(event2.aggregateId)).also {
assertNotNull(it.value) shouldBeEqual "val1val2"
}
assertNotNull(repo.getUntil(event1)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
assertNotNull(repo.getUntil(event2)).also {
assertNotNull(it.value) shouldBeEqual "val1val2"
}
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
eventStream.publish(event2)
repo.applyAndPutToCache(event2)
assertNotNull(repo.getLast(event2.aggregateId)).also {
assertNotNull(it.value) shouldBeEqual "val1val2"
}
assertNotNull(repo.getUntil(event1)).also {
assertNotNull(it.value) shouldBeEqual "val1"
}
assertNotNull(repo.getUntil(event2)).also {
assertNotNull(it.value) shouldBeEqual "val1val2"
}
}
test("ProjectionSnapshotRepositoryInMemory should be thread safe") {
val repo = getRepoTest(2000)
val eventStream: EventStream<TestEvents, IdTest> = EventStreamInMemory()
val repo = getSnapshotRepoTest(eventStream)
val aggregateId = IdTest()
(1..10)
.map { r ->
val versionBuilder = VersionBuilderLocal()
val lock = ReentrantLock()
(0..9)
.map {
GlobalScope.launch {
repeat(10) {
val eventX = EventXTest(num = 1, version = r, aggregateId = aggregateId)
(1..10).map {
val eventX =
lock.withLock {
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
.also { eventStream.publish(it) }
}
repo.applyAndPutToCache(eventX)
}
}
}.joinAll()
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
}
test("removeOldSnapshot") {
val versionBuilder = VersionBuilderLocal()
val eventStream: EventStream<TestEvents, IdTest> = EventStreamInMemory()
val repo = getSnapshotRepoTest(eventStream, SnapshotConfig(2))
val aggregateId = IdTest()
fun buildEndSendEventX() {
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
.also { eventStream.publish(it) }
.also { repo.applyAndPutToCache(it) }
}
buildEndSendEventX()
repo.getLast(aggregateId).num shouldBeEqual 1
buildEndSendEventX()
repo.getLast(aggregateId).num shouldBeEqual 2
buildEndSendEventX()
repo.getLast(aggregateId).num shouldBeEqual 3
buildEndSendEventX()
repo.getLast(aggregateId).num shouldBeEqual 4
}
})
@JvmInline
@@ -105,9 +141,16 @@ private data class EventXTest(
val num: Int,
) : TestEvents
private fun getRepoTest(maxSnapshotCacheSize: Int = 2000): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
ProjectionSnapshotRepositoryInMemory(maxSnapshotCacheSize) { event ->
(this ?: ProjectionTest(event.aggregateId)).let { projection ->
private fun getSnapshotRepoTest(
eventStream: EventStream<TestEvents, IdTest>,
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
ProjectionSnapshotRepositoryInMemory(
eventStream = eventStream,
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
snapshotCacheConfig = snapshotConfig,
) { event ->
this.let { projection ->
when (event) {
is Event1Test -> {
projection.copy(value = (projection.value ?: "") + event.value1)
@@ -120,6 +163,8 @@ private fun getRepoTest(maxSnapshotCacheSize: Int = 2000): ProjectionSnapshotRep
is EventXTest -> {
projection.copy(num = projection.num + event.num)
}
}
}.copy(
lastEventVersion = event.version,
)
}
}