burstflow 0.1.1 → 0.2.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.
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