feat(Tracer Bullet)

This commit is contained in:
2025-08-11 23:47:05 +02:00
parent 582678e226
commit a50b1b3822
43 changed files with 1665 additions and 292 deletions
@@ -1,6 +1,8 @@
package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.DomainEvent
import at.mocode.core.domain.model.AggregateId
import at.mocode.core.domain.model.EventVersion
import at.mocode.infrastructure.eventstore.api.ConcurrencyException
import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore
@@ -26,7 +28,7 @@ class RedisEventStore(
val aggregateId = events.first().aggregateId
require(events.all { it.aggregateId == aggregateId }) { "All events must belong to the same aggregate" }
require(streamId == aggregateId) { "Stream ID must match aggregate ID" }
require(streamId == aggregateId.value) { "Stream ID must match aggregate ID" }
var currentVersion = getStreamVersion(streamId)
@@ -59,7 +61,7 @@ class RedisEventStore(
private fun appendToStreamInternal(event: DomainEvent, streamId: Uuid, currentVersion: Long): Long {
val newVersion = currentVersion + 1
require(event.version == newVersion) { "Event version ${event.version} does not match expected new version $newVersion" }
require(event.version.value == newVersion) { "Event version ${event.version} does not match expected new version $newVersion" }
val streamKey = getStreamKey(streamId)
val allEventsStreamKey = getAllEventsStreamKey()
@@ -102,7 +104,7 @@ class RedisEventStore(
}
} ?: emptyList()
return events.filter { it.version >= fromVersion && (toVersion == null || it.version <= toVersion) }
return events.filter { it.version >= EventVersion(fromVersion) && (toVersion == null || it.version <= EventVersion(toVersion)) }
}
override fun getStreamVersion(streamId: Uuid): Long {
@@ -2,6 +2,7 @@ package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.event.DomainEvent
import at.mocode.core.domain.model.*
import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore
import com.benasher44.uuid.Uuid
@@ -85,8 +86,8 @@ class RedisEventStoreIntegrationTest {
@Test
fun `event publishing and consuming with consumer groups should work`() {
val aggregateId = uuid4()
val event1 = TestCreatedEvent(aggregateId = aggregateId, version = 1L, name = "Test Entity")
val event2 = TestUpdatedEvent(aggregateId = aggregateId, version = 2L, name = "Updated Test Entity")
val event1 = TestCreatedEvent(aggregateId = AggregateId(aggregateId), version = EventVersion(1L), name = "Test Entity")
val event2 = TestUpdatedEvent(aggregateId = AggregateId(aggregateId), version = EventVersion(2L), name = "Updated Test Entity")
val latch = CountDownLatch(2)
val receivedEvents = mutableListOf<DomainEvent>()
@@ -112,34 +113,34 @@ class RedisEventStoreIntegrationTest {
assertEquals(2, receivedEvents.size)
val receivedEvent1 = receivedEvents.find { it.version == 1L } as TestCreatedEvent
assertEquals(aggregateId, receivedEvent1.aggregateId)
val receivedEvent1 = receivedEvents.find { it.version == EventVersion(1L) } as TestCreatedEvent
assertEquals(AggregateId(aggregateId), receivedEvent1.aggregateId)
assertEquals("Test Entity", receivedEvent1.name)
val receivedEvent2 = receivedEvents.find { it.version == 2L } as TestUpdatedEvent
assertEquals(aggregateId, receivedEvent2.aggregateId)
val receivedEvent2 = receivedEvents.find { it.version == EventVersion(2L) } as TestUpdatedEvent
assertEquals(AggregateId(aggregateId), receivedEvent2.aggregateId)
assertEquals("Updated Test Entity", receivedEvent2.name)
}
data class TestCreatedEvent(
override val aggregateId: Uuid,
override val version: Long,
override val aggregateId: AggregateId,
override val version: EventVersion,
val name: String,
override val eventType: String = "TestCreated",
override val eventId: Uuid = uuid4(),
override val eventType: EventType = EventType("TestCreated"),
override val eventId: EventId = EventId(uuid4()),
override val timestamp: Instant = Clock.System.now(),
override val correlationId: Uuid? = null,
override val causationId: Uuid? = null
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : BaseDomainEvent(aggregateId, eventType, version, eventId, timestamp, correlationId, causationId)
data class TestUpdatedEvent(
override val aggregateId: Uuid,
override val version: Long,
override val aggregateId: AggregateId,
override val version: EventVersion,
val name: String,
override val eventType: String = "TestUpdated",
override val eventId: Uuid = uuid4(),
override val eventType: EventType = EventType("TestUpdated"),
override val eventId: EventId = EventId(uuid4()),
override val timestamp: Instant = Clock.System.now(),
override val correlationId: Uuid? = null,
override val causationId: Uuid? = null
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : BaseDomainEvent(aggregateId, eventType, version, eventId, timestamp, correlationId, causationId)
}
@@ -1,6 +1,7 @@
package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.model.*
import at.mocode.infrastructure.eventstore.api.ConcurrencyException
import at.mocode.infrastructure.eventstore.api.EventSerializer
import com.benasher44.uuid.Uuid
@@ -68,8 +69,8 @@ class RedisEventStoreTest {
@Test
fun `append and read events should work correctly for new stream`() {
val aggregateId = uuid4()
val event1 = TestCreatedEvent(aggregateId, 1L, "Test Entity")
val event2 = TestUpdatedEvent(aggregateId, 2L, "Updated Test Entity")
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity")
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity")
eventStore.appendToStream(listOf(event1, event2), aggregateId, 0)
@@ -77,21 +78,21 @@ class RedisEventStoreTest {
assertEquals(2, events.size)
val firstEvent = events[0] as TestCreatedEvent
assertEquals(1L, firstEvent.version)
assertEquals(EventVersion(1L), firstEvent.version)
assertEquals("Test Entity", firstEvent.name)
val secondEvent = events[1] as TestUpdatedEvent
assertEquals(2L, secondEvent.version)
assertEquals(EventVersion(2L), secondEvent.version)
assertEquals("Updated Test Entity", secondEvent.name)
}
@Test
fun `appending with wrong expected version should throw ConcurrencyException`() {
val aggregateId = uuid4()
val event1 = TestCreatedEvent(aggregateId, 1L, "Test Entity")
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity")
eventStore.appendToStream(listOf(event1), aggregateId, 0) // Stream is now at version 1
val event2 = TestUpdatedEvent(aggregateId, 2L, "Updated Test Entity")
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity")
assertThrows<ConcurrencyException> {
eventStore.appendToStream(listOf(event2), aggregateId, 0)
}
@@ -99,15 +100,15 @@ class RedisEventStoreTest {
@Serializable
data class TestCreatedEvent(
@Transient override val aggregateId: Uuid = uuid4(),
@Transient override val version: Long = 0,
@Transient override val aggregateId: AggregateId = AggregateId(uuid4()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, "TestCreated", version)
) : BaseDomainEvent(aggregateId, EventType("TestCreated"), version)
@Serializable
data class TestUpdatedEvent(
@Transient override val aggregateId: Uuid = uuid4(),
@Transient override val version: Long = 0,
@Transient override val aggregateId: AggregateId = AggregateId(uuid4()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, "TestUpdated", version)
) : BaseDomainEvent(aggregateId, EventType("TestUpdated"), version)
}
@@ -2,6 +2,7 @@ package at.mocode.infrastructure.eventstore.redis
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.event.DomainEvent
import at.mocode.core.domain.model.*
import at.mocode.infrastructure.eventstore.api.EventSerializer
import at.mocode.infrastructure.eventstore.api.EventStore
import com.benasher44.uuid.Uuid
@@ -78,8 +79,8 @@ class RedisIntegrationTest {
@Test
fun `event publishing and consuming should be fast and reliable`() {
val aggregateId = uuid4()
val event1 = TestCreatedEvent(aggregateId, 1L, "Test Entity")
val event2 = TestUpdatedEvent(aggregateId, 2L, "Updated Test Entity")
val event1 = TestCreatedEvent(AggregateId(aggregateId), EventVersion(1L), "Test Entity")
val event2 = TestUpdatedEvent(AggregateId(aggregateId), EventVersion(2L), "Updated Test Entity")
val receivedEvents = mutableListOf<DomainEvent>()
eventConsumer.registerEventHandler("TestCreated") { receivedEvents.add(it) }
@@ -91,26 +92,26 @@ class RedisIntegrationTest {
assertEquals(2, receivedEvents.size)
val receivedEvent1 = receivedEvents.find { it.version == 1L } as TestCreatedEvent
assertEquals(aggregateId, receivedEvent1.aggregateId)
val receivedEvent1 = receivedEvents.find { it.version == EventVersion(1L) } as TestCreatedEvent
assertEquals(AggregateId(aggregateId), receivedEvent1.aggregateId)
assertEquals("Test Entity", receivedEvent1.name)
val receivedEvent2 = receivedEvents.find { it.version == 2L } as TestUpdatedEvent
assertEquals(aggregateId, receivedEvent2.aggregateId)
val receivedEvent2 = receivedEvents.find { it.version == EventVersion(2L) } as TestUpdatedEvent
assertEquals(AggregateId(aggregateId), receivedEvent2.aggregateId)
assertEquals("Updated Test Entity", receivedEvent2.name)
}
@Serializable
data class TestCreatedEvent(
@Transient override val aggregateId: Uuid = uuid4(),
@Transient override val version: Long = 0,
@Transient override val aggregateId: AggregateId = AggregateId(uuid4()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, "TestCreated", version)
) : BaseDomainEvent(aggregateId, EventType("TestCreated"), version)
@Serializable
data class TestUpdatedEvent(
@Transient override val aggregateId: Uuid = uuid4(),
@Transient override val version: Long = 0,
@Transient override val aggregateId: AggregateId = AggregateId(uuid4()),
@Transient override val version: EventVersion = EventVersion(0),
val name: String
) : BaseDomainEvent(aggregateId, "TestUpdated", version)
) : BaseDomainEvent(aggregateId, EventType("TestUpdated"), version)
}