Skip to content

Enable the Rails 7.2 framework defaults - #916

Open
suttondemlong wants to merge 2 commits into
masterfrom
chore/rails-7-2-defaults
Open

Enable the Rails 7.2 framework defaults#916
suttondemlong wants to merge 2 commits into
masterfrom
chore/rails-7-2-defaults

Conversation

@suttondemlong

@suttondemlong suttondemlong commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Continues the upgrade plan from #907, which landed Rails 7.2.3.2 with config.load_defaults still at 7.1 and new_framework_defaults_7_2.rb shipping with all five options commented out. This enables them and retires that file.

Independent of the Ruby 3.4 PR (#917). Neither touches a file the other does, so they can merge in either order.

The application diff is 8 lines plus a spec. Everything else is the deleted defaults file.

Commits

  1. Load the Rails 7.2 framework defaults
  2. Only inspect :id on Active Record objects in production

Three things worth a reviewer's attention

1. Four of the five 7.2 options do nothing here

Dumped the settings before and after rather than reading the release notes. load_defaults "7.2" sets exactly five things:

yjit                                          false -> true
active_job.enqueue_after_transaction_commit  :never -> :default
active_record.postgresql_adapter_decode_dates false -> true
active_record.validate_migration_timestamps   false -> true
active_storage.web_image_content_types      unset -> unset
  • yjit is inert until Ruby 3.4. The initializer is guarded by defined?(RubyVM::YJIT.enable), and enable arrived in Ruby 3.3; on 3.2.3 the constant exists but the method does not.
  • Active Storage is not loaded — the require is commented out in config/application.rb — so respond_to?(:active_storage) is false and Rails skips that line entirely.
  • postgresql_adapter_decode_dates only affects queries carrying no type information. The one raw query in the app, Food.fts, runs through find_by_sql, which casts through the model's attribute types, and selects no date columns in any case.
  • validate_migration_timestamps raises on a migration dated more than a day ahead of now. The newest migration here is from October 2017.

2. enqueue_after_transaction_commit is the one real change, and it has a spec

:never enqueued immediately even inside an open transaction; :default asks the adapter. Sidekiq answers true — both its own adapter and the one still shipped in activejob define it that way, so load order does not matter — meaning a job enqueued inside a transaction now waits for the commit, and a rollback drops it rather than leaving Sidekiq holding a job whose rows were never written.

No existing call site changes behaviour. Active Job is reached from exactly three places: TrackableUsage's after_commit, which by definition runs with no transaction open; DataExportSchedulesController#create; and six deliver_later calls, all inside Sidekiq workers or rake tasks. None sits in a transaction. Everything else in app/jobs is perform_async on a plain Sidekiq worker, which never enters Active Job.

So this is protection for code not yet written, which is why it comes with spec/jobs/enqueue_after_transaction_commit_spec.rb. Checked in both directions: on load_defaults 7.1 two of the three examples fail.

The suite is unaffected for a reason worth recording: DatabaseCleaner's :transaction strategy opens its wrapping transaction with joinable: false, and ActiveRecord.after_all_transactions_commit skips non-joinable transactions, so a top-level perform_later in a spec still enqueues immediately. Had that wrapper been joinable, every job spec would have been waiting on a commit that never comes.

3. attributes_for_inspect does not apply to User

#907 deferred config.active_record.attributes_for_inspect here on the grounds that it changes behaviour, so it belongs with this flip. load_defaults does not set it; Rails puts it in the generated production.rb.

It is worth taking. inspect on an Active Record object renders every column, and that string is how attribute values reach logs and Bugsnag reports — on this app those values are health data. With [:id], Profile.new(birth_date: ...).inspect is #<Profile id: nil> instead of a line carrying a date of birth, a sex, a time zone and a screen name. full_inspect still prints everything on demand.

The exception, checked rather than assumed: User is unaffected, because Devise::Models::Authenticatable overrides inspect and wins the method lookup. Devise's version filters through serializable_hash, so credentials were never in that output, but the remaining columns still are. Every other Active Record model takes the new behaviour. Development and test keep the :all default.

This needs no special rollout, contrary to the plan

The upgrade plan flagged this step as a behaviour change needing its own deploy, because some framework defaults change the on-disk format of cached values and signed messages. Checked: none of the five options touches a format. active_support.cache_format_version is 7.1 before and after — 7.2 does not change its default, and config/application.rb pins it explicitly anyway — so there is no old-process/new-process incompatibility during a rollout.

Verification

  • 334 examples, 0 failures (331 on master plus the three new ones).
  • standardrb, erblint --lint-all and zeitwerk:check clean.
  • Boots in development, test and production. Confirmed on a production boot that ActiveRecord::Base.attributes_for_inspect == [:id], Profile#inspect is redacted and Profile#full_inspect is complete.
  • Settings dumped before and after; the five values above are the complete delta.

Not verified

  • Ran against PostgreSQL 15.19 and MongoDB 7.0.14 locally, not the 12.8 and 4.4.9 that docker-compose.yml and the rspec job pin. Nothing here touches SQL or query construction, but CI on the real pins is the gate.
  • yjit cannot be exercised on Ruby 3.2.3 at all, by design — see above.

🤖 Generated with Claude Code

Bumps `config.load_defaults` to 7.2 and retires
config/initializers/new_framework_defaults_7_2.rb, which #907 shipped with
every option commented out.

Dumped the affected settings before and after rather than reading the release
notes. `load_defaults "7.2"` sets exactly five things:

  yjit                                          false -> true
  active_job.enqueue_after_transaction_commit  :never -> :default
  active_record.postgresql_adapter_decode_dates false -> true
  active_record.validate_migration_timestamps   false -> true
  active_storage.web_image_content_types      unset -> unset

Four of the five are inert here:

- `yjit` does nothing until the Ruby bump. The initializer is guarded by
  `defined?(RubyVM::YJIT.enable)`, and `enable` arrived in Ruby 3.3; on 3.2.3
  the constant exists but the method does not. It starts mattering in PR 6.
- Active Storage is not loaded -- the require is commented out in
  config/application.rb -- so `respond_to?(:active_storage)` is false and Rails
  skips that line entirely.
- `postgresql_adapter_decode_dates` only affects queries carrying no type
  information. The one raw query in the app, `Food.fts`, runs through
  `find_by_sql`, which casts through the model's attribute types, and selects
  no date columns in any case.
- `validate_migration_timestamps` raises on a migration dated more than a day
  ahead of now. The newest migration here is from October 2017.

That leaves `enqueue_after_transaction_commit`, which is a real change:
`:never` enqueued immediately even inside an open transaction, and `:default`
asks the adapter. Sidekiq answers true -- both its own adapter and the one
still shipped in activejob define it that way, so it does not matter which
wins the load order -- meaning a job enqueued inside a transaction now waits
for the commit, and a rollback drops it rather than leaving Sidekiq holding a
job whose rows were never written.

No existing call site changes behaviour. Active Job is reached from exactly
three places: `TrackableUsage`'s `after_commit`, which by definition runs with
no transaction open; `DataExportSchedulesController#create`; and six
`deliver_later` calls, all of them inside Sidekiq workers or rake tasks. None
sits in a transaction. Everything else in app/jobs is `perform_async` on a
plain Sidekiq worker, which never enters Active Job. So this is protection for
code not yet written, which is why it comes with a spec of its own.

The suite is unaffected for a reason worth recording: DatabaseCleaner's
:transaction strategy opens its wrapping transaction with `joinable: false`,
and `ActiveRecord.after_all_transactions_commit` skips non-joinable
transactions, so a top-level `perform_later` in a spec still enqueues
immediately. Had that wrapper been joinable, every job spec would have been
waiting on a commit that never comes.

Also settles the deploy-ordering question the upgrade plan flagged for this
step: none of the five options touches an on-disk format.
`active_support.cache_format_version` is 7.1 before and after -- 7.2 does not
change its default, and config/application.rb pins it explicitly anyway -- so
there is no old-process/new-process cache incompatibility and this needs no
special rollout.

Verified: 327 examples, 0 failures; standardrb, erblint and zeitwerk:check
clean; boots in development, test and production. The three new specs were
checked in both directions -- on `load_defaults 7.1` the two transaction
examples fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`config.active_record.attributes_for_inspect` is a 7.2 addition that
`load_defaults` does not set; Rails puts it in the generated production.rb
instead, and #907 deferred it here on the grounds that it changes behaviour
and so belongs with the defaults flip.

It is worth taking rather than skipping. `inspect` on an Active Record object
renders every column, and that string is how attribute values reach logs and
Bugsnag reports -- on this app those values are health data. With `[:id]`,
`Profile.new(birth_date: ...).inspect` is `#<Profile id: nil>` instead of a
line carrying a date of birth, a sex, a time zone and a screen name.
`full_inspect` still prints everything for when you want it.

One exception, checked rather than assumed: `User` is unaffected, because
Devise::Models::Authenticatable overrides `inspect` and wins the method
lookup. Devise's version filters through `serializable_hash`, so credentials
were never in that output, but the remaining columns still are. Every other
Active Record model in the app takes the new behaviour.

Development and test keep the `:all` default, where the whole point of
`inspect` is to see the record.

Verified: booted RAILS_ENV=production and confirmed
`ActiveRecord::Base.attributes_for_inspect == [:id]`, `Profile#inspect`
redacted and `Profile#full_inspect` complete. 327 examples, 0 failures;
standardrb clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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