Convert sending mail to async/await. (#7557)

Mainly because sometimes the email push code raises exceptions where the
stack traces have gotten lost, which is hopefully fixed by this.
This commit is contained in:
Erik Johnston 2020-05-22 13:41:11 +01:00 committed by GitHub
parent 66f2ebc22f
commit 06a02bc1ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 76 deletions

1
changelog.d/7557.misc Normal file
View file

@ -0,0 +1 @@
Convert sending mail to async/await.

View file

@ -290,8 +290,7 @@ class IdentityHandler(BaseHandler):
return changed return changed
@defer.inlineCallbacks async def send_threepid_validation(
def send_threepid_validation(
self, self,
email_address, email_address,
client_secret, client_secret,
@ -319,7 +318,7 @@ class IdentityHandler(BaseHandler):
""" """
# Check that this email/client_secret/send_attempt combo is new or # Check that this email/client_secret/send_attempt combo is new or
# greater than what we've seen previously # greater than what we've seen previously
session = yield self.store.get_threepid_validation_session( session = await self.store.get_threepid_validation_session(
"email", client_secret, address=email_address, validated=False "email", client_secret, address=email_address, validated=False
) )
@ -353,7 +352,7 @@ class IdentityHandler(BaseHandler):
# Send the mail with the link containing the token, client_secret # Send the mail with the link containing the token, client_secret
# and session_id # and session_id
try: try:
yield send_email_func(email_address, token, client_secret, session_id) await send_email_func(email_address, token, client_secret, session_id)
except Exception: except Exception:
logger.exception( logger.exception(
"Error sending threepid validation email to %s", email_address "Error sending threepid validation email to %s", email_address
@ -364,7 +363,7 @@ class IdentityHandler(BaseHandler):
self.hs.clock.time_msec() + self.hs.config.email_validation_token_lifetime self.hs.clock.time_msec() + self.hs.config.email_validation_token_lifetime
) )
yield self.store.start_or_continue_validation_session( await self.store.start_or_continue_validation_session(
"email", "email",
email_address, email_address,
session_id, session_id,

View file

@ -15,7 +15,6 @@
import logging import logging
from twisted.internet import defer
from twisted.internet.error import AlreadyCalled, AlreadyCancelled from twisted.internet.error import AlreadyCalled, AlreadyCancelled
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
@ -132,8 +131,7 @@ class EmailPusher(object):
self._is_processing = False self._is_processing = False
self._start_processing() self._start_processing()
@defer.inlineCallbacks async def _process(self):
def _process(self):
# we should never get here if we are already processing # we should never get here if we are already processing
assert not self._is_processing assert not self._is_processing
@ -142,7 +140,7 @@ class EmailPusher(object):
if self.throttle_params is None: if self.throttle_params is None:
# this is our first loop: load up the throttle params # this is our first loop: load up the throttle params
self.throttle_params = yield self.store.get_throttle_params_by_room( self.throttle_params = await self.store.get_throttle_params_by_room(
self.pusher_id self.pusher_id
) )
@ -151,7 +149,7 @@ class EmailPusher(object):
while True: while True:
starting_max_ordering = self.max_stream_ordering starting_max_ordering = self.max_stream_ordering
try: try:
yield self._unsafe_process() await self._unsafe_process()
except Exception: except Exception:
logger.exception("Exception processing notifs") logger.exception("Exception processing notifs")
if self.max_stream_ordering == starting_max_ordering: if self.max_stream_ordering == starting_max_ordering:
@ -159,8 +157,7 @@ class EmailPusher(object):
finally: finally:
self._is_processing = False self._is_processing = False
@defer.inlineCallbacks async def _unsafe_process(self):
def _unsafe_process(self):
""" """
Main logic of the push loop without the wrapper function that sets Main logic of the push loop without the wrapper function that sets
up logging, measures and guards against multiple instances of it up logging, measures and guards against multiple instances of it
@ -168,12 +165,12 @@ class EmailPusher(object):
""" """
start = 0 if INCLUDE_ALL_UNREAD_NOTIFS else self.last_stream_ordering start = 0 if INCLUDE_ALL_UNREAD_NOTIFS else self.last_stream_ordering
fn = self.store.get_unread_push_actions_for_user_in_range_for_email fn = self.store.get_unread_push_actions_for_user_in_range_for_email
unprocessed = yield fn(self.user_id, start, self.max_stream_ordering) unprocessed = await fn(self.user_id, start, self.max_stream_ordering)
soonest_due_at = None soonest_due_at = None
if not unprocessed: if not unprocessed:
yield self.save_last_stream_ordering_and_success(self.max_stream_ordering) await self.save_last_stream_ordering_and_success(self.max_stream_ordering)
return return
for push_action in unprocessed: for push_action in unprocessed:
@ -201,15 +198,15 @@ class EmailPusher(object):
"throttle_ms": self.get_room_throttle_ms(push_action["room_id"]), "throttle_ms": self.get_room_throttle_ms(push_action["room_id"]),
} }
yield self.send_notification(unprocessed, reason) await self.send_notification(unprocessed, reason)
yield self.save_last_stream_ordering_and_success( await self.save_last_stream_ordering_and_success(
max(ea["stream_ordering"] for ea in unprocessed) max(ea["stream_ordering"] for ea in unprocessed)
) )
# we update the throttle on all the possible unprocessed push actions # we update the throttle on all the possible unprocessed push actions
for ea in unprocessed: for ea in unprocessed:
yield self.sent_notif_update_throttle(ea["room_id"], ea) await self.sent_notif_update_throttle(ea["room_id"], ea)
break break
else: else:
if soonest_due_at is None or should_notify_at < soonest_due_at: if soonest_due_at is None or should_notify_at < soonest_due_at:
@ -227,14 +224,13 @@ class EmailPusher(object):
self.seconds_until(soonest_due_at), self.on_timer self.seconds_until(soonest_due_at), self.on_timer
) )
@defer.inlineCallbacks async def save_last_stream_ordering_and_success(self, last_stream_ordering):
def save_last_stream_ordering_and_success(self, last_stream_ordering):
if last_stream_ordering is None: if last_stream_ordering is None:
# This happens if we haven't yet processed anything # This happens if we haven't yet processed anything
return return
self.last_stream_ordering = last_stream_ordering self.last_stream_ordering = last_stream_ordering
pusher_still_exists = yield self.store.update_pusher_last_stream_ordering_and_success( pusher_still_exists = await self.store.update_pusher_last_stream_ordering_and_success(
self.app_id, self.app_id,
self.email, self.email,
self.user_id, self.user_id,
@ -275,13 +271,12 @@ class EmailPusher(object):
may_send_at = last_sent_ts + throttle_ms may_send_at = last_sent_ts + throttle_ms
return may_send_at return may_send_at
@defer.inlineCallbacks async def sent_notif_update_throttle(self, room_id, notified_push_action):
def sent_notif_update_throttle(self, room_id, notified_push_action):
# We have sent a notification, so update the throttle accordingly. # We have sent a notification, so update the throttle accordingly.
# If the event that triggered the notif happened more than # If the event that triggered the notif happened more than
# THROTTLE_RESET_AFTER_MS after the previous one that triggered a # THROTTLE_RESET_AFTER_MS after the previous one that triggered a
# notif, we release the throttle. Otherwise, the throttle is increased. # notif, we release the throttle. Otherwise, the throttle is increased.
time_of_previous_notifs = yield self.store.get_time_of_last_push_action_before( time_of_previous_notifs = await self.store.get_time_of_last_push_action_before(
notified_push_action["stream_ordering"] notified_push_action["stream_ordering"]
) )
@ -310,14 +305,13 @@ class EmailPusher(object):
"last_sent_ts": self.clock.time_msec(), "last_sent_ts": self.clock.time_msec(),
"throttle_ms": new_throttle_ms, "throttle_ms": new_throttle_ms,
} }
yield self.store.set_throttle_params( await self.store.set_throttle_params(
self.pusher_id, room_id, self.throttle_params[room_id] self.pusher_id, room_id, self.throttle_params[room_id]
) )
@defer.inlineCallbacks async def send_notification(self, push_actions, reason):
def send_notification(self, push_actions, reason):
logger.info("Sending notif email for user %r", self.user_id) logger.info("Sending notif email for user %r", self.user_id)
yield self.mailer.send_notification_mail( await self.mailer.send_notification_mail(
self.app_id, self.user_id, self.email, push_actions, reason self.app_id, self.user_id, self.email, push_actions, reason
) )

View file

@ -26,8 +26,6 @@ from six.moves import urllib
import bleach import bleach
import jinja2 import jinja2
from twisted.internet import defer
from synapse.api.constants import EventTypes from synapse.api.constants import EventTypes
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
from synapse.logging.context import make_deferred_yieldable from synapse.logging.context import make_deferred_yieldable
@ -127,8 +125,7 @@ class Mailer(object):
logger.info("Created Mailer for app_name %s" % app_name) logger.info("Created Mailer for app_name %s" % app_name)
@defer.inlineCallbacks async def send_password_reset_mail(self, email_address, token, client_secret, sid):
def send_password_reset_mail(self, email_address, token, client_secret, sid):
"""Send an email with a password reset link to a user """Send an email with a password reset link to a user
Args: Args:
@ -149,14 +146,13 @@ class Mailer(object):
template_vars = {"link": link} template_vars = {"link": link}
yield self.send_email( await self.send_email(
email_address, email_address,
"[%s] Password Reset" % self.hs.config.server_name, "[%s] Password Reset" % self.hs.config.server_name,
template_vars, template_vars,
) )
@defer.inlineCallbacks async def send_registration_mail(self, email_address, token, client_secret, sid):
def send_registration_mail(self, email_address, token, client_secret, sid):
"""Send an email with a registration confirmation link to a user """Send an email with a registration confirmation link to a user
Args: Args:
@ -177,14 +173,13 @@ class Mailer(object):
template_vars = {"link": link} template_vars = {"link": link}
yield self.send_email( await self.send_email(
email_address, email_address,
"[%s] Register your Email Address" % self.hs.config.server_name, "[%s] Register your Email Address" % self.hs.config.server_name,
template_vars, template_vars,
) )
@defer.inlineCallbacks async def send_add_threepid_mail(self, email_address, token, client_secret, sid):
def send_add_threepid_mail(self, email_address, token, client_secret, sid):
"""Send an email with a validation link to a user for adding a 3pid to their account """Send an email with a validation link to a user for adding a 3pid to their account
Args: Args:
@ -206,20 +201,19 @@ class Mailer(object):
template_vars = {"link": link} template_vars = {"link": link}
yield self.send_email( await self.send_email(
email_address, email_address,
"[%s] Validate Your Email" % self.hs.config.server_name, "[%s] Validate Your Email" % self.hs.config.server_name,
template_vars, template_vars,
) )
@defer.inlineCallbacks async def send_notification_mail(
def send_notification_mail(
self, app_id, user_id, email_address, push_actions, reason self, app_id, user_id, email_address, push_actions, reason
): ):
"""Send email regarding a user's room notifications""" """Send email regarding a user's room notifications"""
rooms_in_order = deduped_ordered_list([pa["room_id"] for pa in push_actions]) rooms_in_order = deduped_ordered_list([pa["room_id"] for pa in push_actions])
notif_events = yield self.store.get_events( notif_events = await self.store.get_events(
[pa["event_id"] for pa in push_actions] [pa["event_id"] for pa in push_actions]
) )
@ -232,7 +226,7 @@ class Mailer(object):
state_by_room = {} state_by_room = {}
try: try:
user_display_name = yield self.store.get_profile_displayname( user_display_name = await self.store.get_profile_displayname(
UserID.from_string(user_id).localpart UserID.from_string(user_id).localpart
) )
if user_display_name is None: if user_display_name is None:
@ -240,14 +234,13 @@ class Mailer(object):
except StoreError: except StoreError:
user_display_name = user_id user_display_name = user_id
@defer.inlineCallbacks async def _fetch_room_state(room_id):
def _fetch_room_state(room_id): room_state = await self.store.get_current_state_ids(room_id)
room_state = yield self.store.get_current_state_ids(room_id)
state_by_room[room_id] = room_state state_by_room[room_id] = room_state
# Run at most 3 of these at once: sync does 10 at a time but email # Run at most 3 of these at once: sync does 10 at a time but email
# notifs are much less realtime than sync so we can afford to wait a bit. # notifs are much less realtime than sync so we can afford to wait a bit.
yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) await concurrently_execute(_fetch_room_state, rooms_in_order, 3)
# actually sort our so-called rooms_in_order list, most recent room first # actually sort our so-called rooms_in_order list, most recent room first
rooms_in_order.sort(key=lambda r: -(notifs_by_room[r][-1]["received_ts"] or 0)) rooms_in_order.sort(key=lambda r: -(notifs_by_room[r][-1]["received_ts"] or 0))
@ -255,19 +248,19 @@ class Mailer(object):
rooms = [] rooms = []
for r in rooms_in_order: for r in rooms_in_order:
roomvars = yield self.get_room_vars( roomvars = await self.get_room_vars(
r, user_id, notifs_by_room[r], notif_events, state_by_room[r] r, user_id, notifs_by_room[r], notif_events, state_by_room[r]
) )
rooms.append(roomvars) rooms.append(roomvars)
reason["room_name"] = yield calculate_room_name( reason["room_name"] = await calculate_room_name(
self.store, self.store,
state_by_room[reason["room_id"]], state_by_room[reason["room_id"]],
user_id, user_id,
fallback_to_members=True, fallback_to_members=True,
) )
summary_text = yield self.make_summary_text( summary_text = await self.make_summary_text(
notifs_by_room, state_by_room, notif_events, user_id, reason notifs_by_room, state_by_room, notif_events, user_id, reason
) )
@ -282,12 +275,11 @@ class Mailer(object):
"reason": reason, "reason": reason,
} }
yield self.send_email( await self.send_email(
email_address, "[%s] %s" % (self.app_name, summary_text), template_vars email_address, "[%s] %s" % (self.app_name, summary_text), template_vars
) )
@defer.inlineCallbacks async def send_email(self, email_address, subject, template_vars):
def send_email(self, email_address, subject, template_vars):
"""Send an email with the given information and template text""" """Send an email with the given information and template text"""
try: try:
from_string = self.hs.config.email_notif_from % {"app": self.app_name} from_string = self.hs.config.email_notif_from % {"app": self.app_name}
@ -317,7 +309,7 @@ class Mailer(object):
logger.info("Sending email to %s" % email_address) logger.info("Sending email to %s" % email_address)
yield make_deferred_yieldable( await make_deferred_yieldable(
self.sendmail( self.sendmail(
self.hs.config.email_smtp_host, self.hs.config.email_smtp_host,
raw_from, raw_from,
@ -332,13 +324,14 @@ class Mailer(object):
) )
) )
@defer.inlineCallbacks async def get_room_vars(
def get_room_vars(self, room_id, user_id, notifs, notif_events, room_state_ids): self, room_id, user_id, notifs, notif_events, room_state_ids
):
my_member_event_id = room_state_ids[("m.room.member", user_id)] my_member_event_id = room_state_ids[("m.room.member", user_id)]
my_member_event = yield self.store.get_event(my_member_event_id) my_member_event = await self.store.get_event(my_member_event_id)
is_invite = my_member_event.content["membership"] == "invite" is_invite = my_member_event.content["membership"] == "invite"
room_name = yield calculate_room_name(self.store, room_state_ids, user_id) room_name = await calculate_room_name(self.store, room_state_ids, user_id)
room_vars = { room_vars = {
"title": room_name, "title": room_name,
@ -350,7 +343,7 @@ class Mailer(object):
if not is_invite: if not is_invite:
for n in notifs: for n in notifs:
notifvars = yield self.get_notif_vars( notifvars = await self.get_notif_vars(
n, user_id, notif_events[n["event_id"]], room_state_ids n, user_id, notif_events[n["event_id"]], room_state_ids
) )
@ -377,9 +370,8 @@ class Mailer(object):
return room_vars return room_vars
@defer.inlineCallbacks async def get_notif_vars(self, notif, user_id, notif_event, room_state_ids):
def get_notif_vars(self, notif, user_id, notif_event, room_state_ids): results = await self.store.get_events_around(
results = yield self.store.get_events_around(
notif["room_id"], notif["room_id"],
notif["event_id"], notif["event_id"],
before_limit=CONTEXT_BEFORE, before_limit=CONTEXT_BEFORE,
@ -392,25 +384,24 @@ class Mailer(object):
"messages": [], "messages": [],
} }
the_events = yield filter_events_for_client( the_events = await filter_events_for_client(
self.storage, user_id, results["events_before"] self.storage, user_id, results["events_before"]
) )
the_events.append(notif_event) the_events.append(notif_event)
for event in the_events: for event in the_events:
messagevars = yield self.get_message_vars(notif, event, room_state_ids) messagevars = await self.get_message_vars(notif, event, room_state_ids)
if messagevars is not None: if messagevars is not None:
ret["messages"].append(messagevars) ret["messages"].append(messagevars)
return ret return ret
@defer.inlineCallbacks async def get_message_vars(self, notif, event, room_state_ids):
def get_message_vars(self, notif, event, room_state_ids):
if event.type != EventTypes.Message: if event.type != EventTypes.Message:
return return
sender_state_event_id = room_state_ids[("m.room.member", event.sender)] sender_state_event_id = room_state_ids[("m.room.member", event.sender)]
sender_state_event = yield self.store.get_event(sender_state_event_id) sender_state_event = await self.store.get_event(sender_state_event_id)
sender_name = name_from_member_event(sender_state_event) sender_name = name_from_member_event(sender_state_event)
sender_avatar_url = sender_state_event.content.get("avatar_url") sender_avatar_url = sender_state_event.content.get("avatar_url")
@ -460,8 +451,7 @@ class Mailer(object):
return messagevars return messagevars
@defer.inlineCallbacks async def make_summary_text(
def make_summary_text(
self, notifs_by_room, room_state_ids, notif_events, user_id, reason self, notifs_by_room, room_state_ids, notif_events, user_id, reason
): ):
if len(notifs_by_room) == 1: if len(notifs_by_room) == 1:
@ -471,17 +461,17 @@ class Mailer(object):
# If the room has some kind of name, use it, but we don't # If the room has some kind of name, use it, but we don't
# want the generated-from-names one here otherwise we'll # want the generated-from-names one here otherwise we'll
# end up with, "new message from Bob in the Bob room" # end up with, "new message from Bob in the Bob room"
room_name = yield calculate_room_name( room_name = await calculate_room_name(
self.store, room_state_ids[room_id], user_id, fallback_to_members=False self.store, room_state_ids[room_id], user_id, fallback_to_members=False
) )
my_member_event_id = room_state_ids[room_id][("m.room.member", user_id)] my_member_event_id = room_state_ids[room_id][("m.room.member", user_id)]
my_member_event = yield self.store.get_event(my_member_event_id) my_member_event = await self.store.get_event(my_member_event_id)
if my_member_event.content["membership"] == "invite": if my_member_event.content["membership"] == "invite":
inviter_member_event_id = room_state_ids[room_id][ inviter_member_event_id = room_state_ids[room_id][
("m.room.member", my_member_event.sender) ("m.room.member", my_member_event.sender)
] ]
inviter_member_event = yield self.store.get_event( inviter_member_event = await self.store.get_event(
inviter_member_event_id inviter_member_event_id
) )
inviter_name = name_from_member_event(inviter_member_event) inviter_name = name_from_member_event(inviter_member_event)
@ -506,7 +496,7 @@ class Mailer(object):
state_event_id = room_state_ids[room_id][ state_event_id = room_state_ids[room_id][
("m.room.member", event.sender) ("m.room.member", event.sender)
] ]
state_event = yield self.store.get_event(state_event_id) state_event = await self.store.get_event(state_event_id)
sender_name = name_from_member_event(state_event) sender_name = name_from_member_event(state_event)
if sender_name is not None and room_name is not None: if sender_name is not None and room_name is not None:
@ -535,7 +525,7 @@ class Mailer(object):
} }
) )
member_events = yield self.store.get_events( member_events = await self.store.get_events(
[ [
room_state_ids[room_id][("m.room.member", s)] room_state_ids[room_id][("m.room.member", s)]
for s in sender_ids for s in sender_ids
@ -567,7 +557,7 @@ class Mailer(object):
} }
) )
member_events = yield self.store.get_events( member_events = await self.store.get_events(
[room_state_ids[room_id][("m.room.member", s)] for s in sender_ids] [room_state_ids[room_id][("m.room.member", s)] for s in sender_ids]
) )

View file

@ -46,7 +46,7 @@ class PasswordResetTestCase(unittest.HomeserverTestCase):
# Email config. # Email config.
self.email_attempts = [] self.email_attempts = []
def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
self.email_attempts.append(msg) self.email_attempts.append(msg)
return return
@ -358,7 +358,7 @@ class ThreepidEmailRestTestCase(unittest.HomeserverTestCase):
# Email config. # Email config.
self.email_attempts = [] self.email_attempts = []
def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
self.email_attempts.append(msg) self.email_attempts.append(msg)
config["email"] = { config["email"] = {