delayed_job_master 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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