excadg 0.1.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 +7 -0
- data/lib/excadg/assertions.rb +22 -0
- data/lib/excadg/broker.rb +112 -0
- data/lib/excadg/data_store.rb +80 -0
- data/lib/excadg/dependency_manager.rb +67 -0
- data/lib/excadg/log.rb +61 -0
- data/lib/excadg/payload/example.rb +102 -0
- data/lib/excadg/payload/wrapper.rb +65 -0
- data/lib/excadg/payload.rb +37 -0
- data/lib/excadg/request.rb +55 -0
- data/lib/excadg/rtimeout.rb +26 -0
- data/lib/excadg/state_machine.rb +95 -0
- data/lib/excadg/tui.rb +103 -0
- data/lib/excadg/vertex.rb +100 -0
- data/lib/excadg/vstate_data.rb +92 -0
- data/lib/excadg.rb +17 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e1d50ffd84523c400ae4733e67c53d54a791cb066cec4a49dea27b4e7072ecda
|
4
|
+
data.tar.gz: 4b5bf03ce6c2e9190e5af55a9f740247281da6ed6522c37a7ee3dd3698352dc9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 274e5395a4d9ed5da1c5aba9a368638757f1571ac38aede22b0f729a794813c4611e41dfb901a9d555c0a202d7b494b47e077b9c379383a7cb116c2f1dd32ecd
|
7
|
+
data.tar.gz: e35cd2c39d26791742b1c9ff1255f3c2edb9247b046a09dad62013c6bd9e48fdae54be73e17ae4e1fb430a385a2ec8bc19cfba63a54deffb21b27d59a196da50
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExcADG
|
4
|
+
# collection of simple assertions
|
5
|
+
module Assertions
|
6
|
+
class << self
|
7
|
+
# asserts that all vars are instances of one of the clss
|
8
|
+
def is_a? vars, clss
|
9
|
+
return if vars.is_a?(Array) && clss == Array
|
10
|
+
|
11
|
+
clss = [clss] unless clss.is_a? Array
|
12
|
+
vars = [vars] unless vars.is_a? Array
|
13
|
+
wrong_vars = vars.reject { |var|
|
14
|
+
clss.any? { |cls|
|
15
|
+
var.is_a? cls
|
16
|
+
}
|
17
|
+
}
|
18
|
+
raise "vars #{wrong_vars} not of classes #{clss}" unless wrong_vars.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
require_relative 'data_store'
|
6
|
+
require_relative 'log'
|
7
|
+
require_relative 'request'
|
8
|
+
require_relative 'vertex'
|
9
|
+
|
10
|
+
module ExcADG
|
11
|
+
# handle requests sending/receiving though Ractor's interface
|
12
|
+
module Broker
|
13
|
+
class << self
|
14
|
+
attr_reader :data_store
|
15
|
+
|
16
|
+
# is used from vertices to send reaqests to the broker
|
17
|
+
# @return data received in response from the main ractor
|
18
|
+
# @raise StandardError if response is a StandardError
|
19
|
+
# @raise CantSendRequest if sending request failed for any reason
|
20
|
+
def ask request
|
21
|
+
raise UnknownRequestType, request unless request.is_a? Request
|
22
|
+
|
23
|
+
begin
|
24
|
+
Ractor.main.send request
|
25
|
+
rescue StandardError => e
|
26
|
+
raise CantSendRequest, cause: e
|
27
|
+
end
|
28
|
+
|
29
|
+
Log.info 'getting response'
|
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
|
36
|
+
end
|
37
|
+
|
38
|
+
# start requests broker for vertices in a separated thread
|
39
|
+
# @return the thread started
|
40
|
+
def run
|
41
|
+
@data_store ||= DataStore.new
|
42
|
+
@broker = Thread.new { loop { process_request } } unless @broker&.alive?
|
43
|
+
|
44
|
+
at_exit {
|
45
|
+
Log.info 'shut down broker'
|
46
|
+
@broker.kill
|
47
|
+
}
|
48
|
+
|
49
|
+
Log.info 'broker is started'
|
50
|
+
@broker
|
51
|
+
end
|
52
|
+
|
53
|
+
# makes a thread to wait for all known vertices to reach a final state
|
54
|
+
# @param timeout total waiting timeout in seconds, nil means wait forever
|
55
|
+
# @param period time between vertices state check
|
56
|
+
def wait_all timeout: 60, period: 1
|
57
|
+
Thread.new {
|
58
|
+
Log.info "timeout is #{timeout} seconds"
|
59
|
+
Timeout.timeout(timeout) {
|
60
|
+
loop {
|
61
|
+
sleep period
|
62
|
+
states = @data_store.to_a.group_by(&:state).keys
|
63
|
+
Log.info "vertices in #{states} exist"
|
64
|
+
# that's the only final states for vertices
|
65
|
+
break if (states - %i[done failed]).empty?
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# waits for an incoming request,
|
74
|
+
# validates request type and content
|
75
|
+
# then makes and sends an answer
|
76
|
+
def process_request
|
77
|
+
begin
|
78
|
+
request = Ractor.receive
|
79
|
+
Log.info "received request: #{request}"
|
80
|
+
request.self.send case request
|
81
|
+
when Request::GetStateData
|
82
|
+
request.filter? ? request.deps.collect { |d| @data_store[d] } : @data_store.to_a
|
83
|
+
when Request::Update
|
84
|
+
@data_store << request.data
|
85
|
+
true
|
86
|
+
when Request::AddVertex
|
87
|
+
Vertex.new payload: request.payload, deps: [request.self]
|
88
|
+
else
|
89
|
+
raise UnknownRequestType
|
90
|
+
end
|
91
|
+
rescue StandardError => e
|
92
|
+
Log.warn "error on message processing: #{e.class} / #{e.message} / #{e.backtrace}"
|
93
|
+
request.self.send RequestProcessingFailed.new cause: e
|
94
|
+
end
|
95
|
+
Log.debug 'message processed'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class UnknownRequestType < StandardError; end
|
100
|
+
class CantSendRequest < StandardError; end
|
101
|
+
|
102
|
+
# error type returned by broker thread in case it failed to process incoming request
|
103
|
+
class RequestProcessingFailed < StandardError
|
104
|
+
attr_reader :cause
|
105
|
+
|
106
|
+
def initialize cause:
|
107
|
+
super
|
108
|
+
@cause = cause
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'assertions'
|
4
|
+
require_relative 'vstate_data'
|
5
|
+
|
6
|
+
module ExcADG
|
7
|
+
# collection of {ExcADG::VStateData} for {ExcADG::Broker}
|
8
|
+
class DataStore
|
9
|
+
attr_reader :size
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
# two hashes to store VStateData and access them fast by either key
|
13
|
+
@by_name = {}
|
14
|
+
@by_vertex = {}
|
15
|
+
@size = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# add new element to the store
|
19
|
+
# adds it to the two hashes to access lated by either attribute
|
20
|
+
# @return current number of elements
|
21
|
+
def << new
|
22
|
+
Assertions.is_a? new, VStateData::Full
|
23
|
+
if (by_name = @by_name[new.name]) && !(by_name.vertex.eql? new.vertex)
|
24
|
+
raise DataSkew,
|
25
|
+
"Vertex named #{new.name} - #{new.vertex} is recorded as #{by_name.vertex} in state"
|
26
|
+
end
|
27
|
+
if (by_vertex = @by_vertex[new.vertex]) && !(by_vertex.name.eql? new.name)
|
28
|
+
raise DataSkew,
|
29
|
+
"Vertex #{new.vertex} named #{new.name} is already named #{by_vertex.name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
@size += 1 unless key? new
|
33
|
+
|
34
|
+
@by_name[new.name] = new if new.name
|
35
|
+
@by_vertex[new.vertex] = new if new.vertex
|
36
|
+
end
|
37
|
+
|
38
|
+
# retrieves VStateData by key
|
39
|
+
# @param key either Vertex or VStateData::Key to retrieve Full state data
|
40
|
+
# @return VStateData::Full for the respective key
|
41
|
+
# @raise StandardError if key is not of a supported type
|
42
|
+
def [] key
|
43
|
+
Assertions.is_a? key, [Vertex, VStateData::Key, Symbol, String]
|
44
|
+
|
45
|
+
case key
|
46
|
+
when Vertex then @by_vertex[key]
|
47
|
+
when Symbol, String then @by_name[key]
|
48
|
+
when VStateData::Key
|
49
|
+
if key.name && @by_name.key?(key.name)
|
50
|
+
@by_name[key.name]
|
51
|
+
elsif key.vertex && @by_vertex.key?(key.vertex)
|
52
|
+
@by_vertex[key.vertex]
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# retrieve all vertices state data,
|
60
|
+
# could contain huge amount of data and be slow to work with;
|
61
|
+
# prefer to access vertices data by name using [] instead
|
62
|
+
def to_a
|
63
|
+
(@by_name.values + @by_vertex.values).uniq
|
64
|
+
end
|
65
|
+
|
66
|
+
# checks if there is data for a given key
|
67
|
+
def key? key
|
68
|
+
case key
|
69
|
+
when Vertex then @by_vertex.key? key
|
70
|
+
when Symbol, String then @by_name.key? key
|
71
|
+
when VStateData::Key
|
72
|
+
@by_name.key?(key.name) || @by_vertex.key?(key.vertex)
|
73
|
+
else
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class DataSkew < StandardError; end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal:true
|
2
|
+
|
3
|
+
require_relative 'assertions'
|
4
|
+
require_relative 'log'
|
5
|
+
require_relative 'vstate_data'
|
6
|
+
|
7
|
+
module ExcADG
|
8
|
+
# manage list of dependencies
|
9
|
+
# dependencies could be of 2 types:
|
10
|
+
# - vertex - see vertex.rb
|
11
|
+
# - symbol - :name attribute of the vertex
|
12
|
+
class DependencyManager
|
13
|
+
attr_reader :deps, :data
|
14
|
+
|
15
|
+
# @param deps list of symbols or vertices to watch for as dependencies
|
16
|
+
def initialize deps:
|
17
|
+
Assertions.is_a? deps, Array
|
18
|
+
Assertions.is_a? deps, [Vertex, Symbol]
|
19
|
+
|
20
|
+
@deps = deps.collect { |raw_dep|
|
21
|
+
case raw_dep
|
22
|
+
when Symbol then VStateData::Key.new name: raw_dep
|
23
|
+
when Vertex then VStateData::Key.new vertex: raw_dep
|
24
|
+
end
|
25
|
+
}
|
26
|
+
@data = []
|
27
|
+
end
|
28
|
+
|
29
|
+
# deduct (update) dependencies with new data
|
30
|
+
# - counts :done dependencies
|
31
|
+
# - preserves :done deps data
|
32
|
+
# @param new_data - Array of VStateData retrieved from the broker
|
33
|
+
def deduct_deps new_data
|
34
|
+
data = filter_foreign new_data
|
35
|
+
Log.debug "received deps: #{data}"
|
36
|
+
|
37
|
+
assert_failed data
|
38
|
+
|
39
|
+
done_deps = data.select(&:done?)
|
40
|
+
@data += done_deps
|
41
|
+
Log.debug "done deps: #{done_deps}"
|
42
|
+
|
43
|
+
@deps.reject! { |dep| done_deps.include? dep }
|
44
|
+
Log.info "deps left: #{@deps.collect(&:to_s)}}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# filters out deps doesn't belong to the manager
|
50
|
+
def filter_foreign new_data
|
51
|
+
if (new_data - @deps).empty?
|
52
|
+
new_data
|
53
|
+
else
|
54
|
+
Log.warn 'non-deps state received, filtering'
|
55
|
+
Log.debug "non-dep states: #{new_data - @deps}"
|
56
|
+
new_data.filter { |state_data| deps.include? state_data }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# checks if any dependencies in the received data are failed
|
61
|
+
def assert_failed data
|
62
|
+
failed_deps = data.select(&:failed?).collect(&:to_s)
|
63
|
+
Log.debug "failed deps: #{failed_deps}"
|
64
|
+
raise "some deps failed: #{failed_deps}" unless failed_deps.empty?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/excadg/log.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
require_relative 'assertions'
|
6
|
+
|
7
|
+
module ExcADG
|
8
|
+
# logging support
|
9
|
+
module Log
|
10
|
+
# ractor-based logger
|
11
|
+
# this ractor logger receives messages from other ractors and log them
|
12
|
+
# it can be useful in case logging shouldn't interrupt other thread
|
13
|
+
# @param dest - what to write logs to, $stdout by default, gets interpret as filename unless IO
|
14
|
+
# @param level - one of the Logger's log levels
|
15
|
+
class RLogger < Ractor
|
16
|
+
def self.new dest: $stdout, level: Logger::INFO
|
17
|
+
super(dest, level) { |dest, level|
|
18
|
+
File.open(dest, 'w+', &:write) unless dest.is_a? IO
|
19
|
+
l ||= Logger.new dest
|
20
|
+
l.level = level
|
21
|
+
l.formatter = proc { |severity, datetime, progname, msg|
|
22
|
+
format('%20s | %4s | %-10s || %s', datetime, severity, progname, msg)
|
23
|
+
}
|
24
|
+
while log = Ractor.receive
|
25
|
+
# Expect 3 args - severity, proc name and message
|
26
|
+
l.public_send(log.first, log[1], &-> { log.last.to_s + "\n" })
|
27
|
+
end
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.method_missing(method, *args, &_block)
|
33
|
+
return if @muted
|
34
|
+
|
35
|
+
r = Ractor.current
|
36
|
+
@main.send [method, r&.to_s || r.object_id, *args]
|
37
|
+
rescue Ractor::ClosedError => e
|
38
|
+
# last hope - there is tty
|
39
|
+
puts "can't send message to logging ractor: #{e}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.respond_to_missing?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
# default logger e.g. for tests
|
47
|
+
@main = RLogger.new
|
48
|
+
@muted = false
|
49
|
+
|
50
|
+
# replaces default logger with a custom logger
|
51
|
+
def self.logger new_logger
|
52
|
+
Assertions.is_a? new_logger, RLogger
|
53
|
+
@main = new_logger
|
54
|
+
end
|
55
|
+
|
56
|
+
# mute logging by ignoring all incoming log requests
|
57
|
+
def self.mute
|
58
|
+
@muted = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../broker'
|
4
|
+
require_relative '../request'
|
5
|
+
|
6
|
+
module ExcADG
|
7
|
+
# payload examples to demonstrate framework capabilities
|
8
|
+
# most of the payloads in the file are used in tests
|
9
|
+
module Payload::Example
|
10
|
+
# minimalistic payload that does nothing
|
11
|
+
# but echoes its args or :ping by default
|
12
|
+
class Echo
|
13
|
+
include Payload
|
14
|
+
def get
|
15
|
+
-> { @args }
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize args
|
19
|
+
args.nil? ? :ping : args
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# dependencies data processing example
|
24
|
+
# deps_data is an Array with a VStateData objects for each dependency
|
25
|
+
# the example just checks that all deps returned :ping, fails otherwise
|
26
|
+
class Receiver
|
27
|
+
include Payload
|
28
|
+
def get
|
29
|
+
lambda { |deps_data|
|
30
|
+
deps_data.collect { |d|
|
31
|
+
raise 'incorrect data received from dependencies' unless d.data.eql? :ping
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# payload that fails
|
38
|
+
class Faulty
|
39
|
+
include Payload
|
40
|
+
|
41
|
+
def get
|
42
|
+
-> { raise @args }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# payload that sleeps @args or 1 second(s)
|
47
|
+
class Sleepy
|
48
|
+
include Payload
|
49
|
+
def get
|
50
|
+
-> { sleep @args }
|
51
|
+
end
|
52
|
+
|
53
|
+
def sanitize args
|
54
|
+
args.is_a?(Integer) ? args : 0.1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# calculate Nth member of the function
|
59
|
+
# y = x(n) * (x(n-1)) where x(0) = 0 and x(1) = 1
|
60
|
+
# it illustrates how payload can be customized with @args
|
61
|
+
# and extended by adding methods
|
62
|
+
class Multiplier
|
63
|
+
include Payload
|
64
|
+
def get
|
65
|
+
-> { get_el x: @args }
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_el x:
|
69
|
+
case x
|
70
|
+
when 0 then 0
|
71
|
+
when 1 then 1
|
72
|
+
else
|
73
|
+
x * get_el(x: x - 1)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# payload that does something on condition received from its deps
|
79
|
+
# see spec/vertex_spec.rb for full example
|
80
|
+
class Condition
|
81
|
+
include Payload
|
82
|
+
def get
|
83
|
+
lambda { |deps_data|
|
84
|
+
ExcADG::Broker.ask ExcADG::Request::AddVertex.new(payload: Echo.new) if deps_data.all? { |d| d.data.eql? :trigger }
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# payload that implements an idiomatic loop by
|
90
|
+
# making N vertices - one for each of its dependencies
|
91
|
+
class Loop
|
92
|
+
include Payload
|
93
|
+
def get
|
94
|
+
lambda { |deps_data|
|
95
|
+
deps_data.first.data.collect { |e|
|
96
|
+
ExcADG::Broker.ask ExcADG::Request::AddVertex.new payload: Echo.new(args: e)
|
97
|
+
}
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
module ExcADG
|
7
|
+
# Payloads that wraps other programs execution
|
8
|
+
module Payload::Wrapper
|
9
|
+
# runs a binary
|
10
|
+
# save its stdout, stderr and exit code to state
|
11
|
+
# provides path to temp file with dependencies data JSON in a DEPS_DATAFILE env variable
|
12
|
+
class Bin
|
13
|
+
include Payload
|
14
|
+
def get
|
15
|
+
lambda { |deps_data|
|
16
|
+
begin
|
17
|
+
f = nil
|
18
|
+
Dir.mktmpdir { |dir|
|
19
|
+
f = File.new File.join(dir, 'data.json'), 'w+'
|
20
|
+
f.write JSON.generate deps_data
|
21
|
+
f.flush
|
22
|
+
stdout, stderr, status = Open3.capture3({ 'DEPS_DATAFILE' => f.path }, args)
|
23
|
+
data = { stdout:, stderr:, exitcode: status.exitstatus }
|
24
|
+
raise CommandFailed, data unless status.exitstatus.zero?
|
25
|
+
|
26
|
+
data
|
27
|
+
}
|
28
|
+
ensure
|
29
|
+
f&.close
|
30
|
+
end
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def sanitize args
|
35
|
+
raise "arguments should be a String, got #{args}" unless args.is_a? String
|
36
|
+
|
37
|
+
args
|
38
|
+
end
|
39
|
+
|
40
|
+
# exception with command execution result
|
41
|
+
# for cases when the command fails
|
42
|
+
class CommandFailed < StandardError
|
43
|
+
# @param data same data as what would be returned by a successful run
|
44
|
+
def initialize data
|
45
|
+
super 'command failed'
|
46
|
+
@data = data
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_json(*args)
|
50
|
+
@data.to_json(*args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# runs a ruby script
|
56
|
+
# behaves same as the Bin reg parameters and return data
|
57
|
+
class Ruby < Bin
|
58
|
+
def sanitize args
|
59
|
+
raise "arguments should be a String, got #{args}" unless args.is_a? String
|
60
|
+
|
61
|
+
"ruby #{args}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExcADG
|
4
|
+
# module to base payload for {ExcADG::Vertex} -es on it,
|
5
|
+
# reason for having this special module to provider simple labmdas
|
6
|
+
# is that Ractor (which is a base for {ExcADG::Vertex})
|
7
|
+
# require its parameters scope to be shareable
|
8
|
+
module Payload
|
9
|
+
attr_accessor :args
|
10
|
+
|
11
|
+
# main method of the payload that holds code to be executed within vertex,
|
12
|
+
# vertex takes care of error processing - there is no need to mask exceptions,
|
13
|
+
# this method should return a Proc, that:
|
14
|
+
# * could receive up to 1 arguments
|
15
|
+
# * 1st argument, if specified, is an {Array} of {ExcADG::VStateData} from the vertex dependencies
|
16
|
+
# * could access @args of the obejct, which was set on object's constructing
|
17
|
+
# @return {Proc}
|
18
|
+
# @raise {ExcADG::Payload::NoPayloadSet} by default to fail vertices with partially-implemented payload
|
19
|
+
def get
|
20
|
+
raise NoPayloadSet, 'payload is empty'
|
21
|
+
end
|
22
|
+
|
23
|
+
# constructor to store arguments for the lambda in the object
|
24
|
+
#
|
25
|
+
# implementation implies that child class could implement {#sanitize}
|
26
|
+
# to transform args as needed
|
27
|
+
def initialize args: nil
|
28
|
+
@args = respond_to?(:sanitize) ? send(:sanitize, args) : args
|
29
|
+
end
|
30
|
+
|
31
|
+
class NoPayloadSet < StandardError; end
|
32
|
+
class IncorrectPayloadArity < StandardError; end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require_relative 'payload/example'
|
37
|
+
require_relative 'payload/wrapper'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'assertions'
|
4
|
+
require_relative 'payload'
|
5
|
+
require_relative 'vstate_data'
|
6
|
+
|
7
|
+
module ExcADG
|
8
|
+
# base class for messages between Ractors
|
9
|
+
class Request
|
10
|
+
attr_reader :self
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@self = Ractor.current
|
14
|
+
end
|
15
|
+
|
16
|
+
# request to get state data
|
17
|
+
class GetStateData < ExcADG::Request
|
18
|
+
attr_reader :deps
|
19
|
+
|
20
|
+
# @param deps Array of VStateData::Key
|
21
|
+
def initialize deps: nil
|
22
|
+
super()
|
23
|
+
@deps = deps
|
24
|
+
end
|
25
|
+
|
26
|
+
def filter?
|
27
|
+
!@deps.nil?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# request to update self state in the central storage
|
32
|
+
class Update < ExcADG::Request
|
33
|
+
attr_reader :data
|
34
|
+
|
35
|
+
def initialize data:
|
36
|
+
super()
|
37
|
+
Assertions.is_a? data, VStateData::Full
|
38
|
+
|
39
|
+
@data = data
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# request to make and start a new vertex
|
44
|
+
class AddVertex < ExcADG::Request
|
45
|
+
attr_reader :payload
|
46
|
+
|
47
|
+
def initialize payload:
|
48
|
+
super()
|
49
|
+
raise "Incorrent payload type: #{payload.class}" unless payload.is_a? Payload
|
50
|
+
|
51
|
+
@payload = payload
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExcADG
|
4
|
+
# simple ractor-safe timeout implementation
|
5
|
+
# @param timeout timeout in seconds
|
6
|
+
# @param block payload to run with timeout
|
7
|
+
module RTimeout
|
8
|
+
def await timeout: nil, &block
|
9
|
+
return block.call if timeout.nil? || timeout.zero?
|
10
|
+
|
11
|
+
timed_out = false
|
12
|
+
payload = Thread.new { Thread.current[:result] = block.call }
|
13
|
+
Thread.new {
|
14
|
+
sleep timeout
|
15
|
+
payload.kill
|
16
|
+
timed_out = true
|
17
|
+
}
|
18
|
+
|
19
|
+
payload.join
|
20
|
+
timed_out ? raise(TimedOutError) : payload[:result]
|
21
|
+
end
|
22
|
+
module_function :await
|
23
|
+
|
24
|
+
class TimedOutError < StandardError; end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rgl/adjacency'
|
4
|
+
require 'rgl/base'
|
5
|
+
|
6
|
+
require_relative 'broker'
|
7
|
+
require_relative 'log'
|
8
|
+
require_relative 'request'
|
9
|
+
require_relative 'vstate_data'
|
10
|
+
|
11
|
+
module ExcADG
|
12
|
+
# carry states and transitions for individual vertices
|
13
|
+
class StateMachine
|
14
|
+
# sets in stone possible state transitions
|
15
|
+
GRAPH = RGL::DirectedAdjacencyGraph.new
|
16
|
+
GRAPH.add_edge :new, :ready
|
17
|
+
GRAPH.add_edge :ready, :done
|
18
|
+
# any transition could end-up in a failed state
|
19
|
+
GRAPH.add_vertex :failed
|
20
|
+
Ractor.make_shareable GRAPH
|
21
|
+
|
22
|
+
# add states graph to the current object
|
23
|
+
def initialize name:
|
24
|
+
@state = :new
|
25
|
+
@state_edge_bindings = {}
|
26
|
+
@state_transition_data = {}
|
27
|
+
|
28
|
+
@name = name
|
29
|
+
end
|
30
|
+
|
31
|
+
# bind action to one of the state graph's edges
|
32
|
+
def bind_action source, target, &block
|
33
|
+
[source, target].each { |state|
|
34
|
+
raise WrongState, "unknown state #{state}" unless GRAPH.has_vertex? state
|
35
|
+
}
|
36
|
+
raise WrongTransition.new source, target unless GRAPH.has_edge? source, target
|
37
|
+
|
38
|
+
edge = GRAPH.edges.find { |e| e.source == source && e.target = target }
|
39
|
+
@state_edge_bindings[edge] = block
|
40
|
+
end
|
41
|
+
|
42
|
+
# transition to next state
|
43
|
+
# @return: state data (result) / nil if it's a final step
|
44
|
+
def step
|
45
|
+
Log.debug 'taking another step'
|
46
|
+
assert_state_transition_bounds
|
47
|
+
|
48
|
+
target_candidates = GRAPH.each_adjacent @state
|
49
|
+
Log.debug "possible candidates: #{target_candidates.size}"
|
50
|
+
return nil if target_candidates.none?
|
51
|
+
raise WrongState, "state #{@state} has more than one adjacent states" unless target_candidates.one?
|
52
|
+
|
53
|
+
target = target_candidates.first
|
54
|
+
Log.debug "found a candidate: #{target}"
|
55
|
+
edge = GRAPH.edges.find { |e| e.source == @state && e.target = target }
|
56
|
+
begin
|
57
|
+
@state_transition_data[target] = @state_edge_bindings[edge].call
|
58
|
+
@state = target
|
59
|
+
Log.debug "moved to #{@state}"
|
60
|
+
rescue StandardError => e
|
61
|
+
Log.error "step failed with #{e} / #{e.backtrace}"
|
62
|
+
@state_transition_data[:failed] = e
|
63
|
+
@state = :failed
|
64
|
+
ensure
|
65
|
+
begin
|
66
|
+
Broker.ask Request::Update.new data: state_data
|
67
|
+
rescue StandardError => e
|
68
|
+
@state_transition_data[:failed] = e
|
69
|
+
@state = :failed
|
70
|
+
Broker.ask Request::Update.new data: state_data
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@state_transition_data[@state]
|
74
|
+
end
|
75
|
+
|
76
|
+
def assert_state_transition_bounds
|
77
|
+
raise NotAllTransitionsBound, GRAPH.edges - @state_edge_bindings.keys unless GRAPH.edges.eql? @state_edge_bindings.keys
|
78
|
+
end
|
79
|
+
|
80
|
+
# makes state data for the current vertex
|
81
|
+
def state_data
|
82
|
+
VStateData::Full.new name: @name, data: @state_transition_data[@state], state: @state
|
83
|
+
end
|
84
|
+
|
85
|
+
class WrongState < StandardError; end
|
86
|
+
|
87
|
+
class WrongTransition < StandardError
|
88
|
+
def initialize src, dest
|
89
|
+
super("Transition #{src} -> #{dest} is not available.")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class NotAllTransitionsBound < StandardError; end
|
94
|
+
end
|
95
|
+
end
|
data/lib/excadg/tui.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'io/console'
|
5
|
+
|
6
|
+
require_relative 'broker'
|
7
|
+
require_relative 'state_machine'
|
8
|
+
|
9
|
+
module ExcADG
|
10
|
+
# render status on the screen
|
11
|
+
module Tui
|
12
|
+
MAX_VERTEX_TO_SHOW = 10
|
13
|
+
DELAY = 0.2
|
14
|
+
DEFAULT_BOX_SIZE = { height: 50, width: 150 }.freeze
|
15
|
+
# TODO: do runtime calc
|
16
|
+
BOX_SIZE = {
|
17
|
+
height: DEFAULT_BOX_SIZE[:height] > (IO.console&.winsize&.first || 1000) ? IO.console.winsize.first : DEFAULT_BOX_SIZE[:height],
|
18
|
+
width: DEFAULT_BOX_SIZE[:width] > (IO.console&.winsize&.last || 1000) ? IO.console.winsize.last : DEFAULT_BOX_SIZE[:width]
|
19
|
+
}.freeze
|
20
|
+
CONTENT_SIZE = {
|
21
|
+
height: BOX_SIZE[:height] - 4, # 2 for borders, 1 for \n, 1 for remark
|
22
|
+
width: BOX_SIZE[:width] - 5 # 2 for borders, 2 to indent
|
23
|
+
}.freeze
|
24
|
+
LINE_TEMPLATE = "| %-#{CONTENT_SIZE[:width]}s |\n".freeze
|
25
|
+
|
26
|
+
@started_at = DateTime.now.strftime('%Q').to_i
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# spawns a thread to show stats to console in background
|
30
|
+
def run
|
31
|
+
Log.info 'spawning tui'
|
32
|
+
@thread = Thread.new {
|
33
|
+
loop {
|
34
|
+
print_in_box stats
|
35
|
+
sleep DELAY
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def summarize has_failed, timed_out
|
41
|
+
@thread.kill
|
42
|
+
print_in_box stats + (print_summary has_failed, timed_out)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# @param content is a list of lines to print
|
48
|
+
def print_in_box content
|
49
|
+
clear
|
50
|
+
print "+-#{'-' * CONTENT_SIZE[:width]}-+\n"
|
51
|
+
content[..CONTENT_SIZE[:height]].each { |line|
|
52
|
+
if line.size > CONTENT_SIZE[:width]
|
53
|
+
printf LINE_TEMPLATE, "#{line[...(CONTENT_SIZE[:width] - 3)]}..."
|
54
|
+
else
|
55
|
+
printf LINE_TEMPLATE, line
|
56
|
+
end
|
57
|
+
}
|
58
|
+
if content.size < CONTENT_SIZE[:height]
|
59
|
+
(CONTENT_SIZE[:height] - content.size).times { printf LINE_TEMPLATE, ' ' }
|
60
|
+
else
|
61
|
+
printf LINE_TEMPLATE, '<some content did not fit and was cropped>'[..CONTENT_SIZE[:width]]
|
62
|
+
end
|
63
|
+
print "+-#{'-' * CONTENT_SIZE[:width]}-+\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_summary has_failed, timed_out
|
67
|
+
[timed_out ? 'execution timed out' : 'execution completed',
|
68
|
+
"#{has_failed ? 'some' : 'no'} vertices failed"]
|
69
|
+
end
|
70
|
+
|
71
|
+
# make summary paragraph on veritces
|
72
|
+
def stats
|
73
|
+
[
|
74
|
+
"time spent (ms): #{DateTime.now.strftime('%Q').to_i - @started_at}",
|
75
|
+
"vertices seen: #{Broker.data_store.size}",
|
76
|
+
'progress:'
|
77
|
+
] + state_stats.collect { |line| " #{line}" }
|
78
|
+
end
|
79
|
+
|
80
|
+
def clear
|
81
|
+
print "\e[2J\e[f"
|
82
|
+
end
|
83
|
+
|
84
|
+
# make states summary, one for a line with consistent placing
|
85
|
+
def state_stats
|
86
|
+
skeleton = StateMachine::GRAPH.vertices.collect { |v| [v, []] }.to_h
|
87
|
+
# rubocop:disable Style/HashTransformValues
|
88
|
+
filled = skeleton.merge Broker.data_store.to_a
|
89
|
+
.group_by(&:state)
|
90
|
+
.collect { |state, vertices| [state, vertices_stats(vertices)] }
|
91
|
+
.to_h
|
92
|
+
# rubocop:enable Style/HashTransformValues
|
93
|
+
filled.collect { |k, v| format '%-10s: %s', k, "#{v.empty? ? '<none>' : v}" }
|
94
|
+
end
|
95
|
+
|
96
|
+
def vertices_stats vertice_pairs
|
97
|
+
full_list = vertice_pairs.collect(&:name)
|
98
|
+
addition = full_list.size > MAX_VERTEX_TO_SHOW ? "... and #{full_list.size - MAX_VERTEX_TO_SHOW} more" : ''
|
99
|
+
full_list[0...MAX_VERTEX_TO_SHOW].join(', ') + addition
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'broker'
|
4
|
+
require_relative 'dependency_manager'
|
5
|
+
require_relative 'log'
|
6
|
+
require_relative 'payload'
|
7
|
+
require_relative 'request'
|
8
|
+
require_relative 'rtimeout'
|
9
|
+
require_relative 'state_machine'
|
10
|
+
|
11
|
+
module ExcADG
|
12
|
+
# Individual vertex of the execution graph to run in a separated Ractor
|
13
|
+
class Vertex < Ractor
|
14
|
+
include RTimeout
|
15
|
+
|
16
|
+
# @return parsed info about the Ractor: number, file, line in file, status
|
17
|
+
def info
|
18
|
+
inspect.scan(/^#<Ractor:#(\d+)\s(.*):(\d+)\s(\w+)>$/).first
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ractor's internal status
|
22
|
+
# @return Symbol, :unknown if parsing failed
|
23
|
+
def status
|
24
|
+
(info&.dig(3) || :unknwon).to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return Ractor's number, -1 if parsing failed
|
28
|
+
def number
|
29
|
+
info&.dig(0).to_i || -1
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{number} #{status}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# below are shortcut methods to access Vertex data from the main Ractor
|
37
|
+
|
38
|
+
# obtains current Vertex-es data by lookup in the Broker's data,
|
39
|
+
# available from the main Ractor only
|
40
|
+
def data
|
41
|
+
Broker.data_store[self]
|
42
|
+
end
|
43
|
+
|
44
|
+
# gets current Vertex's state,
|
45
|
+
# available from the main Ractor only
|
46
|
+
def state
|
47
|
+
data&.state
|
48
|
+
end
|
49
|
+
|
50
|
+
# gets current Vertex's name,
|
51
|
+
# available from the main Ractor only
|
52
|
+
def name
|
53
|
+
data&.name
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
# make a vertex, it runs automagically
|
58
|
+
# @param payload Payload object to run in this Vertex
|
59
|
+
# @param name optional vertex name to identify vertex
|
60
|
+
# @param deps list of other Vertices or names to wait for
|
61
|
+
# @param timeout total time in seconds for the payload to run
|
62
|
+
# @raise Payload::IncorrectPayloadArity in case payload returns function with arity > 1
|
63
|
+
# @raise Payload::NoPayloadSet in case payload provided has incorrect type
|
64
|
+
def new payload:, name: nil, deps: [], timeout: nil
|
65
|
+
raise Payload::NoPayloadSet, "expected payload, got #{payload.class}" unless payload.is_a? Payload
|
66
|
+
|
67
|
+
raise Payload::IncorrectPayloadArity, "arity is #{payload.get.arity}, supported only 0 and 1" unless [0, 1].include? payload.get.arity
|
68
|
+
|
69
|
+
super(payload, name, timeout, DependencyManager.new(deps:)) { |payload, name, timeout, deps_manager|
|
70
|
+
state_machine = StateMachine.new(name: name || "v#{number}".to_sym)
|
71
|
+
Broker.ask Request::Update.new data: state_machine.state_data
|
72
|
+
Log.info 'building vertex lifecycle'
|
73
|
+
state_machine.bind_action(:new, :ready) {
|
74
|
+
until deps_manager.deps.empty?
|
75
|
+
deps_data = Broker.ask Request::GetStateData.new(deps: deps_manager.deps)
|
76
|
+
deps_manager.deduct_deps deps_data
|
77
|
+
sleep 0.2
|
78
|
+
end
|
79
|
+
deps_manager.data
|
80
|
+
}
|
81
|
+
state_machine.bind_action(:ready, :done) {
|
82
|
+
function = payload.get
|
83
|
+
await(timeout:) {
|
84
|
+
case function.arity
|
85
|
+
when 0 then function.call
|
86
|
+
when 1 then function.call state_machine.state_data.data
|
87
|
+
else
|
88
|
+
raise Payload::IncorrectPayloadArity, "unexpected payload arity: #{function.arity}, supported only 0 and 1"
|
89
|
+
end
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
Log.debug "another step fades: #{state_machine.state_data}" while state_machine.step
|
94
|
+
|
95
|
+
Log.debug 'shut down'
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal:true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative 'assertions'
|
6
|
+
require_relative 'state_machine'
|
7
|
+
require_relative 'vertex'
|
8
|
+
|
9
|
+
module ExcADG
|
10
|
+
# Vertex state data as filled by {ExcADG::StateMachine}
|
11
|
+
# to be transferred between vertices
|
12
|
+
module VStateData
|
13
|
+
# class to support comparison and Arrays operations
|
14
|
+
class Key
|
15
|
+
attr_reader :name, :vertex
|
16
|
+
|
17
|
+
def initialize name: nil, vertex: nil
|
18
|
+
raise 'name or vertex are required' if name.nil? && vertex.nil?
|
19
|
+
|
20
|
+
Assertions.is_a?(vertex, Vertex) unless vertex.nil?
|
21
|
+
|
22
|
+
@name = name
|
23
|
+
@vertex = vertex
|
24
|
+
end
|
25
|
+
|
26
|
+
include Comparable
|
27
|
+
def <=> other
|
28
|
+
return nil unless (other.is_a? self.class) || (is_a? other.class)
|
29
|
+
# no mutual fields to compare
|
30
|
+
return nil if (@vertex.nil? && other.name.nil?) || (@name.nil? && other.vertex.nil?)
|
31
|
+
|
32
|
+
# name takes preference
|
33
|
+
@name.nil? || other.name.nil? ? @vertex <=> other.vertex : @name <=> other.name
|
34
|
+
end
|
35
|
+
|
36
|
+
def eql? other
|
37
|
+
(self <=> other)&.zero? || false
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
(@name || @vertex).to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# contains actual data
|
46
|
+
class Full < Key
|
47
|
+
attr_reader :state, :data, :name, :vertex
|
48
|
+
|
49
|
+
# param state: Symbol, one of StateMachine.GRAPH.vertices
|
50
|
+
# param data: all data returned by a Vertice's Payload
|
51
|
+
# param name: Symboli -c name of the associated Vertex
|
52
|
+
# param vertex: Vertex that produced the data
|
53
|
+
def initialize state:, data:, name:, vertex: nil
|
54
|
+
# observation: Ractor.current returns Vertex object if invoked from a Vertex
|
55
|
+
super(name:, vertex: vertex || Ractor.current)
|
56
|
+
@state = state
|
57
|
+
@data = data
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"#{name || vertex} (#{state})"
|
62
|
+
end
|
63
|
+
|
64
|
+
# method omits objects without a good known text representation
|
65
|
+
def to_json(*args)
|
66
|
+
{
|
67
|
+
name: @name,
|
68
|
+
state: @state,
|
69
|
+
data: @data
|
70
|
+
}.to_json(*args)
|
71
|
+
end
|
72
|
+
|
73
|
+
# converts full object to key to use in Hash
|
74
|
+
def to_key
|
75
|
+
Key.new vertex: @vertex, name: @name
|
76
|
+
end
|
77
|
+
|
78
|
+
# auto-generated methods to check states easier;
|
79
|
+
# note: define_method causes these object to become un-shareable
|
80
|
+
# what breaks Broker's messaging
|
81
|
+
def method_missing(method, *_args, &_block)
|
82
|
+
raise NoMethodError unless respond_to_missing? method, _args
|
83
|
+
|
84
|
+
@state.eql? method[...-1].to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
def respond_to_missing? method, *_args
|
88
|
+
method.to_s.end_with?('?') && ExcADG::StateMachine::GRAPH.has_vertex?(method[...-1].to_sym)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/excadg.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal:true
|
2
|
+
|
3
|
+
# Execute Acyclic Directed Graph
|
4
|
+
module ExcADG; end
|
5
|
+
|
6
|
+
require_relative 'excadg/assertions'
|
7
|
+
require_relative 'excadg/broker'
|
8
|
+
require_relative 'excadg/data_store'
|
9
|
+
require_relative 'excadg/dependency_manager'
|
10
|
+
require_relative 'excadg/log'
|
11
|
+
require_relative 'excadg/payload'
|
12
|
+
require_relative 'excadg/request'
|
13
|
+
require_relative 'excadg/rtimeout'
|
14
|
+
require_relative 'excadg/state_machine'
|
15
|
+
require_relative 'excadg/tui'
|
16
|
+
require_relative 'excadg/vertex'
|
17
|
+
require_relative 'excadg/vstate_data'
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: excadg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- skorobogatydmitry
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-07-06 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: "# Description\n\nThat's a library (framework) to execute a graph of
|
14
|
+
dependent tasks (vertices). \nIts main feature is to run all possible tasks independently
|
15
|
+
in parallel once they're ready. \nAnother feature is that the graph is dynamic
|
16
|
+
and any vertex could produce another vertice(s) to extend execution graph.\n"
|
17
|
+
email: skorobogaty.dmitry@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/excadg.rb
|
23
|
+
- lib/excadg/assertions.rb
|
24
|
+
- lib/excadg/broker.rb
|
25
|
+
- lib/excadg/data_store.rb
|
26
|
+
- lib/excadg/dependency_manager.rb
|
27
|
+
- lib/excadg/log.rb
|
28
|
+
- lib/excadg/payload.rb
|
29
|
+
- lib/excadg/payload/example.rb
|
30
|
+
- lib/excadg/payload/wrapper.rb
|
31
|
+
- lib/excadg/request.rb
|
32
|
+
- lib/excadg/rtimeout.rb
|
33
|
+
- lib/excadg/state_machine.rb
|
34
|
+
- lib/excadg/tui.rb
|
35
|
+
- lib/excadg/vertex.rb
|
36
|
+
- lib/excadg/vstate_data.rb
|
37
|
+
homepage: https://rubygems.org/gems/excadg
|
38
|
+
licenses:
|
39
|
+
- LGPL-3.0-only
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 3.3.1
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.5.9
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Execute Acyclic Directed Graph
|
60
|
+
test_files: []
|