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

[feature] Allow using an ActiveRecord::Relation as a dependency #18

Merged
merged 2 commits into from
Feb 4, 2021

Conversation

katyho
Copy link
Collaborator

@katyho katyho commented Feb 4, 2021

Summary

Enable using an ActiveRecord::Relation object as a dependency of a memoized method. Do this by:

  • Adding a new case for ActiveRecord::Relation while extracting dependencies in RedisMemo::Dependency.depends_on
  • Re-use the logic to extract bind params while executing a query. (Is it safe to also hijack the thread local variables that are shared between activerecord/redismemo in this case?)

Follow up:

  • Do we want to be able to specify queries from finder methods? e.g. .find(), .pluck(), which immediately execute the query instead of lazy loading it like ActiveRecord::Relation?

Testing

Added specs

@katyho katyho requested a review from donaldong February 4, 2021 02:03
Copy link
Contributor

@donaldong donaldong left a comment

Choose a reason for hiding this comment

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

Do we want to be able to specify queries from finder methods? e.g. .find(), .pluck(), which immediately execute the query instead of lazy loading it like ActiveRecord::Relation?

Nope, this would not make sense IMO, it's the same as depends on an array of fixed values. Though I think we would expect people to call where.to_a / find / pluck to set depends_on dynamically sometimes. Those queries in the depends_on block might be cached elsewhere separately.

@@ -40,6 +43,20 @@ def depends_on(dependency, **conditions)
end
end

def extract_dependencies_for_relation(relation)
connection = ActiveRecord::Base.connection
query, binds = connection.send(:to_sql_and_binds, relation.arel) # Is there another way to get this info?
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is the right method to use here! we need to thoroughly test the rails versions we support before remove the "beta" tag anyway 😄

Note this method returns a tuple of 3: https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L42

sql, binds, preparable

) unless is_query_cached
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
ensure
RedisMemo::MemoizeQuery::CachedSelect.reset_current_query
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it safe to also hijack the thread local variables that are shared between activerecord/redismemo in this case?

I think there might be an issue: this assumes people won't run actual queries (exec_query) in the depends_on which might not be true. If there is an exec_query while extracting dependencies, it might continue to use whatever thread locals we have set since we have not cleared them yet. We can probably just reset everything before calling dependency_of tho

RedisMemo::ArgumentError,
"Invalid Arel dependency. Query is not enabled for RedisMemo caching."
) unless is_query_cached
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think in this dependency_of, we will can still pick up the thread locals from the previous extract_bind_params.

I do like the with_new_query_context idea tho! Perhaps we should wrap dependency_of in that block instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps we should wrap dependency_of in that block instead.

Hm I think an issue is the memoized exec_query dependency block relies on the bind parameters being extracted previously in the overridden method here. I explicitly set the current_query thread local variable, and expect extract_bind_params to set the bind params thread local variable (retrieved in CachedSelect.current_query_bind_params).

I think we can get around this but would have to change the logic in the exec_query dependency block to be able to extract bind params on the fly.

I think in this dependency_of, we will can still pick up the thread locals from the previous extract_bind_params.

Hm not sure I understand this. Unless another db query occurs within the block that with_new_query_context is wrapped around, we should be okay right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I guess

Unless another db query occurs within the block that with_new_query_context is wrapped around, we should be okay right?

Oh yeah I guess that's true. We don't call exec_query in exec_query.

The case I was thinking about is to have

memoize_method :site_names do |teacher|
  teacher.sites.each do |site|
    depends_on Site.where(id: site.id)
  end
end

but the inner exaction would not be affected by the outer query anyway.

RedisMemo::ArgumentError,
"Invalid Arel dependency. Query is not enabled for RedisMemo caching."
) unless is_query_cached
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I guess

Unless another db query occurs within the block that with_new_query_context is wrapped around, we should be okay right?

Oh yeah I guess that's true. We don't call exec_query in exec_query.

The case I was thinking about is to have

memoize_method :site_names do |teacher|
  teacher.sites.each do |site|
    depends_on Site.where(id: site.id)
  end
end

but the inner exaction would not be affected by the outer query anyway.

@katyho katyho changed the title [feature][wip] Allow using an ActiveRecord::Relation as a dependency [feature] Allow using an ActiveRecord::Relation as a dependency Feb 4, 2021
@katyho katyho merged commit 3d0f550 into main Feb 4, 2021
@katyho katyho deleted the kho/arel_as_dependency branch February 4, 2021 21:36
@donaldong donaldong mentioned this pull request Feb 8, 2021
donaldong added a commit that referenced this pull request Feb 8, 2021
### Features
- #14 Locally cache computation to extract dependencies
- #16 Support overwriting uuid generation
- #18 Allow using an ActiveRecord::Relation as a dependency
- #19 Optimize activerecord-import cache invalidation
- #20 Trace invalidation key and enqueue_to_finish duration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants