freddy-jruby 0.4.9 → 0.5.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
  SHA1:
3
- metadata.gz: 0e34103e16c546862a8431d62287eb20955795c4
4
- data.tar.gz: 88d5d9f3ee6e6334c8ff3a2dbd4676bc0fe31528
3
+ metadata.gz: 8c3a19668f52a0498e6bd99353748130201650c7
4
+ data.tar.gz: 22d1c3dda4ac8b3e1d31912e4361512c2e208647
5
5
  SHA512:
6
- metadata.gz: d6b8a1de5e524b4bfd2481152255e37dfcd5df68c7fb1f8849f60ac5581aff7b6e8fa84f1b76db3dc5cbb3884533a79e27e7cd16e1a993654c509e7e949828e6
7
- data.tar.gz: 915a84bb6418a6d8cd8609734e7138aaa230bb1dd44a3ec303668a87817e7d31ea02f0d14b61442b7541dc0fb863f2a63d51cd28c3e93fd24bf6f0f290c90947
6
+ metadata.gz: 7cebf9e3bd0c72defc7de84902cd8ed98d635008203808441ba0bce1d66ba61674f5580866198ebac21d2e7cae559e20e6558453a6f34919e5e3586f3571df90
7
+ data.tar.gz: 1ff654a4196df7c9c49a191e84c1ada5fe6acf119fb81d0a51e8bbaaffd28cfd1ccda054e53aaee01b429369e962ec72bf5a885cbfa3e12fb84ef6ec4f6adbd3
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.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}
data/lib/freddy.rb CHANGED
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -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-jruby
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
  requirement: !ruby/object:Gem::Requirement
@@ -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