Improve concurrence of ProjectionSnapshotRepositoryInMemory and GameEventHandler
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +71,11 @@ class GameStateRouteTest :
|
||||
val eventHandler by inject<GameEventHandler>()
|
||||
val stateRepo by inject<GameStateRepository>()
|
||||
runBlocking {
|
||||
eventHandler.handle { NewPlayerEvent(gameId, player1, it) }
|
||||
eventHandler.handle { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle { PlayerReadyEvent(gameId, player2, it) }
|
||||
eventHandler.handle {
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) {
|
||||
GameStartedEvent.new(
|
||||
gameId,
|
||||
setOf(player1, player2),
|
||||
@@ -92,7 +92,7 @@ class GameStateRouteTest :
|
||||
it.color shouldBeEqual Card.Color.Red
|
||||
}
|
||||
delay(100)
|
||||
eventHandler.handle {
|
||||
eventHandler.handle(gameId) {
|
||||
CardIsPlayedEvent(
|
||||
gameId,
|
||||
assertNotNull(lastPlayedCard),
|
||||
|
||||
@@ -11,7 +11,8 @@ import eventDemo.app.entity.Player
|
||||
import eventDemo.app.event.GameEventStream
|
||||
import eventDemo.app.event.event.disableShuffleDeck
|
||||
import eventDemo.app.event.projection.GameState
|
||||
import eventDemo.app.event.projection.buildStateFromEventStream
|
||||
import eventDemo.app.event.projection.ProjectionSnapshotRepositoryInMemory
|
||||
import eventDemo.app.event.projection.apply
|
||||
import eventDemo.app.eventListener.PlayerNotificationEventListener
|
||||
import eventDemo.app.eventListener.ReactionEventListener
|
||||
import eventDemo.app.notification.ItsTheTurnOfNotification
|
||||
@@ -148,7 +149,12 @@ class GameStateTest :
|
||||
|
||||
joinAll(player1Job, player2Job)
|
||||
|
||||
val state = id.buildStateFromEventStream(eventStream)
|
||||
val state =
|
||||
ProjectionSnapshotRepositoryInMemory(
|
||||
eventStream = eventStream,
|
||||
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
||||
applyToProjection = GameState::apply,
|
||||
).getLast(id)
|
||||
|
||||
state.aggregateId shouldBeEqual id
|
||||
assertTrue(state.isStarted)
|
||||
|
||||
@@ -6,6 +6,12 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@JvmInline
|
||||
private value class IdTest(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
) : AggregateId
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class VersionBuilderLocalTest :
|
||||
@@ -13,30 +19,34 @@ class VersionBuilderLocalTest :
|
||||
|
||||
test("buildNextVersion") {
|
||||
VersionBuilderLocal().run {
|
||||
buildNextVersion() shouldBeEqual 1
|
||||
buildNextVersion() shouldBeEqual 2
|
||||
buildNextVersion() shouldBeEqual 3
|
||||
val id = IdTest()
|
||||
buildNextVersion(id) shouldBeEqual 1
|
||||
buildNextVersion(id) shouldBeEqual 2
|
||||
buildNextVersion(IdTest()) shouldBeEqual 1
|
||||
buildNextVersion(id) shouldBeEqual 3
|
||||
}
|
||||
}
|
||||
|
||||
test("buildNextVersion concurrently") {
|
||||
val versionBuilder = VersionBuilderLocal()
|
||||
val id = IdTest()
|
||||
(1..20)
|
||||
.map {
|
||||
GlobalScope.launch {
|
||||
(1..1000).map {
|
||||
versionBuilder.buildNextVersion()
|
||||
versionBuilder.buildNextVersion(id)
|
||||
}
|
||||
}
|
||||
}.joinAll()
|
||||
versionBuilder.getLastVersion() shouldBeEqual 20 * 1000
|
||||
versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000
|
||||
}
|
||||
|
||||
test("getLastVersion") {
|
||||
VersionBuilderLocal().run {
|
||||
getLastVersion() shouldBeEqual 0
|
||||
getLastVersion() shouldBeEqual 0
|
||||
getLastVersion() shouldBeEqual 0
|
||||
val id = IdTest()
|
||||
getLastVersion(id) shouldBeEqual 0
|
||||
getLastVersion(id) shouldBeEqual 0
|
||||
getLastVersion(id) shouldBeEqual 0
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user