Search code examples
ruby-on-railssidekiq

Execute a stored procedure asynchronously in a Rails application


I have an application in Rails and what I'm trying to do is execute a database stored procedure asynchronously when a user navigates from one particular page to another - the user must continue his navigation while the stored procedure runs.

I don't really need to have a callback when the procedure finishes, I just need to run it in background.

I'm trying to use the following code:

require 'eventmachine'
require 'em-http'
require 'fiber'

def async_fetch(url)
  f = Fiber.current
  http = EventMachine::HttpRequest.new(url).get :timeout => 10
  http.callback { f.resume(http) }
  http.errback { f.resume(http) }

  return Fiber.yield
end

EventMachine.run do
  Fiber.new{
    url = url_for :controller => 'common', :action => 'execute_stored_procedure'
    data = async_fetch(url)
    EventMachine.stop
  }.resume
end

The problem here is that when the stored procedure starts, the user must be redirected to another page, but the next page remains "pending" and renders only when the procedure finishes.

I tried to use thin (in my development environment) as my server with --threaded option with no success, and now I'm thinking about using Phusion Passenger Enterprise in multithreaded mode in the production server, but it's a commercial version and it does not have any trials, I'm afraid that it's not what I need.

Does anybody know a good solution to achieve this? To execute the stored procedure I have to make a request to the same webserver my application is running, so my webserver must accept multiple connections at a time (multithreaded), is it correct?

Some useful information:

  • Ruby 1.9.3p385
  • Rails 3.2.13
  • SQL Server 2012

Development:

  • Linux lucid32 2.6.32-45-generic #102-Ubuntu (vagrant machine)
  • thin webserver

Production:

  • Linux debian 2.6.32-5-xen-amd64
  • Apache / Phusion Passenger

I really appreciate any help.

UPDATE #1

I tried celluloid as recommended by Jesse. Here is my code:

require 'celluloid/autostart'
class PropertyWorker
  include Celluloid

  def engage(args)
    ActiveRecord::Base.execute_procedure("gaiainc.sp_ins_property_profiles", args[:id])
  end
end

...

def create
    @property = Property.new(params[:property])

    respond_to do |format|
      if @property.save
        PropertyWorker.new.async.engage({:id => @property.id})
        format.html { redirect_to new_enterprise_property_activation_url(@property.enterprise.id, @property.id) }
        format.json { render json: @property, status: :created, location: @property }
      else
        format.html { render action: "new" }
        format.json { render json: @property.errors, status: :unprocessable_entity }
      end
    end
end

When then action 'create' is called, the record is created, the stored procedure starts but the next page is not rendered, the request remains "pending" in the browser until the procedure finishes. As soon as the procedure finishes, the page is rendered.

I can't figure out what is going on. Was the procedure not supposed to run in background?


Solution

  • In something like this, I recommend either Sidekiq or Celluloid. What you want to do is spin off a thread and execute something, returning access to the calling process and having it continue on.

    Sidekiq would require a separate process to run (and Redis), Celluloid would not. Otherwise, they are similar.

    Sidekiq:

    class AsyncProc
      include Sidekiq::Worker
    
      def perform(args)
        CodeToExecuteStoredProcedure.engage! args
      end
    end
    

    You'd call it with:

    AsyncProc.perform_async {whatever: arguments, you: want}
    

    This would schedule a job in Redis and get executed when a spare Sidekiq worker has time

    Celluloid:

    require 'celluloid/autostart'
    class AsyncProc
      include Celluloid
    
      def engage(args)
        CodeToExecuteStoredProcedure.engage! args
      end
    end
    

    And to call it:

    AsyncProc.new.async.engage {whatever: arguments, you: want}
    

    This would execute asynchronously, pretty much right away.