Probabilistic caching toolkit for Ruby. Instead of always hitting the database or always returning a cached value, BipolarCache flips a coin — sometimes you get the cache, sometimes you get the real thing, and when the real thing differs, the cache gets updated.
Useful for caching database counters and other operations where occasional staleness is acceptable and you want to gradually reduce database load without explicit cache invalidation.
BipolarCache.read! → should we cache? (if)
│
no ────┘──── yes
│ │
return actual read cached value
│
chance(cached) > rand?
│
yes ──┘── no
│ │
return compute actual
cached │
cached != actual?
│
yes ──┘── no
│ │
update return
cache actual
return
actual
Add to your Gemfile:
gem"bipolar_cache"Or install directly:
$ gem install bipolar_cache
BipolarCache.read! accepts six keyword arguments, all callables (procs/lambdas):
result=BipolarCache.read!(actual: ->{Post.where(user_id: user.id).count},# the real value (expensive)cached: ->{user.posts_count_cache},# the cached value (cheap)chance: ->(cached_value){cached_value < 10 ? 0.1 : 0.9},# probability of cache hitif: ->{user.caching_enabled?},# enable/disable cachingupdate: ->(value){user.update(posts_count_cache: value)},# write new value to cacherescue: ->(error){Rails.logger.error(error);0}# handle errors)| Parameter | Required | Description |
|---|---|---|
actual | yes | Callable that returns the real value (e.g., a database query) |
cached | yes | Callable that returns the cached value |
chance | yes | Callable that receives the cached value and returns a float 0.0-1.0. Higher = more likely to use cache. |
if | yes | Callable that returns true/false. When false, always returns the actual value. |
update | yes | Callable that receives the actual value and persists it to the cache |
rescue | no | Callable that receives a StandardError. Without it, errors re-raise. |
For Sequel models, the plugin generates all the procs automatically. Your model needs a _count_cache column (e.g., comments_count_cache):
require"bipolar_cache/sequel/plugin_alpha"classUser < Sequel::ModelincludeBipolarCache::Sequel::PluginAlpha# Assumes:# - association: user.comments_dataset# - cache column: user.comments_count_cachebipolar_count_cache:commentsendThis generates four instance methods:
user.comments_count# probabilistic: returns cached or actual countuser.comments_count_refresh!# force: computes actual and updates cacheuser.comments_count_increment!# bump cache by 1 (or by: n)user.comments_count_decrement!# drop cache by 1 (or by: n)bipolar_count_cache:comments,method: "comment_total",# custom method name (default: "{name}_count")chance: 75,# fixed probability as percentage (default: adaptive)actual: ->{comments_dataset.where(visible: true).count},# custom actual proccached: :my_cache_column,# custom cache column/method nameupdate: ->(v){set(my_col: v)},# custom update procrescue: ->(e){log(e);nil},# custom error handlerif: ->{ !new?}# disable caching for unsaved recordsThe chance option controls cache-hit probability:
| Value | Interpretation | Example |
|---|---|---|
0.0 - 1.0 (Float) | Direct probability | chance: 0.9 = 90% cache hit |
1 (Integer) | Direct probability (1.0 = 100%) | chance: 1 = always cache |
2 - 100 (Integer) | Percentage, divided by 100 | chance: 75 = 75% cache hit |
0 | Never use cache | chance: 0 = always compute actual |
| Proc | Dynamic, receives cached value | chance: ->(v) { v > 100 ? 0.95 : 0.5 } |
| (omitted) | Adaptive default | < 10: 10% cache hit, >= 10: 90% cache hit |
bin/setup # install dependencies
rake test # run tests
rake rubocop # lint
rake # run both
bin/console # interactive prompt
Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/bipolar_cache.