bipolar_cache 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14dba1b50e859c7210b98cc2fb557341169d4e97ca5e1e90cf82ed5eeb1a617f
4
- data.tar.gz: 2a26dd62e0ee0cb66e8bc2d4821831850a56d4a212b6574dbde3cb8043dc817a
3
+ metadata.gz: 590637758a1d41dcf7614360ca6c2b3497030558a34832091d710e5938079bae
4
+ data.tar.gz: f1fe7e9f5644cab310a0239d521b40ad10286f33b802473f38f25ac09ba9a351
5
5
  SHA512:
6
- metadata.gz: e10eb1702dcdea29b43c149a6cc364ec8ec999ab45c1334fa931087af7700324ffad07a782e38fa72cad5fbc6d2231a98615bf454da73ce447fd5b9621cc6573
7
- data.tar.gz: 21114e271dfec2fde22731c7ed81b24cd12fcc7286dbd5c4debda8be36f5a67ca795e99ce33629807cc3720b3f70b93fe54a203abbe0652cfd00dc74daa06a30
6
+ metadata.gz: f2fbc688f2d14f886d03f64b85483ee09c8a23c400aba8c6d397aeb742887237c34b1de2d5b4702034a78e61204a6c76c76045be708c46069defe70375e0bdd6
7
+ data.tar.gz: b2ba003ac34a61e45c2a7b57b3e3a138f867b2751b49fe4dd87998865658d0ee9c0c15672e9c695459e2cfad2fefbfa061e8b84364e5f8beb62904be71eea098
data/.rubocop.yml CHANGED
@@ -6,9 +6,19 @@ AllCops:
6
6
  Metrics/AbcSize:
7
7
  Enabled: false
8
8
 
9
+ Metrics/CyclomaticComplexity:
10
+ Enabled: false
11
+
12
+ Metrics/PerceivedComplexity:
13
+ Enabled: false
14
+
9
15
  Metrics/MethodLength:
10
16
  Enabled: false
11
17
 
18
+ Metrics/ClassLength:
19
+ Exclude:
20
+ - "test/**/*"
21
+
12
22
  Style/Documentation:
13
23
  Enabled: false
14
24
 
data/README.md CHANGED
@@ -1,26 +1,135 @@
1
1
  # BipolarCache
2
2
 
3
- Probabalistic caching toolkit useful for caching of database counters, and other operations.
3
+ 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.
4
+
5
+ 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.
6
+
7
+ ## How it works
8
+
9
+ ```
10
+ BipolarCache.read! → should we cache? (if)
11
+
12
+ no ────┘──── yes
13
+ │ │
14
+ return actual read cached value
15
+
16
+ chance(cached) > rand?
17
+
18
+ yes ──┘── no
19
+ │ │
20
+ return compute actual
21
+ cached │
22
+ cached != actual?
23
+
24
+ yes ──┘── no
25
+ │ │
26
+ update return
27
+ cache actual
28
+ return
29
+ actual
30
+ ```
4
31
 
5
32
  ## Installation
6
33
 
7
- Install the gem and add to the application's Gemfile by executing:
34
+ Add to your Gemfile:
8
35
 
9
- $ bundle add bipolar_cache
36
+ ```ruby
37
+ gem "bipolar_cache"
38
+ ```
10
39
 
11
- If bundler is not being used to manage dependencies, install the gem by executing:
40
+ Or install directly:
12
41
 
13
- $ gem install bipolar_cache
42
+ ```
43
+ $ gem install bipolar_cache
44
+ ```
14
45
 
15
46
  ## Usage
16
47
 
48
+ ### Core API
17
49
 
50
+ `BipolarCache.read!` accepts six keyword arguments, all callables (procs/lambdas):
18
51
 
19
- ## Development
52
+ ```ruby
53
+ result = BipolarCache.read!(
54
+ actual: -> { Post.where(user_id: user.id).count }, # the real value (expensive)
55
+ cached: -> { user.posts_count_cache }, # the cached value (cheap)
56
+ chance: ->(cached_value) { cached_value < 10 ? 0.1 : 0.9 }, # probability of cache hit
57
+ if: -> { user.caching_enabled? }, # enable/disable caching
58
+ update: ->(value) { user.update(posts_count_cache: value) }, # write new value to cache
59
+ rescue: ->(error) { Rails.logger.error(error); 0 } # handle errors
60
+ )
61
+ ```
62
+
63
+ | Parameter | Required | Description |
64
+ |-----------|----------|-------------|
65
+ | `actual` | yes | Callable that returns the real value (e.g., a database query) |
66
+ | `cached` | yes | Callable that returns the cached value |
67
+ | `chance` | yes | Callable that receives the cached value and returns a float 0.0-1.0. Higher = more likely to use cache. |
68
+ | `if` | yes | Callable that returns true/false. When false, always returns the actual value. |
69
+ | `update` | yes | Callable that receives the actual value and persists it to the cache |
70
+ | `rescue` | no | Callable that receives a `StandardError`. Without it, errors re-raise. |
71
+
72
+ ### Sequel Plugin (alpha)
73
+
74
+ For [Sequel](https://github.com/jeremyevans/sequel) models, the plugin generates all the procs automatically. Your model needs a `_count_cache` column (e.g., `comments_count_cache`):
75
+
76
+ ```ruby
77
+ require "bipolar_cache/sequel/plugin_alpha"
78
+
79
+ class User < Sequel::Model
80
+ include BipolarCache::Sequel::PluginAlpha
20
81
 
21
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
82
+ # Assumes:
83
+ # - association: user.comments_dataset
84
+ # - cache column: user.comments_count_cache
85
+ bipolar_count_cache :comments
86
+ end
87
+ ```
88
+
89
+ This generates four instance methods:
90
+
91
+ ```ruby
92
+ user.comments_count # probabilistic: returns cached or actual count
93
+ user.comments_count_refresh! # force: computes actual and updates cache
94
+ user.comments_count_increment! # bump cache by 1 (or by: n)
95
+ user.comments_count_decrement! # drop cache by 1 (or by: n)
96
+ ```
97
+
98
+ #### Options
99
+
100
+ ```ruby
101
+ bipolar_count_cache :comments,
102
+ method: "comment_total", # custom method name (default: "{name}_count")
103
+ chance: 75, # fixed probability as percentage (default: adaptive)
104
+ actual: -> { comments_dataset.where(visible: true).count }, # custom actual proc
105
+ cached: :my_cache_column, # custom cache column/method name
106
+ update: ->(v) { set(my_col: v) }, # custom update proc
107
+ rescue: ->(e) { log(e); nil }, # custom error handler
108
+ if: -> { !new? } # disable caching for unsaved records
109
+ ```
110
+
111
+ #### Chance values
112
+
113
+ The `chance` option controls cache-hit probability:
114
+
115
+ | Value | Interpretation | Example |
116
+ |-------|---------------|---------|
117
+ | `0.0` - `1.0` (Float) | Direct probability | `chance: 0.9` = 90% cache hit |
118
+ | `1` (Integer) | Direct probability (1.0 = 100%) | `chance: 1` = always cache |
119
+ | `2` - `100` (Integer) | Percentage, divided by 100 | `chance: 75` = 75% cache hit |
120
+ | `0` | Never use cache | `chance: 0` = always compute actual |
121
+ | Proc | Dynamic, receives cached value | `chance: ->(v) { v > 100 ? 0.95 : 0.5 }` |
122
+ | _(omitted)_ | Adaptive default | < 10: 10% cache hit, >= 10: 90% cache hit |
123
+
124
+ ## Development
22
125
 
23
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
126
+ ```
127
+ bin/setup # install dependencies
128
+ rake test # run tests
129
+ rake rubocop # lint
130
+ rake # run both
131
+ bin/console # interactive prompt
132
+ ```
24
133
 
25
134
  ## Contributing
26
135
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../bipolar_cache"
4
+
3
5
  module BipolarCache
4
6
  module Sequel
5
7
  module PluginAlpha
@@ -21,14 +23,24 @@ module BipolarCache
21
23
 
22
24
  define_method "#{method_name}_increment!" do |by: 1|
23
25
  procs = fetch_or_build_procs(**opts)
26
+ new_value = by + (procs[:cached].call || 0)
27
+ procs[:update].call(new_value)
28
+ new_value
29
+ rescue StandardError => e
30
+ raise unless procs[:rescue].is_a?(Proc)
24
31
 
25
- procs[:update].call(by + procs[:cached].call)
32
+ procs[:rescue].call(e)
26
33
  end
27
34
 
28
35
  define_method "#{method_name}_decrement!" do |by: 1|
29
36
  procs = fetch_or_build_procs(**opts)
37
+ new_value = (-by) + (procs[:cached].call || 0)
38
+ procs[:update].call(new_value)
39
+ new_value
40
+ rescue StandardError => e
41
+ raise unless procs[:rescue].is_a?(Proc)
30
42
 
31
- procs[:update].call((-by) + procs[:cached].call)
43
+ procs[:rescue].call(e)
32
44
  end
33
45
  end
34
46
  end
@@ -54,7 +66,7 @@ module BipolarCache
54
66
  chance: bcsp_proc_chance_from(**opts),
55
67
  rescue: bcsp_proc_rescue_from(**opts),
56
68
  if: bcsp_proc_if_from(**opts)
57
- }
69
+ }.freeze
58
70
  end
59
71
 
60
72
  # stored procs
@@ -94,7 +106,7 @@ module BipolarCache
94
106
  when Proc
95
107
  opts[:update]
96
108
  when String, Symbol
97
- -> { send(opts[:update]) }
109
+ ->(value) { send(opts[:update], value) }
98
110
  else
99
111
  ->(value) { update({ cache_name => value }) }
100
112
  end
@@ -105,14 +117,17 @@ module BipolarCache
105
117
  when Proc
106
118
  opts[:chance]
107
119
  when Integer, Float
120
+ raise ArgumentError, "chance must be >= 0, got #{opts[:chance]}" if opts[:chance].negative?
121
+ raise ArgumentError, "chance must be <= 100, got #{opts[:chance]}" if opts[:chance] > 100
122
+
108
123
  normalised_chance = if opts[:chance] > 1
109
- opts[:chance] / 100
124
+ opts[:chance] / 100.0
110
125
  else
111
126
  opts[:chance]
112
127
  end
113
128
  ->(_value) { normalised_chance }
114
129
  else # default
115
- ->(value) { value < 10 ? 0.1 : 0.9 }
130
+ ->(value) { (value || 0) < 10 ? 0.1 : 0.9 }
116
131
  end
117
132
  end
118
133
 
@@ -121,7 +136,7 @@ module BipolarCache
121
136
  opts[:rescue]
122
137
  else # default
123
138
  lambda { |e|
124
- e.inspect
139
+ warn "[BipolarCache] #{e.class}: #{e.message}"
125
140
  0
126
141
  }
127
142
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BipolarCache
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/bipolar_cache.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "bipolar_cache/version"
4
- require_relative "bipolar_cache/sequel/plugin_alpha"
5
4
 
6
5
  module BipolarCache
7
6
  class Error < StandardError; end
@@ -34,6 +33,8 @@ module BipolarCache
34
33
  actual_value
35
34
  end
36
35
  rescue StandardError => e
37
- procs[:rescue].call(e) if procs[:rescue].is_a?(Proc)
36
+ raise unless procs[:rescue].is_a?(Proc)
37
+
38
+ procs[:rescue].call(e)
38
39
  end
39
40
  end
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bipolar_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andriy Tyurnikov
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-04-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: Probabalistic caching toolkit useful for caching of database counters,
12
+ description: Probabilistic caching toolkit useful for caching of database counters,
14
13
  and other operations.
15
14
  email:
16
15
  - Andriy.Tyurnikov@gmail.com
@@ -29,9 +28,8 @@ homepage: https://github.com/rubakas/bipolar_cache
29
28
  licenses: []
30
29
  metadata:
31
30
  homepage_uri: https://github.com/rubakas/bipolar_cache
32
- source_code_uri: https://github.com/rubakas/bipolar_cache
33
- changelog_uri: https://github.com/rubakas/bipolar_cache
34
- post_install_message:
31
+ source_code_uri: https://github.com/rubakas/bipolar_cache/tree/main
32
+ changelog_uri: https://github.com/rubakas/bipolar_cache/blob/main/CHANGELOG.md
35
33
  rdoc_options: []
36
34
  require_paths:
37
35
  - lib
@@ -46,8 +44,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
44
  - !ruby/object:Gem::Version
47
45
  version: '0'
48
46
  requirements: []
49
- rubygems_version: 3.5.6
50
- signing_key:
47
+ rubygems_version: 4.0.8
51
48
  specification_version: 4
52
- summary: BipolarCache. Probabalistic caching toolkit.
49
+ summary: BipolarCache. Probabilistic caching toolkit.
53
50
  test_files: []