feat: remove snapshot on ProjectionRepository
This commit is contained in:
@@ -7,6 +7,8 @@ import eventDemo.business.entity.Player
|
||||
import eventDemo.business.event.event.GameEvent
|
||||
import eventDemo.business.event.event.NewPlayerEvent
|
||||
import eventDemo.libs.event.VersionBuilderLocal
|
||||
import eventDemo.testApplicationWithConfig
|
||||
import io.kotest.assertions.nondeterministic.until
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
@@ -16,6 +18,7 @@ import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class GameEventHandlerTest :
|
||||
FunSpec({
|
||||
|
||||
@@ -2,7 +2,6 @@ package eventDemo.business.event.projection
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import com.rabbitmq.client.impl.ForgivingExceptionHandler
|
||||
import com.zaxxer.hikari.pool.ProxyConnection
|
||||
import eventDemo.Tag
|
||||
import eventDemo.business.command.GameCommandHandler
|
||||
import eventDemo.business.entity.GameId
|
||||
@@ -17,7 +16,6 @@ import io.kotest.common.KotestInternal
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
@@ -41,13 +39,10 @@ class GameStateRepositoryTest :
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
eventHandler
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||
.also { event ->
|
||||
.also {
|
||||
// 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(repo.get(aggregateId)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||
}
|
||||
}
|
||||
@@ -68,9 +63,9 @@ class GameStateRepositoryTest :
|
||||
|
||||
var state: GameState? = null
|
||||
projectionBus.subscribe {
|
||||
repo.getLast(aggregateId).also {
|
||||
state = it
|
||||
}
|
||||
repo
|
||||
.get(aggregateId)
|
||||
.also { state = it }
|
||||
}
|
||||
|
||||
eventHandler
|
||||
@@ -86,7 +81,7 @@ class GameStateRepositoryTest :
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||
.also {
|
||||
eventually(1.seconds) {
|
||||
assertNotNull(repo.getLast(aggregateId)).also {
|
||||
assertNotNull(repo.get(aggregateId)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||
}
|
||||
}
|
||||
@@ -95,44 +90,7 @@ class GameStateRepositoryTest :
|
||||
}
|
||||
}
|
||||
|
||||
test("getUntil should build the state until the event") {
|
||||
withLogLevel(
|
||||
GameCommandHandler::class.java.name to Level.ERROR,
|
||||
ForgivingExceptionHandler::class.java.name to Level.OFF,
|
||||
ProxyConnection::class.java.name to Level.OFF,
|
||||
Logger.ROOT_LOGGER_NAME to Level.INFO,
|
||||
) {
|
||||
repeat(10) {
|
||||
val aggregateId = GameId()
|
||||
testKoinApplicationWithConfig {
|
||||
val repo = get<GameStateRepository>()
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
|
||||
val event1 =
|
||||
eventHandler
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||
.also { event1 ->
|
||||
assertNotNull(repo.getUntil(event1)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||
}
|
||||
}
|
||||
|
||||
eventHandler
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||
.also { event2 ->
|
||||
assertNotNull(repo.getUntil(event2)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||
}
|
||||
assertNotNull(repo.getUntil(event1)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("getUntil should be concurrently secure").config(tags = setOf(Tag.Concurrence)) {
|
||||
test("get should be concurrently secure").config(tags = setOf(Tag.Concurrence)) {
|
||||
withLogLevel(
|
||||
Logger.ROOT_LOGGER_NAME to Level.ERROR,
|
||||
ForgivingExceptionHandler::class.java.name to Level.OFF,
|
||||
@@ -167,17 +125,12 @@ class GameStateRepositoryTest :
|
||||
includeFirst = false
|
||||
},
|
||||
) {
|
||||
repo.getLast(aggregateId).run {
|
||||
repo.get(aggregateId).run {
|
||||
lastEventVersion shouldBeEqual 200
|
||||
players shouldHaveSize 200
|
||||
}
|
||||
repo.count(aggregateId) shouldBe 21
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xtest("get should be concurrently secure") {
|
||||
tags(Tag.Concurrence)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package eventDemo.business.event.projection
|
||||
|
||||
import eventDemo.cleanProjections
|
||||
import eventDemo.configuration.serializer.UUIDSerializer
|
||||
import eventDemo.libs.event.AggregateId
|
||||
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.withData
|
||||
import io.kotest.engine.names.WithDataTestName
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import redis.clients.jedis.JedisPooled
|
||||
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 :
|
||||
FunSpec({
|
||||
data class TestData(
|
||||
val store: EventStore<TestEvents, IdTest>,
|
||||
val snapshotRepo: ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest>,
|
||||
) : WithDataTestName {
|
||||
override fun dataTestName(): String =
|
||||
"${snapshotRepo::class.simpleName} with ${store::class.simpleName}"
|
||||
}
|
||||
|
||||
val eventStores =
|
||||
listOf(
|
||||
{ EventStoreInMemory<TestEvents, IdTest>() },
|
||||
)
|
||||
val projectionRepo =
|
||||
listOf(
|
||||
::getSnapshotRepoInMemoryTest,
|
||||
::getSnapshotRepoInRedisTest,
|
||||
)
|
||||
|
||||
val list =
|
||||
eventStores.flatMap { store ->
|
||||
projectionRepo.map { repo ->
|
||||
store().let { store -> TestData(store, repo(store, DISABLED_CONFIG)) }
|
||||
}
|
||||
}
|
||||
|
||||
context("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val aggregateId = IdTest()
|
||||
|
||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
||||
eventStore.publish(eventOther)
|
||||
repo.applyAndPutToCache(eventOther)
|
||||
assertNotNull(repo.getUntil(eventOther)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||
}
|
||||
|
||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||
eventStore.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)
|
||||
eventStore.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("getList method must be return all inserted events") {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val aggregateId = IdTest()
|
||||
val otherAggregateId = IdTest()
|
||||
|
||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = otherAggregateId)
|
||||
eventStore.publish(eventOther)
|
||||
repo.applyAndPutToCache(eventOther)
|
||||
assertNotNull(repo.getUntil(eventOther)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||
}
|
||||
|
||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||
eventStore.publish(event1)
|
||||
repo.applyAndPutToCache(event1)
|
||||
|
||||
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
||||
eventStore.publish(event2)
|
||||
repo.applyAndPutToCache(event2)
|
||||
|
||||
repo.getList().apply {
|
||||
any { it.aggregateId == otherAggregateId } shouldBeEqual true
|
||||
any { it.aggregateId == aggregateId } shouldBeEqual true
|
||||
any { it.value == "val1val2" } shouldBeEqual true
|
||||
any { it.value == "valOther" } shouldBeEqual true
|
||||
any { it.lastEventVersion == 2 } shouldBeEqual true
|
||||
any { it.lastEventVersion == 1 } shouldBeEqual true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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).forEach {
|
||||
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
|
||||
assertNotNull(repo.count(aggregateId)) shouldBeEqual 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("removeOldSnapshot") {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val versionBuilder = VersionBuilderLocal()
|
||||
val aggregateId = IdTest()
|
||||
|
||||
suspend fun buildEndSendEventX() {
|
||||
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||
.also { eventStore.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
|
||||
@Serializable
|
||||
private value class IdTest(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
) : AggregateId
|
||||
|
||||
@Serializable
|
||||
private data class ProjectionTest(
|
||||
override val aggregateId: IdTest,
|
||||
override val lastEventVersion: Int = 0,
|
||||
var value: String? = null,
|
||||
var num: Int = 0,
|
||||
) : Projection<IdTest>
|
||||
|
||||
private sealed interface TestEvents : Event<IdTest>
|
||||
|
||||
private data class Event1Test(
|
||||
override val eventId: UUID = UUID.randomUUID(),
|
||||
override val aggregateId: IdTest,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val version: Int,
|
||||
val value1: String,
|
||||
) : TestEvents
|
||||
|
||||
private data class Event2Test(
|
||||
override val eventId: UUID = UUID.randomUUID(),
|
||||
override val aggregateId: IdTest,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val version: Int,
|
||||
val value2: String,
|
||||
) : TestEvents
|
||||
|
||||
private data class EventXTest(
|
||||
override val eventId: UUID = UUID.randomUUID(),
|
||||
override val aggregateId: IdTest,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val version: Int,
|
||||
val num: Int,
|
||||
) : TestEvents
|
||||
|
||||
private fun getSnapshotRepoInMemoryTest(
|
||||
eventStore: EventStore<TestEvents, IdTest>,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> =
|
||||
ProjectionSnapshotRepositoryInMemory(
|
||||
eventStore = eventStore,
|
||||
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||
snapshotCacheConfig = snapshotConfig,
|
||||
applyToProjection = apply,
|
||||
)
|
||||
|
||||
private fun getSnapshotRepoInRedisTest(
|
||||
eventStore: EventStore<TestEvents, IdTest>,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> {
|
||||
val jedis = JedisPooled("redis://localhost:6379")
|
||||
jedis.cleanProjections()
|
||||
return ProjectionSnapshotRepositoryInRedis(
|
||||
eventStore = eventStore,
|
||||
jedis = jedis,
|
||||
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||
snapshotCacheConfig = snapshotConfig,
|
||||
projectionClass = ProjectionTest::class,
|
||||
projectionToJson = { Json.encodeToString(it) },
|
||||
jsonToProjection = { Json.decodeFromString(it) },
|
||||
applyToProjection = apply,
|
||||
)
|
||||
}
|
||||
|
||||
private val apply: ProjectionTest.(TestEvents) -> ProjectionTest = { event ->
|
||||
this.let { projection ->
|
||||
when (event) {
|
||||
is Event1Test -> {
|
||||
projection.copy(value = (projection.value.orEmpty()) + event.value1)
|
||||
}
|
||||
|
||||
is Event2Test -> {
|
||||
projection.copy(value = (projection.value.orEmpty()) + event.value2)
|
||||
}
|
||||
|
||||
is EventXTest -> {
|
||||
projection.copy(num = projection.num + event.num)
|
||||
}
|
||||
}.copy(
|
||||
lastEventVersion = event.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user