burstflow 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +1 -3
  3. data/Gemfile.lock +119 -0
  4. data/burstflow.gemspec +10 -6
  5. data/config/database.yml +4 -3
  6. data/db/migrate/20180101000001_create_workflow.rb +1 -0
  7. data/db/schema.rb +13 -7
  8. data/lib/burstflow.rb +11 -0
  9. data/lib/burstflow/job.rb +102 -0
  10. data/lib/burstflow/job/callbacks.rb +55 -0
  11. data/lib/burstflow/job/exception.rb +8 -0
  12. data/lib/burstflow/job/initialization.rb +35 -0
  13. data/lib/{burst → burstflow/job}/model.rb +1 -3
  14. data/lib/burstflow/job/state.rb +125 -0
  15. data/lib/burstflow/manager.rb +123 -0
  16. data/lib/burstflow/railtie.rb +6 -0
  17. data/lib/burstflow/version.rb +3 -0
  18. data/lib/burstflow/worker.rb +59 -0
  19. data/lib/burstflow/workflow.rb +207 -0
  20. data/lib/burstflow/workflow/builder.rb +91 -0
  21. data/lib/burstflow/workflow/callbacks.rb +66 -0
  22. data/lib/{burst/workflow_helper.rb → burstflow/workflow/configuration.rb} +8 -39
  23. data/lib/burstflow/workflow/exception.rb +8 -0
  24. data/lib/generators/burstflow/install/install_generator.rb +22 -0
  25. data/lib/generators/burstflow/install/templates/create_workflow.rb +15 -0
  26. data/spec/builder_spec.rb +63 -0
  27. data/spec/{burst_spec.rb → burstflow_spec.rb} +1 -1
  28. data/spec/generators/install_generator_spec.rb +27 -0
  29. data/spec/job_spec.rb +18 -8
  30. data/spec/spec_helper.rb +4 -1
  31. data/spec/support/database_clean.rb +4 -1
  32. data/spec/workflow_spec.rb +397 -147
  33. metadata +45 -21
  34. data/db/migrate/20180101000001_create_workflow.rb +0 -13
  35. data/db/seeds.rb +0 -1
  36. data/lib/burst.rb +0 -37
  37. data/lib/burst/builder.rb +0 -48
  38. data/lib/burst/configuration.rb +0 -27
  39. data/lib/burst/job.rb +0 -187
  40. data/lib/burst/manager.rb +0 -79
  41. data/lib/burst/worker.rb +0 -42
  42. data/lib/burst/workflow.rb +0 -148
  43. data/spec/cases_spec.rb +0 -180
@@ -1,12 +1,10 @@
1
- module Burst::Model
1
+ module Burstflow::Job::Model
2
2
 
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do |klass|
6
- klass.include ActiveModel::Model
7
6
  klass.include ActiveModel::Dirty
8
7
  klass.include ActiveModel::Serialization
9
- klass.extend ActiveModel::Callbacks
10
8
 
11
9
  attr_accessor :model
12
10
 
@@ -0,0 +1,125 @@
1
+ module Burstflow::Job::State
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ # mark job as enqueued when it is scheduled to queue
7
+ def enqueue!
8
+ raise Burstflow::Job::InternalError.new(self, "Can't enqueue: already enqueued") if enqueued?
9
+ self.enqueued_at = current_timestamp
10
+ self.started_at = nil
11
+ self.finished_at = nil
12
+ self.failed_at = nil
13
+ self.suspended_at = nil
14
+ self.resumed_at = nil
15
+ end
16
+
17
+ # mark job as started when it is start performing
18
+ def start!
19
+ raise Burstflow::Job::InternalError.new(self, "Can't start: already started") if started?
20
+ raise Burstflow::Job::InternalError.new(self, "Can't start: not enqueued") if !enqueued?
21
+ self.started_at = current_timestamp
22
+ end
23
+
24
+ # mark job as finished when it is finish performing
25
+ def finish!
26
+ raise Burstflow::Job::InternalError.new(self, "Can't finish: already finished") if finished?
27
+ raise Burstflow::Job::InternalError.new(self, "Can't finish: not started") if !started?
28
+ self.finished_at = current_timestamp
29
+ end
30
+
31
+ # mark job as failed when it is failed
32
+ def fail! msg_or_exception
33
+ #raise Burstflow::Job::InternalError.new(self, "Can't fail: already failed") if failed?
34
+ #raise Burstflow::Job::InternalError.new(self, Can't fail: already finished") if finished?
35
+ raise Burstflow::Job::InternalError.new(self, "Can't fail: not started") if !started?
36
+ self.finished_at = self.failed_at = current_timestamp
37
+
38
+ context = {}
39
+ if msg_or_exception.is_a?(::Exception)
40
+ context[:message] = msg_or_exception.message
41
+ context[:klass] = msg_or_exception.class.to_s
42
+ context[:backtrace] = msg_or_exception.backtrace.first(10)
43
+ context[:cause] = msg_or_exception.cause.try(:inspect)
44
+ else
45
+ context[:message] = msg_or_exception
46
+ end
47
+
48
+ self.failure = context
49
+ end
50
+
51
+ # mark job as suspended
52
+ def suspend!
53
+ raise Burstflow::Job::InternalError.new(self, "Can't suspend: already suspended") if suspended?
54
+ raise Burstflow::Job::InternalError.new(self, "Can't suspend: not runnig") if !running?
55
+ self.suspended_at = current_timestamp
56
+ end
57
+
58
+ # mark job as resumed
59
+ def resume!
60
+ raise Burstflow::Job::InternalError.new(self, "Can't resume: already resumed") if resumed?
61
+ raise Burstflow::Job::InternalError.new(self, "Can't resume: not suspended") if !suspended?
62
+ self.resumed_at = current_timestamp
63
+ end
64
+
65
+ def enqueued?
66
+ !enqueued_at.nil?
67
+ end
68
+
69
+ def started?
70
+ !started_at.nil?
71
+ end
72
+
73
+ def finished?
74
+ !finished_at.nil?
75
+ end
76
+
77
+ def running?
78
+ started? && !finished? && !suspended?
79
+ end
80
+
81
+ def scheduled?
82
+ enqueued? && !finished? && !suspended?
83
+ end
84
+
85
+ def failed?
86
+ !failed_at.nil?
87
+ end
88
+
89
+ def suspended?
90
+ !suspended_at.nil? && !resumed?
91
+ end
92
+
93
+ def resumed?
94
+ !resumed_at.nil?
95
+ end
96
+
97
+ def succeeded?
98
+ finished? && !failed?
99
+ end
100
+
101
+ def ready_to_start?
102
+ !running? && !enqueued? && !finished? && !failed? && parents_succeeded?
103
+ end
104
+
105
+ def initial?
106
+ incoming.empty?
107
+ end
108
+
109
+ def parents_succeeded?
110
+ incoming.all? do |id|
111
+ workflow.job(id).succeeded?
112
+ end
113
+ end
114
+
115
+ def current_timestamp
116
+ Time.now.to_i
117
+ end
118
+
119
+ end
120
+
121
+ class_methods do
122
+
123
+ end
124
+
125
+ end
@@ -0,0 +1,123 @@
1
+ module Burstflow
2
+ class Manager
3
+
4
+ attr_accessor :workflow
5
+
6
+ def initialize(workflow)
7
+ @workflow = workflow
8
+ end
9
+
10
+ #workflow management
11
+
12
+ def start_workflow!
13
+ workflow.with_lock do
14
+ workflow.runnig!
15
+
16
+ workflow.initial_jobs.each do |job|
17
+ enqueue_job!(job)
18
+ end
19
+ end
20
+ end
21
+
22
+ def resume_workflow! job_id, data
23
+ workflow.with_lock do
24
+ workflow.resumed!
25
+
26
+ job = workflow.job(job_id)
27
+ resume_job!(job, data)
28
+ end
29
+ end
30
+
31
+
32
+ #Mark job enqueued and enqueue it
33
+ def enqueue_job!(job)
34
+ job.run_callbacks :enqueue do
35
+ job.enqueue!
36
+ job.save! do
37
+ Burstflow::Worker.perform_later(workflow.id, job.id)
38
+ end
39
+ end
40
+ end
41
+
42
+ #Enqueue job for resuming
43
+ def resume_job!(job, data)
44
+ job.save! do
45
+ Burstflow::Worker.perform_later(workflow.id, job.id, data)
46
+ end
47
+ end
48
+
49
+
50
+ #Mark job suspended and forget it until resume
51
+ def suspend_job!(job)
52
+ job.run_callbacks :suspend do
53
+ job.suspend!
54
+ job.save! do
55
+ analyze_workflow_state(job)
56
+ end
57
+ end
58
+ end
59
+
60
+
61
+ #Mark job finished and make further actions
62
+ def finish_job!(job)
63
+ job.finish!
64
+ job.save! do
65
+ analyze_workflow_state(job)
66
+ end
67
+ end
68
+
69
+ #Mark job failed and make further actions
70
+ def fail_job!(job, exception)
71
+ job.run_callbacks :failure do
72
+ job.fail! exception
73
+ workflow.add_error(job)
74
+ workflow.save!
75
+
76
+ job.save! do
77
+ analyze_workflow_state(job)
78
+ end
79
+ end
80
+ end
81
+
82
+ #Mark job finished or suspended depends on result or output
83
+ def job_performed!(job, result)
84
+ if result == Burstflow::Job::SUSPEND || job.output == Burstflow::Job::SUSPEND
85
+ suspend_job!(job)
86
+ else
87
+ finish_job!(job)
88
+ end
89
+ rescue => e
90
+ raise Burstflow::Workflow::InternalError.new(workflow, e.message)
91
+ end
92
+
93
+ private
94
+
95
+ #analyze job completition, current workflow state and perform futher actions
96
+ def analyze_workflow_state job
97
+ unless ActiveRecord::Base.connection.open_transactions > 0
98
+ raise Burstflow::Workflow::InternalError.new(workflow, "analyze_workflow_state must be called in transaction with lock!")
99
+ end
100
+
101
+ if job.succeeded? && job.outgoing.any? && !workflow.has_errors?
102
+ return enqueue_outgoing_jobs(job)
103
+ else
104
+ if workflow.has_scheduled_jobs?
105
+ #do nothing
106
+ #scheduled jobs will perform finish action
107
+ else
108
+ workflow.complete!
109
+ end
110
+ end
111
+ end
112
+
113
+ #enqueue outgoing jobs if all requirements are met
114
+ def enqueue_outgoing_jobs(job)
115
+ job.outgoing.each do |job_id|
116
+ out = workflow.job(job_id)
117
+
118
+ enqueue_job!(out) if out.ready_to_start?
119
+ end
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module Burstflow
3
+ class Railtie < Rails::Engine #:nodoc:
4
+ engine_name 'burstflow'
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Burstflow
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_job'
2
+
3
+ class Burstflow::Worker < ::ActiveJob::Base
4
+
5
+ attr_reader :workflow, :job
6
+
7
+ rescue_from(Exception) do |exception|
8
+ @manager.fail_job!(job, exception)
9
+ end
10
+
11
+ rescue_from(Burstflow::Job::InternalError) do |exception|
12
+ @manager.fail_job!(exception.job, exception)
13
+ end
14
+
15
+ rescue_from(Burstflow::Workflow::InternalError) do |exception|
16
+ exception.workflow.add_error(exception)
17
+ exception.workflow.save!
18
+ end
19
+
20
+ before_perform do
21
+ workflow_id, job_id, resume_data = arguments
22
+
23
+ @workflow = Burstflow::Workflow.find(workflow_id)
24
+ @job = @workflow.job(job_id)
25
+ @manager = @workflow.manager
26
+
27
+ set_incoming_payloads(job)
28
+ end
29
+
30
+ def perform(workflow_id, job_id, resume_data = nil)
31
+ result = if resume_data.nil?
32
+ job.start!
33
+ job.save!
34
+
35
+ job.perform_now
36
+ else
37
+ job.resume!
38
+ job.save!
39
+
40
+ job.resume_now(resume_data)
41
+ end
42
+
43
+ @manager.job_performed!(job, result)
44
+ end
45
+
46
+ private
47
+
48
+ def set_incoming_payloads job
49
+ job.payloads = job.incoming.map do |job_id|
50
+ incoming = workflow.job(job_id)
51
+ {
52
+ id: incoming.id,
53
+ class: incoming.klass.to_s,
54
+ value: incoming.output
55
+ }
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,207 @@
1
+ require 'active_record'
2
+
3
+ require 'burstflow/manager'
4
+
5
+ module Burstflow
6
+ class Workflow < ActiveRecord::Base
7
+ require 'burstflow/workflow/exception'
8
+ require 'burstflow/workflow/builder'
9
+ require 'burstflow/workflow/configuration'
10
+ require 'burstflow/workflow/callbacks'
11
+
12
+ self.table_name_prefix = 'burstflow_'
13
+
14
+ INITIAL = 'initial'.freeze
15
+ RUNNING = 'running'.freeze
16
+ FINISHED = 'finished'.freeze
17
+ FAILED = 'failed'.freeze
18
+ SUSPENDED = 'suspended'.freeze
19
+
20
+ STATUSES = [INITIAL, RUNNING, FINISHED, FAILED, SUSPENDED].freeze
21
+
22
+ include Burstflow::Workflow::Configuration
23
+ include Burstflow::Workflow::Callbacks
24
+
25
+ attr_accessor :manager, :cache
26
+ define_flow_attributes :jobs_config, :failures
27
+
28
+ after_initialize do
29
+ @cache = {}
30
+
31
+ self.status ||= INITIAL
32
+ self.id ||= SecureRandom.uuid
33
+ self.jobs_config ||= {}.with_indifferent_access
34
+ self.failures ||= []
35
+
36
+ @manager = Burstflow::Manager.new(self)
37
+ end
38
+
39
+ STATUSES.each do |name|
40
+ define_method "#{name}?".to_sym do
41
+ self.status == name
42
+ end
43
+ end
44
+
45
+ def attributes
46
+ {
47
+ id: self.id,
48
+ jobs_config: self.jobs_config,
49
+ type: self.class.to_s,
50
+ status: status,
51
+ failures: failures
52
+ }
53
+ end
54
+
55
+ def self.build(*args)
56
+ new.tap do |wf|
57
+ builder = Burstflow::Workflow::Builder.new(wf, *args, &configuration)
58
+ wf.flow = {'jobs_config' => builder.as_json}
59
+ end
60
+ end
61
+
62
+ def reload(*)
63
+ self.cache = {}
64
+ super
65
+ end
66
+
67
+ def start!
68
+ manager.start_workflow!
69
+ self
70
+ end
71
+
72
+ def resume!(job_id, data)
73
+ manager.resume_workflow!(job_id, data)
74
+ self
75
+ end
76
+
77
+ def jobs
78
+ Enumerator.new do |y|
79
+ jobs_config.keys.each do |id|
80
+ y << job(id)
81
+ end
82
+ end
83
+ end
84
+
85
+ def job_hash(id)
86
+ jobs_config[id].deep_dup
87
+ end
88
+
89
+ def job(id)
90
+ Burstflow::Job.from_hash(self, job_hash(id))
91
+ end
92
+
93
+ def set_job(job)
94
+ jobs_config[job.id] = job.as_json
95
+ end
96
+
97
+ def initial_jobs
98
+ cache[:initial_jobs] ||= jobs.select(&:initial?)
99
+ end
100
+
101
+ def add_error job_orexception
102
+ context = {
103
+ created_at: Time.now.to_i
104
+ }
105
+ if job_orexception.is_a?(::Exception)
106
+ context[:message] = job_orexception.message
107
+ context[:klass] = job_orexception.class.to_s
108
+ context[:backtrace] = job_orexception.backtrace.first(10)
109
+ context[:cause] = job_orexception.cause
110
+ else
111
+ context[:job] = job_orexception.id
112
+ end
113
+
114
+ failures.push(context)
115
+ end
116
+
117
+ def has_errors?
118
+ failures.any?
119
+ end
120
+
121
+ def has_scheduled_jobs?
122
+ cache[:has_scheduled_jobs] ||= jobs.any? do |job|
123
+ job.scheduled? || (job.initial? && !job.enqueued?)
124
+ end
125
+ end
126
+
127
+ def has_suspended_jobs?
128
+ cache[:has_suspended_jobs] ||= jobs.any?(&:suspended?)
129
+ end
130
+
131
+ def complete!
132
+ if has_errors?
133
+ failed!
134
+ elsif has_suspended_jobs?
135
+ suspended!
136
+ else
137
+ finished!
138
+ end
139
+ end
140
+
141
+ def first_job
142
+ all_jobs.min_by{|n| n.started_at || Time.now.to_i }
143
+ end
144
+
145
+ def last_job
146
+ all_jobs.max_by{|n| n.finished_at || 0 } if finished?
147
+ end
148
+
149
+ def started_at
150
+ first_job&.started_at
151
+ end
152
+
153
+ def finished_at
154
+ last_job&.finished_at
155
+ end
156
+
157
+ def runnig!
158
+ raise InternalError.new(self, "Can't start: workflow already running") if (running? || suspended?)
159
+ raise InternalError.new(self, "Can't start: workflow already failed") if failed?
160
+ raise InternalError.new(self, "Can't start: workflow already finished") if finished?
161
+ self.status = RUNNING
162
+ save!
163
+ end
164
+
165
+ def failed!
166
+ run_callbacks :failure do
167
+ raise InternalError.new(self, "Can't fail: workflow already failed") if failed?
168
+ raise InternalError.new(self, "Can't fail: workflow already finished") if finished?
169
+ raise InternalError.new(self, "Can't fail: workflow in not runnig") if !(running? || suspended?)
170
+ self.status = FAILED
171
+ save!
172
+ end
173
+ end
174
+
175
+ def finished!
176
+ run_callbacks :finish do
177
+ raise InternalError.new(self, "Can't finish: workflow already finished") if finished?
178
+ raise InternalError.new(self, "Can't finish: workflow already failed") if failed?
179
+ raise InternalError.new(self, "Can't finish: workflow in not runnig") if !running?
180
+ self.status = FINISHED
181
+ save!
182
+ end
183
+ end
184
+
185
+ def suspended!
186
+ run_callbacks :suspend do
187
+ raise InternalError.new(self, "Can't suspend: workflow already finished") if finished?
188
+ raise InternalError.new(self, "Can't suspend: workflow already failed") if failed?
189
+ raise InternalError.new(self, "Can't suspend: workflow in not runnig") if !running?
190
+ self.status = SUSPENDED
191
+ save!
192
+ end
193
+ end
194
+
195
+ def resumed!
196
+ run_callbacks :resume do
197
+ raise InternalError.new(self, "Can't resume: workflow already running") if running?
198
+ raise InternalError.new(self, "Can't resume: workflow already finished") if finished?
199
+ raise InternalError.new(self, "Can't resume: workflow already failed") if failed?
200
+ raise InternalError.new(self, "Can't resume: workflow in not suspended") if !suspended?
201
+ self.status = RUNNING
202
+ save!
203
+ end
204
+ end
205
+
206
+ end
207
+ end