-
Notifications
You must be signed in to change notification settings - Fork 21.8k
Active Record API for general async queries #44446
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
Conversation
end | ||
|
||
def async! # :nodoc: | ||
@async = 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.
I went for a boolean attribute, but an AsyncRelation
subclass could make sense if it makes specializing the various method easier. To be seen once and if we move forward.
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 a subclass would be clearer here so if we do move this forward +1 on that change.
74b3835
to
552963b
Compare
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've reviewed this and tried it out in an application console to get a feel for the API. I think the public API is fine, I was trying to think of a different way that didn't require calling async.count
and then value
every time, but I haven't come up with anything that wouldn't require duplicating existing APIsto enable an
async_count`. It feels awkward to need to call so many methods to essentially do the same thing faster, but I also don't have a better solution at the moment.
@@ -1,6 +1,76 @@ | |||
# frozen_string_literal: true | |||
|
|||
module ActiveRecord | |||
class Promise < BasicObject |
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.
The public methods in here should be documented - I know this is WIP but I didn't want to forget to say that later when it's no longer draft.
end | ||
end | ||
|
||
class AsyncFallbackResult # :nodoc: |
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.
AsyncFallbackResult
makes it sound like the result isn't Async and that it's falling back to a regular query. That doesn't seem to be what's actually happening so maybe a different name to clarify. AsyncPromiseResult
maybe?
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.
AsyncFallbackResult makes it sound like the result isn't Async and that it's falling back to a regular query.
Yes that's the case. It's used when async_enabled?
is false
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.
So while working further on this, I renamed it in ActiveRecord::FutureResult::Complete
, which I think makes it bit more clear.
end | ||
|
||
def async! # :nodoc: | ||
@async = 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.
I think that a subclass would be clearer here so if we do move this forward +1 on that change.
552963b
to
e54b809
Compare
If So more complex use cases could still build on top of promises, but for the trivial "I know I'm going to need the count later", it'd act more like a preload. |
Interesting idea. It would be a bit like asynchronously preloading the query cache? It seems tricky to implement, though. Right now I'm ok with the current |
To clarify, my main issue with the current Implementation is that since So that may be another argument in favor of a subclass, which would allow us to implement and document selectively the methods that would make sense to have an async version. |
68b324a
to
01f6dba
Compare
So It also doesn't help only offering subset of methods, as we'd have to So the natural solution would be a decorator, but then it loses access to the private methods, so there would be quite a lot of duplication. Another possible solution would be to either have extra methods (e.g. I think that from a code maintenance perspective, the later ( |
I tried the named parameter approach in Shopify@37b0f60 |
68322c8
to
1563bc7
Compare
After sleeping on it, I believe that the
Assuming there's consensus on this, the question remain of which methods should have async support. For now I implemented:
What may or may not make sense to support is |
I like the named approach too. It feels like a more natural API to me. 👍🏼 |
c1b1060
to
5ee4b80
Compare
Of the various non-ideal options, I think I personally lean toward |
It's definitely better on the API documentation side. The downside is that it will be a bit harder to keep code in sync (except if So I'm not opposed to it if there's consensus that it's the right tradeoff. |
I think many of these public methods (especially the (It occurs to me that documentation would be particularly tricky if some other options were only/not valid when called in async mode... I don't have a specific example right now, but it does seem fairly plausible that'd be something that comes up?) |
295963d
to
750147e
Compare
Ok, I refactored the PR again, the newly introduced methods are:
There may be other methods that make sense, but I think this PR is chunky enough, so if everyone is satisfied with this API and implementation, we can focus on documentation and eventually add some more in a followup PR. |
Followup: rails#41372 Something we knew we'd need when we implemented `Relation#load_async` but that we chose to delay to have time to think about it. Right now, Active Record async support is limited to "collection results", but among the not so fast queries that would benefit from asynchronicity you often find aggregates (e.g. `count`, `sum`, etc) as well as hand crafted `find_by_sql` queries. `load_async` was easy to add as an API, because `Relation` acts as a collection so it was trivial to simply block whenever it was iterated while retaining total API compatibility. For aggregates and `find_by_sql`, we have no other choice but to return something different in async mode, with its own API. This proof of concept showcase what this API looks like for `Relation#count`: ```ruby Post.where(published: true).count # => 2 promise = Post.where(published: true).count(async: true) # => #<ActiveRecord::Promise status=pending> promise.value # => 2 ``` This API should be applicable to all aggregate methods, as well as all methods returning a single record, or anything other than a `Relation`.
750147e
to
e5b08b9
Compare
I'm so excited for this PR to land. We have dozens of use cases where I think these aggregations will make a massive difference in the overall performance of our codebase. |
Thanks for the great work on this! @casperisfine Super excited to apply this across the board! |
Careful though, this shouldn't be applied blindly to all queries. https://pawelurbanek.com/rails-load-async is a good write on the tradeoffs involved. |
wrap model with example: |
@pynixwang yes that's two very different things. |
@casperisfine I mean keeping async api same as sync api. |
@casperisfine @byroot Is there any plans as to which new version of Rails this PR will be included in? |
@OskarEichler, that will be Rails 7.1. |
Hey, folks, I'm curious if there are any plans for an |
@nettofarah not sure what you mean,
|
oh, you're right. I got confused. |
…iveRecordAllMethod` Follow up rails/rails#44446, rails/rails#37944, rails/rails#46503, and rails/rails#47010. This PR supports some Rails 7.1's new querying methods for `Rails/RedundantActiveRecordAllMethod`.
…iveRecordAllMethod` Follow up rails/rails#44446, rails/rails#37944, rails/rails#46503, and rails/rails#47010. This PR supports some Rails 7.1's new querying methods for `Rails/RedundantActiveRecordAllMethod`.
👋 I'm curious, why does If extending |
It is indeed by design to avoid the classic mistake of using the promise instead of its result which I've seen happen quite a lot with |
Got it, thanks! |
@casperisfine, is there a way to disable this functionality globally by default? in our app we started to get the following error:
as a workaround we were able to use ref1: https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-select_value |
I doubt the error you are showing is related. |
@byroot, at this point this change is the only one we could map to the problem, maybe there were other changes related to async DB queries? |
Are you trying to use multiple statement? Or perhaps have an explicit |
We run one SQL statement that trigger MySQL Function (related to column level encryption) |
All I can tell you is that this MySQL error message has nothing to do with async queries. |
well, it is, before this change everything was working fine, so that is the reason I'm seeking for configuration option that would disable new behaviour of Rails.. |
If you can come up with a reproduction script, I will look at it, but at this stage I really don't believe this change is your issue. Also there is already a setting to disable async queries globally, and they are disabled by default: https://guides.rubyonrails.org/configuring.html#config-active-record-async-query-executor Hence why I really don't believe you are blaming the right change. |
@byroot, thanks a lot! (not that I wished to blame your change, was trying to find the cause, and was too fast to write about our problem here) Setting |
Followup: #41372
Fix: #44169
Something we knew we'd need when we implemented
Relation#load_async
but that we chose to delay to have time to think about it.Right now, Active Record async support is limited to "collection results", but among the not so fast queries that would benefit from asynchronicity you often find aggregates (e.g.
count
,sum
, etc) as well as hand craftedfind_by_sql
queries.load_async
was easy to add as an API, becauseRelation
acts as a collection so it was trivial to simply block whenever it was iterated while retaining total API compatibility.For aggregates and
find_by_sql
, we have no other choice but to return something different in async mode, with its own API.This proof of concept showcase what this API looks like for
Relation#count
:But this API should be applicable to all aggregate methods, as well as all methods returning a single record, or anything other than a
Relation
.