type_balancer_rails 0.2.7 → 0.2.8

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: 3d4ba73db98a7266039db3cf00de37983a4348514d93f7ae1420f4d87be88144
4
- data.tar.gz: 54a10ed555eaaae52dc37b3ca0e8ed86a6f5d61d155333e08ad4fad9da2baa58
3
+ metadata.gz: 9866aa8259f829d620cf73c8646ca94d0df346fb919f2b9c162541e0206e39d7
4
+ data.tar.gz: 1869524e7b9660adae5e6a86e8e6807306d61ad6927ad2192005b12236f2ef09
5
5
  SHA512:
6
- metadata.gz: f289f1d2feadaf05fd1b1e2d4e5b51cd1fa1a6809ccce0fa5ff27347c0c652dc20523ff776339ec2e9a7c5ec3a8f67da9f97d0a1a2ed53287220da3206ed6ec8
7
- data.tar.gz: c18b3c8cc22192f5df37c61e3633406c75718e9c2e27a1d34a16838198b4bcf8b95a267e104e9dbb849c3a0d7991f607d9110a4a18146ec6a6b54faa7fb653ae
6
+ metadata.gz: 98609db2225b4e762f7445bcc2fcf675e7995c49b1c41699ae5e5c2c706f34e0dbe88945b6bcf72f0651c2f92e2648c0515bda048df0631dd322e0abf3caaa5e
7
+ data.tar.gz: 2f8371a767e3301c57ffb3cf9bc16ae9e9e5e6ffe381ef97230632837eef7e9157f2968e40735fc00c8a2662e1f5dc56f5fb12644ed298b36a6162e0c5fe1d74
data/CHANGELOG.md CHANGED
@@ -1,4 +1,29 @@
1
- ## [Unreleased]
1
+ ## [0.2.8] - 2025-05-10
2
+
3
+ - **Rails-style configuration block:**
4
+ You can now configure TypeBalancer Rails in an initializer using:
5
+ ```ruby
6
+ TypeBalancer::Rails.configure do |config|
7
+ config.cache_adapter = TypeBalancer::Rails::CacheAdapter.new
8
+ config.cache_expiry_seconds = 600
9
+ end
10
+ ```
11
+ Direct assignment is still supported for backward compatibility.
12
+
13
+ - **Per-request cache control:**
14
+ `balance_by_type` now accepts:
15
+ - `expires_in:` (override cache expiry for a single call)
16
+ - `cache_reset:` (force cache refresh for a single call)
17
+
18
+ - **Global cache expiry configuration:**
19
+ Set the default cache expiry for all balanced queries via `TypeBalancer::Rails.cache_expiry_seconds`.
20
+
21
+ - **Cache clearing:**
22
+ Use `TypeBalancer::Rails.clear_cache!` to clear all cached balanced results (e.g., from a console or admin task).
23
+
24
+ ### Changed
25
+ - **Caching and pagination are always enabled** for performance and reliability.
26
+ - **Cache keys are now isolated** per model and type field, preventing cross-contamination.
2
27
 
3
28
  ## [0.2.7] - 2025-05-04
4
29
 
data/README.md CHANGED
@@ -26,6 +26,38 @@ Or install it yourself as:
26
26
  $ gem install type_balancer_rails
27
27
  ```
28
28
 
29
+ ## Caching and Performance
30
+
31
+ Type balancing can be computationally expensive, especially on large datasets. To ensure efficient performance, **TypeBalancer Rails automatically caches the balanced ID list for each query**. This means:
32
+ - The balancing algorithm only runs when needed (on cache miss or reset).
33
+ - Subsequent requests for the same query use the cached result, reducing database and CPU load.
34
+ - Caching is essential for production use. Disabling or misconfiguring cache may result in slow queries.
35
+
36
+ **Adjust cache settings thoughtfully:**
37
+ - Shorter expiries mean fresher data but more frequent recalculation.
38
+ - Longer expiries improve performance but may serve stale results if your data changes often.
39
+
40
+ ## Configuration
41
+
42
+ To customize caching and performance, use the Rails-style configuration block in an initializer (e.g., `config/initializers/type_balancer.rb`):
43
+
44
+ ```ruby
45
+ TypeBalancer::Rails.configure do |config|
46
+ # Use the default cache adapter (backed by Rails.cache)
47
+ config.cache_adapter = TypeBalancer::Rails::CacheAdapter.new
48
+
49
+ # Set the global cache expiry (in seconds)
50
+ # Default is 600 (10 minutes). Adjust as needed for your app's freshness/performance needs.
51
+ config.cache_expiry_seconds = 600
52
+ end
53
+ ```
54
+
55
+ > **Note:** You can also set these options directly
56
+ > ```ruby
57
+ > TypeBalancer::Rails.cache_adapter = TypeBalancer::Rails::CacheAdapter.new
58
+ > TypeBalancer::Rails.cache_expiry_seconds = 600
59
+ > ```
60
+
29
61
  ## Usage
30
62
 
31
63
  To balance records by a given type field, use the following syntax:
@@ -112,6 +144,52 @@ Balanced results are cached by default for 10 minutes to improve performance and
112
144
 
113
145
  > **Note:** If you need to retrieve all balanced records, you must manually iterate through all pages.
114
146
 
147
+ ### Per-request Cache Control
148
+
149
+ You can override cache behavior for a single call to `balance_by_type`:
150
+
151
+ - **Custom Expiry:**
152
+ ```ruby
153
+ # Cache the balanced results for 1 hour for this request only
154
+ @posts = Post.all.balance_by_type(type_field: :category, expires_in: 1.hour)
155
+ ```
156
+ - **Force Cache Reset:**
157
+ ```ruby
158
+ # Force recalculation and cache update for this request
159
+ @posts = Post.all.balance_by_type(type_field: :category, cache_reset: true)
160
+ ```
161
+
162
+ #### Controller Example
163
+
164
+ You can pass these options from controller params:
165
+
166
+ ```ruby
167
+ class PostsController < ApplicationController
168
+ def index
169
+ @posts = Post.all.balance_by_type(
170
+ type_field: params[:type_field],
171
+ expires_in: params[:expires_in],
172
+ cache_reset: params[:cache_reset].present?
173
+ )
174
+ end
175
+ end
176
+ ```
177
+
178
+ ## Cache Management and Isolation
179
+
180
+ - **Cache keys are unique per model and type field.**
181
+ - There is no cross-contamination between different models or type fields.
182
+ - If you use multiple type fields or models, each will have its own cache entry.
183
+ - To avoid stale data, clear the cache after bulk updates or schema changes using `TypeBalancer::Rails.clear_cache!`.
184
+
185
+ ## Upgrade Notes
186
+
187
+ - `balance_by_type` now supports per-request `expires_in` and `cache_reset` options.
188
+ - Global cache expiry is configurable via `TypeBalancer::Rails.cache_expiry_seconds`.
189
+ - You can clear all cached results with `TypeBalancer::Rails.clear_cache!`.
190
+ - Caching is always enabled and required for performance.
191
+ - Pagination is always enabled; you must page through results if you want all records.
192
+
115
193
  ## Planned Enhancements
116
194
 
117
195
  - Support for passing a symbol directly to `balance_by_type`, e.g., `balance_by_type(:media_type)`, for more ergonomic usage. This is planned for a future version.
data/example/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- type_balancer_rails (0.2.7)
4
+ type_balancer_rails (0.2.8)
5
5
  activerecord (>= 7.0, < 9.0)
6
6
  activesupport (>= 7.0, < 9.0)
7
- type_balancer (~> 0.2.1)
7
+ type_balancer (>= 0.2.1)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
@@ -20,7 +20,7 @@ Rails.application.configure do
20
20
 
21
21
  # Show full error reports.
22
22
  config.consider_all_requests_local = true
23
- config.cache_store = :null_store
23
+ config.cache_store = :memory_store
24
24
 
25
25
  # Render exception templates for rescuable exceptions and raise for other exceptions.
26
26
  config.action_dispatch.show_exceptions = :rescuable
@@ -0,0 +1,65 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe PostsController, type: :controller do
4
+ fixtures :posts
5
+
6
+ before do
7
+ Rails.cache.clear
8
+ allow(TypeBalancer).to receive(:balance).and_call_original
9
+ end
10
+
11
+ describe 'GET #index' do
12
+ it 'caches the balanced ID list and reuses it' do
13
+ # First call: should invoke the balancer
14
+ get :index
15
+ expect(TypeBalancer).to have_received(:balance).once
16
+ first_result = assigns(:posts).map(&:id)
17
+
18
+ # Second call: should use the cache, not call balancer again
19
+ get :index
20
+ expect(TypeBalancer).to have_received(:balance).once
21
+ second_result = assigns(:posts).map(&:id)
22
+ expect(second_result).to eq(first_result)
23
+ end
24
+
25
+ it 'resets the cache when requested via controller param' do
26
+ # Prime the cache
27
+ get :index
28
+ expect(TypeBalancer).to have_received(:balance).once
29
+ first_result = assigns(:posts).map(&:id)
30
+
31
+ # Simulate cache reset by passing param (assuming controller supports it)
32
+ allow(TypeBalancer).to receive(:balance).and_call_original # reset spy
33
+ get :index, params: { cache_reset: true }
34
+ expect(TypeBalancer).to have_received(:balance).once
35
+ second_result = assigns(:posts).map(&:id)
36
+ expect(second_result).to eq(first_result) # Should still be balanced, but balancer called again
37
+ end
38
+
39
+ it 'isolates cache by type field' do
40
+ get :index, params: { type_field: 'media_type' }
41
+ expect(TypeBalancer).to have_received(:balance).once
42
+ allow(TypeBalancer).to receive(:balance).and_call_original # reset spy
43
+ get :index, params: { type_field: 'other_type' }
44
+ expect(TypeBalancer).to have_received(:balance).once
45
+ end
46
+
47
+ it 'respects per-request expires_in option' do
48
+ get :index, params: { expires_in: 1 }
49
+ expect(TypeBalancer).to have_received(:balance).once
50
+ sleep 2
51
+ allow(TypeBalancer).to receive(:balance).and_call_original # reset spy
52
+ get :index, params: { expires_in: 1 }
53
+ expect(TypeBalancer).to have_received(:balance).once
54
+ end
55
+
56
+ it 'expires cache and calls balancer again after expiry' do
57
+ get :index, params: { expires_in: 1 }
58
+ expect(TypeBalancer).to have_received(:balance).once
59
+ sleep 2
60
+ allow(TypeBalancer).to receive(:balance).and_call_original # reset spy
61
+ get :index, params: { expires_in: 1 }
62
+ expect(TypeBalancer).to have_received(:balance).once
63
+ end
64
+ end
65
+ end
@@ -19,6 +19,7 @@ RSpec.describe Content, type: :model do
19
19
 
20
20
  describe 'type balancing with real records' do
21
21
  before do
22
+ TypeBalancer::Rails.clear_cache!
22
23
  described_class.delete_all
23
24
  @content1 = described_class.create!(title: 'Content 1', content_type: 'blog')
24
25
  @content2 = described_class.create!(title: 'Content 2', content_type: 'news')
@@ -16,6 +16,7 @@ RSpec.describe Post, type: :model do
16
16
  let(:another_video_post) { described_class.create!(title: 'Post 3', media_type: 'video') }
17
17
 
18
18
  before do
19
+ TypeBalancer::Rails.clear_cache!
19
20
  described_class.delete_all
20
21
  video_post; article_post; another_video_post # Create the records
21
22
  end
Binary file
@@ -28,6 +28,11 @@ module TypeBalancer
28
28
  @memory_cache.delete(key)
29
29
  end
30
30
  end
31
+
32
+ def clear_cache!
33
+ ::Rails.cache.clear if defined?(::Rails) && ::Rails.respond_to?(:cache) && ::Rails.cache
34
+ @memory_cache.clear
35
+ end
31
36
  end
32
37
  end
33
38
  end
@@ -11,8 +11,15 @@ module TypeBalancer
11
11
  def balance_by_type(options = {})
12
12
  type_field, offset, per_page = pagination_params(options)
13
13
  cache_key = build_cache_key(type_field)
14
- ids = TypeBalancer::Rails.cache_adapter.fetch(cache_key, expires_in: 10.minutes) do
15
- compute_ids(type_field)
14
+ expires_in = options[:expires_in] || ::TypeBalancer::Rails.cache_expiry_seconds
15
+ cache_reset = options[:cache_reset]
16
+ if cache_reset
17
+ ids = compute_ids(type_field)
18
+ TypeBalancer::Rails.cache_adapter.write(cache_key, ids, expires_in: expires_in)
19
+ else
20
+ ids = TypeBalancer::Rails.cache_adapter.fetch(cache_key, expires_in: expires_in) do
21
+ compute_ids(type_field)
22
+ end
16
23
  end
17
24
  page_ids = ids[offset, per_page] || []
18
25
  return empty_relation if page_ids.empty?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TypeBalancer
4
4
  module Rails
5
- VERSION = '0.2.7'
5
+ VERSION = '0.2.8'
6
6
  end
7
7
  end
@@ -11,8 +11,18 @@ require_relative 'rails/cache_adapter'
11
11
  module TypeBalancer
12
12
  module Rails
13
13
  class << self
14
- attr_accessor :cache_adapter
14
+ attr_accessor :cache_adapter, :cache_expiry_seconds
15
+
16
+ def clear_cache!
17
+ cache_adapter&.clear_cache!
18
+ end
19
+
20
+ # Rails-style configuration block
21
+ def configure
22
+ yield self if block_given?
23
+ end
15
24
  end
16
25
  self.cache_adapter ||= TypeBalancer::Rails::CacheAdapter.new
26
+ self.cache_expiry_seconds ||= 600 # 10 minutes default
17
27
  end
18
28
  end
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  # Runtime dependencies
33
33
  spec.add_dependency 'activerecord', '>= 7.0', '< 9.0'
34
34
  spec.add_dependency 'activesupport', '>= 7.0', '< 9.0'
35
- spec.add_dependency 'type_balancer', '~> 0.2.1'
35
+ spec.add_dependency 'type_balancer', '>= 0.2.1'
36
36
 
37
37
  # For more information and examples about making a new gem, check out our
38
38
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: type_balancer_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Smith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-05 00:00:00.000000000 Z
11
+ date: 2025-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -54,14 +54,14 @@ dependencies:
54
54
  name: type_balancer
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - "~>"
57
+ - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: 0.2.1
60
60
  type: :runtime
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - "~>"
64
+ - - ">="
65
65
  - !ruby/object:Gem::Version
66
66
  version: 0.2.1
67
67
  description: Provides Rails integration for the type_balancer gem
@@ -147,6 +147,7 @@ files:
147
147
  - example/lib/tasks/dev_fixtures.rake
148
148
  - example/script/.keep
149
149
  - example/spec/controllers/contents_controller_spec.rb
150
+ - example/spec/controllers/posts_controller_caching_spec.rb
150
151
  - example/spec/controllers/posts_controller_spec.rb
151
152
  - example/spec/features/contents_balancing_spec.rb
152
153
  - example/spec/features/posts_balancing_spec.rb