← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/db-merge-stable-redux into lp:launchpad/db-devel

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/db-merge-stable-redux into lp:launchpad/db-devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/db-merge-stable-redux/+merge/87299

Merge stable at r14617.
-- 
https://code.launchpad.net/~stevenk/launchpad/db-merge-stable-redux/+merge/87299
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/db-merge-stable-redux into lp:launchpad/db-devel.
=== modified file 'lib/lp/buildmaster/interfaces/buildfarmjob.py'
--- lib/lp/buildmaster/interfaces/buildfarmjob.py	2011-12-24 16:54:44 +0000
+++ lib/lp/buildmaster/interfaces/buildfarmjob.py	2012-01-02 22:40:32 +0000
@@ -140,6 +140,13 @@
         has an entry associated with `job`.
         """
 
+    def getByJobs(jobs):
+        """Get the specific `IBuildFarmJob`s for the given `Job`s.
+
+        Invoked on the specific `IBuildFarmJob`-implementing class that
+        has entries associated with `job`s.
+        """
+
     def generateSlaveBuildCookie():
         """Produce a cookie for the slave as a token of the job it's doing.
 

=== modified file 'lib/lp/buildmaster/model/buildfarmjob.py'
--- lib/lp/buildmaster/model/buildfarmjob.py	2011-12-30 06:14:56 +0000
+++ lib/lp/buildmaster/model/buildfarmjob.py	2012-01-02 22:40:32 +0000
@@ -100,6 +100,10 @@
         """See `IBuildFarmJobOld`."""
         raise NotImplementedError
 
+    def getByJobs(self, job):
+        """See `IBuildFarmJobOld`."""
+        raise NotImplementedError
+
     def jobStarted(self):
         """See `IBuildFarmJobOld`."""
         pass
@@ -160,12 +164,28 @@
         """
         raise NotImplementedError
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        """Preload the build farm jobs to which the given jobs will delegate.
+
+        """
+        pass
+
     @classmethod
     def getByJob(cls, job):
         """See `IBuildFarmJobOld`."""
         store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
         return store.find(cls, cls.job == job).one()
 
+    @classmethod
+    def getByJobs(cls, jobs):
+        """See `IBuildFarmJobOld`.
+        """
+        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+        job_ids = [job.id for job in jobs]
+        return store.find(
+            cls, cls.job_id.is_in(job_ids))
+
     def generateSlaveBuildCookie(self):
         """See `IBuildFarmJobOld`."""
         buildqueue = getUtility(IBuildQueueSet).getByJob(self.job)

=== modified file 'lib/lp/buildmaster/model/buildqueue.py'
--- lib/lp/buildmaster/model/buildqueue.py	2011-12-30 06:14:56 +0000
+++ lib/lp/buildmaster/model/buildqueue.py	2012-01-02 22:40:32 +0000
@@ -16,7 +16,9 @@
     datetime,
     timedelta,
     )
+from itertools import groupby
 import logging
+from operator import attrgetter
 
 import pytz
 from sqlobject import (
@@ -142,6 +144,21 @@
         specific_class = specific_job_classes()[self.job_type]
         return specific_class.getByJob(self.job)
 
+    @staticmethod
+    def preloadSpecificJobData(queues):
+        key = attrgetter('job_type')
+        for job_type, grouped_queues in groupby(queues, key=key):
+            specific_class = specific_job_classes()[job_type]
+            queue_subset = list(grouped_queues)
+            # We need to preload the build farm jobs early to avoid
+            # the call to _set_build_farm_job to look up BuildFarmBuildJobs
+            # one by one.
+            specific_class.preloadBuildFarmJobs(queue_subset)
+            specific_jobs = specific_class.getByJobs(queue_subset)
+            if len(list(specific_jobs)) == 0:
+                continue
+            specific_class.preloadJobsData(specific_jobs)
+
     @property
     def date_started(self):
         """See `IBuildQueue`."""

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2011-12-30 06:14:56 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2012-01-02 22:40:32 +0000
@@ -287,6 +287,18 @@
             PackageBuild.build_farm_job_id == build_farm_job.id).one()
 
     @classmethod
+    def preloadBuildsData(cls, builds):
+        # Circular imports.
+        from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
+        package_builds = load_related(
+            PackageBuild, builds, ['package_build_id'])
+        archives = load_related(Archive, package_builds, ['archive_id'])
+        load_related(Person, archives, ['ownerID'])
+        sprs = load_related(
+            SourcePackageRecipe, builds, ['recipe_id'])
+        SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs)
+
+    @classmethod
     def getByBuildFarmJobs(cls, build_farm_jobs):
         """See `ISpecificBuildFarmJobSource`."""
         if len(build_farm_jobs) == 0:
@@ -294,20 +306,11 @@
         build_farm_job_ids = [
             build_farm_job.id for build_farm_job in build_farm_jobs]
 
-        def eager_load(rows):
-            # Circular imports.
-            from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
-            package_builds = load_related(
-                PackageBuild, rows, ['package_build_id'])
-            archives = load_related(Archive, package_builds, ['archive_id'])
-            load_related(Person, archives, ['ownerID'])
-            sprs = load_related(
-                SourcePackageRecipe, rows, ['recipe_id'])
-            SourcePackageRecipe.preLoadDataForSourcePackageRecipes(sprs)
         resultset = Store.of(build_farm_jobs[0]).find(cls,
             cls.package_build_id == PackageBuild.id,
             PackageBuild.build_farm_job_id.is_in(build_farm_job_ids))
-        return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
+        return DecoratedResultSet(
+            resultset, pre_iter_hook=cls.preloadBuildsData)
 
     @classmethod
     def getRecentBuilds(cls, requester, recipe, distroseries, _now=None):
@@ -428,6 +431,24 @@
         We override this to provide a delegate specific to package builds."""
         self.build_farm_job = BuildFarmBuildJob(self.build)
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        from lp.code.model.sourcepackagerecipebuild import (
+            SourcePackageRecipeBuild,
+            )
+        return list(IStore(SourcePackageRecipeBuildJob).find(
+            SourcePackageRecipeBuild,
+            [SourcePackageRecipeBuildJob.id.is_in([job.id for job in jobs]),
+             SourcePackageRecipeBuildJob.build_id ==
+                 SourcePackageRecipeBuild.id]))
+
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        load_related(Job, jobs, ['job_id'])
+        builds = load_related(
+            SourcePackageRecipeBuild, jobs, ['build_id'])
+        SourcePackageRecipeBuild.preloadBuildsData(builds)
+
     @classmethod
     def new(cls, build, job):
         """See `ISourcePackageRecipeBuildJobSource`."""

=== renamed directory 'lib/lp/services/session/stories' => 'lib/lp/services/feeds/stories'
=== modified file 'lib/lp/soyuz/browser/builder.py'
--- lib/lp/soyuz/browser/builder.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/browser/builder.py	2012-01-02 22:40:32 +0000
@@ -41,6 +41,9 @@
     IBuilder,
     IBuilderSet,
     )
+from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.database.lpstorm import IStore
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
     ApplicationMenu,
@@ -144,7 +147,16 @@
     @cachedproperty
     def builders(self):
         """All active builders"""
-        return list(self.context.getBuilders())
+        def do_eager_load(builders):
+            # Prefetch the jobs' data.
+            queues = IStore(BuildQueue).find(
+                BuildQueue,
+                BuildQueue.builderID.is_in(
+                    builder.id for builder in builders))
+            BuildQueue.preloadSpecificJobData(queues)
+
+        return list(DecoratedResultSet(
+            list(self.context.getBuilders()), pre_iter_hook=do_eager_load))
 
     @property
     def number_of_registered_builders(self):

=== modified file 'lib/lp/soyuz/browser/tests/test_builder.py'
--- lib/lp/soyuz/browser/tests/test_builder.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/browser/tests/test_builder.py	2012-01-02 22:40:32 +0000
@@ -1,14 +1,32 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the lp.soyuz.browser.builder module."""
 
 __metaclass__ = type
 
+from testtools.matchers import LessThan
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.buildmaster.interfaces.builder import IBuilderSet
+from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
 from lp.services.webapp import canonical_url
-from lp.testing import TestCaseWithFactory
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.soyuz.browser.tests.test_builder_views import BuildCreationMixin
+from lp.testing import (
+    record_two_runs,
+    TestCaseWithFactory,
+    )
+from lp.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
+from lp.testing.matchers import HasQueryCount
 from lp.testing.publication import test_traverse
+from lp.testing.views import create_initialized_view
+from lp.translations.interfaces.translationtemplatesbuildjob import (
+    ITranslationTemplatesBuildJobSource,
+    )
 
 
 class TestBuildersNavigation(TestCaseWithFactory):
@@ -38,3 +56,67 @@
         self.assertEqual(
             canonical_url(build),
             request.response.getHeader('location'))
+
+
+def builders_homepage_render():
+    builders = getUtility(IBuilderSet)
+    create_initialized_view(builders, "+index").render()
+
+
+class TestBuildersHomepage(TestCaseWithFactory, BuildCreationMixin):
+
+    layer = LaunchpadFunctionalLayer
+
+    # XXX rvb: the 3 additional queries per build are the result of the calls
+    # to:
+    # - builder.currentjob
+    # - buildqueue.specific_job
+    # These could be converted into cachedproperty and pre-populated in
+    # bulk but several tests assert that the value returned by these
+    # these properties are up to date.  Since they are not really expensive
+    # to compute I'll leave them as regular properties for now.
+
+    def test_builders_binary_package_build_query_count(self):
+        def create_build():
+            build = self.createBinaryPackageBuild()
+            queue = build.queueBuild()
+            queue.markAsBuilding(build.builder)
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(
+            recorder2,
+            HasQueryCount(LessThan(recorder1.count + 3 * 2 + 1)))
+
+    def test_builders_recipe_build_query_count(self):
+        def create_build():
+            build = self.createRecipeBuildWithBuilder()
+            queue = build.queueBuild()
+            queue.markAsBuilding(build.builder)
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(
+            recorder2,
+            HasQueryCount(LessThan(recorder1.count + 3 * 2 + 1)))
+
+    def test_builders_translation_template_build_query_count(self):
+        def create_build():
+            jobset = getUtility(ITranslationTemplatesBuildJobSource)
+            branch = self.factory.makeBranch()
+            specific_job = jobset.create(branch)
+            queueset = getUtility(IBuildQueueSet)
+            # Using rSP is required to get the job id.
+            naked_job = removeSecurityProxy(specific_job.job)
+            job_id = naked_job.id
+            queue = queueset.get(job_id)
+            queue.markAsBuilding(self.factory.makeBuilder())
+
+        recorder1, recorder2 = record_two_runs(
+            builders_homepage_render, create_build, 2)
+
+        self.assertThat(
+            recorder2,
+            HasQueryCount(LessThan(recorder1.count + 3 * 2 + 1)))

=== modified file 'lib/lp/soyuz/browser/tests/test_builder_views.py'
--- lib/lp/soyuz/browser/tests/test_builder_views.py	2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/browser/tests/test_builder_views.py	2012-01-02 22:40:32 +0000
@@ -14,10 +14,7 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
-from lp.buildmaster.enums import (
-    BuildFarmJobType,
-    BuildStatus,
-    )
+from lp.buildmaster.enums import BuildFarmJobType
 from lp.buildmaster.interfaces.buildfarmjob import (
     IBuildFarmJobSource,
     InconsistentBuildFarmJobError,
@@ -159,28 +156,25 @@
             getSpecificJobs, [build_farm_job])
 
 
-class TestBuilderHistoryView(TestCaseWithFactory):
-
-    layer = LaunchpadFunctionalLayer
-
-    nb_objects = 2
-
-    def setUp(self):
-        super(TestBuilderHistoryView, self).setUp()
-        self.builder = self.factory.makeBuilder()
-
-    def createTranslationTemplateBuildWithBuilder(self):
+class BuildCreationMixin(object):
+
+    def createTranslationTemplateBuildWithBuilder(self, builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         build_farm_job_source = getUtility(IBuildFarmJobSource)
         build_farm_job = build_farm_job_source.new(
             BuildFarmJobType.TRANSLATIONTEMPLATESBUILD)
         source = getUtility(ITranslationTemplatesBuildSource)
         branch = self.factory.makeBranch()
         build = source.create(build_farm_job, branch)
-        removeSecurityProxy(build).builder = self.builder
+        removeSecurityProxy(build).builder = builder
         self.addFakeBuildLog(build)
         return build
 
-    def createRecipeBuildWithBuilder(self, private_branch=False):
+    def createRecipeBuildWithBuilder(self, private_branch=False,
+                                     builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         branch2 = self.factory.makeAnyBranch()
         branch1 = self.factory.makeAnyBranch()
         build = self.factory.makeSourcePackageRecipeBuild(
@@ -191,7 +185,7 @@
                 branch1.setPrivate(
                     True, getUtility(IPersonSet).getByEmail(ADMIN_EMAIL))
         Store.of(build).flush()
-        removeSecurityProxy(build).builder = self.builder
+        removeSecurityProxy(build).builder = builder
         self.addFakeBuildLog(build)
         return build
 
@@ -201,19 +195,31 @@
         import transaction
         transaction.commit()
 
-    def createBinaryPackageBuild(self, in_ppa=False):
+    def createBinaryPackageBuild(self, in_ppa=False, builder=None):
+        if builder is None:
+            builder = self.factory.makeBuilder()
         archive = None
         if in_ppa:
             archive = self.factory.makeArchive()
-        build = self.factory.makeBinaryPackageBuild(
-            archive=archive, status=BuildStatus.FULLYBUILT)
+        build = self.factory.makeBinaryPackageBuild(archive=archive)
         naked_build = removeSecurityProxy(build)
-        naked_build.builder = self.builder
+        naked_build.builder = builder
         naked_build.date_started = self.factory.getUniqueDate()
         naked_build.date_finished = self.factory.getUniqueDate()
         self.addFakeBuildLog(build)
         return build
 
+
+class TestBuilderHistoryView(TestCaseWithFactory, BuildCreationMixin):
+
+    layer = LaunchpadFunctionalLayer
+
+    nb_objects = 2
+
+    def setUp(self):
+        super(TestBuilderHistoryView, self).setUp()
+        self.builder = self.factory.makeBuilder()
+
     def test_build_history_queries_count_view_recipe_builds(self):
         # The builder's history view creation (i.e. the call to
         # view.setupBuildList) issues a constant number of queries
@@ -221,7 +227,8 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
-            builder_history_render, self.createRecipeBuildWithBuilder,
+            builder_history_render,
+            partial(self.createRecipeBuildWithBuilder, builder=self.builder),
             self.nb_objects)
 
         # XXX: rvb 2011-11-14 bug=890326: The only query remaining is the
@@ -237,7 +244,8 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
-            builder_history_render, self.createBinaryPackageBuild,
+            builder_history_render,
+            partial(self.createBinaryPackageBuild, builder=self.builder),
             self.nb_objects)
 
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
@@ -248,7 +256,7 @@
         def builder_history_render():
             create_initialized_view(self.builder, '+history').render()
         createBinaryPackageBuildInPPA = partial(
-            self.createBinaryPackageBuild, in_ppa=True)
+            self.createBinaryPackageBuild, in_ppa=True, builder=self.builder)
         recorder1, recorder2 = record_two_runs(
             builder_history_render, createBinaryPackageBuildInPPA,
             self.nb_objects)
@@ -262,21 +270,26 @@
             create_initialized_view(self.builder, '+history').render()
         recorder1, recorder2 = record_two_runs(
             builder_history_render,
-            self.createTranslationTemplateBuildWithBuilder, self.nb_objects)
+            partial(
+                self.createTranslationTemplateBuildWithBuilder,
+                builder=self.builder),
+            self.nb_objects)
 
         self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
 
     def test_build_history_private_build_view(self):
-        self.createRecipeBuildWithBuilder()
-        self.createRecipeBuildWithBuilder(private_branch=True)
+        self.createRecipeBuildWithBuilder(builder=self.builder)
+        self.createRecipeBuildWithBuilder(
+            private_branch=True, builder=self.builder)
         view = create_initialized_view(self.builder, '+history')
         view.setupBuildList()
 
         self.assertIn(None, view.complete_builds)
 
     def test_build_history_private_build_display(self):
-        self.createRecipeBuildWithBuilder()
-        self.createRecipeBuildWithBuilder(private_branch=True)
+        self.createRecipeBuildWithBuilder(builder=self.builder)
+        self.createRecipeBuildWithBuilder(
+            private_branch=True, builder=self.builder)
         view = create_initialized_view(self.builder, '+history')
         private_build_icon_matcher = soupmatchers.HTMLContains(
             soupmatchers.Tag(

=== modified file 'lib/lp/soyuz/interfaces/binarypackagebuild.py'
--- lib/lp/soyuz/interfaces/binarypackagebuild.py	2011-12-24 16:54:44 +0000
+++ lib/lp/soyuz/interfaces/binarypackagebuild.py	2012-01-02 22:40:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -426,6 +426,11 @@
         asserted to not be None/empty.
         """
 
+    def preloadBuildsData(builds):
+        """Prefetch the data related to the builds.
+
+        """
+
 
 class IBuildRescoreForm(Interface):
     """Form for rescoring a build."""

=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py	2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py	2012-01-02 22:40:32 +0000
@@ -865,6 +865,31 @@
             return None
         return resulting_tuple[0]
 
+    def preloadBuildsData(self, builds):
+        # Circular imports.
+        from lp.soyuz.model.distroarchseries import (
+            DistroArchSeries
+            )
+        from lp.registry.model.distroseries import (
+            DistroSeries
+            )
+        from lp.registry.model.distribution import (
+            Distribution
+            )
+        from lp.soyuz.model.archive import Archive
+        from lp.registry.model.person import Person
+        self._prefetchBuildData(builds)
+        distro_arch_series = load_related(
+            DistroArchSeries, builds, ['distro_arch_series_id'])
+        package_builds = load_related(
+            PackageBuild, builds, ['package_build_id'])
+        archives = load_related(Archive, package_builds, ['archive_id'])
+        load_related(Person, archives, ['ownerID'])
+        distroseries = load_related(
+            DistroSeries, distro_arch_series, ['distroseriesID'])
+        load_related(
+            Distribution, distroseries, ['distributionID'])
+
     def getByBuildFarmJobs(self, build_farm_jobs):
         """See `ISpecificBuildFarmJobSource`."""
         if len(build_farm_jobs) == 0:
@@ -873,36 +898,13 @@
         build_farm_job_ids = [
             build_farm_job.id for build_farm_job in build_farm_jobs]
 
-        def eager_load(rows):
-            # Circular imports.
-            from lp.soyuz.model.distroarchseries import (
-                DistroArchSeries
-                )
-            from lp.registry.model.distroseries import (
-                DistroSeries
-                )
-            from lp.registry.model.distribution import (
-                Distribution
-                )
-            from lp.soyuz.model.archive import Archive
-            from lp.registry.model.person import Person
-            self._prefetchBuildData(rows)
-            distro_arch_series = load_related(
-                DistroArchSeries, rows, ['distro_arch_series_id'])
-            package_builds = load_related(
-                PackageBuild, rows, ['package_build_id'])
-            archives = load_related(Archive, package_builds, ['archive_id'])
-            load_related(Person, archives, ['ownerID'])
-            distroseries = load_related(
-                DistroSeries, distro_arch_series, ['distroseriesID'])
-            load_related(
-                Distribution, distroseries, ['distributionID'])
         resultset = Store.of(build_farm_jobs[0]).using(*clause_tables).find(
             BinaryPackageBuild,
             BinaryPackageBuild.package_build == PackageBuild.id,
             PackageBuild.build_farm_job == BuildFarmJob.id,
             BuildFarmJob.id.is_in(build_farm_job_ids))
-        return DecoratedResultSet(resultset, pre_iter_hook=eager_load)
+        return DecoratedResultSet(
+            resultset, pre_iter_hook=self.preloadBuildsData)
 
     def getPendingBuildsForArchSet(self, archseries):
         """See `IBinaryPackageBuildSet`."""

=== modified file 'lib/lp/soyuz/model/buildpackagejob.py'
--- lib/lp/soyuz/model/buildpackagejob.py	2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/model/buildpackagejob.py	2012-01-02 22:40:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -22,6 +22,8 @@
 from lp.buildmaster.interfaces.builder import IBuilderSet
 from lp.buildmaster.model.buildfarmjob import BuildFarmJobOldDerived
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.database.bulk import load_related
+from lp.services.database.lpstorm import IStore
 from lp.services.database.sqlbase import sqlvalues
 from lp.soyuz.enums import (
     ArchivePurpose,
@@ -62,6 +64,14 @@
         We override this to provide a delegate specific to package builds."""
         self.build_farm_job = BuildFarmBuildJob(self.build)
 
+    @staticmethod
+    def preloadBuildFarmJobs(jobs):
+        from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+        return list(IStore(BinaryPackageBuild).find(
+            BinaryPackageBuild,
+            [BuildPackageJob.id.is_in([job.id for job in jobs]),
+             BuildPackageJob.build_id == BinaryPackageBuild.id]))
+
     def score(self):
         """See `IBuildPackageJob`."""
         # Define a table we'll use to calculate the score based on the time
@@ -156,6 +166,14 @@
         """See `IBuildFarmJob`."""
         return self.build.is_virtualized
 
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
+        from lp.services.job.model.job import Job
+        load_related(Job, jobs, ['job_id'])
+        builds = load_related(BinaryPackageBuild, jobs, ['build_id'])
+        getUtility(IBinaryPackageBuildSet).preloadBuildsData(list(builds))
+
     @staticmethod
     def addCandidateSelectionCriteria(processor, virtualized):
         """See `IBuildFarmJob`."""

=== modified file 'lib/lp/testing/tests/test_layers_functional.py'
--- lib/lp/testing/tests/test_layers_functional.py	2011-12-31 00:35:01 +0000
+++ lib/lp/testing/tests/test_layers_functional.py	2012-01-02 22:40:32 +0000
@@ -256,7 +256,7 @@
 
     def xxxtestMemcachedWorking(self):
         # XXX sinzui 2011-12-27 bug=729062: Disabled because lucid_db_lp
-        # reports memcached did not die.
+        # reports memcached did not die.(self):
         client = MemcachedLayer.client or memcache_client_factory()
         key = "BaseTestCase.testMemcachedWorking"
         client.forget_dead_hosts()

=== modified file 'lib/lp/translations/model/translationtemplatesbuildjob.py'
--- lib/lp/translations/model/translationtemplatesbuildjob.py	2012-01-01 02:58:52 +0000
+++ lib/lp/translations/model/translationtemplatesbuildjob.py	2012-01-02 22:40:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -36,6 +36,7 @@
     BranchJobType,
     )
 from lp.services.config import config
+from lp.services.database.bulk import load_related
 from lp.services.database.lpstorm import (
     IMasterStore,
     IStore,
@@ -226,6 +227,31 @@
             return cls(branch_job)
 
     @classmethod
+    def getByJobs(cls, jobs):
+        """See `IBuildFarmJob`.
+
+        Overridden here to search via a BranchJob, rather than a Job.
+        """
+        store = IStore(BranchJob)
+        job_ids = [job.id for job in jobs]
+        branch_jobs = store.find(
+            BranchJob, BranchJob.jobID.is_in(job_ids))
+        return [cls(branch_job) for branch_job in branch_jobs]
+
+    @classmethod
+    def preloadJobsData(cls, jobs):
+        # Circular imports.
+        from lp.code.model.branch import Branch
+        from lp.registry.model.product import Product
+        from lp.code.model.branchcollection import GenericBranchCollection
+        from lp.services.job.model.job import Job
+        contexts = [job.context for job in jobs]
+        load_related(Job, contexts, ['jobID'])
+        branches = load_related(Branch, contexts, ['branchID'])
+        GenericBranchCollection.preloadDataForBranches(branches)
+        load_related(Product, branches, ['productID'])
+
+    @classmethod
     def getByBranch(cls, branch):
         """See `ITranslationTemplatesBuildJobSource`."""
         store = IStore(BranchJob)