← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~ursinha/launchpad/convert-translationuploads-to-job into lp:launchpad

 

Ursula Junque has proposed merging lp:~ursinha/launchpad/convert-translationuploads-to-job into lp:launchpad with lp:~stevenk/launchpad/packagediff-job as a prerequisite.

Commit message:
publishRosettaTranslations now fires a job that uploads and attaches the translation files, to stop slowing down the publisher

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ursinha/launchpad/convert-translationuploads-to-job/+merge/176415

This branch creates a TranslationsUploadJob that is responsible for uploading and attaching translation files to a sourcepackagerelease. Now, whenever a PackageUploadCustom of format ROSETTA_TRANSLATIONS is processed in the publisher queue, a TranslationsUploadJob is created and the publisher can move along.
-- 
https://code.launchpad.net/~ursinha/launchpad/convert-translationuploads-to-job/+merge/176415
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~ursinha/launchpad/convert-translationuploads-to-job into lp:launchpad.
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2013-07-23 15:53:27 +0000
+++ lib/lp/services/config/schema-lazr.conf	2013-07-23 15:53:28 +0000
@@ -1743,6 +1743,10 @@
 module: lp.translations.interfaces.translationpackagingjob
 dbuser: rosettaadmin
 
+[ITranslationsUploadJobSource]
+module: lp.soyuz.interfaces.translationsuploadjob
+dbuser: process_accepted
+
 [IPersonMergeJobSource]
 module: lp.registry.interfaces.persontransferjob
 dbuser: person-merge-job

=== modified file 'lib/lp/services/job/interfaces/job.py'
--- lib/lp/services/job/interfaces/job.py	2013-07-23 15:53:27 +0000
+++ lib/lp/services/job/interfaces/job.py	2013-07-23 15:53:28 +0000
@@ -78,6 +78,13 @@
         Job to generate the diff between two SourcePackageReleases.
         """)
 
+    UPLOAD_TRANSLATIONS_FILES = DBItem(1, """
+        Upload Translations Files
+
+        Job to upload translations files and attach them to a
+        SourcePackageRelease.
+        """)
+
 
 class IJob(Interface):
     """Basic attributes of a job."""

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2013-07-23 15:53:27 +0000
+++ lib/lp/soyuz/configure.zcml	2013-07-23 15:53:28 +0000
@@ -995,6 +995,18 @@
       <allow interface=".interfaces.packagediffjob.IPackageDiffJob" />
     </class>
 
+    <!-- TranslationsUploadJobSource -->
+    <securedutility
+      component=".model.translationsuploadjob.TranslationsUploadJob"
+      provides=".interfaces.translationsuploadjob.ITranslationsUploadJobSource">
+      <allow interface=".interfaces.translationsuploadjob.ITranslationsUploadJobSource" />
+    </securedutility>
+
+    <!-- TranslationsUploadJob -->
+    <class class=".model.translationsuploadjob.TranslationsUploadJob">
+      <allow
+      interface=".interfaces.translationsuploadjob.ITranslationsUploadJob" />
+    </class>
     <webservice:register module="lp.soyuz.interfaces.webservice" />
 
 </configure>

=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-translations.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-translations.txt	2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-translations.txt	2013-07-23 15:53:28 +0000
@@ -31,6 +31,8 @@
     >>> from lp.services.database.constants import UTC_NOW
     >>> from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
 
+    >>> from lp.soyuz.model.translationsuploadjob import TranslationsUploadJob
+
 # Login as an admin.
     >>> login('foo.bar@xxxxxxxxxxxxx')
 
@@ -157,6 +159,15 @@
     ...     status=PackageUploadStatus.NEW)[0]
     >>> queue_item.customfiles[0].publish()
 
+When publish() runs, it creates a TranslationsUploadJob that will effectively
+process the translation files. We need to find and run it to be able to
+verify the imported files.
+    >>> def runPendingTranslationUploadJob():
+    ...     job = list(TranslationsUploadJob.iterReady())[0]
+    ...     job.run()
+
+    >>> runPendingTranslationUploadJob()
+
 As we can see from the translation import queue content.
 
     >>> for entry in translation_import_queue.getAllEntries(target=ubuntu):
@@ -206,6 +217,7 @@
     >>> queue_item = dapper.getPackageUploads(PackageUploadStatus.NEW)[0]
     >>> queue_item.pocket = PackagePublishingPocket.UPDATES
     >>> queue_item.customfiles[0].publish()
+    >>> runPendingTranslationUploadJob()
 
 As we can see from the translation import queue content.
 
@@ -239,6 +251,7 @@
     >>> queue_item.builds[0].build.source_package_release.override(
     ...     component=restricted_component)
     >>> queue_item.customfiles[0].publish()
+    >>> runPendingTranslationUploadJob()
 
 As we can see from the translation import queue content.
 
@@ -340,154 +353,6 @@
     >>> translation_import_queue.getAllEntries(target=ubuntu).count()
     0
 
-
-Translations importer: publishRosettaTranslations
--------------------------------------------------
-
-We create mock objects for SourcePackageRelease, PackageUpload and
-PackageUploadCustom: these will emulate everything we need to document
-different interpretations of "importer" in attachTranslationFiles.
-
-    >>> from zope.interface import implements
-    >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-    >>> from lp.soyuz.model.queue import PackageUploadCustom
-    >>> from lp.soyuz.interfaces.archive import (
-    ...     IArchive, ArchivePurpose)
-    >>> from lp.soyuz.interfaces.queue import (
-    ...     IPackageUpload, IPackageUploadCustom)
-    >>> from lp.registry.interfaces.person import IPerson
-    >>> from lp.soyuz.enums import PackageUploadCustomFormat
-    >>> from lp.soyuz.interfaces.component import IComponentSet
-    >>> from lp.soyuz.interfaces.sourcepackagerelease import (
-    ...     ISourcePackageRelease)
-    >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
-
-    >>> class MockArchive:
-    ...       implements(IArchive)
-    ...       def __init__(self, purpose):
-    ...           self.purpose = purpose
-
-    >>> class MockDistroSeries:
-    ...     implements(IDistroSeries)
-    ...     def __init__(self, version):
-    ...         self.version = version
-
-    >>> class MockSourcePackageRelease:
-    ...       implements(ISourcePackageRelease)
-    ...       def __init__(self, component, creator, upload_distroseries):
-    ...           self.component = getUtility(IComponentSet)[component]
-    ...           self.upload_distroseries = upload_distroseries
-    ...           self.creator = creator
-    ...           self.packageupload = 1
-    ...
-    ...       def attachTranslationFiles(self, file, imported, importer):
-    ...           if (importer is not None and
-    ...               not IPerson.providedBy(importer)):
-    ...               print "`importer` not a person!"
-    ...           print "Imported by: %s" % (
-    ...               getattr(importer, "name", "None"))
-
-    >>> class MockPackageUpload:
-    ...     implements(IPackageUpload)
-    ...     def __init__(self, pocket, auto_sync, sourcepackagerelease,
-    ...                  archive):
-    ...         self.id = 1
-    ...         self.pocket = pocket
-    ...         self.auto_sync = auto_sync
-    ...         self.sourcepackagerelease = sourcepackagerelease
-    ...         self.archive = archive
-    ...
-    ...     def isAutoSyncUpload(self, changed_by_email=None):
-    ...         return self.auto_sync
-
-    >>> class MockPackageUploadCustom(PackageUploadCustom):
-    ...     implements(IPackageUploadCustom)
-    ...     packageupload = None
-    ...
-    ...     def __init__(self):
-    ...         self.customformat = (
-    ...             PackageUploadCustomFormat.ROSETTA_TRANSLATIONS)
-
-For translations from auto-synced packages we consider the importer to be
-'katie' (archive@xxxxxxxxxx).
-
-    >>> katie = getUtility(ILaunchpadCelebrities).katie
-    >>> release_pocket = PackagePublishingPocket.RELEASE
-    >>> archive = MockArchive(ArchivePurpose.PRIMARY)
-
-    >>> distro_series = MockDistroSeries(u'9.04')
-    >>> katie_sourcepackagerelease = MockSourcePackageRelease(
-    ...     'main', katie, distro_series)
-    >>> sync_package_upload = MockPackageUpload(
-    ...     release_pocket, True, katie_sourcepackagerelease, archive)
-    >>> sync_package_upload.isAutoSyncUpload()
-    True
-    >>> translations_upload = MockPackageUploadCustom()
-    >>> translations_upload.packageupload = sync_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-    Imported by: katie
-
-Non-auto-sync uploads by 'katie' still indicate 'katie' as the uploader.
-
-    >>> non_sync_package_upload = MockPackageUpload(
-    ...     release_pocket, False, katie_sourcepackagerelease, archive)
-    >>> non_sync_package_upload.isAutoSyncUpload()
-    False
-    >>> translations_upload.packageupload = non_sync_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-    Imported by: katie
-
-Uploads by anyone else are treated as if importer is the packager.
-
-    >>> person_set = getUtility(IPersonSet)
-    >>> carlos = person_set.getByName('carlos')
-    >>> carlos_sourcepackagerelease = MockSourcePackageRelease(
-    ...     'main', carlos, distro_series)
-    >>> carlos_package_upload = MockPackageUpload(
-    ...     release_pocket, False, carlos_sourcepackagerelease, archive)
-    >>> carlos_package_upload.isAutoSyncUpload()
-    False
-    >>> translations_upload.packageupload = carlos_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-    Imported by: carlos
-
-Uploads for distroseries before Oneiric or later may not be targeted
-to any component but 'main' and 'restricted'.  The upload attempt is ignored.
-
-    >>> katie_sourcepackagerelease = MockSourcePackageRelease(
-    ...     'universe', katie, distro_series)
-    >>> sync_package_upload = MockPackageUpload(
-    ...     release_pocket, True, katie_sourcepackagerelease, archive)
-    >>> translations_upload = MockPackageUploadCustom()
-    >>> translations_upload.packageupload = sync_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-
-For Oneiric the import succeeds for 'universe'.
-
-    >>> distro_series = MockDistroSeries(u'11.10')
-    >>> katie_sourcepackagerelease = MockSourcePackageRelease(
-    ...     'universe', katie, distro_series)
-    >>> sync_package_upload = MockPackageUpload(
-    ...     release_pocket, True, katie_sourcepackagerelease, archive)
-    >>> translations_upload = MockPackageUploadCustom()
-    >>> translations_upload.packageupload = sync_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-    Imported by: katie
-
-And for the 12.04 release the import succeeds for 'universe'.
-
-    >>> distro_series = MockDistroSeries(u'12.04')
-    >>> katie_sourcepackagerelease = MockSourcePackageRelease(
-    ...     'universe', katie, distro_series)
-    >>> sync_package_upload = MockPackageUpload(
-    ...     release_pocket, True, katie_sourcepackagerelease, archive)
-    >>> translations_upload = MockPackageUploadCustom()
-    >>> translations_upload.packageupload = sync_package_upload
-    >>> translations_upload.publishRosettaTranslations()
-    Imported by: katie
-
-
-
 Translations tarball
 ~~~~~~~~~~~~~~~~~~~~
 

=== added file 'lib/lp/soyuz/interfaces/translationsuploadjob.py'
--- lib/lp/soyuz/interfaces/translationsuploadjob.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/translationsuploadjob.py	2013-07-23 15:53:28 +0000
@@ -0,0 +1,31 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+    "ITranslationsUploadJob",
+    "ITranslationsUploadJobSource",
+    ]
+
+from lp.services.job.interfaces.job import (
+    IJobSource,
+    IRunnableJob,
+    )
+
+
+class ITranslationsUploadJobSource(IJobSource):
+    """An interface for acquiring ITranslationsUploadJob."""
+
+    def create(sourcepackagerelease, libraryfilealias):
+        """Create new translations upload job for a source package release."""
+
+    def get(sourcepackagerelease, libraryfilealias):
+        """Retrieve the translation's upload job for a source package release.
+
+        :return: `None` or an `ITranslationsUploadJob`.
+        """ 
+
+
+class ITranslationsUploadJob(IRunnableJob):
+    """A `Job` that uploads and attaches files to a `ISourcePackageRelease`."""

=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py	2013-07-16 08:10:32 +0000
+++ lib/lp/soyuz/model/queue.py	2013-07-23 15:53:28 +0000
@@ -103,6 +103,9 @@
     IPublishingSet,
     name_priority_map,
     )
+from lp.soyuz.interfaces.translationsuploadjob import (
+    ITranslationsUploadJobSource,
+    )
 from lp.soyuz.interfaces.queue import (
     IPackageUpload,
     IPackageUploadBuild,
@@ -1451,19 +1454,10 @@
             # Ubuntu's MOTU told us that they are not able to handle
             # translations like we do in main. We are going to import only
             # packages in main.
-            return
-
-        # Set the importer to package creator.
-        importer = sourcepackagerelease.creator
-
-        # Attach the translation tarball. It's always published.
-        try:
-            sourcepackagerelease.attachTranslationFiles(
-                self.libraryfilealias, True, importer=importer)
-        except DownloadFailed:
-            if logger is not None:
-                debug(logger, "Unable to fetch %s to import it into Rosetta" %
-                    self.libraryfilealias.http_url)
+            return None
+
+        getUtility(ITranslationsUploadJobSource).create(
+            sourcepackagerelease, self.libraryfilealias)
 
     def publishStaticTranslations(self, logger=None):
         """See `IPackageUploadCustom`."""

=== added file 'lib/lp/soyuz/model/translationsuploadjob.py'
--- lib/lp/soyuz/model/translationsuploadjob.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/translationsuploadjob.py	2013-07-23 15:53:28 +0000
@@ -0,0 +1,101 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+    'TranslationsUploadJob',
+    ]
+
+from lazr.delegates import delegates
+import simplejson
+from zope.component import getUtility
+from zope.interface import (
+    classProvides,
+    implements,
+    )
+
+from lp.services.config import config
+from lp.services.database.interfaces import IStore
+from lp.services.job.interfaces.job import JobType
+from lp.services.job.model.job import (
+    EnumeratedSubclass,
+    Job,
+    )
+from lp.services.job.runner import BaseRunnableJob
+from lp.services.librarian.interfaces import ILibraryFileAliasSet
+from lp.soyuz.interfaces.translationsuploadjob import (
+    ITranslationsUploadJob,
+    ITranslationsUploadJobSource,
+    )
+from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+
+
+class TranslationsUploadJobDerived(BaseRunnableJob):
+
+    __metaclass__ = EnumeratedSubclass
+
+    delegates(ITranslationsUploadJob)
+    classProvides(ITranslationsUploadJobSource)
+    config = config.ITranslationsUploadJobSource
+
+    def __init__(self, job):
+        assert job.base_job_type == JobType.UPLOAD_TRANSLATIONS_FILES
+        self.job = job
+        self.context = self
+
+    @classmethod
+    def create(cls, sourcepackagerelease, libraryfilealias):
+        job = Job(
+            base_job_type=JobType.UPLOAD_TRANSLATIONS_FILES,
+            requester=sourcepackagerelease.creator,
+            base_json_data=simplejson.dumps(
+                {'sourcepackagerelease': sourcepackagerelease.id,
+                 'libraryfilealias': libraryfilealias.id}))
+        derived = cls(job)
+        derived.celeryRunOnCommit()
+        return derived
+
+    @classmethod
+    def get(cls, sourcepackagerelease, libraryfilealias):
+        metadata = simplejson.dumps(
+            {'sourcepackagerelease': sourcepackagerelease.id,
+             'libraryfilealias': libraryfilealias.id})
+        return cls(IStore(Job).find(Job, Job.base_json_data == metadata).one())
+
+    @classmethod
+    def iterReady(cls):
+        jobs = IStore(Job).find(
+            Job, Job.id.is_in(Job.ready_jobs),
+            Job.base_job_type == JobType.UPLOAD_TRANSLATIONS_FILES)
+        return [cls(job) for job in jobs]
+
+
+class TranslationsUploadJob(TranslationsUploadJobDerived):
+
+    implements(ITranslationsUploadJob)
+    classProvides(ITranslationsUploadJobSource)
+
+    @property
+    def sourcepackagerelease_id(self):
+        return simplejson.loads(self.base_json_data)['sourcepackagerelease']
+
+    @property
+    def libraryfilealias_id(self):
+        return simplejson.loads(self.base_json_data)['libraryfilealias']
+
+    @property
+    def sourcepackagerelease(self):
+        return SourcePackageRelease.get(self.sourcepackagerelease_id)
+
+    @property
+    def libraryfilealias(self):
+        return getUtility(ILibraryFileAliasSet)[self.libraryfilealias_id]
+
+    def run(self):
+        sourcepackagerelease = self.sourcepackagerelease
+        if sourcepackagerelease is not None:
+            libraryfilealias = self.libraryfilealias
+            importer = sourcepackagerelease.creator
+            sourcepackagerelease.attachTranslationFiles(
+                libraryfilealias, True, importer=importer)

=== added file 'lib/lp/soyuz/tests/test_translationsuploadjob.py'
--- lib/lp/soyuz/tests/test_translationsuploadjob.py	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_translationsuploadjob.py	2013-07-23 15:53:28 +0000
@@ -0,0 +1,143 @@
+# Copyright 2013 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os.path
+
+from testtools.content import text_content
+import transaction
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.soyuz.enums import (
+    PackageDiffStatus,
+    )
+from lp.soyuz.interfaces.translationsuploadjob import (
+    ITranslationsUploadJob,
+    ITranslationsUploadJobSource,
+    )
+from lp.soyuz.model.translationsuploadjob import TranslationsUploadJob
+from lp.services.config import config
+from lp.services.features.testing import FeatureFixture
+from lp.services.job.interfaces.job import JobStatus
+from lp.testing import (
+    admin_logged_in,
+    run_script,
+    TestCaseWithFactory,
+    verifyObject,
+    )
+from lp.services.job.tests import block_on_job
+from lp.testing.fakemethod import FakeMethod
+from lp.testing.layers import (
+    CeleryJobLayer,
+    LaunchpadZopelessLayer,
+    )
+from lp.services.tarfile_helpers import LaunchpadWriteTarFile
+from lp.translations.interfaces.translationimportqueue import (
+    ITranslationImportQueue,
+    )
+
+
+class LocalTestHelper(TestCaseWithFactory):
+
+    def makeJob(self, spr_creator=None, archive=None,
+                sourcepackagerelease=None, libraryfilealias=None):
+        if spr_creator is None:
+            creator = self.factory.makePerson()
+        else:
+            creator = self.factory.makePerson(name=spr_creator)
+        if archive is None:
+            archive = self.factory.makeArchive()
+        if sourcepackagerelease is None:
+            sourcepackagerelease = self.factory.makeSourcePackageRelease(
+                archive=archive, creator=creator)
+        if libraryfilealias is None:
+            libraryfilealias = self.makeTranslationsLFA()
+        return (sourcepackagerelease,
+                getUtility(ITranslationsUploadJobSource).create(
+                    sourcepackagerelease, libraryfilealias))
+
+    def makeTranslationsLFA(self):
+        """Create an LibraryFileAlias containing dummy translation data."""
+        test_tar_content = {
+            'source/po/foo.pot': 'Foo template',
+            'source/po/eo.po': 'Foo translation',
+            }
+        tarfile_content = LaunchpadWriteTarFile.files_to_string(
+            test_tar_content)
+        return self.factory.makeLibraryFileAlias(content=tarfile_content)
+
+
+class TestTranslationsUploadJob(LocalTestHelper):
+
+    layer = LaunchpadZopelessLayer
+
+    def test_job_implements_ITranslationsUploadJob(self):
+        _, job = self.makeJob()
+        self.assertTrue(verifyObject(ITranslationsUploadJob, job))
+
+    def test_job_source_implements_ITranslationsUploadJobSource(self):
+        job_source = getUtility(ITranslationsUploadJobSource)
+        self.assertTrue(verifyObject(ITranslationsUploadJobSource, job_source))
+
+    def test_iterReady(self):
+        _, job1 = self.makeJob()
+        removeSecurityProxy(job1).job._status = JobStatus.COMPLETED
+        _, job2 = self.makeJob()
+        jobs = list(TranslationsUploadJob.iterReady())
+        self.assertEqual(1, len(jobs))
+
+    def test_importer_is_creator(self):
+        spr, job = self.makeJob(spr_creator="foobar")
+        transaction.commit()
+        job.run()
+        translation_import_queue = getUtility(ITranslationImportQueue)
+        entries_in_queue = translation_import_queue.getAllEntries(
+            target=spr.sourcepackage)
+        self.assertEqual(entries_in_queue[0].importer.name, "foobar")
+
+    def test_run(self):
+        archive = self.factory.makeArchive()
+        foo_pkg = self.factory.makeSourcePackageRelease(archive=archive)
+        method = FakeMethod()
+        removeSecurityProxy(foo_pkg).attachTranslationFiles = method
+        spr, job = self.makeJob(archive=archive, sourcepackagerelease=foo_pkg)
+        transaction.commit()
+        job.run()
+        self.assertEqual(method.call_count, 1)
+
+    def test_smoke(self):
+        spr, job = self.makeJob()
+        transaction.commit()
+        out, err, exit_code = run_script(
+            "LP_DEBUG_SQL=1 cronscripts/process-job-source.py -vv %s" % (
+                ITranslationsUploadJobSource.getName()))
+
+        self.addDetail("stdout", text_content(out))
+        self.addDetail("stderr", text_content(err))
+
+        self.assertEqual(0, exit_code)
+        translation_import_queue = getUtility(ITranslationImportQueue)
+        entries_in_queue = translation_import_queue.getAllEntries(
+                target=spr.sourcepackage).count()
+        self.assertEqual(2, entries_in_queue)
+
+
+class TestViaCelery(LocalTestHelper):
+    """TranslationsUploadJob runs under Celery."""
+
+    layer = CeleryJobLayer
+
+    def test_run(self):
+        self.useFixture(FeatureFixture({
+            'jobs.celery.enabled_classes': 'TranslationsUploadJob',
+        }))
+
+        spr, job = self.makeJob()
+        with block_on_job(self):
+            transaction.commit()
+        translation_import_queue = getUtility(ITranslationImportQueue)
+        entries_in_queue = translation_import_queue.getAllEntries(
+                target=spr.sourcepackage).count()
+        self.assertEqual(2, entries_in_queue)