Skip to content

Store freeze opt-out flag outside IsolatedExecutionState - #157

Open
joeljunstrom wants to merge 2 commits into
basecamp:masterfrom
joeljunstrom:fix-fiber-isolation-flag-wipe
Open

Store freeze opt-out flag outside IsolatedExecutionState#157
joeljunstrom wants to merge 2 commits into
basecamp:masterfrom
joeljunstrom:fix-fiber-isolation-flag-wipe

Conversation

@joeljunstrom

@joeljunstrom joeljunstrom commented May 25, 2026

Copy link
Copy Markdown

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. We set config.active_support.isolation_level = :fiber, and Rails applies that in after_initialize: the setter clears the boot scope's storage and repoints lookups at Fiber.current, so the false written at eager-load by Ext::Core::Object and Ext::Core::String is gone. A per-fiber flag is also back to its default inside every new fiber, so a value written at load time can't be relied on either way.

The default true then takes over and Console1984::Freezeable.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 class-level instance variable 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.

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.
Copilot AI balanced review requested due to automatic review settings May 25, 2026 17:24

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_accessor storage 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.

Comment thread lib/console1984/freezeable.rb Outdated
Comment on lines +26 to +36
# 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk up to maintainer, feels like we are trading a lot of complexity for something that would be rare indeed in a console session?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I do not think this is true?

Comment thread lib/console1984/freezeable.rb Outdated
Comment on lines +26 to +30
# 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty darn nit-picky mr robot =)

Comment thread lib/console1984/freezeable.rb Outdated
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)
The previous comment blamed a Rails 8.1 default, which doesn't exist:
load_defaults "8.1" never touches active_support.isolation_level, and
ActiveSupport ends with self.isolation_level = :thread. The level only
changes when an app sets config.active_support.isolation_level, and Rails
applies it in after_initialize, which clears the boot scope's storage and
repoints lookups at Fiber.current. On top of that, a per-fiber flag would
be back to its default inside every new fiber, so a value written at
eager-load time can never be relied on.

The value lives on the class object itself, so it's a class-level instance
variable, not a singleton-class one. Read it with instance_variable_defined?.

Add a test for subclasses. thread_mattr_accessor keyed its storage per
object_id, so a subclass had its own value defaulting to true, and
class-level instance variables aren't inherited either.
Copilot AI review requested due to automatic review settings August 10, 2026 09:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants