-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
5688: Run callbacks for children within fibers #5837
Changes from 11 commits
0b103f7
5e69a45
1092cf0
8933938
083d563
c84a182
de260db
dd7c35b
005c5b4
877f41c
16c9c00
1732a2c
f2ea1d5
85fb2d5
76f69f9
137dbe5
215513d
8ea5046
49552d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# frozen_string_literal: true | ||
|
||
module Mongoid | ||
module Errors | ||
# This error is raised when an around callback is | ||
# defined by the user without a yield | ||
class InvalidAroundCallback < MongoidError | ||
# Create the new error. | ||
# | ||
# @api private | ||
def initialize | ||
super(compose_message('invalid_around_callback')) | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,29 +161,34 @@ def _mongoid_run_child_callbacks(kind, children: nil, &block) | |
# Execute the callbacks of given kind for embedded documents including | ||
# around callbacks. | ||
# | ||
# @note This method is prone to stack overflow errors if the document | ||
# has a large number of embedded documents. It is recommended to avoid | ||
# using around callbacks for embedded documents until a proper solution | ||
# is implemented. | ||
# | ||
# @param [ Symbol ] kind The type of callback to execute. | ||
# @param [ Array<Document> ] children Children to execute callbacks on. If | ||
# nil, callbacks will be executed on all cascadable children of | ||
# the document. | ||
# | ||
# @api private | ||
def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block) | ||
child, *tail = (children || cascadable_children(kind)) | ||
children = (children || cascadable_children(kind)) | ||
with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks | ||
if child.nil? | ||
block&.call | ||
elsif tail.empty? | ||
child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block) | ||
else | ||
child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do | ||
_mongoid_run_child_callbacks_with_around(kind, children: tail, &block) | ||
|
||
return block&.call if children.empty? | ||
|
||
fibers = children.map do |child| | ||
Fiber.new do | ||
child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do | ||
Fiber.yield | ||
end | ||
end | ||
end | ||
|
||
fibers.each(&:resume) | ||
|
||
block&.call | ||
|
||
fibers.reverse.each(&:resume) | ||
|
||
rescue FiberError | ||
raise Mongoid::Errors::InvalidAroundCallback | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we sure that any There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the API docs a Not sure if differentiating these failure conditions would help any, but error specificity is never a bad thing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since our program and not the user handles what fibers are created and resumed, the only condition under which a dead fiber seems to be resumed is if the user does not call yield. This falls under the "attempting to call/resume a dead fiber" bucket. I believe specificity here may be helpful because users don't even need to know fibers are used underneath. However, I'm fine with leaving the error message to be more general as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would think suppressing these fiber errors (possibly with a log line only) would be the right way to go. Probably these will mostly pop-up when terminating an app. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I think about this, it occurs to me that there could be situations where a user might do something with fibers in their own code, in a callback (weird, but feasible). I wonder if it might be safer to explicitly look to see if the fiber is still f = Fiber.new { nil }
f.alive? #-> true
f.resume
f.alive? #-> false There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jamis I think raising TLDR; I do not think we should make Fibers do anything that current callbacks don't do. |
||
end | ||
|
||
# Execute the callbacks of given kind for embedded documents without | ||
|
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.
As per comment elsewhere, I am strongly opposed to introducing a new runtime error during persistence flow which users will need to handle in their code. Callbacks today do not raise such errors.
Now I will have to do:
everywhere in my app.
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.
@johnnyshields -- this exception is raised only for a single case: when an around callback fails to yield. This will always indicate a bug in the users code. If we did not raise the exception here, it would manifest instead as a
FiberError
, which would be confusing to users because it does not describe what the issue is.Rather than risk confusing users with a seeming non-sequitur, we're opting to raise this new error to help direct them to where the bug is in their code. If you always yield from your around callbacks, you'll never encounter the error.
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.
@jamis I understand that, but today (prior to the Fiber version) you can do an around callback which doesn't yield and it doesn't raise any error as far as I'm aware--it just doesn't continue the callback chain.
Is there a possibility that users are using non-yielding callbacks for flow control of their actions?
Also, what if we throw :abort in a callback?
I think this should be looked at in more detail...
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.
Thanks for your concern about this issue, @johnnyshields, but we really have investigated this and and have looked at this, in quite a bit of detail.
An around callback that fails to yield is not as innocuous as you think. Not only does it just fail to "continue the callback chain" -- it fails to perform the intended operation at all. That is to say, if you have an
around_save
without a yield, and you save the record, it will silently fail to save the record at all.This is why I said that this is a bug in their code, and I don't feel bad about surfacing this noisily now, where we didn't before. If someone's code "breaks" because of this, it will break in a way that it should have broken before. I do not believe there is any way that anyone was using non-yielding around callbacks for any useful purpose, because they wouldn't have been functional at all.
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.
Yes, what if someone is intentionally using that behavior like a validation in order to abort the save flow? For example:
App owners will now need to check that no callback is implementing the above pattern. Callbacks in Mongoid models now work differently than controllers, for example, which would allow the above.