droid19 1.0.2

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.
@@ -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