Repository navigation
Conversation
Rails main's experimental `Rails.application.ractorize!` serves requests from non-main Ractors. There, the first `Importer.cloudflare_ips` call fails: reading `Rails.application.config.cloudflare` goes through a class variable (Ruby 4.0), a non-main Ractor can't set `@ips`, and the fallback constants hold unfrozen IPAddrs. On Rails' `before_ractorize` hook, resolve the list once on the main Ractor, fetched or the fallback, and store it frozen and shareable. Without `ractorize!` the hook never runs and nothing changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rails main has an experimental
Rails.application.ractorize!that serves requests from non-main Ractors on Ruby 4.0. With cloudflare-rails loaded, every request fails there, becauseActionDispatch::RemoteIp#proxiescallsImporter.cloudflare_ipson each request:That config read is only the first error. Before writing the fix, I worked through what happens after it in a Rails main app:
Rails.application.config.cloudflare.*is a class variable read on Ruby 4.0.@ips ||= …raisescan not set instance variables of classes/modules by non-main Ractors. A non-main Ractor can't set the memo on any Ruby.rescuethen returnsFallbackIps::IPS_V4 + IPS_V6. Those are frozen Arrays of unfrozenIPAddrs, so a non-main Ractor can't read those constants either, and the request fails.Change
Importer.make_shareable!resolves the list on the main Ractor and stores it withRactor.make_shareable, whether it fetched the list or fell back. The Railtie calls it from Rails'before_ractorizeload hook, the same hook Rails' own railties use. Older Rails never runs that hook, and outsideractorize!nothing changes.Under
ractorize!, the list is therefore resolved once at boot, and non-main Ractors only read it. A fetch that already succeeded was memoized for the life of the process anyway. One difference: if the fetch fails at boot, the fallback stays until restart, where today each request would retry. That retry can't work from a non-main Ractor anyway, because it can't set@ips(step 2).I didn't move the config reads to boot. Once the list is memoized, the request path no longer reaches them, and moving them doesn't fix anything by itself (step 2). That also keeps this PR out of #243's way: #243 rewrites
cloudflare_ips/fetch_with_cache, but this only adds a method next to them and sets@ipsdirectly, so the two compose.Tests
New
.make_shareable!examples: with a successful fetch and with 404s, the list is memoized (one request to the v4 endpoint in total) andRactor.shareable?. They skip withoutRactor.bundle exec rake(all three rack-attack modes) andrubocoppass on Ruby 3.3.8/Rails 7.2, 3.4.11/8.0, 4.0.7/8.1 and 4.0.7/edge.I also checked it in a Rails main app under
ractorize!, with two requests running in a non-main Ractor:GET /posts) fails with the error above.200.cloudflare-rails: error fetching ip addresses … falling back to defaultsline atractorize!, none per request, and the requests return200.REMOTE_ADDR=173.245.48.1(a Cloudflare range) andX-Forwarded-For: 203.0.113.9:request.ipis203.0.113.9andrequest.cloudflare?istrue.I didn't add a CHANGELOG entry, because recent PRs don't add one. I can add one if you'd like.
Conformance case
spec/cloudflare/rails_spec.rb:132: "makes the fetched ips shareable" (and:145, the same for the fallback list)8ca4a9e), Ruby 4.0.7: the specs call the newImporter.make_shareable!, so they can't run there. The bug they cover: with@ipsmemoized on the main Ractor,Importer.cloudflare_ipscalled from a Ractor fails withRactor::IsolationError: can not get unshareable values from instance variables of classes/modules from non-main Ractors (@ips from CloudflareRails::Importer).The specs check
Ractor.shareable?on both the fetched list and the fallback, so either path going back to an unfrozen value fails them; they skip whereRactoris unavailable.