sidekiq-cluster 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2c5be67c437089d629dd84e49d3370f58ffd337d
4
- data.tar.gz: ea264f35a67a922097e22c432381b95573c2796f
3
+ metadata.gz: 0cee3d1d2e77f76ea89ce26c50bb43346cf55ecd
4
+ data.tar.gz: 36cce663074cfda880ebf60e5de3e450786d8c4d
5
5
  SHA512:
6
- metadata.gz: 2f8062fd63c9e0a7c7bb6dff4927e24e98cc61004454acd35abd5b32a2ea0a4b996d9771c89633502d0a914d592d0f7351b27bb01bcaa5fd9d0acd8ab524a474
7
- data.tar.gz: abb384d6f36a0994d5d69b2e66c2c6a1a6a6a169364c3da853f66e663872bfdc8166144b16a5de96674babc422d9f855a9af57c4cbe3b0f1c27a96eb45c1eab6
6
+ metadata.gz: 58a069bb53080b168197b9ce9f1e4cf574fec259fb3f6fbd25fbb383b08d3844ebaab9566c5265d2aa91d5fa3e5af6c680c134247e57a498417378416c4b82f0
7
+ data.tar.gz: 8d041f63d583fb3d3a5108944f7ef1d65978d71e1ee3ecca45968737e032e17b81350e5ec7f24768cfb0d8593ab96bf79ad429aa5ef136e582861ce3654d8802
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require_relative '../lib/sidekiq/cluster/cli'
4
- Sidekiq::Cluster::CLI.new(ARGV).run!
4
+ Sidekiq::Cluster::CLI.new(ARGV).execute!
5
5
 
6
6
 
@@ -1,4 +1,4 @@
1
- require 'sidekiq/cluster/version'
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
 
@@ -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 :name,
15
- :pid_prefix,
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
- def per_process_memory_limit
39
- memory_percentage_limit.to_f / process_count.to_f
18
+ class << self
19
+ attr_accessor :instance
40
20
  end
41
21
 
42
- def run!
43
- initialize_cli_arguments!
44
- print_header!
45
- start_main_loop!
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 start_main_loop!
49
- start_children.each do |descriptor|
50
- processes[descriptor.pid] = descriptor
51
- end
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
- main
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
- def print(*args)
64
- puts(*args)
65
- end
66
-
67
- def stop!(code = 0)
68
- Kernel.exit(code)
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
- private
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
- def print_header!
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
- def init_logger!
90
- self.logger = ::Logger.new(log_device, level: ::Logger::INFO)
52
+ boot
91
53
  end
92
54
 
93
- def split_argv!
94
- self.sidekiq_argv = argv[(argv.index('--') + 1)..-1]
95
- self.argv = argv[0..(argv.index('--') - 1)]
55
+ def boot
56
+ @worker_pool = ::Sidekiq::Cluster::WorkerPool.new(self, config)
57
+ @worker_pool.spawn
96
58
  end
97
59
 
98
- def setup_signal_traps
99
- %w(INT USR1 TERM).each do |sig|
100
- Signal.trap(sig) do
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 main
107
- Thread.new do
108
- info 'watching for outsized Sidekiq processes...'
109
- loop do
110
- sleep 10
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
- def info(*args)
119
- @logger.info(*args)
74
+ kernel.exit(code)
120
75
  end
121
76
 
122
- def error(*args, exception: nil)
123
- @logger.error("exception: #{exception.message}") if exception
124
- @logger.error(*args)
125
- end
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 fork_child(index)
137
- require 'sidekiq'
138
- require 'sidekiq/cli'
139
-
140
- Process.fork do
141
- process_argv = sidekiq_argv.dup << '-P' << pid_file(index)
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
- def pid_file(index)
158
- "#{pid_prefix}.#{index + 1}"
159
- end
94
+ log_device.sync = true
160
95
 
161
- def start_children
162
- Array.new(process_count) do |index|
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 check_pids
169
- print_info = false
170
- @last_info_printed ||= Time.now.to_i
171
- if Time.now.to_i - @last_info_printed > 60
172
- @last_info_printed = Time.now.to_i
173
- print_info = true
174
- end
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 restart_oversized_child(pid)
191
- @pids.delete(pid)
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 replace_pid(old_pid, new_pid)
199
- pd = processes[old_pid]
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 restart_dead_child(pid)
211
- info "pid=#{pid} died, restarting..."
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
- def parse_args!
222
- options = {}
223
- banner = "USAGE".bold.blue + "\n sidekiq-cluster [options] -- [sidekiq-options]".bold.green
224
- parser = ::OptionParser.new(banner) do |opts|
225
- opts.separator ''
226
- opts.separator 'EXAMPLES'.bold.blue
227
- opts.separator ' $ cd rails_app'.bold.magenta
228
- opts.separator ' $ bundle exec sidekiq-cluster -N 2 -- -c 10 -q default,12 -l log/sidekiq.log'.bold.magenta
229
- opts.separator ' '
230
- opts.separator 'SIDEKIQ CLUSTER OPTIONS'.bold.blue
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('-L', '--logfile=FILE',
243
- 'Logfile for the cluster script', ' ') do |v|
244
- self.log_device = v
245
- init_logger!
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
- opts.on('-M', '--max-memory=PERCENT',
248
- 'Maximum percent RAM that this',
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
- self.memory_percentage_limit = v.to_f
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
- opts.on('-N', '--num-processes=NUM',
254
- 'Number of processes to start,',
255
- 'defaults to number of cores - 1', ' ') do |v|
256
- self.process_count = v.to_i
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 log to STDOUT') do |v|
260
- self.logger = Logger.new(nil)
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 sidekiqs') do |_v|
264
- options[:debug] = true
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
- self.print opts
186
+ config.help = true
187
+ stdout.puts opts
268
188
  stop!
269
189
  end
270
190
  end
271
- parser.order!(argv)
272
- print("debug: #{self.inspect}") if options[:debug]
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
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def sum
3
+ reduce(:+)
4
+ end
5
+ 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 = '0.1.1'
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
@@ -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.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
- 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.
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, typically to take advantage of multi-core systems. By default it starts
172
- N - 1 processes, where N is the number of cores on the current system. Sidekiq Cluster
173
- is controlled with CLI flags that appear before `--` (double dash), while any arguments
174
- that follow double dash are passed to each Sidekiq process. The exception is the
175
- `-P pidfile`, which clustering script passes to each sidekiq process individually.
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: []