rocketjob 4.0.0 → 4.1.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.
@@ -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.