4

I am using Rails 4 w/ the impressionist and resque gem.

I am using impressionist to log unique session hits on my article show page. Due to performance issues and no need to display hits to users (it is for admins only), I would like to move logging impressions off into the background.

Normally I would log an impression using impressionist(@article, unique: [:session_hash]) but to move it off into the bg via resque I am now doing something like this...

articles_controller:

def show
  .
  .
  .
  Resque.enqueue(ImpressionLogger, @article.id)
end

app/workers/impression_logger.rb:

class ImpressionLogger 

  @queue = :impression_queue

  def self.perform(article_id)
    article = Article.find(article_id)
    impressionist(article, unique: [:session_hash])
  end

end

When I set it up like this, when resque tries to process the job, it is returning undefined method "impressionist" for ImpressionLogger:Class. What do you guys think the best way to go about this is? I am not sure how to include impressionist methods inside of my resque worker.

Kathan
  • 1,428
  • 2
  • 15
  • 31

3 Answers3

2

The issue

Your problem stems from the fact that it looks like Impressionist works on the controller level due to including a module with the impressionist method in an engine initializer on any instances of ActionController:

https://github.com/charlotte-ruby/impressionist/blob/master/lib/impressionist/engine.rb#L11

You're trying to call the impressionist method from a regular class being invoked in a Resque job, so it's not going to have that method defined.

Solution

It's kind of gross, but if you really want to use impressionist, we can delve into this... Looking at the actual implementation of the impressionist method found here, we see the following:

def impressionist(obj,message=nil,opts={})
  if should_count_impression?(opts)
    if obj.respond_to?("impressionable?")
      if unique_instance?(obj, opts[:unique])
        obj.impressions.create(associative_create_statement({:message => message}))
      end
    else
      # we could create an impression anyway. for classes, too. why not?
      raise "#{obj.class.to_s} is not impressionable!"
    end
  end
end

Assuming that you'd be calling something like this manually (as you want to from a resque job) the key are these three lines:

if unique_instance?(obj, opts[:unique])
  obj.impressions.create(associative_create_statement({:message => message}))
end

The if wrapper only seems to be important if you want to implement this functionality. Which it looks like you do. The call to associative_create_statement seems to be pulling parameters based off of the controller name as well as parameters passed from Rack such as the useragent string and ip address (here). So, you'll have to resolve these values prior to invoking the Resque job.

What I would suggest at this point is implementing a Resque class that takes in two parameters, an article_id and the impression parameters that you want. The resque class would then just directly create the impression on the impressionable object. Your Resque class would become:

class ImpressionLogger 
  @queue = :impression_queue

  def self.perform(article_id, impression_params = {})
    article = Article.find(article_id)
    article.impressions.create(impression_params)
  end
end

And your controller method would look something like this:

def show
  .
  .
  .
  Resque.enqueue(ImpressionLogger, @article.id, associative_create_statement({message: nil})) if unique_instance?(@article, [:session_hash])
end

Disclaimer

There's a fairly big disclaimer that comes with doing it this way though... the method associative_create_statement is marked protected and unique_instance? is marked private... so neither of these is part of the impressionist gem's public API, so this code might break between versions of the gem.

photoionized
  • 5,092
  • 20
  • 23
  • Also, to clarify, this isn't something that can be simply solved by including/extending ImpressionistController in the Resque class since it relies on having access to the underlying Rack parameters as well as things like the controller name for the inbound request. To make it work the same you'll have to resolve the parameters in the controller and then pass them off to Resque. – photoionized Apr 29 '16 at 19:45
  • Thank you very much for this. This indeed works and will hold over as a quick fix. Seems like I just need to take the time and roll out my own solution. @photoionized – Kathan Apr 30 '16 at 00:09
1

Is impressionist installed properly with bundler? If so Rails should be loading it into your environment. I would check whether you can access impressionist functionality elsewhere in your Rails code (i.e. without going through Resque) as the first step to debugging this.

Matt
  • 646
  • 4
  • 11
  • Thanks for the quick response. Yes, impressionist is installed properly. I've been using it without resque for months now and it logs impressions perfectly. The reason for resque is the impressions column in the DB is getting very big, and the sql query to check uniqueness is taking a few seconds. @Matt – Kathan Apr 12 '16 at 20:42
  • Does your rake task have the `:environment` flag in the Rakefile, like `task :task_name => :environment do ...`? That's what passes the Rails environment to the task. – Matt Apr 13 '16 at 21:40
0

How are you starting your resque workers? If you need your Rails environment loaded, try rake environment resque:work.

https://github.com/resque/resque/wiki/FAQ#how-do-i-ensure-my-rails-classesenvironment-is-loaded

Brian
  • 5,300
  • 2
  • 26
  • 32
  • I am loading the environment in a .rake file and starting the worker in the terminal via `rake resque:work QUEUE=*` ... seems like resque is working fine, but when the worker tries to process the job, it puts it in the 'failed jobs' area. @Brian – Kathan Apr 12 '16 at 21:54
  • Can you try changing your terminal command to `rake environment resque:work QUEUE=*"? That should load your Rails environment, including impressionist. – Brian Apr 14 '16 at 02:03
  • Sorry for the late response. This did not work... still getting the same error @Brian – Kathan Apr 19 '16 at 07:37