Merge lp:~mbp/launchpad/612171-diff-generation-spam into lp:launchpad
- 612171-diff-generation-spam
- Merge into devel
Status: | Superseded | ||||||||
---|---|---|---|---|---|---|---|---|---|
Proposed branch: | lp:~mbp/launchpad/612171-diff-generation-spam | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
188 lines (+57/-10) 4 files modified
lib/lp/code/errors.py (+16/-1) lib/lp/code/model/branchmergeproposaljob.py (+12/-8) lib/lp/code/model/tests/test_branchmergeproposaljobs.py (+27/-1) lib/lp/services/job/runner.py (+2/-0) |
||||||||
To merge this branch: | bzr merge lp:~mbp/launchpad/612171-diff-generation-spam | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Aaron Bentley (community) | Needs Fixing | ||
Ian Booth | Pending | ||
Review via email: mp+80288@code.launchpad.net |
This proposal supersedes a proposal from 2011-10-14.
This proposal has been superseded by a proposal from 2011-11-02.
Commit message
Description of the change
This was proposed into the wrong branch, and anyhow had a confused message, here's the right thing:
* log all job failures, even those caused by user errors
* treat 'branch has pending writes' as a retryable job error, rather than immediately mailing the user
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal | # |
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
On 14 October 2011 11:47, Aaron Bentley <email address hidden> wrote:
> On 11-10-13 07:49 PM, Martin Pool wrote:
>> Oh, perhaps I was unclear, I'm not suppressing mail for all user errors.
>> That would be bad.
>
> I'm glad you're not suppressing all user errors. The point of a user error is that the user should be notified. If it's not something the user should be notified about, then it shouldn't be a user error, and conversely, no user errors should be suppressed.
>
> You are suppressing errors about pending writes, but you are also suppressing errors about missing source and target revisions. I believe that kind of error is something the user should be notified about, because either they made a mistake, or they need to push to the source or target.
OK, so we could certainly handle them differently.
On the case of empty branches:
- that seems like a pretty obscure case; I make mistakes with mps all
the time (even the previous iteration of this one) but I don't think
I've ever done that; it's more common to target the wrong branch
- when it does happen, are we sure it's a user error not something else?
- I don't really understand the user story by which someone would try
to merge to or from an empty branch
- it seems more useful to tell them about it at the time they are
proposing the merge rather than some time later
- if we are going to tell them about it, we ought to say which branch
and what they are expected to do about it
- won't the problem be pretty obvious when they look at the mp page?
If we didn't send mail about it, I doubt it would be a high bug.
> The obvious solution is to raise a different exception when there are pending writes.
yep
>
>> I'm logging all user errors
>
> That's great; I have a branch to do that, also: lp:~abentley/launchpad/log-user-errors
>
> I haven't submitted mine yet, because it doesn't have tests. Unfortunately, it appears you haven't tested that, either. The bug is #850974: No indication when a job resulted in a user error
Personally I'm not very rigorous about testing dev/ops oriented
logging, other than making sure it does not crash (eg when formatting
the string.) If we're not getting the information we want, we can
add more logging. You might say, what if something changes that means
the logs are unintentionally not emitted, but I think often those
changes happen at a level that means a unit test won't catch them (eg
different code is run, or not run.) I would never block adding
logging to wait for tests, but of course I don't decide lp's review
policy.
>
>> Then for this particular error, diff generation failure, which we may agree
>> is not really a user error, I'm suppressing the mail. Perhaps I should also
>> formally reclassify it.
>
> You should definitely formally reclassify it. I think you should do that by raising a different exception for pending writes, but if you want to reclassify all UpdatePreviewDi
ok
> It appears that you've deleted the original merge proposal, which makes it hard to continue the conversation. In the future, please consider resubmitting in these circumstances. ...
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal | # |
> On the case of empty branches:
>
> - that seems like a pretty obscure case; I make mistakes with mps all
> the time (even the previous iteration of this one) but I don't think
> I've ever done that; it's more common to target the wrong branch
Okay. Still, your current approach is swatting a fly with a sledgehammer. Sometimes that's necessary, but in this case, a flyswatter is just as convenient.
> - when it does happen, are we sure it's a user error not something else?
Yes, it's something that significantly reduces the value of their merge proposal, and that they generally have the ability to correct.
> - I don't really understand the user story by which someone would try
> to merge to or from an empty branch
One is that the user proposes a merge into the wrong branch. Another is that they register the branch and forget to push it.
> - it seems more useful to tell them about it at the time they are
> proposing the merge rather than some time later
It is, but because we are not generating the diff at the instant the user requests it, there are race conditions.
> - if we are going to tell them about it, we ought to say which branch
These messages refer to the merge proposal, so they do refer to the branch indirectly. It would be an improvement to refer to them explicitly.
> and what they are expected to do about it
That would be an improvement, sure. I don't think the fact that the message can be improved is a good argument for removing it.
> - won't the problem be pretty obvious when they look at the mp page?
It shouldn't be their responsibility to check the page to ensure the diff was correctly generated. People sometimes don't check it until after reviews have started.
> If we didn't send mail about it, I doubt it would be a high bug.
It would be a regression, and regressions are automatically critical. So yes, it wouldn't be high :-)
>> The bug is #850974: No indication when a job resulted in a user error
>
> I would never block adding
> logging to wait for tests, but of course I don't decide lp's review
> policy.
When I fix a bug, I add a test to make sure that it does not regress, unless that's unreasonably difficult. I don't think that's the case here. In fact, TestCase.
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
> It would be a regression, and regressions are automatically critical. So yes, it wouldn't be high :-)
I think lp's definition of critical is screwy but that's beside the point.
What I meant was: if we had never sent mail about attempts to merge empty branches, I doubt people would be queuing up to ask that we should, in the same way they are repeatedly asking us to stop sending pointless mail.
But, since the empty branch error isn't what bug 612171 and dupes complain about, I will update this branch to treat the pending writes case as a non-user error, and then leave the empty branch cases alone.
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
How about something like this? (The tests don't pass yet.)
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -332,6 +332,15 @@
"""Raised if the the preview diff is not ready to run."""
+class BranchHasPendin
+ """Raised if the branch can't be processed because a write is pending.
+
+ In this case the operation can usually be retried in a while.
+
+ See bug 612171.
+ """
+
+
class UpdatePreviewDi
"""A job to update the preview diff for a branch merge proposal.
@@ -346,6 +355,16 @@
user_
+ retry_error_types = [BranchHasPendi
+
+ @classmethod
+ def create(cls, bmp):
+ """See `IMergeProposal
+ job = BranchMergeProp
+ bmp, cls.class_job_type, {})
+ job.max_retries = 20
+ return cls(job)
+
def checkReady(self):
"""Is this job ready to run?"""
bmp = self.branch_
@@ -356,8 +375,8 @@
raise UpdatePreviewDi
if bmp.source_
- raise UpdatePreviewDi
- 'The source branch has pending writes.')
+ raise BranchHasPendin
+ 'The source branch has pending writes')
@staticmethod
@contextli
@@ -386,11 +405,8 @@
def getErrorRecipie
"""Return a list of email-ids to notify about user errors."""
- # All these failures are either obvious user errors in the web ui (eg
- # nothing in the branch), or Launchpad internal errors (eg the branch
- # is being written to). See bug 612171. Either way, the user can't
- # be anything but annoyed by mail about it. -- mbp 2011-10-13
- return []
+ registrant = self.branch_
+ return format_
class CreateMergeProp
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal | # |
Thanks, Martin, that looks great. Exceptions should go in stable/
Martin Pool (mbp) wrote : | # |
I think this now passes all its tests, but I'm being bitten by bug 871596 so I'm not sure. I'll try it on ec2.
I don't understand the comment on this test: specifically how it is checking jobs that have been waiting for a long period of time:
def test_run_
# If the job has been waiting for a significant period of time (15
# minutes for now), we run the job anyway. The checkReady method
# then raises and this is caught as a user error by the job system,
# and as such sends an email to the error recipients, which for this
# job is the merge proposal registrant.
eric = self.factory.
bmp = self.factory.
job = UpdatePreviewDi
[email] = pop_notifications()
'The source branch has no revisions.',
Martin Pool (mbp) wrote : | # |
This does pass its tests in its current form.
I feel there ought to be a (group of) tests that say something like:
A bmpdj on a branch with pending writes does not generate a mail to the user
The job is retried several times
If the branch never gets up to date the job fails and logs an oops
Perhaps testing that it actually is retried would be out of scope and it's enough to check that we are communicating the right thing to the job infrastructure:
A bmpdj on a branch with pending writes doesn't emit mail, doesn't error, and does ask to be retried.
but this seems to be somewhat covered already.
Why do we actually care about pending writes, anyhow? bzr ought to be fairly happy reading from an in-use repository.
Aaron Bentley (abentley) wrote : | # |
This looks very nice. Since it fixes a bug, it should provide a test to ensure the bug stays fixed, i.e. that a pending write does not generate an emeasily ail.
Was providing retry_error_types as a list rather than a tuple deliberate? Given that the main difference between lists and tuples is that lists are mutable, are you intending the class variable to be mutated? Since that would affect all instances of the class, I think that could be confusing.
We care about pending writes because we want the diffs to be in sync with information displayed on the branch page.
If we generate a diff using the last_scanned_id, the diff will be soon be stale, and if the user deleted the relevant revision, the diff will fail to generate. We would also want to schedule a new diff to be generated once the scan completes.
If we generate a diff using an un-scanned last_mirrored id, it will be newer than the other information we display about the branch, and Launchpad could get upset that we ask it to display information about the revision, when the revision hasn't been scanned yet.
Aaron Bentley (abentley) wrote : | # |
Errr, "i.e. that a pending write does not generate an email."
Martin Pool (mbp) wrote : | # |
> This looks very nice. Since it fixes a bug, it should provide a test to
> ensure the bug stays fixed, i.e. that a pending write does not generate an
> emeasily ail.
>
> Was providing retry_error_types as a list rather than a tuple deliberate?
> Given that the main difference between lists and tuples is that lists are
> mutable, are you intending the class variable to be mutated? Since that would
> affect all instances of the class, I think that could be confusing.
It was intentional. I think there is a semantic difference which is that lists convey there can be any arbitrary number of elements and all the elements are treated identically, which is the case here. I think tuples are generally more important when the structure is more like a mathematical vector with different positions having different meanings or dimensions. I don't feel that getting protection against "accidental" mutation is important here because there are so many other ways that can be broken in Python, most obviously by just assigning to the attribute on either the instance or the class.
But, I don't really care, if Launchpad style or consistency wants a tuple I'll change it.
> We care about pending writes because we want the diffs to be in sync with
> information displayed on the branch page.
>
> If we generate a diff using the last_scanned_id, the diff will be soon be
> stale, and if the user deleted the relevant revision, the diff will fail to
> generate. We would also want to schedule a new diff to be generated once the
> scan completes.
>
> If we generate a diff using an un-scanned last_mirrored id, it will be newer
> than the other information we display about the branch, and Launchpad could
> get upset that we ask it to display information about the revision, when the
> revision hasn't been scanned yet.
OK, thanks for explaining that. That confirms Launchpad should just cope with it, certainly without complaining to the user. It seems like we can get quite similar cases if the user just pushes immediately after the diff job starts, rather than just before.
Aaron Bentley (abentley) wrote : | # |
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 11-10-30 08:49 PM, Martin Pool wrote:
>> Was providing retry_error_types as a list rather than a tuple
>> deliberate? Given that the main difference between lists and
>> tuples is that lists are mutable, are you intending the class
>> variable to be mutated? Since that would affect all instances of
>> the class, I think that could be confusing.
>
> It was intentional. I think there is a semantic difference which
> is that lists convey there can be any arbitrary number of elements
> and all the elements are treated identically, which is the case
> here. I think tuples are generally more important when the
> structure is more like a mathematical vector with different
> positions having different meanings or dimensions.
The meaning you give to tuples is not consistent with the way tuples
are used in except clauses. Except clauses use a tuple to specify a
sequence of fixed but arbitrary length, in which all elements are
treated the same. The retry_error_types member is that portion of an
except clause, extracted to a member variable. isinstance has the
same semantics. So I think a tuple conforms to Python style, and a
list varies from it.
> I don't feel that getting protection against "accidental" mutation
> is important here because there are so many other ways that can be
> broken in Python, most obviously by just assigning to the attribute
> on either the instance or the class.
Python usually assumes that we're consenting adults, so there's very
little you can't do, if you try.
There's a big difference between "self.retry_
"self.retry_
affect only the instance, while the first will affect all instances.
You can affect all instances, if you want, but you have to spell that out.
> But, I don't really care, if Launchpad style or consistency wants a
> tuple I'll change it.
Please do, and consider that it's consistency with Python, as well as
with Launchpad.
Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAk6
tVAAniGZHw1CpL2
=ag6f
-----END PGP SIGNATURE-----
Martin Pool (mbp) wrote : | # |
On 31 October 2011 14:19, Aaron Bentley <email address hidden> wrote:
> The meaning you give to tuples is not consistent with the way tuples
> are used in except clauses. Except clauses use a tuple to specify a
> sequence of fixed but arbitrary length, in which all elements are
> treated the same. The retry_error_types member is that portion of an
> except clause, extracted to a member variable. isinstance has the
> same semantics. So I think a tuple conforms to Python style, and a
> list varies from it.
One interesting thing here is that Python accepts
except [IndexError, ValueError], e:
but it won't actually match them: I guess it may be looking for
old-style exceptions that raised a List or something. So I should
definitely fix that, and it's apparently not being tested enough.
>> I don't feel that getting protection against "accidental" mutation
>> is important here because there are so many other ways that can be
>> broken in Python, most obviously by just assigning to the attribute
>> on either the instance or the class.
>
> Python usually assumes that we're consenting adults, so there's very
> little you can't do, if you try.
>
> There's a big difference between "self.retry_
> "self.retry_
> affect only the instance, while the first will affect all instances.
> You can affect all instances, if you want, but you have to spell that out.
I realize that.
Martin Pool (mbp) wrote : | # |
OK, thanks for your patient reviews, here's another update:
* add a test that the job wants to retry on pending writes and that no mail is sent
* fix up the tuple thing
* simplify (or maybe correct) the code that says the job can be retried
Happily the test does fail if retry_error_types is not a tuple.
Preview Diff
1 | === modified file 'lib/lp/code/errors.py' |
2 | --- lib/lp/code/errors.py 2011-08-24 13:09:29 +0000 |
3 | +++ lib/lp/code/errors.py 2011-11-02 03:08:30 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
6 | +# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
7 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | |
9 | """Errors used in the lp/code modules.""" |
10 | @@ -15,6 +15,7 @@ |
11 | 'BranchCreatorNotMemberOfOwnerTeam', |
12 | 'BranchCreatorNotOwner', |
13 | 'BranchExists', |
14 | + 'BranchHasPendingWrites', |
15 | 'BranchTargetError', |
16 | 'BranchTypeError', |
17 | 'BuildAlreadyPending', |
18 | @@ -39,6 +40,7 @@ |
19 | 'TooManyBuilds', |
20 | 'TooNewRecipeFormat', |
21 | 'UnknownBranchTypeError', |
22 | + 'UpdatePreviewDiffNotReady', |
23 | 'UpgradePending', |
24 | 'UserHasExistingReview', |
25 | 'UserNotBranchReviewer', |
26 | @@ -92,6 +94,15 @@ |
27 | BranchCreationException.__init__(self, message) |
28 | |
29 | |
30 | +class BranchHasPendingWrites(Exception): |
31 | + """Raised if the branch can't be processed because a write is pending. |
32 | + |
33 | + In this case the operation can usually be retried in a while. |
34 | + |
35 | + See bug 612171. |
36 | + """ |
37 | + |
38 | + |
39 | class BranchTargetError(Exception): |
40 | """Raised when there is an error determining a branch target.""" |
41 | |
42 | @@ -284,6 +295,10 @@ |
43 | """The requested review is not in a pending state.""" |
44 | |
45 | |
46 | +class UpdatePreviewDiffNotReady(Exception): |
47 | + """Raised if the the preview diff is not ready to run.""" |
48 | + |
49 | + |
50 | class UserHasExistingReview(Exception): |
51 | """The user has an existing review.""" |
52 | |
53 | |
54 | === modified file 'lib/lp/code/model/branchmergeproposaljob.py' |
55 | --- lib/lp/code/model/branchmergeproposaljob.py 2011-09-26 20:00:50 +0000 |
56 | +++ lib/lp/code/model/branchmergeproposaljob.py 2011-11-02 03:08:30 +0000 |
57 | @@ -1,4 +1,4 @@ |
58 | -# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the |
59 | +# Copyright 2009, 2010, 2011 Canonical Ltd. This software is licensed under the |
60 | # GNU Affero General Public License version 3 (see the file LICENSE). |
61 | |
62 | |
63 | @@ -72,6 +72,10 @@ |
64 | ) |
65 | from lp.code.adapters.branch import BranchMergeProposalDelta |
66 | from lp.code.enums import BranchType |
67 | +from lp.code.errors import ( |
68 | + BranchHasPendingWrites, |
69 | + UpdatePreviewDiffNotReady, |
70 | + ) |
71 | from lp.code.interfaces.branchmergeproposal import ( |
72 | IBranchMergeProposalJob, |
73 | IBranchMergeProposalJobSource, |
74 | @@ -328,10 +332,6 @@ |
75 | self.branch_merge_proposal.target_branch.bzr_identity)) |
76 | |
77 | |
78 | -class UpdatePreviewDiffNotReady(Exception): |
79 | - """Raised if the the preview diff is not ready to run.""" |
80 | - |
81 | - |
82 | class UpdatePreviewDiffJob(BranchMergeProposalJobDerived): |
83 | """A job to update the preview diff for a branch merge proposal. |
84 | |
85 | @@ -346,6 +346,10 @@ |
86 | |
87 | user_error_types = (UpdatePreviewDiffNotReady, ) |
88 | |
89 | + retry_error_types = (BranchHasPendingWrites, ) |
90 | + |
91 | + max_retries = 20 |
92 | + |
93 | def checkReady(self): |
94 | """Is this job ready to run?""" |
95 | bmp = self.branch_merge_proposal |
96 | @@ -356,8 +360,8 @@ |
97 | raise UpdatePreviewDiffNotReady( |
98 | 'The target branch has no revisions.') |
99 | if bmp.source_branch.pending_writes: |
100 | - raise UpdatePreviewDiffNotReady( |
101 | - 'The source branch has pending writes.') |
102 | + raise BranchHasPendingWrites( |
103 | + 'The source branch has pending writes') |
104 | |
105 | @staticmethod |
106 | @contextlib.contextmanager |
107 | @@ -814,7 +818,7 @@ |
108 | if IUpdatePreviewDiffJob.providedBy(derived_job): |
109 | try: |
110 | derived_job.checkReady() |
111 | - except UpdatePreviewDiffNotReady: |
112 | + except (UpdatePreviewDiffNotReady, BranchHasPendingWrites): |
113 | # If the job was created under 15 minutes ago wait a bit. |
114 | minutes = ( |
115 | config.codehosting.update_preview_diff_ready_timeout) |
116 | |
117 | === modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py' |
118 | --- lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2011-09-26 20:00:50 +0000 |
119 | +++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py 2011-11-02 03:08:30 +0000 |
120 | @@ -18,6 +18,11 @@ |
121 | from storm.store import Store |
122 | import transaction |
123 | from zope.component import getUtility |
124 | +from zope.security.proxy import removeSecurityProxy |
125 | + |
126 | +from testtools.matchers import ( |
127 | + Equals, |
128 | + ) |
129 | |
130 | from canonical.config import config |
131 | from canonical.launchpad.webapp.testing import verifyObject |
132 | @@ -55,6 +60,7 @@ |
133 | DiffTestCase, |
134 | ) |
135 | from lp.code.subscribers.branchmergeproposal import merge_proposal_modified |
136 | +from lp.services.job.interfaces.job import JobStatus |
137 | from lp.services.job.model.job import Job |
138 | from lp.services.job.runner import JobRunner |
139 | from lp.services.osutils import override_environ |
140 | @@ -231,7 +237,8 @@ |
141 | self.assertEqual( |
142 | ["preview_diff"], bmp_object_events[0].edited_fields) |
143 | |
144 | - def test_run_branches_not_ready(self): |
145 | + def test_run_branches_empty(self): |
146 | + """If the branches are empty, we tell the user.""" |
147 | # If the job has been waiting for a significant period of time (15 |
148 | # minutes for now), we run the job anyway. The checkReady method |
149 | # then raises and this is caught as a user error by the job system, |
150 | @@ -253,6 +260,25 @@ |
151 | 'The source branch has no revisions.', |
152 | email.get_payload(decode=True)) |
153 | |
154 | + def test_run_branches_pending_writes(self): |
155 | + """If the branches are being written, we retry but don't complain.""" |
156 | + eric = self.factory.makePerson(name='eric', email='eric@example.com') |
157 | + bmp = self.factory.makeBranchMergeProposal(registrant=eric) |
158 | + self.factory.makeRevisionsForBranch(bmp.source_branch, count=1) |
159 | + self.factory.makeRevisionsForBranch(bmp.target_branch, count=1) |
160 | + # Kludge a branch being a bit out of date in a way that will make |
161 | + # pending_writes true, without anything else failing. |
162 | + removeSecurityProxy(bmp.source_branch).last_mirrored_id = \ |
163 | + self.factory.getUniqueString() |
164 | + job = UpdatePreviewDiffJob.create(bmp) |
165 | + # pop_notifications() |
166 | + JobRunner([job]).runAll() |
167 | + emails = pop_notifications() |
168 | + self.assertThat(emails, Equals([])) |
169 | + self.assertThat(job.status, Equals(JobStatus.WAITING)) |
170 | + self.assertThat(job.attempt_count, Equals(1)) |
171 | + self.assertThat(job.max_retries, Equals(20)) |
172 | + |
173 | def test_10_minute_lease(self): |
174 | self.useBzrBranches(direct_database=True) |
175 | bmp = create_example_merge(self)[0] |
176 | |
177 | === modified file 'lib/lp/services/job/runner.py' |
178 | --- lib/lp/services/job/runner.py 2011-10-25 04:15:06 +0000 |
179 | +++ lib/lp/services/job/runner.py 2011-11-02 03:08:30 +0000 |
180 | @@ -258,6 +258,8 @@ |
181 | self.logger.debug('Running %r', job) |
182 | self.runJob(job) |
183 | except job.user_error_types, e: |
184 | + self.logger.info('Job %r failed with user error %r' % |
185 | + (job, e)) |
186 | job.notifyUserError(e) |
187 | except Exception: |
188 | info = sys.exc_info() |
On 11-10-13 07:49 PM, Martin Pool wrote:
> Oh, perhaps I was unclear, I'm not suppressing mail for all user errors.
> That would be bad.
I'm glad you're not suppressing all user errors. The point of a user error is that the user should be notified. If it's not something the user should be notified about, then it shouldn't be a user error, and conversely, no user errors should be suppressed.
You are suppressing errors about pending writes, but you are also suppressing errors about missing source and target revisions. I believe that kind of error is something the user should be notified about, because either they made a mistake, or they need to push to the source or target.
The obvious solution is to raise a different exception when there are pending writes.
> I'm logging all user errors
That's great; I have a branch to do that, also: lp:~abentley/launchpad/log-user-errors
I haven't submitted mine yet, because it doesn't have tests. Unfortunately, it appears you haven't tested that, either. The bug is #850974: No indication when a job resulted in a user error
> Then for this particular error, diff generation failure, which we may agree
> is not really a user error, I'm suppressing the mail. Perhaps I should also
> formally reclassify it.
You should definitely formally reclassify it. I think you should do that by raising a different exception for pending writes, but if you want to reclassify all UpdatePreviewDi ffNotReady exceptions, you can just delete UpdatePreviewDi ffJob.user_ error_types
It appears that you've deleted the original merge proposal, which makes it hard to continue the conversation. In the future, please consider resubmitting in these circumstances. Resubmitting will allow you to change the target branch.