Store freeze opt-out flag outside IsolatedExecutionState#157
Store freeze opt-out flag outside IsolatedExecutionState#157joeljunstrom wants to merge 1 commit into
Conversation
The flag was a thread_mattr_accessor, which routes through ActiveSupport::IsolatedExecutionState. Rails 8.1 defaults isolation_level to :fiber and flips it inside after_initialize; the setter clears the old scope's storage before swapping, wiping the false written at eager-load by Ext::Core::Object and Ext::Core::String. The default true then takes over and Refrigerator#freeze_all installs instance_variable_get/set overrides on Object, breaking anything that introspects ivars (e.g. the postgres adapter in destroy_all/update_all). Storing the flag in a singleton-class ivar on the host keeps each Freezeable independent (which is why mattr_accessor was avoided here - its class variable would leak through Object's ancestor chain) and is unaffected by the isolation_level switch.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates how Console1984::Freezeable stores the prevent_instance_data_manipulation_after_freezing flag so it doesn’t reset when ActiveSupport::IsolatedExecutionState is cleared, and adds tests to lock in the intended behavior.
Changes:
- Replace
thread_mattr_accessorstorage with per-host storage on the including class. - Add tests covering default value, persistence across
IsolatedExecutionState.clear, and non-leakage between hosts.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| test/freezeable_test.rb | Adds coverage for default behavior, persistence across isolation state clearing, and isolation between hosts. |
| lib/console1984/freezeable.rb | Changes flag storage to avoid IsolatedExecutionState clearing and prevent ancestor-chain leakage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # true by default. Stored as a singleton-class instance variable on the host so it survives | ||
| # +ActiveSupport::IsolatedExecutionState+ being cleared when Rails switches +isolation_level+ | ||
| # (e.g. the +:thread+ -> +:fiber+ flip the Rails 8.1 default triggers in +after_initialize+). | ||
| # A +mattr_accessor+ would leak the flag across the ancestor chain because +Console1984::Ext::Core::Object+ | ||
| # is included into +Object+; this storage keeps each host independent. | ||
| base.singleton_class.class_eval do | ||
| attr_writer :prevent_instance_data_manipulation_after_freezing | ||
|
|
||
| define_method :prevent_instance_data_manipulation_after_freezing do | ||
| return @prevent_instance_data_manipulation_after_freezing if defined?(@prevent_instance_data_manipulation_after_freezing) | ||
| true |
There was a problem hiding this comment.
idk up to maintainer, feels like we are trading a lot of complexity for something that would be rare indeed in a console session?
| # true by default. Stored as a singleton-class instance variable on the host so it survives | ||
| # +ActiveSupport::IsolatedExecutionState+ being cleared when Rails switches +isolation_level+ | ||
| # (e.g. the +:thread+ -> +:fiber+ flip the Rails 8.1 default triggers in +after_initialize+). | ||
| # A +mattr_accessor+ would leak the flag across the ancestor chain because +Console1984::Ext::Core::Object+ | ||
| # is included into +Object+; this storage keeps each host independent. |
There was a problem hiding this comment.
Pretty darn nit-picky mr robot =)
| attr_writer :prevent_instance_data_manipulation_after_freezing | ||
|
|
||
| define_method :prevent_instance_data_manipulation_after_freezing do | ||
| return @prevent_instance_data_manipulation_after_freezing if defined?(@prevent_instance_data_manipulation_after_freezing) |
We saw our prod consoles starting to break after switching to fiber isolation. This happened immediately when Rails tries to connect to postgres.
The flag was a thread_mattr_accessor, which routes through ActiveSupport::IsolatedExecutionState. Rails 8.1 defaults isolation_level to :fiber and flips it inside after_initialize, the setter clears the old scope's storage before swapping, wiping the false written at eager-load by Ext::Core::Object and Ext::Core::String.
The default true then takes over and Refrigerator#freeze_all installs instance_variable_get/set overrides on Object, breaking anything that introspects ivars (e.g. the postgres adapter in destroy_all/update_all).
Storing the flag in a singleton-class ivar on the host keeps each Freezeable independent (which is why mattr_accessor was avoided here, its class variable would leak through Object's ancestor chain) and is unaffected by the isolation_level switch.