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.
@@ -0,0 +1,79 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../vendor/json_pure-1.1.3/lib'
2
+ require 'json'
3
+ require 'drb'
4
+ require 'timeout'
5
+
6
+ class DroidSpeaker
7
+
8
+ def self.drb_connect(port)
9
+ DRb.start_service()
10
+ @drb = DRbObject.new(nil,"druby://localhost:#{port}")
11
+ end
12
+
13
+ def self.drb_cast(topic, payload, &p)
14
+ @drb.publish(topic, payload, &p)
15
+ end
16
+
17
+ def self.drb_call(topic, payload)
18
+ ## this is the best stupid trick I could think of the force blocking
19
+ lock = Thread.new { sleep 9999 }
20
+ @r = nil
21
+ @drb.publish(topic, payload) do |result|
22
+ @r = result
23
+ lock.kill
24
+ end
25
+ lock.join
26
+ @r
27
+ end
28
+
29
+ def self.send(topic, payload, &p)
30
+ i = new
31
+ i.send(topic, payload, &p)
32
+ i.flush
33
+ end
34
+
35
+ def self.start
36
+ i = new
37
+ yield(i)
38
+ i.flush
39
+ end
40
+
41
+ def initialize
42
+ @message = nil
43
+ @callback = nil
44
+ end
45
+
46
+ def send(topic, payload, &blk)
47
+ if defined?(Log) and event_id = Log.default_options[:event_id]
48
+ payload.merge!(:event_hash => event_id)
49
+ end
50
+
51
+ attrs = { :topic => topic, :payload => payload }
52
+ if blk # this message should be replied
53
+ attrs.merge!(:has_reply => true)
54
+ @callback = blk
55
+ end
56
+ @message = attrs
57
+ end
58
+
59
+ class MessageError < RuntimeError; end
60
+
61
+ def flush
62
+ out = ''
63
+ IO.popen("#{File.dirname(__FILE__)}/../speaker/speak 2>/dev/null", 'w+') do |io|
64
+ io.puts @message.to_json
65
+ out = io.read
66
+ end
67
+ begin
68
+ response = JSON.parse(out)
69
+ response.delete('event_hash')
70
+ @callback.call(response) if @callback
71
+ rescue JSON::ParserError
72
+ if out.include?('Timeout::Error')
73
+ raise Timeout::Error, "Timed out while waiting for a response for #{@message[:topic]}"
74
+ else
75
+ raise MessageError, out
76
+ end
77
+ end
78
+ end
79
+ end
data/speaker/speak ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ $:.unshift '/usr/local/droid/lib'
5
+ require 'heroku_droid'
6
+
7
+ msg = JSON.load(gets)
8
+
9
+ Timeout.timeout(10) do
10
+ HerokuDroid.new('Droid Speaker', :standard_topics => false) do |droid|
11
+ if msg['has_reply']
12
+ @event_hash = msg['payload']['event_hash'] ||= Droid::Basic.new(droid).event_hash
13
+ droid.listen4('event.error') do |d|
14
+ if d.headers[:event_hash] == @event_hash
15
+ puts "Message #{msg['topic']} failed: see event #{@event_hash}"
16
+ Droid.stop_safe
17
+ end
18
+ end
19
+
20
+ droid.publish(msg['topic'], msg['payload']) do |d|
21
+ puts d.params.to_json
22
+ Droid.stop_safe
23
+ end
24
+ else
25
+ droid.publish(msg['topic'], msg['payload'])
26
+ puts({}.to_json)
27
+ Droid.stop_safe
28
+ end
29
+ end
30
+ end
31
+ rescue Object => e
32
+ puts "#{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
33
+ end
data/speaker/test.rb ADDED
@@ -0,0 +1,58 @@
1
+ Thread.new do
2
+ require File.dirname(__FILE__) + '/../lib/heroku_droid'
3
+ HerokuDroid.new('Droid speaker test') do |droid|
4
+ droid.listen4('speaker.test') do |msg|
5
+ if msg['marco'] == 1
6
+ puts "Got 'marco', returning 'polo'..."
7
+ msg.reply('polo' => 1)
8
+ else
9
+ puts "Bad payload"
10
+ msg.reply('error' => 1)
11
+ end
12
+ end
13
+ droid.listen4('speaker.raise') do |msg|
14
+ raise "droid raised exception"
15
+ end
16
+ end
17
+ end
18
+
19
+ sleep 2
20
+
21
+ require File.dirname(__FILE__) + '/droid_speaker'
22
+
23
+ puts "\nTest 1: Sending simple message"
24
+ DroidSpeaker.send('fire.and.forget', 'val' => 1)
25
+ puts "\tSuccess!"
26
+
27
+ puts "\nTest 2: Sending and receiving response"
28
+ DroidSpeaker.send('speaker.test', 'marco' => 1) do |r|
29
+ if r['polo'] == 1
30
+ puts "\tSuccess!"
31
+ else
32
+ puts "\tFailed, result: #{r.inspect}"
33
+ end
34
+ end
35
+
36
+ puts "\nTest 3: Exceptions"
37
+ begin
38
+ DroidSpeaker.send('speaker.raise', {}) do |r|
39
+ puts "\tFailed, expected exception"
40
+ end
41
+ rescue DroidSpeaker::MessageError => e
42
+ if e.message =~ /event \w+/
43
+ puts "\tSuccess!"
44
+ else
45
+ puts "\tFailed, droid speaker raised #{e.message}"
46
+ end
47
+ end
48
+
49
+ puts "\nTest 4: Timeout"
50
+ begin
51
+ DroidSpeaker.send('dont.reply', {}) do |r|
52
+ puts "\tFailed, didnt expect a response"
53
+ end
54
+ rescue Timeout::Error
55
+ puts "\tSuccess!"
56
+ rescue Exception => e
57
+ puts "\tFailed, expected Timeout::Error, got #{e.class.name}"
58
+ end
data/test/base.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'rush'
2
+
3
+ def publish(message, payload={})
4
+ Droid.new('Sender', RabbitmqSetup.credentials) do |d|
5
+ d.publish(message, payload)
6
+ Droid.stop_safe
7
+ end
8
+ sleep(0.1)
9
+ end
10
+
11
+ $exchange = nil
12
+ $mutex = Mutex.new
13
+ def exchange
14
+ $mutex.synchronize { $exchange }
15
+ end
16
+ def save_in_exchange(v)
17
+ $mutex.synchronize { $exchange = v }
18
+ end
19
+
20
+ module RabbitmqSetup
21
+ extend self
22
+
23
+ def credentials
24
+ {
25
+ :vhost => '/test',
26
+ :host => 'localhost',
27
+ :user => 'test',
28
+ :pass => 'test'
29
+ }
30
+ end
31
+
32
+ def run_rabbitmqctl(command)
33
+ Rush::Box.new.bash "rabbitmqctl #{command}"
34
+ rescue Rush::BashFailed => e
35
+ raise unless e.message.match(/already_exists/)
36
+ end
37
+
38
+ def run
39
+ run_rabbitmqctl("add_vhost #{credentials[:vhost]}")
40
+ run_rabbitmqctl("add_user #{credentials[:user]} #{credentials[:pass]}")
41
+ run_rabbitmqctl("map_user_vhost #{credentials[:user]} #{credentials[:vhost]}")
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/../lib/droid'
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ require 'rubygems'
5
+ require 'thread'
6
+ require 'bacon'
7
+
8
+ RabbitmqSetup.run
9
+
10
+ Bacon.summary_on_exit
11
+
12
+ Thread.new do
13
+ Droid.new('Test Droid', RabbitmqSetup.credentials) do |d|
14
+ d.on_error do |msg, e|
15
+ save_in_exchange(e)
16
+ end
17
+
18
+ d.before_filter do |msg|
19
+ if to_save = msg[:save_in_before_filter]
20
+ save_in_exchange(to_save)
21
+ end
22
+ end
23
+
24
+ d.listen4('save.msg') do |msg|
25
+ save_in_exchange(msg)
26
+ end
27
+
28
+ d.listen4('do.nothing') do |msg|
29
+ end
30
+ end
31
+ end
32
+
33
+ sleep(2) # wait until droid is up
34
+ describe Droid do
35
+ before do
36
+ save_in_exchange(nil)
37
+ end
38
+
39
+ it "sends and receives simple messages" do
40
+ publish('save.msg')
41
+ exchange.should.not == nil
42
+ end
43
+
44
+ it "sends messages with params" do
45
+ publish('save.msg', :test => 1)
46
+ exchange[:test].should == 1
47
+ end
48
+
49
+ it "accepts before filters" do
50
+ publish('do.nothing', :save_in_before_filter => 42)
51
+ exchange.should == 42
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ require File.dirname(__FILE__) + '/../lib/heroku_droid'
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ require 'rubygems'
5
+ require 'thread'
6
+ require 'bacon'
7
+
8
+ Bacon.summary_on_exit
9
+
10
+ Thread.new do
11
+ HerokuDroid.new('Test Heroku Droid') do |d|
12
+ d.stats do
13
+ "some stats"
14
+ end
15
+
16
+ d.listen4('pong') do |msg|
17
+ save_in_exchange('worked') if msg['notes'] == 'some stats'
18
+ end
19
+
20
+ d.listen4('save.msg') do |msg|
21
+ save_in_exchange(msg)
22
+ end
23
+ end
24
+ end
25
+
26
+ sleep(2) # wait until droid is up
27
+ describe HerokuDroid do
28
+ before do
29
+ save_in_exchange(nil)
30
+ end
31
+
32
+ it "sends and receives simple messages" do
33
+ publish('save.msg')
34
+ exchange.should.not == nil
35
+ end
36
+
37
+ it "responds to ping with a pong message and instances/droid stats filled in" do
38
+ publish('ping')
39
+ sleep 3
40
+ exchange.should == 'worked'
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/../lib/droid'
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ require 'rubygems'
5
+ require 'thread'
6
+ require 'bacon'
7
+
8
+ Bacon.summary_on_exit
9
+
10
+ Thread.new do
11
+ sleep 2
12
+ TCPServer.new('localhost', 20_001).accept.close
13
+ end
14
+
15
+ describe Droid do
16
+ it "waits for the rabbitmq server to come up before opening the amqp connection" do
17
+ start = Time.now
18
+ Droid.wait_for_tcp_port('localhost', 20_001)
19
+ finish = Time.now
20
+ (finish - start).should > 1
21
+ (finish - start).should < 3
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
7
+ t.spec_files = FileList['spec/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
11
+
12
+ ###
13
+
14
+ require 'rake'
15
+ require 'rake/testtask'
16
+ require 'rake/clean'
17
+ require 'rake/gempackagetask'
18
+ require 'rake/rdoctask'
19
+ require 'fileutils'
20
+
21
+ version = "0.1"
22
+ name = "logger_client"
23
+
24
+ spec = Gem::Specification.new do |s|
25
+ s.name = name
26
+ s.version = version
27
+ s.summary = "Client for Heroku logger"
28
+ s.author = "Pedro Belo"
29
+ s.email = "pedro@heroku.com"
30
+
31
+ s.platform = Gem::Platform::RUBY
32
+ s.has_rdoc = false
33
+
34
+ s.files = %w(Rakefile init.rb) + Dir.glob("{lib,spec}/**/*")
35
+
36
+ s.require_path = "lib"
37
+
38
+ s.add_dependency('rest-client', '>=0.6.2')
39
+ end
40
+
41
+ Rake::GemPackageTask.new(spec) do |p|
42
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
43
+ end
44
+
45
+ task :install => [ :package ] do
46
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
47
+ end
48
+
49
+ task :uninstall => [ :clean ] do
50
+ sh %{sudo gem uninstall #{name}}
51
+ end
52
+
53
+ CLEAN.include [ 'pkg', '*.gem', '.config' ]
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/logger_client'
@@ -0,0 +1,210 @@
1
+ # Logger Client
2
+ # Use as a gem or Rails plugin. Example configuration:
3
+ #
4
+ # Log.configure do |config|
5
+ # config.component = 'core'
6
+ # config.instance = 'userapps.123'
7
+ # config.failsafe = :file
8
+ # end
9
+
10
+ require 'rubygems'
11
+ require 'time'
12
+ require 'syslog'
13
+
14
+ module Log; end
15
+
16
+ class Log::InvalidConfiguration < RuntimeError
17
+ def message
18
+ "Invalid component. Configure with Log.configure { |c| c.component = 'myComponent' }"
19
+ end
20
+ end
21
+
22
+ class Log::Config
23
+ def initialize
24
+ @contents = { :console_log => true }
25
+ end
26
+
27
+ def method_missing(method, value)
28
+ @contents[method.to_s.gsub(/=$/, '').to_sym] = value
29
+ end
30
+
31
+ def to_hash
32
+ @contents
33
+ end
34
+ end
35
+
36
+ module Log
37
+ extend self
38
+
39
+ unless defined? SyslogConvertion
40
+ SyslogConvertion = {
41
+ 'error' => 3,
42
+ 'warning' => 4,
43
+ 'notice' => 5,
44
+ 'debug' => 7,
45
+ }
46
+ end
47
+
48
+ def debug(msg, options={})
49
+ log msg, options.merge(:level => 'debug')
50
+ end
51
+
52
+ def notice(msg, options={})
53
+ log msg, options.merge(:level => 'notice')
54
+ end
55
+
56
+ def warning(msg, options={})
57
+ log msg, options.merge(:level => 'warning')
58
+ end
59
+
60
+ def error(msg, options={})
61
+ # Fake an exception for error messages with no exception object
62
+ # so that we get a backtrace.
63
+ if options[:exception].nil?
64
+ begin
65
+ raise StandardError, msg
66
+ rescue => error
67
+ error.backtrace.shift
68
+ options[:exception] = error
69
+ end
70
+ end
71
+
72
+ log msg, options.merge(:level => 'error')
73
+ end
74
+
75
+ ##########
76
+
77
+ def log(msg, options={})
78
+ console_log(msg)
79
+ syslog(msg, options)
80
+ rescue InvalidConfiguration => e
81
+ raise e
82
+ end
83
+
84
+ def default_error(e)
85
+ # avoid backtrace in /usr or vendor if possible
86
+ system, app = e.backtrace.partition { |b| b =~ /(^\/usr\/|vendor)/ }
87
+ reordered_backtrace = app + system
88
+
89
+ # avoid "/" as the method name (we want the controller action)
90
+ row = 0
91
+ row = 1 if reordered_backtrace[row].match(/in `\/'$/)
92
+
93
+ # get file and method name
94
+ begin
95
+ file, method = reordered_backtrace[row].match(/(.*):in `(.*)'$/)[1..2]
96
+ file.gsub!(/.*\//, '')
97
+ self.log "#{e.class} in #{file} #{method}: #{e.message}", :exception => e, :level => 'error'
98
+ rescue
99
+ self.log "#{e.class} in #{e.backtrace.first}: #{e.message}", :exception => e, :level => 'error'
100
+ end
101
+ end
102
+
103
+ def web_error(args)
104
+ e = args[:exception]
105
+ summary = "#{e.class} processing #{args[:url]}"
106
+
107
+ body = []
108
+ body << "\tURL: #{args[:url]}"
109
+ body << "\tParams: #{args[:params].inspect}"
110
+ body << "\tUser: #{args[:user]}" if args[:user]
111
+ body << "\tBacktrace:"
112
+ body << "\t\t#{e.class} (#{e.message}):"
113
+ body += e.backtrace.map { |l| "\t\t#{l}" }
114
+ body = body.join("\n")
115
+
116
+ log "#{summary}\n#{body}", :level => 'error'
117
+ end
118
+
119
+ def exception(e)
120
+ msg = "Exception #{e.class} -> #{e.message}\n"
121
+ msg += filtered_backtrace(e.backtrace)
122
+ STDERR.puts msg
123
+ Log.error e.message, :exception => e
124
+ end
125
+
126
+ def filtered_backtrace(backtrace)
127
+ backtrace.select do |line|
128
+ !line.match(/^\/usr/)
129
+ end.map do |line|
130
+ " #{line}\n"
131
+ end.join
132
+ end
133
+
134
+ def event(name, options = {})
135
+ console_log("EVENT: #{name} begins")
136
+ result = yield
137
+ console_log("EVENT: #{name} complete")
138
+ result
139
+ end
140
+
141
+ def context(options)
142
+ prev_options = default_options.dup
143
+ default_options.merge!(options)
144
+ yield
145
+ @@default_options = prev_options
146
+ end
147
+
148
+ def configure
149
+ config = Config.new
150
+ yield(config)
151
+ set_default_options(config.to_hash)
152
+ end
153
+
154
+ def set_default_options(options)
155
+ default_options.merge!(options)
156
+ end
157
+
158
+ def default_options
159
+ @@default_options ||= {}
160
+ end
161
+
162
+ def failsafe(params)
163
+ case default_options[:failsafe]
164
+ when :file
165
+ dir = defined?(RAILS_ROOT) ? RAILS_ROOT : '.'
166
+ File.open("#{dir}/failsafe.log", "a") do |f|
167
+ f.puts "#{Time.now} #{params[:log][:level]} : #{params[:log][:message]}"
168
+ f.puts params[:log][:addendum] if params[:log][:addendum]
169
+ end
170
+ when :console, nil
171
+ console_log(params[:log][:message])
172
+ end
173
+ end
174
+
175
+ def console_log(msg)
176
+ STDERR.puts "#{Time.now.iso8601} #{msg}" if default_options[:console_log]
177
+ end
178
+
179
+ def syslog(msg, opts = {})
180
+ @retried = false
181
+ begin
182
+ level = SyslogConvertion[opts[:level]]
183
+ if opts[:exception]
184
+ msg += "\n" + format_syslog_exception(opts[:exception])
185
+ end
186
+ syslog_resource.log(level, '%s', msg)
187
+ rescue Exception => e
188
+ failsafe(:log => { :level => 'error', :message => "could not log to syslog: #{e.class.name} #{e.message}"})
189
+ unless @retried
190
+ @retried = true
191
+ @@syslog.close rescue nil
192
+ @@syslog = Syslog.open(default_options[:component]) rescue nil
193
+ retry
194
+ end
195
+ end
196
+ end
197
+
198
+ def format_syslog_exception(e)
199
+ if e.respond_to?(:backtrace)
200
+ "\t#{e.class}: #{e.message}\n" + e.backtrace.map { |t| "\t#{t}" }.join("\n")
201
+ else
202
+ "\t#{e.class}: #{e.message}"
203
+ end
204
+ end
205
+
206
+ def syslog_resource
207
+ @@syslog ||= Syslog.open(default_options[:component].to_s, Syslog::LOG_PID | Syslog::LOG_CONS, default_options[:syslog_facility] || Syslog::LOG_USER)
208
+ end
209
+ end
210
+