droid 0.9.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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