sidekiq-cluster 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/sidekiq-cluster +1 -1
- data/lib/sidekiq/cluster.rb +4 -1
- data/lib/sidekiq/cluster/cli.rb +130 -210
- data/lib/sidekiq/cluster/config.rb +53 -0
- data/lib/sidekiq/cluster/memory.rb +25 -0
- data/lib/sidekiq/cluster/memory/individual.rb +23 -0
- data/lib/sidekiq/cluster/memory/total.rb +20 -0
- data/lib/sidekiq/cluster/monitors/base.rb +42 -0
- data/lib/sidekiq/cluster/monitors/dead_children.rb +21 -0
- data/lib/sidekiq/cluster/monitors/oom.rb +22 -0
- data/lib/sidekiq/cluster/stdlib_ext.rb +5 -0
- data/lib/sidekiq/cluster/version.rb +30 -1
- data/lib/sidekiq/cluster/worker.rb +114 -0
- data/lib/sidekiq/cluster/worker_pool.rb +113 -0
- data/sidekiq-cluster.gemspec +2 -4
- metadata +46 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cee3d1d2e77f76ea89ce26c50bb43346cf55ecd
|
4
|
+
data.tar.gz: 36cce663074cfda880ebf60e5de3e450786d8c4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58a069bb53080b168197b9ce9f1e4cf574fec259fb3f6fbd25fbb383b08d3844ebaab9566c5265d2aa91d5fa3e5af6c680c134247e57a498417378416c4b82f0
|
7
|
+
data.tar.gz: 8d041f63d583fb3d3a5108944f7ef1d65978d71e1ee3ecca45968737e032e17b81350e5ec7f24768cfb0d8593ab96bf79ad429aa5ef136e582861ce3654d8802
|
data/exe/sidekiq-cluster
CHANGED
data/lib/sidekiq/cluster.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative 'cluster/version'
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
module Cluster
|
@@ -6,4 +6,7 @@ module Sidekiq
|
|
6
6
|
end
|
7
7
|
|
8
8
|
require_relative 'cluster/cli'
|
9
|
+
require_relative 'cluster/config'
|
10
|
+
require_relative 'cluster/memory'
|
11
|
+
require_relative 'cluster/stdlib_ext'
|
9
12
|
|
data/lib/sidekiq/cluster/cli.rb
CHANGED
@@ -3,273 +3,193 @@ require 'etc'
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'logger'
|
5
5
|
require 'colored2'
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
require_relative 'config'
|
9
|
+
require_relative 'worker_pool'
|
10
|
+
require_relative 'version'
|
6
11
|
|
7
12
|
module Sidekiq
|
8
13
|
module Cluster
|
9
|
-
MAX_RAM_PCT = 80
|
10
|
-
|
11
|
-
ProcessDescriptor = Struct.new(:pid, :index, :pidfile)
|
12
|
-
|
13
14
|
class CLI
|
14
|
-
attr_accessor :
|
15
|
-
|
16
|
-
:argv,
|
17
|
-
:sidekiq_argv,
|
18
|
-
:process_count,
|
19
|
-
:pids,
|
20
|
-
:processes,
|
21
|
-
:watch_children,
|
22
|
-
:memory_percentage_limit,
|
23
|
-
:log_device,
|
24
|
-
:logger
|
25
|
-
|
26
|
-
def initialize(argv = ARGV.dup)
|
27
|
-
self.argv = argv
|
28
|
-
self.name = 'default'
|
29
|
-
self.pid_prefix = '/tmp/sidekiq.pid'
|
30
|
-
self.sidekiq_argv = []
|
31
|
-
self.log_device = STDOUT
|
32
|
-
self.memory_percentage_limit = MAX_RAM_PCT
|
33
|
-
self.process_count = Etc.nprocessors - 1
|
34
|
-
self.processes = {}
|
35
|
-
self.pids = []
|
36
|
-
end
|
15
|
+
attr_accessor :argv, :config, :logger, :log_device, :worker_pool, :master_pid
|
16
|
+
attr_reader :stdout, :stdin, :stderr, :kernel
|
37
17
|
|
38
|
-
|
39
|
-
|
18
|
+
class << self
|
19
|
+
attr_accessor :instance
|
40
20
|
end
|
41
21
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
22
|
+
def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
|
23
|
+
@argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
|
24
|
+
args = argv.dup
|
25
|
+
args = %w(--help) if args.nil? || args.empty?
|
26
|
+
self.master_pid = Process.pid
|
27
|
+
self.config = Config.config
|
28
|
+
Config.split_argv(args.dup)
|
29
|
+
self.class.instance = self
|
46
30
|
end
|
47
31
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
self.pids = processes.keys
|
54
|
-
self.watch_children = true
|
32
|
+
def execute!
|
33
|
+
parse_cli_arguments
|
34
|
+
print_header
|
35
|
+
pp config.to_h if config.debug
|
55
36
|
|
56
|
-
|
57
|
-
setup_signal_traps
|
58
|
-
|
59
|
-
Process.waitall
|
60
|
-
info 'shutting down...'
|
61
|
-
end
|
37
|
+
::Process.setproctitle('sidekiq-cluster.' + config.name + ' ' + config.cluster_argv.join(' '))
|
62
38
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
def initialize_cli_arguments!
|
72
|
-
if argv.nil? || argv.empty?
|
73
|
-
argv << '-h'
|
74
|
-
else
|
75
|
-
split_argv! if argv.include?('--')
|
39
|
+
if config.work_dir
|
40
|
+
if Dir.exist?(config.work_dir)
|
41
|
+
Dir.chdir(config.work_dir)
|
42
|
+
else
|
43
|
+
raise "Can not find work dir #{config.work_dir}"
|
44
|
+
end
|
76
45
|
end
|
77
|
-
init_logger!
|
78
|
-
parse_args!
|
79
|
-
end
|
80
46
|
|
81
|
-
|
47
|
+
config.logfile = Dir.pwd + '/' + config.logfile if config.logfile && !config.logfile.start_with?('/')
|
48
|
+
config.pid_prefix = Dir.pwd + '/' + config.pid_prefix if config.pid_prefix && !config.pid_prefix.start_with?('/')
|
82
49
|
|
83
|
-
|
84
|
-
info "starting up sidekiq-cluster for #{name}"
|
85
|
-
info "NOTE: cluster max memory limit is #{memory_percentage_limit.round(2)}% of total"
|
86
|
-
info "NOTE: per sidekiq memory limit is #{per_process_memory_limit.round(2)}% of total"
|
87
|
-
end
|
50
|
+
init_logger
|
88
51
|
|
89
|
-
|
90
|
-
self.logger = ::Logger.new(log_device, level: ::Logger::INFO)
|
52
|
+
boot
|
91
53
|
end
|
92
54
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
55
|
+
def boot
|
56
|
+
@worker_pool = ::Sidekiq::Cluster::WorkerPool.new(self, config)
|
57
|
+
@worker_pool.spawn
|
96
58
|
end
|
97
59
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
handle_sig(sig)
|
102
|
-
end
|
60
|
+
def print(*args)
|
61
|
+
unless config.quiet
|
62
|
+
stdout.puts args.join(' ')
|
103
63
|
end
|
104
64
|
end
|
105
65
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
sleep
|
111
|
-
check_pids
|
112
|
-
break unless @watch_children
|
66
|
+
def stop!(code = 0)
|
67
|
+
if worker_pool
|
68
|
+
unless worker_pool.stopped?
|
69
|
+
worker_pool.stop!
|
70
|
+
sleep 5
|
113
71
|
end
|
114
|
-
info 'leaving the main loop..'
|
115
72
|
end
|
116
|
-
end
|
117
73
|
|
118
|
-
|
119
|
-
@logger.info(*args)
|
74
|
+
kernel.exit(code)
|
120
75
|
end
|
121
76
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
def handle_sig(sig)
|
128
|
-
print "received OS signal #{sig}"
|
129
|
-
# If we're shutting down, we don't need to re-spawn child processes that die
|
130
|
-
@watch_children = false if sig == 'INT' || sig == 'TERM'
|
131
|
-
@pids.each do |pid|
|
132
|
-
Process.kill(sig, pid)
|
77
|
+
def close_logger
|
78
|
+
self.logger.close if logger && logger.respond_to?(:close)
|
79
|
+
if Process.pid != master_pid
|
80
|
+
log_device.close if log_device && log_device.respond_to?(:close)
|
133
81
|
end
|
82
|
+
rescue
|
83
|
+
nil
|
134
84
|
end
|
135
85
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
process_argv << '--tag' << "sidekiq.#{name}.#{index + 1}"
|
143
|
-
info "starting up sidekiq instance #{index} with ARGV: 'bundle exec sidekiq #{process_argv.join(' ')}'"
|
144
|
-
begin
|
145
|
-
cli = Sidekiq::CLI.instance
|
146
|
-
cli.parse process_argv
|
147
|
-
cli.run
|
148
|
-
rescue => e
|
149
|
-
raise e if $DEBUG
|
150
|
-
error e.message
|
151
|
-
error e.backtrace.join("\n")
|
152
|
-
stop!(1)
|
153
|
-
end
|
86
|
+
def init_logger
|
87
|
+
if config.logfile
|
88
|
+
::FileUtils.mkdir_p(::File.dirname(config.logfile))
|
89
|
+
self.log_device = ::File.open(config.logfile, 'a', 0644)
|
90
|
+
else
|
91
|
+
self.log_device = stdout
|
154
92
|
end
|
155
|
-
end
|
156
93
|
|
157
|
-
|
158
|
-
"#{pid_prefix}.#{index + 1}"
|
159
|
-
end
|
94
|
+
log_device.sync = true
|
160
95
|
|
161
|
-
|
162
|
-
|
163
|
-
pid = fork_child(index)
|
164
|
-
ProcessDescriptor.new(pid, index, pid_file(index))
|
165
|
-
end
|
96
|
+
@logger = ::Logger.new(log_device, level: ::Logger::INFO)
|
97
|
+
info('opening log file: ' + (config.logfile ? config.logfile.to_s : 'STDOUT'))
|
166
98
|
end
|
167
99
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
pids.each do |pid|
|
177
|
-
memory_percent_used = `ps -o %mem= -p #{pid}`.to_f
|
178
|
-
info "sidekiq.#{name % '%15s'}[#{processes[pid].index}] —— pid=#{pid.to_s % '%6d'} —— memory pct=#{memory_percent_used.round(2)}%" if print_info
|
179
|
-
if memory_percent_used == 0.0 # child died
|
180
|
-
restart_dead_child(pid)
|
181
|
-
elsif memory_percent_used > per_process_memory_limit
|
182
|
-
info "pid #{pid} crossed memory threshold, used #{memory_percent_used.round(2)}% of RAM, exceeded #{per_process_memory_limit}% —> replacing..."
|
183
|
-
restart_oversized_child(pid)
|
184
|
-
elsif $DEBUG
|
185
|
-
info "#{pid}: #{memory_percent_used.round(2)}"
|
186
|
-
end
|
187
|
-
end
|
100
|
+
def print_header
|
101
|
+
print "Sidekiq Cluster v#{Sidekiq::Cluster::VERSION} — Starting up, Cluster name: [#{config.name}]"
|
102
|
+
print " • max memory limit is : #{config.max_memory_percent.round(2)}%"
|
103
|
+
print " • memory strategy : #{config.memory_strategy.to_s.capitalize}"
|
104
|
+
print " • number of workers is : #{config.process_count}"
|
105
|
+
print " • logfile : #{config.logfile}" if config.logfile
|
106
|
+
print " • pid file path : #{config.pid_prefix}" if config.pid_prefix
|
188
107
|
end
|
189
108
|
|
190
|
-
def
|
191
|
-
|
192
|
-
Process.kill('USR1', pid)
|
193
|
-
sleep 5
|
194
|
-
Process.kill('TERM', pid)
|
195
|
-
@pids << fork_child
|
109
|
+
def prefix
|
110
|
+
Process.pid == master_pid ? ' «master» '.bold.blue : ' «worker» '.bold.green
|
196
111
|
end
|
197
112
|
|
198
|
-
def
|
199
|
-
|
200
|
-
pid_file = pid_file(pd.index)
|
201
|
-
|
202
|
-
::File.unlink(pid_file) if ::File.exist?(pidfile)
|
203
|
-
pids.delete(old_pid)
|
204
|
-
processes.delete(old_pid)
|
205
|
-
|
206
|
-
pd.pid = new_pid
|
207
|
-
processes[new_pid] = pd
|
113
|
+
def info(*args)
|
114
|
+
logger.info(prefix + args.join(' '))
|
208
115
|
end
|
209
116
|
|
210
|
-
def
|
211
|
-
|
212
|
-
|
213
|
-
pd = processes[pid]
|
214
|
-
raise ArgumentError, "Unregistered pid found #{pid}, no existing descriptor found!" unless pd
|
215
|
-
new_pid = fork_child(pd.index)
|
216
|
-
replace_pid(pid, new_pid)
|
217
|
-
|
218
|
-
info "replaced lost pid #{pid} with #{new_pid}"
|
117
|
+
def error(*args, exception: nil)
|
118
|
+
logger.error(prefix + "exception: #{exception.message}") if exception
|
119
|
+
logger.error(prefix + args.join(' '))
|
219
120
|
end
|
220
121
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
opts.separator
|
227
|
-
opts.
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
opts.on('-n', '--name=NAME',
|
233
|
-
'the name of this cluster, used when ',
|
234
|
-
'when running multiple clusters', ' ') do |v|
|
235
|
-
self.name = v
|
236
|
-
end
|
237
|
-
opts.on('-P', '--pidfile=FILE',
|
238
|
-
'Pidfile prefix, ',
|
239
|
-
'eg "/var/www/shared/config/sidekiq.pid"', ' ') do |v|
|
240
|
-
self.pid_prefix = v
|
122
|
+
private
|
123
|
+
|
124
|
+
def parse_cli_arguments
|
125
|
+
banner = 'USAGE'.bold.yellow + "\n sidekiq-cluster [options] -- ".bold.magenta + "[sidekiq-options]".bold.cyan
|
126
|
+
parser = ::OptionParser.new(banner, 26, indent = ' ' * 3) do |opts|
|
127
|
+
opts.separator ::Sidekiq::Cluster::BANNER
|
128
|
+
opts.on('-n', '--name NAME',
|
129
|
+
'the name of this cluster, used when running ',
|
130
|
+
'multiple clusters.', ' ') do |v|
|
131
|
+
config.name = v
|
241
132
|
end
|
242
|
-
opts.on('-
|
243
|
-
'
|
244
|
-
|
245
|
-
|
133
|
+
opts.on('-N', '--num-processes NUM',
|
134
|
+
'Number of worker processes to use for this cluster,',
|
135
|
+
'defaults to the number of cores - 1', ' ') do |v|
|
136
|
+
config.process_count = v.to_i
|
246
137
|
end
|
247
|
-
|
248
|
-
|
138
|
+
|
139
|
+
opts.on('-M', '--max-memory PCT',
|
140
|
+
'Float, the maximum percent total RAM that this',
|
249
141
|
'cluster should not exceed. Defaults to ' +
|
250
142
|
Sidekiq::Cluster::MAX_RAM_PCT.to_s + '%.', ' ') do |v|
|
251
|
-
|
143
|
+
config.max_memory_percent = v.to_f
|
144
|
+
end
|
145
|
+
|
146
|
+
opts.on('-m', '--memory-mode MODE',
|
147
|
+
'Either "total" (default) or "individual".', ' ',
|
148
|
+
'• In the "total" mode the largest worker is restarted if ',
|
149
|
+
' the sum of all workers exceeds the memory limit. ',
|
150
|
+
'• In the "individual" mode, a worker is restarted if it ',
|
151
|
+
' exceeds MaxMemory / NumWorkers.', ' ') do |v|
|
152
|
+
if Config::MEMORY_STRATEGIES.include?(v.downcase.to_sym)
|
153
|
+
config.memory_strategy = v.downcase.to_sym
|
154
|
+
else
|
155
|
+
raise "Invalid strategy '#{v}'. Valid strategies are :#{Config::MEMORY_STRATEGIES.join(', ')}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
opts.on('-P', '--pid-prefix PATH',
|
160
|
+
'Pidfile prefix path used to generate pid files, ',
|
161
|
+
'eg "/var/tmp/cluster.pid"', ' ') do |v|
|
162
|
+
config.pid_prefix = v
|
252
163
|
end
|
253
|
-
|
254
|
-
|
255
|
-
'
|
256
|
-
|
164
|
+
|
165
|
+
opts.on('-L', '--logfile FILE',
|
166
|
+
'The logfile for sidekiq cluster itself', ' ') do |v|
|
167
|
+
config.logfile = v
|
168
|
+
end
|
169
|
+
|
170
|
+
opts.on('-w', '--work-dir DIR',
|
171
|
+
'Directory where to run', ' ') do |v|
|
172
|
+
config.work_dir = v
|
257
173
|
end
|
174
|
+
|
258
175
|
opts.on('-q', '--quiet',
|
259
|
-
'Do not
|
260
|
-
|
176
|
+
'Do not print anything to STDOUT') do |_v|
|
177
|
+
config.quiet = true
|
261
178
|
end
|
179
|
+
|
262
180
|
opts.on('-d', '--debug',
|
263
|
-
'Print debugging info before starting
|
264
|
-
|
181
|
+
'Print debugging info before starting workers') do |_v|
|
182
|
+
config.debug = true
|
265
183
|
end
|
184
|
+
|
266
185
|
opts.on('-h', '--help', 'this help') do |_v|
|
267
|
-
|
186
|
+
config.help = true
|
187
|
+
stdout.puts opts
|
268
188
|
stop!
|
269
189
|
end
|
270
190
|
end
|
271
|
-
|
272
|
-
|
191
|
+
|
192
|
+
parser.order!(argv.dup)
|
273
193
|
end
|
274
194
|
end
|
275
195
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'dry-configurable'
|
3
|
+
require_relative 'version'
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module Cluster
|
7
|
+
class Config
|
8
|
+
MEMORY_STRATEGIES = %i(total individual)
|
9
|
+
ARGV_SEPARATOR = '--'.freeze
|
10
|
+
|
11
|
+
extend ::Dry::Configurable
|
12
|
+
|
13
|
+
setting :spawn_block, ->(worker) {
|
14
|
+
require 'sidekiq'
|
15
|
+
require 'sidekiq/cli'
|
16
|
+
|
17
|
+
process_argv = worker.config.worker_argv.dup << '-P' << worker.pid_file
|
18
|
+
process_argv << '--tag' << "sidekiq.#{worker.config.name}.#{worker.index + 1}"
|
19
|
+
|
20
|
+
cli = Sidekiq::CLI.instance
|
21
|
+
cli.parse %w(bundle exec sidekiq) + process_argv
|
22
|
+
cli.run
|
23
|
+
}
|
24
|
+
|
25
|
+
setting :name, 'default'
|
26
|
+
setting :pid_prefix, '/var/tmp/sidekiq-cluster.pid'
|
27
|
+
setting :process_count, Etc.nprocessors - 1
|
28
|
+
setting :max_memory_percent, MAX_RAM_PCT
|
29
|
+
setting :memory_strategy, :total # also supported :individual
|
30
|
+
setting :logfile
|
31
|
+
setting :work_dir, Dir.pwd
|
32
|
+
setting :cluster_argv, []
|
33
|
+
setting :worker_argv, []
|
34
|
+
setting :debug, false
|
35
|
+
setting :quiet, false
|
36
|
+
setting :help, false
|
37
|
+
|
38
|
+
|
39
|
+
def self.split_argv(argv)
|
40
|
+
configure do |c|
|
41
|
+
if argv.index(ARGV_SEPARATOR)
|
42
|
+
c.worker_argv = argv[(argv.index(ARGV_SEPARATOR) + 1)..-1] || []
|
43
|
+
c.cluster_argv = argv[0..(argv.index(ARGV_SEPARATOR) - 1)] || []
|
44
|
+
else
|
45
|
+
c.worker_argv = []
|
46
|
+
c.cluster_argv = argv
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Cluster
|
3
|
+
module Memory
|
4
|
+
class << self
|
5
|
+
attr_accessor :strategies
|
6
|
+
|
7
|
+
def offenders(worker_pool)
|
8
|
+
name = worker_pool.config.memory_strategy.to_sym
|
9
|
+
strategies[name].new(worker_pool).offenders
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
self.strategies ||= {}
|
14
|
+
|
15
|
+
module MemoryStrategy
|
16
|
+
def self.included(base)
|
17
|
+
::Sidekiq::Cluster::Memory.strategies[base.name.gsub(/.*::/, '').downcase.to_sym] = base
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require_relative 'memory/individual'
|
25
|
+
require_relative 'memory/total'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Cluster
|
3
|
+
module Memory
|
4
|
+
class Individual
|
5
|
+
include MemoryStrategy
|
6
|
+
|
7
|
+
attr_accessor :config, :worker_pool
|
8
|
+
|
9
|
+
def initialize(worker_pool)
|
10
|
+
self.worker_pool = worker_pool
|
11
|
+
self.config = worker_pool.config
|
12
|
+
end
|
13
|
+
|
14
|
+
def offenders
|
15
|
+
worker_pool.find do |worker|
|
16
|
+
worker.memory_used_pct > config.max_memory_percent / worker_pool.size
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative '../stdlib_ext'
|
2
|
+
require_relative 'individual'
|
3
|
+
module Sidekiq
|
4
|
+
module Cluster
|
5
|
+
module Memory
|
6
|
+
class Total < Individual
|
7
|
+
include MemoryStrategy
|
8
|
+
|
9
|
+
def offenders
|
10
|
+
total_ram_pct = worker_pool.map(&:memory_used_pct).sum
|
11
|
+
worker_pool.cli.info("total RAM used by workers is #{'%.2f%%' % total_ram_pct}")
|
12
|
+
if total_ram_pct > config.max_memory_percent
|
13
|
+
worker_pool.sort_by(&:memory_used_pct).inverse[0..1]
|
14
|
+
end || []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Cluster
|
3
|
+
module Monitors
|
4
|
+
SLEEP_DELAY = 5
|
5
|
+
LOGGING_PERIOD = 30
|
6
|
+
|
7
|
+
class Base
|
8
|
+
attr_accessor :pool, :thread
|
9
|
+
|
10
|
+
def initialize(pool)
|
11
|
+
self.pool = pool
|
12
|
+
@last_logged_at = Time.now.to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
self.thread = Thread.new { self.monitor }
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def join
|
21
|
+
thread.join if thread
|
22
|
+
end
|
23
|
+
|
24
|
+
def monitor
|
25
|
+
raise 'Abstract method'
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_periodically(msg, &block)
|
29
|
+
t = Time.now.to_i
|
30
|
+
if t - @last_logged_at > LOGGING_PERIOD
|
31
|
+
pool.cli.info(msg) if msg
|
32
|
+
Array(block.call).each do |result|
|
33
|
+
pool.cli.info(result)
|
34
|
+
end if block
|
35
|
+
@last_logged_at = t
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Cluster
|
5
|
+
module Monitors
|
6
|
+
class DeadChildren < Base
|
7
|
+
def monitor
|
8
|
+
pool.info 'watching for workers that died...'
|
9
|
+
loop do
|
10
|
+
sleep SLEEP_DELAY - 1
|
11
|
+
pool.workers.each(&:check_worker)
|
12
|
+
break unless pool.operational?
|
13
|
+
"monitor for Dead Children is operational, last logged at #{@last_logged_at}"
|
14
|
+
end
|
15
|
+
pool.info 'leaving Dead Children Monitor'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'dead_children'
|
2
|
+
require_relative '../memory'
|
3
|
+
module Sidekiq
|
4
|
+
module Cluster
|
5
|
+
module Monitors
|
6
|
+
class OOM < Base
|
7
|
+
def monitor
|
8
|
+
pool.info 'watching for worker processes exceeding size threshold'
|
9
|
+
loop do
|
10
|
+
sleep SLEEP_DELAY + 1
|
11
|
+
::Sidekiq::Cluster::Memory.offenders(pool).each { |worker| worker.respawn! }
|
12
|
+
break unless pool.operational?
|
13
|
+
log_periodically "monitor for Out Of Memory is operational, last logged at #{@last_logged_at}" do
|
14
|
+
pool.workers.map(&:status)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
pool.info 'leaving Memory Monitor.'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,6 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colored2'
|
4
|
+
|
1
5
|
module Sidekiq
|
2
6
|
module Cluster
|
3
|
-
VERSION
|
7
|
+
VERSION ||= '0.1.2'
|
8
|
+
|
9
|
+
DESCRIPTION = <<-eof
|
10
|
+
This library provides CLI interface for starting multiple copies of Sidekiq in parallel,
|
11
|
+
typically to take advantage of multi-core systems. By default it starts N - 1 processes,
|
12
|
+
where N is the number of cores on the current system. Sidekiq Cluster is controlled with CLI
|
13
|
+
flags that appear before `--` (double dash), while any arguments that follow double dash are
|
14
|
+
passed to each Sidekiq process. The exception is the `-P pidfile`, which clustering script
|
15
|
+
passes to each sidekiq process individually.
|
16
|
+
eof
|
17
|
+
.gsub(/\n /, ' ').freeze
|
18
|
+
|
19
|
+
MAX_RAM_PCT ||= 80.freeze
|
20
|
+
|
21
|
+
# @formatter:off
|
22
|
+
BANNER ||= %Q(
|
23
|
+
#{'EXAMPLES'.bold.yellow}
|
24
|
+
|
25
|
+
$ cd rails_app
|
26
|
+
$ echo 'gem "sidekiq-cluster"' >> Gemfile
|
27
|
+
$ bundle install
|
28
|
+
#{'$ bundle exec sidekiq-cluster -N 2 '.bold.magenta + '--' + ' -c 10 -q default,12 -L log/sidekiq.log'.bold.cyan}
|
29
|
+
|
30
|
+
#{'OPTIONS'.bold.yellow}
|
31
|
+
).freeze
|
32
|
+
# @formatter:on
|
4
33
|
end
|
5
34
|
end
|
6
35
|
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'state_machines'
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module Cluster
|
6
|
+
class Worker
|
7
|
+
|
8
|
+
state_machine :state, initial: :idle do
|
9
|
+
event :start do
|
10
|
+
transition [:idle] => :starting
|
11
|
+
end
|
12
|
+
|
13
|
+
event :started do
|
14
|
+
transition [:starting] => :running
|
15
|
+
end
|
16
|
+
|
17
|
+
event :stop do
|
18
|
+
transition [:idle, :starting, :running] => :stopping
|
19
|
+
end
|
20
|
+
|
21
|
+
event :shutdown do
|
22
|
+
transition [:idle, :starting, :running, :stopping] => :stopped
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
extend Forwardable
|
27
|
+
def_delegators :@cli, :info, :error, :print, :stdout, :stdin, :stderr, :kernel, :master_pid
|
28
|
+
|
29
|
+
attr_accessor :pid, :index, :cli, :config, :state
|
30
|
+
|
31
|
+
def initialize(index, cli)
|
32
|
+
self.config = cli.config
|
33
|
+
self.index = index
|
34
|
+
self.cli = cli
|
35
|
+
self.state = :idle
|
36
|
+
end
|
37
|
+
|
38
|
+
def spawn
|
39
|
+
if master?
|
40
|
+
cli.info "booting worker #{'%2d' % index} with ARGV '#{config.worker_argv.join(' ')}'"
|
41
|
+
self.pid = ::Process.fork do
|
42
|
+
# cli.close_logger
|
43
|
+
cli.init_logger
|
44
|
+
|
45
|
+
cli.info "child #{index}, running spawn block..."
|
46
|
+
begin
|
47
|
+
config.spawn_block[self]
|
48
|
+
rescue => e
|
49
|
+
raise e if $DEBUG
|
50
|
+
error e.message
|
51
|
+
error e.backtrace.join("\n")
|
52
|
+
stop!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
pid
|
57
|
+
end
|
58
|
+
|
59
|
+
def memory_used_pct
|
60
|
+
self.pid = Process.pid if pid.nil?
|
61
|
+
result = `ps -o %mem= -p #{pid}`
|
62
|
+
result.nil? || result == '' ? -1 : result.to_f
|
63
|
+
end
|
64
|
+
|
65
|
+
def memory_used_percent
|
66
|
+
mem = memory_used_pct
|
67
|
+
mem < 0 ? 'DEAD' : sprintf('%.2f%', mem)
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_worker
|
71
|
+
self.pid = Process.pid if pid.nil?
|
72
|
+
respawn! if memory_used_pct == -1 && master?
|
73
|
+
end
|
74
|
+
|
75
|
+
def respawn!
|
76
|
+
if master? && pid
|
77
|
+
cli.info "NOTE: re-spawning child #{index} (pid #{pid}), memory is at #{memory_used_percent}."
|
78
|
+
begin
|
79
|
+
Process.kill('USR1', pid)
|
80
|
+
sleep 5
|
81
|
+
Process.kill('TERM', pid)
|
82
|
+
rescue Errno::ESRCH
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
self.pid = nil
|
86
|
+
spawn
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_signal(sig)
|
91
|
+
Process.kill(sig, pid) if pid
|
92
|
+
rescue Errno::ESRCH
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def pid_file
|
97
|
+
"#{config.pid_prefix}.#{index + 1}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def master?
|
101
|
+
Process.pid == cli.master_pid
|
102
|
+
end
|
103
|
+
|
104
|
+
def log_worker
|
105
|
+
cli.info(status) if memory_used_pct >= 0.0
|
106
|
+
end
|
107
|
+
|
108
|
+
def status
|
109
|
+
"worker.#{config.name}[index=#{index}, pid=#{pid ? pid : 'nil'}] —— memory: #{memory_used_percent}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'state_machines'
|
3
|
+
|
4
|
+
require_relative 'worker'
|
5
|
+
require_relative 'memory'
|
6
|
+
|
7
|
+
require_relative 'monitors/oom'
|
8
|
+
require_relative 'monitors/dead_children'
|
9
|
+
|
10
|
+
module Sidekiq
|
11
|
+
module Cluster
|
12
|
+
class WorkerPool
|
13
|
+
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
state_machine :state, initial: :idle do
|
17
|
+
event :start do
|
18
|
+
transition [:idle] => :starting
|
19
|
+
end
|
20
|
+
|
21
|
+
event :started do
|
22
|
+
transition [:starting] => :running
|
23
|
+
end
|
24
|
+
|
25
|
+
event :stop do
|
26
|
+
transition [:idle, :starting, :running, :stopping] => :stopping
|
27
|
+
end
|
28
|
+
|
29
|
+
event :shutdown do
|
30
|
+
transition [:idle, :starting, :running, :stopping] => :stopped
|
31
|
+
end
|
32
|
+
|
33
|
+
after_transition any => :stopped do |pool, _transition|
|
34
|
+
pool.cli.stop!
|
35
|
+
end
|
36
|
+
|
37
|
+
state :idle, :starting, :started, :stopping, :stopped
|
38
|
+
end
|
39
|
+
|
40
|
+
extend Forwardable
|
41
|
+
def_delegators :@cli, :info, :error, :print, :stdout, :stdin, :stderr, :kernel
|
42
|
+
def_delegators :@workers, :each
|
43
|
+
|
44
|
+
attr_accessor :workers, :config, :cli, :process_count, :monitors
|
45
|
+
|
46
|
+
MONITORS = [Monitors::DeadChildren, Monitors::OOM]
|
47
|
+
|
48
|
+
def initialize(cli, config)
|
49
|
+
self.cli = cli
|
50
|
+
self.config = config
|
51
|
+
self.process_count = config.process_count
|
52
|
+
self.workers = []
|
53
|
+
self.monitors = []
|
54
|
+
self.state = :idle
|
55
|
+
|
56
|
+
@signal_received = false
|
57
|
+
end
|
58
|
+
|
59
|
+
def spawn
|
60
|
+
start!
|
61
|
+
|
62
|
+
create_workers
|
63
|
+
|
64
|
+
info "spawning #{workers.size} workers..."
|
65
|
+
workers.each(&:spawn)
|
66
|
+
|
67
|
+
info "worker pids: #{workers.map(&:pid)}"
|
68
|
+
|
69
|
+
setup_signal_traps
|
70
|
+
started!
|
71
|
+
|
72
|
+
start_monitors
|
73
|
+
|
74
|
+
info 'startup successful'
|
75
|
+
|
76
|
+
Process.waitall
|
77
|
+
monitors.each(&:join)
|
78
|
+
|
79
|
+
info 'all children exited, shutting down'
|
80
|
+
end
|
81
|
+
|
82
|
+
def operational?
|
83
|
+
shutdown! if stopping?
|
84
|
+
stop! if @signal_received
|
85
|
+
state_name == :running
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def create_workers
|
91
|
+
process_count.times { |index| self.workers << Worker.new(index, cli) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def start_monitors
|
95
|
+
self.monitors = MONITORS.map { |monitor| monitor.new(self).start }
|
96
|
+
end
|
97
|
+
|
98
|
+
def setup_signal_traps
|
99
|
+
%w(INT USR1 TERM).each do |sig|
|
100
|
+
Signal.trap(sig) do
|
101
|
+
handle_signal(sig)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle_signal(sig)
|
107
|
+
cli.stderr.puts "received OS signal #{sig}"
|
108
|
+
@signal_received = true if (sig == 'INT' || sig == 'TERM' || sig == 'STOP')
|
109
|
+
workers.each { |w| w.handle_signal(sig) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/sidekiq-cluster.gemspec
CHANGED
@@ -4,10 +4,6 @@ lib = File.expand_path('../lib', __FILE__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
5
|
require 'sidekiq/cluster/version'
|
6
6
|
|
7
|
-
Sidekiq::Cluster::DESCRIPTION = <<-eof
|
8
|
-
This library provides CLI interface for starting multiple copies of Sidekiq in parallel, typically to take advantage of multi-core systems. By default it starts N - 1 processes, where N is the number of cores on the current system. Sidekiq Cluster is controlled with CLI flags that appear before `--` (double dash), while any arguments that follow double dash are passed to each Sidekiq process. The exception is the `-P pidfile`, which clustering script passes to each sidekiq process individually.
|
9
|
-
eof
|
10
|
-
|
11
7
|
Gem::Specification.new do |spec|
|
12
8
|
spec.name = 'sidekiq-cluster'
|
13
9
|
spec.version = Sidekiq::Cluster::VERSION
|
@@ -28,6 +24,8 @@ Gem::Specification.new do |spec|
|
|
28
24
|
|
29
25
|
spec.add_dependency 'colored2'
|
30
26
|
spec.add_dependency 'sidekiq'
|
27
|
+
spec.add_dependency 'dry-configurable'
|
28
|
+
spec.add_dependency 'state_machines'
|
31
29
|
|
32
30
|
spec.add_development_dependency 'rspec', '~> 3.5.0'
|
33
31
|
spec.add_development_dependency 'rspec-its'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-cluster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Gredeskoul
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-configurable
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: state_machines
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
41
69
|
- !ruby/object:Gem::Dependency
|
42
70
|
name: rspec
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,7 +151,7 @@ dependencies:
|
|
123
151
|
- !ruby/object:Gem::Version
|
124
152
|
version: '0'
|
125
153
|
description: |2
|
126
|
-
|
154
|
+
This library provides CLI interface for starting multiple copies of Sidekiq in parallel, typically to take advantage of multi-core systems. By default it starts N - 1 processes, where N is the number of cores on the current system. Sidekiq Cluster is controlled with CLI flags that appear before `--` (double dash), while any arguments that follow double dash are passed to each Sidekiq process. The exception is the `-P pidfile`, which clustering script passes to each sidekiq process individually.
|
127
155
|
email:
|
128
156
|
- kigster@gmail.com
|
129
157
|
executables:
|
@@ -142,7 +170,17 @@ files:
|
|
142
170
|
- exe/sidekiq-cluster
|
143
171
|
- lib/sidekiq/cluster.rb
|
144
172
|
- lib/sidekiq/cluster/cli.rb
|
173
|
+
- lib/sidekiq/cluster/config.rb
|
174
|
+
- lib/sidekiq/cluster/memory.rb
|
175
|
+
- lib/sidekiq/cluster/memory/individual.rb
|
176
|
+
- lib/sidekiq/cluster/memory/total.rb
|
177
|
+
- lib/sidekiq/cluster/monitors/base.rb
|
178
|
+
- lib/sidekiq/cluster/monitors/dead_children.rb
|
179
|
+
- lib/sidekiq/cluster/monitors/oom.rb
|
180
|
+
- lib/sidekiq/cluster/stdlib_ext.rb
|
145
181
|
- lib/sidekiq/cluster/version.rb
|
182
|
+
- lib/sidekiq/cluster/worker.rb
|
183
|
+
- lib/sidekiq/cluster/worker_pool.rb
|
146
184
|
- sidekiq-cluster.gemspec
|
147
185
|
homepage: https://github.com/kigster/sidekiq-cluster
|
148
186
|
licenses:
|
@@ -168,9 +206,10 @@ rubygems_version: 2.4.5.2
|
|
168
206
|
signing_key:
|
169
207
|
specification_version: 4
|
170
208
|
summary: This library provides CLI interface for starting multiple copies of Sidekiq
|
171
|
-
in parallel,
|
172
|
-
N - 1 processes,
|
173
|
-
is controlled with CLI
|
174
|
-
that follow double dash are
|
175
|
-
`-P pidfile`, which clustering script
|
209
|
+
in parallel, typically to take advantage of multi-core systems. By default it
|
210
|
+
starts N - 1 processes, where N is the number of cores on the current system.
|
211
|
+
Sidekiq Cluster is controlled with CLI flags that appear before `--` (double
|
212
|
+
dash), while any arguments that follow double dash are passed to each Sidekiq
|
213
|
+
process. The exception is the `-P pidfile`, which clustering script passes to
|
214
|
+
each sidekiq process individually.
|
176
215
|
test_files: []
|