Skip to content

Commit

Permalink
Optimistic LRU cache.
Browse files Browse the repository at this point in the history
  • Loading branch information
kennethshackleton committed Jun 22, 2024
1 parent d01470f commit 8920fd3
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.bloomberg.selekt.cache.benchmarks

import com.bloomberg.selekt.cache.LruCache
import com.bloomberg.selekt.cache.LinkedLruCache
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.BenchmarkMode
import org.openjdk.jmh.annotations.Level
Expand All @@ -27,11 +27,11 @@ import org.openjdk.jmh.annotations.State

@State(Scope.Thread)
open class CacheInput {
internal lateinit var cache: LruCache<Any>
internal lateinit var cache: LinkedLruCache<Any>

@Setup(Level.Iteration)
fun setUp() {
cache = LruCache(1) {}
cache = LinkedLruCache(1) {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package com.bloomberg.selekt

import com.bloomberg.selekt.cache.LruCache
import com.bloomberg.selekt.cache.LinkedLruCache
import com.bloomberg.selekt.commons.forEachByPosition
import com.bloomberg.selekt.commons.forUntil
import javax.annotation.concurrent.NotThreadSafe
Expand All @@ -31,7 +31,7 @@ internal class SQLConnection(
key: Key?
) : CloseableSQLExecutor {
private val pointer = sqlite.open(path, flags)
private val preparedStatements = LruCache<SQLPreparedStatement>(configuration.maxSqlCacheSize) {
private val preparedStatements = LinkedLruCache<SQLPreparedStatement>(configuration.maxSqlCacheSize) {
it.close()
pooledPreparedStatement = it
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 Bloomberg Finance L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bloomberg.selekt.cache

class CommonLruCache<T : Any>(
@PublishedApi
internal val maxSize: Int,
disposal: (T) -> Unit
) {
@PublishedApi
internal var cache: Any = OptimisticLruCache(maxSize, disposal)

fun evict(key: String) {
when (val cache = cache) {
is OptimisticLruCache<*> -> cache.evict(key)
is LinkedLruCache<*> -> cache.evict(key)
else -> error("Unrecognized cache class: {}")
}
}

fun evictAll() {
when (val cache = cache) {
is OptimisticLruCache<*> -> cache.evictAll()
is LinkedLruCache<*> -> cache.evictAll()
else -> error("Unrecognized cache class: {}")
}
}

@Suppress("UNCHECKED_CAST")
inline fun get(key: String, supplier: () -> T): T = when (cache) {
is OptimisticLruCache<*> -> (cache as OptimisticLruCache<T>).let {
if (it.store.size >= maxSize) {
// Adding an entry to the cache will necessitate the removal of the least recently
// used entry first to honour our maximum size constraint. For the implementation
// of the store currently assigned, this is an O(N) operation. We transform to an
// O(1) implementation.
(transform() as LinkedLruCache<T>).get(key, supplier)
} else {
it.get(key, supplier)
}
}
is LinkedLruCache<*> -> (cache as LinkedLruCache<T>).get(key, supplier)
else -> error("Unrecognized cache class: {}")
}

fun containsKey(key: String) = when (val cache = cache) {
is OptimisticLruCache<*> -> cache.containsKey(key)
is LinkedLruCache<*> -> cache.containsKey(key)
else -> error("Unrecognized cache class: {}")
}

@PublishedApi
internal fun transform() = (cache as OptimisticLruCache<*>).asLruCache().also {
cache = it
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ import com.bloomberg.selekt.collections.map.FastLinkedStringMap
import javax.annotation.concurrent.NotThreadSafe

@NotThreadSafe
class LruCache<T : Any>(maxSize: Int, disposal: (T) -> Unit) {
class LinkedLruCache<T : Any>(
@PublishedApi
@JvmField
@JvmSynthetic
internal val store = FastLinkedStringMap(maxSize, maxSize, false, disposal)
internal val maxSize: Int,
@PublishedApi
@JvmField
internal val store: FastLinkedStringMap<T>
) {
constructor(
maxSize: Int,
disposal: (T) -> Unit
) : this(maxSize, FastLinkedStringMap(
maxSize = maxSize,
disposal = disposal,
accessOrder = true
))

fun evict(key: String) {
store.removeKey(key)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 Bloomberg Finance L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bloomberg.selekt.cache

import com.bloomberg.selekt.collections.map.FastStampedStringMap
import javax.annotation.concurrent.NotThreadSafe

@NotThreadSafe
class OptimisticLruCache<T : Any>(
@PublishedApi
@JvmField
internal val maxSize: Int,
@PublishedApi
@JvmField
internal val disposal: (T) -> Unit
) {
@PublishedApi
@JvmField
internal var store = FastStampedStringMap(capacity = maxSize, disposal = disposal)

fun evict(key: String) {
store.removeKey(key)
}

fun evictAll() {
store.clear()
}

inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier)

fun containsKey(key: String) = store.containsKey(key)

internal fun asLruCache() = LinkedLruCache(
maxSize = maxSize,
store = store.asLinkedMap(maxSize, disposal)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import javax.annotation.concurrent.NotThreadSafe

@NotThreadSafe
class FastStampedStringMap<T>(
capacity: Int
capacity: Int,
private val disposal: (T) -> Unit
) : FastStringMap<T>(capacity) {
private var currentStamp = Int.MIN_VALUE
private var spare: StampedEntry<T>? = null
Expand Down Expand Up @@ -51,9 +52,16 @@ class FastStampedStringMap<T>(
return StampedEntry(index, hashCode, key, value, nextStamp(), store[index])
}

fun removeKey(key: String) {
disposal(super.removeEntry(key).value!!)
}

override fun clear() {
super.clear()
entries().forEach {
disposal(it.value!!)
}
spare = null
super.clear()
}

@PublishedApi
Expand Down Expand Up @@ -81,27 +89,23 @@ class FastStampedStringMap<T>(
}
}

private fun entries(): Iterable<Entry<T>> = store.flatMap {
sequence {
var current = it
while (current != null) {
yield(current)
current = current.after
}
}
}.asIterable()

private fun resetAllStamps() {
entries().sortedBy {
(it as StampedEntry<T>).stamp
}.run {
@Suppress("UNCHECKED_CAST")
(entries() as Iterable<StampedEntry<T>>).sortedBy(StampedEntry<T>::stamp).run {
currentStamp = Int.MIN_VALUE + maxOf(0, size - 1)
forEachIndexed { index, it ->
(it as StampedEntry<T>).stamp = Int.MIN_VALUE + index
it.stamp = Int.MIN_VALUE + index
}
}
}

@Suppress("UNCHECKED_CAST")
@PublishedApi
internal fun removeLastEntry(): StampedEntry<T> = (entries() as Iterable<StampedEntry<T>>)
.minBy(StampedEntry<T>::stamp).let {
(removeEntry(it.key) as StampedEntry<T>).apply { disposal(value!!) }
}

@PublishedApi
internal class StampedEntry<T>(
index: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ open class FastStringMap<T>(capacity: Int) {
value: T
): Entry<T> = Entry(index, hashCode, key, value, store[index])

internal fun entries(): Iterable<Entry<T>> = store.flatMap {
sequence {
var current = it
while (current != null) {
yield(current)
current = current.after
}
}
}

open fun clear() {
store.fill(null)
size = 0
Expand Down
Loading

0 comments on commit 8920fd3

Please sign in to comment.