flow_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +255 -0
  4. data/Rakefile +29 -0
  5. data/app/models/flow_core/application_record.rb +7 -0
  6. data/app/models/flow_core/arc.rb +43 -0
  7. data/app/models/flow_core/arc_guard.rb +18 -0
  8. data/app/models/flow_core/end_place.rb +17 -0
  9. data/app/models/flow_core/instance.rb +115 -0
  10. data/app/models/flow_core/place.rb +63 -0
  11. data/app/models/flow_core/start_place.rb +17 -0
  12. data/app/models/flow_core/task.rb +197 -0
  13. data/app/models/flow_core/token.rb +105 -0
  14. data/app/models/flow_core/transition.rb +143 -0
  15. data/app/models/flow_core/transition_callback.rb +24 -0
  16. data/app/models/flow_core/transition_trigger.rb +24 -0
  17. data/app/models/flow_core/workflow.rb +131 -0
  18. data/config/routes.rb +4 -0
  19. data/db/migrate/20200130200532_create_flow_core_tables.rb +151 -0
  20. data/lib/flow_core.rb +25 -0
  21. data/lib/flow_core/arc_guardable.rb +15 -0
  22. data/lib/flow_core/definition.rb +17 -0
  23. data/lib/flow_core/definition/callback.rb +33 -0
  24. data/lib/flow_core/definition/guard.rb +33 -0
  25. data/lib/flow_core/definition/net.rb +111 -0
  26. data/lib/flow_core/definition/place.rb +33 -0
  27. data/lib/flow_core/definition/transition.rb +107 -0
  28. data/lib/flow_core/definition/trigger.rb +33 -0
  29. data/lib/flow_core/engine.rb +6 -0
  30. data/lib/flow_core/errors.rb +14 -0
  31. data/lib/flow_core/locale/en.yml +26 -0
  32. data/lib/flow_core/task_executable.rb +34 -0
  33. data/lib/flow_core/transition_callbackable.rb +33 -0
  34. data/lib/flow_core/transition_triggerable.rb +27 -0
  35. data/lib/flow_core/version.rb +5 -0
  36. data/lib/flow_core/violations.rb +253 -0
  37. data/lib/tasks/flow_core_tasks.rake +6 -0
  38. 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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class StartPlace < FlowCore::Place
5
+ validates :type,
6
+ uniqueness: {
7
+ scope: :workflow
8
+ }
9
+
10
+ validates :input_arcs,
11
+ length: { is: 0 }
12
+
13
+ def start?
14
+ true
15
+ end
16
+ end
17
+ 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