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

commonMain.ExpirationCache.kt Maven / Gradle / Ivy

package opensavvy.cache

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import opensavvy.logger.Logger.Companion.trace
import opensavvy.logger.loggerFor
import opensavvy.progress.done
import opensavvy.state.coroutines.ProgressiveFlow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes

/**
 * Cache layer that expires values from the previous layer after a specified [duration][expireAfter].
 *
 * To add an [ExpirationCache] to a previous layer, use [Cache.expireAfter][Cache.expireAfter]:
 * ```kotlin
 * val cache = Cache.Default
 *      .expireAfter(5.minutes, Job())
 * ```
 */
internal class ExpirationCache(
	/**
	 * The previous cache layer, from which values will be expired.
	 */
	private val upstream: Cache,
	/**
	 * After how much time should the values from the previous cache layer be expired.
	 *
	 * The values will not be expired exactly this time after the last update.
	 * Instead, the implementation guarantees that the data will be expired between [expireAfter] and [expireAfter] * 2.
	 */
	private val expireAfter: Duration = 5.minutes,
	private val clock: Clock,
	/**
	 * The asynchronous context in which the cleaner runs.
	 *
	 * Cancelling this job will cancel the expiration job, after which this cache will stop expiring data.
	 */
	expirationScope: CoroutineScope,
) : Cache {
	private val log = loggerFor(this)

	private val lastUpdate = HashMap()
	private val lock = Mutex()

	init {
		expirationScope.launch(CoroutineName("$this")) {
			while (isActive) {
				delay(expireAfter)

				lock.withLock("checkExpiredValues()") {
					val now = clock.now()
					val iterator = lastUpdate.iterator()
					while (iterator.hasNext()) {
						val (id, instant) = iterator.next()

						if (instant < now - expireAfter) {
							log.trace(id) { "Expired value:" }
							iterator.remove()
							upstream.expire(id)
						}
					}
				}
			}
		}
	}

	private suspend fun markAsUpdatedNow(id: I) {
		lock.withLock("markAsUpdatedNow($id)") {
			log.trace(id) { "Updated now:" }
			lastUpdate[id] = clock.now()
		}
	}

	override fun get(id: I): ProgressiveFlow = upstream[id]
		.onEach {
			if (it.progress == done())
				markAsUpdatedNow(id)
			// else: it's still loading, no need to count it as done
		}

	override suspend fun update(values: Collection>) {
		// When the upstream is updated, it will signal the modification through the 'get' function,
		// which will catch. No need to update anything else here.
		upstream.update(values)
	}

	override suspend fun expire(ids: Collection) {
		for (ref in ids)
			lock.withLock("expire($ids)") {
				lastUpdate.remove(ref)
			}
		upstream.expire(ids)
	}

	override suspend fun expireAll() {
		lock.withLock("expireAll()") {
			lastUpdate.clear()
		}
		upstream.expireAll()
	}

	companion object
}

/**
 * Age-based [Cache] expiration strategy.
 *
 * ### General behavior
 *
 * The cache starts a worker in [scope]. Every [duration], all values which have not been updated
 * for at least [duration] are expired in the previous layer.
 *
 * This layer considers any non-loading value returned by [get][Cache.get] to be new.
 *
 * If [scope] is cancelled, requests made to this cache continue to work as normal, but no values are ever expired anymore.
 *
 * ### Example
 *
 * ```kotlin
 * val scope: CoroutineScope = …
 *
 * val powersOfTwo = cache { it * 2 }
 *     .cachedInMemory(scope.coroutineContext.job)
 *     .expireAfter(10.minutes, scope, clock)
 * ```
 */
fun  Cache.expireAfter(duration: Duration, scope: CoroutineScope, clock: Clock): Cache =
	ExpirationCache(this, duration, clock, scope)

@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(message = "Specifying the clock explicitly will become mandatory in 2.0.")
fun  Cache.expireAfter(duration: Duration, scope: CoroutineScope): Cache =
	ExpirationCache(this, duration, Clock.System, scope)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy