freddy 0.7.0 → 0.7.1

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
  SHA1:
3
- metadata.gz: 97120d29c1d86841e5dfc5158c31f14d54336387
4
- data.tar.gz: 874dbaf996a7c8b77a6311e8689c991db17ecb9b
3
+ metadata.gz: 157ccf48c23813d9fdb90dd43aa3fc0e6ee8f1f0
4
+ data.tar.gz: 108c3d3d1f11e56fc09d71898d82a2339dffbdbe
5
5
  SHA512:
6
- metadata.gz: b88611a8cfde225622dba6b36d024732f5eaf6d436cd933262d9e2dafd76f412cc535f0e0bc6ea6a8445c2270cfd17b99a3c910448e7fdf61bbed6fa02740e77
7
- data.tar.gz: 0346023d9e030b033e32b14322694ed16c4c14fe465cf7d93f54d907308b8799aa64f493cfe36cecca04cb93da40c771008bc751e7fc0b172707605d14dc6b33
6
+ metadata.gz: e41fa3d55fbe4a56f796000e547b2c8764a6ad26e3520a42b5a139d22c60c84625d0659ae94071ea0ae5ddacbf55fb90c492499fefe73f40a9cbee709b214485
7
+ data.tar.gz: 44f2cc1f17506c03691cdf9cd429ddaba6e057c75e88c4375608be2dc05e9e7ba621e619f85763022913c9b48146f28145f192c6660ec2bc4ce70547806cfce3
data/README.md CHANGED
@@ -138,10 +138,7 @@ responder_handler.shutdown
138
138
 
139
139
  ## Notes about concurrency
140
140
 
141
- The underlying bunny implementation uses 1 responder thread by default. This means that if there is a time-consuming process or a sleep call in a responder then other responders will not receive messages concurrently.
142
- To resolve this problem *freddy* uses a thread pool for running concurrent responders.
143
- The thread pool is shared between *tap_into* and *respond_to* callbacks and the default size is 4.
144
- The thread pool size can be configured by passing the configuration option *max_concurrency*.
141
+ *freddy* uses a thread pool to run concurrent responders. The thread pool is unique for each *tap_into* and *respond_to* responder. Thread pool size can be configured by passing the configuration option *max_concurrency*. Its default value is 4. e.g. If your application has 2 *respond_to* responders and 1 *tap_into* responder with *max_concurrency* set to 3 then your application may process up to 9 messages in parallel.
145
142
 
146
143
 
147
144
  Note that while it is possible to use *deliver_with_response* inside a *respond_to* block,
data/freddy.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  else
9
9
  spec.name = "freddy"
10
10
  end
11
- spec.version = '0.7.0'
11
+ spec.version = '0.7.1'
12
12
  spec.authors = ["Urmas Talimaa"]
13
13
  spec.email = ["urmas.talimaa@gmail.com"]
14
14
  spec.description = %q{Messaging API}
@@ -13,8 +13,10 @@ class Freddy
13
13
  @bunny = bunny
14
14
  end
15
15
 
16
- def create_channel
17
- Channel.new(@bunny.create_channel)
16
+ def create_channel(prefetch: nil)
17
+ bunny_channel = @bunny.create_channel
18
+ bunny_channel.prefetch(prefetch) if prefetch
19
+ Channel.new(bunny_channel)
18
20
  end
19
21
 
20
22
  def close
@@ -30,7 +32,7 @@ class Freddy
30
32
  @channel = channel
31
33
  end
32
34
 
33
- def_delegators :@channel, :topic, :default_exchange, :consumers
35
+ def_delegators :@channel, :topic, :default_exchange, :consumers, :acknowledge
34
36
 
35
37
  def queue(*args)
36
38
  Queue.new(@channel.queue(*args))
@@ -46,10 +48,13 @@ class Freddy
46
48
  end
47
49
 
48
50
  class Queue < Shared::Queue
49
- def subscribe(&block)
50
- @queue.subscribe do |info, properties, payload|
51
+ def subscribe(manual_ack: false, &block)
52
+ @queue.subscribe(manual_ack: manual_ack) do |info, properties, payload|
51
53
  parsed_payload = Payload.parse(payload)
52
- block.call(Delivery.new(parsed_payload, properties, info.routing_key))
54
+ delivery = Delivery.new(
55
+ parsed_payload, properties, info.routing_key, info.delivery_tag
56
+ )
57
+ block.call(delivery)
53
58
  end
54
59
  end
55
60
  end
@@ -12,8 +12,10 @@ class Freddy
12
12
  @hare = hare
13
13
  end
14
14
 
15
- def create_channel
16
- Channel.new(@hare.create_channel)
15
+ def create_channel(prefetch: nil)
16
+ hare_channel = @hare.create_channel
17
+ hare_channel.basic_qos(prefetch) if prefetch
18
+ Channel.new(hare_channel)
17
19
  end
18
20
 
19
21
  def close
@@ -29,7 +31,7 @@ class Freddy
29
31
  @channel = channel
30
32
  end
31
33
 
32
- def_delegators :@channel, :topic, :default_exchange, :consumers
34
+ def_delegators :@channel, :topic, :default_exchange, :consumers, :acknowledge
33
35
 
34
36
  def queue(*args)
35
37
  Queue.new(@channel.queue(*args))
@@ -45,10 +47,13 @@ class Freddy
45
47
  end
46
48
 
47
49
  class Queue < Shared::Queue
48
- def subscribe(&block)
49
- @queue.subscribe do |meta, payload|
50
+ def subscribe(manual_ack: false, &block)
51
+ @queue.subscribe(manual_ack: manual_ack) do |meta, payload|
50
52
  parsed_payload = Payload.parse(payload)
51
- block.call(Delivery.new(parsed_payload, meta, meta.routing_key))
53
+ delivery = Delivery.new(
54
+ parsed_payload, meta, meta.routing_key, meta.delivery_tag
55
+ )
56
+ block.call(delivery)
52
57
  end
53
58
  end
54
59
  end
@@ -24,6 +24,10 @@ class Freddy
24
24
  def name
25
25
  @queue.name
26
26
  end
27
+
28
+ def message_count
29
+ @queue.message_count
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -1,16 +1,23 @@
1
1
  class Freddy
2
2
  module Consumers
3
3
  class RespondToConsumer
4
- def initialize(consume_thread_pool, logger)
5
- @consume_thread_pool = consume_thread_pool
4
+ def self.consume(*attrs, &block)
5
+ new(*attrs).consume(&block)
6
+ end
7
+
8
+ def initialize(logger:, thread_pool:, destination:, channel:, handler_adapter_factory:)
6
9
  @logger = logger
10
+ @consume_thread_pool = thread_pool
11
+ @destination = destination
12
+ @channel = channel
13
+ @handler_adapter_factory = handler_adapter_factory
7
14
  end
8
15
 
9
- def consume(destination, channel, handler_adapter_factory, &block)
10
- consumer = consume_from_destination(destination, channel) do |delivery|
11
- Consumers.log_receive_event(@logger, destination, delivery)
16
+ def consume(&block)
17
+ consumer = consume_from_destination do |delivery|
18
+ Consumers.log_receive_event(@logger, @destination, delivery)
12
19
 
13
- adapter = handler_adapter_factory.for(delivery)
20
+ adapter = @handler_adapter_factory.for(delivery)
14
21
 
15
22
  msg_handler = MessageHandler.new(adapter, delivery)
16
23
  block.call(delivery.payload, msg_handler)
@@ -21,15 +28,19 @@ class Freddy
21
28
 
22
29
  private
23
30
 
24
- def consume_from_destination(destination, channel, &block)
25
- channel.queue(destination).subscribe do |delivery|
31
+ def consume_from_destination(&block)
32
+ @channel.queue(@destination).subscribe(manual_ack: true) do |delivery|
26
33
  process_message(delivery, &block)
27
34
  end
28
35
  end
29
36
 
30
37
  def process_message(delivery, &block)
31
38
  @consume_thread_pool.process do
32
- block.call(delivery)
39
+ begin
40
+ block.call(delivery)
41
+ ensure
42
+ @channel.acknowledge(delivery.tag, false)
43
+ end
33
44
  end
34
45
  end
35
46
  end
@@ -3,24 +3,20 @@ class Freddy
3
3
  class ResponseConsumer
4
4
  def initialize(logger)
5
5
  @logger = logger
6
- @dedicated_thread_pool = Thread.pool(1)
7
6
  end
8
7
 
9
- def consume(queue, &block)
8
+ def consume(channel, queue, &block)
10
9
  @logger.debug "Consuming messages on #{queue.name}"
11
- consumer = queue.subscribe do |delivery|
12
- process_message(queue, delivery, &block)
10
+ queue.subscribe do |delivery|
11
+ process_message(channel, queue, delivery, &block)
13
12
  end
14
- ResponderHandler.new(consumer, @dedicated_thread_pool)
15
13
  end
16
14
 
17
15
  private
18
16
 
19
- def process_message(queue, delivery, &block)
20
- @dedicated_thread_pool.process do
21
- Consumers.log_receive_event(@logger, queue.name, delivery)
22
- block.call(delivery)
23
- end
17
+ def process_message(channel, queue, delivery, &block)
18
+ Consumers.log_receive_event(@logger, queue.name, delivery)
19
+ block.call(delivery)
24
20
  end
25
21
  end
26
22
  end
@@ -1,15 +1,22 @@
1
1
  class Freddy
2
2
  module Consumers
3
3
  class TapIntoConsumer
4
- def initialize(consume_thread_pool, logger)
4
+ def self.consume(*attrs, &block)
5
+ new(*attrs).consume(&block)
6
+ end
7
+
8
+ def initialize(logger:, thread_pool:, pattern:, channel:, options:)
5
9
  @logger = logger
6
- @consume_thread_pool = consume_thread_pool
10
+ @consume_thread_pool = thread_pool
11
+ @pattern = pattern
12
+ @channel = channel
13
+ @options = options
7
14
  end
8
15
 
9
- def consume(pattern, channel, options, &block)
10
- queue = create_queue(pattern, channel, options)
16
+ def consume(&block)
17
+ queue = create_queue
11
18
 
12
- consumer = queue.subscribe do |delivery|
19
+ consumer = queue.subscribe(manual_ack: true) do |delivery|
13
20
  process_message(queue, delivery, &block)
14
21
  end
15
22
 
@@ -18,24 +25,29 @@ class Freddy
18
25
 
19
26
  private
20
27
 
21
- def create_queue(pattern, channel, group: nil)
22
- topic_exchange = channel.topic(Freddy::FREDDY_TOPIC_EXCHANGE_NAME)
28
+ def create_queue
29
+ topic_exchange = @channel.topic(Freddy::FREDDY_TOPIC_EXCHANGE_NAME)
30
+ group = @options.fetch(:group, nil)
23
31
 
24
32
  if group
25
- channel
33
+ @channel
26
34
  .queue("groups.#{group}")
27
- .bind(topic_exchange, routing_key: pattern)
35
+ .bind(topic_exchange, routing_key: @pattern)
28
36
  else
29
- channel
37
+ @channel
30
38
  .queue('', exclusive: true)
31
- .bind(topic_exchange, routing_key: pattern)
39
+ .bind(topic_exchange, routing_key: @pattern)
32
40
  end
33
41
  end
34
42
 
35
43
  def process_message(queue, delivery, &block)
36
44
  @consume_thread_pool.process do
37
- Consumers.log_receive_event(@logger, queue.name, delivery)
38
- block.call delivery.payload, delivery.routing_key
45
+ begin
46
+ Consumers.log_receive_event(@logger, queue.name, delivery)
47
+ block.call delivery.payload, delivery.routing_key
48
+ ensure
49
+ @channel.acknowledge(delivery.tag, false)
50
+ end
39
51
  end
40
52
  end
41
53
  end
@@ -1,11 +1,12 @@
1
1
  class Freddy
2
2
  class Delivery
3
- attr_reader :routing_key, :payload
3
+ attr_reader :routing_key, :payload, :tag
4
4
 
5
- def initialize(payload, metadata, routing_key)
5
+ def initialize(payload, metadata, routing_key, tag)
6
6
  @payload = payload
7
7
  @metadata = metadata
8
8
  @routing_key = routing_key
9
+ @tag = tag
9
10
  end
10
11
 
11
12
  def correlation_id
@@ -19,7 +19,7 @@ class Freddy
19
19
  @response_queue = @channel.queue("", exclusive: true)
20
20
 
21
21
  @response_consumer = Consumers::ResponseConsumer.new(@logger)
22
- @response_consumer.consume(@response_queue, &method(:handle_response))
22
+ @response_consumer.consume(@channel, @response_queue, &method(:handle_response))
23
23
  end
24
24
 
25
25
  def produce(destination, payload, timeout_in_seconds:, delete_on_timeout:, **properties)
data/lib/freddy.rb CHANGED
@@ -17,6 +17,7 @@ class Freddy
17
17
  # @option config [Integer] :port (5672)
18
18
  # @option config [String] :user ('guest')
19
19
  # @option config [String] :pass ('guest')
20
+ # @option config [Integer] :max_concurrency (4)
20
21
  #
21
22
  # @return [Freddy]
22
23
  #
@@ -24,17 +25,14 @@ class Freddy
24
25
  # Freddy.build(Logger.new(STDOUT), user: 'thumper', pass: 'howdy')
25
26
  def self.build(logger = Logger.new(STDOUT), max_concurrency: DEFAULT_MAX_CONCURRENCY, **config)
26
27
  connection = Adapters.determine.connect(config)
27
- consume_thread_pool = Thread.pool(max_concurrency)
28
28
 
29
- new(connection, logger, consume_thread_pool)
29
+ new(connection, logger, max_concurrency)
30
30
  end
31
31
 
32
- def initialize(connection, logger, consume_thread_pool)
32
+ def initialize(connection, logger, max_concurrency)
33
33
  @connection = connection
34
34
  @logger = logger
35
-
36
- @tap_into_consumer = Consumers::TapIntoConsumer.new(consume_thread_pool, @logger)
37
- @respond_to_consumer = Consumers::RespondToConsumer.new(consume_thread_pool, @logger)
35
+ @prefetch_buffer_size = max_concurrency
38
36
 
39
37
  @send_and_forget_producer = Producers::SendAndForgetProducer.new(
40
38
  connection.create_channel, logger
@@ -72,12 +70,17 @@ class Freddy
72
70
  def respond_to(destination, &callback)
73
71
  @logger.info "Listening for requests on #{destination}"
74
72
 
75
- channel = @connection.create_channel
73
+ channel = @connection.create_channel(prefetch: @prefetch_buffer_size)
76
74
  producer = Producers::ReplyProducer.new(channel, @logger)
77
75
  handler_adapter_factory = MessageHandlerAdapters::Factory.new(producer)
78
76
 
79
- @respond_to_consumer.consume(
80
- destination, channel, handler_adapter_factory, &callback
77
+ Consumers::RespondToConsumer.consume(
78
+ logger: @logger,
79
+ thread_pool: Thread.pool(@prefetch_buffer_size),
80
+ destination: destination,
81
+ channel: channel,
82
+ handler_adapter_factory: handler_adapter_factory,
83
+ &callback
81
84
  )
82
85
  end
83
86
 
@@ -105,7 +108,15 @@ class Freddy
105
108
  # end
106
109
  def tap_into(pattern, options = {}, &callback)
107
110
  @logger.debug "Tapping into messages that match #{pattern}"
108
- @tap_into_consumer.consume(pattern, @connection.create_channel, options, &callback)
111
+
112
+ Consumers::TapIntoConsumer.consume(
113
+ logger: @logger,
114
+ thread_pool: Thread.pool(@prefetch_buffer_size),
115
+ pattern: pattern,
116
+ channel: @connection.create_channel(prefetch: @prefetch_buffer_size),
117
+ options: options,
118
+ &callback
119
+ )
109
120
  end
110
121
 
111
122
  # Sends a message to given destination
@@ -1,24 +1,84 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Freddy::Consumers::RespondToConsumer do
4
- let(:consumer) { described_class.new(thread_pool, logger) }
4
+ let(:consumer) do
5
+ described_class.new(
6
+ logger: logger,
7
+ thread_pool: thread_pool,
8
+ destination: destination,
9
+ channel: channel,
10
+ handler_adapter_factory: msg_handler_adapter_factory
11
+ )
12
+ end
5
13
 
6
- let(:connection) { Freddy::Adapters.determine.connect(config) }
7
- let(:thread_pool) { Thread.pool(1) }
14
+ let(:connection) { Freddy::Adapters.determine.connect(config) }
8
15
  let(:destination) { random_destination }
9
- let(:payload) { {pay: 'load'} }
10
- let(:msg_handler) { double }
16
+ let(:payload) { {pay: 'load'} }
17
+ let(:msg_handler_adapter_factory) { double(for: msg_handler_adapter) }
18
+ let(:msg_handler_adapter) { Freddy::MessageHandlerAdapters::NoOpHandler.new }
19
+ let(:prefetch_buffer_size) { 2 }
20
+ let(:thread_pool) { Thread.pool(prefetch_buffer_size) }
11
21
 
12
22
  after do
13
23
  connection.close
14
24
  end
15
25
 
16
- it "doesn't call passed block without any messages" do
17
- consumer.consume destination, connection.create_channel, msg_handler do
18
- @message_received = true
26
+ context 'when no messages' do
27
+ let(:channel) { connection.create_channel }
28
+
29
+ it "doesn't call passed block" do
30
+ consumer.consume do
31
+ @message_received = true
32
+ end
33
+ default_sleep
34
+
35
+ expect(@message_received).to be_falsy
36
+ end
37
+ end
38
+
39
+ context 'when thread pool is full' do
40
+ let(:prefetch_buffer_size) { 1 }
41
+ let(:msg_count) { prefetch_buffer_size + 1 }
42
+ let(:channel) { connection.create_channel(prefetch: prefetch_buffer_size) }
43
+ let(:mutex) { Mutex.new }
44
+ let(:consume_lock) { ConditionVariable.new }
45
+ let(:queue) { channel.queue(destination) }
46
+
47
+ after do
48
+ # Release the final queued message before finishing the test to avoid
49
+ # bunny warnings.
50
+ process_message
19
51
  end
20
- default_sleep
21
52
 
22
- expect(@message_received).to be_falsy
53
+ it 'does not consume more messages' do
54
+ consumer.consume do
55
+ wait_until_released
56
+ end
57
+
58
+ msg_count.times { deliver_message }
59
+
60
+ sleep default_sleep
61
+ expect(queue.message_count).to eq(msg_count - prefetch_buffer_size)
62
+
63
+ process_message
64
+ expect(queue.message_count).to eq(0)
65
+ end
66
+
67
+ def process_message
68
+ release_consume_lock
69
+ sleep default_sleep
70
+ end
71
+
72
+ def deliver_message
73
+ channel.default_exchange.publish('{}', routing_key: destination)
74
+ end
75
+
76
+ def wait_until_released
77
+ mutex.synchronize { consume_lock.wait(mutex) }
78
+ end
79
+
80
+ def release_consume_lock
81
+ mutex.synchronize { consume_lock.broadcast }
82
+ end
23
83
  end
24
84
  end
@@ -13,12 +13,12 @@ describe Freddy::ResponderHandler do
13
13
  count = 0
14
14
 
15
15
  consumer_handler = freddy.respond_to destination do
16
- sleep 0.1
16
+ sleep 0.3
17
17
  count += 1
18
18
  end
19
19
  deliver
20
20
 
21
- sleep 0.05
21
+ sleep 0.15
22
22
  consumer_handler.shutdown
23
23
 
24
24
  expect(count).to eq(1)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: freddy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Urmas Talimaa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-14 00:00:00.000000000 Z
11
+ date: 2017-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -151,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
151
  version: '0'
152
152
  requirements: []
153
153
  rubyforge_project:
154
- rubygems_version: 2.5.1
154
+ rubygems_version: 2.6.8
155
155
  signing_key:
156
156
  specification_version: 4
157
157
  summary: API for inter-application messaging supporting acknowledgements and request-response