rocketjob 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,96 @@
1
+ require 'rocket_job/supervisor/shutdown'
2
+
3
+ module RocketJob
4
+ # Starts a server instance, along with the workers and ensures workers remain running until they need to shutdown.
5
+ class Supervisor
6
+ include SemanticLogger::Loggable
7
+ include Supervisor::Shutdown
8
+
9
+ attr_reader :server, :worker_pool
10
+ attr_accessor :worker_id
11
+
12
+ # Start the Supervisor, using the supplied attributes to create a new Server instance.
13
+ def self.run(attrs = {})
14
+ Thread.current.name = 'rocketjob main'
15
+ RocketJob.create_indexes
16
+ register_signal_handlers
17
+
18
+ server = Server.create!(attrs)
19
+ new(server).run
20
+ ensure
21
+ server&.destroy
22
+ end
23
+
24
+ def initialize(server)
25
+ @server = server
26
+ @worker_pool = WorkerPool.new(server.name, server.filter)
27
+ @mutex = Mutex.new
28
+ end
29
+
30
+ def run
31
+ logger.info "Using MongoDB Database: #{RocketJob::Job.collection.database.name}"
32
+ logger.info('Running with filter', server.filter) if server.filter
33
+ server.started!
34
+ logger.info 'Rocket Job Server started'
35
+
36
+ event_listener = Thread.new { Event.listener }
37
+ Subscribers::Server.subscribe(self) do
38
+ Subscribers::Worker.subscribe(self) do
39
+ Subscribers::Logger.subscribe do
40
+ supervise_pool
41
+ stop!
42
+ end
43
+ end
44
+ end
45
+ rescue ::Mongoid::Errors::DocumentNotFound
46
+ logger.info('Server has been destroyed. Going down hard!')
47
+ rescue Exception => exc
48
+ logger.error('RocketJob::Server is stopping due to an exception', exc)
49
+ ensure
50
+ event_listener.kill if event_listener
51
+ # Logs the backtrace for each running worker
52
+ worker_pool.log_backtraces
53
+ logger.info('Shutdown Complete')
54
+ end
55
+
56
+ def stop!
57
+ server.stop! if server.may_stop?
58
+ worker_pool.stop
59
+ while !worker_pool.join
60
+ logger.info 'Waiting for workers to finish processing ...'
61
+ # One or more workers still running so update heartbeat so that server reports "alive".
62
+ server.refresh(worker_pool.living_count)
63
+ end
64
+ end
65
+
66
+ def supervise_pool
67
+ stagger = true
68
+ while !self.class.shutdown?
69
+ synchronize do
70
+ if server.running?
71
+ worker_pool.prune
72
+ worker_pool.rebalance(server.max_workers, stagger)
73
+ stagger = false
74
+ elsif server.paused?
75
+ worker_pool.stop
76
+ sleep(0.1)
77
+ worker_pool.prune
78
+ stagger = true
79
+ else
80
+ break
81
+ end
82
+ end
83
+
84
+ self.class.wait_for_event(Config.instance.heartbeat_seconds)
85
+
86
+ break if self.class.shutdown?
87
+
88
+ synchronize { server.refresh(worker_pool.living_count) }
89
+ end
90
+ end
91
+
92
+ def synchronize(&block)
93
+ @mutex.synchronize(&block)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_support/concern'
2
+ require 'concurrent'
3
+
4
+ module RocketJob
5
+ class Supervisor
6
+ module Shutdown
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Set shutdown indicator for this server process
11
+ def self.shutdown!
12
+ @shutdown.set
13
+ event!
14
+ end
15
+
16
+ # Returns [true|false] whether the shutdown indicator has been set for this server process
17
+ def self.shutdown?
18
+ @shutdown.set?
19
+ end
20
+
21
+ # An event has occured
22
+ def self.event!
23
+ @event.set
24
+ end
25
+
26
+ # Returns [true|false] whether the shutdown indicator was set before the timeout was reached
27
+ def self.wait_for_event(timeout = nil)
28
+ @event.wait(timeout)
29
+ @event.reset
30
+ end
31
+
32
+ @shutdown = Concurrent::Event.new
33
+ @event = Concurrent::Event.new
34
+
35
+ # Register handlers for the various signals
36
+ # Term:
37
+ # Perform clean shutdown
38
+ #
39
+ def self.register_signal_handlers
40
+ Signal.trap 'SIGTERM' do
41
+ Thread.new do
42
+ shutdown!
43
+ message = 'Shutdown signal (SIGTERM) received. Will shutdown as soon as active jobs/slices have completed.'
44
+ logger.warn(message)
45
+ end
46
+ end
47
+
48
+ Signal.trap 'INT' do
49
+ Thread.new do
50
+ shutdown!
51
+ message = 'Shutdown signal (INT) received. Will shutdown as soon as active jobs/slices have completed.'
52
+ logger.warn(message)
53
+ end
54
+ end
55
+ rescue StandardError
56
+ logger.warn 'SIGTERM handler not installed. Not able to shutdown gracefully'
57
+ end
58
+
59
+ private_class_method :register_signal_handlers
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,3 +1,3 @@
1
1
  module RocketJob
2
- VERSION = '4.0.0'.freeze
2
+ VERSION = '4.1.0'.freeze
3
3
  end
@@ -8,16 +8,18 @@ module RocketJob
8
8
  class Worker
9
9
  include SemanticLogger::Loggable
10
10
  include ActiveSupport::Callbacks
11
- extend Forwardable
12
-
13
- def_delegator :@thread, :alive?
14
- def_delegator :@thread, :backtrace
15
- def_delegator :@thread, :join
16
11
 
17
12
  define_callbacks :running
18
13
 
19
14
  attr_accessor :id, :re_check_seconds, :filter, :current_filter
20
- attr_reader :thread, :name
15
+ attr_reader :thread, :name, :inline
16
+
17
+ # Raised when a worker is killed so that it shutdown immediately, yet cleanly.
18
+ #
19
+ # Note:
20
+ # - It is not recommended to catch this exception since it is to shutdown workers quickly.
21
+ class Shutdown < Interrupt
22
+ end
21
23
 
22
24
  def self.before_running(*filters, &blk)
23
25
  set_callback(:running, :before, *filters, &blk)
@@ -38,37 +40,46 @@ module RocketJob
38
40
  filter: nil)
39
41
  @id = id
40
42
  @server_name = server_name
41
- @shutdown =
42
- if defined?(Concurrent::JavaAtomicBoolean) || defined?(Concurrent::CAtomicBoolean)
43
- Concurrent::AtomicBoolean.new(false)
44
- else
45
- false
46
- end
43
+ @shutdown = Concurrent::Event.new
47
44
  @name = "#{server_name}:#{id}"
48
45
  @re_check_seconds = (re_check_seconds || 60).to_f
49
46
  @re_check_start = Time.now
50
47
  @filter = filter.nil? ? {} : filter.dup
51
48
  @current_filter = @filter.dup
52
49
  @thread = Thread.new { run } unless inline
50
+ @inline = inline
53
51
  end
54
52
 
55
- if defined?(Concurrent::JavaAtomicBoolean) || defined?(Concurrent::CAtomicBoolean)
56
- # Tells this worker to shutdown as soon the current job/slice is complete
57
- def shutdown!
58
- @shutdown.make_true
59
- end
53
+ def alive?
54
+ inline ? true : @thread.alive?
55
+ end
60
56
 
61
- def shutdown?
62
- @shutdown.value
63
- end
64
- else
65
- def shutdown!
66
- @shutdown = true
67
- end
57
+ def backtrace
58
+ inline ? Thread.current.backtrace : @thread.backtrace
59
+ end
68
60
 
69
- def shutdown?
70
- @shutdown
71
- end
61
+ def join(*args)
62
+ @thread.join(*args) unless inline
63
+ end
64
+
65
+ # Send each active worker the RocketJob::ShutdownException so that stops processing immediately.
66
+ def kill
67
+ return true if inline
68
+
69
+ @thread.raise(Shutdown, "Shutdown due to kill request for worker: #{name}") if @thread.alive?
70
+ end
71
+
72
+ def shutdown?
73
+ @shutdown.set?
74
+ end
75
+
76
+ def shutdown!
77
+ @shutdown.set
78
+ end
79
+
80
+ # Returns [true|false] whether the shutdown indicator was set
81
+ def wait_for_shutdown?(timeout = nil)
82
+ @shutdown.wait(timeout)
72
83
  end
73
84
 
74
85
  private
@@ -82,14 +93,13 @@ module RocketJob
82
93
  Thread.current.name = format('rocketjob %03i', id)
83
94
  logger.info 'Started'
84
95
  until shutdown?
96
+ wait = RocketJob::Config.instance.max_poll_seconds
85
97
  if process_available_jobs
86
98
  # Keeps workers staggered across the poll interval so that
87
99
  # all workers don't poll at the same time
88
- sleep rand(RocketJob::Config.instance.max_poll_seconds * 1000) / 1000
89
- else
90
- break if shutdown?
91
- sleep RocketJob::Config.instance.max_poll_seconds
100
+ wait = rand(wait * 1000) / 1000
92
101
  end
102
+ break if wait_for_shutdown?(wait)
93
103
  end
94
104
  logger.info 'Stopping'
95
105
  rescue Exception => exc
@@ -0,0 +1,103 @@
1
+ require 'concurrent-ruby'
2
+ require 'rocket_job/supervisor/shutdown'
3
+
4
+ module RocketJob
5
+ class WorkerPool
6
+ include SemanticLogger::Loggable
7
+
8
+ attr_reader :server_name, :filter, :workers
9
+
10
+ def initialize(server_name, filter)
11
+ @server_name = server_name
12
+ @filter = filter
13
+ @workers = Concurrent::Array.new
14
+ @worker_id = 0
15
+ end
16
+
17
+ # Find a worker in the list by its id
18
+ def find(id)
19
+ workers.find { |worker| worker.id == id }
20
+ end
21
+
22
+ # Add new workers to get back to the `max_workers` if not already at `max_workers`
23
+ # Parameters
24
+ # stagger_start
25
+ # Whether to stagger when the workers poll for work the first time.
26
+ # It spreads out the queue polling over the max_poll_seconds so
27
+ # that not all workers poll at the same time.
28
+ # The worker also responds faster than max_poll_seconds when a new job is created.
29
+ def rebalance(max_workers, stagger_start = false)
30
+ count = max_workers.to_i - living_count
31
+ return 0 unless count > 0
32
+
33
+ logger.info("#{'Stagger ' if stagger_start}Starting #{count} workers")
34
+
35
+ add_one
36
+ count -= 1
37
+ delay = Config.instance.max_poll_seconds.to_f / max_workers
38
+
39
+ count.times.each do
40
+ sleep(delay) if stagger_start
41
+ return -1 if Supervisor.shutdown?
42
+ add_one
43
+ end
44
+ end
45
+
46
+ # Returns [Integer] number of dead workers removed.
47
+ def prune
48
+ remove_count = workers.count - living_count
49
+ return 0 if remove_count.zero?
50
+
51
+ logger.info "Cleaned up #{remove_count} dead workers"
52
+ workers.delete_if { |t| !t.alive? }
53
+ remove_count
54
+ end
55
+
56
+ # Tell all workers to stop working.
57
+ def stop
58
+ workers.each(&:shutdown!)
59
+ end
60
+
61
+ # Kill Worker threads
62
+ def kill
63
+ workers.each(&:kill)
64
+ end
65
+
66
+ # Wait for all workers to stop.
67
+ # Return [true] if all workers stopped
68
+ # Return [false] on timeout
69
+ def join(timeout = 5)
70
+ while (worker = workers.first)
71
+ if worker.join(timeout)
72
+ # Worker thread is dead
73
+ workers.shift
74
+ else
75
+ return false
76
+ end
77
+ end
78
+ true
79
+ end
80
+
81
+ # Returns [Fixnum] number of workers (threads) that are alive
82
+ def living_count
83
+ workers.count(&:alive?)
84
+ end
85
+
86
+ def log_backtraces
87
+ workers.each { |worker| logger.backtrace(thread: worker.thread) if worker.thread && worker.alive? }
88
+ end
89
+
90
+ private
91
+
92
+ def add_one
93
+ workers << Worker.new(id: next_worker_id, server_name: server_name, filter: filter)
94
+ rescue StandardError => exc
95
+ logger.fatal('Cannot start worker', exc)
96
+ end
97
+
98
+ def next_worker_id
99
+ @worker_id += 1
100
+ end
101
+
102
+ end
103
+ end
@@ -23,12 +23,16 @@ module RocketJob
23
23
  autoload :CLI, 'rocket_job/cli'
24
24
  autoload :Config, 'rocket_job/config'
25
25
  autoload :DirmonEntry, 'rocket_job/dirmon_entry'
26
+ autoload :Event, 'rocket_job/event'
26
27
  autoload :Heartbeat, 'rocket_job/heartbeat'
27
28
  autoload :Job, 'rocket_job/job'
28
29
  autoload :JobException, 'rocket_job/job_exception'
29
30
  autoload :Worker, 'rocket_job/worker'
30
31
  autoload :Performance, 'rocket_job/performance'
31
32
  autoload :Server, 'rocket_job/server'
33
+ autoload :Subscriber, 'rocket_job/subscriber'
34
+ autoload :Supervisor, 'rocket_job/supervisor'
35
+ autoload :WorkerPool, 'rocket_job/worker_pool'
32
36
 
33
37
  module Plugins
34
38
  module Job
@@ -69,17 +73,23 @@ module RocketJob
69
73
  end
70
74
 
71
75
  module Sliced
72
- autoload :Input, 'rocket_job/sliced/input'
73
- autoload :Output, 'rocket_job/sliced/output'
74
- autoload :Slice, 'rocket_job/sliced/slice'
75
- autoload :Slices, 'rocket_job/sliced/slices'
76
- autoload :Store, 'rocket_job/sliced/store'
76
+ autoload :Input, 'rocket_job/sliced/input'
77
+ autoload :Output, 'rocket_job/sliced/output'
78
+ autoload :Slice, 'rocket_job/sliced/slice'
79
+ autoload :Slices, 'rocket_job/sliced/slices'
80
+ autoload :Store, 'rocket_job/sliced/store'
77
81
 
78
82
  module Writer
79
- autoload :Input, 'rocket_job/sliced/writer/input'
80
- autoload :Output, 'rocket_job/sliced/writer/output'
83
+ autoload :Input, 'rocket_job/sliced/writer/input'
84
+ autoload :Output, 'rocket_job/sliced/writer/output'
81
85
  end
82
86
  end
87
+
88
+ module Subscribers
89
+ autoload :Logger, 'rocket_job/subscribers/logger'
90
+ autoload :Server, 'rocket_job/subscribers/server'
91
+ autoload :Worker, 'rocket_job/subscribers/worker'
92
+ end
83
93
  end
84
94
 
85
95
  # Add Active Job adapter for Rails
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocketjob
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-14 00:00:00.000000000 Z
11
+ date: 2019-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aasm
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '1.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: '1.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: iostreams
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,7 @@ files:
115
115
  - lib/rocket_job/cli.rb
116
116
  - lib/rocket_job/config.rb
117
117
  - lib/rocket_job/dirmon_entry.rb
118
+ - lib/rocket_job/event.rb
118
119
  - lib/rocket_job/extensions/mongo/logging.rb
119
120
  - lib/rocket_job/extensions/mongoid/clients/options.rb
120
121
  - lib/rocket_job/extensions/mongoid/contextual/mongo.rb
@@ -155,14 +156,23 @@ files:
155
156
  - lib/rocket_job/plugins/transaction.rb
156
157
  - lib/rocket_job/rocket_job.rb
157
158
  - lib/rocket_job/server.rb
159
+ - lib/rocket_job/server/model.rb
160
+ - lib/rocket_job/server/state_machine.rb
158
161
  - lib/rocket_job/sliced/input.rb
159
162
  - lib/rocket_job/sliced/output.rb
160
163
  - lib/rocket_job/sliced/slice.rb
161
164
  - lib/rocket_job/sliced/slices.rb
162
165
  - lib/rocket_job/sliced/writer/input.rb
163
166
  - lib/rocket_job/sliced/writer/output.rb
167
+ - lib/rocket_job/subscriber.rb
168
+ - lib/rocket_job/subscribers/logger.rb
169
+ - lib/rocket_job/subscribers/server.rb
170
+ - lib/rocket_job/subscribers/worker.rb
171
+ - lib/rocket_job/supervisor.rb
172
+ - lib/rocket_job/supervisor/shutdown.rb
164
173
  - lib/rocket_job/version.rb
165
174
  - lib/rocket_job/worker.rb
175
+ - lib/rocket_job/worker_pool.rb
166
176
  - lib/rocketjob.rb
167
177
  homepage: http://rocketjob.io
168
178
  licenses:
@@ -183,8 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
193
  - !ruby/object:Gem::Version
184
194
  version: '0'
185
195
  requirements: []
186
- rubyforge_project:
187
- rubygems_version: 2.7.7
196
+ rubygems_version: 3.0.2
188
197
  signing_key:
189
198
  specification_version: 4
190
199
  summary: Ruby's missing batch system.