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
@@ -0,0 +1,166 @@
1
+ # encoding: UTF-8
2
+ require 'active_support/concern'
3
+
4
+ module RocketJob
5
+ module Plugins
6
+ module Job
7
+ # State machine for RocketJob::Job
8
+ module StateMachine
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ # State Machine events and transitions
13
+ #
14
+ # :queued -> :running -> :completed
15
+ # -> :paused -> :running (if started )
16
+ # -> :queued ( if no started )
17
+ # -> :aborted
18
+ # -> :failed -> :aborted
19
+ # -> :queued
20
+ # -> :aborted
21
+ # -> :queued (when a worker dies)
22
+ # -> :aborted
23
+ aasm column: :state do
24
+ # Job has been created and is queued for processing ( Initial state )
25
+ state :queued, initial: true
26
+
27
+ # Job is running
28
+ state :running
29
+
30
+ # Job has completed processing ( End state )
31
+ state :completed
32
+
33
+ # Job is temporarily paused and no further processing will be completed
34
+ # until this job has been resumed
35
+ state :paused
36
+
37
+ # Job failed to process and needs to be manually re-tried or aborted
38
+ state :failed
39
+
40
+ # Job was aborted and cannot be resumed ( End state )
41
+ state :aborted
42
+
43
+ event :start do
44
+ transitions from: :queued, to: :running
45
+ end
46
+
47
+ event :complete do
48
+ transitions from: :running, to: :completed
49
+ end
50
+
51
+ event :fail do
52
+ transitions from: :queued, to: :failed
53
+ transitions from: :running, to: :failed
54
+ transitions from: :paused, to: :failed
55
+ end
56
+
57
+ event :retry do
58
+ transitions from: :failed, to: :queued
59
+ end
60
+
61
+ event :pause do
62
+ transitions from: :running, to: :paused
63
+ transitions from: :queued, to: :paused
64
+ end
65
+
66
+ event :resume do
67
+ transitions from: :paused, to: :running, if: -> { started_at }
68
+ transitions from: :paused, to: :queued, unless: -> { started_at }
69
+ end
70
+
71
+ event :abort do
72
+ transitions from: :running, to: :aborted
73
+ transitions from: :queued, to: :aborted
74
+ transitions from: :failed, to: :aborted
75
+ transitions from: :paused, to: :aborted
76
+ end
77
+
78
+ event :requeue do
79
+ transitions from: :running, to: :queued,
80
+ if: -> _worker_name { worker_name == _worker_name },
81
+ after: :rocket_job_clear_started_at
82
+ end
83
+ end
84
+ # @formatter:on
85
+
86
+ # Define a before and after callback method for each event
87
+ state_machine_define_event_callbacks(*aasm.state_machine.events.keys)
88
+
89
+ before_start :rocket_job_set_started_at
90
+ before_complete :rocket_job_set_completed_at, :rocket_job_mark_complete
91
+ before_fail :rocket_job_set_completed_at, :rocket_job_increment_failure_count, :rocket_job_set_exception
92
+ before_pause :rocket_job_set_completed_at
93
+ before_abort :rocket_job_set_completed_at
94
+ before_retry :rocket_job_clear_exception
95
+ before_resume :rocket_job_clear_completed_at
96
+ after_complete :rocket_job_destroy_on_complete
97
+
98
+ # Pause all running jobs
99
+ def self.pause_all
100
+ running.each(&:pause!)
101
+ end
102
+
103
+ # Resume all paused jobs
104
+ def self.resume_all
105
+ paused.each(&:resume!)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ # Sets the exception child object for this job based on the
112
+ # supplied Exception instance or message
113
+ def rocket_job_set_exception(worker_name = nil, exc_or_message = nil)
114
+ if exc_or_message.is_a?(Exception)
115
+ self.exception = JobException.from_exception(exc_or_message)
116
+ exception.worker_name = worker_name
117
+ else
118
+ build_exception(
119
+ class_name: 'RocketJob::JobException',
120
+ message: exc_or_message,
121
+ backtrace: [],
122
+ worker_name: worker_name
123
+ )
124
+ end
125
+ end
126
+
127
+ def rocket_job_set_started_at
128
+ self.started_at = Time.now
129
+ end
130
+
131
+ def rocket_job_mark_complete
132
+ self.percent_complete = 100
133
+ end
134
+
135
+ def rocket_job_increment_failure_count
136
+ self.failure_count += 1
137
+ end
138
+
139
+ def rocket_job_clear_exception
140
+ self.completed_at = nil
141
+ self.exception = nil
142
+ self.worker_name = nil
143
+ end
144
+
145
+ def rocket_job_set_completed_at
146
+ self.completed_at = Time.now
147
+ self.worker_name = nil
148
+ end
149
+
150
+ def rocket_job_clear_completed_at
151
+ self.completed_at = nil
152
+ end
153
+
154
+ def rocket_job_clear_started_at
155
+ self.started_at = nil
156
+ self.worker_name = nil
157
+ end
158
+
159
+ def rocket_job_destroy_on_complete
160
+ destroy if destroy_on_complete && !new_record?
161
+ end
162
+ end
163
+
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,167 @@
1
+ # encoding: UTF-8
2
+ require 'active_support/concern'
3
+
4
+ # Worker behavior for a job
5
+ module RocketJob
6
+ module Plugins
7
+ module Job
8
+ module Worker
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ # Run this job later
13
+ #
14
+ # Saves it to the database for processing later by workers
15
+ def self.perform_later(*args, &block)
16
+ if RocketJob::Config.inline_mode
17
+ perform_now(*args, &block)
18
+ else
19
+ job = new(arguments: args)
20
+ block.call(job) if block
21
+ job.save!
22
+ job
23
+ end
24
+ end
25
+
26
+ # Run this job now.
27
+ #
28
+ # The job is not saved to the database since it is processed entriely in memory
29
+ # As a result before_save and before_destroy callbacks will not be called.
30
+ # Validations are still called however prior to calling #perform
31
+ def self.perform_now(*args, &block)
32
+ job = new(arguments: args)
33
+ block.call(job) if block
34
+ job.perform_now
35
+ job
36
+ end
37
+
38
+ # Returns the next job to work on in priority based order
39
+ # Returns nil if there are currently no queued jobs, or processing batch jobs
40
+ # with records that require processing
41
+ #
42
+ # Parameters
43
+ # worker_name [String]
44
+ # Name of the worker that will be processing this job
45
+ #
46
+ # skip_job_ids [Array<BSON::ObjectId>]
47
+ # Job ids to exclude when looking for the next job
48
+ #
49
+ # Note:
50
+ # If a job is in queued state it will be started
51
+ def self.rocket_job_next_job(worker_name, skip_job_ids = nil)
52
+ while (job = rocket_job_retrieve(worker_name, skip_job_ids))
53
+ case
54
+ when job.running?
55
+ # Batch Job
56
+ return job
57
+ when job.expired?
58
+ job.rocket_job_fail_on_exception!(worker_name) { job.destroy }
59
+ logger.info "Destroyed expired job #{job.class.name}, id:#{job.id}"
60
+ else
61
+ job.worker_name = worker_name
62
+ job.rocket_job_fail_on_exception!(worker_name) { job.start }
63
+ return job if job.running?
64
+ end
65
+ end
66
+ end
67
+
68
+ # Requeues all jobs that were running on worker that died
69
+ def self.requeue_dead_worker(worker_name)
70
+ running.each { |job| job.requeue!(worker_name) if job.may_requeue?(worker_name) }
71
+ end
72
+
73
+ # Turn off embedded callbacks. Slow and not used for Jobs
74
+ embedded_callbacks_off
75
+ end
76
+
77
+ # Runs the job now in the current thread.
78
+ #
79
+ # Validations are called prior to running the job.
80
+ #
81
+ # The job is not saved and therefore the following callbacks are _not_ called:
82
+ # * before_save
83
+ # * after_save
84
+ # * before_create
85
+ # * after_create
86
+ #
87
+ # Exceptions are _not_ suppressed and should be handled by the caller.
88
+ def perform_now
89
+ # Call validations
90
+ if respond_to?(:validate!)
91
+ validate!
92
+ elsif invalid?
93
+ raise(MongoMapper::DocumentNotValid, self)
94
+ end
95
+ worker = RocketJob::Worker.new(name: 'inline')
96
+ worker.started
97
+ start if may_start?
98
+ # Raise exceptions
99
+ rocket_job_work(worker, true) if running?
100
+ result
101
+ end
102
+
103
+ def perform(*)
104
+ raise NotImplementedError
105
+ end
106
+
107
+ # Fail this job in the event of an exception.
108
+ #
109
+ # The job is automatically saved only if an exception is raised in the supplied block.
110
+ #
111
+ # worker_name: [String]
112
+ # Name of the worker on which the exception has occurred
113
+ #
114
+ # raise_exceptions: [true|false]
115
+ # Re-raise the exception after updating the job
116
+ # Default: !RocketJob::Config.inline_mode
117
+ def rocket_job_fail_on_exception!(worker_name, raise_exceptions = !RocketJob::Config.inline_mode)
118
+ yield
119
+ rescue Exception => exc
120
+ if failed? || !may_fail?
121
+ self.exception = JobException.from_exception(exc)
122
+ exception.worker_name = worker_name
123
+ save! unless new_record? || destroyed?
124
+ else
125
+ if new_record? || destroyed?
126
+ fail(worker_name, exc)
127
+ else
128
+ fail!(worker_name, exc)
129
+ end
130
+ end
131
+ raise exc if raise_exceptions
132
+ end
133
+
134
+ # Works on this job
135
+ #
136
+ # Returns [true|false] whether this job should be excluded from the next lookup
137
+ #
138
+ # If an exception is thrown the job is marked as failed and the exception
139
+ # is set in the job itself.
140
+ #
141
+ # Thread-safe, can be called by multiple threads at the same time
142
+ def rocket_job_work(worker, raise_exceptions = !RocketJob::Config.inline_mode)
143
+ raise(ArgumentError, 'Job must be started before calling #rocket_job_work') unless running?
144
+ rocket_job_fail_on_exception!(worker.name, raise_exceptions) do
145
+ run_callbacks :perform do
146
+ # Allow callbacks to fail, complete or abort the job
147
+ if running?
148
+ ret = perform(*arguments)
149
+ if collect_output?
150
+ # Result must be a Hash, if not put it in a Hash
151
+ self.result = (ret.is_a?(Hash) || ret.is_a?(BSON::OrderedHash)) ? ret : {result: ret}
152
+ end
153
+ end
154
+ end
155
+ if new_record? || destroyed?
156
+ complete if may_complete?
157
+ else
158
+ may_complete? ? complete! : save!
159
+ end
160
+ end
161
+ false
162
+ end
163
+
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,54 @@
1
+ # encoding: UTF-8
2
+ require 'active_support/concern'
3
+
4
+ module RocketJob
5
+ module Plugins
6
+ # Automatically starts a new instance of this job anytime it fails, aborts, or completes.
7
+ # Failed jobs are aborted so that they cannot be restarted.
8
+ # On destroy this job is destroyed without starting a new copy. Abort the job first to get
9
+ # it to start a new instance before destroying.
10
+ # Include RocketJob::Plugins::Singleton to prevent multiple copies of a job from running at
11
+ # the same time.
12
+ #
13
+ # Note:
14
+ # - The job will not be restarted if:
15
+ # - A validation fails after cloning this job.
16
+ # - The job has expired.
17
+ module Restart
18
+ extend ActiveSupport::Concern
19
+
20
+ # Attributes to exclude when copying across the attributes to the new instance
21
+ RESTART_EXCLUDES = %w(_id state created_at started_at completed_at failure_count worker_name percent_complete exception result run_at)
22
+
23
+ included do
24
+ after_abort :rocket_job_restart_new_instance
25
+ after_complete :rocket_job_restart_new_instance
26
+ after_fail :rocket_job_restart_abort
27
+ end
28
+
29
+ private
30
+
31
+ # Run again in the future, even if this run fails with an exception
32
+ def rocket_job_restart_new_instance
33
+ return if expired?
34
+ attrs = attributes.dup
35
+ RESTART_EXCLUDES.each { |attr| attrs.delete(attr) }
36
+
37
+ # Copy across run_at for future dated jobs
38
+ attrs['run_at'] = run_at if run_at && (run_at > Time.now)
39
+ # Allow Singleton to prevent the creation of a new job if one is already running
40
+ job = self.class.create(attrs)
41
+ if job.persisted?
42
+ logger.info("Started a new job instance: #{job.id}")
43
+ else
44
+ logger.info('New job instance not started since one is already active')
45
+ end
46
+ end
47
+
48
+ def rocket_job_restart_abort
49
+ new_record? ? abort : abort!
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ require 'active_support/concern'
3
+
4
+ module RocketJob
5
+ module Plugins
6
+ # Prevent more than one instance of this job class from running at a time
7
+ module Singleton
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Validation prevents a new job from being saved while one is already running
12
+ validates_each :state do |record, attr, value|
13
+ if (record.running? || record.queued? || record.paused?) && record.rocket_job_singleton_active?
14
+ record.errors.add(attr, "Another instance of #{record.class.name} is already queued or running")
15
+ end
16
+ end
17
+
18
+ # Returns [true|false] whether another instance of this job is already active
19
+ def rocket_job_singleton_active?
20
+ self.class.where(state: [:running, :queued], _id: {'$ne' => id}).exists?
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: UTF-8
2
+ require 'active_support/concern'
3
+ require 'aasm'
4
+ require 'rocket_job/extensions/aasm'
5
+
6
+ module RocketJob
7
+ module Plugins
8
+ # State machine for RocketJob
9
+ #
10
+ # Define before and after callbacks for state machine events
11
+ #
12
+ # Example: Supply a method name to call
13
+ #
14
+ # class MyJob < RocketJob::Job
15
+ # before_fail :let_me_know
16
+ #
17
+ # def let_me_know
18
+ # puts "Oh no, the job has failed with and exception"
19
+ # end
20
+ # end
21
+ #
22
+ # Example: Pass a block
23
+ #
24
+ # class MyJob < RocketJob::Job
25
+ # before_fail do
26
+ # puts "Oh no, the job has failed with an exception"
27
+ # end
28
+ # end
29
+ module StateMachine
30
+ extend ActiveSupport::Concern
31
+
32
+ included do
33
+ include AASM
34
+
35
+ # Adds a :before or :after callback to an event
36
+ # state_machine_add_event_callback(:start, :before, :my_method)
37
+ def self.state_machine_add_event_callback(event_name, action, *methods, &block)
38
+ raise(ArgumentError, 'Cannot supply both a method name and a block') if (methods.size > 0) && block
39
+ raise(ArgumentError, 'Must supply either a method name or a block') unless (methods.size > 0) || block
40
+
41
+ # TODO Somehow get AASM to support options such as :if and :unless to be consistent with other callbacks
42
+ # For example:
43
+ # before_start :my_callback, unless: :encrypted?
44
+ # before_start :my_callback, if: :encrypted?
45
+ if event = aasm.state_machine.events[event_name]
46
+ values = Array(event.options[action])
47
+ code =
48
+ if block
49
+ block
50
+ else
51
+ # Validate methods are any of Symbol String Proc
52
+ methods.each do |method|
53
+ unless method.is_a?(Symbol) || method.is_a?(String)
54
+ raise(ArgumentError, "#{action}_#{event_name} currently does not support any options. Only Symbol and String method names can be supplied.")
55
+ end
56
+ end
57
+ methods
58
+ end
59
+ action == :before ? values.push(code) : values.unshift(code)
60
+ event.options[action] = values.flatten.uniq
61
+ else
62
+ raise(ArgumentError, "Unknown event: #{event_name.inspect}")
63
+ end
64
+ end
65
+
66
+ def self.state_machine_define_event_callbacks(*event_names)
67
+ event_names.each do |event_name|
68
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
69
+ def self.before_#{event_name}(*methods, &block)
70
+ state_machine_add_event_callback(:#{event_name}, :before, *methods, &block)
71
+ end
72
+
73
+ def self.after_#{event_name}(*methods, &block)
74
+ state_machine_add_event_callback(:#{event_name}, :after, *methods, &block)
75
+ end
76
+ RUBY
77
+ end
78
+ end
79
+
80
+ # Patch AASM so that save! is called instead of save
81
+ # So that validations are run before job.requeue! is completed
82
+ # Otherwise it just fails silently
83
+ def aasm_write_state(state, name=:default)
84
+ attr_name = self.class.aasm(name).attribute_name
85
+ old_value = read_attribute(attr_name)
86
+ write_attribute(attr_name, state)
87
+
88
+ begin
89
+ if aasm_skipping_validations(name)
90
+ saved = save(validate: false)
91
+ write_attribute(attr_name, old_value) unless saved
92
+ saved
93
+ else
94
+ save!
95
+ end
96
+ rescue Exception => exc
97
+ write_attribute(attr_name, old_value)
98
+ raise(exc)
99
+ end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: UTF-8
2
2
  module RocketJob #:nodoc
3
- VERSION = '1.3.0'
3
+ VERSION = '2.0.0.rc1'
4
4
  end