From 439fef3e3975a04b6ed3e84c13f588b5f9264319 Mon Sep 17 00:00:00 2001 From: Greg Holt Date: Fri, 2 Jul 2010 20:38:40 +0000 Subject: [PATCH 1/2] wsgi posthooks --- eventlet/wsgi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eventlet/wsgi.py b/eventlet/wsgi.py index 36ceee7..1f80154 100644 --- a/eventlet/wsgi.py +++ b/eventlet/wsgi.py @@ -376,6 +376,9 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): pass finish = time.time() + for hook, args, kwargs in self.environ['eventlet.posthooks']: + hook(self.environ, *args, **kwargs) + self.server.log_message(self.server.log_format % dict( client_ip=self.get_client_ip(), date_time=self.log_date_time_string(), @@ -442,6 +445,7 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): env['wsgi.input'] = env['eventlet.input'] = Input( self.rfile, length, wfile=wfile, wfile_line=wfile_line, chunked_input=chunked) + env['eventlet.posthooks'] = [] return env From c33251aef16151b2dfc999aca9f7b5c2975e3550 Mon Sep 17 00:00:00 2001 From: Greg Holt Date: Sun, 4 Jul 2010 02:38:25 +0000 Subject: [PATCH 2/2] Tests and documentation for WSGI posthooks. --- doc/modules/wsgi.rst | 36 +++++++++++++++++++++++++++++ tests/wsgi_test.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/doc/modules/wsgi.rst b/doc/modules/wsgi.rst index c05485d..a37d35a 100644 --- a/doc/modules/wsgi.rst +++ b/doc/modules/wsgi.rst @@ -21,5 +21,41 @@ To launch a wsgi server, simply create a socket and call :func:`eventlet.wsgi.se You can find a slightly more elaborate version of this code in the file ``examples/wsgi.py``. +Non-Standard Extension to Support Post Hooks +-------------------------------------------- +Eventlet's WSGI server supports a non-standard extension to the WSGI +specification where :samp:`env['eventlet.posthooks']` contains an array of +`post hooks` that will be called after fully sending a response. Each post hook +is a tuple of :samp:`(func, args, kwargs)` and the `func` will be called with +the WSGI environment dictionary, followed by the `args` and then the `kwargs` +in the post hook. + +For example:: + + from eventlet import wsgi + import eventlet + + def hook(env, arg1, arg2, kwarg3=None, kwarg4=None): + print 'Hook called: %s %s %s %s %s' % (env, arg1, arg2, kwarg3, kwarg4) + + def hello_world(env, start_response): + env['eventlet.posthooks'].append( + (hook, ('arg1', 'arg2'), {'kwarg3': 3, 'kwarg4': 4})) + start_response('200 OK', [('Content-Type', 'text/plain')]) + return ['Hello, World!\r\n'] + + wsgi.server(eventlet.listen(('', 8090)), hello_world) + +The above code will print the WSGI environment and the other passed function +arguments for every request processed. + +Post hooks are useful when code needs to be executed after a response has been +fully sent to the client (or when the client disconnects early). One example is +for more accurate logging of bandwidth used, as client disconnects use less +bandwidth than the actual Content-Length. + +API +--- + .. automodule:: eventlet.wsgi :members: diff --git a/tests/wsgi_test.py b/tests/wsgi_test.py index b10dd00..e4046e4 100644 --- a/tests/wsgi_test.py +++ b/tests/wsgi_test.py @@ -818,6 +818,60 @@ class TestHttpd(_TestBase): pass # TODO: should test with OpenSSL greenthread.kill(g) + def test_029_posthooks(self): + posthook1_count = [0] + posthook2_count = [0] + def posthook1(env, value, multiplier=1): + self.assertEquals(env['local.test'], 'test_029_posthooks') + posthook1_count[0] += value * multiplier + def posthook2(env, value, divisor=1): + self.assertEquals(env['local.test'], 'test_029_posthooks') + posthook2_count[0] += value / divisor + + def one_posthook_app(env, start_response): + env['local.test'] = 'test_029_posthooks' + if 'eventlet.posthooks' not in env: + start_response('500 eventlet.posthooks not supported', + [('Content-Type', 'text/plain')]) + else: + env['eventlet.posthooks'].append( + (posthook1, (2,), {'multiplier': 3})) + start_response('200 OK', [('Content-Type', 'text/plain')]) + yield '' + self.site.application = one_posthook_app + sock = eventlet.connect(('localhost', self.port)) + fp = sock.makefile('rw') + fp.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n') + fp.flush() + self.assertEquals(fp.readline(), 'HTTP/1.1 200 OK\r\n') + fp.close() + sock.close() + self.assertEquals(posthook1_count[0], 6) + self.assertEquals(posthook2_count[0], 0) + + def two_posthook_app(env, start_response): + env['local.test'] = 'test_029_posthooks' + if 'eventlet.posthooks' not in env: + start_response('500 eventlet.posthooks not supported', + [('Content-Type', 'text/plain')]) + else: + env['eventlet.posthooks'].append( + (posthook1, (4,), {'multiplier': 5})) + env['eventlet.posthooks'].append( + (posthook2, (100,), {'divisor': 4})) + start_response('200 OK', [('Content-Type', 'text/plain')]) + yield '' + self.site.application = two_posthook_app + sock = eventlet.connect(('localhost', self.port)) + fp = sock.makefile('rw') + fp.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n') + fp.flush() + self.assertEquals(fp.readline(), 'HTTP/1.1 200 OK\r\n') + fp.close() + sock.close() + self.assertEquals(posthook1_count[0], 26) + self.assertEquals(posthook2_count[0], 25) + def test_zero_length_chunked_response(self): def zero_chunked_app(env, start_response): start_response('200 OK', [('Content-type', 'text/plain')])