2020-03-11 20:09:34 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-03-20 17:09:17 -04:00
|
|
|
require 'shellwords'
|
|
|
|
|
2020-03-11 20:09:34 -04:00
|
|
|
module Gitlab
|
|
|
|
module SidekiqCluster
|
|
|
|
# The signals that should terminate both the master and workers.
|
|
|
|
TERMINATE_SIGNALS = %i(INT TERM).freeze
|
|
|
|
|
|
|
|
# The signals that should simply be forwarded to the workers.
|
|
|
|
FORWARD_SIGNALS = %i(TTIN USR1 USR2 HUP).freeze
|
|
|
|
|
|
|
|
# Traps the given signals and yields the block whenever these signals are
|
|
|
|
# received.
|
|
|
|
#
|
|
|
|
# The block is passed the name of the signal.
|
|
|
|
#
|
|
|
|
# Example:
|
|
|
|
#
|
|
|
|
# trap_signals(%i(HUP TERM)) do |signal|
|
|
|
|
# ...
|
|
|
|
# end
|
|
|
|
def self.trap_signals(signals)
|
|
|
|
signals.each do |signal|
|
|
|
|
trap(signal) do
|
|
|
|
yield signal
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.trap_terminate(&block)
|
|
|
|
trap_signals(TERMINATE_SIGNALS, &block)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.trap_forward(&block)
|
|
|
|
trap_signals(FORWARD_SIGNALS, &block)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.signal(pid, signal)
|
|
|
|
Process.kill(signal, pid)
|
|
|
|
true
|
|
|
|
rescue Errno::ESRCH
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.signal_processes(pids, signal)
|
|
|
|
pids.each { |pid| signal(pid, signal) }
|
|
|
|
end
|
|
|
|
|
|
|
|
# Starts Sidekiq workers for the pairs of processes.
|
|
|
|
#
|
|
|
|
# Example:
|
|
|
|
#
|
|
|
|
# start([ ['foo'], ['bar', 'baz'] ], :production)
|
|
|
|
#
|
|
|
|
# This would start two Sidekiq processes: one processing "foo", and one
|
|
|
|
# processing "bar" and "baz". Each one is placed in its own process group.
|
|
|
|
#
|
|
|
|
# queues - An Array containing Arrays. Each sub Array should specify the
|
|
|
|
# queues to use for a single process.
|
|
|
|
#
|
|
|
|
# directory - The directory of the Rails application.
|
|
|
|
#
|
|
|
|
# Returns an Array containing the PIDs of the started processes.
|
2020-03-25 05:08:11 -04:00
|
|
|
def self.start(queues, env: :development, directory: Dir.pwd, max_concurrency: 50, min_concurrency: 0, timeout: CLI::DEFAULT_SOFT_TIMEOUT_SECONDS, dryrun: false)
|
2020-03-11 20:09:34 -04:00
|
|
|
queues.map.with_index do |pair, index|
|
2020-03-25 05:08:11 -04:00
|
|
|
start_sidekiq(pair, env: env,
|
|
|
|
directory: directory,
|
|
|
|
max_concurrency: max_concurrency,
|
|
|
|
min_concurrency: min_concurrency,
|
|
|
|
worker_id: index,
|
|
|
|
timeout: timeout,
|
|
|
|
dryrun: dryrun)
|
2020-03-11 20:09:34 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Starts a Sidekiq process that processes _only_ the given queues.
|
|
|
|
#
|
|
|
|
# Returns the PID of the started process.
|
2020-03-25 05:08:11 -04:00
|
|
|
def self.start_sidekiq(queues, env:, directory:, max_concurrency:, min_concurrency:, worker_id:, timeout:, dryrun:)
|
2020-03-11 20:09:34 -04:00
|
|
|
counts = count_by_queue(queues)
|
|
|
|
|
|
|
|
cmd = %w[bundle exec sidekiq]
|
2020-03-20 17:09:17 -04:00
|
|
|
cmd << "-c#{self.concurrency(queues, min_concurrency, max_concurrency)}"
|
2020-03-11 20:09:34 -04:00
|
|
|
cmd << "-e#{env}"
|
2020-03-25 05:08:11 -04:00
|
|
|
cmd << "-t#{timeout}"
|
2020-03-20 17:09:17 -04:00
|
|
|
cmd << "-gqueues:#{proc_details(counts)}"
|
2020-03-11 20:09:34 -04:00
|
|
|
cmd << "-r#{directory}"
|
|
|
|
|
|
|
|
counts.each do |queue, count|
|
|
|
|
cmd << "-q#{queue},#{count}"
|
|
|
|
end
|
|
|
|
|
|
|
|
if dryrun
|
2020-03-20 17:09:17 -04:00
|
|
|
puts Shellwords.join(cmd) # rubocop:disable Rails/Output
|
2020-03-11 20:09:34 -04:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
pid = Process.spawn(
|
|
|
|
{ 'ENABLE_SIDEKIQ_CLUSTER' => '1',
|
|
|
|
'SIDEKIQ_WORKER_ID' => worker_id.to_s },
|
|
|
|
*cmd,
|
|
|
|
pgroup: true,
|
|
|
|
err: $stderr,
|
|
|
|
out: $stdout
|
|
|
|
)
|
|
|
|
|
|
|
|
wait_async(pid)
|
|
|
|
|
|
|
|
pid
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.count_by_queue(queues)
|
2020-12-16 01:10:11 -05:00
|
|
|
queues.tally
|
2020-03-11 20:09:34 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.proc_details(counts)
|
|
|
|
counts.map do |queue, count|
|
|
|
|
if count == 1
|
|
|
|
queue
|
|
|
|
else
|
|
|
|
"#{queue} (#{count})"
|
|
|
|
end
|
2020-03-20 17:09:17 -04:00
|
|
|
end.join(',')
|
2020-03-11 20:09:34 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.concurrency(queues, min_concurrency, max_concurrency)
|
|
|
|
concurrency_from_queues = queues.length + 1
|
2020-08-05 11:09:59 -04:00
|
|
|
max = max_concurrency > 0 ? max_concurrency : concurrency_from_queues
|
2020-03-11 20:09:34 -04:00
|
|
|
min = [min_concurrency, max].min
|
|
|
|
|
|
|
|
concurrency_from_queues.clamp(min, max)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Waits for the given process to complete using a separate thread.
|
|
|
|
def self.wait_async(pid)
|
|
|
|
Thread.new do
|
|
|
|
Process.wait(pid) rescue Errno::ECHILD
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Returns true if all the processes are alive.
|
|
|
|
def self.all_alive?(pids)
|
|
|
|
pids.each do |pid|
|
|
|
|
return false unless process_alive?(pid)
|
|
|
|
end
|
|
|
|
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.any_alive?(pids)
|
|
|
|
pids_alive(pids).any?
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.pids_alive(pids)
|
|
|
|
pids.select { |pid| process_alive?(pid) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.process_alive?(pid)
|
|
|
|
# Signal 0 tests whether the process exists and we have access to send signals
|
|
|
|
# but is otherwise a noop (doesn't actually send a signal to the process)
|
|
|
|
signal(pid, 0)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.write_pid(path)
|
|
|
|
File.open(path, 'w') do |handle|
|
|
|
|
handle.write(Process.pid.to_s)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|