[IMP] *: create a single template db per module to test

Before this, when testing in parallel (using xdist) each worker would
create its own template database (per module, so 2) then would copy
the database for each test.

This is pretty inefficient as the init of a db is quite expensive in
CPU, and when increasing the number of workers (as the test suite is
rather IO bound) this would trigger a stampede as every worker would
try to create a template at the start of the test suite, leading to
extremely high loads and degraded host performances (e.g. 16 workers
would cause a load of 20 on a 4 cores 8 thread machine, which makes
its use difficult).

Instead we can have a lockfile at a known location of the filesystem,
the first worker to need a template for a module install locks it,
creates the templates, then writes the template's name to the
lockfile.

Every other worker can then lock the lockfile and read the name out,
using the db for duplication.

Note: needs to use `os.open` because the modes of `open` apparently
can't express "open at offset 0 for reading or create for writing",
`r+` refuses to create the file, `w+` still truncates, and `a+` is
undocumented and might not allow seeking back to the start on all
systems so better avoid it.

The implementation would be simplified by using `lockfile` but that's
an additional dependency plus it's deprecated. It recommends
`fasteners` but that seems to suck (not clear if storing stuff in the
lockfile is supported, it opens the lockfile in append mode). Here the
lockfiles are sufficient to do the entire thing.

Conveniently, this turns out to improve *both* walltime CPU time
compared to the original version, likely because while workers now
have to wait on whoever is creating the template they're not competing
for resources with it.
This commit is contained in:
Xavier Morel 2023-06-27 12:51:23 +02:00
parent f22ae357e3
commit d748d4b215

View File

@ -46,6 +46,7 @@ import collections
import configparser
import contextlib
import copy
import fcntl
import functools
import http.client
import itertools
@ -88,11 +89,20 @@ def pytest_addoption(parser):
"blow through the former); localtunnel has no rate-limiting but "
"the servers are way less reliable")
def is_manager(config):
return not hasattr(config, 'workerinput')
# noinspection PyUnusedLocal
def pytest_configure(config):
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'mergebot_test_utils'))
def pytest_unconfigure(config):
if not is_manager(config):
return
for c in config._tmp_path_factory.getbasetemp().iterdir():
if c.is_file() and c.name.startswith('template-'):
subprocess.run(['dropdb', '--if-exists', c.read_text(encoding='utf-8')])
@pytest.fixture(scope='session', autouse=True)
def _set_socket_timeout():
@ -269,12 +279,26 @@ def tunnel(pytestconfig, port):
raise ValueError("Unsupported %s tunnel method" % tunnel)
class DbDict(dict):
def __init__(self, adpath):
def __init__(self, adpath, shared_dir):
super().__init__()
self._adpath = adpath
self._shared_dir = shared_dir
def __missing__(self, module):
self[module] = db = 'template_%s' % uuid.uuid4()
with tempfile.TemporaryDirectory() as d:
with contextlib.ExitStack() as atexit:
f = atexit.enter_context(os.fdopen(os.open(
self._shared_dir / f'template-{module}',
os.O_CREAT | os.O_RDWR
), mode="r+", encoding='utf-8'))
fcntl.lockf(f, fcntl.LOCK_EX)
atexit.callback(fcntl.lockf, f, fcntl.LOCK_UN)
db = f.read()
if db:
self[module] = db
return db
d = atexit.enter_context(tempfile.TemporaryDirectory())
self[module] = db = 'template_%s' % uuid.uuid4()
subprocess.run([
'odoo', '--no-http',
*(['--addons-path', self._adpath] if self._adpath else []),
@ -286,17 +310,25 @@ class DbDict(dict):
check=True,
env={**os.environ, 'XDG_DATA_HOME': d}
)
f.write(db)
f.flush()
os.fsync(f.fileno())
return db
@pytest.fixture(scope='session')
def dbcache(request):
def dbcache(request, tmp_path_factory):
""" Creates template DB once per run, then just duplicates it before
starting odoo and running the testcase
"""
dbs = DbDict(request.config.getoption('--addons-path'))
shared_dir = tmp_path_factory.getbasetemp()
if not is_manager(request.config):
# xdist workers get a subdir as their basetemp, so we need to go one
# level up to deref it
shared_dir = shared_dir.parent
dbs = DbDict(request.config.getoption('--addons-path'), shared_dir)
yield dbs
for db in dbs.values():
subprocess.run(['dropdb', '--if-exists', db], check=True)
@pytest.fixture
def db(request, module, dbcache):