-
Notifications
You must be signed in to change notification settings - Fork 21.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A shortcut to setup controller environment #18546
Conversation
13083fa
to
6e5bab4
Compare
The API is supported for arbitrary controller since rendering depends on |
I've added |
This is great, Ravil! I’m wondering if we shouldn’t actually also pipe ApplicationController.render => ApplicationController.renderer.render. I think that’ll be the default case.
|
Thanks for guiding, David!
I have a few doubts over |
Sweet. I like the idea that ApplicationController.renderer refers to a class, not a method. Then that class has a class-method #render for the common case, but if you need specialization, you can instantiate that class and do that. So I think that's good. Especially since we also have the #defaults approach. |
def for(controller) | ||
Class.new self do | ||
self.controller = controller | ||
self.defaults = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add config/initializers/application_controller_renderer.rb that shows how to use this on setup. Specifically the way to toggle https depending on the env and setting the http_host.
Once the few coding notes have been taken care of, it looks like we're ready to merge? Lovely work, @brainopia. |
55abfd6
to
bf486b5
Compare
David, thank you for code style checking! I've updated code and I'll make sure to reflect it in future pull-request. If there are more tidbits to follow I'd be happy to. I'd like your advice on what would you prefer to see in the initializer. # option 1
class ApplicationRenderer < ApplicationController.renderer
# customize env statically
defaults[:https] = false
defaults[:host] = 'example.org'
def initialize(env = {})
# customize env dynamically
super
end
def render(*args)
# customize render args
super
end
end
# option 2
ApplicationController.renderer.class_eval do
self.defaults = Rack::MockRequest.env_for('https://my.tld')
end Should we create a new renderer class or redefine an existing. It makes sense to support And should we show all possible ways to customize Should we use simple As an alternative to initializer we can add a line (commented out by default) directly to application_controller.rb to show an example: class ApplicationController < ActionController::Base
# renderer.defaults.merge! https: true, host: 'my.tld' <====
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end And of course thorough examples in documentation would alleviate this problem. I'm going to push docs soon (although english is not my native language, so it will look a bit (or a lot, I can't judge) crusty). |
to_a | ||
end | ||
|
||
def set_request!(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is set_request!
supposed to be used by users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, no. Should I mark it as #:nodoc:
, would it be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I mark it as #:nodoc:, would it be enough?
Yes. 👍
bf486b5
to
316c898
Compare
@rafaelfranca I've updated code to reflect your notes, thank you! @dhh The question above regarding initializers (#18546 (comment)) is still actual. |
💣 I forgot that |
316c898
to
14da053
Compare
@rafaelfranca yeah, I've reverted to previous |
ebc4797
to
98c3296
Compare
@dhh here you go 😄 |
class << self | ||
delegate :render, to: :new | ||
|
||
def for(controller) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this method public API?
Awesome work on this @brainopia |
Add `ActionController::Metal#set_request!` to set a request on controller instance without calling dispatch.
To have an easier way to setup a controller instance with custom environment
Render arbitrary templates outside of controller actions
98c3296
to
3011b64
Compare
@rafaelfranca Thank you for the help! I think now I've covered all bases 😄 |
Cool! Now it is good to merge. @dhh want to do the last review? |
Looks good to me! Great work, guys. |
A shortcut to setup controller environment
❤️ 💚 💙 💛 💜 |
🙇 🙇 🙇 |
❤️ this could make it easier for our use case to support poll-based async renders for really big api queries |
# Returns a renderer class (inherited from ActionController::Renderer) | ||
# for the controller. | ||
def renderer | ||
@renderer ||= Renderer.for(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is/should this be thread safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this will return a class to create a new instances of renderer from (so it doesn't matter if it's shared). And even if you use one instance of renderer across threads it's still be ok, since actual rendering happens always on a new controller instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not thread safe. Two unrelated instances can easily be created here. It should almost certainly be wrapped in a mutex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no point.
Renderer.for
- returns a factory for current controller. It won't matter if it's the same factory object or different one for the same controller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brainopia what if two different controllers use this code, but share the same thread and thus the ivar set on the class? When the second controller calls the renderer
method, the @renderer
ivar would already exist, except it'd be instantiated with the wrong controller instance and thus be a factory for the wrong controller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@egilburg each controller represents a different class, ivars won't be shared between them. @renderer
will be set and accessible only inside context of the current controller class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, which is why there's a thread safety issue. Renderer.for(ApplicationController)
should always return the same instance, which it doesn't in this case. If we have any mutable state in the renderer which we'd like to rely on, this will break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renderer
class has mutable defaults
, but it does not matter. The reason is the same why access to the one renderer
class across threads is safe even though access to underlying defaults
is not protected by mutex.
defaults
stores a static (predefined) value. That's enough to be safe in our case.
Dynamic environment should be passed as renderer.new(env)
(if a developer would try to use defaults
to dynamically change the environment he would need to use a mutex himself anyway to wrap a change to defaults
and consequent render
call).
I can explain it further if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ||=
is a read-check-write race condition, and since this is essentially a global (since it is stored on the class) we have to add a lock. Here's a test to demo:
require 'action_pack'
require 'action_controller'
require 'active_support'
require 'concurrent/atomic/count_down_latch'
wait_latch = Concurrent::CountDownLatch.new 100
go_latch = Concurrent::CountDownLatch.new
t = 100.times.map { |i|
Thread.new {
wait_latch.count_down
go_latch.wait
renderer = ActionController::Base.renderer
renderer.defaults[:foo] = i
renderer
}
}
wait_latch.wait
go_latch.count_down
renderers = t.map(&:value)
p renderers.map(&:object_id).uniq.length
p renderers.uniq.map { |r| r.defaults[:foo] }
p ActionController::Base.renderer.defaults[:foo]
On my machine using MRI (ruby 2.3.0dev (2015-09-12 trunk 51832) [x86_64-darwin14]
) I'm seeing between 24 and 34 unique instances of the renderer set on the controller class. Also you will not be able to predict the value of ActionController::Base.renderer.defaults[:foo]
.
I'm adding a mutex for now, but I'll see if I can change this to instantiate the renderer when the inherited hook fires so that we can have lock-free reads.
Oh, thank you guys, you're doing great work. I am glad I belong to this community and this is because of you. Big Thanks. |
We sometimes say "✂️ newline after `private`" in a code review (e.g. rails#18546 (comment), rails#34832 (comment)). Now `Layout/EmptyLinesAroundAccessModifier` cop have new enforced style `EnforcedStyle: only_before` (rubocop/rubocop#7059). That cop and enforced style will reduce the our code review cost.
We sometimes say "✂️ newline after `private`" in a code review (e.g. rails/rails#18546 (comment), rails/rails#34832 (comment)). Now `Layout/EmptyLinesAroundAccessModifier` cop have new enforced style `EnforcedStyle: only_before` (rubocop/rubocop#7059). That cop and enforced style will reduce the our code review cost.
Related to #18409
Some reiteration first.
What we looking for is an easy way to render templates outside of controllers.
View templates are rendered as part of request-response cycle. There are easy cases when templates would not depend on anything except ivars from controller, but a lot of templates use helpers that depend on request environment in one form or another.
Sometimes helpers depend on stuff that's easy to setup, like a scheme, host or script name. But request environment is also used by gems to hold transient data associated with current request (e.g., helpers from devise would raise an exception if request environment hasn't been prepared by its middleware).
Therefore two use-cases arise.
This PR deals with the first part.
Where
_render_options_
could containassigns: { a: 'b' }
to set ivars./cc @dhh