harmoniser 0.13.0 → 0.14.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: 71c030f6ce0d93a7e9bb55a88c3d3cad300f4ab05809944e08442d2bca43cafb
4
- data.tar.gz: da8f3f99f2a9846b35b10a8e986f28e53087a81d2a1a6fd149778e3d82222de1
3
+ metadata.gz: 3a7c3ac5b97ffe50dee2438808e152133893642092fee47a74e49f31e5440686
4
+ data.tar.gz: 156dbce5e8ba12711085e55d2a2b6f823259946d0df42d6cccd7bd29aeba0139
5
5
  SHA512:
6
- metadata.gz: 0774eab4d7682a72e880e764acdb55e05e6e9d10048bbdb3e74ed010293d6dc8c416d9de9d73c3a6752e3c9d1ace45d70dc212a3cc01fb2a33b77d4a494d56d2
7
- data.tar.gz: 2eebe48b9779d08b0e516d13daa9f94b358f851de3999d4ca0ee600cab89ae48396bdd351ff583bc9be566e44f113f2fe14c0ef59ce3378d5fe1f35d88802c83
6
+ metadata.gz: 3c14b5e163b0d3863ae1e25db3dc7aaec5ac6debacfe6cb0148066b5154156619b3692a7ea1e72d03d27e407a6366ad93c879476e04e606f864522ba56ffec59
7
+ data.tar.gz: 546204550ed925fac2e177240ec3266a25d61d81d9034019f76653588af7141c797be2ed42d576000cb7de72699843333b59cfafdb3d50a80a7a31a9596e1172
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0] - 2025-11-25
4
+
5
+ ### Added
6
+ - Introduce Mock functionality for testing without RabbitMQ dependency. Usage:
7
+
8
+ ```ruby
9
+ require "harmoniser/mock"
10
+ Harmoniser::Mock.mock!
11
+
12
+ # Your Publisher/Subscriber code works the same but without RabbitMQ
13
+ class TestPublisher
14
+ include Harmoniser::Publisher
15
+ harmoniser_publisher exchange_name: "my_exchange"
16
+ end
17
+
18
+ exchange = TestPublisher.publish("message", routing_key: "test")
19
+ published_messages = exchange.published_messages # Access captured messages
20
+ ```
21
+
22
+ - Add examples for mock publishing and topology under `examples/` folder
23
+ - Create Channel wrapper (`Harmoniser::Channel`) to isolate RabbitMQ dependencies
24
+
25
+ ### Changed
26
+ - Remove explicit dependencies to Bunny classes except for Consumer, enabling better RabbitMQ isolation through a single, well-defined interface
27
+ - Use Channel wrapper everywhere except for subscriptions (which still require actual Channel instance for Bunny::Consumer)
28
+
29
+
3
30
  ## [0.13.0] - 2025-05-19
4
31
 
5
32
  ### Added
@@ -0,0 +1,66 @@
1
+ require "forwardable"
2
+
3
+ module Harmoniser
4
+ class Channel
5
+ extend Forwardable
6
+
7
+ def_delegators :@bunny_channel,
8
+ :exchange,
9
+ :queue,
10
+ :queue_bind
11
+
12
+ attr_reader :bunny_channel
13
+
14
+ def initialize(bunny_channel)
15
+ @bunny_channel = bunny_channel
16
+ after_initialize
17
+ end
18
+
19
+ private
20
+
21
+ def after_initialize
22
+ bunny_channel.cancel_consumers_before_closing!
23
+ attach_callbacks
24
+ end
25
+
26
+ def attach_callbacks
27
+ bunny_channel.on_error(&method(:on_error_callback).to_proc)
28
+ bunny_channel.on_uncaught_exception(&method(:on_uncaught_exception_callback).to_proc)
29
+ end
30
+
31
+ def on_error_callback(channel, amq_method)
32
+ attributes = {
33
+ amq_method: amq_method,
34
+ exchanges: channel.exchanges.keys,
35
+ queues: channel.consumers.values.map(&:queue)
36
+ }
37
+
38
+ if amq_method.is_a?(AMQ::Protocol::Channel::Close)
39
+ attributes[:reply_code] = amq_method.reply_code
40
+ attributes[:reply_text] = amq_method.reply_text
41
+ end
42
+
43
+ stringified_attributes = attributes.map { |k, v| "#{k} = `#{v}`" }.join(", ")
44
+ Harmoniser.logger.warn("Default on_error handler executed for channel: #{stringified_attributes}")
45
+ maybe_kill_process(amq_method)
46
+ end
47
+
48
+ def on_uncaught_exception_callback(error, consumer)
49
+ handle_error(error, {description: "Uncaught exception from consumer", arguments: consumer.arguments, channel_id: consumer.channel.id, consumer_tag: consumer.consumer_tag, exclusive: consumer.exclusive, no_ack: consumer.no_ack, queue: consumer.queue})
50
+ end
51
+
52
+ def maybe_kill_process(amq_method)
53
+ Process.kill("USR1", Process.pid) if ack_timed_out?(amq_method) && Harmoniser.server?
54
+ end
55
+
56
+ def ack_timed_out?(amq_method)
57
+ return false unless amq_method.is_a?(AMQ::Protocol::Channel::Close)
58
+
59
+ amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
60
+ end
61
+
62
+ def handle_error(exception, ctx)
63
+ Harmoniser.configuration.handle_error(exception, ctx)
64
+ end
65
+ end
66
+ end
@@ -1,4 +1,5 @@
1
1
  require "harmoniser/connection"
2
+ require "harmoniser/channel"
2
3
 
3
4
  module Harmoniser
4
5
  module Connectable
@@ -20,52 +21,7 @@ module Harmoniser
20
21
  def create_channel(consumer_pool_size: 1, consumer_pool_shutdown_timeout: 60)
21
22
  connection
22
23
  .create_channel(nil, consumer_pool_size, false, consumer_pool_shutdown_timeout)
23
- .tap do |channel|
24
- channel.cancel_consumers_before_closing!
25
- attach_callbacks(channel)
26
- end
27
- end
28
-
29
- private
30
-
31
- def attach_callbacks(channel)
32
- channel.on_error(&method(:on_error).to_proc)
33
- channel.on_uncaught_exception(&method(:on_uncaught_exception).to_proc)
34
- end
35
-
36
- def on_error(channel, amq_method)
37
- attributes = {
38
- amq_method: amq_method,
39
- exchanges: channel.exchanges.keys,
40
- queues: channel.consumers.values.map(&:queue)
41
- }
42
-
43
- if amq_method.is_a?(AMQ::Protocol::Channel::Close)
44
- attributes[:reply_code] = amq_method.reply_code
45
- attributes[:reply_text] = amq_method.reply_text
46
- end
47
-
48
- stringified_attributes = attributes.map { |k, v| "#{k} = `#{v}`" }.join(", ")
49
- Harmoniser.logger.warn("Default on_error handler executed for channel: #{stringified_attributes}")
50
- maybe_kill_process(amq_method)
51
- end
52
-
53
- def on_uncaught_exception(error, consumer)
54
- handle_error(error, {description: "Uncaught exception from consumer", arguments: consumer.arguments, channel_id: consumer.channel.id, consumer_tag: consumer.consumer_tag, exclusive: consumer.exclusive, no_ack: consumer.no_ack, queue: consumer.queue})
55
- end
56
-
57
- def maybe_kill_process(amq_method)
58
- Process.kill("USR1", Process.pid) if ack_timed_out?(amq_method) && Harmoniser.server?
59
- end
60
-
61
- def ack_timed_out?(amq_method)
62
- return false unless amq_method.is_a?(AMQ::Protocol::Channel::Close)
63
-
64
- amq_method.reply_text =~ /delivery acknowledgement on channel \d+ timed out/
65
- end
66
-
67
- def handle_error(exception, ctx)
68
- Harmoniser.configuration.handle_error(exception, ctx)
24
+ .yield_self { |bunny_channel| Channel.new(bunny_channel) }
69
25
  end
70
26
  end
71
27
 
@@ -12,7 +12,7 @@ module Harmoniser
12
12
  end
13
13
 
14
14
  def channel
15
- @channel ||= Subscriber.create_channel(consumer_pool_size: @configuration.concurrency, consumer_pool_shutdown_timeout: @configuration.timeout)
15
+ @channel ||= Subscriber.create_channel(consumer_pool_size: @configuration.concurrency, consumer_pool_shutdown_timeout: @configuration.timeout).bunny_channel
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Harmoniser
4
+ module Mock
5
+ class Channel
6
+ class MockExchange
7
+ attr_reader :name, :type, :opts
8
+
9
+ def initialize(name, opts = {})
10
+ @name = name
11
+ @type = opts[:type]
12
+ @opts = opts.except(:type)
13
+ @published_messages = []
14
+ @return_handler = nil
15
+ end
16
+
17
+ def publish(payload, opts = {})
18
+ @published_messages << {payload: payload, opts: opts}
19
+ true
20
+ end
21
+
22
+ def on_return(&block)
23
+ @return_handler = block
24
+ self
25
+ end
26
+
27
+ def published_messages
28
+ @published_messages.dup
29
+ end
30
+
31
+ def reset!
32
+ @published_messages.clear
33
+ @return_handler = nil
34
+ end
35
+ end
36
+
37
+ class MockQueue
38
+ attr_reader :name, :opts
39
+
40
+ def initialize(name, opts = {})
41
+ @name = name
42
+ @opts = opts
43
+ end
44
+ end
45
+
46
+ def initialize
47
+ @exchanges = {}
48
+ @queues = {}
49
+ @bindings = []
50
+ end
51
+
52
+ def exchange(name, opts = {})
53
+ @exchanges[name] ||= MockExchange.new(name, opts)
54
+ end
55
+
56
+ def queue(name, opts = {})
57
+ @queues[name] ||= MockQueue.new(name, opts)
58
+ end
59
+
60
+ def queue_bind(destination_name, exchange_name, opts = {})
61
+ @bindings << {
62
+ destination_name: destination_name,
63
+ exchange_name: exchange_name,
64
+ opts: opts
65
+ }
66
+ true
67
+ end
68
+
69
+ def exchanges
70
+ @exchanges.dup
71
+ end
72
+
73
+ def queues
74
+ @queues.dup
75
+ end
76
+
77
+ def bindings
78
+ @bindings.dup
79
+ end
80
+
81
+ def reset!
82
+ @exchanges.clear
83
+ @queues.clear
84
+ @bindings.clear
85
+ end
86
+
87
+ def bunny_channel
88
+ raise "Cannot access bunny_channel in mock mode. Mock mode is intended for testing only and cannot be used when running Harmoniser as a server process."
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "harmoniser/mock/channel"
4
+
5
+ module Harmoniser
6
+ module Mock
7
+ class Connection
8
+ def initialize(opts = {}, error_handler: nil, logger: nil)
9
+ @opts = opts
10
+ @error_handler = error_handler
11
+ @logger = logger
12
+ @open = false
13
+ end
14
+
15
+ def create_channel(id = nil, consumer_pool_size = 1, consumer_pool_ack = false, consumer_pool_shutdown_timeout = 60)
16
+ Channel.new
17
+ end
18
+
19
+ def open?
20
+ @open
21
+ end
22
+
23
+ def recovering_from_network_failure?
24
+ false
25
+ end
26
+
27
+ def start
28
+ @open = true
29
+ self
30
+ end
31
+
32
+ def close
33
+ @open = false
34
+ true
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "harmoniser/mock/connection"
4
+ require "harmoniser/mock/channel"
5
+ require "harmoniser/connectable"
6
+
7
+ module Harmoniser
8
+ module Mock
9
+ @mocked = false
10
+ @prepended = false
11
+
12
+ class << self
13
+ def mock!
14
+ unless @prepended
15
+ @prepended = true
16
+ Harmoniser::Connectable::ClassMethods.prepend(MockConnectableMethods)
17
+ end
18
+ @mocked = true
19
+ end
20
+
21
+ def disable!
22
+ @mocked = false
23
+ end
24
+
25
+ def mocked?
26
+ @mocked
27
+ end
28
+
29
+ def disabled?
30
+ !@mocked
31
+ end
32
+ end
33
+
34
+ module MockConnectableMethods
35
+ def connection(configuration = Harmoniser.configuration)
36
+ return super unless Harmoniser::Mock.mocked?
37
+
38
+ Harmoniser::Connectable::MUTEX.synchronize do
39
+ @mock_connection ||= Harmoniser::Mock::Connection.new(configuration.connection_opts, error_handler: configuration.error_handler)
40
+ @mock_connection.start unless @mock_connection.open? || @mock_connection.recovering_from_network_failure?
41
+ @mock_connection
42
+ end
43
+ end
44
+
45
+ def connection?
46
+ return super unless Harmoniser::Mock.mocked?
47
+ !!defined?(@mock_connection)
48
+ end
49
+
50
+ def create_channel(consumer_pool_size: 1, consumer_pool_shutdown_timeout: 60)
51
+ return super unless Harmoniser::Mock.mocked?
52
+ connection.create_channel(nil, consumer_pool_size, false, consumer_pool_shutdown_timeout)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -33,12 +33,12 @@ module Harmoniser
33
33
  end
34
34
 
35
35
  def create_exchange
36
- exchange = Bunny::Exchange.new(
37
- Publisher.create_channel,
38
- @harmoniser_exchange_definition.type,
39
- @harmoniser_exchange_definition.name,
40
- @harmoniser_exchange_definition.opts
41
- )
36
+ exchange = Publisher
37
+ .create_channel
38
+ .exchange(
39
+ @harmoniser_exchange_definition.name,
40
+ {type: @harmoniser_exchange_definition.type}.merge(@harmoniser_exchange_definition.opts)
41
+ )
42
42
  handle_return(exchange)
43
43
  exchange
44
44
  end
@@ -41,7 +41,7 @@ module Harmoniser
41
41
  def create_consumer(channel)
42
42
  raise_missing_consumer_definition unless @harmoniser_consumer_definition
43
43
 
44
- ch = channel || Subscriber.create_channel
44
+ ch = channel || Subscriber.create_channel.bunny_channel
45
45
  consumer = Bunny::Consumer.new(
46
46
  ch,
47
47
  @harmoniser_consumer_definition.queue_name,
@@ -5,7 +5,7 @@ module Harmoniser
5
5
  class Topology
6
6
  include Connectable
7
7
 
8
- attr_reader :bindings, :exchanges, :queues
8
+ attr_reader :bindings, :exchanges, :queues, :declared_channel
9
9
 
10
10
  def initialize
11
11
  @bindings = Set.new
@@ -41,25 +41,26 @@ module Harmoniser
41
41
  end
42
42
 
43
43
  def declare
44
- self.class.create_channel.tap do |ch|
44
+ @declared_channel = self.class.create_channel.tap do |ch|
45
45
  declare_exchanges(ch)
46
46
  declare_queues(ch)
47
47
  declare_bindings(ch)
48
- ch.connection.close
49
48
  end
49
+ self.class.connection.close
50
+ self
50
51
  end
51
52
 
52
53
  private
53
54
 
54
55
  def declare_exchanges(channel)
55
56
  exchanges.each do |exchange|
56
- Bunny::Exchange.new(channel, exchange.type, exchange.name, exchange.opts)
57
+ channel.exchange(exchange.name, {type: exchange.type}.merge(exchange.opts))
57
58
  end
58
59
  end
59
60
 
60
61
  def declare_queues(channel)
61
62
  queues.each do |queue|
62
- Bunny::Queue.new(channel, queue.name, queue.opts)
63
+ channel.queue(queue.name, queue.opts)
63
64
  end
64
65
  end
65
66
 
@@ -1,3 +1,3 @@
1
1
  module Harmoniser
2
- VERSION = "0.13.0"
2
+ VERSION = "0.14.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: harmoniser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Lloret
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-19 00:00:00.000000000 Z
11
+ date: 2025-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -42,6 +42,7 @@ files:
42
42
  - bin/setup
43
43
  - harmoniser.gemspec
44
44
  - lib/harmoniser.rb
45
+ - lib/harmoniser/channel.rb
45
46
  - lib/harmoniser/cli.rb
46
47
  - lib/harmoniser/configurable.rb
47
48
  - lib/harmoniser/configuration.rb
@@ -54,6 +55,9 @@ files:
54
55
  - lib/harmoniser/launcher/bounded.rb
55
56
  - lib/harmoniser/launcher/unbounded.rb
56
57
  - lib/harmoniser/loggable.rb
58
+ - lib/harmoniser/mock.rb
59
+ - lib/harmoniser/mock/channel.rb
60
+ - lib/harmoniser/mock/connection.rb
57
61
  - lib/harmoniser/options.rb
58
62
  - lib/harmoniser/parser.rb
59
63
  - lib/harmoniser/publisher.rb