Search code examples
ruby-on-railssidekiq

How to call controller action from _Sidekiq Worker in Rails 5.2.4?


My RoR application triggers data treatment scripts (SAS technology) from Linux operating system. An execute method in the Scheduler::ProductionExecutionsController drives the interactions with the scripts.

Scheduler::ProductionExecutionsController

  # POST /production_executions/1/execute
  def execute
    @production_execution = ProductionExecution.find(params[:id])
    @production_execution.update_attributes(started_at: Time.now, 
                                            status_id: statuses.find { |x| x["code"] == "RUNNING" }.id)

--- Scripts interaction ---

  @production_execution.update_attributes(ended_at: Time.now,
                                          status_id: statuses.find { |x| x["code"] == "FINISHED" }.id,
                                          source_records_count: global_input_count,
                                          processed_count: global_output_count,
                                          error_message: nil
                                          )

  respond_to do |format|
    format.html { redirect_back fallback_location: @production_execution, notice: @msg }
    format.js
  end

This method is called using this syntax: link_to "Go!", execute_scheduler_production_execution_path(execution). The route is defined and it works as expected.

As some scripts can take over a minute to execute, I need to execute the scripts in dedicated jobs, and sometimes schedule them. So I installed Sidekiq and defined a Scheduler::ScriptWorker :

class Scheduler::ScriptWorker
  include Sidekiq::Worker
  sidekiq_options queue: :default, tags: ['script']

  def perform(execution_id)
    puts "Sidekiq job start"
    puts execution_id
    redirect_to execute_scheduler_production_execution_path(execution_id) and return
  end

end

The executions are queued with Scheduler::ScriptWorker.perform_async(@execution.id)

Sidekiq operates correctly, but each time the worker is invoked, it raise the following error:

WARN: NoMethodError: undefined method `execute_scheduler_production_execution_path' for #<Scheduler::ScriptWorker:0x000000000b15ded8>

Is it the right way to do this, and how can I solve this issue? Thanks for you help!


Solution

  • As stated by Sean Huber, converting a front-end job into a backend job requires a touch of architecture. Through refactoring, the experimental execute method was moved to a helper and renamed execute_ssh. A new run_once method in the controller fires the helper's execute_ssh method and reloads the page.

    script_worker.rb

    class Scheduler::ScriptWorker
      include Sidekiq::Worker
      include SchedulerExecutionHelper
      include ParametersHelper
      sidekiq_options queue: :default, tags: ['script'], retry: false
    
      def perform(execution_id)
        puts "Sidekiq job start"
        puts execution_id
        execute_ssh execution_id
      end
    end
    

    scheduler_execution_helper.rb

      def execute_ssh(execution_id)
        puts "--- Switched to helper"
        @production_execution = ProductionExecution.find(execution_id)
        puts @production_execution.id
    
        @production_execution.update_attributes(started_at: Time.now, status_id: statuses.find { |x| x["code"] == "RUNNING" }.id)
    
    --- Scripts interaction ---
    
      @production_execution.update_attributes(ended_at: Time.now,
                                              status_id: statuses.find { |x| x["code"] == "FINISHED" }.id,
                                              source_records_count: global_input_count,
                                              processed_count: global_output_count,
                                              error_message: nil
                                              )
    
      end
    

    production_schedules_controller

      def run_once
        @job = @production_schedule.parent
        @execution = @job.production_executions.build(playground_id: @job.playground_id,
                                                      production_job_id: @job.id,
                                                      environment_id: @production_schedule.environment_id,
                                                      owner_id: current_user.id,
                                                      status_id: options_for('Statuses', 'Scheduler').find { |x| x["code"] == "READY" }.id || 0)
        if @execution.save
          @job.production_events.where(production_execution_id: nil).each do |event|
            execution_event = event.dup
            execution_event.production_execution_id = @execution.id
            execution_event.return_value = 0
            execution_event.status_id = statuses.find { |x| x["code"] == "READY" }.id
            execution_event.save
          end
          Scheduler::ScriptWorker.perform_async(@execution.id)
          redirect_to scheduler_production_job_path(@job)
        else
        end
      end
    

    This way, the controller remains thin, the worker can be easily reused, and the logic is in the helper module.