All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.ks.kes.grpc.GrpcAggregateRepository.kt Maven / Gradle / Ivy

package no.ks.kes.grpc

import com.eventstore.dbclient.*
import com.eventstore.dbclient.ExpectedRevision.expectedRevision
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.reactivex.rxjava3.core.Flowable
import mu.KotlinLogging
import no.ks.kes.grpc.GrpcEventUtil.isIgnorable
import no.ks.kes.lib.*
import no.ks.kes.lib.EventData
import java.util.*
import java.util.concurrent.CompletionException
import java.util.concurrent.ExecutionException
import kotlin.reflect.KClass

private val log = KotlinLogging.logger {}

class GrpcAggregateRepository(
    private val eventStoreDBClient: EventStoreDBClient,
    private val serdes: EventSerdes,
    private val streamIdGenerator: (aggregateType: String, aggregateId: UUID) -> String,
    private val metadataSerdes: EventMetadataSerdes? = null,
    private val allowRetryOnWrite: Boolean = false,
) : AggregateRepository() {

    override fun append(aggregateType: String, aggregateId: UUID, expectedEventNumber: ExpectedEventNumber, eventWrappers: List>) {
        try {
            appendEventsToStream(aggregateType, aggregateId, eventWrappers, expectedEventNumber)
        } catch (e: WriteAbortedException) {
            if (allowRetryOnWrite) {
                log.info { "Retrying write events to stream for aggregateType: $aggregateType, aggregateId: '$aggregateId'" }
                appendEventsToStream(aggregateType, aggregateId, eventWrappers, expectedEventNumber)
            } else {
                log.error(e) { "Got aborted status when we were writing data to stream, with no retry. aggregateType: $aggregateType, aggregateId: '$aggregateId'" }
                throw e
            }
        }
    }

    private fun appendEventsToStream(
        aggregateType: String,
        aggregateId: UUID,
        eventWrappers: List>,
        expectedEventNumber: ExpectedEventNumber,
    ) {
        val streamId = streamIdGenerator.invoke(aggregateType, aggregateId)
        val events = eventWrappers.map { toEventData(it, serdes) }
        try {
            eventStoreDBClient
                .appendToStream(
                    streamId,
                    AppendToStreamOptions.get().expectedRevision(resolveExpectedRevision(expectedEventNumber)),
                    events.iterator()
                )
                .get()
                .also {
                    log.info("wrote ${eventWrappers.size} events to stream ${streamId}, next expected version for this stream is ${it.nextExpectedRevision}")
                }

        } catch (e: ExecutionException) {
            val cause = e.cause
            if (cause is WrongExpectedVersionException) {
                throw RuntimeException(
                    "Actual version did not match expected! streamName: ${cause.streamName}, nextExpectedRevision: ${cause.nextExpectedRevision}, actualVersion: ${cause.actualVersion}",
                    e
                )
            } else if (cause is StatusRuntimeException && cause.status == Status.ABORTED) {
                throw WriteAbortedException("Got aborted status when we were writing data to stream", cause)
            } else {
                throw RuntimeException("Error while appending events to stream $streamId", cause)
            }
        }
    }

    private fun toEventData(event: Event<*>, serdes: EventSerdes): com.eventstore.dbclient.EventData =
        if (serdes.isJson()) {
            EventDataBuilder.json(serdes.getSerializationId(event.eventData::class), serdes.serialize(event.eventData))
        } else {
            EventDataBuilder.binary(serdes.getSerializationId(event.eventData::class), serdes.serialize(event.eventData))
        }.apply {
            if (metadataSerdes != null && event.metadata != null) {
                metadataAsBytes(metadataSerdes.serialize(event.metadata!!))
            }
        }.build()

    private fun resolveExpectedRevision(expectedEventNumber: ExpectedEventNumber): ExpectedRevision =
        when (expectedEventNumber) {
            is ExpectedEventNumber.AggregateDoesNotExist -> ExpectedRevision.noStream()
            is ExpectedEventNumber.AggregateExists -> ExpectedRevision.streamExists()
            is ExpectedEventNumber.Any -> ExpectedRevision.any()
            is ExpectedEventNumber.Exact -> expectedRevision(expectedEventNumber.eventNumber)
        }

    override fun getSerializationId(eventDataClass: KClass>): String =
        serdes.getSerializationId(eventDataClass)

    override fun  read(aggregateId: UUID, aggregateType: String, applicator: (state: A?, event: EventWrapper<*>) -> A?): AggregateReadResult {
        val streamId = streamIdGenerator.invoke(aggregateType, aggregateId)

        return try {
            Flowable
                .fromPublisher(
                    eventStoreDBClient.readStreamReactive(
                        streamId,
                        ReadStreamOptions.get()
                            .forwards()
                            .fromStart()
                            .notResolveLinkTos()
                    )
                )
                .reduce(AggregateContext(aggregateId, streamId, applicator)) { aggregate, message ->
                    handleMessage(aggregate, message)
                }
                .blockingGet()
                .let { toAggregateReadResult(it.state, it.lastStreamPosition, streamId) }
        } catch (e: Exception) {
            when (e) {
                is StreamNotFoundException -> AggregateReadResult.NonExistingAggregate
                is CompletionException ->
                    when (e.cause) {
                        is StreamNotFoundException -> AggregateReadResult.NonExistingAggregate
                        else -> throw e.cause ?: e
                    }
                else -> throw e
            }
        }
    }

    private fun  handleMessage(context: AggregateContext, message: ReadMessage): AggregateContext {
        log.trace { "handleMessage: ${message.toLogString()}" }
        return if (message.hasEvent()) {
            context.apply {
                state = handleEvent(context, message.event)
                lastStreamPosition = message.event?.event?.revision ?: context.lastStreamPosition
            }
        } else {
            log.debug { "Message does not have event (context: $context)" }
            context
        }
    }

    private fun  handleEvent(context: AggregateContext, event: ResolvedEvent): A? =
        if (!event.isIgnorable()) {
            handleEvent(context, event.event)
        } else {
            context.state
        }

    private fun  handleEvent(context: AggregateContext, event: RecordedEvent): A? {
        val metadata = getMetadata(event)
        val eventData = EventUpgrader.upgrade(serdes.deserialize(event.eventData, event.eventType))
        return context.applicator.invoke(
            context.state,
            EventWrapper(
                Event(
                    aggregateId = context.aggregateId,
                    eventData = eventData,
                    metadata = metadata
                ),
                eventNumber = event.revision,
                serializationId = serdes.getSerializationId(eventData::class)
            )
        )
    }

    private fun getMetadata(event: RecordedEvent) =
        if (event.userMetadata.isNotEmpty() && metadataSerdes != null)
            metadataSerdes.deserialize(event.userMetadata)
        else
            null

    private fun ReadMessage.toLogString() = "ReadMessage(" +
            "firstStreamPosition=${if (hasFirstStreamPosition()) firstStreamPosition else null}, " +
            "lastStreamPosition=${if (hasLastStreamPosition()) lastStreamPosition else null}, " +
            "lastAllPosition=${if (hasLastAllPosition()) lastStreamPosition else null}, " +
            "event=${if (hasEvent()) event else null}" +
            ")"

    private fun  toAggregateReadResult(state: A?, lastStreamPosition: Long?, streamId: String): AggregateReadResult {
        return if (lastStreamPosition != null) {
            toAggregateReadResult(state, lastStreamPosition)
        } else {
            // When the aggregate stream has no events
            throw RuntimeException("Error reading ${streamId}, the stream exists but does not contain any events")
        }
    }

    private fun  toAggregateReadResult(state: A?, lastStreamPosition: Long): AggregateReadResult {
        return if (state == null) {
            // When the aggregate stream has events, but applying these did not lead to a initialized state
            AggregateReadResult.UninitializedAggregate(lastStreamPosition)
        } else {
            // When the aggregate stream has events, and applying these has lead to a initialized state
            AggregateReadResult.InitializedAggregate(state, lastStreamPosition)
        }
    }
}

private data class AggregateContext(
    val aggregateId: UUID,
    val streamId: String,
    val applicator: (state: A?, event: EventWrapper<*>) -> A?,
    var state: A? = null,
    var lastStreamPosition: Long? = null,
)





© 2015 - 2025 Weber Informatics LLC | Privacy Policy