From b5dccbfa87cfe4ef19938fd1adc059f2179f69ef Mon Sep 17 00:00:00 2001 From: Keegan McAllister Date: Wed, 28 Nov 2012 00:16:28 -0500 Subject: [PATCH] Return a special value to initiate longpolling Fixes #261. (imported from commit dd0312c3574c9ca924ab76edcb2fb6c03085be32) --- zephyr/decorator.py | 13 +++++++------ zephyr/management/commands/runtornado.py | 7 +++---- zephyr/tests.py | 11 ++++------- zephyr/views.py | 12 ++++++++++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/zephyr/decorator.py b/zephyr/decorator.py index e72395d95f..8090907498 100644 --- a/zephyr/decorator.py +++ b/zephyr/decorator.py @@ -8,17 +8,18 @@ from django.conf import settings from functools import wraps -import types +class _RespondAsynchronously(object): + pass -class TornadoAsyncException(Exception): pass +# Return RespondAsynchronously from an @asynchronous view if the +# response will be provided later by calling handler.finish(), or has +# already been provided this way. We use this for longpolling mode. +RespondAsynchronously = _RespondAsynchronously() def asynchronous(method): @wraps(method) def wrapper(request, *args, **kwargs): - v = method(request, handler=request._tornado_handler, *args, **kwargs) - if v == None or type(v) == types.GeneratorType: - raise TornadoAsyncException - return v + return method(request, handler=request._tornado_handler, *args, **kwargs) if getattr(method, 'csrf_exempt', False): wrapper.csrf_exempt = True return wrapper diff --git a/zephyr/management/commands/runtornado.py b/zephyr/management/commands/runtornado.py index e7f325ff63..0bf1fa1799 100644 --- a/zephyr/management/commands/runtornado.py +++ b/zephyr/management/commands/runtornado.py @@ -190,13 +190,12 @@ class AsyncDjangoHandler(tornado.web.RequestHandler, base.BaseHandler): break if response is None: - from ...decorator import TornadoAsyncException + from ...decorator import RespondAsynchronously try: response = callback(request, *callback_args, **callback_kwargs) - except TornadoAsyncException, e: - # TODO: Maybe add debugging output here - return + if response is RespondAsynchronously: + return except Exception, e: # If the view raised an exception, run it through exception # middleware, and if the exception middleware returns a diff --git a/zephyr/tests.py b/zephyr/tests.py index 55bd1b3fdd..87d5c60e6a 100644 --- a/zephyr/tests.py +++ b/zephyr/tests.py @@ -7,7 +7,7 @@ from django.db.models import Q from zephyr.models import Message, UserProfile, Stream, Recipient, Subscription, \ filter_by_subscriptions, Realm, do_send_message, Client from zephyr.views import json_get_updates, api_get_messages -from zephyr.decorator import TornadoAsyncException +from zephyr.decorator import RespondAsynchronously from zephyr.lib.initial_password import initial_password, initial_api_key import simplejson @@ -457,10 +457,7 @@ class GetUpdatesTest(AuthedTestCase): post_data = {"last": str(1), "first": str(1)} post_data.update(extra_post_data) request = POSTRequestMock(post_data, user, callback) - # json_get_updates returns None, which raises an exception in the - # @asynchronous decorator, which raises a TornadoAsyncException. So this - # is expected, but should probably change. - self.assertRaises(TornadoAsyncException, view_func, request) + self.assertEquals(view_func(request), RespondAsynchronously) def test_json_get_updates(self): """ @@ -497,7 +494,7 @@ class GetUpdatesTest(AuthedTestCase): messages.extend(data) request = POSTRequestMock({"last": str(last_received), "first": "1"}, user, callback) - self.assertRaises(TornadoAsyncException, json_get_updates, request) + self.assertEquals(json_get_updates(request), RespondAsynchronously) self.assertEquals(len(messages), 0) def test_missing_last_received(self): @@ -514,7 +511,7 @@ class GetUpdatesTest(AuthedTestCase): self.assertTrue(message.id > 1) request = POSTRequestMock({}, user, callback) - self.assertRaises(TornadoAsyncException, json_get_updates, request) + self.assertEquals(json_get_updates(request), RespondAsynchronously) class Runner(DjangoTestSuiteRunner): option_list = ( diff --git a/zephyr/views.py b/zephyr/views.py index 935d4284d1..8d8971275e 100644 --- a/zephyr/views.py +++ b/zephyr/views.py @@ -22,7 +22,7 @@ from django.views.decorators.csrf import csrf_exempt from zephyr.decorator import asynchronous, require_post, \ authenticated_api_view, authenticated_json_view, \ - internal_notify_view, \ + internal_notify_view, RespondAsynchronously, \ has_request_variables, POST from zephyr.lib.query import last_n from zephyr.lib.avatar import gravatar_hash @@ -447,7 +447,12 @@ def get_updates_backend(request, user_profile, handler, client_id, dont_block, **kwargs) if resp is not None: send_with_safety_check(resp, handler, **kwargs) - return + + # We have already invoked handler.finish(), so we bypass the usual view + # response path. We are "responding asynchronously" except that it + # already happened. This is slightly weird, but lets us share + # send_with_safety_check with the code below. + return RespondAsynchronously # Enter long-polling mode. # @@ -472,6 +477,9 @@ def get_updates_backend(request, user_profile, handler, client_id, user_profile.add_receive_callback(handler.async_callback(cb)) user_profile.add_pointer_update_callback(handler.async_callback(cb)) + # runtornado recognizes this special return value. + return RespondAsynchronously + def generate_client_id(): return base64.b16encode(os.urandom(16)).lower()