freddy-jruby 0.4.3 → 0.4.9

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: 2f29ab8dfbedae9cbd544b20c86c7adf72d4859e
4
- data.tar.gz: dd7c545605d297356f2fdb27b3fef5fda8735474
3
+ metadata.gz: 0e34103e16c546862a8431d62287eb20955795c4
4
+ data.tar.gz: 88d5d9f3ee6e6334c8ff3a2dbd4676bc0fe31528
5
5
  SHA512:
6
- metadata.gz: 4674e17c54e5dee1b926bf6354987d5a7e2df079a39754d4ed9a332ec705e8046e5fe69112af09d95b0b1be0fa94288c70a12b45eb4f0d5a7c994df9b5024a1b
7
- data.tar.gz: 20784cb06674e8c0135315dfa9566a0933258e3b8cd7f74a722f68e4d7ff9126cec68d3154211c875af128baa39b8a96a5fd56c95461390ac3a5192ee7f94313
6
+ metadata.gz: d6b8a1de5e524b4bfd2481152255e37dfcd5df68c7fb1f8849f60ac5581aff7b6e8fa84f1b76db3dc5cbb3884533a79e27e7cd16e1a993654c509e7e949828e6
7
+ data.tar.gz: 915a84bb6418a6d8cd8609734e7138aaa230bb1dd44a3ec303668a87817e7d31ea02f0d14b61442b7541dc0fb863f2a63d51cd28c3e93fd24bf6f0f290c90947
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Messaging API supporting acknowledgements and request-response
2
2
 
3
- [![Build Status](https://travis-ci.org/salemove/node-freddy.svg?branch=master)](https://travis-ci.org/salemove/node-freddy)
3
+ [![Build Status](https://travis-ci.org/salemove/freddy.svg?branch=master)](https://travis-ci.org/salemove/freddy)
4
4
  [![Code Climate](https://codeclimate.com/github/salemove/freddy/badges/gpa.svg)](https://codeclimate.com/github/salemove/freddy)
5
5
 
6
6
  ## Setup
@@ -12,6 +12,12 @@ logger = Logger.new(STDOUT)
12
12
  freddy = Freddy.build(logger, host: 'localhost', port: 5672, user: 'guest', pass: 'guest')
13
13
  ```
14
14
 
15
+ ## Supported message queues
16
+
17
+ These message queues have been tested and are working with Freddy. Other queues can be added easily:
18
+
19
+ * [RabbitMQ](https://www.rabbitmq.com/)
20
+
15
21
  ## Delivering messages
16
22
 
17
23
  ### Simple delivery
@@ -125,16 +131,9 @@ The following operations are supported:
125
131
 
126
132
  * stop responding
127
133
  ```ruby
128
- responder_handler.cancel
134
+ responder_handler.shutdown
129
135
  ```
130
136
 
131
- * delete the destination
132
- ```ruby
133
- responder_handler.destroy_destination
134
- ```
135
-
136
- * Primary use case is in tests to not leave dangling destinations. It deletes the destination even if there are responders for the same destination in other parts of the system. Use with caution in production code.
137
-
138
137
 
139
138
  ## Notes about concurrency
140
139
 
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.3'
11
+ spec.version = '0.4.9'
12
12
  spec.authors = ["Urmas Talimaa"]
13
13
  spec.email = ["urmas.talimaa@gmail.com"]
14
14
  spec.description = %q{Messaging API}
@@ -25,11 +25,12 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  if RUBY_PLATFORM == 'java'
27
27
  spec.add_dependency 'march_hare', '~> 2.12.0'
28
+ spec.add_dependency 'symbolizer'
28
29
  else
29
30
  spec.add_dependency "bunny", "2.2.0"
31
+ spec.add_dependency "oj", "~> 2.13"
30
32
  end
31
33
 
32
- spec.add_dependency "symbolizer"
33
34
  spec.add_dependency "hamster", "~> 1.0.1.pre.rc3"
34
35
  spec.add_dependency "thread", "~> 0.2"
35
36
  end
data/lib/freddy.rb CHANGED
@@ -1,83 +1,36 @@
1
- if RUBY_PLATFORM == 'java'
2
- require 'march_hare'
3
- else
4
- require 'bunny'
5
- end
6
-
7
1
  require 'json'
8
- require 'symbolizer'
9
2
  require 'thread/pool'
10
3
 
11
- require_relative 'freddy/adaptive_queue'
4
+ require_relative 'freddy/adapters'
12
5
  require_relative 'freddy/consumer'
13
6
  require_relative 'freddy/producer'
14
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'
15
13
 
16
14
  class Freddy
17
- class ErrorResponse < StandardError
18
- DEFAULT_ERROR_MESSAGE = 'Use #response to get the error response'
19
-
20
- attr_reader :response
21
-
22
- def initialize(response)
23
- @response = response
24
- super(format_message(response) || DEFAULT_ERROR_MESSAGE)
25
- end
26
-
27
- private
28
-
29
- def format_message(response)
30
- return unless response.is_a?(Hash)
31
-
32
- message = [response[:error], response[:message]].compact.join(': ')
33
- message.empty? ? nil : message
34
- end
35
- end
36
-
37
- class InvalidRequestError < ErrorResponse
38
- end
39
-
40
- class TimeoutError < ErrorResponse
41
- end
42
-
43
15
  FREDDY_TOPIC_EXCHANGE_NAME = 'freddy-topic'.freeze
44
16
 
45
- def self.format_backtrace(backtrace)
46
- backtrace.map{ |x|
47
- x.match(/^(.+?):(\d+)(|:in `(.+)')$/);
48
- [$1,$2,$4]
49
- }.join "\n"
50
- end
51
-
52
- def self.format_exception(exception)
53
- "#{exception.exception}\n#{format_backtrace(exception.backtrace)}"
54
- end
55
-
56
- def self.notify(name, message, parameters={})
57
- if defined? Airbrake
58
- Airbrake.notify_or_ignore({
59
- error_class: name,
60
- error_message: message,
61
- cgi_data: ENV.to_hash,
62
- parameters: parameters
63
- })
64
- end
65
- end
66
-
67
- def self.notify_exception(exception, parameters={})
68
- if defined? Airbrake
69
- Airbrake.notify_or_ignore(exception, cgi_data: ENV.to_hash, parameters: parameters)
70
- end
71
- end
72
-
73
- def self.build(logger = Logger.new(STDOUT), config)
74
- if RUBY_PLATFORM == 'java'
75
- connection = MarchHare.connect(config)
76
- else
77
- connection = Bunny.new(config)
78
- connection.start
79
- connection
80
- end
17
+ # Creates a new freddy instance
18
+ #
19
+ # @param [Logger] logger
20
+ # instance of a logger, defaults to the STDOUT logger
21
+ # @param [Hash] config
22
+ # rabbitmq connection information
23
+ # @option config [String] :host ('localhost')
24
+ # @option config [Integer] :port (5672)
25
+ # @option config [String] :user ('guest')
26
+ # @option config [String] :pass ('guest')
27
+ #
28
+ # @return [Freddy]
29
+ #
30
+ # @example
31
+ # Freddy.build(Logger.new(STDOUT), user: 'thumper', pass: 'howdy')
32
+ def self.build(logger = Logger.new(STDOUT), config = {})
33
+ connection = Adapters.determine.connect(config)
81
34
 
82
35
  new(connection, logger, config.fetch(:max_concurrency, 4))
83
36
  end
@@ -88,19 +41,41 @@ class Freddy
88
41
  @connection = connection
89
42
  @channel = connection.create_channel
90
43
  @consume_thread_pool = Thread.pool(max_concurrency)
91
- @consumer = Consumer.new channel, logger, @consume_thread_pool
92
44
  @producer = Producer.new channel, logger
45
+ @consumer = Consumer.new logger, @consume_thread_pool, @producer, @connection
93
46
  @request = Request.new channel, logger, @producer, @consumer
94
47
  end
48
+ private :initialize
95
49
 
96
50
  def respond_to(destination, &callback)
97
- @request.respond_to destination, &callback
51
+ @consumer.respond_to destination, &callback
98
52
  end
99
53
 
100
54
  def tap_into(pattern, &callback)
101
55
  @consumer.tap_into pattern, &callback
102
56
  end
103
57
 
58
+ # Sends a message to given destination
59
+ #
60
+ # This is *send and forget* type of delivery. It sends a message to given
61
+ # destination and does not wait for response. This is useful when there are
62
+ # multiple consumers that are using #tap_into or you just do not care about
63
+ # the response.
64
+ #
65
+ # @param [String] destination
66
+ # the queue name
67
+ # @param [Hash] payload
68
+ # the payload that can be serialized to json
69
+ # @param [Hash] options
70
+ # the options for delivery
71
+ # @option options [Integer] :timeout (0)
72
+ # discards the message after given seconds if nobody consumes it. Message
73
+ # won't be discarded if timeout it set to 0 (default).
74
+ #
75
+ # @return [void]
76
+ #
77
+ # @example
78
+ # freddy.deliver 'Metrics', user_id: 5, metric: 'signed_in'
104
79
  def deliver(destination, payload, options = {})
105
80
  timeout = options.fetch(:timeout, 0)
106
81
  opts = {}
@@ -109,6 +84,35 @@ class Freddy
109
84
  @producer.produce destination, payload, opts
110
85
  end
111
86
 
87
+ # Sends a message and waits for the response
88
+ #
89
+ # @param [String] destination
90
+ # the queue name
91
+ # @param [Hash] payload
92
+ # the payload that can be serialized to json
93
+ # @param [Hash] options
94
+ # the options for delivery
95
+ # @option options [Integer] :timeout (3)
96
+ # throws a time out exception after given seconds when there is no response
97
+ # @option options [Boolean] :delete_on_timeout (true)
98
+ # discards the message when timeout error is raised
99
+ #
100
+ # @raise [Freddy::TimeoutError]
101
+ # if nobody responded to the request
102
+ # @raise [Freddy::InvalidRequestError]
103
+ # if the responder responded with an error response
104
+ #
105
+ # @return [Hash] the response
106
+ #
107
+ # @example
108
+ # begin
109
+ # response = freddy.deliver_with_response 'Users', type: 'fetch_all'
110
+ # puts "Got response #{response}"
111
+ # rescue Freddy::TimeoutError
112
+ # puts "Service unavailable"
113
+ # rescue Freddy::InvalidRequestError => e
114
+ # puts "Got error response: #{e.response}"
115
+ # end
112
116
  def deliver_with_response(destination, payload, options = {})
113
117
  timeout = options.fetch(:timeout, 3)
114
118
  delete_on_timeout = options.fetch(:delete_on_timeout, true)
@@ -118,6 +122,12 @@ class Freddy
118
122
  }
119
123
  end
120
124
 
125
+ # Closes the connection with message queue
126
+ #
127
+ # @return [void]
128
+ #
129
+ # @example
130
+ # freddy.close
121
131
  def close
122
132
  @connection.close
123
133
  end
@@ -0,0 +1,13 @@
1
+ class Freddy
2
+ module Adapters
3
+ def self.determine
4
+ if RUBY_PLATFORM == 'java'
5
+ require_relative 'adapters/march_hare_adapter'
6
+ MarchHareAdapter
7
+ else
8
+ require_relative 'adapters/bunny_adapter'
9
+ BunnyAdapter
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,67 @@
1
+ require 'bunny'
2
+
3
+ class Freddy
4
+ module Adapters
5
+ class BunnyAdapter
6
+ def self.connect(config)
7
+ bunny = Bunny.new(config)
8
+ bunny.start
9
+ new(bunny)
10
+ end
11
+
12
+ def initialize(bunny)
13
+ @bunny = bunny
14
+ end
15
+
16
+ def create_channel
17
+ Channel.new(@bunny.create_channel)
18
+ end
19
+
20
+ def close
21
+ @bunny.close
22
+ end
23
+
24
+ class Channel
25
+ extend Forwardable
26
+
27
+ def initialize(channel)
28
+ @channel = channel
29
+ end
30
+
31
+ def_delegators :@channel, :topic, :default_exchange, :consumers
32
+
33
+ def queue(*args)
34
+ Queue.new(@channel.queue(*args))
35
+ end
36
+
37
+ def on_return(&block)
38
+ default_exchange.on_return do |return_info, properties, content|
39
+ block.call(return_info[:reply_code], properties[:correlation_id])
40
+ end
41
+ end
42
+ end
43
+
44
+ class Queue
45
+ def initialize(queue)
46
+ @queue = queue
47
+ end
48
+
49
+ def subscribe(&block)
50
+ @queue.subscribe do |info, properties, payload|
51
+ parsed_payload = Payload.parse(payload)
52
+ block.call(Delivery.new(parsed_payload, properties, info.routing_key))
53
+ end
54
+ end
55
+
56
+ def bind(*args)
57
+ @queue.bind(*args)
58
+ self
59
+ end
60
+
61
+ def name
62
+ @queue.name
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ require 'march_hare'
2
+
3
+ class Freddy
4
+ module Adapters
5
+ class MarchHareAdapter
6
+ def self.connect(config)
7
+ hare = MarchHare.connect(config)
8
+ new(hare)
9
+ end
10
+
11
+ def initialize(hare)
12
+ @hare = hare
13
+ end
14
+
15
+ def create_channel
16
+ Channel.new(@hare.create_channel)
17
+ end
18
+
19
+ def close
20
+ @hare.close
21
+ end
22
+
23
+ class Channel
24
+ extend Forwardable
25
+
26
+ def initialize(channel)
27
+ @channel = channel
28
+ end
29
+
30
+ def_delegators :@channel, :topic, :default_exchange, :consumers
31
+
32
+ def queue(*args)
33
+ Queue.new(@channel.queue(*args))
34
+ end
35
+
36
+ def on_return(&block)
37
+ @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
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ class Queue
46
+ def initialize(queue)
47
+ @queue = queue
48
+ end
49
+
50
+ def subscribe(&block)
51
+ @queue.subscribe do |meta, payload|
52
+ parsed_payload = Payload.parse(payload)
53
+ block.call(Delivery.new(parsed_payload, meta, meta.routing_key))
54
+ end
55
+ end
56
+
57
+ def bind(*args)
58
+ @queue.bind(*args)
59
+ self
60
+ end
61
+
62
+ def name
63
+ @queue.name
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,77 +1,33 @@
1
1
  require_relative 'responder_handler'
2
2
  require_relative 'message_handler'
3
- require_relative 'request'
4
3
  require_relative 'delivery'
4
+ require_relative 'consumers/tap_into_consumer'
5
+ require_relative 'consumers/respond_to_consumer'
6
+ require_relative 'consumers/response_consumer'
5
7
 
6
8
  class Freddy
7
9
  class Consumer
8
-
9
- class EmptyConsumer < Exception
10
- end
11
-
12
- def initialize(channel, logger, consume_thread_pool)
13
- @channel, @logger = channel, logger
14
- @topic_exchange = @channel.topic Freddy::FREDDY_TOPIC_EXCHANGE_NAME
15
- @consume_thread_pool = consume_thread_pool
16
- @dedicated_thread_pool = Thread.pool(1) # used only internally
17
- end
18
-
19
- def consume(destination, options = {}, &block)
20
- raise EmptyConsumer unless block
21
- consume_from_queue create_queue(destination), options, &block
22
- end
23
-
24
- def consume_from_queue(queue, options = {}, &block)
25
- consume_using_pool(queue, options, @consume_thread_pool, &block)
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)
26
16
  end
27
17
 
28
- def dedicated_consume(queue, &block)
29
- consume_using_pool(queue, {}, @dedicated_thread_pool, &block)
18
+ def response_consume(queue, &block)
19
+ @logger.debug "Consuming messages on #{queue.name}"
20
+ @response_consumer.consume(queue, &block)
30
21
  end
31
22
 
32
23
  def tap_into(pattern, &block)
33
- queue = create_queue('', exclusive: true).bind(@topic_exchange, routing_key: pattern)
34
- consumer = queue.subscribe do |payload, delivery|
35
- @consume_thread_pool.process do
36
- block.call parse_payload(payload), delivery.routing_key
37
- end
38
- end
39
24
  @logger.debug "Tapping into messages that match #{pattern}"
40
- ResponderHandler.new consumer, @channel
41
- end
42
-
43
- private
44
-
45
- def consume_using_pool(queue, options, pool, &block)
46
- consumer = queue.subscribe do |payload, delivery|
47
- pool.process do
48
- parsed_payload = parse_payload(payload)
49
- log_receive_event(queue.name, parsed_payload, delivery.correlation_id)
50
- block.call parsed_payload, delivery
51
- end
52
- end
53
- @logger.debug "Consuming messages on #{queue.name}"
54
- ResponderHandler.new consumer, @channel
55
- end
56
-
57
- def parse_payload(payload)
58
- if payload == 'null'
59
- {}
60
- else
61
- Symbolizer.symbolize(JSON(payload))
62
- end
63
- end
64
-
65
- def create_queue(destination, options={})
66
- AdaptiveQueue.new(@channel.queue(destination, options))
25
+ @tap_into_consumer.consume(pattern, @connection.create_channel, &block)
67
26
  end
68
27
 
69
- def log_receive_event(queue_name, payload, correlation_id)
70
- if defined?(Logasm) && @logger.is_a?(Logasm)
71
- @logger.debug "Received message", queue: queue_name, payload: payload, correlation_id: correlation_id
72
- else
73
- @logger.debug "Received message on #{queue_name} with payload #{payload} with correlation_id #{correlation_id}"
74
- end
28
+ def respond_to(destination, &block)
29
+ @logger.info "Listening for requests on #{destination}"
30
+ @respond_to_consumer.consume(destination, @connection.create_channel, &block)
75
31
  end
76
32
  end
77
33
  end
@@ -0,0 +1,47 @@
1
+ class Freddy
2
+ module Consumers
3
+ class RespondToConsumer
4
+ def initialize(consume_thread_pool, producer, logger)
5
+ @consume_thread_pool = consume_thread_pool
6
+ @producer = producer
7
+ @logger = logger
8
+ end
9
+
10
+ def consume(destination, channel, &block)
11
+ consumer = consume_from_destination(destination, channel) do |delivery|
12
+ log_receive_event(destination, delivery)
13
+
14
+ handler_class = MessageHandlers.for_type(delivery.type)
15
+ handler = handler_class.new(@producer, destination, @logger)
16
+
17
+ msg_handler = MessageHandler.new(handler, delivery)
18
+ handler.handle_message delivery.payload, msg_handler, &block
19
+ end
20
+
21
+ ResponderHandler.new(consumer, @consume_thread_pool)
22
+ end
23
+
24
+ private
25
+
26
+ def consume_from_destination(destination, channel, &block)
27
+ channel.queue(destination).subscribe do |delivery|
28
+ process_message(delivery, &block)
29
+ end
30
+ end
31
+
32
+ def process_message(delivery, &block)
33
+ @consume_thread_pool.process do
34
+ block.call(delivery)
35
+ end
36
+ end
37
+
38
+ def log_receive_event(destination, delivery)
39
+ if defined?(Logasm) && @logger.is_a?(Logasm)
40
+ @logger.debug "Received message", queue: destination, payload: delivery.payload, correlation_id: delivery.correlation_id
41
+ else
42
+ @logger.debug "Received message on #{destination} with payload #{delivery.payload} with correlation_id #{delivery.correlation_id}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ class Freddy
2
+ module Consumers
3
+ class ResponseConsumer
4
+ def initialize(logger)
5
+ @logger = logger
6
+ @dedicated_thread_pool = Thread.pool(1)
7
+ end
8
+
9
+ def consume(queue, &block)
10
+ consumer = queue.subscribe do |delivery|
11
+ process_message(queue, delivery, &block)
12
+ end
13
+ ResponderHandler.new(consumer, @dedicated_thread_pool)
14
+ end
15
+
16
+ private
17
+
18
+ def process_message(queue, delivery, &block)
19
+ @dedicated_thread_pool.process do
20
+ log_receive_event(queue.name, delivery)
21
+ block.call(delivery)
22
+ end
23
+ end
24
+
25
+ def log_receive_event(queue_name, delivery)
26
+ if defined?(Logasm) && @logger.is_a?(Logasm)
27
+ @logger.debug "Received message", queue: queue_name, payload: delivery.payload, correlation_id: delivery.correlation_id
28
+ else
29
+ @logger.debug "Received message on #{queue_name} with payload #{delivery.payload} with correlation_id #{delivery.correlation_id}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ class Freddy
2
+ module Consumers
3
+ class TapIntoConsumer
4
+ def initialize(consume_thread_pool)
5
+ @consume_thread_pool = consume_thread_pool
6
+ end
7
+
8
+ def consume(pattern, channel, &block)
9
+ queue = create_queue(pattern, channel)
10
+
11
+ consumer = queue.subscribe do |delivery|
12
+ process_message(delivery, &block)
13
+ end
14
+
15
+ ResponderHandler.new(consumer, @consume_thread_pool)
16
+ end
17
+
18
+ private
19
+
20
+ def create_queue(pattern, channel)
21
+ topic_exchange = channel.topic(Freddy::FREDDY_TOPIC_EXCHANGE_NAME)
22
+
23
+ channel
24
+ .queue('', exclusive: true)
25
+ .bind(topic_exchange, routing_key: pattern)
26
+ end
27
+
28
+ def process_message(delivery, &block)
29
+ @consume_thread_pool.process do
30
+ block.call delivery.payload, delivery.routing_key
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,8 +1,9 @@
1
1
  class Freddy
2
2
  class Delivery
3
- attr_reader :metadata, :routing_key
3
+ attr_reader :routing_key, :payload
4
4
 
5
- def initialize(metadata, routing_key)
5
+ def initialize(payload, metadata, routing_key)
6
+ @payload = payload
6
7
  @metadata = metadata
7
8
  @routing_key = routing_key
8
9
  end
@@ -10,5 +11,13 @@ class Freddy
10
11
  def correlation_id
11
12
  @metadata.correlation_id
12
13
  end
14
+
15
+ def type
16
+ @metadata.type
17
+ end
18
+
19
+ def reply_to
20
+ @metadata.reply_to
21
+ end
13
22
  end
14
23
  end
@@ -0,0 +1,21 @@
1
+ class Freddy
2
+ class ErrorResponse < StandardError
3
+ DEFAULT_ERROR_MESSAGE = 'Use #response to get the error response'
4
+
5
+ attr_reader :response
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ super(format_message(response) || DEFAULT_ERROR_MESSAGE)
10
+ end
11
+
12
+ private
13
+
14
+ def format_message(response)
15
+ return unless response.is_a?(Hash)
16
+
17
+ message = [response[:error], response[:message]].compact.join(': ')
18
+ message.empty? ? nil : message
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ class Freddy
2
+ class InvalidRequestError < ErrorResponse
3
+ end
4
+ end
@@ -1,19 +1,19 @@
1
1
  class Freddy
2
2
  class MessageHandler
3
- attr_reader :destination, :correlation_id
3
+ attr_reader :correlation_id
4
4
 
5
5
  def initialize(adapter, delivery)
6
6
  @adapter = adapter
7
- @metadata = delivery.metadata
8
- @correlation_id = @metadata.correlation_id
7
+ @delivery = delivery
8
+ @correlation_id = @delivery.correlation_id
9
9
  end
10
10
 
11
11
  def success(response = nil)
12
- @adapter.success(@metadata.reply_to, response)
12
+ @adapter.success(@delivery.reply_to, response)
13
13
  end
14
14
 
15
15
  def error(error = {error: "Couldn't process message"})
16
- @adapter.error(@metadata.reply_to, error)
16
+ @adapter.error(@delivery.reply_to, error)
17
17
  end
18
18
  end
19
19
  end
@@ -14,8 +14,8 @@ class Freddy
14
14
  def handle_message(payload, msg_handler, &block)
15
15
  block.call payload, msg_handler
16
16
  rescue Exception => e
17
- @logger.error "Exception occured while processing message from #{Freddy.format_exception(e)}"
18
- Freddy.notify_exception(e, destination: @destination)
17
+ @logger.error "Exception occured while processing message from #{Utils.format_exception(e)}"
18
+ Utils.notify_exception(e, destination: @destination)
19
19
  end
20
20
 
21
21
  def success(*)
@@ -39,13 +39,13 @@ class Freddy
39
39
 
40
40
  if !@correlation_id
41
41
  @logger.error "Received request without correlation_id"
42
- Freddy.notify_exception(e)
42
+ Utils.notify_exception(e)
43
43
  else
44
44
  block.call payload, msg_handler
45
45
  end
46
46
  rescue Exception => e
47
- @logger.error "Exception occured while handling the request with correlation_id #{@correlation_id}: #{Freddy.format_exception(e)}"
48
- Freddy.notify_exception(e, correlation_id: @correlation_id, destination: @destination)
47
+ @logger.error "Exception occured while handling the request with correlation_id #{@correlation_id}: #{Utils.format_exception(e)}"
48
+ Utils.notify_exception(e, correlation_id: @correlation_id, destination: @destination)
49
49
  end
50
50
 
51
51
  def success(reply_to, response)
@@ -0,0 +1,46 @@
1
+ begin
2
+ require 'oj'
3
+ rescue LoadError
4
+ require 'symbolizer'
5
+ require 'json'
6
+ end
7
+
8
+ class Freddy
9
+ class Payload
10
+ def self.parse(payload)
11
+ return {} if payload == 'null'
12
+
13
+ json_handler.parse(payload)
14
+ end
15
+
16
+ def self.dump(payload)
17
+ json_handler.dump(payload)
18
+ end
19
+
20
+ def self.json_handler
21
+ @_json_handler ||= defined?(Oj) ? OjAdapter : JsonAdapter
22
+ end
23
+
24
+ class OjAdapter
25
+ def self.parse(payload)
26
+ Oj.strict_load(payload, symbol_keys: true)
27
+ end
28
+
29
+ def self.dump(payload)
30
+ Oj.dump(payload, mode: :compat)
31
+ end
32
+ end
33
+
34
+ class JsonAdapter
35
+ def self.parse(payload)
36
+ # MRI has :symbolize_keys, but JRuby does not. Not adding it at the
37
+ # moment.
38
+ Symbolizer.symbolize(JSON.parse(payload))
39
+ end
40
+
41
+ def self.dump(payload)
42
+ JSON.dump(payload)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -17,26 +17,10 @@ class Freddy
17
17
  @logger.debug "Producing message #{payload.inspect} to #{destination}"
18
18
 
19
19
  properties = properties.merge(routing_key: destination, content_type: CONTENT_TYPE)
20
- json_payload = payload.to_json
20
+ json_payload = Payload.dump(payload)
21
21
 
22
22
  @topic_exchange.publish json_payload, properties.dup
23
23
  @exchange.publish json_payload, properties.dup
24
24
  end
25
-
26
- def on_return(&block)
27
- if @exchange.respond_to? :on_return # Bunny
28
- @exchange.on_return do |return_info, properties, content|
29
- block.call(return_info[:reply_code], properties[:correlation_id])
30
- end
31
- elsif @channel.respond_to? :on_return # Hare
32
- @channel.on_return do |reply_code, _, exchange_name, _, properties|
33
- if exchange_name != Freddy::FREDDY_TOPIC_EXCHANGE_NAME
34
- block.call(reply_code, properties.correlation_id)
35
- end
36
- end
37
- else
38
- raise OnReturnNotImplemented.new "AMQP implementation doesn't implement on_return"
39
- end
40
- end
41
25
  end
42
26
  end
@@ -11,26 +11,19 @@ class Freddy
11
11
  class Request
12
12
  NO_ROUTE = 312
13
13
 
14
- class EmptyRequest < Exception
15
- end
16
-
17
- class EmptyResponder < Exception
18
- end
19
-
20
14
  def initialize(channel, logger, producer, consumer)
21
15
  @channel, @logger = channel, logger
22
16
  @producer, @consumer = producer, consumer
23
17
  @request_map = Hamster.mutable_hash
24
18
  @request_manager = RequestManager.new @request_map, @logger
25
19
 
26
- @producer.on_return do |reply_code, correlation_id|
20
+ @channel.on_return do |reply_code, correlation_id|
27
21
  if reply_code == NO_ROUTE
28
22
  @request_manager.no_route(correlation_id)
29
23
  end
30
24
  end
31
25
 
32
26
  @listening_for_responses_lock = Mutex.new
33
- @response_queue_lock = Mutex.new
34
27
  end
35
28
 
36
29
  def sync_request(destination, payload, opts)
@@ -63,61 +56,32 @@ class Freddy
63
56
  )
64
57
  end
65
58
 
66
- def respond_to(destination, &block)
67
- raise EmptyResponder unless block
68
-
69
- ensure_response_queue_exists
70
- @logger.info "Listening for requests on #{destination}"
71
- responder_handler = @consumer.consume destination do |payload, delivery|
72
- handler = MessageHandlers.for_type(delivery.metadata.type).new(@producer, destination, @logger)
73
-
74
- msg_handler = MessageHandler.new(handler, delivery)
75
- handler.handle_message payload, msg_handler, &block
76
- end
77
- responder_handler
78
- end
79
-
80
59
  private
81
60
 
82
- def create_response_queue
83
- AdaptiveQueue.new @channel.queue("", exclusive: true)
84
- end
61
+ def handle_response(delivery)
62
+ correlation_id = delivery.correlation_id
85
63
 
86
- def handle_response(payload, delivery)
87
- correlation_id = delivery.metadata.correlation_id
88
- request = @request_map[correlation_id]
89
- if request
64
+ if request = @request_map.delete(correlation_id)
90
65
  @logger.debug "Got response for request to #{request[:destination]} with correlation_id #{correlation_id}"
91
- @request_map.delete correlation_id
92
- request[:callback].call payload, delivery
66
+ request[:callback].call delivery.payload, delivery
93
67
  else
94
68
  @logger.warn "Got rpc response for correlation_id #{correlation_id} but there is no requester"
95
- Freddy.notify 'NoRequesterForResponse', "Got rpc response but there is no requester", correlation_id: correlation_id
69
+ Utils.notify 'NoRequesterForResponse', "Got rpc response but there is no requester", correlation_id: correlation_id
96
70
  end
97
71
  rescue Exception => e
98
72
  destination_report = request ? "to #{request[:destination]}" : ''
99
- @logger.error "Exception occured while handling the response of request made #{destination_report} with correlation_id #{correlation_id}: #{Freddy.format_exception e}"
100
- Freddy.notify_exception(e, destination: request[:destination], correlation_id: correlation_id)
101
- end
102
-
103
- def ensure_response_queue_exists
104
- @response_queue_lock.synchronize do
105
- @response_queue ||= create_response_queue
106
- end
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)
107
75
  end
108
76
 
109
77
  def ensure_listening_to_responses
78
+ return @listening_for_responses if defined?(@listening_for_responses)
79
+
110
80
  @listening_for_responses_lock.synchronize do
111
- if @listening_for_responses
112
- true
113
- else
114
- ensure_response_queue_exists
115
- @request_manager.start
116
- @consumer.dedicated_consume @response_queue do |payload, delivery|
117
- handle_response payload, delivery
118
- end
119
- @listening_for_responses = true
120
- end
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
121
85
  end
122
86
  end
123
87
  end
@@ -33,7 +33,7 @@ class Freddy
33
33
  @requests.delete correlation_id
34
34
 
35
35
  @logger.warn "Request timed out waiting response from #{request[:destination]}, correlation id #{correlation_id}"
36
- Freddy.notify 'RequestTimeout', "Request timed out waiting for response from #{request[:destination]}", {
36
+ Utils.notify 'RequestTimeout', "Request timed out waiting for response from #{request[:destination]}", {
37
37
  correlation_id: correlation_id,
38
38
  destination: request[:destination],
39
39
  timeout: request[:timeout]
@@ -1,21 +1,24 @@
1
1
  class Freddy
2
2
  class ResponderHandler
3
-
4
- def initialize(consumer, channel)
3
+ def initialize(consumer, consume_thread_pool)
5
4
  @consumer = consumer
6
- @channel = channel
5
+ @consume_thread_pool = consume_thread_pool
7
6
  end
8
7
 
9
- def cancel
8
+ # Shutdown responder
9
+ #
10
+ # Stop responding to messages immediately, Waits until all workers are
11
+ # finished and then returns.
12
+ #
13
+ # @return [void]
14
+ #
15
+ # @example
16
+ # responder = freddy.respond_to 'Queue' do |msg, handler|
17
+ # end
18
+ # responder.shutdown
19
+ def shutdown
10
20
  @consumer.cancel
11
- end
12
-
13
- def queue
14
- @consumer.queue
15
- end
16
-
17
- def destroy_destination
18
- @consumer.queue.delete
21
+ @consume_thread_pool.wait(:done)
19
22
  end
20
23
  end
21
24
  end
@@ -1,20 +1,29 @@
1
+ require 'thread'
1
2
  require 'timeout'
2
3
 
3
4
  class Freddy
4
5
  class SyncResponseContainer
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ end
9
+
5
10
  def call(response, delivery)
6
11
  @response = response
7
12
  @delivery = delivery
13
+ @mutex.synchronize { @waiting.wakeup }
8
14
  end
9
15
 
10
16
  def wait_for_response(timeout)
11
- Timeout::timeout(timeout) do
12
- sleep 0.001 until filled?
17
+ @mutex.synchronize do
18
+ @waiting = Thread.current
19
+ @mutex.sleep(timeout)
13
20
  end
14
21
 
15
- if @response[:error] == 'RequestTimeout'
22
+ if @response.nil?
23
+ raise Timeout::Error, 'execution expired'
24
+ elsif @response[:error] == 'RequestTimeout'
16
25
  raise TimeoutError.new(@response)
17
- elsif !@delivery || @delivery.metadata.type == 'error'
26
+ elsif !@delivery || @delivery.type == 'error'
18
27
  raise InvalidRequestError.new(@response)
19
28
  else
20
29
  @response
@@ -0,0 +1,4 @@
1
+ class Freddy
2
+ class TimeoutError < ErrorResponse
3
+ end
4
+ end
@@ -0,0 +1,32 @@
1
+ class Freddy
2
+ class Utils
3
+ def self.format_exception(exception)
4
+ backtrace = exception.backtrace.map do |x|
5
+ x.match(/^(.+?):(\d+)(|:in `(.+)')$/);
6
+ [$1, $2, $4]
7
+ end.join("\n")
8
+
9
+ "#{exception.exception}\n#{backtrace}"
10
+ end
11
+
12
+ def self.notify(name, message, parameters={})
13
+ return unless defined?(Airbrake)
14
+
15
+ Airbrake.notify_or_ignore(
16
+ error_class: name,
17
+ error_message: message,
18
+ cgi_data: ENV.to_hash,
19
+ parameters: parameters
20
+ )
21
+ end
22
+
23
+ def self.notify_exception(exception, parameters={})
24
+ return unless defined?(Airbrake)
25
+
26
+ Airbrake.notify_or_ignore(exception,
27
+ cgi_data: ENV.to_hash,
28
+ parameters: parameters
29
+ )
30
+ end
31
+ end
32
+ end
@@ -10,12 +10,8 @@ describe Freddy::Consumer do
10
10
 
11
11
  after { freddy.close }
12
12
 
13
- it 'raises exception when no consumer is provided' do
14
- expect { consumer.consume destination }.to raise_error described_class::EmptyConsumer
15
- end
16
-
17
13
  it "doesn't call passed block without any messages" do
18
- consumer.consume destination do
14
+ consumer.respond_to destination do
19
15
  @message_received = true
20
16
  end
21
17
  default_sleep
@@ -4,9 +4,7 @@ describe Freddy::MessageHandler do
4
4
  subject(:handler) { described_class.new(adapter, delivery) }
5
5
 
6
6
  let(:adapter) { double }
7
- let(:delivery) { double(metadata: metadata) }
8
- let(:metadata) { double(reply_to: reply_to, correlation_id: 'abc') }
9
-
7
+ let(:delivery) { double(reply_to: reply_to, correlation_id: 'abc') }
10
8
  let(:reply_to) { double }
11
9
 
12
10
  describe '#success' do
@@ -10,10 +10,6 @@ describe Freddy::Request do
10
10
 
11
11
  after { freddy.close }
12
12
 
13
- it 'raises empty responder exception when responding without callback' do
14
- expect {@responder = request.respond_to destination }.to raise_error described_class::EmptyResponder
15
- end
16
-
17
13
  context 'requesting from multiple threads' do
18
14
  let(:nr_of_threads) { 50 }
19
15
 
@@ -8,15 +8,49 @@ describe Freddy::ResponderHandler do
8
8
 
9
9
  after { freddy.close }
10
10
 
11
- it 'can cancel listening for messages' do
12
- consumer_handler = freddy.respond_to destination do
13
- @messages_count ||= 0
14
- @messages_count += 1
11
+ describe '#shutdown' do
12
+ it 'lets ongoing workers to finish' do
13
+ count = 0
14
+
15
+ consumer_handler = freddy.respond_to destination do
16
+ sleep 0.1
17
+ count += 1
18
+ end
19
+ deliver
20
+
21
+ sleep 0.05
22
+ consumer_handler.shutdown
23
+
24
+ expect(count).to eq(1)
25
+ end
26
+
27
+ it 'does not accept new jobs' do
28
+ count = 0
29
+
30
+ consumer_handler = freddy.respond_to destination do
31
+ count += 1
32
+ end
33
+
34
+ consumer_handler.shutdown
35
+ deliver
36
+
37
+ expect(count).to eq(0)
15
38
  end
16
- deliver
17
- consumer_handler.cancel
18
- deliver
19
39
 
20
- expect(@messages_count).to eq 1
40
+ it 'does not touch other handlers' do
41
+ count = 0
42
+
43
+ freddy.respond_to destination do
44
+ count += 1
45
+ end
46
+
47
+ consumer_handler2 = freddy.respond_to random_destination do
48
+ count += 1
49
+ end
50
+ consumer_handler2.shutdown
51
+
52
+ deliver
53
+ expect(count).to eq(1)
54
+ end
21
55
  end
22
56
  end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Freddy::SyncResponseContainer do
4
+ let(:container) { described_class.new }
5
+
6
+ context 'when timeout' do
7
+ subject { container.wait_for_response(0.01) }
8
+
9
+ it 'raises timeout error' do
10
+ expect { subject }.to raise_error(Timeout::Error, 'execution expired')
11
+ end
12
+ end
13
+ end
@@ -62,4 +62,26 @@ describe 'Concurrency' do
62
62
  expect(received1).to be(true)
63
63
  expect(received2).to be(true)
64
64
  end
65
+
66
+ it 'supports adding multiple #tap_into listeners' do
67
+ results = 10.times.map do |id|
68
+ Thread.new do
69
+ freddy1.tap_into "tap_into.listener.#{id}" do
70
+ end
71
+ end
72
+ end.map(&:join)
73
+
74
+ expect(results.count).to eq(10)
75
+ end
76
+
77
+ it 'supports adding multiple #respond_to listeners' do
78
+ results = 10.times.map do |id|
79
+ Thread.new do
80
+ freddy1.respond_to "respond_to.listener.#{id}" do
81
+ end
82
+ end
83
+ end.map(&:join)
84
+
85
+ expect(results.count).to eq(10)
86
+ end
65
87
  end
@@ -23,7 +23,6 @@ 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(/Consuming messages on \S+/)
27
26
  expect(logger1).to have_received(:debug).with(/Received message on \S+ with payload {:pay=>"load"}/)
28
27
  end
29
28
 
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.3
4
+ version: 0.4.9
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-11-14 00:00:00.000000000 Z
11
+ date: 2015-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -113,22 +113,33 @@ files:
113
113
  - Rakefile
114
114
  - freddy.gemspec
115
115
  - lib/freddy.rb
116
- - lib/freddy/adaptive_queue.rb
116
+ - lib/freddy/adapters.rb
117
+ - lib/freddy/adapters/bunny_adapter.rb
118
+ - lib/freddy/adapters/march_hare_adapter.rb
117
119
  - lib/freddy/consumer.rb
120
+ - lib/freddy/consumers/respond_to_consumer.rb
121
+ - lib/freddy/consumers/response_consumer.rb
122
+ - lib/freddy/consumers/tap_into_consumer.rb
118
123
  - lib/freddy/delivery.rb
124
+ - lib/freddy/error_response.rb
125
+ - lib/freddy/invalid_request_error.rb
119
126
  - lib/freddy/message_handler.rb
120
127
  - lib/freddy/message_handlers.rb
128
+ - lib/freddy/payload.rb
121
129
  - lib/freddy/producer.rb
122
130
  - lib/freddy/request.rb
123
131
  - lib/freddy/request_manager.rb
124
132
  - lib/freddy/responder_handler.rb
125
133
  - lib/freddy/sync_response_container.rb
134
+ - lib/freddy/timeout_error.rb
135
+ - lib/freddy/utils.rb
126
136
  - spec/freddy/consumer_spec.rb
127
137
  - spec/freddy/error_response_spec.rb
128
138
  - spec/freddy/freddy_spec.rb
129
139
  - spec/freddy/message_handler_spec.rb
130
140
  - spec/freddy/request_spec.rb
131
141
  - spec/freddy/responder_handler_spec.rb
142
+ - spec/freddy/sync_response_container_spec.rb
132
143
  - spec/integration/concurrency_spec.rb
133
144
  - spec/integration/logging_spec.rb
134
145
  - spec/spec_helper.rb
@@ -163,6 +174,7 @@ test_files:
163
174
  - spec/freddy/message_handler_spec.rb
164
175
  - spec/freddy/request_spec.rb
165
176
  - spec/freddy/responder_handler_spec.rb
177
+ - spec/freddy/sync_response_container_spec.rb
166
178
  - spec/integration/concurrency_spec.rb
167
179
  - spec/integration/logging_spec.rb
168
180
  - spec/spec_helper.rb
@@ -1,34 +0,0 @@
1
- class Freddy
2
- class AdaptiveQueue
3
- def initialize(queue)
4
- @queue = queue
5
- end
6
-
7
- def subscribe(&block)
8
- if hare?
9
- @queue.subscribe do |meta, payload|
10
- block.call(payload, Delivery.new(meta, meta.routing_key))
11
- end
12
- else
13
- @queue.subscribe do |info, properties, payload|
14
- block.call(payload, Delivery.new(properties, info.routing_key))
15
- end
16
- end
17
- end
18
-
19
- def bind(*args)
20
- @queue.bind(*args)
21
- self
22
- end
23
-
24
- def name
25
- @queue.name
26
- end
27
-
28
- private
29
-
30
- def hare?
31
- RUBY_PLATFORM == 'java'
32
- end
33
- end
34
- end