dalliance 0.6.0 → 0.9.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.
data/lib/dalliance.rb CHANGED
@@ -91,6 +91,8 @@ module Dalliance
91
91
  scope :validation_error, -> { where(:dalliance_status => 'validation_error') }
92
92
  scope :processing_error, -> { where(:dalliance_status => 'processing_error') }
93
93
  scope :completed, -> { where(:dalliance_status => 'completed') }
94
+ scope :cancel_requested, -> { where(:dalliance_status => 'cancel_requested') }
95
+ scope :cancelled, -> { where(:dalliance_status => 'cancelled') }
94
96
 
95
97
  state_machine :dalliance_status, :initial => :pending do
96
98
  state :pending
@@ -98,6 +100,8 @@ module Dalliance
98
100
  state :validation_error
99
101
  state :processing_error
100
102
  state :completed
103
+ state :cancel_requested
104
+ state :cancelled
101
105
 
102
106
  #event :queue_dalliance do
103
107
  # transition :processing_error => :pending
@@ -116,7 +120,22 @@ module Dalliance
116
120
  end
117
121
 
118
122
  event :finish_dalliance do
119
- transition :processing => :completed
123
+ transition [:processing, :cancel_requested] => :completed
124
+ end
125
+
126
+ event :reprocess_dalliance do
127
+ transition [:validation_error, :processing_error, :completed] => :pending
128
+ end
129
+
130
+ # Requests the record to stop processing. This does NOT cause processing
131
+ # to stop! Each model is required to handle cancellation on its own by
132
+ # periodically checking the dalliance status
133
+ event :request_cancel_dalliance do
134
+ transition [:pending, :processing] => :cancel_requested
135
+ end
136
+
137
+ event :cancelled_dalliance do
138
+ transition [:cancel_requested] => :cancelled
120
139
  end
121
140
  end
122
141
  #END state_machine(s)
@@ -175,7 +194,9 @@ module Dalliance
175
194
  else
176
195
  self.class.where(id: self.id).update_all(dalliance_status: dalliance_status, dalliance_error_hash: dalliance_error_hash.to_yaml )
177
196
  end
197
+ # rubocop:disable Lint/SuppressedException
178
198
  rescue
199
+ # rubocop:enable Lint/SuppressedException
179
200
  end
180
201
  end
181
202
  end
@@ -185,13 +206,35 @@ module Dalliance
185
206
  end
186
207
 
187
208
  def error_or_completed?
188
- validation_error? || processing_error? || completed?
209
+ validation_error? || processing_error? || completed? || cancelled?
210
+ end
211
+
212
+ # Cancels the job and removes it from the queue if has not already been taken
213
+ # by a worker. If the job is processing, it is up to the job implementation
214
+ # to stop and do any necessary cleanup. If the job does not honor the
215
+ # cancellation request, it will finish processing as normal and finish with a
216
+ # dalliance_status of 'completed'.
217
+ #
218
+ # Jobs can currently only be removed from Resque queues. DelayedJob jobs will
219
+ # not be dequeued, but will immediately exit once taken by a worker.
220
+ def cancel_and_dequeue_dalliance!
221
+ should_dequeue = pending?
222
+
223
+ request_cancel_dalliance!
224
+
225
+ if should_dequeue
226
+ self.dalliance_options[:worker_class].dequeue(self)
227
+ dalliance_log("[dalliance] #{self.class.name}(#{id}) - #{dalliance_status} - Removed from #{processing_queue} queue")
228
+ cancelled_dalliance!
229
+ end
230
+
231
+ true
189
232
  end
190
233
 
191
234
  def validate_dalliance_status
192
235
  unless error_or_completed?
193
- errors.add(:dalliance_status, :invalid)
194
- if defined?(Rails) && ::Rails::VERSION::MAJOR == 5 && ::Rails::VERSION::MINOR >= 0
236
+ errors.add(:dalliance_status, "Processing must be finished or cancelled, but status is '#{dalliance_status}'")
237
+ if defined?(Rails)
195
238
  throw(:abort)
196
239
  else
197
240
  return false
@@ -213,17 +256,46 @@ module Dalliance
213
256
  end
214
257
  end
215
258
 
216
- #Force backgound_processing w/ true
217
- def dalliance_background_process(backgound_processing = nil)
218
- if backgound_processing || (backgound_processing.nil? && self.class.dalliance_options[:background_processing])
219
- self.class.dalliance_options[:worker_class].enqueue(self, processing_queue)
259
+ #Force background_processing w/ true
260
+ def dalliance_background_process(background_processing = nil)
261
+ if background_processing || (background_processing.nil? && self.class.dalliance_options[:background_processing])
262
+ self.class.dalliance_options[:worker_class].enqueue(self, processing_queue, :dalliance_process)
220
263
  else
221
264
  dalliance_process(false)
222
265
  end
223
266
  end
224
267
 
225
- #backgound_processing == false will re-raise any exceptions
226
- def dalliance_process(backgound_processing = false)
268
+ def dalliance_process(background_processing = false)
269
+ do_dalliance_process(
270
+ perform_method: self.class.dalliance_options[:dalliance_method],
271
+ background_processing: background_processing
272
+ )
273
+ end
274
+
275
+ def dalliance_background_reprocess(background_processing = nil)
276
+ # Reset state to 'pending' before queueing up
277
+ # Otherwise the model will stay on completed/processing_error until the job
278
+ # is taken by a worker, which could be a long time after this method is
279
+ # called.
280
+ reprocess_dalliance!
281
+ if background_processing || (background_processing.nil? && self.class.dalliance_options[:background_processing])
282
+ self.class.dalliance_options[:worker_class].enqueue(self, processing_queue, :do_dalliance_reprocess)
283
+ else
284
+ do_dalliance_reprocess(false)
285
+ end
286
+ end
287
+
288
+ def dalliance_reprocess(background_processing = false)
289
+ reprocess_dalliance!
290
+ do_dalliance_reprocess(background_processing)
291
+ end
292
+
293
+ def do_dalliance_process(perform_method:, background_processing: false)
294
+ # The job might have been cancelled after it was queued, but before
295
+ # processing started. Check for that up front before doing any processing.
296
+ cancelled_dalliance! if cancel_requested?
297
+ return if cancelled? # method generated from AASM
298
+
227
299
  start_time = Time.now
228
300
 
229
301
  begin
@@ -233,9 +305,9 @@ module Dalliance
233
305
  build_dalliance_progress_meter(:total_count => calculate_dalliance_progress_meter_total_count).save!
234
306
  end
235
307
 
236
- self.send(self.class.dalliance_options[:dalliance_method])
308
+ self.send(perform_method)
237
309
 
238
- finish_dalliance! unless validation_error?
310
+ finish_dalliance! unless validation_error? || cancelled?
239
311
  rescue StandardError => e
240
312
  #Save the error for future analysis...
241
313
  self.dalliance_error_hash = {:error => e.class.name, :message => e.message, :backtrace => e.backtrace}
@@ -255,14 +327,16 @@ module Dalliance
255
327
  else
256
328
  self.class.where(id: self.id).update_all(dalliance_status: dalliance_status, dalliance_error_hash: dalliance_error_hash.to_yaml )
257
329
  end
330
+ # rubocop:disable Lint/SuppressedException
258
331
  rescue
332
+ # rubocop:enable Lint/SuppressedException
259
333
  end
260
334
  end
261
335
 
262
336
  error_notifier.call(e)
263
337
 
264
- #Don't raise the error if we're backgound_processing...
265
- raise e unless backgound_processing && self.class.dalliance_options[:worker_class].rescue_error?
338
+ # Don't raise the error if we're background processing...
339
+ raise e unless background_processing && self.class.dalliance_options[:worker_class].rescue_error?
266
340
  ensure
267
341
  if self.class.dalliance_options[:dalliance_progress_meter] && dalliance_progress_meter
268
342
  #Works with optimistic locking...
@@ -274,8 +348,11 @@ module Dalliance
274
348
 
275
349
  dalliance_log("[dalliance] #{self.class.name}(#{id}) - #{dalliance_status} #{duration.to_i}")
276
350
 
277
- if self.class.dalliance_options[:duration_column]
278
- self.class.where(id: self.id).update_all(self.class.dalliance_options[:duration_column] => duration.to_i)
351
+ duration_column = self.class.dalliance_options[:duration_column]
352
+ if duration_column.present?
353
+ current_duration = self.send(duration_column) || 0
354
+ self.class.where(id: self.id)
355
+ .update_all(duration_column => current_duration + duration.to_f)
279
356
  end
280
357
  end
281
358
  end
@@ -305,6 +382,19 @@ module Dalliance
305
382
  end
306
383
  end
307
384
 
385
+ private
386
+
387
+ # Executes the reprocessing method defined in the model's dalliance options.
388
+ #
389
+ # @param [Boolean] background_processing
390
+ # flag if this is called from a background worker. Defaults to false.
391
+ def do_dalliance_reprocess(background_processing = false)
392
+ do_dalliance_process(
393
+ perform_method: self.class.dalliance_options[:reprocess_method],
394
+ background_processing: background_processing
395
+ )
396
+ end
397
+
308
398
  module Glue
309
399
  extend ActiveSupport::Concern
310
400
 
@@ -313,15 +403,28 @@ module Dalliance
313
403
  end
314
404
 
315
405
  module ClassMethods
316
- def dalliance(*args)
317
- options = args.last.is_a?(Hash) ? Dalliance.options.merge(args.pop) : Dalliance.options
318
-
319
- case args.length
320
- when 1
321
- options[:dalliance_method] = args[0]
322
- else
323
- raise ArgumentError, "Incorrect number of Arguements provided"
324
- end
406
+ # Enables dalliance processing for this class.
407
+ #
408
+ # @param [Symbol|String] dalliance_method
409
+ # the name of the method to call when processing the model in dalliance
410
+ # @param [Hash] options
411
+ # an optional hash of options for dalliance processing
412
+ # @option options [Symbol] :reprocess_method
413
+ # the name of the method to use to reprocess the model in dalliance
414
+ # @option options [Boolean] :dalliance_process_meter
415
+ # whether or not to display a progress meter
416
+ # @option options [String] :queue
417
+ # the name of the worker queue to use. Default 'dalliance'
418
+ # @option options [String] :duration_column
419
+ # the name of the table column that stores the dalliance processing time. Default 'dalliance_duration'
420
+ # @option options [Object] :logger
421
+ # the logger object to use. Can be nil
422
+ # @option options [Proc] :error_notifier
423
+ # A proc that accepts an error object. Default is a NOP
424
+ def dalliance(dalliance_method, options = {})
425
+ opts = Dalliance.options.merge(options)
426
+
427
+ opts[:dalliance_method] = dalliance_method
325
428
 
326
429
  if dalliance_options.nil?
327
430
  self.dalliance_options = {}
@@ -329,7 +432,7 @@ module Dalliance
329
432
  self.dalliance_options = self.dalliance_options.dup
330
433
  end
331
434
 
332
- self.dalliance_options.merge!(options)
435
+ self.dalliance_options.merge!(opts)
333
436
 
334
437
  include Dalliance
335
438
  end
@@ -7,7 +7,7 @@ module Dalliance
7
7
  ActiveSupport.on_load :active_record do
8
8
  include Dalliance::Glue
9
9
 
10
- ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Dalliance::Schema)
10
+ ActiveRecord::ConnectionAdapters::TableDefinition.include Dalliance::Schema
11
11
  end
12
12
  end
13
13
  end
@@ -46,20 +46,20 @@ module Dalliance
46
46
  #TODO: This is just a stopgap until I fix increment! to be thread-safe
47
47
  def progress
48
48
  begin
49
- _progress = (current_count.to_f / total_count.to_f * 100).to_i
49
+ current_progress = (current_count.to_f / total_count.to_f * 100).to_i
50
50
 
51
51
  #Handle an incorrect total_count...
52
- _progress = 100 if _progress > 100
52
+ current_progress = 100 if current_progress > 100
53
53
  rescue
54
54
  #what, are you diving by zero?
55
- _progress = 0
55
+ current_progress = 0
56
56
  end
57
57
 
58
- _progress
58
+ current_progress
59
59
  end
60
60
 
61
61
  def increment!
62
62
  Dalliance::ProgressMeter.increment_counter(:current_count, self.id)
63
63
  end
64
64
  end
65
- end
65
+ end
@@ -1,7 +1,7 @@
1
1
  module Dalliance
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 6
4
+ MINOR = 9
5
5
  TINY = 0
6
6
  PRE = nil
7
7
 
@@ -1,15 +1,24 @@
1
1
  module Dalliance
2
2
  module Workers
3
- if defined?(Rails) && ((::Rails::VERSION::MAJOR == 4 && ::Rails::VERSION::MINOR >= 2) || ::Rails::VERSION::MAJOR >= 5)
3
+ if defined?(Rails)
4
4
  class DelayedJob < ::ActiveJob::Base
5
5
  queue_as :dalliance
6
6
 
7
- def self.enqueue(instance, queue = 'dalliance')
8
- Dalliance::Workers::DelayedJob.set(queue: queue).perform_later(instance.class.name, instance.id)
7
+ def self.enqueue(instance, queue = 'dalliance', perform_method)
8
+ Dalliance::Workers::DelayedJob
9
+ .set(queue: queue)
10
+ .perform_later(instance.class.name, instance.id, perform_method.to_s)
9
11
  end
10
12
 
11
- def perform(instance_klass, instance_id)
12
- instance_klass.constantize.find(instance_id).dalliance_process(true)
13
+ def self.dequeue(_instance)
14
+ # NOP
15
+ end
16
+
17
+ def perform(instance_klass, instance_id, perform_method)
18
+ instance_klass
19
+ .constantize
20
+ .find(instance_id)
21
+ .send(perform_method, true)
13
22
  end
14
23
 
15
24
  #Delayed job automatically retries, so rescue the error
@@ -18,13 +27,23 @@ module Dalliance
18
27
  end
19
28
  end
20
29
  else
21
- class DelayedJob < Struct.new(:instance_klass, :instance_id)
22
- def self.enqueue(instance, queue = 'dalliance')
23
- ::Delayed::Job.enqueue(self.new(instance.class.name, instance.id), :queue => queue)
30
+ class DelayedJob < Struct.new(:instance_klass, :instance_id, :perform_method)
31
+ def self.enqueue(instance, queue = 'dalliance', perform_method)
32
+ ::Delayed::Job.enqueue(
33
+ self.new(instance.class.name, instance.id, perform_method),
34
+ :queue => queue
35
+ )
36
+ end
37
+
38
+ def self.dequeue(_instance)
39
+ # NOP
24
40
  end
25
41
 
26
42
  def perform
27
- instance_klass.constantize.find(instance_id).dalliance_process(true)
43
+ instance_klass
44
+ .constantize
45
+ .find(instance_id)
46
+ .send(perform_method, true)
28
47
  end
29
48
 
30
49
  #Delayed job automatically retries, so rescue the error
@@ -1,15 +1,37 @@
1
1
  module Dalliance
2
2
  module Workers
3
- if defined?(Rails) && ((::Rails::VERSION::MAJOR == 4 && ::Rails::VERSION::MINOR >= 2) || ::Rails::VERSION::MAJOR >= 5)
3
+ if defined?(Rails)
4
4
  class Resque < ::ActiveJob::Base
5
5
  queue_as :dalliance
6
6
 
7
- def self.enqueue(instance, queue = 'dalliance')
8
- Dalliance::Workers::Resque.set(queue: queue).perform_later(instance.class.name, instance.id)
7
+ def self.enqueue(instance, queue = 'dalliance', perform_method)
8
+ Dalliance::Workers::Resque
9
+ .set(queue: queue)
10
+ .perform_later(instance.class.name, instance.id, perform_method.to_s)
9
11
  end
10
12
 
11
- def perform(instance_klass, instance_id)
12
- instance_klass.constantize.find(instance_id).dalliance_process(true)
13
+ def self.dequeue(instance)
14
+ redis = ::Resque.redis
15
+ queue = instance.processing_queue
16
+
17
+ redis.everything_in_queue(queue).each do |string|
18
+ # Structure looks like, e.g.
19
+ # { 'class' => 'ActiveJob::...', 'args' => [{ 'arguments' => ['SomeClass', 123, 'dalliance_process'] }] }
20
+ data = ::Resque.decode(string)
21
+ dalliance_args = data['args'][0]['arguments']
22
+
23
+ if dalliance_args == [instance.class.name, instance.id, 'dalliance_process'] ||
24
+ dalliance_args == [instance.class.name, instance.id, 'dalliance_reprocess']
25
+ redis.remove_from_queue(queue, string)
26
+ end
27
+ end
28
+ end
29
+
30
+ def perform(instance_klass, instance_id, perform_method)
31
+ instance_klass
32
+ .constantize
33
+ .find(instance_id)
34
+ .send(perform_method, true)
13
35
  end
14
36
 
15
37
  #Resque fails, so don't rescue the error
@@ -19,12 +41,32 @@ module Dalliance
19
41
  end
20
42
  else
21
43
  class Resque
22
- def self.enqueue(instance, queue = 'dalliance')
23
- ::Resque.enqueue_to(queue, self, instance.class.name, instance.id)
44
+ def self.enqueue(instance, queue = 'dalliance', perform_method)
45
+ ::Resque.enqueue_to(queue, self, instance.class.name, instance.id, perform_method.to_s)
46
+ end
47
+
48
+ def self.dequeue(instance)
49
+ redis = ::Resque.redis
50
+ queue = instance.processing_queue
51
+
52
+ redis.everything_in_queue(queue).each do |string|
53
+ # Structure looks like, e.g.
54
+ # { 'class' => 'ActiveJob::...', 'args' => [{ 'arguments' => ['SomeClass', 123, 'dalliance_process'] }] }
55
+ data = ::Resque.decode(string)
56
+ dalliance_args = data['args'][0]['arguments']
57
+
58
+ if dalliance_args == [instance.class.name, instance.id, 'dalliance_process'] ||
59
+ dalliance_args == [instance.class.name, instance.id, 'dalliance_reprocess']
60
+ redis.remove_from_queue(queue, string)
61
+ end
62
+ end
24
63
  end
25
64
 
26
- def self.perform(instance_klass, instance_id)
27
- instance_klass.constantize.find(instance_id).dalliance_process(true)
65
+ def self.perform(instance_klass, instance_id, perform_method)
66
+ instance_klass
67
+ .constantize
68
+ .find(instance_id)
69
+ .send(perform_method, true)
28
70
  end
29
71
 
30
72
  #Resque fails, so don't rescue the error
@@ -77,6 +77,48 @@ RSpec.describe DallianceModel do
77
77
  expect(subject.dalliance_duration).not_to eq(nil)
78
78
  end
79
79
 
80
+ context 'reprocess' do
81
+ before(:all) do
82
+ DallianceModel.dalliance_options[:dalliance_method] = :dalliance_success_method
83
+ DallianceModel.dalliance_options[:worker_class] = Dalliance::Workers::DelayedJob
84
+ DallianceModel.dalliance_options[:queue] = 'dalliance'
85
+ end
86
+
87
+ before do
88
+ subject.dalliance_process
89
+ subject.reload
90
+ end
91
+
92
+ it 'successfully runs the dalliance_reprocess method' do
93
+ subject.dalliance_background_reprocess
94
+ Delayed::Worker.new(:queues => [:dalliance]).work_off
95
+ subject.reload
96
+
97
+ expect(subject).to be_successful
98
+ expect(Delayed::Job.count).to eq(0)
99
+ expect(subject.reprocessed_count).to eq(1)
100
+ end
101
+
102
+ it 'increases the total processing time counter' do
103
+ original_duration = subject.dalliance_duration
104
+ subject.dalliance_background_reprocess
105
+ Delayed::Worker.new(:queues => [:dalliance]).work_off
106
+ subject.reload
107
+
108
+ expect(subject.dalliance_duration).to be_between(original_duration, Float::INFINITY)
109
+ end
110
+
111
+ it "resets the dalliance_status to 'pending'" do
112
+ subject.update_column(:dalliance_status, 'processing_error')
113
+ expect { subject.dalliance_background_reprocess }
114
+ .to change(subject, :dalliance_status)
115
+ .to('pending')
116
+
117
+ Delayed::Worker.new(:queues => [:dalliance]).work_off
118
+ expect(subject).to be_successful
119
+ end
120
+ end
121
+
80
122
  context "another_queue" do
81
123
  let(:queue) { 'dalliance_2'}
82
124