beetle 0.1
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.
- 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
|