Tag: Sidekiq

Adding reentrancy and a on failure fallback for your Sidekiq workers

Few months ago I've created a post about reentrancy: Ruby (Rails, Sinatra) background processing – Reentrancy for your workers is a must be!.

In this post, I will present a nice way to implement such feature for your Sidekiq workers.

Simple reentrancy

Normally a Sidekiq worker looks similar to this one:

class ExampleWorker
  include Sidekiq::Worker

  def perform(*args)
    # Background logic here
  end
end

and if something goes wrong, you should see it in your Sidekiq log (or a bugtracker like Errbit). However there is no reentrancy there. You could catch exceptions and handle reentrancy with this code:

class ExampleWorker
  include Sidekiq::Worker

  def perform(*args)
    # Background logic here
  rescue => exception
    # Do something on failure before reraising
    raise exception
  end
end

but it is not too elegant and if you have multiple Sidekiq workers, than probably you will end-up with a lot of code duplication.

Making your reentrancy code more fancy

Instead of handling reentrancy in every single worker, you could just create a base worker class, that would provide such functionality for all the workers that would inherit from the base one:

class BaseWorker
  include Sidekiq::Worker

  def perform(*args)
    # you need to implement the execute method
    # execute method should contain code you want to execute in the background
    execute(*args)
  rescue => exception
    after_failure(*args) if respond_to?(:after_failure)
    raise exception
  end
end

Now, instead of implementing a perform method in every worker, you need to name it (or rename) execute. Perform method will act as a wrapper that will try to execute your worker code and if it fails, will run the after_failure method (if it exists).

Note that the error will be reraised, but now we have a fallback to do for example some database status changes.

class KeywordsWorker < BaseWorker
  def execute(keyword_name)
    KeywordsService.new.remotely(keyword_name)
  end

  # Bring to an expire state if something goes wrong
  def after_failure(keyword_name)
    KeywordsService.new.expire(keyword_name)
  end
end

Of course we might have workers, that won't require reentrancy at all. Then we just skip the after_failure method and thanks to the respond_to? method, everything will work normally.

Running GitLab 7.1 using Puma instead of a Unicorn

Warning

Warning! Before you do this, please read why you should'nt: why did gitlab 6 switch back to unicorn?

So now, when

you-have-been-warned

let's get started...

Gemfile updates

Nothing special here. Just add:

gem 'puma'

and then:

# From /home/gitlab/gitlab
sudo bundle install --no-deployment
sudo -u gitlab -H bundle install --deployment --without development test postgres

Puma config

Create a puma.rb file in your gitlab config dir and copy/paste this:

app_path = File.expand_path(File.dirname(File.dirname(__FILE__)))

rails_env = ENV['RAILS_ENV'] ||  'production'
environment rails_env

threads 4, 32
workers 2

daemonize true
bind                 "unix://#{app_path}/tmp/puma/sock"
state_path           "#{app_path}/tmp/puma/state"
pidfile              "#{app_path}/tmp/puma/pid"
activate_control_app "unix://#{app_path}/tmp/puma/ctlsock"
stdout_redirect      "#{app_path}/log/puma_access.log", "#{app_path}/log/puma_error.log"

preload_app!

and

mkdir /home/git/gitlab/tmp/puma

At this point, you should be able to execute puma worker:

# You should execute this from a git user
cd /home/git/gitlab && exec bundle exec puma -C /home/git/gitlab/config/puma.rb

[4419] Puma starting in cluster mode...
[4419] * Version 2.9.0 (ruby 2.1.2-p95), codename: Team High Five
[4419] * Min threads: 4, max threads: 32
[4419] * Environment: production
[4419] * Process workers: 2
[4419] * Preloading application
[4419] * Listening on unix:///home/git/gitlab/tmp/puma/sock

Init Script

To manage my Pumas I use Jungle. You can read more about it here. From this point, I assume that you have figured out a way to autostart GitLab Puma process (if not, you'll have to start it each time manually - good luck!).

Unfortunately it is not all. Default GitLab init script (provided with GitLab sources) will try to run Unicorn, so we need to silent it (but we need to keep the Sidekiq part).

To do so, we have to change the /etc/init.d/gitlab script.

start_gitlab() method (line 165) - we have to comment the else case:

  # Then check if the service is running. If it is: don't start again.
  if [ "$web_status" = "0" ]; then
    echo "The Unicorn web server already running with pid $wpid, not restarting."
  # else
    # Remove old socket if it exists
    # rm -f "$socket_path"/gitlab.socket 2>/dev/null
    # Start the web server
    # RAILS_ENV=$RAILS_ENV bin/web start
  fi

wait_for_pids() method (line 78) - we have to remote the first condition from while loop. We no longer check for web_server_pid_path:

wait_for_pids(){
  # We are sleeping a bit here mostly because sidekiq is slow at writing it's pid
  i=0;
  while [ ! -f $sidekiq_pid_path ]; do
    sleep 0.1;
    i=$((i+1))
    if [ $((i%10)) = 0 ]; then
      echo -n "."
    elif [ $((i)) = 301 ]; then
      echo "Waited 30s for the processes to write their pids, something probably went wrong."
      exit 1;
    fi
  done
  echo
}

There are some other places that you could modify - so you would not get any warnings, but hey! Those are just warnings. What I did above is an absolute minimum for starting/stopping Sidekiq without any Unicorn errors.

Nginx server block

And an example Nginx server block (virtual host) config:

upstream git.server.name {
  server unix:///home/git/gitlab/tmp/puma/sock;
}

server {
  server_name git.server.name;
  client_max_body_size 32M;

  keepalive_timeout 5;

  root /home/git/gitlab/public;
  access_log /var/log/nginx/git.server.name.access.log;
  error_log  /var/log/nginx/git.server.name.error.log;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    if (-f $request_filename) {
      break;
    }

    if (!-f $request_filename) {
      proxy_pass http://git.server.name;
      break;
    }
  }
}

After that - you are ready to go! Good luck!

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑