From f4fd47da7815f21ff13c4d806bbb12ef84a4fc6a Mon Sep 17 00:00:00 2001 From: John Bolliger Date: Mon, 28 Sep 2026 14:17:45 -0700 Subject: [PATCH 1/2] Merge default_options, and add default_options_overwrite Each call to default_options replaced the options. In a Rails app, the railtie sets :expires_in and :race_condition_ttl, and then the app initializers run. An initializer that called default_options with other options removed the TTL. Every cached finder call with no :expires_in then wrote an entry that never expired. abacus shows the problem. Its initializer calls default_options(:active_remote_cached_replace_characters => true), and 34 of its 43 cached finder call sites wrote Redis keys with no TTL. In a January 2024 prod key dump, 1,641 of 1,944 of those keys had no expiry. default_options now merges the given options into the current options, so a later call adds to them. The new default_options_overwrite replaces the options, for a caller that wants the old behavior or wants to clear them. default_options({}) no longer clears the options. The specs used it to reset state between examples, so they now call default_options_overwrite({}). Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 44 ++++++++++++- lib/active_remote/cached.rb | 11 +++- .../cached_delete_methods_spec.rb | 2 +- .../cached_exist_methods_spec.rb | 4 +- .../active_remote/cached_find_methods_spec.rb | 4 +- .../cached_search_methods_spec.rb | 8 +-- spec/active_remote/cached_spec.rb | 65 ++++++++++++++++++- 7 files changed, 124 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 06841aa..6ffedcd 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,25 @@ The default cache options used when interacting with the cache can be specified ActiveRemote::Cached.default_options(:expires_in => 1.hour) ``` -In Rails apps, the :race_condition_ttl option defaults to 5 seconds. +In Rails apps, the railtie sets `:expires_in` to 5 minutes and `:race_condition_ttl` to 5 seconds. Change them with `config.active_remote_cached.expires_in` and `config.active_remote_cached.race_condition_ttl`. + +`default_options` merges the given options into the current options. An initializer that adds one option keeps the railtie's TTL: + +```ruby +# config/initializers/active_remote_cached.rb +ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true) +ActiveRemote::Cached.default_options +# => { :expires_in => 5.minutes, :race_condition_ttl => 5.seconds, :active_remote_cached_replace_characters => true } +``` + +To replace all the options, use `default_options_overwrite`. Pass an empty hash to clear them: + +```ruby +ActiveRemote::Cached.default_options_overwrite(:expires_in => 1.hour) +ActiveRemote::Cached.default_options_overwrite({}) +``` + +Without `:expires_in`, a cached finder writes an entry that never expires. #### Local overrides @@ -149,6 +167,30 @@ CI runs this matrix on Ruby 3.1, Ruby 3.4, JRuby 9.4, and JRuby 10.0. `active_remote` 8.0 requires Ruby 3.2 or later. CI does not run that version on Ruby 3.1 or JRuby 9.4. +## Upgrading from 1.2.0 + +### default_options merges + +Before this change, each call to `default_options` replaced the options. In a +Rails app, the railtie sets `:expires_in` and `:race_condition_ttl` before the +app initializers run. An initializer that called `default_options` with other +options removed the TTL, and every cached finder call with no `:expires_in` +wrote an entry that never expired: + +```ruby +# railtie: { :expires_in => 5.minutes, :race_condition_ttl => 5.seconds } +ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true) +# before: { :active_remote_cached_replace_characters => true } +# now: { :expires_in => 5.minutes, :race_condition_ttl => 5.seconds, :active_remote_cached_replace_characters => true } +``` + +After the upgrade, an app like this gets a TTL again. Its finders call the +remote service more often, because entries now expire. + +`default_options({})` no longer clears the options. Use +`default_options_overwrite({})`. Use `default_options_overwrite` to keep the +old replace behavior. + ## Upgrading to 1.2.0 ### Every cache key changes diff --git a/lib/active_remote/cached.rb b/lib/active_remote/cached.rb index 864aecb..5112b25 100644 --- a/lib/active_remote/cached.rb +++ b/lib/active_remote/cached.rb @@ -31,12 +31,21 @@ def self.cache(cache_provider = nil) @cache_provider end + # Merges the given options into the current options, and returns the + # result. A later call adds to the options, so an app initializer keeps the + # :expires_in and :race_condition_ttl that the Rails railtie set before it. def self.default_options(options = nil) - @default_options = options if options + @default_options = default_options.merge(options) if options @default_options || {} end + # Replaces the current options with the given options, and returns them. + # Pass an empty hash to clear the options. + def self.default_options_overwrite(options) + @default_options = options.dup + end + module ClassMethods def cached_methods @cached_methods ||= [] diff --git a/spec/active_remote/cached_delete_methods_spec.rb b/spec/active_remote/cached_delete_methods_spec.rb index b06f956..0aebfd1 100644 --- a/spec/active_remote/cached_delete_methods_spec.rb +++ b/spec/active_remote/cached_delete_methods_spec.rb @@ -56,7 +56,7 @@ def self.search(*) end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'deletes the find cache key' do diff --git a/spec/active_remote/cached_exist_methods_spec.rb b/spec/active_remote/cached_exist_methods_spec.rb index ddd2994..124d239 100644 --- a/spec/active_remote/cached_exist_methods_spec.rb +++ b/spec/active_remote/cached_exist_methods_spec.rb @@ -141,7 +141,7 @@ def self.search(*) end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'returns false before the find cache key is written' do @@ -186,7 +186,7 @@ def self.search(*) end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'returns false before the search cache key is written' do diff --git a/spec/active_remote/cached_find_methods_spec.rb b/spec/active_remote/cached_find_methods_spec.rb index aa0a37c..b311047 100644 --- a/spec/active_remote/cached_find_methods_spec.rb +++ b/spec/active_remote/cached_find_methods_spec.rb @@ -64,7 +64,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'executes find_by_guid when cached_find with guid called' do @@ -117,7 +117,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'overrides the default options with cached_finder options for the fetch call' do diff --git a/spec/active_remote/cached_search_methods_spec.rb b/spec/active_remote/cached_search_methods_spec.rb index d2c9add..b4e2a3d 100644 --- a/spec/active_remote/cached_search_methods_spec.rb +++ b/spec/active_remote/cached_search_methods_spec.rb @@ -95,7 +95,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'executes the search block when a block is passed' do @@ -203,7 +203,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'overrides the default options with cached_finder options for the fetch call' do @@ -232,7 +232,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'and_return results when present' do @@ -307,7 +307,7 @@ def self.search end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end it 'calls the underlying method with params in correct order' do diff --git a/spec/active_remote/cached_spec.rb b/spec/active_remote/cached_spec.rb index de22553..b39051e 100644 --- a/spec/active_remote/cached_spec.rb +++ b/spec/active_remote/cached_spec.rb @@ -65,7 +65,7 @@ class DurationChildClass < DurationOptionClass; end end after do - ::ActiveRemote::Cached.default_options({}) + ::ActiveRemote::Cached.default_options_overwrite({}) end describe '.cache' do @@ -91,17 +91,76 @@ class DurationChildClass < DurationOptionClass; end expect(::ActiveRemote::Cached.default_options).to eq(:expires_in => 100) end - # Passing nil reads the options. Callers must pass an empty hash to clear. it 'keeps the current options when nil is given' do ::ActiveRemote::Cached.default_options(:expires_in => 100) expect(::ActiveRemote::Cached.default_options(nil)).to eq(:expires_in => 100) end + it 'keeps the current options when an empty hash is given' do + ::ActiveRemote::Cached.default_options(:expires_in => 100) + + expect(::ActiveRemote::Cached.default_options({})).to eq(:expires_in => 100) + end + + # The Rails railtie sets :expires_in and :race_condition_ttl. An app + # initializer that sets one more option must not remove them. + it 'merges the given options into the current options' do + ::ActiveRemote::Cached.default_options(:expires_in => 100, :race_condition_ttl => 5) + ::ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true) + + expect(::ActiveRemote::Cached.default_options).to eq( + :expires_in => 100, + :race_condition_ttl => 5, + :active_remote_cached_replace_characters => true + ) + end + + it 'lets the given options win over the current options' do + ::ActiveRemote::Cached.default_options(:expires_in => 100) + ::ActiveRemote::Cached.default_options(:expires_in => 200) + + expect(::ActiveRemote::Cached.default_options).to eq(:expires_in => 200) + end + + it 'does not change a hash that the caller passed' do + first = { :expires_in => 100 } + ::ActiveRemote::Cached.default_options(first) + ::ActiveRemote::Cached.default_options(:race_condition_ttl => 5) + + expect(first).to eq(:expires_in => 100) + end + + it 'gives each finder call the merged options' do + ::ActiveRemote::Cached.default_options(:expires_in => 100) + ::ActiveRemote::Cached.default_options(:active_remote_cached_replace_characters => true) + + expect(::ActiveRemote::Cached.cache).to receive(:fetch).with( + [versioned_prefix, ConfigurationClass.name, '#find', 'guid.guid'], + { :expires_in => 100, :active_remote_cached_replace_characters => true } + ).and_return(:find_result) + + ConfigurationClass.cached_find_by_guid(:guid) + end + end + + describe '.default_options_overwrite' do + it 'replaces the current options' do + ::ActiveRemote::Cached.default_options(:expires_in => 100, :race_condition_ttl => 5) + ::ActiveRemote::Cached.default_options_overwrite(:active_remote_cached_replace_characters => true) + + expect(::ActiveRemote::Cached.default_options).to eq(:active_remote_cached_replace_characters => true) + end + it 'clears the current options when an empty hash is given' do ::ActiveRemote::Cached.default_options(:expires_in => 100) + ::ActiveRemote::Cached.default_options_overwrite({}) + + expect(::ActiveRemote::Cached.default_options).to eq({}) + end - expect(::ActiveRemote::Cached.default_options({})).to eq({}) + it 'returns the new options' do + expect(::ActiveRemote::Cached.default_options_overwrite(:expires_in => 100)).to eq(:expires_in => 100) end end From 55096d8a5d84aaefcdd169f6d329219a3a22598e Mon Sep 17 00:00:00 2001 From: John Bolliger Date: Mon, 28 Sep 2026 14:25:26 -0700 Subject: [PATCH 2/2] Bump to 1.3.0 default_options now merges the given options into the current options, and default_options({}) no longer clears them. Use default_options_overwrite({}) to clear them. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 4 ++-- lib/active_remote/cached/version.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ffedcd..97553c4 100644 --- a/README.md +++ b/README.md @@ -167,11 +167,11 @@ CI runs this matrix on Ruby 3.1, Ruby 3.4, JRuby 9.4, and JRuby 10.0. `active_remote` 8.0 requires Ruby 3.2 or later. CI does not run that version on Ruby 3.1 or JRuby 9.4. -## Upgrading from 1.2.0 +## Upgrading to 1.3.0 ### default_options merges -Before this change, each call to `default_options` replaced the options. In a +Before 1.3.0, each call to `default_options` replaced the options. In a Rails app, the railtie sets `:expires_in` and `:race_condition_ttl` before the app initializers run. An initializer that called `default_options` with other options removed the TTL, and every cached finder call with no `:expires_in` diff --git a/lib/active_remote/cached/version.rb b/lib/active_remote/cached/version.rb index 5d5d5b4..51c952e 100644 --- a/lib/active_remote/cached/version.rb +++ b/lib/active_remote/cached/version.rb @@ -2,6 +2,6 @@ module ActiveRemote module Cached - VERSION = '1.2.0' + VERSION = '1.3.0' end end