Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

[PATCH]content_for and fragment caching

#3409

There are a lot of places where people talk about content_for and fragment caching being incompatible:

#1896 content_for not rendering from partials in layout
http://blog.innerewut.de/2009/5/8/fragement-caching-tip
http://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper...

etc.

Since this seems to be a common gotcha, and it is entirely possible to support this feature, I'm proposing this as a feature request.

One solution would be to store content_for results in a list which gets serialized and stored in the fragment cache. Then when the fragment cache is read the items could be deserialized and content_for could be called for each as the cached data is appended to the buffer. The serialization format could be very simple (i.e. something length prefixed instead of escaped, or with a large random delimiter) to avoid a serious serialization penalty.

I'm volunteering to make a first pass at this, if someone will agree to accept it if it and the tests are up to par.

Reported by Greg Hazel · October 22nd, 2009 @ 06:28 AM

State: hold
Milestone: 3.0.2
Assigned to: José Valim José Valim
Importance: High

Activity

  1. Steve
    Steve
    • Tag changed from caching, content_for, feature_request to caching, content_for, feature_request, patch

    At my company, a simple grep -R "content_for" app | wc -l shows we have 86 places where we use content_for. We would like to use action caching to improve our site performance while still retaining some dynamism, but unfortunately refactoring to remove our reliance on content_for is simply impractical.

    So without further ado I introduce to you... action caching AND fragment caching that both support content_for blocks!! Tryout my patch and let me know if you have any success.

    I added tests for every piece of functionality I added, and I ran the tests for the files I changed. Everything appears to be OK, but I couldn't get the full rails test suite passing even without my patch applied (rvm + ruby1.9.1 + pg + mysql = FAIL!). But like I said, these other tests seem less applicable. ActiveSupport had a bunch of warnings about i8ln and of cource ActiveRecord mysql + postgre wouldn't run. Everything else seemed to be OK.

    March 17th, 2010 @ 07:53 AM

  2. Steve
    Steve

    Whoops that patch was for 3.0b1. I can probably make a similar one for 2.3.6 without much trouble, but... don't try to apply this to 2.3.6.

    Seems a little unfortunate that there is no obvious way to edit or remove my previous reply.

    March 17th, 2010 @ 07:59 AM

  3. Steve
    Steve

    OK maybe I should have let my mind simmer for awhile before I submitted that patch. I just thought of a small problem with the way I implemented the fragment content_for caching, and a good way I could DRY up the code a little bit. Stay tuned for a new patch in the next couple days... I'll also take a look at implementing this for Rails 2.3.6.

    March 17th, 2010 @ 04:06 PM

  4. Steve
    Steve

    Whew! OK I'm making progress. I had a nightmarish merge because I (foolishly) started from the 3.0b1 tag when I was originally working on this, instead of starting from master.

    I definitely made some major improvements over my previous attempt. It works great and tests pass, but I think I can do better. I'll sleep on it then post the new patch. No point in rushing it I guess, especially considering I appear to be the only one interested in this patch. ;)

    Thanks,
    Steve

    March 18th, 2010 @ 09:35 AM

  5. Greg Hazel
    Greg Hazel

    I'm interested! :)

    Take your time, though.

    March 18th, 2010 @ 03:57 PM

  6. Steve
    Steve

    OK I have one failing test left. The problem I'm trying to fight through seems to be that in the testing environment, @controller.view_context always brings back a new object, so trying to assert content_for?(:foo) will of course not work, as it's transitory and will vanish the next time you call @controller.view_context

    I guess I'll look at moving my fragment_for test with content_for to the view tests, but right now it's looking fairly confounding. Plus trying to patch Rails is like coding for a moving target, so it's proved an incredible time-sink: write the tests, writing the code to make them pass, rebase, make the tests pass again, rebase, make the tests pass again.... etc. It's like I'm stuck in an infinite loop. Even if I get them to pass and rebase again and everything still looks good and post a patch really quick, it will probably be almost immediately impossible to apply cleanly.

    How do most people deal with this?

    March 23rd, 2010 @ 09:17 PM

  7. Steve
    Steve
    • Title changed from content_for and fragment caching to [PATCH]content_for and fragment caching

    OK well maybe I was exaggerating a bit. Anyway time will tell. I finally got this working, albeit with a slightly shady test implementation (by memoizing view_context in the test controllers).

    So this probably not the best implementation that is possible, but it's the best implementation I could figure out in a first pass. Here is the basic strategy:

    Action Caching & Fragment Caching

    • Added a method CaptureHelper#all_content_for which returns a deep dup of @_content_for instance variable, with the :layout key deleted.
    • If caches_action is called with :layout => false, grab all_content_for, dump it with Marshal, and cache it separately (using a key prepended with "content_for/" instead of "views/").
    • If fragment_for is called, we have to determine what content_for was added in the block that is being cached. To do this, we grab all_content_for before we enter the block, then we grab all_content_for again after the block and diff the two hashes. For any keys that appear in the last hash and the diff_hash, we sub out whatever was in the earlier value with empty string, and we are left with exactly what was added to @_content_for in the block. We Marshal that and cache it separately.
    • Whenever read_fragment is called, we attempt to look up the cached content_for and "rehydrate" it. Ie, for each key in the Marshalled hash we pull out of the cache, we call content_for(key, value) in order to push that stuff back into the @_content_for variable.

    I only ran the ActionPack tests, since that's all I touched and I didn't want to waste my time getting the pg and system_timer gems installed & working properly on my dev box.

    March 23rd, 2010 @ 10:53 PM

  8. Steve
    Steve

    Also since I (foolishly) started my work on the 3.0b1 tag, it will probably be fairly easy to backport this enhancement to Rails 2.x. I'll work on that next since I don't see my company taking the time and effort to upgrade from 2.3.2 to 3 anytime soon.

    I had to start over from scratch with master as my starting point in order to get the patch working properly, so by now I know this area of the Rails code fairly well. ;)

    March 24th, 2010 @ 02:24 AM

  9. Yehuda Katz (wycats)
    Yehuda Katz (wycats)
    • State changed from new to open
    • Tag changed from caching, content_for, feature_request, patch to caching, content_for, feature_request, patch, rails3
    • Assigned user set to Yehuda Katz (wycats)
    • Milestone cleared.

    March 27th, 2010 @ 07:52 AM

  10. Steve
    Steve

    OK I've backported this to 2.3.2. Should I post that patch here or keep it off to avoid confusion? Does anyone other than me want it for 2.3.2?

    March 29th, 2010 @ 06:41 PM

  11. Yehuda Katz (wycats)
    Yehuda Katz (wycats)

    @steve I reviewed the patch and would like to chat with you. Are you on IRC or GTalk?

    March 29th, 2010 @ 07:25 PM

  12. Yehuda Katz (wycats)
    Yehuda Katz (wycats)
    • State changed from open to hold

    I'm putting this ticket on hold while I work with Steve on improving this patch. It's definitely important and I hope to get it in before 3.0 final.

    March 29th, 2010 @ 09:48 PM

  13. Steve
    Steve

    OK! I was foolish enough to tell Yehuda that I was "just about done" like four days ago. Then I promptly fell into a super amateurish rabbit-hole. Anyway I finally came out the other end with this new, improved, updated patch for your consideration!

    Caching content_for

    • Assumes that the cache_store can store arbitrary ruby objects (or at least hashes with defaults defined).
      • IF this is not true, it will be necessary for "string-only" cache_stores to be enhanced to transparently serialize and de-serialize objects when they are written and read.
    • For the "standard case" (ie, caching with content_for being unused), this will incur merely an extra if statement for action-caching and an extra block evaluation & new Hash being created and then destroyed in the case of fragment caching.
    • Fragment caching is much cleaner than my previous patch
      • content_for keeps an additional Hash while it is evaluating the block to be cached, which is then cached in addition to the content itself. The pointer to the additional Hash is assigned to nil at the end of the block execution.
    • Cache-store issues with possible falloff of related keys has been mitigated, relative to my previous patch.
      • Yehuda identified a potential problem with the fact that content_for was being stored separately from the content itself. This could cause the problem where the content itself still exists, so the associated block or action won't be re-executed, but the associated cached content_for could have been pushed out of the cache-store.
      • This has been solved by storing the content and the content_for stuff together in a hash.

    Other thoughts

    • It would be nice if the tests weren't so implementation specific, but rather tested the essence of the functionality. I tried to make them less brittle but failed.
    • To that end, re-writing all the tests in rspec would be (IMHO) a brilliant improvement and pave the way for better tests in the future and more flexibility in implementation. I understand that this would be an enormous undertaking, but I figure that with like 10 people in the community working on different pieces, we could knock it out in a reasonable amount of time. Maybe we can coordinate something at RailsConf...

    April 6th, 2010 @ 08:24 AM

  14. Steve
    Steve

    I don't think this is a high priority for anyone, but just in case people are wondering about the status of this -- here's an update:

    I made my tests pass by working around a bug (controller.view_context is a new instance of the view_context every time you call it, so I memoized it in my tests). It seems to me that a simple fix for this would be:

    # actionpack/lib/abstract_controller/rendering.rb: line 84
    def view_context
      @template ||= view_context_class.new(lookup_context, view_assigns, self)
    end
    

    A little bonus would be that if legacy Rails apps used the @template instance variable, this might still work for them (assuming that view_context had been called prior to them trying to use it, of course). I'm guessing that there's some reason this wouldn't be ideal, but I'm not sure what that reason is...

    I did a spot check of the cache stores implemented, and I found that they appear to support arbitrary Ruby objects (by utilizing Marshal), so that should be OK.

    April 22nd, 2010 @ 06:50 PM

  15. James Hayton
    James Hayton

    I am just stopping by to say thanks for working on this and that I really hope this makes it into rails 3. It's fairly important imho.

    April 28th, 2010 @ 04:36 AM

  16. varchar
    varchar

    agreed, looking forward to this making it into rails

    April 29th, 2010 @ 12:58 AM

  17. Steve
    Steve

    OK I cleaned up the patch a bit (removed vestige of old approach, squashed my commits, and removed the "def view_context" stuff from the caching_test.rb). The removal of the "def view_context" stuff means that my patch won't work (and accordingly, 2 tests fail), as it depends on view_context being the same object before, during and after rendering.

    To address that, I have also attached a patch that memoizes the view_context in a @template variable (as shown in my last post). Of course, memoizing the view_context causes 2 other tests to fail. Go figure.

    April 30th, 2010 @ 02:05 AM

  18. James Hayton
    James Hayton
    • Importance changed from to High

    Any update on this? Is this not going to make it in Rails3?

    August 24th, 2010 @ 05:12 PM

  19. Steve
    Steve

    @James Hayton -- I honestly haven't tried to apply my patch to the latest Rails3 code base. I'll give it a shot and resolve conflicts/fix issues that come up, then try to reconnect with Yehuda about it...

    Yehuda mentioned it may not make it into Rails 3 till a point release or something.

    August 31st, 2010 @ 11:29 PM

  20. Zach
    Zach

    @Steve, have you tested this patch with the Rails3 release? Could you post the latest patch? I'd be happy to help out with this, but i'd rather not start from scratch. Thanks.

    September 19th, 2010 @ 05:13 AM

  21. Steve
    Steve

    @Zach: honestly, I haven't touched this stuff for months. My 9-5 job that pays the bills has been taking all my time & energy recently, and realistically, I don't see myself being able to make time for this till next week at the earliest.

    I'll try to get to it sooner, but it doesn't look good. FWIW, Yehuda said we can target 3.1 for this.

    September 22nd, 2010 @ 08:38 AM

  22. James Hayton
    James Hayton

    @Steve: Thanks for the updates. Would love for this to happen. Thanks for all the work. Totally understand how things go.

    September 23rd, 2010 @ 08:12 PM

  23. Jeremy Kemper
  24. Joost Schuur
    Joost Schuur

    Allow me to add my encouragement on getting this in too, @Steve. Just ran into this problem myself and will work around it less elegantly for now.

    October 29th, 2010 @ 11:31 AM

  25. Steve
    Steve

    Joost, James, Zach -- thanks. Your encouragement spurs me onward. :)

    October 30th, 2010 @ 07:13 AM

  26. Daniel Gaiottino
    Daniel Gaiottino

    What's the latest status for this? I've run into this problem making it impossible for me to cache certain items which I'd really like cached. The problem most of the time is that we use content_for to send javascript code to the bottom of the dom.

    Will this be included in 3.0.2? Is it testable/usable today?

    November 5th, 2010 @ 01:53 PM

  27. Ben Hutton
    Ben Hutton

    +1... also eagerly awaiting this!

    November 5th, 2010 @ 05:15 PM

  28. Rohit Arondekar
  29. Steve
    Steve

    Actually, yes!

    I've been working on it all this past week. In short, I think I'm going to separate the fragment caching and the action caching support for content_for into two patches. The reason for this is that the fragment part is pretty much done and working, and while I was able to get action caching working with content_for as well, 149 tests in action_pack broke afterwards(!).

    @Daniel -- it's not testable nor usable yet. I'll post a patch soon that can cleanly apply to current-ish HEAD.

    November 17th, 2010 @ 02:03 AM

  30. Steve
    Steve

    Hah! Just got them down to 10 failures. Patch may be forthcoming... :)

    November 17th, 2010 @ 02:56 AM

  31. Steve
    Steve

    [Action pack] tests are green! Feast your eyes on the patch that should apply cleanly to current HEAD.

    It's a bit ugly, but it works. I have an idea for refactoring to something a little less disgusting, but for now... this is what I got. Feel free to check it out and give me your suggestions for making it prettier.

    November 17th, 2010 @ 10:24 AM

  32. Daniel Gaiottino
    Daniel Gaiottino

    What does it mean when it says that this story is on hold?

    Since the patch passes all the tests shouldn't it included in the next release of rails? Still waiting eagerly for this.

    January 5th, 2011 @ 05:26 PM

  33. Steve
    Steve
    • Assigned user changed from Yehuda Katz (wycats) to José Valim

    @Daniel -- honestly I don't think this is patch is going to be accepted. Obviously, I'd love to see it accepted, as I've put numerous hours into getting it working, but I think it's a bit too big (touches too many files), and is probably a bit too complex for the rails team to feel comfortable accepting.

    That said, last I heard from Yehuda, José was supposed to review this patch, but I haven't heard anything about it since then (Nov 17th). So I guess I'll assign it to José and maybe he'll be able to help me get this ready for primetime (?).

    Cheers!

    -Steve

    January 6th, 2011 @ 07:11 AM

  34. Brad Langhorst
    Brad Langhorst

    Getting content_for to work with caches_action might get me to update to rails3 ...
    unless there's a backport somewhere.

    January 21st, 2011 @ 04:45 PM

  35. James Hayton
    James Hayton

    Can someone give an update on this? Would love to know if it will make it to 3.1? Really really useful feature imho.

    February 25th, 2011 @ 06:49 PM

  36. Steve
    Steve

    @James: I think that making action caching (when you're not caching the template) work with content_for might be a bit too heavy for the Rails team to accept. I'll pull out fragment caching and make it a separate patch since it's much smaller and more lightweight IMO. After that, if no rails core team member will review it and accept it or even reject it... I think that will be the end of this (as far as I'm concerned).

    February 25th, 2011 @ 07:03 PM

  37. James Hayton
    James Hayton

    @Steve: I just wish they would at least review it and give an answer one way or the other. I can't believe that there aren't more people clamoring for this. I use action caching extensively with layout => false and this would really help me dry up my views.

    February 25th, 2011 @ 08:34 PM

  38. StackNG
    StackNG

    For those who need this function now:
    I just make a monkey patch for rails 3 based on Steve's great work, and leave the abstract_controller untouched.
    The patch is here: https://gist.github.com/891895, all you have to do is to put it in your config/initializers directory.

    I'm very curious why abstract_controller.view_context returns a new instance instead of a memorized one like @template, too.
    Could anyone point this out?

    March 29th, 2011 @ 07:50 AM

  39. opsb
    opsb

    Thank you so much for this. We really wanted to start using action caching with memcache, it was a no go without this patch.

    May 15th, 2011 @ 01:50 PM