launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00349
[Merge] lp:~stevenk/launchpad/db-add-ifp-job into lp:launchpad
Steve Kowalik has proposed merging lp:~stevenk/launchpad/db-add-ifp-job into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
This branch adds only the plumbing for IDistroSeries.initialiseFromParent() to be run via the job system. There are some other changes required to have this in a useable state, but as it stands right now, the code is testable, and self-contained, so it can land, and I can make the small changes required when the other changes are ready.
The cronscript and other related bits to enable this work will be landed last.
This will require a database review, as well as a new database number.
--
https://code.launchpad.net/~stevenk/launchpad/db-add-ifp-job/+merge/31349
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/db-add-ifp-job into lp:launchpad.
=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql 2010-07-28 16:39:53 +0000
+++ database/schema/comments.sql 2010-07-30 04:10:02 +0000
@@ -2268,6 +2268,13 @@
COMMENT ON COLUMN HWDMIValue.value IS 'The value';
COMMENT ON COLUMN HWDMIValue.handle IS 'The handle to which this key/value pair belongs.';
+-- InitialiseDistroSeriesJob
+
+COMMENT ON TABLE InitialiseDistroSeriesJob IS 'Contains references to job to be run to initialise distro series.';
+COMMENT ON COLUMN InitialiseDistroSeriesJob.distroseries IS 'The distroseries to be initialised.';
+COMMENT ON COLUMN InitialiseDistroSeriesJob.job_type IS 'The type of job';
+COMMENT ON COLUMN InitialiseDistroSeriesJob.json_data IS 'A JSON struct containing data for the job.';
+
-- Job
COMMENT ON TABLE Job IS 'Common info about a job.';
=== added file 'database/schema/patch-2207-99-0.sql'
--- database/schema/patch-2207-99-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2207-99-0.sql 2010-07-30 04:10:02 +0000
@@ -0,0 +1,25 @@
+-- Copyright 2009 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+-- The `InitialiseDistroSeriesJob` table captures the data required for an ifp job.
+
+CREATE TABLE InitialiseDistroSeriesJob (
+ id serial PRIMARY KEY,
+ -- FK to the `Job` record with the "generic" data about this archive
+ -- job.
+ job integer NOT NULL CONSTRAINT initialisedistroseriesjob__job__fk REFERENCES job,
+ -- FK to the associated `InitialiseDistroSeries` record.
+ distroseries integer NOT NULL CONSTRAINT initialisedistroseriesjob__distroseries__fk REFERENCES DistroSeries,
+ -- The particular type of foo job
+ job_type integer NOT NULL,
+ -- JSON data for use by the job
+ json_data text
+);
+
+ALTER TABLE InitialiseDistroSeriesJob ADD CONSTRAINT initialisedistroseriesjob__job__key UNIQUE (job);
+CREATE INDEX initialisedistroseriesjob__distroseries__job_type__idx ON InitialiseDistroSeriesJob(distroseries, job_type);
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 99, 0);
+
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-07-29 08:06:07 +0000
+++ database/schema/security.cfg 2010-07-30 04:10:02 +0000
@@ -159,6 +159,7 @@
public.hwtest = SELECT
public.hwvendorid = SELECT
public.hwvendorname = SELECT
+public.initialisedistroseriesjob = SELECT, INSERT, UPDATE, DELETE
public.job = SELECT, INSERT, UPDATE, DELETE
public.karmacache = SELECT
public.karmacategory = SELECT
=== added file 'lib/lp/soyuz/interfaces/initialisedistroseriesjob.py'
--- lib/lp/soyuz/interfaces/initialisedistroseriesjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/initialisedistroseriesjob.py 2010-07-30 04:10:02 +0000
@@ -0,0 +1,67 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+ "IDoInitialiseDistroSeriesJob",
+ "IDoInitialiseDistroSeriesJobSource",
+ "IInitialiseDistroSeriesJob",
+ "IInitialiseDistroSeriesJobSource",
+ "InitialiseDistroSeriesJobType",
+]
+
+from lazr.enum import DBEnumeratedType, DBItem
+from zope.interface import Attribute, Interface
+from zope.schema import Int, Object
+
+from canonical.launchpad import _
+
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
+from lp.registry.interfaces.distroseries import IDistroSeries
+
+
+class IInitialiseDistroSeriesJob(Interface):
+ """A Job that initialises a distro series, based on a parent."""
+
+ id = Int(
+ title=_('DB ID'), required=True, readonly=True,
+ description=_("The tracking number for this job."))
+
+ distroseries = Object(
+ title=_('The DistroSeries this job is about.'), schema=IDistroSeries,
+ required=True)
+
+ job = Object(
+ title=_('The common Job attributes'), schema=IJob, required=True)
+
+ metadata = Attribute('A dict of data about the job.')
+
+ def destroySelf():
+ """Destroy this object."""
+
+
+class IInitialiseDistroSeriesJobSource(IJobSource):
+ """An interface for acquiring IInitialiseDistroSeriesJobs."""
+
+ def create(distroseries):
+ """Create a new IInitialiseDistroSeriesJobs for a distroseries."""
+
+
+class InitialiseDistroSeriesJobType(DBEnumeratedType):
+
+ DO_INITIALISE = DBItem(0, """
+ Initialise a Distro Series.
+
+ This job initialises a given distro series, creating builds, and
+ populating the archive from the parent distroseries.
+ """)
+
+
+class IDoInitialiseDistroSeriesJob(IRunnableJob):
+ """A Job that initialises a distro series, based on a parent."""
+
+
+class IDoInitialiseDistroSeriesJobSource(IInitialiseDistroSeriesJobSource):
+ """An interface for acquiring IInitialiseDistroSeriesJobs."""
+
=== added file 'lib/lp/soyuz/model/doinitialisedistroseriesjob.py'
--- lib/lp/soyuz/model/doinitialisedistroseriesjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/doinitialisedistroseriesjob.py 2010-07-30 04:10:02 +0000
@@ -0,0 +1,53 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+ "DoInitialiseDistroSeriesJob",
+]
+
+from zope.component import getUtility
+from zope.interface import classProvides, implements
+
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
+
+from lp.services.job.model.job import Job
+from lp.soyuz.interfaces.initialisedistroseriesjob import (
+ InitialiseDistroSeriesJobType, IDoInitialiseDistroSeriesJob,
+ IDoInitialiseDistroSeriesJobSource)
+from lp.soyuz.model.initialisedistroseriesjob import (
+ InitialiseDistroSeriesJob, InitialiseDistroSeriesJobDerived)
+
+
+class DoInitialiseDistroSeriesJob(InitialiseDistroSeriesJobDerived):
+
+ implements(IDoInitialiseDistroSeriesJob)
+
+ class_job_type = InitialiseDistroSeriesJobType.DO_INITIALISE
+ classProvides(IDoInitialiseDistroSeriesJobSource)
+
+ @classmethod
+ def create(cls, distroseries):
+ """See `IDoInitialiseDistroSeriesJob`."""
+ # If there's already a job, don't create a new one.
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ existing_job = store.find(
+ InitialiseDistroSeriesJob,
+ InitialiseDistroSeriesJob.distroseries == distroseries,
+ InitialiseDistroSeriesJob.job_type == cls.class_job_type,
+ InitialiseDistroSeriesJob.job == Job.id,
+ Job.id.is_in(Job.ready_jobs)
+ ).any()
+
+ if existing_job is not None:
+ return cls(existing_job)
+ else:
+ return super(
+ DoInitialiseDistroSeriesJob, cls).create(distroseries)
+
+ def run(self):
+ """See `IRunnableJob`."""
+ self.distroseries.initialiseFromParent()
+
=== added file 'lib/lp/soyuz/model/initialisedistroseriesjob.py'
--- lib/lp/soyuz/model/initialisedistroseriesjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/initialisedistroseriesjob.py 2010-07-30 04:10:02 +0000
@@ -0,0 +1,129 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+__all__ = [
+ "InitialiseDistroSeriesJob",
+ "InitialiseDistroSeriesJobDerived",
+]
+
+import simplejson
+
+from sqlobject import SQLObjectNotFound
+from storm.base import Storm
+from storm.locals import And, Int, Reference, Unicode
+
+from zope.component import getUtility
+from zope.interface import classProvides, implements
+
+from canonical.database.enumcol import EnumCol
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
+
+from lazr.delegates import delegates
+
+from lp.registry.model.distroseries import DistroSeries
+from lp.soyuz.interfaces.initialisedistroseriesjob import (
+ IInitialiseDistroSeriesJob, IInitialiseDistroSeriesJobSource,
+ InitialiseDistroSeriesJobType)
+from lp.services.job.model.job import Job
+from lp.services.job.runner import BaseRunnableJob
+
+
+class InitialiseDistroSeriesJob(Storm):
+ """Base class for jobs related to InitialiseDistroSeriess."""
+
+ implements(IInitialiseDistroSeriesJob)
+
+ __storm_table__ = 'InitialiseDistroSeriesJob'
+
+ id = Int(primary=True)
+
+ job_id = Int(name='job')
+ job = Reference(job_id, Job.id)
+
+ distroseries_id = Int(name='distroseries')
+ distroseries = Reference(distroseries_id, DistroSeries.id)
+
+ job_type = EnumCol(enum=InitialiseDistroSeriesJobType, notNull=True)
+
+ _json_data = Unicode('json_data')
+
+ def __init__(self, distroseries, job_type, metadata):
+ super(InitialiseDistroSeriesJob, self).__init__()
+ json_data = simplejson.dumps(metadata)
+ self.job = Job()
+ self.distroseries = distroseries
+ self.job_type = job_type
+ self._json_data = json_data.decode('utf-8')
+
+ @property
+ def metadata(self):
+ return simplejson.loads(self._json_data)
+
+ @classmethod
+ def get(cls, key):
+ """Return the instance of this class whose key is supplied."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ instance = store.get(cls, key)
+ if instance is None:
+ raise SQLObjectNotFound(
+ 'No occurence of %s has key %s' % (cls.__name__, key))
+ return instance
+
+
+class InitialiseDistroSeriesJobDerived(BaseRunnableJob):
+ """Intermediate class for deriving from InitialiseDistroSeriesJob."""
+ delegates(IInitialiseDistroSeriesJob)
+ classProvides(IInitialiseDistroSeriesJobSource)
+
+ def __init__(self, job):
+ self.context = job
+
+ @classmethod
+ def create(cls, distroseries):
+ """See `IInitialiseDistroSeriesJob`."""
+ # If there's already a job, don't create a new one.
+ job = InitialiseDistroSeriesJob(
+ distroseries, cls.class_job_type, {})
+ return cls(job)
+
+ @classmethod
+ def get(cls, job_id):
+ """Get a job by id.
+
+ :return: the InitialiseDistroSeriesJob with the specified id, as
+ the current InitialiseDistroSeriesJobDerived subclass.
+ :raises: SQLObjectNotFound if there is no job with the specified id,
+ or its job_type does not match the desired subclass.
+ """
+ job = InitialiseDistroSeriesJob.get(job_id)
+ if job.job_type != cls.class_job_type:
+ raise SQLObjectNotFound(
+ 'No object found with id %d and type %s' % (job_id,
+ cls.class_job_type.title))
+ return cls(job)
+
+ @classmethod
+ def iterReady(cls):
+ """Iterate through all ready InitialiseDistroSeriesJobs."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+ jobs = store.find(
+ InitialiseDistroSeriesJob,
+ And(InitialiseDistroSeriesJob.job_type == cls.class_job_type,
+ InitialiseDistroSeriesJob.job == Job.id,
+ Job.id.is_in(Job.ready_jobs),
+ InitialiseDistroSeriesJob.distroseries == DistroSeries.id))
+ return (cls(job) for job in jobs)
+
+ def getOopsVars(self):
+ """See `IRunnableJob`."""
+ vars = BaseRunnableJob.getOopsVars(self)
+ vars.extend([
+ ('distroseries_id', self.context.distroseries.id),
+ ('distroseries_job_id', self.context.id),
+ ('distroseries_job_type', self.context.job_type.title),
+ ])
+ return vars
+
=== added file 'lib/lp/soyuz/tests/test_doinitialisedistroseries.py'
--- lib/lp/soyuz/tests/test_doinitialisedistroseries.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_doinitialisedistroseries.py 2010-07-30 04:10:02 +0000
@@ -0,0 +1,60 @@
+from canonical.testing import LaunchpadZopelessLayer
+from zope.security.proxy import removeSecurityProxy
+
+from lp.soyuz.model.doinitialisedistroseriesjob import (
+ DoInitialiseDistroSeriesJob)
+from lp.testing import TestCaseWithFactory
+from lp.testing.fakemethod import FakeMethod
+
+
+class DoInitialiseDistroSeriesJobTests(TestCaseWithFactory):
+ """Test case for DoInitialiseDistroSeriesJob."""
+
+ layer = LaunchpadZopelessLayer
+
+ def test_getOopsVars(self):
+ distroseries = self.factory.makeDistroSeries()
+ job = DoInitialiseDistroSeriesJob.create(distroseries)
+ vars = job.getOopsVars()
+ self.assertIn(('distroseries_id', distroseries.id), vars)
+ self.assertIn(('distroseries_job_id', job.context.id), vars)
+ self.assertIn(('distroseries_job_type', job.context.job_type.title), vars)
+
+ def _getJobs(self):
+ """Return the pending DoFooJobs as a list."""
+ return list(DoInitialiseDistroSeriesJob.iterReady())
+
+ def _getJobCount(self):
+ """Return the number of DoFooJobs in the queue."""
+ return len(self._getJobs())
+
+ def test_create_only_creates_one(self):
+ distroseries = self.factory.makeDistroSeries()
+ # If there's already a DoInitialiseDistroSeriesJob for a DistroSeries,
+ # DoInitialiseDistroSeriesJob.create() won't create a new one.
+ job = DoInitialiseDistroSeriesJob.create(distroseries)
+
+ # There will now be one job in the queue.
+ self.assertEqual(1, self._getJobCount())
+
+ new_job = DoInitialiseDistroSeriesJob.create(distroseries)
+
+ # The two jobs will in fact be the same job.
+ self.assertEqual(job, new_job)
+
+ # And the queue will still have a length of 1.
+ self.assertEqual(1, self._getJobCount())
+
+ def test_run(self):
+ """Test that DoInitialiseDistroSeriesJob.run() actually
+ initialises builds and copies from the parent."""
+ distroseries = self.factory.makeDistroSeries()
+ distroseries.fake_initialiseFromParent = FakeMethod(result=True)
+ removeSecurityProxy(distroseries).initialiseFromParent = (
+ distroseries.fake_initialiseFromParent)
+
+ job = DoInitialiseDistroSeriesJob.create(distroseries)
+ job.run()
+
+ self.assertEqual(1, distroseries.fake_initialiseFromParent.call_count)
+
=== added file 'lib/lp/soyuz/tests/test_initialisedistroseriesjob.py'
--- lib/lp/soyuz/tests/test_initialisedistroseriesjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_initialisedistroseriesjob.py 2010-07-30 04:10:02 +0000
@@ -0,0 +1,53 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+import unittest
+
+from canonical.testing import LaunchpadZopelessLayer
+
+from lp.soyuz.interfaces.initialisedistroseriesjob import (
+ InitialiseDistroSeriesJobType)
+from lp.soyuz.model.initialisedistroseriesjob import (
+ InitialiseDistroSeriesJob, InitialiseDistroSeriesJobDerived)
+from lp.testing import TestCaseWithFactory
+
+
+class InitialiseDistroSeriesJobTestCase(TestCaseWithFactory):
+ """Test case for basic InitialiseDistroSeriesJob gubbins."""
+
+ layer = LaunchpadZopelessLayer
+
+ def test_instantiate(self):
+ distroseries = self.factory.makeDistroSeries()
+
+ metadata = ('some', 'arbitrary', 'metadata')
+ distroseries_job = InitialiseDistroSeriesJob(
+ distroseries, InitialiseDistroSeriesJobType.DO_INITIALISE, metadata)
+
+ self.assertEqual(distroseries, distroseries_job.distroseries)
+ self.assertEqual(InitialiseDistroSeriesJobType.DO_INITIALISE, distroseries_job.job_type)
+
+ # When we actually access the InitialiseDistroSeriesJob's metadata it gets
+ # deserialized from JSON, so the representation returned by
+ # foo_job.metadata will be different from what we originally
+ # passed in.
+ metadata_expected = [u'some', u'arbitrary', u'metadata']
+ self.assertEqual(metadata_expected, distroseries_job.metadata)
+
+
+class InitialiseDistroSeriesJobDerivedTestCase(TestCaseWithFactory):
+ """Test case for the InitialiseDistroSeriesJobDerived class."""
+
+ layer = LaunchpadZopelessLayer
+
+ def test_create_explodes(self):
+ # InitialiseDistroSeriesJobDerived.create() will blow up because it needs to be
+ # subclassed to work properly.
+ distroseries = self.factory.makeDistroSeries()
+ self.assertRaises(
+ AttributeError, InitialiseDistroSeriesJobDerived.create, distroseries)
+
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromName(__name__)
+
Follow ups