freddy 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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