beetle 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +5 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +82 -0
  4. data/Rakefile +114 -0
  5. data/TODO +7 -0
  6. data/beetle.gemspec +127 -0
  7. data/etc/redis-master.conf +189 -0
  8. data/etc/redis-slave.conf +189 -0
  9. data/examples/README.rdoc +14 -0
  10. data/examples/attempts.rb +66 -0
  11. data/examples/handler_class.rb +64 -0
  12. data/examples/handling_exceptions.rb +73 -0
  13. data/examples/multiple_exchanges.rb +48 -0
  14. data/examples/multiple_queues.rb +43 -0
  15. data/examples/redis_failover.rb +65 -0
  16. data/examples/redundant.rb +65 -0
  17. data/examples/rpc.rb +45 -0
  18. data/examples/simple.rb +39 -0
  19. data/lib/beetle.rb +57 -0
  20. data/lib/beetle/base.rb +78 -0
  21. data/lib/beetle/client.rb +252 -0
  22. data/lib/beetle/configuration.rb +31 -0
  23. data/lib/beetle/deduplication_store.rb +152 -0
  24. data/lib/beetle/handler.rb +95 -0
  25. data/lib/beetle/message.rb +336 -0
  26. data/lib/beetle/publisher.rb +187 -0
  27. data/lib/beetle/r_c.rb +40 -0
  28. data/lib/beetle/subscriber.rb +144 -0
  29. data/script/start_rabbit +29 -0
  30. data/snafu.rb +55 -0
  31. data/test/beetle.yml +81 -0
  32. data/test/beetle/base_test.rb +52 -0
  33. data/test/beetle/bla.rb +0 -0
  34. data/test/beetle/client_test.rb +305 -0
  35. data/test/beetle/configuration_test.rb +5 -0
  36. data/test/beetle/deduplication_store_test.rb +90 -0
  37. data/test/beetle/handler_test.rb +105 -0
  38. data/test/beetle/message_test.rb +744 -0
  39. data/test/beetle/publisher_test.rb +407 -0
  40. data/test/beetle/r_c_test.rb +9 -0
  41. data/test/beetle/subscriber_test.rb +263 -0
  42. data/test/beetle_test.rb +5 -0
  43. data/test/test_helper.rb +20 -0
  44. data/tmp/master/.gitignore +2 -0
  45. data/tmp/slave/.gitignore +3 -0
  46. 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
@@ -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
+
@@ -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
+
@@ -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
@@ -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