shared_broker 1.4.0 → 1.6.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 +16 -0
- data/README.md +49 -2
- data/lib/shared_broker/cipher.rb +98 -69
- data/lib/shared_broker/concurrency/limiter.rb +34 -0
- data/lib/shared_broker/concurrency/semaphore.rb +45 -0
- data/lib/shared_broker/key_provider.rb +78 -0
- data/lib/shared_broker/version.rb +1 -1
- data/lib/shared_broker.rb +27 -10
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24d5ac30bc2bef8c5fd99edbcb8e7e986fa8005fde7cfdb39bfb4f3c6010efa8
|
|
4
|
+
data.tar.gz: d66d983238edb492bddf145c48866ce83a55b88bf6806472910bdafdbbd418ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c25d32bc67d7c28427c533b1f3e23b048a7513740f0e8a51e067f5f5d861b761a3189b7d84d06b977400b21bf022c003c50d4515e46cfdc6cee064d36acbdb38
|
|
7
|
+
data.tar.gz: 1ab4165d6750a4f1d28f3ce67c070d58135d10c793b4eac944ba257b8b01f3dcd0665e36018aa0c32b27076439297e6e8ee0b789f5b943fa2820ef9472d2f09f
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.6.0] - 2026-06-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Encryption Key Rotation & Granularity**: Introduced flexible topic-based encryption keys and multi-version key rotation.
|
|
12
|
+
- `SharedBroker::KeyProvider::Registry` to register multiple keys by key ID and route active keys based on topic glob patterns.
|
|
13
|
+
- `SharedBroker::KeyProvider::Static` for seamless backward compatibility with single global encryption keys.
|
|
14
|
+
- Automatic versioning of encrypted payloads with `_key_id` metadata, enabling decrypting historical messages using rotated keys.
|
|
15
|
+
|
|
16
|
+
## [1.5.0] - 2026-06-10
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Concurrency Control & Adaptive Backpressure.
|
|
20
|
+
- `SharedBroker::Concurrency::Semaphore` pure-Ruby thread-safe semaphore implementation.
|
|
21
|
+
- `SharedBroker::Concurrency::Limiter` to manage dynamic backpressure checks and concurrency limits on message ingestion.
|
|
22
|
+
- Integrated concurrency parameters (`max_concurrency`, `backpressure_check`, `backpressure_backoff`) into `Client#subscribe`.
|
|
23
|
+
|
|
8
24
|
## [1.4.0] - 2026-06-10
|
|
9
25
|
|
|
10
26
|
### Added
|
data/README.md
CHANGED
|
@@ -160,6 +160,41 @@ HYBRID_BROKER = SharedBroker::Client.new(
|
|
|
160
160
|
)
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
#### E. Encryption Key Rotation & Granularity
|
|
164
|
+
|
|
165
|
+
If your system requires encrypting payloads with different keys based on the topic (e.g., highly sensitive financial data vs. general notifications) or rotating keys without breaking the decryption of historical messages in queues, you can configure a Key Provider Registry:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# 1. Initialize Registry with a map of historical/current keys and active key mappings
|
|
169
|
+
key_registry = SharedBroker::KeyProvider::Registry.new(
|
|
170
|
+
keys: {
|
|
171
|
+
"v1" => "a" * 32, # historical key
|
|
172
|
+
"v2" => "b" * 32, # current general key
|
|
173
|
+
"finance_key_1" => "c" * 32 # current finance key
|
|
174
|
+
},
|
|
175
|
+
active_keys: {
|
|
176
|
+
# Topic-specific key mapping using glob patterns
|
|
177
|
+
"payment.*" => "finance_key_1",
|
|
178
|
+
# Fallback key mapping for all other topics
|
|
179
|
+
"*" => "v2"
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 2. Register key provider globally or pass it to Client initialize
|
|
184
|
+
SharedBroker.key_provider = key_registry
|
|
185
|
+
|
|
186
|
+
# Alternatively, pass it directly to the client
|
|
187
|
+
SPOT_BROKER = SharedBroker::Client.new(
|
|
188
|
+
adapter: BROKER_ADAPTER,
|
|
189
|
+
key_provider: key_registry
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
With a key provider registry configured:
|
|
194
|
+
- **Publishing**: Payloads are encrypted using the active key matching the topic pattern, and a `_key_id` metadata tag is automatically appended to the envelope.
|
|
195
|
+
- **Subscribing**: The gem automatically reads the `_key_id` from the payload envelope and decrypts it using the correct historical key version.
|
|
196
|
+
- **Fallback**: If no `_key_id` is present on a received message (e.g., legacy message), it falls back to the key associated with the topic pattern.
|
|
197
|
+
|
|
163
198
|
---
|
|
164
199
|
|
|
165
200
|
## Usage
|
|
@@ -177,14 +212,26 @@ event_data = {
|
|
|
177
212
|
SPOT_BROKER.publish("user.created", event_data)
|
|
178
213
|
```
|
|
179
214
|
|
|
180
|
-
### Subscribing to Events (Consumer with Retry
|
|
181
|
-
To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries and
|
|
215
|
+
### Subscribing to Events (Consumer with Retry, DLQ, Concurrency & Backpressure)
|
|
216
|
+
To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries, backoff rate, concurrency limits, and dynamic backpressure checks:
|
|
182
217
|
|
|
183
218
|
```ruby
|
|
219
|
+
# Basic subscriber
|
|
184
220
|
SPOT_BROKER.subscribe("user.created", "my_consumption_queue", max_retries: 3, backoff_base: 2) do |payload|
|
|
185
221
|
puts "Decrypted event successfully validated & consumed! ID: #{payload[:id]}"
|
|
186
222
|
# execute your business logic here...
|
|
187
223
|
end
|
|
224
|
+
|
|
225
|
+
# Advanced subscriber with Concurrency limits and Dynamic Backpressure
|
|
226
|
+
SPOT_BROKER.subscribe(
|
|
227
|
+
"user.created",
|
|
228
|
+
"my_consumption_queue",
|
|
229
|
+
max_concurrency: 5, # Process up to 5 events concurrently
|
|
230
|
+
backpressure_check: -> { db_overloaded? }, # Stop pulling new events if true
|
|
231
|
+
backpressure_backoff: 2.0 # Check health again after 2 seconds
|
|
232
|
+
) do |payload|
|
|
233
|
+
# business logic running within the concurrency wrapper
|
|
234
|
+
end
|
|
188
235
|
```
|
|
189
236
|
|
|
190
237
|
---
|
data/lib/shared_broker/cipher.rb
CHANGED
|
@@ -1,69 +1,98 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "openssl"
|
|
4
|
-
require "base64"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
module SharedBroker
|
|
8
|
-
module Cipher
|
|
9
|
-
class DecryptionError < StandardError; end
|
|
10
|
-
|
|
11
|
-
ALGORITHM = "aes-256-gcm"
|
|
12
|
-
|
|
13
|
-
def self.encrypt(payload_hash,
|
|
14
|
-
return payload_hash unless
|
|
15
|
-
|
|
16
|
-
unless payload_hash.is_a?(Hash)
|
|
17
|
-
raise ArgumentError, "Expected payload_hash to be a Hash, got #{payload_hash.class} with value #{payload_hash.inspect}"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
|
|
24
|
-
data_to_encrypt = payload_hash.reject { |k, _| k.to_s.start_with?("_") }
|
|
25
|
-
|
|
26
|
-
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
|
27
|
-
cipher.encrypt
|
|
28
|
-
cipher.key = key
|
|
29
|
-
iv = cipher.random_iv
|
|
30
|
-
|
|
31
|
-
encrypted_data = cipher.update(data_to_encrypt.to_json) + cipher.final
|
|
32
|
-
auth_tag = cipher.auth_tag
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
_encrypted: true,
|
|
36
|
-
_iv: Base64.strict_encode64(iv),
|
|
37
|
-
_auth_tag: Base64.strict_encode64(auth_tag),
|
|
38
|
-
_data: Base64.strict_encode64(encrypted_data)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module SharedBroker
|
|
8
|
+
module Cipher
|
|
9
|
+
class DecryptionError < StandardError; end
|
|
10
|
+
|
|
11
|
+
ALGORITHM = "aes-256-gcm"
|
|
12
|
+
|
|
13
|
+
def self.encrypt(payload_hash, key_provider_or_key, topic: nil)
|
|
14
|
+
return payload_hash unless key_provider_or_key
|
|
15
|
+
|
|
16
|
+
unless payload_hash.is_a?(Hash)
|
|
17
|
+
raise ArgumentError, "Expected payload_hash to be a Hash, got #{payload_hash.class} with value #{payload_hash.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
key, key_id = resolve_encryption_key(key_provider_or_key, topic)
|
|
21
|
+
return payload_hash unless key
|
|
22
|
+
|
|
23
|
+
metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
|
|
24
|
+
data_to_encrypt = payload_hash.reject { |k, _| k.to_s.start_with?("_") }
|
|
25
|
+
|
|
26
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
|
27
|
+
cipher.encrypt
|
|
28
|
+
cipher.key = key
|
|
29
|
+
iv = cipher.random_iv
|
|
30
|
+
|
|
31
|
+
encrypted_data = cipher.update(data_to_encrypt.to_json) + cipher.final
|
|
32
|
+
auth_tag = cipher.auth_tag
|
|
33
|
+
|
|
34
|
+
envelope = {
|
|
35
|
+
_encrypted: true,
|
|
36
|
+
_iv: Base64.strict_encode64(iv),
|
|
37
|
+
_auth_tag: Base64.strict_encode64(auth_tag),
|
|
38
|
+
_data: Base64.strict_encode64(encrypted_data)
|
|
39
|
+
}
|
|
40
|
+
envelope[:_key_id] = key_id.to_s if key_id
|
|
41
|
+
|
|
42
|
+
metadata.merge(envelope)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.decrypt(payload_hash, key_provider_or_key, topic: nil)
|
|
46
|
+
return payload_hash unless payload_hash.is_a?(Hash) && payload_hash[:_encrypted]
|
|
47
|
+
return payload_hash unless key_provider_or_key
|
|
48
|
+
|
|
49
|
+
key = resolve_decryption_key(payload_hash, key_provider_or_key, topic)
|
|
50
|
+
return payload_hash unless key
|
|
51
|
+
|
|
52
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM)
|
|
53
|
+
cipher.decrypt
|
|
54
|
+
cipher.key = key
|
|
55
|
+
cipher.iv = Base64.strict_decode64(payload_hash[:_iv])
|
|
56
|
+
cipher.auth_tag = Base64.strict_decode64(payload_hash[:_auth_tag])
|
|
57
|
+
|
|
58
|
+
encrypted_bytes = Base64.strict_decode64(payload_hash[:_data])
|
|
59
|
+
decrypted_json = cipher.update(encrypted_bytes) + cipher.final
|
|
60
|
+
|
|
61
|
+
decrypted_data = JSON.parse(decrypted_json, symbolize_names: true)
|
|
62
|
+
|
|
63
|
+
metadata = payload_hash.select { |k, _| k.to_s.start_with?("_") }
|
|
64
|
+
clean_envelope_metadata!(metadata)
|
|
65
|
+
|
|
66
|
+
decrypted_data.merge(metadata)
|
|
67
|
+
rescue => e
|
|
68
|
+
raise DecryptionError, "Failed to decrypt payload. Error: #{e.message}. Offending payload: #{payload_hash.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def self.resolve_encryption_key(provider_or_key, topic)
|
|
74
|
+
if provider_or_key.respond_to?(:key_for)
|
|
75
|
+
[provider_or_key.key_for(topic), provider_or_key.active_key_id_for(topic)]
|
|
76
|
+
else
|
|
77
|
+
[provider_or_key, nil]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.resolve_decryption_key(payload, provider_or_key, topic)
|
|
82
|
+
if provider_or_key.respond_to?(:key_for_id)
|
|
83
|
+
key_id = payload[:_key_id]
|
|
84
|
+
key_id ? provider_or_key.key_for_id(key_id) : provider_or_key.key_for(topic)
|
|
85
|
+
else
|
|
86
|
+
provider_or_key
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.clean_envelope_metadata!(metadata)
|
|
91
|
+
metadata.delete(:_encrypted)
|
|
92
|
+
metadata.delete(:_iv)
|
|
93
|
+
metadata.delete(:_auth_tag)
|
|
94
|
+
metadata.delete(:_data)
|
|
95
|
+
metadata.delete(:_key_id)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "semaphore"
|
|
4
|
+
|
|
5
|
+
module SharedBroker
|
|
6
|
+
module Concurrency
|
|
7
|
+
class Limiter
|
|
8
|
+
def initialize(max_concurrency: nil, backpressure_check: nil, backpressure_backoff: 1.0)
|
|
9
|
+
@backpressure_check = backpressure_check
|
|
10
|
+
@backpressure_backoff = backpressure_backoff
|
|
11
|
+
@semaphore = max_concurrency ? Semaphore.new(max_concurrency) : nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
wait_while_overloaded
|
|
16
|
+
if @semaphore
|
|
17
|
+
@semaphore.synchronize { yield }
|
|
18
|
+
else
|
|
19
|
+
yield
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def wait_while_overloaded
|
|
26
|
+
return unless @backpressure_check
|
|
27
|
+
|
|
28
|
+
while @backpressure_check.call
|
|
29
|
+
sleep(@backpressure_backoff)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module SharedBroker
|
|
6
|
+
module Concurrency
|
|
7
|
+
class Semaphore
|
|
8
|
+
def initialize(limit)
|
|
9
|
+
unless limit.is_a?(Integer) && limit > 0
|
|
10
|
+
raise ArgumentError, "Expected limit to be a positive Integer, got: #{limit.inspect} (class: #{limit.class})"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@limit = limit
|
|
14
|
+
@count = 0
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@cond = ConditionVariable.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def acquire
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
while @count >= @limit
|
|
22
|
+
@cond.wait(@mutex)
|
|
23
|
+
end
|
|
24
|
+
@count += 1
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def release
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
@count -= 1
|
|
31
|
+
@cond.signal
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def synchronize
|
|
36
|
+
acquire
|
|
37
|
+
begin
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
release
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedBroker
|
|
4
|
+
module KeyProvider
|
|
5
|
+
class KeyNotFoundError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Static key provider for backward compatibility
|
|
8
|
+
class Static
|
|
9
|
+
def initialize(key)
|
|
10
|
+
raise ArgumentError, "Expected key to be a 32-byte String, got #{key.inspect}" unless key.is_a?(String)
|
|
11
|
+
@key = key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def key_for(_topic)
|
|
15
|
+
@key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def key_for_id(_key_id)
|
|
19
|
+
@key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def active_key_id_for(_topic)
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Flexible registry for multiple keys and topic-based routing patterns
|
|
28
|
+
class Registry
|
|
29
|
+
def initialize(keys: {}, active_keys: {})
|
|
30
|
+
validate_inputs!(keys, active_keys)
|
|
31
|
+
@keys = keys.transform_keys(&:to_s)
|
|
32
|
+
@active_keys = active_keys.transform_keys(&:to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_for(topic)
|
|
36
|
+
key_id = active_key_id_for(topic)
|
|
37
|
+
key_for_id(key_id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def key_for_id(key_id)
|
|
41
|
+
return nil if key_id.nil?
|
|
42
|
+
|
|
43
|
+
key = @keys[key_id.to_s]
|
|
44
|
+
return key if key
|
|
45
|
+
|
|
46
|
+
raise KeyNotFoundError, "Key ID #{key_id.inspect} not found in registered keys. Available keys: #{@keys.keys.inspect}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def active_key_id_for(topic)
|
|
50
|
+
topic_str = topic.to_s
|
|
51
|
+
return @active_keys[topic_str] if @active_keys.key?(topic_str)
|
|
52
|
+
|
|
53
|
+
pattern = find_matching_pattern(topic_str)
|
|
54
|
+
return @active_keys[pattern] if pattern
|
|
55
|
+
|
|
56
|
+
fallback_key_id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def validate_inputs!(keys, active_keys)
|
|
62
|
+
unless keys.is_a?(Hash) && active_keys.is_a?(Hash)
|
|
63
|
+
raise ArgumentError, "Expected keys and active_keys to be Hashes, got keys: #{keys.inspect}, active_keys: #{active_keys.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def find_matching_pattern(topic_str)
|
|
68
|
+
@active_keys.keys.find do |pattern|
|
|
69
|
+
pattern != "*" && File.fnmatch?(pattern, topic_str)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fallback_key_id
|
|
74
|
+
@active_keys["*"] || raise(KeyNotFoundError, "No active key configuration found for fallback '*' pattern")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/shared_broker.rb
CHANGED
|
@@ -8,6 +8,9 @@ require_relative "shared_broker/schema_registry/providers/local"
|
|
|
8
8
|
require_relative "shared_broker/schema_registry/providers/http"
|
|
9
9
|
require_relative "shared_broker/validation"
|
|
10
10
|
require_relative "shared_broker/cipher"
|
|
11
|
+
require_relative "shared_broker/key_provider"
|
|
12
|
+
require_relative "shared_broker/concurrency/semaphore"
|
|
13
|
+
require_relative "shared_broker/concurrency/limiter"
|
|
11
14
|
require_relative "shared_broker/middleware_pipeline"
|
|
12
15
|
require_relative "shared_broker/middlewares/open_telemetry_propagation"
|
|
13
16
|
require_relative "shared_broker/adapters/base"
|
|
@@ -18,27 +21,29 @@ require_relative "shared_broker/adapters/redis"
|
|
|
18
21
|
|
|
19
22
|
module SharedBroker
|
|
20
23
|
class << self
|
|
21
|
-
attr_accessor :encryption_key
|
|
24
|
+
attr_accessor :encryption_key, :key_provider
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Default key for development/test if not set
|
|
25
28
|
@encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
|
|
29
|
+
@key_provider = nil
|
|
26
30
|
|
|
27
31
|
class Client
|
|
28
32
|
attr_reader :circuit_breaker, :middleware_pipeline, :adapters, :routing
|
|
29
33
|
|
|
30
|
-
def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil)
|
|
34
|
+
def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil, key_provider: nil)
|
|
31
35
|
setup_adapters(adapter: adapter, adapters: adapters, routing: routing)
|
|
32
36
|
@circuit_breaker = circuit_breaker || CircuitBreaker.new
|
|
33
37
|
resolved_middlewares = middlewares || [SharedBroker::Middlewares::OpenTelemetryPropagation.new]
|
|
34
38
|
@middleware_pipeline = MiddlewarePipeline.new(resolved_middlewares)
|
|
39
|
+
@key_provider = key_provider
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def publish(topic, message, correlation_id: nil)
|
|
38
43
|
metadata = { correlation_id: correlation_id, operation: :publish }
|
|
39
44
|
@middleware_pipeline.execute(topic, message, metadata) do
|
|
40
45
|
SharedBroker::Validation.validate!(topic, message)
|
|
41
|
-
encrypted_msg = SharedBroker::Cipher.encrypt(message,
|
|
46
|
+
encrypted_msg = SharedBroker::Cipher.encrypt(message, active_key_provider, topic: topic)
|
|
42
47
|
|
|
43
48
|
@circuit_breaker.run do
|
|
44
49
|
resolve_adapter(topic).publish(topic, encrypted_msg, correlation_id: correlation_id)
|
|
@@ -46,20 +51,32 @@ module SharedBroker
|
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
|
|
49
|
-
def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, max_concurrency: nil, backpressure_check: nil, backpressure_backoff: 1.0, &block)
|
|
55
|
+
limiter = SharedBroker::Concurrency::Limiter.new(
|
|
56
|
+
max_concurrency: max_concurrency,
|
|
57
|
+
backpressure_check: backpressure_check,
|
|
58
|
+
backpressure_backoff: backpressure_backoff
|
|
59
|
+
)
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
resolve_adapter(topic).subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
|
|
62
|
+
limiter.run do
|
|
63
|
+
decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, active_key_provider, topic: topic)
|
|
64
|
+
SharedBroker::Validation.validate!(topic, decrypted_msg)
|
|
65
|
+
|
|
66
|
+
metadata = { correlation_id: decrypted_msg[:_correlation_id], operation: :subscribe, queue_name: queue_name }
|
|
67
|
+
@middleware_pipeline.execute(topic, decrypted_msg, metadata) do
|
|
68
|
+
block.call(decrypted_msg)
|
|
69
|
+
end
|
|
57
70
|
end
|
|
58
71
|
end
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
private
|
|
62
75
|
|
|
76
|
+
def active_key_provider
|
|
77
|
+
@key_provider || SharedBroker.key_provider || SharedBroker::KeyProvider::Static.new(SharedBroker.encryption_key)
|
|
78
|
+
end
|
|
79
|
+
|
|
63
80
|
def setup_adapters(adapter: nil, adapters: nil, routing: nil)
|
|
64
81
|
if adapter || (adapters.nil? && routing.nil?)
|
|
65
82
|
validate_single_adapter!(adapter)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shared_broker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wesley Lima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -155,6 +155,9 @@ files:
|
|
|
155
155
|
- lib/shared_broker/adapters/redis.rb
|
|
156
156
|
- lib/shared_broker/cipher.rb
|
|
157
157
|
- lib/shared_broker/circuit_breaker.rb
|
|
158
|
+
- lib/shared_broker/concurrency/limiter.rb
|
|
159
|
+
- lib/shared_broker/concurrency/semaphore.rb
|
|
160
|
+
- lib/shared_broker/key_provider.rb
|
|
158
161
|
- lib/shared_broker/middleware_pipeline.rb
|
|
159
162
|
- lib/shared_broker/middlewares/open_telemetry_propagation.rb
|
|
160
163
|
- lib/shared_broker/schema_registry.rb
|