queue-processor 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 88481dcefd871ebdea1fe2286bffe0aa125a379dd219753f28e21959b792e59b
4
+ data.tar.gz: 86d6a3baa440f8d217f2333fde259e09ba5e7c92bb67f2eb873f73ab438e2ca9
5
+ SHA512:
6
+ metadata.gz: bcb8b7cb69e2e0b593287da839c12c000ac8479644a85af9af2b9e1d47f4e2b036fbd0ac3b284ee21defd255e56c540e81ada695593024d3cc15e85f2419120f
7
+ data.tar.gz: bf17501f746ca8d457ee7f965a4e468653b8419013cb12c3de31787692f9ce2de0368998cef0d182247fef43a2fcf0f2a5291b05275bf3b0929af6b1cd0c36fe
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ queue-processor
2
+ ===============
3
+ Queue Processor is a calculation model built on top of Delayed Job designed to process a single root calculation followed by a number of dependent calculations.
4
+
5
+ Installation
6
+ ============
7
+
8
+ Add queue-processor and state_machine to your Gemfile. Note that a specific version of state_machine is required until https://github.com/pluginaweek/state_machine/pull/255 is resolved.
9
+
10
+ ```ruby
11
+ gem 'queue-processor', :git => 'https://github.com/GoodMeasuresLLC/queue-processor.git'
12
+ gem 'state_machine', :git => 'https://github.com/GoodMeasuresLLC/state_machine.git'
13
+ ```
14
+
15
+ Run `bundle install`
16
+
17
+ Requirements
18
+ ============
19
+ You must have 3 models:
20
+
21
+ 1. The RootCalculation: This is the calculation that everything depends on. Performing a new RootCalculation will cause everything in progress for the old calculation to be aborted. For example, a Weekly Calculation that determine's the user's score for the week against their goals.
22
+ 2. A DependentCalculationGroup: These dependent calculations are queued and processed after the RootCalculation is performed. DependentCalculationGroups simply queue additional work, partitioned and prioritized. For example, a DailyCalculation could queue HourlyCalculations prioritized such that tomorrow's calculations are prioritized first. When all the HourlyCalculations complete, they are made available as a group though the DailyCalculation. When the last DailyCalculation completes, the RootCalculation is marked as finished.
23
+ 3. A DependentCalculation: A calculation that depends on the Root Calculation being performed. DependentCalculations are made available in groups and when all are finished, the RootCalculation is marked as complete. For example, an HourlyCalculation could be performed for a particular hour and day of the week. The would be grouped by day of the week and when all the hourly calculations for a day are complete, that week's dependent calculation group would be marked as complete. And when all the hours for the whole week are done, the Root Calculation would be marked as complete.
24
+
25
+ Your models implement methods for performing the calculations and queuing dependent work
26
+
27
+ Examples
28
+ ========
29
+
30
+ ```ruby
31
+ class WeeklyCalculation < ActiveRecord::Base
32
+ include QueueProcessor::RootCalculation
33
+ root_calculation_config do
34
+ self.dependent_calculation_group_association = :daily_calculations
35
+ self.run_at = lambda {|run| Time.now + 0.005.seconds}
36
+ self.priority = lambda {|run| 0}
37
+ end
38
+
39
+ has_many :daily_calculations, :dependent => :destroy
40
+
41
+ # Required hook to perform the calculation associated with the Root Calculation
42
+ def perform_calculation
43
+ update_attributes(:computed_value => rand())
44
+ end
45
+
46
+ # Required hook to create and queue the dependent work
47
+ def create_and_enqueue_dependent_calculations
48
+ (0...7).map do |n|
49
+ self.daily_calculations.create!(:day => n)
50
+ end.each(&:add_to_queue)
51
+ end
52
+ ```
53
+
54
+ ```ruby
55
+ class DailyCalculation < ActiveRecord::Base
56
+ include QueueProcessor::DependentCalculationGroup
57
+ dependent_calculation_group_config do
58
+ self.dependent_calculation_association = :hourly_calculations
59
+ self.parent_calculation = :weekly_calculation
60
+ self.priority = lambda {|daily_calculation| 1}
61
+ end
62
+
63
+ belongs_to :weekly_calculation
64
+ has_many :hourly_calculations, :dependent => :destroy
65
+
66
+ # Required hook to create and queue dependent work, partitioned to this group
67
+ def create_and_enqueue_dependent_calculations
68
+ (0...23).map do |n|
69
+ self.hourly_calculations.create!(:hour => n)
70
+ end.each(&:add_to_queue)
71
+ end
72
+
73
+ # Call back invoked when a calculation finished and any old calculations for this day should be cleaned up
74
+ def cleanup_old_calculations
75
+ begin
76
+ self.with_lock do
77
+ query = DailyCalculation.where(:weekly_calculation_id => self.parent_calculation.id, :day => self.day).where("id < ?", self.id)
78
+ query.destroy_all
79
+ end
80
+ rescue ActiveRecord::RecordNotFound => e
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ ```ruby
87
+ class HourlyCalculation < ActiveRecord::Base
88
+ include QueueProcessor::DependentCalculation
89
+ dependent_calculation_config do
90
+ self.parent_calculation = :daily_calculation
91
+ self.priority = lambda {|daily_calculation| 2}
92
+ end
93
+
94
+ belongs_to :daily_calculation
95
+
96
+ # Required hook that computes the hourly value
97
+ def perform_calculation
98
+ update_attributes(:computed_value => rand())
99
+ end
100
+ end
101
+ ```
102
+
103
+
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'QueueProcessor'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,27 @@
1
+ module QueueProcessor
2
+ module ConfigBase
3
+ extend ActiveSupport::Concern
4
+
5
+ included do |base|
6
+ base.class_eval do
7
+ class_attribute :queue_processor_config
8
+ self.queue_processor_config ||= {}
9
+ extend Config
10
+ end
11
+ end
12
+
13
+ module Config
14
+ private
15
+ def rw_config(key, value, default_value = nil, read_value = nil)
16
+ if value == read_value
17
+ queue_processor_config.include?(key) ? queue_processor_config[key] : default_value
18
+ else
19
+ config = queue_processor_config.clone
20
+ config[key] = value
21
+ self.queue_processor_config = config
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ module QueueProcessor
2
+ module DependentCalculation
3
+ module Config
4
+ extend ActiveSupport::Concern
5
+ include QueueProcessor::ConfigBase
6
+ include QueueProcessor::QueueControlConfig
7
+
8
+ included do
9
+ extend Config
10
+ end
11
+
12
+ module Config
13
+
14
+ def dependent_calculation_config(&block)
15
+ yield self if block_given?
16
+ end
17
+
18
+ # The name of the belongs_to that is our parent
19
+ #
20
+ # * <tt>Default:</tt> :dependent_calculation_group
21
+ # * <tt>Accepts:</tt> Symbol
22
+ def parent_calculation(value = nil)
23
+ rw_config(:parent_calculation, value, :dependent_calculation_group)
24
+ end
25
+ alias_method :parent_calculation=, :parent_calculation
26
+
27
+ # When the calculation should run.
28
+ #
29
+ # * <tt>Default:</tt> A proc evaluating to nil (now in DJ terms)
30
+ # * <tt>Accepts:</tt> Proc or Symbol
31
+ def run_at(value = nil)
32
+ rw_config(:run_at, value, lambda {|object| nil })
33
+ end
34
+ alias_method :run_at=, :run_at
35
+
36
+ # Priority of the calculation
37
+ #
38
+ # * <tt>Default:</tt> A proc delegating priority to the parent
39
+ # * <tt>Accepts:</tt> Proc or Symbol
40
+ def priority(value = nil)
41
+ rw_config(:priority, value, lambda {|object| object.parent_calculation&.priority })
42
+ end
43
+ alias_method :priority=, :priority
44
+
45
+ # Which named queue should be used
46
+ #
47
+ # * <tt>Default:</tt> A proc delegating queue to the parent (if applicable) nil (any queu)
48
+ # * <tt>Accepts:</tt> Proc or Symbol
49
+ def queue(value = nil)
50
+ rw_config(:queue, value, lambda { |object| object.parent_calculation&.queue })
51
+ end
52
+ alias_method :queue=, :queue
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ module QueueProcessor
2
+ module DependentCalculation
3
+ module QueueControl
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ def describe
8
+ "DependentCalculation: #{self.id}"
9
+ end
10
+
11
+ def delayed_jobs
12
+ Delayed::Backend::ActiveRecord::Job.where("handler ilike '%QueueProcessor::DependentCalculation::QueueControl::CalculationRequest%' and handler like '%id: #{id}%'")
13
+ end
14
+
15
+ def process_queued_calculation
16
+ unless parent_calculation && parent_calculation.should_cancel?
17
+ self.perform_calculation
18
+ end
19
+ self.update_attributes(:completed => true)
20
+ Rails.logger.debug {"#{self.describe}: is done"}
21
+
22
+ parent_calculation&.fire_events(:finish)
23
+ end
24
+
25
+ def add_to_queue
26
+ update_attribute(:queued_at, Time.now.utc)
27
+ Delayed::Job.enqueue(CalculationRequest.new(id, queued_at, describe, self.class), delayed_job_options)
28
+ end
29
+
30
+ def in_queue?
31
+ delayed_jobs.count > 0
32
+ end
33
+
34
+ def delayed_job_options
35
+ {:priority=>priority, :run_at => run_at, :queue => queue}.reject {|k,v| v.nil?}
36
+ end
37
+
38
+ class CalculationRequest < Struct.new(:id, :queued_at, :comment, :clazz)
39
+ def perform
40
+ obj = clazz.where(:id => id).first
41
+ if obj.present?
42
+ obj.process_queued_calculation
43
+ else
44
+ Rails.logger.warn("#{clazz.to_s} #{id}: does not exist")
45
+ end
46
+ end
47
+
48
+ def display_name
49
+ comment
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module QueueProcessor
2
+ module DependentCalculation
3
+ module Relationships
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def parent_calculation
8
+ self.send( self.class.parent_calculation )
9
+ end
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,13 @@
1
+ require 'active_support/concern'
2
+
3
+ module QueueProcessor
4
+ module DependentCalculation
5
+ extend ActiveSupport::Concern
6
+
7
+ include QueueProcessor::DependentCalculation::Config
8
+ include QueueProcessor::DependentCalculation::QueueControl
9
+ include QueueProcessor::DependentCalculation::Relationships
10
+
11
+
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ module QueueProcessor
2
+ module DependentCalculationGroup
3
+ module Config
4
+ extend ActiveSupport::Concern
5
+ include QueueProcessor::ConfigBase
6
+ include QueueProcessor::QueueControlConfig
7
+
8
+ included do
9
+ extend Config
10
+ end
11
+
12
+ module Config
13
+ def dependent_calculation_group_config(&block)
14
+ yield self if block_given?
15
+ end
16
+
17
+ # The name of the has_many association giving our calculations
18
+ #
19
+ # * <tt>Default:</tt> :dependent_calculations
20
+ # * <tt>Accepts:</tt> Symbol
21
+ def dependent_calculation_association(value = nil)
22
+ rw_config(:dependent_calculation_association, value, :dependent_calculations)
23
+ end
24
+ alias_method :dependent_calculation_association=, :dependent_calculation_association
25
+
26
+ # The name of the belongs_to association giving the root_calculation
27
+ #
28
+ # * <tt>Default:</tt> :root_calculation
29
+ # * <tt>Accepts:</tt> Symbol
30
+ def parent_calculation(value = nil)
31
+ rw_config(:parent_calculation, value, :root_calculation)
32
+ end
33
+ alias_method :parent_calculation=, :parent_calculation
34
+
35
+ # When the calculation should run.
36
+ #
37
+ # * <tt>Default:</tt> A proc evaluating to nil (now in DJ terms)
38
+ # * <tt>Accepts:</tt> Proc or Symbol
39
+ def run_at(value = nil)
40
+ rw_config(:run_at, value, lambda {|object| nil })
41
+ end
42
+ alias_method :run_at=, :run_at
43
+
44
+ # Priority of the calculation
45
+ #
46
+ # * <tt>Default:</tt> A proc evaluating to 1
47
+ # * <tt>Accepts:</tt> Proc or Symbol
48
+ def priority(value = nil)
49
+ rw_config(:priority, value, lambda {|object| 1 })
50
+ end
51
+ alias_method :priority=, :priority
52
+
53
+
54
+ # Which named queue should be used
55
+ #
56
+ # * <tt>Default:</tt> A proc delegating queue to the parent (if applicable) nil (any queu)
57
+ # * <tt>Accepts:</tt> Proc or Symbol
58
+ def queue(value = nil)
59
+ rw_config(:queue, value, lambda { |object| object.parent_calculation&.queue})
60
+ end
61
+ alias_method :queue=, :queue
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ module QueueProcessor
2
+ module DependentCalculationGroup
3
+ module QueueControl
4
+ extend ActiveSupport::Concern
5
+
6
+ def should_cancel?
7
+ parent_calculation.nil? || parent_calculation.calculation_stale?
8
+ end
9
+
10
+ def describe
11
+ "#{self.class.to_s} - queue dependencies"
12
+ end
13
+
14
+ def queue_work
15
+ Delayed::Job.enqueue(CalculationRequest.new(id, describe, self.class), delayed_job_options)
16
+ end
17
+
18
+ def delayed_jobs
19
+ Delayed::Backend::ActiveRecord::Job.where("handler ilike '%DependentCalculationGroup::QueueControl::CalculationRequest%' and handler like '%id: #{id}%'")
20
+ end
21
+
22
+ def something_queued?
23
+ in_queue? || dependencies_in_queue?
24
+ end
25
+
26
+ def in_queue?
27
+ delayed_jobs.count > 0
28
+ end
29
+
30
+ def dependencies_in_queue?
31
+ dependent_calculations.inject(false) { |result, dependent_calculation| result || dependent_calculation.in_queue? }
32
+ end
33
+
34
+ def delayed_job_options
35
+ {:priority=>priority, :run_at => run_at, :queue => queue}.reject {|k,v| v.nil?}
36
+ end
37
+
38
+ class CalculationRequest < Struct.new(:id, :comment, :clazz)
39
+ def perform
40
+ obj = clazz.where(:id => id).first
41
+ if obj.present?
42
+ obj.fire_events(:process)
43
+ else
44
+ Rails.logger.warn("#{clazz} #{id}: does not exist")
45
+ end
46
+ end
47
+
48
+ def display_name
49
+ "queue dependent calculations for #{comment}"
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,23 @@
1
+ module QueueProcessor
2
+ module DependentCalculationGroup
3
+ module Relationships
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def parent_calculation
8
+ self.send( self.class.parent_calculation )
9
+ end
10
+
11
+ def dependent_calculations
12
+ self.send( self.class.dependent_calculation_association )
13
+ end
14
+
15
+ def dependent_calculations_done?
16
+ dependent_calculations.count == dependent_calculations.where(:completed => true).count
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,77 @@
1
+ module QueueProcessor
2
+ module DependentCalculationGroup
3
+ module StateMachines
4
+ extend ActiveSupport::Concern
5
+
6
+ # optional calculation associated with this group
7
+ def perform_calculation
8
+ end
9
+
10
+ included do
11
+ # this state machine is used to keep track of a group of related calculations that were created from a parent calculation
12
+ # When the dependent calculations all finish, then the root calculation should be marked as done. However, if the parent calculation
13
+ # become stale (by being queued agian) then we should cancel our calculations - they are no longer valid and release the
14
+ # parent calculation to be calculated again
15
+ state_machine :state, :initial => :initial, :use_transactions => false do
16
+ state :initial, :queued, :processing, :waiting, :complete, :cancelled, :old do
17
+ end
18
+
19
+ event :add_to_queue do
20
+ transition :initial => :queued
21
+ end
22
+
23
+ event :process do
24
+ transition :queued => :cancelled, :if => lambda {|me| me.should_cancel? }
25
+ transition :queued => :processing
26
+ end
27
+
28
+ # we don't fire this event, this is just to show you that in cleanup, we expire old calculations
29
+ event :expire do
30
+ transition [:cancelled, :complete] => :old
31
+ end
32
+
33
+ event :finish do
34
+ transition :processing => :complete, :if => lambda {|me| me.dependent_calculations.empty? || me.dependent_calculations_done?}
35
+ transition :processing => :waiting, :if => lambda {|me| !me.dependent_calculations.empty? }
36
+ transition [:processing, :waiting] => :cancelled, :if => lambda {|me| me.should_cancel? && me.dependent_calculations_done? }
37
+ transition [:processing, :waiting] => :complete, :if =>lambda {|me| me.dependent_calculations_done?}
38
+ transition :waiting => :waiting # still more work to do
39
+ end
40
+
41
+ # Put ourselves on the queue
42
+ after_transition :initial => :queued do |me|
43
+ Rails.logger.debug {"#{me.describe}: is being added to the queue"}
44
+
45
+ me.update_attribute(:queued_at, Time.now.utc)
46
+ me.queue_work
47
+ end
48
+
49
+ # We're off the queue, get to work
50
+ after_transition :queued => :processing do |me|
51
+ Rails.logger.debug {"#{me.describe}: is being processed"}
52
+
53
+ me.perform_calculation
54
+ me.create_and_enqueue_dependent_calculations
55
+ me.fire_events(:finish)
56
+ end
57
+
58
+ # We are about to finish, cleanup old calculations and perform any
59
+ # finalization with these calculations. For instance, rank optimizing
60
+ # them.
61
+ before_transition any => [:complete] do |me|
62
+ me.cleanup_old_calculations
63
+ end
64
+
65
+ # Finished - clean up
66
+ after_transition any => [:complete, :cancelled] do |me|
67
+ Rails.logger.debug {"#{me.describe}: is done (#{me.cancelled? ? 'Cancelled' : 'Complete'})"}
68
+ me.parent_calculation.fire_events(:finish_dependent_calculations)
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support/concern'
2
+
3
+ module QueueProcessor
4
+
5
+ module DependentCalculationGroup
6
+ extend ActiveSupport::Concern
7
+
8
+ include QueueProcessor::DependentCalculationGroup::Config
9
+ include QueueProcessor::DependentCalculationGroup::StateMachines
10
+ include QueueProcessor::DependentCalculationGroup::QueueControl
11
+ include QueueProcessor::DependentCalculationGroup::Relationships
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module QueueProcessor
2
+ module QueueControlConfig
3
+ extend ActiveSupport::Concern
4
+
5
+ def run_at
6
+ _config_method(:run_at)
7
+ end
8
+
9
+ def priority
10
+ _config_method(:priority)
11
+ end
12
+
13
+ def queue
14
+ _config_method(:queue)
15
+ end
16
+
17
+ def _config_method(method)
18
+ # call the method on the class, this should either give us a proc, a string or something else
19
+ result = self.class.send(method)
20
+ return nil if result.nil?
21
+
22
+ # call if it a method, send if it we respond to it as a method call, otherwise return the result
23
+ result.respond_to?(:call) ? result.call(self) :
24
+ self.respond_to?(result.to_s.to_sym) ? self.send(result.to_s.to_sym) : result
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ module QueueProcessor
2
+ module RootCalculation
3
+ module Config
4
+ extend ActiveSupport::Concern
5
+ include QueueProcessor::ConfigBase
6
+ include QueueProcessor::QueueControlConfig
7
+
8
+ included do
9
+ extend Config
10
+ end
11
+
12
+ def recover_after
13
+ self.class.recover_after
14
+ end
15
+
16
+ module Config
17
+
18
+ def root_calculation_config(&block)
19
+ yield self if block_given?
20
+ end
21
+
22
+ # The name of the has_many association are our dependent calculations
23
+ #
24
+ # * <tt>Default:</tt> :dependent_calculation_groups
25
+ # * <tt>Accepts:</tt> Symbol
26
+ def dependent_calculation_group_association(value = nil)
27
+ rw_config(:dependent_calculation_group_association, value, :dependent_calculation_groups)
28
+ end
29
+ alias_method :dependent_calculation_group_association=, :dependent_calculation_group_association
30
+
31
+ # When the calculation should run.
32
+ #
33
+ # * <tt>Default:</tt> A proc evaluating to 5 seconds in the future
34
+ # * <tt>Accepts:</tt> Proc or Symbol
35
+ def run_at(value = nil)
36
+ rw_config(:run_at, value, lambda {|object| Time.now + 5.seconds })
37
+ end
38
+ alias_method :run_at=, :run_at
39
+
40
+ # Priority of the calculation
41
+ #
42
+ # * <tt>Default:</tt> A proc evaluating to 0 (highest priority in DJ terms)
43
+ # * <tt>Accepts:</tt> Proc or Symbol
44
+ def priority(value = nil)
45
+ rw_config(:priority, value, lambda {|object| 0 })
46
+ end
47
+ alias_method :priority=, :priority
48
+
49
+ # How long we'll let a job set in the queue before process it anyway
50
+ #
51
+ # * <tt>Default:</tt> nil
52
+ # * <tt>Accepts:</tt> a time, for example: 10.minutes
53
+ def recover_after(value = nil)
54
+ rw_config(:recover_after, value, nil )
55
+ end
56
+ alias_method :recover_after=, :recover_after
57
+
58
+
59
+ # Which named queue should be used
60
+ #
61
+ # * <tt>Default:</tt> nil, any queue
62
+ # * <tt>Accepts:</tt> Proc or Symbol
63
+ def queue(value = nil)
64
+ rw_config(:queue, value, nil)
65
+ end
66
+ alias_method :queue=, :queue
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,145 @@
1
+ module QueueProcessor
2
+ module RootCalculation
3
+ module QueueControl
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+
8
+ def before_enqueue
9
+ end
10
+
11
+ mattr_accessor :delayed_job_queued_at
12
+
13
+ def delayed_jobs
14
+ Delayed::Backend::ActiveRecord::Job.where("handler ilike '%RootCalculation::QueueControl::CalculationRequest%' and handler like '%id: #{id}%'")
15
+ end
16
+
17
+ def add_to_queue
18
+ old_queue_time = self.queued_at
19
+ queue_time = Time.now.utc
20
+
21
+ # We used to round(3) but that was sometimes too aggressive. There seems
22
+ # to either be a float precision error, or there is a difference in the
23
+ # seralization of Time and TimeStamp records via delayed job / postgres
24
+ # that would very rarely cause round(3) comparisions to fail when they
25
+ # should have succeeded
26
+ if (old_queue_time.to_f.round(2) == queue_time.to_f.round(2))
27
+ Rails.logger.warn("#{self.describe}: queued again too quickly")
28
+ sleep(0.001)
29
+ add_to_queue
30
+ return
31
+ end
32
+
33
+ Rails.logger.info("#{self.describe}: added to the queue at #{queue_time} (#{queue_time.to_f})")
34
+
35
+ before_enqueue
36
+ touch
37
+
38
+ # XXX BOGON ALERT DO NOT USE UPDATE_ATTRIBUTE HERE - rails will do a second-based comparision of the timestamp
39
+ # and will not update the column if only the milliseconds are different (https://gist.github.com/johnnaegle/0599240ca925f445f031)
40
+ update_column(:queued_at, queue_time)
41
+
42
+ do_expire_calculation
43
+
44
+ self.queue_work(queue_time)
45
+ end
46
+
47
+ # expire our GMI - make it stale
48
+ def do_expire_calculation
49
+ begin
50
+ self.with_lock do
51
+ fire_events!(:expire_calculation)
52
+ end
53
+ rescue StateMachine::Error => e
54
+ Rails.logger.warn("#{self.describe}: invalid state transition expiring calculation: #{self.attributes}, changed: #{self.changed_attributes.map {|k,v| {k => {v => self.send(k) }}}}")
55
+ raise e
56
+ end
57
+ end
58
+
59
+ # called when the delayed job is deserailized and ready to run. The calculation
60
+ # can be performed if the time the delayed job was queued matches the time
61
+ # the calculation was queued. We round the times to two places as there
62
+ # seem to be float / serialization differences between postgres / YAML /
63
+ # Time / TimeStamp objects. We didn't quite identify the problem, but
64
+ # this fixes error - jobs not running when they should.
65
+ def can_start?
66
+ self.queued_at.present? && self.queued_at.to_f.round(2) == self.delayed_job_queued_at.to_f.round(2)
67
+ end
68
+
69
+ def describe
70
+ "Calculate #{self.class.to_s}: #{self.id}"
71
+ end
72
+
73
+ def queue_work(queued_at)
74
+ Rails.logger.debug {"#{self.describe}: queued at #{queued_at.to_f}"}
75
+ Delayed::Job.enqueue(CalculationRequest.new(self.id, queued_at, self.describe, self.class), delayed_job_options)
76
+ end
77
+
78
+ def delayed_job_options
79
+ {:priority=>priority, :run_at => run_at, :queue => queue}.reject {|k,v| v.nil?}
80
+ end
81
+
82
+
83
+ def requeue_work(queued_at)
84
+ Rails.logger.debug {"#{self.describe}: re-queued at #{queued_at.to_f}"}
85
+ do_expire_calculation # ensure that our calculation in statel - we're in the queue, it better be
86
+ queue_work(queued_at)
87
+ end
88
+
89
+ def something_queued?
90
+ in_queue? || dependencies_in_queue?
91
+ end
92
+
93
+ def in_queue?
94
+ delayed_jobs.count > 0
95
+ end
96
+
97
+ def dependencies_in_queue?
98
+ dependent_calculation_groups.inject(false) { |result, dependent_calculation_group| result|| dependent_calculation_group.something_queued? }
99
+ end
100
+
101
+ class CalculationRequest < Struct.new(:id, :queued_at, :description, :clazz)
102
+ def perform
103
+ obj = clazz.where("id=? and date_trunc('milliseconds',queued_at)=date_trunc('milliseconds',?::timestamp)",id, queued_at).first
104
+
105
+ if (obj.present?)
106
+ Rails.logger.debug {"#{obj.describe}: dequeued by delayed job"}
107
+
108
+ obj.delayed_job_queued_at = queued_at
109
+ if obj.fire_events(:dequeue_processing)
110
+ Rails.logger.info("#{obj.describe}: starting processing.")
111
+ result = obj.fire_events(:start_processing)
112
+
113
+ if (result)
114
+ Rails.logger.info("#{obj.describe}: finished calculation and done in delayed job.")
115
+ end
116
+ else
117
+ # somebody else is running, we have to wait for them to finish
118
+ if (obj.calculating?)
119
+ if obj.recover_after.present? && (Time.now - queued_at) >= obj.recover_after
120
+ Rails.logger.error("#{obj.describe}: has been stuck processing for too long (#{obj.recover_after} seconds) - reseting state machines and re-queueing")
121
+ obj.reset_state_machines!
122
+ obj.add_to_queue
123
+ else
124
+ obj.requeue_work(queued_at)
125
+ end
126
+ end
127
+ end
128
+ else
129
+ obj = clazz.where(:id => id).first
130
+ if obj.nil?
131
+ Rails.logger.warn("#{clazz}: #{id}: Skipping: #{id} - Does not exist")
132
+ else
133
+ Rails.logger.warn("#{obj.describe}: Skipped - Queued at: #{obj.queued_at} doesn't match delayed job queued at: #{queued_at}")
134
+ end
135
+ end
136
+ end
137
+
138
+ def display_name
139
+ "calculate #{description} (id=#{id}, queued_at=#{queued_at})"
140
+ end
141
+ end
142
+
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,24 @@
1
+ module QueueProcessor
2
+ module RootCalculation
3
+ module Relationships
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def current_dependent_calculations
9
+ dependent_calculation_groups.where("created_at >= coalesce(?,?::timestamp without time zone)", self.processing_at, Time.now.utc)
10
+ end
11
+
12
+ def completed_dependent_calculations
13
+ dependent_calculation_groups.where("state = ?", 'complete')
14
+ end
15
+
16
+ def dependent_calculation_groups
17
+ self.send( self.class.dependent_calculation_group_association )
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,225 @@
1
+ module QueueProcessor
2
+ module RootCalculation
3
+ module StateMachines
4
+ extend ActiveSupport::Concern
5
+
6
+ def reset_state_machines!
7
+ reload
8
+ update_attributes(:processing_at => nil,:calculation_state => 'available', :dependent_calculations_state => 'idle', :processing_state => 'idle')
9
+ end
10
+
11
+ def stuck?
12
+ (processing_waiting? || dependent_calculations_waiting?) && !dependencies_in_queue?
13
+ end
14
+
15
+ def finished?
16
+ processing_idle? &&
17
+ calculation_available? &&
18
+ dependent_calculations_idle? &&
19
+ !something_queued?
20
+ end
21
+
22
+
23
+ # We've come off the queue and need to be calculated
24
+ def perform_work
25
+ do_calculation
26
+ check_and_start_dependencies
27
+ publish_calculation
28
+
29
+ true
30
+ end
31
+
32
+ # Perform whatever calculation is associated with this class
33
+ def do_calculation
34
+ Rails.logger.debug {"#{self.describe}: starting calculation"}
35
+
36
+ # mark as waiting
37
+ self.with_lock do
38
+
39
+ begin
40
+ self.fire_events!(:start_calculation)
41
+ rescue StateMachine::Error => e
42
+ Rails.logger.warn("#{self.describe} invalid state transition starting calculation: #{self.attributes}, changed: #{self.changed_attributes.map {|k,v| {k => {v => self.send(k) }}}}")
43
+ raise e
44
+ end
45
+ end
46
+
47
+ # callback to do the actual calculation
48
+ perform_calculation
49
+
50
+ # mark as calculated if we are still waiting (does this need to do a with_lock?)
51
+ self.with_lock do
52
+ self.fire_events(:finish_calculation)
53
+ end
54
+
55
+ Rails.logger.debug {"#{self.describe}: finished calculation"}
56
+ end
57
+
58
+ # Publish the calculation we just finished. The calculation is
59
+ # published before we've finished. We're finished when we've queued any and all dependent
60
+ # calculations, but the number we just computed is available
61
+ def publish_calculation
62
+ Rails.logger.debug {"#{self.describe}: publishing calculation"}
63
+
64
+ # we can only transition from calulating => available. If something else
65
+ # queued this calculation again, we'll keep the calculation unavailable
66
+ self.with_lock do
67
+ self.fire_events(:release_calculation)
68
+ end
69
+ end
70
+
71
+ # we've finished our calculation and all dependent calculations have finished
72
+ def finish_work
73
+ Rails.logger.debug {"#{self.describe}: Cleaning up"}
74
+
75
+ unless(processing_at.nil?)
76
+ elapsed_time = (Time.now - self.processing_at)
77
+ Rails.logger.info("#{self.describe}: finished after #{elapsed_time.round(2)} seconds")
78
+ self.started_processing_at = self.processing_at # so observers can record how long it took to finish
79
+ update_attribute(:processing_at, nil)
80
+ end
81
+ reload
82
+ end
83
+
84
+ # After we finish our calculation (but not all the dependencies) look for dependent work to
85
+ # do and queue it. Set processing at to indicate that we have work in progress and later
86
+ # calculations should wait until the dependent calculations we created all finish.
87
+ def check_and_start_dependencies
88
+ if (has_dependent_calculations?)
89
+ do_dependencies = self.with_lock do
90
+ if (self.processing_at.nil? && !calculation_stale?)
91
+ update_attribute(:processing_at, delayed_job_queued_at)
92
+
93
+ # this performs a callback, create_and_enqueue_dependent_calculations, to do queue up any calculations
94
+ begin
95
+ self.fire_events!(:start_dependent_calculations)
96
+ rescue StateMachine::Error => e
97
+ Rails.logger.warn("#{self.describe}: invalid state transition for start_dependent_calculations: #{self.attributes}, changed: #{self.changed_attributes.map {|k,v| {k => {v => self.send(k) }}}}")
98
+ raise e
99
+ end
100
+
101
+ else
102
+ Rails.logger.warn("#{self.describe}: should have started dependencies, but something else set processing at ")
103
+ false
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ # the queued_at stored with the delayed job. We can go from :idle => :waiting if this matches the queued_at time on the index run
111
+ mattr_accessor :started_processing_at
112
+
113
+ included do
114
+
115
+ state_machine :processing_state, :initial => :idle, :use_transactions => false, :namespace => 'processing' do
116
+ state :idle do
117
+ def calculating?
118
+ false
119
+ end
120
+ end
121
+
122
+ state :started, :waiting do
123
+ def calculating?
124
+ true
125
+ end
126
+ end
127
+
128
+ after_transition :idle => :started, :do => :perform_work
129
+
130
+ before_transition [:started, :waiting] => :idle, :do => :finish_work
131
+
132
+ # an index run can be queued any time. They can be processed if there is nothing later in the queue
133
+ event :dequeue do
134
+ transition :idle => :started, :if => lambda {|me| me.can_start?}
135
+ end
136
+
137
+ event :start do
138
+ transition :started => :waiting, :if => lambda {|me| me.has_dependent_calculations? && me.current_dependent_calculations.present?}
139
+ transition :started => :idle, :if => lambda {|me| !me.has_dependent_calculations? || me.current_dependent_calculations.empty?}
140
+ end
141
+
142
+ event :finish do
143
+ transition :started => :idle, :if => lambda {|me| me.current_dependent_calculations.empty?}
144
+ transition :waiting => :idle
145
+ end
146
+ end
147
+
148
+
149
+ state_machine :calculation_state, :initial => :stale, :use_transactions => true, :namespace => 'calculation' do
150
+ state :available do
151
+ def calculation_available?
152
+ true
153
+ end
154
+ end
155
+
156
+ state :stale, :waiting, :calculated do
157
+ def calculation_available?
158
+ false
159
+ end
160
+ end
161
+
162
+ # expire the GMI whenever we queue this index run
163
+ event :expire do
164
+ transition any => :stale
165
+ end
166
+
167
+ event :start do
168
+ transition [:stale, :available] => :waiting
169
+ end
170
+
171
+ event :finish do
172
+ transition :waiting => :calculated
173
+ end
174
+
175
+ event :release do
176
+ transition :calculated => :available
177
+ end
178
+ end
179
+
180
+
181
+ state_machine :dependent_calculations_state, :initial => :idle, :use_transactions => false, :namespace => 'dependent_calculations' do
182
+ state :idle, :started, :waiting do
183
+ end
184
+
185
+ event :start do
186
+ transition :idle => :idle, :if => lambda {|me|
187
+ me.calculation_stale?
188
+ } # something else queued so just abort early
189
+ transition :idle => :started # otherwise, go ahead and queue up the work
190
+ end
191
+
192
+ # queue up the work
193
+ before_transition :idle => :started do |me|
194
+ me.create_and_enqueue_dependent_calculations
195
+ end
196
+
197
+ after_transition :idle => :started do |me|
198
+ me.fire_events(:finish_dependent_calculations) # maybe we didn't queue anything or we finished it REALLY fast, so check that
199
+ end
200
+
201
+ event :finish do
202
+ # TODO: can finish if there are no calculations to perform or all the calculations are completed
203
+ transition :waiting => :idle, :if => lambda {|me| me.current_dependent_calculations.count == me.current_dependent_calculations.where(:state => [:complete, :cancelled, :old]).count }
204
+
205
+ transition :started => :idle, :if => lambda {|me| !me.current_dependent_calculations.exists? }
206
+ transition :started => :idle, :if => lambda {|me| me.current_dependent_calculations.where(:state => [:complete, :cancelled, :old]).count == me.current_dependent_calculations.count}
207
+ transition :started => :waiting
208
+ end
209
+
210
+ after_transition [:started, :waiting] => :idle do |me|
211
+ Rails.logger.info("#{me.describe} is complete.")
212
+ begin
213
+ me.fire_events!(:finish_processing)
214
+ rescue StateMachine::Error => e
215
+ Rails.logger.warn("#{me.describe} invalid state transition finishing processing: #{me.attributes}, changed: #{me.changed_attributes.map {|k,v| {k => {v => me.send(k) }}}}")
216
+ raise e
217
+ end
218
+
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+
@@ -0,0 +1,13 @@
1
+ require 'active_support/concern'
2
+
3
+ module QueueProcessor
4
+ module RootCalculation
5
+ extend ActiveSupport::Concern
6
+
7
+ include QueueProcessor::RootCalculation::Config
8
+ include QueueProcessor::RootCalculation::StateMachines
9
+ include QueueProcessor::RootCalculation::QueueControl
10
+ include QueueProcessor::RootCalculation::Relationships
11
+
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module QueueProcessor
2
+ VERSION = "0.0.8"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'queue-processor/config_base'
2
+ require 'queue-processor/queue_control_config'
3
+ require 'queue-processor/root_calculation/config'
4
+ require 'queue-processor/root_calculation/queue_control'
5
+ require 'queue-processor/root_calculation/relationships'
6
+ require 'queue-processor/root_calculation/state_machines'
7
+ require 'queue-processor/root_calculation'
8
+ require 'queue-processor/dependent_calculation_group/config'
9
+ require 'queue-processor/dependent_calculation_group/queue_control'
10
+ require 'queue-processor/dependent_calculation_group/relationships'
11
+ require 'queue-processor/dependent_calculation_group/state_machines'
12
+ require 'queue-processor/dependent_calculation_group'
13
+ require 'queue-processor/dependent_calculation/config'
14
+ require 'queue-processor/dependent_calculation/queue_control'
15
+ require 'queue-processor/dependent_calculation/relationships'
16
+ require 'queue-processor/dependent_calculation'
17
+
18
+ module QueueProcessor
19
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :queue-processor do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: queue-processor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
+ platform: ruby
6
+ authors:
7
+ - John Naegle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.1.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: delayed_job
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: delayed_job_active_record
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Queue a main calculation, then groups of dependent calculations and wait
70
+ for them to finish.
71
+ email:
72
+ - john.naegle@goodmeasures.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - MIT-LICENSE
78
+ - README.md
79
+ - Rakefile
80
+ - lib/queue-processor.rb
81
+ - lib/queue-processor/config_base.rb
82
+ - lib/queue-processor/dependent_calculation.rb
83
+ - lib/queue-processor/dependent_calculation/config.rb
84
+ - lib/queue-processor/dependent_calculation/queue_control.rb
85
+ - lib/queue-processor/dependent_calculation/relationships.rb
86
+ - lib/queue-processor/dependent_calculation_group.rb
87
+ - lib/queue-processor/dependent_calculation_group/config.rb
88
+ - lib/queue-processor/dependent_calculation_group/queue_control.rb
89
+ - lib/queue-processor/dependent_calculation_group/relationships.rb
90
+ - lib/queue-processor/dependent_calculation_group/state_machines.rb
91
+ - lib/queue-processor/queue_control_config.rb
92
+ - lib/queue-processor/root_calculation.rb
93
+ - lib/queue-processor/root_calculation/config.rb
94
+ - lib/queue-processor/root_calculation/queue_control.rb
95
+ - lib/queue-processor/root_calculation/relationships.rb
96
+ - lib/queue-processor/root_calculation/state_machines.rb
97
+ - lib/queue-processor/version.rb
98
+ - lib/tasks/queue-processor_tasks.rake
99
+ homepage: https://github.com/GoodMeasuresLLC/queue-processor
100
+ licenses: []
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.1.2
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: A processing model for a main calculation and a set of dependent calculations.
121
+ test_files: []