[svn r96] A batch of exception-related cleanup and fixes.

- insulated Actor against exceptions raised in the received() method and another very-infrequently-occuring bug in run_forever.
  - More verbose when GreenletExit is raised in CoroutinePool, since we're not even sure it happens.
  - Minor optimization in httpd -- exceptions are somewhat expensive to raise -- when keep-alive header isn't present.
  - More verbose exception logging in httpd -- previously these exceptions would have gone unnoticed.
  - Trimmed the logic in greenlib.switch to just call exc_clear() since there's no reason to even bother with the if statement.
This commit is contained in:
which.linden
2008-03-02 18:59:20 -05:00
parent bed461e78d
commit 97f256ccb7
4 changed files with 87 additions and 12 deletions

View File

@@ -23,6 +23,7 @@ THE SOFTWARE.
"""
import collections
import sys
import time
import traceback
@@ -253,8 +254,12 @@ class CoroutinePool(pools.Pool):
result = func(*args, **kw)
if evt is not None:
evt.send(result)
except api.GreenletExit:
pass
except api.GreenletExit, e:
# we're printing this out to see if it ever happens
# in practice
print "GreenletExit raised in coroutine pool", e
if evt is not None:
evt.send(e) # sent as a return value, not an exception
except Exception, e:
traceback.print_exc()
if evt is not None:
@@ -352,12 +357,22 @@ class Actor(object):
while True:
if not self._mailbox:
self._event.wait()
self._event.reset()
self._event = event()
else:
# leave the message in the mailbox until after it's
# been processed so the event doesn't get triggered
# while in the received method
self.received(self._mailbox[0])
try:
self.received(self._mailbox[0])
except KeyboardInterrupt:
raise # allow the program to quit
except:
# we don't want to let the exception escape this
# loop because that would kill the coroutine
e = sys.exc_info()[0]
self.excepted(e)
sys.exc_clear()
self._mailbox.popleft()
def cast(self, message):
@@ -394,9 +409,45 @@ class Actor(object):
>>> api.sleep(0)
received message 2
received message 3
>>> api.kill(a._killer) # test cleanup
"""
raise NotImplementedError()
def excepted(self, exc):
""" Called when the received method raises an exception.
The default implementation simply prints out the raised exception.
Redefine it for customization.
>>> class Exceptor(Actor):
... def received(self, message):
... if message == 'fail':
... message + 1
... else:
... print "received", message
... def excepted(self, exc):
... print "excepted:", exc
>>> a = Exceptor()
>>> a.cast('fail')
>>> api.sleep(0)
excepted: <type 'exceptions.TypeError'>
The main purpose of excepted is to prevent the actor's coroutine
from dying.
>>> a.cast('message 2')
>>> api.sleep(0)
received message 2
If excepted() itself raises an exception, that will kill the coroutine.
>>> api.kill(a._killer) # test cleanup
"""
print "Exception in %s.received(): %s" % (
type(self).__name__, exc)
traceback.print_exc()
def _test():
print "Running doctests. There will be no further output if they succeed."
import doctest

View File

@@ -98,6 +98,16 @@ class TestEvent(tests.TestCase):
api.spawn(send_to_event2)
self.assertEqual(evt.wait(), value2)
def test_double_exception(self):
evt = coros.event()
# send an exception through the event
evt.send(exc=RuntimeError())
self.assertRaises(RuntimeError, evt.wait)
evt.reset()
# shouldn't see the RuntimeError again
api.exc_after(0.001, api.TimeoutError)
self.assertRaises(api.TimeoutError, evt.wait)
class TestCoroutinePool(tests.TestCase):
mode = 'static'
def setUp(self):
@@ -160,7 +170,7 @@ class TestActor(tests.TestCase):
mode = 'static'
def setUp(self):
# raise an exception if we're waiting forever
self._cancel_timeout = api.exc_after(1, RuntimeError())
self._cancel_timeout = api.exc_after(1, api.TimeoutError())
self.actor = IncrActor()
def tearDown(self):
@@ -213,7 +223,22 @@ class TestActor(tests.TestCase):
self.assertEqual(msgs, [1,2,3,4,5])
def test_raising_received(self):
msgs = []
def received(message):
if message == 'fail':
raise RuntimeError()
else:
print "appending"
msgs.append(message)
self.actor.received = received
self.actor.cast('fail')
api.sleep(0)
self.actor.cast('should_appear')
api.sleep(0)
self.assertEqual(['should_appear'], msgs)
if __name__ == '__main__':
tests.main()

View File

@@ -306,10 +306,7 @@ def switch(other=None, value=None, exc=None):
if not (other or hasattr(other, 'run')):
raise SwitchingToDeadGreenlet("Switching to dead greenlet %r %r %r" % (other, value, exc))
_greenlet_context_call('swap_out')
running_exc = sys.exc_info()
if running_exc[0] != None: # see if we're in the middle of an exception handler
sys.exc_clear() # don't pass along exceptions to the other coroutine
del running_exc # tracebacks can create cyclic object references
sys.exc_clear() # don't pass along exceptions to the other coroutine
try:
rval = other.switch(value, exc)
if not rval or not other:

View File

@@ -487,8 +487,8 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler):
request.set_header('Server', self.version_string())
request.set_header('Date', self.date_time_string())
try:
timeout = int(request.get_header('keep-alive'))
except (TypeError, ValueError), e:
timeout = int(request.get_header('keep-alive', timeout))
except TypeError, ValueError:
pass
try:
@@ -522,11 +522,13 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler):
else:
raise
except Exception, e:
self.server.log_message("Exception caught in HttpRequest.handle():\n")
self.server.log_exception(*sys.exc_info())
if not request.response_written():
request.response(500)
request.write('Internal Server Error')
self.socket.close()
raise
raise e # can't do a plain raise since exc_info might have been cleared
self.socket.close()