Christopher Stawarz
Sun, 11 May 2008 15:16:23 -0700
The updated proposal is included below. I've also posted it at http://wsgi.org/wsgi/Specifications/asyncThe bzr repository for my reference implementation (which is only partially updated to match the new spec) is now at
http://pseudogreen.org/bzr/wsgiorg_async_ref/ I'd appreciate your comments. Thanks, Chris Abstract -------- This specification defines a set of extensions that allow WSGI applications to run effectively on asynchronous (aka event driven) servers. Rationale --------- The architecture of an asynchronous server requires all I/O operations, including both interprocess and network communication, to be non-blocking. For a WSGI-compliant server, this requirement extends to all applications run on the server. However, the WSGI specification does not provide sufficient facilities for an application to ensure that its I/O is non-blocking. Specifically, there are two issues: * The methods provided by the input stream (``environ['wsgi.input']``) follow the semantics of the corresponding methods of the ``file`` class. In particular, each of these methods can invoke the underlying I/O function (in this case, ``recv`` on the socket connected to the client) more than once, without giving the application the opportunity to check whether each invocation will block. * WSGI does not provide the application with a mechanism to test arbitrary file descriptors (such as those belonging to sockets or pipes opened by the application) for I/O readiness. This specification defines a standard interface by which asynchronous servers can provide the required facilities to applications. Specification ------------- Servers that want to allow applications to perform non-blocking I/O must add four new variables to the WSGI environment: ``x-wsgiorg.async.input``, ``x-wsgiorg.async.readable``, ``x-wsgiorg.async.writable``, and ``x-wsgiorg.async.timeout``. The following sections describe these extensions. Non-blocking Input Stream ~~~~~~~~~~~~~~~~~~~~~~~~~ The ``x-wsgiorg.async.input`` variable provides a non-blocking replacement for ``wsgi.input``. It is an object with one method, ``read(size)``, that behaves like the ``recv`` method of ``socket.socket``. This means that a call to ``read`` will invoke the underlying socket ``recv`` **no more than once** and return **at most** ``size`` bytes of data (possibly less). In addition, ``read`` may return an empty string (zero bytes) **only** if the client closes the connection or the application attempts to read more data than is specified by the ``CONTENT_LENGTH`` variable. Before each call to ``read``, the application **must** test the input stream for readiness with ``x-wsgiorg.async.readable`` (see below). The result of calling ``read`` on a non-ready input stream is undefined. As with ``wsgi.input``, the server is free to implement ``x-wsgiorg.async.input`` using any technique it chooses (performing reads on demand, pre-reading the request body, etc.). The only requirements are for ``read`` to obey the expected semantics and the input object to be accepted as the first argument to ``x-wsgiorg.async.readable``. Testing File Descriptors for I/O Readiness ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The variables ``x-wsgiorg.async.readable`` and ``x-wsgiorg.async.writable`` are callable objects that accept two positional arguments, one required and one optional. In the following description, these arguments are given the names ``fd`` and ``timeout``, but they are not required to have these names, and the application **must** invoke the callables using positional arguments. The first argument, ``fd``, is either an integer representing a file descriptor or an object with a ``fileno`` method that returns such an integer. (In addition, ``fd`` may be ``x-wsgiorg.async.input``, even if it lacks a ``fileno`` method.) The second, optional argument, ``timeout``, is either ``None`` or a floating-point value in seconds. If omitted, it defaults to ``None``. When called, ``readable`` and ``writable`` return the empty string (``''``), which **must** be yielded by the application iterable to the server (passing through any middleware). The server then suspends execution of the application until one of the following conditions is met: * The specified file descriptor is ready for reading or writing. * ``timeout`` seconds have elapsed without the file descriptor becoming ready for I/O. * The server detects an error or "exceptional" condition (such as out-of-band data) on the file descriptor. Put another way, if the application calls ``readable`` and yields the empty string, it will be suspended until ``select.select([fd],[],[fd],timeout)`` would return. If the application calls ``writable`` and yields the empty string, it will be suspended until ``select.select([],[fd],[fd],timeout)`` would return. If ``timeout`` seconds elapse without the file descriptor becoming ready for I/O, the variable ``x-wsgiorg.async.timeout`` will be true when the application resumes. Otherwise, it will be false. The value of ``x-wsgiorg.async.timeout`` when the application is first started or after it yields each response-body string is undefined. The server may use any technique it desires to detect when an application's file descriptors are ready for I/O. (Most likely, it will add them to the same event loop that it uses for accepting new client connections, receiving requests, and sending responses.) Examples -------- The following application reads the request body and sends it back to the client unmodified. Each time it wants to receive data from the client, it first tests ``environ['x-wsgiorg.async.input']`` for readability and then calls its ``read`` method. If the input stream is not readable after one second, the application sends a ``408 Request Timeout`` response to the client and terminates:: def echo_request_body(environ, start_response): input = environ['x-wsgiorg.async.input'] readable = environ['x-wsgiorg.async.readable'] nbytes = int(environ.get('CONTENT_LENGTH') or 0) output = '' while nbytes: yield readable(input, 1.0) # Time out after 1 second if environ['x-wsgiorg.async.timeout']: msg = 'The request timed out.' start_response('408 Request Timeout', [('Content-Type', 'text/plain'), ('Content-Length', str(len(msg)))]) yield msg return data = input.read(nbytes) if not data: break output += data nbytes -= len(data)content_type = (environ.get('CONTENT_TYPE') or 'application/ octet-stream')
start_response('200 OK', [('Content-Type', content_type),
('Content-Length', str(len(output)))])
yield output
The following middleware component allows an application that uses the
``x-wsgiorg.async`` extensions to run on a server that does not
support them, without any modification to the application's code::
def dummy_async(application):
def wrapper(environ, start_response):
input = environ['wsgi.input']
environ['x-wsgiorg.async.input'] = input
select_args = [None]
def readable(fd, timeout=None):
select_args[0] = ([fd], [], [fd], timeout)
return ''
def writable(fd, timeout=None):
select_args[0] = ([], [fd], [fd], timeout)
return ''
environ['x-wsgiorg.async.readable'] = readable
environ['x-wsgiorg.async.writable'] = writable
for result in application(environ, start_response):
if result or (not select_args[0]):
yield result
else:
if select_args[0][2][0] is input:
environ['x-wsgiorg.async.timeout'] = False
else:
ready = select.select(*select_args[0])
environ['x-wsgiorg.async.timeout'] = (ready ==
([],[],[]))
select_args[0] = None
return wrapper
Problems
--------
* The empty string yielded by an application after calling
``readable`` or ``writable`` must pass through any intervening
middleware and be detected by the server. Although WSGI explicitly
requires middleware to relay such strings to the server (see
`Middleware Handling of Block Boundaries
<http://python.org/dev/peps/pep-0333/#middleware-handling-of-block-boundaries
>`_),
some components may not, making them incompatible with this specification. * Although the extensions described here make it *possible* for applications to run effectively on asynchronous servers, they do not (and cannot) *ensure* that they do so. As is the case with any cooperative multitasking environment, the burden of ensuring that all application code is non-blocking rests with application authors. Other Possibilities ------------------- * To prevent an application that does blocking I/O from blocking the entire server, an asynchronous server could run each instance of the application in a separate thread. However, since asynchronous servers achieve high levels of concurrency by expressly *avoiding* multithreading, this technique will almost always be unacceptable. * The `greenlet <http://codespeak.net/py/dist/greenlet.html>`_ package enables the use of cooperatively-scheduled micro-threads in Python programs, and a WSGI server could potentially use it to pause and resume applications around blocking I/O operations. However, such micro-threading is not part of the Python language or standard library, and some server authors may be unwilling or unable to make use of it. Open Issues ----------- * Some third-party libraries (such as `PycURL <http://pycurl.sourceforge.net/>`_) provide non-blocking interfaces that may need to monitor multiple file descriptors for I/O readiness simultaneously. Since this specification allows an application to wait on only one file descriptor at a time, it may be difficult or impossible for applications to use such libraries. Although this specification could be extended to include an interface for waiting on multiple file descriptors, it is unclear whether it would be easy (or even possible) for all servers to implement it. Also, the appropriate behavior for a multi-descriptor wait is not obvious. (Should the application be resumed when a single descriptor is ready? All of them? Some minimum number?) _______________________________________________ Web-SIG mailing list Web-SIG@python.org Web SIG: http://www.python.org/sigs/web-sig Unsubscribe: http://mail.python.org/mailman/options/web-sig/archive%40mail-archive.com