Skip to content
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

Merged
merged 6 commits into from
Jan 22, 2015

Conversation

brainopia
Copy link
Contributor

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.

  • Ability to render templates in custom request environment.
  • Ability to render templates in an environment that was passed through middleware stack.

This PR deals with the first part.

# this would setup following environment keys: REQUEST_METHOD, SERVER_NAME, SERVER_PORT, QUERY_STRING, PATH_INFO, rack.url_scheme, HTTPS, rack.input
FooController.setup_for(_uri_).render_to_string _render_options_  

# or more detailed setup
controller = FooController.setup_for _uri_, script_name: 'bar', method: 'post', 'env_key' => 'baz'
... # configure controller instance a bit
controller.render_to_string _render_options_ 

Where _render_options_ could contain assigns: { a: 'b' } to set ivars.

/cc @dhh

@brainopia
Copy link
Contributor Author

The API is supported for arbitrary controller since rendering depends on lookup_context and view_context (both can differ between controllers (e.g., different helpers because of helper_method or access to different views because of custom view_paths)).

@brainopia
Copy link
Contributor Author

I've added ActionController::Renderer, example usage can be seen in tests https://github.com/brainopia/rails/blob/action_view_render/actionpack/test/controller/renderer_test.rb

@dhh
Copy link
Member

dhh commented Jan 18, 2015

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.

On Jan 17, 2015, at 4:12 PM, Ravil Bayramgalin notifications@github.com wrote:

I've added ActionController::Renderer, API examples can be seen in tests https://github.com/brainopia/rails/blob/action_view_render/actionpack/test/controller/renderer_test.rb https://github.com/brainopia/rails/blob/action_view_render/actionpack/test/controller/renderer_test.rb

Reply to this email directly or view it on GitHub #18546 (comment).

@brainopia
Copy link
Contributor Author

Thanks for guiding, David!

ApplicationController.render looks good, I've added this shortcut.

I have a few doubts over ApplicationController.renderer.new(...).render instead of ApplicationController.renderer(...).render. Maybe the latter is a bit better? Although the former has an advantage with ability to inherit class from ApplicationController.renderer (a rare use-case). What would you prefer?

@dhh
Copy link
Member

dhh commented Jan 19, 2015

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 = {
Copy link
Member

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.

@dhh
Copy link
Member

dhh commented Jan 19, 2015

Once the few coding notes have been taken care of, it looks like we're ready to merge? Lovely work, @brainopia.

@brainopia
Copy link
Contributor Author

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 ApplicationController.render shortcut, therefore redefining ApplicationController.renderer wins in that aspect compared to inheriting from it. But may be reopening class with class_eval looks a bit inappropriate to show off in the initializer?

And should we show all possible ways to customize renderer behavior (as in option 1) or defaults is enough (as in option 2).

Should we use simple defaults update (as in option 1) or show off Rack::MockRequest.env_for?

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)
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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. 👍

@brainopia
Copy link
Contributor Author

@rafaelfranca I've updated code to reflect your notes, thank you!
The only thing after latest change that makes me a bit uneasy - https://github.com/rails/rails/pull/18546/files#diff-568a4685d0dc681235508ba36884cbf9R32, it's a bit harder to redefine view_context.

@dhh The question above regarding initializers (#18546 (comment)) is still actual.

@rafaelfranca
Copy link
Member

💣 I forgot that view_context could be overridden and it is even documented. I quick search in GitHub show that people is using it https://github.com/search?p=1&q=%22def+view_context%22&ref=searchresults&type=Code&utf8=%E2%9C%93. So I guess we can't change it 😢.

@brainopia
Copy link
Contributor Author

@rafaelfranca yeah, I've reverted to previous :assigns implementation 🍰

@brainopia
Copy link
Contributor Author

@dhh here you go 😄

class << self
delegate :render, to: :new

def for(controller)
Copy link
Member

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?

@rafaelfranca
Copy link
Member

Awesome work on this @brainopia

brainopia added 6 commits January 21, 2015 23:53
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
@brainopia
Copy link
Contributor Author

@rafaelfranca Thank you for the help! I think now I've covered all bases 😄

@rafaelfranca
Copy link
Member

Cool! Now it is good to merge. @dhh want to do the last review?

@dhh
Copy link
Member

dhh commented Jan 22, 2015

Looks good to me! Great work, guys.

rafaelfranca added a commit that referenced this pull request Jan 22, 2015
A shortcut to setup controller environment
@rafaelfranca rafaelfranca merged commit e36ecf4 into rails:master Jan 22, 2015
@rafaelfranca
Copy link
Member

❤️ 💚 💙 💛 💜

@brainopia
Copy link
Contributor Author

🙇 🙇 🙇

@egilburg
Copy link
Contributor

❤️ 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)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

@nafaabout
Copy link

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.

@rafaelfranca rafaelfranca modified the milestones: 5.0.0 [temp], 5.0.0 Dec 30, 2015
kamipo added a commit to kamipo/rails that referenced this pull request Jun 13, 2019
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.
koic pushed a commit to koic/oracle-enhanced that referenced this pull request Apr 15, 2020
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants