rocketjob 3.4.3 → 3.5.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +27 -0
  3. data/bin/rocketjob +1 -1
  4. data/lib/rocket_job/active_worker.rb +4 -3
  5. data/lib/rocket_job/cli.rb +13 -12
  6. data/lib/rocket_job/config.rb +17 -13
  7. data/lib/rocket_job/dirmon_entry.rb +88 -91
  8. data/lib/rocket_job/extensions/mongo/logging.rb +8 -4
  9. data/lib/rocket_job/extensions/mongoid/factory.rb +8 -6
  10. data/lib/rocket_job/extensions/rocket_job_adapter.rb +2 -4
  11. data/lib/rocket_job/heartbeat.rb +7 -8
  12. data/lib/rocket_job/job_exception.rb +6 -5
  13. data/lib/rocket_job/jobs/dirmon_job.rb +5 -7
  14. data/lib/rocket_job/jobs/housekeeping_job.rb +3 -2
  15. data/lib/rocket_job/jobs/on_demand_job.rb +104 -0
  16. data/lib/rocket_job/jobs/simple_job.rb +0 -2
  17. data/lib/rocket_job/jobs/upload_file_job.rb +100 -0
  18. data/lib/rocket_job/performance.rb +15 -10
  19. data/lib/rocket_job/plugins/cron.rb +7 -124
  20. data/lib/rocket_job/plugins/document.rb +8 -10
  21. data/lib/rocket_job/plugins/job/callbacks.rb +0 -1
  22. data/lib/rocket_job/plugins/job/logger.rb +0 -1
  23. data/lib/rocket_job/plugins/job/model.rb +15 -20
  24. data/lib/rocket_job/plugins/job/persistence.rb +3 -13
  25. data/lib/rocket_job/plugins/job/state_machine.rb +1 -2
  26. data/lib/rocket_job/plugins/job/throttle.rb +16 -12
  27. data/lib/rocket_job/plugins/job/worker.rb +15 -19
  28. data/lib/rocket_job/plugins/processing_window.rb +2 -2
  29. data/lib/rocket_job/plugins/restart.rb +3 -4
  30. data/lib/rocket_job/plugins/retry.rb +2 -3
  31. data/lib/rocket_job/plugins/singleton.rb +2 -3
  32. data/lib/rocket_job/plugins/state_machine.rb +19 -23
  33. data/lib/rocket_job/rocket_job.rb +4 -5
  34. data/lib/rocket_job/server.rb +35 -41
  35. data/lib/rocket_job/version.rb +2 -2
  36. data/lib/rocket_job/worker.rb +22 -21
  37. data/lib/rocketjob.rb +2 -0
  38. data/test/config/mongoid.yml +2 -2
  39. data/test/config_test.rb +0 -2
  40. data/test/dirmon_entry_test.rb +161 -134
  41. data/test/dirmon_job_test.rb +80 -78
  42. data/test/job_test.rb +0 -2
  43. data/test/jobs/housekeeping_job_test.rb +0 -1
  44. data/test/jobs/on_demand_job_test.rb +59 -0
  45. data/test/jobs/upload_file_job_test.rb +99 -0
  46. data/test/plugins/cron_test.rb +1 -3
  47. data/test/plugins/job/callbacks_test.rb +8 -13
  48. data/test/plugins/job/defaults_test.rb +0 -1
  49. data/test/plugins/job/logger_test.rb +2 -4
  50. data/test/plugins/job/model_test.rb +1 -2
  51. data/test/plugins/job/persistence_test.rb +0 -2
  52. data/test/plugins/job/state_machine_test.rb +0 -2
  53. data/test/plugins/job/throttle_test.rb +6 -8
  54. data/test/plugins/job/worker_test.rb +1 -2
  55. data/test/plugins/processing_window_test.rb +0 -2
  56. data/test/plugins/restart_test.rb +0 -1
  57. data/test/plugins/retry_test.rb +1 -2
  58. data/test/plugins/singleton_test.rb +0 -2
  59. data/test/plugins/state_machine_event_callbacks_test.rb +1 -2
  60. data/test/plugins/state_machine_test.rb +0 -2
  61. data/test/plugins/transaction_test.rb +5 -7
  62. data/test/test_db.sqlite3 +0 -0
  63. data/test/test_helper.rb +2 -1
  64. metadata +42 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 72a9239aa38ba9581931c3bd6f1402278b3c9d41
4
- data.tar.gz: aa360338a810a8853a463149084554978c57774f
2
+ SHA256:
3
+ metadata.gz: 9dd8c3c0ab00ae83d73d63aef3887f62d1d2a695399f263814ad7a5881aa980a
4
+ data.tar.gz: 715304e4587a9eda64c3cb5f73a69cd72ecdbf4ee232884c61f36666ef7c1a3a
5
5
  SHA512:
6
- metadata.gz: 1ba33a298630312129bc76f34084e49a348e27bba34e6f452a7d990a0c96d800c664db58b04724503d2cdcdd944a3120ccf09e583661453fb93832d2d38458b3
7
- data.tar.gz: f141cfbd428307638df1ac8ed20d00e345483370e6c4c40523d9409d2afc6f2d23b8c49d8b27c6eedac4aa72b48d6168dbfd1880b43cd20aaf452fdf223e4ce9
6
+ metadata.gz: 3bc50f4e10068d2b193747c24880dee618be8df6ab58e3093f442332f93c591796901132490a856ee955aa19cb78abaad6034eb7280e080c494676c7c36d3899
7
+ data.tar.gz: 86d8dbe971e8e44a8e4854d1691078ac93a25f84fd8e482b9e84a3669457ad8886f84fc47007f199625e70fcecc1f4b1403804e38dc50acebd5c06b13b11d20b
data/README.md CHANGED
@@ -17,6 +17,33 @@ Checkout http://rocketjob.io/
17
17
  * Questions? Join the chat room on Gitter for [rocketjob support](https://gitter.im/rocketjob/support)
18
18
  * [Report bugs](https://github.com/rocketjob/rocketjob/issues)
19
19
 
20
+ ## Contributing to the documentation
21
+
22
+ To contribute to the documentation it is as easy as forking the repository
23
+ and then editing the markdown pages directly via the github web interface.
24
+
25
+ For more complex documentation changes checkout the source code locally.
26
+
27
+ #### Local checkout
28
+
29
+ * Fork the repository in github.
30
+ * Checkout your fork of the source code locally.
31
+ * Install Jekyll
32
+ ~~~
33
+ cd docs
34
+ bundle update
35
+ ~~~
36
+ * Run Jekyll web server:
37
+ ~~~
38
+ jekyll serve
39
+ ~~~
40
+ * Open a web browser to view the local documentation:
41
+ [http://127.0.0.1:4000](http://127.0.0.1:4000)
42
+ * Edit the files in the `/docs` folder.
43
+ * Refresh the page to see the changes.
44
+
45
+ Once the changes are complete, submit a github pull request.
46
+
20
47
  ## Upgrading to V3
21
48
 
22
49
  V3 replaces MongoMapper with Mongoid which supports the latest MongoDB Ruby client driver.
data/bin/rocketjob CHANGED
@@ -6,7 +6,7 @@ require 'rocket_job/cli'
6
6
  # Start a rocketjob server instance from the command line
7
7
  begin
8
8
  RocketJob::CLI.new(ARGV).run
9
- rescue => exc
9
+ rescue Exception => exc
10
10
  # Failsafe logger that writes to STDERR
11
11
  SemanticLogger.add_appender(io: STDERR, level: :error, formatter: :color)
12
12
  SemanticLogger['RocketJob'].error('Rocket Job shutting down due to exception', exc)
@@ -4,7 +4,8 @@ module RocketJob
4
4
  # When this server started working on this job / slice
5
5
  attr_accessor :started_at
6
6
 
7
- attr_accessor :name, :job
7
+ attr_accessor :job
8
+ attr_reader :name
8
9
 
9
10
  # Returns [Hash<String:ActiveWorker>] hash of all servers sorted by name
10
11
  # and what they are currently working on.
@@ -16,7 +17,7 @@ module RocketJob
16
17
  def self.all(server_name = nil)
17
18
  servers = []
18
19
  # Need paused, failed or aborted since servers may still be working on active slices
19
- query = RocketJob::Job.where(:state.in => [:running, :paused, :failed, :aborted])
20
+ query = RocketJob::Job.where(:state.in => %i[running paused failed aborted])
20
21
  query = query.where(worker_name: /\A#{server_name}/) if server_name
21
22
  query.each do |job|
22
23
  servers += job.rocket_job_active_workers
@@ -50,7 +51,7 @@ module RocketJob
50
51
 
51
52
  # Returns [String] the name of the server running this worker
52
53
  def server_name
53
- if match = name.to_s.match(/(.*:.*):.*/)
54
+ if (match = name.to_s.match(/(.*:.*):.*/))
54
55
  match[1]
55
56
  else
56
57
  name
@@ -4,7 +4,7 @@ require 'mongoid'
4
4
  require 'rocketjob'
5
5
  require 'rocket_job/extensions/mongoid/factory'
6
6
  module RocketJob
7
- # Command Line Interface parser for RocketJob
7
+ # Command Line Interface parser for Rocket Job
8
8
  class CLI
9
9
  include SemanticLogger::Loggable
10
10
  attr_accessor :name, :workers, :environment, :pidfile, :directory, :quiet,
@@ -41,7 +41,7 @@ module RocketJob
41
41
  opts = {}
42
42
  opts[:name] = name if name
43
43
  opts[:max_workers] = workers if workers
44
- opts[:filter] = {:_type => filter} if filter
44
+ opts[:filter] = {_type: filter} if filter
45
45
  Server.run(opts)
46
46
  end
47
47
 
@@ -70,12 +70,12 @@ module RocketJob
70
70
  # Override Rails log level if command line option was supplied
71
71
  SemanticLogger.default_level = log_level.to_sym if log_level
72
72
 
73
- if Rails.configuration.eager_load
74
- logger.measure_info('Eager loaded Rails and all Engines') do
75
- Rails.application.eager_load!
76
- Rails::Engine.subclasses.each(&:eager_load!)
77
- self.class.eager_load_jobs(File.expand_path('jobs', File.dirname(__FILE__)))
78
- end
73
+ return unless Rails.configuration.eager_load
74
+
75
+ logger.measure_info('Eager loaded Rails and all Engines') do
76
+ Rails.application.eager_load!
77
+ Rails::Engine.subclasses.each(&:eager_load!)
78
+ self.class.eager_load_jobs(File.expand_path('jobs', File.dirname(__FILE__)))
79
79
  end
80
80
  end
81
81
 
@@ -86,12 +86,14 @@ module RocketJob
86
86
  require 'bundler/setup'
87
87
  Bundler.require(environment)
88
88
  rescue LoadError
89
+ nil
89
90
  end
90
91
 
91
92
  require 'rocketjob'
92
93
  begin
93
94
  require 'rocketjob_pro'
94
95
  rescue LoadError
96
+ nil
95
97
  end
96
98
 
97
99
  # Log to file except when booting rails, when it will add the log file path
@@ -138,10 +140,10 @@ module RocketJob
138
140
  end
139
141
 
140
142
  # Eager load files in jobs folder
141
- def self.eager_load_jobs(path = 'jobs')
142
- Pathname.glob("#{path}/**/*.rb").each do |path|
143
+ def self.eager_load_jobs(job_path = 'jobs')
144
+ Pathname.glob("#{job_path}/**/*.rb").each do |path|
143
145
  next if path.directory?
144
- logger.debug "Loading #{path.to_s}"
146
+ logger.debug "Loading #{path}"
145
147
  require path.expand_path.to_s
146
148
  end
147
149
  end
@@ -198,6 +200,5 @@ module RocketJob
198
200
  end
199
201
  parser.parse! argv
200
202
  end
201
-
202
203
  end
203
204
  end
@@ -51,22 +51,26 @@ module RocketJob
51
51
  # Configure Mongoid
52
52
  def self.load!(environment = 'development', file_name = nil, encryption_file_name = nil)
53
53
  config_file = file_name ? Pathname.new(file_name) : Pathname.pwd.join('config/mongoid.yml')
54
- if config_file.file?
55
- logger.debug "Reading Mongo configuration from: #{config_file}"
56
- Mongoid.load!(config_file, environment)
57
- else
58
- raise(ArgumentError, "Mongo Configuration file: #{config_file.to_s} not found")
59
- end
54
+
55
+ raise(ArgumentError, "Mongo Configuration file: #{config_file} not found") unless config_file.file?
56
+
57
+ logger.debug "Reading Mongo configuration from: #{config_file}"
58
+ Mongoid.load!(config_file, environment)
60
59
 
61
60
  # Load Encryption configuration file if present
62
- if defined?(SymmetricEncryption)
63
- config_file = encryption_file_name ? Pathname.new(encryption_file_name) : Pathname.pwd.join('config/symmetric-encryption.yml')
64
- if config_file.file?
65
- logger.debug "Reading SymmetricEncryption configuration from: #{config_file}"
66
- SymmetricEncryption.load!(config_file.to_s, environment)
61
+ return unless defined?(SymmetricEncryption)
62
+
63
+ config_file =
64
+ if encryption_file_name
65
+ Pathname.new(encryption_file_name)
66
+ else
67
+ Pathname.pwd.join('config/symmetric-encryption.yml')
67
68
  end
68
- end
69
- end
70
69
 
70
+ return unless config_file.file?
71
+
72
+ logger.debug "Reading SymmetricEncryption configuration from: #{config_file}"
73
+ SymmetricEncryption.load!(config_file.to_s, environment)
74
+ end
71
75
  end
72
76
  end
@@ -6,9 +6,14 @@ module RocketJob
6
6
  include Plugins::Document
7
7
  include Plugins::StateMachine
8
8
 
9
+ # The default archive directory that is used when the job being queued does not respond
10
+ # to #upload, and does not have an `archive_directory` specified in this entry
11
+ class_attribute :default_archive_directory
12
+ self.default_archive_directory = 'archive'.freeze
13
+
9
14
  store_in collection: 'rocket_job.dirmon_entries'
10
15
 
11
- # User defined name used to identify this DirmonEntry in Mission Control
16
+ # User defined name used to identify this DirmonEntry in the Web Interface.
12
17
  field :name, type: String
13
18
 
14
19
  # Pattern for finding files
@@ -48,7 +53,7 @@ module RocketJob
48
53
  # If supplied, the file will be moved to this directory before the job is started
49
54
  # If the file was in a sub-directory, the corresponding sub-directory will
50
55
  # be created in the archive directory.
51
- field :archive_directory, type: String
56
+ field :archive_directory, type: String, default: default_archive_directory
52
57
 
53
58
  # If this DirmonEntry is in the failed state, exception contains the cause
54
59
  embeds_one :exception, class_name: 'RocketJob::JobException'
@@ -59,7 +64,7 @@ module RocketJob
59
64
  # Exceeding this number will result in an exception being logged in a failed Dirmon instance.
60
65
  # Dirmon processing will continue with new instances.
61
66
  # TODO: Implement max_hits
62
- #field :max_hits, type: Integer, default: 100
67
+ # field :max_hits, type: Integer, default: 100
63
68
 
64
69
  #
65
70
  # Read-only attributes
@@ -71,6 +76,11 @@ module RocketJob
71
76
  # Unique index on pattern to help prevent two entries from scanning the same files
72
77
  index({pattern: 1}, background: true, unique: true, drop_dups: true)
73
78
 
79
+ before_validation :strip_whitespace
80
+ validates_presence_of :pattern, :job_class_name, :archive_directory
81
+ validate :job_is_a_rocket_job
82
+ validate :job_has_properties
83
+
74
84
  # State Machine events and transitions
75
85
  #
76
86
  # :pending -> :enabled -> :disabled
@@ -108,27 +118,6 @@ module RocketJob
108
118
  end
109
119
  end
110
120
 
111
- # @formatter:on
112
- validates_presence_of :pattern, :job_class_name
113
-
114
- validates_each :job_class_name do |record, attr, value|
115
- exists =
116
- begin
117
- value.nil? ? false : record.job_class.ancestors.include?(RocketJob::Job)
118
- rescue NameError
119
- false
120
- end
121
- record.errors.add(attr, 'job_class_name must be defined and must be derived from RocketJob::Job') unless exists
122
- end
123
-
124
- validates_each :properties do |record, attr, value|
125
- if record.job_class && (methods = record.job_class.instance_methods)
126
- value.each_pair do |k, v|
127
- record.errors.add(attr, "Unknown property: #{k.inspect} with value: #{v}") unless methods.include?("#{k}=".to_sym)
128
- end
129
- end
130
- end
131
-
132
121
  # Security Settings
133
122
  #
134
123
  # A whitelist of paths from which to process files.
@@ -147,7 +136,7 @@ module RocketJob
147
136
  #
148
137
  # Returns [Array<String>] a copy of the whitelisted paths
149
138
  def self.get_whitelist_paths
150
- self.whitelist_paths.dup
139
+ whitelist_paths.dup
151
140
  end
152
141
 
153
142
  # Add a path to the whitelist
@@ -155,8 +144,8 @@ module RocketJob
155
144
  def self.add_whitelist_path(path)
156
145
  # Confirms that path exists
157
146
  path = Pathname.new(path).realpath.to_s
158
- self.whitelist_paths << path
159
- self.whitelist_paths.uniq!
147
+ whitelist_paths << path
148
+ whitelist_paths.uniq!
160
149
  path
161
150
  end
162
151
 
@@ -165,8 +154,8 @@ module RocketJob
165
154
  def self.delete_whitelist_path(path)
166
155
  # Confirms that path exists
167
156
  path = Pathname.new(path).realpath.to_s
168
- self.whitelist_paths.delete(path)
169
- self.whitelist_paths.uniq!
157
+ whitelist_paths.delete(path)
158
+ whitelist_paths.uniq!
170
159
  path
171
160
  end
172
161
 
@@ -193,30 +182,8 @@ module RocketJob
193
182
  counts
194
183
  end
195
184
 
196
- # The default archive directory that is used when the job being queued does not respond
197
- # to #upload, and does not have an `archive_directory` specified in this entry
198
- class_attribute :default_archive_directory
199
-
200
- self.default_archive_directory = '_archive'.freeze
201
-
202
- # Returns [Pathname] the archive_directory if set, otherwise the default_archive_directory
203
- # Creates the archive directory if one is set
204
- def archive_pathname(file_pathname)
205
- if archive_directory
206
- path = Pathname.new(archive_directory)
207
- begin
208
- path.mkpath unless path.exist?
209
- rescue Errno::ENOENT => exc
210
- raise(Errno::ENOENT, "DirmonJob failed to create archive directory: #{path}, #{exc.message}")
211
- end
212
- path.realpath
213
- else
214
- file_pathname.dirname.join(self.class.default_archive_directory).realdirpath
215
- end
216
- end
217
-
218
185
  # Passes each filename [Pathname] found that matches the pattern into the supplied block
219
- def each(&block)
186
+ def each
220
187
  SemanticLogger.named_tagged(dirmon_entry: id.to_s) do
221
188
  # Case insensitive filename matching
222
189
  Pathname.glob(pattern, File::FNM_CASEFOLD).each do |pathname|
@@ -234,7 +201,7 @@ module RocketJob
234
201
  next if file_name.include?(self.class.default_archive_directory)
235
202
 
236
203
  # Security check?
237
- if (whitelist_paths.size > 0) && whitelist_paths.none? { |whitepath| file_name.to_s.start_with?(whitepath) }
204
+ if whitelist_paths.size.positive? && whitelist_paths.none? { |whitepath| file_name.to_s.start_with?(whitepath) }
238
205
  logger.error "Skipping file: #{file_name} since it is not in any of the whitelisted paths: #{whitelist_paths.join(', ')}"
239
206
  next
240
207
  end
@@ -244,7 +211,7 @@ module RocketJob
244
211
  logger.error "Skipping file: #{file_name} since it is not writable by the current user. Must be able to delete/move the file after queueing the job"
245
212
  next
246
213
  end
247
- block.call(pathname)
214
+ yield(pathname)
248
215
  end
249
216
  end
250
217
  end
@@ -264,7 +231,7 @@ module RocketJob
264
231
  end
265
232
  end
266
233
 
267
- # Returns the Job to be queued
234
+ # Returns the Job to be created.
268
235
  def job_class
269
236
  return if job_class_name.nil?
270
237
  job_class_name.constantize
@@ -272,47 +239,44 @@ module RocketJob
272
239
  nil
273
240
  end
274
241
 
275
- # Queues the job for the supplied pathname
242
+ # Archives the file and kicks off a proxy job to upload the file.
276
243
  def later(pathname)
277
- if klass = job_class
278
- logger.measure_info "Enqueued: #{name}, Job class: #{job_class_name}" do
279
- job = klass.new(properties)
280
- upload_file(job, pathname)
281
- job.save!
282
- job
283
- end
284
- else
285
- raise(ArgumentError, "Cannot instantiate a class for: #{job_class_name.inspect}")
286
- end
244
+ job_id = BSON::ObjectId.new
245
+ archived_file_name = archive_file(job_id, pathname)
246
+
247
+ job = RocketJob::Jobs::UploadFileJob.create!(
248
+ job_class_name: job_class_name,
249
+ properties: properties,
250
+ description: "#{name}: #{pathname.basename}",
251
+ upload_file_name: archived_file_name.to_s,
252
+ original_file_name: pathname.to_s,
253
+ job_id: job_id
254
+ )
255
+
256
+ logger.info(
257
+ message: 'Created RocketJob::Jobs::UploadFileJob',
258
+ payload: {
259
+ dirmon_entry_name: name,
260
+ upload_file_name: archived_file_name.to_s,
261
+ original_file_name: pathname.to_s,
262
+ job_class_name: job_class_name,
263
+ job_id: job_id.to_s,
264
+ upload_job_id: job.id.to_s
265
+ }
266
+ )
267
+ job
287
268
  end
288
269
 
289
270
  private
290
271
 
291
- class_attribute :whitelist_paths
292
- self.whitelist_paths = Concurrent::Array.new
293
-
294
- # Upload the file to the job
295
- def upload_file(job, pathname)
296
- if job.respond_to?(:upload)
297
- # With RocketJob Pro the file can be uploaded directly into the Job itself
298
- job.upload(pathname.to_s)
299
- archive_directory ? archive_file(job, pathname) : pathname.unlink
300
- else
301
- upload_default(job, pathname)
302
- end
272
+ # strip whitespaces from all variables that reference paths or patterns
273
+ def strip_whitespace
274
+ self.pattern = pattern.strip unless pattern.nil?
275
+ self.archive_directory = archive_directory.strip unless archive_directory.nil?
303
276
  end
304
277
 
305
- # Archives the file for a job where there was no #upload method
306
- def upload_default(job, pathname)
307
- full_file_name = archive_file(job, pathname)
308
- if job.respond_to?(:upload_file_name=)
309
- job.upload_file_name = full_file_name
310
- elsif job.respond_to?(:full_file_name=)
311
- job.full_file_name = full_file_name
312
- else
313
- raise(ArgumentError, "#{job_class_name} must either have attribute 'upload_file_name' or 'full_file_name'")
314
- end
315
- end
278
+ class_attribute :whitelist_paths
279
+ self.whitelist_paths = Concurrent::Array.new
316
280
 
317
281
  # Move the file to the archive directory
318
282
  #
@@ -322,14 +286,47 @@ module RocketJob
322
286
  #
323
287
  # Note:
324
288
  # - Works across partitions when the file and the archive are on different partitions
325
- def archive_file(job, pathname)
289
+ def archive_file(job_id, pathname)
326
290
  target_path = archive_pathname(pathname)
327
291
  target_path.mkpath
328
- target_file_name = target_path.join("#{job.id}_#{pathname.basename}")
292
+ target_file_name = target_path.join("#{job_id}_#{pathname.basename}")
329
293
  # In case the file is being moved across partitions
330
294
  FileUtils.move(pathname.to_s, target_file_name.to_s)
331
295
  target_file_name.to_s
332
296
  end
333
297
 
298
+ # Returns [Pathname] to the archive directory, and creates it if it does not exist.
299
+ #
300
+ # If `archive_directory` is a relative path, it is appended to the `file_pathname`.
301
+ # If `archive_directory` is an absolute path, it is returned as-is.
302
+ def archive_pathname(file_pathname)
303
+ path = Pathname.new(archive_directory)
304
+ path = file_pathname.dirname.join(archive_directory) if path.relative?
305
+
306
+ begin
307
+ path.mkpath unless path.exist?
308
+ rescue Errno::ENOENT => exc
309
+ raise(Errno::ENOENT, "DirmonJob failed to create archive directory: #{path}, #{exc.message}")
310
+ end
311
+ path.realpath
312
+ end
313
+
314
+ # Validates job_class is a Rocket Job
315
+ def job_is_a_rocket_job
316
+ klass = job_class
317
+ return if job_class_name.nil? || klass&.ancestors&.include?(RocketJob::Job)
318
+ errors.add(:job_class_name, "Job #{job_class_name} must be defined and inherit from RocketJob::Job")
319
+ end
320
+
321
+ # Does the job have all the supplied properties
322
+ def job_has_properties
323
+ klass = job_class
324
+ return unless klass
325
+
326
+ properties.each_pair do |k, _v|
327
+ next if klass.public_method_defined?("#{k}=".to_sym)
328
+ errors.add(:properties, "Unknown Property: Attempted to set a value for #{k.inspect} which is not allowed on the job #{job_class_name}")
329
+ end
330
+ end
334
331
  end
335
332
  end