![JAR search and dependency download from the Maven repository](/logo.png)
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