async-graph 0.1.1 → 0.1.2
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 +4 -4
- data/.github/workflows/ci.yml +31 -2
- data/.github/workflows/docs.yml +36 -0
- data/.github/workflows/release.yml +20 -3
- data/.gitignore +15 -0
- data/README.erb +26 -1
- data/README.md +26 -1
- data/examples/all_in_one_runner.rb +48 -0
- data/examples/app_graph.rb +2 -0
- data/examples/execute_jobs.rb +12 -12
- data/examples/graph_run.rb +29 -165
- data/examples/reset.rb +10 -18
- data/examples/run.sh +4 -1
- data/lib/async-graph/graph.rb +160 -25
- data/lib/async-graph/graph_validation.rb +83 -0
- data/lib/async-graph/runner.rb +255 -0
- data/lib/async-graph/version.rb +1 -1
- data/lib/async-graph.rb +1 -0
- data/spec/async_graph_spec.rb +152 -0
- metadata +5 -1
data/lib/async-graph/graph.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "graph_validation"
|
|
4
|
+
|
|
3
5
|
module AsyncGraph
|
|
4
6
|
FINISH = :__finish__
|
|
5
7
|
|
|
@@ -7,21 +9,8 @@ module AsyncGraph
|
|
|
7
9
|
Edge = Struct.new(:to, :branch, keyword_init: true)
|
|
8
10
|
AwaitSignal = Struct.new(:requests, keyword_init: true)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
when Array then object.map { |value| symbolize(value) }
|
|
13
|
-
when Hash then object.each_with_object({}) { |(key, value), memo| memo[key.to_sym] = symbolize(value) }
|
|
14
|
-
else object
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.stringify(object)
|
|
19
|
-
case object
|
|
20
|
-
when Array then object.map { |value| stringify(value) }
|
|
21
|
-
when Hash then object.each_with_object({}) { |(key, value), memo| memo[key.to_s] = stringify(value) }
|
|
22
|
-
else object
|
|
23
|
-
end
|
|
24
|
-
end
|
|
12
|
+
class ValidationError < StandardError; end
|
|
13
|
+
class JoinConflictError < StandardError; end
|
|
25
14
|
|
|
26
15
|
class Command < Struct.new(:update, :goto, keyword_init: true)
|
|
27
16
|
def self.goto(node) = new(goto: node)
|
|
@@ -44,6 +33,16 @@ module AsyncGraph
|
|
|
44
33
|
def finished? = true
|
|
45
34
|
end
|
|
46
35
|
|
|
36
|
+
class JoinParked < Struct.new(:joins, keyword_init: true)
|
|
37
|
+
def parked? = true
|
|
38
|
+
def released? = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class JoinReleased < Struct.new(:token, :joins, keyword_init: true)
|
|
42
|
+
def parked? = false
|
|
43
|
+
def released? = true
|
|
44
|
+
end
|
|
45
|
+
|
|
47
46
|
# Interesting options for issuing multiple external jobs from one logical step:
|
|
48
47
|
# 1. Model fan-out explicitly in the graph as a diamond.
|
|
49
48
|
# 2. Use await.all(...) inside one node to queue a batch and suspend once.
|
|
@@ -80,46 +79,97 @@ module AsyncGraph
|
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
class Graph
|
|
82
|
+
include GraphValidation
|
|
83
|
+
|
|
83
84
|
attr_reader :entry
|
|
84
85
|
|
|
85
86
|
def initialize(&block)
|
|
86
87
|
@nodes = {}
|
|
87
88
|
@edges = Hash.new { |hash, key| hash[key] = [] }
|
|
88
89
|
@join_expects = {}
|
|
90
|
+
@validated = false
|
|
89
91
|
instance_eval(&block) if block
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
def node(name, &block)
|
|
94
|
+
def node(name, &block)
|
|
95
|
+
name = name.to_sym
|
|
96
|
+
raise ValidationError, "Node #{name} requires a block" unless block
|
|
97
|
+
raise ValidationError, "Node #{name} is already defined" if @nodes.key?(name)
|
|
98
|
+
|
|
99
|
+
invalidate_validation!
|
|
100
|
+
@nodes[name] = block
|
|
101
|
+
end
|
|
93
102
|
|
|
94
103
|
def edge(from, to, branch: nil)
|
|
95
104
|
if from.is_a?(Array)
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
sources = from.map(&:to_sym)
|
|
106
|
+
validate_join_sources!(to, sources)
|
|
107
|
+
|
|
108
|
+
target = to.to_sym
|
|
109
|
+
raise ValidationError, "Join target #{target} is already defined" if @join_expects.key?(target)
|
|
110
|
+
|
|
111
|
+
invalidate_validation!
|
|
112
|
+
@join_expects[target] = sources
|
|
113
|
+
sources.each { |item| edge(item, target, branch: item) }
|
|
98
114
|
else
|
|
99
|
-
|
|
115
|
+
invalidate_validation!
|
|
116
|
+
add_edge(from.to_sym, Edge.new(to: to.to_sym, branch: branch&.to_sym))
|
|
100
117
|
end
|
|
101
118
|
end
|
|
102
119
|
|
|
103
|
-
def set_entry_point(name)
|
|
120
|
+
def set_entry_point(name)
|
|
121
|
+
invalidate_validation!
|
|
122
|
+
@entry = name.to_sym
|
|
123
|
+
end
|
|
124
|
+
|
|
104
125
|
def set_finish_point(name) = edge(name, FINISH)
|
|
105
126
|
|
|
106
127
|
def step(state:, node:, resolved: {})
|
|
128
|
+
validate!
|
|
129
|
+
|
|
107
130
|
current = node.to_sym
|
|
108
131
|
return Finished.new(state:) if current == FINISH
|
|
109
132
|
|
|
133
|
+
validate_known_node!(current, context: "Step node")
|
|
110
134
|
await = Await.new(resolved)
|
|
111
|
-
result = catch(:await) { [:ok, call_node(@nodes
|
|
135
|
+
result = catch(:await) { [:ok, call_node(@nodes[current], state, await)] }
|
|
112
136
|
return Suspended.new(state:, node: current, requests: result.requests) if result.is_a?(AwaitSignal)
|
|
113
137
|
|
|
114
138
|
Advanced.new(**advance(current, state, result.last))
|
|
115
139
|
end
|
|
116
140
|
|
|
117
141
|
def edges_from(node)
|
|
118
|
-
|
|
142
|
+
validate!
|
|
143
|
+
|
|
144
|
+
current = node.to_sym
|
|
145
|
+
validate_known_node!(current, context: "Node")
|
|
146
|
+
@edges[current].yield_self { |edges| edges.empty? ? [Edge.new(to: FINISH)] : edges }
|
|
119
147
|
end
|
|
120
148
|
|
|
121
|
-
def join?(node)
|
|
122
|
-
|
|
149
|
+
def join?(node)
|
|
150
|
+
validate!
|
|
151
|
+
@join_expects.key?(node.to_sym)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def join_for(node)
|
|
155
|
+
validate!
|
|
156
|
+
@join_expects.fetch(node.to_sym) { raise ValidationError, "Node #{node} is not a join node" }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def process_join(token:, joins:)
|
|
160
|
+
validate!
|
|
161
|
+
|
|
162
|
+
current, expects, fork_uid, source = join_context(token)
|
|
163
|
+
bucket_key = join_bucket_key(fork_uid, current)
|
|
164
|
+
current_joins = joins.dup
|
|
165
|
+
state = token.fetch(:state, {})
|
|
166
|
+
states = next_join_states(current, source, current_joins[bucket_key], state)
|
|
167
|
+
|
|
168
|
+
return park_join(current_joins, bucket_key, current, states) unless join_complete?(states, expects)
|
|
169
|
+
|
|
170
|
+
current_joins.delete(bucket_key)
|
|
171
|
+
release_join(current_joins, current, fork_uid, expects, states)
|
|
172
|
+
end
|
|
123
173
|
|
|
124
174
|
private
|
|
125
175
|
|
|
@@ -136,13 +186,98 @@ module AsyncGraph
|
|
|
136
186
|
when Hash
|
|
137
187
|
{ state: state.merge(result), destinations: edges_from(node) }
|
|
138
188
|
when Command
|
|
189
|
+
destinations =
|
|
190
|
+
if result.goto
|
|
191
|
+
target = result.goto.to_sym
|
|
192
|
+
validate_destination!(target, context: "Goto target")
|
|
193
|
+
[Edge.new(to: target)]
|
|
194
|
+
else
|
|
195
|
+
edges_from(node)
|
|
196
|
+
end
|
|
197
|
+
|
|
139
198
|
{
|
|
140
199
|
state: state.merge(result.update || {}),
|
|
141
|
-
destinations:
|
|
200
|
+
destinations: destinations
|
|
142
201
|
}
|
|
143
202
|
else
|
|
144
203
|
{ state: state, destinations: edges_from(node) }
|
|
145
204
|
end
|
|
146
205
|
end
|
|
206
|
+
|
|
207
|
+
def add_edge(from, edge)
|
|
208
|
+
@edges[from] << edge unless @edges[from].any? { |existing| existing.to == edge.to && existing.branch == edge.branch }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def join_context(token)
|
|
212
|
+
current = token.fetch(:node)&.to_sym
|
|
213
|
+
validate_known_node!(current, context: "Join node")
|
|
214
|
+
raise ValidationError, "Node #{current} is not a join node" unless join?(current)
|
|
215
|
+
|
|
216
|
+
fork_uid = token.fetch(:fork_uid)
|
|
217
|
+
raise ValidationError, "Join token for #{current} is missing fork_uid" if fork_uid.nil? || fork_uid.to_s.empty?
|
|
218
|
+
|
|
219
|
+
source = token.fetch(:source_node)&.to_sym
|
|
220
|
+
raise ValidationError, "Join token for #{current} is missing source_node" unless source
|
|
221
|
+
|
|
222
|
+
expects = join_for(current)
|
|
223
|
+
unless expects.include?(source)
|
|
224
|
+
raise ValidationError, "Join token for #{current} arrived from unexpected source #{source}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
[current, expects, fork_uid, source]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def join_bucket_key(fork_uid, node)
|
|
231
|
+
:"#{fork_uid}:#{node}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def next_join_states(current, source, bucket, state)
|
|
235
|
+
states = (bucket || {join_node: current, states: {}}).fetch(:states, {}).dup
|
|
236
|
+
if states.key?(source) && states[source] != state
|
|
237
|
+
raise JoinConflictError, "Join node #{current} received conflicting state for source #{source}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
states[source] = state
|
|
241
|
+
states
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def join_complete?(states, expects)
|
|
245
|
+
(expects - states.keys).empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def park_join(current_joins, bucket_key, current, states)
|
|
249
|
+
current_joins[bucket_key] = {
|
|
250
|
+
join_node: current,
|
|
251
|
+
states: states
|
|
252
|
+
}
|
|
253
|
+
JoinParked.new(joins: current_joins)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def release_join(current_joins, current, fork_uid, expects, states)
|
|
257
|
+
JoinReleased.new(
|
|
258
|
+
token: {
|
|
259
|
+
token_uid: "#{fork_uid}.join",
|
|
260
|
+
node: current,
|
|
261
|
+
state: merge_join_states(states, expects, current),
|
|
262
|
+
fork_uid: nil,
|
|
263
|
+
branch: nil,
|
|
264
|
+
source_node: nil,
|
|
265
|
+
awaits: {}
|
|
266
|
+
},
|
|
267
|
+
joins: current_joins
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def merge_join_states(states, expects, current)
|
|
272
|
+
expects.each_with_object({}) do |source, memo|
|
|
273
|
+
state = states.fetch(source)
|
|
274
|
+
conflicting = (memo.keys & state.keys).reject { |key| memo[key] == state[key] }
|
|
275
|
+
unless conflicting.empty?
|
|
276
|
+
raise JoinConflictError, "Join node #{current} has conflicting values for: #{conflicting.join(', ')}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
memo.merge!(state)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
147
282
|
end
|
|
148
283
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsyncGraph
|
|
4
|
+
module GraphValidation
|
|
5
|
+
def validate!
|
|
6
|
+
return self if @validated
|
|
7
|
+
|
|
8
|
+
errors = validation_errors
|
|
9
|
+
raise ValidationError, errors.uniq.join("; ") unless errors.empty?
|
|
10
|
+
|
|
11
|
+
@validated = true
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def validation_errors
|
|
18
|
+
[].tap do |errors|
|
|
19
|
+
add_entry_validation_errors(errors)
|
|
20
|
+
add_edge_validation_errors(errors)
|
|
21
|
+
add_join_validation_errors(errors)
|
|
22
|
+
end.uniq
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_entry_validation_errors(errors)
|
|
26
|
+
errors << "Entry point is not set" unless @entry
|
|
27
|
+
return unless @entry && !@nodes.key?(@entry)
|
|
28
|
+
|
|
29
|
+
errors << "Entry point #{@entry} is not defined"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_edge_validation_errors(errors)
|
|
33
|
+
@edges.each do |from, edges|
|
|
34
|
+
errors << "Edge source #{from} is not defined" unless @nodes.key?(from)
|
|
35
|
+
add_edge_target_validation_errors(errors, edges)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_edge_target_validation_errors(errors, edges)
|
|
40
|
+
edges.each do |edge|
|
|
41
|
+
next if edge.to == FINISH || @nodes.key?(edge.to)
|
|
42
|
+
|
|
43
|
+
errors << "Edge target #{edge.to} is not defined"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_join_validation_errors(errors)
|
|
48
|
+
@join_expects.each do |target, sources|
|
|
49
|
+
errors << "Join target #{target} is not defined" unless @nodes.key?(target)
|
|
50
|
+
sources.each do |source|
|
|
51
|
+
errors << "Join source #{source} for #{target} is not defined" unless @nodes.key?(source)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def invalidate_validation!
|
|
57
|
+
@validated = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_join_sources!(target, sources)
|
|
61
|
+
target = target.to_sym
|
|
62
|
+
raise ValidationError, "Join target #{target} requires at least one source" if sources.empty?
|
|
63
|
+
|
|
64
|
+
duplicates = sources.group_by(&:itself).filter_map { |source, items| source if items.size > 1 }
|
|
65
|
+
return if duplicates.empty?
|
|
66
|
+
|
|
67
|
+
raise ValidationError, "Join target #{target} has duplicate sources: #{duplicates.join(', ')}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_known_node!(node, context:)
|
|
71
|
+
raise ValidationError, "#{context} is not set" unless node
|
|
72
|
+
return if @nodes.key?(node)
|
|
73
|
+
|
|
74
|
+
raise ValidationError, "#{context} #{node} is not defined"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_destination!(node, context:)
|
|
78
|
+
return if node == FINISH
|
|
79
|
+
|
|
80
|
+
validate_known_node!(node, context:)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsyncGraph
|
|
4
|
+
class Runner
|
|
5
|
+
Run = Struct.new(:status, :tokens, :joins, :result, keyword_init: true) do
|
|
6
|
+
def running? = status.to_sym == :running
|
|
7
|
+
def finished? = status.to_sym == :finished
|
|
8
|
+
|
|
9
|
+
def to_h
|
|
10
|
+
{
|
|
11
|
+
status: status.to_s,
|
|
12
|
+
tokens: tokens,
|
|
13
|
+
joins: joins,
|
|
14
|
+
result: result
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Result = Struct.new(:status, :tokens, :joins, :state, :requests, :request_refs, keyword_init: true) do
|
|
20
|
+
def token = tokens.first
|
|
21
|
+
def parked? = status == :parked
|
|
22
|
+
def released? = status == :released
|
|
23
|
+
def suspended? = status == :suspended
|
|
24
|
+
def advanced? = status == :advanced
|
|
25
|
+
def finished? = status == :finished
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(graph)
|
|
29
|
+
@graph = graph
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start(state:, token_uid: "t1")
|
|
33
|
+
@graph.validate!
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
token_uid: token_uid,
|
|
37
|
+
node: @graph.entry,
|
|
38
|
+
state: state,
|
|
39
|
+
fork_uid: nil,
|
|
40
|
+
branch: nil,
|
|
41
|
+
source_node: nil,
|
|
42
|
+
awaits: {}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def start_run(state:, token_uid: "t1")
|
|
47
|
+
Run.new(
|
|
48
|
+
status: :running,
|
|
49
|
+
tokens: [start(state:, token_uid:)],
|
|
50
|
+
joins: {},
|
|
51
|
+
result: nil
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def step(token:, joins:, resolved: {}, &)
|
|
56
|
+
return build_join_result(@graph.process_join(token:, joins:)) if join_token?(token)
|
|
57
|
+
|
|
58
|
+
graph_step = @graph.step(
|
|
59
|
+
state: token.fetch(:state),
|
|
60
|
+
node: token.fetch(:node),
|
|
61
|
+
resolved: resolved
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
case graph_step
|
|
65
|
+
when Suspended
|
|
66
|
+
build_suspended_result(token, joins, graph_step, &)
|
|
67
|
+
when Advanced
|
|
68
|
+
build_advanced_result(token, joins, graph_step)
|
|
69
|
+
when Finished
|
|
70
|
+
build_finished_result(joins, graph_step.state)
|
|
71
|
+
else
|
|
72
|
+
raise ValidationError, "Unsupported graph step result #{graph_step.class}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def advance_run(run:, resolved_for: nil, &)
|
|
77
|
+
current_run = normalize_run(run)
|
|
78
|
+
return current_run if current_run.finished?
|
|
79
|
+
|
|
80
|
+
next_tokens, joins, final_state = advance_tokens(
|
|
81
|
+
current_run,
|
|
82
|
+
resolved_for: resolved_for,
|
|
83
|
+
&
|
|
84
|
+
)
|
|
85
|
+
Run.new(
|
|
86
|
+
status: run_status(final_state, next_tokens, joins),
|
|
87
|
+
tokens: next_tokens,
|
|
88
|
+
joins: joins,
|
|
89
|
+
result: final_state
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def normalize_run(run)
|
|
96
|
+
return run if run.is_a?(Run)
|
|
97
|
+
|
|
98
|
+
Run.new(
|
|
99
|
+
status: run.fetch(:status).to_sym,
|
|
100
|
+
tokens: run.fetch(:tokens, []),
|
|
101
|
+
joins: run.fetch(:joins, {}),
|
|
102
|
+
result: run[:result]
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def join_token?(token)
|
|
107
|
+
token[:source_node] && @graph.join?(token.fetch(:node))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_join_result(join_result)
|
|
111
|
+
case join_result
|
|
112
|
+
when JoinParked
|
|
113
|
+
Result.new(
|
|
114
|
+
status: :parked,
|
|
115
|
+
tokens: [],
|
|
116
|
+
joins: join_result.joins,
|
|
117
|
+
state: nil,
|
|
118
|
+
requests: [],
|
|
119
|
+
request_refs: {}
|
|
120
|
+
)
|
|
121
|
+
when JoinReleased
|
|
122
|
+
Result.new(
|
|
123
|
+
status: :released,
|
|
124
|
+
tokens: [join_result.token],
|
|
125
|
+
joins: join_result.joins,
|
|
126
|
+
state: join_result.token.fetch(:state),
|
|
127
|
+
requests: [],
|
|
128
|
+
request_refs: {}
|
|
129
|
+
)
|
|
130
|
+
else
|
|
131
|
+
raise ValidationError, "Unsupported join result #{join_result.class}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_suspended_result(token, joins, graph_step, &)
|
|
136
|
+
awaits = token.fetch(:awaits, {}).dup
|
|
137
|
+
request_refs = graph_step.requests.to_h do |request|
|
|
138
|
+
key = request.key.to_sym
|
|
139
|
+
[key, awaits[key] ||= bind_request(request, &)]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Result.new(
|
|
143
|
+
status: :suspended,
|
|
144
|
+
tokens: [
|
|
145
|
+
{
|
|
146
|
+
token_uid: token.fetch(:token_uid),
|
|
147
|
+
node: graph_step.node,
|
|
148
|
+
state: graph_step.state,
|
|
149
|
+
fork_uid: token[:fork_uid],
|
|
150
|
+
branch: token[:branch],
|
|
151
|
+
source_node: token[:source_node],
|
|
152
|
+
awaits: awaits
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
joins: joins,
|
|
156
|
+
state: graph_step.state,
|
|
157
|
+
requests: graph_step.requests,
|
|
158
|
+
request_refs: request_refs
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def build_advanced_result(token, joins, graph_step)
|
|
163
|
+
destinations = graph_step.destinations
|
|
164
|
+
if destinations.size > 1
|
|
165
|
+
fork_uid = fork_uid_for(token.fetch(:token_uid))
|
|
166
|
+
tokens = destinations.map do |edge|
|
|
167
|
+
suffix = edge.branch || edge.to
|
|
168
|
+
{
|
|
169
|
+
token_uid: "#{token.fetch(:token_uid)}.#{suffix}",
|
|
170
|
+
node: edge.to,
|
|
171
|
+
state: graph_step.state,
|
|
172
|
+
fork_uid: fork_uid,
|
|
173
|
+
branch: edge.branch,
|
|
174
|
+
source_node: token.fetch(:node),
|
|
175
|
+
awaits: {}
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return Result.new(
|
|
180
|
+
status: :advanced,
|
|
181
|
+
tokens: tokens,
|
|
182
|
+
joins: joins,
|
|
183
|
+
state: graph_step.state,
|
|
184
|
+
requests: [],
|
|
185
|
+
request_refs: {}
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
edge = destinations.first
|
|
190
|
+
return build_finished_result(joins, graph_step.state) if edge.to == FINISH
|
|
191
|
+
|
|
192
|
+
Result.new(
|
|
193
|
+
status: :advanced,
|
|
194
|
+
tokens: [
|
|
195
|
+
{
|
|
196
|
+
token_uid: token.fetch(:token_uid),
|
|
197
|
+
node: edge.to,
|
|
198
|
+
state: graph_step.state,
|
|
199
|
+
fork_uid: token[:fork_uid],
|
|
200
|
+
branch: token[:branch],
|
|
201
|
+
source_node: token.fetch(:node),
|
|
202
|
+
awaits: {}
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
joins: joins,
|
|
206
|
+
state: graph_step.state,
|
|
207
|
+
requests: [],
|
|
208
|
+
request_refs: {}
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_finished_result(joins, state)
|
|
213
|
+
Result.new(
|
|
214
|
+
status: :finished,
|
|
215
|
+
tokens: [],
|
|
216
|
+
joins: joins,
|
|
217
|
+
state: state,
|
|
218
|
+
requests: [],
|
|
219
|
+
request_refs: {}
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def bind_request(request, &)
|
|
224
|
+
return yield(request) if block_given?
|
|
225
|
+
|
|
226
|
+
raise ArgumentError, "Runner#step requires a block to bind external IDs for new requests"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def advance_tokens(run, resolved_for:, &)
|
|
230
|
+
next_tokens = []
|
|
231
|
+
joins = run.joins
|
|
232
|
+
final_state = run.result
|
|
233
|
+
|
|
234
|
+
run.tokens.each do |token|
|
|
235
|
+
outcome = step(
|
|
236
|
+
token: token,
|
|
237
|
+
joins: joins,
|
|
238
|
+
resolved: resolved_for&.call(token) || {},
|
|
239
|
+
&
|
|
240
|
+
)
|
|
241
|
+
joins = outcome.joins
|
|
242
|
+
next_tokens.concat(outcome.tokens)
|
|
243
|
+
final_state = outcome.state if outcome.finished?
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
[next_tokens, joins, final_state]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def run_status(final_state, next_tokens, joins)
|
|
250
|
+
final_state && next_tokens.empty? && joins.empty? ? :finished : :running
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def fork_uid_for(token_uid) = "fork-#{token_uid}"
|
|
254
|
+
end
|
|
255
|
+
end
|
data/lib/async-graph/version.rb
CHANGED
data/lib/async-graph.rb
CHANGED