legion-transport 1.2.7 → 1.3.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/CHANGELOG.md +33 -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/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 +10 -0
- data/lib/legion/transport/settings.rb +33 -12
- data/lib/legion/transport/tenant_provisioner.rb +35 -0
- data/lib/legion/transport/tenant_quota.rb +78 -0
- data/lib/legion/transport/tenant_topology.rb +52 -0
- data/lib/legion/transport/version.rb +1 -1
- data/lib/legion/transport.rb +7 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55c23d109605074f58032f420c732b94ab43df16c73d5b2d0fe7aeedc2b66918
|
|
4
|
+
data.tar.gz: 7f80b6e30c52a32bd367a1a98ddd867e462affaf6f72885c2bb17ad2f215b2b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 579732a7b2688876efbecbdfeb26ece70d24d2a596104b87920cb8663f1ff61f0cb1baabe02158756a2408eaac4603360ef8902b443c1be20acbab674f4f0ceb
|
|
7
|
+
data.tar.gz: 05d79297bc68c5820c252ce6513c488807d2c4e323c77ad4d680dffdd5334cd7c466d9676f3048fe45cb092741fbe0d7ab45b104140989bb114e29bec9dd4d81
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Legion::Transport ChangeLog
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-03-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- RabbitMQ cluster support: `cluster_nodes`, `connection_pool_size`, `region`, `management_port`, `quorum_queue_policy` settings
|
|
7
|
+
- Cluster node rotation: `cluster_nodes` merged into `resolved_hosts` with shuffle for load distribution
|
|
8
|
+
- Connection pool (`Helpers::Pool`): mutex-protected pool of Bunny sessions with configurable size and timeout
|
|
9
|
+
- Channel pool (`Helpers::ChannelPool`): per-connection ring buffer of channels with borrow/return
|
|
10
|
+
- Region header injection: `x-legion-region` and `x-legion-region-affinity` headers on published messages when region is configured
|
|
11
|
+
- Quorum queue policy helper (`Helpers::Policy`): idempotent HTTP PUT to RabbitMQ Management API, opt-in via `quorum_queue_policy.enabled`
|
|
12
|
+
- Connection failover: retry loop across all cluster nodes on TCPConnectionFailed/AuthFailure/ECONNREFUSED
|
|
13
|
+
- `Legion::Transport::PoolTimeout` and `Legion::Transport::ClusterUnavailable` error classes
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `connection_timeout` default 1 -> 10, `network_recovery_interval` default 1 -> 2
|
|
17
|
+
- `build_bunny_opts` now merges `cluster_nodes` into resolved hosts before building Bunny options
|
|
18
|
+
|
|
19
|
+
## [1.2.9] - 2026-03-21
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `Connection::SSL` module refactored to use `Legion::Crypt::TLS.resolve` for TLS configuration
|
|
23
|
+
- Removed legacy `use_tls?`, `tls_cert`, `tls_key`, `ca_certs`, `verify_peer?` methods
|
|
24
|
+
- TLS options now merged into Bunny connection opts via `tls_options` method
|
|
25
|
+
- SSL module auto-required and included in `Connection` class
|
|
26
|
+
|
|
27
|
+
## [1.2.8] - 2026-03-21
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `TenantTopology` module: exchange/queue name prefixing with tenant context (`t.<tenant_id>.<name>`); disabled by default; shared exchanges (`legion.control`, `legion.health`, `legion.audit`) are never prefixed; delegates to `Legion::TenantContext.current_tenant_id` when no explicit tenant_id given
|
|
31
|
+
- `TenantProvisioner` module: provisions and deprovisions RabbitMQ topology (topic exchanges for tasks/results/events + fanout DLX) for a given tenant; accepts optional channel kwarg for reuse
|
|
32
|
+
- `TenantQuota` module: application-level sliding-window rate limiting per tenant; enforces `messages_per_second` and `bytes_per_second` limits from settings; raises `TenantQuota::QuotaExceededError` on violation
|
|
33
|
+
- `Settings.tenant_topology` defaults block: `enabled: false`, `prefix_format`, `shared_exchanges`, `auto_provision: true`, `quotas: {}`
|
|
34
|
+
- 61 new specs covering all three modules (227 total, 0 failures)
|
|
35
|
+
|
|
3
36
|
## [1.2.7] - 2026-03-20
|
|
4
37
|
|
|
5
38
|
### Added
|
|
@@ -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,7 @@ 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
|
|
127
128
|
%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
129
|
chain_id debug].each do |header|
|
|
129
130
|
next unless @options.key? header
|
|
@@ -172,6 +173,15 @@ module Legion
|
|
|
172
173
|
|
|
173
174
|
private
|
|
174
175
|
|
|
176
|
+
def inject_region_header
|
|
177
|
+
region = Legion::Settings[:transport][:region] rescue nil # rubocop:disable Style/RescueModifier
|
|
178
|
+
return if region.nil?
|
|
179
|
+
|
|
180
|
+
@options[:headers]['x-legion-region'] = region
|
|
181
|
+
affinity = @options[:region_affinity] || 'prefer_local'
|
|
182
|
+
@options[:headers]['x-legion-region-affinity'] = affinity
|
|
183
|
+
end
|
|
184
|
+
|
|
175
185
|
def spool_message(error)
|
|
176
186
|
return unless defined?(Legion::Transport::Spool)
|
|
177
187
|
|
|
@@ -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',
|
|
@@ -99,17 +99,38 @@ module Legion
|
|
|
99
99
|
}
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
def self.tenant_topology
|
|
103
|
+
{
|
|
104
|
+
enabled: false,
|
|
105
|
+
prefix_format: 't.%<tenant_id>s.',
|
|
106
|
+
shared_exchanges: %w[legion.control legion.health legion.audit],
|
|
107
|
+
auto_provision: true,
|
|
108
|
+
quotas: {}
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
def self.default
|
|
113
|
+
cluster_csv = ENV.fetch('transport.cluster_nodes', '')
|
|
103
114
|
{
|
|
104
|
-
type:
|
|
105
|
-
connected:
|
|
106
|
-
logger_level:
|
|
107
|
-
messages:
|
|
108
|
-
prefetch:
|
|
109
|
-
exchanges:
|
|
110
|
-
queues:
|
|
111
|
-
connection:
|
|
112
|
-
channel:
|
|
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
|
+
}
|
|
113
134
|
}
|
|
114
135
|
end
|
|
115
136
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'tenant_topology'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Transport
|
|
7
|
+
module TenantProvisioner
|
|
8
|
+
EXCHANGE_TYPES = %w[tasks results events].freeze
|
|
9
|
+
|
|
10
|
+
def self.provision(tenant_id, channel: nil)
|
|
11
|
+
ch = channel || Legion::Transport.connection.create_channel
|
|
12
|
+
EXCHANGE_TYPES.each do |type|
|
|
13
|
+
name = TenantTopology.exchange_name(type, tenant_id: tenant_id)
|
|
14
|
+
ch.topic(name, durable: true)
|
|
15
|
+
end
|
|
16
|
+
dlx = TenantTopology.exchange_name('dlx', tenant_id: tenant_id)
|
|
17
|
+
ch.fanout(dlx, durable: true)
|
|
18
|
+
ch.close unless channel
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.deprovision(tenant_id, channel: nil)
|
|
22
|
+
ch = channel || Legion::Transport.connection.create_channel
|
|
23
|
+
(EXCHANGE_TYPES + ['dlx']).each do |type|
|
|
24
|
+
name = TenantTopology.exchange_name(type, tenant_id: tenant_id)
|
|
25
|
+
begin
|
|
26
|
+
ch.exchange_delete(name)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
ch.close unless channel
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'tenant_topology'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Transport
|
|
7
|
+
module TenantQuota
|
|
8
|
+
class QuotaExceededError < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
WINDOW_SECONDS = 1
|
|
12
|
+
|
|
13
|
+
@counters = {}
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def check_publish(tenant_id, message_size: 0)
|
|
18
|
+
return true unless enabled?
|
|
19
|
+
|
|
20
|
+
msg_limit = rate_limit(tenant_id)
|
|
21
|
+
size_limit = byte_limit(tenant_id)
|
|
22
|
+
return true if msg_limit.nil? && size_limit.nil?
|
|
23
|
+
|
|
24
|
+
now = current_window
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
@counters[tenant_id] ||= { window: now, count: 0, bytes: 0 }
|
|
27
|
+
entry = @counters[tenant_id]
|
|
28
|
+
if entry[:window] != now
|
|
29
|
+
entry[:window] = now
|
|
30
|
+
entry[:count] = 0
|
|
31
|
+
entry[:bytes] = 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise QuotaExceededError, "Tenant #{tenant_id} exceeded message rate quota (#{msg_limit} msg/s)" if msg_limit && entry[:count] >= msg_limit
|
|
35
|
+
|
|
36
|
+
if size_limit && (entry[:bytes] + message_size) > size_limit
|
|
37
|
+
raise QuotaExceededError, "Tenant #{tenant_id} exceeded byte rate quota (#{size_limit} bytes/s)"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
entry[:count] += 1
|
|
41
|
+
entry[:bytes] += message_size
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enabled?
|
|
47
|
+
TenantTopology.enabled?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reset!
|
|
51
|
+
@mutex.synchronize { @counters.clear }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def current_window
|
|
57
|
+
(::Time.now.to_f / WINDOW_SECONDS).floor
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def rate_limit(tenant_id)
|
|
61
|
+
quota_settings(tenant_id)&.dig(:messages_per_second)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def byte_limit(tenant_id)
|
|
65
|
+
quota_settings(tenant_id)&.dig(:bytes_per_second)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def quota_settings(tenant_id)
|
|
69
|
+
return nil unless defined?(Legion::Settings)
|
|
70
|
+
|
|
71
|
+
Legion::Settings.dig(:transport, :tenant_topology, :quotas, tenant_id.to_sym)
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Transport
|
|
5
|
+
module TenantTopology
|
|
6
|
+
SHARED_EXCHANGES = %w[legion.control legion.health legion.audit].freeze
|
|
7
|
+
|
|
8
|
+
def self.exchange_name(base_name, tenant_id: nil)
|
|
9
|
+
return base_name unless enabled?
|
|
10
|
+
|
|
11
|
+
tid = tenant_id || current_tenant_id
|
|
12
|
+
return base_name if tid.nil? || tid == 'default' || shared?(base_name)
|
|
13
|
+
|
|
14
|
+
"t.#{tid}.#{base_name}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.queue_name(base_name, tenant_id: nil)
|
|
18
|
+
return base_name unless enabled?
|
|
19
|
+
|
|
20
|
+
tid = tenant_id || current_tenant_id
|
|
21
|
+
return base_name if tid.nil? || tid == 'default'
|
|
22
|
+
|
|
23
|
+
"t.#{tid}.#{base_name}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.shared?(name)
|
|
27
|
+
SHARED_EXCHANGES.any? { |prefix| name.start_with?(prefix) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.enabled?
|
|
31
|
+
settings = transport_settings
|
|
32
|
+
settings.is_a?(Hash) && settings.dig(:tenant_topology, :enabled) == true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.current_tenant_id
|
|
36
|
+
return nil unless defined?(Legion::TenantContext)
|
|
37
|
+
|
|
38
|
+
Legion::TenantContext.current_tenant_id
|
|
39
|
+
rescue StandardError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method def self.transport_settings
|
|
44
|
+
return {} unless defined?(Legion::Settings)
|
|
45
|
+
|
|
46
|
+
Legion::Settings[:transport] || {}
|
|
47
|
+
rescue StandardError
|
|
48
|
+
{}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
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,9 +40,15 @@ 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'
|
|
45
49
|
require_relative 'transport/message'
|
|
46
50
|
require_relative 'transport/spool'
|
|
51
|
+
require_relative 'transport/tenant_topology'
|
|
52
|
+
require_relative 'transport/tenant_provisioner'
|
|
53
|
+
require_relative 'transport/tenant_quota'
|
|
47
54
|
end
|
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.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- lib/legion/transport/connection/ssl.rb
|
|
105
105
|
- lib/legion/transport/connection/vault.rb
|
|
106
106
|
- lib/legion/transport/consumer.rb
|
|
107
|
+
- lib/legion/transport/errors.rb
|
|
107
108
|
- lib/legion/transport/exchange.rb
|
|
108
109
|
- lib/legion/transport/exchanges/agent.rb
|
|
109
110
|
- lib/legion/transport/exchanges/crypt.rb
|
|
@@ -111,6 +112,9 @@ files:
|
|
|
111
112
|
- lib/legion/transport/exchanges/lex.rb
|
|
112
113
|
- lib/legion/transport/exchanges/node.rb
|
|
113
114
|
- lib/legion/transport/exchanges/task.rb
|
|
115
|
+
- lib/legion/transport/helpers/channel_pool.rb
|
|
116
|
+
- lib/legion/transport/helpers/policy.rb
|
|
117
|
+
- lib/legion/transport/helpers/pool.rb
|
|
114
118
|
- lib/legion/transport/local.rb
|
|
115
119
|
- lib/legion/transport/message.rb
|
|
116
120
|
- lib/legion/transport/messages/check_subtask.rb
|
|
@@ -130,6 +134,9 @@ files:
|
|
|
130
134
|
- lib/legion/transport/queues/task_update.rb
|
|
131
135
|
- lib/legion/transport/settings.rb
|
|
132
136
|
- lib/legion/transport/spool.rb
|
|
137
|
+
- lib/legion/transport/tenant_provisioner.rb
|
|
138
|
+
- lib/legion/transport/tenant_quota.rb
|
|
139
|
+
- lib/legion/transport/tenant_topology.rb
|
|
133
140
|
- lib/legion/transport/version.rb
|
|
134
141
|
- sonar-project.properties
|
|
135
142
|
homepage: https://github.com/LegionIO/legion-transport
|