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
ActiveSupport::CurrentAttributes provides a thread-isolated attributes singleton #29180
Conversation
…ributes singleton
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.
Some minor details, but 👍 on the concept.
end | ||
|
||
def person=(person) | ||
attributes[:person] = person |
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.
Reading this I'd think super
would just work here. Should we make that work or too much of a hassle?
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.
Do take a swing and see what contortions are needed, if you'd like 👍
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.
Actually, not sure about that. We declare the attributes in the same class. They didn't come from super. I think super is too clever.
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.
We make it work in AR/AMo, where they similarly belong to the current class.
It just means that instead of defining the built-in methods in the class directly, we put them into an anonymous module included for that purpose.
It's cleverer than we need to be, but I think it's simple enough that it's worth the gain in "do what I mean" expressiveness. It'll help people who do expect it to work, and I can't think of any way it'd manage to unpleasantly surprise someone who doesn't.
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.
Pushed a commit for this. Opted to not define the singleton methods within the module, since those should just be left be by users.
Person = Struct.new(:name, :time_zone) | ||
|
||
class Current < ActiveSupport::CurrentAttributes | ||
attribute :world, :account, :person |
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.
Could this be conflated with attribute
from Active Model and Active Record?
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.
It's the same concept, so I think it's good that it's using the same name.
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.
I still think we could confuse people into thinking that the Active Model attributes type casting stuff is available here.
So since these are mostly attributes
in name only — there's no dirty tracking, aliasing or other goodies from Active Model — perhaps we should just think of them as accessors?
class Current < ActiveSupport::CurrentAccessors
accesses :world, :account, :person # Or maybe even `attr_accessor`?
end
|
||
Person = Struct.new(:name, :time_zone) | ||
|
||
class Current < ActiveSupport::CurrentAttributes |
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 we namespace this and Person
so it won't clash with other tests?
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.
Done.
# end | ||
# end | ||
# end | ||
def expose(exposed_attributes) |
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.
Perhaps make this **exposed_attributes
so we get the type features for free (and the method won't NoMethodError on keys
.
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.
On the other hand why not just do this:
def expose(exposed_attributes)
old_attributes, @attributes = @attributes, exposed_attributes
yield
ensure
@attributes = old_attributes
end
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.
If we don't call the writers, we don't trigger stuff like Time.zone =
.
# around everywhere: | ||
# | ||
# # app/services/current.rb | ||
# require 'active_support/current_attributes' |
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.
active_support/all
should include this, so the require won't be necessary.
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.
I just set CurrentAttributes
up as an autoload in the top level active_support.rb
mirroring ActiveSupport::Concern
.
Since module Current
thread_mattr_accessor :user
module_function
def reset
Current.user = nil
end
end and in controllers I'm doing the hygiene: class ApplicationController < ActionController::Base
around_action :reset_current
def reset_current
Current.reset
yield
ensure
Current.reset
end
end What would be nice is if we provide a fire-and-forget functionality which will do the resetting for each request. I'm thinking either a class flag ( |
Thus they won't conflict with other Current and Person stubs.
@dhh was toying with the idea of generating |
I believe we'll be able to reset fully automatically through the executor. |
@matthewd ahh, right, of course! |
Right, we could keep track of the subclasses: class ActiveSupport::CurrentAttributes
mattr_accessor(:subclasses) { [] }
class << self
def inherited(subclass)
subclasses << subclass
end
def reset_all
subclasses.each(&:reset)
end
end
end Then schedule an |
Define instance level accessors in an included module such that `super` in an overriden accessor works, akin to Active Model.
Follow the example of concerns, autoload in the top level Active Support file.
e7e8c93
to
a2e8717
Compare
Two quick things:
|
I think both approach aren't incompatible at all, in the app I'm working currently we have As @dhh said this should be used carefully and in a few cases. |
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.
👍 but would like to see the recommendation by @matthewd / @kaspth in #29180 (comment) done before of merge
When does that executor run? Want to make sure we're clearing both before and after a request starts, just to ensure nothing is leaking anywhere. Also, I think we can make it explicit that a CurrentAttributes class participates in this auto-resetting scheme. Maybe just have |
Jon,
|
@dhh executor runs before and after the request in a middleware |
@dhh IMO this is great addition to the framework. I'm using everywhere
|
Skips the need for users to actively embed something that resets their CurrentAttributes instances.
test "current customer is assigned and cleared" do | ||
get "/customers/set_current_customer" | ||
assert_equal 200, last_response.status | ||
assert_match(/david, Copenhagen/, last_response.body) |
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.
It would be nice if I could figure out how to test that the customer
and Time.zone
is reset after the request as well. Any bright ideas? 😄
Also remove the stale puts debugging.
Got the auto-reset setup! One thing that bothers me about But these attributes are more purely about broadening the places where they're accessible within an app. They're accessors. How about: class Current < ActiveSupport::CurrentAccessors
accesses :world, :account, :person # Or maybe even `attr_accessor`?
end |
Awesome, @kaspth! The attributes thing doesn't bother me the slightest. Accessors is just beating around the bush, and imo less clear. Ruby itself uses attr_accessor, which is short for attribute_accessor. Here, we're actually declaring the attributes. Anyway, this is all looking good to ship. I'll update the docs and merge. Much appreciate the API debate and the implementation upgrades 🙏👏🎉 |
setup do | ||
build_app | ||
|
||
app_file "app/services/current.rb", <<-RUBY |
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.
For consistency
-app_file "app/services/current.rb", <<-RUBY
+app_file "app/models/current.rb", <<-RUBY
https://github.com/rails/rails/pull/29180/files#diff-3c3c0f647bc4702f9453c173a707aa06R10
end | ||
|
||
def compute_attributes(keys) | ||
keys.collect { |key| [ key, public_send(key) ] }.to_h |
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.
-keys.collect { |key| [ key, public_send(key) ] }.to_h
+keys.collect { |key, _| [ key, public_send(key) ] }.to_h
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.
Why? We're iterating over just an array of the keys, not a full hash.
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.
Oh, sorry I just now saw https://github.com/rails/rails/pull/29180/files#diff-3c3c0f647bc4702f9453c173a707aa06R167
Ah yeah, good point. 👍 on sticking with I noticed that storing instances in |
@kaspth do you have any idea on timeline for this feature to be released? i'm currently using a hacky homegrown implementation that i'd love to replace right away with this implementation (especially since i'm working on some related changes in my codebase right now). |
Rails 5.2 sometime this year. |
I am very opposed to this feature but after what happened last time I opposed a change on Rails I am not willing to fight this fight. It is a massive footgun and you're doing a massive disservice to the framework to introduce a global state. Instead, I have stated my case in a blog post: http://ryanbigg.com/2017/06/current-considered-harmful. Afterall, this is what DHH encouraged last time.
Choose to interpret it as bile if you wish. I meant it in a well-meaning fashion. Everyone's entitled to their own opinions, right? |
A blog post is a great outlet to provide a different take on Current. Thanks for taking the time to write it up.
I disagree with most of what's written, and found the "DHH just did this! 😱" pretty hilarious, but good to have a different take.
This pattern is used by Basecamp, GitHub, Shopify, and many others. It's not a new invention, but an extraction. Like almost all features in Rails.
Anyway, as always, you're free to decline the unagi. We can enjoy the rest of the menu together.
… On Jun 22, 2017, at 06:58, Ryan Bigg ***@***.***> wrote:
I am very opposed to this feature but after what happened last time I opposed a change on Rails I am not willing to fight this fight.
Instead, I have stated my case in a blog post: http://ryanbigg.com/2017/06/current-considered-harmful. I will not enter into any more discussion on this.
—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub, or mute the thread.
|
@dhh I agree that we can enjoy the rest of the menu together. But it's the overall direction of the framework that a lot of people are worried about; meanwhile the rest of the programming community shies away from bad design patterns, Rails happily embraces them. |
Rails has been embracing patterns that some people consider to be "bad" since inception, yet we are somehow still here and in better shape than ever. I'm going to go with yesterday's weather and predict that the inclusion of Current will not change that conclusion one iota either. Anyway, enjoy the rest of the menu! |
@clairity We have extracted the feature into a gem: https://github.com/coup-mobility/activesupport-current_attributes. Can be used with Rails >= 4.2. |
Thanks, Ahmed! Please retain the original copyright in the license file.
…On Fri, Jul 7, 2017 at 4:17 PM, Ahmed Hazem ***@***.***> wrote:
@clairity <https://github.com/clairity> We have extracted the feature
into a gem: https://github.com/coup-mobility/activesupport-
current_attributes.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#29180 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAKtYwnK8LA_FwTCiOUzfdzGejEvhDKks5sLj3jgaJpZM4NiKMV>
.
|
yes thanks @ahazem! i dropped it into my app and it's looking good so far! |
I opened an issue to track the required license change in that gem https://github.com/coup-mobility/activesupport-current_attributes/issues/2 |
@rafaelfranca @dhh please check https://github.com/coup-mobility/activesupport-current_attributes/commit/1cd6ffc0cfed385e2385c26e68e6403db4039ffa this commit should fix copyright issue please just confirm! |
@ahazem you might want to pull in its latest incarnation from the master branch. I've made some extra commits since merge. https://github.com/rails/rails/commits/master/activesupport/lib/active_support/current_attributes.rb :) |
@kaspth Nice, will pull them in! Thanks for pointing that out. |
Abstract super class that provides a thread-isolated attributes singleton.
Primary use case is keeping all the per-request attributes easily available to the whole system.
The following full app-like example demonstrates how to use a Current class to
facilitate easy access to the global, per-request attributes without passing them deeply
around everywhere:
A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result.
Current should only be used for a few, top-level globals, like account, user, and request details.
The attributes stuck in Current should be used by more or less all actions on all requests. If you start
sticking controller-specific attributes in there, you're going to create a mess.