views:

256

answers:

1

I am building a small WSGI application and I am having an intermittent problem with SQLAlchemy throwing an UnboundExceptionError.

When it happens, it seems to happen exclusively on the second request the browser makes. Refreshing the page (and all following page view attempts) run fine. It seems to only happen on the second request.

I am working with a lot of technologies that are new to me, so I am not entirely clear what I should be looking at to try and figure this out.

  • CherryPyWSGIServer
  • Routes
  • AuthKit
  • WebOb
  • SQLAlchemy
  • jinja2

Here are my SQLAlchemy related setup:

product_table = Table('product', metadata,
    Column('productId', Integer, primary_key=True),
    Column('name', String(255)),
    Column('created', DateTime, default=func.now()),
    Column('updated', DateTime, default=func.now(),
        onupdate=func.current_timestamp()),
)

productversion_table = Table('productVersion', metadata,
    Column('productVersionId', Integer, primary_key=True),
    Column('productId', Integer, ForeignKey("product.productId"),
        nullable=False),
    Column('name', String(255)),
    Column('created', DateTime, default=func.now()),
    Column('updated', DateTime, default=func.now(),
        onupdate=func.current_timestamp()),
)

sqlalchemy.orm.mapper(Product,
    product_table,
    properties={
        'product_versions':
            sqlalchemy.orm.relation(
               ProductVersion,
                backref='product',
                cascade='all,delete-orphan')})

sqlalchemy.orm.mapper(ProductVersion,
    productversion_table,
    properties={ })

Here is my controller:

class Base(object):

    def __init__(self, projenv, configuration, protected=True):
        self.protected = protected
        self.projenv = projenv
        self.configuration = configuration
        self.jinja2_env = Environment(
            loader=PackageLoader('my.webapp', 'views'))

    def __call__(self, environ, start_response):
        if self.protected:
            authkit.authorize.authorize_request(environ,
                authkit.permissions.RemoteUser())

        return self.handle_request_wrapper(environ, start_response)

    def handle_request_wrapper(self, environ, start_response):
        request = Request(environ)
        response = Response()
        model_and_view = self.handle_request(request, response)
        if model_and_view['redirect']:
            response = HTTPTemporaryRedirect(
                location=model_and_view['redirect_url'])
        else:
            response.content_type = model_and_view['content_type']
            template = self.jinja2_env.get_template(model_and_view['view'])
            content = template.render(**model_and_view['model'])
            response.body = str(content)
        return response(environ, start_response)


class Product(Base):

    def handle_request(self, request, response):
        model_and_view = Base.handle_request(self, request, response)

        url, match = request.environ['wsgiorg.routing_args']

        product_repository = product_repository_from_config(self.configuration)

        model_and_view['view'] = 'products/product.html'
        model_and_view['model']['product'] = \
            product_repository.get_product(match['product_id'])
        return model_and_view

Here is my Product repository code:

def product_repository_from_config(configuration):
    session = session_from_config(configuration)
    return SqlAlchemyProductRepository(session, configuration)

class SqlAlchemyProductRepository(object):
    """SQLAlchemey Based ProductRepository."""

    def __init__(self, session, configuration = None):
        self.configuration = configuration
        self.session = session

    def get_product(self, product_id):
        return self.session.query(Product).filter_by(
            productId=product_id).first()

Here is my ORM utility:

engines = {}

def setup_session(engine, **kwargs):
    session = sqlalchemy.orm.sessionmaker(bind=engine, **kwargs)
    return session()

def session_from_config(configuration, init=False, **kwargs):
    engine = engine_from_config(configuration, init)
    return setup_session(engine, **kwargs)

def engine_from_config(configuration, init=False):
    """Create an SQLAlchemy engine from a configuration object."""

    config = configuration.to_dict()

    key = pickle.dumps(config)

    if key not in engines:

        engine = sqlalchemy.engine_from_config(configuration.to_dict(),
                prefix = 'db.')

        configure_mapping_for_engine(engine, init)

        engines[key] = engine

    return engines[key]

Here is my view (jinja2):

{% extends "shell.html" %}
{% set title = "Product - " + product.name %}
{% block content %}
<h1>Product</h1>
<ul>
<li><a href="{{ url_for('products') }}">Product List</a></li>
</ul>
<form method="post" action="{{ url_for('products/update', product_id=product.productId) }}">
Name <input type="text" name="name" value="{{ product.name|e }}" /><br />
<input type="submit" value="Update" />
</form>
<form enctype="multipart/form-data" method="post" action="{{ url_for('products/versions/add', product_id=product.productId) }}">
Version Name <input type="text" name="name" />
<input type="submit" value="Add Version" />
</form>


<ul>
{% for product_version in product.product_versions %}
<li>{{ product_version.name }}
<ul>
<li><a href="{{ url_for('products/versions/delete', product_id=product.productId, product_version_id=product_version.productVersionId) }}">delete</a></li>
</ul>
</li>
{% endfor %}
</ul>
{% endblock %}

The error I get is:

UnboundExecutionError: Parent instance <Product at 0x9150c8c> is not bound to a Session; lazy load operation of attribute 'product_versions' cannot proceed

The stack trace shows this being thrown from:

{% for product_version in product.product_versions %}

What could cause my instance to become unbound from the Session between the time that I get it from the repository to the time that it is evaluated by jinja2 in the template?

I am leaning in the direction of thinking it might be the authkit call getting in the way of things but I am not sure what it could possibly be doing since it actually happens before the session is created and should probably not impact anything that happens later?

+2  A: 

Generally, you should have a scheme whereby a single Session exists for each request, local to the current thread, and only torn down at the end of the request (if at all, it can also be re-used on the next request). Typically you'd see the creation and cleanup code of the session all within the overarching request wrapper, in this case it would probably be within handle_request_wrapper(). WSGI middleware like authkit shouldn't really have access to the session in your own app unless you are linking them together somehow. In all the code above there's no (edit: OK I can vaguely see where its getting set up, creating a new engine on each request? never do that....also the session is probably getting gc'ed before request is complete) indication when your setup_session() is actually called or what happens to the returned session as the request proceeds. In any case since you aren't using a contextual manager it would be necessary to pass this single Session to all functions involved in a request - since if it were a global object, you'd get concurrent threads accessing it simultaneously which will cause immediate issues - and this is a pretty cumbersome pattern.

The scoped_session is provided to make the "session per thread" pattern super easy, and the "setup/teardown on request boundaries" pattern is setup by default when using Pylons for example. A visual (albeit ASCII) illustration of this lifecycle is at http://www.sqlalchemy.org/docs/05/session.html#lifespan-of-a-contextual-session .

zzzeek
@zzzeek I believe that engine_from_config() is setup such that it will only create one engine per request regardless of how many times engine_from_config() is called. I will look into scoped_session to see if it works for my purpose.
Beau Simensen
mmm not the engine_from_config I wrote in SQLAlchemy core :)
zzzeek