burstflow 0.1.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/burst/job.rb ADDED
@@ -0,0 +1,187 @@
1
+ class Burst::Job
2
+
3
+ include Burst::Model
4
+
5
+ define_stored_attributes :id, :workflow_id, :klass, :params, :incoming, :outgoing, :payloads, :output
6
+ define_stored_attributes :enqueued_at, :started_at, :finished_at, :failed_at, :suspended_at, :resumed_at
7
+
8
+ SUSPEND = 'suspend'.freeze
9
+
10
+ class Error < ::RuntimeError; end
11
+
12
+ def initialize(workflow, hash_store = {})
13
+ @workflow = workflow
14
+ assign_default_values(hash_store)
15
+ end
16
+
17
+ def assign_default_values(hash_store)
18
+ set_model(hash_store.deep_dup)
19
+
20
+ self.id ||= SecureRandom.uuid
21
+ self.workflow_id ||= @workflow.id
22
+ self.klass ||= self.class.to_s
23
+ self.incoming ||= []
24
+ self.outgoing ||= []
25
+ end
26
+
27
+ def reload
28
+ assign_default_values(@workflow.get_job_hash(self.id))
29
+ end
30
+
31
+ def save!
32
+ @workflow.with_lock do
33
+ @workflow.set_job(self)
34
+ @workflow.save!
35
+ yield if block_given?
36
+ end
37
+ end
38
+
39
+ def self.from_hash(workflow, hash_store)
40
+ hash_store[:klass].constantize.new(workflow, hash_store)
41
+ end
42
+
43
+ # execute this code by ActiveJob. You may return Burst::Job::SUSPEND to suspend job, or call suspend method
44
+ def perform; end
45
+
46
+ # execute this code when resumes after suspending
47
+ def resume(data)
48
+ set_output(data)
49
+ end
50
+
51
+ # store data to be available for next jobs
52
+ def set_output(data)
53
+ self.output = data
54
+ end
55
+
56
+ # mark execution as suspended
57
+ def suspend
58
+ set_output(SUSPEND)
59
+ end
60
+
61
+ def configure
62
+ @workflow.with_lock do
63
+ yield
64
+ @workflow.resolve_dependencies
65
+ @workflow.save!
66
+ @workflow.all_jobs.to_a.each(&:save!)
67
+ reload
68
+ end
69
+ end
70
+
71
+ def run(klass, opts = {})
72
+ opts[:after] = [*opts[:after], self.id].uniq
73
+ opts[:before] = [*opts[:before], *self.outgoing].uniq
74
+ @workflow.run(klass, opts)
75
+ end
76
+
77
+ def attributes
78
+ {
79
+ workflow_id: self.workflow_id,
80
+ id: self.id,
81
+ klass: self.klass,
82
+ params: params,
83
+ incoming: self.incoming,
84
+ outgoing: self.outgoing,
85
+ output: output,
86
+ started_at: started_at,
87
+ enqueued_at: enqueued_at,
88
+ finished_at: finished_at,
89
+ failed_at: failed_at,
90
+ suspended_at: suspended_at,
91
+ resumed_at: resumed_at
92
+ }
93
+ end
94
+
95
+ # mark job as enqueued when it is scheduled to queue
96
+ def enqueue!
97
+ raise Error.new('Already enqueued') if enqueued?
98
+ self.enqueued_at = current_timestamp
99
+ self.started_at = nil
100
+ self.finished_at = nil
101
+ self.failed_at = nil
102
+ self.suspended_at = nil
103
+ self.resumed_at = nil
104
+ end
105
+
106
+ # mark job as started when it is start performing
107
+ def start!
108
+ raise Error.new('Already started') if started?
109
+ self.started_at = current_timestamp
110
+ end
111
+
112
+ # mark job as finished when it is finish performing
113
+ def finish!
114
+ raise Error.new('Already finished') if finished?
115
+ self.finished_at = current_timestamp
116
+ end
117
+
118
+ # mark job as failed when it is failed
119
+ def fail!
120
+ raise Error.new('Already failed') if failed?
121
+ self.finished_at = self.failed_at = current_timestamp
122
+ end
123
+
124
+ # mark job as suspended
125
+ def suspend!
126
+ self.suspended_at = current_timestamp
127
+ end
128
+
129
+ # mark job as resumed
130
+ def resume!
131
+ raise Error.new('Not suspended ') unless suspended?
132
+ raise Error.new('Already resumed ') if resumed?
133
+ self.resumed_at = current_timestamp
134
+ end
135
+
136
+ def enqueued?
137
+ !enqueued_at.nil?
138
+ end
139
+
140
+ def started?
141
+ !started_at.nil?
142
+ end
143
+
144
+ def finished?
145
+ !finished_at.nil?
146
+ end
147
+
148
+ def running?
149
+ started? && !finished? && !suspended?
150
+ end
151
+
152
+ def failed?
153
+ !failed_at.nil?
154
+ end
155
+
156
+ def suspended?
157
+ !suspended_at.nil? && !resumed?
158
+ end
159
+
160
+ def resumed?
161
+ !resumed_at.nil?
162
+ end
163
+
164
+ def succeeded?
165
+ finished? && !failed?
166
+ end
167
+
168
+ def ready_to_start?
169
+ !running? && !enqueued? && !finished? && !failed? && parents_succeeded?
170
+ end
171
+
172
+ def initial?
173
+ incoming.empty?
174
+ end
175
+
176
+ def current_timestamp
177
+ Time.now.to_i
178
+ end
179
+
180
+ def parents_succeeded?
181
+ incoming.all? do |id|
182
+ @workflow.get_job(id).succeeded?
183
+ end
184
+ end
185
+
186
+
187
+ end
@@ -0,0 +1,79 @@
1
+ class Burst::Manager
2
+
3
+ attr_accessor :workflow
4
+
5
+ def initialize(workflow)
6
+ @workflow = workflow
7
+ end
8
+
9
+ def start
10
+ workflow.with_lock do
11
+ raise 'Already started' unless workflow.initial?
12
+
13
+ workflow.initial_jobs.each do |job|
14
+ enqueue_job!(job)
15
+ end
16
+ end
17
+ end
18
+
19
+ def enqueue_job!(job)
20
+ job.enqueue!
21
+ job.save! do
22
+ Burst::Worker.perform_later(workflow.id, job.id)
23
+ end
24
+ end
25
+
26
+ def resume!(job, data)
27
+ job.resume!
28
+ job.save! do
29
+ Burst::Worker.perform_later(workflow.id, job.id, data)
30
+ end
31
+ end
32
+
33
+ def start_job!(job)
34
+ job.start!
35
+ job.save!
36
+
37
+ job.perform
38
+ end
39
+
40
+ def resume_job!(job, data)
41
+ job.resume(data)
42
+ end
43
+
44
+ def suspend_job!(job)
45
+ job.suspend!
46
+ job.save!
47
+ end
48
+
49
+ def finish_job!(job)
50
+ job.finish!
51
+ job.save!
52
+
53
+ workflow.with_lock do
54
+ enqueue_outgoing_jobs(job)
55
+ end
56
+ end
57
+
58
+ def job_performed!(job, result)
59
+ if result == Burst::Job::SUSPEND || job.output == Burst::Job::SUSPEND
60
+ suspend_job!(job)
61
+ else
62
+ finish_job!(job)
63
+ end
64
+ end
65
+
66
+ def fail_job!(job)
67
+ job.fail!
68
+ job.save!
69
+ end
70
+
71
+ def enqueue_outgoing_jobs(job)
72
+ job.outgoing.each do |job_id|
73
+ out = workflow.get_job(job_id)
74
+
75
+ enqueue_job!(out) if out.ready_to_start?
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,49 @@
1
+ module Burst::Model
2
+
3
+ extend ActiveSupport::Concern
4
+
5
+ included do |klass|
6
+ klass.include ActiveModel::Model
7
+ klass.include ActiveModel::Dirty
8
+ klass.include ActiveModel::Serialization
9
+ klass.extend ActiveModel::Callbacks
10
+
11
+ attr_accessor :model
12
+
13
+ def set_model(model)
14
+ @model = model
15
+ end
16
+
17
+ def attributes=(hash)
18
+ hash.each do |key, value|
19
+ send("#{key}=", value)
20
+ end
21
+ end
22
+
23
+ def ==(other)
24
+ instance_variable_get('@model') == other.instance_variable_get('@model')
25
+ end
26
+
27
+ def as_json(*_args)
28
+ serializable_hash
29
+ end
30
+ end
31
+
32
+ class_methods do
33
+ def define_stored_attributes(*keys)
34
+ keys.each do |key|
35
+ define_attribute_methods key.to_sym
36
+
37
+ define_method key.to_sym do
38
+ return @model[key.to_s]
39
+ end
40
+
41
+ define_method "#{key}=".to_sym do |v|
42
+ send("#{key}_will_change!") if v != send(key)
43
+ return @model[key.to_s] = v
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,42 @@
1
+ class Burst::Worker < ::ActiveJob::Base
2
+
3
+ def perform(workflow_id, job_id, resume_data = nil)
4
+ setup(workflow_id, job_id)
5
+
6
+ job.payloads = incoming_payloads
7
+
8
+ result = if resume_data.nil?
9
+ @manager.start_job!(job)
10
+ else
11
+ @manager.resume_job!(job, resume_data)
12
+ end
13
+
14
+ @manager.job_performed!(job, result)
15
+ rescue StandardError => e
16
+ @manager.fail_job!(job)
17
+ raise e
18
+ end
19
+
20
+
21
+ private
22
+
23
+ attr_reader :workflow, :job
24
+
25
+ def setup(workflow_id, job_id)
26
+ @workflow = Burst::Workflow.find(workflow_id)
27
+ @job = @workflow.get_job(job_id)
28
+ @manager = @workflow.manager
29
+ end
30
+
31
+ def incoming_payloads
32
+ job.incoming.map do |job_id|
33
+ incoming = workflow.get_job(job_id)
34
+ {
35
+ id: incoming.id,
36
+ class: incoming.klass.to_s,
37
+ payload: incoming.output
38
+ }
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,148 @@
1
+ class Burst::Workflow < ActiveRecord::Base
2
+
3
+ self.table_name_prefix = 'burst_'
4
+
5
+ INITIAL = 'initial'.freeze
6
+ RUNNING = 'running'.freeze
7
+ FINISHED = 'finished'.freeze
8
+ FAILED = 'failed'.freeze
9
+ SUSPENDED = 'suspended'.freeze
10
+
11
+ include Burst::WorkflowHelper
12
+ include Burst::Builder
13
+
14
+ attr_accessor :manager, :job_cache
15
+ define_flow_attributes :jobs, :klass
16
+
17
+ after_initialize do
18
+ initialize_builder
19
+
20
+ @job_cache = {}
21
+
22
+ self.id ||= SecureRandom.uuid
23
+ self.jobs ||= {}.with_indifferent_access
24
+ self.klass ||= self.class.to_s
25
+
26
+ @manager = Burst::Manager.new(self)
27
+ end
28
+
29
+ def attributes
30
+ {
31
+ id: self.id,
32
+ jobs: self.jobs,
33
+ klass: self.klass,
34
+ status: status
35
+ }
36
+ end
37
+
38
+ def self.build(*args)
39
+ wf = new
40
+ wf.configure(*args)
41
+ wf.resolve_dependencies
42
+ wf
43
+ end
44
+
45
+ def reload(options = nil)
46
+ self.job_cache = {}
47
+ super
48
+ end
49
+
50
+ def start!
51
+ save!
52
+ manager.start
53
+ end
54
+
55
+ def resume!(job_id, data)
56
+ manager.resume!(get_job(job_id), data)
57
+ end
58
+
59
+ def status
60
+ if failed?
61
+ FAILED
62
+ elsif suspended?
63
+ SUSPENDED
64
+ elsif running?
65
+ RUNNING
66
+ elsif finished?
67
+ FINISHED
68
+ else
69
+ INITIAL
70
+ end
71
+ end
72
+
73
+ def initial?
74
+ status == INITIAL
75
+ end
76
+
77
+ def finished?
78
+ all_jobs.all?(&:finished?)
79
+ end
80
+
81
+ def started?
82
+ !!started_at
83
+ end
84
+
85
+ def running?
86
+ started? && !finished?
87
+ end
88
+
89
+ def failed?
90
+ all_jobs.any?(&:failed?)
91
+ end
92
+
93
+ def suspended?
94
+ !failed? && all_jobs.any?(&:suspended?)
95
+ end
96
+
97
+ def all_jobs
98
+ Enumerator.new do |y|
99
+ jobs.keys.each do |id|
100
+ y << get_job(id)
101
+ end
102
+ end
103
+ end
104
+
105
+ def get_job(id)
106
+ if job = @job_cache[id]
107
+ job
108
+ else
109
+ job = Burst::Job.from_hash(self, jobs[id].deep_dup)
110
+ @job_cache[job.id] = job
111
+ job
112
+ end
113
+ end
114
+
115
+ def set_job(job)
116
+ jobs[job.id] = job.as_json
117
+ end
118
+
119
+ def initial_jobs
120
+ all_jobs.select(&:initial?)
121
+ end
122
+
123
+ def find_job(id_or_klass)
124
+ id = if jobs.key?(id_or_klass)
125
+ id_or_klass
126
+ else
127
+ find_id_by_klass(id_or_klass)
128
+ end
129
+
130
+ get_job(id)
131
+ end
132
+
133
+ def get_job_hash(id)
134
+ jobs[id]
135
+ end
136
+
137
+ private
138
+
139
+ def find_id_by_klass(klass)
140
+ finded = jobs.select do |_, job|
141
+ job[:klass].to_s == klass.to_s
142
+ end
143
+
144
+ raise 'Duplicat job detected' if finded.count > 1
145
+ finded.first.second[:id]
146
+ end
147
+
148
+ end
@@ -0,0 +1,86 @@
1
+ module Burst::WorkflowHelper
2
+
3
+ extend ActiveSupport::Concern
4
+
5
+ class JSONBWithIndifferentAccess
6
+
7
+ def self.dump(hash)
8
+ hash.as_json
9
+ end
10
+
11
+ def self.load(hash)
12
+ hash ||= {}
13
+ hash = JSON.parse(hash) if hash.is_a? String
14
+ hash.with_indifferent_access
15
+ end
16
+
17
+ end
18
+
19
+ included do |_klass|
20
+ serialize :flow, JSONBWithIndifferentAccess
21
+
22
+ def first_job
23
+ all_jobs.min_by{|n| n.started_at || Time.now.to_i }
24
+ end
25
+
26
+ def last_job
27
+ all_jobs.max_by{|n| n.finished_at || 0 } if finished?
28
+ end
29
+
30
+ def started_at
31
+ first_job&.started_at
32
+ end
33
+
34
+ def finished_at
35
+ last_job&.finished_at
36
+ end
37
+
38
+ def configure(*args)
39
+ instance_exec *args, &self.class.configuration
40
+ end
41
+ end
42
+
43
+ class_methods do
44
+ def define_flow_attributes(*keys)
45
+ keys.each do |key|
46
+ define_method key.to_sym do
47
+ return flow[key.to_s]
48
+ end
49
+
50
+ define_method "#{key}=".to_sym do |v|
51
+ return flow[key.to_s] = v
52
+ end
53
+ end
54
+ end
55
+
56
+ def configure(&block)
57
+ @configuration = block
58
+ end
59
+
60
+ def configuration
61
+ @configuration
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+
68
+ # class Test
69
+ # def self.configure &block
70
+ # @c = block
71
+ # end
72
+
73
+ # def self.c
74
+ # @c
75
+ # end
76
+
77
+ # def go
78
+ # puts "111111"
79
+ # end
80
+
81
+ # def run
82
+ # instance_eval &Test.c
83
+ # end
84
+
85
+
86
+ # end
data/lib/burst.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'bundler/setup'
4
+ Bundler.require(:default)
5
+
6
+ require 'active_support/all'
7
+ require 'active_support/dependencies'
8
+ require 'active_record'
9
+ require 'active_job'
10
+
11
+ require 'pathname'
12
+ require 'securerandom'
13
+
14
+ require 'burst/configuration'
15
+ require 'burst/model'
16
+ require 'burst/builder'
17
+ require 'burst/manager'
18
+ require 'burst/job'
19
+ require 'burst/workflow_helper'
20
+ require 'burst/workflow'
21
+ require 'burst/worker'
22
+
23
+ module Burst
24
+
25
+ def self.root
26
+ Pathname.new(__FILE__).parent.parent
27
+ end
28
+
29
+ def self.configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ def self.configure
34
+ yield configuration
35
+ end
36
+
37
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Burst do
4
+ end