-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cache): Implement simple LRU cache
- Loading branch information
Showing
8 changed files
with
196 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package com.grohden.repotagger.cache | ||
|
||
|
||
private data class Node<K, V>( | ||
val key: K, | ||
val value: V, | ||
var next: Node<K, V>? = null, | ||
var prev: Node<K, V>? = null | ||
) | ||
|
||
/** | ||
* Simple LRU cache (I think) | ||
* | ||
* This is basically a linked list with a replacement | ||
* and get algorithm that reposition it's head and tail | ||
*/ | ||
class LRUCache<K, V>( | ||
private val capacity: Int | ||
) { | ||
val size get() = cache.size | ||
|
||
private var head: Node<K, V>? = null | ||
private var tail: Node<K, V>? = null | ||
private val cache: HashMap<K, Node<K, V>> = hashMapOf() | ||
|
||
private fun replaceWithNewHead(node: Node<K, V>) { | ||
if (head == null) { | ||
node.prev = tail | ||
node.next = null | ||
head = node | ||
|
||
return | ||
} | ||
|
||
head?.takeIf { node.key != it.key }?.let { oldHead -> | ||
oldHead.next = node | ||
node.prev = oldHead | ||
node.next = null | ||
head = node | ||
} | ||
} | ||
|
||
private fun replaceWithNewTail(node: Node<K, V>) { | ||
if (tail == null) { | ||
node.next = head | ||
node.prev = null | ||
tail = node | ||
|
||
return | ||
} | ||
|
||
tail?.takeIf { node.key != it.key }?.let { oldTail -> | ||
oldTail.prev = node | ||
node.next = oldTail | ||
node.prev = null | ||
tail = node | ||
} | ||
} | ||
|
||
fun get(key: K): V? { | ||
val node = cache[key] ?: return null | ||
|
||
replaceWithNewHead(node) | ||
|
||
return node.value | ||
} | ||
|
||
|
||
/** | ||
* Similar to [get] but uses a default provider | ||
* in case the value is not found in cache, and also | ||
* puts this new value in cache | ||
* | ||
* TODO: review the suspend usage | ||
*/ | ||
suspend fun getOrDefaultAndPut(key: K, orElse: suspend () -> V): V { | ||
return get(key) ?: orElse().also { put(key, it) } | ||
} | ||
|
||
fun put(key: K, value: V) { | ||
val node = Node(key, value).also { | ||
cache[key] = it | ||
} | ||
|
||
replaceWithNewHead(node) | ||
|
||
if (cache.size >= capacity) { | ||
cache.remove(tail!!.key) | ||
} | ||
|
||
(tail ?: node).let(this::replaceWithNewTail) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.grohden.repotagger.cache | ||
|
||
import org.amshove.kluent.shouldBe | ||
import org.amshove.kluent.shouldBeEqualTo | ||
import kotlin.test.Test | ||
|
||
class LRUCacheTest { | ||
@Test | ||
fun `should not grow with duplicated keys`() { | ||
val instance = LRUCache<String, String>(4) | ||
|
||
instance.apply { | ||
put("bazz", "1") | ||
put("bar", "1") | ||
put("bar", "2") | ||
put("bar", "3") | ||
} | ||
|
||
instance.size shouldBe 2 | ||
} | ||
|
||
@Test | ||
fun `should respect capacity`() { | ||
val instance = LRUCache<String, String>(4) | ||
|
||
instance.apply { | ||
put("1", "1") | ||
put("2", "1") | ||
put("3", "1") | ||
put("4", "1") | ||
put("5", "1") | ||
} | ||
|
||
instance.size shouldBe 4 | ||
} | ||
|
||
@Test | ||
fun `should update cache contents for same key`() { | ||
val instance = LRUCache<String, String>(4) | ||
|
||
instance.apply { | ||
put("bar", "1") | ||
put("bar", "2") | ||
put("bar", "3") | ||
} | ||
|
||
instance.get("bar") shouldBeEqualTo "3" | ||
} | ||
|
||
@Test | ||
fun `should evict last updated when at limit`() { | ||
val instance = LRUCache<String, String>(4) | ||
|
||
instance.apply { | ||
put("1", "1") | ||
put("2", "2") | ||
put("3", "3") | ||
put("4", "4") | ||
put("5", "5") | ||
} | ||
|
||
instance.get("1") shouldBe null | ||
instance.get("2") shouldBe "2" | ||
instance.get("5") shouldBe "5" | ||
} | ||
|
||
} |