From dd45d323056421fc002988aeaad053db25aded9c Mon Sep 17 00:00:00 2001 From: Luca Spinazzola Date: Fri, 24 Nov 2023 14:15:52 -0500 Subject: [PATCH] use cache4k as LruCache backing that is thread safe. fixes #75 --- gradle/libs.versions.toml | 4 +- kamel-core/build.gradle.kts | 1 + .../kotlin/io/kamel/core/cache/JvmLruCache.kt | 29 ---- .../kotlin/io/kamel/core/cache/LruCache.kt | 26 ++- .../kotlin/io/kamel/core/cache/LruCache.kt | 26 --- .../io/kamel/core/cache/common/LruCache.kt | 161 ------------------ 6 files changed, 29 insertions(+), 218 deletions(-) delete mode 100644 kamel-core/src/commonJvmMain/kotlin/io/kamel/core/cache/JvmLruCache.kt delete mode 100644 kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/LruCache.kt delete mode 100644 kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/common/LruCache.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d80df053..437608c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ com-android-application = { id = "com.android.application", version.ref = "agp" [versions] +cache4k = "0.12.0" kotlin = "1.9.21" agp = "8.1.4" @@ -30,7 +31,7 @@ batik = "1.17" [libraries] -androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" } +cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } @@ -51,6 +52,7 @@ dev-icerock-moko-resources-test = { module = "dev.icerock.moko:resources-test", pdvrieze-xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" } +androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } google-android-material = { module = "com.google.android.material:material", version.ref = "material" } diff --git a/kamel-core/build.gradle.kts b/kamel-core/build.gradle.kts index 7cbeac16..8ba58358 100644 --- a/kamel-core/build.gradle.kts +++ b/kamel-core/build.gradle.kts @@ -66,6 +66,7 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.client.core) implementation(libs.okio) + implementation(libs.cache4k) } } diff --git a/kamel-core/src/commonJvmMain/kotlin/io/kamel/core/cache/JvmLruCache.kt b/kamel-core/src/commonJvmMain/kotlin/io/kamel/core/cache/JvmLruCache.kt deleted file mode 100644 index 38c7ab77..00000000 --- a/kamel-core/src/commonJvmMain/kotlin/io/kamel/core/cache/JvmLruCache.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.kamel.core.cache - -private const val LoadFactor = 0.75F - -/** - * Cache implementation which evicts items using an LRU algorithm. - */ -internal actual class LruCache actual constructor(override val maxSize: Int) : Cache { - - private val cache: MutableMap = object : LinkedHashMap(maxSize, LoadFactor, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean = size > maxSize - } - - override val size: Int - get() = cache.size - - init { - require(maxSize >= 0) { "Cache max size must be positive number" } - } - - override fun get(key: K): V? = cache[key] - - override fun set(key: K, value: V) = cache.set(key, value) - - override fun remove(key: K): Boolean = cache.remove(key) != null - - override fun clear(): Unit = cache.clear() - -} \ No newline at end of file diff --git a/kamel-core/src/commonMain/kotlin/io/kamel/core/cache/LruCache.kt b/kamel-core/src/commonMain/kotlin/io/kamel/core/cache/LruCache.kt index be866d61..c64ef010 100644 --- a/kamel-core/src/commonMain/kotlin/io/kamel/core/cache/LruCache.kt +++ b/kamel-core/src/commonMain/kotlin/io/kamel/core/cache/LruCache.kt @@ -3,4 +3,28 @@ package io.kamel.core.cache /** * Cache implementation which evicts items using an LRU algorithm. */ -internal expect class LruCache(maxSize: Int) : Cache \ No newline at end of file +internal class LruCache(override val maxSize: Int) : Cache { + + private val cache: io.github.reactivecircus.cache4k.Cache = + io.github.reactivecircus.cache4k.Cache.Builder() + .maximumCacheSize(maxSize.toLong()) + .build() + + override val size: Int + get() = cache.asMap().size + + init { + require(maxSize >= 0) { "Cache max size must be positive number" } + } + + override fun get(key: K): V? = cache.get(key) + + override fun set(key: K, value: V) = cache.put(key, value) + + override fun remove(key: K): Boolean { + cache.invalidate(key) + return true + } + + override fun clear(): Unit = cache.invalidateAll() +} \ No newline at end of file diff --git a/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/LruCache.kt b/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/LruCache.kt deleted file mode 100644 index 7cd3a565..00000000 --- a/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/LruCache.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.kamel.core.cache - - -/** - * Cache implementation which evicts items using an LRU algorithm. - */ -internal actual class LruCache actual constructor(override val maxSize: Int) : Cache { - - private val cache: io.kamel.core.cache.common.LruCache = io.kamel.core.cache.common.LruCache(maxSize) - - override val size: Int - get() = cache.size() - - init { - require(maxSize >= 0) { "Cache max size must be positive number" } - } - - override fun get(key: K): V? = cache[key] - - override fun set(key: K, value: V) = cache.set(key, value) - - override fun remove(key: K): Boolean = cache.remove(key) != null - - override fun clear(): Unit = cache.clear() - -} \ No newline at end of file diff --git a/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/common/LruCache.kt b/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/common/LruCache.kt deleted file mode 100644 index aa0f16fa..00000000 --- a/kamel-core/src/nonJvmMain/kotlin/io/kamel/core/cache/common/LruCache.kt +++ /dev/null @@ -1,161 +0,0 @@ -package io.kamel.core.cache.common - -internal typealias Weigher = (Key, Value?) -> Int - -/** - * Multiplatform LRU cache implementation. - * https://github.com/apollographql/apollo-kotlin/blob/main/apollo-normalized-cache-api/src/commonMain/kotlin/com/apollographql/apollo3/cache/normalized/api/internal/LruCache.kt - * - * Implementation is based on usage of [LinkedHashMap] as a container for the cache and custom - * double linked queue to track LRU property. - * - * [maxSize] - maximum size of the cache, can be anything bytes, number of entries etc. By default is number o entries. - * [weigher] - to be called to calculate the estimated size (weight) of the cache entry defined by its [Key] and [Value]. - * By default it returns 1. - * - * Cache trim performed only on new entry insertion. - */ -internal class LruCache( - private val maxSize: Int, - private val weigher: Weigher = { _, _ -> 1 }, -) { - private val cache = LinkedHashMap>(0, 0.75f) - private var headNode: Node? = null - private var tailNode: Node? = null - private var size: Int = 0 - - operator fun get(key: Key): Value? { - val node = cache[key] - if (node != null) { - moveNodeToHead(node) - } - return node?.value - } - - operator fun set(key: Key, value: Value) { - val node = cache[key] - if (node == null) { - cache[key] = addNode(key, value) - } else { - node.value = value - moveNodeToHead(node) - } - - trim() - } - - fun remove(key: Key): Value? { - return removeUnsafe(key) - } - - fun keys() = cache.keys - - private fun removeUnsafe(key: Key): Value? { - val nodeToRemove = cache.remove(key) - val value = nodeToRemove?.value - if (nodeToRemove != null) { - unlinkNode(nodeToRemove) - } - return value - } - - fun remove(keys: Collection) { - keys.forEach { key -> removeUnsafe(key) } - } - - fun clear() { - cache.clear() - headNode = null - tailNode = null - size = 0 - } - - fun size(): Int { - return size - } - - fun dump(): Map { - return cache.mapValues { (_, value) -> - @Suppress("UNCHECKED_CAST") - value.value as Value - } - } - - private fun trim() { - var nodeToRemove = tailNode - while (nodeToRemove != null && size > maxSize) { - cache.remove(nodeToRemove.key) - unlinkNode(nodeToRemove) - nodeToRemove = tailNode - } - } - - private fun addNode(key: Key, value: Value?): Node { - val node = Node( - key = key, - value = value, - next = headNode, - prev = null, - ) - - headNode = node - - if (node.next == null) { - tailNode = headNode - } else { - node.next?.prev = headNode - } - - size += weigher(key, value) - - return node - } - - private fun moveNodeToHead(node: Node) { - if (node.prev == null) { - return - } - - node.prev?.next = node.next - - if (node.next == null) { - tailNode = node.prev - } else { - node.next?.prev = node.prev - } - - node.next = headNode - node.prev = null - - headNode?.prev = node - headNode = node - } - - private fun unlinkNode(node: Node) { - if (node.prev == null) { - this.headNode = node.next - } else { - node.prev?.next = node.next - } - - if (node.next == null) { - this.tailNode = node.prev - } else { - node.next?.prev = node.prev - } - - size -= weigher(node.key!!, node.value) - - node.key = null - node.value = null - node.next = null - node.prev = null - } - - private class Node( - var key: Key?, - var value: Value?, - var next: Node?, - var prev: Node?, - ) -} \ No newline at end of file