Merge lp:~cjwatson/launchpad/snap-upload-better-retry into lp:launchpad
- snap-upload-better-retry
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18426 |
Proposed branch: | lp:~cjwatson/launchpad/snap-upload-better-retry |
Merge into: | lp:launchpad |
Diff against target: |
357 lines (+133/-30) 8 files modified
lib/lp/code/model/branchmergeproposaljob.py (+7/-2) lib/lp/code/model/tests/test_branchmergeproposaljobs.py (+34/-0) lib/lp/services/job/model/job.py (+3/-1) lib/lp/services/job/tests/test_celery.py (+10/-10) lib/lp/services/job/tests/test_job.py (+12/-5) lib/lp/services/job/tests/test_runner.py (+2/-1) lib/lp/snappy/model/snapbuildjob.py (+27/-9) lib/lp/snappy/tests/test_snapbuildjob.py (+38/-2) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-upload-better-retry |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+326548@code.launchpad.net |
Commit message
Make SnapStoreUploadJob retries go via celery and be much more responsive.
Description of the change
There were several problems here. The lifecycle method overrides in SnapStoreUploadJob were subtly wrong and thus bypassed celeryRunOnCommit when retrying. The lease was held for much longer than the retry delay, even after the job had finished, which introduced several minutes of unnecessary delay (before fixing this, I checked that all the iterReady implementations honour Job.ready_jobs). Finally, we needed a shorter retry delay for the first few attempts to poll the status endpoint; that polling doesn't involve much work on the Launchpad side, and it may well succeed quite quickly for small packages.
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/code/model/branchmergeproposaljob.py' | |||
2 | --- lib/lp/code/model/branchmergeproposaljob.py 2016-09-06 15:34:38 +0000 | |||
3 | +++ lib/lp/code/model/branchmergeproposaljob.py 2017-06-29 18:31:47 +0000 | |||
4 | @@ -675,8 +675,13 @@ | |||
5 | 675 | continue | 675 | continue |
6 | 676 | # We have now seen this merge proposal. | 676 | # We have now seen this merge proposal. |
7 | 677 | seen_merge_proposals.add(bmp.id) | 677 | seen_merge_proposals.add(bmp.id) |
10 | 678 | # If the job is running, then skip it | 678 | # If the job is running or can't currently be run due to its |
11 | 679 | if job.status == JobStatus.RUNNING: | 679 | # lease or its start time, then skip it. |
12 | 680 | if (job.status == JobStatus.RUNNING or | ||
13 | 681 | (job.lease_expires is not None and | ||
14 | 682 | job.lease_expires >= datetime.now(pytz.UTC)) or | ||
15 | 683 | (job.scheduled_start is not None and | ||
16 | 684 | job.scheduled_start > datetime.now(pytz.UTC))): | ||
17 | 680 | continue | 685 | continue |
18 | 681 | derived_job = bmp_job.makeDerived() | 686 | derived_job = bmp_job.makeDerived() |
19 | 682 | # If the job is an update preview diff, then check that it is | 687 | # If the job is an update preview diff, then check that it is |
20 | 683 | 688 | ||
21 | === modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py' | |||
22 | --- lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2017-05-25 17:22:38 +0000 | |||
23 | +++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2017-06-29 18:31:47 +0000 | |||
24 | @@ -559,6 +559,40 @@ | |||
25 | 559 | jobs = self.job_source.iterReady() | 559 | jobs = self.job_source.iterReady() |
26 | 560 | self.assertEqual(0, len(jobs)) | 560 | self.assertEqual(0, len(jobs)) |
27 | 561 | 561 | ||
28 | 562 | def test_iterReady_new_merge_proposal_update_diff_leased(self): | ||
29 | 563 | # If either the diff or the email job has an acquired lease, then | ||
30 | 564 | # iterReady skips it. | ||
31 | 565 | self.makeBranchMergeProposal( | ||
32 | 566 | set_state=BranchMergeProposalStatus.NEEDS_REVIEW) | ||
33 | 567 | [update_diff_job] = self.job_source.iterReady() | ||
34 | 568 | self.assertIsInstance(update_diff_job, UpdatePreviewDiffJob) | ||
35 | 569 | update_diff_job.acquireLease() | ||
36 | 570 | self.assertEqual(0, len(self.job_source.iterReady())) | ||
37 | 571 | update_diff_job.start() | ||
38 | 572 | update_diff_job.complete() | ||
39 | 573 | [email_job] = self.job_source.iterReady() | ||
40 | 574 | self.assertIsInstance(email_job, MergeProposalNeedsReviewEmailJob) | ||
41 | 575 | email_job.acquireLease() | ||
42 | 576 | self.assertEqual(0, len(self.job_source.iterReady())) | ||
43 | 577 | |||
44 | 578 | def test_iterReady_new_merge_proposal_update_diff_scheduled(self): | ||
45 | 579 | # If either the diff or the email job has a scheduled start time in | ||
46 | 580 | # the future, then iterReady skips it. | ||
47 | 581 | self.makeBranchMergeProposal( | ||
48 | 582 | set_state=BranchMergeProposalStatus.NEEDS_REVIEW) | ||
49 | 583 | [update_diff_job] = self.job_source.iterReady() | ||
50 | 584 | self.assertIsInstance(update_diff_job, UpdatePreviewDiffJob) | ||
51 | 585 | update_diff_job.start() | ||
52 | 586 | update_diff_job.queue() | ||
53 | 587 | self.assertEqual(0, len(self.job_source.iterReady())) | ||
54 | 588 | update_diff_job.start() | ||
55 | 589 | update_diff_job.complete() | ||
56 | 590 | [email_job] = self.job_source.iterReady() | ||
57 | 591 | self.assertIsInstance(email_job, MergeProposalNeedsReviewEmailJob) | ||
58 | 592 | email_job.start() | ||
59 | 593 | email_job.queue() | ||
60 | 594 | self.assertEqual(0, len(self.job_source.iterReady())) | ||
61 | 595 | |||
62 | 562 | def makeBranchMergeProposal(self, set_state=None): | 596 | def makeBranchMergeProposal(self, set_state=None): |
63 | 563 | # Make a merge proposal that would have a ready update diff job. | 597 | # Make a merge proposal that would have a ready update diff job. |
64 | 564 | bmp = self.factory.makeBranchMergeProposal(set_state=set_state) | 598 | bmp = self.factory.makeBranchMergeProposal(set_state=set_state) |
65 | 565 | 599 | ||
66 | === modified file 'lib/lp/services/job/model/job.py' | |||
67 | --- lib/lp/services/job/model/job.py 2015-10-14 15:22:01 +0000 | |||
68 | +++ lib/lp/services/job/model/job.py 2017-06-29 18:31:47 +0000 | |||
69 | @@ -1,4 +1,4 @@ | |||
71 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
72 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
73 | 3 | 3 | ||
74 | 4 | """ORM object representing jobs.""" | 4 | """ORM object representing jobs.""" |
75 | @@ -206,6 +206,8 @@ | |||
76 | 206 | if self.status != JobStatus.WAITING: | 206 | if self.status != JobStatus.WAITING: |
77 | 207 | self._set_status(JobStatus.WAITING) | 207 | self._set_status(JobStatus.WAITING) |
78 | 208 | self.date_finished = datetime.datetime.now(UTC) | 208 | self.date_finished = datetime.datetime.now(UTC) |
79 | 209 | # Release the lease to allow short retry delays to be effective. | ||
80 | 210 | self.lease_expires = None | ||
81 | 209 | if add_commit_hook is not None: | 211 | if add_commit_hook is not None: |
82 | 210 | add_commit_hook() | 212 | add_commit_hook() |
83 | 211 | if manage_transaction: | 213 | if manage_transaction: |
84 | 212 | 214 | ||
85 | === modified file 'lib/lp/services/job/tests/test_celery.py' | |||
86 | --- lib/lp/services/job/tests/test_celery.py 2015-08-04 02:26:26 +0000 | |||
87 | +++ lib/lp/services/job/tests/test_celery.py 2017-06-29 18:31:47 +0000 | |||
88 | @@ -1,4 +1,4 @@ | |||
90 | 1 | # Copyright 2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2012-2017 Canonical Ltd. This software is licensed under the |
91 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
92 | 3 | 3 | ||
93 | 4 | """Tests for running jobs via Celery.""" | 4 | """Tests for running jobs via Celery.""" |
94 | @@ -93,9 +93,10 @@ | |||
95 | 93 | self.job.lease_expires = datetime.now(UTC) | 93 | self.job.lease_expires = datetime.now(UTC) |
96 | 94 | raise RetryException | 94 | raise RetryException |
97 | 95 | elif self.job.attempt_count == 2: | 95 | elif self.job.attempt_count == 2: |
101 | 96 | # The retry delay is 5 seconds, but the lease is for nearly | 96 | # The retry delay is 5 seconds, but the lease is for nearly 10 |
102 | 97 | # 10 seconds, so the job will be rescheduled 10 seconds in | 97 | # seconds. However, the job releases the lease when it's |
103 | 98 | # the future. | 98 | # requeued, so the job will again be rescheduled for 5 seconds |
104 | 99 | # (retry_delay) in the future. | ||
105 | 99 | raise RetryException | 100 | raise RetryException |
106 | 100 | 101 | ||
107 | 101 | 102 | ||
108 | @@ -185,10 +186,9 @@ | |||
109 | 185 | iso8601.parse_date(d) | 186 | iso8601.parse_date(d) |
110 | 186 | for d in job.job.base_json_data['dates_started']] | 187 | for d in job.job.base_json_data['dates_started']] |
111 | 187 | 188 | ||
116 | 188 | # The first attempt's lease is set to the end of the job, so | 189 | # The first attempt's lease is set to the end of the job, so the |
117 | 189 | # the second attempt should start roughly 5 seconds after the | 190 | # second attempt should start roughly 5 seconds after the first. The |
118 | 190 | # first. The third attempt has to wait out the full 10 second | 191 | # third attempt should start roughly 5 seconds after the second. |
115 | 191 | # lease, so it should start roughly 10 seconds after the second. | ||
119 | 192 | self.assertThat(dates_started, HasLength(3)) | 192 | self.assertThat(dates_started, HasLength(3)) |
120 | 193 | self.assertThat(dates_started, | 193 | self.assertThat(dates_started, |
121 | 194 | MatchesListwise([ | 194 | MatchesListwise([ |
122 | @@ -197,8 +197,8 @@ | |||
123 | 197 | GreaterThan(dates_started[0] + timedelta(seconds=4)), | 197 | GreaterThan(dates_started[0] + timedelta(seconds=4)), |
124 | 198 | LessThan(dates_started[0] + timedelta(seconds=8))), | 198 | LessThan(dates_started[0] + timedelta(seconds=8))), |
125 | 199 | MatchesAll( | 199 | MatchesAll( |
128 | 200 | GreaterThan(dates_started[1] + timedelta(seconds=8)), | 200 | GreaterThan(dates_started[1] + timedelta(seconds=4)), |
129 | 201 | LessThan(dates_started[1] + timedelta(seconds=13))), | 201 | LessThan(dates_started[1] + timedelta(seconds=8))), |
130 | 202 | ])) | 202 | ])) |
131 | 203 | self.assertEqual(3, job.attempt_count) | 203 | self.assertEqual(3, job.attempt_count) |
132 | 204 | self.assertEqual(JobStatus.COMPLETED, job.status) | 204 | self.assertEqual(JobStatus.COMPLETED, job.status) |
133 | 205 | 205 | ||
134 | === modified file 'lib/lp/services/job/tests/test_job.py' | |||
135 | --- lib/lp/services/job/tests/test_job.py 2015-07-30 01:50:11 +0000 | |||
136 | +++ lib/lp/services/job/tests/test_job.py 2017-06-29 18:31:47 +0000 | |||
137 | @@ -1,4 +1,4 @@ | |||
139 | 1 | # Copyright 2009-2011 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
140 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
141 | 3 | 3 | ||
142 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
143 | @@ -7,11 +7,11 @@ | |||
144 | 7 | datetime, | 7 | datetime, |
145 | 8 | timedelta, | 8 | timedelta, |
146 | 9 | ) | 9 | ) |
147 | 10 | from pytz import UTC | ||
148 | 11 | import time | 10 | import time |
149 | 12 | 11 | ||
150 | 13 | from lazr.jobrunner.jobrunner import LeaseHeld | 12 | from lazr.jobrunner.jobrunner import LeaseHeld |
151 | 14 | import pytz | 13 | import pytz |
152 | 14 | from pytz import UTC | ||
153 | 15 | from storm.locals import Store | 15 | from storm.locals import Store |
154 | 16 | from testtools.matchers import Equals | 16 | from testtools.matchers import Equals |
155 | 17 | import transaction | 17 | import transaction |
156 | @@ -186,6 +186,13 @@ | |||
157 | 186 | self.assertNotEqual(None, job.date_finished) | 186 | self.assertNotEqual(None, job.date_finished) |
158 | 187 | self.assertEqual(job.status, JobStatus.WAITING) | 187 | self.assertEqual(job.status, JobStatus.WAITING) |
159 | 188 | 188 | ||
160 | 189 | def test_queue_clears_lease_expires(self): | ||
161 | 190 | """Queueing a job releases its lease.""" | ||
162 | 191 | job = Job(_status=JobStatus.RUNNING) | ||
163 | 192 | job.lease_expires = UTC_NOW | ||
164 | 193 | job.queue() | ||
165 | 194 | self.assertIsNone(job.lease_expires) | ||
166 | 195 | |||
167 | 189 | def test_suspend(self): | 196 | def test_suspend(self): |
168 | 190 | """A job that is in the WAITING state can be suspended.""" | 197 | """A job that is in the WAITING state can be suspended.""" |
169 | 191 | job = Job(_status=JobStatus.WAITING) | 198 | job = Job(_status=JobStatus.WAITING) |
170 | @@ -218,12 +225,12 @@ | |||
171 | 218 | job.status, | 225 | job.status, |
172 | 219 | JobStatus.WAITING) | 226 | JobStatus.WAITING) |
173 | 220 | 227 | ||
176 | 221 | def test_resume_clears_lease_expiry(self): | 228 | def test_resume_clears_lease_expires(self): |
177 | 222 | """A job that resumes should null out the lease_expiry.""" | 229 | """A job that resumes should null out the lease_expires.""" |
178 | 223 | job = Job(_status=JobStatus.SUSPENDED) | 230 | job = Job(_status=JobStatus.SUSPENDED) |
179 | 224 | job.lease_expires = UTC_NOW | 231 | job.lease_expires = UTC_NOW |
180 | 225 | job.resume() | 232 | job.resume() |
182 | 226 | self.assertIs(None, job.lease_expires) | 233 | self.assertIsNone(job.lease_expires) |
183 | 227 | 234 | ||
184 | 228 | def test_resume_when_running(self): | 235 | def test_resume_when_running(self): |
185 | 229 | """When a job is running, attempting to resume is invalid.""" | 236 | """When a job is running, attempting to resume is invalid.""" |
186 | 230 | 237 | ||
187 | === modified file 'lib/lp/services/job/tests/test_runner.py' | |||
188 | --- lib/lp/services/job/tests/test_runner.py 2016-05-06 09:28:28 +0000 | |||
189 | +++ lib/lp/services/job/tests/test_runner.py 2017-06-29 18:31:47 +0000 | |||
190 | @@ -1,4 +1,4 @@ | |||
192 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the |
193 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
194 | 3 | 3 | ||
195 | 4 | """Tests for job-running facilities.""" | 4 | """Tests for job-running facilities.""" |
196 | @@ -349,6 +349,7 @@ | |||
197 | 349 | MatchesAll( | 349 | MatchesAll( |
198 | 350 | GreaterThan(expected_delay - timedelta(minutes=1)), | 350 | GreaterThan(expected_delay - timedelta(minutes=1)), |
199 | 351 | LessThan(expected_delay + timedelta(minutes=1)))) | 351 | LessThan(expected_delay + timedelta(minutes=1)))) |
200 | 352 | self.assertIsNone(job.lease_expires) | ||
201 | 352 | self.assertNotIn(job, runner.completed_jobs) | 353 | self.assertNotIn(job, runner.completed_jobs) |
202 | 353 | self.assertIn(job, runner.incomplete_jobs) | 354 | self.assertIn(job, runner.incomplete_jobs) |
203 | 354 | 355 | ||
204 | 355 | 356 | ||
205 | === modified file 'lib/lp/snappy/model/snapbuildjob.py' | |||
206 | --- lib/lp/snappy/model/snapbuildjob.py 2017-06-14 10:25:23 +0000 | |||
207 | +++ lib/lp/snappy/model/snapbuildjob.py 2017-06-29 18:31:47 +0000 | |||
208 | @@ -197,7 +197,6 @@ | |||
209 | 197 | ) | 197 | ) |
210 | 198 | 198 | ||
211 | 199 | retry_error_types = (UploadNotScannedYetResponse, RetryableSnapStoreError) | 199 | retry_error_types = (UploadNotScannedYetResponse, RetryableSnapStoreError) |
212 | 200 | retry_delay = timedelta(minutes=1) | ||
213 | 201 | max_retries = 20 | 200 | max_retries = 20 |
214 | 202 | 201 | ||
215 | 203 | config = config.ISnapStoreUploadJobSource | 202 | config = config.ISnapStoreUploadJobSource |
216 | @@ -255,29 +254,29 @@ | |||
217 | 255 | # Ideally we'd just override Job._set_status or similar, but | 254 | # Ideally we'd just override Job._set_status or similar, but |
218 | 256 | # lazr.delegates makes that difficult, so we use this to override all | 255 | # lazr.delegates makes that difficult, so we use this to override all |
219 | 257 | # the individual Job lifecycle methods instead. | 256 | # the individual Job lifecycle methods instead. |
221 | 258 | def _do_lifecycle(self, method, *args, **kwargs): | 257 | def _do_lifecycle(self, method_name, *args, **kwargs): |
222 | 259 | old_store_upload_status = self.snapbuild.store_upload_status | 258 | old_store_upload_status = self.snapbuild.store_upload_status |
224 | 260 | method(*args, **kwargs) | 259 | getattr(super(SnapStoreUploadJob, self), method_name)(*args, **kwargs) |
225 | 261 | if self.snapbuild.store_upload_status != old_store_upload_status: | 260 | if self.snapbuild.store_upload_status != old_store_upload_status: |
226 | 262 | notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild)) | 261 | notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild)) |
227 | 263 | 262 | ||
228 | 264 | def start(self, *args, **kwargs): | 263 | def start(self, *args, **kwargs): |
230 | 265 | self._do_lifecycle(self.job.start, *args, **kwargs) | 264 | self._do_lifecycle("start", *args, **kwargs) |
231 | 266 | 265 | ||
232 | 267 | def complete(self, *args, **kwargs): | 266 | def complete(self, *args, **kwargs): |
234 | 268 | self._do_lifecycle(self.job.complete, *args, **kwargs) | 267 | self._do_lifecycle("complete", *args, **kwargs) |
235 | 269 | 268 | ||
236 | 270 | def fail(self, *args, **kwargs): | 269 | def fail(self, *args, **kwargs): |
238 | 271 | self._do_lifecycle(self.job.fail, *args, **kwargs) | 270 | self._do_lifecycle("fail", *args, **kwargs) |
239 | 272 | 271 | ||
240 | 273 | def queue(self, *args, **kwargs): | 272 | def queue(self, *args, **kwargs): |
242 | 274 | self._do_lifecycle(self.job.queue, *args, **kwargs) | 273 | self._do_lifecycle("queue", *args, **kwargs) |
243 | 275 | 274 | ||
244 | 276 | def suspend(self, *args, **kwargs): | 275 | def suspend(self, *args, **kwargs): |
246 | 277 | self._do_lifecycle(self.job.suspend, *args, **kwargs) | 276 | self._do_lifecycle("suspend", *args, **kwargs) |
247 | 278 | 277 | ||
248 | 279 | def resume(self, *args, **kwargs): | 278 | def resume(self, *args, **kwargs): |
250 | 280 | self._do_lifecycle(self.job.resume, *args, **kwargs) | 279 | self._do_lifecycle("resume", *args, **kwargs) |
251 | 281 | 280 | ||
252 | 282 | def getOopsVars(self): | 281 | def getOopsVars(self): |
253 | 283 | """See `IRunnableJob`.""" | 282 | """See `IRunnableJob`.""" |
254 | @@ -285,15 +284,34 @@ | |||
255 | 285 | oops_vars.append(('error_detail', self.error_detail)) | 284 | oops_vars.append(('error_detail', self.error_detail)) |
256 | 286 | return oops_vars | 285 | return oops_vars |
257 | 287 | 286 | ||
258 | 287 | @property | ||
259 | 288 | def retry_delay(self): | ||
260 | 289 | """See `BaseRunnableJob`.""" | ||
261 | 290 | if "status_url" in self.metadata and self.store_url is None: | ||
262 | 291 | # At the moment we have to poll the status endpoint to find out | ||
263 | 292 | # if the store has finished scanning. Try to deal with easy | ||
264 | 293 | # cases quickly without hammering our job runners or the store | ||
265 | 294 | # too badly. | ||
266 | 295 | delays = (15, 15, 30, 30) | ||
267 | 296 | try: | ||
268 | 297 | return timedelta(seconds=delays[self.attempt_count - 1]) | ||
269 | 298 | except IndexError: | ||
270 | 299 | pass | ||
271 | 300 | return timedelta(minutes=1) | ||
272 | 301 | |||
273 | 288 | def run(self): | 302 | def run(self): |
274 | 289 | """See `IRunnableJob`.""" | 303 | """See `IRunnableJob`.""" |
275 | 290 | client = getUtility(ISnapStoreClient) | 304 | client = getUtility(ISnapStoreClient) |
276 | 291 | try: | 305 | try: |
277 | 292 | if "status_url" not in self.metadata: | 306 | if "status_url" not in self.metadata: |
278 | 293 | self.metadata["status_url"] = client.upload(self.snapbuild) | 307 | self.metadata["status_url"] = client.upload(self.snapbuild) |
279 | 308 | # We made progress, so reset attempt_count. | ||
280 | 309 | self.attempt_count = 1 | ||
281 | 294 | if self.store_url is None: | 310 | if self.store_url is None: |
282 | 295 | self.store_url, self.store_revision = ( | 311 | self.store_url, self.store_revision = ( |
283 | 296 | client.checkStatus(self.metadata["status_url"])) | 312 | client.checkStatus(self.metadata["status_url"])) |
284 | 313 | # We made progress, so reset attempt_count. | ||
285 | 314 | self.attempt_count = 1 | ||
286 | 297 | if self.snapbuild.snap.store_channels: | 315 | if self.snapbuild.snap.store_channels: |
287 | 298 | if self.store_revision is None: | 316 | if self.store_revision is None: |
288 | 299 | raise ManualReview( | 317 | raise ManualReview( |
289 | 300 | 318 | ||
290 | === modified file 'lib/lp/snappy/tests/test_snapbuildjob.py' | |||
291 | --- lib/lp/snappy/tests/test_snapbuildjob.py 2017-06-14 10:25:23 +0000 | |||
292 | +++ lib/lp/snappy/tests/test_snapbuildjob.py 2017-06-29 18:31:47 +0000 | |||
293 | @@ -7,6 +7,8 @@ | |||
294 | 7 | 7 | ||
295 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
296 | 9 | 9 | ||
297 | 10 | from datetime import timedelta | ||
298 | 11 | |||
299 | 10 | from fixtures import FakeLogger | 12 | from fixtures import FakeLogger |
300 | 11 | from testtools.matchers import ( | 13 | from testtools.matchers import ( |
301 | 12 | Equals, | 14 | Equals, |
302 | @@ -256,7 +258,6 @@ | |||
303 | 256 | self.assertWebhookDeliveries(snapbuild, ["Pending"]) | 258 | self.assertWebhookDeliveries(snapbuild, ["Pending"]) |
304 | 257 | # Try again. The upload part of the job is retried, and this time | 259 | # Try again. The upload part of the job is retried, and this time |
305 | 258 | # it succeeds. | 260 | # it succeeds. |
306 | 259 | job.lease_expires = None | ||
307 | 260 | job.scheduled_start = None | 261 | job.scheduled_start = None |
308 | 261 | client.upload.calls = [] | 262 | client.upload.calls = [] |
309 | 262 | client.upload.failure = None | 263 | client.upload.failure = None |
310 | @@ -403,7 +404,6 @@ | |||
311 | 403 | self.assertWebhookDeliveries(snapbuild, ["Pending"]) | 404 | self.assertWebhookDeliveries(snapbuild, ["Pending"]) |
312 | 404 | # Try again. The upload part of the job is not retried, and this | 405 | # Try again. The upload part of the job is not retried, and this |
313 | 405 | # time the scan completes. | 406 | # time the scan completes. |
314 | 406 | job.lease_expires = None | ||
315 | 407 | job.scheduled_start = None | 407 | job.scheduled_start = None |
316 | 408 | client.upload.calls = [] | 408 | client.upload.calls = [] |
317 | 409 | client.checkStatus.calls = [] | 409 | client.checkStatus.calls = [] |
318 | @@ -594,3 +594,39 @@ | |||
319 | 594 | snapbuild.id, footer) | 594 | snapbuild.id, footer) |
320 | 595 | self.assertWebhookDeliveries( | 595 | self.assertWebhookDeliveries( |
321 | 596 | snapbuild, ["Pending", "Failed to release to channels"]) | 596 | snapbuild, ["Pending", "Failed to release to channels"]) |
322 | 597 | |||
323 | 598 | def test_retry_delay(self): | ||
324 | 599 | # The job is retried every minute, unless it just made one of its | ||
325 | 600 | # first four attempts to poll the status endpoint, in which case the | ||
326 | 601 | # delays are 15/15/30/30 seconds. | ||
327 | 602 | self.useFixture(FakeLogger()) | ||
328 | 603 | snapbuild = self.makeSnapBuild() | ||
329 | 604 | job = SnapStoreUploadJob.create(snapbuild) | ||
330 | 605 | client = FakeSnapStoreClient() | ||
331 | 606 | client.upload.failure = UploadFailedResponse( | ||
332 | 607 | "Proxy error", can_retry=True) | ||
333 | 608 | self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient)) | ||
334 | 609 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): | ||
335 | 610 | JobRunner([job]).runAll() | ||
336 | 611 | self.assertNotIn("status_url", job.metadata) | ||
337 | 612 | self.assertEqual(timedelta(seconds=60), job.retry_delay) | ||
338 | 613 | job.scheduled_start = None | ||
339 | 614 | client.upload.failure = None | ||
340 | 615 | client.upload.result = self.status_url | ||
341 | 616 | client.checkStatus.failure = UploadNotScannedYetResponse() | ||
342 | 617 | for expected_delay in (15, 15, 30, 30, 60): | ||
343 | 618 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): | ||
344 | 619 | JobRunner([job]).runAll() | ||
345 | 620 | self.assertIn("status_url", job.metadata) | ||
346 | 621 | self.assertIsNone(job.store_url) | ||
347 | 622 | self.assertEqual( | ||
348 | 623 | timedelta(seconds=expected_delay), job.retry_delay) | ||
349 | 624 | job.scheduled_start = None | ||
350 | 625 | client.checkStatus.failure = None | ||
351 | 626 | client.checkStatus.result = (self.store_url, 1) | ||
352 | 627 | with dbuser(config.ISnapStoreUploadJobSource.dbuser): | ||
353 | 628 | JobRunner([job]).runAll() | ||
354 | 629 | self.assertEqual(self.store_url, job.store_url) | ||
355 | 630 | self.assertIsNone(job.error_message) | ||
356 | 631 | self.assertEqual([], pop_notifications()) | ||
357 | 632 | self.assertEqual(JobStatus.COMPLETED, job.job.status) |