push_notifications: Lock message while we mark it pending for push.

Deleting a message can race with sending a push notification for it.
b47535d8bb handled the case where the Message row has gone away --
but in such cases, it is also possible for `access_message` to
succeed, but for the save of `user_message.flags` to fail, because the
UserMessage row has been deleted by then.

Take a lock on the Message row over the accesses of, and updates to,
the relevant UserMessage row.  This guarantees that the
message's (non-)existence is consistent across that transaction.

Partial fix for #16502.
This commit is contained in:
Alex Vandiver 2023-05-18 15:51:21 +00:00 committed by Tim Abbott
parent 12310189ed
commit 1184bdc934
1 changed files with 36 additions and 33 deletions

View File

@ -1060,43 +1060,46 @@ def handle_push_notification(user_profile_id: int, missed_message: Dict[str, Any
# BUG: Investigate why it's possible to get here. # BUG: Investigate why it's possible to get here.
return # nocoverage return # nocoverage
try: with transaction.atomic(savepoint=False):
(message, user_message) = access_message(user_profile, missed_message["message_id"]) try:
except JsonableError: (message, user_message) = access_message(
if ArchivedMessage.objects.filter(id=missed_message["message_id"]).exists(): user_profile, missed_message["message_id"], lock_message=True
# If the cause is a race with the message being deleted, )
# that's normal and we have no need to log an error. except JsonableError:
return if ArchivedMessage.objects.filter(id=missed_message["message_id"]).exists():
logging.info( # If the cause is a race with the message being deleted,
"Unexpected message access failure handling push notifications: %s %s", # that's normal and we have no need to log an error.
user_profile.id, return
missed_message["message_id"], logging.info(
) "Unexpected message access failure handling push notifications: %s %s",
return user_profile.id,
if user_message is not None:
# If the user has read the message already, don't push-notify.
if user_message.flags.read or user_message.flags.active_mobile_push_notification:
return
# Otherwise, we mark the message as having an active mobile
# push notification, so that we can send revocation messages
# later.
user_message.flags.active_mobile_push_notification = True
user_message.save(update_fields=["flags"])
else:
# Users should only be getting push notifications into this
# queue for messages they haven't received if they're
# long-term idle; anything else is likely a bug.
if not user_profile.long_term_idle:
logger.error(
"Could not find UserMessage with message_id %s and user_id %s",
missed_message["message_id"], missed_message["message_id"],
user_profile_id,
exc_info=True,
) )
return return
if user_message is not None:
# If the user has read the message already, don't push-notify.
if user_message.flags.read or user_message.flags.active_mobile_push_notification:
return
# Otherwise, we mark the message as having an active mobile
# push notification, so that we can send revocation messages
# later.
user_message.flags.active_mobile_push_notification = True
user_message.save(update_fields=["flags"])
else:
# Users should only be getting push notifications into this
# queue for messages they haven't received if they're
# long-term idle; anything else is likely a bug.
if not user_profile.long_term_idle:
logger.error(
"Could not find UserMessage with message_id %s and user_id %s",
missed_message["message_id"],
user_profile_id,
exc_info=True,
)
return
trigger = missed_message["trigger"] trigger = missed_message["trigger"]
mentioned_user_group_name = None mentioned_user_group_name = None
# mentioned_user_group_id will be None if the user is personally mentioned # mentioned_user_group_id will be None if the user is personally mentioned