Skip to content

Commit bc6f986

Browse files
authored
fixes for AdvancedLRU with naive implementtion, LRUCache, and AnyCallback (#144)
1 parent 0e3b610 commit bc6f986

File tree

12 files changed

+362
-193
lines changed

12 files changed

+362
-193
lines changed
Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,19 @@
11
package com.igorwojda.cache.advancedlru
22

3-
import org.amshove.kluent.shouldBeEqualTo
4-
import org.junit.jupiter.api.Test
5-
import java.util.*
3+
import java.time.Clock
4+
import java.time.Duration
65

7-
class AdvancedLRUCache(private val capacity: Int) {
8-
fun put(key: String, value: Int, priority: Int, expiryTime: Long) {
9-
TODO("Add your solution here")
10-
}
11-
12-
fun get(key: String): Int? {
13-
TODO("Add your solution here")
14-
}
15-
16-
// Returns fixed system time in milliseconds
17-
private fun getSystemTimeForExpiry() = 1000
6+
interface LRUCache<K: Any, V: Any> {
7+
fun put(key: K, value: V, priority: Int, ttl: Duration)
8+
fun get(key: K): V?
189
}
1910

20-
private class Test {
21-
@Test
22-
fun `add and get`() {
23-
val cache = AdvancedLRUCache(2)
24-
cache.put("A", 1, 5, 5000)
25-
26-
cache.get("A") shouldBeEqualTo 1
27-
}
28-
29-
@Test
30-
fun `evict by priority`() {
31-
val cache = AdvancedLRUCache(2)
32-
cache.put("A", 1, 1, 3000)
33-
cache.put("B", 2, 3, 4000)
34-
cache.put("C", 3, 4, 5000)
35-
36-
// This should be null because "A" was evicted due to lower priority.
37-
cache.get("A") shouldBeEqualTo null
38-
cache.get("B") shouldBeEqualTo 2
39-
cache.get("C") shouldBeEqualTo 3
11+
class AdvancedLRUCache<K: Any, V: Any>(private val capacity: Int, private val clock: Clock = Clock.systemDefaultZone()): LRUCache<K, V> {
12+
override fun put(key: K, value: V, priority: Int, ttl: Duration) {
13+
TODO("Add your solution here")
4014
}
4115

42-
@Test
43-
fun `evict by expiry`() {
44-
val cache = AdvancedLRUCache(2)
45-
cache.put("A", 1, 1, 500)
46-
cache.put("B", 2, 3, 700)
47-
48-
// This should be null because "A" was evicted due to expiry.
49-
cache.get("A") shouldBeEqualTo null
50-
cache.get("B") shouldBeEqualTo null
16+
override fun get(key: K): V? {
17+
TODO("Add your solution here")
5118
}
5219
}

src/test/kotlin/com/igorwojda/cache/advancedlru/README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,39 @@ limit. In cases where the addition of new items exceeds this capacity, ensure th
1111
following sequence of operations:
1212

1313
- Firstly, discard items that have exceeded their validity period (`expiryTime` > `getSystemTimeForExpiry()`).
14-
- If there are no items past their validity, identify the items with the lowest priority rating and from these, remove
14+
- If there are no items past their validity, identify the items with the earliest expiry time, and from those the items with the lowest priority rating, and from these remove
1515
the item that was least recently accessed or used.
1616

17-
To simplify expiry logic testing use provided `getSystemTime()` method (instead of `System.currentTimeMillis()`) that
18-
will return fixed system time in milliseconds.
17+
To simplify expiry logic testing use the provided `Clock` to determine the current time in milliseconds using `clock.millis()`.
1918

20-
[Challenge](Challenge.kt) | [Solution](Solution.kt)
19+
[Challenge](Challenge.kt) | [Solution](Solution.kt) | [Tests](Tests.kt)
2120

2221
## Examples
2322

2423
```kotlin
2524
val cache = AdvancedLRUCache(2)
26-
cache.put("A", 1, 5, 5000)
25+
cache.put("A", 1, 5, Duration.ofMinutes(15))
2726
cache.get("A") // 1
2827
```
2928

3029
```kotlin
31-
val cache = AdvancedLRUCache(2)
32-
cache.put("A", 1, 1, 3000)
33-
cache.put("B", 2, 3, 4000)
34-
cache.put("C", 3, 4, 5000)
30+
val cache = AdvancedLRUCache(2, Clock.fixed(...)) // testing clock, fixed at a moment in time
31+
cache.put("A", 1, 5, Duration.ofMinutes(15))
32+
cache.put("B", 2, 1, Duration.ofMinutes(15))
33+
cache.put("C", 3, 10, Duration.ofMinutes(15))
3534

3635

37-
cache.get("A") // null - "A" was evicted due to lower priority.
38-
cache.get("B") // 2
36+
cache.get("A") // 1
37+
cache.get("B") // null - "B" was evicted due to lower priority.
3938
cache.get("C") // 3
4039
```
4140

4241
```kotlin
43-
val cache = AdvancedLRUCache(2)
44-
cache.put("A", 1, 1, 500)
45-
cache.put("B", 2, 3, 700)
42+
val cache = AdvancedLRUCache(100)
43+
cache.put("A", 1, 1, Duration.ofMillis(1))
44+
cache.put("B", 2, 1, Duration.ofMillis(1))
45+
46+
sleep(100)
4647

4748
cache.get("A") // null - "A" was evicted due to expiry.
4849
cache.get("B") // null - "B" was evicted due to expiry.

src/test/kotlin/com/igorwojda/cache/advancedlru/Solution.kt

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,109 @@
11
package com.igorwojda.cache.advancedlru
22

3+
import java.time.Clock
4+
import java.time.Duration
35
import java.util.*
46

5-
// Implementation is using combination of HashMap and LinkedList.
6-
// Time Complexity: O(1)
7-
private object Solution1 {
8-
class AdvancedLRUCache(private val capacity: Int) {
9-
private val map: MutableMap<String, CacheItem> = mutableMapOf()
10-
private val priorityQueue: PriorityQueue<CacheItem> = PriorityQueue()
7+
// Implementation is using combination of HashMap and PriorityQueue.
8+
// Time Complexity: O(N) (JVM priority queue is O(log(n)) on offer/poll methods and O(N) on remove(item) method)
9+
internal object Solution1 {
10+
class AdvancedLRUCache<K: Any, V: Any>(private val capacity: Int, private val clock: Clock = Clock.systemDefaultZone()) : LRUCache<K, V> {
11+
private val map: MutableMap<K, CacheItem<K, V>> = mutableMapOf()
1112

12-
fun put(key: String, value: Int, priority: Int, expiryTime: Long) {
13-
if (map.containsKey(key)) {
14-
remove(key)
15-
}
13+
private val expiryQueue: PriorityQueue<CacheItem<K, V>> = PriorityQueue { item1, item2 ->
14+
compareBy<CacheItem<K, V>>({ it.expiryTime }).compare(item1, item2)
15+
}
1616

17-
if (map.size == capacity) {
18-
clearCache()
19-
}
17+
private val priorityQueue: PriorityQueue<CacheItem<K, V>> = PriorityQueue { item1, item2 ->
18+
compareBy<CacheItem<K, V>>({ it.priority }, { it.lastUsed }).compare(item1, item2)
19+
}
2020

21-
val item = CacheItem(key, value, priority, expiryTime)
21+
override fun put(key: K, value: V, priority: Int, ttl: Duration) {
22+
remove(key)
23+
checkAndExpireCachedItems()
24+
25+
val item = CacheItem(key, value, priority, clock.millis() + ttl.toMillis(), clock.millis())
2226
map[key] = item
23-
priorityQueue.add(item)
27+
28+
expiryQueue.offer(item)
29+
priorityQueue.offer(item)
2430
}
2531

26-
fun get(key: String): Int? {
32+
override fun get(key: K): V? {
2733
val item = map[key]
2834

29-
return if (item == null || item.expiryTime < getSystemTimeForExpiry()) {
35+
return if (item == null) {
36+
null
37+
} else if (item.expiryTime < clock.millis()) {
38+
expiryQueue.remove(item)
39+
priorityQueue.remove(item)
3040
null
3141
} else {
32-
item.lastUsed = System.currentTimeMillis()
42+
priorityQueue.remove(item)
43+
priorityQueue.add(item.touch(clock.millis()))
3344
item.value
3445
}
3546
}
3647

37-
private fun remove(key: String) {
48+
private fun remove(key: K) {
3849
val item = map[key]
3950

4051
item?.let {
41-
it.expiryTime = 0L // Mark as expired for next eviction
52+
expiryQueue.remove(item)
53+
priorityQueue.remove(item)
4254
map.remove(key)
4355
}
4456
}
4557

46-
private fun clearCache() {
47-
while (priorityQueue.isNotEmpty() && priorityQueue.peek().expiryTime < getSystemTimeForExpiry()) {
48-
val item = priorityQueue.poll()
58+
private fun checkAndExpireCachedItems() {
59+
if (map.size < capacity) return
4960

50-
if (map.containsKey(item.key) && map[item.key] == item) {
51-
map.remove(item.key)
52-
}
61+
while (expiryQueue.isNotEmpty() && expiryQueue.peek().expiryTime < clock.millis()) {
62+
val item = expiryQueue.poll()
63+
map.remove(item.key)
64+
priorityQueue.remove(item)
5365
}
5466

67+
if (map.size < capacity) return
68+
5569
if (priorityQueue.isEmpty()) return
5670

5771
val item = priorityQueue.poll()
72+
map.remove(item.key)
73+
expiryQueue.remove(item)
74+
}
5875

59-
if (map.containsKey(item.key) && map[item.key] == item) {
60-
map.remove(item.key)
76+
private class CacheItem<K, V>(
77+
val key: K,
78+
val value: V,
79+
val priority: Int,
80+
val expiryTime: Long,
81+
val lastUsed: Long
82+
) {
83+
// only compare equality by `key`
84+
override fun equals(other: Any?): Boolean {
85+
if (this === other) return true
86+
if (other !is CacheItem<*, *>) return false
87+
if (key == other.key) return true
88+
return false
6189
}
62-
}
6390

64-
private data class CacheItem(
65-
val key: String,
66-
var value: Int,
67-
var priority: Int,
68-
var expiryTime: Long,
69-
) : Comparable<CacheItem> {
70-
var lastUsed: Long = System.currentTimeMillis()
71-
72-
override fun compareTo(other: CacheItem) = when {
73-
expiryTime != other.expiryTime -> expiryTime.compareTo(other.expiryTime)
74-
priority != other.priority -> priority.compareTo(other.priority)
75-
else -> lastUsed.compareTo(other.lastUsed)
91+
override fun hashCode(): Int {
92+
return key.hashCode()
93+
}
94+
95+
fun touch(
96+
lastUsed: Long = this.lastUsed
97+
) = CacheItem(key, value, priority, expiryTime, lastUsed)
98+
99+
override fun toString(): String {
100+
return "CacheItem(key='$key', value=$value, priority=$priority, expiryTime=$expiryTime, lastUsed=$lastUsed)"
76101
}
77102
}
78103

79-
// Returns fixed system time in milliseconds
80-
private fun getSystemTimeForExpiry() = 1000
104+
override fun toString(): String {
105+
return "AdvancedLRUCache(capacity=$capacity, clock=$clock, map=$map, priorityQueue=$expiryQueue)"
106+
}
81107
}
82108
}
83109

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.igorwojda.cache.advancedlru
2+
3+
import org.amshove.kluent.shouldBeEqualTo
4+
import org.junit.jupiter.api.Test
5+
import java.time.Clock
6+
import java.time.Duration
7+
import java.time.Instant
8+
import java.time.ZoneId
9+
10+
class Tests {
11+
// Easily switch between a known solution and Challenge code
12+
private val classUnderTest: (capacity: Int, clock: Clock)->LRUCache<String, String> = ::AdvancedLRUCache // or SolutionN::AdvancedLRUCache
13+
14+
private val testClock = object : Clock() {
15+
private var testTime = Instant.now()
16+
override fun instant(): Instant {
17+
return testTime
18+
}
19+
20+
fun incTime(duration: Duration) {
21+
testTime += duration
22+
}
23+
24+
override fun withZone(zone: ZoneId?): Clock = TODO("Not yet implemented")
25+
override fun getZone(): ZoneId = systemDefaultZone().zone
26+
}
27+
28+
@Test
29+
fun `add and get immediately`() {
30+
val cache = classUnderTest(2, testClock)
31+
32+
cache.put("A", "apple", 0, Duration.ofMinutes(15))
33+
cache.get("A") shouldBeEqualTo "apple"
34+
35+
cache.put("B", "bee", 0, Duration.ofMinutes(15))
36+
cache.get("B") shouldBeEqualTo "bee"
37+
38+
cache.put("C", "cat", 0, Duration.ofMinutes(15))
39+
cache.get("C") shouldBeEqualTo "cat"
40+
41+
cache.put("E", "echo", 0, Duration.ofMinutes(15))
42+
cache.get("E") shouldBeEqualTo "echo"
43+
}
44+
45+
@Test
46+
fun `evict by priority`() {
47+
val cache = classUnderTest(4, testClock)
48+
49+
// all have the same expiry
50+
cache.put("B", "bee", 3, Duration.ofMinutes(15))
51+
cache.put("A", "apple", 1, Duration.ofMinutes(15)) // lowest priority
52+
cache.put("C", "cat", 5, Duration.ofMinutes(15))
53+
cache.put("D", "door", 7, Duration.ofMinutes(15))
54+
cache.put("E", "echo", 9, Duration.ofMinutes(15)) // causes eviction
55+
56+
// This should be null because "A" was evicted due to lower priority and no items have reached expiry time
57+
cache.get("A") shouldBeEqualTo null
58+
cache.get("B") shouldBeEqualTo "bee"
59+
cache.get("C") shouldBeEqualTo "cat"
60+
cache.get("D") shouldBeEqualTo "door"
61+
cache.get("E") shouldBeEqualTo "echo"
62+
}
63+
64+
@Test
65+
fun `evict by priority and last used`() {
66+
val cache = classUnderTest(4, testClock)
67+
68+
// some have the same priority
69+
cache.put("C", "cat", 1, Duration.ofMinutes(12)) // priority 1
70+
cache.put("A", "apple", 1, Duration.ofMinutes(20)) // priority 1
71+
cache.put("B", "bee", 1, Duration.ofMinutes(10)) // priority 1
72+
cache.put("D", "door", 7, Duration.ofMinutes(5))
73+
74+
// but are accessed most recently in a different order...
75+
testClock.incTime(Duration.ofSeconds(1))
76+
cache.get("A")
77+
testClock.incTime(Duration.ofSeconds(1))
78+
cache.get("C")
79+
testClock.incTime(Duration.ofSeconds(1))
80+
cache.get("B")
81+
testClock.incTime(Duration.ofSeconds(1))
82+
83+
cache.put("E", "echo", 9, Duration.ofMinutes(15)) // causes eviction
84+
85+
// This should be null because "A" was evicted due to lower priority.
86+
println(cache)
87+
cache.get("A") shouldBeEqualTo null
88+
cache.get("B") shouldBeEqualTo "bee"
89+
cache.get("C") shouldBeEqualTo "cat"
90+
cache.get("D") shouldBeEqualTo "door"
91+
cache.get("E") shouldBeEqualTo "echo"
92+
}
93+
94+
@Test
95+
fun `evict by expiry time`() {
96+
val cache = classUnderTest(100, testClock)
97+
98+
cache.put("A", "apple", 1, Duration.ofMinutes(15))
99+
cache.put("B", "bee", 3, Duration.ofMinutes(20))
100+
101+
testClock.incTime(Duration.ofMinutes(16))
102+
103+
// This should be null because "A" was evicted due to expiry.
104+
cache.get("A") shouldBeEqualTo null
105+
106+
testClock.incTime(Duration.ofMinutes(20))
107+
108+
cache.put("C", "cat", 5, Duration.ofMinutes(15)) // causes eviction
109+
cache.put("D", "door", 5, Duration.ofMinutes(15)) // causes eviction
110+
111+
// this should be null because "B" was evicted due to expiry and later inserts
112+
cache.get("B") shouldBeEqualTo null
113+
114+
testClock.incTime(Duration.ofMinutes(14))
115+
116+
// still here as clock has not moved past expiry
117+
cache.get("C") shouldBeEqualTo "cat"
118+
cache.get("D") shouldBeEqualTo "door"
119+
}
120+
}

0 commit comments

Comments
 (0)