launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23769
[Merge] lp:~cjwatson/launchpad/snap-store-upload-job-commit into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-store-upload-job-commit into lp:launchpad.
Commit message:
Consistently commit transactions in SnapStoreUploadJob.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1833424 in Launchpad itself: "snap:build:0.1 doesn't trigger on store_upload_status == 'Uploaded'"
https://bugs.launchpad.net/launchpad/+bug/1833424
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-store-upload-job-commit/+merge/369243
We were previously ensuring that the transaction is committed when the job fails, but not when raising a retryable exception or after sending event notifications. The latter case caused some webhook deliveries not to be sent.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-store-upload-job-commit into lp:launchpad.
=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
--- lib/lp/snappy/model/snapbuildjob.py 2019-06-19 14:04:34 +0000
+++ lib/lp/snappy/model/snapbuildjob.py 2019-06-24 12:07:53 +0000
@@ -1,4 +1,4 @@
-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap build jobs."""
@@ -280,11 +280,15 @@
# Ideally we'd just override Job._set_status or similar, but
# lazr.delegates makes that difficult, so we use this to override all
# the individual Job lifecycle methods instead.
- def _do_lifecycle(self, method_name, *args, **kwargs):
+ def _do_lifecycle(self, method_name, manage_transaction=False,
+ *args, **kwargs):
old_store_upload_status = self.snapbuild.store_upload_status
- getattr(super(SnapStoreUploadJob, self), method_name)(*args, **kwargs)
+ getattr(super(SnapStoreUploadJob, self), method_name)(
+ *args, manage_transaction=manage_transaction, **kwargs)
if self.snapbuild.store_upload_status != old_store_upload_status:
notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild))
+ if manage_transaction:
+ transaction.commit()
def start(self, *args, **kwargs):
self._do_lifecycle("start", *args, **kwargs)
@@ -329,39 +333,42 @@
"""See `IRunnableJob`."""
client = getUtility(ISnapStoreClient)
try:
- if "status_url" not in self.store_metadata:
- self.status_url = client.upload(self.snapbuild)
- # We made progress, so reset attempt_count.
- self.attempt_count = 1
- if self.store_url is None:
- # This is no longer strictly necessary as the store is handling
- # releases via the release intent, but we export various fields
- # via the api, so once this is called, we're done with
- # this task
- self.store_url, self.store_revision = (
- client.checkStatus(self.status_url))
- self.error_message = None
- except self.retry_error_types:
- raise
- except Exception as e:
- if (isinstance(e, SnapStoreError) and e.can_retry and
- self.attempt_count <= self.max_retries):
- raise RetryableSnapStoreError(e.message, detail=e.detail)
- self.error_message = str(e)
- self.error_messages = getattr(e, "messages", None)
- self.error_detail = getattr(e, "detail", None)
- if isinstance(e, UnauthorizedUploadResponse):
- mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild)
- mailer.sendAll()
- elif isinstance(e, BadRefreshResponse):
- mailer = SnapBuildMailer.forRefreshFailure(self.snapbuild)
- mailer.sendAll()
- elif isinstance(e, UploadFailedResponse):
- mailer = SnapBuildMailer.forUploadFailure(self.snapbuild)
- mailer.sendAll()
- elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)):
- mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild)
- mailer.sendAll()
+ try:
+ if "status_url" not in self.store_metadata:
+ self.status_url = client.upload(self.snapbuild)
+ # We made progress, so reset attempt_count.
+ self.attempt_count = 1
+ if self.store_url is None:
+ # This is no longer strictly necessary as the store is
+ # handling releases via the release intent, but we
+ # export various fields via the API, so once this is
+ # called, we're done with this task.
+ self.store_url, self.store_revision = (
+ client.checkStatus(self.status_url))
+ self.error_message = None
+ except self.retry_error_types:
+ raise
+ except Exception as e:
+ if (isinstance(e, SnapStoreError) and e.can_retry and
+ self.attempt_count <= self.max_retries):
+ raise RetryableSnapStoreError(e.message, detail=e.detail)
+ self.error_message = str(e)
+ self.error_messages = getattr(e, "messages", None)
+ self.error_detail = getattr(e, "detail", None)
+ mailer_factory = None
+ if isinstance(e, UnauthorizedUploadResponse):
+ mailer_factory = SnapBuildMailer.forUnauthorizedUpload
+ elif isinstance(e, BadRefreshResponse):
+ mailer_factory = SnapBuildMailer.forRefreshFailure
+ elif isinstance(e, UploadFailedResponse):
+ mailer_factory = SnapBuildMailer.forUploadFailure
+ elif isinstance(e,
+ (BadScanStatusResponse, ScanFailedResponse)):
+ mailer_factory = SnapBuildMailer.forUploadScanFailure
+ if mailer_factory is not None:
+ mailer_factory(self.snapbuild).sendAll()
+ raise
+ except Exception:
# The normal job infrastructure will abort the transaction, but
# we want to commit instead: the only database changes we make
# are to this job's metadata and should be preserved.
=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
--- lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-20 13:16:35 +0000
+++ lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-24 12:07:53 +0000
@@ -17,6 +17,7 @@
MatchesListwise,
MatchesStructure,
)
+import transaction
from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
@@ -58,6 +59,17 @@
from lp.testing.mail_helpers import pop_notifications
+def run_isolated_jobs(jobs):
+ """Run a sequence of jobs, ensuring transaction isolation.
+
+ We abort the transaction after each job to make sure that there is no
+ relevant uncommitted work.
+ """
+ for job in jobs:
+ JobRunner([job]).runAll()
+ transaction.abort()
+
+
@implementer(ISnapStoreClient)
class FakeSnapStoreClient:
@@ -161,7 +173,7 @@
client.checkStatus.result = (self.store_url, 1)
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -182,7 +194,7 @@
client.upload.failure = ValueError("An upload failure")
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -208,7 +220,7 @@
"Authorization failed.")
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -254,7 +266,7 @@
"Proxy error", can_retry=True)
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -272,7 +284,7 @@
client.upload.result = self.status_url
client.checkStatus.result = (self.store_url, 1)
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -299,7 +311,7 @@
client.upload.failure = BadRefreshResponse("SSO melted.")
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -350,7 +362,7 @@
"Failed to upload", detail="The proxy exploded.\n")
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -399,7 +411,7 @@
client.checkStatus.failure = UploadNotScannedYetResponse()
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -417,7 +429,7 @@
client.checkStatus.failure = None
client.checkStatus.result = (self.store_url, 1)
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -448,7 +460,7 @@
{"message": "Confinement not allowed.", "link": "link2"}])
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -497,7 +509,7 @@
client.checkStatus.result = (self.store_url, 1)
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual([((snapbuild,), {})], client.upload.calls)
self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -520,7 +532,7 @@
"Proxy error", can_retry=True)
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertNotIn("status_url", job.metadata)
self.assertEqual(timedelta(seconds=60), job.retry_delay)
job.scheduled_start = None
@@ -529,7 +541,7 @@
client.checkStatus.failure = UploadNotScannedYetResponse()
for expected_delay in (15, 15, 30, 30, 60):
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertIn("status_url", job.snapbuild.store_upload_metadata)
self.assertIsNone(job.store_url)
self.assertEqual(
@@ -538,7 +550,7 @@
client.checkStatus.failure = None
client.checkStatus.result = (self.store_url, 1)
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
self.assertEqual(self.store_url, job.store_url)
self.assertIsNone(job.error_message)
self.assertEqual([], pop_notifications())
@@ -556,7 +568,7 @@
client.checkStatus.result = (self.store_url, 1)
self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
previous_upload = client.upload.calls
previous_checkStatus = client.checkStatus.calls
@@ -570,7 +582,7 @@
# Run the job again
with dbuser(config.ISnapStoreUploadJobSource.dbuser):
- JobRunner([job]).runAll()
+ run_isolated_jobs([job])
# Release is not called due to release intent in upload
# but ensure that we have not called upload twice
Follow ups