~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/archivepublisher/domination.py

pocomment becomes the last member of the Condemned 36.

Show diffs side-by-side

added added

removed removed

Lines of Context:
53
53
__all__ = ['Dominator']
54
54
 
55
55
from datetime import timedelta
56
 
import functools
57
 
import operator
58
56
 
59
57
import apt_pkg
60
58
from storm.expr import (
68
66
    flush_database_updates,
69
67
    sqlvalues,
70
68
    )
71
 
from canonical.launchpad.interfaces.lpstorm import IMasterStore
 
69
from canonical.launchpad.interfaces.lpstorm import IStore
72
70
from lp.registry.model.sourcepackagename import SourcePackageName
73
71
from lp.soyuz.enums import (
74
72
    BinaryPackageFormat,
87
85
apt_pkg.InitSystem()
88
86
 
89
87
 
90
 
def _compare_packages_by_version_and_date(get_release, p1, p2):
91
 
    """Compare publications p1 and p2 by their version; using Debian rules.
92
 
 
93
 
    If the publications are for the same package, compare by datecreated
94
 
    instead. This lets newer records win.
95
 
    """
96
 
    if get_release(p1).id == get_release(p2).id:
97
 
        return cmp(p1.datecreated, p2.datecreated)
98
 
 
99
 
    return apt_pkg.VersionCompare(get_release(p1).version,
100
 
                                  get_release(p2).version)
 
88
def join_spr_spn():
 
89
    """Join condition: SourcePackageRelease/SourcePackageName."""
 
90
    return (
 
91
        SourcePackageName.id == SourcePackageRelease.sourcepackagenameID)
 
92
 
 
93
 
 
94
def join_spph_spr():
 
95
    """Join condition: SourcePackageRelease/SourcePackagePublishingHistory.
 
96
    """
 
97
    # Avoid circular imports.
 
98
    from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 
99
 
 
100
    return (
 
101
        SourcePackageRelease.id ==
 
102
            SourcePackagePublishingHistory.sourcepackagereleaseID)
 
103
 
 
104
 
 
105
class SourcePublicationTraits:
 
106
    """Basic generalized attributes for `SourcePackagePublishingHistory`.
 
107
 
 
108
    Used by `GeneralizedPublication` to hide the differences from
 
109
    `BinaryPackagePublishingHistory`.
 
110
    """
 
111
    @staticmethod
 
112
    def getPackageName(spph):
 
113
        """Return the name of this publication's source package."""
 
114
        return spph.sourcepackagerelease.sourcepackagename.name
 
115
 
 
116
    @staticmethod
 
117
    def getPackageRelease(spph):
 
118
        """Return this publication's `SourcePackageRelease`."""
 
119
        return spph.sourcepackagerelease
 
120
 
 
121
 
 
122
class BinaryPublicationTraits:
 
123
    """Basic generalized attributes for `BinaryPackagePublishingHistory`.
 
124
 
 
125
    Used by `GeneralizedPublication` to hide the differences from
 
126
    `SourcePackagePublishingHistory`.
 
127
    """
 
128
    @staticmethod
 
129
    def getPackageName(bpph):
 
130
        """Return the name of this publication's binary package."""
 
131
        return bpph.binarypackagerelease.binarypackagename.name
 
132
 
 
133
    @staticmethod
 
134
    def getPackageRelease(bpph):
 
135
        """Return this publication's `BinaryPackageRelease`."""
 
136
        return bpph.binarypackagerelease
 
137
 
 
138
 
 
139
class GeneralizedPublication:
 
140
    """Generalize handling of publication records.
 
141
 
 
142
    This allows us to write code that can be dealing with either
 
143
    `SourcePackagePublishingHistory`s or `BinaryPackagePublishingHistory`s
 
144
    without caring which.  Differences are abstracted away in a traits
 
145
    class.
 
146
    """
 
147
    def __init__(self, is_source=True):
 
148
        if is_source:
 
149
            self.traits = SourcePublicationTraits
 
150
        else:
 
151
            self.traits = BinaryPublicationTraits
 
152
 
 
153
    def getPackageName(self, pub):
 
154
        """Get the package's name."""
 
155
        return self.traits.getPackageName(pub)
 
156
 
 
157
    def getPackageVersion(self, pub):
 
158
        """Obtain the version string for a publicaiton record."""
 
159
        return self.traits.getPackageRelease(pub).version
 
160
 
 
161
    def compare(self, pub1, pub2):
 
162
        """Compare publications by version.
 
163
 
 
164
        If both publications are for the same version, their creation dates
 
165
        break the tie.
 
166
        """
 
167
        version_comparison = apt_pkg.VersionCompare(
 
168
            self.getPackageVersion(pub1), self.getPackageVersion(pub2))
 
169
 
 
170
        if version_comparison == 0:
 
171
            # Use dates as tie breaker.
 
172
            return cmp(pub1.datecreated, pub2.datecreated)
 
173
        else:
 
174
            return version_comparison
101
175
 
102
176
 
103
177
class Dominator:
116
190
        self.logger = logger
117
191
        self.archive = archive
118
192
 
119
 
    def _dominatePublications(self, pubs):
 
193
    def dominatePackage(self, publications, live_versions, generalization):
 
194
        """Dominate publications for a single package.
 
195
 
 
196
        The latest publication for any version in `live_versions` stays
 
197
        active.  Any older publications (including older publications for
 
198
        live versions with multiple publications) are marked as superseded by
 
199
        the respective oldest live releases that are newer than the superseded
 
200
        ones.
 
201
 
 
202
        Any versions that are newer than anything in `live_versions` are
 
203
        marked as deleted.  This should not be possible in Soyuz-native
 
204
        archives, but it can happen during archive imports when the
 
205
        previous latest version of a package has disappeared from the Sources
 
206
        list we import.
 
207
 
 
208
        :param publications: Iterable of publications for the same package,
 
209
            in the same archive, series, and pocket, all with status
 
210
            `PackagePublishingStatus.PUBLISHED`.
 
211
        :param live_versions: Iterable of version strings that are still
 
212
            considered live for this package.  The given publications will
 
213
            remain active insofar as they represent any of these versions;
 
214
            older publications will be marked as superseded and newer ones
 
215
            as deleted.
 
216
        :param generalization: A `GeneralizedPublication` helper representing
 
217
            the kind of publications these are--source or binary.
 
218
        """
 
219
        # Go through publications from latest version to oldest.  This
 
220
        # makes it easy to figure out which release superseded which:
 
221
        # the dominant is always the oldest live release that is newer
 
222
        # than the one being superseded.
 
223
        publications = sorted(
 
224
            publications, cmp=generalization.compare, reverse=True)
 
225
 
 
226
        current_dominant = None
 
227
        dominant_version = None
 
228
 
 
229
        for pub in publications:
 
230
            version = generalization.getPackageVersion(pub)
 
231
            # There should never be two published releases with the same
 
232
            # version.  So this comparison is really a string
 
233
            # comparison, not a version comparison: if the versions are
 
234
            # equal by either measure, they're from the same release.
 
235
            if dominant_version is not None and version == dominant_version:
 
236
                # This publication is for a live version, but has been
 
237
                # superseded by a newer publication of the same version.
 
238
                # Supersede it.
 
239
                pub.supersede(current_dominant, logger=self.logger)
 
240
            elif version in live_versions:
 
241
                # This publication stays active; if any publications
 
242
                # that follow right after this are to be superseded,
 
243
                # this is the release that they are superseded by.
 
244
                current_dominant = pub
 
245
                dominant_version = version
 
246
            elif current_dominant is None:
 
247
                # This publication is no longer live, but there is no
 
248
                # newer version to supersede it either.  Therefore it
 
249
                # must be deleted.
 
250
                pub.requestDeletion(None)
 
251
            else:
 
252
                # This publication is superseded.  This is what we're
 
253
                # here to do.
 
254
                pub.supersede(current_dominant, logger=self.logger)
 
255
 
 
256
    def _dominatePublications(self, pubs, generalization):
120
257
        """Perform dominations for the given publications.
121
258
 
 
259
        Keep the latest published version for each package active,
 
260
        superseding older versions.
 
261
 
122
262
        :param pubs: A dict mapping names to a list of publications. Every
123
263
            publication must be PUBLISHED or PENDING, and the first in each
124
264
            list will be treated as dominant (so should be the latest).
 
265
        :param generalization: A `GeneralizedPublication` helper representing
 
266
            the kind of publications these are--source or binary.
125
267
        """
126
268
        self.logger.debug("Dominating packages...")
127
 
 
128
 
        for name in pubs.keys():
129
 
            assert pubs[name], (
130
 
                "Empty list of publications for %s" % name)
131
 
            for pubrec in pubs[name][1:]:
132
 
                pubrec.supersede(pubs[name][0], logger=self.logger)
133
 
 
134
 
    def _sortPackages(self, pkglist, is_source=True):
 
269
        for name, publications in pubs.iteritems():
 
270
            assert publications, "Empty list of publications for %s." % name
 
271
            # Since this always picks the latest version as the live
 
272
            # one, this dominatePackage call will never result in a
 
273
            # deletion.
 
274
            latest_version = generalization.getPackageVersion(publications[0])
 
275
            self.dominatePackage(
 
276
                publications, [latest_version], generalization)
 
277
 
 
278
    def _sortPackages(self, pkglist, generalization):
135
279
        """Map out packages by name, and sort by descending version.
136
280
 
137
281
        :param pkglist: An iterable of `SourcePackagePublishingHistory` or
138
282
            `BinaryPackagePublishingHistory`.
139
 
        :param is_source: Whether this call involves source package
140
 
            publications.  If so, work with `SourcePackagePublishingHistory`.
141
 
            If not, work with `BinaryPackagepublishingHistory`.
142
 
        :return: A dict mapping each package name (as UTF-8 encoded string)
143
 
            to a list of publications from `pkglist`, newest first.
 
283
        :param generalization: A `GeneralizedPublication` helper representing
 
284
            the kind of publications these are--source or binary.
 
285
        :return: A dict mapping each package name to a list of publications
 
286
            from `pkglist`, newest first.
144
287
        """
145
288
        self.logger.debug("Sorting packages...")
146
289
 
147
 
        if is_source:
148
 
            get_release = operator.attrgetter("sourcepackagerelease")
149
 
            get_name = operator.attrgetter("sourcepackagename")
150
 
        else:
151
 
            get_release = operator.attrgetter("binarypackagerelease")
152
 
            get_name = operator.attrgetter("binarypackagename")
153
 
 
154
290
        outpkgs = {}
155
291
        for inpkg in pkglist:
156
 
            key = get_name(get_release(inpkg)).name.encode('utf-8')
 
292
            key = generalization.getPackageName(inpkg)
157
293
            outpkgs.setdefault(key, []).append(inpkg)
158
294
 
159
 
        sort_order = functools.partial(
160
 
            _compare_packages_by_version_and_date, get_release)
161
295
        for package_pubs in outpkgs.itervalues():
162
 
            package_pubs.sort(cmp=sort_order, reverse=True)
 
296
            package_pubs.sort(cmp=generalization.compare, reverse=True)
163
297
 
164
298
        return outpkgs
165
299
 
287
421
        # Avoid circular imports.
288
422
        from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
289
423
 
 
424
        generalization = GeneralizedPublication(is_source=False)
 
425
 
290
426
        for distroarchseries in distroseries.architectures:
291
427
            self.logger.debug(
292
428
                "Performing domination across %s/%s (%s)",
312
448
                ),
313
449
                group_by=BinaryPackageName.id,
314
450
                having=Count(BinaryPackagePublishingHistory.id) > 1)
315
 
            binaries = IMasterStore(BinaryPackagePublishingHistory).find(
 
451
            binaries = IStore(BinaryPackagePublishingHistory).find(
316
452
                BinaryPackagePublishingHistory,
317
453
                BinaryPackageRelease.id ==
318
454
                    BinaryPackagePublishingHistory.binarypackagereleaseID,
322
458
                    BinaryPackageFormat.DDEB,
323
459
                bpph_location_clauses)
324
460
            self.logger.debug("Dominating binaries...")
325
 
            self._dominatePublications(self._sortPackages(binaries, False))
 
461
            self._dominatePublications(
 
462
                self._sortPackages(binaries, generalization), generalization)
 
463
 
 
464
    def _composeActiveSourcePubsCondition(self, distroseries, pocket):
 
465
        """Compose ORM condition for restricting relevant source pubs."""
 
466
        # Avoid circular imports.
 
467
        from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 
468
 
 
469
        return And(
 
470
            SourcePackagePublishingHistory.status ==
 
471
                PackagePublishingStatus.PUBLISHED,
 
472
            SourcePackagePublishingHistory.distroseries == distroseries,
 
473
            SourcePackagePublishingHistory.archive == self.archive,
 
474
            SourcePackagePublishingHistory.pocket == pocket,
 
475
            )
326
476
 
327
477
    def dominateSources(self, distroseries, pocket):
328
478
        """Perform domination on source package publications.
332
482
        """
333
483
        # Avoid circular imports.
334
484
        from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 
485
 
 
486
        generalization = GeneralizedPublication(is_source=True)
 
487
 
335
488
        self.logger.debug(
336
489
            "Performing domination across %s/%s (Source)",
337
490
            distroseries.name, pocket.title)
338
 
        spph_location_clauses = And(
339
 
            SourcePackagePublishingHistory.status ==
340
 
                PackagePublishingStatus.PUBLISHED,
341
 
            SourcePackagePublishingHistory.distroseries == distroseries,
342
 
            SourcePackagePublishingHistory.archive == self.archive,
343
 
            SourcePackagePublishingHistory.pocket == pocket,
344
 
            )
 
491
 
 
492
        spph_location_clauses = self._composeActiveSourcePubsCondition(
 
493
            distroseries, pocket)
 
494
        having_multiple_active_publications = (
 
495
            Count(SourcePackagePublishingHistory.id) > 1)
345
496
        candidate_source_names = Select(
346
497
            SourcePackageName.id,
347
 
            And(
348
 
                SourcePackageRelease.sourcepackagenameID ==
349
 
                    SourcePackageName.id,
350
 
                SourcePackagePublishingHistory.sourcepackagereleaseID ==
351
 
                    SourcePackageRelease.id,
352
 
                spph_location_clauses,
353
 
            ),
 
498
            And(join_spph_spr(), join_spr_spn(), spph_location_clauses),
354
499
            group_by=SourcePackageName.id,
355
 
            having=Count(SourcePackagePublishingHistory.id) > 1)
356
 
        sources = IMasterStore(SourcePackagePublishingHistory).find(
 
500
            having=having_multiple_active_publications)
 
501
        sources = IStore(SourcePackagePublishingHistory).find(
357
502
            SourcePackagePublishingHistory,
358
 
            SourcePackageRelease.id ==
359
 
                SourcePackagePublishingHistory.sourcepackagereleaseID,
 
503
            join_spph_spr(),
360
504
            SourcePackageRelease.sourcepackagenameID.is_in(
361
505
                candidate_source_names),
362
506
            spph_location_clauses)
 
507
 
363
508
        self.logger.debug("Dominating sources...")
364
 
        self._dominatePublications(self._sortPackages(sources))
 
509
        self._dominatePublications(
 
510
            self._sortPackages(sources, generalization), generalization)
365
511
        flush_database_updates()
366
512
 
 
513
    def findPublishedSourcePackageNames(self, distroseries, pocket):
 
514
        """Find names of currently published source packages."""
 
515
        result = IStore(SourcePackageName).find(
 
516
            SourcePackageName.name,
 
517
            join_spph_spr(),
 
518
            join_spr_spn(),
 
519
            self._composeActiveSourcePubsCondition(distroseries, pocket))
 
520
        return result.config(distinct=True)
 
521
 
 
522
    def findPublishedSPPHs(self, distroseries, pocket, package_name):
 
523
        """Find currently published source publications for given package."""
 
524
        # Avoid circular imports.
 
525
        from lp.soyuz.model.publishing import SourcePackagePublishingHistory
 
526
 
 
527
        return IStore(SourcePackagePublishingHistory).find(
 
528
            SourcePackagePublishingHistory,
 
529
            join_spph_spr(),
 
530
            join_spr_spn(),
 
531
            SourcePackageName.name == package_name,
 
532
            self._composeActiveSourcePubsCondition(distroseries, pocket))
 
533
 
 
534
    def dominateRemovedSourceVersions(self, distroseries, pocket,
 
535
                                      package_name, live_versions):
 
536
        """Dominate source publications based on a set of "live" versions.
 
537
 
 
538
        Active publications for the "live" versions will remain active.  All
 
539
        other active publications for the same package (and the same archive,
 
540
        distroseries, and pocket) are marked superseded.
 
541
 
 
542
        Unlike traditional domination, this allows multiple versions of a
 
543
        package to stay active in the same distroseries, archive, and pocket.
 
544
 
 
545
        :param distroseries: `DistroSeries` to dominate.
 
546
        :param pocket: `PackagePublishingPocket` to dominate.
 
547
        :param package_name: Source package name, as text.
 
548
        :param live_versions: Iterable of all version strings that are to
 
549
            remain active.
 
550
        """
 
551
        generalization = GeneralizedPublication(is_source=True)
 
552
        pubs = self.findPublishedSPPHs(distroseries, pocket, package_name)
 
553
        self.dominatePackage(pubs, live_versions, generalization)
 
554
 
367
555
    def judge(self, distroseries, pocket):
368
556
        """Judge superseded sources and binaries."""
369
557
        # Avoid circular imports.