Add hotfix for Koschei issue with resolving dependencies
For more details see https://github.com/msimacek/koschei/issues/16
This commit is contained in:
parent
ff2911e105
commit
b97270624b
2 changed files with 442 additions and 0 deletions
429
roles/koschei/files/resolver.py
Normal file
429
roles/koschei/files/resolver.py
Normal file
|
@ -0,0 +1,429 @@
|
|||
# Copyright (C) 2014 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
# Author: Michael Simacek <msimacek@redhat.com>
|
||||
|
||||
import os
|
||||
import time
|
||||
import hawkey
|
||||
import itertools
|
||||
import librepo
|
||||
import dnf.subject
|
||||
import dnf.sack
|
||||
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from koschei.models import (Package, Dependency, DependencyChange, Repo,
|
||||
ResolutionProblem, RepoGenerationRequest, Build,
|
||||
CompactDependencyChange, BuildrootProblem)
|
||||
from koschei import util
|
||||
from koschei.service import KojiService
|
||||
from koschei.srpm_cache import SRPMCache
|
||||
from koschei.repo_cache import RepoCache
|
||||
from koschei.backend import check_package_state, Backend
|
||||
|
||||
|
||||
class AbstractResolverTask(object):
|
||||
def __init__(self, log, db, koji_session,
|
||||
srpm_cache, repo_cache, backend):
|
||||
self.log = log
|
||||
self.db = db
|
||||
self.koji_session = koji_session
|
||||
self.srpm_cache = srpm_cache
|
||||
self.repo_cache = repo_cache
|
||||
self.backend = backend
|
||||
self.problems = []
|
||||
self.sack = None
|
||||
self.group = None
|
||||
self.resolved_packages = {}
|
||||
|
||||
def get_srpm_pkg(self, name, evr=None):
|
||||
if evr:
|
||||
# pylint: disable=W0633
|
||||
epoch, version, release = evr
|
||||
hawk_pkg = hawkey.Query(self.sack)\
|
||||
.filter(name=name, epoch=epoch or 0,
|
||||
arch='src', version=version,
|
||||
release=release)
|
||||
else:
|
||||
hawk_pkg = hawkey.Query(self.sack)\
|
||||
.filter(name=name, arch='src',
|
||||
latest_per_arch=True)
|
||||
if hawk_pkg:
|
||||
return hawk_pkg[0]
|
||||
|
||||
def prepare_goal(self, srpm=None):
|
||||
goal = hawkey.Goal(self.sack)
|
||||
problems = []
|
||||
for name in self.group:
|
||||
sltr = hawkey.Selector(self.sack).set(name=name)
|
||||
goal.install(select=sltr)
|
||||
if srpm:
|
||||
for reldep in srpm.requires:
|
||||
subj = dnf.subject.Subject(str(reldep))
|
||||
sltr = subj.get_best_selector(self.sack)
|
||||
# pylint: disable=E1103
|
||||
if sltr is None or not sltr.matches():
|
||||
problems.append("No package found for: {}".format(reldep))
|
||||
else:
|
||||
goal.install(select=sltr)
|
||||
return goal, problems
|
||||
|
||||
def store_deps(self, repo_id, package_id, installs):
|
||||
new_deps = []
|
||||
for install in installs or []:
|
||||
if install.arch != 'src':
|
||||
dep = Dependency(repo_id=repo_id, package_id=package_id,
|
||||
name=install.name, epoch=install.epoch,
|
||||
version=install.version,
|
||||
release=install.release,
|
||||
arch=install.arch)
|
||||
new_deps.append(dep)
|
||||
|
||||
if new_deps:
|
||||
# pylint: disable=E1101
|
||||
table = Dependency.__table__
|
||||
dicts = [{c.name: getattr(dep, c.name) for c in table.c
|
||||
if not c.primary_key}
|
||||
for dep in new_deps]
|
||||
self.db.connection().execute(table.insert(), dicts)
|
||||
self.db.expire_all()
|
||||
|
||||
def compute_dependency_distances(self, srpm, deps):
|
||||
dep_map = {dep.name: dep for dep in deps}
|
||||
visited = set()
|
||||
level = 1
|
||||
reldeps = srpm.requires
|
||||
while level < 5 and reldeps:
|
||||
pkgs_on_level = set(hawkey.Query(self.sack).filter(provides=reldeps))
|
||||
reldeps = {req for pkg in pkgs_on_level if pkg not in visited
|
||||
for req in pkg.requires}
|
||||
visited.update(pkgs_on_level)
|
||||
for pkg in pkgs_on_level:
|
||||
dep = dep_map.get(pkg.name)
|
||||
if dep and dep.distance is None:
|
||||
dep.distance = level
|
||||
level += 1
|
||||
|
||||
def resolve_dependencies(self, package, srpm):
|
||||
goal, problems = self.prepare_goal(srpm)
|
||||
if goal is not None:
|
||||
resolved = False
|
||||
if not problems:
|
||||
resolved = goal.run()
|
||||
problems = goal.problems
|
||||
self.resolved_packages[package.id] = resolved
|
||||
if resolved:
|
||||
# pylint: disable=E1101
|
||||
deps = [Dependency(name=pkg.name, epoch=pkg.epoch,
|
||||
version=pkg.version, release=pkg.release,
|
||||
arch=pkg.arch)
|
||||
for pkg in goal.list_installs() if pkg.arch != 'src']
|
||||
self.compute_dependency_distances(srpm, deps)
|
||||
return deps
|
||||
else:
|
||||
for problem in sorted(set(problems)):
|
||||
entry = dict(package_id=package.id,
|
||||
problem=problem)
|
||||
self.problems.append(entry)
|
||||
|
||||
def get_deps_from_db(self, package_id, repo_id):
|
||||
deps = self.db.query(Dependency)\
|
||||
.filter_by(repo_id=repo_id,
|
||||
package_id=package_id)
|
||||
return deps.all()
|
||||
|
||||
def create_dependency_changes(self, deps1, deps2, package_id,
|
||||
apply_id=None):
|
||||
if not deps1 or not deps2:
|
||||
# TODO packages with no deps
|
||||
return []
|
||||
|
||||
def key(dep):
|
||||
return (dep.name, dep.epoch, dep.version, dep.release)
|
||||
|
||||
old = util.set_difference(deps1, deps2, key)
|
||||
new = util.set_difference(deps2, deps1, key)
|
||||
|
||||
def create_change(name):
|
||||
return CompactDependencyChange(
|
||||
package_id=package_id, applied_in_id=apply_id, dep_name=name,
|
||||
prev_epoch=None, prev_version=None, prev_release=None,
|
||||
curr_epoch=None, curr_version=None, curr_release=None)
|
||||
|
||||
changes = {}
|
||||
for dep in old:
|
||||
change = create_change(dep.name)
|
||||
change.update(prev_version=dep.version, prev_epoch=dep.epoch,
|
||||
prev_release=dep.release, distance=dep.distance)
|
||||
changes[dep.name] = change
|
||||
for dep in new:
|
||||
change = changes.get(dep.name) or create_change(dep.name)
|
||||
change.update(curr_version=dep.version, curr_epoch=dep.epoch,
|
||||
curr_release=dep.release, distance=dep.distance)
|
||||
changes[dep.name] = change
|
||||
return changes.values() if changes else []
|
||||
|
||||
def update_dependency_changes(self, changes, apply_id=None):
|
||||
# pylint: disable=E1101
|
||||
self.db.query(DependencyChange)\
|
||||
.filter_by(applied_in_id=apply_id)\
|
||||
.delete(synchronize_session=False)
|
||||
if changes:
|
||||
self.db.execute(DependencyChange.__table__.insert(),
|
||||
changes)
|
||||
self.db.expire_all()
|
||||
|
||||
def get_prev_build_for_comparison(self, build):
|
||||
return self.db.query(Build)\
|
||||
.filter_by(package_id=build.package_id)\
|
||||
.filter(Build.id < build.id)\
|
||||
.filter(Build.deps_resolved == True)\
|
||||
.order_by(Build.id.desc()).first()
|
||||
|
||||
def prepare_sack(self, repo_id):
|
||||
for_arch = util.config['dependency']['for_arch']
|
||||
sack = dnf.sack.Sack(arch=for_arch)
|
||||
repos = self.repo_cache.get_repos(repo_id)
|
||||
if repos:
|
||||
util.add_repos_to_sack(repo_id, repos, sack)
|
||||
self.sack = sack
|
||||
|
||||
|
||||
class GenerateRepoTask(AbstractResolverTask):
|
||||
|
||||
def get_packages(self, expunge=True):
|
||||
packages = self.db.query(Package)\
|
||||
.filter(Package.ignored == False)\
|
||||
.options(joinedload(Package.last_build))\
|
||||
.options(joinedload(Package.last_complete_build))\
|
||||
.all()
|
||||
# detaches objects from ORM, prevents spurious queries that hinder
|
||||
# performance
|
||||
if expunge:
|
||||
self.db.expunge_all()
|
||||
return packages
|
||||
|
||||
def update_repo_index(self, repo_id):
|
||||
index_path = os.path.join(util.config['directories']['repodata'], 'index')
|
||||
with open(index_path, 'w') as index:
|
||||
index.write('{}\n'.format(repo_id))
|
||||
|
||||
def synchronize_resolution_state(self):
|
||||
packages = self.get_packages(expunge=False)
|
||||
for pkg in packages:
|
||||
curr_state = self.resolved_packages.get(pkg.id)
|
||||
if curr_state is not None:
|
||||
prev_state = pkg.msg_state_string
|
||||
pkg.resolved = curr_state
|
||||
check_package_state(pkg, prev_state)
|
||||
|
||||
for state in True, False:
|
||||
ids = [id for id, resolved in self.resolved_packages.iteritems()
|
||||
if resolved is state]
|
||||
if ids:
|
||||
self.db.query(Package).filter(Package.id.in_(ids))\
|
||||
.update({'resolved': state}, synchronize_session=False)
|
||||
|
||||
def get_build_for_comparison(self, package):
|
||||
"""
|
||||
Returns newest build which should be used for dependency
|
||||
comparisons or None if it shouldn't be compared at all
|
||||
"""
|
||||
last_build = package.last_build
|
||||
if last_build and last_build.state in Build.FINISHED_STATES:
|
||||
if last_build.deps_resolved:
|
||||
return last_build
|
||||
if last_build.deps_processed:
|
||||
# unresolved build, skip it
|
||||
return self.get_prev_build_for_comparison(last_build)
|
||||
|
||||
def generate_dependency_changes(self, packages, repo_id):
|
||||
changes = []
|
||||
for package in packages:
|
||||
srpm = self.get_srpm_pkg(package.name)
|
||||
if not srpm:
|
||||
continue
|
||||
curr_deps = self.resolve_dependencies(package, srpm)
|
||||
if curr_deps is not None:
|
||||
last_build = self.get_build_for_comparison(package)
|
||||
if last_build:
|
||||
prev_deps = self.get_deps_from_db(last_build.package_id,
|
||||
last_build.repo_id)
|
||||
if prev_deps is not None:
|
||||
changes += self.create_dependency_changes(prev_deps,
|
||||
curr_deps,
|
||||
package.id)
|
||||
return changes
|
||||
|
||||
def try_install_buildroot(self):
|
||||
goal, _ = self.prepare_goal()
|
||||
return goal.run(), goal.problems
|
||||
|
||||
def run(self, repo_id):
|
||||
start = time.time()
|
||||
self.log.info("Generating new repo")
|
||||
repo = Repo(repo_id=repo_id)
|
||||
self.db.add(repo)
|
||||
self.db.flush()
|
||||
self.backend.refresh_latest_builds()
|
||||
packages = self.get_packages()
|
||||
srpm_repo = self.srpm_cache.get_repodata()
|
||||
self.prepare_sack(repo_id)
|
||||
if not self.sack:
|
||||
self.log.error('Cannot generate repo: {}'.format(repo_id))
|
||||
return
|
||||
self.update_repo_index(repo_id)
|
||||
util.add_repo_to_sack('src', srpm_repo, self.sack)
|
||||
# TODO repo_id
|
||||
self.group = util.get_build_group(self.koji_session)
|
||||
base_installable, base_problems = self.try_install_buildroot()
|
||||
if not base_installable:
|
||||
self.log.info("Build group not resolvable")
|
||||
repo.base_resolved = False
|
||||
self.db.execute(BuildrootProblem.__table__.insert(),
|
||||
[{'repo_id': repo.repo_id, 'problem': problem}
|
||||
for problem in base_problems])
|
||||
self.db.commit()
|
||||
return
|
||||
self.log.info("Resolving dependencies")
|
||||
resolution_start = time.time()
|
||||
changes = self.generate_dependency_changes(packages, repo_id)
|
||||
resolution_end = time.time()
|
||||
self.db.query(ResolutionProblem).delete(synchronize_session=False)
|
||||
# pylint: disable=E1101
|
||||
if self.problems:
|
||||
self.db.execute(ResolutionProblem.__table__.insert(), self.problems)
|
||||
self.synchronize_resolution_state()
|
||||
self.update_dependency_changes(changes)
|
||||
repo.base_resolved = True
|
||||
self.db.commit()
|
||||
end = time.time()
|
||||
|
||||
self.log.info(("New repo done. Resolution time: {} minutes\n"
|
||||
"Overall time: {} minutes.")
|
||||
.format((resolution_end - resolution_start) / 60,
|
||||
(end - start) / 60))
|
||||
|
||||
class ProcessBuildsTask(AbstractResolverTask):
|
||||
|
||||
def process_build(self, build):
|
||||
if build.repo_id:
|
||||
self.log.info("Processing build {}".format(build.id))
|
||||
prev = self.get_prev_build_for_comparison(build)
|
||||
srpm = self.get_srpm_pkg(build.package.name, (build.epoch,
|
||||
build.version,
|
||||
build.release))
|
||||
if not srpm:
|
||||
return
|
||||
curr_deps = self.resolve_dependencies(package=build.package,
|
||||
srpm=srpm)
|
||||
self.store_deps(build.repo_id, build.package_id, curr_deps)
|
||||
if curr_deps is not None:
|
||||
build.deps_resolved = True
|
||||
if prev and prev.repo_id:
|
||||
prev_deps = self.get_deps_from_db(prev.package_id,
|
||||
prev.repo_id)
|
||||
if prev_deps and curr_deps:
|
||||
changes = self.create_dependency_changes(
|
||||
prev_deps, curr_deps, package_id=build.package_id,
|
||||
apply_id=build.id)
|
||||
self.update_dependency_changes(changes, apply_id=build.id)
|
||||
keep_builds = util.config['dependency']['keep_build_deps_for']
|
||||
boundary_build = self.db.query(Build)\
|
||||
.filter_by(package_id=build.package_id)\
|
||||
.order_by(Build.id.desc())\
|
||||
.offset(keep_builds).first()
|
||||
if boundary_build and boundary_build.repo_id:
|
||||
self.db.query(Dependency)\
|
||||
.filter_by(package_id=build.package_id)\
|
||||
.filter(Dependency.repo_id <
|
||||
boundary_build.repo_id)\
|
||||
.delete(synchronize_session=False)
|
||||
|
||||
def run(self):
|
||||
# pylint: disable=E1101
|
||||
unprocessed = self.db.query(Build)\
|
||||
.filter_by(deps_processed=False)\
|
||||
.filter(Build.repo_id != None)\
|
||||
.options(joinedload(Build.package))\
|
||||
.order_by(Build.repo_id).all()
|
||||
# TODO repo_id
|
||||
self.group = util.get_build_group(self.koji_session)
|
||||
|
||||
# do this before processing to avoid multiple runs of createrepo
|
||||
for build in unprocessed:
|
||||
self.srpm_cache.get_srpm(build.package.name,
|
||||
build.version, build.release)
|
||||
srpm_repo = self.srpm_cache.get_repodata()
|
||||
|
||||
for repo_id, builds in itertools.groupby(unprocessed,
|
||||
lambda build: build.repo_id):
|
||||
builds = list(builds)
|
||||
if repo_id is not None:
|
||||
self.prepare_sack(repo_id)
|
||||
if self.sack:
|
||||
util.add_repos_to_sack('srpm', {'src': srpm_repo}, self.sack)
|
||||
for build in builds:
|
||||
self.process_build(build)
|
||||
self.db.query(Build).filter(Build.id.in_([b.id for b in builds]))\
|
||||
.update({'deps_processed': True},
|
||||
synchronize_session=False)
|
||||
self.db.commit()
|
||||
|
||||
class Resolver(KojiService):
|
||||
|
||||
def __init__(self, log=None, db=None, koji_session=None,
|
||||
srpm_cache=None, repo_cache=None, backend=None):
|
||||
super(Resolver, self).__init__(log=log, db=db,
|
||||
koji_session=koji_session)
|
||||
self.srpm_cache = (srpm_cache
|
||||
or SRPMCache(koji_session=self.koji_session))
|
||||
self.repo_cache = repo_cache or RepoCache()
|
||||
self.backend = backend or Backend(db=self.db,
|
||||
koji_session=self.koji_session,
|
||||
log=self.log,
|
||||
srpm_cache=self.srpm_cache)
|
||||
|
||||
def get_handled_exceptions(self):
|
||||
return ([librepo.LibrepoException] +
|
||||
super(Resolver, self).get_handled_exceptions())
|
||||
|
||||
def create_task(self, cls):
|
||||
return cls(log=self.log, db=self.db, koji_session=self.koji_session,
|
||||
srpm_cache=self.srpm_cache, repo_cache=self.repo_cache,
|
||||
backend=self.backend)
|
||||
|
||||
def process_repo_generation_requests(self):
|
||||
latest_request = self.db.query(RepoGenerationRequest)\
|
||||
.order_by(RepoGenerationRequest.repo_id
|
||||
.desc())\
|
||||
.first()
|
||||
if latest_request:
|
||||
repo_id = latest_request.repo_id
|
||||
[last_repo] = (self.db.query(Repo.repo_id)
|
||||
.order_by(Repo.repo_id.desc())
|
||||
.first() or [0])
|
||||
if repo_id > last_repo:
|
||||
self.create_task(GenerateRepoTask).run(repo_id)
|
||||
self.db.query(RepoGenerationRequest)\
|
||||
.filter(RepoGenerationRequest.repo_id <= repo_id)\
|
||||
.delete()
|
||||
self.db.commit()
|
||||
|
||||
def main(self):
|
||||
self.create_task(ProcessBuildsTask).run()
|
||||
self.process_repo_generation_requests()
|
|
@ -108,3 +108,16 @@
|
|||
- cron-db-cleanup
|
||||
tags:
|
||||
- koschei
|
||||
|
||||
# See https://github.com/msimacek/koschei/issues/16
|
||||
- name: HOTFIX koschei resolver.py
|
||||
copy: >
|
||||
src=resolver.py
|
||||
dest=/usr/lib/python2.7/site-packages/koschei/resolver.py
|
||||
owner=root group=root mode=0644
|
||||
when: env == "production"
|
||||
notify:
|
||||
- restart koschei-resolver
|
||||
tags:
|
||||
- koschei
|
||||
- hotfix
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue