mbus 1.0.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 +7 -0
- data/README.mediawiki +169 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/bin/messagebus_swarm +77 -0
- data/lib/messagebus.rb +62 -0
- data/lib/messagebus/client.rb +166 -0
- data/lib/messagebus/cluster_map.rb +161 -0
- data/lib/messagebus/connection.rb +118 -0
- data/lib/messagebus/consumer.rb +447 -0
- data/lib/messagebus/custom_errors.rb +37 -0
- data/lib/messagebus/dottable_hash.rb +113 -0
- data/lib/messagebus/error_status.rb +42 -0
- data/lib/messagebus/logger.rb +45 -0
- data/lib/messagebus/message.rb +168 -0
- data/lib/messagebus/messagebus_types.rb +107 -0
- data/lib/messagebus/producer.rb +187 -0
- data/lib/messagebus/swarm.rb +49 -0
- data/lib/messagebus/swarm/controller.rb +296 -0
- data/lib/messagebus/swarm/drone.rb +195 -0
- data/lib/messagebus/swarm/drone/logging_worker.rb +53 -0
- data/lib/messagebus/validations.rb +68 -0
- data/lib/messagebus/version.rb +36 -0
- data/messagebus.gemspec +29 -0
- data/spec/messagebus/client_spec.rb +157 -0
- data/spec/messagebus/cluster_map_spec.rb +178 -0
- data/spec/messagebus/consumer_spec.rb +338 -0
- data/spec/messagebus/dottable_hash_spec.rb +137 -0
- data/spec/messagebus/message_spec.rb +93 -0
- data/spec/messagebus/producer_spec.rb +147 -0
- data/spec/messagebus/swarm/controller_spec.rb +73 -0
- data/spec/messagebus/validations_spec.rb +71 -0
- data/spec/spec_helper.rb +10 -0
- data/vendor/gems/stomp.rb +23 -0
- data/vendor/gems/stomp/client.rb +360 -0
- data/vendor/gems/stomp/connection.rb +583 -0
- data/vendor/gems/stomp/errors.rb +39 -0
- data/vendor/gems/stomp/ext/hash.rb +24 -0
- data/vendor/gems/stomp/message.rb +68 -0
- metadata +138 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
# Copyright (c) 2012, Groupon, Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright
|
12
|
+
# notice, this list of conditions and the following disclaimer in the
|
13
|
+
# documentation and/or other materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
20
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
21
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
22
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
23
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
25
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
require 'json'
|
32
|
+
|
33
|
+
module Messagebus
|
34
|
+
module Swarm
|
35
|
+
##
|
36
|
+
# This is a composition of a consumer and a separate message processor.
|
37
|
+
# It allows you to use Plain-Old-Ruby-Objects to do message processing.
|
38
|
+
# See #initialize for how the messages are delegated.
|
39
|
+
class Drone
|
40
|
+
# Raise this error when you want to abort processing a message and not
|
41
|
+
# acknowledge it.
|
42
|
+
class AbortProcessing < StandardError; end
|
43
|
+
|
44
|
+
INITIALIZING = "initializing"
|
45
|
+
RECEIVING = "receiving"
|
46
|
+
PROCESSING = "processing"
|
47
|
+
COMPLETED = "completed"
|
48
|
+
|
49
|
+
STOP_PROCESSING_MESSAGE = "stop processing message"
|
50
|
+
|
51
|
+
attr_reader :state, :id
|
52
|
+
|
53
|
+
##
|
54
|
+
# Expected options:
|
55
|
+
# * :ack_on_error (default false): whether to ack the message when an error was raised. Aliased to auto_acknowledge for backwards compatibility
|
56
|
+
# * :consumer (required): a consumer object for that topic
|
57
|
+
# * :destination_name (required): the message bus queue/topic name
|
58
|
+
# * :id: an id for this drone (just used for debugging)
|
59
|
+
# * :logger (required): the logger to publish messages to
|
60
|
+
# * :worker_class (required): the actual worker that will be used to do the processing
|
61
|
+
#
|
62
|
+
# As messages come down, they will be passed to the worker's perform or
|
63
|
+
# perform_on_destination method. The methods will be called in the
|
64
|
+
# following priority (if they exist):
|
65
|
+
# * perform_on_destination(message_payload, destination_message_came_from)
|
66
|
+
# * perform(message_payload)
|
67
|
+
#
|
68
|
+
# A message is processed by:
|
69
|
+
# message = receive_message
|
70
|
+
# begin
|
71
|
+
# worker.perform(message)
|
72
|
+
# ack(message)
|
73
|
+
# rescue AbortProcessing
|
74
|
+
# # raise this error if you want to say "Don't ack me"
|
75
|
+
# rescue StandardError
|
76
|
+
# ack(message) if ack_on_error
|
77
|
+
# end
|
78
|
+
def initialize(options)
|
79
|
+
@auto_acknowledge = options.fetch(:ack_on_error, options[:auto_acknowledge])
|
80
|
+
@consumer, @destination_name, @worker_class, @id, @logger = options.values_at(:consumer, :destination_name, :worker_class, :id, :logger)
|
81
|
+
@state = INITIALIZING
|
82
|
+
@logger.debug { "initializing a drone drone_id=#{@id}" }
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# This is the main loop for the drone's work. This will not return until
|
87
|
+
# the drone is stopped via #stop.
|
88
|
+
#
|
89
|
+
# If the consumer hasn't been started yet, this method will start it. It
|
90
|
+
# also will auto close the consumer in that case.
|
91
|
+
def processing_loop
|
92
|
+
@processing = true
|
93
|
+
|
94
|
+
auto_started_consumer = false
|
95
|
+
begin
|
96
|
+
if !@consumer.started?
|
97
|
+
@logger.debug "auto starting the consumer drone_id=#{@id}"
|
98
|
+
@consumer.start
|
99
|
+
auto_started_consumer = true
|
100
|
+
end
|
101
|
+
|
102
|
+
while @processing
|
103
|
+
message = nil
|
104
|
+
begin
|
105
|
+
@logger.debug "waiting for a message"
|
106
|
+
|
107
|
+
@state = RECEIVING
|
108
|
+
message = @consumer.receive
|
109
|
+
# intentional === here, this is used as a signal, so we can use object equality
|
110
|
+
# to check if we received the signal
|
111
|
+
if message === STOP_PROCESSING_MESSAGE
|
112
|
+
@logger.info "received a stop message, exiting drone_id=#{@id}, message=#{message.inspect}"
|
113
|
+
@state = COMPLETED
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
@logger.info "received message drone_id=#{@id}, message_id=#{message.message_id}"
|
118
|
+
|
119
|
+
@state = PROCESSING
|
120
|
+
@logger.info "processing message drone_id=#{@id}, message_id=#{message.message_id}, worker=#{@worker_class.name}"
|
121
|
+
|
122
|
+
raw_message = extract_underlying_message_body(message)
|
123
|
+
@logger.debug { "drone_id=#{@id} message_id=#{message.message_id}, message=#{raw_message.inspect}" }
|
124
|
+
|
125
|
+
worker_perform(@logger, @destination_name, @worker_class, @consumer, @auto_acknowledge, message, raw_message)
|
126
|
+
|
127
|
+
@state = COMPLETED
|
128
|
+
rescue => except
|
129
|
+
@logger.warn "exception processing message drone_id=#{@id}, message_id=#{message && message.message_id}, exception=\"#{except.message}\", exception_backtrace=#{except.backtrace.join("|")}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
ensure
|
133
|
+
if auto_started_consumer
|
134
|
+
@logger.debug "auto stopping the consumer drone_id=#{@id}"
|
135
|
+
@consumer.stop
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
@logger.info("gracefully exited drone_id=#{@id}")
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Stops this drone from processing any additional jobs.
|
144
|
+
# This will not abort any in progress jobs.
|
145
|
+
def stop
|
146
|
+
@logger.info("received stop message, current_state=#{@state}, processing=#{@processing}, drone_id=#{@id}")
|
147
|
+
return if !@processing
|
148
|
+
|
149
|
+
@processing = false
|
150
|
+
@consumer.insert_sentinel_value(STOP_PROCESSING_MESSAGE)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
def blocking_on_receive?(state)
|
155
|
+
state == RECEIVING
|
156
|
+
end
|
157
|
+
|
158
|
+
def worker_perform(logger, destination_name, worker_class, consumer, auto_acknowledge, message, raw_message)
|
159
|
+
begin
|
160
|
+
if worker_class.respond_to?("perform_on_destination")
|
161
|
+
worker_class.perform_on_destination(raw_message, destination_name)
|
162
|
+
else
|
163
|
+
worker_class.perform(raw_message)
|
164
|
+
end
|
165
|
+
|
166
|
+
# acknowledge unless exception is thrown (auto perform == true)
|
167
|
+
logger.debug "processing complete, acknowledging message, drone_id=#{@id}, message_id=#{message.message_id}"
|
168
|
+
consumer.ack
|
169
|
+
|
170
|
+
# This is just a clean error people can throw to trigger an abort
|
171
|
+
rescue AbortProcessing => e
|
172
|
+
logger.info "processing aborted drone_id=#{@id}, message_id=#{message.message_id}"
|
173
|
+
rescue => worker_exception
|
174
|
+
logger.warn "processing failed drone_id=#{@id}, message_id=#{message.message_id} exception=\"#{worker_exception.message}\", exception_backtrace=#{worker_exception.backtrace.join("|")}"
|
175
|
+
|
176
|
+
if auto_acknowledge
|
177
|
+
logger.info "despite failure, auto acknowledging message, drone_id=#{@id}, message_id=#{message.message_id}"
|
178
|
+
consumer.ack
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def extract_underlying_message_body(message)
|
184
|
+
payload = message.raw_message.payload
|
185
|
+
if payload.json?
|
186
|
+
JSON.parse(payload.stringPayload)
|
187
|
+
elsif payload.binary?
|
188
|
+
payload.binaryPayload
|
189
|
+
elsif payload.string?
|
190
|
+
payload.stringPayload
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Copyright (c) 2012, Groupon, Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright
|
12
|
+
# notice, this list of conditions and the following disclaimer in the
|
13
|
+
# documentation and/or other materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
20
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
21
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
22
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
23
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
25
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
require 'json'
|
32
|
+
|
33
|
+
module Messagebus
|
34
|
+
module Swarm
|
35
|
+
class Drone
|
36
|
+
##
|
37
|
+
# Use this for easy testing that a messagebus message is received.
|
38
|
+
# Example config:
|
39
|
+
# -
|
40
|
+
# :destination: jms.topic.some_destination_you_want_to_debug
|
41
|
+
# :subscription_id: <some_subscription_id>
|
42
|
+
# :worker: Messagebus::Swarm::Drone::LoggingWorker
|
43
|
+
# :drones: 1
|
44
|
+
class LoggingWorker
|
45
|
+
def self.perform_on_destination(message, destination)
|
46
|
+
log_message = %|received a message. destination=#{destination}, message=#{message.inspect}|
|
47
|
+
Rails.logger.info(log_message) if defined?(Rails.logger)
|
48
|
+
puts log_message
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Copyright (c) 2012, Groupon, Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright
|
12
|
+
# notice, this list of conditions and the following disclaimer in the
|
13
|
+
# documentation and/or other materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
20
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
21
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
22
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
23
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
25
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
module Messagebus
|
32
|
+
# :nodoc:all
|
33
|
+
module Validations
|
34
|
+
def valid_host?(string)
|
35
|
+
Messagebus::SERVER_REGEX.match(string)
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_destination_config(name, is_consumer = false, options = {})
|
39
|
+
raise InvalidDestination.new("destination name is nil") unless name
|
40
|
+
|
41
|
+
if is_consumer && name.match(/^jms.topic/) && options[:subscription_id].nil?
|
42
|
+
raise InvalidDestination.new("destination type TOPIC requires a subscription_id")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_connection_config(host_params, options = {})
|
47
|
+
if options[:ack_type] &&
|
48
|
+
options[:ack_type] != Messagebus::ACK_TYPE_AUTO_CLIENT &&
|
49
|
+
options[:ack_type] != Messagebus::ACK_TYPE_CLIENT
|
50
|
+
raise InvalidAcknowledgementType.new(options[:ack_type])
|
51
|
+
end
|
52
|
+
|
53
|
+
if host_params.nil?
|
54
|
+
raise InvalidHost.new(host_params)
|
55
|
+
end
|
56
|
+
|
57
|
+
if host_params.is_a?(Array)
|
58
|
+
host_params.each do |string|
|
59
|
+
unless valid_host?(string)
|
60
|
+
raise InvalidHost.new("host should be defined as <host>:<port>, received: #{host_params}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
else
|
64
|
+
raise InvalidHost.new("host should be defined as <host>:<port>, received #{host_params}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (c) 2012, Groupon, Inc.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions
|
6
|
+
# are met:
|
7
|
+
#
|
8
|
+
# Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
#
|
11
|
+
# Redistributions in binary form must reproduce the above copyright
|
12
|
+
# notice, this list of conditions and the following disclaimer in the
|
13
|
+
# documentation and/or other materials provided with the distribution.
|
14
|
+
#
|
15
|
+
# Neither the name of GROUPON nor the names of its contributors may be
|
16
|
+
# used to endorse or promote products derived from this software without
|
17
|
+
# specific prior written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
20
|
+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
21
|
+
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
22
|
+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
23
|
+
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
24
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
25
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
26
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
27
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
28
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
29
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
module Messagebus
|
32
|
+
MAJOR = 1
|
33
|
+
MINOR = 0
|
34
|
+
PATCH = 0
|
35
|
+
VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}"
|
36
|
+
end
|
data/messagebus.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require "#{File.dirname(__FILE__)}/lib/messagebus/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Pradeep Jawahar", "Lin Zhao", "Erik Weathers"]
|
6
|
+
gem.email = ["pjawahar@groupon.com", "lin@groupon.com", "eweathers@groupon.com"]
|
7
|
+
gem.description = %q{Messagebus integration gem}
|
8
|
+
gem.summary = %q{Messagebus Client}
|
9
|
+
gem.homepage = "https://github.com/groupon/Message-Bus"
|
10
|
+
|
11
|
+
gem.executables = %w(messagebus_swarm)
|
12
|
+
gem.files = Dir['bin/*'] + Dir["lib/**/*.rb"] + Dir["vendor/**/*"] + %w(README.mediawiki Rakefile messagebus.gemspec)
|
13
|
+
gem.test_files = Dir["spec*/**/*.rb"]
|
14
|
+
gem.name = "mbus"
|
15
|
+
gem.require_paths = ["vendor/gems", "lib"]
|
16
|
+
gem.version = Messagebus::VERSION
|
17
|
+
gem.license = 'BSD'
|
18
|
+
|
19
|
+
gem.required_rubygems_version = ">= 1.3.6"
|
20
|
+
|
21
|
+
if RUBY_VERSION < "1.9"
|
22
|
+
# json is built into ruby 1.9
|
23
|
+
gem.add_dependency "json"
|
24
|
+
end
|
25
|
+
gem.add_dependency "thrift", "0.9.0"
|
26
|
+
|
27
|
+
gem.add_development_dependency "rspec"
|
28
|
+
gem.add_development_dependency "simplecov"
|
29
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Messagebus::Client do
|
4
|
+
before do
|
5
|
+
@config = {
|
6
|
+
"enable_auto_init_connections" => true,
|
7
|
+
"clusters" => [
|
8
|
+
{
|
9
|
+
"type" => "producer-cluster",
|
10
|
+
"address" => "localhost:61613",
|
11
|
+
"destinations" => [
|
12
|
+
"jms.queue.testQueue1",
|
13
|
+
"jms.topic.testTopic1"
|
14
|
+
]
|
15
|
+
}
|
16
|
+
]
|
17
|
+
}
|
18
|
+
|
19
|
+
@logger = mock(Logger, :info => true, :error => true, :debug => true, :warn => true)
|
20
|
+
Logger.stub(:new).and_return(@logger)
|
21
|
+
|
22
|
+
@cluster_map = mock(Messagebus::ClusterMap, :start => true, :stop => true)
|
23
|
+
Messagebus::ClusterMap.stub!(:new).and_return(@cluster_map)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "constructing a client" do
|
28
|
+
describe "when creating a singleton logger" do
|
29
|
+
it "creates a logger using tmp directory if configuration is omitted" do
|
30
|
+
Messagebus::Client.new(@config)
|
31
|
+
Messagebus::Client.logger.should == @logger
|
32
|
+
end
|
33
|
+
|
34
|
+
it "creates a logger with the supplied log_file" do
|
35
|
+
Logger.should_receive(:new).with("log/messagebus-client.log").and_return(@logger)
|
36
|
+
Messagebus::Client.new(@config.merge("log_file" => "log/messagebus-client.log"))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "automatically starts the cluster if configured" do
|
41
|
+
@cluster_map.should_receive(:start)
|
42
|
+
Messagebus::Client.new(@config.merge("enable_auto_init_connections" => true)).start
|
43
|
+
end
|
44
|
+
|
45
|
+
it "automatically defaults to cluster not started" do
|
46
|
+
@cluster_map.should_not_receive(:start)
|
47
|
+
Messagebus::Client.new(@config)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".start (class method)" do
|
52
|
+
it "auto closes the connection when a block is given" do
|
53
|
+
@cluster_map.should_receive(:start)
|
54
|
+
@cluster_map.should_receive(:stop)
|
55
|
+
Messagebus::Client.start(@config.merge("enable_auto_init_connections" => true)) {}
|
56
|
+
end
|
57
|
+
|
58
|
+
it "makes sure it stops if the block errors out" do
|
59
|
+
@cluster_map.should_receive(:start)
|
60
|
+
@cluster_map.should_receive(:stop)
|
61
|
+
proc do
|
62
|
+
Messagebus::Client.start(@config.merge("enable_auto_init_connections" => true)) do
|
63
|
+
raise "error123"
|
64
|
+
end
|
65
|
+
end.should raise_error("error123")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "doesn't auto close if no block passed" do
|
69
|
+
@cluster_map.should_receive(:start)
|
70
|
+
@cluster_map.should_not_receive(:stop)
|
71
|
+
Messagebus::Client.start(@config.merge("enable_auto_init_connections" => true))
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#start" do
|
77
|
+
it "delegates to the cluster map start" do
|
78
|
+
@cluster_map.should_receive(:start)
|
79
|
+
@cluster_map.should_not_receive(:stop)
|
80
|
+
Messagebus::Client.new(@config.merge("enable_auto_init_connections" => true)).start
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#stop" do
|
85
|
+
it "delegates to the cluster map stop" do
|
86
|
+
@cluster_map.should_receive(:stop)
|
87
|
+
Messagebus::Client.new(@config).stop
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#logger" do
|
92
|
+
it "shares the logger across instances" do
|
93
|
+
Messagebus::Client.new(@config).logger.should be(Messagebus::Client.new(@config).logger)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#publish" do
|
98
|
+
before do
|
99
|
+
@producer = mock(Messagebus::Producer, :publish => true)
|
100
|
+
@cluster_map.stub!(:find).and_return(@producer)
|
101
|
+
@producer.stub!(:started?).and_return(true)
|
102
|
+
|
103
|
+
@message = mock(Messagebus::Message, :create => true, :message_id => "ee4ae017f409dc3f4aad38dddeb7bf88")
|
104
|
+
Messagebus::Message.stub!(:create).and_return(@message)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "creates a message from the provided object" do
|
108
|
+
Messagebus::Message.should_receive(:create).with({:benjamin => :franklins})
|
109
|
+
Messagebus::Client.new(@config).publish("jms.queue.Testqueue1", {:benjamin => :franklins})
|
110
|
+
end
|
111
|
+
|
112
|
+
it "requires an existing destination" do
|
113
|
+
@producer.should_not_receive(:publish)
|
114
|
+
lambda {
|
115
|
+
@cluster_map.stub!(:find).and_return(nil)
|
116
|
+
Messagebus::Client.new(@config).publish("jms.queue.testQueue1", {:benjamin => :franklins})
|
117
|
+
}.should raise_error(Messagebus::Client::InvalidDestinationError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "sends a safe message by default" do
|
121
|
+
@producer.should_receive(:publish)
|
122
|
+
Messagebus::Client.new(@config).publish("jms.queue.testQueue1", {:benjamin => :franklins})
|
123
|
+
end
|
124
|
+
|
125
|
+
it "sends an unsafe message by when the safe parameter is false" do
|
126
|
+
@producer.should_receive(:publish)
|
127
|
+
Messagebus::Client.new(@config).publish("jms.queue.testQueue1", {:benjamin => :franklins}, 0, false)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "sends a message with headers" do
|
131
|
+
headers = {"priority" => 6}
|
132
|
+
@producer.should_receive(:publish).with("jms.queue.testQueue1", @message, headers, false)
|
133
|
+
Messagebus::Client.new(@config).publish("jms.queue.testQueue1", {:benjamin => :franklins}, 0, false, false, headers)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "#reload_config_on_interval" do
|
138
|
+
it "reloads config when the interval is reached" do
|
139
|
+
@cluster_map.should_receive(:update_config).with(@config)
|
140
|
+
Messagebus::Client.new(@config).reload_config_on_interval(@config,0)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "#headers" do
|
145
|
+
it "creates a header from current time and the delay milliseconds provided to publish" do
|
146
|
+
delay = 3
|
147
|
+
time = Time.now
|
148
|
+
Time.stub!(:now).and_return(time)
|
149
|
+
|
150
|
+
client = Messagebus::Client.new(@config)
|
151
|
+
client.headers(delay).should == {
|
152
|
+
Messagebus::Producer::SCHEDULED_DELIVERY_TIME_MS_HEADER => ((time.to_i * 1000) + delay).to_s
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|