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.
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