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
1=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
2--- lib/lp/snappy/model/snapbuildjob.py 2019-06-19 14:04:34 +0000
3+++ lib/lp/snappy/model/snapbuildjob.py 2019-06-24 12:07:53 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
6+# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Snap build jobs."""
10@@ -280,11 +280,15 @@
11 # Ideally we'd just override Job._set_status or similar, but
12 # lazr.delegates makes that difficult, so we use this to override all
13 # the individual Job lifecycle methods instead.
14- def _do_lifecycle(self, method_name, *args, **kwargs):
15+ def _do_lifecycle(self, method_name, manage_transaction=False,
16+ *args, **kwargs):
17 old_store_upload_status = self.snapbuild.store_upload_status
18- getattr(super(SnapStoreUploadJob, self), method_name)(*args, **kwargs)
19+ getattr(super(SnapStoreUploadJob, self), method_name)(
20+ *args, manage_transaction=manage_transaction, **kwargs)
21 if self.snapbuild.store_upload_status != old_store_upload_status:
22 notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild))
23+ if manage_transaction:
24+ transaction.commit()
25
26 def start(self, *args, **kwargs):
27 self._do_lifecycle("start", *args, **kwargs)
28@@ -329,39 +333,42 @@
29 """See `IRunnableJob`."""
30 client = getUtility(ISnapStoreClient)
31 try:
32- if "status_url" not in self.store_metadata:
33- self.status_url = client.upload(self.snapbuild)
34- # We made progress, so reset attempt_count.
35- self.attempt_count = 1
36- if self.store_url is None:
37- # This is no longer strictly necessary as the store is handling
38- # releases via the release intent, but we export various fields
39- # via the api, so once this is called, we're done with
40- # this task
41- self.store_url, self.store_revision = (
42- client.checkStatus(self.status_url))
43- self.error_message = None
44- except self.retry_error_types:
45- raise
46- except Exception as e:
47- if (isinstance(e, SnapStoreError) and e.can_retry and
48- self.attempt_count <= self.max_retries):
49- raise RetryableSnapStoreError(e.message, detail=e.detail)
50- self.error_message = str(e)
51- self.error_messages = getattr(e, "messages", None)
52- self.error_detail = getattr(e, "detail", None)
53- if isinstance(e, UnauthorizedUploadResponse):
54- mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild)
55- mailer.sendAll()
56- elif isinstance(e, BadRefreshResponse):
57- mailer = SnapBuildMailer.forRefreshFailure(self.snapbuild)
58- mailer.sendAll()
59- elif isinstance(e, UploadFailedResponse):
60- mailer = SnapBuildMailer.forUploadFailure(self.snapbuild)
61- mailer.sendAll()
62- elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)):
63- mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild)
64- mailer.sendAll()
65+ try:
66+ if "status_url" not in self.store_metadata:
67+ self.status_url = client.upload(self.snapbuild)
68+ # We made progress, so reset attempt_count.
69+ self.attempt_count = 1
70+ if self.store_url is None:
71+ # This is no longer strictly necessary as the store is
72+ # handling releases via the release intent, but we
73+ # export various fields via the API, so once this is
74+ # called, we're done with this task.
75+ self.store_url, self.store_revision = (
76+ client.checkStatus(self.status_url))
77+ self.error_message = None
78+ except self.retry_error_types:
79+ raise
80+ except Exception as e:
81+ if (isinstance(e, SnapStoreError) and e.can_retry and
82+ self.attempt_count <= self.max_retries):
83+ raise RetryableSnapStoreError(e.message, detail=e.detail)
84+ self.error_message = str(e)
85+ self.error_messages = getattr(e, "messages", None)
86+ self.error_detail = getattr(e, "detail", None)
87+ mailer_factory = None
88+ if isinstance(e, UnauthorizedUploadResponse):
89+ mailer_factory = SnapBuildMailer.forUnauthorizedUpload
90+ elif isinstance(e, BadRefreshResponse):
91+ mailer_factory = SnapBuildMailer.forRefreshFailure
92+ elif isinstance(e, UploadFailedResponse):
93+ mailer_factory = SnapBuildMailer.forUploadFailure
94+ elif isinstance(e,
95+ (BadScanStatusResponse, ScanFailedResponse)):
96+ mailer_factory = SnapBuildMailer.forUploadScanFailure
97+ if mailer_factory is not None:
98+ mailer_factory(self.snapbuild).sendAll()
99+ raise
100+ except Exception:
101 # The normal job infrastructure will abort the transaction, but
102 # we want to commit instead: the only database changes we make
103 # are to this job's metadata and should be preserved.
104
105=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
106--- lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-20 13:16:35 +0000
107+++ lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-24 12:07:53 +0000
108@@ -17,6 +17,7 @@
109 MatchesListwise,
110 MatchesStructure,
111 )
112+import transaction
113 from zope.interface import implementer
114 from zope.security.proxy import removeSecurityProxy
115
116@@ -58,6 +59,17 @@
117 from lp.testing.mail_helpers import pop_notifications
118
119
120+def run_isolated_jobs(jobs):
121+ """Run a sequence of jobs, ensuring transaction isolation.
122+
123+ We abort the transaction after each job to make sure that there is no
124+ relevant uncommitted work.
125+ """
126+ for job in jobs:
127+ JobRunner([job]).runAll()
128+ transaction.abort()
129+
130+
131 @implementer(ISnapStoreClient)
132 class FakeSnapStoreClient:
133
134@@ -161,7 +173,7 @@
135 client.checkStatus.result = (self.store_url, 1)
136 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
137 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
138- JobRunner([job]).runAll()
139+ run_isolated_jobs([job])
140 self.assertEqual([((snapbuild,), {})], client.upload.calls)
141 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
142 self.assertContentEqual([job], snapbuild.store_upload_jobs)
143@@ -182,7 +194,7 @@
144 client.upload.failure = ValueError("An upload failure")
145 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
146 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
147- JobRunner([job]).runAll()
148+ run_isolated_jobs([job])
149 self.assertEqual([((snapbuild,), {})], client.upload.calls)
150 self.assertEqual([], client.checkStatus.calls)
151 self.assertContentEqual([job], snapbuild.store_upload_jobs)
152@@ -208,7 +220,7 @@
153 "Authorization failed.")
154 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
155 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
156- JobRunner([job]).runAll()
157+ run_isolated_jobs([job])
158 self.assertEqual([((snapbuild,), {})], client.upload.calls)
159 self.assertEqual([], client.checkStatus.calls)
160 self.assertContentEqual([job], snapbuild.store_upload_jobs)
161@@ -254,7 +266,7 @@
162 "Proxy error", can_retry=True)
163 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
164 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
165- JobRunner([job]).runAll()
166+ run_isolated_jobs([job])
167 self.assertEqual([((snapbuild,), {})], client.upload.calls)
168 self.assertEqual([], client.checkStatus.calls)
169 self.assertContentEqual([job], snapbuild.store_upload_jobs)
170@@ -272,7 +284,7 @@
171 client.upload.result = self.status_url
172 client.checkStatus.result = (self.store_url, 1)
173 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
174- JobRunner([job]).runAll()
175+ run_isolated_jobs([job])
176 self.assertEqual([((snapbuild,), {})], client.upload.calls)
177 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
178 self.assertContentEqual([job], snapbuild.store_upload_jobs)
179@@ -299,7 +311,7 @@
180 client.upload.failure = BadRefreshResponse("SSO melted.")
181 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
182 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
183- JobRunner([job]).runAll()
184+ run_isolated_jobs([job])
185 self.assertEqual([((snapbuild,), {})], client.upload.calls)
186 self.assertEqual([], client.checkStatus.calls)
187 self.assertContentEqual([job], snapbuild.store_upload_jobs)
188@@ -350,7 +362,7 @@
189 "Failed to upload", detail="The proxy exploded.\n")
190 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
191 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
192- JobRunner([job]).runAll()
193+ run_isolated_jobs([job])
194 self.assertEqual([((snapbuild,), {})], client.upload.calls)
195 self.assertEqual([], client.checkStatus.calls)
196 self.assertContentEqual([job], snapbuild.store_upload_jobs)
197@@ -399,7 +411,7 @@
198 client.checkStatus.failure = UploadNotScannedYetResponse()
199 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
200 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
201- JobRunner([job]).runAll()
202+ run_isolated_jobs([job])
203 self.assertEqual([((snapbuild,), {})], client.upload.calls)
204 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
205 self.assertContentEqual([job], snapbuild.store_upload_jobs)
206@@ -417,7 +429,7 @@
207 client.checkStatus.failure = None
208 client.checkStatus.result = (self.store_url, 1)
209 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
210- JobRunner([job]).runAll()
211+ run_isolated_jobs([job])
212 self.assertEqual([], client.upload.calls)
213 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
214 self.assertContentEqual([job], snapbuild.store_upload_jobs)
215@@ -448,7 +460,7 @@
216 {"message": "Confinement not allowed.", "link": "link2"}])
217 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
218 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
219- JobRunner([job]).runAll()
220+ run_isolated_jobs([job])
221 self.assertEqual([((snapbuild,), {})], client.upload.calls)
222 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
223 self.assertContentEqual([job], snapbuild.store_upload_jobs)
224@@ -497,7 +509,7 @@
225 client.checkStatus.result = (self.store_url, 1)
226 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
227 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
228- JobRunner([job]).runAll()
229+ run_isolated_jobs([job])
230 self.assertEqual([((snapbuild,), {})], client.upload.calls)
231 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
232 self.assertContentEqual([job], snapbuild.store_upload_jobs)
233@@ -520,7 +532,7 @@
234 "Proxy error", can_retry=True)
235 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
236 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
237- JobRunner([job]).runAll()
238+ run_isolated_jobs([job])
239 self.assertNotIn("status_url", job.metadata)
240 self.assertEqual(timedelta(seconds=60), job.retry_delay)
241 job.scheduled_start = None
242@@ -529,7 +541,7 @@
243 client.checkStatus.failure = UploadNotScannedYetResponse()
244 for expected_delay in (15, 15, 30, 30, 60):
245 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
246- JobRunner([job]).runAll()
247+ run_isolated_jobs([job])
248 self.assertIn("status_url", job.snapbuild.store_upload_metadata)
249 self.assertIsNone(job.store_url)
250 self.assertEqual(
251@@ -538,7 +550,7 @@
252 client.checkStatus.failure = None
253 client.checkStatus.result = (self.store_url, 1)
254 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
255- JobRunner([job]).runAll()
256+ run_isolated_jobs([job])
257 self.assertEqual(self.store_url, job.store_url)
258 self.assertIsNone(job.error_message)
259 self.assertEqual([], pop_notifications())
260@@ -556,7 +568,7 @@
261 client.checkStatus.result = (self.store_url, 1)
262 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
263 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
264- JobRunner([job]).runAll()
265+ run_isolated_jobs([job])
266
267 previous_upload = client.upload.calls
268 previous_checkStatus = client.checkStatus.calls
269@@ -570,7 +582,7 @@
270
271 # Run the job again
272 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
273- JobRunner([job]).runAll()
274+ run_isolated_jobs([job])
275
276 # Release is not called due to release intent in upload
277 # but ensure that we have not called upload twice