1
0
mirror of https://github.com/django/django.git synced 2025-03-06 07:22:32 +00:00

Refs #29499 -- Fixed race condition in update_or_create() test.

The usage of time.sleep() could result in the update_or_create() thread winning
the race to create the row if the backend takes a while to create a new
connection in the main thread.

Relying on threading.Event ensures that the flow of execution is systematically
yield back and forth between the main thread and the thread in charge of
performing the background update_or_create().
This commit is contained in:
Simon Charette 2025-01-14 19:56:28 -05:00 committed by Sarah Boyce
parent de9f46d707
commit 6cfe00ee43

View File

@ -1,7 +1,8 @@
import time
import traceback
from datetime import date, datetime, timedelta
from threading import Thread
from threading import Event, Thread, Timer
from unittest.mock import patch
from django.core.exceptions import FieldError
from django.db import DatabaseError, IntegrityError, connection
@ -687,56 +688,75 @@ class UpdateOrCreateTransactionTests(TransactionTestCase):
can update while it holds the lock. The updated field isn't a field in
'defaults', so update_or_create() shouldn't have an effect on it.
"""
lock_status = {"lock_count": 0}
locked_for_update = Event()
save_allowed = Event()
def birthday_sleep():
lock_status["lock_count"] += 1
time.sleep(0.5)
def wait_or_fail(event, message):
if not event.wait(5):
raise AssertionError(message)
def birthday_yield():
# At this point the row should be locked as create or update
# defaults are only called once the SELECT FOR UPDATE is issued.
locked_for_update.set()
# Yield back the execution to the main thread until it allows
# save() to proceed.
save_allowed.clear()
return date(1940, 10, 10)
def update_birthday_slowly():
person_save = Person.save
def wait_for_allowed_save(*args, **kwargs):
wait_or_fail(save_allowed, "Test took too long to allow save")
return person_save(*args, **kwargs)
def update_person():
try:
Person.objects.update_or_create(
first_name="John", defaults={"birthday": birthday_sleep}
)
with patch.object(Person, "save", wait_for_allowed_save):
Person.objects.update_or_create(
first_name="John",
defaults={"last_name": "Doe", "birthday": birthday_yield},
)
finally:
# Avoid leaking connection for Oracle
# Avoid leaking connection for Oracle.
connection.close()
def lock_wait(expected_lock_count):
# timeout after ~0.5 seconds
for i in range(20):
time.sleep(0.025)
if lock_status["lock_count"] == expected_lock_count:
return True
self.skipTest("Database took too long to lock the row")
# update_or_create in a separate thread.
t = Thread(target=update_birthday_slowly)
before_start = datetime.now()
t = Thread(target=update_person)
t.start()
lock_wait(1)
wait_or_fail(locked_for_update, "Database took too long to lock row")
# Create object *after* initial attempt by update_or_create to get obj
# but before creation attempt.
Person.objects.create(
person = Person(
first_name="John", last_name="Lennon", birthday=date(1940, 10, 9)
)
lock_wait(2)
# At this point, the thread is pausing for 0.5 seconds, so now attempt
# to modify object before update_or_create() calls save(). This should
# be blocked until after the save().
# Don't use person.save() as it's gated by the save_allowed event.
person_save(person, force_insert=True)
# Now that the row is created allow the update_or_create() logic to
# attempt a save(force_insert) that will inevitably fail and wait
# until it yields back execution after performing a subsequent
# locked select for update with an intent to save(force_update).
locked_for_update.clear()
save_allowed.set()
wait_or_fail(locked_for_update, "Database took too long to lock row")
allow_save = Timer(0.5, save_allowed.set)
before_start = datetime.now()
allow_save.start()
# The following update() should block until the update_or_create()
# initiated save() is allowed to proceed by the `allow_save` timer
# setting `save_allowed` after 0.5 seconds.
Person.objects.filter(first_name="John").update(last_name="NotLennon")
after_update = datetime.now()
# Wait for thread to finish
# Wait for thread to finish.
t.join()
# Check call to update_or_create() succeeded and the subsequent
# (blocked) call to update().
updated_person = Person.objects.get(first_name="John")
self.assertEqual(
updated_person.birthday, date(1940, 10, 10)
) # set by update_or_create()
self.assertEqual(updated_person.last_name, "NotLennon") # set by update()
self.assertGreater(after_update - before_start, timedelta(seconds=1))
# Confirm update_or_create() performed an update.
self.assertEqual(updated_person.birthday, date(1940, 10, 10))
# Confirm update() was the last statement to run.
self.assertEqual(updated_person.last_name, "NotLennon")
# Confirm update() blocked at least the duration of the timer.
self.assertGreater(after_update - before_start, timedelta(seconds=0.5))
class InvalidCreateArgumentsTests(TransactionTestCase):