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

net.bjoernpetersen.musicbot.internal.loader.DefaultResourceCache.kt Maven / Gradle / Ivy

There is a newer version: 0.25.0
Show newest version
package net.bjoernpetersen.musicbot.internal.loader

import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import mu.KotlinLogging
import net.bjoernpetersen.musicbot.api.player.Song
import net.bjoernpetersen.musicbot.spi.loader.Resource
import net.bjoernpetersen.musicbot.spi.loader.ResourceCache
import net.bjoernpetersen.musicbot.spi.loader.SongLoader
import net.bjoernpetersen.musicbot.spi.plugin.PluginLookup
import java.time.Duration
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext

internal class DefaultResourceCache @Inject private constructor(
    loader: CacheSongLoader
) : ResourceCache, CoroutineScope {

    private val logger = KotlinLogging.logger {}

    private var isClosed = false
    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + job

    private val cleanupJob = SupervisorJob(job)
    private val cleanupScope =
        CoroutineScope(coroutineContext + cleanupJob + CoroutineExceptionHandler { _, throwable ->
            logger.error(throwable) { "Exception during cleanup" }
        })

    @Suppress("EXPERIMENTAL_API_USAGE")
    private val cache = CacheBuilder.newBuilder()
        .maximumSize(128)
        .expireAfterAccess(1, TimeUnit.HOURS)
        .removalListener> {
            cleanupScope.launch {
                val deferred = it.value
                if ((deferred.isCompleted || deferred.isActive) && !deferred.isCancelled) {
                    deferred.join()
                    if (deferred.getCompletionExceptionOrNull() == null) {
                        val resource = deferred.getCompleted()
                        resource.free()
                    }
                }
            }
        }
        .build(loader)

    override suspend fun get(song: Song): Resource {
        if (isClosed) throw IllegalStateException("ResourceCache is closed")
        return coroutineScope {
            withContext(coroutineContext) {
                val asyncResource = cache[song]
                val resource = asyncResource.await()
                if (resource.isValid)
                    resource
                else {
                    cache.invalidate(song)
                    cache[song].await()
                }
            }
        }
    }

    override suspend fun close() {
        isClosed = true

        cache.invalidateAll()
        cache.cleanUp()

        coroutineScope {
            withContext(coroutineContext) {
                logger.info { "Waiting for resource cache to clean up. This may take up to a minute." }
                withTimeout(Duration.ofMinutes(1).toMillis()) {
                    try {
                        cleanupJob.children.forEach { it.join() }
                    } catch (e: TimeoutCancellationException) {
                        logger.warn { "Resource cache clean up timed out after one minute." }
                    }
                }
            }
        }

        job.cancel()

        logger.info { "Resource cache closed." }
    }
}

private class CacheSongLoader @Inject constructor(
    private val songLoader: SongLoader,
    private val pluginLookup: PluginLookup
) : CacheLoader>(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO

    @Suppress("DeferredIsResult")
    override fun load(key: Song): Deferred {
        return async(start = CoroutineStart.LAZY) {
            val provider = pluginLookup.lookup(key.provider)!!
            songLoader.load(provider, key)
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy