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