legionio 1.6.10 → 1.6.11
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/CHANGELOG.md +7 -0
- data/lib/legion/cli/broker_command.rb +152 -0
- data/lib/legion/cli.rb +4 -0
- data/lib/legion/dispatch/local.rb +40 -0
- data/lib/legion/dispatch.rb +26 -0
- data/lib/legion/extensions.rb +31 -1
- data/lib/legion/ingress.rb +16 -1
- data/lib/legion/service.rb +10 -0
- data/lib/legion/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33862e1884c9ba2e72a1dc6920fb32626bce8a54a3a866065ae8413f3b351639
|
|
4
|
+
data.tar.gz: 7f78f235733a55b9561d28a454f8d3d5287814b5e4ae627ee83de821ef41a4ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04ae595b050e2790044d84c382b2f42fa2dc34516245695f75728764d0365a203636c1cbd9dffbc7f0c18ec95586f06922310f2933fa6a38e4583a0d2307c5ff
|
|
7
|
+
data.tar.gz: 6ea1841ad50f8fb26c9243ea01194576aee7813721b5a1677c0452e8aa71e99ea5788430d003ac5aa1ea910a25310b565c62b17819c77d1e24cc811260d01eea
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.6.11] - 2026-03-26
|
|
6
|
+
|
|
5
7
|
### Added
|
|
8
|
+
- `Legion::Dispatch` module with pluggable strategy interface and `Local` implementation using `Concurrent::FixedThreadPool`
|
|
9
|
+
- Local dispatch wiring in `extensions.rb`: `dispatch_local_actors` registers non-remote extensions in thread pool
|
|
10
|
+
- `Ingress.local_runner?` short-circuit: runners for `remote_invocable? false` extensions skip AMQP round-trip
|
|
11
|
+
- `setup_dispatch` in `Service` boot sequence with graceful shutdown
|
|
12
|
+
- `legion broker stats` and `legion broker cleanup` CLI commands for RabbitMQ management
|
|
6
13
|
- End-to-end integration test for TBI Phase 5 self-generating functions loop (9 examples)
|
|
7
14
|
- Test dependencies: lex-codegen, lex-eval added to Gemfile for integration testing
|
|
8
15
|
- Specs for `legion codegen` CLI subcommand (8 subcommands, 22 examples)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module CLI
|
|
9
|
+
class Broker < Thor
|
|
10
|
+
namespace 'broker'
|
|
11
|
+
|
|
12
|
+
def self.exit_on_failure?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
17
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
18
|
+
class_option :host, type: :string, default: 'localhost', desc: 'RabbitMQ management host'
|
|
19
|
+
class_option :port, type: :numeric, default: 15_672, desc: 'RabbitMQ management port'
|
|
20
|
+
class_option :user, type: :string, default: 'guest', desc: 'RabbitMQ management username'
|
|
21
|
+
class_option :password, type: :string, default: 'guest', desc: 'RabbitMQ management password'
|
|
22
|
+
class_option :vhost, type: :string, default: '/', desc: 'RabbitMQ vhost'
|
|
23
|
+
|
|
24
|
+
desc 'stats', 'Show RabbitMQ broker statistics (queues, exchanges, consumers, DLX)'
|
|
25
|
+
def stats
|
|
26
|
+
out = formatter
|
|
27
|
+
data = fetch_stats
|
|
28
|
+
|
|
29
|
+
if options[:json]
|
|
30
|
+
out.json(data)
|
|
31
|
+
else
|
|
32
|
+
out.header('RabbitMQ Broker Stats')
|
|
33
|
+
out.spacer
|
|
34
|
+
out.detail({
|
|
35
|
+
queues: data[:queues],
|
|
36
|
+
exchanges: data[:exchanges],
|
|
37
|
+
consumers: data[:consumers],
|
|
38
|
+
dlx: data[:dlx]
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
rescue Legion::CLI::Error => e
|
|
42
|
+
formatter.error(e.message)
|
|
43
|
+
exit(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc 'cleanup', 'Find (and optionally delete) orphaned queues with 0 consumers and 0 messages'
|
|
47
|
+
option :execute, type: :boolean, default: false, desc: 'Actually delete orphaned queues (default: dry-run)'
|
|
48
|
+
def cleanup
|
|
49
|
+
out = formatter
|
|
50
|
+
orphans = find_orphans
|
|
51
|
+
|
|
52
|
+
if orphans.empty?
|
|
53
|
+
out.success('No orphaned queues found')
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if options[:json]
|
|
58
|
+
out.json({ orphaned_queues: orphans, deleted: options[:execute] })
|
|
59
|
+
delete_orphans(orphans) if options[:execute]
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
out.header("Orphaned Queues (#{orphans.size})")
|
|
64
|
+
orphans.each { |q| out.warn(q) }
|
|
65
|
+
out.spacer
|
|
66
|
+
|
|
67
|
+
if options[:execute]
|
|
68
|
+
delete_orphans(orphans)
|
|
69
|
+
out.success("Deleted #{orphans.size} orphaned queue(s)")
|
|
70
|
+
else
|
|
71
|
+
out.warn('Dry-run mode — pass --execute to delete')
|
|
72
|
+
end
|
|
73
|
+
rescue Legion::CLI::Error => e
|
|
74
|
+
formatter.error(e.message)
|
|
75
|
+
exit(1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
no_commands do # rubocop:disable Metrics/BlockLength
|
|
79
|
+
def formatter
|
|
80
|
+
@formatter ||= Output::Formatter.new(
|
|
81
|
+
json: options[:json],
|
|
82
|
+
color: !options[:no_color]
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def vhost_encoded
|
|
89
|
+
ERB::Util.url_encode(options[:vhost])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def management_api(path)
|
|
93
|
+
uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
|
|
94
|
+
req = Net::HTTP::Get.new(uri)
|
|
95
|
+
req.basic_auth(options[:user], options[:password])
|
|
96
|
+
|
|
97
|
+
response = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
|
|
98
|
+
http.request(req)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
raise Legion::CLI::Error, "Management API error #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
102
|
+
|
|
103
|
+
::JSON.parse(response.body, symbolize_names: true)
|
|
104
|
+
rescue Errno::ECONNREFUSED
|
|
105
|
+
raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
106
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
107
|
+
raise Legion::CLI::Error, "Timed out connecting to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def management_delete(path)
|
|
111
|
+
uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
|
|
112
|
+
req = Net::HTTP::Delete.new(uri)
|
|
113
|
+
req.basic_auth(options[:user], options[:password])
|
|
114
|
+
|
|
115
|
+
Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
|
|
116
|
+
http.request(req)
|
|
117
|
+
end
|
|
118
|
+
rescue Errno::ECONNREFUSED
|
|
119
|
+
raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch_stats
|
|
123
|
+
queues = management_api("/queues/#{vhost_encoded}")
|
|
124
|
+
exchanges = management_api("/exchanges/#{vhost_encoded}")
|
|
125
|
+
|
|
126
|
+
total_consumers = queues.sum { |q| q[:consumers].to_i }
|
|
127
|
+
dlx_count = queues.count { |q| q.dig(:arguments, :'x-dead-letter-exchange') }
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
queues: queues.size,
|
|
131
|
+
exchanges: exchanges.size,
|
|
132
|
+
consumers: total_consumers,
|
|
133
|
+
dlx: dlx_count
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_orphans
|
|
138
|
+
queues = management_api("/queues/#{vhost_encoded}")
|
|
139
|
+
queues
|
|
140
|
+
.select { |q| q[:consumers].to_i.zero? && q[:messages].to_i.zero? }
|
|
141
|
+
.map { |q| q[:name].to_s }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def delete_orphans(orphans)
|
|
145
|
+
orphans.each do |name|
|
|
146
|
+
management_delete("/queues/#{vhost_encoded}/#{ERB::Util.url_encode(name)}")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -66,6 +66,7 @@ module Legion
|
|
|
66
66
|
autoload :Debug, 'legion/cli/debug_command'
|
|
67
67
|
autoload :CodegenCommand, 'legion/cli/codegen_command'
|
|
68
68
|
autoload :Bootstrap, 'legion/cli/bootstrap_command'
|
|
69
|
+
autoload :Broker, 'legion/cli/broker_command'
|
|
69
70
|
|
|
70
71
|
module Groups
|
|
71
72
|
autoload :Ai, 'legion/cli/groups/ai_group'
|
|
@@ -275,6 +276,9 @@ module Legion
|
|
|
275
276
|
desc 'dev SUBCOMMAND', 'Generators, docs, marketplace, and shell completion'
|
|
276
277
|
subcommand 'dev', Legion::CLI::Groups::Dev
|
|
277
278
|
|
|
279
|
+
desc 'broker SUBCOMMAND', 'RabbitMQ broker management (stats, cleanup)'
|
|
280
|
+
subcommand 'broker', Legion::CLI::Broker
|
|
281
|
+
|
|
278
282
|
desc 'tree', 'Print a tree of all available commands'
|
|
279
283
|
def tree
|
|
280
284
|
legion_print_command_tree(self.class, ::File.basename($PROGRAM_NAME), '')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent-ruby'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Dispatch
|
|
7
|
+
class Local
|
|
8
|
+
def initialize(pool_size: nil)
|
|
9
|
+
max = pool_size || Legion::Settings.dig(:dispatch, :local_pool_size) || 8
|
|
10
|
+
@pool = Concurrent::FixedThreadPool.new(max)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def start; end
|
|
14
|
+
|
|
15
|
+
def submit(&block)
|
|
16
|
+
@pool.post do
|
|
17
|
+
block.call
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Legion::Logging.error "[Dispatch::Local] #{e.message}" if defined?(Legion::Logging)
|
|
20
|
+
Legion::Logging.debug e.backtrace&.first(5) if defined?(Legion::Logging)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop
|
|
25
|
+
return unless @pool.running?
|
|
26
|
+
|
|
27
|
+
@pool.shutdown
|
|
28
|
+
@pool.wait_for_termination(15)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def capacity
|
|
32
|
+
{
|
|
33
|
+
pool_size: @pool.max_length,
|
|
34
|
+
queue_length: @pool.queue_length,
|
|
35
|
+
running: @pool.running?
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'dispatch/local'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Dispatch
|
|
7
|
+
class << self
|
|
8
|
+
def dispatcher
|
|
9
|
+
@dispatcher ||= Local.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def submit(&)
|
|
13
|
+
dispatcher.submit(&)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def shutdown
|
|
17
|
+
@dispatcher&.stop
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@dispatcher&.stop
|
|
22
|
+
@dispatcher = nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -89,6 +89,8 @@ module Legion
|
|
|
89
89
|
|
|
90
90
|
@running_instances&.clear
|
|
91
91
|
|
|
92
|
+
Legion::Dispatch.shutdown if defined?(Legion::Dispatch) && Legion::Dispatch.instance_variable_get(:@dispatcher)
|
|
93
|
+
|
|
92
94
|
@loaded_extensions.each do |name|
|
|
93
95
|
Catalog.transition(name, :stopped)
|
|
94
96
|
unregister_capabilities(name)
|
|
@@ -283,6 +285,7 @@ module Legion
|
|
|
283
285
|
end
|
|
284
286
|
|
|
285
287
|
hook_subscription_actors_pooled(sub_actors) unless sub_actors.empty?
|
|
288
|
+
dispatch_local_actors(@local_tasks) unless @local_tasks.empty?
|
|
286
289
|
|
|
287
290
|
@pending_actors.clear
|
|
288
291
|
Legion::Logging.info(
|
|
@@ -290,7 +293,8 @@ module Legion
|
|
|
290
293
|
"every:#{@timer_tasks.count}," \
|
|
291
294
|
"poll:#{@poll_tasks.count}," \
|
|
292
295
|
"once:#{@once_tasks.count}," \
|
|
293
|
-
"loop:#{@loop_tasks.count}"
|
|
296
|
+
"loop:#{@loop_tasks.count}," \
|
|
297
|
+
"local:#{@local_tasks.count}"
|
|
294
298
|
)
|
|
295
299
|
@loaded_extensions&.each { |name| Catalog.transition(name, :running) }
|
|
296
300
|
end
|
|
@@ -466,6 +470,32 @@ module Legion
|
|
|
466
470
|
true
|
|
467
471
|
end
|
|
468
472
|
|
|
473
|
+
def dispatch_local_actors(actors)
|
|
474
|
+
require 'legion/dispatch'
|
|
475
|
+
|
|
476
|
+
actors.each do |actor_hash|
|
|
477
|
+
ext_name = actor_hash[:extension_name]
|
|
478
|
+
|
|
479
|
+
runner_mod = actor_hash[:runner_class]
|
|
480
|
+
unless runner_mod
|
|
481
|
+
actor_str = actor_hash[:actor_class].to_s
|
|
482
|
+
runner_str = actor_str.sub('::Actor::', '::Runners::')
|
|
483
|
+
runner_mod = begin
|
|
484
|
+
Kernel.const_get(runner_str)
|
|
485
|
+
rescue NameError
|
|
486
|
+
Legion::Logging.warn "[LocalDispatch] runner not found for #{ext_name}: #{runner_str}" if defined?(Legion::Logging)
|
|
487
|
+
next
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
actor_hash[:runner_module] = runner_mod
|
|
492
|
+
actor_hash[:running_class] = actor_hash[:actor_class]
|
|
493
|
+
@running_instances&.push(actor_hash[:actor_class])
|
|
494
|
+
|
|
495
|
+
Legion::Logging.info "[LocalDispatch] registered: #{ext_name}/#{actor_hash[:actor_name]}" if defined?(Legion::Logging)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
469
499
|
public
|
|
470
500
|
|
|
471
501
|
def unregister_capabilities(gem_name)
|
data/lib/legion/ingress.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
|
|
40
40
|
# Normalize and execute via Legion::Runner.run.
|
|
41
41
|
# Returns the runner result hash.
|
|
42
|
-
def run(payload:, runner_class: nil, function: nil, source: 'unknown', principal: nil, **opts) # rubocop:disable Metrics/ParameterLists,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
42
|
+
def run(payload:, runner_class: nil, function: nil, source: 'unknown', principal: nil, **opts) # rubocop:disable Metrics/ParameterLists,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize
|
|
43
43
|
Legion::Logging.info "[Ingress] run: source=#{source} runner_class=#{runner_class} function=#{function}" if defined?(Legion::Logging)
|
|
44
44
|
check_subtask = opts.fetch(:check_subtask, true)
|
|
45
45
|
generate_task = opts.fetch(:generate_task, true)
|
|
@@ -79,6 +79,12 @@ module Legion
|
|
|
79
79
|
|
|
80
80
|
Legion::Events.emit('ingress.received', runner_class: rc.to_s, function: fn, source: source)
|
|
81
81
|
|
|
82
|
+
if local_runner?(rc)
|
|
83
|
+
Legion::Logging.debug "[Ingress] local short-circuit: #{rc}.#{fn}" if defined?(Legion::Logging)
|
|
84
|
+
klass = rc.is_a?(String) ? Kernel.const_get(rc) : rc
|
|
85
|
+
return klass.send(fn.to_sym, **message)
|
|
86
|
+
end
|
|
87
|
+
|
|
82
88
|
runner_block = lambda {
|
|
83
89
|
Legion::Runner.run(
|
|
84
90
|
runner_class: rc,
|
|
@@ -114,6 +120,15 @@ module Legion
|
|
|
114
120
|
{ success: false, status: 'task.blocked', error: { code: 'insufficient_consent', message: e.message } }
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
def local_runner?(runner_class)
|
|
124
|
+
return false unless defined?(Legion::Extensions) && Legion::Extensions.local_tasks.is_a?(Array)
|
|
125
|
+
|
|
126
|
+
klass = runner_class.is_a?(String) ? Kernel.const_get(runner_class) : runner_class
|
|
127
|
+
Legion::Extensions.local_tasks.any? { |t| t[:runner_module] == klass }
|
|
128
|
+
rescue NameError
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
117
132
|
private
|
|
118
133
|
|
|
119
134
|
def parse_payload(payload)
|
data/lib/legion/service.rb
CHANGED
|
@@ -50,6 +50,8 @@ module Legion
|
|
|
50
50
|
register_logging_hooks
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
setup_dispatch
|
|
54
|
+
|
|
53
55
|
if cache
|
|
54
56
|
begin
|
|
55
57
|
require 'legion/cache'
|
|
@@ -339,6 +341,12 @@ module Legion
|
|
|
339
341
|
Legion::Logging.warn "Legion::Apollo failed to load: #{e.message}"
|
|
340
342
|
end
|
|
341
343
|
|
|
344
|
+
def setup_dispatch
|
|
345
|
+
require 'legion/dispatch'
|
|
346
|
+
Legion::Dispatch.dispatcher.start
|
|
347
|
+
Legion::Logging.info "[Service] Dispatch started (strategy: #{Legion::Dispatch.dispatcher.class.name})"
|
|
348
|
+
end
|
|
349
|
+
|
|
342
350
|
def setup_transport
|
|
343
351
|
Legion::Logging.info 'Setting up Legion::Transport'
|
|
344
352
|
require 'legion/transport'
|
|
@@ -505,6 +513,8 @@ module Legion
|
|
|
505
513
|
@cluster_leader = nil
|
|
506
514
|
end
|
|
507
515
|
|
|
516
|
+
shutdown_component('Dispatch') { Legion::Dispatch.shutdown } if defined?(Legion::Dispatch)
|
|
517
|
+
|
|
508
518
|
ext_timeout = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
|
|
509
519
|
shutdown_component('Extensions', timeout: ext_timeout) { Legion::Extensions.shutdown }
|
|
510
520
|
Legion::Readiness.mark_not_ready(:extensions)
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.
|
|
4
|
+
version: 1.6.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -514,6 +514,7 @@ files:
|
|
|
514
514
|
- lib/legion/cli/audit_command.rb
|
|
515
515
|
- lib/legion/cli/auth_command.rb
|
|
516
516
|
- lib/legion/cli/bootstrap_command.rb
|
|
517
|
+
- lib/legion/cli/broker_command.rb
|
|
517
518
|
- lib/legion/cli/chain.rb
|
|
518
519
|
- lib/legion/cli/chain_command.rb
|
|
519
520
|
- lib/legion/cli/chat/agent_delegator.rb
|
|
@@ -734,6 +735,8 @@ files:
|
|
|
734
735
|
- lib/legion/digital_worker/registry.rb
|
|
735
736
|
- lib/legion/digital_worker/risk_tier.rb
|
|
736
737
|
- lib/legion/digital_worker/value_metrics.rb
|
|
738
|
+
- lib/legion/dispatch.rb
|
|
739
|
+
- lib/legion/dispatch/local.rb
|
|
737
740
|
- lib/legion/docs/site_generator.rb
|
|
738
741
|
- lib/legion/events.rb
|
|
739
742
|
- lib/legion/extensions.rb
|