async-graph 0.1.2 → 0.1.3
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 +1 -1
- data/README.erb +9 -3
- data/README.md +9 -3
- data/examples/all_in_one_inline_runner.rb +41 -0
- data/examples/{all_in_one_runner.rb → all_in_one_jobs_runner.rb} +1 -1
- data/examples/graph_run.rb +1 -1
- data/examples/run.sh +6 -3
- data/lib/async-graph/graph.rb +32 -7
- data/lib/async-graph/runner.rb +13 -6
- data/lib/async-graph/version.rb +1 -1
- data/spec/async_graph_spec.rb +90 -0
- data/spec/runner_spec.rb +357 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fb46c9d6fc4ea1d3954655810b8605acdb74a84e3299f4910a2e8af30e19618
|
|
4
|
+
data.tar.gz: 8384644bec8efa74c592fd2bef726ccee2c41fe7596b802ea1f26fedde3ee25f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa56a0e83197515c7e66f869a8edfc8dd4db46a9fb08f030d38d05af93bc2e6b44168c689706b8061f5b6f63b57dcf8fd1d9d963260911805aaf1882ef7007fc
|
|
7
|
+
data.tar.gz: '083844bd7e389b9ccf3b68ed642cce69730c64f324efe416f3496e0531a9ea42f23c237ddcfb6cf1f20da73f3289c4ecdad31b3a7ef0ed8dd2785be340538bb0'
|
data/.github/workflows/ci.yml
CHANGED
data/README.erb
CHANGED
|
@@ -51,7 +51,7 @@ snapshot while your application still owns persistence and external jobs.
|
|
|
51
51
|
|
|
52
52
|
## Demo
|
|
53
53
|
|
|
54
|
-
The repository includes
|
|
54
|
+
The repository includes three runnable examples in `examples/`:
|
|
55
55
|
|
|
56
56
|
- persisted multi-pass flow with external job persistence:
|
|
57
57
|
|
|
@@ -59,10 +59,16 @@ The repository includes two runnable examples in `examples/`:
|
|
|
59
59
|
bash examples/run.sh
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
- self-contained runner loop with inline `:add` / `:subtract` request handling
|
|
62
|
+
- self-contained runner loop with inline `:add` / `:subtract` request handling through `resolve_request:`:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
ruby examples/
|
|
65
|
+
ruby examples/all_in_one_inline_runner.rb
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- self-contained runner loop that still suspends, binds requests to in-memory job IDs, and resumes in the same process:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ruby examples/all_in_one_jobs_runner.rb
|
|
66
72
|
```
|
|
67
73
|
|
|
68
74
|
## Documentation
|
data/README.md
CHANGED
|
@@ -51,7 +51,7 @@ snapshot while your application still owns persistence and external jobs.
|
|
|
51
51
|
|
|
52
52
|
## Demo
|
|
53
53
|
|
|
54
|
-
The repository includes
|
|
54
|
+
The repository includes three runnable examples in `examples/`:
|
|
55
55
|
|
|
56
56
|
- persisted multi-pass flow with external job persistence:
|
|
57
57
|
|
|
@@ -59,10 +59,16 @@ The repository includes two runnable examples in `examples/`:
|
|
|
59
59
|
bash examples/run.sh
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
- self-contained runner loop with inline `:add` / `:subtract` request handling
|
|
62
|
+
- self-contained runner loop with inline `:add` / `:subtract` request handling through `resolve_request:`:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
ruby examples/
|
|
65
|
+
ruby examples/all_in_one_inline_runner.rb
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- self-contained runner loop that still suspends, binds requests to in-memory job IDs, and resumes in the same process:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ruby examples/all_in_one_jobs_runner.rb
|
|
66
72
|
```
|
|
67
73
|
|
|
68
74
|
## Documentation
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../lib/async-graph"
|
|
5
|
+
|
|
6
|
+
runner = AsyncGraph::Runner.new(
|
|
7
|
+
AsyncGraph::Graph.new do
|
|
8
|
+
node :calculate do |state, await|
|
|
9
|
+
results = await.all(
|
|
10
|
+
added: [:add, {left: state[:left], right: state[:right]}],
|
|
11
|
+
subtracted: [:subtract, {left: state[:total], right: state[:discount]}]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
added: results[:added],
|
|
16
|
+
subtracted: results[:subtracted],
|
|
17
|
+
answer: results[:added] - results[:subtracted]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
set_entry_point :calculate
|
|
22
|
+
set_finish_point :calculate
|
|
23
|
+
end
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
run = runner.start_run state: {left: 7, right: 5, total: 20, discount: 3}
|
|
27
|
+
|
|
28
|
+
until run.finished?
|
|
29
|
+
run = runner.advance_run(
|
|
30
|
+
run: run,
|
|
31
|
+
resolve_request: lambda do |kind, payload|
|
|
32
|
+
case kind
|
|
33
|
+
when :add then payload[:left] + payload[:right]
|
|
34
|
+
when :subtract then payload[:left] - payload[:right]
|
|
35
|
+
else AsyncGraph::DEFER
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
puts JSON.pretty_generate(run.result)
|
|
@@ -29,7 +29,7 @@ run = runner.start_run state: {left: 7, right: 5, total: 20, discount: 3}
|
|
|
29
29
|
until run.finished?
|
|
30
30
|
run = runner.advance_run(
|
|
31
31
|
run: run,
|
|
32
|
-
|
|
32
|
+
resolved: lambda do |token|
|
|
33
33
|
token[:awaits].each_with_object({}) do |(key, request_id), memo|
|
|
34
34
|
memo[key.to_s] = results[request_id] if results.key?(request_id)
|
|
35
35
|
end
|
data/examples/graph_run.rb
CHANGED
|
@@ -7,7 +7,7 @@ def advance_graph_state!(graph_state, job_list, jobs_by_uid)
|
|
|
7
7
|
graph_uid = graph_state.fetch(:graph_uid)
|
|
8
8
|
next_run = RUNNER.advance_run(
|
|
9
9
|
run: graph_state.fetch(:run),
|
|
10
|
-
|
|
10
|
+
resolved: lambda do |token|
|
|
11
11
|
token.fetch(:awaits, {}).each_with_object({}) do |(key, job_uid), memo|
|
|
12
12
|
job = jobs_by_uid[job_uid]
|
|
13
13
|
memo[key.to_s] = job[:result] if job&.[](:status) == "done"
|
data/examples/run.sh
CHANGED
|
@@ -3,7 +3,7 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
cd "$(dirname "$0")"
|
|
5
5
|
|
|
6
|
-
for file in app_graph.rb reset.rb graph_run.rb execute_jobs.rb
|
|
6
|
+
for file in app_graph.rb reset.rb graph_run.rb execute_jobs.rb all_in_one_inline_runner.rb all_in_one_jobs_runner.rb; do
|
|
7
7
|
ruby -c "$file"
|
|
8
8
|
done
|
|
9
9
|
|
|
@@ -41,5 +41,8 @@ ruby graph_run.rb
|
|
|
41
41
|
cat graph_states.json
|
|
42
42
|
cat jobs.json
|
|
43
43
|
|
|
44
|
-
printf '\n== all-in-one runner ==\n'
|
|
45
|
-
ruby
|
|
44
|
+
printf '\n== all-in-one inline runner ==\n'
|
|
45
|
+
ruby all_in_one_inline_runner.rb
|
|
46
|
+
|
|
47
|
+
printf '\n== all-in-one jobs runner ==\n'
|
|
48
|
+
ruby all_in_one_jobs_runner.rb
|
data/lib/async-graph/graph.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "graph_validation"
|
|
|
4
4
|
|
|
5
5
|
module AsyncGraph
|
|
6
6
|
FINISH = :__finish__
|
|
7
|
+
DEFER = Object.new.freeze
|
|
7
8
|
|
|
8
9
|
Request = Struct.new(:key, :kind, :payload, keyword_init: true)
|
|
9
10
|
Edge = Struct.new(:to, :branch, keyword_init: true)
|
|
@@ -48,15 +49,20 @@ module AsyncGraph
|
|
|
48
49
|
# 2. Use await.all(...) inside one node to queue a batch and suspend once.
|
|
49
50
|
# 3. Use a two-phase API such as await.defer + await.resolve_all.
|
|
50
51
|
class Await
|
|
51
|
-
def initialize(resolved)
|
|
52
|
+
def initialize(resolved, resolve_request = nil)
|
|
52
53
|
@resolved = resolved
|
|
54
|
+
@resolve_request = resolve_request
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def call(key, kind, payload = {})
|
|
56
58
|
key = key.to_s
|
|
57
59
|
return @resolved[key] if @resolved.key?(key)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
request = Request.new(key:, kind:, payload:)
|
|
62
|
+
resolved = resolve(request)
|
|
63
|
+
throw :await, AwaitSignal.new(requests: [request]) if resolved.equal?(DEFER)
|
|
64
|
+
|
|
65
|
+
resolved
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
def all(definitions)
|
|
@@ -64,18 +70,37 @@ module AsyncGraph
|
|
|
64
70
|
[key.to_s, [kind, payload || {}]]
|
|
65
71
|
end
|
|
66
72
|
|
|
73
|
+
resolved = {}
|
|
67
74
|
missing = normalized.filter_map do |key, (kind, payload)|
|
|
68
|
-
|
|
75
|
+
if @resolved.key?(key)
|
|
76
|
+
resolved[key] = @resolved[key]
|
|
77
|
+
next
|
|
78
|
+
end
|
|
69
79
|
|
|
70
|
-
Request.new(key:, kind:, payload:)
|
|
80
|
+
request = Request.new(key:, kind:, payload:)
|
|
81
|
+
value = resolve(request)
|
|
82
|
+
if value.equal?(DEFER)
|
|
83
|
+
request
|
|
84
|
+
else
|
|
85
|
+
resolved[key] = value
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
throw :await, AwaitSignal.new(requests: missing) unless missing.empty?
|
|
74
91
|
|
|
75
|
-
normalized.keys.to_h { |key| [key.to_sym,
|
|
92
|
+
normalized.keys.to_h { |key| [key.to_sym, resolved[key]] }
|
|
76
93
|
end
|
|
77
94
|
|
|
78
95
|
def to_proc = method(:call).to_proc
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def resolve(request)
|
|
100
|
+
return DEFER unless @resolve_request
|
|
101
|
+
|
|
102
|
+
@resolve_request.call(request.kind, request.payload)
|
|
103
|
+
end
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
class Graph
|
|
@@ -124,14 +149,14 @@ module AsyncGraph
|
|
|
124
149
|
|
|
125
150
|
def set_finish_point(name) = edge(name, FINISH)
|
|
126
151
|
|
|
127
|
-
def step(state:, node:, resolved: {})
|
|
152
|
+
def step(state:, node:, resolved: {}, resolve_request: nil)
|
|
128
153
|
validate!
|
|
129
154
|
|
|
130
155
|
current = node.to_sym
|
|
131
156
|
return Finished.new(state:) if current == FINISH
|
|
132
157
|
|
|
133
158
|
validate_known_node!(current, context: "Step node")
|
|
134
|
-
await = Await.new(resolved)
|
|
159
|
+
await = Await.new(resolved, resolve_request)
|
|
135
160
|
result = catch(:await) { [:ok, call_node(@nodes[current], state, await)] }
|
|
136
161
|
return Suspended.new(state:, node: current, requests: result.requests) if result.is_a?(AwaitSignal)
|
|
137
162
|
|
data/lib/async-graph/runner.rb
CHANGED
|
@@ -52,13 +52,14 @@ module AsyncGraph
|
|
|
52
52
|
)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def step(token:, joins:, resolved: {}, &)
|
|
55
|
+
def step(token:, joins:, resolved: {}, resolve_request: nil, &)
|
|
56
56
|
return build_join_result(@graph.process_join(token:, joins:)) if join_token?(token)
|
|
57
57
|
|
|
58
58
|
graph_step = @graph.step(
|
|
59
59
|
state: token.fetch(:state),
|
|
60
60
|
node: token.fetch(:node),
|
|
61
|
-
resolved: resolved
|
|
61
|
+
resolved: resolved,
|
|
62
|
+
resolve_request: resolve_request
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
case graph_step
|
|
@@ -73,13 +74,18 @@ module AsyncGraph
|
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
def advance_run(run:, resolved_for: nil, &)
|
|
77
|
+
def advance_run(run:, resolved: nil, resolved_for: nil, resolve_request: nil, &)
|
|
78
|
+
if resolved && resolved_for
|
|
79
|
+
raise ArgumentError, "Runner#advance_run accepts either resolved: or resolved_for:, not both"
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
current_run = normalize_run(run)
|
|
78
83
|
return current_run if current_run.finished?
|
|
79
84
|
|
|
80
85
|
next_tokens, joins, final_state = advance_tokens(
|
|
81
86
|
current_run,
|
|
82
|
-
|
|
87
|
+
resolved_source: resolved || resolved_for,
|
|
88
|
+
resolve_request: resolve_request,
|
|
83
89
|
&
|
|
84
90
|
)
|
|
85
91
|
Run.new(
|
|
@@ -226,7 +232,7 @@ module AsyncGraph
|
|
|
226
232
|
raise ArgumentError, "Runner#step requires a block to bind external IDs for new requests"
|
|
227
233
|
end
|
|
228
234
|
|
|
229
|
-
def advance_tokens(run,
|
|
235
|
+
def advance_tokens(run, resolved_source:, resolve_request:, &)
|
|
230
236
|
next_tokens = []
|
|
231
237
|
joins = run.joins
|
|
232
238
|
final_state = run.result
|
|
@@ -235,7 +241,8 @@ module AsyncGraph
|
|
|
235
241
|
outcome = step(
|
|
236
242
|
token: token,
|
|
237
243
|
joins: joins,
|
|
238
|
-
resolved:
|
|
244
|
+
resolved: resolved_source&.call(token) || {},
|
|
245
|
+
resolve_request: resolve_request,
|
|
239
246
|
&
|
|
240
247
|
)
|
|
241
248
|
joins = outcome.joins
|
data/lib/async-graph/version.rb
CHANGED
data/spec/async_graph_spec.rb
CHANGED
|
@@ -47,6 +47,96 @@ RSpec.describe AsyncGraph::Graph do
|
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
it "resolves await.call inline through resolve_request" do
|
|
51
|
+
graph = described_class.new do
|
|
52
|
+
node :calculate do |state, await|
|
|
53
|
+
sum = await.call(:sum, :add, left: state[:left], right: state[:right])
|
|
54
|
+
{sum: sum}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
set_entry_point :calculate
|
|
58
|
+
set_finish_point :calculate
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
step = graph.step(
|
|
62
|
+
state: {left: 7, right: 5},
|
|
63
|
+
node: graph.entry,
|
|
64
|
+
resolve_request: lambda do |kind, payload|
|
|
65
|
+
case kind
|
|
66
|
+
when :add then payload[:left] + payload[:right]
|
|
67
|
+
else AsyncGraph::DEFER
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
aggregate_failures do
|
|
73
|
+
expect(step).to be_a(AsyncGraph::Advanced)
|
|
74
|
+
expect(step.state).to eq(left: 7, right: 5, sum: 12)
|
|
75
|
+
expect(step.destinations.map(&:to)).to eq([AsyncGraph::FINISH])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "suspends await.all only for deferred requests" do
|
|
80
|
+
graph = described_class.new do
|
|
81
|
+
node :calculate do |state, await|
|
|
82
|
+
results = await.all(
|
|
83
|
+
added: [:add, {left: state[:left], right: state[:right]}],
|
|
84
|
+
discounted: [:discount, {total: state[:total], amount: state[:discount]}]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
added: results[:added],
|
|
89
|
+
discounted: results[:discounted]
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
set_entry_point :calculate
|
|
94
|
+
set_finish_point :calculate
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
step = graph.step(
|
|
98
|
+
state: {left: 7, right: 5, total: 20, discount: 3},
|
|
99
|
+
node: graph.entry,
|
|
100
|
+
resolve_request: lambda do |kind, payload|
|
|
101
|
+
case kind
|
|
102
|
+
when :add then payload[:left] + payload[:right]
|
|
103
|
+
else AsyncGraph::DEFER
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
aggregate_failures do
|
|
109
|
+
expect(step).to be_a(AsyncGraph::Suspended)
|
|
110
|
+
expect(step.requests.map(&:key)).to eq(["discounted"])
|
|
111
|
+
expect(step.requests.map(&:kind)).to eq([:discount])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
resumed = graph.step(
|
|
115
|
+
state: step.state,
|
|
116
|
+
node: step.node,
|
|
117
|
+
resolved: {"discounted" => 17},
|
|
118
|
+
resolve_request: lambda do |kind, payload|
|
|
119
|
+
case kind
|
|
120
|
+
when :add then payload[:left] + payload[:right]
|
|
121
|
+
else AsyncGraph::DEFER
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
aggregate_failures do
|
|
127
|
+
expect(resumed).to be_a(AsyncGraph::Advanced)
|
|
128
|
+
expect(resumed.state).to eq(
|
|
129
|
+
left: 7,
|
|
130
|
+
right: 5,
|
|
131
|
+
total: 20,
|
|
132
|
+
discount: 3,
|
|
133
|
+
added: 12,
|
|
134
|
+
discounted: 17
|
|
135
|
+
)
|
|
136
|
+
expect(resumed.destinations.map(&:to)).to eq([AsyncGraph::FINISH])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
50
140
|
it "builds a barrier edge from multiple sources" do
|
|
51
141
|
graph = described_class.new do
|
|
52
142
|
node :left do
|
data/spec/runner_spec.rb
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe AsyncGraph::Runner do
|
|
4
|
+
it "creates a start token from the graph entry point" do
|
|
5
|
+
graph = AsyncGraph::Graph.new do
|
|
6
|
+
node :start do
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
set_entry_point :start
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
token = described_class.new(graph).start(state: {user_id: 7})
|
|
13
|
+
|
|
14
|
+
expect(token).to eq(
|
|
15
|
+
token_uid: "t1",
|
|
16
|
+
node: :start,
|
|
17
|
+
state: {user_id: 7},
|
|
18
|
+
fork_uid: nil,
|
|
19
|
+
branch: nil,
|
|
20
|
+
source_node: nil,
|
|
21
|
+
awaits: {}
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "builds an opaque persisted run from the entry point" do
|
|
26
|
+
graph = AsyncGraph::Graph.new do
|
|
27
|
+
node :start do
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
set_entry_point :start
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
run = described_class.new(graph).start_run(state: {user_id: 7})
|
|
34
|
+
|
|
35
|
+
aggregate_failures do
|
|
36
|
+
expect(run).to be_running
|
|
37
|
+
expect(run.result).to be_nil
|
|
38
|
+
expect(run.to_h).to eq(
|
|
39
|
+
status: "running",
|
|
40
|
+
tokens: [
|
|
41
|
+
{
|
|
42
|
+
token_uid: "t1",
|
|
43
|
+
node: :start,
|
|
44
|
+
state: {user_id: 7},
|
|
45
|
+
fork_uid: nil,
|
|
46
|
+
branch: nil,
|
|
47
|
+
source_node: nil,
|
|
48
|
+
awaits: {}
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
joins: {},
|
|
52
|
+
result: nil
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "spawns branch tokens for fan-out and finishes on finish edges" do
|
|
58
|
+
graph = AsyncGraph::Graph.new do
|
|
59
|
+
node :split do
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
node :left do
|
|
63
|
+
{left_ready: true}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
node :right do
|
|
67
|
+
{right_ready: true}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
set_entry_point :split
|
|
71
|
+
edge :split, :left, branch: :left
|
|
72
|
+
edge :split, :right, branch: :right
|
|
73
|
+
set_finish_point :left
|
|
74
|
+
set_finish_point :right
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
runner = described_class.new(graph)
|
|
78
|
+
split = runner.step(
|
|
79
|
+
token: runner.start(state: {user_id: 7}),
|
|
80
|
+
joins: {}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
aggregate_failures do
|
|
84
|
+
expect(split).to be_advanced
|
|
85
|
+
expect(split.tokens).to eq(
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
token_uid: "t1.left",
|
|
89
|
+
node: :left,
|
|
90
|
+
state: {user_id: 7},
|
|
91
|
+
fork_uid: "fork-t1",
|
|
92
|
+
branch: :left,
|
|
93
|
+
source_node: :split,
|
|
94
|
+
awaits: {}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
token_uid: "t1.right",
|
|
98
|
+
node: :right,
|
|
99
|
+
state: {user_id: 7},
|
|
100
|
+
fork_uid: "fork-t1",
|
|
101
|
+
branch: :right,
|
|
102
|
+
source_node: :split,
|
|
103
|
+
awaits: {}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
left = runner.step(token: split.tokens.first, joins: split.joins)
|
|
110
|
+
right = runner.step(token: split.tokens.last, joins: split.joins)
|
|
111
|
+
|
|
112
|
+
aggregate_failures do
|
|
113
|
+
expect(left).to be_finished
|
|
114
|
+
expect(left.state).to eq(user_id: 7, left_ready: true)
|
|
115
|
+
expect(right).to be_finished
|
|
116
|
+
expect(right.state).to eq(user_id: 7, right_ready: true)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "advances a persisted run without exposing joins to the caller" do
|
|
121
|
+
graph = AsyncGraph::Graph.new do
|
|
122
|
+
node :split do
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
node :left do
|
|
126
|
+
{left_ready: true}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
node :right do
|
|
130
|
+
{right_ready: true}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
node :merge do |state, await|
|
|
134
|
+
profile = await.call(:profile, :fetch_profile, user_id: state[:user_id])
|
|
135
|
+
{profile: profile}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
set_entry_point :split
|
|
139
|
+
edge :split, :left, branch: :left
|
|
140
|
+
edge :split, :right, branch: :right
|
|
141
|
+
edge %i[left right], :merge
|
|
142
|
+
set_finish_point :merge
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
runner = described_class.new(graph)
|
|
146
|
+
run = runner.start_run(state: {user_id: 7})
|
|
147
|
+
run1 = runner.advance_run(run: run)
|
|
148
|
+
run2 = runner.advance_run(run: run1)
|
|
149
|
+
run3 = runner.advance_run(run: run2)
|
|
150
|
+
run4 = runner.advance_run(run: run3) { |_| "job-1" }
|
|
151
|
+
run5 = runner.advance_run(
|
|
152
|
+
run: run4,
|
|
153
|
+
resolved_for: ->(token) { token[:awaits][:profile] == "job-1" ? {"profile" => {id: 7, name: "Ada"}} : {} }
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
aggregate_failures do
|
|
157
|
+
expect(run1.tokens.map { |token| token[:token_uid] }).to eq(%w[t1.left t1.right])
|
|
158
|
+
expect(run2.tokens.map { |token| token[:node] }).to eq(%i[merge merge])
|
|
159
|
+
expect(run3.tokens.map { |token| token[:token_uid] }).to eq(["fork-t1.join"])
|
|
160
|
+
expect(run4.tokens.first.fetch(:awaits)).to eq(profile: "job-1")
|
|
161
|
+
expect(run5).to be_finished
|
|
162
|
+
expect(run5.result).to eq(
|
|
163
|
+
user_id: 7,
|
|
164
|
+
left_ready: true,
|
|
165
|
+
right_ready: true,
|
|
166
|
+
profile: {id: 7, name: "Ada"}
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "finishes a run inline when resolve_request handles all awaits" do
|
|
172
|
+
graph = AsyncGraph::Graph.new do
|
|
173
|
+
node :calculate do |state, await|
|
|
174
|
+
results = await.all(
|
|
175
|
+
added: [:add, {left: state[:left], right: state[:right]}],
|
|
176
|
+
subtracted: [:subtract, {left: state[:total], right: state[:discount]}]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
added: results[:added],
|
|
181
|
+
subtracted: results[:subtracted],
|
|
182
|
+
answer: results[:added] - results[:subtracted]
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
set_entry_point :calculate
|
|
187
|
+
set_finish_point :calculate
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
runner = described_class.new(graph)
|
|
191
|
+
run = runner.advance_run(
|
|
192
|
+
run: runner.start_run(state: {left: 7, right: 5, total: 20, discount: 3}),
|
|
193
|
+
resolve_request: lambda do |kind, payload|
|
|
194
|
+
case kind
|
|
195
|
+
when :add then payload[:left] + payload[:right]
|
|
196
|
+
when :subtract then payload[:left] - payload[:right]
|
|
197
|
+
else AsyncGraph::DEFER
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
aggregate_failures do
|
|
203
|
+
expect(run).to be_finished
|
|
204
|
+
expect(run.result).to eq(
|
|
205
|
+
left: 7,
|
|
206
|
+
right: 5,
|
|
207
|
+
total: 20,
|
|
208
|
+
discount: 3,
|
|
209
|
+
added: 12,
|
|
210
|
+
subtracted: 17,
|
|
211
|
+
answer: -5
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "binds only deferred requests when resolve_request handles part of await.all inline" do
|
|
217
|
+
graph = AsyncGraph::Graph.new do
|
|
218
|
+
node :calculate do |state, await|
|
|
219
|
+
results = await.all(
|
|
220
|
+
added: [:add, {left: state[:left], right: state[:right]}],
|
|
221
|
+
discounted: [:discount, {total: state[:total], amount: state[:discount]}]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
{
|
|
225
|
+
added: results[:added],
|
|
226
|
+
discounted: results[:discounted]
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
set_entry_point :calculate
|
|
231
|
+
set_finish_point :calculate
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
resolve_request = lambda do |kind, payload|
|
|
235
|
+
case kind
|
|
236
|
+
when :add then payload[:left] + payload[:right]
|
|
237
|
+
else AsyncGraph::DEFER
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
runner = described_class.new(graph)
|
|
242
|
+
first = runner.advance_run(
|
|
243
|
+
run: runner.start_run(state: {left: 7, right: 5, total: 20, discount: 3}),
|
|
244
|
+
resolved: ->(_token) { nil },
|
|
245
|
+
resolve_request: resolve_request
|
|
246
|
+
) { |request| "job-#{request.key}" }
|
|
247
|
+
|
|
248
|
+
finished = runner.advance_run(
|
|
249
|
+
run: first,
|
|
250
|
+
resolved: lambda do |token|
|
|
251
|
+
token[:awaits][:discounted] == "job-discounted" ? {"discounted" => 17} : {}
|
|
252
|
+
end,
|
|
253
|
+
resolve_request: resolve_request
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
aggregate_failures do
|
|
257
|
+
expect(first).to be_running
|
|
258
|
+
expect(first.tokens.first.fetch(:awaits)).to eq(discounted: "job-discounted")
|
|
259
|
+
expect(finished).to be_finished
|
|
260
|
+
expect(finished.result).to eq(
|
|
261
|
+
left: 7,
|
|
262
|
+
right: 5,
|
|
263
|
+
total: 20,
|
|
264
|
+
discount: 3,
|
|
265
|
+
added: 12,
|
|
266
|
+
discounted: 17
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "reuses existing request bindings when a suspended token is retried" do
|
|
272
|
+
graph = AsyncGraph::Graph.new do
|
|
273
|
+
node :fetch do |state, await|
|
|
274
|
+
user = await.call(:user, :fetch_user, user_id: state[:user_id])
|
|
275
|
+
{user: user}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
set_entry_point :fetch
|
|
279
|
+
set_finish_point :fetch
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
runner = described_class.new(graph)
|
|
283
|
+
first = runner.step(token: runner.start(state: {user_id: 7}), joins: {}) { |_| "job-1" }
|
|
284
|
+
second = runner.step(token: first.token, joins: first.joins)
|
|
285
|
+
finished = runner.step(
|
|
286
|
+
token: first.token,
|
|
287
|
+
joins: first.joins,
|
|
288
|
+
resolved: {"user" => {id: 7, name: "Ada"}}
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
aggregate_failures do
|
|
292
|
+
expect(first).to be_suspended
|
|
293
|
+
expect(first.token.fetch(:awaits)).to eq(user: "job-1")
|
|
294
|
+
expect(second).to be_suspended
|
|
295
|
+
expect(second.token.fetch(:awaits)).to eq(user: "job-1")
|
|
296
|
+
expect(second.request_refs).to eq(user: "job-1")
|
|
297
|
+
expect(finished).to be_finished
|
|
298
|
+
expect(finished.state).to eq(
|
|
299
|
+
user_id: 7,
|
|
300
|
+
user: {id: 7, name: "Ada"}
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "parks and releases join tokens through the runner" do
|
|
306
|
+
graph = AsyncGraph::Graph.new do
|
|
307
|
+
node :split do
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
node :left do
|
|
311
|
+
{left_ready: true}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
node :right do
|
|
315
|
+
{right_ready: true}
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
node :merge do
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
set_entry_point :split
|
|
322
|
+
edge :split, :left, branch: :left
|
|
323
|
+
edge :split, :right, branch: :right
|
|
324
|
+
edge %i[left right], :merge
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
runner = described_class.new(graph)
|
|
328
|
+
split = runner.step(
|
|
329
|
+
token: runner.start(state: {user_id: 7}),
|
|
330
|
+
joins: {}
|
|
331
|
+
)
|
|
332
|
+
left = runner.step(token: split.tokens.first, joins: split.joins)
|
|
333
|
+
right = runner.step(token: split.tokens.last, joins: split.joins)
|
|
334
|
+
parked = runner.step(token: left.token, joins: left.joins)
|
|
335
|
+
released = runner.step(token: right.token, joins: parked.joins)
|
|
336
|
+
|
|
337
|
+
aggregate_failures do
|
|
338
|
+
expect(parked).to be_parked
|
|
339
|
+
expect(parked.joins.keys).to eq([:'fork-t1:merge'])
|
|
340
|
+
expect(released).to be_released
|
|
341
|
+
expect(released.tokens).to eq(
|
|
342
|
+
[
|
|
343
|
+
{
|
|
344
|
+
token_uid: "fork-t1.join",
|
|
345
|
+
node: :merge,
|
|
346
|
+
state: {user_id: 7, left_ready: true, right_ready: true},
|
|
347
|
+
fork_uid: nil,
|
|
348
|
+
branch: nil,
|
|
349
|
+
source_node: nil,
|
|
350
|
+
awaits: {}
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
)
|
|
354
|
+
expect(released.joins).to eq({})
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-graph
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artem Borodkin
|
|
@@ -114,7 +114,8 @@ files:
|
|
|
114
114
|
- Rakefile
|
|
115
115
|
- async-graph.gemspec
|
|
116
116
|
- bin/console
|
|
117
|
-
- examples/
|
|
117
|
+
- examples/all_in_one_inline_runner.rb
|
|
118
|
+
- examples/all_in_one_jobs_runner.rb
|
|
118
119
|
- examples/app_graph.rb
|
|
119
120
|
- examples/execute_jobs.rb
|
|
120
121
|
- examples/graph_run.rb
|
|
@@ -126,6 +127,7 @@ files:
|
|
|
126
127
|
- lib/async-graph/runner.rb
|
|
127
128
|
- lib/async-graph/version.rb
|
|
128
129
|
- spec/async_graph_spec.rb
|
|
130
|
+
- spec/runner_spec.rb
|
|
129
131
|
- spec/spec_helper.rb
|
|
130
132
|
homepage: https://rubygems.org/gems/async-graph
|
|
131
133
|
licenses:
|