rocketjob 3.4.3 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +27 -0
- data/bin/rocketjob +1 -1
- data/lib/rocket_job/active_worker.rb +4 -3
- data/lib/rocket_job/cli.rb +13 -12
- data/lib/rocket_job/config.rb +17 -13
- data/lib/rocket_job/dirmon_entry.rb +88 -91
- data/lib/rocket_job/extensions/mongo/logging.rb +8 -4
- data/lib/rocket_job/extensions/mongoid/factory.rb +8 -6
- data/lib/rocket_job/extensions/rocket_job_adapter.rb +2 -4
- data/lib/rocket_job/heartbeat.rb +7 -8
- data/lib/rocket_job/job_exception.rb +6 -5
- data/lib/rocket_job/jobs/dirmon_job.rb +5 -7
- data/lib/rocket_job/jobs/housekeeping_job.rb +3 -2
- data/lib/rocket_job/jobs/on_demand_job.rb +104 -0
- data/lib/rocket_job/jobs/simple_job.rb +0 -2
- data/lib/rocket_job/jobs/upload_file_job.rb +100 -0
- data/lib/rocket_job/performance.rb +15 -10
- data/lib/rocket_job/plugins/cron.rb +7 -124
- data/lib/rocket_job/plugins/document.rb +8 -10
- data/lib/rocket_job/plugins/job/callbacks.rb +0 -1
- data/lib/rocket_job/plugins/job/logger.rb +0 -1
- data/lib/rocket_job/plugins/job/model.rb +15 -20
- data/lib/rocket_job/plugins/job/persistence.rb +3 -13
- data/lib/rocket_job/plugins/job/state_machine.rb +1 -2
- data/lib/rocket_job/plugins/job/throttle.rb +16 -12
- data/lib/rocket_job/plugins/job/worker.rb +15 -19
- data/lib/rocket_job/plugins/processing_window.rb +2 -2
- data/lib/rocket_job/plugins/restart.rb +3 -4
- data/lib/rocket_job/plugins/retry.rb +2 -3
- data/lib/rocket_job/plugins/singleton.rb +2 -3
- data/lib/rocket_job/plugins/state_machine.rb +19 -23
- data/lib/rocket_job/rocket_job.rb +4 -5
- data/lib/rocket_job/server.rb +35 -41
- data/lib/rocket_job/version.rb +2 -2
- data/lib/rocket_job/worker.rb +22 -21
- data/lib/rocketjob.rb +2 -0
- data/test/config/mongoid.yml +2 -2
- data/test/config_test.rb +0 -2
- data/test/dirmon_entry_test.rb +161 -134
- data/test/dirmon_job_test.rb +80 -78
- data/test/job_test.rb +0 -2
- data/test/jobs/housekeeping_job_test.rb +0 -1
- data/test/jobs/on_demand_job_test.rb +59 -0
- data/test/jobs/upload_file_job_test.rb +99 -0
- data/test/plugins/cron_test.rb +1 -3
- data/test/plugins/job/callbacks_test.rb +8 -13
- data/test/plugins/job/defaults_test.rb +0 -1
- data/test/plugins/job/logger_test.rb +2 -4
- data/test/plugins/job/model_test.rb +1 -2
- data/test/plugins/job/persistence_test.rb +0 -2
- data/test/plugins/job/state_machine_test.rb +0 -2
- data/test/plugins/job/throttle_test.rb +6 -8
- data/test/plugins/job/worker_test.rb +1 -2
- data/test/plugins/processing_window_test.rb +0 -2
- data/test/plugins/restart_test.rb +0 -1
- data/test/plugins/retry_test.rb +1 -2
- data/test/plugins/singleton_test.rb +0 -2
- data/test/plugins/state_machine_event_callbacks_test.rb +1 -2
- data/test/plugins/state_machine_test.rb +0 -2
- data/test/plugins/transaction_test.rb +5 -7
- data/test/test_db.sqlite3 +0 -0
- data/test/test_helper.rb +2 -1
- metadata +42 -36
@@ -4,22 +4,26 @@ module Mongo
|
|
4
4
|
class Monitoring
|
5
5
|
class CommandLogSubscriber
|
6
6
|
include SemanticLogger::Loggable
|
7
|
-
|
7
|
+
logger.name = 'Mongo'
|
8
8
|
|
9
9
|
def started(event)
|
10
10
|
@event_command = event.command
|
11
11
|
end
|
12
12
|
|
13
13
|
def succeeded(event)
|
14
|
-
logger.debug(message:
|
14
|
+
logger.debug(message: prefix(event),
|
15
|
+
duration: (event.duration * 1000),
|
16
|
+
payload: @event_command)
|
15
17
|
end
|
16
18
|
|
17
19
|
def failed(event)
|
18
|
-
logger.debug(message:
|
20
|
+
logger.debug(message: "#{prefix(event)} Failed: #{event.message}",
|
21
|
+
duration: (event.duration * 1000),
|
22
|
+
payload: @event_command)
|
19
23
|
end
|
20
24
|
|
21
25
|
def prefix(event)
|
22
|
-
"#{event.address
|
26
|
+
"#{event.address} | #{event.database_name}.#{event.command_name}"
|
23
27
|
end
|
24
28
|
end
|
25
29
|
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
require 'mongoid/factory'
|
2
2
|
|
3
3
|
module RocketJob
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
module Mongoid
|
5
|
+
module Factory
|
6
|
+
def from_db(*args)
|
7
|
+
super(*args)
|
8
|
+
rescue NameError
|
9
|
+
RocketJob::Job.instantiate(attributes, selected_fields)
|
10
|
+
end
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
13
|
-
::Mongoid::Factory.
|
15
|
+
::Mongoid::Factory.extend(RocketJob::Mongoid::Factory)
|
@@ -70,20 +70,18 @@ module ActiveJob
|
|
70
70
|
job
|
71
71
|
end
|
72
72
|
|
73
|
-
private
|
74
|
-
|
75
73
|
def self.active_job_params(active_job)
|
76
74
|
params = {
|
77
75
|
description: active_job.class.name,
|
78
76
|
data: active_job.serialize,
|
79
77
|
active_job_id: active_job.job_id,
|
80
78
|
active_job_class: active_job.class.name,
|
81
|
-
active_job_queue: active_job.queue_name
|
79
|
+
active_job_queue: active_job.queue_name
|
82
80
|
}
|
83
81
|
params[:priority] = active_job.priority if active_job.respond_to?(:priority) && active_job.priority
|
84
82
|
params
|
85
83
|
end
|
86
|
-
|
84
|
+
private_class_method :active_job_params
|
87
85
|
end
|
88
86
|
end
|
89
87
|
end
|
data/lib/rocket_job/heartbeat.rb
CHANGED
@@ -16,23 +16,22 @@ module RocketJob
|
|
16
16
|
#
|
17
17
|
|
18
18
|
# Percentage utilization for the server process alone
|
19
|
-
#field :process_cpu, type: Integer
|
19
|
+
# field :process_cpu, type: Integer
|
20
20
|
# Kilo Bytes used by the server process (Virtual & Physical)
|
21
|
-
#field :process_mem_phys_kb, type: Integer
|
22
|
-
#field :process_mem_virt_kb, type: Integer
|
21
|
+
# field :process_mem_phys_kb, type: Integer
|
22
|
+
# field :process_mem_virt_kb, type: Integer
|
23
23
|
|
24
24
|
#
|
25
25
|
# System Information. Future.
|
26
26
|
#
|
27
27
|
|
28
28
|
# Percentage utilization for the host machine
|
29
|
-
#field :host_cpu, type: Integer
|
29
|
+
# field :host_cpu, type: Integer
|
30
30
|
# Kilo Bytes Available on the host machine (Physical)
|
31
|
-
#field :host_mem_avail_phys_kbytes, type: Float
|
32
|
-
#field :host_mem_avail_virt_kbytes, type: Float
|
31
|
+
# field :host_mem_avail_phys_kbytes, type: Float
|
32
|
+
# field :host_mem_avail_virt_kbytes, type: Float
|
33
33
|
|
34
34
|
# If available
|
35
|
-
#field :load_average, type: Float
|
35
|
+
# field :load_average, type: Float
|
36
36
|
end
|
37
37
|
end
|
38
|
-
|
@@ -22,13 +22,14 @@ module RocketJob
|
|
22
22
|
field :record_number, type: Integer
|
23
23
|
|
24
24
|
# Returns [JobException] built from the supplied exception
|
25
|
-
def self.from_exception(exc)
|
25
|
+
def self.from_exception(exc, **args)
|
26
26
|
new(
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
args.merge(
|
28
|
+
class_name: exc.class.name,
|
29
|
+
message: exc.message,
|
30
|
+
backtrace: exc.backtrace || []
|
31
|
+
)
|
30
32
|
)
|
31
33
|
end
|
32
|
-
|
33
34
|
end
|
34
35
|
end
|
@@ -40,7 +40,7 @@ module RocketJob
|
|
40
40
|
# Start a new job when this one completes, fails, or aborts
|
41
41
|
include RocketJob::Plugins::Restart
|
42
42
|
|
43
|
-
self.priority =
|
43
|
+
self.priority = 30
|
44
44
|
|
45
45
|
# Number of seconds between directory scans. Default 5 mins
|
46
46
|
field :check_seconds, type: Float, default: 300.0, copy_on_restart: true
|
@@ -72,11 +72,10 @@ module RocketJob
|
|
72
72
|
DirmonEntry.enabled.each do |entry|
|
73
73
|
entry.each do |pathname|
|
74
74
|
# BSON Keys cannot contain periods
|
75
|
-
key
|
76
|
-
previous_size
|
77
|
-
|
78
|
-
|
79
|
-
end
|
75
|
+
key = pathname.to_s.tr('.', '_')
|
76
|
+
previous_size = previous_file_names[key]
|
77
|
+
size = check_file(entry, pathname, previous_size)
|
78
|
+
new_file_names[key] = size if size
|
80
79
|
end
|
81
80
|
end
|
82
81
|
self.previous_file_names = new_file_names
|
@@ -96,7 +95,6 @@ module RocketJob
|
|
96
95
|
size
|
97
96
|
end
|
98
97
|
end
|
99
|
-
|
100
98
|
end
|
101
99
|
end
|
102
100
|
end
|
@@ -54,12 +54,13 @@ module RocketJob
|
|
54
54
|
end
|
55
55
|
|
56
56
|
RocketJob::Job.aborted.where(completed_at: {'$lte' => aborted_retention.seconds.ago}).destroy_all if aborted_retention
|
57
|
-
|
57
|
+
if completed_retention
|
58
|
+
RocketJob::Job.completed.where(completed_at: {'$lte' => completed_retention.seconds.ago}).destroy_all
|
59
|
+
end
|
58
60
|
RocketJob::Job.failed.where(completed_at: {'$lte' => failed_retention.seconds.ago}).destroy_all if failed_retention
|
59
61
|
RocketJob::Job.paused.where(completed_at: {'$lte' => paused_retention.seconds.ago}).destroy_all if paused_retention
|
60
62
|
RocketJob::Job.queued.where(created_at: {'$lte' => queued_retention.seconds.ago}).destroy_all if queued_retention
|
61
63
|
end
|
62
|
-
|
63
64
|
end
|
64
65
|
end
|
65
66
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Generalized Job.
|
2
|
+
#
|
3
|
+
# Create or schedule a generalized job for one off fixes or cleanups.
|
4
|
+
#
|
5
|
+
# Example: Iterate over all rows in a table:
|
6
|
+
# code = <<~CODE
|
7
|
+
# User.unscoped.all.order('updated_at DESC').each |user|
|
8
|
+
# user.cleanse_attributes!
|
9
|
+
# user.save!
|
10
|
+
# end
|
11
|
+
# CODE
|
12
|
+
#
|
13
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
14
|
+
# code: code,
|
15
|
+
# description: 'Cleanse users'
|
16
|
+
# )
|
17
|
+
#
|
18
|
+
# Example: Test job in a console:
|
19
|
+
# code = <<~CODE
|
20
|
+
# User.unscoped.all.order('updated_at DESC').each |user|
|
21
|
+
# user.cleanse_attributes!
|
22
|
+
# user.save!
|
23
|
+
# end
|
24
|
+
# CODE
|
25
|
+
#
|
26
|
+
# job = RocketJob::Jobs::OnDemandJob.new(code: code, description: 'cleanse users')
|
27
|
+
# job.perform_now
|
28
|
+
#
|
29
|
+
# Example: Pass input data:
|
30
|
+
# code = <<~CODE
|
31
|
+
# puts data['a'] * data['b']
|
32
|
+
# CODE
|
33
|
+
#
|
34
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
35
|
+
# code: code,
|
36
|
+
# data: {'a' => 10, 'b' => 2}
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# Example: Retain output:
|
40
|
+
# code = <<~CODE
|
41
|
+
# {'value' => data['a'] * data['b']}
|
42
|
+
# CODE
|
43
|
+
#
|
44
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
45
|
+
# code: code,
|
46
|
+
# collect_output: true,
|
47
|
+
# data: {'a' => 10, 'b' => 2}
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# Example: Schedule the job to run nightly at 2am Eastern:
|
51
|
+
#
|
52
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
53
|
+
# cron_schedule: '0 2 * * * America/New_York',
|
54
|
+
# code: code
|
55
|
+
# )
|
56
|
+
#
|
57
|
+
# Example: Change the job priority, description, etc.
|
58
|
+
#
|
59
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
60
|
+
# code: code,
|
61
|
+
# description: 'Cleanse users',
|
62
|
+
# priority: 30
|
63
|
+
# )
|
64
|
+
#
|
65
|
+
# Example: Automatically retry up to 5 times on failure:
|
66
|
+
#
|
67
|
+
# RocketJob::Jobs::OnDemandJob.create!(
|
68
|
+
# retry_limit: 5
|
69
|
+
# code: code
|
70
|
+
# )
|
71
|
+
module RocketJob
|
72
|
+
module Jobs
|
73
|
+
class OnDemandJob < RocketJob::Job
|
74
|
+
include RocketJob::Plugins::Cron
|
75
|
+
include RocketJob::Plugins::Retry
|
76
|
+
|
77
|
+
self.priority = 90
|
78
|
+
self.description = 'Generalized Job'
|
79
|
+
self.destroy_on_complete = false
|
80
|
+
self.retry_limit = 0
|
81
|
+
|
82
|
+
# Be sure to store key names only as Strings, not Symbols
|
83
|
+
field :data, type: Hash, default: {}
|
84
|
+
field :code, type: String
|
85
|
+
|
86
|
+
validates :code, presence: true
|
87
|
+
validates_each :code do |job, attr, _value|
|
88
|
+
begin
|
89
|
+
job.send(:load_code)
|
90
|
+
rescue Exception => exc
|
91
|
+
job.errors.add(attr, "Failed to parse :code, #{exc.inspect}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
before_perform :load_code
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def load_code
|
100
|
+
instance_eval("def perform\n#{code}\nend", __FILE__, __LINE__)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
begin
|
3
|
+
require 'iostreams'
|
4
|
+
rescue LoadError
|
5
|
+
# Optional dependency
|
6
|
+
end
|
7
|
+
|
8
|
+
module RocketJob
|
9
|
+
module Jobs
|
10
|
+
# Job to upload a file into another job.
|
11
|
+
#
|
12
|
+
# Intended for use by DirmonJob to upload a file into a specified job.
|
13
|
+
#
|
14
|
+
# Can be used directly for any job class, as long as that job responds
|
15
|
+
# to `#upload`.
|
16
|
+
class UploadFileJob < RocketJob::Job
|
17
|
+
self.priority = 30
|
18
|
+
|
19
|
+
# Name of the job class to instantiate and upload the file into.
|
20
|
+
field :job_class_name, type: String, user_editable: true
|
21
|
+
|
22
|
+
# Properties to assign to the job when it is created.
|
23
|
+
field :properties, type: Hash, default: {}, user_editable: true
|
24
|
+
|
25
|
+
# File to upload
|
26
|
+
field :upload_file_name, type: String, user_editable: true
|
27
|
+
|
28
|
+
# The original Input file name.
|
29
|
+
# Used by #upload to extract the IOStreams when present.
|
30
|
+
field :original_file_name, type: String, user_editable: true
|
31
|
+
|
32
|
+
# Optionally set the job id for the downstream job
|
33
|
+
# Useful when for example the archived file should contain the job id for the downstream job.
|
34
|
+
field :job_id, type: BSON::ObjectId
|
35
|
+
|
36
|
+
validates_presence_of :upload_file_name, :job_class_name
|
37
|
+
validate :job_is_a_rocket_job
|
38
|
+
validate :job_implements_upload
|
39
|
+
validate :file_exists
|
40
|
+
|
41
|
+
# Create the job and upload the file into it.
|
42
|
+
def perform
|
43
|
+
job = job_class.new(properties)
|
44
|
+
job.id = job_id if job_id
|
45
|
+
upload_file(job)
|
46
|
+
job.save!
|
47
|
+
rescue StandardError => exc
|
48
|
+
# Prevent partial uploads
|
49
|
+
job&.cleanup! if job.respond_to?(:cleanup!)
|
50
|
+
raise(exc)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def job_class
|
56
|
+
@job_class ||= job_class_name.constantize
|
57
|
+
rescue NameError
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def upload_file(job)
|
62
|
+
if job.respond_to?(:upload)
|
63
|
+
if original_file_name && defined?(IOStreams)
|
64
|
+
streams = IOStreams.streams_for_file_name(original_file_name)
|
65
|
+
job.upload(upload_file_name, streams: streams)
|
66
|
+
else
|
67
|
+
job.upload(upload_file_name)
|
68
|
+
end
|
69
|
+
elsif job.respond_to?(:upload_file_name=)
|
70
|
+
job.upload_file_name = upload_file_name
|
71
|
+
elsif job.respond_to?(:full_file_name=)
|
72
|
+
job.full_file_name = upload_file_name
|
73
|
+
else
|
74
|
+
raise(ArgumentError, "Model #{job_class_name} must implement '#upload', or have attribute 'upload_file_name' or 'full_file_name'")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Validates job_class is a Rocket Job
|
79
|
+
def job_is_a_rocket_job
|
80
|
+
klass = job_class
|
81
|
+
return if klass.nil? || klass.ancestors&.include?(RocketJob::Job)
|
82
|
+
errors.add(:job_class_name, "Model #{job_class_name} must be defined and inherit from RocketJob::Job")
|
83
|
+
end
|
84
|
+
|
85
|
+
VALID_INSTANCE_METHODS = %i[upload upload_file_name= full_file_name=].freeze
|
86
|
+
|
87
|
+
# Validates job_class is a Rocket Job
|
88
|
+
def job_implements_upload
|
89
|
+
klass = job_class
|
90
|
+
return if klass.nil? || klass.instance_methods.any? { |m| VALID_INSTANCE_METHODS.include?(m) }
|
91
|
+
errors.add(:job_class_name, "#{job_class} must implement any one of: :#{VALID_INSTANCE_METHODS.join(' :')} instance methods")
|
92
|
+
end
|
93
|
+
|
94
|
+
def file_exists
|
95
|
+
return if upload_file_name.nil? || File.exist?(upload_file_name)
|
96
|
+
errors.add(:upload_file_name, "Upload file: #{upload_file_name} does not exist.")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -18,7 +18,9 @@ module RocketJob
|
|
18
18
|
# Loads the queue with jobs to be processed once the queue is loaded.
|
19
19
|
# Retain the first and last job for timings, all others are destroyed on completion.
|
20
20
|
def run_test_case(count = self.count)
|
21
|
-
|
21
|
+
if RocketJob::Server.where(:state.in => %w[running paused]).count.zero?
|
22
|
+
raise 'Please start servers before starting the performance test'
|
23
|
+
end
|
22
24
|
|
23
25
|
count_running_workers
|
24
26
|
|
@@ -33,21 +35,19 @@ module RocketJob
|
|
33
35
|
running += server.heartbeat.workers unless server.zombie?
|
34
36
|
end
|
35
37
|
puts "Waiting for #{running} workers"
|
36
|
-
break if running
|
38
|
+
break if running.zero?
|
37
39
|
sleep 1
|
38
40
|
end
|
39
41
|
|
40
42
|
puts 'Enqueuing jobs'
|
41
43
|
first = RocketJob::Jobs::SimpleJob.create!(priority: 1, destroy_on_complete: false)
|
42
|
-
(count - 2).times {
|
44
|
+
(count - 2).times { RocketJob::Jobs::SimpleJob.create! }
|
43
45
|
last = RocketJob::Jobs::SimpleJob.create!(priority: 100, destroy_on_complete: false)
|
44
46
|
|
45
47
|
puts 'Resuming workers'
|
46
48
|
RocketJob::Server.resume_all
|
47
49
|
|
48
|
-
|
49
|
-
sleep 3
|
50
|
-
end
|
50
|
+
sleep 3 until last.reload.completed?
|
51
51
|
|
52
52
|
duration = last.reload.completed_at - first.reload.started_at
|
53
53
|
first.destroy
|
@@ -67,13 +67,19 @@ module RocketJob
|
|
67
67
|
# Parse command line options
|
68
68
|
def parse(argv)
|
69
69
|
parser = OptionParser.new do |o|
|
70
|
-
o.on('-c',
|
70
|
+
o.on('-c',
|
71
|
+
'--count COUNT',
|
72
|
+
'Count of jobs to enqueue') do |arg|
|
71
73
|
self.count = arg.to_i
|
72
74
|
end
|
73
|
-
o.on('-m',
|
75
|
+
o.on('-m',
|
76
|
+
'--mongo MONGO_CONFIG_FILE_NAME',
|
77
|
+
'Path and filename of config file. Default: config/mongoid.yml') do |arg|
|
74
78
|
self.mongo_config = arg
|
75
79
|
end
|
76
|
-
o.on('-e',
|
80
|
+
o.on('-e',
|
81
|
+
'--environment ENVIRONMENT',
|
82
|
+
'The environment to run the app on (Default: RAILS_ENV || RACK_ENV || development)') do |arg|
|
77
83
|
self.environment = arg
|
78
84
|
end
|
79
85
|
end
|
@@ -95,6 +101,5 @@ module RocketJob
|
|
95
101
|
end
|
96
102
|
puts "Running: #{workers} workers, distributed across #{servers} servers"
|
97
103
|
end
|
98
|
-
|
99
104
|
end
|
100
105
|
end
|