legion-transport 1.2.7 → 1.2.8

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: 675cb607fc415c57ee0a7ed48d60caea0fc951a6ce1ac8a092e3f10bc40b229e
4
+ data.tar.gz: 2e9f5410fca189ba407efebafc6b716289cf2890f914fefe456cb0cea5332fd8
5
5
  SHA512:
6
- metadata.gz: ceb892d4f4628ee94357b23762ebf5eccfa7deefe0684aa83c5cc6b9122ec1f7b71623e20a84bf9966b24a0693ce93d87f652e37c7a1deac23ec630737474dfd
7
- data.tar.gz: 46f5eec9db954d8e57615a3a0185f9475a4e43052e6be055420892378a9da21e48ddcfaf948e928fd875f57011eaf5cd1114c36e2d1144b1cee44d02695842ce
6
+ metadata.gz: c6610f64e0958da4ec59124c697c5ec611caabb3d6c08e296518d683c7212135e483d217c66a53b06ed38e6a9b03db3546f0efa7e18c71ca27d8dd35ea1249eb
7
+ data.tar.gz: ecbcc22eca4559cf40169501810798b02dcb5e2302ff97da1266db9a390fa4d7247b79e74fcad49e972a496a19fb0b7ccfb6aac4403db01ccd414f1055f56cb8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Legion::Transport ChangeLog
2
2
 
3
+ ## [1.2.8] - 2026-03-21
4
+
5
+ ### Added
6
+ - `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
7
+ - `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
8
+ - `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
9
+ - `Settings.tenant_topology` defaults block: `enabled: false`, `prefix_format`, `shared_exchanges`, `auto_provision: true`, `quotas: {}`
10
+ - 61 new specs covering all three modules (227 total, 0 failures)
11
+
3
12
  ## [1.2.7] - 2026-03-20
4
13
 
5
14
  ### Added
@@ -99,17 +99,28 @@ 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
103
113
  {
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
114
+ type: 'rabbitmq',
115
+ connected: false,
116
+ logger_level: ENV['transport.logger_level'] || 'info',
117
+ messages: messages,
118
+ prefetch: ENV['transport.prefetch'].to_i,
119
+ exchanges: exchanges,
120
+ queues: queues,
121
+ connection: connection,
122
+ channel: channel,
123
+ tenant_topology: tenant_topology
113
124
  }
114
125
  end
115
126
  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.2.8'
6
6
  end
7
7
  end
@@ -44,4 +44,7 @@ module Legion
44
44
  require_relative 'transport/exchange'
45
45
  require_relative 'transport/message'
46
46
  require_relative 'transport/spool'
47
+ require_relative 'transport/tenant_topology'
48
+ require_relative 'transport/tenant_provisioner'
49
+ require_relative 'transport/tenant_quota'
47
50
  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.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -130,6 +130,9 @@ files:
130
130
  - lib/legion/transport/queues/task_update.rb
131
131
  - lib/legion/transport/settings.rb
132
132
  - lib/legion/transport/spool.rb
133
+ - lib/legion/transport/tenant_provisioner.rb
134
+ - lib/legion/transport/tenant_quota.rb
135
+ - lib/legion/transport/tenant_topology.rb
133
136
  - lib/legion/transport/version.rb
134
137
  - sonar-project.properties
135
138
  homepage: https://github.com/LegionIO/legion-transport