excadg 0.2.5 → 0.4.0
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/README.md +71 -19
- data/bin/excadg +19 -18
- data/lib/excadg/broker.rb +112 -80
- data/lib/excadg/data_store.rb +4 -2
- data/lib/excadg/log.rb +3 -2
- data/lib/excadg/payload/wrapper.rb +9 -10
- data/lib/excadg/request.rb +5 -1
- data/lib/excadg/rtimeout.rb +2 -1
- data/lib/excadg/state_machine.rb +2 -1
- data/lib/excadg/tui/block.rb +5 -5
- data/lib/excadg/tui/format.rb +3 -3
- data/lib/excadg/tui.rb +40 -28
- data/lib/excadg/vertex.rb +17 -8
- data/lib/excadg/vtracker.rb +12 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f27b6553ab8e3c1e7e77fadfeb9a4cdae08cd950063c7ab42436294c01fbae93
|
4
|
+
data.tar.gz: '06389a1abf75c660f102b6dd4675bd4af7f21014d674c724215dd13525ffed9e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66431e9b380cfb4027dfa439566ada4e32e013e468a11deeced5be56f9da5668f9f9cedbb2d62a91eb570c61f805d276bbd98058cc867560fd75eebdf0f8b47b
|
7
|
+
data.tar.gz: c77bbe41f90c1d939d2a8b3c129abe1258aba0058e1a1e8ccfb4ce797b14dc4aa45c0803d91544c7504abfe23178fc824254a9146cb6b95dda04287f208e69d3
|
data/README.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
- [Description](#description)
|
2
|
+
- [Usage](#usage)
|
3
|
+
- [Tool](#tool)
|
4
|
+
- [Framework](#framework)
|
5
|
+
- [Payload implementation](#payload-implementation)
|
6
|
+
- [Vertices constructing](#vertices-constructing)
|
7
|
+
- [Tips](#tips)
|
8
|
+
- [Internals](#internals)
|
9
|
+
- [Overview](#overview)
|
10
|
+
- [Vertice processing states](#vertice-processing-states)
|
11
|
+
- [{ExcADG::Broker}](#excadgbroker)
|
12
|
+
- [{ExcADG::DataStore}](#excadgdatastore)
|
13
|
+
- [{ExcADG::StateMachine}](#excadgstatemachine)
|
14
|
+
- [{ExcADG::Payload}](#excadgpayload)
|
15
|
+
- [{ExcADG::Log}](#excadglog)
|
16
|
+
- [{ExcADG::VTracker}](#excadgvtracker)
|
17
|
+
- [Development](#development)
|
18
|
+
- [Gem](#gem)
|
19
|
+
- [Testing](#testing)
|
20
|
+
- [Docs](#docs)
|
21
|
+
- [Next](#next)
|
22
|
+
- [Core](#core)
|
23
|
+
- [TUI](#tui)
|
24
|
+
- [Payload](#payload)
|
25
|
+
|
1
26
|
# Description
|
2
27
|
|
3
28
|
That's a library (framework) to execute a graph of dependent tasks (vertices).
|
@@ -8,11 +33,17 @@ Another feature is that the graph is dynamic and any vertex could produce anothe
|
|
8
33
|
|
9
34
|
## Tool
|
10
35
|
|
36
|
+
tl;dr
|
37
|
+
``` bash
|
38
|
+
./bin/adgen --range 1:5 --file mygraph.yaml --count 30
|
39
|
+
./bin/excadg --graph mygraph.yaml -l mygraph.log -d mygraph.yaml --gdump mygraph.jpg
|
40
|
+
```
|
41
|
+
|
11
42
|
There is a tool script in `bin` folder called `excadg`. Run `./bin/excadg --help` for available options.
|
12
43
|
It allows to run basic payload graphs specified by a YAML config. See [config/](config/) folder for sample configs.
|
13
44
|
|
14
45
|
Another tool is `bin/adgen`, it has `--help` as well. It's suitable to generate relatively complex random graphs.
|
15
|
-
|
46
|
+
|
16
47
|
|
17
48
|
## Framework
|
18
49
|
|
@@ -62,23 +93,27 @@ In this case, it'd be what `system 'echo here I am'` returns - `true`.
|
|
62
93
|
|
63
94
|
Dependencies could be specified with both - {ExcADG::Vertex} objects and names. E.g.
|
64
95
|
``` ruby
|
65
|
-
Broker.
|
96
|
+
Broker.instance.start
|
66
97
|
|
67
98
|
v1 = Vertex.new payload: MyPayload.new
|
68
99
|
Vertex.new name: :v2, payload: MyPayload.new
|
69
100
|
|
70
101
|
Vertex.new name: :final, payload: MyPayload.new, deps: [v1, :v2]
|
71
102
|
|
72
|
-
Broker.wait_all
|
103
|
+
Broker.instance.wait_all
|
73
104
|
```
|
74
105
|
|
75
|
-
*See [Broker section](#excadgbroker) for `Broker.
|
106
|
+
*See [Broker section](#excadgbroker) for `Broker.instance.start` and `Broker.instance.wait_all` usage.*
|
76
107
|
|
77
108
|
Using actual objects looks simpler, but it's less convenient, as it requires you to construct all vertices as they appear in the execution graph.
|
78
109
|
However, names allows you to spawn vertices in arbitrary order and expect framework to figure execution order on the fly. See [run tool](#tool)'s code as an example of using names.
|
79
110
|
|
80
111
|
*There is no need to store {ExcADG::Vertex} objects, as it and its data are available through [Broker](#excadgbroker)'s [DataStore](#excadgdatastore) and there is no interface to communicate with an {ExcADG::Vertex} directly.*
|
81
112
|
|
113
|
+
## Tips
|
114
|
+
|
115
|
+
If your app doesn't use all CPU cores with this library or has # of expected vertices much bigger than # of CPUs, try to `export RUBY_MAX_CPU=<num of cores> RUBY_MN_THREADS=1` to engage all cores. *See https://bugs.ruby-lang.org/issues/20618 for details.*
|
116
|
+
|
82
117
|
# Internals
|
83
118
|
|
84
119
|
## Overview
|
@@ -97,7 +132,7 @@ Internally, each vertice go through a sequence of states. Now it's **new**, **re
|
|
97
132
|
{ExcADG::Vertex} starts as a **new** vertex and waits for its dependencies.
|
98
133
|
When vertex received all dependencies states and made sure they're **done**, it becomes **ready** and starts its payload.
|
99
134
|
When payload finishes, the vertex transitions to the **done** state.
|
100
|
-
If any of the stages fails, the vertex becomes **failed**. It could happen as due to a failed dependency (a stage failed) as well as any other error occurred (e.g. it receives an incorrect data from broker). A single failed vertex makes all vertices depending on it to fail. This way
|
135
|
+
If any of the stages fails, the vertex becomes **failed**. It could happen as due to a failed dependency (a stage failed) as well as any other error occurred (e.g. it receives an incorrect data from broker). A single failed vertex makes all vertices depending on it to fail. This way [failures cascading through the graph](config/faulty.yaml).
|
101
136
|
|
102
137
|
## {ExcADG::Broker}
|
103
138
|
|
@@ -107,10 +142,11 @@ When a vertex changes its state, it (actually, state machine does that) notifies
|
|
107
142
|
|
108
143
|
Broker is desired to be as thin as possible to keep most of the work for vertices.
|
109
144
|
|
110
|
-
Each application should invoke
|
111
|
-
Its counterpart is
|
145
|
+
Each application should invoke `Broker.instance.start` to enable messages processing.
|
146
|
+
Its counterpart is `Broker.instance.wait_all` which waits for all known vertices to reach one of the terminal states (**done** or **failed**) within specified timeout. Same as `Broker.instance.start`, it spawns a thread and returns it. The main application could keep it in background and query or hang on `.join` once all vertices are spawned. Once the thread finishes, the main app could lookup vertices execution results in `Broker.instance.data_store` (see [DataStore](#excadgdatastore)). Broker could track all the seen vertices and their dependencies using builtin {ExcADG::VTracker}, add `track:true` to the `start` call to enable tracking.
|
112
147
|
|
113
|
-
>
|
148
|
+
> Note 1: tracking is a purely optional, broker itself requires {ExcADG::DataStore} only
|
149
|
+
> Note 2: beware that broker constantly uses main Ractor's ports - incoming and outgoing. Hence, `Ractor#take`, `Ractor.yield` or any other messaging in the main ractor conflict with broker.
|
114
150
|
|
115
151
|
## {ExcADG::DataStore}
|
116
152
|
|
@@ -141,6 +177,12 @@ Second way is built-in. Vertex invokes payload with {ExcADG::Array} of dependenc
|
|
141
177
|
|
142
178
|
The library has its own logger based on Ractors. You could call {ExcADG::Log#unmute} to enable these logs.
|
143
179
|
|
180
|
+
## {ExcADG::VTracker}
|
181
|
+
|
182
|
+
It's an optional component which allows to track all vertices to be able to repro the full graph. There is no central place that stores the whole graph due to the ExcADG's core princilple - allow to spawn vertices at any time from any place. This class is introduced in order to support doing basic execution visualization (see {ExcADG::Tui}) and analysis.
|
183
|
+
|
184
|
+
The tracker is integrated to the Broker and can be accessed by `ExcADG::Broker.instance.vtracker`. It's disabled by default to speed-up execution, but can be enabled on borker's startup by `Broker.instance.start track: true`.
|
185
|
+
|
144
186
|
# Development
|
145
187
|
|
146
188
|
The project is based on RVM => there is a .ruby-gemset file.
|
@@ -180,14 +222,24 @@ Logging is disabled by default, but it could be useful to debug tests. Add `ExcA
|
|
180
222
|
|
181
223
|
> here is a list of improvemets could be implementex next
|
182
224
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
225
|
+
## Core
|
226
|
+
1. implement throttling: allow to `:suspend` vertices that polls deps
|
227
|
+
- limit # of simultaneously running vertices
|
228
|
+
- limit # of vertices allowed to spawn
|
229
|
+
2. implement checks using tracker
|
230
|
+
- for loops (it also leads to cases when there are no nodes to start from)
|
231
|
+
- for unreachable islands (optional, it could be expected)
|
232
|
+
- for the failure root cause
|
233
|
+
3. move Vertice's tests to system test suite, make UTs for Vertice class
|
234
|
+
|
235
|
+
## TUI
|
236
|
+
1. make timeouts more flexible in the excadg tool
|
237
|
+
2. allow to focus on a certain vertex to see what's it waiting for and what's waiting for it
|
238
|
+
3. allow to dump focused vertice's state
|
239
|
+
4. improve messages for timed out execution
|
240
|
+
5. split TUI to another gem
|
241
|
+
|
242
|
+
## Payload
|
243
|
+
4. make a loop payload template
|
244
|
+
5. allow to stream shell payload stdout/err in realtime to logs and files
|
245
|
+
6. add native ruby example with a dynamic graph
|
data/bin/excadg
CHANGED
@@ -43,7 +43,7 @@ OptionParser.new { |opts|
|
|
43
43
|
opts.on('-d', '--dump FILENAME', 'dump all vertices state data to the file in the end') { |o|
|
44
44
|
options[:dump] = o
|
45
45
|
}
|
46
|
-
opts.on('--gdump FILENAME', 'dump
|
46
|
+
opts.on('--gdump FILENAME', 'dump final execution graph to the file specified') { |o|
|
47
47
|
options[:gdump] = o
|
48
48
|
}
|
49
49
|
opts.on('-t', '--timeout SECONDS', 'for how long to wait for the vertices to execute') { |o|
|
@@ -75,55 +75,56 @@ raise ArgumentError, 'at least one vertex should be ready to start' if runnable_
|
|
75
75
|
|
76
76
|
ExcADG::Log.info 'collect execution graph from config'
|
77
77
|
|
78
|
-
all_vertex_names = config.keys.collect!(&:to_sym)
|
79
|
-
graph = RGL::DirectedAdjacencyGraph.new
|
80
78
|
config.each_pair { |id, vconfig|
|
81
79
|
name = id.to_sym
|
82
80
|
payload = if vconfig&.key?('command')
|
83
81
|
ExcADG::Payload::Wrapper::Bin.new args: vconfig['command']
|
84
82
|
elsif vconfig&.key?('sleep')
|
85
83
|
ExcADG::Payload::Example::Sleepy.new args: vconfig['sleep'].to_i
|
84
|
+
elsif vconfig&.key?('fail')
|
85
|
+
ExcADG::Payload::Example::Faulty.new args: vconfig['fail'] || 'injected failure'
|
86
86
|
else
|
87
87
|
ExcADG::Payload::Example::Echo.new args: vconfig
|
88
88
|
end
|
89
89
|
deps_v_names = (vconfig&.dig('dependencies') || []).collect(&:to_sym)
|
90
|
-
|
91
|
-
raise "there is no dependency named '#{dep}' in the graph for vertex '#{id}'" unless all_vertex_names.include? dep
|
92
|
-
|
93
|
-
graph.add_edge name, dep
|
94
|
-
}
|
95
|
-
|
96
|
-
ExcADG::Vertex.new name:, payload:, deps: deps_v_names
|
90
|
+
ExcADG::Vertex.new name:, payload:, deps: deps_v_names, timeout: options[:timeout]
|
97
91
|
}
|
98
92
|
|
99
|
-
graph.write_to_graphic_file(options[:gdump].split('.').last, options[:gdump].split('.')[...-1].join('.')) unless options[:gdump].nil?
|
100
|
-
|
101
93
|
ExcADG::Log.info 'starting state data broker'
|
102
|
-
ExcADG::Broker.
|
94
|
+
ExcADG::Broker.instance.start track: true
|
103
95
|
|
104
96
|
ui_drawer&.run
|
105
97
|
|
106
98
|
ExcADG::Log.info 'watching for all vertices to complete'
|
107
99
|
timed_out = false
|
108
100
|
begin
|
109
|
-
ExcADG::Broker.wait_all(timeout: options[:timeout]).join
|
101
|
+
waiter = ExcADG::Broker.instance.wait_all(timeout: options[:timeout]).join
|
110
102
|
rescue Timeout::Error
|
111
103
|
ExcADG::Log.error 'execution timed out'
|
112
104
|
timed_out = true
|
105
|
+
rescue Interrupt
|
106
|
+
ExcADG::Broker.instance.teardown
|
107
|
+
waiter&.kill
|
113
108
|
end
|
114
|
-
vertice_by_state = ExcADG::Broker.data_store.to_a.group_by(&:state)
|
115
109
|
|
116
|
-
ExcADG::Log.info "
|
117
|
-
has_failed =
|
110
|
+
ExcADG::Log.info "vertice counts by state: #{ExcADG::Broker.instance.vtracker.by_state.transform_values(&:size)}"
|
111
|
+
has_failed = ExcADG::Broker.instance.vtracker.by_state.key? :failed
|
118
112
|
ui_drawer&.summarize has_failed, timed_out
|
119
113
|
|
120
114
|
unless options[:dump].nil?
|
121
115
|
ExcADG::Log.info "writing data to #{options[:dump]}"
|
122
116
|
File.open(options[:dump], 'w+') { |f|
|
123
|
-
f.write JSON.dump ExcADG::Broker.data_store.to_a
|
117
|
+
f.write JSON.dump ExcADG::Broker.instance.data_store.to_a
|
124
118
|
}
|
125
119
|
end
|
126
120
|
|
121
|
+
unless options[:gdump].nil?
|
122
|
+
extension = options[:gdump].split('.').last
|
123
|
+
name = options[:gdump].split('.')[...-1].join('.')
|
124
|
+
ExcADG::Broker.instance.vtracker.graph.write_to_graphic_file(extension, name)
|
125
|
+
ExcADG::Log.debug "graph is saved to #{options[:gdump]}"
|
126
|
+
end
|
127
|
+
|
127
128
|
sleep 0.1 # let the logger to print message
|
128
129
|
exit_code = 0
|
129
130
|
exit_code |= 1 if has_failed
|
data/lib/excadg/broker.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'timeout'
|
4
|
+
require 'singleton'
|
4
5
|
|
5
6
|
require_relative 'data_store'
|
6
7
|
require_relative 'log'
|
@@ -9,91 +10,121 @@ require_relative 'vertex'
|
|
9
10
|
|
10
11
|
module ExcADG
|
11
12
|
# handle requests sending/receiving though Ractor's interface
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
resp = Ractor.receive
|
31
|
-
Log.debug "got response #{resp}"
|
32
|
-
raise resp if resp.is_a? StandardError
|
33
|
-
|
34
|
-
Log.debug 'returning response'
|
35
|
-
resp
|
13
|
+
class Broker
|
14
|
+
include Singleton
|
15
|
+
|
16
|
+
attr_reader :data_store, :vtracker
|
17
|
+
|
18
|
+
# is used from vertices to send requests to the broker
|
19
|
+
# @param request {Request}
|
20
|
+
# @return data received in response from the main ractor
|
21
|
+
# @raise {StandardError} if response is a StandardError
|
22
|
+
# @raise {CantSendRequest} if sending request failed for any reason
|
23
|
+
# @raise {UnknownRequestType} if request is not a {Request}
|
24
|
+
def self.ask request
|
25
|
+
raise UnknownRequestType, request unless request.is_a? Request
|
26
|
+
|
27
|
+
begin
|
28
|
+
Ractor.main.send request
|
29
|
+
rescue StandardError => e
|
30
|
+
raise CantSendRequest, cause: e
|
36
31
|
end
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
@broker = Thread.new { loop { process_request } } unless @broker&.alive?
|
33
|
+
Log.info 'getting response'
|
34
|
+
resp = Ractor.receive
|
35
|
+
Log.debug "got response #{resp}"
|
36
|
+
raise resp if resp.is_a? StandardError
|
43
37
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
}
|
38
|
+
Log.debug 'returning response'
|
39
|
+
resp
|
40
|
+
end
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
# start processing vertices asks
|
43
|
+
# @param track whether to track vertices using {VTracker}
|
44
|
+
# @return messanges processing {Thread}
|
45
|
+
def start track: true
|
46
|
+
@vtracker = VTracker.new if track
|
47
|
+
Thread.report_on_exception = false
|
48
|
+
@messenger = Thread.new { loop { process_request } }
|
49
|
+
|
50
|
+
at_exit {
|
51
|
+
Log.info 'shutting down messenger'
|
52
|
+
@messenger.kill
|
53
|
+
Log.info 'messenger is stut down'
|
54
|
+
}
|
55
|
+
|
56
|
+
Log.info 'broker is started'
|
57
|
+
@messenger
|
58
|
+
end
|
59
|
+
|
60
|
+
# stop processing vertices asks
|
61
|
+
# usually follows {#wait_all}
|
62
|
+
def teardown
|
63
|
+
return if @messenger.nil?
|
64
|
+
|
65
|
+
@messenger.kill while @messenger.alive?
|
66
|
+
end
|
52
67
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
68
|
+
# makes a thread to wait for all known vertices to reach a final state;
|
69
|
+
# it expects some vertices to be started in the outer scope,
|
70
|
+
# so it waits even if there are no vertices at all yet
|
71
|
+
# @param timeout total waiting timeout in seconds, nil means wait forever
|
72
|
+
# @param period time between vertices state check
|
73
|
+
# @return {Thread} that waits for all deps, typical usage is `Broker.instance.wait_all.join`
|
74
|
+
def wait_all timeout: 60, period: 1
|
75
|
+
Thread.report_on_exception = false
|
76
|
+
Thread.new {
|
77
|
+
Log.info "timeout is #{timeout || '∞'} seconds"
|
78
|
+
Timeout.timeout(timeout) {
|
79
|
+
loop {
|
80
|
+
sleep period
|
81
|
+
if @data_store.empty?
|
82
|
+
Log.info 'no vertices in data store, keep waiting'
|
83
|
+
next
|
84
|
+
end
|
85
|
+
states = @data_store.to_a.group_by(&:state).keys
|
86
|
+
Log.info "vertices in #{states} states exist"
|
87
|
+
# that's the only final states for vertices
|
88
|
+
break if (states - %i[done failed]).empty?
|
67
89
|
}
|
68
90
|
}
|
69
|
-
|
91
|
+
}
|
92
|
+
end
|
70
93
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
private
|
95
|
+
|
96
|
+
def initialize
|
97
|
+
@data_store = DataStore.new
|
98
|
+
end
|
99
|
+
|
100
|
+
# waits for an incoming request,
|
101
|
+
# validates request type and content
|
102
|
+
# then constructs and sends an answer
|
103
|
+
# does not fail on {StandardError}, logs & sends it back instead to keep running;
|
104
|
+
# the other side crashes in this case, causing the {Vertex} to fail, what's still
|
105
|
+
# better than crashing the whole messaging
|
106
|
+
def process_request
|
107
|
+
request = Ractor.receive
|
108
|
+
Log.info "received request: #{request}"
|
109
|
+
request.self.send case request
|
110
|
+
when Request::GetStateData
|
111
|
+
@vtracker&.track request.self, request.deps
|
112
|
+
request.filter? ? request.deps.collect { |d| @data_store[d] } : @data_store.to_a
|
113
|
+
when Request::Update
|
114
|
+
@data_store << request.data
|
115
|
+
@vtracker&.track request.self
|
116
|
+
true
|
117
|
+
when Request::AddVertex
|
118
|
+
v = Vertex.new payload: request.payload, deps: [request.self]
|
119
|
+
@vtracker&.track v
|
120
|
+
v
|
121
|
+
else
|
122
|
+
raise UnknownRequestType
|
123
|
+
end
|
124
|
+
rescue StandardError => e
|
125
|
+
# TODO: add threshold
|
126
|
+
Log.warn "error on message processing: #{e.class} / #{e.message} / #{e.backtrace}"
|
127
|
+
request.self.send RequestProcessingFailed.new cause: e
|
97
128
|
end
|
98
129
|
|
99
130
|
class UnknownRequestType < StandardError; end
|
@@ -101,11 +132,12 @@ module ExcADG
|
|
101
132
|
|
102
133
|
# error type returned by broker thread in case it failed to process incoming request
|
103
134
|
class RequestProcessingFailed < StandardError
|
104
|
-
attr_reader :
|
135
|
+
attr_reader :cause_backtrace
|
105
136
|
|
106
137
|
def initialize cause:
|
107
|
-
|
108
|
-
|
138
|
+
# storing the cause itself could creak messaging, as it could contain non-shareable objects
|
139
|
+
super cause.message
|
140
|
+
set_backtrace cause.backtrace
|
109
141
|
end
|
110
142
|
end
|
111
143
|
end
|
data/lib/excadg/data_store.rb
CHANGED
@@ -6,8 +6,6 @@ require_relative 'vstate_data'
|
|
6
6
|
module ExcADG
|
7
7
|
# collection of {ExcADG::VStateData} for {ExcADG::Broker}
|
8
8
|
class DataStore
|
9
|
-
attr_reader :size
|
10
|
-
|
11
9
|
def initialize
|
12
10
|
# two hashes to store VStateData and access them fast by either key
|
13
11
|
@by_name = {}
|
@@ -75,6 +73,10 @@ module ExcADG
|
|
75
73
|
end
|
76
74
|
end
|
77
75
|
|
76
|
+
def empty?
|
77
|
+
@size.zero?
|
78
|
+
end
|
79
|
+
|
78
80
|
class DataSkew < StandardError; end
|
79
81
|
end
|
80
82
|
end
|
data/lib/excadg/log.rb
CHANGED
@@ -29,7 +29,7 @@ module ExcADG
|
|
29
29
|
end
|
30
30
|
|
31
31
|
# default logger
|
32
|
-
@main =
|
32
|
+
@main = nil
|
33
33
|
|
34
34
|
# logging is muted by default
|
35
35
|
@muted = true
|
@@ -37,11 +37,12 @@ module ExcADG
|
|
37
37
|
def self.method_missing(method, *args, &_block)
|
38
38
|
return if @muted
|
39
39
|
|
40
|
+
@main ||= RLogger.new
|
40
41
|
r = Ractor.current
|
41
42
|
@main.send [method, r&.to_s || r.object_id, *args]
|
42
43
|
rescue Ractor::ClosedError => e
|
43
44
|
# last hope - there is tty
|
44
|
-
puts "can't send message to logging ractor: #{e}"
|
45
|
+
puts "can't send message to logging ractor: #{e}, message: #{args}"
|
45
46
|
end
|
46
47
|
|
47
48
|
def self.respond_to_missing?
|
@@ -13,21 +13,20 @@ module ExcADG
|
|
13
13
|
include Payload
|
14
14
|
def get
|
15
15
|
lambda { |deps_data|
|
16
|
-
begin
|
17
|
-
f = nil
|
18
16
|
Dir.mktmpdir { |dir|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
Log.debug "temp dir #{} for data is ready"
|
18
|
+
stdout, stderr, status = File.open(File.join(dir, 'data.json'), 'w+') { |f|
|
19
|
+
f.write JSON.generate deps_data
|
20
|
+
f.flush
|
21
|
+
Log.debug "data is in #{f.path}"
|
22
|
+
Open3.capture3({ 'DEPS_DATAFILE' => f.path }, args)
|
23
|
+
}
|
24
|
+
Log.debug "payload process finished"
|
23
25
|
data = { stdout:, stderr:, exitcode: status.exitstatus }
|
24
26
|
raise CommandFailed, data unless status.exitstatus.zero?
|
25
|
-
|
27
|
+
Log.debug "returning data"
|
26
28
|
data
|
27
29
|
}
|
28
|
-
ensure
|
29
|
-
f&.close
|
30
|
-
end
|
31
30
|
}
|
32
31
|
end
|
33
32
|
|
data/lib/excadg/request.rb
CHANGED
@@ -13,11 +13,15 @@ module ExcADG
|
|
13
13
|
@self = Ractor.current
|
14
14
|
end
|
15
15
|
|
16
|
+
def to_s
|
17
|
+
"#{self.class} from #{@self}"
|
18
|
+
end
|
19
|
+
|
16
20
|
# request to get state data
|
17
21
|
class GetStateData < ExcADG::Request
|
18
22
|
attr_reader :deps
|
19
23
|
|
20
|
-
# @param deps Array of VStateData::Key
|
24
|
+
# @param deps {Array} of VStateData::Key
|
21
25
|
def initialize deps: nil
|
22
26
|
super()
|
23
27
|
@deps = deps
|
data/lib/excadg/rtimeout.rb
CHANGED
@@ -11,13 +11,14 @@ module ExcADG
|
|
11
11
|
timed_out = false
|
12
12
|
Thread.report_on_exception = false
|
13
13
|
payload = Thread.new { Thread.current[:result] = block.call }
|
14
|
-
Thread.new {
|
14
|
+
countdown_t = Thread.new {
|
15
15
|
sleep timeout
|
16
16
|
payload.kill
|
17
17
|
timed_out = true
|
18
18
|
}
|
19
19
|
|
20
20
|
payload.join
|
21
|
+
countdown_t.kill
|
21
22
|
timed_out ? raise(TimedOutError) : payload[:result]
|
22
23
|
end
|
23
24
|
module_function :await
|
data/lib/excadg/state_machine.rb
CHANGED
@@ -55,6 +55,7 @@ module ExcADG
|
|
55
55
|
edge = GRAPH.edges.find { |e| e.source == @state && e.target = target }
|
56
56
|
|
57
57
|
with_fault_processing {
|
58
|
+
Log.debug "calling payload for #{edge}"
|
58
59
|
@state_transition_data[target] = @state_edge_bindings[edge].call
|
59
60
|
@state = target
|
60
61
|
Log.debug "moved to #{@state}"
|
@@ -66,7 +67,7 @@ module ExcADG
|
|
66
67
|
def with_fault_processing
|
67
68
|
yield if block_given?
|
68
69
|
rescue StandardError => e
|
69
|
-
Log.error "step failed with #{e} / #{e.backtrace}"
|
70
|
+
Log.error "step failed with '#{e}' / #{e.backtrace}"
|
70
71
|
@state_transition_data[:failed] = e
|
71
72
|
@state = :failed
|
72
73
|
ensure
|
data/lib/excadg/tui/block.rb
CHANGED
@@ -116,11 +116,11 @@ module ExcADG::Tui
|
|
116
116
|
# {#column} and {#row} are enough to make blocks;
|
117
117
|
# in case you need to align a single block, use e.g. `Block.column("one", "two") { |blk| blk.box!.pad! 2 }`
|
118
118
|
def initialize arg
|
119
|
-
case arg
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
119
|
+
@array = case arg
|
120
|
+
when Array then arg
|
121
|
+
when String then [arg]
|
122
|
+
else [arg.to_s]
|
123
|
+
end
|
124
124
|
@width = @array.collect(&:size).max
|
125
125
|
end
|
126
126
|
end
|
data/lib/excadg/tui/format.rb
CHANGED
@@ -9,7 +9,7 @@ module ExcADG::Tui
|
|
9
9
|
module Format
|
10
10
|
# add horizontal padding to the block
|
11
11
|
# @param size number of spaces to add
|
12
|
-
def h_pad! size
|
12
|
+
def h_pad! size = 1
|
13
13
|
@array.collect! { |row|
|
14
14
|
"#{' ' * size}#{row}#{' ' * size}"
|
15
15
|
}
|
@@ -19,7 +19,7 @@ module ExcADG::Tui
|
|
19
19
|
|
20
20
|
# add vertical padding to the block
|
21
21
|
# @param size number of spaces to add
|
22
|
-
def v_pad! size
|
22
|
+
def v_pad! size = 1
|
23
23
|
filler = ' ' * @width
|
24
24
|
size.times {
|
25
25
|
self >> filler
|
@@ -30,7 +30,7 @@ module ExcADG::Tui
|
|
30
30
|
|
31
31
|
# adds spaces around the block
|
32
32
|
# @param size number of spaces to add
|
33
|
-
def pad! size
|
33
|
+
def pad! size = 1
|
34
34
|
v_pad! size
|
35
35
|
h_pad! size
|
36
36
|
end
|
data/lib/excadg/tui.rb
CHANGED
@@ -13,7 +13,7 @@ module ExcADG
|
|
13
13
|
module Tui
|
14
14
|
MAX_VERTEX_TO_SHOW = 10
|
15
15
|
DELAY = 0.2
|
16
|
-
DEFAULT_BOX_SIZE = { height:
|
16
|
+
DEFAULT_BOX_SIZE = { height: 70, width: 200 }.freeze
|
17
17
|
|
18
18
|
@started_at = DateTime.now.strftime('%Q').to_i
|
19
19
|
|
@@ -21,6 +21,7 @@ module ExcADG
|
|
21
21
|
# spawns a thread to show stats to console in background
|
22
22
|
def run
|
23
23
|
Log.info 'spawning tui'
|
24
|
+
Thread.report_on_exception = false
|
24
25
|
@thread = Thread.new {
|
25
26
|
loop {
|
26
27
|
# print_in_box stats
|
@@ -41,22 +42,26 @@ module ExcADG
|
|
41
42
|
# private
|
42
43
|
|
43
44
|
def get_summary has_failed, timed_out
|
44
|
-
[timed_out ? 'execution timed out' : 'execution completed',
|
45
|
-
"#{has_failed ? 'some' : 'no'} vertices failed"]
|
45
|
+
[timed_out ? 'execution timed out' : '🮱 execution completed',
|
46
|
+
"#{has_failed ? '🯀 some' : '🮱 no'} vertices failed"]
|
46
47
|
end
|
47
48
|
|
48
49
|
# make summary paragraph on veritces
|
49
50
|
def stats summary: nil
|
50
51
|
Block.column(
|
51
|
-
Block.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
52
|
+
Block.row(
|
53
|
+
Block.column(summary || '🮚 running').h_pad!(1).v_align!(:center).box!.h_pad!,
|
54
|
+
Block.column(
|
55
|
+
*[
|
56
|
+
"🮚 time spent (ms): #{DateTime.now.strftime('%Q').to_i - @started_at}",
|
57
|
+
"# vertices seen: #{Broker.instance.vtracker.graph.vertices.size}",
|
58
|
+
'🮶 progress:'
|
59
|
+
] + state_stats,
|
60
|
+
align: :left
|
61
|
+
).h_pad!(2)
|
62
|
+
),
|
63
|
+
Block.row('🮲 🮳 pending vertices and their dependencies:').pad!,
|
64
|
+
Block.row(*pending_vertices || 'tracking is n/a', align: :top).h_pad!,
|
60
65
|
align: :left
|
61
66
|
).fit!(width: @content_size[:width], height: @content_size[:height], fill: true)
|
62
67
|
.box!(corners: :sharp)
|
@@ -67,31 +72,38 @@ module ExcADG
|
|
67
72
|
end
|
68
73
|
|
69
74
|
def refresh_sizes
|
70
|
-
box_size = {
|
71
|
-
height: IO.console&.winsize&.first.nil? || DEFAULT_BOX_SIZE[:height] < IO.console.winsize.first ? DEFAULT_BOX_SIZE[:height] : IO.console.winsize.first,
|
72
|
-
width: IO.console&.winsize&.last.nil? || DEFAULT_BOX_SIZE[:width] < IO.console&.winsize&.last ? DEFAULT_BOX_SIZE[:width] : IO.console.winsize.last
|
73
|
-
}.freeze
|
74
75
|
@content_size = {
|
75
|
-
height:
|
76
|
-
width:
|
77
|
-
}
|
78
|
-
@line_template = "│ %-#{@content_size[:width]}s │\n"
|
76
|
+
height: (IO.console&.winsize&.first.nil? || DEFAULT_BOX_SIZE[:height] < IO.console.winsize.first ? DEFAULT_BOX_SIZE[:height] : IO.console.winsize.first) - 4,
|
77
|
+
width: (IO.console&.winsize&.last.nil? || DEFAULT_BOX_SIZE[:width] < IO.console&.winsize&.last ? DEFAULT_BOX_SIZE[:width] : IO.console.winsize.last) - 2
|
78
|
+
}
|
79
79
|
end
|
80
80
|
|
81
81
|
# make states summary, one for a line with consistent placing
|
82
82
|
def state_stats
|
83
83
|
skeleton = StateMachine::GRAPH.vertices.collect { |v| [v, []] }.to_h
|
84
|
-
|
85
|
-
filled = skeleton.merge Broker.data_store.to_a
|
86
|
-
.group_by(&:state)
|
87
|
-
.collect { |state, vertices| [state, vertices_stats(vertices)] }
|
88
|
-
.to_h
|
89
|
-
# rubocop:enable Style/HashTransformValues
|
84
|
+
filled = skeleton.merge(Broker.instance.vtracker.by_state.transform_values { |vertices| vertices.collect(&:name).join(', ') })
|
90
85
|
filled.collect { |k, v| format ' %-10s: %s', k, "#{v.empty? ? '<none>' : v}" }
|
91
86
|
end
|
92
87
|
|
93
|
-
|
94
|
-
|
88
|
+
# gather pending vertices (in :new state) from the tracker
|
89
|
+
# and render dependencies they're waiting for
|
90
|
+
def pending_vertices
|
91
|
+
return nil if Broker.instance.vtracker.nil?
|
92
|
+
if Broker.instance.vtracker.by_state[:new].nil? || Broker.instance.vtracker.by_state[:new].empty?
|
93
|
+
return Block.column('... no pending vertices').h_pad!
|
94
|
+
end
|
95
|
+
|
96
|
+
Broker.instance.vtracker.by_state[:new].sort_by(&:name).collect { |v|
|
97
|
+
deps = Broker.instance.vtracker.get_deps v
|
98
|
+
deps = nil if deps.empty?
|
99
|
+
deps&.sort_by!(&:state)
|
100
|
+
width = 20
|
101
|
+
Block.column(
|
102
|
+
Block.column(v).fit!(width: width - 4).h_pad!.box!,
|
103
|
+
'🮦',
|
104
|
+
Block.column(*deps || 'no deps to wait').fit!(width: width - 4).h_pad!.box!
|
105
|
+
).fit!(width: width, fill: true).h_pad!(2)
|
106
|
+
}
|
95
107
|
end
|
96
108
|
end
|
97
109
|
end
|
data/lib/excadg/vertex.rb
CHANGED
@@ -30,8 +30,14 @@ module ExcADG
|
|
30
30
|
info&.dig(0).to_i || -1
|
31
31
|
end
|
32
32
|
|
33
|
+
# only main ractor could try to get real symbolic vertex name and state
|
34
|
+
# other ractors (e.g. logger) could only use builtin data
|
33
35
|
def to_s
|
34
|
-
|
36
|
+
if Ractor.current == Ractor.main
|
37
|
+
"#{name || number} #{state || status}"
|
38
|
+
else
|
39
|
+
"#{number} #{status}"
|
40
|
+
end
|
35
41
|
end
|
36
42
|
|
37
43
|
# below are shortcut methods to access Vertex data from the main Ractor
|
@@ -39,7 +45,7 @@ module ExcADG
|
|
39
45
|
# obtains current Vertex-es data by lookup in the Broker's data,
|
40
46
|
# available from the main Ractor only
|
41
47
|
def data
|
42
|
-
Broker.data_store[self
|
48
|
+
Broker.instance.data_store&.[](self)
|
43
49
|
end
|
44
50
|
|
45
51
|
# gets current Vertex's state,
|
@@ -88,13 +94,16 @@ module ExcADG
|
|
88
94
|
}
|
89
95
|
state_machine.bind_action(:ready, :done) {
|
90
96
|
function = payload.get
|
97
|
+
Log.debug "payload has arity #{function.arity}"
|
91
98
|
await(timeout: vtimeout.payload) {
|
92
|
-
case function.arity
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
99
|
+
result = case function.arity
|
100
|
+
when 0 then function.call
|
101
|
+
when 1 then function.call state_machine.state_data.data
|
102
|
+
else
|
103
|
+
raise Payload::IncorrectPayloadArity, "unexpected payload arity: #{function.arity}, supported only 0 and 1"
|
104
|
+
end
|
105
|
+
Log.debug 'payload finished'
|
106
|
+
result
|
98
107
|
}
|
99
108
|
}
|
100
109
|
|
data/lib/excadg/vtracker.rb
CHANGED
@@ -4,7 +4,7 @@ require 'rgl/adjacency'
|
|
4
4
|
|
5
5
|
module ExcADG
|
6
6
|
# tracker for {Vertex}-es graph:
|
7
|
-
# it's hooked by {Broker} to register dependencies polling events
|
7
|
+
# it's hooked by {Broker} to register dependencies polling (and other) events
|
8
8
|
# and make an actual graph of vertices with states in runtime
|
9
9
|
#
|
10
10
|
# it's not possible to do this in other way, because vertices can be spawned:
|
@@ -21,22 +21,29 @@ module ExcADG
|
|
21
21
|
# register the vertex and its new known deps in the @graph and by_state cache
|
22
22
|
# @param vertice vertice that requested info about deps
|
23
23
|
# @param deps list of dependencies as supplied by {Request::GetStateData}
|
24
|
-
def track vertex, deps
|
24
|
+
def track vertex, deps = []
|
25
25
|
Assertions.is_a? vertex, Vertex
|
26
26
|
Assertions.is_a? deps, Array
|
27
27
|
|
28
|
+
@graph.add_vertex vertex
|
28
29
|
add_to_states_cache vertex, vertex.state
|
29
30
|
|
30
31
|
deps.each { |raw_dep|
|
31
|
-
# it could be not Vertex, so do a lookup through data store
|
32
|
-
next unless Broker.data_store[raw_dep]
|
32
|
+
# it could be not a Vertex, so do a lookup through data store
|
33
|
+
next unless Broker.instance.data_store[raw_dep]
|
33
34
|
|
34
|
-
dep_data = Broker.data_store[raw_dep]
|
35
|
+
dep_data = Broker.instance.data_store[raw_dep]
|
35
36
|
add_to_states_cache dep_data.vertex, dep_data.state
|
36
37
|
@graph.add_edge vertex, dep_data.vertex
|
37
38
|
}
|
38
39
|
end
|
39
40
|
|
41
|
+
# get all vertex's dependencies
|
42
|
+
# @param vertex {Vertex} vertex to lookup known dependencies for
|
43
|
+
def get_deps vertex
|
44
|
+
@graph.adjacent_vertices vertex
|
45
|
+
end
|
46
|
+
|
40
47
|
private
|
41
48
|
|
42
49
|
# adds a given {Vertex} in state to @by_state cache;
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: excadg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- skorobogatydmitry
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rgl
|