diff --git a/src/pylorax/api/compose.py b/src/pylorax/api/compose.py index 751da299..1a6d72f5 100644 --- a/src/pylorax/api/compose.py +++ b/src/pylorax/api/compose.py @@ -296,7 +296,8 @@ def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type, test_m projects = sorted(set(module_nver+package_nver), key=lambda p: p[0].lower()) deps = [] try: - with yumlock.lock: + # This can possibly update repodata and reset the YumBase object. + with yumlock.lock_check: (installed_size, deps) = projects_depsolve_with_size(yumlock.yb, projects, recipe.group_names, with_core=False) except ProjectsError as e: log.error("start_build depsolve: %s", str(e)) diff --git a/src/pylorax/api/server.py b/src/pylorax/api/server.py index 2acc131b..15d5a339 100644 --- a/src/pylorax/api/server.py +++ b/src/pylorax/api/server.py @@ -28,7 +28,6 @@ from pylorax.api.v0 import v0_api from pylorax.sysutils import joinpaths GitLock = namedtuple("GitLock", ["repo", "lock", "dir"]) -YumLock = namedtuple("YumLock", ["yb", "lock"]) server = Flask(__name__) diff --git a/src/pylorax/api/yumbase.py b/src/pylorax/api/yumbase.py index 11b38d40..6c5b15db 100644 --- a/src/pylorax/api/yumbase.py +++ b/src/pylorax/api/yumbase.py @@ -23,6 +23,8 @@ import ConfigParser from fnmatch import fnmatchcase from glob import glob import os +from threading import Lock +import time import yum from yum.Errors import YumBaseError @@ -31,6 +33,74 @@ yum.logginglevels._added_handlers = True from pylorax.sysutils import joinpaths +class YumLock(object): + """Hold the YumBase object and a Lock to control access to it. + + self.yb is a property that returns the YumBase object, but it *may* change + from one call to the next if the upstream repositories have changed. + """ + def __init__(self, conf, expire_secs=6*60*60): + self._conf = conf + self._lock = Lock() + self.yb = get_base_object(self._conf) + self._expire_secs = expire_secs + self._expire_time = time.time() + self._expire_secs + + @property + def lock(self): + """Check for repo updates (using expiration time) and return the lock + + If the repository has been updated, tear down the old YumBase and + create a new one. This is the only way to force yum to use the new + metadata. + """ + if time.time() > self._expire_time: + return self.lock_check + return self._lock + + @property + def lock_check(self): + """Force a check for repo updates and return the lock + + If the repository has been updated, tear down the old YumBase and + create a new one. This is the only way to force yum to use the new + metadata. + + Use this method sparingly, it removes the repodata and downloads a new copy every time. + """ + self._expire_time = time.time() + self._expire_secs + if self._haveReposChanged(): + self._destroyYb() + self.yb = get_base_object(self._conf) + return self._lock + + def _destroyYb(self): + # Do our best to get yum to let go of all the things... + self.yb.pkgSack.dropCachedData() + for s in self.yb.pkgSack.sacks.values(): + s.close() + del s + del self.yb.pkgSack + self.yb.closeRpmDB() + del self.yb.tsInfo + del self.yb.ts + self.yb.close() + del self.yb + + def _haveReposChanged(self): + """Return True if the repo has new metadata""" + # This is a total kludge, yum doesn't really expect to deal with things changing while the + # object is is use. + try: + before = [(r.id, r.repoXML.checksums["sha256"]) for r in sorted(self.yb.repos.listEnabled())] + for r in sorted(self.yb.repos.listEnabled()): + r.metadata_expire = 0 + del r.repoXML + after = [(r.id, r.repoXML.checksums["sha256"]) for r in sorted(self.yb.repos.listEnabled())] + return before != after + except Exception: + return False + def get_base_object(conf): """Get the Yum object with settings from the config file diff --git a/src/sbin/lorax-composer b/src/sbin/lorax-composer index c76f2f4f..ca226415 100755 --- a/src/sbin/lorax-composer +++ b/src/sbin/lorax-composer @@ -40,8 +40,8 @@ from pylorax.api.config import configure, make_yum_dirs, make_queue_dirs from pylorax.api.compose import test_templates from pylorax.api.queue import start_queue_monitor from pylorax.api.recipes import open_or_create_repo, commit_recipe_directory -from pylorax.api.server import server, GitLock, YumLock -from pylorax.api.yumbase import get_base_object +from pylorax.api.server import server, GitLock +from pylorax.api.yumbase import YumLock VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum) @@ -283,14 +283,14 @@ if __name__ == '__main__': # Get a YumBase to share with the requests try: - yb = get_base_object(server.config["COMPOSER_CFG"]) + server.config["YUMLOCK"] = YumLock(server.config["COMPOSER_CFG"]) except RuntimeError: # Error has already been logged. Just exit cleanly. sys.exit(1) - server.config["YUMLOCK"] = YumLock(yb=yb, lock=Lock()) # Depsolve the templates and make a note of the failures for /api/status to report - server.config["TEMPLATE_ERRORS"] = test_templates(yb, server.config["COMPOSER_CFG"].get("composer", "share_dir")) + with server.config["YUMLOCK"].lock: + server.config["TEMPLATE_ERRORS"] = test_templates(server.config["YUMLOCK"].yb, server.config["COMPOSER_CFG"].get("composer", "share_dir")) # Setup access to the git repo server.config["REPO_DIR"] = opts.BLUEPRINTS