queue-processor 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +103 -0
- data/Rakefile +38 -0
- data/lib/queue-processor/config_base.rb +27 -0
- data/lib/queue-processor/dependent_calculation/config.rb +56 -0
- data/lib/queue-processor/dependent_calculation/queue_control.rb +56 -0
- data/lib/queue-processor/dependent_calculation/relationships.rb +15 -0
- data/lib/queue-processor/dependent_calculation.rb +13 -0
- data/lib/queue-processor/dependent_calculation_group/config.rb +65 -0
- data/lib/queue-processor/dependent_calculation_group/queue_control.rb +55 -0
- data/lib/queue-processor/dependent_calculation_group/relationships.rb +23 -0
- data/lib/queue-processor/dependent_calculation_group/state_machines.rb +77 -0
- data/lib/queue-processor/dependent_calculation_group.rb +13 -0
- data/lib/queue-processor/queue_control_config.rb +28 -0
- data/lib/queue-processor/root_calculation/config.rb +70 -0
- data/lib/queue-processor/root_calculation/queue_control.rb +145 -0
- data/lib/queue-processor/root_calculation/relationships.rb +24 -0
- data/lib/queue-processor/root_calculation/state_machines.rb +225 -0
- data/lib/queue-processor/root_calculation.rb +13 -0
- data/lib/queue-processor/version.rb +3 -0
- data/lib/queue-processor.rb +19 -0
- data/lib/tasks/queue-processor_tasks.rake +4 -0
- metadata +121 -0
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,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,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
|
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: []
|