-
Notifications
You must be signed in to change notification settings - Fork 52
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
Do not depend on #method_missing for #respond_to? #14
Conversation
@exAspArk let me know if you have questions on the description. Its quite an edge case, but one we at GitLab ran into. Please let me know what you think about the patch. |
lib/batch_loader.rb
Outdated
LEFT_INSTANCE_METHODS.include?(method_name) || method_missing(:respond_to?, method_name, include_private) | ||
return true if LEFT_INSTANCE_METHODS.include?(method_name) | ||
|
||
loaded_value = defined?(@loaded_value) ? @loaded_value : __sync! |
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.
@ZJvandeWeg hey, thanks a lot for opening the PR!
After the object is batch loaded, the original method_missing implementation is not called anymore after a
__replace_with!
call.
Whenrespond_to?
is called on the loaded object afterwords this will call#method_missing
on the loaded object.
I think that method_missing(:respond_to?, method_name, include_private)
doesn't call the original method_missing
, it calls respond_to?(method_name, include_private)
through public_send
? Or there is another issue which can be reproduced with a test?
However, reusing already loaded value for respond_to?
make perfect sense to me.
Currently, __sync!
is designed to "sync" and "replace" the BatchLoader instance (with the cache
option true
by default). And "replacing" the object is quite overhead because __replace_with!
isn't the fastest method :)
For example, we could make __sync!
idempotent and smarter. So, if we call it multiple times and it was already executed – don't do anything. Something like:
def __sync!
+ return self if @replaced
loaded_value = __sync
if @cache
+ @replaced = true
__replace_with!(loaded_value)
else
loaded_value
end
end
This way it'll also respect the cache
flag. I.e. if user decided not to cache loaded value, it always will be loaded over and over again and returned to the user.
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 think that method_missing(:respond_to?, method_name, include_private) doesn't call the original method_missing, it calls respond_to?(method_name, include_private) through public_send? Or there is another issue which can be reproduced with a test?
Fair enough, I'll write a test to show what happens.
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.
@exAspArk I'll update this branch in a bit to have a failing test case. The first commit will be the failing test case, where the last commit will rectify this.
On the __sync!
idea, we can't call :respond_to?
on it, given it will keep recursively calling itself and thus fail when the stack gets too deep.
I've checked out if I can refactor to create a loaded_value
method. But first wanted to get your view on it, because you have a better idea how this library works. 😄
I'll rebase the test case commit away, but for now it might be good for you to checkout what I mean. |
@exAspArk Is there something I can do to move this along? 😄 |
@ZJvandeWeg hey, sorry for the delay, busy end of the year :) I'll review it on Friday. |
@ZJvandeWeg I see what you mean! We replace the Just one suggestion: # Instead of:
loaded_value = defined?(@loaded_value) ? @loaded_value : __sync!
loaded_value.respond_to?(method_name, include_private)
# Use:
__sync!.respond_to?(method_name, include_private) Since there is a Here is an example of the test which will be fixed with the changes: it 'syncs the object on every call' do
loaded_user = post.user_lazy(cache: false)
expect(User).to receive(:where).with(id: [1]).once.and_call_original
loaded_user.respond_to?(:id)
expect(User).to receive(:where).with(id: [1]).once.and_call_original
loaded_user.respond_to?(:id)
end Thanks a lot for the test and awesome commit messages! |
# Use:
__sync!.respond_to?(method_name, include_private) @exAspArk This won't work, as I also disagree with the test which you propose, actually. Because this will be testing the caching being turned of. However, the bug I ran into was the dependency of let me know what you think! 😄 |
@ZJvandeWeg oh, yeah, you're right. There will be a recursion =/ What do you think about: def respond_to?(method_name, include_private = false)
LEFT_INSTANCE_METHODS.include?(method_name) || __loaded_value.respond_to?(method_name, include_private)
end
private
def __loaded_value
result = __sync!
@cache ? @loaded_value : result
end ? It'll take the # no recursion
it 'syncs the object just once' do
loaded_user = post.user_lazy
expect(loaded_user.respond_to?(:id)).to eq(true)
end
# no cache
it 'syncs the object on every call with cache false flag' do
loaded_user = post.user_lazy(cache: false)
expect(User).to receive(:where).with(id: [1]).once.and_call_original
loaded_user.respond_to?(:id)
expect(User).to receive(:where).with(id: [1]).once.and_call_original
loaded_user.respond_to?(:id)
end |
After the object is batch loaded, the original method_missing implementation is not called anymore after a `__replace_with!` call. When `respond_to?` is called on the loaded object afterwards this will call `#method_missing` on the loaded object. However, the loaded object can itself have an implementation of method_missing, in this case only to determine if it responds to a symbol. Other than the current implementation, the `#method_missing` could be listed in `IMPLEMENTED_INSTANCE_METHODS`, so it won't be rerouted to the lazy loaded object. This however calls `__sync!` again, with the overhead that entails. Detected this behaviour first for #4, but that was the wrong patch.
@exAspArk Thanks for pointing me to the cache feature about twice now 😅 Its not something we use at the moment, so I keep forgetting about it. Your suggestions work, and I think I better understand the gem now to have more confidence in my own code too. Thanks for taking the time anyway. Much appreciated. I've squashed the commits into one, and looked over the diff once more, it seems all good to me. A bit noisy on the |
@ZJvandeWeg awesome!
Yeah, it is not so useful in production environment but may be helpful in test environment or REPL to avoid any side effects. However, it currently plays a dual role:
Absolutely. Even though I personally prefer Four-Phase Test practice to make each test more "independent" and readable over DRY and overusing RSpec features like Thanks a lot for your contribution! And sorry that it took me so long to understand the problem and review the PR. I'll release it in |
After the object is batch loaded, the original method_missing
implementation is not called anymore after a
__replace_with!
call.When
respond_to?
is called on the loaded object afterwords this willcall
#method_missing
on the loaded object. However, the loaded objectcan itself have an implementation of method_missing, in this case only
to determine if it responds to a symbol.
Other than the current implementation, the
#method_missing
could belisted in
IMPLEMENTED_INSTANCE_METHODS
, so it won't be rerouted to thelazy loaded object. This however calls
__sync!
again, with theoverhead that entails.
Detected this behaviour first for #4, but that was the wrong patch.