shared_broker 1.2.0 → 1.4.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 +60 -45
- data/README.md +204 -155
- data/lib/shared_broker/schema_registry/providers/http.rb +61 -0
- data/lib/shared_broker/schema_registry/providers/local.rb +38 -0
- data/lib/shared_broker/schema_registry.rb +25 -0
- data/lib/shared_broker/validation.rb +27 -32
- data/lib/shared_broker/version.rb +5 -5
- data/lib/shared_broker.rb +58 -10
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b161a64893a9f15f9bb4568c1b281c2851904bc9e8730e8992d5f6d0847a4de4
|
|
4
|
+
data.tar.gz: 6a771b8df2dc371516b2d7f97d6e157ff493141034106c40236f952c6aef0f2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '06494d11f1575ac5e13353acad3e9896f9ff7e5b7f64b164cecaa39b492aadeb170590e2ec9c9628c80691d08fc521e07e384fa43d2fe4e7874424704e39aedb'
|
|
7
|
+
data.tar.gz: 6781b513d75bd232c5c75738a6078aad08d096133e410335ec707ac203548b653af222bbb37ee7594fff40044e8122f8e362b12f68927939a7a19655f021ca16
|
data/CHANGELOG.md
CHANGED
|
@@ -1,45 +1,60 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [1.
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.4.0] - 2026-06-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Schema Registry Integration. Support for pluggable schema registries allowing dynamic schema validation during `publish` and `subscribe` actions.
|
|
12
|
+
- `Local` Schema Provider (default) leveraging `dry-schema` for backward compatibility.
|
|
13
|
+
- `Http` Schema Provider fetching JSON schemas over HTTP/HTTPS with in-memory caching and TTL.
|
|
14
|
+
- Validation using standard JSON Schema syntax for the `Http` provider.
|
|
15
|
+
|
|
16
|
+
## [1.3.0] - 2026-06-08
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Hybrid Multi-Adapter Routing to allow publishing and subscribing to different broker adapters (e.g., RabbitMQ, Kafka, Redis, or InMemory) dynamically.
|
|
20
|
+
- Support for exact match and wildcard matching (using File.fnmatch? glob patterns) routing rules, with default fallback adapter.
|
|
21
|
+
- Full backward compatibility with legacy single-adapter initialization.
|
|
22
|
+
|
|
23
|
+
## [1.2.0] - 2026-06-07
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Pipeline of customizable Middlewares (Interceptors) for publishing and subscribing messages.
|
|
27
|
+
- Distributed Tracing using W3C Trace Context (`traceparent`/`tracestate`) context injection and extraction.
|
|
28
|
+
|
|
29
|
+
## [1.1.1] - 2026-06-06
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Restructured `README.md` to clearly separate minimum required configuration from optional/advanced setups.
|
|
33
|
+
|
|
34
|
+
## [1.1.0] - 2026-06-06
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Scalable Adapters:
|
|
38
|
+
- Apache Kafka adapter (`SharedBroker::Adapters::Kafka`) with dynamic dependency loading.
|
|
39
|
+
- Redis Pub/Sub adapter (`SharedBroker::Adapters::Redis`) with list-based DLQ routing.
|
|
40
|
+
- Mock test configurations for Kafka and Redis to run adapter unit tests.
|
|
41
|
+
|
|
42
|
+
## [1.0.0] - 2026-06-06
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Fault Tolerance & Resilience features:
|
|
46
|
+
- Exponential backoff retry loop for message processing.
|
|
47
|
+
- Automatic Dead Letter Queue (DLQ) routing with rich metadata headers (`x_failed_at`, `x_exception_class`, `x_exception_message`, `x_original_routing_key`).
|
|
48
|
+
- Thread-safe `CircuitBreaker` wrapping all outbound publisher calls.
|
|
49
|
+
- Schema Validation and Security:
|
|
50
|
+
- Outbound/Inbound boundary validation using `dry-schema`.
|
|
51
|
+
- Transparent AES-256-GCM symmetric payload encryption by default, configurable with `SharedBroker.encryption_key`.
|
|
52
|
+
- Comprehensive test coverage for all the above features using isolated fakes and Minitest.
|
|
53
|
+
|
|
54
|
+
## [0.1.0] - 2026-06-06
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- Initial release with the pluggable `Client` messaging system.
|
|
58
|
+
- `InMemory` adapter for local testing.
|
|
59
|
+
- `RabbitMQ` adapter using the `bunny` gem.
|
|
60
|
+
- Basic OpenTelemetry instrumentation utility (`SharedBroker::Telemetry`).
|
data/README.md
CHANGED
|
@@ -1,155 +1,204 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
1
|
+
[](https://badge.fury.io/rb/shared_broker)
|
|
2
|
+
|
|
3
|
+
# SharedBroker
|
|
4
|
+
|
|
5
|
+
`SharedBroker` is a high-performance Ruby library designed to simplify event-based communication (asynchronous messaging) and telemetry (observability) in Rails microservice architectures.
|
|
6
|
+
|
|
7
|
+
The library implements the **Adapter Pattern** to decouple your application from physical queue providers, allowing easy broker swapping and clean synchronous testing with an in-memory adapter.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Key Features
|
|
12
|
+
|
|
13
|
+
- **Pluggable Messaging**: Adapter pattern supporting:
|
|
14
|
+
- `InMemory`: Synchronous local simulation for fast TDD testing (no inline external I/O stubs required).
|
|
15
|
+
- `RabbitMQ`: Robust connection using the `bunny` gem.
|
|
16
|
+
- `Kafka`: High-throughput adapter using the `kafka` gem.
|
|
17
|
+
- `Redis`: Light-weight Pub/Sub broker using the `redis` gem.
|
|
18
|
+
- **Resilience & Fault Tolerance**:
|
|
19
|
+
- **Automatic Retry**: Automatic retry mechanism on message processing failures using exponential backoff.
|
|
20
|
+
- **Dead Letter Queue (DLQ)**: Messages that exhaust their retries are automatically moved to a DLQ (`#{queue_name}.dlq` or a custom topic/list depending on the adapter) containing error metadata headers.
|
|
21
|
+
- **Circuit Breaker**: Integrated thread-safe Circuit Breaker wrapping message publication to prevent cascading failures.
|
|
22
|
+
- **Security & Data Validation**:
|
|
23
|
+
- **Strict Schema Validation**: Integration with `dry-schema` to validate message structures on both publish (boundaries out) and subscribe (boundaries in).
|
|
24
|
+
- **Transparent Payload Encryption**: Payloads are automatically encrypted at rest using AES-256-GCM via `SharedBroker.encryption_key`.
|
|
25
|
+
- **Integrated OpenTelemetry**: Centralized SDK configuration with auto-instrumentation for all supported libraries (ActiveRecord, Bunny, Faraday, Rails, PG, etc.).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Add this line to your application's `Gemfile`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem "shared_broker", path: "gems/shared_broker" # for local gem
|
|
35
|
+
# or when published:
|
|
36
|
+
# gem "shared_broker"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
And execute:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bundle install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Create an initializer in your Rails application (`config/initializers/shared_broker.rb`). Below is the breakdown of what is **required** versus what is **optional**.
|
|
50
|
+
|
|
51
|
+
### 1. Required Configuration (Minimum Setup)
|
|
52
|
+
|
|
53
|
+
You must configure the adapter depending on the environment, initialize the client, and configure the payload encryption key (required since AES-256-GCM is active by default):
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
require "shared_broker"
|
|
57
|
+
|
|
58
|
+
# A. Configure Payload Encryption Key (AES-256-GCM)
|
|
59
|
+
# Expects a 32-byte string. Use a secure production key in production.
|
|
60
|
+
SharedBroker.encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
|
|
61
|
+
|
|
62
|
+
# B. Configure the Adapter based on Environment
|
|
63
|
+
if Rails.env.test?
|
|
64
|
+
# In-memory adapter prevents external queue dependency during unit tests
|
|
65
|
+
BROKER_ADAPTER = SharedBroker::Adapters::InMemory.new
|
|
66
|
+
else
|
|
67
|
+
# Connects to real RabbitMQ broker
|
|
68
|
+
amqp_url = ENV.fetch("RABBITMQ_URL") { "amqp://guest:guest@localhost:5672" }
|
|
69
|
+
BROKER_ADAPTER = SharedBroker::Adapters::RabbitMQ.new(amqp_url: amqp_url)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# C. Instantiate the Client by Injecting the Adapter
|
|
73
|
+
SPOT_BROKER = SharedBroker::Client.new(adapter: BROKER_ADAPTER)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 2. Optional Configuration
|
|
79
|
+
|
|
80
|
+
These features can be configured optionally depending on your needs.
|
|
81
|
+
|
|
82
|
+
#### A. Event Payload Validation & Schema Registry
|
|
83
|
+
|
|
84
|
+
`SharedBroker` supports validation on outbound (`publish`) and inbound (`subscribe`) boundaries. It includes a pluggable **Schema Registry** supporting both local definitions and remote registry servers.
|
|
85
|
+
|
|
86
|
+
##### A1. Local Validation (dry-schema - default)
|
|
87
|
+
Register schemas to validate payload structure locally using `dry-schema`:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
user_created_schema = Dry::Schema.Params do
|
|
91
|
+
required(:id).filled(:integer)
|
|
92
|
+
required(:email).filled(:string)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Registered on the default local provider
|
|
96
|
+
SharedBroker::Validation.register("user.created", user_created_schema)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
##### A2. Http Schema Registry (JSON Schema)
|
|
100
|
+
Configure `SharedBroker` to fetch schemas dynamically from an HTTP-based Schema Registry. The HTTP provider validates payloads using the standard JSON Schema specification and caches schemas in-memory to prevent validation latency:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Configure the HTTP Schema Registry provider
|
|
104
|
+
SharedBroker::SchemaRegistry.provider = SharedBroker::SchemaRegistry::Providers::Http.new(
|
|
105
|
+
url: "https://schema-registry.mycorp.internal",
|
|
106
|
+
headers: { "Authorization" => "Bearer my-secret-token" },
|
|
107
|
+
cache_ttl: 300 # cache schemas in-memory for 5 minutes
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
When configured with the HTTP provider, any published or subscribed event will automatically trigger a lookup against `https://schema-registry.mycorp.internal/schemas/{topic}.json`.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#### B. Custom Circuit Breaker
|
|
114
|
+
By default, the client instantiates a standard Circuit Breaker. You can provide a custom one to tune the failure threshold and recovery window:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
custom_circuit_breaker = SharedBroker::CircuitBreaker.new(
|
|
118
|
+
failure_threshold: 5, # trip circuit after 5 failures
|
|
119
|
+
recovery_timeout: 30 # wait 30 seconds before attempting recovery
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
SPOT_BROKER = SharedBroker::Client.new(
|
|
123
|
+
adapter: BROKER_ADAPTER,
|
|
124
|
+
circuit_breaker: custom_circuit_breaker
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### C. Initialize Distributed Tracing (OpenTelemetry)
|
|
129
|
+
Initialize the OpenTelemetry SDK with auto-instrumentation for the microservice:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
SharedBroker::Telemetry.configure(service_name: "my_microservice")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### D. Hybrid Multi-Adapter Routing
|
|
136
|
+
If your system requires directing different topics to different message brokers (e.g., Kafka for telemetry, RabbitMQ for transactional events, Redis for quick cache invalidation), you can configure `SharedBroker::Client` with multiple adapters and a routing table:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# 1. Initialize physical adapters
|
|
140
|
+
rabbit_adapter = SharedBroker::Adapters::RabbitMQ.new(amqp_url: "amqp://guest:guest@localhost:5672")
|
|
141
|
+
kafka_adapter = SharedBroker::Adapters::Kafka.new(seed_brokers: ["localhost:9092"])
|
|
142
|
+
redis_adapter = SharedBroker::Adapters::Redis.new(redis_url: "redis://localhost:6379")
|
|
143
|
+
|
|
144
|
+
# 2. Instantiate the Client in Hybrid mode
|
|
145
|
+
HYBRID_BROKER = SharedBroker::Client.new(
|
|
146
|
+
adapters: {
|
|
147
|
+
rabbitmq: rabbit_adapter,
|
|
148
|
+
kafka: kafka_adapter,
|
|
149
|
+
redis: redis_adapter
|
|
150
|
+
},
|
|
151
|
+
routing: {
|
|
152
|
+
# Exact routing match
|
|
153
|
+
"payment.processed" => :rabbitmq,
|
|
154
|
+
# Wildcard routing match
|
|
155
|
+
"telemetry.*" => :kafka,
|
|
156
|
+
"cache.*" => :redis,
|
|
157
|
+
# Default fallback routing
|
|
158
|
+
"*" => :rabbitmq
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Usage
|
|
166
|
+
|
|
167
|
+
### Publishing Events
|
|
168
|
+
Send events by passing the topic name and a structured payload (must be a `Hash`):
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
event_data = {
|
|
172
|
+
id: 1,
|
|
173
|
+
email: "test@example.com"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# The payload will be validated against its dry-schema, encrypted, and published safely.
|
|
177
|
+
SPOT_BROKER.publish("user.created", event_data)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Subscribing to Events (Consumer with Retry and DLQ)
|
|
181
|
+
To start a persistent event subscriber daemon, register a queue/group name associated with the topic. You can customize the retries and backoff rate:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
SPOT_BROKER.subscribe("user.created", "my_consumption_queue", max_retries: 3, backoff_base: 2) do |payload|
|
|
185
|
+
puts "Decrypted event successfully validated & consumed! ID: #{payload[:id]}"
|
|
186
|
+
# execute your business logic here...
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Running Gem Tests
|
|
193
|
+
|
|
194
|
+
To run the unit test suite using **Minitest**:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
bundle exec rake test
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
This Gem is available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "json-schema"
|
|
6
|
+
|
|
7
|
+
module SharedBroker
|
|
8
|
+
module SchemaRegistry
|
|
9
|
+
module Providers
|
|
10
|
+
class Http
|
|
11
|
+
def initialize(url:, headers: {}, cache_ttl: 300)
|
|
12
|
+
@base_url = url.chomp("/")
|
|
13
|
+
@headers = headers
|
|
14
|
+
@cache_ttl = cache_ttl
|
|
15
|
+
@cache = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate!(topic, payload)
|
|
19
|
+
schema = fetch_schema(topic)
|
|
20
|
+
return unless schema
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
JSON::Validator.validate!(schema, payload)
|
|
24
|
+
rescue JSON::Schema::ValidationError => e
|
|
25
|
+
raise SharedBroker::Validation::ValidationError,
|
|
26
|
+
"Schema validation failed for topic #{topic.inspect} against schema #{schema.inspect}. Offending payload: #{payload.inspect}. Error: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear_cache
|
|
31
|
+
@cache.clear
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def fetch_schema(topic)
|
|
37
|
+
cached = @cache[topic.to_s]
|
|
38
|
+
return cached[:schema] if cached && cached[:expires_at] > Time.now
|
|
39
|
+
|
|
40
|
+
schema = download_schema(topic)
|
|
41
|
+
@cache[topic.to_s] = { schema: schema, expires_at: Time.now + @cache_ttl } if schema
|
|
42
|
+
schema
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def download_schema(topic)
|
|
46
|
+
uri = URI("#{@base_url}/schemas/#{topic}.json")
|
|
47
|
+
request = Net::HTTP::Get.new(uri)
|
|
48
|
+
@headers.each { |k, v| request[k.to_s] = v.to_s }
|
|
49
|
+
|
|
50
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
51
|
+
http.request(request)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
55
|
+
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-schema"
|
|
4
|
+
|
|
5
|
+
module SharedBroker
|
|
6
|
+
module SchemaRegistry
|
|
7
|
+
module Providers
|
|
8
|
+
class Local
|
|
9
|
+
def initialize
|
|
10
|
+
@schemas = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(topic, schema)
|
|
14
|
+
unless schema.respond_to?(:call)
|
|
15
|
+
raise ArgumentError, "Expected schema to respond to :call, got #{schema.class} with value #{schema.inspect}. Expected shape: respond_to?(:call)"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@schemas[topic.to_s] = schema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def validate!(topic, payload)
|
|
22
|
+
schema = @schemas[topic.to_s]
|
|
23
|
+
return unless schema
|
|
24
|
+
|
|
25
|
+
result = schema.call(payload)
|
|
26
|
+
return if result.success?
|
|
27
|
+
|
|
28
|
+
raise SharedBroker::Validation::ValidationError,
|
|
29
|
+
"Schema validation failed for topic #{topic.inspect}. Expected keys: #{schema.rules.keys.inspect}, got payload: #{payload.inspect}. Errors: #{result.errors.to_h.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear
|
|
33
|
+
@schemas.clear
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedBroker
|
|
4
|
+
module SchemaRegistry
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :provider
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.validate!(topic, payload)
|
|
10
|
+
resolved_provider = provider || default_provider
|
|
11
|
+
resolved_provider.validate!(topic, payload)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clear_cache
|
|
15
|
+
return unless provider.respond_to?(:clear_cache)
|
|
16
|
+
|
|
17
|
+
provider.clear_cache
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.default_provider
|
|
21
|
+
@default_provider ||= SharedBroker::SchemaRegistry::Providers::Local.new
|
|
22
|
+
end
|
|
23
|
+
private_class_method :default_provider
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def self.clear
|
|
29
|
-
@schemas.clear
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedBroker
|
|
4
|
+
module Validation
|
|
5
|
+
class ValidationError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def self.register(topic, schema)
|
|
8
|
+
provider = SharedBroker::SchemaRegistry.provider || SharedBroker::SchemaRegistry.send(:default_provider)
|
|
9
|
+
if provider.respond_to?(:register)
|
|
10
|
+
provider.register(topic, schema)
|
|
11
|
+
else
|
|
12
|
+
raise RuntimeError, "Current SchemaRegistry provider #{provider.class} does not support local registration. Expected a provider that responds to :register."
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.validate!(topic, message)
|
|
17
|
+
SharedBroker::SchemaRegistry.validate!(topic, message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.clear
|
|
21
|
+
SharedBroker::SchemaRegistry.clear_cache
|
|
22
|
+
provider = SharedBroker::SchemaRegistry.provider || SharedBroker::SchemaRegistry.send(:default_provider)
|
|
23
|
+
provider.clear if provider.respond_to?(:clear)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SharedBroker
|
|
4
|
-
VERSION = "1.
|
|
5
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SharedBroker
|
|
4
|
+
VERSION = "1.4.0"
|
|
5
|
+
end
|
data/lib/shared_broker.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "shared_broker/version"
|
|
4
4
|
require_relative "shared_broker/telemetry"
|
|
5
5
|
require_relative "shared_broker/circuit_breaker"
|
|
6
|
+
require_relative "shared_broker/schema_registry"
|
|
7
|
+
require_relative "shared_broker/schema_registry/providers/local"
|
|
8
|
+
require_relative "shared_broker/schema_registry/providers/http"
|
|
6
9
|
require_relative "shared_broker/validation"
|
|
7
10
|
require_relative "shared_broker/cipher"
|
|
8
11
|
require_relative "shared_broker/middleware_pipeline"
|
|
@@ -22,16 +25,11 @@ module SharedBroker
|
|
|
22
25
|
@encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
|
|
23
26
|
|
|
24
27
|
class Client
|
|
25
|
-
attr_reader :circuit_breaker, :middleware_pipeline
|
|
28
|
+
attr_reader :circuit_breaker, :middleware_pipeline, :adapters, :routing
|
|
26
29
|
|
|
27
|
-
def initialize(adapter
|
|
28
|
-
|
|
29
|
-
raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got #{adapter.class} with value #{adapter.inspect}"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
@adapter = adapter
|
|
30
|
+
def initialize(adapter: nil, adapters: nil, routing: nil, circuit_breaker: nil, middlewares: nil)
|
|
31
|
+
setup_adapters(adapter: adapter, adapters: adapters, routing: routing)
|
|
33
32
|
@circuit_breaker = circuit_breaker || CircuitBreaker.new
|
|
34
|
-
|
|
35
33
|
resolved_middlewares = middlewares || [SharedBroker::Middlewares::OpenTelemetryPropagation.new]
|
|
36
34
|
@middleware_pipeline = MiddlewarePipeline.new(resolved_middlewares)
|
|
37
35
|
end
|
|
@@ -43,13 +41,13 @@ module SharedBroker
|
|
|
43
41
|
encrypted_msg = SharedBroker::Cipher.encrypt(message, SharedBroker.encryption_key)
|
|
44
42
|
|
|
45
43
|
@circuit_breaker.run do
|
|
46
|
-
|
|
44
|
+
resolve_adapter(topic).publish(topic, encrypted_msg, correlation_id: correlation_id)
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
|
|
52
|
-
|
|
50
|
+
resolve_adapter(topic).subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
|
|
53
51
|
decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, SharedBroker.encryption_key)
|
|
54
52
|
SharedBroker::Validation.validate!(topic, decrypted_msg)
|
|
55
53
|
|
|
@@ -59,5 +57,55 @@ module SharedBroker
|
|
|
59
57
|
end
|
|
60
58
|
end
|
|
61
59
|
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def setup_adapters(adapter: nil, adapters: nil, routing: nil)
|
|
64
|
+
if adapter || (adapters.nil? && routing.nil?)
|
|
65
|
+
validate_single_adapter!(adapter)
|
|
66
|
+
@adapters = { default: adapter }
|
|
67
|
+
@routing = { "*" => :default }
|
|
68
|
+
else
|
|
69
|
+
validate_multi_adapters!(adapters, routing)
|
|
70
|
+
@adapters = adapters
|
|
71
|
+
@routing = routing.transform_keys(&:to_s)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_single_adapter!(adapter)
|
|
76
|
+
unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
|
|
77
|
+
raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got: #{adapter.inspect} (must shape like SharedBroker::Adapters::Base)"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_multi_adapters!(adapters, routing)
|
|
82
|
+
unless adapters.is_a?(Hash) && routing.is_a?(Hash)
|
|
83
|
+
raise ArgumentError, "Expected adapters and routing to be Hashes, got adapters: #{adapters.inspect} (class: #{adapters.class}), routing: #{routing.inspect} (class: #{routing.class})"
|
|
84
|
+
end
|
|
85
|
+
adapters.each do |key, ad|
|
|
86
|
+
unless ad.respond_to?(:publish) && ad.respond_to?(:subscribe)
|
|
87
|
+
raise ArgumentError, "Expected adapter #{key.inspect} to respond to :publish and :subscribe, got: #{ad.inspect} (class: #{ad.class})"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_adapter(topic)
|
|
93
|
+
topic_str = topic.to_s
|
|
94
|
+
return @adapters[@routing[topic_str]] if @routing.key?(topic_str)
|
|
95
|
+
|
|
96
|
+
@routing.each do |pattern, adapter_key|
|
|
97
|
+
next if pattern == "*"
|
|
98
|
+
if File.fnmatch?(pattern, topic_str)
|
|
99
|
+
return @adapters[adapter_key]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
fallback_key = @routing["*"]
|
|
104
|
+
if fallback_key && @adapters.key?(fallback_key)
|
|
105
|
+
@adapters[fallback_key]
|
|
106
|
+
else
|
|
107
|
+
raise RuntimeError, "No adapter resolved for topic: #{topic.inspect}. Expected one of #{@routing.keys.inspect}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
62
110
|
end
|
|
63
111
|
end
|
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.4.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-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '1.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: json-schema
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '4.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '4.0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: opentelemetry-api
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -143,6 +157,9 @@ files:
|
|
|
143
157
|
- lib/shared_broker/circuit_breaker.rb
|
|
144
158
|
- lib/shared_broker/middleware_pipeline.rb
|
|
145
159
|
- lib/shared_broker/middlewares/open_telemetry_propagation.rb
|
|
160
|
+
- lib/shared_broker/schema_registry.rb
|
|
161
|
+
- lib/shared_broker/schema_registry/providers/http.rb
|
|
162
|
+
- lib/shared_broker/schema_registry/providers/local.rb
|
|
146
163
|
- lib/shared_broker/telemetry.rb
|
|
147
164
|
- lib/shared_broker/validation.rb
|
|
148
165
|
- lib/shared_broker/version.rb
|