Improve concurrence of ProjectionSnapshotRepositoryInMemory and GameEventHandler
This commit is contained in:
@@ -37,7 +37,7 @@ data class ICantPlayCommand(
|
|||||||
if (playableCards.isEmpty()) {
|
if (playableCards.isEmpty()) {
|
||||||
val takenCard = state.deck.stack.first()
|
val takenCard = state.deck.stack.first()
|
||||||
|
|
||||||
eventHandler.handle {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
PlayerHavePassEvent(
|
PlayerHavePassEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
player = payload.player,
|
player = payload.player,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ data class IWantToJoinTheGameCommand(
|
|||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
) {
|
) {
|
||||||
if (!state.isStarted) {
|
if (!state.isStarted) {
|
||||||
eventHandler.handle {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
player = payload.player,
|
player = payload.player,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ data class IWantToPlayCardCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.canBePlayThisCard(payload.player, payload.card)) {
|
if (state.canBePlayThisCard(payload.player, payload.card)) {
|
||||||
eventHandler.handle {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
CardIsPlayedEvent(
|
CardIsPlayedEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
card = payload.card,
|
card = payload.card,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ data class IamReadyToPlayCommand(
|
|||||||
} else if (playerIsAlreadyReady) {
|
} else if (playerIsAlreadyReady) {
|
||||||
playerErrorNotifier("You are already ready")
|
playerErrorNotifier("You are already ready")
|
||||||
} else {
|
} else {
|
||||||
eventHandler.handle {
|
eventHandler.handle(payload.aggregateId) {
|
||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = payload.aggregateId,
|
aggregateId = payload.aggregateId,
|
||||||
player = payload.player,
|
player = payload.player,
|
||||||
|
|||||||
@@ -9,5 +9,8 @@ import eventDemo.libs.event.Event
|
|||||||
interface EventHandler<E : Event<ID>, ID : AggregateId> {
|
interface EventHandler<E : Event<ID>, ID : AggregateId> {
|
||||||
fun registerProjectionBuilder(builder: (E) -> Unit)
|
fun registerProjectionBuilder(builder: (E) -> Unit)
|
||||||
|
|
||||||
fun handle(buildEvent: (version: Int) -> E): E
|
fun handle(
|
||||||
|
aggregateId: ID,
|
||||||
|
buildEvent: (version: Int) -> E,
|
||||||
|
): E
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ package eventDemo.app.event
|
|||||||
import eventDemo.app.entity.GameId
|
import eventDemo.app.entity.GameId
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.libs.event.VersionBuilder
|
import eventDemo.libs.event.VersionBuilder
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stream to publish and read the played card event.
|
* A stream to publish and read the played card event.
|
||||||
@@ -12,18 +16,26 @@ class GameEventHandler(
|
|||||||
private val eventStream: GameEventStream,
|
private val eventStream: GameEventStream,
|
||||||
private val versionBuilder: VersionBuilder,
|
private val versionBuilder: VersionBuilder,
|
||||||
) : EventHandler<GameEvent, GameId> {
|
) : EventHandler<GameEvent, GameId> {
|
||||||
private val projectionsBuilders: MutableList<(GameEvent) -> Unit> = mutableListOf()
|
private val projectionsBuilders: ConcurrentLinkedQueue<(GameEvent) -> Unit> = ConcurrentLinkedQueue()
|
||||||
|
private val locks: ConcurrentHashMap<GameId, ReentrantLock> = ConcurrentHashMap()
|
||||||
|
|
||||||
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
|
override fun registerProjectionBuilder(builder: GameProjectionBuilder) {
|
||||||
projectionsBuilders.add(builder)
|
projectionsBuilders.add(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(buildEvent: (version: Int) -> GameEvent): GameEvent =
|
override fun handle(
|
||||||
buildEvent(versionBuilder.buildNextVersion()).also { event ->
|
aggregateId: GameId,
|
||||||
eventStream.publish(event)
|
buildEvent: (version: Int) -> GameEvent,
|
||||||
projectionsBuilders.forEach { it(event) }
|
): GameEvent =
|
||||||
eventBus.publish(event)
|
locks
|
||||||
}
|
.computeIfAbsent(aggregateId) { ReentrantLock() }
|
||||||
|
.withLock {
|
||||||
|
buildEvent(versionBuilder.buildNextVersion(aggregateId))
|
||||||
|
.also { eventStream.publish(it) }
|
||||||
|
}.also { event ->
|
||||||
|
projectionsBuilders.forEach { it(event) }
|
||||||
|
eventBus.publish(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias GameProjectionBuilder = (GameEvent) -> Unit
|
typealias GameProjectionBuilder = (GameEvent) -> Unit
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package eventDemo.app.event.projection
|
package eventDemo.app.event.projection
|
||||||
|
|
||||||
import eventDemo.app.entity.Card
|
import eventDemo.app.entity.Card
|
||||||
import eventDemo.app.entity.GameId
|
|
||||||
import eventDemo.app.event.GameEventStream
|
|
||||||
import eventDemo.app.event.event.CardIsPlayedEvent
|
import eventDemo.app.event.event.CardIsPlayedEvent
|
||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
import eventDemo.app.event.event.GameStartedEvent
|
import eventDemo.app.event.event.GameStartedEvent
|
||||||
@@ -14,23 +12,8 @@ import eventDemo.app.event.event.PlayerReadyEvent
|
|||||||
import eventDemo.app.event.event.PlayerWinEvent
|
import eventDemo.app.event.event.PlayerWinEvent
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
|
||||||
fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState {
|
fun GameState.apply(event: GameEvent): GameState =
|
||||||
val events = eventStream.readAll(this)
|
this.let { state ->
|
||||||
if (events.isEmpty()) return GameState(this)
|
|
||||||
return events.buildStateFromEvents().also {
|
|
||||||
KotlinLogging.logger {}.warn { "state is build from scratch for game: $this " }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Collection<GameEvent>.buildStateFromEvents(): GameState {
|
|
||||||
val gameId = this.firstOrNull()?.aggregateId ?: error("Cannot build GameState from an empty list")
|
|
||||||
return fold(GameState(gameId)) { state, event ->
|
|
||||||
state.apply(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun GameState?.apply(event: GameEvent): GameState =
|
|
||||||
(this ?: GameState(event.aggregateId)).let { state ->
|
|
||||||
val logger = KotlinLogging.logger { }
|
val logger = KotlinLogging.logger { }
|
||||||
if (event is PlayerActionEvent) {
|
if (event is PlayerActionEvent) {
|
||||||
if (state.currentPlayerTurn != event.player) {
|
if (state.currentPlayerTurn != event.player) {
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import eventDemo.app.event.GameEventStream
|
|||||||
import eventDemo.app.event.event.GameEvent
|
import eventDemo.app.event.event.GameEvent
|
||||||
|
|
||||||
class GameStateRepository(
|
class GameStateRepository(
|
||||||
private val eventStream: GameEventStream,
|
eventStream: GameEventStream,
|
||||||
eventHandler: GameEventHandler,
|
eventHandler: GameEventHandler,
|
||||||
maxSnapshotCacheSize: Int = 20,
|
snapshotConfig: SnapshotConfig = SnapshotConfig(),
|
||||||
) {
|
) {
|
||||||
private val projectionsSnapshot =
|
private val projectionsSnapshot =
|
||||||
ProjectionSnapshotRepositoryInMemory(
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
applyToProjection = GameState?::apply,
|
eventStream = eventStream,
|
||||||
maxSnapshotCacheSize = maxSnapshotCacheSize,
|
snapshotCacheConfig = snapshotConfig,
|
||||||
|
applyToProjection = GameState::apply,
|
||||||
|
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -27,9 +29,7 @@ class GameStateRepository(
|
|||||||
*
|
*
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
* It fetches it from the local cache if possible, otherwise it builds it.
|
||||||
*/
|
*/
|
||||||
fun getLast(gameId: GameId): GameState =
|
fun getLast(gameId: GameId): GameState = projectionsSnapshot.getLast(gameId)
|
||||||
projectionsSnapshot.getLast(gameId)
|
|
||||||
?: gameId.buildStateFromEventStream(eventStream)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [GameState] to the specific [event][GameEvent].
|
* Get the [GameState] to the specific [event][GameEvent].
|
||||||
@@ -37,8 +37,5 @@ class GameStateRepository(
|
|||||||
*
|
*
|
||||||
* It fetches it from the local cache if possible, otherwise it builds it.
|
* It fetches it from the local cache if possible, otherwise it builds it.
|
||||||
*/
|
*/
|
||||||
fun getUntil(event: GameEvent): GameState =
|
fun getUntil(event: GameEvent): GameState = projectionsSnapshot.getUntil(event)
|
||||||
projectionsSnapshot.getUntil(event)
|
|
||||||
?: (eventStream.readAll(event.aggregateId).takeWhile { it != event } + event)
|
|
||||||
.buildStateFromEvents()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,172 @@ package eventDemo.app.event.projection
|
|||||||
|
|
||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.event.AggregateId
|
||||||
import eventDemo.libs.event.Event
|
import eventDemo.libs.event.Event
|
||||||
|
import eventDemo.libs.event.EventStream
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
data class SnapshotConfig(
|
||||||
|
val maxSnapshotCacheSize: Int = 20,
|
||||||
|
val maxSnapshotCacheTtl: Duration = 10.minutes,
|
||||||
|
)
|
||||||
|
|
||||||
class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID : AggregateId>(
|
class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID : AggregateId>(
|
||||||
private val maxSnapshotCacheSize: Int = 20,
|
private val eventStream: EventStream<E, ID>,
|
||||||
private val applyToProjection: P?.(event: E) -> P,
|
private val initialStateBuilder: (ID) -> P,
|
||||||
|
private val snapshotCacheConfig: SnapshotConfig = SnapshotConfig(),
|
||||||
|
private val applyToProjection: P.(event: E) -> P,
|
||||||
) {
|
) {
|
||||||
private val projectionsSnapshot: ConcurrentHashMap<E, P> = ConcurrentHashMap()
|
private val projectionsSnapshot: ConcurrentHashMap<ID, ConcurrentLinkedQueue<Pair<P, Instant>>> = ConcurrentHashMap()
|
||||||
|
|
||||||
fun applyAndPutToCache(event: E): P {
|
/**
|
||||||
// lock here
|
* Create a snapshot for the event
|
||||||
return projectionsSnapshot
|
*
|
||||||
.filterKeys { it.aggregateId == event.aggregateId }
|
* 1. get the last snapshot with a version lower than that of the event
|
||||||
.toList()
|
* 2. get the events with a greater version of the snapshot
|
||||||
.find { (e, _) -> e.version == (event.version - 1) }
|
* 3. apply the event to the snapshot
|
||||||
?.second
|
* 4. apply the new event to the projection
|
||||||
.applyToProjection(event)
|
* 5. save it
|
||||||
.also { projectionsSnapshot.put(event, it) }
|
* 6. remove old one
|
||||||
.also { removeOldSnapshot() }
|
*/
|
||||||
// Unlock here
|
fun applyAndPutToCache(event: E): P =
|
||||||
|
getUntil(event)
|
||||||
|
.also {
|
||||||
|
save(it)
|
||||||
|
removeOldSnapshot(it.aggregateId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the last version of the [Projection] from the cache.
|
||||||
|
*
|
||||||
|
* 1. get the last snapshot
|
||||||
|
* 2. get the missing event to the snapshot
|
||||||
|
* 3. apply the missing events to the snapshot
|
||||||
|
*/
|
||||||
|
fun getLast(aggregateId: ID): P {
|
||||||
|
val lastSnapshot = getLastSnapshot(aggregateId)?.first
|
||||||
|
val missingEventOfSnapshot = getEventAfterTheSnapshot(aggregateId, lastSnapshot)
|
||||||
|
return lastSnapshot.applyEvents(aggregateId, missingEventOfSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeOldSnapshot() {
|
/**
|
||||||
if (projectionsSnapshot.size > maxSnapshotCacheSize) {
|
* Build the [Projection] to the specific [event][Event].
|
||||||
val numberToRemove = projectionsSnapshot.size - maxSnapshotCacheSize
|
*
|
||||||
|
* It does not contain the [events][Event] it after this one.
|
||||||
|
*
|
||||||
|
* 1. get the last snapshot before the event
|
||||||
|
* 2. get the events with a greater version of the snapshot but lower of passed event
|
||||||
|
* 3. apply the events to the snapshot
|
||||||
|
*/
|
||||||
|
fun getUntil(event: E): P {
|
||||||
|
val lastSnapshot = getLastSnapshotBeforeOrEqualEvent(event)?.first
|
||||||
|
if (lastSnapshot?.lastEventVersion == event.version) {
|
||||||
|
return lastSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
projectionsSnapshot
|
val missingEventOfSnapshot =
|
||||||
.keys
|
eventStream.readGreaterOfVersion(
|
||||||
.sortedBy { it.version }
|
event.aggregateId,
|
||||||
.take(numberToRemove)
|
lastSnapshot?.lastEventVersion ?: 0,
|
||||||
.forEach { event ->
|
)
|
||||||
projectionsSnapshot.remove(event)
|
|
||||||
}
|
return if (lastSnapshot?.lastEventVersion == event.version) {
|
||||||
|
lastSnapshot
|
||||||
|
} else {
|
||||||
|
lastSnapshot.applyEvents(event.aggregateId, missingEventOfSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last version of the [Projection] from the cache.
|
* Remove the oldest snapshot.
|
||||||
|
*
|
||||||
|
* The rules are pass in the controller.
|
||||||
*/
|
*/
|
||||||
fun getLast(aggregateId: ID): P? =
|
private fun removeOldSnapshot(aggregateId: ID) {
|
||||||
projectionsSnapshot
|
projectionsSnapshot[aggregateId]?.let { queue ->
|
||||||
.filter { it.key.aggregateId == aggregateId }
|
// never remove the last one
|
||||||
.maxByOrNull { (event, _) -> event.version }
|
val theLastOne = getLastSnapshot(aggregateId)
|
||||||
?.value
|
|
||||||
|
// remove the oldest by time
|
||||||
|
val now = Clock.System.now()
|
||||||
|
val deadLine = now - snapshotCacheConfig.maxSnapshotCacheTtl
|
||||||
|
val toRemove = queue.filter { deadLine > it.second }
|
||||||
|
(toRemove - theLastOne).forEach { queue.remove(it) }
|
||||||
|
|
||||||
|
// Remove if size exceeds the limit
|
||||||
|
if (queue.size > snapshotCacheConfig.maxSnapshotCacheSize) {
|
||||||
|
val numberToRemove = projectionsSnapshot.size - snapshotCacheConfig.maxSnapshotCacheSize
|
||||||
|
if (numberToRemove > 0) {
|
||||||
|
queue
|
||||||
|
.sortedByDescending { it.first.lastEventVersion }
|
||||||
|
.take(numberToRemove)
|
||||||
|
.let { it - theLastOne }
|
||||||
|
.forEach { queue.remove(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the [Projection] to the specific [event][Event].
|
* Save the snapshot.
|
||||||
* It does not contain the [events][Event] it after this one.
|
|
||||||
*/
|
*/
|
||||||
fun getUntil(event: E): P? = projectionsSnapshot.get(event)
|
private fun save(projection: P) {
|
||||||
|
projectionsSnapshot
|
||||||
|
.computeIfAbsent(projection.aggregateId) { ConcurrentLinkedQueue() }
|
||||||
|
.add(Pair(projection, Clock.System.now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last snapshot when the version is lower of then event version
|
||||||
|
*/
|
||||||
|
private fun getLastSnapshotBeforeOrEqualEvent(event: E) =
|
||||||
|
projectionsSnapshot[event.aggregateId]
|
||||||
|
?.sortedByDescending { it.first.lastEventVersion }
|
||||||
|
?.find { it.first.lastEventVersion <= event.version }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last snapshot (with the higher version).
|
||||||
|
*/
|
||||||
|
private fun getLastSnapshot(aggregateId: ID) =
|
||||||
|
projectionsSnapshot[aggregateId]
|
||||||
|
?.maxByOrNull { it.first.lastEventVersion }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the events from the [event stream][EventStream] when the version is higher of the snapshot.
|
||||||
|
*
|
||||||
|
* If the snapshot is null, it takes all events from the event [event stream][EventStream]
|
||||||
|
*/
|
||||||
|
private fun getEventAfterTheSnapshot(
|
||||||
|
aggregateId: ID,
|
||||||
|
snapshot: P?,
|
||||||
|
) = eventStream
|
||||||
|
.readGreaterOfVersion(aggregateId, snapshot?.lastEventVersion ?: 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply events to the projection.
|
||||||
|
*/
|
||||||
|
private fun P?.applyEvents(
|
||||||
|
aggregateId: ID,
|
||||||
|
eventsToApply: Set<E>,
|
||||||
|
): P =
|
||||||
|
eventsToApply
|
||||||
|
.fold(this ?: initialStateBuilder(aggregateId), applyToProjectionSecure)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the [applyToProjection] lambda to avoid duplicate apply of the same event.
|
||||||
|
*/
|
||||||
|
private val applyToProjectionSecure: P.(event: E) -> P = { event ->
|
||||||
|
if (event.version == lastEventVersion + 1) {
|
||||||
|
applyToProjection(event)
|
||||||
|
} else if (event.version <= lastEventVersion) {
|
||||||
|
KotlinLogging.logger { }.warn { "Event is already is the Projection, skip apply." }
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
error("The version of the event must follow directly after the version of the projection.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ReactionEventListener(
|
|||||||
) {
|
) {
|
||||||
if (state.isReady && !state.isStarted) {
|
if (state.isReady && !state.isStarted) {
|
||||||
val reactionEvent =
|
val reactionEvent =
|
||||||
eventHandler.handle {
|
eventHandler.handle(state.aggregateId) {
|
||||||
GameStartedEvent.new(
|
GameStartedEvent.new(
|
||||||
id = state.aggregateId,
|
id = state.aggregateId,
|
||||||
players = state.players,
|
players = state.players,
|
||||||
@@ -65,7 +65,7 @@ class ReactionEventListener(
|
|||||||
val winner = state.playerHasNoCardLeft().firstOrNull()
|
val winner = state.playerHasNoCardLeft().firstOrNull()
|
||||||
if (winner != null) {
|
if (winner != null) {
|
||||||
val reactionEvent =
|
val reactionEvent =
|
||||||
eventHandler.handle {
|
eventHandler.handle(state.aggregateId) {
|
||||||
PlayerWinEvent(
|
PlayerWinEvent(
|
||||||
aggregateId = state.aggregateId,
|
aggregateId = state.aggregateId,
|
||||||
player = winner,
|
player = winner,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import eventDemo.app.event.GameEventBus
|
|||||||
import eventDemo.app.event.GameEventHandler
|
import eventDemo.app.event.GameEventHandler
|
||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.projection.GameStateRepository
|
import eventDemo.app.event.projection.GameStateRepository
|
||||||
|
import eventDemo.app.event.projection.SnapshotConfig
|
||||||
import eventDemo.app.eventListener.PlayerNotificationEventListener
|
import eventDemo.app.eventListener.PlayerNotificationEventListener
|
||||||
import eventDemo.libs.command.CommandStreamChannelBuilder
|
import eventDemo.libs.command.CommandStreamChannelBuilder
|
||||||
import eventDemo.libs.event.EventBusInMemory
|
import eventDemo.libs.event.EventBusInMemory
|
||||||
@@ -37,7 +38,7 @@ val appKoinModule =
|
|||||||
GameEventStream(EventStreamInMemory())
|
GameEventStream(EventStreamInMemory())
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
GameStateRepository(get(), get())
|
GameStateRepository(get(), get(), snapshotConfig = SnapshotConfig())
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
CommandStreamChannelBuilder<GameCommand>()
|
CommandStreamChannelBuilder<GameCommand>()
|
||||||
|
|||||||
@@ -23,4 +23,9 @@ interface EventStream<E : Event<ID>, ID : AggregateId> {
|
|||||||
|
|
||||||
/** Reads all events associated with a given aggregate ID */
|
/** Reads all events associated with a given aggregate ID */
|
||||||
fun readAll(aggregateId: ID): Set<E>
|
fun readAll(aggregateId: ID): Set<E>
|
||||||
|
|
||||||
|
fun readGreaterOfVersion(
|
||||||
|
aggregateId: ID,
|
||||||
|
version: Int,
|
||||||
|
): Set<E>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ class EventStreamInMemory<E : Event<ID>, ID : AggregateId> : EventStream<E, ID>
|
|||||||
private val events: Queue<E> = ConcurrentLinkedQueue()
|
private val events: Queue<E> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
override fun publish(event: E) {
|
override fun publish(event: E) {
|
||||||
events.add(event)
|
if (events.none { it.eventId == event.eventId }) {
|
||||||
logger.atInfo {
|
events.add(event)
|
||||||
message = "Event published: $event"
|
logger.atInfo {
|
||||||
payload = mapOf("event" to event)
|
message = "Event published: $event"
|
||||||
|
payload = mapOf("event" to event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,15 @@ class EventStreamInMemory<E : Event<ID>, ID : AggregateId> : EventStream<E, ID>
|
|||||||
events
|
events
|
||||||
.filter { it.aggregateId == aggregateId }
|
.filter { it.aggregateId == aggregateId }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
|
override fun readGreaterOfVersion(
|
||||||
|
aggregateId: ID,
|
||||||
|
version: Int,
|
||||||
|
): Set<E> =
|
||||||
|
events
|
||||||
|
.filter { it.aggregateId == aggregateId }
|
||||||
|
.filter { it.version > version }
|
||||||
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified R : E, E : Event<ID>, ID : AggregateId> EventStream<E, ID>.readLastOf(aggregateId: ID): R? =
|
inline fun <reified R : E, E : Event<ID>, ID : AggregateId> EventStream<E, ID>.readLastOf(aggregateId: ID): R? =
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
interface VersionBuilder {
|
interface VersionBuilder {
|
||||||
fun buildNextVersion(): Int
|
fun buildNextVersion(aggregateId: AggregateId): Int
|
||||||
|
|
||||||
fun getLastVersion(): Int
|
fun getLastVersion(aggregateId: AggregateId): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class VersionBuilderLocal : VersionBuilder {
|
class VersionBuilderLocal : VersionBuilder {
|
||||||
private val version: AtomicInteger = AtomicInteger(0)
|
private val logger = KotlinLogging.logger { }
|
||||||
|
private val versions: ConcurrentHashMap<AggregateId, AtomicInteger> = ConcurrentHashMap()
|
||||||
|
|
||||||
override fun buildNextVersion(): Int = version.addAndGet(1)
|
override fun buildNextVersion(aggregateId: AggregateId): Int =
|
||||||
|
versionOfAggregate(aggregateId)
|
||||||
|
.addAndGet(1)
|
||||||
|
.also { logger.debug { "New version $it" } }
|
||||||
|
|
||||||
override fun getLastVersion(): Int = version.toInt()
|
override fun getLastVersion(aggregateId: AggregateId): Int = versionOfAggregate(aggregateId).toInt()
|
||||||
|
|
||||||
|
private fun versionOfAggregate(aggregateId: AggregateId) =
|
||||||
|
versions
|
||||||
|
.computeIfAbsent(aggregateId) { AtomicInteger(0) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class GameStateBuilderTest :
|
|||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -41,7 +41,7 @@ class GameStateBuilderTest :
|
|||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -52,7 +52,7 @@ class GameStateBuilderTest :
|
|||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -63,7 +63,7 @@ class GameStateBuilderTest :
|
|||||||
PlayerReadyEvent(
|
PlayerReadyEvent(
|
||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -77,7 +77,7 @@ class GameStateBuilderTest :
|
|||||||
id = gameId,
|
id = gameId,
|
||||||
players = setOf(player1, player2),
|
players = setOf(player1, player2),
|
||||||
shuffleIsDisabled = true,
|
shuffleIsDisabled = true,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -94,7 +94,7 @@ class GameStateBuilderTest :
|
|||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
card = playedCard,
|
card = playedCard,
|
||||||
player = player1,
|
player = player1,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
@@ -111,7 +111,7 @@ class GameStateBuilderTest :
|
|||||||
aggregateId = gameId,
|
aggregateId = gameId,
|
||||||
card = playedCard,
|
card = playedCard,
|
||||||
player = player2,
|
player = player2,
|
||||||
version = versionBuilder.buildNextVersion(),
|
version = versionBuilder.buildNextVersion(gameId),
|
||||||
)
|
)
|
||||||
apply(event).also { state ->
|
apply(event).also { state ->
|
||||||
state.aggregateId shouldBeEqual gameId
|
state.aggregateId shouldBeEqual gameId
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class GameStateRepositoryTest :
|
|||||||
val repo = get<GameStateRepository>()
|
val repo = get<GameStateRepository>()
|
||||||
val eventHandler = get<GameEventHandler>()
|
val eventHandler = get<GameEventHandler>()
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
.also { event ->
|
.also { event ->
|
||||||
assertNotNull(repo.getUntil(event)).also {
|
assertNotNull(repo.getUntil(event)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
@@ -48,7 +48,7 @@ class GameStateRepositoryTest :
|
|||||||
val eventHandler = get<GameEventHandler>()
|
val eventHandler = get<GameEventHandler>()
|
||||||
|
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
.also {
|
.also {
|
||||||
assertNotNull(repo.getLast(aggregateId)).also {
|
assertNotNull(repo.getLast(aggregateId)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
@@ -56,7 +56,7 @@ class GameStateRepositoryTest :
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||||
.also {
|
.also {
|
||||||
assertNotNull(repo.getLast(aggregateId)).also {
|
assertNotNull(repo.getLast(aggregateId)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||||
@@ -74,7 +74,7 @@ class GameStateRepositoryTest :
|
|||||||
|
|
||||||
val event1 =
|
val event1 =
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||||
.also { event1 ->
|
.also { event1 ->
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||||
@@ -82,7 +82,7 @@ class GameStateRepositoryTest :
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||||
.also { event2 ->
|
.also { event2 ->
|
||||||
assertNotNull(repo.getUntil(event2)).also {
|
assertNotNull(repo.getUntil(event2)).also {
|
||||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||||
@@ -108,7 +108,7 @@ class GameStateRepositoryTest :
|
|||||||
repeat(100) { r2 ->
|
repeat(100) { r2 ->
|
||||||
val playerX = Player("player$r$r2")
|
val playerX = Player("player$r$r2")
|
||||||
eventHandler
|
eventHandler
|
||||||
.handle {
|
.handle(aggregateId) {
|
||||||
NewPlayerEvent(
|
NewPlayerEvent(
|
||||||
aggregateId = aggregateId,
|
aggregateId = aggregateId,
|
||||||
player = playerX,
|
player = playerX,
|
||||||
@@ -119,8 +119,10 @@ class GameStateRepositoryTest :
|
|||||||
}
|
}
|
||||||
}.joinAll()
|
}.joinAll()
|
||||||
|
|
||||||
repo.getLast(aggregateId).players shouldHaveSize 1000
|
repo.getLast(aggregateId).run {
|
||||||
repo.getLast(aggregateId).lastEventVersion shouldBeEqual 1000
|
lastEventVersion shouldBeEqual 1000
|
||||||
|
players shouldHaveSize 1000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package eventDemo.app.event.projection
|
|||||||
|
|
||||||
import eventDemo.libs.event.AggregateId
|
import eventDemo.libs.event.AggregateId
|
||||||
import eventDemo.libs.event.Event
|
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.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
@@ -11,6 +14,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@@ -18,53 +23,84 @@ class ProjectionSnapshotRepositoryInMemoryTest :
|
|||||||
FunSpec({
|
FunSpec({
|
||||||
|
|
||||||
test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
test("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
||||||
repeat(10) {
|
val eventStream: EventStream<TestEvents, IdTest> = EventStreamInMemory()
|
||||||
val repo = getRepoTest()
|
val repo = getSnapshotRepoTest(eventStream)
|
||||||
val aggregateId = IdTest()
|
val aggregateId = IdTest()
|
||||||
|
|
||||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
||||||
repo.applyAndPutToCache(eventOther)
|
eventStream.publish(eventOther)
|
||||||
assertNotNull(repo.getUntil(eventOther)).also {
|
repo.applyAndPutToCache(eventOther)
|
||||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
assertNotNull(repo.getUntil(eventOther)).also {
|
||||||
}
|
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||||
|
}
|
||||||
|
|
||||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||||
repo.applyAndPutToCache(event1)
|
eventStream.publish(event1)
|
||||||
assertNotNull(repo.getLast(event1.aggregateId)).also {
|
repo.applyAndPutToCache(event1)
|
||||||
assertNotNull(it.value) shouldBeEqual "val1"
|
assertNotNull(repo.getLast(event1.aggregateId)).also {
|
||||||
}
|
assertNotNull(it.value) shouldBeEqual "val1"
|
||||||
assertNotNull(repo.getUntil(event1)).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)
|
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
||||||
repo.applyAndPutToCache(event2)
|
eventStream.publish(event2)
|
||||||
assertNotNull(repo.getLast(event2.aggregateId)).also {
|
repo.applyAndPutToCache(event2)
|
||||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
assertNotNull(repo.getLast(event2.aggregateId)).also {
|
||||||
}
|
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||||
assertNotNull(repo.getUntil(event1)).also {
|
}
|
||||||
assertNotNull(it.value) shouldBeEqual "val1"
|
assertNotNull(repo.getUntil(event1)).also {
|
||||||
}
|
assertNotNull(it.value) shouldBeEqual "val1"
|
||||||
assertNotNull(repo.getUntil(event2)).also {
|
}
|
||||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
assertNotNull(repo.getUntil(event2)).also {
|
||||||
}
|
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("ProjectionSnapshotRepositoryInMemory should be thread safe") {
|
test("ProjectionSnapshotRepositoryInMemory should be thread safe") {
|
||||||
val repo = getRepoTest(2000)
|
val eventStream: EventStream<TestEvents, IdTest> = EventStreamInMemory()
|
||||||
|
val repo = getSnapshotRepoTest(eventStream)
|
||||||
val aggregateId = IdTest()
|
val aggregateId = IdTest()
|
||||||
(1..10)
|
val versionBuilder = VersionBuilderLocal()
|
||||||
.map { r ->
|
val lock = ReentrantLock()
|
||||||
|
(0..9)
|
||||||
|
.map {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
repeat(10) {
|
(1..10).map {
|
||||||
val eventX = EventXTest(num = 1, version = r, aggregateId = aggregateId)
|
val eventX =
|
||||||
|
lock.withLock {
|
||||||
|
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||||
|
.also { eventStream.publish(it) }
|
||||||
|
}
|
||||||
repo.applyAndPutToCache(eventX)
|
repo.applyAndPutToCache(eventX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.joinAll()
|
}.joinAll()
|
||||||
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
|
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
|
@JvmInline
|
||||||
@@ -105,9 +141,16 @@ private data class EventXTest(
|
|||||||
val num: Int,
|
val num: Int,
|
||||||
) : TestEvents
|
) : TestEvents
|
||||||
|
|
||||||
private fun getRepoTest(maxSnapshotCacheSize: Int = 2000): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
|
private fun getSnapshotRepoTest(
|
||||||
ProjectionSnapshotRepositoryInMemory(maxSnapshotCacheSize) { event ->
|
eventStream: EventStream<TestEvents, IdTest>,
|
||||||
(this ?: ProjectionTest(event.aggregateId)).let { projection ->
|
snapshotConfig: SnapshotConfig = SnapshotConfig(2000),
|
||||||
|
): ProjectionSnapshotRepositoryInMemory<TestEvents, ProjectionTest, IdTest> =
|
||||||
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
|
eventStream = eventStream,
|
||||||
|
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||||
|
snapshotCacheConfig = snapshotConfig,
|
||||||
|
) { event ->
|
||||||
|
this.let { projection ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is Event1Test -> {
|
is Event1Test -> {
|
||||||
projection.copy(value = (projection.value ?: "") + event.value1)
|
projection.copy(value = (projection.value ?: "") + event.value1)
|
||||||
@@ -120,6 +163,8 @@ private fun getRepoTest(maxSnapshotCacheSize: Int = 2000): ProjectionSnapshotRep
|
|||||||
is EventXTest -> {
|
is EventXTest -> {
|
||||||
projection.copy(num = projection.num + event.num)
|
projection.copy(num = projection.num + event.num)
|
||||||
}
|
}
|
||||||
}
|
}.copy(
|
||||||
|
lastEventVersion = event.version,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ class GameStateRouteTest :
|
|||||||
val eventHandler by inject<GameEventHandler>()
|
val eventHandler by inject<GameEventHandler>()
|
||||||
val stateRepo by inject<GameStateRepository>()
|
val stateRepo by inject<GameStateRepository>()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
eventHandler.handle { NewPlayerEvent(gameId, player1, it) }
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||||
eventHandler.handle { NewPlayerEvent(gameId, player2, it) }
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||||
eventHandler.handle { PlayerReadyEvent(gameId, player1, it) }
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||||
eventHandler.handle { PlayerReadyEvent(gameId, player2, it) }
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||||
eventHandler.handle {
|
eventHandler.handle(gameId) {
|
||||||
GameStartedEvent.new(
|
GameStartedEvent.new(
|
||||||
gameId,
|
gameId,
|
||||||
setOf(player1, player2),
|
setOf(player1, player2),
|
||||||
@@ -92,7 +92,7 @@ class GameStateRouteTest :
|
|||||||
it.color shouldBeEqual Card.Color.Red
|
it.color shouldBeEqual Card.Color.Red
|
||||||
}
|
}
|
||||||
delay(100)
|
delay(100)
|
||||||
eventHandler.handle {
|
eventHandler.handle(gameId) {
|
||||||
CardIsPlayedEvent(
|
CardIsPlayedEvent(
|
||||||
gameId,
|
gameId,
|
||||||
assertNotNull(lastPlayedCard),
|
assertNotNull(lastPlayedCard),
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import eventDemo.app.entity.Player
|
|||||||
import eventDemo.app.event.GameEventStream
|
import eventDemo.app.event.GameEventStream
|
||||||
import eventDemo.app.event.event.disableShuffleDeck
|
import eventDemo.app.event.event.disableShuffleDeck
|
||||||
import eventDemo.app.event.projection.GameState
|
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.PlayerNotificationEventListener
|
||||||
import eventDemo.app.eventListener.ReactionEventListener
|
import eventDemo.app.eventListener.ReactionEventListener
|
||||||
import eventDemo.app.notification.ItsTheTurnOfNotification
|
import eventDemo.app.notification.ItsTheTurnOfNotification
|
||||||
@@ -148,7 +149,12 @@ class GameStateTest :
|
|||||||
|
|
||||||
joinAll(player1Job, player2Job)
|
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
|
state.aggregateId shouldBeEqual id
|
||||||
assertTrue(state.isStarted)
|
assertTrue(state.isStarted)
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.joinAll
|
import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
private value class IdTest(
|
||||||
|
override val id: UUID = UUID.randomUUID(),
|
||||||
|
) : AggregateId
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class VersionBuilderLocalTest :
|
class VersionBuilderLocalTest :
|
||||||
@@ -13,30 +19,34 @@ class VersionBuilderLocalTest :
|
|||||||
|
|
||||||
test("buildNextVersion") {
|
test("buildNextVersion") {
|
||||||
VersionBuilderLocal().run {
|
VersionBuilderLocal().run {
|
||||||
buildNextVersion() shouldBeEqual 1
|
val id = IdTest()
|
||||||
buildNextVersion() shouldBeEqual 2
|
buildNextVersion(id) shouldBeEqual 1
|
||||||
buildNextVersion() shouldBeEqual 3
|
buildNextVersion(id) shouldBeEqual 2
|
||||||
|
buildNextVersion(IdTest()) shouldBeEqual 1
|
||||||
|
buildNextVersion(id) shouldBeEqual 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("buildNextVersion concurrently") {
|
test("buildNextVersion concurrently") {
|
||||||
val versionBuilder = VersionBuilderLocal()
|
val versionBuilder = VersionBuilderLocal()
|
||||||
|
val id = IdTest()
|
||||||
(1..20)
|
(1..20)
|
||||||
.map {
|
.map {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
(1..1000).map {
|
(1..1000).map {
|
||||||
versionBuilder.buildNextVersion()
|
versionBuilder.buildNextVersion(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.joinAll()
|
}.joinAll()
|
||||||
versionBuilder.getLastVersion() shouldBeEqual 20 * 1000
|
versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
test("getLastVersion") {
|
test("getLastVersion") {
|
||||||
VersionBuilderLocal().run {
|
VersionBuilderLocal().run {
|
||||||
getLastVersion() shouldBeEqual 0
|
val id = IdTest()
|
||||||
getLastVersion() shouldBeEqual 0
|
getLastVersion(id) shouldBeEqual 0
|
||||||
getLastVersion() shouldBeEqual 0
|
getLastVersion(id) shouldBeEqual 0
|
||||||
|
getLastVersion(id) shouldBeEqual 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user