beetle 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +114 -0
- data/TODO +7 -0
- data/beetle.gemspec +127 -0
- data/etc/redis-master.conf +189 -0
- data/etc/redis-slave.conf +189 -0
- data/examples/README.rdoc +14 -0
- data/examples/attempts.rb +66 -0
- data/examples/handler_class.rb +64 -0
- data/examples/handling_exceptions.rb +73 -0
- data/examples/multiple_exchanges.rb +48 -0
- data/examples/multiple_queues.rb +43 -0
- data/examples/redis_failover.rb +65 -0
- data/examples/redundant.rb +65 -0
- data/examples/rpc.rb +45 -0
- data/examples/simple.rb +39 -0
- data/lib/beetle.rb +57 -0
- data/lib/beetle/base.rb +78 -0
- data/lib/beetle/client.rb +252 -0
- data/lib/beetle/configuration.rb +31 -0
- data/lib/beetle/deduplication_store.rb +152 -0
- data/lib/beetle/handler.rb +95 -0
- data/lib/beetle/message.rb +336 -0
- data/lib/beetle/publisher.rb +187 -0
- data/lib/beetle/r_c.rb +40 -0
- data/lib/beetle/subscriber.rb +144 -0
- data/script/start_rabbit +29 -0
- data/snafu.rb +55 -0
- data/test/beetle.yml +81 -0
- data/test/beetle/base_test.rb +52 -0
- data/test/beetle/bla.rb +0 -0
- data/test/beetle/client_test.rb +305 -0
- data/test/beetle/configuration_test.rb +5 -0
- data/test/beetle/deduplication_store_test.rb +90 -0
- data/test/beetle/handler_test.rb +105 -0
- data/test/beetle/message_test.rb +744 -0
- data/test/beetle/publisher_test.rb +407 -0
- data/test/beetle/r_c_test.rb +9 -0
- data/test/beetle/subscriber_test.rb +263 -0
- data/test/beetle_test.rb +5 -0
- data/test/test_helper.rb +20 -0
- data/tmp/master/.gitignore +2 -0
- data/tmp/slave/.gitignore +3 -0
- metadata +192 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# multiple_queues.rb
|
2
|
+
# this example shows how to route a message thru two queues to two handlers
|
3
|
+
# we're using the client.configure block to not duplicate our settings
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# ! check the examples/README.rdoc for information on starting your redis/rabbit !
|
7
|
+
#
|
8
|
+
# start it with ruby multiple_queues.rb
|
9
|
+
|
10
|
+
require "rubygems"
|
11
|
+
require File.expand_path(File.dirname(__FILE__)+"/../lib/beetle")
|
12
|
+
|
13
|
+
# set Beetle log level to info, less noisy than debug
|
14
|
+
Beetle.config.logger.level = Logger::INFO
|
15
|
+
|
16
|
+
# setup client
|
17
|
+
client = Beetle::Client.new
|
18
|
+
|
19
|
+
# this is our block configuration option, set options are used for all configs within it
|
20
|
+
# in this example all items will use: exchange => foobar and key => foobar
|
21
|
+
# so creating the two queues queue_1 and queue_2 will be bound to the same exchange
|
22
|
+
# and same key, the message foobar will also use those setting
|
23
|
+
client.configure :exchange => :foobar, :key => "foobar" do |config|
|
24
|
+
config.queue :queue_1
|
25
|
+
config.queue :queue_2
|
26
|
+
config.message :foobar
|
27
|
+
# different than other examples we use the option to configure a handler with a simple blocl
|
28
|
+
# rather than subclassing Beetle::Handler, this allow for very easy handlers to be created
|
29
|
+
# with a minimal amount of code
|
30
|
+
config.handler(:queue_1) {|message| puts "received message on queue 1: " + message.data}
|
31
|
+
# both queues will be getting the same messages ...
|
32
|
+
config.handler(:queue_2) {|message| puts "received message on queue 2: " + message.data}
|
33
|
+
end
|
34
|
+
|
35
|
+
# ... and publish a message, we expect both queue handlers to output the message
|
36
|
+
client.publish(:foobar, "baz")
|
37
|
+
|
38
|
+
# start the listen loop and stop listening after 0.1 seconds
|
39
|
+
# this should be more than enough time to finish processing our messages
|
40
|
+
client.listen do
|
41
|
+
EM.add_timer(0.1) { client.stop_listening }
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Testing redis failover functionality
|
2
|
+
require "rubygems"
|
3
|
+
require File.expand_path(File.dirname(__FILE__)+"/../lib/beetle")
|
4
|
+
|
5
|
+
Beetle.config.logger.level = Logger::INFO
|
6
|
+
Beetle.config.redis_hosts = "localhost:6379, localhost:6380"
|
7
|
+
Beetle.config.servers = "localhost:5672, localhost:5673"
|
8
|
+
|
9
|
+
# instantiate a client
|
10
|
+
client = Beetle::Client.new
|
11
|
+
|
12
|
+
# register a durable queue named 'test'
|
13
|
+
# this implicitly registers a durable topic exchange called 'test'
|
14
|
+
client.register_queue(:test)
|
15
|
+
client.purge(:test)
|
16
|
+
client.register_message(:test, :redundant => true)
|
17
|
+
|
18
|
+
# publish some test messages
|
19
|
+
# at this point, the exchange will be created on the server and the queue will be bound to the exchange
|
20
|
+
N = 10
|
21
|
+
n = 0
|
22
|
+
N.times do |i|
|
23
|
+
n += client.publish(:test, "Hello#{i+1}")
|
24
|
+
end
|
25
|
+
puts "published #{n} test messages"
|
26
|
+
puts
|
27
|
+
|
28
|
+
# check whether we were able to publish all messages
|
29
|
+
if n != 2*N
|
30
|
+
puts "could not publish all messages"
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
|
34
|
+
# register a handler for the test message, listing on queue "test"
|
35
|
+
k = 0
|
36
|
+
client.register_handler(:test) do |m|
|
37
|
+
k += 1
|
38
|
+
puts "Received test message from server #{m.server}"
|
39
|
+
puts "Message content: #{m.data}"
|
40
|
+
puts
|
41
|
+
sleep 1
|
42
|
+
end
|
43
|
+
|
44
|
+
# hack to switch redis programmatically
|
45
|
+
class Beetle::DeduplicationStore
|
46
|
+
def switch_redis
|
47
|
+
slave = redis_instances.find{|r| r.server != redis.server}
|
48
|
+
redis.shutdown rescue nil
|
49
|
+
logger.info "Beetle: shut down master #{redis.server}"
|
50
|
+
slave.slaveof("no one")
|
51
|
+
logger.info "Beetle: enabled master mode on #{slave.server}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# start listening
|
56
|
+
# this starts the event machine loop using EM.run
|
57
|
+
# the block passed to listen will be yielded as the last step of the setup process
|
58
|
+
client.listen do
|
59
|
+
trap("INT") { client.stop_listening }
|
60
|
+
EM.add_timer(5) { client.deduplication_store.switch_redis }
|
61
|
+
EM.add_timer(11) { client.stop_listening }
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "Received #{k} test messages"
|
65
|
+
raise "Your setup is borked" if N != k
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# redundant.rb
|
2
|
+
#
|
3
|
+
#
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# ! check the examples/README.rdoc for information on starting your redis/rabbit !
|
7
|
+
#
|
8
|
+
# start it with ruby redundant.rb
|
9
|
+
|
10
|
+
require "rubygems"
|
11
|
+
require File.expand_path("../lib/beetle", File.dirname(__FILE__))
|
12
|
+
|
13
|
+
# set Beetle log level to info, less noisy than debug
|
14
|
+
Beetle.config.logger.level = Logger::INFO
|
15
|
+
|
16
|
+
# setup client
|
17
|
+
client = Beetle::Client.new
|
18
|
+
|
19
|
+
# use two servers
|
20
|
+
Beetle.config.servers = "localhost:5672, localhost:5673"
|
21
|
+
# instantiate a client
|
22
|
+
client = Beetle::Client.new
|
23
|
+
|
24
|
+
# register a durable queue named 'test'
|
25
|
+
# this implicitly registers a durable topic exchange called 'test'
|
26
|
+
client.register_queue(:test)
|
27
|
+
client.purge(:test)
|
28
|
+
client.register_message(:test, :redundant => true)
|
29
|
+
|
30
|
+
# publish some test messages
|
31
|
+
# at this point, the exchange will be created on the server and the queue will be bound to the exchange
|
32
|
+
N = 3
|
33
|
+
n = 0
|
34
|
+
N.times do |i|
|
35
|
+
n += client.publish(:test, "Hello#{i+1}")
|
36
|
+
end
|
37
|
+
puts "published #{n} test messages"
|
38
|
+
puts
|
39
|
+
|
40
|
+
expected_publish_count = 2*N
|
41
|
+
if n != expected_publish_count
|
42
|
+
puts "could not publish all messages"
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
|
46
|
+
# register a handler for the test message, listing on queue "test"
|
47
|
+
k = 0
|
48
|
+
client.register_handler(:test) do |m|
|
49
|
+
k += 1
|
50
|
+
puts "Received test message from server #{m.server}"
|
51
|
+
puts m.msg_id
|
52
|
+
p m.header
|
53
|
+
puts "Message content: #{m.data}"
|
54
|
+
puts
|
55
|
+
end
|
56
|
+
|
57
|
+
# start listening
|
58
|
+
# this starts the event machine event loop using EM.run
|
59
|
+
# the block passed to listen will be yielded as the last step of the setup process
|
60
|
+
client.listen do
|
61
|
+
EM.add_timer(0.1) { client.stop_listening }
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "Received #{k} test messages"
|
65
|
+
raise "Your setup is borked" if N != k
|
data/examples/rpc.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# A simple usage example for Beetle
|
2
|
+
require "rubygems"
|
3
|
+
require File.expand_path(File.dirname(__FILE__)+"/../lib/beetle")
|
4
|
+
|
5
|
+
# suppress debug messages
|
6
|
+
Beetle.config.logger.level = Logger::DEBUG
|
7
|
+
|
8
|
+
# instantiate a client
|
9
|
+
client = Beetle::Client.new(:servers => "localhost:5672, localhost:5673")
|
10
|
+
|
11
|
+
# register a durable queue named 'test'
|
12
|
+
# this implicitly registers a durable topic exchange called 'test'
|
13
|
+
client.register_queue(:echo)
|
14
|
+
client.register_message(:echo)
|
15
|
+
|
16
|
+
if ARGV.include?("--server")
|
17
|
+
# register a handler for the test message, listing on queue "test"
|
18
|
+
# echoing all data sent to it
|
19
|
+
client.register_handler(:echo) do |m|
|
20
|
+
# send data back to publisher
|
21
|
+
m.data
|
22
|
+
end
|
23
|
+
|
24
|
+
# start the subscriber
|
25
|
+
client.listen do
|
26
|
+
puts "started echo server"
|
27
|
+
trap("INT") { puts "stopped echo server"; client.stop_listening }
|
28
|
+
end
|
29
|
+
else
|
30
|
+
n = 100
|
31
|
+
ms = Benchmark.ms do
|
32
|
+
n.times do |i|
|
33
|
+
content = "Hello #{i}"
|
34
|
+
# puts "performing RPC with message content '#{content}'"
|
35
|
+
status, result = client.rpc(:echo, content)
|
36
|
+
# puts "status #{status}"
|
37
|
+
# puts "result #{result}"
|
38
|
+
# puts
|
39
|
+
$stderr.puts "processing failure for message '#{content}'" if result != content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
printf "Runtime: %dms\n", ms
|
43
|
+
printf "Milliseconds per RPC: %.1f\n", ms/n
|
44
|
+
end
|
45
|
+
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# simple.rb
|
2
|
+
# this example shows you a very basic message/handler setup
|
3
|
+
#
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# ! check the examples/README.rdoc for information on starting your redis/rabbit !
|
7
|
+
#
|
8
|
+
# start it with ruby multiple_exchanges.rb
|
9
|
+
|
10
|
+
require "rubygems"
|
11
|
+
require File.expand_path("../lib/beetle", File.dirname(__FILE__))
|
12
|
+
|
13
|
+
# set Beetle log level to info, less noisy than debug
|
14
|
+
Beetle.config.logger.level = Logger::INFO
|
15
|
+
|
16
|
+
# setup client
|
17
|
+
client = Beetle::Client.new
|
18
|
+
client.register_queue(:test)
|
19
|
+
client.register_message(:test)
|
20
|
+
|
21
|
+
# purge the test queue
|
22
|
+
client.purge(:test)
|
23
|
+
|
24
|
+
# empty the dedup store
|
25
|
+
client.deduplication_store.flushdb
|
26
|
+
|
27
|
+
# register our handler to the message, check out the message.rb for more stuff you can get from the message object
|
28
|
+
client.register_handler(:test) {|message| puts "got message: #{message.data}"}
|
29
|
+
|
30
|
+
# publish our message
|
31
|
+
client.publish(:test, 'bam')
|
32
|
+
|
33
|
+
# start listening
|
34
|
+
# this starts the event machine event loop using EM.run
|
35
|
+
# the block passed to listen will be yielded as the last step of the setup process
|
36
|
+
client.listen do
|
37
|
+
EM.add_timer(0.1) { client.stop_listening }
|
38
|
+
end
|
39
|
+
|
data/lib/beetle.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'mq'
|
3
|
+
require 'bunny'
|
4
|
+
require 'uuid4r'
|
5
|
+
require 'active_support'
|
6
|
+
require 'redis'
|
7
|
+
|
8
|
+
module Beetle
|
9
|
+
|
10
|
+
# abstract superclass for Beetle specific exceptions
|
11
|
+
class Error < StandardError; end
|
12
|
+
# raised when Beetle detects configuration errors
|
13
|
+
class ConfigurationError < Error; end
|
14
|
+
# raised when trying to access an unknown message
|
15
|
+
class UnknownMessage < Error; end
|
16
|
+
# raised when trying to access an unknown queue
|
17
|
+
class UnknownQueue < Error; end
|
18
|
+
# raised when no redis master server can be found
|
19
|
+
class NoRedisMaster < Error; end
|
20
|
+
# raised when two redis master servers are found
|
21
|
+
class TwoRedisMasters < Error; end
|
22
|
+
|
23
|
+
# AMQP options for exchange creation
|
24
|
+
EXCHANGE_CREATION_KEYS = [:auto_delete, :durable, :internal, :nowait, :passive]
|
25
|
+
# AMQP options for queue creation
|
26
|
+
QUEUE_CREATION_KEYS = [:passive, :durable, :exclusive, :auto_delete, :no_wait]
|
27
|
+
# AMQP options for queue bindings
|
28
|
+
QUEUE_BINDING_KEYS = [:key, :no_wait]
|
29
|
+
# AMQP options for message publishing
|
30
|
+
PUBLISHING_KEYS = [:key, :mandatory, :immediate, :persistent, :reply_to]
|
31
|
+
# AMQP options for subscribing to queues
|
32
|
+
SUBSCRIPTION_KEYS = [:ack, :key]
|
33
|
+
|
34
|
+
# use ruby's autoload mechanism for loading beetle classes
|
35
|
+
lib_dir = File.expand_path(File.dirname(__FILE__) + '/beetle/')
|
36
|
+
Dir["#{lib_dir}/*.rb"].each do |libfile|
|
37
|
+
autoload File.basename(libfile)[/^(.*)\.rb$/, 1].classify, libfile
|
38
|
+
end
|
39
|
+
|
40
|
+
# returns the default configuration object and yields it if a block is given
|
41
|
+
def self.config
|
42
|
+
#:yields: config
|
43
|
+
@config ||= Configuration.new
|
44
|
+
block_given? ? yield(@config) : @config
|
45
|
+
end
|
46
|
+
|
47
|
+
# FIXME: there should be a better way to test
|
48
|
+
if defined?(Mocha)
|
49
|
+
def self.reraise_expectation_errors! #:nodoc:
|
50
|
+
raise if $!.is_a?(Mocha::ExpectationError)
|
51
|
+
end
|
52
|
+
else
|
53
|
+
def self.reraise_expectation_errors! #:nodoc:
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
data/lib/beetle/base.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Beetle
|
2
|
+
# Abstract base class shared by Publisher and Subscriber
|
3
|
+
class Base
|
4
|
+
|
5
|
+
attr_accessor :options, :servers, :server #:nodoc:
|
6
|
+
|
7
|
+
def initialize(client, options = {}) #:nodoc:
|
8
|
+
@options = options
|
9
|
+
@client = client
|
10
|
+
@servers = @client.servers.clone
|
11
|
+
@server = @servers[rand @servers.size]
|
12
|
+
@exchanges = {}
|
13
|
+
@queues = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def logger
|
19
|
+
self.class.logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.logger
|
23
|
+
Beetle.config.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def error(text)
|
27
|
+
logger.error text
|
28
|
+
raise Error.new(text)
|
29
|
+
end
|
30
|
+
|
31
|
+
def current_host
|
32
|
+
@server.split(':').first
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_port
|
36
|
+
@server =~ /:(\d+)$/ ? $1.to_i : 5672
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_current_server(s)
|
40
|
+
@server = s
|
41
|
+
end
|
42
|
+
|
43
|
+
def each_server
|
44
|
+
@servers.each { |s| set_current_server(s); yield }
|
45
|
+
end
|
46
|
+
|
47
|
+
def exchanges
|
48
|
+
@exchanges[@server] ||= {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def exchange(name)
|
52
|
+
exchanges[name] ||= create_exchange!(name, @client.exchanges[name])
|
53
|
+
end
|
54
|
+
|
55
|
+
def queues
|
56
|
+
@queues[@server] ||= {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def queue(name)
|
60
|
+
queues[name] ||=
|
61
|
+
begin
|
62
|
+
opts = @client.queues[name]
|
63
|
+
error("You are trying to bind a queue #{name} which is not configured!") unless opts
|
64
|
+
logger.debug("Beetle: binding queue #{name} with internal name #{opts[:amqp_name]} on server #{@server}")
|
65
|
+
queue_name = opts[:amqp_name]
|
66
|
+
creation_options = opts.slice(*QUEUE_CREATION_KEYS)
|
67
|
+
the_queue = nil
|
68
|
+
@client.bindings[name].each do |binding_options|
|
69
|
+
exchange_name = binding_options[:exchange]
|
70
|
+
binding_options = binding_options.slice(*QUEUE_BINDING_KEYS)
|
71
|
+
the_queue = bind_queue!(queue_name, creation_options, exchange_name, binding_options)
|
72
|
+
end
|
73
|
+
the_queue
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Beetle
|
2
|
+
# This class provides the interface through which messaging is configured for both
|
3
|
+
# message producers and consumers. It keeps references to an instance of a
|
4
|
+
# Beetle::Subscriber, a Beetle::Publisher (both of which are instantiated on demand),
|
5
|
+
# and a reference to an instance of Beetle::DeduplicationStore.
|
6
|
+
#
|
7
|
+
# Configuration of exchanges, queues, messages, and message handlers is done by calls to
|
8
|
+
# corresponding register_ methods. Note that these methods just build up the
|
9
|
+
# configuration, they don't interact with the AMQP servers.
|
10
|
+
#
|
11
|
+
# On the publisher side, publishing a message will ensure that the exchange it will be
|
12
|
+
# sent to, and each of the queues bound to the exchange, will be created on demand. On
|
13
|
+
# the subscriber side, exchanges, queues, bindings and queue subscriptions will be
|
14
|
+
# created when the application calls the listen method. An application can decide to
|
15
|
+
# subscribe to only a subset of the configured queues by passing a list of queue names
|
16
|
+
# to the listen method.
|
17
|
+
#
|
18
|
+
# The net effect of this strategy is that producers and consumers can be started in any
|
19
|
+
# order, so that no message is lost if message producers are accidentally started before
|
20
|
+
# the corresponding consumers.
|
21
|
+
class Client
|
22
|
+
# the AMQP servers available for publishing
|
23
|
+
attr_reader :servers
|
24
|
+
|
25
|
+
# an options hash for the configured exchanges
|
26
|
+
attr_reader :exchanges
|
27
|
+
|
28
|
+
# an options hash for the configured queues
|
29
|
+
attr_reader :queues
|
30
|
+
|
31
|
+
# an options hash for the configured queue bindings
|
32
|
+
attr_reader :bindings
|
33
|
+
|
34
|
+
# an options hash for the configured messages
|
35
|
+
attr_reader :messages
|
36
|
+
|
37
|
+
# the deduplication store to use for this client
|
38
|
+
attr_reader :deduplication_store
|
39
|
+
|
40
|
+
# create a fresh Client instance from a given configuration object
|
41
|
+
def initialize(config = Beetle.config)
|
42
|
+
@servers = config.servers.split(/ *, */)
|
43
|
+
@exchanges = {}
|
44
|
+
@queues = {}
|
45
|
+
@messages = {}
|
46
|
+
@bindings = {}
|
47
|
+
@deduplication_store = DeduplicationStore.new(config.redis_hosts, config.redis_db)
|
48
|
+
end
|
49
|
+
|
50
|
+
# register an exchange with the given _name_ and a set of _options_:
|
51
|
+
# [<tt>:type</tt>]
|
52
|
+
# the type option will be overwritten and always be <tt>:topic</tt>, beetle does not allow fanout exchanges
|
53
|
+
# [<tt>:durable</tt>]
|
54
|
+
# the durable option will be overwritten and always be true. this is done to ensure that exchanges are never deleted
|
55
|
+
|
56
|
+
def register_exchange(name, options={})
|
57
|
+
name = name.to_s
|
58
|
+
raise ConfigurationError.new("exchange #{name} already configured") if exchanges.include?(name)
|
59
|
+
exchanges[name] = options.symbolize_keys.merge(:type => :topic, :durable => true)
|
60
|
+
end
|
61
|
+
|
62
|
+
# register a durable, non passive, non auto_deleted queue with the given _name_ and an _options_ hash:
|
63
|
+
# [<tt>:exchange</tt>]
|
64
|
+
# the name of the exchange this queue will be bound to (defaults to the name of the queue)
|
65
|
+
# [<tt>:key</tt>]
|
66
|
+
# the binding key (defaults to the name of the queue)
|
67
|
+
# automatically registers the specified exchange if it hasn't been registered yet
|
68
|
+
|
69
|
+
def register_queue(name, options={})
|
70
|
+
name = name.to_s
|
71
|
+
raise ConfigurationError.new("queue #{name} already configured") if queues.include?(name)
|
72
|
+
opts = {:exchange => name, :key => name}.merge!(options.symbolize_keys)
|
73
|
+
opts.merge! :durable => true, :passive => false, :exclusive => false, :auto_delete => false, :amqp_name => name
|
74
|
+
exchange = opts.delete(:exchange).to_s
|
75
|
+
key = opts.delete(:key)
|
76
|
+
queues[name] = opts
|
77
|
+
register_binding(name, :exchange => exchange, :key => key)
|
78
|
+
end
|
79
|
+
|
80
|
+
# register an additional binding for an already configured queue _name_ and an _options_ hash:
|
81
|
+
# [<tt>:exchange</tt>]
|
82
|
+
# the name of the exchange this queue will be bound to (defaults to the name of the queue)
|
83
|
+
# [<tt>:key</tt>]
|
84
|
+
# the binding key (defaults to the name of the queue)
|
85
|
+
# automatically registers the specified exchange if it hasn't been registered yet
|
86
|
+
|
87
|
+
def register_binding(queue_name, options={})
|
88
|
+
name = queue_name.to_s
|
89
|
+
opts = options.symbolize_keys
|
90
|
+
exchange = (opts[:exchange] || name).to_s
|
91
|
+
key = (opts[:key] || name).to_s
|
92
|
+
(bindings[name] ||= []) << {:exchange => exchange, :key => key}
|
93
|
+
register_exchange(exchange) unless exchanges.include?(exchange)
|
94
|
+
queues = (exchanges[exchange][:queues] ||= [])
|
95
|
+
queues << name unless queues.include?(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# register a persistent message with a given _name_ and an _options_ hash:
|
99
|
+
# [<tt>:key</tt>]
|
100
|
+
# specifies the routing key for message publishing (defaults to the name of the message)
|
101
|
+
# [<tt>:ttl</tt>]
|
102
|
+
# specifies the time interval after which the message will be silently dropped (seconds).
|
103
|
+
# defaults to Message::DEFAULT_TTL.
|
104
|
+
# [<tt>:redundant</tt>]
|
105
|
+
# specifies whether the message should be published redundantly (defaults to false)
|
106
|
+
|
107
|
+
def register_message(message_name, options={})
|
108
|
+
name = message_name.to_s
|
109
|
+
raise ConfigurationError.new("message #{name} already configured") if messages.include?(name)
|
110
|
+
opts = {:exchange => name, :key => name}.merge!(options.symbolize_keys)
|
111
|
+
opts.merge! :persistent => true
|
112
|
+
opts[:exchange] = opts[:exchange].to_s
|
113
|
+
messages[name] = opts
|
114
|
+
end
|
115
|
+
|
116
|
+
# registers a handler for a list of queues (which must have been registered
|
117
|
+
# previously). The handler will be invoked when any messages arrive on the queue.
|
118
|
+
#
|
119
|
+
# Examples:
|
120
|
+
# register_handler([:foo, :bar], :timeout => 10.seconds) { |message| puts "received #{message}" }
|
121
|
+
#
|
122
|
+
# on_error = lambda{ puts "something went wrong with baz" }
|
123
|
+
# on_failure = lambda{ puts "baz has finally failed" }
|
124
|
+
#
|
125
|
+
# register_handler(:baz, :exceptions => 1, :errback => on_error, :failback => on_failure) { puts "received baz" }
|
126
|
+
#
|
127
|
+
# register_handler(:bar, BarHandler)
|
128
|
+
#
|
129
|
+
# For details on handler classes see class Beetle::Handler
|
130
|
+
|
131
|
+
def register_handler(queues, *args, &block)
|
132
|
+
queues = Array(queues).map(&:to_s)
|
133
|
+
queues.each {|q| raise UnknownQueue.new(q) unless self.queues.include?(q)}
|
134
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
135
|
+
handler = args.shift
|
136
|
+
raise ArgumentError.new("too many arguments for handler registration") unless args.empty?
|
137
|
+
subscriber.register_handler(queues, opts, handler, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
# this is a convenience method to configure exchanges, queues, messages and handlers
|
141
|
+
# with a common set of options. allows one to call all register methods without the
|
142
|
+
# register_ prefix.
|
143
|
+
#
|
144
|
+
# Example:
|
145
|
+
# client.configure :exchange => :foobar do |config|
|
146
|
+
# config.queue :q1, :key => "foo"
|
147
|
+
# config.queue :q2, :key => "bar"
|
148
|
+
# config.message :foo
|
149
|
+
# config.message :bar
|
150
|
+
# config.handler :q1 { puts "got foo"}
|
151
|
+
# config.handler :q2 { puts "got bar"}
|
152
|
+
# end
|
153
|
+
def configure(options={}) #:yields: config
|
154
|
+
yield Configurator.new(self, options)
|
155
|
+
end
|
156
|
+
|
157
|
+
# publishes a message. the given options hash is merged with options given on message registration.
|
158
|
+
def publish(message_name, data=nil, opts={})
|
159
|
+
message_name = message_name.to_s
|
160
|
+
raise UnknownMessage.new("unknown message #{message_name}") unless messages.include?(message_name)
|
161
|
+
publisher.publish(message_name, data, opts)
|
162
|
+
end
|
163
|
+
|
164
|
+
# sends the given message to one of the configured servers and returns the result of running the associated handler.
|
165
|
+
#
|
166
|
+
# unexpected behavior can ensue if the message gets routed to more than one recipient, so be careful.
|
167
|
+
def rpc(message_name, data=nil, opts={})
|
168
|
+
message_name = message_name.to_s
|
169
|
+
raise UnknownMessage.new("unknown message #{message_name}") unless messages.include?(message_name)
|
170
|
+
publisher.rpc(message_name, data, opts)
|
171
|
+
end
|
172
|
+
|
173
|
+
# purges the given queue on all configured servers
|
174
|
+
def purge(queue_name)
|
175
|
+
queue_name = queue_name.to_s
|
176
|
+
raise UnknownQueue.new("unknown queue #{queue_name}") unless queues.include?(queue_name)
|
177
|
+
publisher.purge(queue_name)
|
178
|
+
end
|
179
|
+
|
180
|
+
# start listening to a list of messages (default to all registered messages).
|
181
|
+
# runs the given block before entering the eventmachine loop.
|
182
|
+
def listen(messages=self.messages.keys, &block)
|
183
|
+
messages = messages.map(&:to_s)
|
184
|
+
messages.each{|m| raise UnknownMessage.new("unknown message #{m}") unless self.messages.include?(m)}
|
185
|
+
subscriber.listen(messages, &block)
|
186
|
+
end
|
187
|
+
|
188
|
+
# stops the eventmachine loop
|
189
|
+
def stop_listening
|
190
|
+
subscriber.stop!
|
191
|
+
end
|
192
|
+
|
193
|
+
# disconnects the publisher from all servers it's currently connected to
|
194
|
+
def stop_publishing
|
195
|
+
publisher.stop
|
196
|
+
end
|
197
|
+
|
198
|
+
# traces all messages received on all queues. useful for debugging message flow.
|
199
|
+
def trace(&block)
|
200
|
+
queues.each do |name, opts|
|
201
|
+
opts.merge! :durable => false, :auto_delete => true, :amqp_name => queue_name_for_tracing(name)
|
202
|
+
end
|
203
|
+
register_handler(queues.keys) do |msg|
|
204
|
+
puts "-----===== new message =====-----"
|
205
|
+
puts "SERVER: #{msg.server}"
|
206
|
+
puts "HEADER: #{msg.header.inspect}"
|
207
|
+
puts "MSGID: #{msg.msg_id}"
|
208
|
+
puts "DATA: #{msg.data}"
|
209
|
+
end
|
210
|
+
subscriber.listen(messages.keys, &block)
|
211
|
+
end
|
212
|
+
|
213
|
+
# evaluate the ruby files matching the given +glob+ pattern in the context of the client instance.
|
214
|
+
def load(glob)
|
215
|
+
b = binding
|
216
|
+
Dir[glob].each do |f|
|
217
|
+
eval(File.read(f), b, f)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# returns the configured Logger instance
|
222
|
+
def logger
|
223
|
+
@logger ||= Beetle.config.logger
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
class Configurator #:nodoc:all
|
229
|
+
def initialize(client, options={})
|
230
|
+
@client = client
|
231
|
+
@options = options
|
232
|
+
end
|
233
|
+
def method_missing(method, *args, &block)
|
234
|
+
super unless %w(exchange queue binding message handler).include?(method.to_s)
|
235
|
+
options = @options.merge(args.last.is_a?(Hash) ? args.pop : {})
|
236
|
+
@client.send("register_#{method}", *(args+[options]), &block)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def publisher
|
241
|
+
@publisher ||= Publisher.new(self)
|
242
|
+
end
|
243
|
+
|
244
|
+
def subscriber
|
245
|
+
@subscriber ||= Subscriber.new(self)
|
246
|
+
end
|
247
|
+
|
248
|
+
def queue_name_for_tracing(queue)
|
249
|
+
"trace-#{queue}-#{`hostname`.chomp}-#{$$}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|