Skip to content
This repository

Allow prepare to be async #605

Closed
wants to merge 4 commits into from

3 participants

Adrià Casajús Andrew Grigorev Ben Darnell
Adrià Casajús

Allow prepare to be async like the normal get/post/... methods using the asynchronous decorator and having to call end_prepare.

Andrew Grigorev

I think it would be good to mention this feature in docstring of asynchronous decorator.

ps. there was many requests for this, I also faced with need of asynchronous prepare to get a redis db connection from pool (or establish it if there are no free connections)... now i have to add r = redis.get_locked_connection() in the beginning of every handlers get/post methods... (of course, it could be implemented in custom BaseHandler, but it looks bad to change API semantics in the application this way)

Adrià Casajús

Done :)

Ben Darnell
Collaborator

Needs a test case. Tests should cover asynchronous prepare with both sync and async main methods, and also cover the case where the request is finished in prepare.

Maybe call the end method finish_prepare for consistency with finish? If I were starting from scratch I'd say that the \@asynchronous decorator would call the decorated method with an extra callback argument, but I guess it's not worth making the behavior of prepare and get/post/etc different.

Adrià Casajús

Added the test case.

I was just trying to mimic the @asynchronous behavior for prepare. I don't think it's worth having two different ways of treating async stuff.

Adrià Casajús

Is there more input wrt this or will it be merged?

Ben Darnell
Collaborator

Sorry to leave this in the queue for so long. My thinking on asynchronous methods has evolved with the use of coroutines and Futures in 3.0. We can say now that a method is asynchronous if it returns a Future, and this means we don't have to give each potentially-asynchronous method its own finish method. The @asynchronous decorator already detects if its target returns a Future and runs finish() automatically (this is not just a convenience, but turns out to be necessary for error handling). I'd like to do something similar for prepare(), and potentially other methods.

Ben Darnell bdarnell closed this pull request from a commit May 12, 2013
Ben Darnell Allow prepare to be asynchronous, and detect coroutines by their result.
The prepare method does not use the @asynchronous decorator, only
@gen.coroutine (or @return_future; it detects the Future return type).
The same logic is now available for the regular http verb methods as well.

Closes #605.
ca8495d
Ben Darnell bdarnell closed this in ca8495d May 12, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
65  tornado/test/web_test.py
@@ -953,3 +953,68 @@ def test_raise_with_reason(self):
953 953
     def test_httperror_str(self):
954 954
         self.assertEqual(str(HTTPError(682, reason="Foo")), "HTTP 682: Foo")
955 955
 wsgi_safe.append(RaiseWithReasonTest)
  956
+
  957
+
  958
+
  959
+class BasePrepareTestHandler(RequestHandler):
  960
+
  961
+    def initialize( self, test ):
  962
+        self.test = test
  963
+
  964
+    def on_finish(self):
  965
+        self.test.on_finish()
  966
+
  967
+class AsyncPrepareAsyncHandler(BasePrepareTestHandler):
  968
+
  969
+    @asynchronous
  970
+    def prepare( self ):
  971
+        self.flush(callback=self.end_prepare)
  972
+
  973
+    @asynchronous
  974
+    def get(self):
  975
+        self.write( "hello" )
  976
+        self.flush(callback=self.finish)
  977
+
  978
+class AsyncPrepareSyncHandler(BasePrepareTestHandler):
  979
+
  980
+    @asynchronous
  981
+    def prepare( self ):
  982
+        self.set_header( "X-Test", "hello" )
  983
+        self.flush(callback=self.end_prepare)
  984
+
  985
+    def get(self):
  986
+        self.write( "hello" )
  987
+
  988
+class SyncPrepareAsyncHandler(BasePrepareTestHandler):
  989
+
  990
+    @asynchronous
  991
+    def get(self):
  992
+        self.write( "hello" )
  993
+        self.flush(callback=self.finish)
  994
+
  995
+class SyncPrepareSyncHandler(BasePrepareTestHandler):
  996
+
  997
+    def get(self):
  998
+        self.write( "hello" )
  999
+
  1000
+class AsyncPrepareTest(WebTestCase):
  1001
+
  1002
+    def get_handlers(self):
  1003
+        return [('/aa', AsyncPrepareAsyncHandler, dict(test=self)),
  1004
+                ('/as', AsyncPrepareSyncHandler, dict(test=self)),
  1005
+                ('/sa', SyncPrepareAsyncHandler, dict(test=self)),
  1006
+                ('/ss', SyncPrepareSyncHandler, dict(test=self))]
  1007
+
  1008
+    def test_async(self):
  1009
+        for url in ( '/aa', '/as', '/sa', '/ss' ):
  1010
+          logging.debug( "Trying connection to %s" % url )
  1011
+          s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
  1012
+          s.connect(("localhost", self.get_http_port()))
  1013
+          self.stream = IOStream(s, io_loop=self.io_loop)
  1014
+          self.stream.write(b("GET %s HTTP/1.0\r\n\r\n" % url))
  1015
+          self.wait()
  1016
+
  1017
+    def on_finish(self):
  1018
+        logging.debug('connection closed')
  1019
+        self.stream.close()
  1020
+        self.stop()
30  tornado/web.py
@@ -112,6 +112,8 @@ def __init__(self, application, request, **kwargs):
112 112
         self._headers_written = False
113 113
         self._finished = False
114 114
         self._auto_finish = True
  115
+        self._prepared = False
  116
+        self._auto_run = True
115 117
         self._transforms = None  # will be set in _execute
116 118
         self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
117 119
                      application.ui_methods.iteritems())
@@ -1056,7 +1058,17 @@ def _execute(self, transforms, *args, **kwargs):
1056 1058
             if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
1057 1059
                self.application.settings.get("xsrf_cookies"):
1058 1060
                 self.check_xsrf_cookie()
  1061
+            self._method_args = ( args, kwargs )
1059 1062
             self.prepare()
  1063
+            if self._auto_run:
  1064
+                self.end_prepare()
  1065
+        except Exception, e:
  1066
+            self._handle_request_exception(e)
  1067
+
  1068
+    def end_prepare( self ):
  1069
+        self._prepared = True
  1070
+        try:
  1071
+            args, kwargs = self._method_args
1060 1072
             if not self._finished:
1061 1073
                 args = [self.decode_argument(arg) for arg in args]
1062 1074
                 kwargs = dict((k, self.decode_argument(v, name=k))
@@ -1151,12 +1163,28 @@ def _on_download(self, response):
1151 1163
               self.write("Downloaded!")
1152 1164
               self.finish()
1153 1165
 
  1166
+    This decorator also allows to execute the prepare method asynchronously.
  1167
+    The request handler has to call self.end_prepare() to continue the request
  1168
+    processing.
  1169
+
  1170
+        class MyRequestHandler(web.RequestHandler):
  1171
+           @web.asynchronous
  1172
+           def prepare(self):
  1173
+              doSomeStuff( "somevalue" , callback = self._on_done)
  1174
+
  1175
+           def _on_done(self):
  1176
+              #This is the end of prepare
  1177
+              self.end_prepare()
  1178
+
1154 1179
     """
1155 1180
     @functools.wraps(method)
1156 1181
     def wrapper(self, *args, **kwargs):
1157 1182
         if self.application._wsgi:
1158 1183
             raise Exception("@asynchronous is not supported for WSGI apps")
1159  
-        self._auto_finish = False
  1184
+        if not self._prepared:
  1185
+            self._auto_run = False
  1186
+        else:
  1187
+            self._auto_finish = False
1160 1188
         with stack_context.ExceptionStackContext(
1161 1189
             self._stack_context_handle_exception):
1162 1190
             return method(self, *args, **kwargs)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.