Skip to content

Commit

Permalink
Added evicting cache to util.
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-santa committed Aug 31, 2020
1 parent 59fd942 commit 68bfcd4
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package no.nav.personbruker.innloggingsstatus.pdl.cache

import no.nav.personbruker.dittnav.common.util.list.partitionFromIndex
import org.slf4j.LoggerFactory
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write

class EvictingCache<K, V>(config: EvictingCacheConfig = EvictingCacheConfig()) {

private val evictionThreshold = config.evictionThreshold
private val massEvictionCoolDownMinutes = config.massEvictionCoolDownMinutes

private val entryMap = mutableMapOf<K, EntryWrapper<K, V>>()
private val entryOrder = mutableListOf<EntryWrapper<K, V>>()

private val lock = ReentrantReadWriteLock()

private var lastEviction: Instant? = null

private val wrapperBuilder = { key: K, value: V -> EntryWrapper(key, value, config.entryLifetimeMinutes) }


suspend fun getEntry(key: K, supplier: suspend (K) -> V?): V? {

val entry = readEntry(key)

return when {
entry == null -> addEntry(key, supplier)
entry.isExpired() -> replaceEntry(key, supplier)
else -> entry.value
}
}

private fun readEntry(key: K) = lock.read {
entryMap[key]
}

private suspend fun addEntry(key: K, supplier: suspend (K) -> V?): V? {
checkThreshold()

val entry = supplier(key)

if (entry == null) {
return null
}
val entryWrapper = wrapperBuilder(key, entry)

lock.write {
entryOrder.add(entryWrapper)
entryMap[key] = entryWrapper
}

return entry
}

private fun checkThreshold() {
if (entryMap.size >= evictionThreshold && canPerformMassEviction()) {
evictAllExpired()
}
}

private fun canPerformMassEviction(): Boolean = lock.read {
return lastEviction?.plus(massEvictionCoolDownMinutes, ChronoUnit.MINUTES)
?.isBefore(Instant.now())
?: true
}

private suspend fun replaceEntry(key: K, supplier: suspend (K) -> V?): V? {
evictEntry(key)
return addEntry(key, supplier)
}


private fun evictEntry(key: K) = lock.write {
entryMap.remove(key)?.let {
entryOrder.remove(it)
}
}

private fun evictAllExpired() {
val start = System.nanoTime()
val evicted = performEvictionOfExpiredEntries()
val durationNanos = System.nanoTime() - start
log.debug("Evicted $evicted entries from cache in ${durationNanos / 1000} µs.")
}

private fun performEvictionOfExpiredEntries(): Int = lock.write {
val numberOfEntriesToEvict = 1 + entryOrder.indexOfLast { it.isExpired() }
val remainingValidEntries = entryOrder.partitionFromIndex(numberOfEntriesToEvict)

entryMap.clear()
remainingValidEntries.map {
it.key to it
}.toMap(entryMap)

entryOrder.clear()
entryOrder.addAll(remainingValidEntries)

lastEviction = Instant.now()

return numberOfEntriesToEvict
}

companion object {
private val log = LoggerFactory.getLogger(EvictingCache::class.java)
}
}

private data class EntryWrapper<K, V> (
val key: K,
val value: V,
val entryLifeTimeMinutes: Long
) {
val timeOfCreation: Instant = Instant.now()
val timeOfExpiry: Instant = timeOfCreation.plus(entryLifeTimeMinutes, ChronoUnit.MINUTES)

fun isExpired() = timeOfExpiry < Instant.now()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package no.nav.personbruker.innloggingsstatus.pdl.cache

data class EvictingCacheConfig (
val evictionThreshold: Int = 1024,
val entryLifetimeMinutes: Long = 15,
val massEvictionCoolDownMinutes: Long = 5
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package no.nav.personbruker.dittnav.common.util.list

fun <T> List<T>.partitionToIndex(indexExclusive: Int): List<T> {
return when {
indexExclusive > size -> toList()
indexExclusive < 1 -> emptyList()
else -> subList(0, indexExclusive).toList()
}
}

fun <T> List<T>.mutablePartitionToIndex(indexExclusive: Int): MutableList<T> {
return when {
indexExclusive > size -> toMutableList()
indexExclusive < 1 -> mutableListOf()
else -> subList(0, indexExclusive).toMutableList()
}
}

fun <T> List<T>.partitionFromIndex(indexInclusive: Int): List<T> {
return when {
indexInclusive < 1 -> toList()
indexInclusive >= size -> emptyList()
else -> subList(indexInclusive, size).toList()
}
}

fun <T> List<T>.mutablePartitionFromIndex(indexInclusive: Int): MutableList<T> {
return when {
indexInclusive < 1 -> toMutableList()
indexInclusive >= size -> mutableListOf()
else -> subList(indexInclusive, size).toMutableList()
}
}

0 comments on commit 68bfcd4

Please sign in to comment.