rocketjob 1.3.0 → 2.0.0.rc1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +201 -0
  3. data/README.md +15 -10
  4. data/bin/rocketjob +3 -1
  5. data/bin/rocketjob_perf +92 -0
  6. data/lib/rocket_job/cli.rb +71 -31
  7. data/lib/rocket_job/config.rb +21 -23
  8. data/lib/rocket_job/dirmon_entry.rb +63 -45
  9. data/lib/rocket_job/extensions/aasm.rb +56 -0
  10. data/lib/rocket_job/extensions/mongo.rb +23 -0
  11. data/lib/rocket_job/job.rb +9 -433
  12. data/lib/rocket_job/jobs/dirmon_job.rb +20 -20
  13. data/lib/rocket_job/jobs/simple_job.rb +12 -0
  14. data/lib/rocket_job/plugins/document.rb +69 -0
  15. data/lib/rocket_job/plugins/job/callbacks.rb +92 -0
  16. data/lib/rocket_job/plugins/job/defaults.rb +40 -0
  17. data/lib/rocket_job/plugins/job/logger.rb +36 -0
  18. data/lib/rocket_job/plugins/job/model.rb +288 -0
  19. data/lib/rocket_job/plugins/job/persistence.rb +167 -0
  20. data/lib/rocket_job/plugins/job/state_machine.rb +166 -0
  21. data/lib/rocket_job/plugins/job/worker.rb +167 -0
  22. data/lib/rocket_job/plugins/restart.rb +54 -0
  23. data/lib/rocket_job/plugins/singleton.rb +26 -0
  24. data/lib/rocket_job/plugins/state_machine.rb +105 -0
  25. data/lib/rocket_job/version.rb +1 -1
  26. data/lib/rocket_job/worker.rb +150 -119
  27. data/lib/rocketjob.rb +43 -21
  28. data/test/config_test.rb +12 -0
  29. data/test/dirmon_entry_test.rb +81 -85
  30. data/test/dirmon_job_test.rb +40 -28
  31. data/test/job_test.rb +14 -257
  32. data/test/plugins/job/callbacks_test.rb +163 -0
  33. data/test/plugins/job/defaults_test.rb +52 -0
  34. data/test/plugins/job/logger_test.rb +58 -0
  35. data/test/plugins/job/model_test.rb +97 -0
  36. data/test/plugins/job/persistence_test.rb +81 -0
  37. data/test/plugins/job/state_machine_test.rb +118 -0
  38. data/test/plugins/job/worker_test.rb +183 -0
  39. data/test/plugins/restart_test.rb +185 -0
  40. data/test/plugins/singleton_test.rb +94 -0
  41. data/test/plugins/state_machine_event_callbacks_test.rb +101 -0
  42. data/test/plugins/state_machine_test.rb +64 -0
  43. data/test/test_helper.rb +3 -36
  44. metadata +64 -19
  45. data/lib/rocket_job/concerns/singleton.rb +0 -33
  46. data/lib/rocket_job/concerns/worker.rb +0 -214
  47. data/test/files/_archive/archived.txt +0 -3
  48. data/test/job_worker_test.rb +0 -86
  49. data/test/jobs/test_job.rb +0 -46
  50. data/test/worker_test.rb +0 -97
@@ -1,11 +1,10 @@
1
- require 'thread_safe'
1
+ require 'concurrent'
2
2
  require 'pathname'
3
3
  require 'fileutils'
4
- require 'aasm'
5
4
  module RocketJob
6
5
  class DirmonEntry
7
- include MongoMapper::Document
8
- include AASM
6
+ include Plugins::Document
7
+ include Plugins::StateMachine
9
8
 
10
9
  # @formatter:off
11
10
  # User defined name used to identify this DirmonEntry in Mission Control
@@ -67,9 +66,6 @@ module RocketJob
67
66
  # be created in the archive directory.
68
67
  key :archive_directory, String
69
68
 
70
- # Method to perform on the job, usually :perform
71
- key :perform_method, Symbol, default: :perform
72
-
73
69
  # If this DirmonEntry is in the failed state, exception contains the cause
74
70
  one :exception, class_name: 'RocketJob::JobException'
75
71
 
@@ -85,7 +81,7 @@ module RocketJob
85
81
  # Read-only attributes
86
82
  #
87
83
 
88
- # Current state, as set by AASM
84
+ # Current state, as set by the state machine. Do not modify directly.
89
85
  key :state, Symbol, default: :pending
90
86
 
91
87
  # State Machine events and transitions
@@ -120,19 +116,13 @@ module RocketJob
120
116
  transitions from: :failed, to: :disabled
121
117
  end
122
118
 
123
- event :fail do
119
+ event :fail, before: :set_exception do
124
120
  transitions from: :enabled, to: :failed
125
121
  end
126
122
  end
127
123
 
128
124
  # @formatter:on
129
- validates_presence_of :pattern, :job_class_name, :perform_method
130
-
131
- validates_each :perform_method do |record, attr, value|
132
- if (klass = record.job_class) && !klass.instance_methods.include?(value)
133
- record.errors.add(attr, "Method not implemented by #{record.job_class_name}")
134
- end
135
- end
125
+ validates_presence_of :pattern, :job_class_name
136
126
 
137
127
  validates_each :job_class_name do |record, attr, value|
138
128
  exists =
@@ -145,8 +135,8 @@ module RocketJob
145
135
  end
146
136
 
147
137
  validates_each :arguments do |record, attr, value|
148
- if (klass = record.job_class) && klass.instance_methods.include?(record.perform_method)
149
- count = klass.argument_count(record.perform_method)
138
+ if klass = record.job_class
139
+ count = klass.rocket_job_argument_count
150
140
  record.errors.add(attr, "There must be #{count} argument(s)") if value.size != count
151
141
  end
152
142
  end
@@ -206,8 +196,39 @@ module RocketJob
206
196
  path
207
197
  end
208
198
 
199
+ # Returns [Hash<String:Integer>] of the number of dirmon entries in each state.
200
+ # Note: If there are no workers in that particular state then the hash will not have a value for it.
201
+ #
202
+ # Example dirmon entries in every state:
203
+ # RocketJob::DirmonEntry.counts_by_state
204
+ # # => {
205
+ # :pending => 1,
206
+ # :enabled => 37,
207
+ # :failed => 1,
208
+ # :disabled => 3
209
+ # }
210
+ #
211
+ # Example no dirmon entries:
212
+ # RocketJob::Job.counts_by_state
213
+ # # => {}
214
+ def self.counts_by_state
215
+ counts = {}
216
+ collection.aggregate([
217
+ {
218
+ '$group' => {
219
+ _id: '$state',
220
+ count: {'$sum' => 1}
221
+ }
222
+ }
223
+ ]
224
+ ).each do |result|
225
+ counts[result['_id']] = result['count']
226
+ end
227
+ counts
228
+ end
229
+
209
230
  # The default archive directory that is used when the job being queued does not respond
210
- # to #file_store_upload or #upload, and do not have an `archive_directory` specified in this entry
231
+ # to #upload, and does not have an `archive_directory` specified in this entry
211
232
  cattr_accessor :default_archive_directory
212
233
 
213
234
  @@default_archive_directory = '_archive'.freeze
@@ -217,7 +238,11 @@ module RocketJob
217
238
  def archive_pathname(file_pathname)
218
239
  if archive_directory
219
240
  path = Pathname.new(archive_directory)
220
- path.mkpath unless path.exist?
241
+ begin
242
+ path.mkpath unless path.exist?
243
+ rescue Errno::ENOENT => exc
244
+ raise(Errno::ENOENT, "DirmonJob failed to create archive directory: #{path}, #{exc.message}")
245
+ end
221
246
  path.realpath
222
247
  else
223
248
  file_pathname.dirname.join(self.class.default_archive_directory).realdirpath
@@ -226,7 +251,7 @@ module RocketJob
226
251
 
227
252
  # Passes each filename [Pathname] found that matches the pattern into the supplied block
228
253
  def each(&block)
229
- logger.tagged("DirmonEntry:#{id}") do
254
+ logger.fast_tag("DirmonEntry:#{id}") do
230
255
  # Case insensitive filename matching
231
256
  Pathname.glob(pattern, File::FNM_CASEFOLD).each do |pathname|
232
257
  next if pathname.directory?
@@ -253,7 +278,7 @@ module RocketJob
253
278
  end
254
279
 
255
280
  # Set exception information for this DirmonEntry and fail it
256
- def fail_with_exception!(worker_name, exc_or_message)
281
+ def set_exception(worker_name, exc_or_message)
257
282
  if exc_or_message.is_a?(Exception)
258
283
  self.exception = JobException.from_exception(exc_or_message)
259
284
  exception.worker_name = worker_name
@@ -265,10 +290,9 @@ module RocketJob
265
290
  worker_name: worker_name
266
291
  )
267
292
  end
268
- fail!
269
293
  end
270
294
 
271
- @@whitelist_paths = ThreadSafe::Array.new
295
+ @@whitelist_paths = Concurrent::Array.new
272
296
 
273
297
  # Returns the Job to be queued
274
298
  def job_class
@@ -280,19 +304,17 @@ module RocketJob
280
304
 
281
305
  # Queues the job for the supplied pathname
282
306
  def later(pathname)
283
- job = job_class.new(
284
- properties.merge(
285
- arguments: arguments,
286
- properties: properties,
287
- perform_method: perform_method
288
- )
289
- )
290
- upload_file(job, pathname)
291
- job.save!
292
- job
307
+ if klass = job_class
308
+ job = klass.new(properties.merge(arguments: arguments))
309
+ upload_file(job, pathname)
310
+ job.save!
311
+ job
312
+ else
313
+ raise(ArgumentError, "Cannot instantiate a class for: #{job_class_name.inspect}")
314
+ end
293
315
  end
294
316
 
295
- protected
317
+ private
296
318
 
297
319
  # Instance method to return whitelist paths
298
320
  def whitelist_paths
@@ -301,13 +323,7 @@ module RocketJob
301
323
 
302
324
  # Upload the file to the job
303
325
  def upload_file(job, pathname)
304
- if job.respond_to?(:file_store_upload)
305
- # Allow the job to determine what to do with the file
306
- # Pass the pathname as a string, not a Pathname (IO) instance
307
- # so that it can read the file directly
308
- job.file_store_upload(pathname.to_s)
309
- archive_directory ? archive_file(job, pathname) : pathname.unlink
310
- elsif job.respond_to?(:upload)
326
+ if job.respond_to?(:upload)
311
327
  # With RocketJob Pro the file can be uploaded directly into the Job itself
312
328
  job.upload(pathname.to_s)
313
329
  archive_directory ? archive_file(job, pathname) : pathname.unlink
@@ -316,15 +332,17 @@ module RocketJob
316
332
  end
317
333
  end
318
334
 
319
- # Archives the file for a job where there was no #file_store_upload or #upload method
335
+ # Archives the file for a job where there was no #upload method
320
336
  def upload_default(job, pathname)
321
337
  full_file_name = archive_file(job, pathname)
322
- if job.respond_to?(:full_file_name=)
338
+ if job.respond_to?(:upload_file_name=)
339
+ job.upload_file_name = full_file_name
340
+ elsif job.respond_to?(:full_file_name=)
323
341
  job.full_file_name = full_file_name
324
342
  elsif job.arguments.first.is_a?(Hash)
325
343
  job.arguments.first[:full_file_name] = full_file_name
326
344
  else
327
- raise(ArgumentError, "#{job_class_name} must either have attribute 'full_file_name' or the first argument must be a Hash")
345
+ raise(ArgumentError, "#{job_class_name} must either have attribute 'upload_file_name' or the first argument must be a Hash")
328
346
  end
329
347
  end
330
348
 
@@ -0,0 +1,56 @@
1
+ require 'aasm'
2
+
3
+ # The following patches can be removed once the following PR has been merged into AASM:
4
+ # https://github.com/aasm/aasm/pull/269
5
+
6
+ AASM::Core::Event
7
+ module AASM::Core
8
+ class Event
9
+ def initialize_copy(orig)
10
+ super
11
+ @transitions = @transitions.collect { |transition| transition.clone }
12
+ @guards = @guards.dup
13
+ @unless = @unless.dup
14
+ @options = {}
15
+ orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
16
+ end
17
+ end
18
+ end
19
+
20
+ AASM::Core::State
21
+ module AASM::Core
22
+ class State
23
+ # called internally by Ruby 1.9 after clone()
24
+ def initialize_copy(orig)
25
+ super
26
+ @options = {}
27
+ orig.options.each_pair { |name, setting| @options[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
28
+ end
29
+ end
30
+ end
31
+
32
+ AASM::Core::Transition
33
+ module AASM::Core
34
+ class Transition
35
+ def initialize_copy(orig)
36
+ super
37
+ @guards = @guards.dup
38
+ @unless = @unless.dup
39
+ @opts = {}
40
+ orig.opts.each_pair { |name, setting| @opts[name] = setting.is_a?(Hash) || setting.is_a?(Array) ? setting.dup : setting }
41
+ end
42
+ end
43
+ end
44
+
45
+ AASM::StateMachine
46
+ module AASM
47
+ class StateMachine
48
+ def initialize_copy(orig)
49
+ super
50
+ @states = orig.states.collect { |state| state.clone }
51
+ @events = {}
52
+ orig.events.each_pair { |name, event| @events[name] = event.clone }
53
+ end
54
+ end
55
+ end
56
+
@@ -0,0 +1,23 @@
1
+ require 'mongo'
2
+
3
+ Mongo::Logging
4
+ module Mongo
5
+ module Logging
6
+
7
+ # Remove annoying message on startup
8
+ def write_logging_startup_message
9
+ end
10
+
11
+ # Cleanup output
12
+ def log(level, msg)
13
+ MongoClient.logger.send(level, msg)
14
+ end
15
+
16
+ protected
17
+
18
+ def log_operation(name, payload, duration)
19
+ MongoClient.logger.benchmark_trace(name, duration: (duration * 1000), payload: payload)
20
+ end
21
+
22
+ end
23
+ end