philiprehberger-memo
Practical memoization with TTL, LRU eviction, and thread-safety
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-memo"
Or install directly:
gem install philiprehberger-memo
Usage
require "philiprehberger/memo"
class UserService
include Philiprehberger::Memo
def find(id)
User.find(id)
end
memo :find
def expensive_query(filters)
# ...
end
memo :expensive_query, ttl: 300
def config
# ...
end
memo :config, max_size: 100
end
service = UserService.new
service.find(1) # executes query
service.find(1) # returns cached result
service.clear_memo(:find) # manual invalidation
service.clear_all_memos # clear everything
Cache Stats
Monitor hit/miss ratios for tuning TTL and max_size:
service = UserService.new
service.find(1) # miss
service.find(1) # hit
service.find(2) # miss
service.memo_stats(:find)
# => { hits: 1, misses: 2, hit_rate: 0.3333 }
Inspecting Cached Calls
Check whether a specific call is already cached, list the methods with live caches, and measure per-method cache size without invoking the method.
service = UserService.new
service.find(1)
service.memoized?(:find, 1) # => true
service.memoized?(:find, 2) # => false
service.cache_size(:find) # => 1
service.memo_keys # => [:find]
memoized? does not count as a hit or miss in memo_stats, so it is safe
to use in hot paths or test assertions.
Surgical Invalidation
Drop a single cached call signature without clearing the entire method cache.
service = UserService.new
service.find(1)
service.find(2)
service.forget_memo(:find, 1) # => true, only the (1) entry is removed
service.memoized?(:find, 1) # => false
service.memoized?(:find, 2) # => true
Pruning Expired Entries
Caches with a TTL can evict expired entries eagerly without resetting stats.
cache = Philiprehberger::Memo::Cache.new(ttl: 60)
cache.set(:a, 1)
sleep(61)
cache.set(:b, 2)
cache.prune_expired # => 1
cache.keys # => [:b]
Features
- Per-instance caching (not class-level)
- Handles
nilandfalsereturn values correctly - Optional TTL (time-based expiration)
- Optional max_size with LRU eviction
- Thread-safe with per-instance mutex
- Works with positional and keyword arguments
API
Philiprehberger::Memo (mixin)
| Method | Description |
|---|---|
memo :method_name, ttl: nil, max_size: nil |
Memoize a method with optional TTL and LRU eviction (class-level) |
#clear_memo(method_name) |
Clear cached results for a specific memoized method |
#clear_all_memos |
Clear all memoized caches on the instance |
#memo_stats(method_name) |
Return { hits:, misses:, hit_rate: } for a memoized method |
#memoized?(method_name, *args, **kwargs) |
Return true when a non-expired cached value exists for the call |
#cache_size(method_name) |
Number of cached entries for a memoized method (0 when absent) |
#memo_keys |
Names of methods that currently have caches on this instance |
#forget_memo(method_name, *args, **kwargs) |
Remove a single cached call signature; returns true when an entry is dropped |
Philiprehberger::Memo::Cache
| Method | Description |
|---|---|
.new(ttl: nil, max_size: nil) |
Create a cache with optional TTL (seconds) and max size |
#get(key) |
Fetch a cached value; returns [found, value] |
#set(key, value) |
Store a value in the cache |
#key?(key) |
true when a non-expired entry exists; does not affect stats |
#delete(key) |
Remove a specific entry; returns true when an entry was removed |
#keys |
All non-expired cache keys in LRU order |
#size |
Current number of cached entries |
#stats |
Return { hits:, misses:, hit_rate: } |
#prune_expired |
Remove all expired entries and return the count removed |
#clear |
Remove all entries and reset stats |
Philiprehberger::Memo::Wrapper
| Method | Description |
|---|---|
.apply(klass, method_name, ttl:, max_size:) |
Wrap a method with memoization via define_method |
Philiprehberger::Memo::Error
| Method | Description |
|---|---|
.new(...) |
Custom error class for memo-related failures (inherits StandardError) |
Development
bundle install
bundle exec rspec
bundle exec rubocop
Support
If you find this project useful: