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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/legion/transport/settings.rb +20 -9
- 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 +3 -0
- 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: 675cb607fc415c57ee0a7ed48d60caea0fc951a6ce1ac8a092e3f10bc40b229e
|
|
4
|
+
data.tar.gz: 2e9f5410fca189ba407efebafc6b716289cf2890f914fefe456cb0cea5332fd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
105
|
-
connected:
|
|
106
|
-
logger_level:
|
|
107
|
-
messages:
|
|
108
|
-
prefetch:
|
|
109
|
-
exchanges:
|
|
110
|
-
queues:
|
|
111
|
-
connection:
|
|
112
|
-
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
|
data/lib/legion/transport.rb
CHANGED
|
@@ -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.
|
|
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
|