db: Make EngineFacade a Facade
This blueprint proposes a new declarative system of connection management and session and transaction scoping for all projects that currently make use of oslo.db.session.EngineFacade. Change-Id: I8c0efa3b859c2fe5f06f4c0416c5323362d7ef72
This commit is contained in:
parent
740b16d661
commit
ec52f4370b
767
specs/kilo/make-enginefacade-a-facade.rst
Normal file
767
specs/kilo/make-enginefacade-a-facade.rst
Normal file
@ -0,0 +1,767 @@
|
||||
================================
|
||||
Make EngineFacade into a Facade
|
||||
================================
|
||||
|
||||
https://blueprints.launchpad.net/oslo.db/+spec/make-enginefacade-a-facade
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
The oslo.db.sqlalchemy.session.EngineFacade class serves as the gateway
|
||||
to the SQLAlchemy Engine and Session objects within many OpenStack projects,
|
||||
including Ceilometer, Glance, Heat, Ironic, Keystone, Neutron, Nova, and
|
||||
Sahara. However, the object is severely under-functional; while it provides
|
||||
a function call that ultimately calls ``create_engine()`` and
|
||||
``sessionmaker()``, consuming projects receive no other utility from this
|
||||
object, and in order to solve closely related problems that all of them
|
||||
share, each invent their own systems, all of which are different,
|
||||
verbose, and error prone, with various performance, stability,
|
||||
and scalability issues.
|
||||
|
||||
Registry Functionality
|
||||
----------------------
|
||||
|
||||
In the first case, EngineFacade as used by projects needs to act as a
|
||||
thread-safe registry, a feature which it does not provide and for
|
||||
which each consuming project has had to invent directly. These
|
||||
inventions are verbose and inconsistent.
|
||||
|
||||
For example, in Keystone, the EngineFacade is created thusly in
|
||||
keystone/common/sql/core.py::
|
||||
|
||||
_engine_facade = None
|
||||
|
||||
def _get_engine_facade():
|
||||
global _engine_facade
|
||||
|
||||
if not _engine_facade:
|
||||
_engine_facade = db_session.EngineFacade.from_config(CONF)
|
||||
|
||||
return _engine_facade
|
||||
|
||||
In Ironic we have this; Sahara contains something similar::
|
||||
|
||||
_FACADE = None
|
||||
|
||||
def _create_facade_lazily():
|
||||
global _FACADE
|
||||
if _FACADE is None:
|
||||
_FACADE = db_session.EngineFacade(
|
||||
CONF.database.connection,
|
||||
**dict(CONF.database.iteritems())
|
||||
)
|
||||
return _FACADE
|
||||
|
||||
However in Nova, we get a similar pattern but with one very critical twist::
|
||||
|
||||
_ENGINE_FACADE = None
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
def _create_facade_lazily():
|
||||
global _LOCK, _ENGINE_FACADE
|
||||
if _ENGINE_FACADE is None:
|
||||
with _LOCK:
|
||||
if _ENGINE_FACADE is None:
|
||||
_ENGINE_FACADE = db_session.EngineFacade.from_config(CONF)
|
||||
return _ENGINE_FACADE
|
||||
|
||||
Each library invents their own system of establishing EngineFacade as a
|
||||
singleton, and providing CONF into it; none of which are alike.
|
||||
Nova happened to discover that this singleton pattern isn't threadsafe,
|
||||
and added a mutex, however the lack of this critical improvement remains a
|
||||
bug in all the other systems.
|
||||
|
||||
Transactional Resource Functionality
|
||||
-------------------------------------
|
||||
|
||||
Adding a fully functional creational pattern is an easy win,
|
||||
but the problem goes beyond that. EngineFacade ends its work at ``get_engine()``
|
||||
or ``get_session()``; the former returns a SQLAlchemy Engine object, which
|
||||
itself is only a factory for connections, and the latter returns a
|
||||
SQLAlchemy Session object, ready for use but otherwise unassociated with
|
||||
any specific connection or transactional context.
|
||||
|
||||
The definition of "facade" is a layer that conceals the use of fine-
|
||||
grained APIs behind a layer that is coarse-grained and tailored to the
|
||||
use case at hand. By this definition, the EngineFacade currently is
|
||||
only a factory, and not a facade.
|
||||
|
||||
The harm caused by this lack of guidance on EngineFacade's part is widespread.
|
||||
While the failure to provide an adequate creational pattern leads
|
||||
each Openstack project to invent its own workaround,
|
||||
the failure to provide any guidance on connectivity or transactional
|
||||
scope gives rise to a much more significant pattern of poor implementations
|
||||
on the part of all Openstack projects. Each project observed illustrates
|
||||
mis-use of engines, sessions and transactions to a greater or lesser degree,
|
||||
more often than not having direct consequences for performance, stability,
|
||||
and maintainability.
|
||||
|
||||
The general theme among all projects is that while they are all
|
||||
presented as web services, there is no structure in place which
|
||||
establishes connectivity and transactional scope for a service method
|
||||
as a whole. Individual methods include explicit boilerplate which
|
||||
establishes some kind of connectivity, either within a transaction or
|
||||
not. The format of this boilerplate alone is not only inconsistent
|
||||
between projects, it's inconsistent within a single project and even
|
||||
in a single module, sometimes intentionally and sometimes not.
|
||||
Equally if not more seriously, individual API methods very frequently
|
||||
proceed across a span of multiple connections and non-connected
|
||||
transactions within the scope of a single operation, and in some cases
|
||||
the multiple transactions are even nested. The use of multiple
|
||||
connections and transactions is in the first place a major performance
|
||||
impediment, and in the second place dilutes the usefulness of
|
||||
transactional logic in the first place as an API method is not
|
||||
actually atomic. When transactions are actually nested, the risk surface
|
||||
for deadlocks increases significantly.
|
||||
|
||||
Transactional Scoping Examples
|
||||
-------------------------------
|
||||
|
||||
This section will detail some specific examples of the issues just described,
|
||||
for those who are curious.
|
||||
|
||||
We first show Neutron's system, which is the most
|
||||
organized and probably has the fewest issues of this nature.
|
||||
Neutron has a system where all database operations proceed
|
||||
where a neutron.context.Context object is passed; the Context object serves
|
||||
as home base to a SQLAlchemy Session that was ultimately retrieved
|
||||
from EngineFacade. A method excerpt looks like this::
|
||||
|
||||
def add_resource_association(self, context, service_type, provider_name,
|
||||
resource_id):
|
||||
|
||||
# ...
|
||||
|
||||
with context.session.begin(subtransactions=True):
|
||||
assoc = ProviderResourceAssociation(provider_name=provider_name,
|
||||
resource_id=resource_id)
|
||||
context.session.add(assoc)
|
||||
|
||||
We see that while the Context object at least allows that all operations
|
||||
are given access to the same Session, the method still has to state
|
||||
that it wishes to begin a transaction, and that it needs to support the
|
||||
fact that the Session may already be within a transaction. Neutron's system
|
||||
is a little verbose, and suffers from the issue that individual methods called
|
||||
in series may invoke their work within distinct transactions on new
|
||||
connections each time, but at least ensures that just one Session is in
|
||||
play for a given API method from start to finish; this prevents the issue
|
||||
of inadvertent multiple transaction nesting, as the Session's ``begin()``
|
||||
method will disallow a nested call from opening a new connection.
|
||||
|
||||
Next we look at Keystone. Keystone has some database-related helper functions
|
||||
but they don't serve any functional purpose other than some naming abstraction.
|
||||
Keystone has a lot of short "lookup" methods, so many of them
|
||||
look like this::
|
||||
|
||||
@sql.handle_conflicts(conflict_type='trust')
|
||||
def list_trusts(self):
|
||||
session = sql.get_session()
|
||||
trusts = session.query(TrustModel).filter_by(deleted_at=None)
|
||||
return [trust_ref.to_dict() for trust_ref in trusts]
|
||||
|
||||
Above, the ``sql.get_session()`` call is just another call to
|
||||
EngineFacade.get_session(), and that's where the connectivity is set up.
|
||||
The ``sql.handle_conflicts()`` call doesn't have any role in establishing
|
||||
this session.
|
||||
|
||||
The above call uses the SQLAlchemy Session in "autocommit" mode; in
|
||||
this mode, SQLAlchemy essentially creates connection/transaction
|
||||
context on a per-query basis, and discards it when the query is complete;
|
||||
using the Python Database API (DBAPI), there is no cross-platform option to
|
||||
prevent a transaction from ultimately being present; hence "autocommit"
|
||||
doesn't mean, "no transaction".
|
||||
|
||||
In all but the most minimal cases, using the Session in "autocommit"
|
||||
mode is not a good approach to take, and is discouraged in SQLAlchemy's
|
||||
own documentation (see
|
||||
http://docs.sqlalchemy.org/en/rel_0_9/orm/session.html#autocommit-mode),
|
||||
as it means a series of queries will each proceed upon a brand new connection
|
||||
and transaction per query, wasting database resources with expensive rollbacks
|
||||
and even creating a new database connection per query under slight
|
||||
load, where the connection pool is in overflow mode. oslo.db
|
||||
itself also emits a "pessimistic ping" on each connection, where a
|
||||
"SELECT 1" is emitted in order to ensure the connection is alive, so emitting
|
||||
three queries in "autocommit" mode means you're actually emitting *six*
|
||||
queries.
|
||||
|
||||
It's true that for a method like the above where exactly one SELECT is
|
||||
emitted and definitely nothing else, there is a little less Python
|
||||
overhead in that the Session does not build up an internal state
|
||||
object for the transaction, but this is only a tiny optimization; if
|
||||
optimization at that scale is needed, there are other ways to make the
|
||||
above system vastly more performant (e.g. use baked queries, column-
|
||||
based queries, or Core queries).
|
||||
|
||||
While both Keystone and Neutron have the issue of implicit use of
|
||||
"autocommit" mode, Nova has more significant issues, both because
|
||||
it is more complex at the database level and is also more performance
|
||||
critical regarding persistence.
|
||||
|
||||
Within Nova, the connectivity system is more or less equivalent to
|
||||
that of Keystone; many explicit calls to get_session() and heavy use of
|
||||
the session in "autocommit" mode, most commonly through the model_query()
|
||||
function. But more critical is that the complexity of Nova's API without a
|
||||
foolproof system of maintaining transaction scope leads to
|
||||
a widespread use of multiple transactions per API call, in some cases
|
||||
concurrently, which has definite stability and performance implications.
|
||||
|
||||
A typical Nova method looks like::
|
||||
|
||||
@require_admin_context
|
||||
def cell_update(context, cell_name, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
cell_query = _cell_get_by_name_query(context, cell_name,
|
||||
session=session)
|
||||
if not cell_query.update(values):
|
||||
raise exception.CellNotFound(cell_name=cell_name)
|
||||
cell = cell_query.first()
|
||||
return cell
|
||||
|
||||
In the above call, the ``get_session()`` call returns a brand new session
|
||||
upon which a transaction is begun; the method then calls into
|
||||
``_cell_get_by_name_query``, passing in the Session in an effort to
|
||||
ensure this sub-method uses the same transaction. The intent here is
|
||||
good, that the ``cell_update()`` method knows it should share its
|
||||
transactional context with a sub-method.
|
||||
|
||||
However, this is a burdensome and verbose coding pattern which is
|
||||
inconsistently applied. In those areas where it fails to be applied,
|
||||
the end result is that a single operation invokes several new
|
||||
connections and transactions, sometimes within a nested set of calls;
|
||||
this is wasteful and slow and is a key risk factor for deadlocks.
|
||||
Examples of non-nested, multiple connection/session use within a
|
||||
single call are easy to find. Truly nested transactions are less
|
||||
frequent; one is nova/db/api.py -> floating_ip_bulk_destroy. In this
|
||||
method, we see::
|
||||
|
||||
@require_context
|
||||
def floating_ip_bulk_destroy(context, ips):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
project_id_to_quota_count = collections.defaultdict(int)
|
||||
for ip_block in _ip_range_splitter(ips):
|
||||
query = model_query(context, models.FloatingIp).\
|
||||
filter(models.FloatingIp.address.in_(ip_block)).\
|
||||
filter_by(auto_assigned=False)
|
||||
rows = query.all()
|
||||
for row in rows:
|
||||
project_id_to_quota_count[row['project_id']] -= 1
|
||||
model_query(context, models.FloatingIp).\
|
||||
filter(models.FloatingIp.address.in_(ip_block)).\
|
||||
soft_delete(synchronize_session='fetch')
|
||||
for project_id, count in project_id_to_quota_count.iteritems():
|
||||
try:
|
||||
reservations = quota.QUOTAS.reserve(context,
|
||||
project_id=project_id,
|
||||
floating_ips=count)
|
||||
quota.QUOTAS.commit(context,
|
||||
reservations,
|
||||
project_id=project_id)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_("Failed to update usages bulk "
|
||||
"deallocating floating IP"))
|
||||
|
||||
|
||||
The entire method is within ``session.begin()``. But within that, we
|
||||
first see a two calls to ``model_query()``, each of which forget to pass the
|
||||
session along, so ``model_query()`` makes it's own session and transaction
|
||||
for each. But more seriously, ``get_session()`` is called many times again, without
|
||||
any way of passing a session through, down below when ``quota.QUOTAS.commit``
|
||||
is called within a loop. The interface for this starts outside of the
|
||||
database API, in nova/quota.py, where no ``session`` argument is available::
|
||||
|
||||
def commit(self, context, reservations, project_id=None, user_id=None):
|
||||
"""Commit reservations."""
|
||||
if project_id is None:
|
||||
project_id = context.project_id
|
||||
# If user_id is None, then we use the user_id in context
|
||||
if user_id is None:
|
||||
user_id = context.user_id
|
||||
|
||||
db.reservation_commit(context, reservations, project_id=project_id,
|
||||
user_id=user_id)
|
||||
|
||||
``db.reservation_commit`` is back in nova/db/api.py, where we
|
||||
see a whole new call to ``get_session()``, ``begin()``, calling a second
|
||||
time through the ``@_retry_on_deadlock`` decorator which also would best
|
||||
know how to manage its scope at the topmost-level::
|
||||
|
||||
@require_context
|
||||
@_retry_on_deadlock
|
||||
def reservation_commit(context, reservations, project_id=None, user_id=None):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
_project_usages, user_usages = _get_project_user_quota_usages(
|
||||
context, session, project_id, user_id)
|
||||
reservation_query = _quota_reservations_query(session, context,
|
||||
reservations)
|
||||
for reservation in reservation_query.all():
|
||||
usage = user_usages[reservation.resource]
|
||||
if reservation.delta >= 0:
|
||||
usage.reserved -= reservation.delta
|
||||
usage.in_use += reservation.delta
|
||||
reservation_query.soft_delete(synchronize_session=False)
|
||||
|
||||
In the above example, we first see that the "ad-hoc-session" system and
|
||||
"more than one way to do it" approach of ``model_query()`` leads
|
||||
to coding errors that are silently masked, but in the case of
|
||||
``reservation_commit()``, the architecture itself disallows this coding
|
||||
error to even be corrected.
|
||||
|
||||
Examples of non-nested multiple sessions and transactions in one API call can
|
||||
be found by using an assertion within the test suite. The two main
|
||||
areas this occurs in the current code are:
|
||||
|
||||
* instance_create() calls get_session(),
|
||||
then ec2_instance_create() -> models.save() -> get_session()
|
||||
|
||||
* aggregate_create() calls get_session(),
|
||||
then aggregate_get() -> model_query() -> get_session()
|
||||
|
||||
The above examples can be fixed manually, but rather than adding
|
||||
more boilerplate, decorators, and imperative arguments to solve the problem
|
||||
as individual cases are identified, the solution should instead be to replace
|
||||
all imperative database code involving transaction scope with a purely
|
||||
declarative facade that handles connectivity, transaction scoping and
|
||||
related features like method retrying in a consistent and context-aware
|
||||
fashion across all projects.
|
||||
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
The change is to replace the use of get_session(), get_engine(), and
|
||||
special context managers with a new set of decorators and
|
||||
context managers, which themselves are invoked from a
|
||||
simple import that replaces the usual EngineFacade logic.
|
||||
|
||||
The import will essentially allow a single symbol that handles the work
|
||||
of ``EngineFacade`` and ``CONF`` behind the scenes::
|
||||
|
||||
from oslo.db import enginefacade as sql
|
||||
|
||||
This symbol will provide two key decorators, ``reader()`` and
|
||||
``writer()``, as well as context managers which
|
||||
mirror their behavior, ``using_reader()`` and ``using_writer()``.
|
||||
The decorators deliver a SQLAlchemy Session object to
|
||||
the existing ``context`` argument of API methods::
|
||||
|
||||
@sql.reader
|
||||
def some_api_method(context):
|
||||
# work with context.session
|
||||
|
||||
@sql.writer
|
||||
def some_other_api_method(context):
|
||||
# work with context.session
|
||||
|
||||
Whereas the context managers receive this ``context`` argument locally::
|
||||
|
||||
def some_api_method(context):
|
||||
with sql.using_reader(context) as session:
|
||||
# work with session
|
||||
|
||||
def some_other_api_method(context):
|
||||
with sql.using_writer(context) as session:
|
||||
# work with session
|
||||
|
||||
|
||||
Transaction Scope
|
||||
-----------------
|
||||
|
||||
These decorators and context managers will acquire a new Session using
|
||||
methods similar to that of the current ``get_session()`` function if
|
||||
one is not already scoped, or if one is already scoped, will return
|
||||
that existing Session. The Session will then unconditionally be
|
||||
within a transaction using ``begin()``, or we may better yet switch to
|
||||
the default mode of ``Session`` which is that of "autocommit=False".
|
||||
The state of this transaction will be to remain open until the method
|
||||
ends, either by raising an exception (unconditional rollback) or by
|
||||
completing (either a commit() or a close(), depending on reader/writer
|
||||
semantics).
|
||||
|
||||
The goal is that any level of nested calls can all call upon
|
||||
``reader()`` or ``writer()`` and participate in an already ongoing
|
||||
transaction. Only the outermost call within the scope actually ends
|
||||
the transaction except in the case of an exception; the ``writer()``
|
||||
method will emit a ``commit()`` and the ``reader()`` method will
|
||||
``close()`` the session, ensuring that the underlying connection is
|
||||
rolled back in a lightweight way.
|
||||
|
||||
Context and Thread Locals
|
||||
-------------------------
|
||||
|
||||
The proposal at the moment expects a "context" object, which can be
|
||||
any Python object, to be present in order to provide some object that
|
||||
bridges all elements of a call stack together. Most APIs with the notable
|
||||
exception of Keystone appear to already include a context argument.
|
||||
|
||||
To support a pattern that does not include a "context" argument, the only
|
||||
alternative is to use thread locals. In discussions with the community,
|
||||
the use of thread locals has the two concerns of: 1. it requires early patching
|
||||
at the eventlet level and 2. thread locals are seen as "action at a distance",
|
||||
more "implicit" than "explicit".
|
||||
|
||||
The proposal as stated here can be made to work with thread locals using
|
||||
this recipe::
|
||||
|
||||
# at the top of the api module
|
||||
GLOBAL_CONTEXT = threading.local()
|
||||
|
||||
|
||||
def some_api_method():
|
||||
with sql.using_writer(GLOBAL_CONTEXT) as session:
|
||||
# work with session
|
||||
|
||||
Whether or not we build in the above pattern, or we get Keystone to use
|
||||
an explicit context object, is not yet decided. See "Alternatives" for
|
||||
a listing of various options.
|
||||
|
||||
Reader vs. Writer
|
||||
-----------------
|
||||
|
||||
At the outset, ``reader()`` vs. ``writer()`` only intend to allow a block
|
||||
of functionality to mark itself as only requiring read-only access, or
|
||||
involving write access. At the very least, it can indicate if the outermost
|
||||
block need to be concerned about committing a transaction. Beyond that,
|
||||
this declaration can be used to determine if a particular method or block
|
||||
is suitable for "retry on deadlock", and also allows systems that attempt
|
||||
to split logic between "reader" and "writer" database links to know upfront
|
||||
which blocks should be routed where.
|
||||
|
||||
While a fully specified description for open-ended support of multiple
|
||||
databases is out of scope for this spec, as part of the
|
||||
implementation here we will necessarily implement at least what is already
|
||||
present. The existing EngineFacade features a "slave_engine" attribute
|
||||
as well as a "use_slave" flag on ``get_session()`` and ``get_engine()``;
|
||||
at least the Nova project and possibly others currently make use of this
|
||||
flag. So we will carry over an equivalent level of functionality into
|
||||
``reader()`` and ``writer()`` to start.
|
||||
|
||||
Beyond maintaining existing functionality, more comprehensive and
|
||||
potentially elaborate systems of multiple database support will be
|
||||
made easier to specify and implement subsequent to the rollout
|
||||
of this specification. This is because consuming projects will greatly reduce
|
||||
their verbosity down to a simple declarative level, leaving oslo.db
|
||||
free to expand upon the underlying machinery without incurring additional
|
||||
across-the-board changes in projects (hence one of the main reasons "facades"
|
||||
are used).
|
||||
|
||||
The behavior for nesting of readers and writers is as follows:
|
||||
|
||||
1. A ``reader()`` block that ultimately calls upon methods that then invoke
|
||||
``writer()`` should raise an exception; it means this ``reader()`` is not
|
||||
really a ``reader()`` at all.
|
||||
|
||||
2. A ``writer()`` block that ultimately
|
||||
calls upon methods that invoke ``reader()`` should pass successfully;
|
||||
those ``reader()`` blocks will in fact be made to act as a ``writer()``
|
||||
if they are called within the context of a ``writer()`` block.
|
||||
|
||||
Core Connection Methods
|
||||
-----------------------
|
||||
|
||||
For those methods that use Core only, corresponding methods
|
||||
``reader_connection()`` and ``writer_connection()`` are supplied,
|
||||
which instead of returning a ``sqlalchemy.orm.Session``, return a
|
||||
``sqlalchemy.engine.Connection``::
|
||||
|
||||
@sql.writer_connection
|
||||
def some_core_api_method(context):
|
||||
context.connection.execute(<statement>)
|
||||
|
||||
def some_core_api_method(context):
|
||||
with sql.using_writer_connection(context) as conn:
|
||||
conn.execute(<statement>)
|
||||
|
||||
``reader_connection()`` and ``writer_connection()`` will integrate with
|
||||
``reader()`` and ``writer()``, such that the outermost context will establish
|
||||
the ``sqlalchemy.engine.Connection`` that is to be used for the full
|
||||
context, whether or not it is associated with a ``Session``. This means
|
||||
the following:
|
||||
|
||||
1. If a ``reader_connection()`` or ``writer_connection()`` manager is invoked
|
||||
first, a ``sqlalchemy.engine.Connection``
|
||||
is associated with the context, and not a ``Session``.
|
||||
|
||||
2. If a ``reader()`` or ``writer()`` manager is invoked first, a ``Session``
|
||||
is associated with the context, which will contain within it a
|
||||
``sqlalchemy.engine.Connection``.
|
||||
|
||||
3. If a ``reader_connection()`` or ``writer_connection()`` manager is invoked
|
||||
and there is already a ``Session`` present, the ``Session.connection()``
|
||||
method of that ``Session`` is used to get at the ``Connection``.
|
||||
|
||||
4. If a ``reader()`` or ``writer()`` manager is invoked and there is already
|
||||
a ``Connection`` present, the new ``Session`` is created, and it is
|
||||
bound directly to this existing ``Connection``.
|
||||
|
||||
Integration with Configuration / Startup
|
||||
-----------------------------------------
|
||||
|
||||
The ``reader()``, ``writer()`` and other methods will be calling upon
|
||||
functional equivalents of the current ``get_session()`` and
|
||||
``get_engine()`` methods within oslo.db, as well as handling the logic
|
||||
that currently consists of invoking an ``EngineFacade`` and combining
|
||||
it with ``CONF``. That is, the consuming application does not refer
|
||||
to ``EngineFacade`` or ``CONF`` at all; the interaction with ``CONF``
|
||||
is performed similarly as it is now within oslo.db only, and is done
|
||||
under a mutex so that it is thread safe, in the way that Nova performs
|
||||
this task.
|
||||
|
||||
For applications that currently have special logic to add keys to ``CONF``
|
||||
or ``EngineFacade``, additional API methods will be provided. For example,
|
||||
Sahara wants to ensure the ``sqlite_fk`` flag is set to ``True``. The
|
||||
pattern will look like::
|
||||
|
||||
from oslo.db import enginefacade as sql
|
||||
|
||||
sql.configure(sqlite_fk=True)
|
||||
|
||||
def some_api_method():
|
||||
with sql.reader() as session:
|
||||
# work with session
|
||||
|
||||
Retry on Deadlock / Other failures
|
||||
-----------------------------------
|
||||
|
||||
Oslo.db provides the ``@wrap_db_retry()`` decorator, which allows an API
|
||||
method to replay itself on failure. Per
|
||||
https://review.openstack.org/#/c/109549/, we will be adding specificity
|
||||
to this decorator, which allows it to explicitly indicate that a method
|
||||
should be retried when a deadlock condition occurs. We can look into
|
||||
integrating this feature into the ``reader()`` and ``writer()`` decorators
|
||||
as well.
|
||||
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
A key decision here is that of the decorator vs. the context manager,
|
||||
as well as the use of thread locals.
|
||||
|
||||
Example forms:
|
||||
|
||||
1. Decorator, using context::
|
||||
|
||||
@sql.reader
|
||||
def some_api_method(context):
|
||||
# work with context.session
|
||||
|
||||
@sql.writer
|
||||
def some_other_api_method(context):
|
||||
# work with context.session
|
||||
|
||||
|
||||
2. Decorator, using thread local; here, the ``session`` argument is injected
|
||||
into the argument list of the API method within the scope of the decorator,
|
||||
it is *not* present in the outer call to the API method::
|
||||
|
||||
@sql.reader
|
||||
def some_api_method(session):
|
||||
# work with session
|
||||
|
||||
@sql.writer
|
||||
def some_other_api_method(session):
|
||||
# work with session
|
||||
|
||||
3. Context manager, using context::
|
||||
|
||||
def some_api_method(context):
|
||||
with sql.using_reader(context) as session:
|
||||
# work with session
|
||||
|
||||
def some_other_api_method(context):
|
||||
with sql.using_writer(context) as session:
|
||||
# work with session
|
||||
|
||||
4. Context manager, using implicit thread local::
|
||||
|
||||
def some_api_method():
|
||||
with sql.using_reader() as session:
|
||||
# work with session
|
||||
|
||||
def some_other_api_method():
|
||||
with sql.using_writer() as session:
|
||||
# work with session
|
||||
|
||||
5. Context manager, using explicit thread local::
|
||||
|
||||
def some_api_method():
|
||||
with sql.using_reader(GLOBAL_CONTEXT) as session:
|
||||
# work with session
|
||||
|
||||
def some_other_api_method():
|
||||
with sql.using_writer(GLOBAL_CONTEXT) as session:
|
||||
# work with session
|
||||
|
||||
The author favors approach #1. It should be noted that *all* the above
|
||||
approaches can be supported at the same time, if projects cannot agree
|
||||
on an approach.
|
||||
|
||||
Advantages to using a decorator only with an explicit context are:
|
||||
|
||||
1. The need for thread locals or any issues with eventlet is removed.
|
||||
|
||||
2. The "Retry on deadlock" and other "retry" features could be
|
||||
integrated into the ``reader()`` / ``writer()`` decorators, such that
|
||||
all API methods automatically gain this feature. As it stands,
|
||||
applications need to constantly push out new changes each time an
|
||||
unavoidable deadlock situation is detected in the wild, adding their
|
||||
``@_retry_on_deadlock()`` decorators to ever more API methods.
|
||||
|
||||
3. The decorator reduces nesting depth compared to context managers, and
|
||||
is ultimately less verbose, save for the need to have a "context"
|
||||
argument.
|
||||
|
||||
4. Decorators eliminate the possibility of this already-present
|
||||
antipattern::
|
||||
|
||||
def some_api_method():
|
||||
with sql.writer() as session:
|
||||
# do something with session
|
||||
|
||||
# transaction completes here
|
||||
|
||||
for element in stuff:
|
||||
# new transaction per element
|
||||
some_other_api_method_with_db(element)
|
||||
|
||||
Above, we are inadvertently performing any number of distinct
|
||||
transactions, first with the ``sql.writer()``, then with each call to
|
||||
some_other_api_method_with_db(). This antipattern can already be seen
|
||||
in methods like Nova's ``instance_create()`` method, paraphrased
|
||||
below::
|
||||
|
||||
@require_context
|
||||
def instance_create(context, values):
|
||||
|
||||
# ... about halfway through
|
||||
|
||||
session = get_session()
|
||||
|
||||
# session / connection / transaction #1
|
||||
with session.begin():
|
||||
# does some things with instnace_ref
|
||||
|
||||
# session / connection / transaction #2
|
||||
ec2_instance_create(context, instance_ref['uuid'])
|
||||
|
||||
# session / connection / transaction #3
|
||||
_instance_extra_create(context, {'instance_uuid': instance_ref['uuid']})
|
||||
|
||||
return instance_ref
|
||||
|
||||
Because the context manager allows unnecessary choices about when a
|
||||
transaction can begin and end within a method, we open ourselves up to make
|
||||
the wrong choice, as is already occurring in current code. Using a decorator,
|
||||
this antipattern is impossible::
|
||||
|
||||
@sql.writer()
|
||||
def some_api_method(context):
|
||||
# do something with context.session
|
||||
|
||||
for element in stuff:
|
||||
# uses same session / transaction guaranteed
|
||||
some_other_api_method_with_db(context, element)
|
||||
|
||||
# transaction completes here
|
||||
|
||||
One advantage to using an implicit "thread local" context is that it is impossible
|
||||
to inadvertently switch contexts in the middle of a call-chain, which would
|
||||
again lead to the nested-transaction issue.
|
||||
|
||||
An advantage of using context managers with implicit threadlocals is that
|
||||
it would be easier for Keystone to migrate to this system.
|
||||
|
||||
|
||||
Impact on Existing APIs
|
||||
-----------------------
|
||||
|
||||
Existing projects would need to integrate into some form of the
|
||||
patterns given.
|
||||
|
||||
|
||||
Security impact
|
||||
---------------
|
||||
|
||||
none
|
||||
|
||||
Performance Impact
|
||||
------------------
|
||||
|
||||
Performance will be dramatically improved as the current use of many
|
||||
redundant and disconnected sessions and transactions will be joined together.
|
||||
|
||||
|
||||
Configuration Impact
|
||||
--------------------
|
||||
|
||||
none.
|
||||
|
||||
|
||||
Developer Impact
|
||||
----------------
|
||||
|
||||
new patterns for developers to be aware of.
|
||||
|
||||
Testing Impact
|
||||
--------------
|
||||
|
||||
As most test suites currently make the simple decision of working with
|
||||
SQLite and allowing API methods to make use of their usual get_session() /
|
||||
get_engine() logic without any change or injection, little to no changes
|
||||
should be needed at first. Within oslo.db, the "opportunistic" fixtures
|
||||
as well as the DbTestCase system will be made to integrate with the
|
||||
new context manager/decorator system.
|
||||
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Mike Bayer
|
||||
|
||||
Milestones
|
||||
----------
|
||||
|
||||
Target Milestone for completion:
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
|
||||
Incubation
|
||||
==========
|
||||
|
||||
Adoption
|
||||
--------
|
||||
|
||||
Library
|
||||
-------
|
||||
|
||||
Anticipated API Stabilization
|
||||
-----------------------------
|
||||
|
||||
|
||||
Documentation Impact
|
||||
====================
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This work is licensed under a Creative Commons Attribution 3.0
|
||||
Unported License.
|
||||
http://creativecommons.org/licenses/by/3.0/legalcode
|
||||
|
Loading…
Reference in New Issue
Block a user