legion-transport 1.2.8 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +2 -1
- data/CODEOWNERS +1 -0
- data/lib/legion/transport/connection/ssl.rb +26 -18
- data/lib/legion/transport/connection.rb +62 -17
- data/lib/legion/transport/errors.rb +8 -0
- data/lib/legion/transport/exchanges/logging.rb +13 -0
- data/lib/legion/transport/helpers/channel_pool.rb +72 -0
- data/lib/legion/transport/helpers/policy.rb +55 -0
- data/lib/legion/transport/helpers/pool.rb +74 -0
- data/lib/legion/transport/message.rb +22 -0
- data/lib/legion/transport/messages/region_re_route.rb +44 -0
- data/lib/legion/transport/queue.rb +1 -0
- data/lib/legion/transport/queues/region_outbound.rb +38 -0
- data/lib/legion/transport/settings.rb +23 -13
- data/lib/legion/transport/version.rb +1 -1
- data/lib/legion/transport.rb +4 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 131a06c178704a57af2c9baef440704001342842e02d15a090297c09572505b5
|
|
4
|
+
data.tar.gz: e30dd4bdaac3fdfb42cf588f9512f3a3b930ae840b79f34ea073a960276e7fbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89b5e05ec97fad24f0e191ac67bf4362afe758d1d4b1481629b6fa5575355e0ec56fa793d78d0f2cf3111213bf1087865d6c3791c234e41884adf3d13bf28568
|
|
7
|
+
data.tar.gz: 41bff2584db88fd03cc904bbe2996c628d4fdf689f6b793557f6207efb31fa808ad95e9e0aaa350026760d06c95e0411c62e75c9ca6087f21e6d1dd5a3f99acb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Legion::Transport ChangeLog
|
|
2
2
|
|
|
3
|
+
## [1.3.2] - 2026-03-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Transport::Exchanges::Logging`: topic exchange (`legion.logging`) for structured log event publishing
|
|
7
|
+
- `Legion::Transport::Queues::RegionOutbound`: durable outbound queues for cross-region message routing (per-peer, skips current region)
|
|
8
|
+
- `Legion::Transport::Messages::RegionReRoute`: re-route message type for forwarding tasks to target regions
|
|
9
|
+
|
|
10
|
+
## [1.3.0] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- RabbitMQ cluster support: `cluster_nodes`, `connection_pool_size`, `region`, `management_port`, `quorum_queue_policy` settings
|
|
14
|
+
- Cluster node rotation: `cluster_nodes` merged into `resolved_hosts` with shuffle for load distribution
|
|
15
|
+
- Connection pool (`Helpers::Pool`): mutex-protected pool of Bunny sessions with configurable size and timeout
|
|
16
|
+
- Channel pool (`Helpers::ChannelPool`): per-connection ring buffer of channels with borrow/return
|
|
17
|
+
- Region header injection: `x-legion-region` and `x-legion-region-affinity` headers on published messages when region is configured
|
|
18
|
+
- Quorum queue policy helper (`Helpers::Policy`): idempotent HTTP PUT to RabbitMQ Management API, opt-in via `quorum_queue_policy.enabled`
|
|
19
|
+
- Connection failover: retry loop across all cluster nodes on TCPConnectionFailed/AuthFailure/ECONNREFUSED
|
|
20
|
+
- `Legion::Transport::PoolTimeout` and `Legion::Transport::ClusterUnavailable` error classes
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `connection_timeout` default 1 -> 10, `network_recovery_interval` default 1 -> 2
|
|
24
|
+
- `build_bunny_opts` now merges `cluster_nodes` into resolved hosts before building Bunny options
|
|
25
|
+
|
|
26
|
+
## [1.2.9] - 2026-03-21
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `Connection::SSL` module refactored to use `Legion::Crypt::TLS.resolve` for TLS configuration
|
|
30
|
+
- Removed legacy `use_tls?`, `tls_cert`, `tls_key`, `ca_certs`, `verify_peer?` methods
|
|
31
|
+
- TLS options now merged into Bunny connection opts via `tls_options` method
|
|
32
|
+
- SSL module auto-required and included in `Connection` class
|
|
33
|
+
|
|
3
34
|
## [1.2.8] - 2026-03-21
|
|
4
35
|
|
|
5
36
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -25,7 +25,8 @@ Legion::Transport
|
|
|
25
25
|
│ ├── Agent # Agent communication exchange (identity-bound: GAIA frames, preferences, proactive)
|
|
26
26
|
│ ├── Crypt # Encryption exchange
|
|
27
27
|
│ ├── Extensions # Extension exchange
|
|
28
|
-
│
|
|
28
|
+
│ ├── Lex # LEX exchange (inherits Extensions)
|
|
29
|
+
│ └── Logging # Log event exchange (legion.logging) for structured log event publishing
|
|
29
30
|
├── Queue # Base queue class (extends Bunny::Queue)
|
|
30
31
|
│ └── Queues/
|
|
31
32
|
│ ├── Node # Node queue
|
data/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @Esity
|
|
@@ -4,32 +4,40 @@ module Legion
|
|
|
4
4
|
module Transport
|
|
5
5
|
module Connection
|
|
6
6
|
module SSL
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
end
|
|
7
|
+
def tls_options(tls_config: nil, port: nil)
|
|
8
|
+
return {} unless defined?(Legion::Crypt::TLS)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
10
|
+
tls_config ||= tls_settings
|
|
11
|
+
port ||= transport_port
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
end
|
|
13
|
+
tls = Legion::Crypt::TLS.resolve(tls_config, port: port)
|
|
14
|
+
return {} unless tls[:enabled]
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
{
|
|
17
|
+
tls: true,
|
|
18
|
+
tls_cert: tls[:cert],
|
|
19
|
+
tls_key: tls[:key],
|
|
20
|
+
tls_ca_certificates: [tls[:ca]].compact,
|
|
21
|
+
verify_peer: tls[:verify] != :none
|
|
22
|
+
}
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def tls_settings
|
|
28
|
+
return {} unless defined?(Legion::Settings)
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
Legion::Settings[:transport][:tls] || {}
|
|
31
|
+
rescue StandardError
|
|
32
|
+
{}
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def
|
|
32
|
-
|
|
35
|
+
def transport_port
|
|
36
|
+
return nil unless defined?(Legion::Settings)
|
|
37
|
+
|
|
38
|
+
Legion::Settings[:transport][:connection][:port]
|
|
39
|
+
rescue StandardError
|
|
40
|
+
nil
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
end
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'concurrent-ruby'
|
|
4
|
+
require_relative 'connection/ssl'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Transport
|
|
7
8
|
module Connection
|
|
8
9
|
class << self
|
|
10
|
+
include Legion::Transport::Connection::SSL
|
|
11
|
+
|
|
9
12
|
def settings
|
|
10
13
|
Legion::Settings[:transport]
|
|
11
14
|
end
|
|
@@ -33,7 +36,7 @@ module Legion
|
|
|
33
36
|
nil
|
|
34
37
|
else
|
|
35
38
|
@session ||= Concurrent::AtomicReference.new(
|
|
36
|
-
|
|
39
|
+
create_session_with_failover(connection_name: connection_name)
|
|
37
40
|
)
|
|
38
41
|
@channel_thread = Concurrent::ThreadLocalVar.new(nil)
|
|
39
42
|
session.start
|
|
@@ -42,20 +45,8 @@ module Legion
|
|
|
42
45
|
Legion::Settings[:transport][:connected] = true
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if session.respond_to? :on_unblocked
|
|
48
|
-
session.on_unblocked do
|
|
49
|
-
Legion::Transport.logger.info('Legion::Transport is no longer being blocked by RabbitMQ')
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
if session.respond_to? :after_recovery_completed
|
|
54
|
-
session.after_recovery_completed do
|
|
55
|
-
Legion::Transport.logger.info('Legion::Transport has completed recovery')
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
48
|
+
register_session_callbacks
|
|
49
|
+
apply_quorum_policy_if_enabled
|
|
59
50
|
true
|
|
60
51
|
end
|
|
61
52
|
|
|
@@ -95,22 +86,76 @@ module Legion
|
|
|
95
86
|
|
|
96
87
|
private
|
|
97
88
|
|
|
89
|
+
def create_session_with_failover(connection_name:)
|
|
90
|
+
opts = build_bunny_opts(connection_name: connection_name)
|
|
91
|
+
hosts = opts[:hosts] || [{ host: opts[:host] || '127.0.0.1', port: opts[:port] || 5672 }]
|
|
92
|
+
last_error = nil
|
|
93
|
+
|
|
94
|
+
hosts.each do |host_entry|
|
|
95
|
+
attempt_opts = opts.dup
|
|
96
|
+
if host_entry.is_a?(Hash)
|
|
97
|
+
attempt_opts[:host] = host_entry[:host]
|
|
98
|
+
attempt_opts[:port] = host_entry[:port]
|
|
99
|
+
end
|
|
100
|
+
attempt_opts.delete(:hosts)
|
|
101
|
+
|
|
102
|
+
return connector.new(attempt_opts)
|
|
103
|
+
rescue Bunny::TCPConnectionFailed, Bunny::PossibleAuthenticationFailureError, Errno::ECONNREFUSED => e
|
|
104
|
+
last_error = e
|
|
105
|
+
host_desc = host_entry.is_a?(Hash) ? "#{host_entry[:host]}:#{host_entry[:port]}" : host_entry
|
|
106
|
+
Legion::Transport.logger.warn("Connection failed to #{host_desc}: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
raise Legion::Transport::ClusterUnavailable, "All cluster nodes exhausted: #{last_error&.message}" if defined?(Legion::Transport::ClusterUnavailable)
|
|
110
|
+
|
|
111
|
+
raise last_error || StandardError.new('No cluster nodes available')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def register_session_callbacks
|
|
115
|
+
session.on_blocked { Legion::Transport.logger.warn('Legion::Transport is being blocked by RabbitMQ!') } if session.respond_to?(:on_blocked)
|
|
116
|
+
|
|
117
|
+
if session.respond_to?(:on_unblocked)
|
|
118
|
+
session.on_unblocked do
|
|
119
|
+
Legion::Transport.logger.info('Legion::Transport is no longer being blocked by RabbitMQ')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return unless session.respond_to?(:after_recovery_completed)
|
|
124
|
+
|
|
125
|
+
session.after_recovery_completed do
|
|
126
|
+
Legion::Transport.logger.info('Legion::Transport has completed recovery')
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def apply_quorum_policy_if_enabled
|
|
131
|
+
return unless defined?(Legion::Transport::Helpers::Policy)
|
|
132
|
+
|
|
133
|
+
Legion::Transport::Helpers::Policy.apply_quorum_policy!
|
|
134
|
+
rescue StandardError
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
98
138
|
def build_bunny_opts(connection_name:)
|
|
99
139
|
conn_settings = Legion::Settings[:transport][:connection].dup
|
|
100
140
|
resolved = conn_settings.delete(:resolved_hosts) || []
|
|
101
141
|
|
|
142
|
+
cluster_nodes = Array(Legion::Settings[:transport][:cluster_nodes])
|
|
143
|
+
all_hosts = (resolved + cluster_nodes).uniq
|
|
144
|
+
all_hosts.shuffle! if all_hosts.length > 1
|
|
145
|
+
|
|
102
146
|
opts = conn_settings.merge(
|
|
103
147
|
connection_name: connection_name,
|
|
104
148
|
logger: Legion::Transport.logger,
|
|
105
149
|
log_level: :warn
|
|
106
150
|
)
|
|
107
151
|
|
|
108
|
-
if
|
|
109
|
-
opts[:hosts] =
|
|
152
|
+
if all_hosts.length > 1
|
|
153
|
+
opts[:hosts] = all_hosts.map { |h| { host: h.split(':').first, port: h.split(':').last.to_i } }
|
|
110
154
|
opts.delete(:host)
|
|
111
155
|
opts.delete(:port)
|
|
112
156
|
end
|
|
113
157
|
|
|
158
|
+
opts.merge!(tls_options)
|
|
114
159
|
opts
|
|
115
160
|
end
|
|
116
161
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Transport
|
|
5
|
+
module Helpers
|
|
6
|
+
class ChannelPool
|
|
7
|
+
def initialize(connection:, size: 10, prefetch: 2)
|
|
8
|
+
@connection = connection
|
|
9
|
+
@size = size
|
|
10
|
+
@prefetch = prefetch
|
|
11
|
+
@available = []
|
|
12
|
+
@in_use = []
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def borrow
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
purge_closed_unsafe
|
|
19
|
+
|
|
20
|
+
if (ch = @available.pop)
|
|
21
|
+
@in_use << ch
|
|
22
|
+
return ch
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
total = @available.size + @in_use.size
|
|
26
|
+
return nil if total >= @size
|
|
27
|
+
|
|
28
|
+
ch = @connection.create_channel
|
|
29
|
+
ch.prefetch(@prefetch) if ch.respond_to?(:prefetch)
|
|
30
|
+
@in_use << ch
|
|
31
|
+
ch
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def return(channel)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@in_use.delete(channel)
|
|
38
|
+
return unless channel.respond_to?(:open?) && channel.open?
|
|
39
|
+
return if (@available.size + @in_use.size) >= @size
|
|
40
|
+
|
|
41
|
+
@available << channel
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def purge_closed
|
|
46
|
+
@mutex.synchronize { purge_closed_unsafe }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def close_all
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
(@available + @in_use).each do |ch|
|
|
52
|
+
ch.close rescue nil # rubocop:disable Style/RescueModifier
|
|
53
|
+
end
|
|
54
|
+
@available.clear
|
|
55
|
+
@in_use.clear
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def size
|
|
60
|
+
@mutex.synchronize { @available.size + @in_use.size }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def purge_closed_unsafe
|
|
66
|
+
@available.reject! { |c| !c.respond_to?(:open?) || !c.open? }
|
|
67
|
+
@in_use.reject! { |c| !c.respond_to?(:open?) || !c.open? }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Transport
|
|
8
|
+
module Helpers
|
|
9
|
+
module Policy
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def apply_quorum_policy!(settings: nil)
|
|
13
|
+
settings ||= Legion::Settings[:transport]
|
|
14
|
+
policy = settings[:quorum_queue_policy]
|
|
15
|
+
return false unless policy && policy[:enabled]
|
|
16
|
+
|
|
17
|
+
conn = settings[:connection]
|
|
18
|
+
host = conn[:host] || '127.0.0.1'
|
|
19
|
+
port = settings[:management_port] || 15_672
|
|
20
|
+
user = conn[:user] || 'guest'
|
|
21
|
+
pass = conn[:password] || 'guest'
|
|
22
|
+
vhost = conn[:vhost] || '/'
|
|
23
|
+
|
|
24
|
+
encoded_vhost = URI.encode_www_form_component(vhost)
|
|
25
|
+
uri = URI("http://#{host}:#{port}/api/policies/#{encoded_vhost}/legion-quorum")
|
|
26
|
+
|
|
27
|
+
body = {
|
|
28
|
+
pattern: policy[:pattern] || '^legion\\.',
|
|
29
|
+
definition: {
|
|
30
|
+
'x-queue-type': 'quorum',
|
|
31
|
+
'x-delivery-limit': policy[:delivery_limit] || 5
|
|
32
|
+
},
|
|
33
|
+
'apply-to': 'queues',
|
|
34
|
+
priority: 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
req = Net::HTTP::Put.new(uri)
|
|
38
|
+
req.basic_auth(user, pass)
|
|
39
|
+
req.content_type = 'application/json'
|
|
40
|
+
req.body = ::JSON.dump(body)
|
|
41
|
+
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.open_timeout = 5
|
|
44
|
+
http.read_timeout = 5
|
|
45
|
+
response = http.request(req)
|
|
46
|
+
|
|
47
|
+
response.code.start_with?('2')
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Legion::Transport.logger.warn("Quorum policy apply failed: #{e.message}") if defined?(Legion::Transport)
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Transport
|
|
5
|
+
module Helpers
|
|
6
|
+
class Pool
|
|
7
|
+
def initialize(size: 1, timeout: 5, &block)
|
|
8
|
+
@size = size
|
|
9
|
+
@timeout = timeout
|
|
10
|
+
@factory = block
|
|
11
|
+
@available = []
|
|
12
|
+
@in_use = []
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@condition = ConditionVariable.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def checkout
|
|
18
|
+
deadline = Time.now + @timeout
|
|
19
|
+
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
loop do
|
|
22
|
+
@available.reject! { |c| c.respond_to?(:closed?) && c.closed? }
|
|
23
|
+
|
|
24
|
+
if (conn = @available.pop)
|
|
25
|
+
@in_use << conn
|
|
26
|
+
return conn
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
total = @available.size + @in_use.size
|
|
30
|
+
if total < @size
|
|
31
|
+
conn = @factory.call
|
|
32
|
+
@in_use << conn
|
|
33
|
+
return conn
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
remaining = deadline - Time.now
|
|
37
|
+
raise Legion::Transport::PoolTimeout, 'timed out waiting for available connection' if remaining <= 0
|
|
38
|
+
|
|
39
|
+
@condition.wait(@mutex, remaining)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def checkin(connection)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
@in_use.delete(connection)
|
|
47
|
+
@available << connection if connection.respond_to?(:open?) && connection.open?
|
|
48
|
+
@condition.signal
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def size
|
|
53
|
+
@mutex.synchronize { @available.size + @in_use.size }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def shutdown
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
(@available + @in_use).each do |conn|
|
|
59
|
+
conn.close rescue nil # rubocop:disable Style/RescueModifier
|
|
60
|
+
end
|
|
61
|
+
@available.clear
|
|
62
|
+
@in_use.clear
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def connected?
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
(@available + @in_use).any? { |c| c.respond_to?(:open?) && c.open? }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -124,6 +124,8 @@ module Legion
|
|
|
124
124
|
def headers
|
|
125
125
|
@options[:headers] ||= Concurrent::Hash.new
|
|
126
126
|
@options[:headers]['legion_protocol_version'] ||= '2.0'
|
|
127
|
+
inject_region_header
|
|
128
|
+
inject_legion_region_header
|
|
127
129
|
%i[task_id relationship_id trigger_namespace_id trigger_function_id parent_id master_id runner_namespace runner_class namespace_id function_id function
|
|
128
130
|
chain_id debug].each do |header|
|
|
129
131
|
next unless @options.key? header
|
|
@@ -172,6 +174,26 @@ module Legion
|
|
|
172
174
|
|
|
173
175
|
private
|
|
174
176
|
|
|
177
|
+
def inject_region_header
|
|
178
|
+
region = Legion::Settings[:transport][:region] rescue nil # rubocop:disable Style/RescueModifier
|
|
179
|
+
return if region.nil?
|
|
180
|
+
|
|
181
|
+
@options[:headers]['x-legion-region'] = region
|
|
182
|
+
affinity = @options[:region_affinity] || 'prefer_local'
|
|
183
|
+
@options[:headers]['x-legion-region-affinity'] = affinity
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def inject_legion_region_header
|
|
187
|
+
return unless defined?(Legion::Region) &&
|
|
188
|
+
Legion::Region.respond_to?(:current) &&
|
|
189
|
+
Legion::Region.current
|
|
190
|
+
|
|
191
|
+
@options[:headers]['region'] = Legion::Region.current
|
|
192
|
+
@options[:headers]['region_affinity'] = @options[:region_affinity] ||
|
|
193
|
+
(defined?(Legion::Settings) && Legion::Settings.dig(:region, :default_affinity)) ||
|
|
194
|
+
'prefer_local'
|
|
195
|
+
end
|
|
196
|
+
|
|
175
197
|
def spool_message(error)
|
|
176
198
|
return unless defined?(Legion::Transport::Spool)
|
|
177
199
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Transport
|
|
5
|
+
module Messages
|
|
6
|
+
class RegionReRoute < Legion::Transport::Message
|
|
7
|
+
def exchange
|
|
8
|
+
Legion::Transport::Exchanges::Task
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def routing_key
|
|
12
|
+
"region.reroute.#{target_region}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def message
|
|
16
|
+
{
|
|
17
|
+
original_payload: @options[:original_payload] || @options.except(:target_region),
|
|
18
|
+
target_region: target_region,
|
|
19
|
+
source_region: source_region,
|
|
20
|
+
rerouted_at: Time.now.to_i
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate
|
|
25
|
+
raise ArgumentError, 'target_region is required' unless @options[:target_region].is_a?(String) && !@options[:target_region].empty?
|
|
26
|
+
|
|
27
|
+
@valid = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def target_region
|
|
33
|
+
@options[:target_region]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def source_region
|
|
37
|
+
@options[:source_region] ||
|
|
38
|
+
(defined?(Legion::Settings) && Legion::Settings.dig(:region, :current)) ||
|
|
39
|
+
'unknown'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Transport
|
|
5
|
+
module Queues
|
|
6
|
+
module RegionOutbound
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def declare_all
|
|
10
|
+
peers = defined?(Legion::Settings) && Legion::Settings.dig(:region, :peers)
|
|
11
|
+
return [] unless peers.is_a?(Array) && !peers.empty?
|
|
12
|
+
|
|
13
|
+
current = defined?(Legion::Region) ? Legion::Region.current : nil
|
|
14
|
+
peers.reject { |p| p == current }.map do |peer|
|
|
15
|
+
declare_outbound(peer)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def declare_outbound(target_region)
|
|
20
|
+
queue_name = queue_name_for(target_region)
|
|
21
|
+
channel = Legion::Transport::Connection.channel
|
|
22
|
+
channel.queue(
|
|
23
|
+
queue_name,
|
|
24
|
+
durable: true,
|
|
25
|
+
arguments: { 'x-dead-letter-exchange' => 'tasks.dlx' }
|
|
26
|
+
)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
Legion::Transport.logger.warn "RegionOutbound: failed to declare queue for #{target_region}: #{e.message}"
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def queue_name_for(target_region)
|
|
33
|
+
"legion.tasks.outbound.#{target_region}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -16,11 +16,11 @@ module Legion
|
|
|
16
16
|
|
|
17
17
|
{
|
|
18
18
|
read_timeout: 1,
|
|
19
|
-
heartbeat: 30,
|
|
19
|
+
heartbeat: (ENV['transport.connection.heartbeat'] || 30).to_i,
|
|
20
20
|
automatically_recover: true,
|
|
21
21
|
continuation_timeout: 4000,
|
|
22
|
-
network_recovery_interval:
|
|
23
|
-
connection_timeout:
|
|
22
|
+
network_recovery_interval: (ENV['transport.connection.recovery_interval'] || 2).to_i,
|
|
23
|
+
connection_timeout: (ENV['transport.connection.connection_timeout'] || 10).to_i,
|
|
24
24
|
frame_max: 65_536,
|
|
25
25
|
user: ENV['transport.connection.user'] || 'guest',
|
|
26
26
|
password: ENV['transport.connection.password'] || 'guest',
|
|
@@ -110,17 +110,27 @@ module Legion
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def self.default
|
|
113
|
+
cluster_csv = ENV.fetch('transport.cluster_nodes', '')
|
|
113
114
|
{
|
|
114
|
-
type:
|
|
115
|
-
connected:
|
|
116
|
-
logger_level:
|
|
117
|
-
messages:
|
|
118
|
-
prefetch:
|
|
119
|
-
exchanges:
|
|
120
|
-
queues:
|
|
121
|
-
connection:
|
|
122
|
-
channel:
|
|
123
|
-
tenant_topology:
|
|
115
|
+
type: 'rabbitmq',
|
|
116
|
+
connected: false,
|
|
117
|
+
logger_level: ENV['transport.logger_level'] || 'info',
|
|
118
|
+
messages: messages,
|
|
119
|
+
prefetch: ENV['transport.prefetch'].to_i,
|
|
120
|
+
exchanges: exchanges,
|
|
121
|
+
queues: queues,
|
|
122
|
+
connection: connection,
|
|
123
|
+
channel: channel,
|
|
124
|
+
tenant_topology: tenant_topology,
|
|
125
|
+
cluster_nodes: cluster_csv.empty? ? [] : cluster_csv.split(',').map(&:strip),
|
|
126
|
+
connection_pool_size: (ENV['transport.connection_pool_size'] || 1).to_i,
|
|
127
|
+
region: ENV.fetch('transport.region', nil),
|
|
128
|
+
management_port: (ENV['transport.management_port'] || 15_672).to_i,
|
|
129
|
+
quorum_queue_policy: {
|
|
130
|
+
enabled: ENV['transport.quorum_queue_policy.enabled'] == 'true',
|
|
131
|
+
pattern: ENV['transport.quorum_queue_policy.pattern'] || '^legion\\.',
|
|
132
|
+
delivery_limit: (ENV['transport.quorum_queue_policy.delivery_limit'] || 5).to_i
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
135
|
end
|
|
126
136
|
end
|
data/lib/legion/transport.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'legion/transport/version'
|
|
4
4
|
require 'legion/settings'
|
|
5
5
|
require 'legion/transport/settings'
|
|
6
|
+
require_relative 'transport/errors'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module Transport
|
|
@@ -39,6 +40,9 @@ module Legion
|
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
require_relative 'transport/helpers/pool'
|
|
44
|
+
require_relative 'transport/helpers/channel_pool'
|
|
45
|
+
require_relative 'transport/helpers/policy'
|
|
42
46
|
require_relative 'transport/common'
|
|
43
47
|
require_relative 'transport/queue'
|
|
44
48
|
require_relative 'transport/exchange'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-transport
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2
|
|
4
|
+
version: 1.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- ".rubocop.yml"
|
|
95
95
|
- CHANGELOG.md
|
|
96
96
|
- CLAUDE.md
|
|
97
|
+
- CODEOWNERS
|
|
97
98
|
- Gemfile
|
|
98
99
|
- LICENSE
|
|
99
100
|
- README.md
|
|
@@ -104,18 +105,24 @@ files:
|
|
|
104
105
|
- lib/legion/transport/connection/ssl.rb
|
|
105
106
|
- lib/legion/transport/connection/vault.rb
|
|
106
107
|
- lib/legion/transport/consumer.rb
|
|
108
|
+
- lib/legion/transport/errors.rb
|
|
107
109
|
- lib/legion/transport/exchange.rb
|
|
108
110
|
- lib/legion/transport/exchanges/agent.rb
|
|
109
111
|
- lib/legion/transport/exchanges/crypt.rb
|
|
110
112
|
- lib/legion/transport/exchanges/extensions.rb
|
|
111
113
|
- lib/legion/transport/exchanges/lex.rb
|
|
114
|
+
- lib/legion/transport/exchanges/logging.rb
|
|
112
115
|
- lib/legion/transport/exchanges/node.rb
|
|
113
116
|
- lib/legion/transport/exchanges/task.rb
|
|
117
|
+
- lib/legion/transport/helpers/channel_pool.rb
|
|
118
|
+
- lib/legion/transport/helpers/policy.rb
|
|
119
|
+
- lib/legion/transport/helpers/pool.rb
|
|
114
120
|
- lib/legion/transport/local.rb
|
|
115
121
|
- lib/legion/transport/message.rb
|
|
116
122
|
- lib/legion/transport/messages/check_subtask.rb
|
|
117
123
|
- lib/legion/transport/messages/dynamic.rb
|
|
118
124
|
- lib/legion/transport/messages/lex_register.rb
|
|
125
|
+
- lib/legion/transport/messages/region_re_route.rb
|
|
119
126
|
- lib/legion/transport/messages/request_cluster_secret.rb
|
|
120
127
|
- lib/legion/transport/messages/subtask.rb
|
|
121
128
|
- lib/legion/transport/messages/task.rb
|
|
@@ -126,6 +133,7 @@ files:
|
|
|
126
133
|
- lib/legion/transport/queues/node.rb
|
|
127
134
|
- lib/legion/transport/queues/node_crypt.rb
|
|
128
135
|
- lib/legion/transport/queues/node_status.rb
|
|
136
|
+
- lib/legion/transport/queues/region_outbound.rb
|
|
129
137
|
- lib/legion/transport/queues/task_log.rb
|
|
130
138
|
- lib/legion/transport/queues/task_update.rb
|
|
131
139
|
- lib/legion/transport/settings.rb
|