Resend emails periodically.

master
Robert Bruce Park 7 years ago
parent 322108df73
commit 63573fa24a

@ -11,6 +11,9 @@ from britney2.policies.rest import Rest
from britney2.policies.policy import BasePolicy, PolicyVerdict from britney2.policies.policy import BasePolicy, PolicyVerdict
# Recurring emails should never be more than this many days apart
MAX_FREQUENCY = 30
API_PREFIX = 'https://api.launchpad.net/1.0/' API_PREFIX = 'https://api.launchpad.net/1.0/'
USER = API_PREFIX + '~' USER = API_PREFIX + '~'
@ -25,13 +28,13 @@ BOTS = {
MESSAGE = """From: Ubuntu Release Team <noreply@canonical.com> MESSAGE = """From: Ubuntu Release Team <noreply@canonical.com>
To: {recipients} To: {recipients}
X-Proposed-Migration: notice X-Proposed-Migration: notice
Subject: [proposed-migration] {source_name} {version} stuck in {series}-proposed Subject: [proposed-migration] {source_name} {version} stuck in {series}-proposed for {rounded_age} days.
Hi, Hi,
{source_name} {version} needs attention. {source_name} {version} needs attention.
It has been stuck in {series}-proposed for {age} day{plural}. It has been stuck in {series}-proposed for {rounded_age} days.
You either sponsored or uploaded this package, please investigate why it hasn't been approved for migration. You either sponsored or uploaded this package, please investigate why it hasn't been approved for migration.
@ -82,6 +85,8 @@ class EmailPolicy(BasePolicy, Rest):
def __init__(self, options, suite_info, dry_run=False): def __init__(self, options, suite_info, dry_run=False):
super().__init__('email', options, suite_info, {'unstable'}) super().__init__('email', options, suite_info, {'unstable'})
self.filename = os.path.join(options.unstable, 'EmailCache') self.filename = os.path.join(options.unstable, 'EmailCache')
# Maps lp username -> email address
self.addresses = {}
# Dict of dicts; maps pkg name -> pkg version -> boolean # Dict of dicts; maps pkg name -> pkg version -> boolean
self.emails_by_pkg = defaultdict(dict) self.emails_by_pkg = defaultdict(dict)
# self.cache contains self.emails_by_pkg from previous run # self.cache contains self.emails_by_pkg from previous run
@ -99,6 +104,8 @@ class EmailPolicy(BasePolicy, Rest):
def _scrape_gpg_emails(self, person): def _scrape_gpg_emails(self, person):
"""Find email addresses from one person's GPG keys.""" """Find email addresses from one person's GPG keys."""
if person in self.addresses:
return self.addresses[person]
addresses = [] addresses = []
gpg = self.query_lp_rest_api(person + '/gpg_keys', {}) gpg = self.query_lp_rest_api(person + '/gpg_keys', {})
for key in gpg['entries']: for key in gpg['entries']:
@ -121,15 +128,12 @@ class EmailPolicy(BasePolicy, Rest):
match = re.match(r'^.*<(.+@.+)>$', uid) match = re.match(r'^.*<(.+@.+)>$', uid)
if match: if match:
addresses.append(match.group(1)) addresses.append(match.group(1))
return addresses address = self.addresses[person] = address_chooser(addresses)
return address
def scrape_gpg_emails(self, people): def scrape_gpg_emails(self, people):
"""Find email addresses from GPG keys.""" """Find email addresses from GPG keys."""
addresses = [] return [self._scrape_gpg_emails(person) for person in (people or [])]
for person in people or []:
address = address_chooser(self._scrape_gpg_emails(person))
addresses.append(address)
return addresses
def lp_get_emails(self, pkg, version): def lp_get_emails(self, pkg, version):
"""Ask LP who uploaded this package.""" """Ask LP who uploaded this package."""
@ -153,15 +157,24 @@ class EmailPolicy(BasePolicy, Rest):
def apply_policy_impl(self, email_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse): def apply_policy_impl(self, email_info, suite, source_name, source_data_tdist, source_data_srcdist, excuse):
"""Send email if package is rejected.""" """Send email if package is rejected."""
# Have more patience for things blocked in update_output.txt
max_age = 5 if excuse.is_valid else 1
series = self.options.series series = self.options.series
version = source_data_srcdist.version version = source_data_srcdist.version
sent = self.cache.get(source_name, {}).get(version, False)
age = excuse.daysold or 0 age = excuse.daysold or 0
stuck = age >= max_age rounded_age = int(age)
age = int(age) stuck = age >= 3
plural = 's' if age != 1 else '' cached = self.cache.get(source_name, {}).get(version)
try:
emails, sent_age = cached
sent = (age - sent_age) < min(MAX_FREQUENCY, age / 2.0)
except TypeError:
# This exception happens when source_name, version never seen before
emails = []
sent_age = 0
sent = False
# TODO: This transitional code can only trigger on the first
# production run, feel free to drop this shortly after merging.
if cached is True:
sent = True
if self.dry_run: if self.dry_run:
self.log("[email dry run] Considering: %s/%s: %s" % self.log("[email dry run] Considering: %s/%s: %s" %
(source_name, version, "stuck" if stuck else "not stuck")) (source_name, version, "stuck" if stuck else "not stuck"))
@ -171,7 +184,8 @@ class EmailPolicy(BasePolicy, Rest):
# don't update the cache file in dry run mode; we'll see all output each time # don't update the cache file in dry run mode; we'll see all output each time
return PolicyVerdict.PASS return PolicyVerdict.PASS
if stuck and not sent: if stuck and not sent:
emails = self.lp_get_emails(source_name, version) if not emails:
emails = self.lp_get_emails(source_name, version)
if emails: if emails:
recipients = ', '.join(emails) recipients = ', '.join(emails)
msg = MESSAGE.format(**locals()) msg = MESSAGE.format(**locals())
@ -181,11 +195,11 @@ class EmailPolicy(BasePolicy, Rest):
server = smtplib.SMTP('localhost') server = smtplib.SMTP('localhost')
server.sendmail('noreply@canonical.com', emails, msg) server.sendmail('noreply@canonical.com', emails, msg)
server.quit() server.quit()
sent = True sent_age = age
except socket.error as err: except socket.error as err:
self.log("Failed to send mail! Is SMTP server running?") self.log("Failed to send mail! Is SMTP server running?")
self.log(err) self.log(err)
self.emails_by_pkg[source_name][version] = sent self.emails_by_pkg[source_name][version] = (emails, sent_age)
self.save_state() self.save_state()
return PolicyVerdict.PASS return PolicyVerdict.PASS

@ -193,11 +193,9 @@ class T(unittest.TestCase):
"""Know when not to send any emails.""" """Know when not to send any emails."""
lp.return_value = ['example@email.com'] lp.return_value = ['example@email.com']
e = EmailPolicy(FakeOptions, None) e = EmailPolicy(FakeOptions, None)
FakeExcuse.is_valid = False
FakeExcuse.daysold = 0.002 FakeExcuse.daysold = 0.002
e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse) e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
FakeExcuse.is_valid = True FakeExcuse.daysold = 2.98
FakeExcuse.daysold = 4.28
e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse) e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
# Would email but no address found # Would email but no address found
FakeExcuse.daysold = 10.12 FakeExcuse.daysold = 10.12
@ -218,33 +216,25 @@ class T(unittest.TestCase):
@patch('britney2.policies.email.EmailPolicy.lp_get_emails') @patch('britney2.policies.email.EmailPolicy.lp_get_emails')
@patch('britney2.policies.email.smtplib', autospec=True) @patch('britney2.policies.email.smtplib', autospec=True)
def test_smtp_days(self, smtp, lp): def test_smtp_repetition(self, smtp, lp):
"""Pluralize correctly.""" """Resend mails periodically, with decreasing frequency."""
sendmail = smtp.SMTP().sendmail
lp.return_value = ['email@address.com'] lp.return_value = ['email@address.com']
sendmail = smtp.SMTP().sendmail
e = EmailPolicy(FakeOptions, None) e = EmailPolicy(FakeOptions, None)
FakeExcuse.is_valid = False called = []
# day e.cache = {}
FakeExcuse.daysold = 1.01 for hours in range(0, 5000):
e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse) previous = sendmail.call_count
_, args, _ = sendmail.mock_calls[-1] age = hours / 24
text = args[2] FakeExcuse.daysold = age
self.assertEquals(sendmail.call_count, 1) e.apply_policy_impl(None, None, 'unity8', None, FakeSourceData, FakeExcuse)
self.assertIn(' 1 day.', text) if sendmail.call_count > previous:
# days e.initialise(None) # Refill e.cache from disk
FakeExcuse.daysold = 4.9 called.append(age)
e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse) # Emails were sent when daysold reached these values:
_, args, _ = sendmail.mock_calls[-1] self.assertSequenceEqual(called, [
text = args[2] 3.0, 6.0, 12.0, 24.0, 48.0, 78.0, 108.0, 138.0, 168.0, 198.0
self.assertEquals(sendmail.call_count, 2) ])
self.assertIn(' 4 days.', text)
# day, exactly 1
FakeExcuse.daysold = 1
e.apply_policy_impl(None, None, 'chromium-browser', None, FakeSourceData, FakeExcuse)
_, args, _ = sendmail.mock_calls[-1]
text = args[2]
self.assertEquals(sendmail.call_count, 3)
self.assertIn(' 1 day.', text)
if __name__ == '__main__': if __name__ == '__main__':

Loading…
Cancel
Save