Sidekiqの安全な再起動方法

sidekiqとは、Rubyで非同期処理を行うためのライブラリです。
sidekiqを安全に再起動する構成の一例をここで紹介します。

## 課題
sidekiqで実行しているワーカーの更新には、sidekiqの再起動が必要になります。
capistrano等でデプロイを自動化している場合、sidekiqの再起動も同時に行いたいです。
しかし、それには問題があります。
sidekiqで実行するようなワーカーは、数十分、数時間かかるものもあるかもしれません。そのようなジョブが実行中にデプロイされることで再起動がかかり、また一から実行する、なんてことになってしまいます。
ですので、ワーカーが終了したかをチェックし、終了を確認してから再起動する必要があります。

[capistrano-sidekiq](https://github.com/seuros/capistrano-sidekiq)では、上記の課題を解決できませんでした。
なので独自で再起動の仕組みを実装します。

## 環境
ubuntu 18.04.2 LTS
rails 5.2.1
sidekiq 5.2.2 (ActiveJobと連携)
supervisord 3.3.1
ruby 2.5.1 (rbenvでインストール)
capistrano 3.11.0


## 目標
capistranoでデプロイしたときに、ワーカーが全て終了しているsidekiqプロセスはすぐに再起動、ワーカーが実行中のsidekiqプロセスはワーカーが全て終了したら再起動するようにする。
railsサーバは複数ホストの場合も想定。

## supervisor
sidekiqのプロセスをsupervisorで管理します。

設定ファイルの例です。

```conf:/etc/supervisor/conf.d/sidekiq.conf
# 通常のワーカー
[program:normal]
directory=(railsのrootディレクトリ)
environment=RAILS_ENV=production
command=/home/ubuntu/.rbenv/shims/bundle exec sidekiq --config config/sidekiq/process/normal.yml
process_name=normal
numprocs=1
autostart=true
user=ubuntu
redirect_stderr=true
stdout_logfile=/var/log/supervisor/sidekiq_normal.log

# sidekiqを再起動するためのワーカー
[program:restart]
directory=(railsのrootディレクトリ)
environment=RAILS_ENV=production
command=/home/ubuntu/.rbenv/shims/bundle exec sidekiq --config config/sidekiq/process/restart.yml
process_name=restart
numprocs=1
autostart=true
user=ubuntu
redirect_stderr=true
stdout_logfile=/var/log/supervisor/sidekiq_restart.log
```

sidekiqを再起動するためのワーカーを用意するのがポイントです。

## sidekiqの設定
railsのsidekiq設定の例です。

```yaml:config/sidekiq/process/normal.yml
:tag: normal
:verbose: true
:concurrency: 5
:queues:
 - user
 - purchase
 - default
```

```yaml:config/sidekiq/process/restart.yml
:tag: restart
:verbose: true
:concurrency: 1
:queues:
 - <%= `hostname`.strip %>
:max_retries: 0
```

queuesにhostnameを設定するのがポイントです。
複数ホストの場合は、ワーカーがどのhostで実行されるかは、sidekiqが勝手に決めてしまいます。
デプロイしたホストで必ずsidekiq再起動ワーカーを実行してほしいので(supervisorで再起動するため)、このようなテクニックを使っています。
この方法は[公式](https://github.com/mperham/sidekiq/wiki/FAQ#how-do-i-ensure-a-job-processes-on-a-given-machine)で紹介されています。


## sidekiq再起動ワーカー
ワーカーを再起動させるワーカーを用意します。

```ruby
class RestartSidekiqProcessWorker < ActiveJob::Base
	def perform
	  # 新規キューの実行を受付けないようにする
	  target_processes.each(&:quiet!)
	  sleep 10

	  target_ids = target_processes.map {|ps| ps['identity']}
	  # 実行中ワーカーが完了するまで監視して安全に終了させる
	  while true
	    # 全てのワーカーが終了かつすでに再起動済みでないプロセスは再起動する
	    target_processes.select{|ps| ps['busy'] == 0 && target_ids.include?(ps['identity'])}.each do |ps|
	      restart(ps)
	      target_ids.delete(ps['identity'])
	    end
	    break if target_ids.count == 0
	    sleep 10
	  end
	end


	private

	def target_processes
	  # 自分自身のプロセスは無視
	  target = Sidekiq::ProcessSet.new.to_a.delete_if{|ps| ps['tag'] == 'restart'}
	  # 自分のhost以外は無視
	  hostname = `hostname`.strip
	  target.delete_if{|ps| ps['hostname'] != hostname}
	end

	def restart(ps)
	  # supervisorで再起動
	  ps_name = ps['tag']
	  `sudo supervisorctl restart sidekiq:#{ps_name}`
	end
end

```

ワーカーを実行するスクリプトを用意します。
queueにhostnameを指定することで、必ずこのスクリプトを実行したホストでワーカーが実行されます。

```ruby:script/sidekiq/restart.rb
RestartSidekiqProcessWorker.set(queue: `hostname`.strip).perform_later
```

## capistranoの設定
taskを用意し、deploy後に上記ワーカーを実行するように設定します。

```ruby:lib/capistrano/sidekiq.rake
namespace :sidekiq do
  task :restart do
    on roles(:app) do
      with rails_env: fetch(:rails_env) do
        within current_path do
          execute :bundle, :exec, :rails, :runner, "script/sidekiq/restart.rb"
        end
      end
    end
  end
  before 'deploy:published', 'sidekiq:restart'
end
```

以上の構成で、capistranoでデプロイした時に、自動で安全に再起動されます。
なお、sidekiq再起動ワーカーのプロセスは再起動されません。このワーカーに変更があった場合は、手動で再起動する必要があります。

ワーカーを再起動するワーカーを用意する、というややこしい構成になってしまいました。
もっとシンプルな方法があればいいのですが…。

## 参考
https://github.com/mperham/sidekiq/wiki/FAQ#how-do-i-ensure-a-job-processes-on-a-given-machine
http://n8.hatenablog.com/entry/2015/05/01/100424