rocketjob 6.0.0.rc3 → 6.0.3
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/README.md +26 -0
- data/lib/rocket_job/batch/categories.rb +26 -24
- data/lib/rocket_job/batch/io.rb +128 -128
- data/lib/rocket_job/batch/worker.rb +14 -12
- data/lib/rocket_job/category/base.rb +10 -7
- data/lib/rocket_job/category/input.rb +61 -1
- data/lib/rocket_job/category/output.rb +9 -0
- data/lib/rocket_job/dirmon_entry.rb +1 -1
- data/lib/rocket_job/job_exception.rb +1 -1
- data/lib/rocket_job/jobs/conversion_job.rb +21 -17
- data/lib/rocket_job/jobs/dirmon_job.rb +24 -35
- data/lib/rocket_job/jobs/housekeeping_job.rb +4 -5
- data/lib/rocket_job/jobs/on_demand_batch_job.rb +11 -5
- data/lib/rocket_job/jobs/on_demand_job.rb +6 -2
- data/lib/rocket_job/jobs/upload_file_job.rb +4 -0
- data/lib/rocket_job/plugins/cron.rb +60 -20
- data/lib/rocket_job/plugins/job/persistence.rb +36 -0
- data/lib/rocket_job/plugins/restart.rb +3 -110
- data/lib/rocket_job/plugins/state_machine.rb +2 -2
- data/lib/rocket_job/plugins/throttle_dependent_jobs.rb +10 -5
- data/lib/rocket_job/sliced/bzip2_output_slice.rb +18 -19
- data/lib/rocket_job/sliced/compressed_slice.rb +3 -6
- data/lib/rocket_job/sliced/encrypted_bzip2_output_slice.rb +49 -0
- data/lib/rocket_job/sliced/encrypted_slice.rb +4 -6
- data/lib/rocket_job/sliced/input.rb +42 -54
- data/lib/rocket_job/sliced/slice.rb +7 -3
- data/lib/rocket_job/sliced/slices.rb +12 -9
- data/lib/rocket_job/sliced/writer/input.rb +46 -18
- data/lib/rocket_job/sliced.rb +1 -19
- data/lib/rocket_job/subscribers/secret_config.rb +17 -0
- data/lib/rocket_job/supervisor.rb +1 -0
- data/lib/rocket_job/version.rb +1 -1
- data/lib/rocketjob.rb +4 -3
- metadata +11 -12
- data/lib/rocket_job/batch/tabular/input.rb +0 -133
- data/lib/rocket_job/batch/tabular/output.rb +0 -67
- data/lib/rocket_job/batch/tabular.rb +0 -58
|
@@ -13,6 +13,8 @@ module RocketJob
|
|
|
13
13
|
# false: do not save nil values to the output categories.
|
|
14
14
|
field :nils, type: ::Mongoid::Boolean, default: false
|
|
15
15
|
|
|
16
|
+
validates_inclusion_of :serializer, in: %i[none compress encrypt bz2 encrypted_bz2 bzip2]
|
|
17
|
+
|
|
16
18
|
# Renders [String] the header line.
|
|
17
19
|
# Returns [nil] if no header is needed.
|
|
18
20
|
def render_header
|
|
@@ -20,6 +22,13 @@ module RocketJob
|
|
|
20
22
|
|
|
21
23
|
tabular.render_header
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
def data_store(job)
|
|
27
|
+
RocketJob::Sliced::Output.new(
|
|
28
|
+
collection_name: build_collection_name(:output, job),
|
|
29
|
+
slice_class: serializer_class
|
|
30
|
+
)
|
|
31
|
+
end
|
|
23
32
|
end
|
|
24
33
|
end
|
|
25
34
|
end
|
|
@@ -173,7 +173,7 @@ module RocketJob
|
|
|
173
173
|
counts
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
-
#
|
|
176
|
+
# Yields [IOStreams::Path] for each file found that matches the current pattern.
|
|
177
177
|
def each
|
|
178
178
|
SemanticLogger.named_tagged(dirmon_entry: id.to_s) do
|
|
179
179
|
# Case insensitive filename matching
|
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
# Convert to and from CSV, JSON, xlsx, and PSV files.
|
|
2
2
|
#
|
|
3
3
|
# Example, Convert CSV file to JSON.
|
|
4
|
-
# job = RocketJob::ConversionJob.new
|
|
5
|
-
# job.
|
|
4
|
+
# job = RocketJob::Jobs::ConversionJob.new
|
|
5
|
+
# job.input_category.file_name = "data.csv"
|
|
6
6
|
# job.output_category.file_name = "data.json"
|
|
7
7
|
# job.save!
|
|
8
8
|
#
|
|
9
9
|
# Example, Convert JSON file to PSV and compress it with GZip.
|
|
10
|
-
# job = RocketJob::ConversionJob.new
|
|
11
|
-
# job.
|
|
10
|
+
# job = RocketJob::Jobs::ConversionJob.new
|
|
11
|
+
# job.input_category.file_name = "data.json"
|
|
12
12
|
# job.output_category.file_name = "data.psv.gz"
|
|
13
13
|
# job.save!
|
|
14
14
|
#
|
|
15
15
|
# Example, Read a CSV file that has been zipped from a remote website and the convert it to a GZipped json file.
|
|
16
|
-
# job = RocketJob::ConversionJob.new
|
|
17
|
-
# job.
|
|
16
|
+
# job = RocketJob::Jobs::ConversionJob.new
|
|
17
|
+
# job.input_category.file_name = "https://example.org/file.zip"
|
|
18
18
|
# job.output_category.file_name = "data.json.gz"
|
|
19
19
|
# job.save!
|
|
20
20
|
#
|
|
21
21
|
module RocketJob
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
module Jobs
|
|
23
|
+
class ConversionJob < RocketJob::Job
|
|
24
|
+
include RocketJob::Batch
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
self.destroy_on_complete = false
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Detects file extension for its type
|
|
29
|
+
input_category format: :auto
|
|
30
|
+
output_category format: :auto
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
# Upload the file specified in `input_category.file_name` unless already uploaded.
|
|
33
|
+
before_batch :upload, unless: :record_count
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# When the job completes it will write the result to `output_category.file_name`.
|
|
36
|
+
after_batch :cleanup!, :download
|
|
37
|
+
|
|
38
|
+
def perform(hash)
|
|
39
|
+
hash
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -30,59 +30,48 @@ module RocketJob
|
|
|
30
30
|
#
|
|
31
31
|
# If another DirmonJob instance is already queued or running, then the create
|
|
32
32
|
# above will fail with:
|
|
33
|
-
#
|
|
33
|
+
# Validation failed: State Another instance of this job is already queued or running
|
|
34
34
|
#
|
|
35
35
|
# Or to start DirmonJob and ignore errors if already running
|
|
36
36
|
# RocketJob::Jobs::DirmonJob.create
|
|
37
37
|
class DirmonJob < RocketJob::Job
|
|
38
|
-
|
|
39
|
-
include RocketJob::Plugins::Singleton
|
|
40
|
-
# Start a new job when this one completes, fails, or aborts
|
|
41
|
-
include RocketJob::Plugins::Restart
|
|
38
|
+
include RocketJob::Plugins::Cron
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
# Runs every 5 minutes by default
|
|
41
|
+
self.cron_schedule = "*/5 * * * * UTC"
|
|
42
|
+
self.description = "Directory Monitor"
|
|
43
|
+
self.priority = 30
|
|
47
44
|
|
|
48
45
|
# Hash[file_name, size]
|
|
49
46
|
field :previous_file_names, type: Hash, default: {}, copy_on_restart: true
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Iterate over each Dirmon entry looking for new files
|
|
54
|
-
# If a new file is found, it is not processed immediately, instead
|
|
55
|
-
# it is passed to the next run of this job along with the file size.
|
|
56
|
-
# If the file size has not changed, the Job is kicked off.
|
|
48
|
+
# Checks the directories for new files, starting jobs if files have not changed since the last run.
|
|
57
49
|
def perform
|
|
58
50
|
check_directories
|
|
59
51
|
end
|
|
60
52
|
|
|
61
53
|
private
|
|
62
54
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Checks the directories for new files, starting jobs if files have not changed
|
|
69
|
-
# since the last run
|
|
55
|
+
# Iterate over each Dirmon Entry looking for new files
|
|
56
|
+
# If a new file is found, it is not processed immediately, instead
|
|
57
|
+
# it is passed to the next run of this job along with the file size.
|
|
58
|
+
# If the file size has not changed, the Job is kicked off.
|
|
70
59
|
def check_directories
|
|
71
60
|
new_file_names = {}
|
|
72
|
-
DirmonEntry.enabled.each do |
|
|
73
|
-
|
|
74
|
-
# S3 files are only visible once completely uploaded.
|
|
75
|
-
unless
|
|
76
|
-
logger.info("File: #{
|
|
77
|
-
|
|
61
|
+
DirmonEntry.enabled.each do |dirmon_entry|
|
|
62
|
+
dirmon_entry.each do |path|
|
|
63
|
+
# Skip file size checking since S3 files are only visible once completely uploaded.
|
|
64
|
+
unless path.partial_files_visible?
|
|
65
|
+
logger.info("File: #{path}. Starting: #{dirmon_entry.job_class_name}")
|
|
66
|
+
dirmon_entry.later(path)
|
|
78
67
|
next
|
|
79
68
|
end
|
|
80
69
|
|
|
81
70
|
# BSON Keys cannot contain periods
|
|
82
|
-
key =
|
|
71
|
+
key = path.to_s.tr(".", "_")
|
|
83
72
|
previous_size = previous_file_names[key]
|
|
84
73
|
# Check every few minutes for a file size change before trying to process the file.
|
|
85
|
-
size = check_file(
|
|
74
|
+
size = check_file(dirmon_entry, path, previous_size)
|
|
86
75
|
new_file_names[key] = size if size
|
|
87
76
|
end
|
|
88
77
|
end
|
|
@@ -91,14 +80,14 @@ module RocketJob
|
|
|
91
80
|
|
|
92
81
|
# Checks if a file should result in starting a job
|
|
93
82
|
# Returns [Integer] file size, or nil if the file started a job
|
|
94
|
-
def check_file(
|
|
95
|
-
size =
|
|
83
|
+
def check_file(dirmon_entry, path, previous_size)
|
|
84
|
+
size = path.size
|
|
96
85
|
if previous_size && (previous_size == size)
|
|
97
|
-
logger.info("File stabilized: #{
|
|
98
|
-
|
|
86
|
+
logger.info("File stabilized: #{path}. Starting: #{dirmon_entry.job_class_name}")
|
|
87
|
+
dirmon_entry.later(path)
|
|
99
88
|
nil
|
|
100
89
|
else
|
|
101
|
-
logger.info("Found file: #{
|
|
90
|
+
logger.info("Found file: #{path}. File size: #{size}")
|
|
102
91
|
# Keep for the next run
|
|
103
92
|
size
|
|
104
93
|
end
|
|
@@ -27,12 +27,11 @@ module RocketJob
|
|
|
27
27
|
# )
|
|
28
28
|
class HousekeepingJob < RocketJob::Job
|
|
29
29
|
include RocketJob::Plugins::Cron
|
|
30
|
-
include RocketJob::Plugins::Singleton
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
self.
|
|
31
|
+
# Runs every 15 minutes on the 15 minute period
|
|
32
|
+
self.cron_schedule = "0,15,30,45 * * * * UTC"
|
|
33
|
+
self.description = "Cleans out historical jobs, and zombie servers."
|
|
34
|
+
self.priority = 25
|
|
36
35
|
|
|
37
36
|
# Whether to destroy zombie servers automatically
|
|
38
37
|
field :destroy_zombies, type: Mongoid::Boolean, default: true, user_editable: true, copy_on_restart: true
|
|
@@ -65,27 +65,29 @@ module RocketJob
|
|
|
65
65
|
module Jobs
|
|
66
66
|
class OnDemandBatchJob < RocketJob::Job
|
|
67
67
|
include RocketJob::Plugins::Cron
|
|
68
|
+
include RocketJob::Plugins::Retry
|
|
68
69
|
include RocketJob::Batch
|
|
69
70
|
include RocketJob::Batch::Statistics
|
|
70
71
|
|
|
71
72
|
self.priority = 90
|
|
72
|
-
self.description = "Batch Job"
|
|
73
|
+
self.description = "On Demand Batch Job"
|
|
73
74
|
self.destroy_on_complete = false
|
|
75
|
+
self.retry_limit = 0
|
|
74
76
|
|
|
75
77
|
# Code that is performed against every row / record.
|
|
76
|
-
field :code, type: String
|
|
78
|
+
field :code, type: String, user_editable: true, copy_on_restart: true
|
|
77
79
|
|
|
78
80
|
# Optional code to execute before the batch is run.
|
|
79
81
|
# Usually to upload data into the job.
|
|
80
|
-
field :before_code, type: String
|
|
82
|
+
field :before_code, type: String, user_editable: true, copy_on_restart: true
|
|
81
83
|
|
|
82
84
|
# Optional code to execute after the batch is run.
|
|
83
85
|
# Usually to upload data into the job.
|
|
84
|
-
field :after_code, type: String
|
|
86
|
+
field :after_code, type: String, user_editable: true, copy_on_restart: true
|
|
85
87
|
|
|
86
88
|
# Data that is made available to the job during the perform.
|
|
87
89
|
# Be sure to store key names only as Strings, not Symbols.
|
|
88
|
-
field :data, type: Hash, default: {}
|
|
90
|
+
field :data, type: Hash, default: {}, user_editable: true, copy_on_restart: true
|
|
89
91
|
|
|
90
92
|
validates :code, presence: true
|
|
91
93
|
validate :validate_code
|
|
@@ -143,6 +145,10 @@ module RocketJob
|
|
|
143
145
|
rescue Exception => e
|
|
144
146
|
errors.add(field, "Failed to load :#{field}, #{e.inspect}")
|
|
145
147
|
end
|
|
148
|
+
|
|
149
|
+
# Allow multiple instances of this job to run with the same cron schedule
|
|
150
|
+
def rocket_job_cron_singleton_check
|
|
151
|
+
end
|
|
146
152
|
end
|
|
147
153
|
end
|
|
148
154
|
end
|
|
@@ -78,8 +78,8 @@ module RocketJob
|
|
|
78
78
|
self.retry_limit = 0
|
|
79
79
|
|
|
80
80
|
# Be sure to store key names only as Strings, not Symbols
|
|
81
|
-
field :data, type: Hash, default: {}, copy_on_restart: true
|
|
82
|
-
field :code, type: String, copy_on_restart: true
|
|
81
|
+
field :data, type: Hash, default: {}, user_editable: true, copy_on_restart: true
|
|
82
|
+
field :code, type: String, user_editable: true, copy_on_restart: true
|
|
83
83
|
|
|
84
84
|
validates :code, presence: true
|
|
85
85
|
validate :validate_code
|
|
@@ -97,6 +97,10 @@ module RocketJob
|
|
|
97
97
|
rescue Exception => e
|
|
98
98
|
errors.add(:code, "Failed to parse :code, #{e.inspect}")
|
|
99
99
|
end
|
|
100
|
+
|
|
101
|
+
# Allow multiple instances of this job to run with the same cron schedule
|
|
102
|
+
def rocket_job_cron_singleton_check
|
|
103
|
+
end
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
end
|
|
@@ -57,6 +57,10 @@ module RocketJob
|
|
|
57
57
|
|
|
58
58
|
def upload_file(job)
|
|
59
59
|
if job.respond_to?(:upload)
|
|
60
|
+
# Return the database connection for this thread back to the connection pool
|
|
61
|
+
# in case the upload takes a long time and the database connection expires.
|
|
62
|
+
ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
|
|
63
|
+
|
|
60
64
|
if original_file_name
|
|
61
65
|
job.upload(upload_file_name, file_name: original_file_name)
|
|
62
66
|
else
|
|
@@ -14,41 +14,81 @@ module RocketJob
|
|
|
14
14
|
extend ActiveSupport::Concern
|
|
15
15
|
|
|
16
16
|
included do
|
|
17
|
-
include Restart
|
|
18
|
-
|
|
19
17
|
field :cron_schedule, type: String, class_attribute: true, user_editable: true, copy_on_restart: true
|
|
20
18
|
|
|
19
|
+
# Whether to prevent another instance of this job from running with the exact _same_ cron schedule.
|
|
20
|
+
# Another job instance with a different `cron_schedule` string is permitted.
|
|
21
|
+
field :cron_singleton, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true, copy_on_restart: true
|
|
22
|
+
|
|
23
|
+
# Whether to re-schedule the next job occurrence when this job starts, or when it is complete.
|
|
24
|
+
#
|
|
25
|
+
# `true`: Create a new scheduled instance of this job after it has started. (Default)
|
|
26
|
+
# - Ensures that the next scheduled instance is not missed because the current instance is still running.
|
|
27
|
+
# - Any changes to fields marked with `copy_on_restart` of `true` will be saved to the new scheduled instance
|
|
28
|
+
# _only_ if they were changed during an `after_start` callback.
|
|
29
|
+
# Changes to these during other callbacks or during the `perform` will not be saved to the new scheduled
|
|
30
|
+
# instance.
|
|
31
|
+
# - To prevent this job creating any new duplicate instances during subsequent processing,
|
|
32
|
+
# its `cron_schedule` is set to `nil`.
|
|
33
|
+
#
|
|
34
|
+
# `false`: Create a new scheduled instance of this job on `fail`, or `abort`.
|
|
35
|
+
# - Prevents the next scheduled instance from running or being scheduled while the current instance is
|
|
36
|
+
# still running.
|
|
37
|
+
# - Any changes to fields marked with `copy_on_restart` of `true` will be saved to the new scheduled instance
|
|
38
|
+
# at any time until after the job has failed, or is aborted.
|
|
39
|
+
# - To prevent this job creating any new duplicate instances during subsequent processing,
|
|
40
|
+
# its `cron_schedule` is set to `nil` after it fails or is aborted.
|
|
41
|
+
field :cron_after_start, type: Mongoid::Boolean, default: true, class_attribute: true, user_editable: true, copy_on_restart: true
|
|
42
|
+
|
|
21
43
|
validates_each :cron_schedule do |record, attr, value|
|
|
22
44
|
record.errors.add(attr, "Invalid cron_schedule: #{value.inspect}") if value && !Fugit::Cron.new(value)
|
|
23
45
|
end
|
|
46
|
+
validate :rocket_job_cron_singleton_check
|
|
47
|
+
|
|
24
48
|
before_save :rocket_job_cron_set_run_at
|
|
25
49
|
|
|
26
|
-
|
|
50
|
+
after_start :rocket_job_cron_on_start
|
|
51
|
+
after_abort :rocket_job_cron_end_state
|
|
52
|
+
after_complete :rocket_job_cron_end_state
|
|
53
|
+
after_fail :rocket_job_cron_end_state
|
|
54
|
+
end
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def rocket_job_restart_new_instance
|
|
31
|
-
return unless cron_schedule
|
|
56
|
+
def rocket_job_cron_set_run_at
|
|
57
|
+
return if cron_schedule.nil? || !(cron_schedule_changed? && !run_at_changed?)
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
self.run_at = Fugit::Cron.new(cron_schedule).next_time.to_utc_time
|
|
60
|
+
end
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
# - create a new instance scheduled to run in the future.
|
|
38
|
-
# - clear out the `cron_schedule` so this instance will not schedule another instance to run on completion.
|
|
39
|
-
# Overrides: RocketJob::Plugins::Restart#rocket_job_restart_abort
|
|
40
|
-
def rocket_job_restart_abort
|
|
41
|
-
return unless cron_schedule
|
|
62
|
+
private
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
def rocket_job_cron_on_start
|
|
65
|
+
return unless cron_schedule && cron_after_start
|
|
66
|
+
|
|
67
|
+
current_cron_schedule = cron_schedule
|
|
68
|
+
update_attribute(:cron_schedule, nil)
|
|
69
|
+
create_restart!(cron_schedule: current_cron_schedule)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rocket_job_cron_end_state
|
|
73
|
+
return unless cron_schedule && !cron_after_start
|
|
74
|
+
|
|
75
|
+
current_cron_schedule = cron_schedule
|
|
76
|
+
update_attribute(:cron_schedule, nil)
|
|
77
|
+
create_restart!(cron_schedule: current_cron_schedule)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns [true|false] whether another instance of this job with the same cron schedule is already active
|
|
81
|
+
def rocket_job_cron_duplicate?
|
|
82
|
+
self.class.with(read: {mode: :primary}) do |conn|
|
|
83
|
+
conn.where(:state.in => %i[queued running failed paused], :id.ne => id, cron_schedule: cron_schedule).exists?
|
|
45
84
|
end
|
|
46
85
|
end
|
|
47
86
|
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
# Prevent creation of a new job when another is running with the same cron schedule.
|
|
88
|
+
def rocket_job_cron_singleton_check
|
|
89
|
+
return if cron_schedule.nil? || completed? || aborted? || !rocket_job_cron_duplicate?
|
|
50
90
|
|
|
51
|
-
self.
|
|
91
|
+
errors.add(:state, "Another instance of #{self.class.name} is already queued, running, failed, or paused with the same cron schedule: #{cron_schedule}")
|
|
52
92
|
end
|
|
53
93
|
end
|
|
54
94
|
end
|
|
@@ -70,6 +70,29 @@ module RocketJob
|
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Create a new instance of this job, copying across only the `copy_on_restart` attributes.
|
|
74
|
+
# Copy across input and output categories to new scheduled job so that all of the
|
|
75
|
+
# settings are remembered between instance. Example: slice_size
|
|
76
|
+
def create_restart!(**overrides)
|
|
77
|
+
if expired?
|
|
78
|
+
logger.info("Job has expired. Not creating a new instance.")
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
job_attrs = self.class.rocket_job_restart_attributes.each_with_object({}) do |attr, attrs|
|
|
83
|
+
attrs[attr] = send(attr)
|
|
84
|
+
end
|
|
85
|
+
job_attrs.merge!(overrides)
|
|
86
|
+
|
|
87
|
+
job = self.class.new(job_attrs)
|
|
88
|
+
job.input_categories = input_categories if respond_to?(:input_categories)
|
|
89
|
+
job.output_categories = output_categories if respond_to?(:output_categories)
|
|
90
|
+
|
|
91
|
+
job.save_with_retry!
|
|
92
|
+
|
|
93
|
+
logger.info("Created a new job instance: #{job.id}")
|
|
94
|
+
end
|
|
95
|
+
|
|
73
96
|
# Set in-memory job to complete if `destroy_on_complete` and the job has been destroyed
|
|
74
97
|
def reload
|
|
75
98
|
return super unless destroy_on_complete
|
|
@@ -85,6 +108,19 @@ module RocketJob
|
|
|
85
108
|
self
|
|
86
109
|
end
|
|
87
110
|
end
|
|
111
|
+
|
|
112
|
+
# Save with retry in case persistence takes a moment.
|
|
113
|
+
def save_with_retry!(retry_limit = 10, sleep_interval = 0.5)
|
|
114
|
+
count = 0
|
|
115
|
+
while count < retry_limit
|
|
116
|
+
return true if save
|
|
117
|
+
|
|
118
|
+
logger.info("Retrying to persist new scheduled instance: #{errors.messages.inspect}")
|
|
119
|
+
sleep(sleep_interval)
|
|
120
|
+
count += 1
|
|
121
|
+
end
|
|
122
|
+
save!
|
|
123
|
+
end
|
|
88
124
|
end
|
|
89
125
|
end
|
|
90
126
|
end
|
|
@@ -2,128 +2,21 @@ require "active_support/concern"
|
|
|
2
2
|
|
|
3
3
|
module RocketJob
|
|
4
4
|
module Plugins
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Notes:
|
|
8
|
-
# * Restartable jobs automatically abort if they fail. This prevents the failed job from being retried.
|
|
9
|
-
# - To disable this behavior, add the following empty method:
|
|
10
|
-
# def rocket_job_restart_abort
|
|
11
|
-
# end
|
|
12
|
-
# * On destroy this job is destroyed without starting a new instance.
|
|
13
|
-
# * On Abort a new instance is created.
|
|
14
|
-
# * Include `RocketJob::Plugins::Singleton` to prevent multiple copies of a job from running at
|
|
15
|
-
# the same time.
|
|
16
|
-
# * The job will not be restarted if:
|
|
17
|
-
# - A validation fails after creating the new instance of this job.
|
|
18
|
-
# - The job has expired.
|
|
19
|
-
# * Only the fields that have `copy_on_restart: true` will be passed onto the new instance of this job.
|
|
20
|
-
#
|
|
21
|
-
# Example:
|
|
22
|
-
#
|
|
23
|
-
# class RestartableJob < RocketJob::Job
|
|
24
|
-
# include RocketJob::Plugins::Restart
|
|
25
|
-
#
|
|
26
|
-
# # Retain the completed job under the completed tab in Rocket Job Web Interface.
|
|
27
|
-
# self.destroy_on_complete = false
|
|
28
|
-
#
|
|
29
|
-
# # Will be copied to the new job on restart.
|
|
30
|
-
# field :limit, type: Integer, copy_on_restart: true
|
|
31
|
-
#
|
|
32
|
-
# # Will _not_ be copied to the new job on restart.
|
|
33
|
-
# field :list, type: Array, default: [1,2,3]
|
|
34
|
-
#
|
|
35
|
-
# # Set run_at every time a new instance of the job is created.
|
|
36
|
-
# after_initialize set_run_at, if: :new_record?
|
|
37
|
-
#
|
|
38
|
-
# def perform
|
|
39
|
-
# puts "The limit is #{limit}"
|
|
40
|
-
# puts "The list is #{list}"
|
|
41
|
-
# 'DONE'
|
|
42
|
-
# end
|
|
43
|
-
#
|
|
44
|
-
# private
|
|
45
|
-
#
|
|
46
|
-
# # Run this job in 30 minutes.
|
|
47
|
-
# def set_run_at
|
|
48
|
-
# self.run_at = 30.minutes.from_now
|
|
49
|
-
# end
|
|
50
|
-
# end
|
|
51
|
-
#
|
|
52
|
-
# job = RestartableJob.create!(limit: 10, list: [4,5,6])
|
|
53
|
-
# job.reload.state
|
|
54
|
-
# # => :queued
|
|
55
|
-
#
|
|
56
|
-
# job.limit
|
|
57
|
-
# # => 10
|
|
58
|
-
#
|
|
59
|
-
# job.list
|
|
60
|
-
# # => [4,5,6]
|
|
61
|
-
#
|
|
62
|
-
# # Wait 30 minutes ...
|
|
63
|
-
#
|
|
64
|
-
# job.reload.state
|
|
65
|
-
# # => :completed
|
|
66
|
-
#
|
|
67
|
-
# # A new instance was automatically created.
|
|
68
|
-
# job2 = RestartableJob.last
|
|
69
|
-
# job2.state
|
|
70
|
-
# # => :queued
|
|
71
|
-
#
|
|
72
|
-
# job2.limit
|
|
73
|
-
# # => 10
|
|
74
|
-
#
|
|
75
|
-
# job2.list
|
|
76
|
-
# # => [1,2,3]
|
|
5
|
+
# @deprecated
|
|
77
6
|
module Restart
|
|
78
7
|
extend ActiveSupport::Concern
|
|
79
8
|
|
|
80
9
|
included do
|
|
81
|
-
after_abort :
|
|
82
|
-
after_complete :
|
|
10
|
+
after_abort :create_restart!
|
|
11
|
+
after_complete :create_restart!
|
|
83
12
|
after_fail :rocket_job_restart_abort
|
|
84
13
|
end
|
|
85
14
|
|
|
86
15
|
private
|
|
87
16
|
|
|
88
|
-
# Run again in the future, even if this run fails with an exception
|
|
89
|
-
def rocket_job_restart_new_instance
|
|
90
|
-
if expired?
|
|
91
|
-
logger.info("Job has expired. Not creating a new instance.")
|
|
92
|
-
return
|
|
93
|
-
end
|
|
94
|
-
job_attrs =
|
|
95
|
-
rocket_job_restart_attributes.each_with_object({}) { |attr, attrs| attrs[attr] = send(attr) }
|
|
96
|
-
job = self.class.new(job_attrs)
|
|
97
|
-
|
|
98
|
-
# Copy across input and output categories to new scheduled job so that all of the
|
|
99
|
-
# settings are remembered between instance. Example: slice_size
|
|
100
|
-
job.input_categories = input_categories if respond_to?(:input_categories)
|
|
101
|
-
job.output_categories = output_categories if respond_to?(:output_categories)
|
|
102
|
-
|
|
103
|
-
rocket_job_restart_save(job)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
17
|
def rocket_job_restart_abort
|
|
107
18
|
new_record? ? abort : abort!
|
|
108
19
|
end
|
|
109
|
-
|
|
110
|
-
# Allow Singleton to prevent the creation of a new job if one is already running
|
|
111
|
-
# Retry since the delete may not have persisted to disk yet.
|
|
112
|
-
def rocket_job_restart_save(job, retry_limit = 10, sleep_interval = 0.5)
|
|
113
|
-
count = 0
|
|
114
|
-
while count < retry_limit
|
|
115
|
-
if job.save
|
|
116
|
-
logger.info("Created a new job instance: #{job.id}")
|
|
117
|
-
return true
|
|
118
|
-
else
|
|
119
|
-
logger.info("Job already active, retrying after a short sleep")
|
|
120
|
-
sleep(sleep_interval)
|
|
121
|
-
end
|
|
122
|
-
count += 1
|
|
123
|
-
end
|
|
124
|
-
logger.error("New job instance not started: #{job.errors.messages.inspect}")
|
|
125
|
-
false
|
|
126
|
-
end
|
|
127
20
|
end
|
|
128
21
|
end
|
|
129
22
|
end
|
|
@@ -36,8 +36,8 @@ module RocketJob
|
|
|
36
36
|
raise(ArgumentError, "Cannot supply both a method name and a block") if methods.size.positive? && block
|
|
37
37
|
raise(ArgumentError, "Must supply either a method name or a block") unless methods.size.positive? || block
|
|
38
38
|
|
|
39
|
-
#
|
|
40
|
-
# For example:
|
|
39
|
+
# Limitation with AASM. It only supports guards on event transitions, not for callbacks.
|
|
40
|
+
# For example, AASM does not support callback options such as :if and :unless, yet Rails callbacks do.
|
|
41
41
|
# before_start :my_callback, unless: :encrypted?
|
|
42
42
|
# before_start :my_callback, if: :encrypted?
|
|
43
43
|
event = aasm.state_machine.events[event_name]
|
|
@@ -11,17 +11,22 @@ module RocketJob
|
|
|
11
11
|
extend ActiveSupport::Concern
|
|
12
12
|
|
|
13
13
|
included do
|
|
14
|
-
class_attribute :
|
|
15
|
-
self.dependent_jobs = nil
|
|
14
|
+
field :dependent_jobs, type: Array, class_attribute: true, user_editable: true, copy_on_restart: true
|
|
16
15
|
|
|
17
|
-
define_throttle :
|
|
18
|
-
define_batch_throttle :
|
|
16
|
+
define_throttle :dependent_jobs_running?
|
|
17
|
+
define_batch_throttle :dependent_jobs_running? if respond_to?(:define_batch_throttle)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class_methods do
|
|
21
|
+
def depends_on_job(*jobs)
|
|
22
|
+
self.dependent_jobs = Array(jobs).collect(&:to_s)
|
|
23
|
+
end
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
private
|
|
22
27
|
|
|
23
28
|
# Checks if there are any dependent jobs are running
|
|
24
|
-
def
|
|
29
|
+
def dependent_jobs_running?
|
|
25
30
|
return false if dependent_jobs.blank?
|
|
26
31
|
|
|
27
32
|
jobs_count = RocketJob::Job.running.where(:_type.in => dependent_jobs).count
|