Hello everybody,

I wasn't able to find any information on how to run Pylons app that
uses SQLAlchemy ORM on top of single-thread coroutine based WSGI
server like gevent.pywsgi.WSGIServer so I decided this information
could be usable for somebody else.

I was about to start small backoffice project when I decided to gevent
package a try. Main benefits I was expecting by using it were:

* no locking for shared obejcts
* ability to use long-polling controller actions
* no thread switching and GIL overhead
* fun (IMHO, that one is a must otherwise any project is doomed)

After reading gevent docs and googling for a while I realized there
was no readily available recipe for what I wanted to do. I found some
posts that lead me to solution that was successfully deployed today.

First of all, database engine has to be "green". In other words it has
to support non-blocking exec(). Luckily, my long time favorite
PostgreSQL has psycopg2 engine that has such a support. It just needed
to be configured properly:

# myproject.lib.gevent
from gevent.socket import wait_read, wait_write
from psycopg2.extensions import set_wait_callback, POLL_OK, POLL_READ,
POLL_WRITE
from psycopg2 import OperationalError

def gevent_wait_callback(conn, timeout=None):
    fd = conn.fileno()
    while 1:
        state = conn.poll()
        if state == POLL_OK:
            return
        elif state == POLL_READ:
            wait_read(fd, timeout=timeout)
        elif state == POLL_WRITE:
            wait_write(fd, timeout=timeout)
        else:
            raise OperationalError('Unknown connection state %s' %
state)


def make_psycopg2_green():
    set_wait_callback(gevent_wait_callback)

Second, threading.local has to be replaced/monkeypatched by
gevent.local in order to make Pylons properly handle single-thread
multiple-request conditions. I couldn't find better solution for that
than patching paster script (I used virtualenv so there was local copy
of it). I just put "from gevent import monkey; monkey.patch_thread()"
as the very first Python code.

Third, there was no paste.server_runner available so I implemented it
and used it in "[server.main]" section of whatever.ini.

# myproject.lib.gevent
def server_runner(app, global_conf, host, port, spawn='default',
**kwargs):
    WSGIServer((host, int(port)), app, host=host, port=int(port),
spawn=spawn, **kwargs).run_forever()

The above function can be user as following:
[server.main]
paste.server_runner = myproject.lib.gevent:server_runner

Or, if you add paste.server_runner entry point to your project:
[server.main]
use = myproject#entry_point_name

At that point I was able to do something crazy in controller action:

def action(self):
    # next call could "block" on gevent.event.wait() for pretty long
time until content is changed  by some other request/activity
    content = model.get_content(since=request.if_modified_since,
timeout=WHATEVER_YOU_LIKE)
    if not content:
        abort(304, 'Not modified')
    response.last_modified = model.content_modification_time()
    return jsonify(content)

Not to mention I was able to run my existing Pylons + SQLAlchemy
project by making above modifications to it. I have not tried to
measure performance difference because my main goal was not to
increase it but to be able to write long-poll Ajax application using
my beloved Pylons/SQLAlchemy/PostgreSQL combination.

Well, this is probably not the best way to do it but that's what
worked for me. I hope this could save some time to somebody else who
is looking for similar solution.

Cheers,
Alexander

-- 
You received this message because you are subscribed to the Google Groups 
"pylons-discuss" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/pylons-discuss?hl=en.

Reply via email to