droid 0.9.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.swp
2
+ pkg/
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "droid"
8
+ gem.summary = %Q{AMQP Wrapper Library}
9
+ gem.description = %Q{Easy to use AMQP Library with constructs for typical usage patterns}
10
+ gem.email = "ricardo@heroku.com"
11
+ gem.homepage = "http://heroku.com"
12
+ gem.authors = ["Ricardo Chimal, Jr."]
13
+
14
+ gem.add_development_dependency "baconmocha", ">= 0"
15
+
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ gem.add_dependency 'json_pure', '>= 1.2.0'
18
+ gem.add_dependency 'rest-client', '>= 1.2.0'
19
+ gem.add_dependency 'amqp', '0.6.7'
20
+ gem.add_dependency 'bunny', '~> 0.6.0'
21
+ gem.add_dependency 'SystemTimer', '~> 1.2.0'
22
+ gem.add_dependency 'eventmachine_httpserver', '0.2.0'
23
+ end
24
+ Jeweler::GemcutterTasks.new
25
+ rescue LoadError
26
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
27
+ end
28
+
29
+ require 'rake/testtask'
30
+ Rake::TestTask.new(:spec) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.verbose = true
34
+ end
35
+
36
+ task :spec => :check_dependencies
37
+
38
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/bleedq ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+
5
+ require 'pp'
6
+ require 'droid/sync'
7
+
8
+ queue_name = ARGV.shift or fail "usage: #{File.basename($0)} <queue>"
9
+
10
+ b = Droid.bunny
11
+ q = b.queue(queue_name)
12
+
13
+ puts "Bleeding queue #{queue_name}"
14
+
15
+ while ((msg = q.pop)[:payload] != :queue_empty) do
16
+ pp msg
17
+ end
18
+
19
+ puts "done."
data/droid.gemspec ADDED
@@ -0,0 +1,100 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{droid}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ricardo Chimal, Jr."]
12
+ s.date = %q{2010-10-21}
13
+ s.default_executable = %q{bleedq}
14
+ s.description = %q{Easy to use AMQP Library with constructs for typical usage patterns}
15
+ s.email = %q{ricardo@heroku.com}
16
+ s.executables = ["bleedq"]
17
+ s.files = [
18
+ ".gitignore",
19
+ "Rakefile",
20
+ "VERSION",
21
+ "bin/bleedq",
22
+ "droid.gemspec",
23
+ "examples/async_reply.rb",
24
+ "examples/heroku_async_reply.rb",
25
+ "examples/sync.rb",
26
+ "examples/worker.rb",
27
+ "lib/droid.rb",
28
+ "lib/droid/em.rb",
29
+ "lib/droid/heroku.rb",
30
+ "lib/droid/heroku/local_stats.rb",
31
+ "lib/droid/heroku/logger_client.rb",
32
+ "lib/droid/heroku/memcache_cluster.rb",
33
+ "lib/droid/heroku/stats.rb",
34
+ "lib/droid/json_server.rb",
35
+ "lib/droid/monkey.rb",
36
+ "lib/droid/publish.rb",
37
+ "lib/droid/queue.rb",
38
+ "lib/droid/request.rb",
39
+ "lib/droid/sync.rb",
40
+ "lib/droid/utilization.rb",
41
+ "lib/droid/utils.rb",
42
+ "lib/heroku_droid.rb",
43
+ "lib/local_stats.rb",
44
+ "lib/memcache_cluster.rb",
45
+ "lib/stats.rb",
46
+ "spec/publish_spec.rb",
47
+ "spec/response_spec.rb",
48
+ "spec/spec_helper.rb",
49
+ "spec/utils_spec.rb",
50
+ "spec/wait_for_port_spec.rb"
51
+ ]
52
+ s.homepage = %q{http://heroku.com}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.6}
56
+ s.summary = %q{AMQP Wrapper Library}
57
+ s.test_files = [
58
+ "spec/publish_spec.rb",
59
+ "spec/response_spec.rb",
60
+ "spec/spec_helper.rb",
61
+ "spec/utils_spec.rb",
62
+ "spec/wait_for_port_spec.rb",
63
+ "examples/async_reply.rb",
64
+ "examples/heroku_async_reply.rb",
65
+ "examples/sync.rb",
66
+ "examples/worker.rb"
67
+ ]
68
+
69
+ if s.respond_to? :specification_version then
70
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
71
+ s.specification_version = 3
72
+
73
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
74
+ s.add_development_dependency(%q<baconmocha>, [">= 0"])
75
+ s.add_runtime_dependency(%q<json_pure>, [">= 1.2.0"])
76
+ s.add_runtime_dependency(%q<rest-client>, [">= 1.2.0"])
77
+ s.add_runtime_dependency(%q<amqp>, ["= 0.6.7"])
78
+ s.add_runtime_dependency(%q<bunny>, ["~> 0.6.0"])
79
+ s.add_runtime_dependency(%q<SystemTimer>, ["~> 1.2.0"])
80
+ s.add_runtime_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
81
+ else
82
+ s.add_dependency(%q<baconmocha>, [">= 0"])
83
+ s.add_dependency(%q<json_pure>, [">= 1.2.0"])
84
+ s.add_dependency(%q<rest-client>, [">= 1.2.0"])
85
+ s.add_dependency(%q<amqp>, ["= 0.6.7"])
86
+ s.add_dependency(%q<bunny>, ["~> 0.6.0"])
87
+ s.add_dependency(%q<SystemTimer>, ["~> 1.2.0"])
88
+ s.add_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
89
+ end
90
+ else
91
+ s.add_dependency(%q<baconmocha>, [">= 0"])
92
+ s.add_dependency(%q<json_pure>, [">= 1.2.0"])
93
+ s.add_dependency(%q<rest-client>, [">= 1.2.0"])
94
+ s.add_dependency(%q<amqp>, ["= 0.6.7"])
95
+ s.add_dependency(%q<bunny>, ["~> 0.6.0"])
96
+ s.add_dependency(%q<SystemTimer>, ["~> 1.2.0"])
97
+ s.add_dependency(%q<eventmachine_httpserver>, ["= 0.2.0"])
98
+ end
99
+ end
100
+
@@ -0,0 +1,25 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'droid'
3
+
4
+ def log
5
+ Droid.log
6
+ end
7
+
8
+ Droid.new('Example Reply') do |droid|
9
+ droid.worker('example.target').subscribe do |req|
10
+ log.debug "headers: #{req.header.headers.inspect}"
11
+ log.debug "event_hash should be woot -> #{req.droid_headers[:event_hash]}"
12
+ req.reply(:target_received_at => Time.now.to_i)
13
+ req.ack
14
+ end
15
+
16
+ droid.listener('example.check.target').subscribe do |req|
17
+ req.publish('example.target', { :checking => Time.now.to_i }) do |req2|
18
+ log.debug "event_hash should be woot -> #{req2.droid_headers[:event_hash]}"
19
+ log.info "We're done checking!"
20
+ Droid.stop_safe
21
+ end
22
+ end
23
+
24
+ droid.timer(2) { Droid.publish('example.check.target', {:sent_at => Time.now.to_i}, {:event_hash => "woot"}) }
25
+ end
@@ -0,0 +1,22 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib/'
2
+ require 'heroku_droid'
3
+
4
+ def log
5
+ Droid.log
6
+ end
7
+
8
+ HerokuDroid.new('Example Reply') do |droid|
9
+ droid.worker('example.target').subscribe do |req|
10
+ req.reply(:target_received_at => Time.now.to_i)
11
+ req.ack
12
+ end
13
+
14
+ droid.listener('example.check.target').subscribe do |req|
15
+ req.publish('example.target', { :checking => Time.now.to_i }) do |req2|
16
+ log.info "We're done checking!"
17
+ Droid.stop_safe
18
+ end
19
+ end
20
+
21
+ EM.add_timer(2) { Droid.publish('example.check.target', :sent_at => Time.now.to_i ) }
22
+ end
data/examples/sync.rb ADDED
@@ -0,0 +1,32 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'droid'
3
+
4
+ def log
5
+ Droid.log
6
+ end
7
+
8
+ if ARGV[0] == 'listen'
9
+ Droid.new('Example Worker') do |droid|
10
+ droid.worker('example.bunny.worker').subscribe do |req|
11
+ req.ack
12
+ log.info "Work done, replying..."
13
+ req.reply(:t => Time.now.to_i)
14
+ end
15
+
16
+ droid.listener('example.bunny.listener').subscribe do |req|
17
+ log.info "I heard #{req['t']}"
18
+ end
19
+ end
20
+ exit(0)
21
+ end
22
+
23
+
24
+ require 'droid/sync'
25
+
26
+ log.info "publishing to example.bunny.listener.."
27
+ Droid.publish('example.bunny.listener', :t => Time.now.to_i)
28
+
29
+ log.info "publishing to example.bunner.worker, expecting a result..."
30
+ res = Droid.call('example.bunny.worker', :t => Time.now.to_i)
31
+ log.info "received #{res.inspect}"
32
+
@@ -0,0 +1,58 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'droid'
3
+
4
+ def log
5
+ Droid.log
6
+ end
7
+
8
+ @gum_chews = 0
9
+ @toffee_chews = 0
10
+
11
+ Droid.new('Example Worker') do |droid|
12
+
13
+ # auto acks the message
14
+ @gum = droid.worker('example.chew.gum').subscribe do |req|
15
+ log.info "flavor: #{req['flavor']}, packs: #{req['packs']}"
16
+ log.info req.msg.inspect
17
+ log.info req.droid_headers
18
+
19
+ @gum_chews += 1
20
+ end
21
+
22
+ # explicit ack
23
+ @toffee = droid.worker('example.chew.toffee').subscribe(:auto_ack => false) do |req|
24
+ log.info "flavor: #{req['flavor']}"
25
+ log.info req.msg.inspect
26
+ log.info req.droid_headers
27
+
28
+ req.ack
29
+
30
+ @toffee_chews += 1
31
+ end
32
+
33
+ droid.periodic_timer(2) do
34
+ log.debug "checking gum & toffee chews"
35
+
36
+ if @gum_chews == 3 && @gum
37
+ @gum.destroy
38
+ @gum = nil
39
+ end
40
+
41
+ if @toffee_chews == 2 && @toffee
42
+ @toffee.destroy
43
+ @toffee = nil
44
+ end
45
+
46
+ if @toffee.nil? && @gum.nil?
47
+ Droid.stop_safe
48
+ end
49
+ end
50
+
51
+ droid.timer(2) do
52
+ droid.publish('example.chew.gum', :flavor => 'spearmint', :packs => 2)
53
+ droid.publish('example.chew.gum', :flavor => 'bubblegum', :packs => 3)
54
+ droid.publish('example.chew.gum', :flavor => 'peppermint', :packs => 1)
55
+ droid.publish('example.chew.toffee', :flavor => 'caramel')
56
+ droid.publish('example.chew.toffee', :flavor => 'licorish')
57
+ end
58
+ end
data/lib/droid.rb CHANGED
@@ -1,529 +1,141 @@
1
- require 'socket'
2
- require 'digest/md5'
3
- require File.dirname(__FILE__) + '/../vendor/logger_client/init'
4
-
5
- $:.unshift *Dir[File.dirname(__FILE__) + '/../vendor/*/lib']
6
- require 'json'
1
+ require 'uri'
2
+ require 'amqp'
7
3
  require 'mq'
8
- require 'time'
9
- require 'bunny'
10
-
11
- require File.dirname(__FILE__) + '/utilization'
12
-
13
- class Droid
14
- DEFAULT_TTL = 300
15
-
16
- class BadPayload < RuntimeError; end
17
-
18
- ## basic ops
19
- ## ######
20
- ## publish / broadcast
21
- ## listen / subscribe
22
-
23
- def self.con_type
24
- Thread.current['con_type'] ||= :sync
25
- end
26
-
27
- def self.con_type=(type)
28
- Thread.current['con_type'] = type
29
- end
30
4
 
31
- def self.async?
32
- return @@async == true rescue false
33
- end
5
+ if !defined?(JSON) && !defined?(JSON_LOADED)
6
+ require 'json/pure'
7
+ end
34
8
 
35
- def self.async(&blk)
36
- ensure_con_type(:async, &blk)
37
- end
9
+ require 'droid/monkey'
38
10
 
39
- def self.sync(&blk)
40
- ensure_con_type(:sync, &blk)
41
- end
42
-
43
- def self.ensure_con_type(type, &blk)
44
- old_type = con_type
45
- self.con_type = type
46
- begin
47
- blk.call
48
- ensure
49
- self.con_type = old_type
50
- end
51
- end
11
+ require 'droid/utils'
12
+ require 'droid/publish'
13
+ require 'droid/request'
14
+ require 'droid/utilization'
15
+ require 'droid/queue'
16
+ require 'droid/em'
52
17
 
53
- def self.new_event_hash
54
- s = Time.now.to_s + self.object_id.to_s + rand(100).to_s
55
- Digest::MD5.hexdigest(s)
18
+ class Droid
19
+ def self.version
20
+ @@version ||= File.read(File.dirname(__FILE__) + '/../VERSION').strip
56
21
  end
57
22
 
58
- def self.queue(name, options = {})
59
- reconnect_on_error do
60
- if con_type == :async
61
- MQ.queue(name, options)
62
- else
63
- bunny.queue(name, options)
64
- end
65
- end
23
+ def self.name
24
+ @@name
66
25
  end
67
26
 
68
- if defined? Bunny::Client # the blog below doesn't work with newer versions of bunny
69
- # Disable bunny's 1 second socket connect timeout since the
70
- # reconnect_on_error method sets up a separate 10 second timeout.
71
- # Using a timeout of zero sets an "infinite" timeout and has the nice
72
- # benefit of not starting up another thread.
73
- ::Bunny::Client.send(:remove_const, :CONNECT_TIMEOUT)
74
- ::Bunny::Client::CONNECT_TIMEOUT = 0
27
+ def self.name=(name)
28
+ @@name = name
75
29
  end
76
30
 
77
- def self.reconnect_on_error
78
- Timeout::timeout(20) do
79
- begin
80
- yield
81
- rescue Bunny::ProtocolError
82
- sleep 0.5
83
- retry
84
- rescue Bunny::ConnectionError
85
- sleep 0.5
86
- @@bunny = nil
87
- retry
88
- rescue Bunny::ServerDownError
89
- sleep 0.5
90
- @@bunny = nil
91
- retry
92
- end
93
- end
31
+ def self.log=(log)
32
+ @@log = log
94
33
  end
95
34
 
96
- def self.call(key, payload, options={})
97
- sync do
98
- reply_to = key + '.reply.' + Droid.gensym
99
- @q = nil
100
- begin
101
- reconnect_on_error do
102
-
103
- ## this is retarded - i shouldn't be binding here - just popping the queue - need to teach hermes/em
104
- @q = queue(reply_to, :auto_delete => true)
105
- @q.bind(exchange, :key => reply_to)
106
-
107
- payload[:reply_to] = reply_to
108
- publish(key, payload, options)
35
+ def self.log
36
+ @@log ||= begin
37
+ require 'logger'
38
+ Logger.class_eval <<EORUBY
39
+ alias_method :notice, :info
109
40
 
110
- pop(reply_to)
41
+ alias_method :error_og, :error
42
+ def error(err, opts={})
43
+ e = opts[:exception]
44
+ if e.respond_to?(:backtrace)
45
+ err += "\n" + e.backtrace.join("\n ")
46
+ end
47
+ error_og(err)
111
48
  end
112
- ensure
113
- # for some reason the auto_delete flag is not working correctly with Bunny
114
- # so we're deleting the queue manually here
115
- @q.delete if @q
116
- end
117
- end
118
- end
119
-
120
- def self.pop(queue)
121
- loop do
122
- raise "POP must be sync" unless con_type == :sync
123
- result = queue(queue).pop
124
- result = result[:payload] if result.is_a?(Hash)
125
- return JSON.parse(result) unless result == :queue_empty
126
- sleep 0.1
127
- end
128
- end
129
-
130
- def self.push(queue_name, payload, options={})
131
- reconnect_on_error do
132
- queue(queue_name).publish(payload_to_data(payload, options))
133
- end
134
- end
135
-
136
- def self.payload_to_data(payload, options)
137
- raise BadPayload unless payload.is_a?(Hash)
138
-
139
- payload[:event_hash] ||= new_event_hash
140
- payload[:published_on] = options[:published_on] || Time.now.getgm.to_i
141
- payload[:ttl] ||= (options[:ttl] || DEFAULT_TTL).to_i
142
-
143
- payload.to_json
144
- end
145
-
146
- def self.publish(key, payload, options={})
147
- payload[:message_id] = new_event_hash
148
- res =
149
- reconnect_on_error do
150
- exchange.publish(payload_to_data(payload, options), :key => key, :immediate => options[:immediate])
151
- end
152
-
153
- unless options[:log] == false
154
- Log.notice "amqp_message action=published key=#{key} #{payload_summary(payload)}"
155
- end
156
-
157
- res
158
- end
159
-
160
- def self.header_keys
161
- @header_keys ||= [:exchange, :delivery_mode, :delivery_tag, :redelivered, :consumer_tag, :content_type, :key, :priority]
162
- end
163
-
164
- def self.payload_summary(payload)
165
- payload = payload.select do |k, v|
166
- !header_keys.include?(k.to_sym)
49
+ EORUBY
50
+ Logger.new($stderr)
167
51
  end
168
- return ' -> (empty payload)' if payload.empty?
169
- resume = payload.map do |k, v|
170
- v = v.to_s
171
- v = v[0..37] + '...' if v.size > 40
172
- "#{k}=#{v}"
173
- end.join(", ")
174
- " -> #{resume}"
175
52
  end
176
53
 
177
- def self.exchange
178
- if con_type == :async
179
- MQ.topic('amq.topic')
180
- else
181
- bunny.exchange("amq.topic")
182
- end
183
- end
184
-
185
- def self.default_options
186
- uri = URI.parse(ENV["AMQP_URI"] || 'rabbit://guest:guest@localhost:5672/')
187
- raise "invalid AMQP_URI [#{uri.to_s}]" unless uri.scheme == "rabbit"
188
- {
189
- :vhost => uri.path,
190
- :host => uri.host,
191
- :user => uri.user,
192
- :port => uri.port,
193
- :pass => uri.password
194
- }
195
- end
196
-
197
54
  def self.default_config
198
- default_options
55
+ uri = URI.parse(ENV["AMQP_URL"] || 'amqp://guest:guest@localhost:5672/')
56
+ {
57
+ :vhost => uri.path,
58
+ :host => uri.host,
59
+ :user => uri.user,
60
+ :port => uri.port || 5672,
61
+ :pass => uri.password
62
+ }
63
+ rescue Object => e
64
+ raise "invalid AMQP_URL: (#{uri.inspect}) #{e.class} -> #{e.message}"
199
65
  end
200
66
 
201
- def self.new_bunny
202
- b = Bunny.new(default_options)
203
- b.start
204
- b
205
- end
67
+ def self.start(opts={})
68
+ config = opts[:config] || self.default_config
206
69
 
207
- def self.bunny
208
- @@bunny ||= new_bunny
209
- end
70
+ wait_for_tcp_port(config[:host], config[:port])
210
71
 
211
- def self.start(options = nil, &blk)
212
- async do
213
- begin
214
- Signal.trap('INT') { AMQP.stop{ EM.stop } }
215
- Signal.trap('TERM'){ AMQP.stop{ EM.stop } }
216
- EM.run do
217
- AMQP.start(options || default_options)
218
- blk.call if blk
219
- end
220
- rescue AMQP::Error => e
221
- STDERR.puts "Caught #{e.class}, sleeping to avoid inittab thrashing"
222
- sleep 5
223
- STDERR.puts "Done."
224
- raise
72
+ begin
73
+ ::Signal.trap('INT') { ::AMQP.stop{ ::EM.stop } }
74
+ ::Signal.trap('TERM'){ ::AMQP.stop{ ::EM.stop } }
75
+
76
+ ::AMQP.start(config) do
77
+ yield if block_given?
225
78
  end
79
+ rescue ::AMQP::Error => e
80
+ log.debug "Caught #{e.class}, sleeping to avoid inittab thrashing"
81
+ sleep 5
82
+ log.debug "Done."
83
+ raise
226
84
  end
227
85
  end
228
86
 
229
87
  def self.stop_safe
230
- EM.add_timer(1) { AMQP.stop { EM.stop }}
88
+ ::EM.add_timer(0.2) { ::AMQP.stop { ::EM.stop } }
231
89
  end
232
90
 
233
- def self.gen_queue(droid, key)
234
- dn = droid
235
- dn = dn.name if dn.respond_to?(:name)
236
- dn ||= "d"
237
- dn.gsub!(" ", "")
238
- "#{self.gen_instance_queue(key)}.#{dn}"
91
+ def self.closing?
92
+ ::AMQP.closing?
239
93
  end
240
94
 
241
- def self.gen_instance_queue(key)
242
- "#{key}.#{LocalStats.slot}.#{LocalStats.ion_instance_id}"
95
+ def self.handle_error(err)
96
+ log.error "#{err.class}: #{err.message}", :exception => err
243
97
  end
244
98
 
245
- def self.gensym
246
- values = [
247
- rand(0x0010000),
248
- rand(0x0010000),
249
- rand(0x0010000),
250
- rand(0x0010000),
251
- rand(0x0010000),
252
- rand(0x1000000),
253
- rand(0x1000000),
254
- ]
255
- "%04x%04x%04x%04x%04x%06x%06x" % values
256
- end
99
+ def self.wait_for_tcp_port(host, port, opts={})
100
+ require 'system_timer'
101
+ require 'socket'
257
102
 
258
- def self.wait_for_tcp_port(host, port, options={:retries => 5, :timeout => 5})
259
- require 'timeout'
260
- options[:retries].times do
103
+ opts[:retries] ||= 6
104
+ opts[:timeout] ||= 5
105
+
106
+ opts[:retries].times do
261
107
  begin
262
- Timeout::timeout(options[:timeout]) {
108
+ SystemTimer::timeout(opts[:timeout]) do
263
109
  TCPSocket.new(host.to_s, port).close
264
- }
110
+ end
265
111
  return
266
- rescue Object
267
- Log.notice "#{host}:#{port} not available, waiting..."
112
+ rescue Object => e
113
+ log.info "#{host}:#{port} not available, waiting... #{e.class}: #{e.message}"
268
114
  sleep 1
269
115
  end
270
116
  end
271
117
 
272
- raise "#{host}:#{port} did not come up after #{options[:retries]} retries"
118
+ raise "#{host}:#{port} did not come up after #{opts[:retries]} retries"
273
119
  end
274
120
 
275
- # Trap exceptions leaving the block and log them. Do not re-raise.
276
- def self.trap_exceptions
277
- yield
278
- rescue => boom
279
- Log.default_error boom
280
- end
281
-
282
- # Add a one-shot timer.
283
- def self.timer(duration, &bk)
284
- EM.add_timer(duration) { trap_exceptions(&bk) }
285
- end
286
-
287
- # Add a periodic timer. If the now argument is true, run the block
288
- # immediately in addition to scheduling the periodic timer.
289
- def self.periodic_timer(duration, now=false, &bk)
290
- timer(1, &bk) if now
291
- EM.add_periodic_timer(duration) { trap_exceptions(&bk) }
292
- end
121
+ def initialize(name, opts={})
122
+ log.info "=== #{name} droid initializing"
293
123
 
294
- def timer(duration, &bk) ; self.class.timer(duration, &bk) ; end
295
- def periodic_timer(duration, now=false, &bk) ; self.class.periodic_timer(duration, now, &bk) ; end
296
-
297
- class Basic
298
- def initialize(droid, options={})
299
- @droid = droid
300
- end
301
-
302
- def exchange
303
- Droid.exchange
304
- end
305
-
306
- def headers
307
- @headers ||= { :event_hash => self.event_hash }
308
- end
309
-
310
- def event_hash
311
- @event_hash ||= Droid.new_event_hash
312
- end
313
-
314
- def publish(key, payload, options={}, &blk)
315
- raise BadPayload unless payload.is_a?(Hash)
316
-
317
- result =if blk
318
- headers[:reply_to] = key + '.reply.' + Droid.gensym
319
- @droid.listen4(headers[:reply_to], { :temp => true }, &blk)
320
- end
321
-
322
- Droid.publish(key, headers.merge(payload), {:log => true}.merge(options))
323
-
324
- result
325
- end
326
-
327
- def payload_summary(payload)
328
- Droid.payload_summary(payload)
124
+ self.class.name = name
125
+ self.class.start do
126
+ yield self if block_given?
329
127
  end
330
128
  end
331
129
 
332
- class Listener < Basic
333
- attr_accessor :params
334
-
335
- def initialize(droid, key, options={})
336
- @key = key
337
- @options = options
338
- queue = @options.delete(:queue) || Droid.gen_queue(droid, key)
339
- auto_delete = @options.has_key?(:auto_delete) ? !!@options.delete(:auto_delete) : true
340
- @mq = MQ.new
341
- @prefetch = !!@options[:prefetch]
342
- @mq.prefetch(@options[:prefetch]) if @prefetch
343
- @q = @mq.queue(queue, :auto_delete => auto_delete)
344
- super(droid, options)
345
- end
346
-
347
- def destroy
348
- @q.unsubscribe
349
- @mq.close
350
- end
351
-
352
- def mq
353
- @mq
354
- end
355
-
356
- def exchange
357
- Droid.exchange
358
- end
359
-
360
- def error(e)
361
- begin
362
- publish("event.error", :event_hash => headers[:event_hash]) # hermes fail whale
363
- msg = "#{e.class}: #{e.message}\n #{e.backtrace.join("\n ")}\n"
364
- stderr_puts "About to log error #{headers[:event_hash]}"
365
- stderr_puts e.message
366
- stderr_puts msg
367
- Log.error "amqp_message action=error class='#{e.class}' message='#{e.message}'", :exception => e
368
- @droid.error_handler.call(@message, e, self) if @droid.error_handler
369
- rescue Exception => e
370
- stderr_puts "error handling error! #{e.inspect}"
371
- end
372
- end
373
-
374
- def stderr_puts(msg)
375
- STDERR.puts msg
376
- end
377
-
378
- def defer(&blk)
379
- EM.defer(lambda do
380
- begin
381
- blk.call
382
- rescue => e
383
- error(e)
384
- end
385
- end)
386
- end
387
-
388
- def reply(payload, options={})
389
- publish(headers[:reply_to], payload, options)
390
- end
391
-
392
- def listen(opts={}, &blk)
393
- opts[:temp] = opts[:temp] === true
394
- opts[:ack] = opts[:ack] === true
395
-
396
- if @prefetch
397
- opts[:ack] = true # we must ack messages received in order for prefetch to work
398
- opts[:temp] = false # doesn't make sense for it to be temporary if we're setting prefetch
399
- end
400
- @q.bind(exchange, :key => @key ).subscribe(:ack => opts[:ack]) do |info, data|
401
- Utilization.monitor(@key, :temp => opts[:temp]) do
402
- begin
403
- parse(info, data)
404
-
405
- if opts[:detail]
406
- callargs = [self.dup, info, data]
407
- else
408
- callargs = [self.dup]
409
- end
410
-
411
- ttl = headers[:ttl]
412
- start = Time.now.getgm.to_i
413
- published_on = headers[:published_on]
414
- age = start - published_on
415
-
416
- Log.notice "amqp_message action=received key=#{@key} ttl=#{ttl} age=#{age} #{payload_summary(params)}"
417
-
418
- if (ttl == -1) or (age <= ttl)
419
- @droid.before_filter.call(*callargs) if @droid.before_filter
420
- blk.call(*callargs)
421
-
422
- finished = Time.now.getgm.to_i
423
- Log.notice "amqp_message action=processed key=#{@key} elapsed=#{finished-start} ttl=#{ttl} age=#{age} #{payload_summary(params)}"
424
- else
425
- Log.error "amqp_message action=timeout key=#{@key} ttl=#{ttl} age=#{age} #{payload_summary(params)}"
426
- info.ack if opts[:ack]
427
- end
428
- rescue => e
429
- error(e)
430
- ensure
431
- if opts[:temp]
432
- @q.unbind(exchange)
433
- @q.delete
434
- end
435
- end
436
- end
437
- end
438
-
439
- self
440
- end
441
-
442
- def requeue(opts={})
443
- opts[:ttl] ||= 10
444
-
445
- now = Time.now.getgm.to_i
446
-
447
- payload = @params.merge(extra_headers)
448
- payload.delete('ttl')
449
- payload[:ttl] = opts[:ttl]
450
-
451
- newpayload = Droid.payload_to_data(payload, :published_on => (headers[:published_on] || now))
452
- Log.notice("droid_requeue key=#{@key} #{payload_summary(newpayload)}")
453
- @q.publish(newpayload)
454
- end
455
-
456
- def parse(info, data)
457
- @headers = nil
458
- @params = JSON.parse(data)
459
-
460
- headers[:event_hash] = @params.delete('event_hash') if @params['event_hash']
461
- headers[:reply_to] = @params.delete('reply_to') if @params['reply_to']
462
- headers[:published_on] = @params.delete('published_on').to_i rescue 0
463
- headers[:ttl] = @params.delete('ttl').to_i rescue -1
464
- headers[:ttl] = -1 if headers[:ttl] == 0
465
- headers.merge!(info.properties) # add protocol headers
466
- end
467
-
468
- def extra_headers
469
- extra = {}
470
- [:event_hash, :reply_to, :published_on].each do |key|
471
- extra[key] = headers[key]
472
- end
473
- extra
474
- end
475
-
476
- def [](key)
477
- @params[key.to_s]
478
- end
479
-
480
- def unsubscribe
481
- @q.unsubscribe
482
- end
130
+ def publish(*args)
131
+ Droid.publish(*args)
483
132
  end
484
133
 
485
- attr_accessor :name
486
- attr_reader :error_handler, :before_filter
487
-
488
- def initialize(name, credentials, &blk)
489
- Log.notice "=== #{name} droid initializing"
490
- credentials[:port] ||= 5672
491
- self.class.wait_for_tcp_port(credentials[:host], credentials[:port], :retries => 6) # retry for 30s before giving up
492
-
493
- @name = name
494
- Log.notice "=== #{name} droid starting"
495
- self.class.start(credentials) do
496
- blk.call(self)
497
- end
498
- self
499
- end
500
-
501
- def publish(key, payload={}, options={}, &blk)
502
- Basic.new(self, options).publish(key, payload, options, &blk)
134
+ def log
135
+ self.class.log
503
136
  end
504
137
 
505
- def listen4(key, options={}, &blk)
506
- Listener.new(self, key, options).listen({
507
- :temp => options.delete(:temp),
508
- :detail => options.delete(:detail),
509
- :ack => options.delete(:ack),
510
- }, &blk)
511
- end
512
-
513
- def on_error(&blk)
514
- @error_handler = blk
515
- end
516
-
517
- def before_filter(&blk)
518
- blk ? @before_filter = blk : @before_filter
519
- end
520
-
521
- def stats(&blk)
522
- @stats = blk
523
- Log.notice call_stats
524
- end
525
-
526
- def call_stats
527
- @stats ? @stats.call : nil
528
- end
138
+ include Droid::QueueMethods
139
+ include Droid::BackwardsCompatibleMethods
140
+ include Droid::EMTimerUtils
529
141
  end