Skip to content

Only preload misses on multifetch cache#31250

Merged
kaspth merged 1 commit intorails:masterfrom
lsylvester:only-preload-misses-on-multifetch-cache
Mar 15, 2018
Merged

Only preload misses on multifetch cache#31250
kaspth merged 1 commit intorails:masterfrom
lsylvester:only-preload-misses-on-multifetch-cache

Conversation

@lsylvester
Copy link
Copy Markdown
Contributor

Summary

When rendering a collection with cached: true for an ActiveRecord::Relation that has preload values, the preload would normally only be required for rendering the partial when it misses the cache and nor required for generating the cache key.

Currently, we have to chose between using preload, which loads unused ActiveRecord instances when the have a high cache hit rate, or not preloading the associations, which can result in N + 1 queries when there are a large number of cache misses.

This PR attempts to optimise this be only preloading the associations for the records that miss the cache.

An example application to demonstrate this behaviour is at https://github.com/lsylvester/multifetch-demo. An example from the log demonstrating this behaviour is:

  Rendering categories/index.html.erb within layouts/application
  Category Load (0.1ms)  SELECT "categories".* FROM "categories"
  Product Load (0.4ms)  SELECT "products".* FROM "products" WHERE "products"."category_id" IN (?, ?, ?, ?)  [["category_id", 4], ["category_id", 5], ["category_id", 8], ["category_id", 9]]
  Rendered collection of categories/_category.html.erb [18 / 22 cache hits] (57.9ms)

Without this change, we would either so a query for each of the categories that miss the cache, or have to load products for every category.

Concerns and Further Work

There may be instances where we want to preload the associations for generation of cache keys, or we may want to have all of the associations loaded because the collection is used in other contexts other than the current rendering.

Currently there is no API to support this, but it is possible to acheive the load behaviour by calling load on the association before it is passed to the render call.

I am looking for feedback on:

  • Is preloading associations only for models that miss the cache reasonable default behaviour? Or should this be explicitly opted into when calling render?
  • If it is default behaviour, is calling load on the relation before rendering a suffient API for opting out, or should an additional option to the render call be added.

If it is ok to be default behaviour then I assume that I would need to add some application config to control this behavour to prevent breaking existing application and to provide deprication warnings.

I am also not sure about the structure of this code is it is very tightly connected to both ActionView and ActiveRecord, so any feedback in that area is welcome.

@rails-bot
Copy link
Copy Markdown

r? @eileencodes

(@rails-bot has picked a reviewer for you, use r? to override)

@matthewd
Copy link
Copy Markdown
Member

Wow, this is super neat!

The tight coupling does seem less than ideal, but the basic principle of highlighting the subset of records that are "active" and only applying the preload to those is great!

Copy link
Copy Markdown
Member

@rafaelfranca rafaelfranca left a comment

Choose a reason for hiding this comment

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

We need to fix a lot of rubocop violations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can we make the arguments always required?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

collection_cache_association_loading.

@rafaelfranca
Copy link
Copy Markdown
Member

I think the coupling is fine at this level. Of course, it would be better if we can find a way to avoid it, but if not we are using the right mechanisms and it is similar to what we are doing with the controller runtime logs.

@lsylvester lsylvester force-pushed the only-preload-misses-on-multifetch-cache branch 3 times, most recently from 1309daa to 5f1e76f Compare November 28, 2017 05:44
@lsylvester
Copy link
Copy Markdown
Contributor Author

@rafaelfranca The rubocop violations have been fixed and other suggestions implemented.

@lsylvester lsylvester force-pushed the only-preload-misses-on-multifetch-cache branch from 5f1e76f to d04a32f Compare March 6, 2018 03:11
@kaspth kaspth merged commit 0085380 into rails:master Mar 15, 2018
@kaspth
Copy link
Copy Markdown
Contributor

kaspth commented Mar 15, 2018

Thanks!

y-yagi added a commit that referenced this pull request Apr 12, 2018
…ion-error-with-http-caching-and-collection-caching"

This reverts commit 8b0cdbe.

Reason: #31250 is not backported to 5-2-stable, and this change broke build.
https://travis-ci.org/rails/rails/builds/365396137
y-yagi referenced this pull request Apr 12, 2018
…r-with-http-caching-and-collection-caching

Fix ActiveRecord::ImmutableRelation is raised when collection caching and HTTP caching are used together
@ghiculescu
Copy link
Copy Markdown
Member

ghiculescu commented Mar 4, 2022

This PR creates a potential problem if you are using a custom cache key for the cached: option. For example:

@view.render partial: "test/partial", collection: @post, cached: proc { |post| [post, post.author] }

In this case it might not sense to use Russian-doll caching, as in, you may not want to update every single post when an author is changed. Instead you can use post.author in your cache key to ensure that the view is updated correctly if the author changes.

But if you do this you get an n+1 on post.author if you do @posts = Post.preload(:author) in your controller. As a workaround you can do @posts = Post.preload(:author).to_a, but that's a bit surprising and it's easy to forget to do.

I think this PR is great if you do cached: true, but is problematic if you do cached: proc {}. @kaspth would you accept a PR that modifies it so this logic only applies when doing cached: true?

@eileencodes
Copy link
Copy Markdown
Member

@ghiculescu can you open a new issue with a reproduction? Or a PR with a repro that fixes this? Otherwise the notification from this will get lost and forgotten.

@ghiculescu
Copy link
Copy Markdown
Member

@eileencodes I haven't tested it thoroughly but this is what I had in mind: #44616

ghiculescu added a commit to ghiculescu/rails that referenced this pull request May 7, 2022
rails#31250 optimised rendering a collection with `cached: true`, but also with `cached: proc {}`. The problem is the proc may reference associations that should be preloaded to avoid n+1s in generating the cache key.

For example:

```ruby
@posts = Post.preload(:author)

@view.render partial: "test/partial", collection: @post, cached: proc { |post| [post, post.author] }
```

This will n+1 on `post.author`.

To fix this, this PR will always preload the collection if there's a proc given for `cached`. `cached: true` will still not preload unless it renders, as it did in rails#31250.
@ghiculescu
Copy link
Copy Markdown
Member

@eileencodes I haven't tested it thoroughly but this is what I had in mind: #44616

Took a bit longer than I planned, but this is now a green build and should be a fix for the issue 👍

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.

7 participants