mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 22:43:42 +00:00
scheduled_email: Consistently lock users table.
Only clear_scheduled_emails previously took a lock on the users before
removing them; make deliver_scheduled_emails do so as well, by using
prefetch_related to ensure that the table appears in the SELECT. This
is not necessary for correctness, since all accesses of
ScheduledEmailUser first access the ScheduledEmail and lock it; it is
merely for consistency.
Since SELECT ... FOR UPDATE takes an UPDATE lock on all tables
mentioned in the SELECT, merely doing the prefetch is sufficient to
lock both tables; no `on=(...)` is needed to `select_for_update`.
This also does not address the pre-existing potential deadlock from
these two use cases, where both try to lock the same ScheduledEmail
rows in opposite orders.
(cherry picked from commit 4c518c2bba)
This commit is contained in:
@@ -394,15 +394,15 @@ def clear_scheduled_emails(user_id: int, email_type: Optional[int] = None) -> No
|
|||||||
# We need to obtain a FOR UPDATE lock on the selected rows to keep a concurrent
|
# We need to obtain a FOR UPDATE lock on the selected rows to keep a concurrent
|
||||||
# execution of this function (or something else) from deleting them before we access
|
# execution of this function (or something else) from deleting them before we access
|
||||||
# the .users attribute.
|
# the .users attribute.
|
||||||
items = ScheduledEmail.objects.filter(users__in=[user_id]).select_for_update()
|
items = (
|
||||||
|
ScheduledEmail.objects.filter(users__in=[user_id])
|
||||||
|
.prefetch_related("users")
|
||||||
|
.select_for_update()
|
||||||
|
)
|
||||||
if email_type is not None:
|
if email_type is not None:
|
||||||
items = items.filter(type=email_type)
|
items = items.filter(type=email_type)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
# Now we want a FOR UPDATE lock on the item.users rows
|
|
||||||
# to prevent a concurrent transaction from mutating them
|
|
||||||
# simultaneously.
|
|
||||||
item.users.all().select_for_update()
|
|
||||||
item.users.remove(user_id)
|
item.users.remove(user_id)
|
||||||
if item.users.all().count() == 0:
|
if item.users.all().count() == 0:
|
||||||
# Due to our transaction holding the row lock we have a guarantee
|
# Due to our transaction holding the row lock we have a guarantee
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ Usage: ./manage.py deliver_scheduled_emails
|
|||||||
while True:
|
while True:
|
||||||
found_rows = False
|
found_rows = False
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
email_jobs_to_deliver = ScheduledEmail.objects.filter(
|
email_jobs_to_deliver = (
|
||||||
scheduled_timestamp__lte=timezone_now()
|
ScheduledEmail.objects.filter(scheduled_timestamp__lte=timezone_now())
|
||||||
).select_for_update()
|
.prefetch_related("users")
|
||||||
|
.select_for_update()
|
||||||
|
)
|
||||||
if email_jobs_to_deliver:
|
if email_jobs_to_deliver:
|
||||||
found_rows = True
|
found_rows = True
|
||||||
for job in email_jobs_to_deliver:
|
for job in email_jobs_to_deliver:
|
||||||
|
|||||||
Reference in New Issue
Block a user