The project is in a healthy, maintained state
A lightweight, thread-safe in-memory LRU cache with TTL expiration and tag-based bulk invalidation for Ruby applications.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-cache_kit

Tests Gem Version Last updated

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_kit

Usage

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
end

TTL Expiration

cache.set("session", "abc", ttl: 60) # expires in 60 seconds
cache.get("session")                  # => "abc"
# ... 60 seconds later ...
cache.get("session")                  # => nil

Tag-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:1

Eviction 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.prune

Batch 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") # => 4

Pruning 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")       # => nil

Bulk 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 rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT