Merge lp:~cjwatson/launchpad/snap-store-upload-job-commit into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18995
Proposed branch: lp:~cjwatson/launchpad/snap-store-upload-job-commit
Merge into: lp:launchpad
Diff against target: 277 lines (+71/-52)
2 files modified
lib/lp/snappy/model/snapbuildjob.py (+43/-36)
lib/lp/snappy/tests/test_snapbuildjob.py (+28/-16)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-store-upload-job-commit
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+369243@code.launchpad.net

Commit message

Consistently commit transactions in SnapStoreUploadJob.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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 @@
1# Copyright 2016-2017 Canonical Ltd. This software is licensed under the1# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap build jobs."""4"""Snap build jobs."""
@@ -280,11 +280,15 @@
280 # Ideally we'd just override Job._set_status or similar, but280 # Ideally we'd just override Job._set_status or similar, but
281 # lazr.delegates makes that difficult, so we use this to override all281 # lazr.delegates makes that difficult, so we use this to override all
282 # the individual Job lifecycle methods instead.282 # the individual Job lifecycle methods instead.
283 def _do_lifecycle(self, method_name, *args, **kwargs):283 def _do_lifecycle(self, method_name, manage_transaction=False,
284 *args, **kwargs):
284 old_store_upload_status = self.snapbuild.store_upload_status285 old_store_upload_status = self.snapbuild.store_upload_status
285 getattr(super(SnapStoreUploadJob, self), method_name)(*args, **kwargs)286 getattr(super(SnapStoreUploadJob, self), method_name)(
287 *args, manage_transaction=manage_transaction, **kwargs)
286 if self.snapbuild.store_upload_status != old_store_upload_status:288 if self.snapbuild.store_upload_status != old_store_upload_status:
287 notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild))289 notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild))
290 if manage_transaction:
291 transaction.commit()
288292
289 def start(self, *args, **kwargs):293 def start(self, *args, **kwargs):
290 self._do_lifecycle("start", *args, **kwargs)294 self._do_lifecycle("start", *args, **kwargs)
@@ -329,39 +333,42 @@
329 """See `IRunnableJob`."""333 """See `IRunnableJob`."""
330 client = getUtility(ISnapStoreClient)334 client = getUtility(ISnapStoreClient)
331 try:335 try:
332 if "status_url" not in self.store_metadata:336 try:
333 self.status_url = client.upload(self.snapbuild)337 if "status_url" not in self.store_metadata:
334 # We made progress, so reset attempt_count.338 self.status_url = client.upload(self.snapbuild)
335 self.attempt_count = 1339 # We made progress, so reset attempt_count.
336 if self.store_url is None:340 self.attempt_count = 1
337 # This is no longer strictly necessary as the store is handling341 if self.store_url is None:
338 # releases via the release intent, but we export various fields342 # This is no longer strictly necessary as the store is
339 # via the api, so once this is called, we're done with343 # handling releases via the release intent, but we
340 # this task344 # export various fields via the API, so once this is
341 self.store_url, self.store_revision = (345 # called, we're done with this task.
342 client.checkStatus(self.status_url))346 self.store_url, self.store_revision = (
343 self.error_message = None347 client.checkStatus(self.status_url))
344 except self.retry_error_types:348 self.error_message = None
345 raise349 except self.retry_error_types:
346 except Exception as e:350 raise
347 if (isinstance(e, SnapStoreError) and e.can_retry and351 except Exception as e:
348 self.attempt_count <= self.max_retries):352 if (isinstance(e, SnapStoreError) and e.can_retry and
349 raise RetryableSnapStoreError(e.message, detail=e.detail)353 self.attempt_count <= self.max_retries):
350 self.error_message = str(e)354 raise RetryableSnapStoreError(e.message, detail=e.detail)
351 self.error_messages = getattr(e, "messages", None)355 self.error_message = str(e)
352 self.error_detail = getattr(e, "detail", None)356 self.error_messages = getattr(e, "messages", None)
353 if isinstance(e, UnauthorizedUploadResponse):357 self.error_detail = getattr(e, "detail", None)
354 mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild)358 mailer_factory = None
355 mailer.sendAll()359 if isinstance(e, UnauthorizedUploadResponse):
356 elif isinstance(e, BadRefreshResponse):360 mailer_factory = SnapBuildMailer.forUnauthorizedUpload
357 mailer = SnapBuildMailer.forRefreshFailure(self.snapbuild)361 elif isinstance(e, BadRefreshResponse):
358 mailer.sendAll()362 mailer_factory = SnapBuildMailer.forRefreshFailure
359 elif isinstance(e, UploadFailedResponse):363 elif isinstance(e, UploadFailedResponse):
360 mailer = SnapBuildMailer.forUploadFailure(self.snapbuild)364 mailer_factory = SnapBuildMailer.forUploadFailure
361 mailer.sendAll()365 elif isinstance(e,
362 elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)):366 (BadScanStatusResponse, ScanFailedResponse)):
363 mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild)367 mailer_factory = SnapBuildMailer.forUploadScanFailure
364 mailer.sendAll()368 if mailer_factory is not None:
369 mailer_factory(self.snapbuild).sendAll()
370 raise
371 except Exception:
365 # The normal job infrastructure will abort the transaction, but372 # The normal job infrastructure will abort the transaction, but
366 # we want to commit instead: the only database changes we make373 # we want to commit instead: the only database changes we make
367 # are to this job's metadata and should be preserved.374 # are to this job's metadata and should be preserved.
368375
=== 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 @@
17 MatchesListwise,17 MatchesListwise,
18 MatchesStructure,18 MatchesStructure,
19 )19 )
20import transaction
20from zope.interface import implementer21from zope.interface import implementer
21from zope.security.proxy import removeSecurityProxy22from zope.security.proxy import removeSecurityProxy
2223
@@ -58,6 +59,17 @@
58from lp.testing.mail_helpers import pop_notifications59from lp.testing.mail_helpers import pop_notifications
5960
6061
62def run_isolated_jobs(jobs):
63 """Run a sequence of jobs, ensuring transaction isolation.
64
65 We abort the transaction after each job to make sure that there is no
66 relevant uncommitted work.
67 """
68 for job in jobs:
69 JobRunner([job]).runAll()
70 transaction.abort()
71
72
61@implementer(ISnapStoreClient)73@implementer(ISnapStoreClient)
62class FakeSnapStoreClient:74class FakeSnapStoreClient:
6375
@@ -161,7 +173,7 @@
161 client.checkStatus.result = (self.store_url, 1)173 client.checkStatus.result = (self.store_url, 1)
162 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))174 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
163 with dbuser(config.ISnapStoreUploadJobSource.dbuser):175 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
164 JobRunner([job]).runAll()176 run_isolated_jobs([job])
165 self.assertEqual([((snapbuild,), {})], client.upload.calls)177 self.assertEqual([((snapbuild,), {})], client.upload.calls)
166 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)178 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
167 self.assertContentEqual([job], snapbuild.store_upload_jobs)179 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -182,7 +194,7 @@
182 client.upload.failure = ValueError("An upload failure")194 client.upload.failure = ValueError("An upload failure")
183 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))195 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
184 with dbuser(config.ISnapStoreUploadJobSource.dbuser):196 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
185 JobRunner([job]).runAll()197 run_isolated_jobs([job])
186 self.assertEqual([((snapbuild,), {})], client.upload.calls)198 self.assertEqual([((snapbuild,), {})], client.upload.calls)
187 self.assertEqual([], client.checkStatus.calls)199 self.assertEqual([], client.checkStatus.calls)
188 self.assertContentEqual([job], snapbuild.store_upload_jobs)200 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -208,7 +220,7 @@
208 "Authorization failed.")220 "Authorization failed.")
209 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))221 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
210 with dbuser(config.ISnapStoreUploadJobSource.dbuser):222 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
211 JobRunner([job]).runAll()223 run_isolated_jobs([job])
212 self.assertEqual([((snapbuild,), {})], client.upload.calls)224 self.assertEqual([((snapbuild,), {})], client.upload.calls)
213 self.assertEqual([], client.checkStatus.calls)225 self.assertEqual([], client.checkStatus.calls)
214 self.assertContentEqual([job], snapbuild.store_upload_jobs)226 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -254,7 +266,7 @@
254 "Proxy error", can_retry=True)266 "Proxy error", can_retry=True)
255 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))267 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
256 with dbuser(config.ISnapStoreUploadJobSource.dbuser):268 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
257 JobRunner([job]).runAll()269 run_isolated_jobs([job])
258 self.assertEqual([((snapbuild,), {})], client.upload.calls)270 self.assertEqual([((snapbuild,), {})], client.upload.calls)
259 self.assertEqual([], client.checkStatus.calls)271 self.assertEqual([], client.checkStatus.calls)
260 self.assertContentEqual([job], snapbuild.store_upload_jobs)272 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -272,7 +284,7 @@
272 client.upload.result = self.status_url284 client.upload.result = self.status_url
273 client.checkStatus.result = (self.store_url, 1)285 client.checkStatus.result = (self.store_url, 1)
274 with dbuser(config.ISnapStoreUploadJobSource.dbuser):286 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
275 JobRunner([job]).runAll()287 run_isolated_jobs([job])
276 self.assertEqual([((snapbuild,), {})], client.upload.calls)288 self.assertEqual([((snapbuild,), {})], client.upload.calls)
277 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)289 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
278 self.assertContentEqual([job], snapbuild.store_upload_jobs)290 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -299,7 +311,7 @@
299 client.upload.failure = BadRefreshResponse("SSO melted.")311 client.upload.failure = BadRefreshResponse("SSO melted.")
300 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))312 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
301 with dbuser(config.ISnapStoreUploadJobSource.dbuser):313 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
302 JobRunner([job]).runAll()314 run_isolated_jobs([job])
303 self.assertEqual([((snapbuild,), {})], client.upload.calls)315 self.assertEqual([((snapbuild,), {})], client.upload.calls)
304 self.assertEqual([], client.checkStatus.calls)316 self.assertEqual([], client.checkStatus.calls)
305 self.assertContentEqual([job], snapbuild.store_upload_jobs)317 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -350,7 +362,7 @@
350 "Failed to upload", detail="The proxy exploded.\n")362 "Failed to upload", detail="The proxy exploded.\n")
351 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))363 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
352 with dbuser(config.ISnapStoreUploadJobSource.dbuser):364 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
353 JobRunner([job]).runAll()365 run_isolated_jobs([job])
354 self.assertEqual([((snapbuild,), {})], client.upload.calls)366 self.assertEqual([((snapbuild,), {})], client.upload.calls)
355 self.assertEqual([], client.checkStatus.calls)367 self.assertEqual([], client.checkStatus.calls)
356 self.assertContentEqual([job], snapbuild.store_upload_jobs)368 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -399,7 +411,7 @@
399 client.checkStatus.failure = UploadNotScannedYetResponse()411 client.checkStatus.failure = UploadNotScannedYetResponse()
400 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))412 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
401 with dbuser(config.ISnapStoreUploadJobSource.dbuser):413 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
402 JobRunner([job]).runAll()414 run_isolated_jobs([job])
403 self.assertEqual([((snapbuild,), {})], client.upload.calls)415 self.assertEqual([((snapbuild,), {})], client.upload.calls)
404 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)416 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
405 self.assertContentEqual([job], snapbuild.store_upload_jobs)417 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -417,7 +429,7 @@
417 client.checkStatus.failure = None429 client.checkStatus.failure = None
418 client.checkStatus.result = (self.store_url, 1)430 client.checkStatus.result = (self.store_url, 1)
419 with dbuser(config.ISnapStoreUploadJobSource.dbuser):431 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
420 JobRunner([job]).runAll()432 run_isolated_jobs([job])
421 self.assertEqual([], client.upload.calls)433 self.assertEqual([], client.upload.calls)
422 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)434 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
423 self.assertContentEqual([job], snapbuild.store_upload_jobs)435 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -448,7 +460,7 @@
448 {"message": "Confinement not allowed.", "link": "link2"}])460 {"message": "Confinement not allowed.", "link": "link2"}])
449 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))461 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
450 with dbuser(config.ISnapStoreUploadJobSource.dbuser):462 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
451 JobRunner([job]).runAll()463 run_isolated_jobs([job])
452 self.assertEqual([((snapbuild,), {})], client.upload.calls)464 self.assertEqual([((snapbuild,), {})], client.upload.calls)
453 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)465 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
454 self.assertContentEqual([job], snapbuild.store_upload_jobs)466 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -497,7 +509,7 @@
497 client.checkStatus.result = (self.store_url, 1)509 client.checkStatus.result = (self.store_url, 1)
498 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))510 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
499 with dbuser(config.ISnapStoreUploadJobSource.dbuser):511 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
500 JobRunner([job]).runAll()512 run_isolated_jobs([job])
501 self.assertEqual([((snapbuild,), {})], client.upload.calls)513 self.assertEqual([((snapbuild,), {})], client.upload.calls)
502 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)514 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
503 self.assertContentEqual([job], snapbuild.store_upload_jobs)515 self.assertContentEqual([job], snapbuild.store_upload_jobs)
@@ -520,7 +532,7 @@
520 "Proxy error", can_retry=True)532 "Proxy error", can_retry=True)
521 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))533 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
522 with dbuser(config.ISnapStoreUploadJobSource.dbuser):534 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
523 JobRunner([job]).runAll()535 run_isolated_jobs([job])
524 self.assertNotIn("status_url", job.metadata)536 self.assertNotIn("status_url", job.metadata)
525 self.assertEqual(timedelta(seconds=60), job.retry_delay)537 self.assertEqual(timedelta(seconds=60), job.retry_delay)
526 job.scheduled_start = None538 job.scheduled_start = None
@@ -529,7 +541,7 @@
529 client.checkStatus.failure = UploadNotScannedYetResponse()541 client.checkStatus.failure = UploadNotScannedYetResponse()
530 for expected_delay in (15, 15, 30, 30, 60):542 for expected_delay in (15, 15, 30, 30, 60):
531 with dbuser(config.ISnapStoreUploadJobSource.dbuser):543 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
532 JobRunner([job]).runAll()544 run_isolated_jobs([job])
533 self.assertIn("status_url", job.snapbuild.store_upload_metadata)545 self.assertIn("status_url", job.snapbuild.store_upload_metadata)
534 self.assertIsNone(job.store_url)546 self.assertIsNone(job.store_url)
535 self.assertEqual(547 self.assertEqual(
@@ -538,7 +550,7 @@
538 client.checkStatus.failure = None550 client.checkStatus.failure = None
539 client.checkStatus.result = (self.store_url, 1)551 client.checkStatus.result = (self.store_url, 1)
540 with dbuser(config.ISnapStoreUploadJobSource.dbuser):552 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
541 JobRunner([job]).runAll()553 run_isolated_jobs([job])
542 self.assertEqual(self.store_url, job.store_url)554 self.assertEqual(self.store_url, job.store_url)
543 self.assertIsNone(job.error_message)555 self.assertIsNone(job.error_message)
544 self.assertEqual([], pop_notifications())556 self.assertEqual([], pop_notifications())
@@ -556,7 +568,7 @@
556 client.checkStatus.result = (self.store_url, 1)568 client.checkStatus.result = (self.store_url, 1)
557 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))569 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
558 with dbuser(config.ISnapStoreUploadJobSource.dbuser):570 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
559 JobRunner([job]).runAll()571 run_isolated_jobs([job])
560572
561 previous_upload = client.upload.calls573 previous_upload = client.upload.calls
562 previous_checkStatus = client.checkStatus.calls574 previous_checkStatus = client.checkStatus.calls
@@ -570,7 +582,7 @@
570582
571 # Run the job again583 # Run the job again
572 with dbuser(config.ISnapStoreUploadJobSource.dbuser):584 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
573 JobRunner([job]).runAll()585 run_isolated_jobs([job])
574586
575 # Release is not called due to release intent in upload587 # Release is not called due to release intent in upload
576 # but ensure that we have not called upload twice588 # but ensure that we have not called upload twice