delayed_job_master 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 97335497d66ae19f719fdfd655d184507e985b42
4
+ data.tar.gz: d723ca1ed73489e737cf2653e4685ab0644a4a26
5
+ SHA512:
6
+ metadata.gz: 56e8b33e80dee3d82b370f3275cc638fee882a458260c5304977c9379fcb6523aec185231038e0629fefb090c78d960ed4fa28e674e4b1a38d702b0492dacce0
7
+ data.tar.gz: 4b26b8ffbe453683d07809afc5a968dcb115429b0c41a1b2593da12996e5ba1d814e65126d9afa63ad63ae52d353322be2f02385ce4e630e7ff849e549d522be
@@ -0,0 +1,19 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.project
4
+ /Gemfile.lock
5
+ /gemfiles/.bundle
6
+ /gemfiles/vendor
7
+ /gemfiles/*gemfile.lock
8
+ /_yardoc/
9
+ /bin/
10
+ /coverage/
11
+ /doc/
12
+ /pkg/
13
+ /log/
14
+ /tmp/
15
+ /spec/reports/
16
+ /spec/**/db/*.sqlite3
17
+ /spec/**/log/*.log
18
+ /spec/**/tmp/pids/*.pid
19
+ /vendor/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ --require rails_helper
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ services:
3
+ - mongodb
4
+ rvm:
5
+ - 2.3.4
6
+ - 2.4.1
7
+ gemfile:
8
+ - gemfiles/active_record.gemfile
9
+ - gemfiles/mongoid.gemfile
10
+ before_install:
11
+ - gem install bundler -v 1.15.3
12
+ before_script:
13
+ - cd spec/dummy
14
+ - bundle exec rake db:migrate RAILS_ENV=test
15
+ - cd ../..
16
+ script: bundle exec rspec
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at kaneta@sitebridge.co.jp. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in delayed_job_master.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Yoshikazu Kaneta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,158 @@
1
+ # DelayedJobMaster
2
+
3
+ A simple delayed_job master process to control multiple workers.
4
+
5
+ ## Features
6
+
7
+ * Preload application and fork workers fastly.
8
+ * Monitor workers and fork new workers if necessary.
9
+ * Restart workers with memory limitation.
10
+ * Trap USR1 signal to reopen log files.
11
+ * Trap USR2 signal to restart master and workers.
12
+ * Auto-scale worker processes.
13
+
14
+ ## Dependencies
15
+
16
+ * ruby 2.3+
17
+ * delayed_job 4.1
18
+
19
+ Supported delayed_job backends:
20
+
21
+ * delayed_job_active_record 4.1
22
+ * delayed_job_mongoid 2.3
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'delayed_job_master', require: false
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Create default files:
37
+
38
+ $ rails generate delayed_job_master
39
+
40
+ This command creates `bin/delayed_job_master` and `config/delayed_job_master.rb`.
41
+
42
+ ## Configuration
43
+
44
+ Edit `config/delayed_job_master.rb`:
45
+
46
+ ```ruby
47
+ # working directory
48
+ working_directory Dir.pwd
49
+
50
+ # monitor wait time in second
51
+ monitor_wait 5
52
+
53
+ # path to pid file
54
+ pid_file "#{Dir.pwd}/tmp/pids/delayed_job_master.pid"
55
+
56
+ # path to log file
57
+ log_file "#{Dir.pwd}/log/delayed_job_master.log"
58
+
59
+ # log level
60
+ log_level :info
61
+
62
+ # worker1
63
+ add_worker do |worker|
64
+ # queue name for the worker
65
+ worker.queues %w(queue1)
66
+
67
+ # worker control (:static or :dynamic)
68
+ worker.control :static
69
+
70
+ # worker count
71
+ worker.count 1
72
+
73
+ # max memory in MB
74
+ worker.max_memory 300
75
+
76
+ # configs below are same as delayed_job, see https://github.com/collectiveidea/delayed_job
77
+ # worker.sleep_delay 5
78
+ # worker.read_ahead 5
79
+ # worker.max_attempts 25
80
+ # worker.max_run_time 4.hours
81
+ # worker.min_priority 1
82
+ # worker.max_priority 10
83
+ end
84
+
85
+ # worker2
86
+ add_worker do |worker|
87
+ worker.queues %w(queue2)
88
+ worker.control :dynamic
89
+ worker.count 2
90
+ end
91
+
92
+ before_fork do |master, worker_info|
93
+ Delayed::Worker.before_fork if defined?(Delayed::Worker)
94
+ end
95
+
96
+ after_fork do |master, worker_info|
97
+ Delayed::Worker.after_fork if defined?(Delayed::Worker)
98
+ end
99
+ ```
100
+
101
+ ## Usage
102
+
103
+ Start master:
104
+
105
+ $ RAILS_ENV=production bin/delayed_job_master -c config/delayed_job_master.rb -D
106
+
107
+ Command line options:
108
+
109
+ * -c, --config: Specify configuration file.
110
+ * -D, --daemon: Start master as a daemon.
111
+
112
+ Stop master and workers gracefully:
113
+
114
+ $ kill -TERM `cat tmp/pids/delayed_job_master.pid`
115
+
116
+ Stop master and workers forcefully:
117
+
118
+ $ kill -QUIT `cat tmp/pids/delayed_job_master.pid`
119
+
120
+ Reopen log files:
121
+
122
+ $ kill -USR1 `cat tmp/pids/delayed_job_master.pid`
123
+
124
+ Restart gracefully:
125
+
126
+ $ kill -USR2 `cat tmp/pids/delayed_job_master.pid`
127
+
128
+ Workers handle each signal as follows:
129
+
130
+ * TERM: Workers stop after finishing current jobs.
131
+ * QUIT: Workers are killed immediately.
132
+ * USR1: Workers reopen log files.
133
+ * USR2: New workers start, old workers stop after finishing current jobs.
134
+
135
+ ## Worker status
136
+
137
+ `ps` command shows worker status as follows:
138
+
139
+ ```
140
+ $ ps aux
141
+ ... delayed_job.0 (queue1) [BUSY] # BUSY process is currently proceeding some jobs
142
+ ```
143
+
144
+ After graceful restart, you may find OLD process.
145
+
146
+ ```
147
+ $ ps aux
148
+ ... delayed_job.0 (queue1) [BUSY] [OLD] # OLD process will stop after finishing current jobs.
149
+ ```
150
+
151
+ ## Contributing
152
+
153
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kanety/delayed_job_master. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
154
+
155
+ ## License
156
+
157
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
158
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'delayed/master/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "delayed_job_master"
8
+ spec.version = Delayed::Master::VERSION
9
+ spec.authors = ["Yoshikazu Kaneta"]
10
+ spec.email = ["kaneta@sitebridge.co.jp"]
11
+
12
+ spec.summary = %q{A simple delayed_job master process to control multiple workers}
13
+ spec.description = %q{A simple delayed_job master process to control multiple workers}
14
+ spec.homepage = "https://github.com/kanety/delayed_job_master"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.required_ruby_version = '>= 2.3'
23
+
24
+ spec.add_dependency "delayed_job", ">= 4.1"
25
+ spec.add_dependency "get_process_mem"
26
+
27
+ spec.add_development_dependency "rails"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rspec-rails"
30
+ spec.add_development_dependency "simplecov"
31
+ spec.add_development_dependency "sqlite3"
32
+ spec.add_development_dependency "delayed_job_mongoid", ">= 2.3"
33
+ spec.add_development_dependency "delayed_job_active_record", ">= 4.1"
34
+ end
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "delayed_job_active_record", "~> 4.1.0"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "delayed_job_mongoid", "~> 2.3.0"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,115 @@
1
+ require 'fileutils'
2
+ require 'logger'
3
+ require 'ostruct'
4
+ require_relative 'util/file_reopener'
5
+ require_relative 'master/version'
6
+ require_relative 'master/command'
7
+ require_relative 'master/callback'
8
+ require_relative 'master/worker_info'
9
+ require_relative 'master/worker_pool'
10
+ require_relative 'master/signal_handler'
11
+
12
+ module Delayed
13
+ class Master
14
+ attr_reader :config, :logger, :worker_infos
15
+
16
+ def initialize(argv)
17
+ config = Command.new(argv).config
18
+ @config = OpenStruct.new(config).freeze
19
+ @logger = setup_logger(@config.log_file, @config.log_level)
20
+ @worker_infos = []
21
+
22
+ @signal_handler = SignalHandler.new(self)
23
+ @worker_pool = WorkerPool.new(self, config)
24
+ end
25
+
26
+ def run
27
+ load_app
28
+ show_worker_configs
29
+ daemonize if @config.daemon
30
+
31
+ create_pid_file
32
+ @logger.info "started master #{Process.pid}"
33
+
34
+ @signal_handler.register
35
+ @worker_pool.init
36
+ @worker_pool.monitor_while { stop? }
37
+
38
+ remove_pid_file
39
+ @logger.info "shut down master"
40
+ end
41
+
42
+ def load_app
43
+ require File.join(@config.working_directory, 'config', 'environment')
44
+ require_relative 'master/job_counter'
45
+ require_relative 'worker/extension'
46
+ end
47
+
48
+ def prepared?
49
+ @worker_pool.prepared?
50
+ end
51
+
52
+ def quit
53
+ kill_workers
54
+ @stop = true
55
+ end
56
+
57
+ def stop
58
+ @signal_handler.dispatch('TERM')
59
+ @stop = true
60
+ end
61
+
62
+ def stop?
63
+ @stop
64
+ end
65
+
66
+ def reopen_files
67
+ @signal_handler.dispatch('USR1')
68
+ @logger.info "reopening files..."
69
+ Delayed::Util::FileReopener.reopen
70
+ @logger.info "reopened"
71
+ end
72
+
73
+ def restart
74
+ @signal_handler.dispatch('USR2')
75
+ @logger.info "restarting master..."
76
+ exec(*([$0] + ARGV))
77
+ end
78
+
79
+ def kill_workers
80
+ @signal_handler.dispatch('KILL')
81
+ end
82
+
83
+ def wait_workers
84
+ Process.waitall
85
+ end
86
+
87
+ private
88
+
89
+ def setup_logger(log_file, log_level)
90
+ FileUtils.mkdir_p(File.dirname(log_file))
91
+ logger = Logger.new(log_file)
92
+ logger.level = log_level
93
+ logger
94
+ end
95
+
96
+ def create_pid_file
97
+ FileUtils.mkdir_p(File.dirname(@config.pid_file))
98
+ File.write(@config.pid_file, Process.pid)
99
+ end
100
+
101
+ def remove_pid_file
102
+ File.delete(@config.pid_file) if File.exist?(@config.pid_file)
103
+ end
104
+
105
+ def daemonize
106
+ Process.daemon(true)
107
+ end
108
+
109
+ def show_worker_configs
110
+ @config.worker_configs.each do |config|
111
+ puts "#{config[:count]} worker for '#{config[:queues].join(',')}' under #{config[:control]} control"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,13 @@
1
+ module Delayed
2
+ class Master
3
+ class Callback
4
+ def initialize(config = {})
5
+ @config = config.select { |k, _| [:before_fork, :after_fork].include?(k) }
6
+ end
7
+
8
+ def run(name, *args)
9
+ @config[name].call(*args) if @config[name]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ require 'optparse'
2
+ require_relative 'dsl'
3
+
4
+ module Delayed
5
+ class Master
6
+ class Command
7
+ attr_reader :config
8
+
9
+ def initialize(args)
10
+ @config = {}
11
+
12
+ OptionParser.new { |opt|
13
+ opt.banner = <<-EOS
14
+ #{File.basename($PROGRAM_NAME)} #{Delayed::Master::VERSION}
15
+ Usage: #{File.basename($PROGRAM_NAME)} [options]
16
+ EOS
17
+
18
+ opt.on('-h', '--help', '-v', '--version', 'Show this message') do |boolean|
19
+ puts opt
20
+ exit
21
+ end
22
+ opt.on('-D', '--daemon', 'Start master as a daemon') do |boolean|
23
+ @config[:daemon] = boolean
24
+ end
25
+ opt.on('-c', '--config=FILE', 'Specify config file') do |file|
26
+ @config.merge!(DSL.new(file).config)
27
+ end
28
+ }.parse(args)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ module Delayed
2
+ class Master
3
+ class DSL
4
+ SIMPLE_CONFIGS = [:working_directory, :log_file, :log_level, :pid_file, :monitor_wait]
5
+ CALLBACK_CONFIGS = [:before_fork, :after_fork]
6
+
7
+ attr_reader :config
8
+
9
+ def initialize(config_file)
10
+ @config = { worker_configs: [] }
11
+ instance_eval(File.read(config_file))
12
+ @config
13
+ end
14
+
15
+ def add_worker
16
+ setting = WorkerSetting.new(control: :static, count: 1)
17
+ yield setting
18
+ setting.config[:exit_on_complete] = true if setting.config[:control] == :dynamic
19
+ @config[:worker_configs] << setting.config
20
+ end
21
+
22
+ SIMPLE_CONFIGS.each do |key|
23
+ define_method(key) do |value|
24
+ @config[key] = value
25
+ end
26
+ end
27
+
28
+ CALLBACK_CONFIGS.each do |key|
29
+ define_method(key) do |&block|
30
+ @config[key] = block
31
+ end
32
+ end
33
+
34
+ class WorkerSetting
35
+ SIMPLE_CONFIGS = [:control, :count, :max_memory,
36
+ :min_priority, :max_priority, :sleep_delay, :read_ahead, :exit_on_complete,
37
+ :max_attempts, :max_run_time]
38
+ ARRAY_CONFIGS = [:queues]
39
+
40
+ attr_reader :config
41
+
42
+ def initialize(default = {})
43
+ @config = default
44
+ end
45
+
46
+ SIMPLE_CONFIGS.each do |key|
47
+ define_method(key) do |value|
48
+ @config[key] = value
49
+ end
50
+ end
51
+
52
+ ARRAY_CONFIGS.each do |key|
53
+ define_method(key) do |value|
54
+ @config[key] = Array(value)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,8 @@
1
+ case Delayed::Worker.backend.to_s
2
+ when 'Delayed::Backend::ActiveRecord::Job'
3
+ require_relative 'job_counter/active_record'
4
+ when 'Delayed::Backend::Mongoid::Job'
5
+ require_relative 'job_counter/mongoid'
6
+ else
7
+ raise "Unsupported backend: #{Delayed::Worker.backend}"
8
+ end
@@ -0,0 +1,24 @@
1
+ # JobCounter depends on delayed_job_active_record.
2
+ # See https://github.com/collectiveidea/delayed_job_active_record/blob/master/lib/delayed/backend/active_record.rb
3
+ module Delayed
4
+ class Master
5
+ class JobCounter
6
+ class << self
7
+ def count(config)
8
+ jobs = ready_to_run(config[:max_run_time] || Delayed::Worker::DEFAULT_MAX_RUN_TIME)
9
+ jobs.where!("priority >= ?", config[:min_priority]) if config[:min_priority]
10
+ jobs.where!("priority <= ?", config[:max_priority]) if config[:max_priority]
11
+ jobs.where!(queue: config[:queues]) if config[:queues].any?
12
+ jobs.count
13
+ end
14
+
15
+ private
16
+
17
+ def ready_to_run(max_run_time)
18
+ db_time_now = Delayed::Job.db_time_now
19
+ Delayed::Job.where("(run_at <= ? AND (locked_at IS NULL OR locked_at < ?)) AND failed_at IS NULL", db_time_now, db_time_now - max_run_time)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # JobCounter depends on delayed_job_mongoid.
2
+ # See https://github.com/collectiveidea/delayed_job_mongoid/blob/master/lib/delayed/backend/mongoid.rb
3
+ module Delayed
4
+ class Master
5
+ class JobCounter
6
+ class << self
7
+ def count(config)
8
+ right_now = Delayed::Job.db_time_now
9
+ jobs = reservation_criteria(right_now, config[:max_run_time] || Delayed::Worker::DEFAULT_MAX_RUN_TIME)
10
+ jobs = jobs.gte(priority: config[:min_priority].to_i) if config[:min_priority]
11
+ jobs = jobs.lte(priority: config[:max_priority].to_i) if config[:max_priority]
12
+ jobs = jobs.any_in(queue: config[:queues]) if config[:queues].any?
13
+ jobs.count
14
+ end
15
+
16
+ private
17
+
18
+ def reservation_criteria(right_now, max_run_time)
19
+ criteria = Delayed::Job.where(
20
+ run_at: { '$lte' => right_now },
21
+ failed_at: nil
22
+ ).any_of(
23
+ { locked_at: nil },
24
+ locked_at: { '$lt' => (right_now - max_run_time) }
25
+ )
26
+
27
+ criteria
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ module Delayed
2
+ class Master
3
+ class SignalHandler
4
+ def initialize(master)
5
+ @master = master
6
+ @logger = master.logger
7
+ @worker_infos = master.worker_infos
8
+ end
9
+
10
+ def register
11
+ %w(TERM INT QUIT USR1 USR2).each do |signal|
12
+ trap(signal) do
13
+ Thread.new do
14
+ @logger.info "received #{signal} signal"
15
+ case signal
16
+ when 'TERM', 'INT'
17
+ @master.stop
18
+ when 'QUIT'
19
+ @master.quit
20
+ when 'USR1'
21
+ @master.reopen_files
22
+ when 'USR2'
23
+ @master.restart
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def dispatch(signal)
31
+ @worker_infos.each do |worker_info|
32
+ next unless worker_info.pid
33
+ begin
34
+ Process.kill signal, worker_info.pid
35
+ @logger.info "sent #{signal} signal to worker #{worker_info.pid}"
36
+ rescue
37
+ @logger.error "failed to send #{signal} signal to worker #{worker_info.pid}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ module Delayed
2
+ class Master
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module Delayed
2
+ class Master
3
+ class WorkerInfo
4
+ attr_reader :index, :config
5
+ attr_accessor :pid
6
+
7
+ def initialize(index, config = {})
8
+ @index = index
9
+ @config = config
10
+ end
11
+
12
+ def title
13
+ titles = ["delayed_job.#{@index}"]
14
+ titles << "(#{@config[:queues].join(',')})" if @config[:queues]
15
+ titles.join(' ')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,128 @@
1
+ module Delayed
2
+ class Master
3
+ class WorkerPool
4
+ def initialize(master, config = {})
5
+ @master = master
6
+ @logger = master.logger
7
+ @worker_infos = master.worker_infos
8
+
9
+ @config = OpenStruct.new(config).freeze
10
+ @static_worker_configs, @dynamic_worker_configs = @config.worker_configs.partition { |wc| wc[:control] == :static }
11
+
12
+ @callback = Delayed::Master::Callback.new(config)
13
+ end
14
+
15
+ def init
16
+ @static_worker_configs.each_with_index do |config, i|
17
+ worker_info = Delayed::Master::WorkerInfo.new(i, config)
18
+ @worker_infos << worker_info
19
+ fork_worker(worker_info)
20
+ @logger.info "started worker #{worker_info.pid}"
21
+ end
22
+
23
+ @prepared = true
24
+ debug_worker_infos
25
+ end
26
+
27
+ def monitor_while(&block)
28
+ loop do
29
+ break if block.call
30
+ check_pid
31
+ check_dynamic_worker
32
+ sleep @config.monitor_wait.to_i
33
+ end
34
+ end
35
+
36
+ def prepared?
37
+ @prepared
38
+ end
39
+
40
+ private
41
+
42
+ def fork_worker(worker_info)
43
+ @callback.run(:before_fork, @master, worker_info)
44
+ worker_info.pid = fork do
45
+ @callback.run(:after_fork, @master, worker_info)
46
+ $0 = worker_info.title
47
+ worker = create_new_worker(worker_info)
48
+ worker.start
49
+ end
50
+ end
51
+
52
+ def create_new_worker(worker_info)
53
+ worker = Delayed::Worker.new(worker_info.config)
54
+ [:max_run_time, :max_attempts].each do |key|
55
+ value = worker_info.config[key]
56
+ Delayed::Worker.send("#{key}=", value) if value
57
+ end
58
+ [:max_memory].each do |key|
59
+ value = worker_info.config[key]
60
+ worker.send("#{key}=", value) if value
61
+ end
62
+ worker.master_logger = @logger
63
+ worker
64
+ end
65
+
66
+ def check_pid
67
+ pid = wait_pid
68
+ return unless pid
69
+ worker_info = @worker_infos.detect { |wi| wi.pid == pid }
70
+ return unless worker_info
71
+
72
+ case worker_info.config[:control]
73
+ when :static
74
+ fork_alt_worker(worker_info)
75
+ when :dynamic
76
+ @worker_infos.delete(worker_info)
77
+ end
78
+ end
79
+
80
+ def wait_pid
81
+ Process.waitpid(-1, Process::WNOHANG)
82
+ rescue Errno::ECHILD
83
+ nil
84
+ end
85
+
86
+ def check_dynamic_worker
87
+ @dynamic_worker_configs.each do |worker_config|
88
+ current_count = @worker_infos.count { |wi| wi.config[:queues] == worker_config[:queues] }
89
+ remaining_count = worker_config[:count] - current_count
90
+ if remaining_count > 0 && (job_count = count_job_for_worker(worker_config)) > 0
91
+ [remaining_count, job_count].min.times do
92
+ fork_dynamic_worker(worker_config)
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def count_job_for_worker(worker_config)
99
+ Delayed::Master::JobCounter.count(worker_config)
100
+ end
101
+
102
+ def fork_dynamic_worker(worker_config)
103
+ worker_info = Delayed::Master::WorkerInfo.new(@worker_infos.size, worker_config)
104
+ @worker_infos << worker_info
105
+
106
+ @logger.info "forking dynamic worker..."
107
+ fork_worker(worker_info)
108
+ @logger.info "forked worker #{worker_info.pid}"
109
+
110
+ debug_worker_infos
111
+ end
112
+
113
+ def fork_alt_worker(worker_info)
114
+ @logger.info "worker #{worker_info.pid} seems to be killed, forking alternative worker..."
115
+ fork_worker(worker_info)
116
+ @logger.info "forked worker #{worker_info.pid}"
117
+
118
+ debug_worker_infos
119
+ end
120
+
121
+ def debug_worker_infos
122
+ @worker_infos.each do |worker_info|
123
+ @logger.debug "#{worker_info.pid}: #{worker_info.title}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,16 @@
1
+ module Delayed
2
+ class Util
3
+ class FileReopener
4
+ class << self
5
+ def reopen
6
+ ObjectSpace.each_object(File) do |file|
7
+ next if file.closed? || !file.sync
8
+ file.reopen file.path
9
+ file.sync = true
10
+ file.flush
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'plugins/memory_checker'
2
+ require_relative 'plugins/signal_handler'
3
+ require_relative 'plugins/status_notifier'
4
+
5
+ module Delayed
6
+ class Worker
7
+ attr_accessor :master_logger, :max_memory
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require 'get_process_mem'
2
+
3
+ class Delayed::Plugins::WorkerMemoryChecker < Delayed::Plugin
4
+ callbacks do |lifecycle|
5
+ lifecycle.after(:perform) do |worker, job|
6
+ next unless worker.max_memory
7
+ mem = GetProcessMem.new
8
+ if mem.mb > worker.max_memory
9
+ worker.master_logger.info "shutting down worker #{Process.pid} because it consumes large memory #{mem.mb.to_i} MB..."
10
+ worker.stop
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Delayed::Worker.plugins << Delayed::Plugins::WorkerMemoryChecker
@@ -0,0 +1,27 @@
1
+ class Delayed::Plugins::SignalHandler < Delayed::Plugin
2
+ callbacks do |lifecycle|
3
+ lifecycle.before(:execute) do |worker|
4
+ worker.instance_eval do
5
+ trap('USR1') do
6
+ Thread.new do
7
+ master_logger.info "reopening files..."
8
+ Delayed::Util::FileReopener.reopen
9
+ master_logger.info "reopened"
10
+ end
11
+ end
12
+ trap('USR2') do
13
+ Thread.new do
14
+ $0 = "#{$0} [OLD]"
15
+ master_logger.info "shutting down worker #{Process.pid}..."
16
+ stop
17
+ end
18
+ end
19
+ end
20
+ end
21
+ lifecycle.after(:execute) do |worker|
22
+ worker.master_logger.info "shut down worker #{Process.pid}"
23
+ end
24
+ end
25
+ end
26
+
27
+ Delayed::Worker.plugins << Delayed::Plugins::SignalHandler
@@ -0,0 +1,13 @@
1
+ class Delayed::Plugins::StatusNotifier < Delayed::Plugin
2
+ callbacks do |lifecycle|
3
+ lifecycle.around(:perform) do |worker, job, &block|
4
+ title = $0
5
+ $0 = "#{title} [BUSY]"
6
+ ret = block.call
7
+ $0 = title
8
+ ret
9
+ end
10
+ end
11
+ end
12
+
13
+ Delayed::Worker.plugins << Delayed::Plugins::StatusNotifier
@@ -0,0 +1,14 @@
1
+ require 'rails/generators'
2
+
3
+ class DelayedJobMasterGenerator < Rails::Generators::Base
4
+ source_root File.join(File.dirname(__FILE__), 'templates')
5
+
6
+ def create_script_file
7
+ template 'script', 'bin/delayed_job_master'
8
+ chmod 'bin/delayed_job_master', 0o755
9
+ end
10
+
11
+ def create_config_file
12
+ template 'config.rb', 'config/delayed_job_master.rb'
13
+ end
14
+ end
@@ -0,0 +1,52 @@
1
+ # working directory
2
+ working_directory Dir.pwd
3
+
4
+ # monitor wait time in second
5
+ monitor_wait 5
6
+
7
+ # path to pid file
8
+ pid_file "#{Dir.pwd}/tmp/pids/delayed_job_master.pid"
9
+
10
+ # path to log file
11
+ log_file "#{Dir.pwd}/log/delayed_job_master.log"
12
+
13
+ # log level
14
+ log_level :info
15
+
16
+ # worker1
17
+ add_worker do |worker|
18
+ # queue name for the worker
19
+ worker.queues %w(queue1)
20
+
21
+ # worker control (:static or :dynamic)
22
+ worker.control :static
23
+
24
+ # worker count
25
+ worker.count 1
26
+
27
+ # max memory in MB
28
+ worker.max_memory 300
29
+
30
+ # configs below are same as delayed_job, see https://github.com/collectiveidea/delayed_job
31
+ # worker.sleep_delay 5
32
+ # worker.read_ahead 5
33
+ # worker.max_attempts 25
34
+ # worker.max_run_time 4.hours
35
+ # worker.min_priority 1
36
+ # worker.max_priority 10
37
+ end
38
+
39
+ # worker2
40
+ add_worker do |worker|
41
+ worker.queues %w(queue2)
42
+ worker.control :dynamic
43
+ worker.count 2
44
+ end
45
+
46
+ before_fork do |master, worker_info|
47
+ Delayed::Worker.before_fork if defined?(Delayed::Worker)
48
+ end
49
+
50
+ after_fork do |master, worker_info|
51
+ Delayed::Worker.after_fork if defined?(Delayed::Worker)
52
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'delayed/master'
5
+ Delayed::Master.new(ARGV).run
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: delayed_job_master
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Yoshikazu Kaneta
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: delayed_job
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: get_process_mem
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
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: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
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
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: delayed_job_mongoid
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '2.3'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '2.3'
125
+ - !ruby/object:Gem::Dependency
126
+ name: delayed_job_active_record
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '4.1'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '4.1'
139
+ description: A simple delayed_job master process to control multiple workers
140
+ email:
141
+ - kaneta@sitebridge.co.jp
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".rspec"
148
+ - ".travis.yml"
149
+ - CODE_OF_CONDUCT.md
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - delayed_job_master.gemspec
155
+ - gemfiles/active_record.gemfile
156
+ - gemfiles/mongoid.gemfile
157
+ - lib/delayed/master.rb
158
+ - lib/delayed/master/callback.rb
159
+ - lib/delayed/master/command.rb
160
+ - lib/delayed/master/dsl.rb
161
+ - lib/delayed/master/job_counter.rb
162
+ - lib/delayed/master/job_counter/active_record.rb
163
+ - lib/delayed/master/job_counter/mongoid.rb
164
+ - lib/delayed/master/signal_handler.rb
165
+ - lib/delayed/master/version.rb
166
+ - lib/delayed/master/worker_info.rb
167
+ - lib/delayed/master/worker_pool.rb
168
+ - lib/delayed/util/file_reopener.rb
169
+ - lib/delayed/worker/extension.rb
170
+ - lib/delayed/worker/plugins/memory_checker.rb
171
+ - lib/delayed/worker/plugins/signal_handler.rb
172
+ - lib/delayed/worker/plugins/status_notifier.rb
173
+ - lib/generators/delayed_job_master_generator.rb
174
+ - lib/generators/templates/config.rb
175
+ - lib/generators/templates/script
176
+ homepage: https://github.com/kanety/delayed_job_master
177
+ licenses:
178
+ - MIT
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '2.3'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.5.2.1
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: A simple delayed_job master process to control multiple workers
200
+ test_files: []