NoMethodError: undefined method `capture_position=' for nil:NilClass #524

Closed
heaven opened this Issue May 5, 2012 · 13 comments

Projects

None yet

5 participants

heaven commented May 5, 2012

Hi,

I am not actually sure about if this is haml issue or I am doing something wrong.
Basically I have context — an controller instance and a block (a part of view). What I am trying to do is to capture the block output, but in separate thread, so:

Thread.new do
  sleep(10)
  out = context.capture(block)
end

Raises:

ERROR NoMethodError: undefined method `capture_position=' for nil:NilClass
        #    /home/heaven/.rvm/gems/ruby-1.9.3-p0-falcon@crm/gems/haml-3.1.4/lib/haml/helpers.rb:363:in `capture_haml'

What I need to do is to release rails app faster, so when a get request comes in I need to respond some data fast and process another part in background.

If I am trying to run context.capture(block) in main thread it works as expected.

Thanks in advance,
Alex

Owner
norman commented Jun 15, 2012

Have you resolved this issue? Not sure this is a Haml issue at all, I'd have to see more of your code to know.

heaven commented Jun 19, 2012

Hi, I am trying to improve this approach http://blog.plataformatec.com.br/2009/09/how-to-avoid-dog-pile-effect-rails-app/ and render a part of view in separate thread. For example in view I have:

- cache "key_name", :expires_delta => 30.minutes do
  %h1 Some haml code here

Then I created cache store class (SmarterMemcacheStore) that inherits from MemCacheStore and also patched rails fragment_for and read_fragment methods to also pass that haml &block to cache.read method (originally it pass &block only to write mothod). As result, in my SmarterMemcacheStore class I override 2 methods: read and write and both receive a block (a part of my view).

In method read I am trying to generate new cache if existing one is expired (call the block). So the flow is:

  1. Return nil if cache does not exists (fragment_for method will call the block and create cache), it's not async but is okay because it will hapen only once after the app startup
  2. Return cache if it exists and not expired (default behavior)
  3. If cache exists and is expired we still should return it and run block to update the cache (this is what I trying to do — to run block to generate new cache inside read method)

Last part makes no sense if I will call the block in main thread because it will not be async and actually ActionView::Helpers::CacheHelper#fragment_for will do that itself if method read will return nil. So, I am trying to run the block in separate thread:

if Time.now > expires_at
  return data if exist?("lock_#{key}") or not block_given?

  orig_write("lock_#{key}", true, :expires_in => lock_expires_in)

  Thread.new do
    eval("output_buffer = ActionView::OutputBuffer.new", block.binding) #otherwise getting error from haml
    context = eval("self", block.binding)
    out = context.capture(&block)
    write(key, out, options)
  end
end

This works except one moment that captured html output also contains some server logs and could look like this:

<h1Started GET "/assets/fontawesome-webfont.woff" for 127.0.0.1 at [...] >Some haml code here</h1>

I am understanding the reason for that — 2 threads trying to access the same output, but can't find how to avoid this. And I am actually not understand why logs appears in output_buffer.

The reason for all this work is that some views could took about 1 minute to built, so default rails behavior is not good for me. The best I can do now without all this ugly hacks — simply create a lock in memcahed when cache is expired and return nil (or return outdated cache if lock for requested key already exists). In this case fragment_for method will run the block in main thread and user who requested that page with outdated cache will be forced to wait until new cache will be ready. All other users will get the outdated cache at this time (when lock exists) so only one user will suffer from slowdown at the moment. But the app I built is for company internal usage and there are not too many users thus very often same users suffering from slowdown.

Thanks in advance,
Alex

Owner
norman commented Jun 19, 2012

If you can make a minimal Rails app that reproduces your issue and put it on Github, I'll take a look at it.

heaven commented Jun 22, 2012

Hi, I will create it at this weekend, thank you!

heaven commented Jul 2, 2012

Hi, sorry for that big delay. Test app is here — git://github.com/heaven/Haml-test.git

Steps to go:

  1. Upload the mysql dump (stored in db directory)
  2. Run memcached daemon
  3. Run app in production mode
  4. Open index page, wait 5 seconds (until cache expire) and update the page (debugger should be launched after few seconds)
  5. See lib/active_support/cache/smarter_mem_cache_store.rb for details

There is also lib/cache_patch.rb. It patches fragment_for and read_fragment methods to pass block to read method in SmarterMemCacheStore (by default block is passed only to write method).

Member

I’ve been looking at this, and it looks like using init_haml_helpers will get it working:

context = eval("self", block.binding)
context.init_haml_helpers
out = context.capture(&block)

I don’t know the Rails integration enough to suggest this as a fix though — is sharing this object across threads safe? (@norman?)

Owner
norman commented Jul 5, 2012

@mattwildig I think it depends on what you do with the variables you gain access to by passing the binding over to the other thread. If you have some kind of shared state and you make changes, then of course it's going to be a problem. However if you're using Haml for what it's intended for and don't have anything other than presentation logic in your templates, then I think it should be fine. @heaven why don't you give @mattwildig's solution a try and let us know if it works for you?

heaven commented Jul 5, 2012

Hi, I wasn't at my work machine so had no chance to check that, will do right now, thank you.

heaven commented Jul 5, 2012

Just tested this solution, it helped with capture_position error, but doesn't help with server logs appear in cache. This is an example: http://oi50.tinypic.com/ipqzk4.jpg

I think the problem is in that 2 threads write to same output. Everything fine if I will not try to refresh the page at the moment when new cache is being generated.

heaven commented Jul 5, 2012

@norman @mattwildig Also from time to time it raises this error:

NoMethodError: undefined method `split' for nil:NilClass
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers.rb:349:in `block in capture_haml'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers.rb:569:in `with_haml_buffer'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers.rb:341:in `capture_haml'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers/xss_mods.rb:61:in `capture_haml_with_haml_xss'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers/action_view_mods.rb:93:in `capture_with_haml'
/releases/20120705160129/lib/active_support/cache/smarter_mem_cache_store.rb:21:in `block in read'

Line 21 in smarter_mem_cache_store — value = context.capture(&block)

heaven commented Jul 5, 2012

And this is also happen from time to time:

NoMethodError: undefined method `capture_position=' for nil:NilClass
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers.rb:363:in `ensure in capture_haml'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers.rb:363:in `capture_haml'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers/xss_mods.rb:61:in `capture_haml_with_haml_xss'
/shared/bundle/ruby/1.9.1/gems/haml-3.1.4/lib/haml/helpers/action_view_mods.rb:93:in `capture_with_haml'
/releases/20120705160129/lib/active_support/cache/smarter_mem_cache_store.rb:21:in `block in read'
tbraun89 commented Nov 8, 2013

Using init_haml_helpers before using the haml helper commands solved this issue for me.

Member
k0kubun commented Mar 28, 2017

I think #524 (comment) is enough. Closing.

@k0kubun k0kubun closed this Mar 28, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment