5

I have this in my _profile.html.erb partial:

<% cache [current_user.roles.first, profile, @selected_profile, params[:rating]] do %>

Yet this is what I see in my server log:

Read fragment views/profiles/26-20161212033839290582/profiles/52-20161213010040474070/profiles/14-20161213015458288839/profiles/34-20161212035644491093/profiles/33-20161212035644237925/profiles/38-20161207092843851446/profiles/35-20161212040016291016/profiles/36-20161212040016565707/profiles/4-20161213021028862933/profiles/39-20161207092843925084/profiles/46-20161207092844067579/profiles/47-20161207223703646028/profiles/37-20161212040016656625/660bdc6ad0b20e4c5329112cf79946f7 (0.1ms)

I am seeing nothing about roles there.

What's happening is that if I log in as a user with an admin role, and then login as a user with another role, I am seeing the cached profiles that are displayed as if I am an admin and not as that other user with the correct view.

What could be causing this and how do I fix it?

Edit 1

If I change the cache statement to be this:

<% cache [current_user, profile, @selected_profile, params[:rating]] do %>

And refresh, this is what the logs look like:

Write fragment views/users/2-20161218005548388099/profiles/37-20161212040016656625///bb163edd4a8c7af2db71a04243338e7b (0.1ms)
  Rendered collection of profiles/_profile.html.erb [1 times] (25.4ms)
Write fragment views/profiles/26-20161212033839290582/profiles/52-20161213010040474070/profiles/14-20161213015458288839/profiles/34-20161212035644491093/profiles/33-20161212035644237925/profiles/38-20161207092843851446/profiles/35-20161212040016291016/profiles/36-20161212040016565707/profiles/4-20161213021028862933/profiles/39-20161207092843925084/profiles/46-20161207092844067579/profiles/47-20161207223703646028/profiles/37-20161212040016656625/83ddeaa031bf68e602ce66af2d268317 (0.1ms)

Edit 2

When I binding.pry into the _profile.html.erb, I get the following:

[3] pry(#<#<Class:0x007f968480ec98>>)> current_user.roles.first
=> #<Role:0x007f969e4422a8 id: 1, name: "admin", resource_id: nil, resource_type: nil, created_at: Mon, 12 Sep 2016 00:38:47 UTC +00:00, updated_at: Mon, 12 Sep 2016 00:38:47 UTC +00:00>

I even tried the following:

<% cache [current_user.roles.first.name, profile, @selected_profile, params[:rating]] do %>

And it still gives me the same cached results when logged in as a non-admin user as it does an admin user.

Edit 3

Here is the cache block that calls the collection that invokes _profile.html.erb:

  <% cache @profiles do %>
    <div class="wrapper wrapper-content">
    <% @profiles.to_a.in_groups_of(3, false).each do |profiles| %>
        <div class="row">
            <%= render partial: "profile", collection: profiles %>
        </div>
    <% end %>
    </div>
  <% end %>
marcamillion
  • 32,933
  • 55
  • 189
  • 380
  • any chance this is nested in another cache block? what does `current_user` return right before this cache block? – Edmund Lee Dec 22 '16 at 01:52
  • I would suggest writing a helper spec or test that checks what the cache method actually is generating. To me it looks like something is forcing a recursion through an array of different profiles for some reason. Note the way you're doing this is not recommended http://api.rubyonrails.org/classes/ActionView/Helpers/CacheHelper.html#method-i-cache perhaps the easier solution is to rethink the key you want – engineerDave Dec 22 '16 at 20:35
  • @EdmundLee You are right. It is nested. I added the parent `cache block` as `Edit 3`. Refresh the question. – marcamillion Dec 23 '16 at 00:30
  • @marcamillion are you on rails 3 or 4? – Edmund Lee Dec 23 '16 at 03:50
  • @EdmundLee Rails 5. – marcamillion Dec 23 '16 at 05:14
  • @marcamillion your outer `cache @profiles do ` is breaking the inner cache. see: http://stackoverflow.com/a/41293940/1536309 – Blair Anderson Dec 24 '16 at 00:27

2 Answers2

2

Your parent cache is preventing the children caches from doing their job correctly. You can either remove the parent cache, or just move it to the top and then each child can be separate

Replace <% cache @profiles do %> with:

<% cache [current_user.roles.first.try(:name), @profiles, @selected_profile, params[:rating]] do %>

and then the inner _profile.html.erb

<% cache [current_user.roles.first.try(:name), profile, @selected_profile, params[:rating]] do %>
Blair Anderson
  • 19,463
  • 8
  • 77
  • 114
  • So this works, but I am not sure if it works just because I removed the `cache @profiles do`. I did try that initially and it worked. So once I remove the outtermost cache, this problem goes away. But I don't want to have to remove it. Ideally I would like to be able to cache them from there. How do I fix that outtermost cache so that it works? – marcamillion Dec 23 '16 at 05:22
  • @marcamillion Are you sure you want to cache the same fragment for each user? seems like the logic – Blair Anderson Dec 23 '16 at 06:23
2

According to rails' documentation, you can simply name all the dependencies as the cache name. Refer to ActionView::Helpers::CacheHelper#cache

Since you are nesting cache (or called Russian Doll Caching), you will need to name all the dependencies in your nested cache at the outest cache block. Something like this:

<% cache [@profiles, current_user, current_user.roles] do <%>
  # ...
<% end %>

Looking at the ActionView::Helpers::CacheHelper#fragment_name_with_digest method, it simply uses whatever you pass in to build the cache name. So you just need to make sure anything that could change is included in ANY affected cache block.

If you are using Rails 3

which doesn't seem to be the case, but just something to keep in mind, remember to add a version to the cache name, and bump the version whenever there is a change to the template. You will need to bump the version for all the outer block. Or opt to use cache digest gem, which already comes with Rails 4.

Edit

Let's identify all the variables here. You have the parent view which renders a collection of @profiles. And the view will change according to if the current user is an admin. Not sure why params[:rating] would matter in this case. And I am not sure what @selected_profile is for either. But I assume it should be included.

Here's what I would do.

# In User model, add a method to tell if it's an admin
# because "user.roles.first" seems fragile to me. what if the "roles" collection is sorted differently?
def admin?
  roles.where(name: 'admin').present?
end

# In the view
<% cache [@profiles, current_user, current_user.admin?, @selected_profile] do %>
  # ...

When rendering collections, you can actually pass in a cached option. And "If your collection cache depends on multiple sources (try to avoid this to keep things simple), you can name all these dependencies as part of a block that returns an array" according to here and here.

<%= render partial: "profile", collection: profiles, cached: -> profile { [ profile, current_user, current_user.admin?, @selected_profile ] } %>

And you shouldn't need to have another cache block in the partial.

Community
  • 1
  • 1
Edmund Lee
  • 2,514
  • 20
  • 29
  • I am using Rails 5. Unfortunately, just adding `current_user, current_user.roles` to the outest cache block doesn't seem to work :( – marcamillion Dec 23 '16 at 05:20