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