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 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: []