
com.github.jasync.sql.db.pool.ActorBasedObjectPool.kt Maven / Gradle / Ivy
package com.github.jasync.sql.db.pool
import com.github.jasync.sql.db.util.Failure
import com.github.jasync.sql.db.util.Success
import com.github.jasync.sql.db.util.Try
import com.github.jasync.sql.db.util.XXX
import com.github.jasync.sql.db.util.failed
import com.github.jasync.sql.db.util.map
import com.github.jasync.sql.db.util.mapTry
import com.github.jasync.sql.db.util.onComplete
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import mu.KotlinLogging
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlin.coroutines.CoroutineContext
private val logger = KotlinLogging.logger {}
// consider ticker channel when its stable
// https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/ticker.html
// https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md#ticker-channels
object TestConnectionScheduler {
private val executor: ScheduledExecutorService by lazy {
Executors.newSingleThreadScheduledExecutor { r ->
val t = Executors.defaultThreadFactory().newThread(r)
t.isDaemon = true
t
}
}
fun scheduleAtFixedRate(periodMillis: Long, task: () -> Unit): ScheduledFuture<*> {
return executor.scheduleAtFixedRate(task, periodMillis, periodMillis, TimeUnit.MILLISECONDS)
}
}
@Suppress("EXPERIMENTAL_API_USAGE")
class ActorBasedObjectPool
internal constructor(
objectFactory: ObjectFactory,
configuration: PoolConfiguration,
testItemsPeriodically: Boolean,
extraTimeForTimeoutCompletion: Long = TimeUnit.SECONDS.toMillis(30)
) : AsyncObjectPool, CoroutineScope {
@Suppress("unused", "RedundantVisibilityModifier")
public constructor(
objectFactory: ObjectFactory,
configuration: PoolConfiguration
) : this(objectFactory, configuration, true)
private val job = SupervisorJob() + configuration.coroutineDispatcher
override val coroutineContext: CoroutineContext get() = job
var closed = false
private var testItemsFuture: ScheduledFuture<*>? = null
init {
if (testItemsPeriodically) {
logger.info { "registering pool for periodic connection tests $this - $configuration" }
testItemsFuture = TestConnectionScheduler.scheduleAtFixedRate(configuration.validationInterval) {
try {
testAvailableItems()
} catch (t: Throwable) {
logger.debug(t) { "got exception when testing items" }
}
}
}
}
override fun take(): CompletableFuture {
if (closed) {
throw PoolAlreadyTerminatedException()
}
val future = CompletableFuture()
val offered = actor.offer(Take(future))
if (!offered) {
future.completeExceptionally(Exception("could not offer to actor"))
}
return future
}
override fun giveBack(item: T): CompletableFuture> {
val future = CompletableFuture()
val offered = actor.offer(GiveBack(item, future))
if (!offered) {
future.completeExceptionally(Exception("could not offer to actor"))
}
return future.map { this }
}
override fun close(): CompletableFuture> {
if (closed) {
return CompletableFuture.completedFuture(this)
}
logger.info { "closing the pool" }
closed = true
val future = CompletableFuture()
val offered = actor.offer(Close(future))
if (!offered) {
future.completeExceptionally(Exception("could not offer to actor"))
}
testItemsFuture?.cancel(true)
return future.map {
job.cancel()
this
}
}
fun testAvailableItems() {
if (closed) {
logger.trace { "testAvailableItems - not working, pool is closed" }
return
}
logger.trace { "testAvailableItems - starting" }
val offered = actor.offer(TestAvailableItems())
if (!offered) {
logger.warn { "failed to offer to actor - testAvailableItems()" }
}
}
private val actorInstance = ObjectPoolActor(objectFactory, configuration, extraTimeForTimeoutCompletion) { actor }
private val actor: SendChannel> = actor(
context = configuration.coroutineDispatcher,
capacity = Int.MAX_VALUE,
start = CoroutineStart.DEFAULT,
onCompletion = null
) {
for (message in channel) {
try {
actorInstance.onReceive(message)
} catch (t: Throwable) {
logger.warn(t) { "uncaught Throwable" }
}
}
}
val availableItems: List get() = actorInstance.availableItemsList
val usedItems: List get() = actorInstance.usedItemsList
val waitingForItem: List> get() = actorInstance.waitingForItemList
val usedItemsSize: Int get() = actorInstance.usedItemsSize
val waitingForItemSize: Int get() = actorInstance.waitingForItemSize
val availableItemsSize: Int get() = actorInstance.availableItemsSize
}
@Suppress("unused")
private sealed class ActorObjectPoolMessage {
override fun toString(): String {
return "${javaClass.simpleName} @${hashCode()}"
}
}
private class Take(val future: CompletableFuture) : ActorObjectPoolMessage()
private class GiveBack(
val returnedItem: T,
val future: CompletableFuture,
val exception: Throwable? = null,
val originalTime: Long? = null
) : ActorObjectPoolMessage() {
override fun toString(): String {
return "GiveBack: ${returnedItem.id} hasError=" +
if (exception != null)
"${exception.javaClass.simpleName} - ${exception.message}"
else "false"
}
}
private class Created(
val itemCreateId: Int,
val item: Try,
val takeAskFuture: CompletableFuture?
) : ActorObjectPoolMessage() {
override fun toString(): String {
val id = when (item) {
is Success -> item.value.id
else -> "failed"
}
return "Created: createRequest=$itemCreateId -> object=$id"
}
}
private class TestAvailableItems : ActorObjectPoolMessage()
private class Close(val future: CompletableFuture) : ActorObjectPoolMessage()
@Suppress("REDUNDANT_ELSE_IN_WHEN")
private class ObjectPoolActor(
private val objectFactory: ObjectFactory,
private val configuration: PoolConfiguration,
private val extraTimeForTimeoutCompletion: Long,
private val channelProvider: () -> SendChannel>
) {
private val availableItems: Queue> = LinkedList>()
private val waitingQueue: Queue> = LinkedList>()
private val inUseItems = WeakHashMap>()
private val inCreateItems = mutableMapOf>>()
private var createIndex = 0
private val channel: SendChannel> by lazy { channelProvider() }
val availableItemsList: List get() = availableItems.map { it.item }
val usedItemsList: List get() = inUseItems.keys.toList()
val waitingForItemList: List> get() = waitingQueue.toList()
val usedItemsSize: Int get() = inUseItems.size
val waitingForItemSize: Int get() = waitingQueue.size
val availableItemsSize: Int get() = availableItems.size
var closed = false
fun onReceive(message: ActorObjectPoolMessage) {
logger.trace { "received message: $message ; $poolStatusString" }
when (message) {
is Take -> handleTake(message)
is GiveBack -> handleGiveBack(message)
is Created -> handleCreated(message)
is TestAvailableItems -> handleTestAvailableItems()
is Close -> handleClose(message)
else -> XXX("no handle for message $message")
}
scheduleNewItemsIfNeeded()
}
private fun scheduleNewItemsIfNeeded() {
logger.trace { "scheduleNewItemsIfNeeded - $poolStatusString" }
// deal with inconsistency in case we have items but also waiting futures
while (availableItems.size > 0 && waitingQueue.isNotEmpty()) {
val future = waitingQueue.peek()
val wasBorrowed = borrowFirstAvailableItem(future)
if (wasBorrowed) {
waitingQueue.remove()
logger.trace { "scheduleNewItemsIfNeeded - borrowed object ; $poolStatusString" }
return
}
}
// deal with inconsistency in case we have waiting futures, and but we can create new items for them
while (availableItems.isEmpty()
&& waitingQueue.isNotEmpty()
&& totalItems < configuration.maxObjects
&& waitingQueue.size > inCreateItems.size
) {
createObject(null)
logger.trace { "scheduleNewItemsIfNeeded - creating new object ; $poolStatusString" }
}
}
private val poolStatusString: String
get() =
"availableItems=${availableItems.size} waitingQueue=${waitingQueue.size} inUseItems=${inUseItems.size} inCreateItems=${inCreateItems.size} ${this.channel}"
private fun handleClose(message: Close) {
try {
closed = true
channel.close()
availableItems.forEach { it.item.destroy() }
availableItems.clear()
inUseItems.forEach {
it.value.cleanedByPool = true
it.key.destroy()
}
inUseItems.clear()
waitingQueue.forEach { it.completeExceptionally(PoolAlreadyTerminatedException()) }
waitingQueue.clear()
inCreateItems.values.forEach { it.item.completeExceptionally(PoolAlreadyTerminatedException()) }
inCreateItems.clear()
message.future.complete(Unit)
} catch (e: Exception) {
message.future.completeExceptionally(e)
}
}
private fun handleTestAvailableItems() {
sendAvailableItemsToTest()
checkItemsInCreationForTimeout()
checkItemsInTestOrQueryForTimeout()
logger.trace { "testAvailableItems - done testing" }
}
private fun checkItemsInTestOrQueryForTimeout() {
inUseItems.entries.removeAll { entry ->
val holder = entry.value
val item = entry.key
var timeouted = false
if (holder.isInTest && holder.timeElapsed > configuration.testTimeout) {
logger.trace { "failed to test item ${item.id} after ${holder.timeElapsed} ms, will destroy it" }
holder.cleanedByPool = true
item.destroy()
holder.testFuture!!.completeExceptionally(TimeoutException("failed to test item ${item.id} after ${holder.timeElapsed} ms"))
timeouted = true
}
if (!holder.isInTest && configuration.queryTimeout != null
&& holder.timeElapsed > configuration.queryTimeout + extraTimeForTimeoutCompletion
) {
logger.error { "timeout query item ${item.id} after ${holder.timeElapsed} ms and was not cleaned by connection as it should, will destroy it - timeout is ${configuration.queryTimeout}" }
holder.cleanedByPool = true
item.destroy()
timeouted = true
}
timeouted
}
}
private fun checkItemsInCreationForTimeout() {
inCreateItems.entries.removeAll {
val timeout = it.value.timeElapsed > configuration.createTimeout
if (timeout) {
logger.trace { "failed to create item ${it.key} after ${it.value.timeElapsed} ms" }
it.value.item.completeExceptionally(TimeoutException("failed to create item ${it.key} after ${it.value.timeElapsed} ms"))
}
timeout
}
}
private fun T.destroy() {
logger.trace { "destroy item ${this.id}" }
objectFactory.destroy(this)
}
private fun sendAvailableItemsToTest() {
availableItems.forEach {
val item = it.item
logger.trace { "test: ${item.id} available ${it.timeElapsed} ms" }
when {
it.timeElapsed > configuration.maxIdle -> {
logger.trace { "releasing idle item ${item.id}" }
item.destroy()
}
configuration.maxObjectTtl != null && System.currentTimeMillis() - item.creationTime > configuration.maxObjectTtl -> {
logger.trace { "releasing item past ttl ${item.id}" }
item.destroy()
}
else -> {
val test = objectFactory.test(item)
inUseItems[item] = ItemInUseHolder(item.id, isInTest = true, testFuture = test)
test.mapTry { _, t ->
offerOrLog(GiveBack(item, CompletableFuture(), t, originalTime = it.time)) { "test item" }
}
}
}
}
availableItems.clear()
}
private fun offerOrLog(message: ActorObjectPoolMessage, logMessage: () -> String) {
val offered = channel.offer(message)
if (!offered) {
logger.warn { "failed to offer on ${logMessage()}" }
}
}
private fun handleCreated(message: Created) {
val removed = inCreateItems.remove(message.itemCreateId)
if (removed == null) {
logger.warn { "could not find connection ${message.itemCreateId}" }
}
val future = message.takeAskFuture
if (future == null) {
when (message.item) {
is Failure -> logger.debug { "failed to create connection, with no callback attached " }
is Success -> {
availableItems.add(PoolObjectHolder(message.item.value))
}
}
} else {
when (message.item) {
is Failure -> future.completeExceptionally(message.item.exception)
is Success -> {
try {
message.item.value.borrowTo(future)
} catch (e: Exception) {
future.completeExceptionally(e)
}
}
}
}
}
private fun T.borrowTo(future: CompletableFuture, validate: Boolean = true) {
if (validate) {
validate(this)
}
inUseItems[this] = ItemInUseHolder(this.id, isInTest = false)
logger.trace { "borrowed: ${this.id} ; $poolStatusString" }
future.complete(this)
}
private fun handleGiveBack(message: GiveBack) {
try {
val removed = inUseItems.remove(message.returnedItem)
removed?.apply { cleanedByPool = true }
if (removed == null) {
val isFromOurPool: Boolean = this.availableItems.any { holder -> message.returnedItem === holder.item }
logger.trace { "give back got item not in use: ${message.returnedItem.id} isFromOurPool=$isFromOurPool ; $poolStatusString" }
if (isFromOurPool) {
message.future.failed(IllegalStateException("This item has already been returned"))
} else {
message.future.failed(IllegalArgumentException("The returned item did not come from this pool."))
}
return
}
if (message.exception != null) {
logger.trace { "GiveBack got exception, so destroying item ${message.returnedItem.id}, exception is ${message.exception.javaClass.simpleName} - ${message.exception.message}" }
throw message.exception
}
validate(message.returnedItem)
message.future.complete(Unit)
if (waitingQueue.isEmpty()) {
if (availableItems.any { holder -> message.returnedItem === holder.item }) {
logger.warn { "trying to give back an item to the pool twice ${message.returnedItem.id}, will ignore that" }
return
}
availableItems.add(
when (message.originalTime) {
null -> PoolObjectHolder(message.returnedItem)
else -> PoolObjectHolder(message.returnedItem, message.originalTime)
}
)
logger.trace { "add ${message.returnedItem.id} to available items, size is ${availableItems.size}" }
} else {
val waitingFuture = waitingQueue.remove()
message.returnedItem.borrowTo(waitingFuture, validate = false)
}
} catch (e: Throwable) {
logger.trace(e) { "GiveBack caught exception, so destroying item ${message.returnedItem.id} " }
try {
message.returnedItem.destroy()
} catch (e1: Throwable) {
logger.trace(e1) { "GiveBack caught exception, destroy also caught exception ${message.returnedItem.id} " }
}
message.future.completeExceptionally(e)
}
}
private fun handleTake(message: Take) {
//take from available
while (availableItems.isNotEmpty()) {
val future = message.future
val wasBorrowed = borrowFirstAvailableItem(future)
if (wasBorrowed)
return
}
// available is empty
createNewItemPutInWaitQueue(message)
}
private fun borrowFirstAvailableItem(future: CompletableFuture): Boolean {
val itemHolder = availableItems.remove()
try {
validateTtl(itemHolder.item)
itemHolder.item.borrowTo(future)
return true
} catch (e: Exception) {
logger.debug { "validation of object '${itemHolder.item.id}' failed, removing it from pool: ${e.message}" }
itemHolder.item.destroy()
}
return false
}
private fun validateTtl(item: T) {
val age = System.currentTimeMillis() - item.creationTime
if (configuration.maxObjectTtl != null && age > configuration.maxObjectTtl) {
throw MaxTtlPassedException(item.id, age, configuration.maxObjectTtl)
}
}
private val totalItems: Int get() = inUseItems.size + inCreateItems.size + availableItems.size
private fun createNewItemPutInWaitQueue(message: Take) {
try {
if (totalItems < configuration.maxObjects) {
createObject(message.future)
} else {
if (waitingQueue.size < configuration.maxQueueSize) {
waitingQueue.add(message.future)
logger.trace { "no items available (${inUseItems.size} used), added to waiting queue (${waitingQueue.size} waiting)" }
} else {
logger.trace { "no items available (${inUseItems.size} used), and the waitQueue is full (${waitingQueue.size} waiting)" }
message.future.completeExceptionally(PoolExhaustedException("There are no objects available and the waitQueue is full"))
}
}
} catch (e: Exception) {
message.future.completeExceptionally(e)
}
}
private fun createObject(future: CompletableFuture?) {
val created = objectFactory.create()
val itemCreateId = createIndex
createIndex++
inCreateItems[itemCreateId] = ObjectHolder(created)
logger.trace { "createObject createRequest=$itemCreateId" }
created.onComplete { tried ->
offerOrLog(Created(itemCreateId, tried, future)) {
"failed to offer on created item $itemCreateId"
}
}
}
private fun validate(item: T) {
val tried = objectFactory.validate(item)
when (tried) {
is Failure -> throw tried.exception
}
}
}
private open class PoolObjectHolder(val item: T, val time: Long = System.currentTimeMillis()) {
val timeElapsed: Long get() = System.currentTimeMillis() - time
}
private class ObjectHolder(val item: T) {
val time = System.currentTimeMillis()
val timeElapsed: Long get() = System.currentTimeMillis() - time
}
private data class ItemInUseHolder(
val itemId: String,
val isInTest: Boolean,
val testFuture: CompletableFuture? = null,
val time: Long = System.currentTimeMillis(),
var cleanedByPool: Boolean = false
) {
val timeElapsed: Long get() = System.currentTimeMillis() - time
@Suppress("unused", "ProtectedInFinal")
protected fun finalize() {
if (!cleanedByPool) {
logger.warn { "LEAK DETECTED for item $this - $timeElapsed ms since in use" }
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy