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 hit
if: -> { user.caching_enabled? }, # enable/disable caching
update: ->(value) { user.update(posts_count_cache: value) }, # write new value to cache
rescue: ->(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"
class User < Sequel::Model
include BipolarCache::Sequel::PluginAlpha
# Assumes:
# - association: user.comments_dataset
# - cache column: user.comments_count_cache
bipolar_count_cache :comments
endThis generates four instance methods:
user.comments_count # probabilistic: returns cached or actual count
user.comments_count_refresh! # force: computes actual and updates cache
user.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 proc
cached: :my_cache_column, # custom cache column/method name
update: ->(v) { set(my_col: v) }, # custom update proc
rescue: ->(e) { log(e); nil }, # custom error handler
if: -> { !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.