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/lib/droid/sync.rb CHANGED
@@ -0,0 +1,88 @@
1
+ require 'droid'
2
+ require 'bunny'
3
+ require 'system_timer'
4
+
5
+ class Droid
6
+ class SyncException < RuntimeError; end
7
+
8
+ def self.reset_bunny
9
+ @@bunny = nil
10
+ end
11
+
12
+ def self.bunny
13
+ @@bunny ||= begin
14
+ b = Bunny.new(default_config)
15
+ b.start
16
+ b
17
+ end
18
+ end
19
+
20
+ def self.start(name, opts={})
21
+ raise SyncException, "start block is not allowed under sync operation"
22
+ end
23
+
24
+ def self.reconnect_on_error
25
+ SystemTimer::timeout(20) do
26
+ begin
27
+ yield if block_given?
28
+ rescue Bunny::ProtocolError
29
+ sleep 0.5
30
+ retry
31
+ rescue Bunny::ConnectionError
32
+ sleep 0.5
33
+ reset_bunny
34
+ retry
35
+ rescue Bunny::ServerDownError
36
+ sleep 0.5
37
+ reset_bunny
38
+ retry
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.pop(q)
44
+ begin
45
+ loop do
46
+ result = q.pop
47
+ result = result[:payload] if result.is_a?(Hash)
48
+ return JSON.parse(result) unless result == :queue_empty
49
+ sleep 0.1
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.call(queue_name, data, opts={}, popts={})
55
+ opts[:reply_to] ||= Droid::Utils.generate_reply_to(queue_name)
56
+ q = nil
57
+ begin
58
+ reconnect_on_error do
59
+ q = bunny.queue(opts[:reply_to], :auto_delete => true)
60
+ publish_to_ex(queue_name, data, opts, popts)
61
+ pop(q)
62
+ end
63
+ ensure
64
+ if q
65
+ q.delete rescue nil
66
+ end
67
+ end
68
+ end
69
+
70
+ # override publish methods
71
+ def self.publish_to_q(queue_name, data, opts={}, popts={})
72
+ reconnect_on_error do
73
+ q = bunny.queue(queue_name)
74
+ json, popts = Droid::Utils.format_publish(data, opts, popts)
75
+ q.publish(json, popts)
76
+ end
77
+ log.info "amqp_publish queue=#{queue_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
78
+ end
79
+
80
+ def self.publish_to_ex(ex_name, data, opts={}, popts={})
81
+ reconnect_on_error do
82
+ ex = bunny.exchange(ex_name)
83
+ json, popts = Droid::Utils.format_publish(data, opts, popts)
84
+ ex.publish(json, popts)
85
+ end
86
+ log.info "amqp_publish exchange=#{ex_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
87
+ end
88
+ 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
10
+ @@data = { }
11
+
12
+ def start
13
+ @@start
14
+ end
15
+
16
+ def reinit
17
+ @@start = Time.now
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
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
64
+ begin
65
+ yield if block_given?
66
+ ensure
67
+ t2 = Time.now
68
+ record(topic, t2 - t1)
69
+ end
70
+ end
71
+
72
+ def report_data
73
+ data = {}
74
+ t2 = Time.now
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
data/lib/heroku_droid.rb CHANGED
@@ -1,86 +1 @@
1
- require 'rubygems'
2
- require 'time'
3
- require 'uri'
4
-
5
- require File.dirname(__FILE__) + '/droid'
6
- require File.dirname(__FILE__) + '/local_stats'
7
-
8
- require File.dirname(__FILE__) + '/../vendor/logger_client/init'
9
- Log.configure do |c|
10
- slot = LocalStats.slot
11
- slot = 'railgun' if slot == 'userapps'
12
- c.component = slot
13
- c.instance_name = LocalStats.this_instance_name
14
- end
15
-
16
- class HerokuDroid < Droid
17
- attr_reader :extended_stats
18
- attr_reader :force_syslog
19
-
20
- def initialize(name, options = {}, &block)
21
- @name = name
22
- @force_syslog = (options[:force_syslog] == true)
23
- @extended_stats = !(options[:extended_stats] == false)
24
- Droid.new(name, self.class.default_options) do |d|
25
- standard_topics(d) unless options[:standard_topics] == false
26
- LocalStats.attach
27
- block.call(d)
28
- end
29
- end
30
-
31
- def ruby_path
32
- if File.exists?("/usr/ruby1.8.6/bin/ruby")
33
- "/usr/ruby1.8.6/bin"
34
- else
35
- "/usr/local/bin"
36
- end
37
- end
38
-
39
- def standard_topics(droid)
40
- queue_name = Droid.gen_queue(droid, 'ping') + ".pid#{Process.pid}"
41
- droid.listen4('ping', :queue => queue_name) do |msg|
42
- Utilization.latency = (Time.now.to_f - msg['departed_at']).abs
43
- end
44
-
45
- blk = Proc.new do |d|
46
- begin
47
- t1 = Time.now
48
- response = {}.merge(LocalStats.stats)
49
-
50
- estats = nil
51
- if self.extended_stats
52
- estats = droid.call_stats
53
- estats = { :notes => estats } unless estats.kind_of?(Hash)
54
- estats[:notes] ||= estats.map { |k, v| "#{v} #{k}" }.join(", ")
55
- estats.merge!(LocalStats.extended_stats)
56
- end
57
-
58
- response.merge!({
59
- :extended_stats => estats,
60
- :droid_name => name,
61
- :utilization => Utilization.report,
62
- :latency => Utilization.latency,
63
- })
64
-
65
- d.publish('pong', response.merge(:stat_collection => (Time.now - t1)))
66
- rescue Object => e
67
- Log.error "Ping Block Error: #{e.class} -> #{e.message}", :exception => e
68
- STDERR.puts e.backtrace.join("\n") + "\n"
69
- end
70
- end
71
-
72
- blk.call(droid)
73
- EM.add_periodic_timer(50 + (rand*15).to_i) { blk.call(droid) }
74
- EM.add_periodic_timer(360) { Utilization.reinit }
75
-
76
- return unless LocalStats.slot == self.name.downcase or self.force_syslog
77
- end
78
-
79
- def self.publish_oneshot(topic, params={})
80
- HerokuDroid.new('One-shot publish') do |droid|
81
- droid.publish(topic, params)
82
- HerokuDroid.stop_safe
83
- end
84
- end
85
- end
86
-
1
+ require 'droid/heroku'
data/lib/local_stats.rb CHANGED
@@ -1,143 +1 @@
1
- require 'rest_client'
2
-
3
- module LocalStats
4
- extend self
5
-
6
- def load_avg
7
- File.read('/proc/loadavg').split(' ', 2).first.to_f
8
- end
9
-
10
- def memory_info
11
- info = {}
12
- File.read('/proc/meminfo').split("\n").each do |line|
13
- name, value = line.split(/:\s+/, 2)
14
- info[name] = value.to_i
15
- end
16
- info
17
- end
18
-
19
- def memory_use
20
- info = memory_info
21
- used = info['MemTotal'] - info['MemFree'] - info['Cached'] - info['Buffers']
22
- (100 * used / info['MemTotal']).to_i
23
- end
24
-
25
- def swap_use
26
- _, total, use, _ = `free | grep Swap:`.strip.split
27
- return 0 if total.to_i == 0
28
- (100 * use.to_i / total.to_i).to_i
29
- end
30
-
31
- def local_ip
32
- @local_ip ||= fetch_local_ip
33
- end
34
-
35
- def fetch_local_ip
36
- `/sbin/ifconfig eth0 | grep inet | awk '{print $2}'`.gsub('addr:', '').strip
37
- end
38
-
39
- def public_ip
40
- retries = 0
41
- @public_ip ||= begin
42
- RestClient.get("http://169.254.169.254/latest/meta-data/public-ipv4").strip
43
- rescue RestClient::RequestTimeout => e
44
- retries += 1
45
- raise if retries > 5
46
- sleep 1
47
- retry
48
- end
49
- end
50
-
51
- def disk_stats
52
- raw = `df -P | grep -v tmpfs | grep -v udev | grep -v slugs | awk '{print $1 " ## " $6 " ## " $5}'`.split("\n").collect { |d| d }
53
- raw.shift
54
- raw.collect do |d|
55
- tokens = d.split("##").collect { |t| t.strip }
56
- {
57
- :device => tokens[0],
58
- :path => tokens[1],
59
- :usage => tokens[2].gsub('%','').to_i
60
- }
61
- end
62
- end
63
-
64
- def ion_instance_id
65
- @ion_instnace_id ||= File.read('/etc/heroku/ion_instance_id').strip rescue nil
66
- end
67
-
68
- def slot
69
- @slot ||= File.read('/etc/heroku/slot').strip.gsub(/64$/,'').split('-').first rescue nil
70
- end
71
-
72
- def this_instance_name
73
- return nil if slot.nil? or ion_instance_id.nil?
74
- "#{slot}.#{ion_instance_id}"
75
- end
76
-
77
- def stats
78
- {
79
- :slot => slot,
80
- :ion_id => ion_instance_id,
81
- :load_avg => load_avg,
82
- :memory_use => memory_use,
83
- :swap_use => swap_use,
84
- :local_ip => local_ip,
85
- :instance_name => this_instance_name,
86
- :public_ip => public_ip,
87
- :net_in_rate => receive_rate,
88
- :net_out_rate => transmit_rate
89
- }
90
- end
91
-
92
- def extended_stats
93
- {
94
- :disk_stats => disk_stats
95
- }
96
- end
97
-
98
- # sample ifstat every 30 seconds
99
- IFSTAT_INTERVAL = 30
100
-
101
- # bytes received per second and bytes transmitted per
102
- # second as a two tuple
103
- def transfer_rates
104
- [receive_rate, transmit_rate]
105
- end
106
-
107
- # bytes received per second
108
- def receive_rate
109
- @rxrate || 0
110
- end
111
-
112
- # bytes transmitted per second
113
- def transmit_rate
114
- @txrate || 0
115
- end
116
-
117
- # sample the current RX and TX bytes from ifconfig and
118
- # return as a two-tuple.
119
- def sample
120
- data = ifconfig.match(/RX bytes:(\d+).*TX bytes:(\d+)/)
121
- [data[1].to_i, data[2].to_i]
122
- rescue => boom
123
- Log.notice "error sampling network rate: #{boom.class} #{boom.message}"
124
- end
125
-
126
- def update_counters
127
- rx, tx = sample
128
- @rxrate = (rx - @rx) / IFSTAT_INTERVAL if @rx
129
- @txrate = (tx - @tx) / IFSTAT_INTERVAL if @tx
130
- @rx, @tx = rx, tx
131
- end
132
-
133
- # called when a droid starts up - setup timers and whatnot
134
- def attach
135
- @rx, @tx, @rxrate, @txrate = nil
136
- update_counters
137
- EM.add_periodic_timer(IFSTAT_INTERVAL) { update_counters }
138
- end
139
-
140
- def ifconfig
141
- `/sbin/ifconfig eth0`
142
- end
143
- end
1
+ require 'droid/heroku/local_stats'