Browse files

Wader now raises exceptions on certain classes of fatal http response…

…s for invalid authentication, unknown streams and invalid parameters.
  • Loading branch information...
1 parent 2a551de commit e0ac8b661dc87c530006a064c8b238ed40a226ce Hayes Davis committed Aug 1, 2010
View
51 docs/NOTES.md
@@ -32,4 +32,53 @@ Capabilities
Event Dispatch
--------------
* How can it generically handle dispatching events?
-** Could route event/tweet to particular queue using resque given configurable queue and class name
+** Could route event/tweet to particular queue using resque given configurable queue and class name
+
+Streaming API Scenarios
+-----------------------
+* No connection at all - nobody is home
+ * Retry maximum number of times, then give up
+* Various disconnections (clean or otherwise)
+ * Retry up to max times, then give up
+* Error codes: See http://dev.twitter.com/pages/streaming_api_response_codes
+ * 401 and 403 - Auth issues
+ * Likely to happen at startup
+ * Should probably stop the entire flamingod with a message
+ * 404 - Not found
+ * Likely an invalid path
+ * Should probably stop the entire flamingod
+ * 406 - Not acceptable
+ * Could happen due to lack of params. If there are no params, we should
+ go into a waiting state to wait for more params.
+ * If we get this and we do have params, it's a big deal because it impacts
+ our connectivity to twitter. Need to do something to alert the user.
+ * 413 - Parameters too long, outside of counts for role
+ * A big deal, it means we can't connect to Twitter. Needs immediate fixing.
+ * User should be alerted in some way
+ * 416 - Unacceptable range, outside of values for role
+ * Same as 406, 413
+ * 500 - Server error
+ * Probably should wait and retry after some period
+ * 503 - Overloaded
+ * Probably should wait and retry after some (longish) period
+
+Fatal Error Classes:
+* Authentication: 401, 403
+* Invalid Stream: 404
+* Invalid Parameters: 406, 413, 416
+
+Retry-able Transient Error Classes:
+* Server unavailable
+* Connection closed
+* Server HTTP Errors: 5XX
+
+Should Introduce a Connectivity Status
+--------------------------------------
+Flamingo:Stream:Status
+ * Connecting - The wader is in the process of connecting to the server
+ * Connected - The wader is connected and receiving tweets
+ * Disconnected-Retry - The wader is not connected but is trying
+ * Disconnected-Fatal - The wader is disconnected and can't get reconnected
+
+Flamingo:Stream:Status:Message
+ * String describing what happened
View
35 lib/flamingo/daemon/flamingod.rb
@@ -58,14 +58,25 @@ def trap_signals
end
def restart_wader
- Flamingo.logger.info "Flamingod restarting wader pid=#{@wader.pid} with SIGINT"
- @wader.kill("INT")
+ if @wader
+ Flamingo.logger.info "Flamingod restarting wader pid=#{@wader.pid} with SIGINT"
+ @wader.kill("INT")
+ else
+ Flamingo.logger.info "Wader is not started. Attempting to start new wader."
+ @wader = start_new_wader
+ end
end
def signal_children(sig)
pids = (children.map {|c| c.pid}).join(",")
Flamingo.logger.info "Flamingod sending SIG#{sig} to pids=#{pids}"
- children.each {|child| child.signal(sig) }
+ children.each do |child|
+ begin
+ child.signal(sig)
+ rescue => e
+ Flamingo.logger.info "Failure sending SIG#{sig} to child #{child.pid}: #{e}"
+ end
+ end
end
def terminate!
@@ -75,7 +86,7 @@ def terminate!
end
def children
- [@wader,@web_server] + @dispatchers
+ ([@wader,@web_server] + @dispatchers).compact
end
def start_children
@@ -92,9 +103,10 @@ def start_children
def wait_on_children()
until exit_signaled?
child_pid = Process.wait(-1)
+ child_status = $?
unless exit_signaled?
- if @wader.pid == child_pid
- @wader = start_new_wader
+ if @wader && @wader.pid == child_pid
+ handle_wader_exit(child_status)
elsif @web_server.pid == child_pid
@web_server = start_new_web_server
elsif (to_delete = @dispatchers.find{|d| d.pid == child_pid})
@@ -106,6 +118,17 @@ def wait_on_children()
end
end
end
+
+ def handle_wader_exit(status)
+ if WaderProcess.fatal_exit?(status)
+ Flamingo.logger.error "Wader exited with status "+
+ "#{status.exitstatus} and cannot be automatically restarted"
+ $stderr.write("Wader exited with fatal error. Check the the log.")
+ terminate!
+ else
+ @wader = start_new_wader
+ end
+ end
def run_as_daemon
pid_file = PidFile.new
View
32 lib/flamingo/daemon/wader_process.rb
@@ -1,6 +1,23 @@
module Flamingo
module Daemon
class WaderProcess < ChildProcess
+
+ EXIT_CLEAN = 0
+
+ EXIT_FATAL_RANGE = 100..199
+ EXIT_AUTHENTICATION = 100
+ EXIT_INVALID_PARAMS = 101
+ EXIT_UNKNOWN_STREAM = 102
+
+
+ class << self
+ def fatal_exit?(status)
+ if status
+ EXIT_FATAL_RANGE.include?(status.exitstatus)
+ end
+ end
+ end
+
def register_signal_handlers
trap("INT") { stop }
end
@@ -16,8 +33,19 @@ def run
@wader = Flamingo::Wader.new(screen_name,password,stream)
Flamingo.logger.info "Starting wader on pid=#{Process.pid} under pid=#{Process.ppid}"
- @wader.run
- Flamingo.logger.info "Wader pid=#{Process.pid} stopped"
+
+ exit_code = EXIT_CLEAN
+ begin
+ @wader.run
+ rescue Flamingo::Wader::AuthenticationError
+ exit_code = EXIT_AUTHENTICATION
+ rescue Flamingo::Wader::UnknownStreamError
+ exit_code = EXIT_UNKNOWN_STREAM
+ rescue Flamingo::Wader::InvalidParametersError
+ exit_code = EXIT_INVALID_PARAMS
+ end
+ Flamingo.logger.info "Wader pid=#{Process.pid} stopped with code #{exit_code}"
+ exit(exit_code)
end
def stop
View
37 lib/flamingo/wader.rb
@@ -1,5 +1,23 @@
module Flamingo
class Wader
+
+ class FatalError < StandardError
+ end
+
+ class FatalHttpStatusError < FatalError
+
+ attr_accessor :code
+
+ def initialize(message,code)
+ super(message)
+ self.code = code
+ end
+ end
+
+ class AuthenticationError < FatalHttpStatusError; end
+ class UnknownStreamError < FatalHttpStatusError; end
+ class InvalidParametersError < FatalHttpStatusError; end
+
attr_accessor :screen_name, :password, :stream, :connection
def initialize(screen_name,password,stream)
@@ -25,7 +43,16 @@ def run
end
connection.on_error do |message|
- dispatch_error(:generic,message)
+ code = connection.code
+ if [401,403].include?(code)
+ stop_and_raise!(AuthenticationError.new(message,code))
+ elsif code == 404
+ stop_and_raise!(UnknownStreamError.new(message,code))
+ elsif [406,413,416].include?(code)
+ stop_and_raise!(InvalidParametersError.new(message,code))
+ else
+ dispatch_error(:generic,message)
+ end
end
connection.on_reconnect do |timeout, retries|
@@ -45,6 +72,7 @@ def run
stop
end
end
+ raise @error if @error
end
def stop
@@ -53,15 +81,20 @@ def stop
end
private
+ def stop_and_raise!(error)
+ stop
+ @error = error
+ end
+
def dispatch_event(event_json)
Flamingo.logger.debug "Wader dispatched event"
Resque.enqueue(Flamingo::DispatchEvent, event_json)
- # Resque.enqueue(Flamingo::DispatchEvent, {:src => "flamingo:#{stream.name}"}, event_json)
end
def dispatch_error(type,message,data={})
Flamingo.logger.error "Received error: #{message}"
Resque.enqueue(Flamingo::DispatchError, type, message, data)
end
+
end
end
View
7 test/test_helper.rb
@@ -0,0 +1,7 @@
+require 'rubygems'
+require 'test/unit'
+require 'mockingbird'
+#require "/Users/hayesdavis/Appozite/Projects/mockingbird/lib/mockingbird"
+
+$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
+require "flamingo"
View
30 test/wader/helper.rb
@@ -0,0 +1,30 @@
+require "test/unit"
+require "#{File.dirname(__FILE__)}/../test_helper"
+
+class MockStream < Flamingo::Stream
+
+ def initialize()
+ super(:filter,Flamingo::StreamParams.new(:filter))
+ end
+
+ def connect(opts={})
+ opts = opts.merge({:host=>'localhost',:port=>8080})
+ Twitter::JSONStream.connect(opts)
+ end
+
+end
+
+# This is a little odd but turns out to be a reasonably good way to test the
+# wader. All the wader ever does is enqueue jobs for further processing so
+# we can monitor its behavior by seeing what jobs it enqueues
+module Resque
+
+ def after_enqueue(&block)
+ @handler = block
+ end
+
+ def enqueue(*args)
+ @handler.call(*args) if @handler
+ end
+
+end
View
68 test/wader/test_fatal_http_responses.rb
@@ -0,0 +1,68 @@
+require "#{File.dirname(__FILE__)}/helper"
+
+class TestAuthentication < Test::Unit::TestCase
+
+ def setup
+ Flamingo.config = Flamingo::Config.new
+ Flamingo.logger = Logger.new("/dev/null")
+ end
+
+ def test_unauthorized_is_fatal
+ run_test_for_status_code(401,"Unauthorized",
+ Flamingo::Wader::AuthenticationError)
+ end
+
+ def test_forbidden_is_fatal
+ run_test_for_status_code(403,"Forbidden",
+ Flamingo::Wader::AuthenticationError)
+ end
+
+ def test_unknown_is_fatal
+ run_test_for_status_code(404,"Unknown",
+ Flamingo::Wader::UnknownStreamError)
+ end
+
+ def test_not_acceptable_is_fatal
+ run_test_for_status_code(406,"Not Acceptable",
+ Flamingo::Wader::InvalidParametersError)
+ end
+
+ def test_too_long_is_fatal
+ run_test_for_status_code(413,"Too Long",
+ Flamingo::Wader::InvalidParametersError)
+ end
+
+ def test_range_unacceptable_is_fatal
+ run_test_for_status_code(416,"Range Unacceptable",
+ Flamingo::Wader::InvalidParametersError)
+ end
+
+ private
+ def run_test_for_status_code(code,message,error_type)
+ Mockingbird.setup(:port=>8080) do
+ status code, message
+ end
+
+ error = nil
+
+ wader = Flamingo::Wader.new('user','pass',MockStream.new)
+
+ Resque.after_enqueue do |job,*args|
+ fail("Shouldn't enqueue anything")
+ end
+
+ begin
+ wader.run
+ rescue => e
+ error = e
+ assert_equal(error_type,e.class)
+ assert_equal(code,e.code)
+ end
+
+ assert_not_nil(error)
+
+ ensure
+ Mockingbird.teardown
+ end
+
+end

0 comments on commit e0ac8b6

Please sign in to comment.