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.
- checksums.yaml +4 -4
- data/lib/rocket_job/cli.rb +2 -2
- data/lib/rocket_job/event.rb +163 -0
- data/lib/rocket_job/jobs/housekeeping_job.rb +7 -7
- data/lib/rocket_job/plugins/transaction.rb +1 -1
- data/lib/rocket_job/rocket_job.rb +7 -0
- data/lib/rocket_job/server.rb +5 -356
- data/lib/rocket_job/server/model.rb +138 -0
- data/lib/rocket_job/server/state_machine.rb +60 -0
- data/lib/rocket_job/subscriber.rb +79 -0
- data/lib/rocket_job/subscribers/logger.rb +75 -0
- data/lib/rocket_job/subscribers/server.rb +71 -0
- data/lib/rocket_job/subscribers/worker.rb +61 -0
- data/lib/rocket_job/supervisor.rb +96 -0
- data/lib/rocket_job/supervisor/shutdown.rb +63 -0
- data/lib/rocket_job/version.rb +1 -1
- data/lib/rocket_job/worker.rb +41 -31
- data/lib/rocket_job/worker_pool.rb +103 -0
- data/lib/rocketjob.rb +17 -7
- metadata +15 -6
@@ -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
|
data/lib/rocket_job/version.rb
CHANGED
data/lib/rocket_job/worker.rb
CHANGED
@@ -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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
@shutdown.make_true
|
59
|
-
end
|
53
|
+
def alive?
|
54
|
+
inline ? true : @thread.alive?
|
55
|
+
end
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
data/lib/rocketjob.rb
CHANGED
@@ -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,
|
73
|
-
autoload :Output,
|
74
|
-
autoload :Slice,
|
75
|
-
autoload :Slices,
|
76
|
-
autoload :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,
|
80
|
-
autoload :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.
|
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:
|
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.
|
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.
|
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
|
-
|
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.
|