droid19 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ require 'droid/heroku'
@@ -0,0 +1 @@
1
+ require 'droid/heroku/local_stats'
@@ -0,0 +1 @@
1
+ require 'droid/heroku/memcache_cluster'
@@ -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