sidekiq-process_manager 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3d1b1462578e76a03c0908d2e04fe30162a06947e5176e478b7cbf51f797c2d
4
+ data.tar.gz: bd2f58e84676cac41f9a44bbb4299038b30912e319d793164eccac1a79dff2c7
5
+ SHA512:
6
+ metadata.gz: e0dd9349a7ec9d5c6ca15728218ef087d824e5b074ac1725393948a00834765d1291a7b3a775b39a6b9cb4905892a2878d19adefb405183db916c9622f882a17
7
+ data.tar.gz: adf75b664e86f909341822c65954995def3729205c0d73e69a766d69541d60bce84241330b7d9b7917eb778e50400ebb44a67ac9a4e7fa766a9b61b56f46ffa6
data/MIT_LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2019 Brian Durand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # Sidekiq::ProcessManager
2
+
3
+ [![Build Status](https://travis-ci.com/bdurand/sidekiq-process_manager.svg?branch=master)](https://travis-ci.com/bdurand/sidekiq-process_manager)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/ed89164d480af0e1442e/maintainability)](https://codeclimate.com/github/bdurand/sidekiq-process_manager/maintainability)
5
+
6
+ This gem provides a command line script for managing [sidekiq](https://github.com/mperham/sidekiq) processes. It starts up a process that then forks multiple sidekiq processes and manages their life cycle. This is important for large sidekiq installations, since without it on MRI ruby, sidekiq will only use one CPU core. By starting multiple processes you make all cores available.
7
+
8
+ The sidekiq processes can all be managed by sending signals to the manager process. This process simply forwards the signals on to the child processes, allowing you to control the sidekiq processes as you normally would.
9
+
10
+ If one of the sidekiq processes dies unexpectedly, the process manager automatically starts a new sidekiq process to replace it.
11
+
12
+ ## Pre-Forking
13
+
14
+ You can use pre-forking to improve memory utilization on the child sidekiq processes. This is similar to using pre-forking in a web server like puma or unicorn. You application will be pre-loaded by the master process and the child sidekiq processes will utilize the loaded code via copy-on-write memory. The overall effect is that you should be able to run more sidekiq processes in a lower memory footprint.
15
+
16
+ One issue with pre-forking is that any file descriptors (including network connections) your application has open when it forks will be shared between all the processes. This can lead to race conditions and other problems. To fix it, you must close and reopen all database connections, etc. after the child sidekiq processes have been forked.
17
+
18
+ To do this, your application will need to add an initializer with calls to `Sidekiq::ProcessManager.after_fork` and `Sidekiq::ProcessManager.before_fork`.
19
+
20
+ The `before_fork` hook is called on the master process right before it starts forking processes. You can use this to close connections on the master process that you don't need.
21
+
22
+ The `after_fork` hook is called after a forked sidekiq process has initialized the application. You can use this to re-establish connections so that each process gets it's own streams.
23
+
24
+ At a minimum, you'll probably want the following hooks to close and re-open the ActiveRecord database connection:
25
+
26
+ ```ruby
27
+ Sidekiq::ProcessManager.before_fork do
28
+ ActiveRecord::Base.connection.disconnect!
29
+ end
30
+
31
+ Sidekiq::ProcessManager.after_fork do
32
+ ActiveRecord::Base.connection.reconnect!
33
+ end
34
+ ```
35
+
36
+ If you're already using a pre-forking web server, you'll need to do most of the same things for sidekiq as well.
37
+
38
+ ## Pre-Booting
39
+
40
+ If your application can't be pre-forked, you can at load the gem files and libraries your application depends on instead by specifying a preboot file. This file will be loaded by the master process and any code loaded will be copy-on-write shared with the child processes.
41
+
42
+ For a Rails application, you would normally want to preboot the `config/boot.rb` file.
43
+
44
+ ## Usage
45
+
46
+ Install the gem in your sidekiq process and run it with `bundle exec sidekiq-process-manager` or, if you use [bundle binstubs](https://bundler.io/man/bundle-binstubs.1.html), `bin/sidekiq-process-manager`. Command line arguments are passed through to `sidekiq`. If you want to supply on of the `sidekiq-process_manager` specific options, those options should come first and the `sidekiq` options should appear after a `--` flag
47
+
48
+ ```bash
49
+ bundle exec sidekiq-process-manager -C config/sidekiq.yml
50
+ ```
51
+
52
+ or
53
+
54
+
55
+ ```bash
56
+ bundle exec sidekiq-process-manager --no-prefork -- -C config/sidekiq.yml
57
+ ```
58
+
59
+ You can specify the number of sidekiq processes to run with the `--processes` argument or with the `SIDEKIQ_PROCESSES` environment variable. The default number of processes is 1.
60
+
61
+ ```bash
62
+ bundle exec sidekiq-process-manager --processes 4
63
+ ```
64
+
65
+ or
66
+
67
+ ```bash
68
+ SIDEKIQ_PROCESSES=4 bundle exec sidekiq-process-manager
69
+ ```
70
+
71
+ You can turn pre-forking on or off with the --prefork or --no-prefork flag. You can also specify to turn on pre-forking with the `SIDEKIQ_PREFORK` environment variable.
72
+
73
+ ```bash
74
+ bundle exec sidekiq-process-manager --processes 4 --prefork
75
+ ```
76
+
77
+ or
78
+
79
+ ```bash
80
+ SIDEKIQ_PREFORK=1 SIDEKIQ_PROCESSES=4 bundle exec sidekiq-process-manager
81
+ ```
82
+
83
+ You can turn pre-booting on with the `--preboot` argument or with the `SIDEKIQ_PREBOOT` environment variable.
84
+
85
+ ```bash
86
+ bundle exec sidekiq-process-manager --processes 4 --preboot config/boot.rb
87
+ ```
88
+
89
+ or
90
+
91
+ ```bash
92
+ SIDEKIQ_PREBOOT=config/boot.rb SIDEKIQ_PROCESSES=4 bundle exec sidekiq-process-manager
93
+ ```
94
+
95
+ ## Alternatives
96
+
97
+ Any process manager can be an alternative (service, update, systemd, monit, god, etc.).
98
+
99
+ The advantages this gem can provide are:
100
+
101
+ 1. Pre-forking can be useful on systems with many cores but not enough memory to run enough sidekiq processes to use them all.
102
+
103
+ 2. Running in the foreground with output going to standard out instead of as daemon can integrate better into containerized environments.
104
+
105
+ 3. Built with sidekiq in mind so signal passing is consistent.
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative "../lib/sidekiq-process_manager"
6
+
7
+ DEFAULT_PROCESS_COUNT = 1
8
+
9
+ options = {
10
+ process_count: Integer(ENV.fetch('SIDEKIQ_PROCESSES', DEFAULT_PROCESS_COUNT)),
11
+ prefork: !ENV.fetch("SIDEKIQ_PREFORK", "").empty?,
12
+ preboot: ENV["SIDEKIQ_PREBOOT"],
13
+ mode: nil,
14
+ }
15
+
16
+ parser = OptionParser.new do |opts|
17
+ opts.banner = "Usage: sidekiq-process-manager [options] [--] [sidekiq options]"
18
+
19
+ opts.on('--processes PROCESSES', Integer, "Number of processes to spin up (can also specify with SIDEKIQ_PROCESSES)") do |count|
20
+ options[:process_count] = count
21
+ end
22
+
23
+ opts.on('--[no-]prefork', "Use prefork for spinning up sidekiq processes (can also specify with SIDEKIQ_PREFORK)") do |prefork|
24
+ options[:prefork] = prefork
25
+ end
26
+
27
+ opts.on('--preboot FILE', "Preboot the processes (can also specify with SIDEKIQ_PREBOOT)") do |preboot|
28
+ options[:preboot] = preboot
29
+ end
30
+
31
+ opts.on('--testing', "Enable test mode") do |testing|
32
+ options[:mode] = :testing if testing
33
+ end
34
+
35
+ opts.on("--help", "Prints this help") do
36
+ puts opts
37
+ exit
38
+ end
39
+
40
+ opts.separator(<<~DESCR)
41
+
42
+ After the manager options, pass in any options for the sidekiq processes.
43
+ Additionally, passing in the optional `--` will explicitly end the manager options and begin the sidekiq opts.
44
+ E.g.
45
+ $ sidekiq-process-manager --no-prefork -- -C config/sidekiq.rb
46
+ Calls sidekiq with `sidekiq -C config/sidekiq.rb`
47
+ DESCR
48
+ end
49
+
50
+ sidekiq_args = []
51
+ begin
52
+ parser.order!(ARGV) { |nonopt| sidekiq_args << nonopt }
53
+ rescue OptionParser::InvalidOption => err
54
+ # Handle the case where a user doesn't put in the `--` to separate the args
55
+ sidekiq_args.concat(err.args)
56
+ end
57
+
58
+ ARGV[0, 0] = sidekiq_args
59
+
60
+ begin
61
+ manager = Sidekiq::ProcessManager::Manager.new(**options)
62
+ manager.start
63
+ rescue => e
64
+ STDERR.puts e.message
65
+ STDERR.puts e.backtrace.join($/)
66
+ exit 1
67
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "sidekiq/cli"
5
+
6
+ module Sidekiq
7
+ module ProcessManager
8
+ class Manager
9
+
10
+ attr_reader :cli
11
+
12
+ def initialize(process_count: 1, prefork: false, preboot: nil, mode: nil, silent: false)
13
+ # Get the number of processes to fork
14
+ @process_count = process_count
15
+ raise ArgumentError.new("Process count must be greater than 1") if @process_count < 1
16
+
17
+ @prefork = (prefork && process_count > 1)
18
+ @preboot = preboot if process_count > 1 && !prefork
19
+
20
+ if mode == :testing
21
+ require_relative "../../../spec/support/mocks"
22
+ @cli = MockSidekiqCLI.new(silent)
23
+ else
24
+ @cli = Sidekiq::CLI.instance
25
+ end
26
+
27
+ @silent = silent
28
+ @pids = []
29
+ @terminated_pids = []
30
+ @started = false
31
+ @monitor = Monitor.new
32
+ end
33
+
34
+ # Start the process manager. This method will start the specified number
35
+ # of sidekiq processes and monitor them. It will only exit once all child
36
+ # processes have exited. If a child process dies unexpectedly, it will be
37
+ # restarted.
38
+ #
39
+ # Child processes are manged by sending the signals you would normally send
40
+ # to a sidekiq process to the process manager instead.
41
+ def start
42
+ raise "Process manager already started" if started?
43
+ @started = true
44
+
45
+ load_sidekiq
46
+
47
+ master_pid = ::Process.pid
48
+
49
+ # Trap signals that will be forwarded to child processes
50
+ [:INT, :TERM, :USR1, :USR2, :TSTP, :TTIN].each do |signal|
51
+ ::Signal.trap(signal) do
52
+ if ::Process.pid == master_pid
53
+ send_signal_to_children(signal)
54
+ end
55
+ end
56
+ end
57
+
58
+ # Ensure that child processes receive the term signal when the master process exits.
59
+ at_exit do
60
+ if ::Process.pid == master_pid && @process_count > 0
61
+ @pids.each do |pid|
62
+ send_signal_to_children(:TERM)
63
+ end
64
+ end
65
+ end
66
+
67
+ @process_count.times do
68
+ start_child_process!
69
+ end
70
+
71
+ log_info("Process manager started with pid #{::Process.pid}")
72
+ monitor_child_processes
73
+ log_info("Process manager #{::Process.pid} exiting")
74
+ end
75
+
76
+ # Helper to wait on the manager to wait on child processes to start up.
77
+ def wait(timeout = 5)
78
+ start_time = Time.now
79
+ while Time.now < start_time + timeout
80
+ return if @pids.size == @process_count
81
+ sleep(0.01)
82
+ end
83
+
84
+ raise Timeout::Error.new("child processes failed to start in #{timeout} seconds")
85
+ end
86
+
87
+ # Helper to gracefully stop all child processes.
88
+ def stop
89
+ @process_count = 0
90
+ send_signal_to_children(:TSTP)
91
+ send_signal_to_children(:TERM)
92
+ end
93
+
94
+ def pids
95
+ @pids.dup
96
+ end
97
+
98
+ def started?
99
+ @started
100
+ end
101
+
102
+ private
103
+
104
+ def log_info(message)
105
+ return if @silent
106
+ if $stdout.tty?
107
+ $stdout.write("#{message}#{$/}")
108
+ $stdout.flush
109
+ else
110
+ Sidekiq.logger.info(message)
111
+ end
112
+ end
113
+
114
+ def log_warning(message)
115
+ return if @silent
116
+ if $stderr.tty?
117
+ $stderr.write("#{message}#{$/}")
118
+ $stderr.flush
119
+ else
120
+ Sidekiq.logger.warn(message)
121
+ end
122
+ end
123
+
124
+ def load_sidekiq
125
+ @cli.parse
126
+ Sidekiq.options[:daemon] = false
127
+ Sidekiq.options[:pidfile] = false
128
+ if @prefork
129
+ log_info("Pre-forking application")
130
+ @cli.send(:boot_system)
131
+ Sidekiq::ProcessManager.run_before_fork_hooks
132
+ elsif @preboot && !@preboot.empty?
133
+ if ::File.exist?(@preboot)
134
+ require ::File.expand_path(@preboot).sub(/\.rb\Z/, "")
135
+ else
136
+ log_warning("Could not find preboot file #{@preboot}")
137
+ end
138
+ end
139
+ end
140
+
141
+ def set_program_name!
142
+ $PROGRAM_NAME = "sidekiq process manager #{Sidekiq.options[:tag]} [#{@pids.size} processes]"
143
+ end
144
+
145
+ def start_child_process!
146
+ @pids << fork do
147
+ @process_count = 0
148
+ @pids.clear
149
+ Sidekiq::ProcessManager.run_after_fork_hooks
150
+ @cli.run
151
+ end
152
+ log_info("Forked sidekiq process with pid #{@pids.last}")
153
+ set_program_name!
154
+ end
155
+
156
+ def send_signal_to_children(signal)
157
+ log_info("Process manager trapped signal #{signal}")
158
+ @process_count = 0 if (signal == :INT || signal == :TERM)
159
+ @pids.each do |pid|
160
+ begin
161
+ log_info("Sending signal #{signal} to sidekiq process #{pid}")
162
+ ::Process.kill(signal, pid)
163
+ rescue => e
164
+ log_warning("Error sending signal #{signal} to sidekiq process #{pid}: #{e.inspect}")
165
+ end
166
+ end
167
+ end
168
+
169
+ # Listen for child processes dying and restart if necessary.
170
+ def monitor_child_processes
171
+ loop do
172
+ pid = ::Process.wait
173
+ @pids.delete(pid)
174
+ log_info("Sidekiq process #{pid} exited")
175
+
176
+ # If there are not enough processes running, start a replacement one.
177
+ if @process_count > @pids.size
178
+ start_child_process! if @pids.size < @process_count
179
+ end
180
+
181
+ set_program_name!
182
+
183
+ if @pids.empty?
184
+ break
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module ProcessManager
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "process_manager/version"
4
+ require_relative "process_manager/manager"
5
+
6
+ module Sidekiq
7
+ module ProcessManager
8
+
9
+ class << self
10
+ def before_fork(&block)
11
+ @before_fork ||= []
12
+ @before_fork << block
13
+ end
14
+
15
+ def after_fork(&block)
16
+ @after_fork ||= []
17
+ @after_fork << block
18
+ end
19
+
20
+ def run_before_fork_hooks
21
+ if defined?(@before_fork) && @before_fork
22
+ @before_fork.each do |block|
23
+ block.call
24
+ end
25
+ end
26
+ @before_fork = nil
27
+ end
28
+
29
+ def run_after_fork_hooks
30
+ if defined?(@after_fork) && @after_fork
31
+ @after_fork.each do |block|
32
+ block.call
33
+ end
34
+ end
35
+ @after_fork = nil
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sidekiq/process_manager"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sidekiq/process_manager/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sidekiq-process_manager"
7
+ spec.version = Sidekiq::ProcessManager::VERSION
8
+ spec.authors = ["Brian Durand"]
9
+ spec.email = ["bbdurand@gmail.com"]
10
+
11
+ spec.summary = "Process manager for forking and monitoring multiple sidekiq processes."
12
+ spec.homepage = "https://github.com/bdurand/sidekiq-process_manager"
13
+ spec.license = "MIT"
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ ignore_files = %w(
18
+ .gitignore
19
+ .travis.yml
20
+ Appraisals
21
+ Gemfile
22
+ Gemfile.lock
23
+ Rakefile
24
+ gemfiles/
25
+ spec/
26
+ )
27
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
28
+ `git ls-files -z`.split("\x0").reject{ |f| ignore_files.any?{ |path| f.start_with?(path) } }
29
+ end
30
+ spec.bindir = "bin"
31
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.required_ruby_version = '>= 2.2.2'
35
+
36
+ spec.add_dependency "sidekiq", ">= 3.0"
37
+
38
+ spec.add_development_dependency "bundler", "~> 1.10"
39
+ spec.add_development_dependency "rake"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "appraisal"
42
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq-process_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brian Durand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
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: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: appraisal
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - bbdurand@gmail.com
86
+ executables:
87
+ - sidekiq-process-manager
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - MIT_LICENSE.txt
92
+ - README.md
93
+ - bin/sidekiq-process-manager
94
+ - lib/sidekiq-process_manager.rb
95
+ - lib/sidekiq/process_manager.rb
96
+ - lib/sidekiq/process_manager/manager.rb
97
+ - lib/sidekiq/process_manager/version.rb
98
+ - sidekiq-process_manager.gemspec
99
+ homepage: https://github.com/bdurand/sidekiq-process_manager
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 2.2.2
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.0.3
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Process manager for forking and monitoring multiple sidekiq processes.
122
+ test_files: []