A bit of context before going to the solution...

I was trying to call a bulk insertion of some objects in the database in a Rails app hosted in Heroku, taking data from an user uploaded CSV file and processing it via find_or_create_by for each row, updating all the attributes when necessary and finally persisting the data in the database.

As the data rows grew in number (over 5k entries usually) the timeout limit of 30 seconds imposed by Heroku wasn't enough. So I had to setup the delayed_job gem with the HireFire gem for a cheaper experience in Heroku for the bulk process.

So I did it this way.

# controllers/the_model_controller.rb
TheModel.delay.import_from_csv params[:file]  # The offending line  
redirect_to root_url, notice: 'Data inserted successfully'

# models/the_model.rb
class TheModel  
  def self.import_from_csv(file)
    CSV.foreach(file_path, headers: true) do |row|
      temp_stuff = row.to_hash
      the_model = TheModel.find_or_create_by(:attribute1 => temp_stuff['attribute1'])
      # ... fill with data
      the_model.save!
    end
  end
end  

With the delayed_job ruby gem I was always getting undefined_method errors.

Routing Error  
undefined method `import_from_csv' for class `TheModel'  

Of course, it is right. You need to have an already existing (and persisted) object for delayed_job to work this way.

The workaround I found is pretty simple actually, though it is not very well documented IMO. You just have to create a singleton version of your class and mark the asynchronous methods with handle_asynchronously :your_async_method.

Above example, fixed:

# controllers/the_model_controller.rb
TheModel.import_from_csv params[:file]  # Without 'delay' now  
redirect_to root_url, notice: 'Data inserted successfully'

# models/the_model.rb
class TheModel  
  # Those are two < symbols (the blog is screwing them up)
  class << self
    def from_csv(file_path)
      CSV.foreach(file_path, headers: true) do |row|      
        temp_stuff = row.to_hash
        the_model = TheModel.find_or_create_by(:attribute1 => temp_stuff['attribute1'])
        # ... fill with data
        the_model.save!
      end
    end

    handle_asynchronously :from_csv
  end

  # My importer as a class method
  def self.import_from_csv(file)
    TheModel.from_csv file.path
  end
end  

And now it works nicely. Also, only one insertion to the Delayed::Job queue database.

Tags

rails , ruby , delayed_job , heroku

About the author
comments powered by Disqus