support_table_cache 1.1.4 → 1.1.5

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: 5102ba8b7a75ecc7186dc527880ff0278f9e259b020fc8b36b8e3e72c4b0c150
4
- data.tar.gz: ebf2696d9ea3917895038b35989d2c23c5b418db4f405c4b36812e6da59f7691
3
+ metadata.gz: 9fe5a9190c32271b431faed20e0b6ddbeb195cd05c927cae94c03b5c7d20bc8a
4
+ data.tar.gz: 8f8bc59c1a0eb6240712e50fb34da28f45d071e1c6b59e28d647d8bcf7e17896
5
5
  SHA512:
6
- metadata.gz: eae75a3e8afea4fcf9cff14fec02110ab0f332685bd912ca3f37fdd17baeeaa2db107824cbd0f6bec18acdf501815012476fd48f50d63b3a38c0767daae7cf98
7
- data.tar.gz: 1be4a2c1013221d4af65eb26dbfd5e9c56eb130a4bdc94ce540854f11a3df7dd7526e264ba645f7e46340cd43de8bef8899837043031aeae8897e463113e4fc5
6
+ metadata.gz: c833468a94fed3f16efd45022c56cbf19c755d6f0e2f48a2d93b8b92c2d946bd46f246170a623f2edeade892aa017851d581794fc7505c4efd5c5bce3fdf29b9
7
+ data.tar.gz: d4307c2d653184fe890e8aa8af0c548ef340d817141fddcb69e8a083ffcd235fa7d0d124b98e6ece0dd99e5a8fc549db1a8a986acf571aada17cb6f0619c67e1
data/AGENTS.md ADDED
@@ -0,0 +1,74 @@
1
+ # Copilot Instructions for support_table_cache
2
+
3
+ ## Project Overview
4
+
5
+ This is a Ruby gem that adds transparent caching to ActiveRecord support/lookup tables. It intercepts `find_by` queries and `belongs_to` associations to cache small, rarely-changing reference tables (statuses, types, categories) without code changes.
6
+
7
+ **Core principle**: Cache entries keyed by unique attribute combinations, auto-invalidated on record changes via `after_commit` callbacks.
8
+
9
+ ## Architecture
10
+
11
+ - **lib/support_table_cache.rb**: Main module with `cache_by` DSL, cache configuration, and invalidation logic
12
+ - **lib/support_table_cache/find_by_override.rb**: Prepends to model class to intercept `find_by` calls
13
+ - **lib/support_table_cache/relation_override.rb**: Prepends to `ActiveRecord::Relation` to handle scoped queries (e.g., `where(group: 'x').find_by(name: 'y')`)
14
+ - **lib/support_table_cache/associations.rb**: Extends `belongs_to` with `cache_belongs_to` to cache foreign key lookups
15
+ - **lib/support_table_cache/memory_cache.rb**: In-process cache implementation (use `support_table_cache = :memory`)
16
+
17
+ See [ARCHITECTURE.md](../ARCHITECTURE.md) for detailed flow diagrams showing cache key generation, invalidation, and association caching sequences.
18
+
19
+ ## Key Patterns
20
+
21
+ ### Model Configuration
22
+ Models use `cache_by` to declare unique keys that can be cached. Support composite keys and case-insensitivity:
23
+ ```ruby
24
+ cache_by :name, case_sensitive: false
25
+ cache_by [:group, :code]
26
+ cache_by :name, where: {deleted_at: nil} # For default scopes
27
+ ```
28
+
29
+ ### Cache Key Structure
30
+ Cache keys are `[ClassName, {attr1: val1, attr2: val2}]` arrays with sorted attribute names. Case-insensitive values are downcased before keying.
31
+
32
+ ### Module Prepending Pattern
33
+ Uses `prepend` to wrap ActiveRecord methods (`find_by`) rather than monkey-patching. This allows `super` to call original behavior on cache misses or when caching disabled.
34
+
35
+ ## Testing
36
+
37
+ - **Multi-version testing**: Uses Appraisal gem to test against ActiveRecord 5.0-8.0 (see [Appraisals](../Appraisals))
38
+ - **Run tests**: `bundle exec rspec` (default rake task) or `bundle exec appraisal rspec` for all versions
39
+ - **Test setup**: In-memory SQLite database created in [spec/spec_helper.rb](../spec/spec_helper.rb) with test tables
40
+ - **Test isolation**: Tests wrapped with `SupportTableCache.testing!` in RSpec `config.before` to prevent cache pollution
41
+
42
+ ### Code Style
43
+ Use **standardrb** for linting. Run `standardrb --fix` before committing. CI enforces this on ActiveRecord 8.0 matrix entry.
44
+
45
+ ## Common Operations
46
+
47
+ ### Adding Cache Support to Models
48
+ 1. Include `SupportTableCache` in model class
49
+ 2. Call `cache_by` with unique key attributes
50
+ 3. Optionally set `self.support_table_cache_ttl = 5.minutes`
51
+ 4. For associations: include `SupportTableCache::Associations` in parent model, then `cache_belongs_to :association_name`
52
+
53
+ ### Cache Invalidation
54
+ Automatic via `after_commit` callback that clears all cache key variations (both old and new attribute values on updates). No manual invalidation needed unless using in-memory cache across processes.
55
+
56
+ ### Debugging Cache Behavior
57
+ - Use `fetch_by` instead of `find_by` to raise error if query won't hit cache
58
+ - Disable caching in block: `Model.disable_cache { ... }` or globally `SupportTableCache.disable { ... }`
59
+ - Check if caching enabled: inspect `support_table_cache_by_attributes` class attribute
60
+
61
+ ## Development Workflow
62
+
63
+ 1. **Running specs locally**: `bundle exec rspec` (uses Ruby 3.3+ and ActiveRecord 8.0 from Gemfile)
64
+ 2. **Testing specific AR version**: `bundle exec appraisal activerecord_7 rspec`
65
+ 3. **Generating all gemfiles**: `bundle exec appraisal generate`
66
+ 4. **Lint before commit**: `standardrb --fix`
67
+ 5. **Release**: Only from `main` branch (enforced by `Rakefile` pre-release check)
68
+
69
+ ## Important Constraints
70
+
71
+ - **Target models**: Only for small tables (few hundred rows max)
72
+ - **Unique keys only**: `cache_by` attributes must define unique constraints
73
+ - **No runtime scopes**: Cannot use `cache_belongs_to` with scoped associations (checked at configuration time)
74
+ - **In-memory cache caveat**: Per-process, not invalidated across processes—only use for truly static data or with TTL
data/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.1.5
8
+
9
+ ### Fixed
10
+
11
+ - Replaced thread local variables with fiber local variables to prevent the possibility of behavior from leaking across fibers when disabling the cache in a block.
12
+ - Allow setting the cache to an in-memory cache by setting `support_table_cache` to `true`.
13
+
7
14
  ## 1.1.4
8
15
 
9
16
  ### Fixed
@@ -13,21 +20,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
20
  ## 1.1.3
14
21
 
15
22
  ### Fixed
23
+
16
24
  - Avoid calling methods that require a database connection when setting up belongs to caching.
17
25
 
18
26
  ## 1.1.2
19
27
 
20
28
  ### Fixed
29
+
21
30
  - Do not cache records where only some of the columns have been loaded with a call to `select`.
22
31
 
23
32
  ## 1.1.1
24
33
 
25
34
  ### Fixed
35
+
26
36
  - Fixed disabled and disable_cache methods to yield a block to match the documentation.
27
37
 
28
38
  ## 1.1.0
29
39
 
30
40
  ### Added
41
+
31
42
  - Added fetch_by and fetch_by! methods that can verify the result will be cacheable.
32
43
  - Allow configuring cache storage on a per class basis.
33
44
  - Allow disabling caching on per class basis.
@@ -36,15 +47,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
36
47
  - Added test mode to intialize new caches within a test block.
37
48
 
38
49
  ### Changed
50
+
39
51
  - Changed fiber local variables used for disabling the cache to thread local variables.
40
52
  - Using find_by! on a relation will now use the cache.
41
53
 
42
54
  ## 1.0.1
43
55
 
44
56
  ### Added
57
+
45
58
  - Preserve scope on relations terminated with a `find_by`.
46
59
 
47
60
  ## 1.0.0
48
61
 
49
62
  ### Added
63
+
50
64
  - Add SupportTableCache concern to enable automatic caching on models when calling `find_by` with unique key parameters.
data/README.md CHANGED
@@ -19,6 +19,18 @@ end
19
19
 
20
20
  With this gem, you can avoid the database query associated with the `find_by` call. You don't need to alter your code in any way other than to include `SupportTableCache` in your model and telling it the attributes that comprise a unique key, which can be used for caching.
21
21
 
22
+ ## Table of Contents
23
+
24
+ - [Usage](#usage)
25
+ - [Setting the Cache](#setting-the-cache)
26
+ - [Disabling Caching](#disabling-caching)
27
+ - [Caching Belongs to Associations](#caching-belongs-to-associations)
28
+ - [Testing](#testing)
29
+ - [Companion Gems](#companion-gems)
30
+ - [Installation](#installation)
31
+ - [Contributing](#contributing)
32
+ - [License](#license)
33
+
22
34
  ## Usage
23
35
 
24
36
  To use the gem, you need to include it in you models and then specify which attributes can be used for caching with the `cache_by` method. A caching attribute must be a unique key on the model. For a composite unique key, you can specify an array of attributes. If any of the attributes are case-insensitive strings, you need to specify that as well.
@@ -115,6 +127,9 @@ end
115
127
 
116
128
  You can include `SupportTableCache::Associations` in your `ApplicationRecord` class to make association caching available on all models.
117
129
 
130
+ > [!NOTE]
131
+ > You still need to set up the target model to cache by the primary key used by the belongs to association. Otherwise the association will not be cached.
132
+
118
133
  ### Testing
119
134
 
120
135
  Caching may interfere with tests by allowing data created in one test to leak into subsequent tests. You can resolve this by wrapping your tests with the `SupportTableCache.testing!` method.
@@ -132,14 +147,16 @@ class MiniTest::Spec
132
147
  around do |tests|
133
148
  SupportTableCache.testing!(&tests)
134
149
  end
135
- =end
136
-
150
+ end
137
151
  ```
138
152
 
139
- ### Maintaining Data
153
+ ### Companion Gems
140
154
 
141
155
  You can use the companion [support_table_data gem](https://github.com/bdurand/support_table_data) to provide functionality for loading static data into your support tables as well as adding helper functions to make looking up specific rows much easier.
142
156
 
157
+ > [!TIP]
158
+ > The [support_table](https://github.com/bdurand/support_table) gem combines both gems in a drop in solution for Rails applications.
159
+
143
160
  ## Installation
144
161
 
145
162
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.4
1
+ 1.1.5
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SupportTableCache
4
+ # Utility class for managing fiber-local variables. This implementation
5
+ # does not pollute the global namespace.
6
+ class FiberLocals
7
+ def initialize
8
+ @mutex = Mutex.new
9
+ @locals = {}
10
+ end
11
+
12
+ def [](key)
13
+ fiber_locals = nil
14
+ @mutex.synchronize do
15
+ fiber_locals = @locals[Fiber.current.object_id]
16
+ end
17
+ return nil if fiber_locals.nil?
18
+
19
+ fiber_locals[key]
20
+ end
21
+
22
+ def with(key, value)
23
+ fiber_id = Fiber.current.object_id
24
+ fiber_locals = nil
25
+ previous_value = nil
26
+ inited_vars = false
27
+
28
+ begin
29
+ @mutex.synchronize do
30
+ fiber_locals = @locals[fiber_id]
31
+ if fiber_locals.nil?
32
+ fiber_locals = {}
33
+ @locals[fiber_id] = fiber_locals
34
+ inited_vars = true
35
+ end
36
+ end
37
+
38
+ previous_value = fiber_locals[key]
39
+ fiber_locals[key] = value
40
+
41
+ yield
42
+ ensure
43
+ if inited_vars
44
+ @mutex.synchronize do
45
+ @locals.delete(fiber_id)
46
+ end
47
+ else
48
+ fiber_locals[key] = previous_value
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "support_table_cache/associations"
4
+ require_relative "support_table_cache/fiber_locals"
4
5
  require_relative "support_table_cache/find_by_override"
5
6
  require_relative "support_table_cache/relation_override"
6
7
  require_relative "support_table_cache/memory_cache"
@@ -10,6 +11,13 @@ require_relative "support_table_cache/memory_cache"
10
11
  module SupportTableCache
11
12
  extend ActiveSupport::Concern
12
13
 
14
+ NOT_SET = Object.new.freeze
15
+ private_constant :NOT_SET
16
+
17
+ @fiber_locals = FiberLocals.new
18
+ @cache = NOT_SET
19
+ @disabled = false
20
+
13
21
  included do
14
22
  # @api private Used to store the list of attribute names used for caching.
15
23
  class_attribute :support_table_cache_by_attributes, instance_accessor: false
@@ -42,14 +50,7 @@ module SupportTableCache
42
50
  # @yield Executes the provided block with caching disabled or enabled.
43
51
  # @return [Object] The return value of the block.
44
52
  def disable_cache(disabled = true, &block)
45
- varname = "support_table_cache_disabled:#{name}"
46
- save_val = Thread.current.thread_variable_get(varname)
47
- begin
48
- Thread.current.thread_variable_set(varname, !!disabled)
49
- yield
50
- ensure
51
- Thread.current.thread_variable_set(varname, save_val)
52
- end
53
+ SupportTableCache.with_fiber_local("support_table_cache_disabled:#{name}", !!disabled, &block)
53
54
  end
54
55
 
55
56
  # Enable the caching behavior for this class within the block. The enabled setting
@@ -81,10 +82,10 @@ module SupportTableCache
81
82
  # Set a class-specific cache to use in lieu of the global cache.
82
83
  #
83
84
  # @param cache [ActiveSupport::Cache::Store, Symbol] The cache instance to use. You can also
84
- # specify the value :memory to use an optimized in-memory cache.
85
+ # specify the value :memory or true to use an optimized in-memory cache.
85
86
  # @return [void]
86
87
  def support_table_cache=(cache)
87
- cache = MemoryCache.new if cache == :memory
88
+ cache = MemoryCache.new if cache == :memory || cache == true
88
89
  self.support_table_cache_impl = cache
89
90
  end
90
91
 
@@ -127,7 +128,7 @@ module SupportTableCache
127
128
  private
128
129
 
129
130
  def support_table_cache_disabled?
130
- current_block_value = Thread.current.thread_variable_get("support_table_cache_disabled:#{name}")
131
+ current_block_value = SupportTableCache.fiber_local_value("support_table_cache_disabled:#{name}")
131
132
  if current_block_value.nil?
132
133
  SupportTableCache.disabled?
133
134
  else
@@ -150,13 +151,7 @@ module SupportTableCache
150
151
  # @return [Object, nil] The return value of the block if a block is given, nil otherwise.
151
152
  def disable(disabled = true, &block)
152
153
  if block
153
- save_val = Thread.current.thread_variable_get(:support_table_cache_disabled)
154
- begin
155
- Thread.current.thread_variable_set(:support_table_cache_disabled, !!disabled)
156
- yield
157
- ensure
158
- Thread.current.thread_variable_set(:support_table_cache_disabled, save_val)
159
- end
154
+ SupportTableCache.with_fiber_local("support_table_cache_disabled", !!disabled, &block)
160
155
  else
161
156
  @disabled = !!disabled
162
157
  end
@@ -174,9 +169,9 @@ module SupportTableCache
174
169
  # Return true if caching has been disabled.
175
170
  # @return [Boolean]
176
171
  def disabled?
177
- block_value = Thread.current.thread_variable_get(:support_table_cache_disabled)
172
+ block_value = SupportTableCache.fiber_local_value("support_table_cache_disabled")
178
173
  if block_value.nil?
179
- !!(defined?(@disabled) && @disabled)
174
+ !!@disabled
180
175
  else
181
176
  block_value
182
177
  end
@@ -197,7 +192,7 @@ module SupportTableCache
197
192
  def cache
198
193
  if testing_cache
199
194
  testing_cache
200
- elsif defined?(@cache)
195
+ elsif @cache != NOT_SET
201
196
  @cache
202
197
  elsif defined?(Rails.cache)
203
198
  Rails.cache
@@ -211,14 +206,11 @@ module SupportTableCache
211
206
  # @yield Executes the provided block in test mode.
212
207
  # @return [Object] The return value of the block.
213
208
  def testing!(&block)
214
- save_val = Thread.current.thread_variable_get(:support_table_cache_test_cache)
209
+ save_val = SupportTableCache.fiber_local_value("support_table_cache_test_cache")
215
210
  if save_val.nil?
216
- Thread.current.thread_variable_set(:support_table_cache_test_cache, MemoryCache.new)
217
- end
218
- begin
211
+ SupportTableCache.with_fiber_local("support_table_cache_test_cache", MemoryCache.new, &block)
212
+ else
219
213
  yield
220
- ensure
221
- Thread.current.thread_variable_set(:support_table_cache_test_cache, save_val)
222
214
  end
223
215
  end
224
216
 
@@ -227,9 +219,9 @@ module SupportTableCache
227
219
  # @return [SupportTableCache::MemoryCache, nil] The test cache or nil if not in test mode.
228
220
  # @api private
229
221
  def testing_cache
230
- unless defined?(@cache) && @cache.nil?
231
- Thread.current.thread_variable_get(:support_table_cache_test_cache)
232
- end
222
+ return nil if @cache.nil?
223
+
224
+ SupportTableCache.fiber_local_value("support_table_cache_test_cache")
233
225
  end
234
226
 
235
227
  # Generate a consistent cache key for a set of attributes. It will return nil if the attributes
@@ -258,6 +250,14 @@ module SupportTableCache
258
250
 
259
251
  [klass.name, sorted_attributes]
260
252
  end
253
+
254
+ def fiber_local_value(varname)
255
+ @fiber_locals[varname]
256
+ end
257
+
258
+ def with_fiber_local(varname, value, &block)
259
+ @fiber_locals.with(varname, value, &block)
260
+ end
261
261
  end
262
262
 
263
263
  # Remove the cache entry for this record.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: support_table_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
@@ -43,6 +43,7 @@ executables: []
43
43
  extensions: []
44
44
  extra_rdoc_files: []
45
45
  files:
46
+ - AGENTS.md
46
47
  - ARCHITECTURE.md
47
48
  - CHANGELOG.md
48
49
  - MIT-LICENSE
@@ -50,6 +51,7 @@ files:
50
51
  - VERSION
51
52
  - lib/support_table_cache.rb
52
53
  - lib/support_table_cache/associations.rb
54
+ - lib/support_table_cache/fiber_locals.rb
53
55
  - lib/support_table_cache/find_by_override.rb
54
56
  - lib/support_table_cache/memory_cache.rb
55
57
  - lib/support_table_cache/relation_override.rb
@@ -72,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
74
  - !ruby/object:Gem::Version
73
75
  version: '0'
74
76
  requirements: []
75
- rubygems_version: 3.6.9
77
+ rubygems_version: 4.0.3
76
78
  specification_version: 4
77
79
  summary: Automatic ActiveRecord caching for small support tables.
78
80
  test_files: []