mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 15:35:46 +07:00
[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:
parent
f22ae357e3
commit
d748d4b215
46
conftest.py
46
conftest.py
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user