burstflow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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