freddy 0.4.9 → 0.5.0

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: 8aee28c6b8c64e053dfd864eb43e05f4d350606c
4
- data.tar.gz: 3cf659c4cb47be3b564fe992da89603c6c3649cf
3
+ metadata.gz: 204b0809f519d05653570d35444e705960059438
4
+ data.tar.gz: 288c0710940438afff7d86d1e55d5baeb1c4adc4
5
5
  SHA512:
6
- metadata.gz: fcc93283eb0ff7d457ab33f4f4c87d1c6969a60b3fc7bda09b21cd4fa88fa198089d1b4317bab95bfb2b88a7ab6df2abb29af1bc42116954fa9bfa23027f84db
7
- data.tar.gz: 46079754f78b019f506445d46040ff846e02568acd497a053dd0c1067a44a4329b18d5c3beefa8abfab7b0d55dcde9effc5e616112a15f54497a46818ee3313e
6
+ metadata.gz: 078ba7ed00fff8ace0932121c56d71f7bb0149fa56444cd6c3c68506d5a687a645a5a3551445b3e7320ae22fa636aee63031c5e6b809735ef9ebe81279f7af3a
7
+ data.tar.gz: e5ef6ddd2fd83dac05c97a5e005c019b46dada0759f901e0b3e9d390f0e995f9a50007b88a22681a60b6e9d2c5ed713f61aa97b31f5f2d29d2d200a1be86bf13
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  else
9
9
  spec.name = "freddy"
10
10
  end
11
- spec.version = '0.4.9'
11
+ spec.version = '0.5.0'
12
12
  spec.authors = ["Urmas Talimaa"]
13
13
  spec.email = ["urmas.talimaa@gmail.com"]
14
14
  spec.description = %q{Messaging API}
@@ -1,18 +1,12 @@
1
1
  require 'json'
2
2
  require 'thread/pool'
3
+ require 'hamster/mutable_hash'
3
4
 
4
- require_relative 'freddy/adapters'
5
- require_relative 'freddy/consumer'
6
- require_relative 'freddy/producer'
7
- require_relative 'freddy/request'
8
- require_relative 'freddy/payload'
9
- require_relative 'freddy/error_response'
10
- require_relative 'freddy/invalid_request_error'
11
- require_relative 'freddy/timeout_error'
12
- require_relative 'freddy/utils'
5
+ Dir[File.dirname(__FILE__) + '/freddy/*.rb'].each(&method(:require))
13
6
 
14
7
  class Freddy
15
8
  FREDDY_TOPIC_EXCHANGE_NAME = 'freddy-topic'.freeze
9
+ DEFAULT_MAX_CONCURRENCY = 4
16
10
 
17
11
  # Creates a new freddy instance
18
12
  #
@@ -29,30 +23,84 @@ class Freddy
29
23
  #
30
24
  # @example
31
25
  # Freddy.build(Logger.new(STDOUT), user: 'thumper', pass: 'howdy')
32
- def self.build(logger = Logger.new(STDOUT), config = {})
26
+ def self.build(logger = Logger.new(STDOUT), max_concurrency: DEFAULT_MAX_CONCURRENCY, **config)
33
27
  connection = Adapters.determine.connect(config)
28
+ consume_thread_pool = Thread.pool(max_concurrency)
34
29
 
35
- new(connection, logger, config.fetch(:max_concurrency, 4))
30
+ new(connection, logger, consume_thread_pool)
36
31
  end
37
32
 
38
- attr_reader :channel, :consumer, :producer, :request
39
-
40
- def initialize(connection, logger, max_concurrency)
33
+ def initialize(connection, logger, consume_thread_pool)
41
34
  @connection = connection
42
- @channel = connection.create_channel
43
- @consume_thread_pool = Thread.pool(max_concurrency)
44
- @producer = Producer.new channel, logger
45
- @consumer = Consumer.new logger, @consume_thread_pool, @producer, @connection
46
- @request = Request.new channel, logger, @producer, @consumer
35
+ @logger = logger
36
+
37
+ @tap_into_consumer = Consumers::TapIntoConsumer.new(consume_thread_pool)
38
+ @respond_to_consumer = Consumers::RespondToConsumer.new(consume_thread_pool, @logger)
39
+
40
+ @send_and_forget_producer = Producers::SendAndForgetProducer.new(
41
+ connection.create_channel, logger
42
+ )
43
+ @send_and_wait_response_producer = Producers::SendAndWaitResponseProducer.new(
44
+ connection.create_channel, logger
45
+ )
47
46
  end
48
47
  private :initialize
49
48
 
49
+ # Listens and responds to messages
50
+ #
51
+ # This consumes messages on a given destination. It is useful for messages
52
+ # that have to be processed once and then a result must be sent.
53
+ #
54
+ # @param [String] destination
55
+ # the queue name
56
+ #
57
+ # @yieldparam [Hash<Symbol => Object>] message
58
+ # Received message as a ruby hash with symbolized keys
59
+ # @yieldparam [#success, #error] handler
60
+ # Handler for responding to messages. Use handler#success for successful
61
+ # respone and handler#error for error response.
62
+ #
63
+ # @return [#shutdown]
64
+ #
65
+ # @example
66
+ # freddy.respond_to 'RegistrationService' do |attributes, handler|
67
+ # if id = register(attributes)
68
+ # handler.success(id: id)
69
+ # else
70
+ # handler.error(message: 'Can not do')
71
+ # end
72
+ # end
50
73
  def respond_to(destination, &callback)
51
- @consumer.respond_to destination, &callback
74
+ @logger.info "Listening for requests on #{destination}"
75
+
76
+ channel = @connection.create_channel
77
+ producer = Producers::ReplyProducer.new(channel, @logger)
78
+ handler_factory = MessageHandlers::Factory.new(producer, @logger)
79
+
80
+ @respond_to_consumer.consume(destination, channel, handler_factory, &callback)
52
81
  end
53
82
 
83
+ # Listens for messages without consuming them
84
+ #
85
+ # This listens for messages on a given destination or destinations without
86
+ # consuming them. It is useful for general messages that two or more clients
87
+ # are interested.
88
+ #
89
+ # @param [String] pattern
90
+ # the destination pattern. Use `#` wildcard for matching 0 or more words.
91
+ # Use `*` to match exactly one word.
92
+ #
93
+ # @yield [message] Yields received message to the block
94
+ #
95
+ # @return [#shutdown]
96
+ #
97
+ # @example
98
+ # freddy.tap_into 'notifications.*' do |message|
99
+ # puts "Notification showed #{message.inspect}"
100
+ # end
54
101
  def tap_into(pattern, &callback)
55
- @consumer.tap_into pattern, &callback
102
+ @logger.debug "Tapping into messages that match #{pattern}"
103
+ @tap_into_consumer.consume(pattern, @connection.create_channel, &callback)
56
104
  end
57
105
 
58
106
  # Sends a message to given destination
@@ -81,7 +129,7 @@ class Freddy
81
129
  opts = {}
82
130
  opts[:expiration] = (timeout * 1000).to_i if timeout > 0
83
131
 
84
- @producer.produce destination, payload, opts
132
+ @send_and_forget_producer.produce(destination, payload, opts)
85
133
  end
86
134
 
87
135
  # Sends a message and waits for the response
@@ -117,8 +165,8 @@ class Freddy
117
165
  timeout = options.fetch(:timeout, 3)
118
166
  delete_on_timeout = options.fetch(:delete_on_timeout, true)
119
167
 
120
- @request.sync_request destination, payload, {
121
- timeout: timeout, delete_on_timeout: delete_on_timeout
168
+ @send_and_wait_response_producer.produce destination, payload, {
169
+ timeout_in_seconds: timeout, delete_on_timeout: delete_on_timeout
122
170
  }
123
171
  end
124
172
 
@@ -24,6 +24,8 @@ class Freddy
24
24
  class Channel
25
25
  extend Forwardable
26
26
 
27
+ NO_ROUTE = 312
28
+
27
29
  def initialize(channel)
28
30
  @channel = channel
29
31
  end
@@ -34,9 +36,11 @@ class Freddy
34
36
  Queue.new(@channel.queue(*args))
35
37
  end
36
38
 
37
- def on_return(&block)
39
+ def on_no_route(&block)
38
40
  default_exchange.on_return do |return_info, properties, content|
39
- block.call(return_info[:reply_code], properties[:correlation_id])
41
+ if return_info[:reply_code] == NO_ROUTE
42
+ block.call(properties[:correlation_id])
43
+ end
40
44
  end
41
45
  end
42
46
  end
@@ -23,6 +23,8 @@ class Freddy
23
23
  class Channel
24
24
  extend Forwardable
25
25
 
26
+ NO_ROUTE = 312
27
+
26
28
  def initialize(channel)
27
29
  @channel = channel
28
30
  end
@@ -33,10 +35,10 @@ class Freddy
33
35
  Queue.new(@channel.queue(*args))
34
36
  end
35
37
 
36
- def on_return(&block)
38
+ def on_no_route(&block)
37
39
  @channel.on_return do |reply_code, _, exchange_name, _, properties|
38
- if exchange_name != Freddy::FREDDY_TOPIC_EXCHANGE_NAME
39
- block.call(reply_code, properties.correlation_id)
40
+ if exchange_name != Freddy::FREDDY_TOPIC_EXCHANGE_NAME && reply_code == NO_ROUTE
41
+ block.call(properties.correlation_id)
40
42
  end
41
43
  end
42
44
  end
@@ -0,0 +1 @@
1
+ Dir[File.dirname(__FILE__) + '/consumers/*.rb'].each(&method(:require))
@@ -1,18 +1,16 @@
1
1
  class Freddy
2
2
  module Consumers
3
3
  class RespondToConsumer
4
- def initialize(consume_thread_pool, producer, logger)
4
+ def initialize(consume_thread_pool, logger)
5
5
  @consume_thread_pool = consume_thread_pool
6
- @producer = producer
7
6
  @logger = logger
8
7
  end
9
8
 
10
- def consume(destination, channel, &block)
9
+ def consume(destination, channel, handler_factory, &block)
11
10
  consumer = consume_from_destination(destination, channel) do |delivery|
12
11
  log_receive_event(destination, delivery)
13
12
 
14
- handler_class = MessageHandlers.for_type(delivery.type)
15
- handler = handler_class.new(@producer, destination, @logger)
13
+ handler = handler_factory.build(delivery.type, destination)
16
14
 
17
15
  msg_handler = MessageHandler.new(handler, delivery)
18
16
  handler.handle_message delivery.payload, msg_handler, &block
@@ -7,6 +7,7 @@ class Freddy
7
7
  end
8
8
 
9
9
  def consume(queue, &block)
10
+ @logger.debug "Consuming messages on #{queue.name}"
10
11
  consumer = queue.subscribe do |delivery|
11
12
  process_message(queue, delivery, &block)
12
13
  end
@@ -1,13 +1,23 @@
1
1
  class Freddy
2
2
  module MessageHandlers
3
- def self.for_type(type)
4
- type == 'request' ? RequestHandler : StandardMessageHandler
3
+ class Factory
4
+ def initialize(producer, logger)
5
+ @producer = producer
6
+ @logger = logger
7
+ end
8
+
9
+ def build(type, destination)
10
+ if type == 'request'
11
+ RequestHandler.new(@producer, destination, @logger)
12
+ else
13
+ StandardMessageHandler.new(destination, @logger)
14
+ end
15
+ end
5
16
  end
6
17
 
7
18
  class StandardMessageHandler
8
- def initialize(producer, destination, logger)
19
+ def initialize(destination, logger)
9
20
  @destination = destination
10
- @producer = producer
11
21
  @logger = logger
12
22
  end
13
23
 
@@ -39,7 +49,6 @@ class Freddy
39
49
 
40
50
  if !@correlation_id
41
51
  @logger.error "Received request without correlation_id"
42
- Utils.notify_exception(e)
43
52
  else
44
53
  block.call payload, msg_handler
45
54
  end
@@ -0,0 +1 @@
1
+ Dir[File.dirname(__FILE__) + '/producers/*.rb'].each(&method(:require))
@@ -0,0 +1,22 @@
1
+ class Freddy
2
+ module Producers
3
+ class ReplyProducer
4
+ CONTENT_TYPE = 'application/json'.freeze
5
+
6
+ def initialize(channel, logger)
7
+ @logger = logger
8
+ @exchange = channel.default_exchange
9
+ end
10
+
11
+ def produce(destination, payload, properties)
12
+ @logger.debug "Sending message #{payload.inspect} to #{destination}"
13
+
14
+ properties = properties.merge(
15
+ routing_key: destination, content_type: CONTENT_TYPE
16
+ )
17
+
18
+ @exchange.publish Payload.dump(payload), properties
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ class Freddy
2
+ module Producers
3
+ class SendAndForgetProducer
4
+ CONTENT_TYPE = 'application/json'.freeze
5
+
6
+ def initialize(channel, logger)
7
+ @logger = logger
8
+ @exchange = channel.default_exchange
9
+ @topic_exchange = channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
10
+ end
11
+
12
+ def produce(destination, payload, properties)
13
+ @logger.debug "Sending message #{payload.inspect} to #{destination}"
14
+
15
+ properties = properties.merge(routing_key: destination, content_type: CONTENT_TYPE)
16
+ json_payload = Payload.dump(payload)
17
+
18
+ # Connection adapters handle thread safety for #publish themselves. No
19
+ # need to lock these.
20
+ @topic_exchange.publish json_payload, properties.dup
21
+ @exchange.publish json_payload, properties.dup
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,89 @@
1
+ class Freddy
2
+ module Producers
3
+ class SendAndWaitResponseProducer
4
+ CONTENT_TYPE = 'application/json'.freeze
5
+
6
+ def initialize(channel, logger)
7
+ @logger = logger
8
+ @channel = channel
9
+
10
+ @request_manager = RequestManager.new(@logger)
11
+
12
+ @exchange = @channel.default_exchange
13
+ @topic_exchange = @channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
14
+
15
+ @channel.on_no_route do |correlation_id|
16
+ @request_manager.no_route(correlation_id)
17
+ end
18
+
19
+ @response_queue = @channel.queue("", exclusive: true)
20
+ @request_manager.start
21
+
22
+ @response_consumer = Consumers::ResponseConsumer.new(@logger)
23
+ @response_consumer.consume(@response_queue, &method(:handle_response))
24
+ end
25
+
26
+ def produce(destination, payload, properties)
27
+ timeout_in_seconds = properties.fetch(:timeout_in_seconds)
28
+ container = SyncResponseContainer.new
29
+ async_request destination, payload, properties, &container
30
+ container.wait_for_response(timeout_in_seconds + 0.1)
31
+ end
32
+
33
+ private
34
+
35
+ def async_request(destination, payload, timeout_in_seconds:, delete_on_timeout:, **properties, &block)
36
+ correlation_id = SecureRandom.uuid
37
+ @request_manager.store(correlation_id,
38
+ callback: block,
39
+ destination: destination,
40
+ expires_at: Time.now + timeout_in_seconds
41
+ )
42
+
43
+ if delete_on_timeout
44
+ properties[:expiration] = (timeout_in_seconds * 1000).to_i
45
+ end
46
+
47
+ properties = properties.merge(
48
+ routing_key: destination, content_type: CONTENT_TYPE,
49
+ correlation_id: correlation_id, reply_to: @response_queue.name,
50
+ mandatory: true, type: 'request'
51
+ )
52
+ json_payload = Payload.dump(payload)
53
+
54
+ @logger.debug "Publishing request with payload #{payload.inspect} "\
55
+ "to #{destination}, waiting for response on "\
56
+ "#{@response_queue.name} with correlation_id #{correlation_id}"
57
+
58
+ # Connection adapters handle thread safety for #publish themselves. No
59
+ # need to lock these.
60
+ @topic_exchange.publish json_payload, properties.dup
61
+ @exchange.publish json_payload, properties.dup
62
+ end
63
+
64
+ def handle_response(delivery)
65
+ correlation_id = delivery.correlation_id
66
+
67
+ if request = @request_manager.delete(correlation_id)
68
+ process_response(request, delivery)
69
+ else
70
+ warning = "Got rpc response for correlation_id #{correlation_id} "\
71
+ "but there is no requester"
72
+ @logger.warn message
73
+ Utils.notify 'NoRequesterForResponse', warning, correlation_id: correlation_id
74
+ end
75
+ end
76
+
77
+ def process_response(request, delivery)
78
+ @logger.debug "Got response for request to #{request[:destination]} "\
79
+ "with correlation_id #{delivery.correlation_id}"
80
+ request[:callback].call(delivery.payload, delivery)
81
+ rescue => e
82
+ @logger.error "Exception occured while handling the response of "\
83
+ "request made to #{request[:destination]} with "\
84
+ "correlation_id #{correlation_id}: #{Utils.format_exception(e)}"
85
+ Utils.notify_exception(e, destination: request[:destination], correlation_id: correlation_id)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,8 +1,9 @@
1
1
  class Freddy
2
2
  class RequestManager
3
3
 
4
- def initialize(requests, logger)
5
- @requests, @logger = requests, logger
4
+ def initialize(logger)
5
+ @requests = Hamster.mutable_hash
6
+ @logger = logger
6
7
  end
7
8
 
8
9
  def start
@@ -21,11 +22,19 @@ class Freddy
21
22
  end
22
23
  end
23
24
 
25
+ def store(correlation_id, opts)
26
+ @requests.store(correlation_id, opts)
27
+ end
28
+
29
+ def delete(correlation_id)
30
+ @requests.delete(correlation_id)
31
+ end
32
+
24
33
  private
25
34
 
26
35
  def clear_timeouts(now)
27
36
  @requests.each do |key, value|
28
- timeout(key, value) if now > value[:timeout]
37
+ timeout(key, value) if now > value[:expires_at]
29
38
  end
30
39
  end
31
40
 
@@ -36,7 +45,7 @@ class Freddy
36
45
  Utils.notify 'RequestTimeout', "Request timed out waiting for response from #{request[:destination]}", {
37
46
  correlation_id: correlation_id,
38
47
  destination: request[:destination],
39
- timeout: request[:timeout]
48
+ expires_at: request[:expires_at]
40
49
  }
41
50
 
42
51
  request[:callback].call({error: 'RequestTimeout', message: 'Timed out waiting for response'}, nil)
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Freddy::Consumers::RespondToConsumer do
4
+ let(:consumer) { described_class.new(thread_pool, logger) }
5
+
6
+ let(:connection) { Freddy::Adapters.determine.connect(config) }
7
+ let(:thread_pool) { Thread.pool(1) }
8
+ let(:destination) { random_destination }
9
+ let(:payload) { {pay: 'load'} }
10
+ let(:msg_handler) { double }
11
+
12
+ after do
13
+ connection.close
14
+ end
15
+
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
19
+ end
20
+ default_sleep
21
+
22
+ expect(@message_received).to be_falsy
23
+ end
24
+ end
@@ -18,7 +18,8 @@ describe Freddy do
18
18
  it 'removes the message from the queue after the timeout' do
19
19
  # Assume that there already is a queue. Otherwise will get an early
20
20
  # return.
21
- freddy.channel.queue(destination)
21
+ consumer = freddy.respond_to(destination) { }
22
+ consumer.shutdown
22
23
 
23
24
  freddy.deliver(destination, {}, timeout: 0.1)
24
25
  sleep 0.2
@@ -35,7 +36,8 @@ describe Freddy do
35
36
  it 'keeps the message in the queue' do
36
37
  # Assume that there already is a queue. Otherwise will get an early
37
38
  # return.
38
- freddy.channel.queue(destination)
39
+ consumer = freddy.respond_to(destination) { }
40
+ consumer.shutdown
39
41
 
40
42
  freddy.deliver(destination, {})
41
43
  default_sleep # to ensure everything is properly cleaned
@@ -67,21 +69,6 @@ describe Freddy do
67
69
  }
68
70
  end
69
71
 
70
- it 'does not leak consumers' do
71
- respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
72
-
73
- old_count = freddy.channel.consumers.keys.count
74
-
75
- response1 = freddy.deliver_with_response(destination, {a: 'b'})
76
- response2 = freddy.deliver_with_response(destination, {a: 'b'})
77
-
78
- expect(response1).to eq(res: 'yey')
79
- expect(response2).to eq(res: 'yey')
80
-
81
- new_count = freddy.channel.consumers.keys.count
82
- expect(new_count).to be(old_count + 1)
83
- end
84
-
85
72
  it 'responds to the correct requester' do
86
73
  respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
87
74
 
@@ -95,17 +82,11 @@ describe Freddy do
95
82
 
96
83
  context 'when queue does not exist' do
97
84
  it 'gives a no route error' do
98
- begin
99
- Timeout::timeout(0.5) do
100
- expect {
101
- freddy.deliver_with_response(destination, {a: 'b'}, timeout: 3)
102
- }.to raise_error(Freddy::InvalidRequestError) {|error|
103
- expect(error.response).to eq(error: 'Specified queue does not exist')
104
- }
105
- end
106
- rescue Timeout::Error
107
- fail('Received a timeout error instead of the no route error')
108
- end
85
+ expect {
86
+ freddy.deliver_with_response(destination, {a: 'b'}, timeout: 1)
87
+ }.to raise_error(Freddy::InvalidRequestError) {|error|
88
+ expect(error.response).to eq(error: 'Specified queue does not exist')
89
+ }
109
90
  end
110
91
  end
111
92
 
@@ -124,7 +105,8 @@ describe Freddy do
124
105
  it 'removes the message from the queue' do
125
106
  # Assume that there already is a queue. Otherwise will get an early
126
107
  # return.
127
- freddy.channel.queue(destination)
108
+ consumer = freddy.respond_to(destination) { }
109
+ consumer.shutdown
128
110
 
129
111
  expect {
130
112
  freddy.deliver_with_response(destination, {}, timeout: 0.1)
@@ -143,7 +125,8 @@ describe Freddy do
143
125
  it 'removes the message from the queue' do
144
126
  # Assume that there already is a queue. Otherwise will get an early
145
127
  # return.
146
- freddy.channel.queue(destination)
128
+ consumer = freddy.respond_to(destination) { }
129
+ consumer.shutdown
147
130
 
148
131
  expect {
149
132
  freddy.deliver_with_response(destination, {}, timeout: 0.1, delete_on_timeout: false)
@@ -84,4 +84,26 @@ describe 'Concurrency' do
84
84
 
85
85
  expect(results.count).to eq(10)
86
86
  end
87
+
88
+ context 'concurrent executions of deliver_with_response' do
89
+ let(:nr_of_threads) { 50 }
90
+ let(:payload) { {pay: 'load'} }
91
+ let(:msg_counter) { Hamster.mutable_set }
92
+ let(:queue_name) { random_destination }
93
+
94
+ before do
95
+ spawn_echo_responder(freddy1, queue_name)
96
+ end
97
+
98
+ it 'is supported' do
99
+ nr_of_threads.times.map do |index|
100
+ Thread.new do
101
+ response = freddy1.deliver_with_response(queue_name, payload)
102
+ msg_counter << index
103
+ expect(response).to eq(payload)
104
+ end
105
+ end.each(&:join)
106
+ expect(msg_counter.count).to eq(nr_of_threads)
107
+ end
108
+ end
87
109
  end
@@ -23,12 +23,15 @@ describe 'Logging' do
23
23
 
24
24
  it 'logs all consumed messages' do
25
25
  expect(logger1).to have_received(:info).with(/Listening for requests on \S+/)
26
- expect(logger1).to have_received(:debug).with(/Received message on \S+ with payload {:pay=>"load"}/)
26
+ expect(logger1).to have_received(:debug).with(
27
+ /Received message on \S+ with payload {:pay=>"load"}/
28
+ )
27
29
  end
28
30
 
29
31
  it 'logs all produced messages' do
30
32
  expect(logger2).to have_received(:debug).with(/Consuming messages on \S+/)
31
- expect(logger2).to have_received(:debug).with(/Publishing request to \S+, waiting for response on amq.gen-\S+ with correlation_id .*/)
32
- expect(logger2).to have_received(:debug).with(/Producing message {:pay=>"load"} to \S+/)
33
+ expect(logger2).to have_received(:debug).with(
34
+ /Publishing request with payload {:pay=>"load"} to \S+, waiting for response on amq.gen-\S+ with correlation_id .*/
35
+ )
33
36
  end
34
37
  end
@@ -2,6 +2,7 @@ require 'pry'
2
2
  require 'securerandom'
3
3
  require 'freddy'
4
4
  require 'logger'
5
+ require 'hamster/experimental/mutable_set'
5
6
 
6
7
  Thread.abort_on_exception = true
7
8
 
@@ -38,3 +39,9 @@ end
38
39
  def config
39
40
  {host: 'localhost', port: 5672, user: 'guest', pass: 'guest'}
40
41
  end
42
+
43
+ def spawn_echo_responder(freddy, queue_name)
44
+ freddy.respond_to queue_name do |payload, msg_handler|
45
+ msg_handler.success(payload)
46
+ end
47
+ end
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.4.9
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Urmas Talimaa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-16 00:00:00.000000000 Z
11
+ date: 2016-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -116,7 +116,7 @@ files:
116
116
  - lib/freddy/adapters.rb
117
117
  - lib/freddy/adapters/bunny_adapter.rb
118
118
  - lib/freddy/adapters/march_hare_adapter.rb
119
- - lib/freddy/consumer.rb
119
+ - lib/freddy/consumers.rb
120
120
  - lib/freddy/consumers/respond_to_consumer.rb
121
121
  - lib/freddy/consumers/response_consumer.rb
122
122
  - lib/freddy/consumers/tap_into_consumer.rb
@@ -126,18 +126,19 @@ files:
126
126
  - lib/freddy/message_handler.rb
127
127
  - lib/freddy/message_handlers.rb
128
128
  - lib/freddy/payload.rb
129
- - lib/freddy/producer.rb
130
- - lib/freddy/request.rb
129
+ - lib/freddy/producers.rb
130
+ - lib/freddy/producers/reply_producer.rb
131
+ - lib/freddy/producers/send_and_forget_producer.rb
132
+ - lib/freddy/producers/send_and_wait_response_producer.rb
131
133
  - lib/freddy/request_manager.rb
132
134
  - lib/freddy/responder_handler.rb
133
135
  - lib/freddy/sync_response_container.rb
134
136
  - lib/freddy/timeout_error.rb
135
137
  - lib/freddy/utils.rb
136
- - spec/freddy/consumer_spec.rb
138
+ - spec/freddy/consumers/respond_to_consumer_spec.rb
137
139
  - spec/freddy/error_response_spec.rb
138
140
  - spec/freddy/freddy_spec.rb
139
141
  - spec/freddy/message_handler_spec.rb
140
- - spec/freddy/request_spec.rb
141
142
  - spec/freddy/responder_handler_spec.rb
142
143
  - spec/freddy/sync_response_container_spec.rb
143
144
  - spec/integration/concurrency_spec.rb
@@ -168,11 +169,10 @@ signing_key:
168
169
  specification_version: 4
169
170
  summary: API for inter-application messaging supporting acknowledgements and request-response
170
171
  test_files:
171
- - spec/freddy/consumer_spec.rb
172
+ - spec/freddy/consumers/respond_to_consumer_spec.rb
172
173
  - spec/freddy/error_response_spec.rb
173
174
  - spec/freddy/freddy_spec.rb
174
175
  - spec/freddy/message_handler_spec.rb
175
- - spec/freddy/request_spec.rb
176
176
  - spec/freddy/responder_handler_spec.rb
177
177
  - spec/freddy/sync_response_container_spec.rb
178
178
  - spec/integration/concurrency_spec.rb
@@ -1,33 +0,0 @@
1
- require_relative 'responder_handler'
2
- require_relative 'message_handler'
3
- require_relative 'delivery'
4
- require_relative 'consumers/tap_into_consumer'
5
- require_relative 'consumers/respond_to_consumer'
6
- require_relative 'consumers/response_consumer'
7
-
8
- class Freddy
9
- class Consumer
10
- def initialize(logger, consume_thread_pool, producer, connection)
11
- @logger = logger
12
- @connection = connection
13
- @tap_into_consumer = Consumers::TapIntoConsumer.new(consume_thread_pool)
14
- @respond_to_consumer = Consumers::RespondToConsumer.new(consume_thread_pool, producer, @logger)
15
- @response_consumer = Consumers::ResponseConsumer.new(@logger)
16
- end
17
-
18
- def response_consume(queue, &block)
19
- @logger.debug "Consuming messages on #{queue.name}"
20
- @response_consumer.consume(queue, &block)
21
- end
22
-
23
- def tap_into(pattern, &block)
24
- @logger.debug "Tapping into messages that match #{pattern}"
25
- @tap_into_consumer.consume(pattern, @connection.create_channel, &block)
26
- end
27
-
28
- def respond_to(destination, &block)
29
- @logger.info "Listening for requests on #{destination}"
30
- @respond_to_consumer.consume(destination, @connection.create_channel, &block)
31
- end
32
- end
33
- end
@@ -1,26 +0,0 @@
1
- require_relative 'request'
2
- require 'json'
3
-
4
- class Freddy
5
- class Producer
6
- OnReturnNotImplemented = Class.new(NoMethodError)
7
-
8
- CONTENT_TYPE = 'application/json'.freeze
9
-
10
- def initialize(channel, logger)
11
- @channel, @logger = channel, logger
12
- @exchange = @channel.default_exchange
13
- @topic_exchange = @channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
14
- end
15
-
16
- def produce(destination, payload, properties={})
17
- @logger.debug "Producing message #{payload.inspect} to #{destination}"
18
-
19
- properties = properties.merge(routing_key: destination, content_type: CONTENT_TYPE)
20
- json_payload = Payload.dump(payload)
21
-
22
- @topic_exchange.publish json_payload, properties.dup
23
- @exchange.publish json_payload, properties.dup
24
- end
25
- end
26
- end
@@ -1,88 +0,0 @@
1
- require_relative 'producer'
2
- require_relative 'consumer'
3
- require_relative 'request_manager'
4
- require_relative 'sync_response_container'
5
- require_relative 'message_handlers'
6
- require 'thread'
7
- require 'securerandom'
8
- require 'hamster/mutable_hash'
9
-
10
- class Freddy
11
- class Request
12
- NO_ROUTE = 312
13
-
14
- def initialize(channel, logger, producer, consumer)
15
- @channel, @logger = channel, logger
16
- @producer, @consumer = producer, consumer
17
- @request_map = Hamster.mutable_hash
18
- @request_manager = RequestManager.new @request_map, @logger
19
-
20
- @channel.on_return do |reply_code, correlation_id|
21
- if reply_code == NO_ROUTE
22
- @request_manager.no_route(correlation_id)
23
- end
24
- end
25
-
26
- @listening_for_responses_lock = Mutex.new
27
- end
28
-
29
- def sync_request(destination, payload, opts)
30
- timeout_seconds = opts.fetch(:timeout)
31
- container = SyncResponseContainer.new
32
- async_request destination, payload, opts, &container
33
- container.wait_for_response(timeout_seconds + 0.1)
34
- end
35
-
36
- def async_request(destination, payload, options, &block)
37
- timeout = options.fetch(:timeout)
38
- delete_on_timeout = options.fetch(:delete_on_timeout)
39
- options.delete(:timeout)
40
- options.delete(:delete_on_timeout)
41
-
42
- ensure_listening_to_responses
43
-
44
- correlation_id = SecureRandom.uuid
45
- @request_map.store(correlation_id, callback: block, destination: destination, timeout: Time.now + timeout)
46
-
47
- @logger.debug "Publishing request to #{destination}, waiting for response on #{@response_queue.name} with correlation_id #{correlation_id}"
48
-
49
- if delete_on_timeout
50
- options[:expiration] = (timeout * 1000).to_i
51
- end
52
-
53
- @producer.produce destination, payload, options.merge(
54
- correlation_id: correlation_id, reply_to: @response_queue.name,
55
- mandatory: true, type: 'request'
56
- )
57
- end
58
-
59
- private
60
-
61
- def handle_response(delivery)
62
- correlation_id = delivery.correlation_id
63
-
64
- if request = @request_map.delete(correlation_id)
65
- @logger.debug "Got response for request to #{request[:destination]} with correlation_id #{correlation_id}"
66
- request[:callback].call delivery.payload, delivery
67
- else
68
- @logger.warn "Got rpc response for correlation_id #{correlation_id} but there is no requester"
69
- Utils.notify 'NoRequesterForResponse', "Got rpc response but there is no requester", correlation_id: correlation_id
70
- end
71
- rescue Exception => e
72
- destination_report = request ? "to #{request[:destination]}" : ''
73
- @logger.error "Exception occured while handling the response of request made #{destination_report} with correlation_id #{correlation_id}: #{Utils.format_exception e}"
74
- Utils.notify_exception(e, destination: request[:destination], correlation_id: correlation_id)
75
- end
76
-
77
- def ensure_listening_to_responses
78
- return @listening_for_responses if defined?(@listening_for_responses)
79
-
80
- @listening_for_responses_lock.synchronize do
81
- @response_queue ||= @channel.queue("", exclusive: true)
82
- @request_manager.start
83
- @consumer.response_consume(@response_queue, &method(:handle_response))
84
- @listening_for_responses = true
85
- end
86
- end
87
- end
88
- end
@@ -1,21 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Freddy::Consumer do
4
- let(:freddy) { Freddy.build(logger, config) }
5
-
6
- let(:destination) { random_destination }
7
- let(:payload) { {pay: 'load'} }
8
-
9
- let(:consumer) { freddy.consumer }
10
-
11
- after { freddy.close }
12
-
13
- it "doesn't call passed block without any messages" do
14
- consumer.respond_to destination do
15
- @message_received = true
16
- end
17
- default_sleep
18
-
19
- expect(@message_received).to be_falsy
20
- end
21
- end
@@ -1,37 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Freddy::Request do
4
- let(:freddy) { Freddy.build(logger, config) }
5
-
6
- let(:destination) { random_destination }
7
- let(:payload) { {pay: 'load'} }
8
-
9
- let(:request) { freddy.request }
10
-
11
- after { freddy.close }
12
-
13
- context 'requesting from multiple threads' do
14
- let(:nr_of_threads) { 50 }
15
-
16
- before do
17
- freddy.respond_to 'thread-queue' do |payload, msg_handler|
18
- msg_handler.success(payload)
19
- end
20
- end
21
-
22
- it 'handles multiple threads' do
23
- require 'hamster/experimental/mutable_set'
24
- msg_counter = Hamster.mutable_set
25
- nr_of_threads.times.map do |index|
26
- Thread.new do
27
- response = freddy.deliver_with_response 'thread-queue', payload
28
- msg_counter << index
29
- expect(response).to eq(payload)
30
- end
31
- end.each(&:join)
32
- expect(msg_counter.count).to eq(nr_of_threads)
33
- end
34
-
35
- end
36
-
37
- end