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 +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/harmoniser/channel.rb +66 -0
- data/lib/harmoniser/connectable.rb +2 -46
- data/lib/harmoniser/launcher/bounded.rb +1 -1
- data/lib/harmoniser/mock/channel.rb +92 -0
- data/lib/harmoniser/mock/connection.rb +38 -0
- data/lib/harmoniser/mock.rb +56 -0
- data/lib/harmoniser/publisher.rb +6 -6
- data/lib/harmoniser/subscriber.rb +1 -1
- data/lib/harmoniser/topology.rb +6 -5
- data/lib/harmoniser/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a7c3ac5b97ffe50dee2438808e152133893642092fee47a74e49f31e5440686
|
|
4
|
+
data.tar.gz: 156dbce5e8ba12711085e55d2a2b6f823259946d0df42d6cccd7bd29aeba0139
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
.
|
|
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
|
data/lib/harmoniser/publisher.rb
CHANGED
|
@@ -33,12 +33,12 @@ module Harmoniser
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def create_exchange
|
|
36
|
-
exchange =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
data/lib/harmoniser/topology.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
63
|
+
channel.queue(queue.name, queue.opts)
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
|
data/lib/harmoniser/version.rb
CHANGED
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.
|
|
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-
|
|
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
|