Skip to content

Conversation

@willbryant
Copy link
Contributor

As mentioned in #2288, this reduces the amount of redundant re-calculation needed when gems like rack-attack call Request#ip more than once.

Feedback on the code factoring most welcome. This seemed like the least invasive way to do it; the usual simple @ip ||= begin..end pattern inside the method wouldn't work due to the return statements, but if we don't like the idea of the logic being in one place and the caching another, I can set the ivar in each return statement.

I did feel like doing it like this makes it very easy to add caching to other methods included from Helper without rewriting them, though, which could be desired over time?

Copy link
Contributor

@jeremyevans jeremyevans left a comment

Choose a reason for hiding this comment

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

This is certainly an noninvasive way to add caching, but super is not all that optimized, and often ip is only called once per request. I'd prefer a conditional at the top of the current method, and setting the ivar in each explicit/implicit return. However, I don't object to this approach, so let's wait for feedback from @ioquatix and see what he prefers.

@ioquatix
Copy link
Member

I am fine with this approach.

The alternative is to move it into the Helper. Currently that module is stateless, and is used by code outside of Rack. Introducing an instance variable cache makes Helper stateful, and may break downstream code, or the assumptions it makes about Helper#ip.

The only downside to this change is that caching is injected around the computation with no way to change the computation itself. Personally, when I introduce some kind of cache, I'd prefer to have two methods, e.g. protected def compute_ip and def ip = @ip ||= compute_ip. This way, sub-classes can modify compute_ip and still have the correct caching behaviour. In other words, if someone is sub-classing Rack::Request and overrides def ip, that override won't be subject to caching which I feel is a downside.

@willbryant
Copy link
Contributor Author

In general I like that approach (#ip/#compute_ip) too. However, this has the same issue you mention earlier -

Introducing an instance variable cache makes Helper stateful, and may break downstream code, or the assumptions it makes about Helper#ip.

As either the #ip/#compute_ip methods are both in Helper, and it becomes stateful; or, #compute_ip is in Helper and #ip is not, and we've changed the API to Helper as it no longer responds to ip.

@ioquatix
Copy link
Member

ioquatix commented Feb 23, 2025

In principle, in Helper, you just have def compute_ip; current logic; end; def ip = current_ip - however such a design would only make sense if we expected people to customise this behaviour. And if it's problematic, we could introduce it later.

@willbryant
Copy link
Contributor Author

willbryant commented Feb 23, 2025

Yep seems reasonable, sort of similar to my first draft but avoids super and as you say, makes it easier to override.

I've force-pushed that. I made compute_ip private to keep the freedom to change mind later.

@ioquatix
Copy link
Member

ioquatix commented Feb 24, 2025

I'm okay with this, and even thought I suggested it, I also don't mind the simpler approach either.

The benefit of this approach is that we removed the call to super (which is probably negligible), but more importantly, we still provide a way to correctly override the implementation of determining the ip without breaking existing code that uses def ip; ... super (thought it may not benefit as much from the cache). The only point of contention I can see here is whether that's actually useful in practice. Making it private by default is a reasonable choice.

Let's see what @jeremyevans has to say about it.

Copy link
Contributor

@jeremyevans jeremyevans left a comment

Choose a reason for hiding this comment

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

Looking at other Request methods, the caching we are adding here is most similar to params, so the way @willbryant was originally adding caching was consistent with the current code. I didn't realize this at first, because of where the method was defined.

I worry that this changes behavior for applications that include a module in Request that overrides ip, because now their override is no longer used. So in attempting to make caching of ip automatic for subclasses, we are breaking one use case. That's not an automatic blocker, but we should consider whether that is an appropriate tradeoff. If so, we should at least note this as a backwards incompatibility in the CHANGELOG.

I don't have objections to the separate compute_ip method, though we don't use that approach for params.

Note that Helper is already stateful depending on your definition, it just uses env caching instead of ivar caching for handling state changes.

Considering all that, my preference would be for @willbryant's original commit, with the method moved near the def params that does the same thing. However, if we decide that automatic subclass caching is more important than backwards compatibility, I'm OK with this approach (still with the method moved).

@willbryant willbryant force-pushed the cache_request_ip branch 2 times, most recently from 7f8bf04 to e9801b7 Compare February 24, 2025 09:15
@willbryant
Copy link
Contributor Author

Done, and added @ip = nil to match params, which I assume was done for the benefit of people running in higher warning levels.

@ioquatix ioquatix merged commit 431042a into rack:main Feb 26, 2025
16 checks passed
@ioquatix
Copy link
Member

Thanks for your contribution.

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.

3 participants