db: Add long-lived-transactionalized-db-fixtures
This feature will overhaul the system by which oslo.db provides connectivity and fixture support for database-enabled tests. The new features will allow tests to be run against any number of database backends, to allow "opportunistic" backend URLs to be specified on a per-environment basis, as well as to allow any number of successive tests to run against a fixed schema that is maintained across tests without the need to rebuild the schema on each test. Change-Id: I47cfe81b6169b64ff001ebbe28998f0df9b6f1fe
This commit is contained in:
parent
740b16d661
commit
b5f6ca1b1d
499
specs/kilo/long-lived-transactionalized-db-fixtures.rst
Normal file
499
specs/kilo/long-lived-transactionalized-db-fixtures.rst
Normal file
@ -0,0 +1,499 @@
|
||||
=============================================================================
|
||||
Run database tests within long-lived schemas and transactionalized containers
|
||||
=============================================================================
|
||||
|
||||
https://blueprints.launchpad.net/oslo/+spec/long-lived-transactionalized-db-fixtures
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
OpenStack applications currently do not include a standard technique of
|
||||
testing database-enabled code against any number of potential database
|
||||
backends. The techniques that are available themselves have issues
|
||||
that keep them from being practical in a general sense.
|
||||
|
||||
* It is common that the test suite of a particular OpenStack component
|
||||
is ultimately hardcoded to run on the SQLite backend. SQLite is chosen
|
||||
both for it's simplicity and great speed of schema setup, but is not
|
||||
the actual database used in OpenStack environments. The system by which
|
||||
this connectivity is established is often buried and spread across
|
||||
multiple files, with no option to alter what database is used.
|
||||
|
||||
* There is no solution to the case of the component that wishes to run hundreds
|
||||
of tests against a common schema, without requiring that the schema
|
||||
in question is built and torn down for every test. This makes it
|
||||
infeasible for the test suite to be run against non-SQLite backends on
|
||||
a regular basis, as the cost of this setup and teardown is too time
|
||||
consuming. A typical approach to
|
||||
this problem is to run tests inside of transactions which are rolled
|
||||
back at the end of the test, thereby allowing the schema to
|
||||
remain present. However, experiments with simplistic, single-database
|
||||
versions of this approach have failed due to the use of parallel testing,
|
||||
as well as being able to allow tests themselves to retain rudimental
|
||||
"commit/rollback" functionality without impacting the isolation of the
|
||||
fixture.
|
||||
|
||||
* While there is a system by which an application can run a suite of tests
|
||||
against other backends, known as the "Opportunistic" test suite, currently
|
||||
the "opportunistic" suite will fix those tests against a single, alternate
|
||||
backend, such as MySQL or Postgresql. While a "stub" suite can be added
|
||||
for each desired backend so that the same tests run on each, this approach
|
||||
does not accommodate attempting to run the suite against new backends
|
||||
not anticipated by the application, and also places the responsibility
|
||||
on the developer to ensure that all tests which apply to variable backends
|
||||
include that these "stubs" are present in multitude.
|
||||
|
||||
* The connection URL for "opportunistic" tests is also a fixed URL, with a
|
||||
hardcoded username/password and the requirement that the database is on
|
||||
localhost. If such a URL is connectable, it is used. This hardcoding
|
||||
and "opportunistic" style makes it inconvenient to control how tests run;
|
||||
for example, a workstation that has both MySQL and Postgresql servers running
|
||||
with the hardcoded username/password available cannot control a test run to
|
||||
only test against one or the other, or neither, of these databases; the suite
|
||||
will always run applicable tests against both based on their presence. It
|
||||
is also not possible for the developer to point the test suite at a
|
||||
server running on a host other than "localhost". The scheme used by
|
||||
this system is also hardcoded to the notion of a user/password/hostname
|
||||
based configuration; it can't work for databases that connect using a
|
||||
data source name or other symbolic name.
|
||||
|
||||
* The system by which "opportunistic" tests create and drop databases does
|
||||
not offer adequate extensibility for new backends.
|
||||
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
The proposed changes will in all cases ensure that total compatibility is
|
||||
retained for all current test setup techniques. The opportunistic system
|
||||
will continue to work as it does now, but offer environmental options to
|
||||
alter its behavior.
|
||||
|
||||
The key component will be the oslo/db/sqlchemy/provision.py
|
||||
module, as well as the oslo/db/sqlalchemy/test_base.py module.
|
||||
This provision module currently contains logic which handles the creation
|
||||
and dropping of anonymously named databases within the Postgresql and
|
||||
MySQL backends. When combined with the OpportunisticFixture, it
|
||||
creates/drops an anonymously named database per test setup/teardown.
|
||||
test_base includes the base test classes and fixtures which make
|
||||
use of provision.py for connectivity.
|
||||
|
||||
These modules will be enhanced to include several new constructs,
|
||||
described in the following sections.
|
||||
|
||||
Backend
|
||||
-------
|
||||
|
||||
This is an extensible "backend" system, based on a base class known as
|
||||
the Backend and used by the provision module. Backend will
|
||||
encapsulate the tasks needed in order to run tests against a particular
|
||||
backend, including:
|
||||
|
||||
* the ability to detect if a given database is available, either
|
||||
"opportunistically" or through environmental settings
|
||||
|
||||
* the ability to create, drop, and detect the presence of "anonymously"
|
||||
named databases
|
||||
|
||||
* the ability to produce a connection URL that will directly access this
|
||||
database upon connect
|
||||
|
||||
* the ability to drop individual all schema objects within
|
||||
a database of this type.
|
||||
|
||||
Logic for these features can vary across backends. For SQLite, which may be
|
||||
using in-memory databases or file-based databases, the URL scheme
|
||||
is SQLite-specific and needs to be generated from an anonymous schema
|
||||
name in a special way. For a Postgresql database, the backend will benefit
|
||||
by including the feature of disconnecting all users from a target database
|
||||
before dropping it, or being able to handle Postgresql's ENUM
|
||||
type that must be dropped explicitly when erasing the objects within
|
||||
a database.
|
||||
|
||||
Opportunistic URLs
|
||||
-------------------
|
||||
|
||||
The Backend system can report on whether or not a database of a particular
|
||||
backend type (e.g. MySQL, SQlite, Postgresql, etc) is available based on
|
||||
the "opportunistic" URL system. This system defaults to searching for
|
||||
a database given a fixed connectivity profile. Suppose the system
|
||||
includes a Backend implementation for each of: SQlite, Postgresql, and
|
||||
MySQL. Each of these backend implementations reports on a candidate
|
||||
"opportunistic" URL; a URL such as
|
||||
"postgresql://openstack_citest:openstack_citest@localhost" that can be tested
|
||||
for connectivity. Without any configuration, the system will attempt
|
||||
to make available "opportunstic" URLs for each BackendImpl that is implemented.
|
||||
In this way, the system works pretty much as it does today.
|
||||
|
||||
However, to make it configurable at runtime, we will enhance the role
|
||||
of the OS_TEST_DBAPI_ADMIN_CONNECTION environment variable. The
|
||||
current system allows this variable to specify a single "override" URL
|
||||
that is linked to the SQLite tests, but not the "opportunistic" ones.
|
||||
In the new system, it will allow a list of URLs, separated by a
|
||||
semicolon. For example, a value that allows tests to run against a specific
|
||||
SQLite database as well as a Postgresql database::
|
||||
|
||||
export OS_TEST_DBAPI_ADMIN_CONNECTION=\
|
||||
/path/to/sqlite.db;\
|
||||
postgresql+psycopg2://scott:tiger@localhost/openstack
|
||||
|
||||
When an explcit OS_TEST_DBAPI_ADMIN_CONNECTION is present, those URLs
|
||||
determine the complete list of BackendImpls that will report
|
||||
themselves as available, and overrides the usually fixed
|
||||
"opportunistic" URLs. With this function, the list of database
|
||||
backends as well as their full connectivity information can be
|
||||
determined at runtime.
|
||||
|
||||
|
||||
Provisioning
|
||||
------------
|
||||
|
||||
The provision module will call upon Backend in order to produce a
|
||||
"provisioning" system that works at three levels: database, schema,
|
||||
and transaction. The management of these three levels of resource will
|
||||
be maintained over the span of any number of tests.
|
||||
|
||||
A "database" will typically be maintained on a per-backend basis over
|
||||
the span of all tests run within a single Python process. By ensuring
|
||||
that an anonymous database is created per process for a given backend,
|
||||
the test suite can be safely run in parallel with no danger of concurrent
|
||||
tests colliding with each other. The current approach is that this database
|
||||
is created and dropped per-test; allowing the same database to persist across
|
||||
all tests in a run will reduce load and complexity.
|
||||
|
||||
A "schema" consists of a set of tables and other schema constructs that
|
||||
are created within a database. The vast majority of OpenStack applications
|
||||
run their tests within a single schema corresponding to their models.
|
||||
Most of these tests only need to exercise data manipulation within these
|
||||
schemas; a second class of test, the "migration" test, is less common and
|
||||
requires that it actually create and drop components of these schemas.
|
||||
|
||||
To support tests that exercise data manipulation within a fixed schema,
|
||||
the provisioning system will call upon an app-specific "create schema" hook
|
||||
when a newly created database is about to be used, within the scope of a
|
||||
so-called "schema scope". This schema will then remain in place as long
|
||||
as additional tests which also specify the same scope continue to be
|
||||
invoked. A "schema scope" is a string symbolic name
|
||||
that any number of tests can refer to, to state that they all run within
|
||||
the same schema. For example, if four different test suites in Nova all
|
||||
stated that their "SCHEMA_SCOPE" is "nova-cells", and these suites all referred
|
||||
to a "create schema" function that generated the nova model, the
|
||||
"create schema" function would be invoked just once, and then all four test
|
||||
suites would be run fully against the target database. The cleanup of data
|
||||
changes made by these tests is achieved using transaction rollbacks, rather
|
||||
than by dropping the whole database.
|
||||
|
||||
To support tests that are testing schema migrations and wish to create and
|
||||
drop their own schema elements, those tests specify a "SCHEMA_SCOPE" of None;
|
||||
the provisioning system will provide to these tests an empty database, and
|
||||
upon release of the provision, a DROP will be performed for any schema objects
|
||||
that still remain.
|
||||
|
||||
A "transaction" is an optional unit that is built up and torn down on a
|
||||
per-test basis. This feature is used when the test base specifies that
|
||||
it wishes to have "transactional" support, which is implied when a non-None
|
||||
"SCHEMA_SCOPE" is specified. This feature makes use of SQLAlchemy's
|
||||
Engine and Connection system in order to produce a mock "transaction"
|
||||
environment transparently provided to the test. Within this environment,
|
||||
any calls to "commit" the transaction don't actually commit for real.
|
||||
Tests are given the ability to emit rollbacks that work by also wrapping
|
||||
the environment within a SAVEPOINT. This is based on a technique that
|
||||
is commonly used with SQLAlchemy and is presented in various forms within
|
||||
the documentation as well as in talks; in this case, the technique will be
|
||||
enhanced to work not just at the ORM level but at the Core level as well,
|
||||
so that even applications that use the Core directly can participate in
|
||||
the transactionalized environment.
|
||||
|
||||
The SQLite backend has long had issues with SAVEPOINT, however in support
|
||||
of this feature, the backend is repaired in oslo.db using recent
|
||||
hooks; see https://review.openstack.org/#/c/113152/ for the review.
|
||||
|
||||
Fixture Integration
|
||||
-------------------
|
||||
|
||||
The provisioning system will be integrated into the test suite by taking
|
||||
advantage of the `testresources <https://pypi.python.org/pypi/testresources>`_
|
||||
library, which provides a system of
|
||||
allocating resources that may last across the span of multiple tests.
|
||||
``testresources`` works by maintaining the state of various resources
|
||||
within a dependency tree, that is tracked as many tests proceed. Only
|
||||
when a given resource reports itself as "dirty" is it torn down
|
||||
for the next test, and the final teardown only occurs once that resource
|
||||
is no longer needed.
|
||||
|
||||
Tests that use testresources by default will function normally, however
|
||||
the resources that they require will be fully created and dropped on a
|
||||
per-test basis, unless additional steps are taken which are specific
|
||||
to the testtools package. The tests therefore
|
||||
will remain compatible with any style of test runner, however the optimization
|
||||
or resources require the use of the testr or testtools runner, or with
|
||||
some extra work, the standard Python unittest runner.
|
||||
|
||||
In order to optimise resources among multiple tests, the tests must
|
||||
be assembled into the ``OptimisingTestSuite`` object provided by
|
||||
testresources. Integration of ``OptimisingTestSuite`` typically
|
||||
requires that the unittest-supported
|
||||
``load_tests()`` directive be stated either within an individual test module,
|
||||
or at the package level (e.g. ``__init__.py``), which will replace the usual
|
||||
system of test discovery with one which assembles the tests into a master
|
||||
``OptimisingTestSuite``. It is assumed that we will be able to provide
|
||||
a single oslo.db directive that can be dropped into the top-level
|
||||
``__init__.py`` file of a test suite as a whole in order to provide this
|
||||
effect.
|
||||
|
||||
In order to integrate with ``testresources``, the concepts of "database",
|
||||
"schema", and "transaction" will be implemented as individual test resource
|
||||
object types.
|
||||
|
||||
Scenarios
|
||||
---------
|
||||
|
||||
Scenarios refers to the use of a tool like `testscenarios
|
||||
<https://pypi.python.org/pypi/testscenarios/>`_, so that individual
|
||||
tests can be run multiple times against different backends. The
|
||||
existing Opportunistic fixture system will be enhanced such that the
|
||||
"DRIVER" attribute, which refers right now to a single type of
|
||||
database backend, can refer to a set of types. Each test will then be
|
||||
run against those drivers that are deemed to be available by the
|
||||
Backend system.
|
||||
|
||||
Usage within Actual Tests
|
||||
-------------------------
|
||||
|
||||
Real world tests take advantage of the system by using
|
||||
``oslo.db.sqlalchemy.DbTestCase``. This test case superclass acts much
|
||||
like it always has, providing ``self.session`` and ``self.engine`` members to
|
||||
use for database connectivity. However, the class can now mark via
|
||||
class-level annotations which databases it is appropriate towards, and what
|
||||
schema. For example, Nova can suggest a test suite against the Nova schema
|
||||
and to run against SQLite, Postgresql, and MySQL as follows::
|
||||
|
||||
class SomeNovatest(DbTestCase):
|
||||
|
||||
SCHEMA_SCOPE = "nova-cells"
|
||||
DRIVER = ('sqlite', 'postgresql', 'mysql')
|
||||
|
||||
def generate_schema(self, engine):
|
||||
"""Generate schema objects to be used within a test."""
|
||||
|
||||
nova.create_all_tables(engine)
|
||||
|
||||
def test_something(self):
|
||||
# do an actual test
|
||||
|
||||
The above class specifies how schemas are to be generated within the
|
||||
``generate_schema()`` method, which is called upon by the provisioning system
|
||||
to produce a schema corresponding to the "nova-cells" schema scope.
|
||||
As many test suites may use the same ``generate_schema()`` method, it is
|
||||
probably best to link ``generate_schema()`` with ``SCHEMA_SCOPE="nova-cells"``
|
||||
on a common mixin.
|
||||
|
||||
In order to integrate with testresources, the above set of directives will
|
||||
be used to compute the full set of test resource manager objects to
|
||||
be delivered via the ``.resources`` hook; this is an attribute that's bound
|
||||
to the ``DbTestCase`` class itself which testresources looks for in order
|
||||
to determine what kinds of resource objects are needed for the specific test.
|
||||
The implementation uses a Python descriptor for ``.resources`` so that its
|
||||
value is dynamically determined on a per-test basis.
|
||||
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
The decision to use testresources is made against two other variants
|
||||
that don't use it. All three variants are discussed here.
|
||||
|
||||
* The testresources library provides a means of spanning resources across
|
||||
tests that integrates with the mechanics of the standard Python
|
||||
unittest.TestSuite object, as well as the load_tests() hook which is used
|
||||
to estalibish TestSuite objects into a single OptimisingTestSuite.
|
||||
These mechanics are not fully or at all available in other commonly used
|
||||
test runners, including nose and py.test.
|
||||
|
||||
Advantages to testresources include that it is the standard system that
|
||||
goes along with the other use of testtools, and provides a sophisticated
|
||||
system of organizing tests to make the best use of resources declared by
|
||||
each. It's test manager API sets up a clear system of declaration and
|
||||
dependency between the various types of resource proposed in the
|
||||
provisioning system.
|
||||
|
||||
Disadvantages are that the optimising behavior is only available
|
||||
with a testtools-style run, or with a unittest-style run if additional
|
||||
steps are taken to integrate OptimisingTestSuite, as unittest itself
|
||||
does not appear to honor a package-level load_tests() hook.
|
||||
|
||||
Still to be resolved are some remaining issues with the load_tests() hook
|
||||
as implemented in the top-level ``__init__.py`` file when the "start"
|
||||
directory is that directory itself; it seems that the ``load_tests()``
|
||||
hook is skipped in this case, and may require that oslo.db's own tests
|
||||
are reorganized such that all tests can be loaded from named packages.
|
||||
However note that this issue is not a blocker; the ``load_tests()`` hook
|
||||
works fine as placed within specific test modules or within ``__init__.py``
|
||||
files that are loaded as packages, which is the case for the vast majority
|
||||
of openstack tests suites.
|
||||
|
||||
* Maintain awareness of test suite start/end per process using the Testr
|
||||
"instance provision" hooks. These hooks allow a set of fixed database
|
||||
names to be generated before tests run, to provide this name to the
|
||||
provisioning system within each subprocess, and finally after all
|
||||
test suites are finished, to emit a DROP for each database name on all
|
||||
available backends. The system can create databases lazily and only
|
||||
drop those which actually got created.
|
||||
|
||||
The configuration looks like this::
|
||||
|
||||
instance_provision=${PYTHON:-python} -m oslo.db.sqlalchemy.provision echo $INSTANCE_COUNT
|
||||
instance_execute=OSLO_SCHEMA_TOKEN=$INSTANCE_ID $COMMAND
|
||||
instance_dispose=${PYTHON:-python} -m oslo.db.sqlalchemy.provision drop --conditional $INSTANCE_IDS
|
||||
|
||||
The "instance provision" hook does not actually create any databases; only
|
||||
string names of databases that will be used if a database of a particular
|
||||
backend is requested during the test run. The "instance dispose" hook
|
||||
then delivers these names to the "drop" command, which will drop the
|
||||
named database on all possible backends if it is shown to exist; else the
|
||||
name is skipped.
|
||||
|
||||
This system runs mostly as efficiently as the testresources system,
|
||||
and still degrades gracefully when using other test runners.
|
||||
|
||||
The advantage to this system is that it is independent of the mechanics
|
||||
of unittest, and has only very simplistic hooks within testr which can
|
||||
easily be made to work with other test runners as well. It also does not
|
||||
require any package- or module-level load_tests() hooks and does not involve
|
||||
any changes to the ordering of tests.
|
||||
|
||||
Disadvantages include that it is more of a "homegrown" approach that
|
||||
reinvents a lot of what testresources already does. It may be more
|
||||
advantageous to look into enhancing testresources itself to be more
|
||||
easily integrated with other kinds of test runners.
|
||||
|
||||
* Maintain awareness of test suite start/end process by ensuring that the
|
||||
suite always runs within a special shell script that essentially runs
|
||||
the same commands and environmental settings as the testr hook.
|
||||
|
||||
This system is similar to that of using testr hooks, and both systems
|
||||
can coexist.
|
||||
|
||||
The disadvantages include not just those of the testr approach but also
|
||||
that shell scripts are complicated and ad-hoc, so in that sense there's
|
||||
even more code being reinvented here.
|
||||
|
||||
|
||||
Impact on Existing APIs
|
||||
-----------------------
|
||||
|
||||
Test suites which wish to take advantage of this system will need to base
|
||||
themselves on the new mechanics of DbTestCase, and to rework any existing
|
||||
systems they have of setting up connections or schemas to work within
|
||||
the new system. They will also need some kind of module- or package-level
|
||||
load_tests() directive in order to load up the OptimisingTestSuite system.
|
||||
|
||||
Security impact
|
||||
---------------
|
||||
|
||||
none
|
||||
|
||||
Performance Impact
|
||||
------------------
|
||||
|
||||
A key deliverable of this blueprint is to significantly improve performance
|
||||
for test suites that wish to run many tests against a common schema on
|
||||
heterogeneous database backends.
|
||||
|
||||
Configuration Impact
|
||||
--------------------
|
||||
|
||||
The configuration of the test runner may be impacted based on integration
|
||||
approach. The changes should be deliverable to gate runs without any
|
||||
direct changes to gates.
|
||||
|
||||
Developer Impact
|
||||
----------------
|
||||
|
||||
Developers should be aware of the DbTestCase base fixture, its
|
||||
implications, and will want to use it for tests that work against
|
||||
the database in a serious way.
|
||||
|
||||
Testing Impact
|
||||
--------------
|
||||
|
||||
The individual components of the system will have their own tests
|
||||
within oslo.db, to ensure database setup/teardown as well as to ensure
|
||||
that the transactional container works as expected.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Mike Bayer has already prototyped everything except scenario support,
|
||||
based on the use of testresources.
|
||||
|
||||
Robert Collins is also contributing towards issues observed in ensuring
|
||||
that testtools loads up all Python packages as packages, so that the
|
||||
load_tests() hook runs in all cases.
|
||||
|
||||
Milestones
|
||||
----------
|
||||
|
||||
N/A
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
* Build out provisioning system and backend system. This is already
|
||||
complete including the integration with testresources.
|
||||
|
||||
* build out the test scenarios integration - still a TODO
|
||||
|
||||
* implement the means by which load_tests() will be integrated, this is
|
||||
complete.
|
||||
|
||||
* documentation
|
||||
|
||||
|
||||
Incubation
|
||||
==========
|
||||
|
||||
N/A
|
||||
|
||||
Adoption
|
||||
--------
|
||||
|
||||
Nova, Neutron and Keystone might be good starts.
|
||||
|
||||
Library
|
||||
-------
|
||||
|
||||
oslo.db
|
||||
|
||||
Anticipated API Stabilization
|
||||
-----------------------------
|
||||
|
||||
unknown
|
||||
|
||||
Documentation Impact
|
||||
====================
|
||||
|
||||
Docstrings regarding DbTestCase.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
Testresources and testscenarios.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
Original bug: https://bugs.launchpad.net/oslo/+bug/1339206
|
||||
|
||||
Current prototypes: https://review.openstack.org/#/q/status:open+project:openstack/oslo.db+branch:master+topic:bug/1339206,n,z
|
||||
|
||||
|
||||
.. 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