philiprehberger-cache_kit
In-memory LRU cache with TTL, tags, and thread safety
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-cache_kit"Or install directly:
gem install philiprehberger-cache_kitUsage
require "philiprehberger/cache_kit"
cache = Philiprehberger::CacheKit::Store.new(max_size: 500)
# Basic get/set
cache.set("user:1", { name: "Alice" }, ttl: 300)
cache.get("user:1") # => { name: "Alice" }Fetch (compute-on-miss)
Thread-safe get-or-compute. The block is only called if the key is missing or expired, and the entire operation holds the lock to prevent duplicate computation.
user = cache.fetch("user:1", ttl: 300) do
User.find(1) # only called on cache miss
endTTL Expiration
cache.set("session", "abc", ttl: 60) # expires in 60 seconds
cache.get("session") # => "abc"
# ... 60 seconds later ...
cache.get("session") # => nilTag-based Invalidation
cache.set("user:1", data1, tags: ["users"])
cache.set("user:2", data2, tags: ["users"])
cache.set("post:1", data3, tags: ["posts"])
cache.invalidate_tag("users") # removes user:1 and user:2, keeps post:1Eviction Callback
Register hooks that fire when entries are evicted by LRU pressure or TTL expiry.
cache.on_evict do |key, value|
logger.info("Evicted #{key}")
end
# Fires on LRU eviction
cache.set("overflow", "data") # triggers callback if cache is full
# Fires on TTL expiry (during get or prune)
cache.pruneBatch Get
Retrieve multiple keys in a single lock acquisition. Returns a hash of found entries only, skipping misses.
cache.set("a", 1)
cache.set("b", 2)
cache.get_many("a", "b", "missing")
# => { "a" => 1, "b" => 2 }Bulk Set
cache.set_many({ 'a' => 1, 'b' => 2, 'c' => 3 }, ttl: 300, tags: ['batch'])Compact
evicted = cache.compact # => 3 (number of expired entries removed)Refresh
cache.refresh('key', ttl: 600) # => true (reset TTL without changing value)Hash-like Access
cache["user:1"] = { name: "Alice" }
cache["user:1"] # => { name: "Alice" }LRU Eviction
cache = Philiprehberger::CacheKit::Store.new(max_size: 3)
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
cache.set("d", 4) # evicts "a" (least recently used)
cache.get("a") # => nil
cache.get("d") # => 4Pruning Expired Entries
cache.set("a", 1, ttl: 1)
cache.set("b", 2, ttl: 1)
cache.set("c", 3)
# ... after TTL expires ...
cache.prune # => 2 (removed expired entries)Stats
cache.set("a", 1)
cache.get("a") # hit
cache.get("missing") # miss
cache.stats
# => { size: 1, hits: 1, misses: 1, evictions: 0 }Stats by Tag
Track per-tag hit, miss, and eviction counters.
cache.set("user:1", data, tags: ["users"])
cache.set("post:1", data, tags: ["posts"])
cache.get("user:1") # hit on "users" tag
cache.stats(tag: "users")
# => { hits: 1, misses: 0, evictions: 0 }
cache.stats(tag: "posts")
# => { hits: 0, misses: 0, evictions: 0 }TTL Introspection
Read the remaining or absolute expiration without consuming the entry.
cache.set("session", "abc", ttl: 60)
cache.ttl("session") # => 59.87 (Float seconds)
cache.expire_at("session") # => 2026-04-14 14:41:23 +0000 (Time)
cache.set("permanent", "x")
cache.ttl("permanent") # => nil (no TTL)
cache.ttl("missing") # => nilBulk Delete
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
cache.delete_many("a", "b", "missing") # => 2 (count actually removed)
cache.keys # => ["c"]Keys by Tag
Inspect which keys carry a tag without invalidating them.
cache.set("user:1", data1, tags: ["users"])
cache.set("user:2", data2, tags: ["users"])
cache.set("post:1", data3, tags: ["posts"])
cache.keys_by_tag("users") # => ["user:1", "user:2"]
cache.keys_by_tag(:users) # => ["user:1", "user:2"]
cache.keys_by_tag("none") # => []Atomic Counters
Atomically increment and decrement numeric entries. Missing or expired keys are initialized to 0 before the delta is applied.
cache.increment("views") # => 1
cache.increment("views") # => 2
cache.increment("views", by: 5) # => 7
cache.increment("views", ttl: 3600) # => 8 (and resets TTL to 3600s)
cache.decrement("quota", by: 2) # => -2 (if "quota" was missing)Non-numeric values raise Philiprehberger::CacheKit::Error.
Snapshot and Restore
Serialize cache state for warm restarts. The snapshot captures all entries with their remaining TTL, tags, and LRU order.
# Save state before shutdown
data = cache.snapshot
File.write("cache.bin", Marshal.dump(data))
# Restore on startup
new_cache = Philiprehberger::CacheKit::Store.new(max_size: 500)
new_cache.restore(Marshal.load(File.read("cache.bin")))API
| Method | Description |
|---|---|
Store.new(max_size: 1000) |
Create a cache with max entries |
Store#get(key) |
Get a value (nil if missing/expired) |
Store#set(key, value, ttl:, tags:) |
Store a value |
Store#fetch(key, ttl:, tags:, &block) |
Get or compute a value (thread-safe) |
Store#delete(key) |
Delete a key |
Store#invalidate_tag(tag) |
Remove all entries with a tag |
Store#clear |
Remove all entries |
Store#size |
Number of entries |
Store#key?(key) |
Check if a key exists and is not expired |
Store#keys |
Returns all non-expired keys |
Store#[](key) |
Hash-like read (alias for get) |
Store#[]=(key, value) |
Hash-like write (alias for set without TTL/tags) |
Store#stats |
Returns { size:, hits:, misses:, evictions: }
|
Store#stats(tag: name) |
Returns { hits:, misses:, evictions: } for a tag |
Store#prune |
Remove all expired entries, returns count removed |
Store#on_evict { |key, value| } |
Register eviction callback |
Store#get_many(*keys) |
Batch get, returns { key => value } for found entries |
#set_many(hash, ttl:, tags:) |
Bulk set multiple entries |
#compact |
Prune expired entries, return eviction count |
#refresh(key, ttl:) |
Reset TTL without changing value |
Store#snapshot |
Serialize cache state to a hash |
Store#restore(data) |
Restore cache state from a snapshot |
Store#ttl(key) |
Remaining seconds until expiry (nil if none/missing/expired) |
Store#expire_at(key) |
Absolute expiration Time (nil if none/missing/expired) |
Store#delete_many(*keys) |
Bulk-delete, returns count removed |
Store#keys_by_tag(tag) |
Keys associated with a tag (non-expired) |
Store#increment(key, by:, ttl:) |
Atomic numeric increment |
Store#decrement(key, by:, ttl:) |
Atomic numeric decrement |
Development
bundle install
bundle exec rspec
bundle exec rubocopSupport
If you find this project useful: