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

Fix thread_mattr_accessor share variable superclass with subclass #25681

Merged
merged 1 commit into from
Aug 8, 2016

Conversation

willnet
Copy link
Contributor

@willnet willnet commented Jul 4, 2016

Summary

The current implementation of thread_mattr_accessor set variable
sharing superclass with subclass. So the method doesn't work as documented.

Precondition

class Account
  thread_mattr_accessor :user
end

class Customer < Account
end

Account.user = "DHH"
Account.user  #=> "DHH"
Customer.user = "Rafael"
Customer.user # => "Rafael"

Documented behavior

Account.user  # => "DHH"

Actual behavior

Account.user  # => "Rafael"

Current implementation set variable statically likes Thread[:attr_Account_user],
and Customer also use it.

Make variable name dynamic to use own thread-local variable.

@rails-bot
Copy link

Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @senny (or someone else) soon.

If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes.

Please see the contribution instructions for more information.

thread_mattr_reader :shaq, instance_reader: false
thread_mattr_accessor :camp, instance_accessor: false
end
class Foo
Copy link
Member

Choose a reason for hiding this comment

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

Are anonymous classes not sufficient to reproduce the issue? Would:

@class = Class.new do

end

@subclass = Class.new(@class) do

end

not work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anonymous classes don't work with thread_mattr_accessor.
thread_mattr_accessor uses class name as part of thread local variable name.

Following code set Thread[:attr_Account_user].

class Account
  thread_mattr_accessor :user
end

But anonymous class set Thread[:attr_user]. Its subclass use same variable because
both of them don't have name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If anonymous class use thread_mattr_accessor raise error by my pull request.
I will fix If it's needed.

Copy link
Member

Choose a reason for hiding this comment

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

If it worked before it would be nice to have it working still.

Copy link
Member

Choose a reason for hiding this comment

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

In this case I think it'd be better for it to raise: the previous behaviour treated all anonymous classes as one, so that doesn't seem terribly useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@senny OK. I added a commit not to raise error if use thread_mattr_accessor with anonymous class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, Bad timing. Which is do you think better? @senny

Copy link
Member

Choose a reason for hiding this comment

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

What @matthewd pointed out sounds reasonable. If it didn't work properly before then we should be good to raise. Would be nice to have the inline classes in the test-case work though. Maybe using def name; "MyClass" end does the trick.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I reverted a previous commit and add a commit using the trick to use anonymous class in test case 😃.

@senny
Copy link
Member

senny commented Jul 25, 2016

@willnet good catch! Added a minor note about the test style. If possible I'd like to stick to the anonymous classes that we had before.

@senny
Copy link
Member

senny commented Aug 2, 2016

@willnet can you squash your changes into a single commit? There's no need for us to keep track of that revert in your branch. Makes the history a lot cleaner if everything related to a change is in the same commit. Thanks.

@willnet willnet force-pushed the fix-thread_mattr_accessor branch 2 times, most recently from 4eb6c98 to 345eaf5 Compare August 2, 2016 12:58
@willnet
Copy link
Contributor Author

willnet commented Aug 2, 2016

@senny OK, I did it 😃.

@senny
Copy link
Member

senny commented Aug 2, 2016

@willnet awesome! Sorry one last thing I forgot to mention, could you add an entry to the CHANGELOG outlining the fix?

@willnet
Copy link
Contributor Author

willnet commented Aug 2, 2016

@senny OK, I did it 😄.

The current implementation of `thread_mattr_accessor` set variable
sharing superclass with subclass. So the method doesn't work as documented.

Precondition

    class Account
      thread_mattr_accessor :user
    end

    class Customer < Account
    end

    Account.user = "DHH"
    Account.user  #=> "DHH"
    Customer.user = "Rafael"
    Customer.user # => "Rafael"

Documented behavior

    Account.user  # => "DHH"

Actual behavior

    Account.user  # => "Rafael"

Current implementation set variable statically likes `Thread[:attr_Account_user]`,
and customer also use it.

Make variable name dynamic to use own thread-local variable.
@senny senny merged commit 3529e58 into rails:master Aug 8, 2016
senny added a commit that referenced this pull request Aug 8, 2016
Fix `thread_mattr_accessor` share variable superclass with subclass
@senny
Copy link
Member

senny commented Aug 8, 2016

@willnet thanks 💛 ! Merged with a slightly modified version of the CHANGELOG entry.

senny added a commit that referenced this pull request Aug 8, 2016
Fix `thread_mattr_accessor` share variable superclass with subclass
@senny
Copy link
Member

senny commented Aug 8, 2016

Backported to 5-0-stable: 3469a1b

@willnet
Copy link
Contributor Author

willnet commented Aug 8, 2016

寿司ゆき:thanks

@willnet willnet deleted the fix-thread_mattr_accessor branch August 8, 2016 12:54
@fxn
Copy link
Member

fxn commented Aug 8, 2016

Let me double-check this patch, just in case.

Given:

class A
  thread_cattr_accessor :foo
  self.foo = 1
end

class B < A
end

before this change the attribute was shared/inherited:

B.foo # => 1

and after this patch the attribute is no longer shared/inherited:

B.foo # => nil

This interpolation has been there since the initial implementation.

Which documentation does the description of the PR refer to?

@dhh what do you think?

@senny
Copy link
Member

senny commented Aug 8, 2016

@fxn here's the docs this was referring to: http://api.rubyonrails.org/classes/Module.html#method-i-thread_mattr_accessor

screen shot 2016-08-08 at 16 57 03

@fxn
Copy link
Member

fxn commented Aug 8, 2016

OK. And the sentence

if the parent class changes the value, the value of subclasses is not changed

also seems to match the new behaviour in my example.

Looking good then. The only thing that bothers me in these macros is that the documentation leaks the implementation (Thread.current[some_internal_key]. I believe end-users should just use the reader/writer interface.

@senny
Copy link
Member

senny commented Aug 8, 2016

@fxn totally agree on leaking the implementation. I'm going to remove the Thread.current[:attr_Current_user] from the docs. I only looked at thread_mattr_accessor which interestingly doesn't show them. It's only in the reader / writer docs...

@senny
Copy link
Member

senny commented Aug 8, 2016

@fxn on second thought, without the Thread.current references the docs for thread_mattr_reader and thread_mattr_writer don't make any sense. The thing is that these methods don't make a whole lot of sense without revealing the variable name. Why would you define a reader or a writer for a variable you don't know.

@fxn
Copy link
Member

fxn commented Aug 8, 2016

@senny totally agree.

A reader without a writer is useless if the user is not supposed to use the private key directly. And viceversa, why write something you cannot read? I think the public interface here is the accessor, and the reader/writer would be private methods that help implement the accessor.

If you want something to be private you can issue a manual private call.

senny added a commit that referenced this pull request Oct 20, 2016
This is a follow up to #25681, specifically this comment:
#25681 (comment)

The way the thread local variable is stored is an implementation detail
and subject to change. It makes no sense to only generate a reader or
writer as you'd have to know where to read from or where it writes to.
@senny
Copy link
Member

senny commented Oct 20, 2016

I pushed a commit that hides the _reader and _writer methods from the API docs: dce4751

senny added a commit that referenced this pull request Oct 20, 2016
This is a follow up to #25681, specifically this comment:
#25681 (comment)

The way the thread local variable is stored is an implementation detail
and subject to change. It makes no sense to only generate a reader or
writer as you'd have to know where to read from or where it writes to.
@@ -41,14 +41,14 @@ def thread_mattr_reader(*syms)
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}
Thread.current[:"attr_#{name}_#{sym}"]
Thread.current["attr_"+ name + "_#{sym}"]
Copy link

@atzorvas atzorvas May 19, 2017

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#{name} is different from + name + in class_eval.

#{name} is evaluated immediately on class_eval, but + name + is evaluated on executing method defined in class_eval

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

7 participants