flow_core 0.0.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +255 -0
- data/Rakefile +29 -0
- data/app/models/flow_core/application_record.rb +7 -0
- data/app/models/flow_core/arc.rb +43 -0
- data/app/models/flow_core/arc_guard.rb +18 -0
- data/app/models/flow_core/end_place.rb +17 -0
- data/app/models/flow_core/instance.rb +115 -0
- data/app/models/flow_core/place.rb +63 -0
- data/app/models/flow_core/start_place.rb +17 -0
- data/app/models/flow_core/task.rb +197 -0
- data/app/models/flow_core/token.rb +105 -0
- data/app/models/flow_core/transition.rb +143 -0
- data/app/models/flow_core/transition_callback.rb +24 -0
- data/app/models/flow_core/transition_trigger.rb +24 -0
- data/app/models/flow_core/workflow.rb +131 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20200130200532_create_flow_core_tables.rb +151 -0
- data/lib/flow_core.rb +25 -0
- data/lib/flow_core/arc_guardable.rb +15 -0
- data/lib/flow_core/definition.rb +17 -0
- data/lib/flow_core/definition/callback.rb +33 -0
- data/lib/flow_core/definition/guard.rb +33 -0
- data/lib/flow_core/definition/net.rb +111 -0
- data/lib/flow_core/definition/place.rb +33 -0
- data/lib/flow_core/definition/transition.rb +107 -0
- data/lib/flow_core/definition/trigger.rb +33 -0
- data/lib/flow_core/engine.rb +6 -0
- data/lib/flow_core/errors.rb +14 -0
- data/lib/flow_core/locale/en.yml +26 -0
- data/lib/flow_core/task_executable.rb +34 -0
- data/lib/flow_core/transition_callbackable.rb +33 -0
- data/lib/flow_core/transition_triggerable.rb +27 -0
- data/lib/flow_core/version.rb +5 -0
- data/lib/flow_core/violations.rb +253 -0
- data/lib/tasks/flow_core_tasks.rake +6 -0
- metadata +123 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
class Place < FlowCore::ApplicationRecord
|
5
|
+
self.table_name = "flow_core_places"
|
6
|
+
|
7
|
+
FORBIDDEN_ATTRIBUTES = %i[workflow_id created_at updated_at].freeze
|
8
|
+
|
9
|
+
belongs_to :workflow, class_name: "FlowCore::Workflow"
|
10
|
+
|
11
|
+
# NOTE: Place - out -> Transition - in -> Place
|
12
|
+
has_many :input_arcs, -> { where direction: :out },
|
13
|
+
class_name: "FlowCore::Arc", inverse_of: :place, dependent: :delete_all
|
14
|
+
has_many :output_arcs, -> { where direction: :in },
|
15
|
+
class_name: "FlowCore::Arc", inverse_of: :place, dependent: :delete_all
|
16
|
+
|
17
|
+
has_many :output_transitions, through: :output_arcs, class_name: "FlowCore::Transition", source: :transition
|
18
|
+
|
19
|
+
before_destroy :prevent_destroy
|
20
|
+
after_create :reset_workflow_verification
|
21
|
+
after_destroy :reset_workflow_verification
|
22
|
+
|
23
|
+
def output_implicit_or_split?
|
24
|
+
output_arcs.size > 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def input_or_join?
|
28
|
+
input_arcs.size > 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def input_sequence?
|
32
|
+
input_arcs.size == 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def output_sequence?
|
36
|
+
output_arcs.size == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def start?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def end?
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def can_destroy?
|
48
|
+
workflow.instances.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def reset_workflow_verification
|
54
|
+
workflow.reset_workflow_verification!
|
55
|
+
end
|
56
|
+
|
57
|
+
def prevent_destroy
|
58
|
+
unless can_destroy?
|
59
|
+
raise FlowCore::ForbiddenOperation, "Found exists instance, destroy transition will lead serious corruption"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
class Task < FlowCore::ApplicationRecord
|
5
|
+
self.table_name = "flow_core_tasks"
|
6
|
+
|
7
|
+
belongs_to :workflow, class_name: "FlowCore::Workflow"
|
8
|
+
belongs_to :transition, class_name: "FlowCore::Transition"
|
9
|
+
|
10
|
+
belongs_to :instance, class_name: "FlowCore::Instance"
|
11
|
+
belongs_to :created_by_token, class_name: "FlowCore::Token", optional: true
|
12
|
+
|
13
|
+
belongs_to :executable, polymorphic: true, optional: true
|
14
|
+
|
15
|
+
serialize :payload
|
16
|
+
|
17
|
+
delegate :payload, to: :instance, prefix: :instance, allow_nil: false
|
18
|
+
|
19
|
+
enum stage: {
|
20
|
+
created: 0,
|
21
|
+
enabled: 1,
|
22
|
+
finished: 11,
|
23
|
+
terminated: 12
|
24
|
+
}
|
25
|
+
|
26
|
+
scope :errored, -> { where.not(errored_at: nil) }
|
27
|
+
scope :suspended, -> { where.not(suspended_at: nil) }
|
28
|
+
|
29
|
+
after_initialize do
|
30
|
+
self.payload ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
before_validation do
|
34
|
+
self.workflow ||= instance&.workflow
|
35
|
+
end
|
36
|
+
|
37
|
+
validate do
|
38
|
+
next unless executable
|
39
|
+
|
40
|
+
unless executable.is_a? FlowCore::TaskExecutable
|
41
|
+
errors.add :executable, :invalid
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def errored?
|
46
|
+
errored_at.present?
|
47
|
+
end
|
48
|
+
|
49
|
+
def suspended?
|
50
|
+
suspended_at.present?
|
51
|
+
end
|
52
|
+
|
53
|
+
def can_enable?
|
54
|
+
return false unless created?
|
55
|
+
|
56
|
+
if input_free_tokens.size == transition.input_arcs.size
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Note: It's impossible of it create by a token and needs another token (AND join) to enable?
|
61
|
+
same_origin_tasks.enabled.any?
|
62
|
+
end
|
63
|
+
|
64
|
+
def can_finish?
|
65
|
+
return false unless enabled?
|
66
|
+
|
67
|
+
return false if errored? || suspended?
|
68
|
+
|
69
|
+
if executable
|
70
|
+
executable.finished?
|
71
|
+
else
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def can_terminate?
|
77
|
+
created? || enabled?
|
78
|
+
end
|
79
|
+
|
80
|
+
def enable
|
81
|
+
return false unless can_enable?
|
82
|
+
|
83
|
+
transaction do
|
84
|
+
input_free_tokens.each(&:lock!)
|
85
|
+
update! stage: :enabled, enabled_at: Time.zone.now
|
86
|
+
|
87
|
+
transition.on_task_enabled(self)
|
88
|
+
end
|
89
|
+
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
def finish
|
94
|
+
return false unless can_finish?
|
95
|
+
|
96
|
+
transaction do
|
97
|
+
# terminate other racing tasks
|
98
|
+
instance.tasks.enabled.where(created_by_token: created_by_token).find_each do |task|
|
99
|
+
task.terminate! reason: "Same origin task #{id} finished"
|
100
|
+
end
|
101
|
+
|
102
|
+
input_locked_tokens.each { |token| token.consume! by: self }
|
103
|
+
update! stage: :finished, finished_at: Time.zone.now
|
104
|
+
|
105
|
+
transition.on_task_finished(self)
|
106
|
+
end
|
107
|
+
|
108
|
+
create_output_token!
|
109
|
+
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
def terminate(reason:)
|
114
|
+
return false unless can_terminate?
|
115
|
+
|
116
|
+
transaction do
|
117
|
+
update! stage: :terminated, terminated_at: Time.zone.now, terminate_reason: reason
|
118
|
+
transition.on_task_terminated(self)
|
119
|
+
end
|
120
|
+
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def enable!
|
125
|
+
enable || raise(FlowCore::InvalidTransition, "Can't enable Task##{id}")
|
126
|
+
end
|
127
|
+
|
128
|
+
def finish!
|
129
|
+
finish || raise(FlowCore::InvalidTransition, "Can't finish Task##{id}")
|
130
|
+
end
|
131
|
+
|
132
|
+
def terminate!(reason:)
|
133
|
+
terminate(reason: reason) || raise(FlowCore::InvalidTransition, "Can't terminate Task##{id}")
|
134
|
+
end
|
135
|
+
|
136
|
+
def error!(error)
|
137
|
+
transaction do
|
138
|
+
update! errored_at: Time.zone.now, error_reason: error.message
|
139
|
+
transition.on_task_errored(self, error)
|
140
|
+
|
141
|
+
instance.error!
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def rescue!
|
146
|
+
return unless errored?
|
147
|
+
|
148
|
+
transaction do
|
149
|
+
update! errored_at: nil, rescued_at: Time.zone.now
|
150
|
+
transition.on_task_rescued(self)
|
151
|
+
|
152
|
+
instance.rescue!
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def suspend!
|
157
|
+
transaction do
|
158
|
+
update! suspended_at: Time.zone.now
|
159
|
+
transition.on_task_suspended(self)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def resume!
|
164
|
+
transaction do
|
165
|
+
update! suspended_at: nil, resumed_at: Time.zone.now
|
166
|
+
transition.on_task_resumed(self)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_output_token!
|
171
|
+
return if output_token_created
|
172
|
+
|
173
|
+
transaction do
|
174
|
+
transition.create_tokens_for_output(task: self)
|
175
|
+
update! output_token_created: true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def input_tokens
|
182
|
+
instance.tokens.where(place: transition.input_places)
|
183
|
+
end
|
184
|
+
|
185
|
+
def input_free_tokens
|
186
|
+
input_tokens.free.uniq(&:place_id)
|
187
|
+
end
|
188
|
+
|
189
|
+
def input_locked_tokens
|
190
|
+
input_tokens.locked.uniq(&:place_id)
|
191
|
+
end
|
192
|
+
|
193
|
+
def same_origin_tasks
|
194
|
+
instance.tasks.where(created_by_token: created_by_token)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
class Token < FlowCore::ApplicationRecord
|
5
|
+
self.table_name = "flow_core_tokens"
|
6
|
+
|
7
|
+
belongs_to :workflow, class_name: "FlowCore::Workflow"
|
8
|
+
belongs_to :instance, class_name: "FlowCore::Instance"
|
9
|
+
belongs_to :place, class_name: "FlowCore::Place"
|
10
|
+
|
11
|
+
belongs_to :created_by_task, class_name: "FlowCore::Task", optional: true
|
12
|
+
belongs_to :consumed_by_task, class_name: "FlowCore::Task", optional: true
|
13
|
+
|
14
|
+
enum stage: {
|
15
|
+
free: 0,
|
16
|
+
locked: 1,
|
17
|
+
consumed: 11,
|
18
|
+
terminated: 12
|
19
|
+
}
|
20
|
+
|
21
|
+
after_create :auto_create_task
|
22
|
+
after_create :auto_enable_task
|
23
|
+
after_create :auto_finish_instance, if: ->(token) { token.place.is_a? EndPlace }
|
24
|
+
|
25
|
+
before_validation do
|
26
|
+
self.workflow ||= instance&.workflow
|
27
|
+
end
|
28
|
+
|
29
|
+
def can_lock?
|
30
|
+
free?
|
31
|
+
end
|
32
|
+
|
33
|
+
def can_consume?
|
34
|
+
locked?
|
35
|
+
end
|
36
|
+
|
37
|
+
def can_terminate?
|
38
|
+
free? || locked?
|
39
|
+
end
|
40
|
+
|
41
|
+
def lock
|
42
|
+
return false unless can_lock?
|
43
|
+
|
44
|
+
update! stage: :locked, locked_at: Time.zone.now
|
45
|
+
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def consume(by:)
|
50
|
+
return false unless can_consume?
|
51
|
+
|
52
|
+
update! stage: :consumed,
|
53
|
+
consumed_by_task: by,
|
54
|
+
consumed_at: Time.zone.now
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def terminate
|
60
|
+
return false unless can_terminate?
|
61
|
+
|
62
|
+
update! stage: :terminated,
|
63
|
+
terminated_at: Time.zone.now
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def lock!
|
69
|
+
lock || raise(FlowCore::InvalidTransition, "Can't lock Task##{id}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def consume!(by:)
|
73
|
+
consume(by: by) || raise(FlowCore::InvalidTransition, "Can't consume Task##{id}")
|
74
|
+
end
|
75
|
+
|
76
|
+
def terminate!
|
77
|
+
terminate || raise(FlowCore::InvalidTransition, "Can't terminate Task##{id}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_task!
|
81
|
+
return if task_created
|
82
|
+
|
83
|
+
transaction do
|
84
|
+
place.output_transitions.each do |transition|
|
85
|
+
transition.create_task_if_needed(token: self)
|
86
|
+
end
|
87
|
+
update! task_created: true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def auto_create_task
|
94
|
+
create_task!
|
95
|
+
end
|
96
|
+
|
97
|
+
def auto_enable_task
|
98
|
+
instance.tasks.created.where(transition: place.output_transitions).find_each(&:enable)
|
99
|
+
end
|
100
|
+
|
101
|
+
def auto_finish_instance
|
102
|
+
instance.finish!
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
class Transition < FlowCore::ApplicationRecord
|
5
|
+
self.table_name = "flow_core_transitions"
|
6
|
+
|
7
|
+
FORBIDDEN_ATTRIBUTES = %i[workflow_id created_at updated_at].freeze
|
8
|
+
|
9
|
+
belongs_to :workflow, class_name: "FlowCore::Workflow"
|
10
|
+
|
11
|
+
# NOTE: Place - out -> Transition - in -> Place
|
12
|
+
has_many :input_arcs, -> { where(direction: :in) },
|
13
|
+
class_name: "FlowCore::Arc", inverse_of: :transition, dependent: :delete_all
|
14
|
+
has_many :output_arcs, -> { where(direction: :out) },
|
15
|
+
class_name: "FlowCore::Arc", inverse_of: :transition, dependent: :delete_all
|
16
|
+
|
17
|
+
has_many :input_places, through: :input_arcs, class_name: "FlowCore::Place", source: :place
|
18
|
+
|
19
|
+
has_one :trigger, class_name: "FlowCore::TransitionTrigger", dependent: :delete
|
20
|
+
has_many :callbacks, class_name: "FlowCore::TransitionCallback", dependent: :delete_all
|
21
|
+
|
22
|
+
before_destroy :prevent_destroy
|
23
|
+
after_create :reset_workflow_verification
|
24
|
+
after_destroy :reset_workflow_verification
|
25
|
+
|
26
|
+
def output_and_split?
|
27
|
+
output_arcs.includes(:guards).all? { |arc| arc.guards.empty? }
|
28
|
+
end
|
29
|
+
|
30
|
+
def output_explicit_or_split?
|
31
|
+
output_arcs.includes(:guards).select { |arc| arc.guards.any? } < output_arcs.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def input_and_join?
|
35
|
+
input_arcs.size > 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def input_sequence?
|
39
|
+
input_arcs.size == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def output_sequence?
|
43
|
+
output_arcs.size == 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify(violations:)
|
47
|
+
trigger&.on_verify(self, violations)
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_task_if_needed(token:)
|
51
|
+
instance = token.instance
|
52
|
+
candidate_tasks = instance.tasks.created.where(transition: self)
|
53
|
+
|
54
|
+
# TODO: Is it possible that a input place has more than one free tokens? if YES we should handle it
|
55
|
+
if candidate_tasks.empty?
|
56
|
+
token.instance.tasks.create! transition: self, created_by_token: token
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_tokens_for_output(task:)
|
61
|
+
instance = task.instance
|
62
|
+
arcs = output_arcs.includes(:place, :guards).to_a
|
63
|
+
|
64
|
+
end_arc = arcs.find { |arc| arc.place.is_a? EndPlace }
|
65
|
+
if end_arc
|
66
|
+
if end_arc.guards.empty? || end_arc.guards.map { |guard| guard.permit? task }.reduce(&:&)
|
67
|
+
instance.tokens.create! created_by_task: task, place: end_arc.place
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
arcs.delete(end_arc)
|
72
|
+
end
|
73
|
+
|
74
|
+
candidate_arcs = arcs.select do |arc|
|
75
|
+
arc.guards.empty? || arc.guards.map { |guard| guard.permit? task }.reduce(&:&)
|
76
|
+
end
|
77
|
+
|
78
|
+
if candidate_arcs.empty?
|
79
|
+
trigger&.on_error(task, FlowCore::NoNewTokenCreated.new)
|
80
|
+
end
|
81
|
+
|
82
|
+
candidate_arcs.each do |arc|
|
83
|
+
instance.tokens.create! created_by_task: task, place: arc.place
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def on_task_created(task)
|
88
|
+
trigger&.on_task_created(task)
|
89
|
+
callbacks.each { |callback| callback.call task }
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_task_enabled(task)
|
93
|
+
trigger&.on_task_enabled(task)
|
94
|
+
callbacks.each { |callback| callback.call task }
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_task_finished(task)
|
98
|
+
trigger&.on_task_finished(task)
|
99
|
+
callbacks.each { |callback| callback.call task }
|
100
|
+
end
|
101
|
+
|
102
|
+
def on_task_terminated(task)
|
103
|
+
trigger&.on_task_terminated(task)
|
104
|
+
callbacks.each { |callback| callback.call task }
|
105
|
+
end
|
106
|
+
|
107
|
+
def on_task_errored(task, error)
|
108
|
+
trigger&.on_task_errored(task, error)
|
109
|
+
callbacks.each { |callback| callback.call task }
|
110
|
+
end
|
111
|
+
|
112
|
+
def on_task_rescued(task)
|
113
|
+
trigger&.on_task_rescued(task)
|
114
|
+
callbacks.each { |callback| callback.call task }
|
115
|
+
end
|
116
|
+
|
117
|
+
def on_task_suspended(task)
|
118
|
+
trigger&.on_task_suspended(task)
|
119
|
+
callbacks.each { |callback| callback.call task }
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_task_resumed(task)
|
123
|
+
trigger&.on_task_resumed(task)
|
124
|
+
callbacks.each { |callback| callback.call task }
|
125
|
+
end
|
126
|
+
|
127
|
+
def can_destroy?
|
128
|
+
workflow.instances.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def reset_workflow_verification
|
134
|
+
workflow.reset_workflow_verification!
|
135
|
+
end
|
136
|
+
|
137
|
+
def prevent_destroy
|
138
|
+
unless can_destroy?
|
139
|
+
raise FlowCore::ForbiddenOperation, "Found exists instance, destroy transition will lead serious corruption"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|