1

In my Rails 3.2 project, I am using SuckerPunch to run a expensive background task when a model is created/updated.

Users can do different types of interactions on this model. Most of the times these updates are pretty well spaced out, however for some other actions like re-ordering, bulk-updates etc, those POST requests can come in very frequently, and that's when it overwhelms the server.

My question is, what would be the most elegant/smart strategy to start the background job when first update happens, but wait for say 10 seconds to make sure no more updates are coming in to that Model (Table, not a row) and then execute the job. So effectively throttling without queuing.

My sucker_punch worker looks something like this:

class StaticMapWorker
    include SuckerPunch::Job
    workers 10

    def perform(map,markers)
        #perform some expensive job
    end
end

It gets called from Marker and 'Map' model and sometimes from controllers (for update_all cases)like so:

after_save :generate_static_map_html

def generate_static_map_html
    StaticMapWorker.new.async.perform(self.map, self.map.markers)
end

So, a pretty standard setup for running background job. How do I make the job wait or not schedule until there are no updates for x seconds on my Model (or Table)

If it helps, Map has_many Markers so triggering the job with logic that when any marker associations of a map update would be alright too.

Shaunak
  • 17,377
  • 5
  • 53
  • 84

1 Answers1

1

What you are looking for is delayed jobs, implemented through ActiveJob's perform_later. According to the edge guides, that isn't implemented in sucker_punch.
ActiveJob::QueueAdapters comparison

Fret not, however, because you can implement it yourself pretty simply. When your job retrieves the job from the queue, first perform some math on the records modified_at timestamp, comparing it to 10 seconds ago. If the model has been modified, simply add the job to the queue and abort gracefully.

code!

As per the example 2/5 of the way down the page, explaining how to add a job within a worker Github sucker punch

class StaticMapWorker
  include SuckerPunch::Job
  workers 10

  def perform(map,markers)
    if Map.where(modified_at: 10.seconds.ago..Time.now).count > 0
      StaticMapWorker.new.async.perform(map,markers)
    else
      #perform some expensive job
    end
  end
end
Andy Gauge
  • 1,428
  • 2
  • 14
  • 25
  • Thanks Andy, great idea, but I think it is not going to work in this scenario I think, or may be I am missing something. Here's why. When user updates say a map first time, the job gets fired and re-queued because of the time check. In the mean while, user makes another update on the map. This will start another job chain! And both will wait for 10 seconds window to expire, but I still end up running 2 jobs, when I only want to run the last queued job. Am I making sense? – Shaunak Dec 30 '15 at 15:20
  • 1
    That's a good point. I think the best approach is a state on the model. This should be stored in the database, likely a new table. It links the state of each model. You can use a simple string to make it easy to read. `rails g migration CreateModelStateTable model:string state:string` Your perform method checks to see if the state of Map is 'processing' or 'queued' and check the `modified_at` time before beginning work. That way jobs will drop out if work is being performed, requeue if work is needed, and drop if work is done.[state_machine](https://github.com/pluginaweek/state_machine) – Andy Gauge Dec 30 '15 at 15:42
  • +1 That definitely sounds like it will work and would be the second piece to your answer. Let me give it a shot. thanks! – Shaunak Dec 30 '15 at 15:50