Observability

Two behaviours for knowing what your cache is doing: eviction callbacks and statistics.

onEviction

Register a callback that fires when entries are automatically evicted by other behaviours (TTL expiry, LRU eviction, memory pressure). Does NOT fire on explicit remove() calls.

val cache = fastCache<String, DatabaseConnection> {
    expireAfterAccess(5.minutes)
    onEviction { key, value ->
        value.close()  // clean up the connection
        logger.info("Evicted: $key")
    }
}

Use cases:

  • Resource cleanup — close connections, release file handles, cancel timers
  • Logging — track what the cache is dropping and when
  • Metrics — feed eviction events into Prometheus, Micrometer, or custom dashboards
The handler is called synchronously during the processing loop. Keep it fast and non-blocking — if you need to do I/O, launch a coroutine from the handler.

What counts as eviction?

Operation Fires onEviction?
TTL expires (expireAfterAccess / expireAfterWrite) Yes
LRU eviction (maxEntries / maxMemoryUsage) Yes
Hard TTL eviction (refreshAfterWrite with hardTtl) Yes
Explicit cache.remove(key) No
cache.clear() No

statistics

Track hits, misses, puts, and evictions. The statistics() builder method returns a StatisticsBehaviour handle — hold onto it to call snapshot() later:

import io.peekandpoke.ultra.cache.FastCache
import io.peekandpoke.ultra.cache.fastCache

lateinit var stats: FastCache.StatisticsBehaviour<String, String>

val cache = fastCache<String, String> {
    expireAfterAccess(10.minutes)
    maxEntries(1000)
    stats = statistics()
}

// Use the cache...
cache.put("a", "1")
cache.getOrPut("b") { "2" }
cache.get("a")        // hit
cache.get("missing")  // miss

// Take a snapshot
val snapshot = stats.snapshot()
println(snapshot.hitCount)       // 1
println(snapshot.missCount)      // 1
println(snapshot.putCount)       // 2
println(snapshot.evictionCount)  // 0
println(snapshot.requestCount)   // 2 (hits + misses)
println(snapshot.hitRate)        // 0.5

CacheStats data class

The snapshot() method returns an immutable CacheStats instance. Old snapshots are not affected by subsequent cache operations.

Property Type Description
hitCount Long Successful get() / has() lookups
missCount Long Failed get() / has() lookups
putCount Long Total put() calls (including getOrPut misses)
evictionCount Long Entries removed by behaviours (not explicit remove())
requestCount Long hitCount + missCount
hitRate Double hitCount / requestCount (NaN if no requests)

How statistics counts

Statistics processes every action in every batch, not just the last-per-key. If a key is read 5 times in one batch, that's 5 hits. Evictions are counted via the eviction listener — the same mechanism used by onEviction.

Combining both

lateinit var stats: FastCache.StatisticsBehaviour<String, String>

val cache = fastCache<String, String> {
    expireAfterWrite(10.minutes)
    maxEntries(5000)
    onEviction { key, _ ->
        logger.debug("Evicted: $key")
    }
    stats = statistics()
}

// Periodic monitoring
fun reportCacheHealth() {
    val s = stats.snapshot()
    metrics.gauge("cache.hit_rate", s.hitRate)
    metrics.counter("cache.evictions", s.evictionCount)
}