shared_broker 1.1.0 → 1.2.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: a691e6fa4ea7eb0d9b7974a55ed217c25a325be29c7beba0211d05633e947c02
4
- data.tar.gz: '09abf8886190595bbcf09d7faeaf0676f848c763b8efef6ed092c8b23eac5853'
3
+ metadata.gz: d47439f3383e9d40d13664b67dc8b77318b64f628e6a1efa0f4684130ff2ff4b
4
+ data.tar.gz: 85d081f58ac1608a1629b29f14c634b370b963c65338eb7f9a32ace26bc90502
5
5
  SHA512:
6
- metadata.gz: 96d856ee56c3d1db971a0a751920d03b65c3f67922b77ef67e42443f2586cc6e6063a5301fff1e050e8f2b81ab334e4479d80739604938c76f1880824692b097
7
- data.tar.gz: 46d39087d7e875d84cd8a82223e68edadd965651e795c013c1da85cbfc847663fe594ed90bf8b1f60c5eb7ade7f14056f4aed3fbb5fe2a46e43fa5bc7aaf7ed0
6
+ metadata.gz: 25151e9182b26d17334f04b121831703eee6c8b95981ac2b235085472501f06ec97c2a6b14cedc434dd3ee49963577b8eceedc175680525025d31e3246ac81dd
7
+ data.tar.gz: 07cb36e946dd24260fdd70d7cd995f4aeae8167b764d1df18e2026bc523481ec4d788308f720494472985a3b1a4adc7ae8751ded5abbd20b02c8c475c10a97c6
data/CHANGELOG.md ADDED
@@ -0,0 +1,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.2.0] - 2026-06-07
9
+
10
+ ### Added
11
+ - Pipeline of customizable Middlewares (Interceptors) for publishing and subscribing messages.
12
+ - Distributed Tracing using W3C Trace Context (`traceparent`/`tracestate`) context injection and extraction.
13
+
14
+ ## [1.1.1] - 2026-06-06
15
+
16
+ ### Changed
17
+ - Restructured `README.md` to clearly separate minimum required configuration from optional/advanced setups.
18
+
19
+ ## [1.1.0] - 2026-06-06
20
+
21
+ ### Added
22
+ - Scalable Adapters:
23
+ - Apache Kafka adapter (`SharedBroker::Adapters::Kafka`) with dynamic dependency loading.
24
+ - Redis Pub/Sub adapter (`SharedBroker::Adapters::Redis`) with list-based DLQ routing.
25
+ - Mock test configurations for Kafka and Redis to run adapter unit tests.
26
+
27
+ ## [1.0.0] - 2026-06-06
28
+
29
+ ### Added
30
+ - Fault Tolerance & Resilience features:
31
+ - Exponential backoff retry loop for message processing.
32
+ - Automatic Dead Letter Queue (DLQ) routing with rich metadata headers (`x_failed_at`, `x_exception_class`, `x_exception_message`, `x_original_routing_key`).
33
+ - Thread-safe `CircuitBreaker` wrapping all outbound publisher calls.
34
+ - Schema Validation and Security:
35
+ - Outbound/Inbound boundary validation using `dry-schema`.
36
+ - Transparent AES-256-GCM symmetric payload encryption by default, configurable with `SharedBroker.encryption_key`.
37
+ - Comprehensive test coverage for all the above features using isolated fakes and Minitest.
38
+
39
+ ## [0.1.0] - 2026-06-06
40
+
41
+ ### Added
42
+ - Initial release with the pluggable `Client` messaging system.
43
+ - `InMemory` adapter for local testing.
44
+ - `RabbitMQ` adapter using the `bunny` gem.
45
+ - Basic OpenTelemetry instrumentation utility (`SharedBroker::Telemetry`).
data/README.md CHANGED
@@ -44,36 +44,55 @@ bundle install
44
44
 
45
45
  ## Configuration
46
46
 
47
- Create an initializer in your Rails application (`config/initializers/shared_broker.rb`):
47
+ Create an initializer in your Rails application (`config/initializers/shared_broker.rb`). Below is the breakdown of what is **required** versus what is **optional**.
48
+
49
+ ### 1. Required Configuration (Minimum Setup)
50
+
51
+ 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):
48
52
 
49
53
  ```ruby
50
54
  require "shared_broker"
51
55
 
52
- # 1. Configure Validation Schemas (dry-schema)
53
- user_created_schema = Dry::Schema.Params do
54
- required(:id).filled(:integer)
55
- required(:email).filled(:string)
56
- end
57
- SharedBroker::Validation.register("user.created", user_created_schema)
58
-
59
- # 2. Configure Payload Encryption Key (AES-256-GCM)
60
- # Expects a 32-byte string. Default key is used in development if ENV is not set.
56
+ # A. Configure Payload Encryption Key (AES-256-GCM)
57
+ # Expects a 32-byte string. Use a secure production key in production.
61
58
  SharedBroker.encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
62
59
 
63
- # 3. Configure the Adapter based on Environment
60
+ # B. Configure the Adapter based on Environment
64
61
  if Rails.env.test?
65
62
  # In-memory adapter prevents external queue dependency during unit tests
66
63
  BROKER_ADAPTER = SharedBroker::Adapters::InMemory.new
67
- elsif Rails.env.production?
68
- # High-throughput production setup using Kafka
69
- BROKER_ADAPTER = SharedBroker::Adapters::Kafka.new(seed_brokers: ["kafka-1:9092", "kafka-2:9092"])
70
64
  else
71
- # Connects to RabbitMQ or Redis for development
65
+ # Connects to real RabbitMQ broker
72
66
  amqp_url = ENV.fetch("RABBITMQ_URL") { "amqp://guest:guest@localhost:5672" }
73
67
  BROKER_ADAPTER = SharedBroker::Adapters::RabbitMQ.new(amqp_url: amqp_url)
74
68
  end
75
69
 
76
- # 4. Instantiate the Client by Injecting the Adapter and optional custom Circuit Breaker configuration
70
+ # C. Instantiate the Client by Injecting the Adapter
71
+ SPOT_BROKER = SharedBroker::Client.new(adapter: BROKER_ADAPTER)
72
+ ```
73
+
74
+ ---
75
+
76
+ ### 2. Optional Configuration
77
+
78
+ These features can be configured optionally depending on your needs.
79
+
80
+ #### A. Event Payload Validation (dry-schema)
81
+ Register schemas to validate payload structure automatically on outbound (`publish`) and inbound (`subscribe`) boundaries:
82
+
83
+ ```ruby
84
+ user_created_schema = Dry::Schema.Params do
85
+ required(:id).filled(:integer)
86
+ required(:email).filled(:string)
87
+ end
88
+
89
+ SharedBroker::Validation.register("user.created", user_created_schema)
90
+ ```
91
+
92
+ #### B. Custom Circuit Breaker
93
+ By default, the client instantiates a standard Circuit Breaker. You can provide a custom one to tune the failure threshold and recovery window:
94
+
95
+ ```ruby
77
96
  custom_circuit_breaker = SharedBroker::CircuitBreaker.new(
78
97
  failure_threshold: 5, # trip circuit after 5 failures
79
98
  recovery_timeout: 30 # wait 30 seconds before attempting recovery
@@ -83,8 +102,12 @@ SPOT_BROKER = SharedBroker::Client.new(
83
102
  adapter: BROKER_ADAPTER,
84
103
  circuit_breaker: custom_circuit_breaker
85
104
  )
105
+ ```
106
+
107
+ #### C. Initialize Distributed Tracing (OpenTelemetry)
108
+ Initialize the OpenTelemetry SDK with auto-instrumentation for the microservice:
86
109
 
87
- # 5. Initialize Telemetry (OpenTelemetry)
110
+ ```ruby
88
111
  SharedBroker::Telemetry.configure(service_name: "my_microservice")
89
112
  ```
90
113
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SharedBroker
4
+ class MiddlewarePipeline
5
+ def initialize(middlewares)
6
+ @middlewares = Array(middlewares)
7
+ end
8
+
9
+ def execute(topic, message, metadata = {}, &block)
10
+ run_middleware(0, topic, message, metadata, &block)
11
+ end
12
+
13
+ private
14
+
15
+ def run_middleware(index, topic, message, metadata, &block)
16
+ if index >= @middlewares.size
17
+ yield
18
+ else
19
+ @middlewares[index].call(topic, message, metadata) do
20
+ run_middleware(index + 1, topic, message, metadata, &block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "opentelemetry"
4
+
5
+ module SharedBroker
6
+ module Middlewares
7
+ class OpenTelemetryPropagation
8
+ def call(topic, message, metadata)
9
+ if metadata[:operation] == :publish
10
+ carrier = {}
11
+ ::OpenTelemetry.propagation.inject(carrier)
12
+ carrier.each do |key, value|
13
+ message["_#{key}".to_sym] = value
14
+ end
15
+ yield
16
+ elsif metadata[:operation] == :subscribe
17
+ carrier = {}
18
+ message.each do |key, value|
19
+ if key.to_s.start_with?("_trace")
20
+ carrier[key.to_s.sub(/^_/, "")] = value
21
+ end
22
+ end
23
+
24
+ parent_context = ::OpenTelemetry.propagation.extract(carrier)
25
+ ::OpenTelemetry::Context.with_current(parent_context) do
26
+ tracer = ::OpenTelemetry.tracer_provider.tracer("shared_broker")
27
+ tracer.in_span("#{topic} process", kind: :consumer) do |span|
28
+ span.set_attribute("messaging.system", "shared_broker")
29
+ span.set_attribute("messaging.destination", topic)
30
+ yield
31
+ end
32
+ end
33
+ else
34
+ yield
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SharedBroker
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/shared_broker.rb CHANGED
@@ -1,51 +1,63 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "shared_broker/version"
4
- require_relative "shared_broker/telemetry"
5
- require_relative "shared_broker/circuit_breaker"
6
- require_relative "shared_broker/validation"
7
- require_relative "shared_broker/cipher"
8
- require_relative "shared_broker/adapters/base"
9
- require_relative "shared_broker/adapters/in_memory"
10
- require_relative "shared_broker/adapters/rabbit_mq"
11
- require_relative "shared_broker/adapters/kafka"
12
- require_relative "shared_broker/adapters/redis"
13
-
14
- module SharedBroker
15
- class << self
16
- attr_accessor :encryption_key
17
- end
18
-
19
- # Default key for development/test if not set
20
- @encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
21
-
22
- class Client
23
- attr_reader :circuit_breaker
24
-
25
- def initialize(adapter:, circuit_breaker: nil)
26
- unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
27
- raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got #{adapter.class} with value #{adapter.inspect}"
28
- end
29
-
30
- @adapter = adapter
31
- @circuit_breaker = circuit_breaker || CircuitBreaker.new
32
- end
33
-
34
- def publish(topic, message, correlation_id: nil)
35
- SharedBroker::Validation.validate!(topic, message)
36
- encrypted_msg = SharedBroker::Cipher.encrypt(message, SharedBroker.encryption_key)
37
-
38
- @circuit_breaker.run do
39
- @adapter.publish(topic, encrypted_msg, correlation_id: correlation_id)
40
- end
41
- end
42
-
43
- def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
44
- @adapter.subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
45
- decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, SharedBroker.encryption_key)
46
- SharedBroker::Validation.validate!(topic, decrypted_msg)
47
- block.call(decrypted_msg)
48
- end
49
- end
50
- end
51
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shared_broker/version"
4
+ require_relative "shared_broker/telemetry"
5
+ require_relative "shared_broker/circuit_breaker"
6
+ require_relative "shared_broker/validation"
7
+ require_relative "shared_broker/cipher"
8
+ require_relative "shared_broker/middleware_pipeline"
9
+ require_relative "shared_broker/middlewares/open_telemetry_propagation"
10
+ require_relative "shared_broker/adapters/base"
11
+ require_relative "shared_broker/adapters/in_memory"
12
+ require_relative "shared_broker/adapters/rabbit_mq"
13
+ require_relative "shared_broker/adapters/kafka"
14
+ require_relative "shared_broker/adapters/redis"
15
+
16
+ module SharedBroker
17
+ class << self
18
+ attr_accessor :encryption_key
19
+ end
20
+
21
+ # Default key for development/test if not set
22
+ @encryption_key = ENV.fetch("SHARED_BROKER_ENCRYPTION_KEY") { "a" * 32 }
23
+
24
+ class Client
25
+ attr_reader :circuit_breaker, :middleware_pipeline
26
+
27
+ def initialize(adapter:, circuit_breaker: nil, middlewares: nil)
28
+ unless adapter.respond_to?(:publish) && adapter.respond_to?(:subscribe)
29
+ raise ArgumentError, "Expected adapter to respond to :publish and :subscribe, got #{adapter.class} with value #{adapter.inspect}"
30
+ end
31
+
32
+ @adapter = adapter
33
+ @circuit_breaker = circuit_breaker || CircuitBreaker.new
34
+
35
+ resolved_middlewares = middlewares || [SharedBroker::Middlewares::OpenTelemetryPropagation.new]
36
+ @middleware_pipeline = MiddlewarePipeline.new(resolved_middlewares)
37
+ end
38
+
39
+ def publish(topic, message, correlation_id: nil)
40
+ metadata = { correlation_id: correlation_id, operation: :publish }
41
+ @middleware_pipeline.execute(topic, message, metadata) do
42
+ SharedBroker::Validation.validate!(topic, message)
43
+ encrypted_msg = SharedBroker::Cipher.encrypt(message, SharedBroker.encryption_key)
44
+
45
+ @circuit_breaker.run do
46
+ @adapter.publish(topic, encrypted_msg, correlation_id: correlation_id)
47
+ end
48
+ end
49
+ end
50
+
51
+ def subscribe(topic, queue_name, max_retries: 3, backoff_base: 2, &block)
52
+ @adapter.subscribe(topic, queue_name, max_retries: max_retries, backoff_base: backoff_base) do |raw_message|
53
+ decrypted_msg = SharedBroker::Cipher.decrypt(raw_message, SharedBroker.encryption_key)
54
+ SharedBroker::Validation.validate!(topic, decrypted_msg)
55
+
56
+ metadata = { correlation_id: decrypted_msg[:_correlation_id], operation: :subscribe, queue_name: queue_name }
57
+ @middleware_pipeline.execute(topic, decrypted_msg, metadata) do
58
+ block.call(decrypted_msg)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Gemini Antigravity
7
+ - Wesley Lima
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-06 00:00:00.000000000 Z
11
+ date: 2026-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -125,11 +125,12 @@ dependencies:
125
125
  description: Shared library for asynchronous messaging, distributed tracing, and customizable
126
126
  adapters.
127
127
  email:
128
- - antigravity@google.com
128
+ - wesleyskap@gmail.com
129
129
  executables: []
130
130
  extensions: []
131
131
  extra_rdoc_files: []
132
132
  files:
133
+ - CHANGELOG.md
133
134
  - README.md
134
135
  - Rakefile
135
136
  - lib/shared_broker.rb
@@ -140,6 +141,8 @@ files:
140
141
  - lib/shared_broker/adapters/redis.rb
141
142
  - lib/shared_broker/cipher.rb
142
143
  - lib/shared_broker/circuit_breaker.rb
144
+ - lib/shared_broker/middleware_pipeline.rb
145
+ - lib/shared_broker/middlewares/open_telemetry_propagation.rb
143
146
  - lib/shared_broker/telemetry.rb
144
147
  - lib/shared_broker/validation.rb
145
148
  - lib/shared_broker/version.rb
@@ -149,7 +152,7 @@ licenses:
149
152
  - MIT
150
153
  metadata:
151
154
  source_code_uri: https://github.com/wesleyskap/shared_broker
152
- changelog_uri: https://github.com/wesleyskap/shared_broker/blob/main/CHANGELOG.md
155
+ changelog_uri: https://github.com/wesleyskap/shared_broker/blob/master/CHANGELOG.md
153
156
  post_install_message:
154
157
  rdoc_options: []
155
158
  require_paths: