droid 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+