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 +4 -4
- data/freddy.gemspec +1 -1
- data/lib/freddy.rb +72 -24
- data/lib/freddy/adapters/bunny_adapter.rb +6 -2
- data/lib/freddy/adapters/march_hare_adapter.rb +5 -3
- data/lib/freddy/consumers.rb +1 -0
- data/lib/freddy/consumers/respond_to_consumer.rb +3 -5
- data/lib/freddy/consumers/response_consumer.rb +1 -0
- data/lib/freddy/message_handlers.rb +14 -5
- data/lib/freddy/producers.rb +1 -0
- data/lib/freddy/producers/reply_producer.rb +22 -0
- data/lib/freddy/producers/send_and_forget_producer.rb +25 -0
- data/lib/freddy/producers/send_and_wait_response_producer.rb +89 -0
- data/lib/freddy/request_manager.rb +13 -4
- data/spec/freddy/consumers/respond_to_consumer_spec.rb +24 -0
- data/spec/freddy/freddy_spec.rb +13 -30
- data/spec/integration/concurrency_spec.rb +22 -0
- data/spec/integration/logging_spec.rb +6 -3
- data/spec/spec_helper.rb +7 -0
- metadata +9 -9
- data/lib/freddy/consumer.rb +0 -33
- data/lib/freddy/producer.rb +0 -26
- data/lib/freddy/request.rb +0 -88
- data/spec/freddy/consumer_spec.rb +0 -21
- data/spec/freddy/request_spec.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c3a19668f52a0498e6bd99353748130201650c7
|
4
|
+
data.tar.gz: 22d1c3dda4ac8b3e1d31912e4361512c2e208647
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cebf9e3bd0c72defc7de84902cd8ed98d635008203808441ba0bce1d66ba61674f5580866198ebac21d2e7cae559e20e6558453a6f34919e5e3586f3571df90
|
7
|
+
data.tar.gz: 1ff654a4196df7c9c49a191e84c1ada5fe6acf119fb81d0a51e8bbaaffd28cfd1ccda054e53aaee01b429369e962ec72bf5a885cbfa3e12fb84ef6ec4f6adbd3
|
data/freddy.gemspec
CHANGED
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
|
-
|
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),
|
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,
|
30
|
+
new(connection, logger, consume_thread_pool)
|
36
31
|
end
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
def initialize(connection, logger, max_concurrency)
|
33
|
+
def initialize(connection, logger, consume_thread_pool)
|
41
34
|
@connection = connection
|
42
|
-
@
|
43
|
-
|
44
|
-
@
|
45
|
-
@
|
46
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
121
|
-
|
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
|
39
|
+
def on_no_route(&block)
|
38
40
|
default_exchange.on_return do |return_info, properties, content|
|
39
|
-
|
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
|
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(
|
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,
|
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
|
-
|
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
|
@@ -1,13 +1,23 @@
|
|
1
1
|
class Freddy
|
2
2
|
module MessageHandlers
|
3
|
-
|
4
|
-
|
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(
|
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(
|
5
|
-
@requests
|
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[:
|
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
|
-
|
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
|
data/spec/freddy/freddy_spec.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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.
|
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.
|
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(
|
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(
|
32
|
-
|
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
|
+
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:
|
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/
|
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/
|
130
|
-
- lib/freddy/
|
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/
|
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/
|
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
|
data/lib/freddy/consumer.rb
DELETED
@@ -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
|
data/lib/freddy/producer.rb
DELETED
@@ -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
|
data/lib/freddy/request.rb
DELETED
@@ -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
|
data/spec/freddy/request_spec.rb
DELETED
@@ -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
|