Merge ~robru/britney/+git/britney2-ubuntu:send-emails-daily into ~ubuntu-release/britney/+git/britney2-ubuntu:master

Proposed by Robert Bruce Park on 2017-03-17
Status: Merged
Approved by: Iain Lane on 2017-03-23
Approved revision: 63573fa24a315105178c154906b8686cc84e62e5
Merged at revision: 63573fa24a315105178c154906b8686cc84e62e5
Proposed branch: ~robru/britney/+git/britney2-ubuntu:send-emails-daily
Merge into: ~ubuntu-release/britney/+git/britney2-ubuntu:master
Diff against target: 190 lines (+49/-45)
2 files modified
britney2/policies/email.py (+31/-17)
tests/test_email.py (+18/-28)
Reviewer Review Type Date Requested Status
Iain Lane 2017-03-17 Approve on 2017-03-23
Review via email: mp+320270@code.launchpad.net

Description of the Change

Resend emails periodically, such that 2^daysold days pass in between each mail sent, with a maximum of 30, so that emails are never sent less often than monthly (see tests for explicit list of emailing schedule).

To post a comment you must log in.
Iain Lane (laney) wrote :

Hmm.

Well, I'm just one reviewer, so take this with appropriate weight (if someone else wants to merge, they can).

I think that the initial schedule is too aggressive. I get that you want to psychologically burden people so that they do the work, but I think that we should be giving people some time to actually do it. As an initial proposal, we can remove the '2', and '4' levels? And an idea to ramp up the pressure if you want to do that is to put in the email subject and/or body the amount of times you've reminded about that upload so far (I guess that it should be possible to calculate this so you don't have to store it).

  Subject: [proposed-migration] glib2.0 is stuck in zesty-proposed (2nd reminder)
  Body: You are bad, this is the 2nd time we've emailed you about this.

Also, some comments/questions inline.

review: Needs Information
Robert Bruce Park (robru) wrote :

Not sure I understand your review laney, are you saying you want emails sent after 1 day, then 8 days after that, then 16 days after that, then 30 days thereafter?

Robert Bruce Park (robru) :
Robert Bruce Park (robru) wrote :

Ok, aside from the mail frequency, I think I've addressed all your concerns, please merge. possibly the frequency issue should be decided by vote? Steve wants it to be quite frequent to maximize nagginess.

Iain Lane (laney) wrote :

Righto - the code looks good to me as it stands so on that basis I'm happy, thanks. I've only eyeballed and ran the testsuite though so I could be missing something.

Still not convinced by the aggressiveness of the frequency. I think the initial emails sent at 1, 2, then 4 days is too much. If there's a problem then I think people should be given some time to sort it out. Sending an email the very day after the first one, which itself is only maybe 1 day after the initial upload, is not giving people a reasonable chance to sort out the problem IMO. 2^n might be nice because it's a neat formula but I think it produces bad results. To strawman, I personally think that there should be at least 3 clear days between nags. Backing off as we get further away from the upload date is a nice feature though - I think that aspect should be retained.

Like I said initially, if the consensus of the rest of the team is to disagree with me then somebody else could merge.

(And I still think it would be cute to say in the subject that it's the nth (where n > 1) reminder, but that's not a requirement.)

Iain Lane (laney) wrote :

Ok, it now has a 3 day threshold for everything (valid or not valid).

I think that *might* be acceptable, so let's go with it and see if it turns out to be.

If we need to go back to the 1 and 5 cases, then I propose to set a lower bound on the frequency of 3 days but we'll see when we come to it.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/britney2/policies/email.py b/britney2/policies/email.py
2index 60947c8..c488e58 100644
3--- a/britney2/policies/email.py
4+++ b/britney2/policies/email.py
5@@ -11,6 +11,9 @@ from britney2.policies.rest import Rest
6 from britney2.policies.policy import BasePolicy, PolicyVerdict
7
8
9+# Recurring emails should never be more than this many days apart
10+MAX_FREQUENCY = 30
11+
12 API_PREFIX = 'https://api.launchpad.net/1.0/'
13 USER = API_PREFIX + '~'
14
15@@ -25,13 +28,13 @@ BOTS = {
16 MESSAGE = """From: Ubuntu Release Team <noreply@canonical.com>
17 To: {recipients}
18 X-Proposed-Migration: notice
19-Subject: [proposed-migration] {source_name} {version} stuck in {series}-proposed
20+Subject: [proposed-migration] {source_name} {version} stuck in {series}-proposed for {rounded_age} days.
21
22 Hi,
23
24 {source_name} {version} needs attention.
25
26-It has been stuck in {series}-proposed for {age} day{plural}.
27+It has been stuck in {series}-proposed for {rounded_age} days.
28
29 You either sponsored or uploaded this package, please investigate why it hasn't been approved for migration.
30
31@@ -82,6 +85,8 @@ class EmailPolicy(BasePolicy, Rest):
32 def __init__(self, options, suite_info, dry_run=False):
33 super().__init__('email', options, suite_info, {'unstable'})
34 self.filename = os.path.join(options.unstable, 'EmailCache')
35+ # Maps lp username -> email address
36+ self.addresses = {}
37 # Dict of dicts; maps pkg name -> pkg version -> boolean
38 self.emails_by_pkg = defaultdict(dict)
39 # self.cache contains self.emails_by_pkg from previous run
40@@ -99,6 +104,8 @@ class EmailPolicy(BasePolicy, Rest):
41
42 def _scrape_gpg_emails(self, person):
43 """Find email addresses from one person's GPG keys."""
44+ if person in self.addresses:
45+ return self.addresses[person]
46 addresses = []
47 gpg = self.query_lp_rest_api(person + '/gpg_keys', {})
48 for key in gpg['entries']:
49@@ -121,15 +128,12 @@ class EmailPolicy(BasePolicy, Rest):
50 match = re.match(r'^.*<(.+@.+)>$', uid)
51 if match:
52 addresses.append(match.group(1))
53- return addresses
54+ address = self.addresses[person] = address_chooser(addresses)
55+ return address
56
57 def scrape_gpg_emails(self, people):
58 """Find email addresses from GPG keys."""
59- addresses = []
60- for person in people or []:
61- address = address_chooser(self._scrape_gpg_emails(person))
62- addresses.append(address)
63- return addresses
64+ return [self._scrape_gpg_emails(person) for person in (people or [])]
65
66 def lp_get_emails(self, pkg, version):
67 """Ask LP who uploaded this package."""
68@@ -153,15 +157,24 @@ class EmailPolicy(BasePolicy, Rest):
69
70 def apply_policy_impl(self, email_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
71 """Send email if package is rejected."""
72- # Have more patience for things blocked in update_output.txt
73- max_age = 5 if excuse.is_valid else 1
74 series = self.options.series
75 version = source_data_srcdist.version
76- sent = self.cache.get(source_name, {}).get(version, False)
77 age = excuse.daysold or 0
78- stuck = age >= max_age
79- age = int(age)
80- plural = 's' if age != 1 else ''
81+ rounded_age = int(age)
82+ stuck = age >= 3
83+ cached = self.cache.get(source_name, {}).get(version)
84+ try:
85+ emails, sent_age = cached
86+ sent = (age - sent_age) < min(MAX_FREQUENCY, age / 2.0)
87+ except TypeError:
88+ # This exception happens when source_name, version never seen before
89+ emails = []
90+ sent_age = 0
91+ sent = False
92+ # TODO: This transitional code can only trigger on the first
93+ # production run, feel free to drop this shortly after merging.
94+ if cached is True:
95+ sent = True
96 if self.dry_run:
97 self.log("[email dry run] Considering: %s/%s: %s" %
98 (source_name, version, "stuck" if stuck else "not stuck"))
99@@ -171,7 +184,8 @@ class EmailPolicy(BasePolicy, Rest):
100 # don't update the cache file in dry run mode; we'll see all output each time
101 return PolicyVerdict.PASS
102 if stuck and not sent:
103- emails = self.lp_get_emails(source_name, version)
104+ if not emails:
105+ emails = self.lp_get_emails(source_name, version)
106 if emails:
107 recipients = ', '.join(emails)
108 msg = MESSAGE.format(**locals())
109@@ -181,11 +195,11 @@ class EmailPolicy(BasePolicy, Rest):
110 server = smtplib.SMTP('localhost')
111 server.sendmail('noreply@canonical.com', emails, msg)
112 server.quit()
113- sent = True
114+ sent_age = age
115 except socket.error as err:
116 self.log("Failed to send mail! Is SMTP server running?")
117 self.log(err)
118- self.emails_by_pkg[source_name][version] = sent
119+ self.emails_by_pkg[source_name][version] = (emails, sent_age)
120 self.save_state()
121 return PolicyVerdict.PASS
122
123diff --git a/tests/test_email.py b/tests/test_email.py
124index a456141..fde6405 100755
125--- a/tests/test_email.py
126+++ b/tests/test_email.py
127@@ -193,11 +193,9 @@ class T(unittest.TestCase):
128 """Know when not to send any emails."""
129 lp.return_value = ['example@email.com']
130 e = EmailPolicy(FakeOptions, None)
131- FakeExcuse.is_valid = False
132 FakeExcuse.daysold = 0.002
133 e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
134- FakeExcuse.is_valid = True
135- FakeExcuse.daysold = 4.28
136+ FakeExcuse.daysold = 2.98
137 e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
138 # Would email but no address found
139 FakeExcuse.daysold = 10.12
140@@ -218,33 +216,25 @@ class T(unittest.TestCase):
141
142 @patch('britney2.policies.email.EmailPolicy.lp_get_emails')
143 @patch('britney2.policies.email.smtplib', autospec=True)
144- def test_smtp_days(self, smtp, lp):
145- """Pluralize correctly."""
146- sendmail = smtp.SMTP().sendmail
147+ def test_smtp_repetition(self, smtp, lp):
148+ """Resend mails periodically, with decreasing frequency."""
149 lp.return_value = ['email@address.com']
150+ sendmail = smtp.SMTP().sendmail
151 e = EmailPolicy(FakeOptions, None)
152- FakeExcuse.is_valid = False
153- # day
154- FakeExcuse.daysold = 1.01
155- e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
156- _, args, _ = sendmail.mock_calls[-1]
157- text = args[2]
158- self.assertEquals(sendmail.call_count, 1)
159- self.assertIn(' 1 day.', text)
160- # days
161- FakeExcuse.daysold = 4.9
162- e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
163- _, args, _ = sendmail.mock_calls[-1]
164- text = args[2]
165- self.assertEquals(sendmail.call_count, 2)
166- self.assertIn(' 4 days.', text)
167- # day, exactly 1
168- FakeExcuse.daysold = 1
169- e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
170- _, args, _ = sendmail.mock_calls[-1]
171- text = args[2]
172- self.assertEquals(sendmail.call_count, 3)
173- self.assertIn(' 1 day.', text)
174+ called = []
175+ e.cache = {}
176+ for hours in range(0, 5000):
177+ previous = sendmail.call_count
178+ age = hours / 24
179+ FakeExcuse.daysold = age
180+ e.apply_policy_impl(None, None, 'unity8', None, FakeSourceData, FakeExcuse)
181+ if sendmail.call_count > previous:
182+ e.initialise(None) # Refill e.cache from disk
183+ called.append(age)
184+ # Emails were sent when daysold reached these values:
185+ self.assertSequenceEqual(called, [
186+ 3.0, 6.0, 12.0, 24.0, 48.0, 78.0, 108.0, 138.0, 168.0, 198.0
187+ ])
188
189
190 if __name__ == '__main__':

Subscribers

People subscribed via source and target branches