简体   繁体   English

为什么在第二个WSGI请求中从SQLAlchemy收到间歇性的UnboundExecutionError?

[英]Why would I get an intermittent UnboundExecutionError from SQLAlchemy on second WSGI request?

I am building a small WSGI application and I am having an intermittent problem with SQLAlchemy throwing an UnboundExceptionError . 我正在构建一个小型WSGI应用程序,并且在SQLAlchemy抛出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 CherryPyWSGIServer
  • Routes 路线
  • AuthKit AuthKit
  • WebOb 的WebOb
  • SQLAlchemy SQLAlchemy的
  • jinja2 Jinja2的

Here are my SQLAlchemy related setup: 这是我与SQLAlchemy相关的设置:

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: 这是我的ORM实用程序:

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): 这是我的观点(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? 在我从存储库获取实例到模板中jinja2对其进行评估的时间之间,是什么导致我的实例与Session脱钩的?

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? 我倾向于认为这可能是authkit调用妨碍了工作,但是我不确定它可能会做什么,因为它实际上是在创建会话之前发生的,并且可能不会影响以后发生的任何事情吗?

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() . 通常,您会在总体请求包装器中看到所有会话的创建和清除代码,在这种情况下,它很可能在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. 除非您以某种方式将它们链接在一起,否则诸如authkit之类的WSGI中间件实际上不应访问您自己的应用程序中的会话。 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. 在上面的所有代码中都没有(编辑:确定,我可以隐约看到它的设置位置,在每个请求上创建一个新引擎吗?永远不要这样做。...会话也可能在请求完成之前被gc'ed)指示何时实际调用setup_session()或随着请求的进行返回的会话发生了什么。 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. 提供scoped_session可以使“每个线程的会话”模式变得非常容易,例如,在使用Pylons时,默认情况下会设置“在请求边界上的设置/删除”模式。 A visual (albeit ASCII) illustration of this lifecycle is at http://www.sqlalchemy.org/docs/05/session.html#lifespan-of-a-contextual-session . http://www.sqlalchemy.org/docs/05/session.html#lifespan-of-a-contextual-session上可以直观地看到此生命周期(尽管是ASCII)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM