droid19 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +41 -0
- data/VERSION +1 -0
- data/bin/bleedq +19 -0
- data/droid.gemspec +97 -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 +140 -0
- data/lib/droid/em.rb +55 -0
- data/lib/droid/heroku.rb +102 -0
- data/lib/droid/heroku/local_stats.rb +145 -0
- data/lib/droid/heroku/logger_client.rb +218 -0
- data/lib/droid/heroku/memcache_cluster.rb +152 -0
- data/lib/droid/heroku/stats.rb +14 -0
- data/lib/droid/json_server.rb +109 -0
- data/lib/droid/monkey.rb +8 -0
- data/lib/droid/publish.rb +31 -0
- data/lib/droid/queue.rb +196 -0
- data/lib/droid/request.rb +110 -0
- data/lib/droid/sync.rb +87 -0
- data/lib/droid/utilization.rb +113 -0
- data/lib/droid/utils.rb +113 -0
- data/lib/heroku_droid.rb +1 -0
- data/lib/local_stats.rb +1 -0
- data/lib/memcache_cluster.rb +1 -0
- data/lib/stats.rb +1 -0
- data/spec/publish_spec.rb +27 -0
- data/spec/response_spec.rb +47 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/utils_spec.rb +43 -0
- data/spec/wait_for_port_spec.rb +16 -0
- metadata +190 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'droid/utils'
|
2
|
+
|
3
|
+
class Droid
|
4
|
+
class UnknownReplyTo < RuntimeError; end
|
5
|
+
|
6
|
+
class Request
|
7
|
+
attr_reader :qobj, :header, :raw_message
|
8
|
+
attr_reader :droid_headers, :msg, :start
|
9
|
+
|
10
|
+
def initialize(qobj, header, raw_message)
|
11
|
+
@qobj = qobj
|
12
|
+
@header = header
|
13
|
+
@raw_message = raw_message
|
14
|
+
|
15
|
+
@droid_headers = Droid::Utils.parse_custom_headers(header.headers)
|
16
|
+
@msg = Droid::Utils.parse_message(raw_message)
|
17
|
+
|
18
|
+
@start = Time.now.getgm.to_i
|
19
|
+
|
20
|
+
@acked = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def q
|
24
|
+
qobj.q
|
25
|
+
end
|
26
|
+
|
27
|
+
def ex
|
28
|
+
qobj.ex
|
29
|
+
end
|
30
|
+
|
31
|
+
def mq
|
32
|
+
qobj.mq
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](field)
|
36
|
+
msg[field.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :params :msg
|
40
|
+
|
41
|
+
def age
|
42
|
+
return -1 unless droid_headers[:published_on]
|
43
|
+
start - droid_headers[:published_on]
|
44
|
+
end
|
45
|
+
|
46
|
+
def ttl
|
47
|
+
droid_headers[:ttl] || -1
|
48
|
+
end
|
49
|
+
|
50
|
+
def expired?
|
51
|
+
return false if ttl == -1
|
52
|
+
age > ttl
|
53
|
+
end
|
54
|
+
|
55
|
+
def acked?
|
56
|
+
@acked == true
|
57
|
+
end
|
58
|
+
|
59
|
+
def ack
|
60
|
+
return if acked?
|
61
|
+
header.ack
|
62
|
+
@acked = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def reply(data, opts={}, popts={})
|
66
|
+
opts.merge!(default_publish_opts)
|
67
|
+
reply_to = droid_headers[:reply_to] || self.msg['reply_to']
|
68
|
+
raise UnknownReplyTo unless reply_to
|
69
|
+
Droid.reply_to_q(reply_to, data, opts, popts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def publish(name, data, opts={}, popts={}, &block)
|
73
|
+
opts = default_publish_opts.merge(opts)
|
74
|
+
if block
|
75
|
+
opts[:reply_to] ||= Droid::Utils.generate_reply_to(name)
|
76
|
+
Droid::ReplyQueue.new(opts[:reply_to]).subscribe(&block)
|
77
|
+
end
|
78
|
+
Droid.publish(name, data, opts, popts)
|
79
|
+
end
|
80
|
+
|
81
|
+
def requeue(ropts={})
|
82
|
+
h = droid_headers.dup
|
83
|
+
h[:requeued] = true
|
84
|
+
h.delete(:ttl)
|
85
|
+
popts = { :headers => h }
|
86
|
+
ropts[:ttl] ||= 10
|
87
|
+
Droid.publish_to_q(q.name, msg, ropts, popts)
|
88
|
+
end
|
89
|
+
|
90
|
+
def defer(&blk)
|
91
|
+
EM.defer(lambda do
|
92
|
+
begin
|
93
|
+
blk.call
|
94
|
+
rescue => e
|
95
|
+
Droid.handle_error(e)
|
96
|
+
end
|
97
|
+
end)
|
98
|
+
end
|
99
|
+
|
100
|
+
def default_publish_opts
|
101
|
+
{
|
102
|
+
:event_hash => droid_headers[:event_hash]
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def data_summary
|
107
|
+
Droid::Utils.format_data_summary(msg, droid_headers)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/droid/sync.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'droid'
|
2
|
+
require 'bunny'
|
3
|
+
|
4
|
+
class Droid
|
5
|
+
class SyncException < RuntimeError; end
|
6
|
+
|
7
|
+
def self.reset_bunny
|
8
|
+
@@bunny = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bunny
|
12
|
+
@@bunny ||= begin
|
13
|
+
b = Bunny.new(default_config)
|
14
|
+
b.start
|
15
|
+
b
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.start(name, opts={})
|
20
|
+
raise SyncException, "start block is not allowed under sync operation"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.reconnect_on_error
|
24
|
+
SystemTimer::timeout(20) do
|
25
|
+
begin
|
26
|
+
yield if block_given?
|
27
|
+
rescue Bunny::ProtocolError
|
28
|
+
sleep 0.5
|
29
|
+
retry
|
30
|
+
rescue Bunny::ConnectionError
|
31
|
+
sleep 0.5
|
32
|
+
reset_bunny
|
33
|
+
retry
|
34
|
+
rescue Bunny::ServerDownError
|
35
|
+
sleep 0.5
|
36
|
+
reset_bunny
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.pop(q)
|
43
|
+
begin
|
44
|
+
loop do
|
45
|
+
result = q.pop
|
46
|
+
result = result[:payload] if result.is_a?(Hash)
|
47
|
+
return JSON.parse(result) unless result == :queue_empty
|
48
|
+
sleep 0.1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.call(queue_name, data, opts={}, popts={})
|
54
|
+
opts[:reply_to] ||= Droid::Utils.generate_reply_to(queue_name)
|
55
|
+
q = nil
|
56
|
+
begin
|
57
|
+
reconnect_on_error do
|
58
|
+
q = bunny.queue(opts[:reply_to], :auto_delete => true)
|
59
|
+
publish_to_ex(queue_name, data, opts, popts)
|
60
|
+
pop(q)
|
61
|
+
end
|
62
|
+
ensure
|
63
|
+
if q
|
64
|
+
q.delete rescue nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# override publish methods
|
70
|
+
def self.publish_to_q(queue_name, data, opts={}, popts={})
|
71
|
+
reconnect_on_error do
|
72
|
+
q = bunny.queue(queue_name)
|
73
|
+
json, popts = Droid::Utils.format_publish(data, opts, popts)
|
74
|
+
q.publish(json, popts)
|
75
|
+
end
|
76
|
+
log.info "amqp_publish queue=#{queue_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.publish_to_ex(ex_name, data, opts={}, popts={})
|
80
|
+
reconnect_on_error do
|
81
|
+
ex = bunny.exchange(ex_name)
|
82
|
+
json, popts = Droid::Utils.format_publish(data, opts, popts)
|
83
|
+
ex.publish(json, popts)
|
84
|
+
end
|
85
|
+
log.info "amqp_publish exchange=#{ex_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class Droid
|
2
|
+
module Utilization
|
3
|
+
extend self
|
4
|
+
|
5
|
+
@latency = 0.0
|
6
|
+
def latency=(val); @latency = val; end
|
7
|
+
def latency; @latency; end
|
8
|
+
|
9
|
+
@@start = Time.now.getutc
|
10
|
+
@@data = { }
|
11
|
+
|
12
|
+
def start
|
13
|
+
@@start
|
14
|
+
end
|
15
|
+
|
16
|
+
def reinit
|
17
|
+
@@start = Time.now.getutc
|
18
|
+
@@data = { }
|
19
|
+
end
|
20
|
+
|
21
|
+
def data(topic)
|
22
|
+
@@data[topic] ||= {
|
23
|
+
'msgs' => 0,
|
24
|
+
'time' => 0.0
|
25
|
+
}
|
26
|
+
@@data[topic]
|
27
|
+
end
|
28
|
+
|
29
|
+
def topics
|
30
|
+
@@data.keys
|
31
|
+
end
|
32
|
+
|
33
|
+
def record(topic, secs)
|
34
|
+
d = data(topic)
|
35
|
+
d['msgs'] += 1
|
36
|
+
d['time'] += secs
|
37
|
+
end
|
38
|
+
|
39
|
+
def calc_utilization(topic, t2=nil)
|
40
|
+
d = data(topic)
|
41
|
+
t1 = @@start
|
42
|
+
t2 ||= Time.now.getutc.to_i
|
43
|
+
secs = (t2 - t1)
|
44
|
+
secs = 1 if secs <= 0.0
|
45
|
+
if d['msgs'] == 0
|
46
|
+
avg = 0.0
|
47
|
+
else
|
48
|
+
avg = d['time'] / d['msgs']
|
49
|
+
end
|
50
|
+
utilization = d['time'] / secs
|
51
|
+
{
|
52
|
+
'avg' => avg,
|
53
|
+
'secs' => secs,
|
54
|
+
'utilization' => utilization,
|
55
|
+
'msgs' => d['msgs'],
|
56
|
+
'msgs_per_sec' => d['msgs'] / secs
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def monitor(topic, opts={})
|
61
|
+
topic = 'temporary' if opts[:temp]
|
62
|
+
|
63
|
+
t1 = Time.now.getutc
|
64
|
+
begin
|
65
|
+
yield if block_given?
|
66
|
+
ensure
|
67
|
+
t2 = Time.now.getutc
|
68
|
+
record(topic, t2 - t1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def report_data
|
73
|
+
data = {}
|
74
|
+
t2 = Time.now.getutc
|
75
|
+
topics.each do |topic|
|
76
|
+
data[topic] = calc_utilization(topic, t2)
|
77
|
+
end
|
78
|
+
data
|
79
|
+
end
|
80
|
+
|
81
|
+
def report_summary(data=nil)
|
82
|
+
data ||= report_data
|
83
|
+
|
84
|
+
summary = {
|
85
|
+
'avg' => 0.0,
|
86
|
+
'utilization' => 0.0,
|
87
|
+
'msgs' => 0,
|
88
|
+
'msgs_per_sec' => 0.0,
|
89
|
+
'secs' => 0.0
|
90
|
+
}
|
91
|
+
data.each do |topic, d|
|
92
|
+
summary['utilization'] += d['utilization']
|
93
|
+
summary['msgs'] += d['msgs']
|
94
|
+
summary['msgs_per_sec'] += d['msgs_per_sec']
|
95
|
+
summary['avg'] += d['avg']
|
96
|
+
summary['secs'] += d['secs']
|
97
|
+
end
|
98
|
+
if data.size < 1
|
99
|
+
summary['avg'] = 0.0
|
100
|
+
else
|
101
|
+
summary['avg'] /= data.size
|
102
|
+
end
|
103
|
+
|
104
|
+
summary
|
105
|
+
end
|
106
|
+
|
107
|
+
def report
|
108
|
+
data = report_data
|
109
|
+
summary = report_summary(data)
|
110
|
+
{ 'data' => data, 'summary' => summary }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/droid/utils.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class Droid
|
5
|
+
DEFAULT_TTL = 300
|
6
|
+
|
7
|
+
class BadPayload < RuntimeError; end
|
8
|
+
|
9
|
+
module Utils
|
10
|
+
def self.parse_message(raw)
|
11
|
+
return { } unless raw
|
12
|
+
JSON.parse(raw)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse_custom_headers(headers)
|
16
|
+
return { } unless headers
|
17
|
+
|
18
|
+
h = headers.dup
|
19
|
+
|
20
|
+
h[:published_on] = h[:published_on].to_i
|
21
|
+
|
22
|
+
h[:ttl] = h[:ttl].to_i rescue -1
|
23
|
+
h[:ttl] = -1 if h[:ttl] == 0
|
24
|
+
|
25
|
+
h
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.create_event_hash
|
29
|
+
s = Time.now.to_s + self.object_id.to_s + rand(100).to_s
|
30
|
+
'd' + Digest::MD5.hexdigest(s)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.extract_custom_headers(hash, opts={}, popts={})
|
34
|
+
popts[:headers] ||= {}
|
35
|
+
headers = popts[:headers]
|
36
|
+
|
37
|
+
headers[:published_on] ||= hash.delete('published_on') || opts[:published_on] || Time.now.getgm.to_i
|
38
|
+
headers[:ttl] ||= hash.delete('ttl') || (opts[:ttl] || Droid::DEFAULT_TTL).to_i
|
39
|
+
headers[:reply_to] ||= opts[:reply_to] if opts[:reply_to]
|
40
|
+
|
41
|
+
# this is the event hash that gets transferred through various publish/reply actions
|
42
|
+
headers[:event_hash] ||= hash.delete('event_hash') || opts[:event_hash] || create_event_hash
|
43
|
+
|
44
|
+
# this value should be unique for each published/received message pair
|
45
|
+
headers[:message_id] ||= create_event_hash
|
46
|
+
|
47
|
+
# some strange behavior with integers makes it better to
|
48
|
+
# convert all amqp headers to strings to avoid any problems
|
49
|
+
headers.each { |k,v| headers[k] = v.to_s }
|
50
|
+
|
51
|
+
[hash, headers]
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.format_publish(data, opts={}, popts={})
|
55
|
+
raise Droid::BadPayload unless data.is_a?(Hash)
|
56
|
+
|
57
|
+
hash, headers = extract_custom_headers(data, opts, popts)
|
58
|
+
|
59
|
+
popts[:content_type] ||= 'application/json'
|
60
|
+
|
61
|
+
[hash.to_json, popts]
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.generate_queue(exchange_name, second_name=nil)
|
65
|
+
second_name ||= $$
|
66
|
+
"#{generate_name_for_instance(exchange_name)}.#{second_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.generate_name_for_instance(name)
|
70
|
+
"#{name}.#{Socket.gethostname}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.generate_reply_to(name)
|
74
|
+
"temp.reply.#{name}.#{self.generate_sym}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.generate_sym
|
78
|
+
values = [
|
79
|
+
rand(0x0010000),
|
80
|
+
rand(0x0010000),
|
81
|
+
rand(0x0010000),
|
82
|
+
rand(0x0010000),
|
83
|
+
rand(0x0010000),
|
84
|
+
rand(0x1000000),
|
85
|
+
rand(0x1000000),
|
86
|
+
]
|
87
|
+
"%04x%04x%04x%04x%04x%06x%06x" % values
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.data_summary(json)
|
91
|
+
return '-> (empty)' if json.empty?
|
92
|
+
summary = json.map do |k,v|
|
93
|
+
v = v.to_s
|
94
|
+
v = v[0..37] + '...' if v.size > 40
|
95
|
+
"#{k}=#{v}"
|
96
|
+
end.join(', ')
|
97
|
+
"-> #{summary}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.format_data_summary(data, headers)
|
101
|
+
"(data) " + Droid::Utils.data_summary(data) + " (headers) " + Droid::Utils.data_summary(headers)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Need this to be backwards compatible
|
106
|
+
def self.gen_queue(droid, name)
|
107
|
+
dn = droid
|
108
|
+
dn = dn.name if dn.respond_to?(:name)
|
109
|
+
dn ||= "d"
|
110
|
+
dn = dn.gsub(" ", "")
|
111
|
+
Droid::Utils.generate_queue(name, droid)
|
112
|
+
end
|
113
|
+
end
|
data/lib/heroku_droid.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'droid/heroku'
|
data/lib/local_stats.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'droid/heroku/local_stats'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'droid/heroku/memcache_cluster'
|
data/lib/stats.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'droid/heroku/stats'
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'AMQP Publish' do
|
4
|
+
before do
|
5
|
+
@json, @publish_opts = Droid::Utils.format_publish({:x => 1, :y => 2}, {}, {})
|
6
|
+
Droid::Utils.stubs(:format_publish).with({:x => 1, :y => 2}, {}, {}).returns([@json, @publish_opts])
|
7
|
+
end
|
8
|
+
|
9
|
+
it "publishes a message to a queue" do
|
10
|
+
@q = mock('queue')
|
11
|
+
::MQ.expects(:queue).with('topic').returns(@q)
|
12
|
+
@q.expects(:publish).with(@json, @publish_opts)
|
13
|
+
Droid.publish_to_q('topic', :x => 1, :y => 2)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "publishes a message to an exchange" do
|
17
|
+
@ex = mock('exchange')
|
18
|
+
::MQ.expects(:direct).with('topic').returns(@ex)
|
19
|
+
@ex.expects(:publish).with(@json, @publish_opts)
|
20
|
+
Droid.publish_to_ex('topic', :x => 1, :y => 2)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "by default publishes to an exchange" do
|
24
|
+
Droid.expects(:publish_to_ex).with('topic', {:x => 1, :y => 2}, {}, {})
|
25
|
+
Droid.publish('topic', :x => 1, :y => 2)
|
26
|
+
end
|
27
|
+
end
|