droid 0.9.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 ADDED
File without changes
@@ -0,0 +1,86 @@
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
+
@@ -0,0 +1,143 @@
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
@@ -0,0 +1,127 @@
1
+ require 'memcache'
2
+ require 'yaml'
3
+ require 'digest/sha1'
4
+
5
+ # Manages a pool of memcache servers. This class should not be called
6
+ # outside of the reactor - it does not account for asynchronous access
7
+ # to the server list.
8
+ module MemcacheCluster
9
+ extend self
10
+
11
+ # A MemCache object configured with heroku's internal memcache namespace.
12
+ def heroku
13
+ cache('0Xfa15837Z')
14
+ end
15
+
16
+ def cache_retry(prefix, opts={})
17
+ opts[:retries] ||= 5
18
+ opts[:delay] ||= 0.5
19
+
20
+ retried = 0
21
+ begin
22
+ c = cache(prefix)
23
+ yield c if block_given?
24
+ rescue MemCache::MemCacheError => e
25
+ Log.error "#{e.class} -> #{e.message}", :exception => e
26
+ raise if retried > opts[:retries]
27
+ retried += 1
28
+ sleep opts[:delay]
29
+ @caches = { }
30
+ retry
31
+ end
32
+ end
33
+
34
+ def set(prefix, *args)
35
+ res = nil
36
+ cache_retry(prefix) do |c|
37
+ res = c.set(*args)
38
+ end
39
+ res
40
+ end
41
+
42
+ def get(prefix, *args)
43
+ res = nil
44
+ cache_retry(prefix) do |c|
45
+ res = c.get(*args)
46
+ end
47
+ res
48
+ end
49
+
50
+ # Create listeners for standard memcache cluster related topics.
51
+ def attach(droid, file='memcached.yml')
52
+ load_from_file(file)
53
+
54
+ droid.listen4('memcache.up', :queue => "memcache.up.#{LocalStats.this_instance_name}.#$$") { |msg| add(msg['address'], msg['port']) }
55
+ droid.listen4('instance.down', :queue => "instance.down.#{LocalStats.this_instance_name}.#$$") { |msg| remove(msg['local_ip']) if msg['slot'] == 'memcache' }
56
+ EM.add_timer(1) { droid.publish('memcache.needed', {}) }
57
+ end
58
+
59
+ # A MemCache object configured with the given prefix.
60
+ def cache(prefix, options={})
61
+ caches[prefix] ||=
62
+ MemCache.new(servers, options.merge(:namespace => prefix))
63
+ end
64
+
65
+ alias_method :[], :cache
66
+
67
+ def caches
68
+ reload_if_stale
69
+ @caches ||= {}
70
+ end
71
+
72
+ def servers
73
+ reload_if_stale
74
+ @servers ||= []
75
+ end
76
+
77
+ def add(ip, port)
78
+ host = [ip, port].join(':')
79
+ return if servers.include?(host)
80
+
81
+ log { "#{host} added" }
82
+ @servers.push host
83
+ @servers.sort!
84
+ @caches = {}
85
+ write_to_file
86
+ @last_read = Time.now
87
+ end
88
+
89
+ def remove(host)
90
+ if servers.reject!{ |s| s =~ /^#{host}/ }
91
+ log { "#{host} removed" }
92
+ caches.clear
93
+ write_to_file
94
+ end
95
+ end
96
+
97
+ def reload_if_stale
98
+ if @last_read &&
99
+ (Time.now - @last_read) > 5 &&
100
+ File.mtime(@file) > @last_read
101
+ log { "server list modified. reloading." }
102
+ load_from_file(@file)
103
+ end
104
+ rescue => boom
105
+ # ignore errors accessing/reading file.
106
+ end
107
+
108
+ def load_from_file(file)
109
+ @file = file
110
+ @last_read = Time.now
111
+ @servers = YAML.load(File.read(file)) rescue []
112
+ @caches = {}
113
+ end
114
+
115
+ def write_to_file
116
+ log { "writing server list: #{@file}" }
117
+ File.open(@file, 'w') do |f|
118
+ f.flock(File::LOCK_EX)
119
+ f.write YAML.dump(@servers)
120
+ f.flock(File::LOCK_UN)
121
+ end
122
+ end
123
+
124
+ def log(type=:debug)
125
+ Log.send(type, "memcached: #{yield}")
126
+ end
127
+ end
data/lib/stats.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Stats
2
+ # The MemCache instance used to manipulate stats.
3
+ def cache
4
+ MemcacheCluster.cache("heroku:stats")
5
+ end
6
+
7
+ # Increment a stat counter. If the counter does not exist,
8
+ # yield to the block and use the result as the current counter
9
+ # value. With no block, the counter will be started at zero.
10
+ def increment(key, amount=1)
11
+ if (value = cache.incr(key, amount)).nil?
12
+ value = yield if block_given?
13
+ value = (value || 0) + amount
14
+ cache.add(key, value.to_s, 0, true)
15
+ end
16
+ rescue => boom
17
+ Log.default_error(boom)
18
+ nil
19
+ end
20
+
21
+ # Set the stat counter to a specific value.
22
+ def sample(key, value)
23
+ cache.set(key, value.to_s, 0, true)
24
+ rescue => boom
25
+ Log.default_error(boom)
26
+ nil
27
+ end
28
+
29
+ extend self
30
+ end
@@ -0,0 +1,90 @@
1
+ module Utilization
2
+ extend self
3
+
4
+ @latency = 0.0
5
+ def latency=(val); @latency = val; end
6
+ def latency; @latency; end
7
+
8
+ @@start = Time.now
9
+ @@data = { }
10
+
11
+ def reinit
12
+ @@start = Time.now
13
+ @@data = { }
14
+ end
15
+
16
+ def data(topic)
17
+ @@data[topic] ||= {
18
+ 'msgs' => 0,
19
+ 'time' => 0.0
20
+ }
21
+ @@data[topic]
22
+ end
23
+
24
+ def topics
25
+ @@data.keys
26
+ end
27
+
28
+ def record(topic, secs)
29
+ d = data(topic)
30
+ d['msgs'] += 1
31
+ d['time'] += secs
32
+ end
33
+
34
+ def calc_utilization(topic, t2=nil)
35
+ d = data(topic)
36
+ t1 = @@start
37
+ t2 ||= Time.now
38
+ secs = (t2 - t1)
39
+ secs = 1 if secs <= 0.0
40
+ if d['msgs'] == 0
41
+ avg = 0.0
42
+ else
43
+ avg = d['time'] / d['msgs']
44
+ end
45
+ utilization = d['time'] / secs
46
+ {
47
+ :avg => avg,
48
+ :secs => secs,
49
+ :utilization => utilization,
50
+ :msgs => d['msgs'],
51
+ :msgs_per_sec => d['msgs'] / secs
52
+ }
53
+ end
54
+
55
+ def monitor(topic, opts={})
56
+ topic = 'temporary' if opts[:temp]
57
+
58
+ t1 = Time.now
59
+ begin
60
+ yield if block_given?
61
+ ensure
62
+ t2 = Time.now
63
+ record(topic, t2 - t1)
64
+ end
65
+ end
66
+
67
+ def report
68
+ data = {}
69
+ t2 = Time.now
70
+ topics.each do |topic|
71
+ data[topic] = calc_utilization(topic, t2)
72
+ end
73
+
74
+ summary = { :avg => 0.0, :utilization => 0.0, :msgs => 0, :msgs_per_sec => 0.0, :secs => 0.0 }
75
+ data.each do |topic, d|
76
+ summary[:utilization] += d[:utilization]
77
+ summary[:msgs] += d[:msgs]
78
+ summary[:msgs_per_sec] += d[:msgs_per_sec]
79
+ summary[:avg] += d[:avg]
80
+ summary[:secs] += d[:secs]
81
+ end
82
+ if data.size < 1
83
+ summary[:avg] = 0.0
84
+ else
85
+ summary[:avg] /= data.size
86
+ end
87
+
88
+ { :summary => summary, :data => data }
89
+ end
90
+ end