← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/handle-concurrent into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/handle-concurrent into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #648075 in Launchpad itself: "Automatic translations export fails intermittently"
  https://bugs.launchpad.net/launchpad/+bug/648075

For more details, see:
https://code.launchpad.net/~abentley/launchpad/handle-concurrent/+merge/62511

= Summary =
Fix bug #648075: Automatic translations export fails intermittently

== Proposed fix ==
This bug appeared to be a transient failure, but was actually because the last_mirrored_id of the branches was stale.  This branch fixes it by raising a better exception from DirectBranchError and handling the error in the export script.

== Pre-implementation notes ==
Discussed with Deryck

== Implementation details ==
Fixing the branches requires calling Branch.branchChanged, which takes a bunch of parameters.  In order to avoid duplicating code, I moved the parameter generation into get_branch_info and get_db_branch_info.

There was a lot of lint, especially wrt inline comments.

I also renamed Branch.branchChanged's stacked_on_location to stacked_on_url, to match its interface.


== Tests ==
bin/test -t test_raises_StaleLastMirrored -t test_exportToStaleBranch

== Demo and Q/A ==
Hmm.  Not sure if this is qa-able, because of how it interacts with branches.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/scripts/tests/test_translations_to_branch.py
  lib/lp/code/xmlrpc/codehosting.py
  lib/lp/code/errors.py
  lib/lp/code/interfaces/branch.py
  lib/lp/code/model/directbranchcommit.py
  lib/lp/codehosting/vfs/branchfs.py
  lib/lp/codehosting/bzrutils.py
  lib/lp/code/model/branch.py
  lib/lp/translations/scripts/translations_to_branch.py
  lib/lp/code/tests/test_directbranchcommit.py
-- 
https://code.launchpad.net/~abentley/launchpad/handle-concurrent/+merge/62511
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/handle-concurrent into lp:launchpad.
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2011-02-14 03:03:17 +0000
+++ lib/lp/code/errors.py	2011-05-26 16:16:46 +0000
@@ -32,6 +32,7 @@
     'NoSuchBranch',
     'PrivateBranchRecipe',
     'ReviewNotPending',
+    'StaleLastMirrored',
     'TooManyBuilds',
     'TooNewRecipeFormat',
     'UnknownBranchTypeError',
@@ -186,7 +187,7 @@
 class BranchMergeProposalExists(InvalidBranchMergeProposal):
     """Raised if there is already a matching BranchMergeProposal."""
 
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class InvalidNamespace(Exception):
@@ -216,6 +217,24 @@
     _message_prefix = "No such branch"
 
 
+class StaleLastMirrored(Exception):
+    """Raised when last_mirrored_id is out of date with on-disk value."""
+
+    def __init__(self, db_branch, info):
+        """Constructor.
+
+        :param db_branch: The database branch.
+        :param info: A dict of information about the branch, as produced by
+            lp.codehosting.bzrutils.get_branch_info
+        """
+        self.db_branch = db_branch
+        self.info = info
+        Exception.__init__(
+            self,
+            'Database last_mirrored_id %s does not match on-disk value %s' %
+            (db_branch.last_mirrored_id, self.info['last_revision_id']))
+
+
 class PrivateBranchRecipe(Exception):
 
     webservice_error(400)

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2011-05-12 11:39:29 +0000
+++ lib/lp/code/interfaces/branch.py	2011-05-26 16:16:46 +0000
@@ -13,6 +13,7 @@
     'BzrIdentityMixin',
     'DEFAULT_BRANCH_STATUS_IN_LISTING',
     'get_blacklisted_hostnames',
+    'get_db_branch_info',
     'IBranch',
     'IBranchBatchNavigator',
     'IBranchCloud',
@@ -180,7 +181,7 @@
             raise LaunchpadValidationError(structured(message))
 
         if IBranch.providedBy(self.context) and self.context.url == str(uri):
-            return # url was not changed
+            return  # url was not changed
 
         if uri.path == '/':
             message = _(
@@ -407,13 +408,13 @@
             title=_("The bug-branch link objects that link this branch "
                     "to bugs."),
             readonly=True,
-            value_type=Reference(schema=Interface)) # Really IBugBranch
+            value_type=Reference(schema=Interface))  # Really IBugBranch
 
     linked_bugs = exported(
         CollectionField(
             title=_("The bugs linked to this branch."),
         readonly=True,
-        value_type=Reference(schema=Interface))) # Really IBug
+        value_type=Reference(schema=Interface)))  # Really IBug
 
     def getLinkedBugTasks(user, status_filter):
         """Return a result set for the tasks that are relevant to this branch.
@@ -428,7 +429,7 @@
 
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
-        bug=Reference(schema=Interface)) # Really IBug
+        bug=Reference(schema=Interface))  # Really IBug
     @export_write_operation()
     @operation_for_version('beta')
     def linkBug(bug, registrant):
@@ -440,7 +441,7 @@
 
     @call_with(user=REQUEST_USER)
     @operation_parameters(
-        bug=Reference(schema=Interface)) # Really IBug
+        bug=Reference(schema=Interface))  # Really IBug
     @export_write_operation()
     @operation_for_version('beta')
     def unlinkBug(bug, user):
@@ -455,12 +456,12 @@
         CollectionField(
             title=_("Specification linked to this branch."),
             readonly=True,
-            value_type=Reference(Interface)), # Really ISpecificationBranch
+            value_type=Reference(Interface)),  # Really ISpecificationBranch
         as_of="beta")
 
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
-        spec=Reference(schema=Interface)) # Really ISpecification
+        spec=Reference(schema=Interface))  # Really ISpecification
     @export_write_operation()
     @operation_for_version('beta')
     def linkSpecification(spec, registrant):
@@ -472,7 +473,7 @@
 
     @call_with(user=REQUEST_USER)
     @operation_parameters(
-        spec=Reference(schema=Interface)) # Really ISpecification
+        spec=Reference(schema=Interface))  # Really ISpecification
     @export_write_operation()
     @operation_for_version('beta')
     def unlinkSpecification(spec, user):
@@ -497,7 +498,7 @@
         CollectionField(
             title=_("BranchSubscriptions associated to this branch."),
             readonly=True,
-            value_type=Reference(Interface))) # Really IBranchSubscription
+            value_type=Reference(Interface)))  # Really IBranchSubscription
 
     subscribers = exported(
         CollectionField(
@@ -614,7 +615,8 @@
         merged_revnos=List(Int(
             title=_('The target-branch revno of the merge.'))))
     @call_with(visible_by_user=REQUEST_USER)
-    @operation_returns_collection_of(Interface) # Really IBranchMergeProposal.
+    # Really IBranchMergeProposal
+    @operation_returns_collection_of(Interface)
     @export_read_operation()
     @operation_for_version('beta')
     def getMergeProposals(status=None, visible_by_user=None,
@@ -722,7 +724,7 @@
 
     def associatedSuiteSourcePackages():
         """Return the suite source packages that this branch is linked to.
-        
+
         :return: A list of suite source packages ordered by pocket.
         """
 
@@ -771,7 +773,7 @@
         code_review_level=Choice(
             title=_("The level of code review notification emails."),
             vocabulary=CodeReviewNotificationLevel))
-    @operation_returns_entry(Interface) # Really IBranchSubscription
+    @operation_returns_entry(Interface)  # Really IBranchSubscription
     @call_with(subscribed_by=REQUEST_USER)
     @export_write_operation()
     @operation_for_version('beta')
@@ -794,7 +796,7 @@
         person=Reference(
             title=_("The person to unsubscribe"),
             schema=IPerson))
-    @operation_returns_entry(Interface) # Really IBranchSubscription
+    @operation_returns_entry(Interface)  # Really IBranchSubscription
     @export_read_operation()
     @operation_for_version('beta')
     def getSubscription(person):
@@ -1042,10 +1044,10 @@
     @operation_parameters(
         project=Reference(
             title=_("The project the branch belongs to."),
-            schema=Interface, required=False), # Really IProduct
+            schema=Interface, required=False),  # Really IProduct
         source_package=Reference(
             title=_("The source package the branch belongs to."),
-            schema=Interface, required=False)) # Really ISourcePackage
+            schema=Interface, required=False))  # Really ISourcePackage
     @export_write_operation()
     @operation_for_version('beta')
     def setTarget(user, project=None, source_package=None):
@@ -1307,10 +1309,10 @@
     @collection_default_content()
     def getBranches(limit=50, eager_load=True):
         """Return a collection of branches.
-        
-        :param eager_load: If True (the default because this is used in the 
-            web service and it needs the related objects to create links) eager
-            load related objects (products, code imports etc).
+
+        :param eager_load: If True (the default because this is used in the
+            web service and it needs the related objects to create links)
+            eager load related objects (products, code imports etc).
         """
 
 
@@ -1425,3 +1427,22 @@
         return False
     celebs = getUtility(ILaunchpadCelebrities)
     return user.inTeam(celebs.admin) or user.inTeam(celebs.bazaar_experts)
+
+
+def get_db_branch_info(stacked_on_url, last_revision_id, control_string,
+                       branch_string, repository_string):
+    """Return a dict of branch info suitable for Branch.branchChanged.
+
+    :param stacked_on_url: The URL the branch is stacked on.
+    :param last_revision_id: The branch tip revision_id.
+    :param control_string: The control format marker as a string.
+    :param branch_string: The branch format marker as a string.
+    :param repository_string: The repository format marker as a string.
+    """
+    info = {}
+    info['stacked_on_url'] = stacked_on_url
+    info['last_revision_id'] = last_revision_id
+    info['control_format'] = ControlFormat.get_enum(control_string)
+    info['branch_format'] = BranchFormat.get_enum(branch_string)
+    info['repository_format'] = RepositoryFormat.get_enum(repository_string)
+    return info

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2011-05-12 11:39:29 +0000
+++ lib/lp/code/model/branch.py	2011-05-26 16:16:46 +0000
@@ -318,8 +318,8 @@
         params = BugTaskSearchParams(user=user, linked_branches=self.id,
             status=status_filter)
         tasks = shortlist(getUtility(IBugTaskSet).search(params), 1000)
-        # Post process to discard irrelevant tasks: we only return one task per
-        # bug, and cannot easily express this in sql (yet).
+        # Post process to discard irrelevant tasks: we only return one task
+        # per bug, and cannot easily express this in sql (yet).
         return filter_bugtasks_by_context(self.target.context, tasks)
 
     def linkBug(self, bug, registrant):
@@ -593,6 +593,7 @@
             result = result.order_by(BranchRevision.sequence)
         else:
             result = result.order_by(Desc(BranchRevision.sequence))
+
         def eager_load(rows):
             revisions = map(operator.itemgetter(1), rows)
             load_related(RevisionAuthor, revisions, ['revision_author_id'])
@@ -742,8 +743,8 @@
         """Helper for associatedSuiteSourcePackages."""
         # This is eager loaded by BranchCollection.getBranches.
         series_set = getUtility(IFindOfficialBranchLinks)
-        # Order by the pocket to get the release one first. If changing this be
-        # sure to also change BranchCollection.getBranches.
+        # Order by the pocket to get the release one first. If changing this
+        # be sure to also change BranchCollection.getBranches.
         links = series_set.findForBranch(self).order_by(
             SeriesSourcePackageBranch.pocket)
         return [link.suite_sourcepackage for link in links]
@@ -1012,17 +1013,17 @@
         else:
             return getUtility(IBranchLookup).getByUniqueName(location)
 
-    def branchChanged(self, stacked_on_location, last_revision_id,
+    def branchChanged(self, stacked_on_url, last_revision_id,
                       control_format, branch_format, repository_format):
         """See `IBranch`."""
         self.mirror_status_message = None
-        if stacked_on_location == '' or stacked_on_location is None:
+        if stacked_on_url == '' or stacked_on_url is None:
             stacked_on_branch = None
         else:
-            stacked_on_branch = self._findStackedBranch(stacked_on_location)
+            stacked_on_branch = self._findStackedBranch(stacked_on_url)
             if stacked_on_branch is None:
                 self.mirror_status_message = (
-                    'Invalid stacked on location: ' + stacked_on_location)
+                    'Invalid stacked on location: ' + stacked_on_url)
         self.stacked_on = stacked_on_branch
         if self.branch_type == BranchType.HOSTED:
             self.last_mirrored = UTC_NOW
@@ -1215,7 +1216,7 @@
         try:
             simplejson.loads(config)
             self.merge_queue_config = config
-        except ValueError: # The json string is invalid
+        except ValueError:  # The json string is invalid
             raise InvalidMergeQueueConfig
 
 

=== modified file 'lib/lp/code/model/directbranchcommit.py'
--- lib/lp/code/model/directbranchcommit.py	2010-10-29 14:02:52 +0000
+++ lib/lp/code/model/directbranchcommit.py	2011-05-26 16:16:46 +0000
@@ -21,7 +21,11 @@
 
 from canonical.config import config
 from canonical.launchpad.interfaces.lpstorm import IMasterObject
-from lp.codehosting.bzrutils import get_stacked_on_url
+from lp.code.errors import StaleLastMirrored
+from lp.codehosting.bzrutils import (
+    get_branch_info,
+    get_stacked_on_url,
+    )
 from lp.services.mail.sendmail import format_address_for_person
 from lp.services.osutils import override_environ
 
@@ -99,12 +103,16 @@
 
         self.bzrbranch.lock_write()
         self.is_locked = True
-
         try:
             self.revision_tree = self.bzrbranch.basis_tree()
             self.transform_preview = TransformPreview(self.revision_tree)
             assert self.transform_preview.find_conflicts() == [], (
                 "TransformPreview is not in a consistent state.")
+            if not no_race_check:
+                last_revision = self.bzrbranch.last_revision()
+                if last_revision != db_branch.last_mirrored_id:
+                    raise StaleLastMirrored(
+                        db_branch, get_branch_info(self.bzrbranch))
 
             self.is_open = True
         except:

=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
--- lib/lp/code/tests/test_directbranchcommit.py	2010-12-09 07:14:34 +0000
+++ lib/lp/code/tests/test_directbranchcommit.py	2011-05-26 16:16:46 +0000
@@ -5,12 +5,14 @@
 
 __metaclass__ = type
 
+from testtools.testcase import ExpectedException
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing.layers import (
     DatabaseFunctionalLayer,
     ZopelessDatabaseLayer,
     )
+from lp.code.errors import StaleLastMirrored
 from lp.code.model.directbranchcommit import (
     ConcurrentUpdateError,
     DirectBranchCommit,
@@ -119,7 +121,7 @@
         committer.last_scanned_id = (
             committer.bzrbranch.last_revision())
         committer.writeFile('file.txt', 'contents')
-        revision_id = committer.commit('')
+        committer.commit('')
         branch_revision_id = committer.bzrbranch.last_revision()
         branch_revision = committer.bzrbranch.repository.get_revision(
             branch_revision_id)
@@ -269,7 +271,7 @@
     def test_getDir_creates_dir_in_existing_dir(self):
         # _getDir creates directories inside ones that already existed
         # in a previously committed version of the branch.
-        existing_id = self.committer._getDir('po')
+        self.committer._getDir('po')
         self._setUpCommitter()
         new_dir_id = self.committer._getDir('po/main/files')
         self.assertTrue('po/main' in self.committer.path_ids)
@@ -323,3 +325,20 @@
         committer = DirectBranchCommit(branch)
         self.addCleanup(committer.unlock)
         self.assertIn('noreply', committer.getBzrCommitterID())
+
+
+class TestStaleLastMirroredID(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_raises_StaleLastMirrored(self):
+        """Raise if the on-disk revision doesn't match last-mirrored."""
+        self.useBzrBranches(direct_database=True)
+        bzr_id = self.factory.getUniqueString()
+        db_branch, tree = self.create_branch_and_tree()
+        tree.commit('unchanged', committer='jrandom@xxxxxxxxxxx')
+        with ExpectedException(StaleLastMirrored, '.*'):
+            committer = DirectBranchCommit(
+                db_branch, committer_id=bzr_id)
+            self.addCleanup(committer.unlock)
+            committer.commit('')

=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
--- lib/lp/code/xmlrpc/codehosting.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/xmlrpc/codehosting.py	2011-05-26 16:16:46 +0000
@@ -34,11 +34,6 @@
     NotFoundError,
     )
 from lp.app.validators import LaunchpadValidationError
-from lp.code.bzr import (
-    BranchFormat,
-    ControlFormat,
-    RepositoryFormat,
-    )
 from lp.code.enums import BranchType
 from lp.code.errors import (
     BranchCreationException,
@@ -47,6 +42,7 @@
     NoLinkedBranch,
     UnknownBranchTypeError,
     )
+from lp.code.interfaces.branch import get_db_branch_info
 from lp.code.interfaces import branchpuller
 from lp.code.interfaces.branchlookup import (
     IBranchLookup,
@@ -194,6 +190,7 @@
                 target = IBranchTarget(context)
                 namespace = target.getNamespace(requester)
                 branch_name = namespace.findUnusedName('trunk')
+
                 def link_func(new_branch):
                     link = ICanHasLinkedBranch(context)
                     link.setBranch(new_branch, requester)
@@ -274,16 +271,13 @@
             if branch is None:
                 return faults.NoBranchWithID(branch_id)
 
-            control_format = ControlFormat.get_enum(control_string)
-            branch_format = BranchFormat.get_enum(branch_string)
-            repository_format = RepositoryFormat.get_enum(repository_string)
-
             if requester == LAUNCHPAD_SERVICES:
                 branch = removeSecurityProxy(branch)
 
-            branch.branchChanged(
-                stacked_on_location, last_revision_id, control_format,
-                branch_format, repository_format)
+            info = get_db_branch_info(
+                stacked_on_location, last_revision_id, control_string,
+                branch_string, repository_string)
+            branch.branchChanged(**info)
 
             return True
 

=== modified file 'lib/lp/codehosting/bzrutils.py'
--- lib/lp/codehosting/bzrutils.py	2010-10-21 20:44:03 +0000
+++ lib/lp/codehosting/bzrutils.py	2011-05-26 16:16:46 +0000
@@ -11,6 +11,7 @@
 __all__ = [
     'add_exception_logging_hook',
     'DenyingServer',
+    'get_branch_info',
     'get_branch_stacked_on_url',
     'get_stacked_on_url',
     'get_vfs_format_classes',
@@ -369,6 +370,24 @@
         return None
 
 
+def get_branch_info(branch):
+    """Get information about the branch for branchChanged.
+
+    :return: a dict containing 'stacked_on_url', 'last_revision_id',
+        'control_string', 'branch_string', 'repository_string'.
+    """
+    info = {}
+    info['stacked_on_url'] = get_stacked_on_url(branch)
+    info['last_revision_id'] = branch.last_revision()
+    # XXX: Aaron Bentley 2008-06-13
+    # Bazaar does not provide a public API for learning about
+    # format markers.  Fix this in Bazaar, then here.
+    info['control_string'] = branch.bzrdir._format.get_format_string()
+    info['branch_string'] = branch._format.get_format_string()
+    info['repository_string'] = branch.repository._format.get_format_string()
+    return info
+
+
 @contextmanager
 def read_locked(branch):
     branch.lock_read()

=== modified file 'lib/lp/codehosting/vfs/branchfs.py'
--- lib/lp/codehosting/vfs/branchfs.py	2011-03-29 03:24:05 +0000
+++ lib/lp/codehosting/vfs/branchfs.py	2011-05-26 16:16:46 +0000
@@ -105,7 +105,10 @@
     CONTROL_TRANSPORT,
     LAUNCHPAD_SERVICES,
     )
-from lp.codehosting.bzrutils import get_stacked_on_url
+from lp.codehosting.bzrutils import (
+    get_branch_info,
+    get_stacked_on_url,
+    )
 from lp.codehosting.vfs.branchfsclient import (
     BranchFileSystemClient,
     )
@@ -689,23 +692,16 @@
             try:
                 branch = BzrDir.open_from_transport(transport).open_branch(
                     ignore_fallbacks=True)
-                last_revision = branch.last_revision()
-                stacked_on_url = self._normalize_stacked_on_url(branch)
-                # XXX: Aaron Bentley 2008-06-13
-                # Bazaar does not provide a public API for learning about
-                # format markers.  Fix this in Bazaar, then here.
-                control_string = branch.bzrdir._format.get_format_string()
-                branch_string = branch._format.get_format_string()
-                repository_string = \
-                    branch.repository._format.get_format_string()
+                info = get_branch_info(branch)
+                info['stacked_on_url'] = (
+                    self._normalize_stacked_on_url(branch))
             finally:
                 if jail_info.transports:
                     jail_info.transports.remove(transport)
-            if stacked_on_url is None:
-                stacked_on_url = ''
+            if info['stacked_on_url'] is None:
+                info['stacked_on_url'] = ''
             return self._branchfs_client.branchChanged(
-                data['id'], stacked_on_url, last_revision,
-                control_string, branch_string, repository_string)
+                data['id'], **info)
 
         @no_traceback_failures
         def handle_error(failure=None, **kw):

=== modified file 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
--- lib/lp/translations/scripts/tests/test_translations_to_branch.py	2010-12-21 19:42:00 +0000
+++ lib/lp/translations/scripts/tests/test_translations_to_branch.py	2011-05-26 16:16:46 +0000
@@ -9,6 +9,7 @@
 from textwrap import dedent
 
 from bzrlib.errors import NotBranchError
+from testtools.matchers import MatchesRegex
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
@@ -142,6 +143,30 @@
         self.assertEqual(
             None, re.search("INFO\s+Committed [0-9]+ file", stderr))
 
+    def test_exportToStaleBranch(self):
+        # Attempting to export to a stale branch marks it for scanning.
+        self.useBzrBranches(direct_database=False)
+        exporter = ExportTranslationsToBranch(test_args=[])
+        exporter.logger = BufferLogger()
+        productseries = self.factory.makeProductSeries()
+        db_branch, tree = self.create_branch_and_tree(
+            product=productseries.product)
+        removeSecurityProxy(productseries).translations_branch = db_branch
+        db_branch.last_mirrored_id = 'stale-id'
+        db_branch.last_scanned_id = db_branch.last_mirrored_id
+        self.becomeDbUser('translationstobranch')
+        self.assertFalse(db_branch.pending_writes)
+        self.assertNotEqual(
+            db_branch.last_mirrored_id, tree.branch.last_revision())
+        exporter._exportToBranch(productseries)
+        self.assertEqual(
+            db_branch.last_mirrored_id, tree.branch.last_revision())
+        self.assertTrue(db_branch.pending_writes)
+        matches = MatchesRegex(
+            '(.|\n)*WARNING Skipped .* due to stale DB info and scheduled'
+            ' scan.')
+        self.assertThat(exporter.logger.getLogBuffer(), matches)
+
     def test_exportToBranches_handles_nonascii_exceptions(self):
         # There's an exception handler in _exportToBranches that must
         # cope well with non-ASCII exception strings.

=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
--- lib/lp/translations/scripts/translations_to_branch.py	2010-12-23 17:30:02 +0000
+++ lib/lp/translations/scripts/translations_to_branch.py	2011-05-26 16:16:46 +0000
@@ -34,6 +34,8 @@
     SLAVE_FLAVOR,
     )
 from lp.app.enums import ServiceUsage
+from lp.code.errors import StaleLastMirrored
+from lp.code.interfaces.branch import get_db_branch_info
 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
 from lp.code.model.directbranchcommit import (
     ConcurrentUpdateError,
@@ -180,7 +182,6 @@
                     template.date_last_updated > changed_since):
                     yield pofile
 
-
     def _exportToBranch(self, source):
         """Export translations for source into source.translations_branch.
 
@@ -189,7 +190,17 @@
         self.logger.info("Exporting %s." % source.title)
         self._checkForObjections(source)
 
-        committer = self._prepareBranchCommit(source.translations_branch)
+        try:
+            committer = self._prepareBranchCommit(source.translations_branch)
+        except StaleLastMirrored, e:
+            source.translations_branch.branchChanged(
+                **get_db_branch_info(**e.info))
+            self.logger.warning(
+                'Skipped %s due to stale DB info and scheduled scan.',
+                source.translations_branch.bzr_identity)
+            if self.txn:
+                self.txn.commit()
+            return
         self.logger.debug("Created DirectBranchCommit.")
         if self.txn:
             self.txn.commit()