installability: Exploit equvialency to reduce choices

For some cases, like aspell-dictionary, a number of packages can
satisfy the dependency (e.g. all aspell-*).  In the particular
example, most (all?) of the aspell-* look so similar to the extent
that reverse dependencies cannot tell two aspell-* packages apart (IRT
to installability and co-installability).

This patch attempts to help the installability tester by detecting
such cases and reducing the number of candidates for a given choice.

Reported-In: <20140716134823.GA11795@x230-buxy.home.ouaza.com>
Signed-off-by: Niels Thykier <niels@thykier.net>
debian
Niels Thykier 11 years ago
parent e9a7a07856
commit 72daebd67c

@ -12,6 +12,7 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from britney_util import ifilter_except, iter_except from britney_util import ifilter_except, iter_except
@ -28,7 +29,7 @@ class _RelationBuilder(object):
self._new_breaks = set(binary_data[1]) self._new_breaks = set(binary_data[1])
def add_dependency_clause(self, or_clause): def add_dependency_clause(self, or_clause, frozenset=frozenset):
"""Add a dependency clause """Add a dependency clause
The clause must be a sequence of (name, version, architecture) The clause must be a sequence of (name, version, architecture)
@ -48,12 +49,12 @@ class _RelationBuilder(object):
binary = self._binary binary = self._binary
itbuilder = self._itbuilder itbuilder = self._itbuilder
package_table = itbuilder._package_table package_table = itbuilder._package_table
reverse_package_table = itbuilder._reverse_package_table
okay = False okay = False
for dep_tuple in clause: for dep_tuple in clause:
okay = True okay = True
reverse_relations = itbuilder._reverse_relations(dep_tuple) rdeps, _, rdep_relations = itbuilder._reverse_relations(dep_tuple)
reverse_relations[0].add(binary) rdeps.add(binary)
rdep_relations.add(clause)
self._new_deps.add(clause) self._new_deps.add(clause)
if not okay: if not okay:
@ -193,7 +194,7 @@ class InstallabilityTesterBuilder(object):
if binary in self._reverse_package_table: if binary in self._reverse_package_table:
return self._reverse_package_table[binary] return self._reverse_package_table[binary]
rel = [set(), set()] rel = [set(), set(), set()]
self._reverse_package_table[binary] = rel self._reverse_package_table[binary] = rel
return rel return rel
@ -227,18 +228,21 @@ class InstallabilityTesterBuilder(object):
# operations in _check_loop since we only have to check one # operations in _check_loop since we only have to check one
# set (instead of two) and we remove a few duplicates here # set (instead of two) and we remove a few duplicates here
# and there. # and there.
#
# At the same time, intern the rdep sets
for pkg in reverse_package_table: for pkg in reverse_package_table:
if pkg not in package_table: if pkg not in package_table:
raise RuntimeError("%s/%s/%s referenced but not added!" % pkg) raise RuntimeError("%s/%s/%s referenced but not added!" % pkg)
if not reverse_package_table[pkg][1]:
# no rconflicts - ignore
continue
deps, con = package_table[pkg] deps, con = package_table[pkg]
if not con: rdeps, rcon, rdep_relations = reverse_package_table[pkg]
con = intern_set(reverse_package_table[pkg][1]) if rcon:
else: if not con:
con = intern_set(con | reverse_package_table[pkg][1]) con = intern_set(rcon)
package_table[pkg] = (deps, con) else:
con = intern_set(con | rcon)
package_table[pkg] = (deps, con)
reverse_package_table[pkg] = (intern_set(rdeps), con,
intern_set(rdep_relations))
# Check if we can expand broken. # Check if we can expand broken.
for t in not_broken(iter_except(check.pop, KeyError)): for t in not_broken(iter_except(check.pop, KeyError)):
@ -308,8 +312,95 @@ class InstallabilityTesterBuilder(object):
# add all rdeps (except those already in the safe_set) # add all rdeps (except those already in the safe_set)
check.update(reverse_package_table[pkg][0] - safe_set) check.update(reverse_package_table[pkg][0] - safe_set)
eqv_table = self._build_eqv_packages_table(package_table,
reverse_package_table)
return InstallabilitySolver(package_table, return InstallabilitySolver(package_table,
reverse_package_table, reverse_package_table,
self._testing, self._broken, self._testing, self._broken,
self._essentials, safe_set) self._essentials, safe_set,
eqv_table)
def _build_eqv_packages_table(self, package_table,
reverse_package_table,
frozenset=frozenset):
"""Attempt to build a table of equivalent packages
This method attempts to create a table of packages that are
equivalent (in terms of installability). If two packages (A
and B) are equivalent then testing the installability of A is
the same as testing the installability of B. This equivalency
also applies to co-installability.
The example cases:
* aspell-*
* ispell-*
Cases that do *not* apply:
* MTA's
The theory:
The packages A and B are equivalent iff:
reverse_depends(A) == reverse_depends(B) AND
conflicts(A) == conflicts(B) AND
depends(A) == depends(B)
Where "reverse_depends(X)" is the set of reverse dependencies
of X, "conflicts(X)" is the set of negative dependencies of X
(Breaks and Conflicts plus the reverse ones of those combined)
and "depends(X)" is the set of strong dependencies of X
(Depends and Pre-Depends combined).
To be honest, we are actually equally interested another
property as well, namely substitutability. The package A can
always used instead of B, iff:
reverse_depends(A) >= reverse_depends(B) AND
conflicts(A) <= conflicts(B) AND
depends(A) == depends(B)
(With the same definitions as above). Note that equivalency
is just a special-case of substitutability, where A and B can
substitute each other (i.e. a two-way substituation).
Finally, note that the "depends(A) == depends(B)" for
substitutability is actually not a strict requirement. There
are cases where those sets are different without affecting the
property.
"""
# Despite talking about substitutability, the method currently
# only finds the equivalence cases. Lets leave
# substitutability for a future version.
find_eqv_table = defaultdict(list)
eqv_table = {}
for pkg in reverse_package_table:
rdeps = reverse_package_table[pkg][2]
if not rdeps:
# we don't care for things without rdeps (because
# it is not worth it)
continue
deps, con = package_table[pkg]
ekey = (deps, con, rdeps)
find_eqv_table[ekey].append(pkg)
for pkg_list in find_eqv_table.itervalues():
if len(pkg_list) < 2:
continue
if (len(pkg_list) == 2 and pkg_list[0][0] == pkg_list[1][0]
and pkg_list[0][2] == pkg_list[1][2]):
# This is a (most likely) common and boring case. It
# is when pkgA depends on pkgB and is satisfied with
# any version available. However, at most one version
# of pkgB will be available in testing, so other
# filters will make this case redundant.
continue
eqv_set = frozenset(pkg_list)
for pkg in pkg_list:
eqv_table[pkg] = eqv_set
return eqv_table

@ -24,7 +24,7 @@ from britney_util import (ifilter_only, iter_except)
class InstallabilitySolver(InstallabilityTester): class InstallabilitySolver(InstallabilityTester):
def __init__(self, universe, revuniverse, testing, broken, essentials, def __init__(self, universe, revuniverse, testing, broken, essentials,
safe_set): safe_set, eqv_table):
"""Create a new installability solver """Create a new installability solver
universe is a dict mapping package tuples to their universe is a dict mapping package tuples to their
@ -44,7 +44,7 @@ class InstallabilitySolver(InstallabilityTester):
(simplifies caches and dependency checking) (simplifies caches and dependency checking)
""" """
InstallabilityTester.__init__(self, universe, revuniverse, testing, InstallabilityTester.__init__(self, universe, revuniverse, testing,
broken, essentials, safe_set) broken, essentials, safe_set, eqv_table)
def solve_groups(self, groups): def solve_groups(self, groups):

@ -20,7 +20,7 @@ from britney_util import iter_except
class InstallabilityTester(object): class InstallabilityTester(object):
def __init__(self, universe, revuniverse, testing, broken, essentials, def __init__(self, universe, revuniverse, testing, broken, essentials,
safe_set): safe_set, eqv_table):
"""Create a new installability tester """Create a new installability tester
universe is a dict mapping package tuples to their universe is a dict mapping package tuples to their
@ -51,6 +51,7 @@ class InstallabilityTester(object):
self._essentials = essentials self._essentials = essentials
self._revuniverse = revuniverse self._revuniverse = revuniverse
self._safe_set = safe_set self._safe_set = safe_set
self._eqv_table = eqv_table
# Cache of packages known to be broken - we deliberately do not # Cache of packages known to be broken - we deliberately do not
# include "broken" in it. See _optimize for more info. # include "broken" in it. See _optimize for more info.
@ -235,8 +236,9 @@ class InstallabilityTester(object):
never.update(ess_never) never.update(ess_never)
# curry check_loop # curry check_loop
check_loop = partial(self._check_loop, universe, testing, musts, check_loop = partial(self._check_loop, universe, testing,
never, choices, cbroken) self._eqv_table, musts, never, choices,
cbroken)
# Useful things to remember: # Useful things to remember:
@ -359,8 +361,9 @@ class InstallabilityTester(object):
return verdict return verdict
def _check_loop(self, universe, testing, musts, never, def _check_loop(self, universe, testing, eqv_table, musts, never,
choices, cbroken, check): choices, cbroken, check, len=len,
frozenset=frozenset):
"""Finds all guaranteed dependencies via "check". """Finds all guaranteed dependencies via "check".
If it returns False, t is not installable. If it returns True If it returns False, t is not installable. If it returns True
@ -368,8 +371,6 @@ class InstallabilityTester(object):
returns True, then t is installable. returns True, then t is installable.
""" """
# Local variables for faster access... # Local variables for faster access...
l = len
fset = frozenset
not_satisfied = partial(ifilter, musts.isdisjoint) not_satisfied = partial(ifilter, musts.isdisjoint)
# While we have guaranteed dependencies (in check), examine all # While we have guaranteed dependencies (in check), examine all
@ -401,9 +402,9 @@ class InstallabilityTester(object):
# - not in testing # - not in testing
# - known to be broken (by cache) # - known to be broken (by cache)
# - in never # - in never
candidates = fset((depgroup & testing) - never) candidates = frozenset((depgroup & testing) - never)
if l(candidates) == 0: if len(candidates) == 0:
# We got no candidates to satisfy it - this # We got no candidates to satisfy it - this
# package cannot be installed with the current # package cannot be installed with the current
# testing # testing
@ -413,21 +414,43 @@ class InstallabilityTester(object):
cbroken.add(cur) cbroken.add(cur)
testing.remove(cur) testing.remove(cur)
return False return False
if l(candidates) == 1: if len(candidates) == 1:
# only one possible solution to this choice and we # only one possible solution to this choice and we
# haven't seen it before # haven't seen it before
check.update(candidates) check.update(candidates)
musts.update(candidates) musts.update(candidates)
else: else:
possible_eqv = set(x for x in candidates if x in eqv_table)
if len(possible_eqv) > 1:
# Exploit equvialency to reduce the number of
# candidates if possible. Basically, this
# code maps "similar" candidates into a single
# candidate that will give a identical result
# to any other candidate it eliminates.
#
# See InstallabilityTesterBuilder's
# _build_eqv_packages_table method for more
# information on how this works.
new_cand = set(x for x in candidates if x not in possible_eqv)
for chosen in iter_except(possible_eqv.pop, KeyError):
new_cand.add(chosen)
possible_eqv -= eqv_table[chosen]
if len(new_cand) == 1:
check.update(new_cand)
musts.update(new_cand)
continue
candidates = frozenset(new_cand)
# defer this choice till later # defer this choice till later
choices.add(candidates) choices.add(candidates)
return True return True
def _get_min_pseudo_ess_set(self, arch): def _get_min_pseudo_ess_set(self, arch):
if arch not in self._cache_ess: if arch not in self._cache_ess:
# The minimal essential set cache is not present - # The minimal essential set cache is not present -
# compute it now. # compute it now.
testing = self._testing testing = self._testing
eqv_table = self._eqv_table
cbroken = self._cache_broken cbroken = self._cache_broken
universe = self._universe universe = self._universe
safe_set = self._safe_set safe_set = self._safe_set
@ -439,8 +462,9 @@ class InstallabilityTester(object):
not_satisified = partial(ifilter, start.isdisjoint) not_satisified = partial(ifilter, start.isdisjoint)
while ess_base: while ess_base:
self._check_loop(universe, testing, start, ess_never,\ self._check_loop(universe, testing, eqv_table,
ess_choices, cbroken, ess_base) start, ess_never, ess_choices,
cbroken, ess_base)
if ess_choices: if ess_choices:
# Try to break choices where possible # Try to break choices where possible
nchoice = set() nchoice = set()

Loading…
Cancel
Save