delayed_job_master 1.2.0 → 2.0.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -9
  3. data/.travis.yml +13 -6
  4. data/CHANGELOG.md +7 -0
  5. data/README.md +12 -16
  6. data/delayed_job_master.gemspec +0 -2
  7. data/gemfiles/{mongoid.gemfile → rails50.gemfile} +2 -2
  8. data/gemfiles/rails51.gemfile +7 -0
  9. data/gemfiles/{active_record.gemfile → rails52.gemfile} +0 -0
  10. data/gemfiles/rails60.gemfile +6 -0
  11. data/lib/delayed/master.rb +34 -41
  12. data/lib/delayed/master/config.rb +16 -5
  13. data/lib/delayed/master/forker.rb +49 -0
  14. data/lib/delayed/master/job_checker.rb +98 -0
  15. data/lib/delayed/master/job_counter.rb +25 -7
  16. data/lib/delayed/master/monitoring.rb +59 -0
  17. data/lib/delayed/master/plugins/memory_checker.rb +24 -0
  18. data/lib/delayed/master/plugins/signal_handler.rb +31 -0
  19. data/lib/delayed/master/plugins/status_notifier.rb +17 -0
  20. data/lib/delayed/master/signaler.rb +41 -0
  21. data/lib/delayed/master/version.rb +1 -1
  22. data/lib/delayed/master/worker.rb +18 -8
  23. data/lib/delayed/master/worker_extension.rb +19 -0
  24. data/lib/delayed_job_master.rb +1 -0
  25. data/lib/generators/delayed_job_master/templates/config.rb +0 -4
  26. data/lib/generators/delayed_job_master/templates/script +3 -2
  27. metadata +15 -27
  28. data/lib/delayed/master/callback.rb +0 -13
  29. data/lib/delayed/master/job_counter/active_record.rb +0 -24
  30. data/lib/delayed/master/job_counter/mongoid.rb +0 -32
  31. data/lib/delayed/master/signal_handler.rb +0 -47
  32. data/lib/delayed/master/worker_pool.rb +0 -141
  33. data/lib/delayed/worker/extension.rb +0 -9
  34. data/lib/delayed/worker/plugins/memory_checker.rb +0 -20
  35. data/lib/delayed/worker/plugins/signal_handler.rb +0 -31
  36. data/lib/delayed/worker/plugins/status_notifier.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 548efefc42b3ecd02bcb85da66da4a8e001376057a54ee7a636adf5177346923
4
- data.tar.gz: 6c83c6a641d334f262bb191f68649c90cfa083ede76ace8e960c9b7a1ce99b9d
3
+ metadata.gz: 90f6ec1ebe00243f5d69adf6a92a6e2716772b7a9af68b7301220bae2cfc55d7
4
+ data.tar.gz: 30db6bc4a8183622514a9e4fcad3f5130e3061cff0bb973b834e5e0c9db5ffb0
5
5
  SHA512:
6
- metadata.gz: 81b40df17053df90e59c84c3e53bbc854ba0e623cc8185811725673867532f68525a65a86d732914f117f56788ebee607466d469e546f3be4332e2935ff17b60
7
- data.tar.gz: c77e25e5aa04b6f4c11c3e7c630c3c8dcd45f691c5088cac3a8f07bb6980317419c1b5a45149c2154c4982991593f67f06bebef32dc27915e4d4958a05590981
6
+ metadata.gz: 6958a025ee232142b6a3049b25ea62075a08dea1446ddc0b29ee3f26d5ef1dd9351e6df46a2ad2a8d8068238e44a84bce6140639ff62aa8a0c8f2ad0ba43c2fe
7
+ data.tar.gz: c863131bde412c128fc3f144fd5ba1bb472cd8850a0c31a32d0500e043572e389ce7184c6a98a0b57de31bd5c434601c5641c64ea00b7a7db1a41bd11095f4dc
data/.gitignore CHANGED
@@ -1,19 +1,11 @@
1
1
  /.bundle/
2
- /.yardoc
3
2
  /.project
4
3
  /Gemfile.lock
5
- /gemfiles/.bundle
6
- /gemfiles/vendor
7
4
  /gemfiles/*gemfile.lock
8
- /_yardoc/
9
- /bin/
10
5
  /coverage/
11
- /doc/
12
6
  /pkg/
13
- /log/
14
- /tmp/
15
7
  /spec/reports/
16
8
  /spec/**/db/*.sqlite3
17
9
  /spec/**/log/*.log
18
- /spec/**/tmp/pids/*.pid
10
+ /spec/**/tmp/*
19
11
  /vendor/
@@ -1,16 +1,23 @@
1
1
  language: ruby
2
- services:
3
- - mongodb
4
2
  rvm:
5
3
  - 2.3
6
4
  - 2.4
7
5
  - 2.5
8
6
  - 2.6
9
7
  gemfile:
10
- - gemfiles/active_record.gemfile
11
- - gemfiles/mongoid.gemfile
8
+ - gemfiles/rails50.gemfile
9
+ - gemfiles/rails51.gemfile
10
+ - gemfiles/rails52.gemfile
11
+ - gemfiles/rails60.gemfile
12
+ matrix:
13
+ exclude:
14
+ - rvm: 2.3
15
+ gemfile: gemfiles/rails60.gemfile
16
+ - rvm: 2.4
17
+ gemfile: gemfiles/rails60.gemfile
12
18
  before_script:
13
19
  - cd spec/dummy
14
- - bundle exec rake db:migrate RAILS_ENV=test
20
+ - bundle exec rake dbs:migrate RAILS_ENV=test
15
21
  - cd ../..
16
- script: bundle exec rspec
22
+ script:
23
+ - bundle exec rspec
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.0.0
4
+
5
+ * Support multiple databases.
6
+ * Logging memory usage.
7
+ * Remove static worker support.
8
+ * Remove mongoid support.
9
+
3
10
  ## 1.2.0
4
11
 
5
12
  * Add destroy_failed_jobs for worker.
data/README.md CHANGED
@@ -5,11 +5,9 @@ A simple delayed_job master process to control multiple workers.
5
5
  ## Features
6
6
 
7
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.
8
+ * Monitor job queues and fork new workers on demand.
9
+ * Trap signals to restart / reopen log files.
10
+ * Support multiple databases.
13
11
 
14
12
  ## Dependencies
15
13
 
@@ -19,7 +17,6 @@ A simple delayed_job master process to control multiple workers.
19
17
  ## Supported delayed_job backends
20
18
 
21
19
  * delayed_job_active_record 4.1
22
- * delayed_job_mongoid 2.3
23
20
 
24
21
  ## Installation
25
22
 
@@ -33,11 +30,11 @@ And then execute:
33
30
 
34
31
  $ bundle
35
32
 
36
- Create config files:
33
+ ## Generate files
37
34
 
38
- $ rails generate delayed_job_master:config
35
+ Generate `bin/delayed_job_master` and `config/delayed_job_master.rb`:
39
36
 
40
- This command creates `bin/delayed_job_master` and `config/delayed_job_master.rb`.
37
+ $ rails generate delayed_job_master:config
41
38
 
42
39
  ## Configuration
43
40
 
@@ -59,14 +56,14 @@ log_file "#{Dir.pwd}/log/delayed_job_master.log"
59
56
  # log level
60
57
  log_level :info
61
58
 
59
+ # databases for checking queued jobs if you have multiple databases
60
+ # databases [:production]
61
+
62
62
  # worker1
63
63
  add_worker do |worker|
64
64
  # queue name for the worker
65
65
  worker.queues %w(queue1)
66
66
 
67
- # worker control (:static or :dynamic)
68
- worker.control :static
69
-
70
67
  # worker count
71
68
  worker.count 1
72
69
 
@@ -86,7 +83,6 @@ end
86
83
  # worker2
87
84
  add_worker do |worker|
88
85
  worker.queues %w(queue2)
89
- worker.control :dynamic
90
86
  worker.count 2
91
87
  end
92
88
 
@@ -146,19 +142,19 @@ Workers handle each signal as follows:
146
142
 
147
143
  ```
148
144
  $ ps aux
149
- ... delayed_job.0 (queue1) [BUSY] # BUSY process is currently proceeding some jobs
145
+ ... delayed_job.0: worker[0] (queue1) [BUSY] # BUSY process is currently proceeding some jobs
150
146
  ```
151
147
 
152
148
  After graceful restart, you may find OLD process.
153
149
 
154
150
  ```
155
151
  $ ps aux
156
- ... delayed_job.0 (queue1) [BUSY] [OLD] # OLD process will stop after finishing current jobs.
152
+ ... delayed_job.0: worker[0] (queue1) [BUSY] [OLD] # OLD process will stop after finishing current jobs.
157
153
  ```
158
154
 
159
155
  ## Contributing
160
156
 
161
- 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.
157
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kanety/delayed_job_master.
162
158
 
163
159
  ## License
164
160
 
@@ -8,7 +8,6 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Delayed::Master::VERSION
9
9
  spec.authors = ["Yoshikazu Kaneta"]
10
10
  spec.email = ["kaneta@sitebridge.co.jp"]
11
-
12
11
  spec.summary = %q{A simple delayed_job master process to control multiple workers}
13
12
  spec.description = %q{A simple delayed_job master process to control multiple workers}
14
13
  spec.homepage = "https://github.com/kanety/delayed_job_master"
@@ -29,6 +28,5 @@ Gem::Specification.new do |spec|
29
28
  spec.add_development_dependency "rspec-rails"
30
29
  spec.add_development_dependency "simplecov"
31
30
  spec.add_development_dependency "sqlite3"
32
- spec.add_development_dependency "delayed_job_mongoid", ">= 2.3"
33
31
  spec.add_development_dependency "delayed_job_active_record", ">= 4.1"
34
32
  end
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "delayed_job_mongoid", "~> 2.3.0"
4
- gem "rails", "~> 5.2.0"
3
+ gem "delayed_job_active_record", "~> 4.1.0"
4
+ gem "rails", "~> 5.0.0"
5
5
  gem "sqlite3", "~> 1.3.6"
6
6
 
7
7
  gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "delayed_job_active_record", "~> 4.1.0"
4
+ gem "rails", "~> 5.1.0"
5
+ gem "sqlite3", "~> 1.3.6"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "delayed_job_active_record", "~> 4.1.0"
4
+ gem "rails", "~> 6.0.0"
5
+
6
+ gemspec path: "../"
@@ -2,10 +2,9 @@ require 'fileutils'
2
2
  require 'logger'
3
3
  require_relative 'master/version'
4
4
  require_relative 'master/command'
5
- require_relative 'master/callback'
6
5
  require_relative 'master/worker'
7
- require_relative 'master/worker_pool'
8
- require_relative 'master/signal_handler'
6
+ require_relative 'master/monitoring'
7
+ require_relative 'master/signaler'
9
8
  require_relative 'master/util/file_reopener'
10
9
 
11
10
  module Delayed
@@ -17,79 +16,74 @@ module Delayed
17
16
  @logger = setup_logger(@config.log_file, @config.log_level)
18
17
  @workers = []
19
18
 
20
- @signal_handler = SignalHandler.new(self)
21
- @worker_pool = WorkerPool.new(self, @config)
19
+ @signaler = Signaler.new(self)
20
+ @monitoring = Monitoring.new(self)
22
21
  end
23
22
 
24
23
  def run
25
- load_app
26
- show_config
24
+ print_config
27
25
  daemonize if @config.daemon
28
26
 
29
- create_pid_file
30
- @logger.info "started master #{Process.pid}"
27
+ @logger.info "started master #{Process.pid}".tap { |msg| puts msg }
31
28
 
32
- @signal_handler.register
33
- @worker_pool.init
34
- @worker_pool.monitor_while { stop? }
29
+ handle_pid_file do
30
+ @signaler.register
31
+ @prepared = true
32
+ @monitoring.monitor_while { stop? }
33
+ end
35
34
 
36
- remove_pid_file
37
35
  @logger.info "shut down master"
38
36
  end
39
37
 
40
- def load_app
41
- require File.join(@config.working_directory, 'config', 'environment')
42
- require_relative 'master/job_counter'
43
- require_relative 'worker/extension'
44
- end
45
-
46
38
  def prepared?
47
- @worker_pool.prepared?
39
+ @prepared
48
40
  end
49
41
 
50
42
  def quit
51
- kill_workers
43
+ @signaler.dispatch(:KILL)
52
44
  @stop = true
53
45
  end
54
46
 
55
47
  def stop
56
- @signal_handler.dispatch('TERM')
48
+ @signaler.dispatch(:TERM)
57
49
  @stop = true
58
50
  end
59
51
 
60
52
  def stop?
61
- @stop
53
+ @stop == true
62
54
  end
63
55
 
64
56
  def reopen_files
65
- @signal_handler.dispatch('USR1')
57
+ @signaler.dispatch(:USR1)
66
58
  @logger.info "reopening files..."
67
59
  Util::FileReopener.reopen
68
60
  @logger.info "reopened"
69
61
  end
70
62
 
71
63
  def restart
72
- @signal_handler.dispatch('USR2')
64
+ @signaler.dispatch(:USR2)
73
65
  @logger.info "restarting master..."
74
66
  exec(*([$0] + ARGV))
75
67
  end
76
68
 
77
- def kill_workers
78
- @signal_handler.dispatch('KILL')
79
- end
80
-
81
- def wait_workers
82
- Process.waitall
83
- end
84
-
85
69
  private
86
70
 
87
71
  def setup_logger(log_file, log_level)
88
- FileUtils.mkdir_p(File.dirname(log_file))
72
+ FileUtils.mkdir_p(File.dirname(log_file)) if log_file.is_a?(String)
89
73
  logger = Logger.new(log_file)
90
74
  logger.level = log_level
91
75
  logger
92
76
  end
77
+
78
+ def daemonize
79
+ Process.daemon(true)
80
+ end
81
+
82
+ def handle_pid_file
83
+ create_pid_file
84
+ yield
85
+ remove_pid_file
86
+ end
93
87
 
94
88
  def create_pid_file
95
89
  FileUtils.mkdir_p(File.dirname(@config.pid_file))
@@ -100,13 +94,12 @@ module Delayed
100
94
  File.delete(@config.pid_file) if File.exist?(@config.pid_file)
101
95
  end
102
96
 
103
- def daemonize
104
- Process.daemon(true)
105
- end
106
-
107
- def show_config
108
- @config.workers.each do |setting|
109
- puts "#{setting.count} worker for '#{setting.queues.join(',')}' under #{setting.control} control"
97
+ def print_config
98
+ @logger.info "databases: #{@config.databases.join(', ')}" if @config.databases
99
+ @config.worker_settings.each do |setting|
100
+ message = "worker[#{setting.id}]: #{setting.count} processes"
101
+ message << " (#{setting.queues.join(', ')})" if setting.queues.respond_to?(:join)
102
+ @logger.info message
110
103
  end
111
104
  end
112
105
  end
@@ -1,7 +1,7 @@
1
1
  module Delayed
2
2
  class Master
3
3
  class Config
4
- SIMPLE_CONFIGS = [:working_directory, :log_file, :log_level, :pid_file, :monitor_wait, :daemon]
4
+ SIMPLE_CONFIGS = [:working_directory, :log_file, :log_level, :pid_file, :monitor_wait, :daemon, :databases]
5
5
  CALLBACK_CONFIGS = [:before_fork, :after_fork, :before_monitor, :after_monitor]
6
6
 
7
7
  attr_reader :data, :workers
@@ -12,14 +12,17 @@ module Delayed
12
12
  read(file) if file
13
13
  end
14
14
 
15
+ def worker_settings
16
+ @workers
17
+ end
18
+
15
19
  def read(file)
16
- instance_eval(File.read(file))
20
+ instance_eval(File.read(file), file)
17
21
  end
18
22
 
19
23
  def add_worker
20
- worker = WorkerSetting.new(control: :static, count: 1)
24
+ worker = WorkerSetting.new(id: @workers.size, count: 1, exit_on_complete: true)
21
25
  yield worker
22
- worker.exit_on_complete(true) if worker.control == :dynamic
23
26
  @workers << worker
24
27
  end
25
28
 
@@ -27,6 +30,10 @@ module Delayed
27
30
  @data.select { |k, _| CALLBACK_CONFIGS.include?(k) }
28
31
  end
29
32
 
33
+ def run_callback(key, *args)
34
+ @data[key].call(*args)
35
+ end
36
+
30
37
  SIMPLE_CONFIGS.each do |key|
31
38
  define_method(key) do |value = nil|
32
39
  if value
@@ -48,7 +55,7 @@ module Delayed
48
55
  end
49
56
 
50
57
  class WorkerSetting
51
- SIMPLE_CONFIGS = [:control, :count, :max_memory,
58
+ SIMPLE_CONFIGS = [:id, :count, :max_memory,
52
59
  :min_priority, :max_priority, :sleep_delay, :read_ahead, :exit_on_complete,
53
60
  :max_attempts, :max_run_time, :destroy_failed_jobs]
54
61
  ARRAY_CONFIGS = [:queues]
@@ -59,6 +66,10 @@ module Delayed
59
66
  @data = default
60
67
  end
61
68
 
69
+ def control(value = nil)
70
+ puts "DEPRECATION WARNING: deprecated control setting is called from #{caller(1, 1).first}. Remove it from your config file."
71
+ end
72
+
62
73
  SIMPLE_CONFIGS.each do |key|
63
74
  define_method(key) do |value = nil|
64
75
  if value
@@ -0,0 +1,49 @@
1
+ module Delayed
2
+ class Master
3
+ class Forker
4
+ def initialize(master)
5
+ @master = master
6
+ @config = master.config
7
+ end
8
+
9
+ def new_worker(worker)
10
+ @master.logger.info "forking #{worker.name}..."
11
+ fork_worker(worker)
12
+ @master.logger.info "forked #{worker.name} with pid #{worker.pid}"
13
+
14
+ @master.workers << worker
15
+ end
16
+
17
+ private
18
+
19
+ def fork_worker(worker)
20
+ @config.run_callback(:before_fork, @master, worker)
21
+ worker.pid = fork do
22
+ worker.pid = Process.pid
23
+ worker.instance = create_instance(worker)
24
+ @config.run_callback(:after_fork, @master, worker)
25
+ $0 = worker.process_title
26
+ worker.instance.start
27
+ end
28
+ end
29
+
30
+ def create_instance(worker)
31
+ require_relative 'worker_extension'
32
+
33
+ instance = Delayed::Worker.new(worker.setting.data)
34
+ [:max_run_time, :max_attempts, :destroy_failed_jobs].each do |key|
35
+ if (value = worker.setting.send(key))
36
+ Delayed::Worker.send("#{key}=", value)
37
+ end
38
+ end
39
+ [:max_memory].each do |key|
40
+ if (value = worker.setting.send(key))
41
+ instance.send("#{key}=", value)
42
+ end
43
+ end
44
+ instance.master_logger = @master.logger
45
+ instance
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,98 @@
1
+ require_relative 'job_counter'
2
+
3
+ module Delayed
4
+ class Master
5
+ class JobChecker
6
+ def initialize(master)
7
+ @master = master
8
+ @config = master.config
9
+ @spec_names = target_spec_names
10
+
11
+ define_models
12
+ extend_after_fork_callback
13
+ end
14
+
15
+ def check
16
+ workers = []
17
+ mon = Monitor.new
18
+
19
+ threads = @spec_names.map do |spec_name|
20
+ Thread.new(spec_name) do |spec_name|
21
+ find_jobs_in_db(spec_name) do |setting|
22
+ mon.synchronize do
23
+ workers << Worker.new(index: @master.workers.size + workers.size, database: spec_name, setting: setting)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ threads.each(&:join)
30
+
31
+ workers
32
+ end
33
+
34
+ private
35
+
36
+ def define_models
37
+ @spec_names.each do |spec_name|
38
+ klass = Class.new(Delayed::Job)
39
+ klass_name = "DelayedJob#{spec_name.capitalize}"
40
+ unless Delayed::Master.const_defined?(klass_name)
41
+ Delayed::Master.const_set(klass_name, klass)
42
+ Delayed::Master.const_get(klass_name).establish_connection(spec_name)
43
+ end
44
+ end
45
+ end
46
+
47
+ def model_for(spec_name)
48
+ Delayed::Master.const_get("DelayedJob#{spec_name.capitalize}")
49
+ end
50
+
51
+ def extend_after_fork_callback
52
+ prc = @config.after_fork
53
+ @config.after_fork do |master, worker|
54
+ prc.call(master, worker)
55
+ if worker.database && ActiveRecord::Base.connection_pool.spec.name != worker.database.to_s
56
+ ActiveRecord::Base.establish_connection(worker.database)
57
+ end
58
+ end
59
+ end
60
+
61
+ def target_spec_names
62
+ if @config.databases.nil? || @config.databases.empty?
63
+ load_spec_names.select { |spec_name| has_delayed_job_table?(spec_name) }
64
+ else
65
+ @config.databases
66
+ end
67
+ end
68
+
69
+ def load_spec_names
70
+ if Rails::VERSION::MAJOR >= 6
71
+ configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
72
+ configs.reject(&:replica?).map { |c| c.spec_name.to_sym }
73
+ else
74
+ [Rails.env.to_sym]
75
+ end
76
+ end
77
+
78
+ def has_delayed_job_table?(spec_name)
79
+ ActiveRecord::Base.establish_connection(spec_name)
80
+ ActiveRecord::Base.connection.tables.include?('delayed_jobs')
81
+ end
82
+
83
+ def find_jobs_in_db(spec_name)
84
+ counter = JobCounter.new(model_for(spec_name))
85
+
86
+ @config.worker_settings.each do |setting|
87
+ count = @master.workers.count { |worker| worker.setting.queues == setting.queues }
88
+ slot = setting.count - count
89
+ if slot > 0 && (job_count = counter.count(setting)) > 0
90
+ [slot, job_count].min.times do
91
+ yield setting
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,8 +1,26 @@
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}"
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
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+
10
+ def count(setting)
11
+ jobs = ready_to_run(setting.max_run_time || Delayed::Worker::DEFAULT_MAX_RUN_TIME)
12
+ jobs.where!("priority >= ?", setting.min_priority) if setting.min_priority
13
+ jobs.where!("priority <= ?", setting.max_priority) if setting.max_priority
14
+ jobs.where!(queue: setting.queues) if setting.queues.any?
15
+ jobs.count
16
+ end
17
+
18
+ private
19
+
20
+ def ready_to_run(max_run_time)
21
+ db_time_now = @klass.db_time_now
22
+ @klass.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)
23
+ end
24
+ end
25
+ end
8
26
  end
@@ -0,0 +1,59 @@
1
+ require_relative 'forker'
2
+ require_relative 'job_checker' if defined?(Delayed::Backend::ActiveRecord)
3
+
4
+ module Delayed
5
+ class Master
6
+ class Monitoring
7
+ def initialize(master)
8
+ @master = master
9
+ @config = master.config
10
+ @forker = Forker.new(master)
11
+ @job_checker = JobChecker.new(master)
12
+ end
13
+
14
+ def monitor_while(&block)
15
+ loop do
16
+ break if block.call
17
+ monitor do
18
+ check_terminated
19
+ check_queued_jobs
20
+ end
21
+ sleep @config.monitor_wait.to_i
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def monitor
28
+ @config.run_callback(:before_monitor, @master)
29
+ yield
30
+ @config.run_callback(:after_monitor, @master)
31
+ rescue Exception => e
32
+ @master.logger.warn "#{e.class}: #{e.message} at #{__FILE__}: #{__LINE__}"
33
+ end
34
+
35
+ def check_terminated
36
+ if (pid = terminated_pid)
37
+ @master.logger.debug "found terminated pid: #{pid}"
38
+ @master.workers.reject! { |worker| worker.pid == pid }
39
+ end
40
+ end
41
+
42
+ def terminated_pid
43
+ Process.waitpid(-1, Process::WNOHANG)
44
+ rescue Errno::ECHILD
45
+ nil
46
+ end
47
+
48
+ def check_queued_jobs
49
+ @master.logger.debug "checking jobs..."
50
+
51
+ new_workers = @job_checker.check
52
+ new_workers.each do |worker|
53
+ @master.logger.info "found jobs for #{worker.info}"
54
+ @forker.new_worker(worker)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ require 'get_process_mem'
2
+
3
+ module Delayed
4
+ class Master
5
+ module Plugins
6
+ class MemoryChecker < Delayed::Plugin
7
+ callbacks do |lifecycle|
8
+ lifecycle.before(:perform) do |worker, job|
9
+ mem = GetProcessMem.new
10
+ worker.master_logger.info "performing #{job.name}, memory: #{mem.mb.to_i} MB"
11
+ end
12
+ lifecycle.after(:perform) do |worker, job|
13
+ mem = GetProcessMem.new
14
+ worker.master_logger.info "performed #{job.name}, memory: #{mem.mb.to_i} MB"
15
+ if worker.max_memory && mem.mb > worker.max_memory
16
+ worker.master_logger.info "shutting down worker #{Process.pid} because it consumes large memory..."
17
+ worker.stop
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module Delayed
2
+ class Master
3
+ module Plugins
4
+ class SignalHandler < Delayed::Plugin
5
+ callbacks do |lifecycle|
6
+ lifecycle.before(:execute) do |worker|
7
+ worker.instance_eval do
8
+ trap(:USR1) do
9
+ Thread.new do
10
+ master_logger.info "reopening files..."
11
+ Delayed::Master::Util::FileReopener.reopen
12
+ master_logger.info "reopened"
13
+ end
14
+ end
15
+ trap(:USR2) do
16
+ Thread.new do
17
+ $0 = "#{$0} [OLD]"
18
+ master_logger.info "shutting down worker #{Process.pid}..."
19
+ stop
20
+ end
21
+ end
22
+ end
23
+ end
24
+ lifecycle.after(:execute) do |worker|
25
+ worker.master_logger.info "shut down worker #{Process.pid}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Delayed
2
+ class Master
3
+ module Plugins
4
+ class StatusNotifier < Delayed::Plugin
5
+ callbacks do |lifecycle|
6
+ lifecycle.around(:perform) do |worker, job, &block|
7
+ title = $0
8
+ $0 = "#{title} [BUSY]"
9
+ ret = block.call
10
+ $0 = title
11
+ ret
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Delayed
2
+ class Master
3
+ class Signaler
4
+ def initialize(master)
5
+ @master = master
6
+ end
7
+
8
+ def register
9
+ signals = [[:TERM, :stop], [:INT, :stop], [:QUIT, :quit], [:USR1, :reopen_files], [:USR2, :restart]]
10
+ signals.each do |signal, method|
11
+ register_signal(signal, method)
12
+ end
13
+ end
14
+
15
+ def dispatch(signal)
16
+ @master.workers.each do |worker|
17
+ next unless worker.pid
18
+ dispatch_to(signal, worker.pid)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def register_signal(signal, method)
25
+ trap(signal) do
26
+ Thread.new do
27
+ @master.logger.info "received #{signal} signal"
28
+ @master.public_send(method)
29
+ end
30
+ end
31
+ end
32
+
33
+ def dispatch_to(signal, pid)
34
+ Process.kill(signal, pid)
35
+ @master.logger.info "sent #{signal} signal to worker #{pid}"
36
+ rescue
37
+ @master.logger.error "failed to send #{signal} signal to worker #{pid}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  module Delayed
2
2
  class Master
3
- VERSION = "1.2.0"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -1,18 +1,28 @@
1
1
  module Delayed
2
2
  class Master
3
3
  class Worker
4
- attr_reader :index, :setting
4
+ attr_accessor :index, :setting, :database
5
5
  attr_accessor :pid, :instance
6
6
 
7
- def initialize(index, setting)
8
- @index = index
9
- @setting = setting
7
+ def initialize(attrs = {})
8
+ attrs.each do |k, v|
9
+ send("#{k}=", v)
10
+ end
10
11
  end
11
12
 
12
- def title
13
- titles = ["delayed_job.#{@index}"]
14
- titles << "(#{@setting.queues.join(',')})" if @setting.queues
15
- titles.join(' ')
13
+ def name
14
+ "worker[#{@setting.id}]"
15
+ end
16
+
17
+ def info
18
+ str = name
19
+ str << " @#{@database}" if @database
20
+ str << " (#{@setting.queues.join(', ')})" if @setting.queues.respond_to?(:join)
21
+ str
22
+ end
23
+
24
+ def process_title
25
+ "delayed_job.#{@index}: #{info}"
16
26
  end
17
27
  end
18
28
  end
@@ -0,0 +1,19 @@
1
+ module Delayed
2
+ class Worker
3
+ attr_accessor :master_logger, :max_memory
4
+ end
5
+ end
6
+
7
+ require_relative 'plugins/memory_checker'
8
+ require_relative 'plugins/signal_handler'
9
+ require_relative 'plugins/status_notifier'
10
+
11
+ [
12
+ Delayed::Master::Plugins::MemoryChecker,
13
+ Delayed::Master::Plugins::SignalHandler,
14
+ Delayed::Master::Plugins::StatusNotifier
15
+ ].each do |plugin|
16
+ unless Delayed::Worker.plugins.include?(plugin)
17
+ Delayed::Worker.plugins << plugin
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ require 'delayed/master'
@@ -18,9 +18,6 @@ add_worker do |worker|
18
18
  # queue name for the worker
19
19
  worker.queues %w(queue1)
20
20
 
21
- # worker control (:static or :dynamic)
22
- worker.control :static
23
-
24
21
  # worker count
25
22
  worker.count 1
26
23
 
@@ -40,7 +37,6 @@ end
40
37
  # worker2
41
38
  add_worker do |worker|
42
39
  worker.queues %w(queue2)
43
- worker.control :dynamic
44
40
  worker.count 2
45
41
  end
46
42
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/setup'
4
- require 'delayed/master'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4
+ require 'delayed_job_master'
5
+
5
6
  Delayed::Master.new(ARGV).run
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: delayed_job_master
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshikazu Kaneta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-01 00:00:00.000000000 Z
11
+ date: 2019-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: delayed_job
@@ -108,20 +108,6 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
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
111
  - !ruby/object:Gem::Dependency
126
112
  name: delayed_job_active_record
127
113
  requirement: !ruby/object:Gem::Requirement
@@ -153,24 +139,26 @@ files:
153
139
  - README.md
154
140
  - Rakefile
155
141
  - delayed_job_master.gemspec
156
- - gemfiles/active_record.gemfile
157
- - gemfiles/mongoid.gemfile
142
+ - gemfiles/rails50.gemfile
143
+ - gemfiles/rails51.gemfile
144
+ - gemfiles/rails52.gemfile
145
+ - gemfiles/rails60.gemfile
158
146
  - lib/delayed/master.rb
159
- - lib/delayed/master/callback.rb
160
147
  - lib/delayed/master/command.rb
161
148
  - lib/delayed/master/config.rb
149
+ - lib/delayed/master/forker.rb
150
+ - lib/delayed/master/job_checker.rb
162
151
  - lib/delayed/master/job_counter.rb
163
- - lib/delayed/master/job_counter/active_record.rb
164
- - lib/delayed/master/job_counter/mongoid.rb
165
- - lib/delayed/master/signal_handler.rb
152
+ - lib/delayed/master/monitoring.rb
153
+ - lib/delayed/master/plugins/memory_checker.rb
154
+ - lib/delayed/master/plugins/signal_handler.rb
155
+ - lib/delayed/master/plugins/status_notifier.rb
156
+ - lib/delayed/master/signaler.rb
166
157
  - lib/delayed/master/util/file_reopener.rb
167
158
  - lib/delayed/master/version.rb
168
159
  - lib/delayed/master/worker.rb
169
- - lib/delayed/master/worker_pool.rb
170
- - lib/delayed/worker/extension.rb
171
- - lib/delayed/worker/plugins/memory_checker.rb
172
- - lib/delayed/worker/plugins/signal_handler.rb
173
- - lib/delayed/worker/plugins/status_notifier.rb
160
+ - lib/delayed/master/worker_extension.rb
161
+ - lib/delayed_job_master.rb
174
162
  - lib/generators/delayed_job_master/config_generator.rb
175
163
  - lib/generators/delayed_job_master/templates/config.rb
176
164
  - lib/generators/delayed_job_master/templates/script
@@ -1,13 +0,0 @@
1
- module Delayed
2
- class Master
3
- class Callback
4
- def initialize(config)
5
- @callbacks = config.callbacks
6
- end
7
-
8
- def run(name, *args)
9
- @callbacks[name].call(*args) if @callbacks[name]
10
- end
11
- end
12
- end
13
- end
@@ -1,24 +0,0 @@
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(setting)
8
- jobs = ready_to_run(setting.max_run_time || Delayed::Worker::DEFAULT_MAX_RUN_TIME)
9
- jobs.where!("priority >= ?", setting.min_priority) if setting.min_priority
10
- jobs.where!("priority <= ?", setting.max_priority) if setting.max_priority
11
- jobs.where!(queue: setting.queues) if setting.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
@@ -1,32 +0,0 @@
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(setting)
8
- right_now = Delayed::Job.db_time_now
9
- jobs = reservation_criteria(right_now, setting.max_run_time || Delayed::Worker::DEFAULT_MAX_RUN_TIME)
10
- jobs = jobs.gte(priority: setting.min_priority.to_i) if setting.min_priority
11
- jobs = jobs.lte(priority: setting.max_priority.to_i) if setting.max_priority
12
- jobs = jobs.any_in(queue: setting.queues) if setting.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
@@ -1,47 +0,0 @@
1
- module Delayed
2
- class Master
3
- class SignalHandler
4
- def initialize(master)
5
- @master = master
6
- @logger = master.logger
7
- @workers = master.workers
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
- @workers.each do |worker|
32
- next unless worker.pid
33
- dispatch_to(signal, worker.pid)
34
- end
35
- end
36
-
37
- private
38
-
39
- def dispatch_to(signal, pid)
40
- Process.kill signal, pid
41
- @logger.info "sent #{signal} signal to worker #{pid}"
42
- rescue
43
- @logger.error "failed to send #{signal} signal to worker #{pid}"
44
- end
45
- end
46
- end
47
- end
@@ -1,141 +0,0 @@
1
- module Delayed
2
- class Master
3
- class WorkerPool
4
- def initialize(master, config)
5
- @master = master
6
- @config = config
7
-
8
- @logger = master.logger
9
- @workers = master.workers
10
-
11
- @static_settings, @dynamic_settings = config.workers.partition { |conf| conf.control == :static }
12
- @callback = Delayed::Master::Callback.new(config)
13
- end
14
-
15
- def init
16
- @static_settings.each_with_index do |setting, i|
17
- worker = Delayed::Master::Worker.new(i, setting)
18
- @workers << worker
19
- fork_worker(worker)
20
- @logger.info "started worker #{worker.pid}"
21
- end
22
-
23
- @prepared = true
24
- print_workers
25
- end
26
-
27
- def monitor_while(&block)
28
- loop do
29
- break if block.call
30
- monitor do
31
- check_pid
32
- check_dynamic_worker
33
- end
34
- sleep @config.monitor_wait.to_i
35
- end
36
- end
37
-
38
- def prepared?
39
- @prepared
40
- end
41
-
42
- private
43
-
44
- def fork_worker(worker)
45
- @callback.run(:before_fork, @master, worker)
46
- worker.pid = fork do
47
- worker.pid = Process.pid
48
- worker.instance = create_instance(worker)
49
- @callback.run(:after_fork, @master, worker)
50
- $0 = worker.title
51
- worker.instance.start
52
- end
53
- end
54
-
55
- def create_instance(worker)
56
- instance = Delayed::Worker.new(worker.setting.data)
57
- [:max_run_time, :max_attempts, :destroy_failed_jobs].each do |key|
58
- if (value = worker.setting.send(key))
59
- Delayed::Worker.send("#{key}=", value)
60
- end
61
- end
62
- [:max_memory].each do |key|
63
- if (value = worker.setting.send(key))
64
- instance.send("#{key}=", value)
65
- end
66
- end
67
- instance.master_logger = @logger
68
- instance
69
- end
70
-
71
- def monitor
72
- @callback.run(:before_monitor, @master)
73
- yield
74
- @callback.run(:after_monitor, @master)
75
- rescue Exception => e
76
- @logger.warn "#{e.class}: #{e.message} at #{__FILE__}: #{__LINE__}"
77
- end
78
-
79
- def check_pid
80
- pid = wait_pid
81
- return unless pid
82
- worker = @workers.detect { |worker| worker.pid == pid }
83
- return unless worker
84
-
85
- case worker.setting.control
86
- when :static
87
- fork_alt_worker(worker)
88
- when :dynamic
89
- @workers.delete(worker)
90
- end
91
- end
92
-
93
- def wait_pid
94
- Process.waitpid(-1, Process::WNOHANG)
95
- rescue Errno::ECHILD
96
- nil
97
- end
98
-
99
- def check_dynamic_worker
100
- @dynamic_settings.each do |setting|
101
- current_count = @workers.count { |worker| worker.setting.queues == setting.queues }
102
- remaining_count = setting.count - current_count
103
- if remaining_count > 0 && (job_count = count_job(setting)) > 0
104
- [remaining_count, job_count].min.times do
105
- fork_dynamic_worker(setting)
106
- end
107
- end
108
- end
109
- end
110
-
111
- def count_job(setting)
112
- Delayed::Master::JobCounter.count(setting)
113
- end
114
-
115
- def fork_dynamic_worker(setting)
116
- worker = Delayed::Master::Worker.new(@workers.size, setting)
117
- @workers << worker
118
-
119
- @logger.info "forking dynamic worker..."
120
- fork_worker(worker)
121
- @logger.info "forked worker #{worker.pid}"
122
-
123
- print_workers
124
- end
125
-
126
- def fork_alt_worker(worker)
127
- @logger.info "worker #{worker.pid} seems to be killed, forking alternative worker..."
128
- fork_worker(worker)
129
- @logger.info "forked worker #{worker.pid}"
130
-
131
- print_workers
132
- end
133
-
134
- def print_workers
135
- @workers.each do |worker|
136
- @logger.debug "#{worker.pid}: #{worker.title}"
137
- end
138
- end
139
- end
140
- end
141
- end
@@ -1,9 +0,0 @@
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
@@ -1,20 +0,0 @@
1
- require 'get_process_mem'
2
-
3
- module Delayed
4
- module Plugins
5
- class WorkerMemoryChecker < Delayed::Plugin
6
- callbacks do |lifecycle|
7
- lifecycle.after(:perform) do |worker, job|
8
- next unless worker.max_memory
9
- mem = GetProcessMem.new
10
- if mem.mb > worker.max_memory
11
- worker.master_logger.info "shutting down worker #{Process.pid} because it consumes large memory #{mem.mb.to_i} MB..."
12
- worker.stop
13
- end
14
- end
15
- end
16
- end
17
- end
18
- end
19
-
20
- Delayed::Worker.plugins << Delayed::Plugins::WorkerMemoryChecker
@@ -1,31 +0,0 @@
1
- module Delayed
2
- module Plugins
3
- class SignalHandler < Delayed::Plugin
4
- callbacks do |lifecycle|
5
- lifecycle.before(:execute) do |worker|
6
- worker.instance_eval do
7
- trap('USR1') do
8
- Thread.new do
9
- master_logger.info "reopening files..."
10
- Delayed::Master::Util::FileReopener.reopen
11
- master_logger.info "reopened"
12
- end
13
- end
14
- trap('USR2') do
15
- Thread.new do
16
- $0 = "#{$0} [OLD]"
17
- master_logger.info "shutting down worker #{Process.pid}..."
18
- stop
19
- end
20
- end
21
- end
22
- end
23
- lifecycle.after(:execute) do |worker|
24
- worker.master_logger.info "shut down worker #{Process.pid}"
25
- end
26
- end
27
- end
28
- end
29
- end
30
-
31
- Delayed::Worker.plugins << Delayed::Plugins::SignalHandler
@@ -1,17 +0,0 @@
1
- module Delayed
2
- module Plugins
3
- class StatusNotifier < Delayed::Plugin
4
- callbacks do |lifecycle|
5
- lifecycle.around(:perform) do |worker, job, &block|
6
- title = $0
7
- $0 = "#{title} [BUSY]"
8
- ret = block.call
9
- $0 = title
10
- ret
11
- end
12
- end
13
- end
14
- end
15
- end
16
-
17
- Delayed::Worker.plugins << Delayed::Plugins::StatusNotifier