-
Notifications
You must be signed in to change notification settings - Fork 898
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
Drop supports_feature? methods #22883
Conversation
106a21d
to
078b3a9
Compare
update:
ok, I'll see if that is used anywhere. Since these sometimes get exposed as columns, WIP: possibly drop supports_order / validate_supports |
FYI, I tried this PR with my rails 7 branch and things look good. |
The only thing I can think of that I didn't see covered in any of the comments here (unless I missed it), is that we have a handful of virtual columns representing "supports", which are meant to be accessed over the API. As long as those still work, I'm good with this. |
Thanks I changed Highlights / Supports Attribute to better highlight |
078b3a9
to
06e74fa
Compare
update:
update:
|
06e74fa
to
f02cb40
Compare
update:
|
update:
|
f02cb40
to
17cdfc8
Compare
@@ -99,24 +87,46 @@ def unsupported | |||
class_methods do | |||
# This is the DSL used a class level to define what is supported | |||
def supports(feature, &block) | |||
define_supports_feature_methods(feature, &block) | |||
self.supports_features = supports_features.merge(feature.to_sym => block || true) |
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.
My only concern here is that I think this needs to be made thread-safe. I think this is because we could lazy load classes at run-time in different threads (think 2 different requests coming that each need to lazy load things).
I know that lazy loading has bit us in the past with this - see the way the unsupported
method is defined to use a Concurrent::Hash for this reason.
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 the Rails pattern for defining class level configuration that inherits from parent classes and then inherit columns from parents and allow overriding.
- define class variable https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/attributes.rb#L11
- append to class variable https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/attributes.rb#L231
- use variable to set methods https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/attributes.rb#L265
We use this same pattern for virtual_attributes:
- https://github.com/kbrock/activerecord-virtual_attributes/blob/master/lib/active_record/virtual_attributes.rb#L41
- https://github.com/kbrock/activerecord-virtual_attributes/blob/master/lib/active_record/virtual_attributes.rb#L62
Since these values are set at class load time, and Rails has a class loader mutex to protect the other loaders, I am pretty sure we are in the clear.
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.
supported_features
, similar to rails attribute API, only modifies the class at load time and should be protected by the rails class loader mutex.
unsupported
is constantly modified at runtime. While I am not happy with this race condition, this PR focuses on removing the extra methods that were causing the infinite backtraces. In the future we can hopefully drop unsupported
and use supported_feature
instead.
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 created #22898 as a place holder for the next phase
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 code does a get->modify->set, and another thread can get in the middle. In the past, 2 threads could lazy-load 2 separate classes, particularly in the API.
only modifies the class at load time and should be protected by the rails class loader mutex.
@jrafanie Can you comment here? Not sure if this is now completely handled by zeitwerk or not, but this was a problem in the past and is why we had to wrap all of those class loading places in the special Rails handler, particularly in the API.
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.
hmm, there is discussion of thread safety for code reloading. I'm wondering if we need to implement something similar to what we had previously for dev mode reloading only. I think autoloading in production mode is fine as long as it's after the application is initialized/rails loaded.
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 code does a get->modify->set, and another thread can get in the middle. In the past, 2 threads could lazy-load 2 separate classes, particularly in the API.
Yes, The 'get, modify, set' of unsupported[]
is bad. This is the existing behavior.
The first commit's goal is to just consolidate the existing logic.
Rails uses this patter when defining methods: def field_name ; attr_reader(:field_name) ; end
instead of putting the actual gook into the defined method. (not always, but generally)
def supports(feature, &block)
define_method("supports_#{feature}?") do
+ common_method(feature)
+ end
+ end
+
+ def common_method(feature) # sorry rubocop for indentation
unsupported.delete(feature)
if block
instance_eval(&block) # gook (not for the class level)
else
unsupported.add(feature) # some kind of modify
end
!!unsupported[feature]
end
- end
I can see if there is a way to make this fact more obvious.
But again, I wasn't as concerned about fixing the code knowing that #22898 will remove the clear ; modify ; get
to a just plain get
.
only modifies the class at load time
[...] this was a problem in the past and is why we had to wrap all of those class loading places in the special Rails handler
There have been issues when one class referenced another class at load time. virtual_delegates
used to have this problem but we changed it to lazy reference the other class. has_many
also has this problem, and why we use class_name=String
rather than class references.
In our case, we are following the example from the attributes()
api. It defines Symbols/Strings that are put into a Hash
at load time and never changed after that.
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.
Ok discussed with @kbrock and I was mistakenly thinking that the class_attribute would be shared, so that child classes would be pushing values and could clobber each other. However, what's actually happening is the parent class loads first, then each child creates a copy of the parent class' feature and then inserts their own into the copy.
So this is fine 👍
update:
update:
|
17cdfc8
to
bebeadb
Compare
From Pull Request: ManageIQ/manageiq#22883
cross repo test failures:
|
update:
|
@Fryguy You have any concerns dropping the |
Nope I love it |
update:
|
previous implementation of supports ignored all return values new version expects a string/nil return value
Also, unsupported_reason_add is no longer responsible for defaulting the reason
consolidates is_supported and reason into a single value: true: always block: conditionally string: never (and reason) This will make it easier for us to store in a hash
… method. The 2 forms are similar if you take in account that the class form just returns supported=true for the block form of the supports check. an instance is passed in so the correct unsupported_reason_add is called and the reason is stored in the correct unsupported variable.
We were defining methods to determine supported features We are now using a hash with the stored values
now it only calls supports?() once (via unsupported_reason) rather than twice before. Besides that, it is the same
Checked commits kbrock/manageiq@15330bf~...181a03e with ruby 2.7.8, rubocop 1.56.3, haml-lint 0.51.0, and yamllint |
Do not merge until we talk
Overview
Change implementation of
supports
to not define methods.&:supports_feature?
calls(technically we do not need
unsupported_reason_add
to add this feature. But that initiative is also removingsupports_feature
- which is definitely needed)Followup:
Before
supports :feature
andsupports_not :feature
definessupports_{feature}?
.supports_{feature}?
callsadd_unsupported_reason
to store the reason.supports?(feature)
callssupports_{feature}?
under the covers.supports?(unknown_feature)
detects whensupports_{unknown_feature}?
is not defined and directly callsadd_unsupported_reason
.After
supports :feature
andsupports_not :feature
stores the reasons in a class level hash.supports_{feature}?
callsadd_unsupported_reason
to store the reason.supports?(feature)
uses class level hash to determine the reason. It leveragesadd_unsupported_reason
the same as before.supports?(unknown_feature)
notices that the feature is not in the class level hash and directly callsadd_unsupported_reason
.Highlights
Fix infinite stack trace for Supports Attribute
We have a feature called
supports_attributes
that introduces a virtual attributesupports_{feature}
(note: no trailing question mark). Since this is a boolean attribute, Rails defines this method with a question mark, which conflicts with the method thatsupports :feature
defines. (see before number 1)So when the code tries to determine if the feature is defined (see before number 4), it sees the rails attribute definition and assumes it is always defined and calls the rails attribute accessors.
Rails 6.1 worked fine with this bug, but in rails 7, it causes an infinite stack trace. This PR removes this duplicate definition issue and fixes both rails 6.1 and 7.0
Dropping supports_feature? methods
The
supports_{feature}?
method is useful when usingdetect
orselect
--detect(&:supports_feature?)
, so we used it a few times in the ui-classic and in core. We have pruned these out but unfortunately we do loose this shortcut. I thinksubclasses_supporting
has made those calls no longer necessary.Determining if supports_feature? methods are used
Searching the code base for calls to
supports_feature?
is a little tricky since there are methods with the namesupports_feature?
defined, but they are not using thesupports :feature
mechanism. There is an initiative (linked at the top) to converge on thesupports :feature
mechanism, but that has been a background threadReducing the number of methods defined
I remember having a conversation with @chessbyte to cut down on the number of methods defined in classes, but I can't find an issue actually stating this goal. That was the main reason I had embarked on this journey some dozen PRs ago.
We have not completed to goal to converge all the supports type mechanisms.
But this PR completes the goal to reduce all the extra methods defined on the class.
We did reduce quite a few methods defined by #22578 - that no longer defines the methods for every base class. That PR definitely exposed the problem we are seeing now, but supports should not be going through the rails attributes code, so that is why I am suggesting this solution