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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4289f15bf500b987315e7316a94dfbb57f44f9b4d1fd08f5595181172729858
4
- data.tar.gz: 398d87bb9ea0a8e6802e9500ec82ed6fba72cdda980260756c0262cba1807c9e
3
+ metadata.gz: 55c23d109605074f58032f420c732b94ab43df16c73d5b2d0fe7aeedc2b66918
4
+ data.tar.gz: 7f80b6e30c52a32bd367a1a98ddd867e462affaf6f72885c2bb17ad2f215b2b8
5
5
  SHA512:
6
- metadata.gz: ceb892d4f4628ee94357b23762ebf5eccfa7deefe0684aa83c5cc6b9122ec1f7b71623e20a84bf9966b24a0693ce93d87f652e37c7a1deac23ec630737474dfd
7
- data.tar.gz: 46f5eec9db954d8e57615a3a0185f9475a4e43052e6be055420892378a9da21e48ddcfaf948e928fd875f57011eaf5cd1114c36e2d1144b1cee44d02695842ce
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 settings
8
- Legion::Settings[:transport][:tls] || {}
9
- end
7
+ def tls_options(tls_config: nil, port: nil)
8
+ return {} unless defined?(Legion::Crypt::TLS)
10
9
 
11
- def use_vault_pki?
12
- settings[:use_vault_pki] && Legion::Settings[:crypt][:vault][:connected]
13
- end
10
+ tls_config ||= tls_settings
11
+ port ||= transport_port
14
12
 
15
- def use_tls?
16
- settings[:use_tls] || Legion::Settings[:transport][:connection][:port].to_i == 5671
17
- end
13
+ tls = Legion::Crypt::TLS.resolve(tls_config, port: port)
14
+ return {} unless tls[:enabled]
18
15
 
19
- def tls_cert
20
- settings[:tls_cert]
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
- def tls_key
24
- settings[:tls_key]
25
- end
25
+ private
26
+
27
+ def tls_settings
28
+ return {} unless defined?(Legion::Settings)
26
29
 
27
- def ca_certs
28
- settings[:ca_certs]
30
+ Legion::Settings[:transport][:tls] || {}
31
+ rescue StandardError
32
+ {}
29
33
  end
30
34
 
31
- def verify_peer?
32
- settings[:verify_peer] || false
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
- connector.new(build_bunny_opts(connection_name: connection_name))
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
- session.on_blocked { Legion::Transport.logger.warn('Legion::Transport is being blocked by RabbitMQ!') } if session.respond_to? :on_blocked
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 resolved.length > 1
109
- opts[:hosts] = resolved.map { |h| { host: h.split(':').first, port: h.split(':').last.to_i } }
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Transport
5
+ class PoolTimeout < StandardError; end
6
+ class ClusterUnavailable < StandardError; end
7
+ end
8
+ 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: 1,
23
- connection_timeout: 1,
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: 'rabbitmq',
105
- connected: false,
106
- logger_level: ENV['transport.logger_level'] || 'info',
107
- messages: messages,
108
- prefetch: ENV['transport.prefetch'].to_i,
109
- exchanges: exchanges,
110
- queues: queues,
111
- connection: connection,
112
- channel: 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Transport
5
- VERSION = '1.2.7'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
@@ -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.2.7
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