rocketjob 6.0.0.rc3 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -0
- data/lib/rocket_job/batch/categories.rb +24 -20
- 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/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 +7 -5
- data/lib/rocket_job/jobs/on_demand_job.rb +2 -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 +1 -2
- 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/version.rb +1 -1
- data/lib/rocketjob.rb +2 -2
- metadata +8 -10
- 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
@@ -67,6 +67,8 @@ module RocketJob
|
|
67
67
|
# Returns [Integer] the number of records processed in the slice
|
68
68
|
#
|
69
69
|
# Note: The slice will be removed from processing when this method completes
|
70
|
+
#
|
71
|
+
# @deprecated Please open a ticket if you need this behavior.
|
70
72
|
def work_first_slice(&block)
|
71
73
|
raise "#work_first_slice can only be called from within before_batch callbacks" unless sub_state == :before
|
72
74
|
|
@@ -142,19 +144,19 @@ module RocketJob
|
|
142
144
|
# Perform individual slice without callbacks
|
143
145
|
def rocket_job_perform_slice(slice, &block)
|
144
146
|
slice.processing_record_number ||= 0
|
145
|
-
records = []
|
146
147
|
append = false
|
147
148
|
|
148
|
-
# Skip processed records in this slice if it has no output
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
149
|
+
# Skip processed records in this slice if it has no output categories.
|
150
|
+
records =
|
151
|
+
if slice.processing_record_number.to_i > 1
|
152
|
+
append = true
|
153
|
+
logger.info("Resuming previously incomplete slice from record number #{slice.processing_record_number}")
|
154
|
+
slice.records[slice.processing_record_number - 1..-1]
|
155
|
+
else
|
156
|
+
# Reprocess all records in this slice.
|
157
|
+
slice.processing_record_number = 0
|
158
|
+
slice.records
|
159
|
+
end
|
158
160
|
|
159
161
|
count = 0
|
160
162
|
RocketJob::Sliced::Writer::Output.collect(self, input_slice: slice, append: append) do |writer|
|
@@ -246,7 +248,7 @@ module RocketJob
|
|
246
248
|
unless new_record?
|
247
249
|
# Fail job iff no other worker has already finished it
|
248
250
|
# Must set write concern to at least 1 since we need the nModified back
|
249
|
-
result
|
251
|
+
result = self.class.with(write: {w: 1}) do |query|
|
250
252
|
query.
|
251
253
|
where(id: id, state: :running, sub_state: :processing).
|
252
254
|
update({"$set" => {state: :failed, worker_name: worker_name}})
|
@@ -11,7 +11,6 @@ module RocketJob
|
|
11
11
|
|
12
12
|
# Whether to compress, encrypt, or use the bzip2 serialization for data in this category.
|
13
13
|
field :serializer, type: ::Mongoid::StringifiedSymbol, default: :compress
|
14
|
-
validates_inclusion_of :serializer, in: %i[none compress encrypt bzip2]
|
15
14
|
|
16
15
|
# The header columns when the file does not include a header row.
|
17
16
|
# Note:
|
@@ -49,10 +48,12 @@ module RocketJob
|
|
49
48
|
Sliced::CompressedSlice
|
50
49
|
when :encrypt
|
51
50
|
Sliced::EncryptedSlice
|
52
|
-
when :bzip2
|
51
|
+
when :bzip2, :bz2
|
53
52
|
Sliced::BZip2OutputSlice
|
53
|
+
when :encrypted_bz2
|
54
|
+
Sliced::EncryptedBZip2OutputSlice
|
54
55
|
else
|
55
|
-
raise(ArgumentError, "serialize: #{serializer.inspect} must be :none, :compress, :encrypt, or :
|
56
|
+
raise(ArgumentError, "serialize: #{serializer.inspect} must be :none, :compress, :encrypt, :bz2, or :encrypted_bz2")
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
@@ -65,14 +66,16 @@ module RocketJob
|
|
65
66
|
)
|
66
67
|
end
|
67
68
|
|
68
|
-
def reset_tabular
|
69
|
-
@tabular = nil
|
70
|
-
end
|
71
|
-
|
72
69
|
# Returns [true|false] whether this category has the attributes defined for tabular to work.
|
73
70
|
def tabular?
|
74
71
|
format.present?
|
75
72
|
end
|
73
|
+
|
74
|
+
def build_collection_name(direction, job)
|
75
|
+
collection_name = "rocket_job.#{direction}s.#{job.id}"
|
76
|
+
collection_name << ".#{name}" unless name == :main
|
77
|
+
collection_name
|
78
|
+
end
|
76
79
|
end
|
77
80
|
end
|
78
81
|
end
|
@@ -10,6 +10,7 @@ module RocketJob
|
|
10
10
|
|
11
11
|
# Slice size for this input collection
|
12
12
|
field :slice_size, type: Integer, default: 100
|
13
|
+
validates_presence_of :slice_size
|
13
14
|
|
14
15
|
#
|
15
16
|
# The fields below only apply if the field `format` has been set:
|
@@ -82,7 +83,7 @@ module RocketJob
|
|
82
83
|
field :header_cleanser, type: ::Mongoid::StringifiedSymbol, default: :default
|
83
84
|
validates :header_cleanser, inclusion: %i[default none]
|
84
85
|
|
85
|
-
|
86
|
+
validates_inclusion_of :serializer, in: %i[none compress encrypt]
|
86
87
|
|
87
88
|
# Cleanses the header column names when `cleanse_header` is true
|
88
89
|
def cleanse_header!
|
@@ -105,6 +106,65 @@ module RocketJob
|
|
105
106
|
skip_unknown: skip_unknown
|
106
107
|
)
|
107
108
|
end
|
109
|
+
|
110
|
+
def data_store(job)
|
111
|
+
RocketJob::Sliced::Input.new(
|
112
|
+
collection_name: build_collection_name(:input, job),
|
113
|
+
slice_class: serializer_class,
|
114
|
+
slice_size: slice_size
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns [IOStreams::Path] of file to upload.
|
119
|
+
# Auto-detects file format from file name when format is :auto.
|
120
|
+
def upload_path(stream = nil, original_file_name: nil)
|
121
|
+
unless stream || file_name
|
122
|
+
raise(ArgumentError, "Either supply a file name to upload, or set input_collection.file_name first")
|
123
|
+
end
|
124
|
+
|
125
|
+
path = IOStreams.new(stream || file_name)
|
126
|
+
path.file_name = original_file_name if original_file_name
|
127
|
+
self.file_name = path.file_name
|
128
|
+
|
129
|
+
# Auto detect the format based on the upload file name if present.
|
130
|
+
if format == :auto
|
131
|
+
self.format = path.format || :csv
|
132
|
+
# Rebuild tabular with new values.
|
133
|
+
@tabular = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Remove non-printable characters from tabular input formats.
|
137
|
+
if tabular?
|
138
|
+
# Cannot change the length of fixed width lines.
|
139
|
+
replace = format == :fixed ? " " : ""
|
140
|
+
path.option_or_stream(:encode, encoding: "UTF-8", cleaner: :printable, replace: replace)
|
141
|
+
end
|
142
|
+
path
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return a lambda to extract the header row from the uploaded file.
|
146
|
+
def extract_header_callback(on_first)
|
147
|
+
return on_first unless tabular? && tabular.header?
|
148
|
+
|
149
|
+
case mode
|
150
|
+
when :line
|
151
|
+
lambda do |line|
|
152
|
+
tabular.parse_header(line)
|
153
|
+
cleanse_header!
|
154
|
+
self.columns = tabular.header.columns
|
155
|
+
# Call chained on_first if present
|
156
|
+
on_first&.call(line)
|
157
|
+
end
|
158
|
+
when :array
|
159
|
+
lambda do |row|
|
160
|
+
tabular.header.columns = row
|
161
|
+
cleanse_header!
|
162
|
+
self.columns = category.tabular.header.columns
|
163
|
+
# Call chained on_first if present
|
164
|
+
on_first&.call(line)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
108
168
|
end
|
109
169
|
end
|
110
170
|
end
|
@@ -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
|
@@ -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
|
@@ -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
|