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

com.cultureamp.eventsourcing.RelationalDatabaseEventStore.kt Maven / Gradle / Ivy

The newest version!
package com.cultureamp.eventsourcing

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
import com.fasterxml.jackson.datatype.joda.JodaModule
import com.fasterxml.jackson.module.kotlin.SingletonSupport
import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.jodatime.datetime
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.vendors.H2Dialect
import org.jetbrains.exposed.sql.vendors.PostgreSQLDialect
import java.sql.SQLException
import java.util.UUID
import kotlin.reflect.KClass

val defaultObjectMapper = ObjectMapper()
    .registerModule(kotlinModule { singletonSupport(SingletonSupport.CANONICALIZE) })
    .registerModule(JodaModule())
    .configure(WRITE_DATES_AS_TIMESTAMPS, false)
    .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)

val defaultEventsTableName = "events"
val defaultEventTypeResolver = CanonicalNameEventTypeResolver

class RelationalDatabaseEventStore @PublishedApi internal constructor(
    private val db: Database,
    val events: Events,
    val eventsSequenceStats: EventsSequenceStats?,
    private val metadataClass: Class,
    private val objectMapper: ObjectMapper,
    private val eventTypeResolver: EventTypeResolver,
    private val blockingLockUntilTransactionEnd: Transaction.() -> CommandError? = { null },
    private val afterSinkHook: (List>) -> Unit = { },
    private val eventsSinkTable: Events = events,
) : EventStore {

    companion object {
        inline fun  create(
            db: Database,
            objectMapper: ObjectMapper = defaultObjectMapper,
            eventsTableName: String = defaultEventsTableName,
            noinline afterSinkHook: (List>) -> Unit = { },
            eventTypeResolver: EventTypeResolver = defaultEventTypeResolver,
            eventsSequenceStats: EventsSequenceStats? = RelationalDatabaseEventsSequenceStats(db, eventTypeResolver, defaultEventsSequenceStatsTableName).also { it.createSchemaIfNotExists() },
        ): RelationalDatabaseEventStore {
            val jsonb: Table.(String) -> Column = when (db.dialect) {
                is H2Dialect -> Table::text
                is PostgreSQLDialect -> Table::jsonb
                else -> throw UnsupportedOperationException("${db.dialect} not currently supported")
            }
            val blockingLockUntilTransactionEnd: Transaction.() -> CommandError? = when (db.dialect) {
                is H2Dialect -> { { null } }
                is PostgreSQLDialect -> Transaction::pgAdvisoryXactLock
                else -> throw UnsupportedOperationException("${db.dialect} not currently supported")
            }
            return RelationalDatabaseEventStore(db, Events(eventsTableName, jsonb), eventsSequenceStats, M::class.java, objectMapper, eventTypeResolver, blockingLockUntilTransactionEnd, afterSinkHook)
        }
    }

    fun createSchemaIfNotExists() {
        transaction(db) {
            SchemaUtils.create(events, eventsSinkTable)
        }
    }

    override fun sink(newEvents: List>, aggregateId: UUID): Either {
        val sunkSequencedEvents = try {
            transaction(db) {
                blockingLockUntilTransactionEnd()?.let { Left(it) } ?: run {
                    newEvents.map { event ->
                        val body = objectMapper.writeValueAsString(event.domainEvent)
                        val domainEventClass = event.domainEvent.javaClass
                        val metadata = objectMapper.writeValueAsString(event.metadata)
                        validateSerialization(domainEventClass, body, metadata)
                        val eventType = eventTypeResolver.serialize(domainEventClass)
                        val insertResult = eventsSinkTable.insert { row ->
                            row[eventsSinkTable.aggregateSequence] = event.aggregateSequence
                            row[eventsSinkTable.eventId] = event.id
                            row[eventsSinkTable.aggregateId] = aggregateId
                            row[eventsSinkTable.aggregateType] = event.aggregateType
                            row[eventsSinkTable.eventType] = eventType
                            row[eventsSinkTable.createdAt] = event.createdAt
                            row[eventsSinkTable.body] = body
                            row[eventsSinkTable.metadata] = metadata
                        }
                        val dryRunMode = (events != eventsSinkTable)
                        if (dryRunMode) {
                            SequencedEvent(event, -1)
                        } else {
                            val insertedSequence = insertResult[eventsSinkTable.sequence]
                            eventsSequenceStats?.save(event.domainEvent::class, insertedSequence)
                            SequencedEvent(event, insertedSequence)
                        }
                    }.let { Right(it) }
                }
            }
        } catch (e: ExposedSQLException) {
            if (e.message.orEmpty().contains("violates unique constraint") || e.message.orEmpty().contains("Unique index or primary key violation")) {
                Left(ConcurrencyError)
            } else {
                throw e
            }
        }
        return sunkSequencedEvents.map {
            afterSinkHook(it)
            it.last().sequence
        }
    }

    private fun validateSerialization(domainEventClass: Class, body: String, metadata: String) {
        // prove that json body can be deserialized, which catches invalid fields types, e.g. interfaces
        try {
            objectMapper.readValue(body, domainEventClass)
        } catch (e: JsonProcessingException) {
            throw EventBodySerializationException(e)
        }

        try {
            objectMapper.readValue(metadata, metadataClass)
        } catch (e: JsonProcessingException) {
            throw EventMetadataSerializationException(e)
        }
    }

    private fun rowToSequencedEvent(row: ResultRow): SequencedEvent = row.let {
        val eventType = eventTypeResolver.deserialize(row[events.aggregateType], row[events.eventType])
        val domainEvent = objectMapper.readValue(row[events.body], eventType)
        val metadata = objectMapper.readValue(row[events.metadata], metadataClass)

        SequencedEvent(
            Event(
                id = row[events.eventId],
                aggregateId = row[events.aggregateId],
                aggregateSequence = row[events.aggregateSequence],
                aggregateType = row[events.aggregateType],
                createdAt = row[events.createdAt],
                metadata = metadata,
                domainEvent = domainEvent,
            ),
            row[events.sequence],
        )
    }

    override fun getAfter(sequence: Long, eventClasses: List>, batchSize: Int): List> {
        return transaction(db) {
            events
                .select {
                    val eventTypeMatches = if (eventClasses.isNotEmpty()) {
                        events.eventType.inList(eventClasses.map { eventTypeResolver.serialize(it.java) })
                    } else {
                        Op.TRUE
                    }
                    events.sequence greater sequence and eventTypeMatches
                }
                .orderBy(events.sequence)
                .limit(batchSize)
                .map(::rowToSequencedEvent)
        }
    }

    override fun eventsFor(aggregateId: UUID): List> {
        return transaction(db) {
            events
                .select { events.aggregateId eq aggregateId }
                .orderBy(events.sequence)
                .map(::rowToSequencedEvent)
                .map { it.event }
        }
    }
}

open class EventDataException(e: Exception) : Throwable(e)
class EventBodySerializationException(e: Exception) : EventDataException(e)
class EventMetadataSerializationException(e: Exception) : EventDataException(e)

internal fun  String.asClass(): Class? {
    @Suppress("UNCHECKED_CAST")
    return Class.forName(this) as Class?
}

class Events(tableName: String = defaultEventsTableName, jsonb: Table.(String) -> Column = Table::jsonb) :
    Table(tableName) {
    val sequence = long("sequence").autoIncrement()
    val eventId = uuid("id")
    val aggregateSequence = long("aggregate_sequence")
    val aggregateId = uuid("aggregate_id")
    val aggregateType = varchar("aggregate_type", 128)
    val eventType = varchar("event_type", 256)
    val createdAt = datetime("created_at")
    val body = jsonb("json_body")
    val metadata = jsonb("metadata")
    override val primaryKey: PrimaryKey = PrimaryKey(sequence)

    init {
        uniqueIndex(eventId)
        uniqueIndex(aggregateId, aggregateSequence)
        nonUniqueIndex(eventType, aggregateType)
    }
}

private fun Table.nonUniqueIndex(vararg columns: Column<*>) = index(false, *columns)

object ConcurrencyError : RetriableError
object LockingError : CommandError

fun Transaction.pgAdvisoryXactLock(): CommandError? {
    val lockTimeoutMilliseconds = 10_000
    try {
        exec("SET LOCAL lock_timeout = '${lockTimeoutMilliseconds}ms';")
        exec("SELECT pg_advisory_xact_lock(-1)")
    } catch (e: SQLException) {
        if (e.message.orEmpty().contains("canceling statement due to lock timeout")) {
            return LockingError
        } else {
            throw e
        }
    }
    return null
}

interface EventTypeResolver {
    fun serialize(domainEventClass: Class): String
    fun deserialize(aggregateType: String, eventType: String): Class
}

object CanonicalNameEventTypeResolver : EventTypeResolver {
    override fun serialize(domainEventClass: Class) = domainEventClass.canonicalName

    override fun deserialize(aggregateType: String, eventType: String) = eventType.asClass()!!
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy