main.io.github.bluegroundltd.outbox.TransactionalOutboxImpl.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of transactional-outbox-core Show documentation
Show all versions of transactional-outbox-core Show documentation
Easily implement the transactional outbox pattern in your JVM application
package io.github.bluegroundltd.outbox
import io.github.bluegroundltd.outbox.event.InstantOutboxEvent
import io.github.bluegroundltd.outbox.event.InstantOutboxPublisher
import io.github.bluegroundltd.outbox.item.OutboxItem
import io.github.bluegroundltd.outbox.item.OutboxPayload
import io.github.bluegroundltd.outbox.item.OutboxStatus
import io.github.bluegroundltd.outbox.item.OutboxType
import io.github.bluegroundltd.outbox.item.factory.OutboxItemFactory
import io.github.bluegroundltd.outbox.store.OutboxFilter
import io.github.bluegroundltd.outbox.store.OutboxStore
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.EnumSet
import java.util.concurrent.ExecutorService
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@SuppressWarnings("LongParameterList", "TooGenericExceptionCaught")
internal class TransactionalOutboxImpl(
private val clock: Clock,
private val outboxHandlers: Map,
private val monitorLocksProvider: OutboxLocksProvider,
private val cleanupLocksProvider: OutboxLocksProvider,
private val outboxStore: OutboxStore,
private val instantOutboxPublisher: InstantOutboxPublisher,
private val outboxItemFactory: OutboxItemFactory,
private val rerunAfterDuration: Duration,
private val executor: ExecutorService,
private val decorators: List = emptyList(),
private val threadPoolTimeOut: Duration
) : TransactionalOutbox {
private var inShutdownMode = AtomicBoolean(false)
companion object {
private const val LOGGER_PREFIX = "[OUTBOX]"
private val logger: Logger = LoggerFactory.getLogger(TransactionalOutboxImpl::class.java)
private val STATUSES_ELIGIBLE_FOR_PROCESSING = EnumSet.of(OutboxStatus.PENDING, OutboxStatus.RUNNING)
}
override fun add(type: OutboxType, payload: OutboxPayload, shouldPublishAfterInsertion: Boolean) {
logger.info("$LOGGER_PREFIX Adding item of type: ${type.getType()} and payload: $payload")
when {
shouldPublishAfterInsertion -> outboxItemFactory.makeInstantOutbox(type, payload)
else -> outboxItemFactory.makeScheduledOutboxItem(type, payload)
}.run { outboxStore.insert(this) }
.takeIf { shouldPublishAfterInsertion }
?.let { instantOutboxPublisher.publish(InstantOutboxEvent(outbox = it)) }
}
override fun processInstantOutbox(outbox: OutboxItem) {
runCatching {
logger.info("$LOGGER_PREFIX Instant processing of \"${outbox.type.getType()}\" outbox")
executor.execute(
decorate(OutboxItemProcessor(outbox, outboxHandlers[outbox.type]!!, outboxStore, clock))
)
}.onFailure {
logger.error("$LOGGER_PREFIX Failure in instant handling", it)
}
}
override fun monitor() {
if (inShutdownMode.get()) {
logger.info("$LOGGER_PREFIX Shutdown in process, no longer accepting items for processing")
return
}
runCatching {
monitorLocksProvider.acquire()
val items = fetchEligibleItems()
if (items.isEmpty()) {
logger.info("$LOGGER_PREFIX No outbox items to process")
} else {
logger.info("$LOGGER_PREFIX Will process ${items.size} outbox items")
}
markForProcessing(items)
.map { outboxStore.update(it) }
.forEach { processItem(it) }
}.onFailure {
logger.error("$LOGGER_PREFIX Failure in monitor", it)
}
runCatching { monitorLocksProvider.release() }.onFailure {
logger.error("$LOGGER_PREFIX Failed to release lock of $monitorLocksProvider", it)
}
}
private fun fetchEligibleItems(): List {
val (eligibleItems, erroneouslyFetchedItems) = outboxStore
.fetch(OutboxFilter(Instant.now(clock)))
.partition { it.status in STATUSES_ELIGIBLE_FOR_PROCESSING }
erroneouslyFetchedItems.forEach {
logger.warn(
"$LOGGER_PREFIX Outbox item with id ${it.id} erroneously fetched, as its status is ${it.status}. " +
"Expected status to be one of $STATUSES_ELIGIBLE_FOR_PROCESSING"
)
}
return eligibleItems
}
private fun markForProcessing(items: List): List {
val now = Instant.now(clock)
return items.map {
it.copy(
status = OutboxStatus.RUNNING,
lastExecution = now,
rerunAfter = now.plus(rerunAfterDuration)
)
}
}
private fun processItem(item: OutboxItem) {
val processor = decorate(OutboxItemProcessor(item, outboxHandlers[item.type]!!, outboxStore, clock))
try {
executor.execute(processor)
} catch (exception: RejectedExecutionException) {
revertToPending(item)
outboxStore.update(item)
}
}
private fun decorate(processor: OutboxItemProcessor) =
decorators.fold(processor as Runnable) { decorated, decorator ->
decorator.decorate(decorated)
}
private fun revertToPending(item: OutboxItem) {
logger.info("$LOGGER_PREFIX Outbox item with id ${item.id} is reverting to PENDING")
item.status = OutboxStatus.PENDING
item.nextRun = Instant.now(clock)
item.rerunAfter = null
}
override fun shutdown() {
if (!inShutdownMode.compareAndSet(false, true)) {
logger.info("$LOGGER_PREFIX Outbox shutdown already in progress")
return
}
logger.info("$LOGGER_PREFIX Shutting down the outbox")
executor.shutdown()
val notExecutedRunnables =
try {
if (!executor.awaitTermination(threadPoolTimeOut.toSeconds(), TimeUnit.SECONDS)) {
logger.debug("$LOGGER_PREFIX Forcing outbox shutdown")
executor.shutdownNow()
} else {
logger.debug("$LOGGER_PREFIX All tasks executed")
emptyList()
}
} catch (exception: Exception) {
logger.warn("$LOGGER_PREFIX Shutdown failed.", exception)
throw exception
}
notExecutedRunnables.filterIsInstance().forEach {
val item = it.getItem()
revertToPending(item)
outboxStore.update(item)
}
logger.info("$LOGGER_PREFIX Outbox shutdown completed")
}
override fun cleanup() {
if (inShutdownMode.get()) {
logger.info("$LOGGER_PREFIX Shutdown in process, deferring cleanup")
return
}
var wasLockingAcquired = false
try {
cleanupLocksProvider.acquire()
wasLockingAcquired = true
val now = Instant.now(clock)
logger.info("$LOGGER_PREFIX Cleaning up completed outbox items, with deleteAfter <= $now")
outboxStore.deleteCompletedItems(now)
} catch (exception: Exception) {
logger.error("$LOGGER_PREFIX Failure in cleanup", exception)
} finally {
if (wasLockingAcquired) {
try {
cleanupLocksProvider.release()
} catch (exception: Exception) {
logger.error("$LOGGER_PREFIX Failed to release cleanup lock ($cleanupLocksProvider)", exception)
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy