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 +2 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/bin/bleedq +19 -0
- data/droid.gemspec +100 -0
- data/examples/async_reply.rb +25 -0
- data/examples/heroku_async_reply.rb +22 -0
- data/examples/sync.rb +32 -0
- data/examples/worker.rb +58 -0
- data/lib/droid.rb +88 -476
- data/lib/droid/em.rb +55 -0
- data/lib/droid/heroku.rb +102 -0
- data/lib/droid/heroku/local_stats.rb +145 -0
- data/{vendor/logger_client/lib → lib/droid/heroku}/logger_client.rb +9 -5
- data/lib/droid/heroku/memcache_cluster.rb +129 -0
- data/lib/droid/heroku/stats.rb +30 -0
- data/lib/droid/json_server.rb +109 -0
- data/lib/droid/monkey.rb +8 -0
- data/lib/droid/publish.rb +24 -0
- data/lib/droid/queue.rb +196 -0
- data/lib/droid/request.rb +110 -0
- data/lib/droid/sync.rb +88 -0
- data/lib/droid/utilization.rb +113 -0
- data/lib/droid/utils.rb +113 -0
- data/lib/heroku_droid.rb +1 -86
- data/lib/local_stats.rb +1 -143
- data/lib/memcache_cluster.rb +1 -129
- data/lib/stats.rb +1 -30
- data/spec/publish_spec.rb +27 -0
- data/spec/response_spec.rb +47 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/utils_spec.rb +43 -0
- data/{test/wait_for_port_test.rb → spec/wait_for_port_spec.rb} +2 -9
- metadata +109 -35
- data/README.md +0 -34
- data/lib/utilization.rb +0 -90
- data/test/base.rb +0 -43
- data/test/droid_test.rb +0 -53
- data/test/heroku_droid_test.rb +0 -42
- data/vendor/logger_client/Rakefile +0 -53
- data/vendor/logger_client/init.rb +0 -1
- data/vendor/logger_client/test.rb +0 -18
data/.gitignore
ADDED
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
|
+
|
data/examples/worker.rb
ADDED
@@ -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 '
|
2
|
-
require '
|
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
|
-
|
32
|
-
|
33
|
-
|
5
|
+
if !defined?(JSON) && !defined?(JSON_LOADED)
|
6
|
+
require 'json/pure'
|
7
|
+
end
|
34
8
|
|
35
|
-
|
36
|
-
ensure_con_type(:async, &blk)
|
37
|
-
end
|
9
|
+
require 'droid/monkey'
|
38
10
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
18
|
+
class Droid
|
19
|
+
def self.version
|
20
|
+
@@version ||= File.read(File.dirname(__FILE__) + '/../VERSION').strip
|
56
21
|
end
|
57
22
|
|
58
|
-
def self.
|
59
|
-
|
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
|
-
|
69
|
-
|
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.
|
78
|
-
|
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.
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
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
|
-
|
113
|
-
|
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
|
-
|
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.
|
202
|
-
|
203
|
-
b.start
|
204
|
-
b
|
205
|
-
end
|
67
|
+
def self.start(opts={})
|
68
|
+
config = opts[:config] || self.default_config
|
206
69
|
|
207
|
-
|
208
|
-
@@bunny ||= new_bunny
|
209
|
-
end
|
70
|
+
wait_for_tcp_port(config[:host], config[:port])
|
210
71
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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(
|
88
|
+
::EM.add_timer(0.2) { ::AMQP.stop { ::EM.stop } }
|
231
89
|
end
|
232
90
|
|
233
|
-
def self.
|
234
|
-
|
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.
|
242
|
-
"#{
|
95
|
+
def self.handle_error(err)
|
96
|
+
log.error "#{err.class}: #{err.message}", :exception => err
|
243
97
|
end
|
244
98
|
|
245
|
-
def self.
|
246
|
-
|
247
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
103
|
+
opts[:retries] ||= 6
|
104
|
+
opts[:timeout] ||= 5
|
105
|
+
|
106
|
+
opts[:retries].times do
|
261
107
|
begin
|
262
|
-
|
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
|
-
|
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 #{
|
118
|
+
raise "#{host}:#{port} did not come up after #{opts[:retries]} retries"
|
273
119
|
end
|
274
120
|
|
275
|
-
|
276
|
-
|
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
|
-
|
295
|
-
|
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
|
-
|
333
|
-
|
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
|
-
|
486
|
-
|
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
|
-
|
506
|
-
|
507
|
-
|
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
|