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

no.ks.kes.lib.CmdHandler.kt Maven / Gradle / Ivy

package no.ks.kes.lib

import mu.KotlinLogging
import java.time.Instant
import kotlin.reflect.KClass

private val log = KotlinLogging.logger {}

@Suppress("UNCHECKED_CAST")
abstract class CmdHandler(private val repository: AggregateRepository, aggregateConfiguration: AggregateConfiguration) {

    protected val applicators = mutableMapOf>, (a: A, c: Cmd) -> Result>()
    protected val initializers = mutableMapOf>, (c: Cmd) -> Result>()
    private val validatedAggregateConfiguration = aggregateConfiguration.getConfiguration { repository.getSerializationId(it) }


    protected inline fun > apply(crossinline handler: A.(C) -> Result) {
        check(!applicators.containsKey(C::class as KClass>)) { "There are multiple \"apply\" configurations for the command ${C::class.simpleName} in the command handler ${this::class.simpleName}, only a single \"apply\" handler is allowed for each command" }
        applicators[C::class as KClass>] = { a, c -> handler.invoke(a, c as C) }
    }

    protected inline fun > init(crossinline handler: (C) -> Result) {
        check(!initializers.containsKey(C::class as KClass>)) { "There are multiple \"init\" configurations for the command ${C::class.simpleName} in the command handler ${this::class.simpleName}, only a single \"init\" handler is allowed for each command" }
        initializers[C::class as KClass>] = { c -> handler.invoke(c as C) }
    }

    fun handledCmds(): Set>> = applicators.keys.toSet() + initializers.keys.toSet()

    /**
     * Handler that is synchronized meaning only one thread may issue a command at the time
     */
    @Synchronized
    fun handle(cmd: Cmd): A = handleUnsynchronized(cmd)

    /**
     * Handler that is not synchronized. To be used in cases where handling multiple commands from several threads simultaneously is safe. Use with care
     */
    fun handleUnsynchronized(cmd: Cmd): A {
        val readResult = readAggregate(cmd)

        return when (val result = invokeHandler(cmd, readResult)) {
            is Result.Fail,
            is Result.RetryOrFail,
            is Result.Error ->
                throw result.exception!!
            is Result.Succeed -> {
                appendDerivedEvents(validatedAggregateConfiguration.aggregateType, readResult, cmd, result.derivedEventWrappers)
                result.derivedEventWrappers.fold(
                        when (readResult) {
                            is AggregateReadResult.InitializedAggregate<*> -> readResult.aggregateState as A
                            is AggregateReadResult.NonExistingAggregate, is AggregateReadResult.UninitializedAggregate -> null
                        }
                ) { a, e ->
                    validatedAggregateConfiguration.applyEvent(
                        EventWrapper(
                            event = e,
                            eventNumber = -1,
                            serializationId = repository.getSerializationId(e.eventData::class as KClass>)
                        ), a
                    )
                }
                    ?: error("applying derived events to the aggregate resulted in a null-state!")
            }
        }
    }


    @Synchronized
    fun handleAsync(cmd: Cmd<*>, retryNumber: Int): AsyncResult {
        val readResult = readAggregate(cmd as Cmd)

        return when (val result = invokeHandler(cmd, readResult)) {
            is Result.Fail -> {
                appendDerivedEvents(validatedAggregateConfiguration.aggregateType, readResult, cmd, result.eventWrappers)
                log.error("execution of ${cmd::class.simpleName} failed permanently: $cmd", result.exception!!); AsyncResult.Fail
            }
            is Result.RetryOrFail -> {
                val nextExecution = result.retryStrategy.invoke(retryNumber)
                log.error("execution of ${cmd::class.simpleName} failed with retry, ${
                    nextExecution?.let { "next retry at $it" }
                            ?: " but all retries are exhausted"
                }", result.exception!!)
                if (nextExecution == null) {
                    appendDerivedEvents(validatedAggregateConfiguration.aggregateType, readResult, cmd, result.eventWrappers)
                    AsyncResult.Fail
                } else {
                    AsyncResult.Retry(nextExecution)
                }
            }
            is Result.Succeed -> {
                appendDerivedEvents(validatedAggregateConfiguration.aggregateType, readResult, cmd, result.derivedEventWrappers)
                AsyncResult.Success
            }
            is Result.Error -> {
                AsyncResult.Error(result.exception!!)
            }
        }
    }

    private fun appendDerivedEvents(aggregateType: String, readResult: AggregateReadResult, cmd: Cmd, eventWrappers: List>>) {
        if (eventWrappers.isNotEmpty())
            repository.append(aggregateType, cmd.aggregateId, resolveExpectedEventNumber(readResult, cmd.useOptimisticLocking()), eventWrappers)
    }

    private fun resolveExpectedEventNumber(readResult: AggregateReadResult, useOptimisticLocking: Boolean): ExpectedEventNumber =
            when (readResult) {
                is AggregateReadResult.NonExistingAggregate -> ExpectedEventNumber.AggregateDoesNotExist
                is AggregateReadResult.InitializedAggregate<*> -> if (useOptimisticLocking) ExpectedEventNumber.Exact(readResult.eventNumber) else ExpectedEventNumber.AggregateExists
                is AggregateReadResult.UninitializedAggregate -> if (useOptimisticLocking) ExpectedEventNumber.Exact(readResult.eventNumber) else ExpectedEventNumber.AggregateExists
            }

    private fun readAggregate(cmd: Cmd): AggregateReadResult =
            repository.read(cmd.aggregateId, validatedAggregateConfiguration)

    private fun invokeHandler(cmd: Cmd, readResult: AggregateReadResult): Result =
            when (readResult) {
                is AggregateReadResult.NonExistingAggregate, is AggregateReadResult.UninitializedAggregate -> {
                    initializers[cmd::class]
                            ?.run {
                                try {
                                    invoke(cmd)
                                } catch (e: Exception) {
                                    Result.Error(e)
                                }
                            }
                            ?: error("Aggregate ${cmd.aggregateId} does not exist, and cmd ${cmd::class.simpleName} is not configured as an initializer. Consider adding an \"init\" configuration for this command.")
                }
                is AggregateReadResult.InitializedAggregate<*> -> {
                    applicators[cmd::class]
                            ?.run {
                                try {
                                    invoke(readResult.aggregateState as A, cmd)
                                } catch (e: Exception) {
                                    Result.Error(e)
                                }
                            }
                            ?: error("No handler found for cmd ${cmd::class.simpleName}")
                }
            }


    sealed class Result(val exception: Exception?) {

        class Fail private constructor(exception: Exception, val eventWrappers: List>>) : Result(exception) {
            constructor(eventWrapper: Event>, exception: Exception) : this(exception, listOf(eventWrapper))
            constructor(eventWrappers: List>>, exception: Exception) : this(exception, eventWrappers)
            constructor(exception: Exception) : this(exception, emptyList())
        }

        class RetryOrFail private constructor(exception: Exception, val eventWrappers: List>>, val retryStrategy: (Int) -> Instant?) : Result(exception) {
            constructor(eventWrapper: Event>, exception: Exception, retryStrategy: (Int) -> Instant?) : this(exception, listOf(eventWrapper), retryStrategy)
            constructor(eventWrappers: List>>, exception: Exception, retryStrategy: (Int) -> Instant?) : this(exception, eventWrappers, retryStrategy)
            constructor(exception: Exception, retryStrategy: (Int) -> Instant?) : this(exception, emptyList(), retryStrategy)
            constructor(eventWrapper: Event>, exception: Exception) : this(exception, listOf(eventWrapper), RetryStrategies.DEFAULT)
            constructor(eventWrappers: List>>, exception: Exception) : this(exception, eventWrappers, RetryStrategies.DEFAULT)
            constructor(exception: Exception) : this(exception, emptyList(), RetryStrategies.DEFAULT)
        }

        internal class Error(exception: Exception) : Result(exception)

        class Succeed(val derivedEventWrappers: List>>) : Result(null) {
            constructor(event: Event>) : this(listOf(event))
            constructor() : this(emptyList())
        }
    }

    sealed class AsyncResult {
        object Success : AsyncResult()
        object Fail : AsyncResult()
        class Retry(val nextExecution: Instant) : AsyncResult()
        class Error(val exception: Exception) : AsyncResult()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy