Skip to content

Fixed get_or_create() to re-raise IntegrityError when object still does not exist. Refs #36489 #19676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Jkhall81
Copy link
Contributor

Trac ticket number

Refs #36489

Branch description

Previously, QuerySet.get_or_create() would suprress all IntegrityError exceptions that occurred during object creation, under the assumption that they were due to race conditions. However, this also masked real issues like missing required fields, unique constraint violations, or invalid foreign keys.

This change modifies the behavior to re-raise the IntegrityError if a subsequent .get() still fails after .create() -- ensuring only true race conditions are handled silently.

Checklist

  • [ x] This PR targets the main branch.
  • [ x] The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • [x ] I have checked the "Has patch" ticket flag in the Trac system.
  • [x ] I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

Notes

  • Includes a PostgreSQL-only regression test (get_or_create_race) using ThreadPoolExecutor to simulate concurrent access.
  • The test confirms that reverse OneToOne cache remains correct and only one object is created in a race.

@Jkhall81 Jkhall81 force-pushed the 36489-fix-get-or-create-cache-bug branch 3 times, most recently from 20ed5e1 to f522c80 Compare July 27, 2025 01:48
@Jkhall81 Jkhall81 force-pushed the 36489-fix-get-or-create-cache-bug branch 3 times, most recently from a3c5faf to 615c54c Compare July 27, 2025 02:02
Copy link
Contributor

@sarahboyce sarahboyce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR

.gitignore Outdated
@@ -17,3 +17,4 @@ tests/.coverage*
build/
tests/report/
tests/screenshots/
test_postgres.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test_postgres.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

@@ -979,17 +979,17 @@ def get_or_create(self, defaults=None, **kwargs):
return self.get(**kwargs), False
except self.model.DoesNotExist:
params = self._extract_model_params(defaults, **kwargs)
# Try to create an object using passed params.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep this comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment has been put back.

@@ -0,0 +1,12 @@
from django.db import models
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to create a new test folder for this fix and can use get_or_create for example
We should try to reuse existing models where we can

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests moved, removed my postgres test.



@unittest.skipUnless(
connection.vendor == "postgresql", "This test only runs on PostgreSQL"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is only being run only on postgres? I don't see anything in the ticket discussion to suggest this is a postgres specific issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use PostgreSQL because this bug revolves around a race condition during concurrent get_or_create() calls involving a OneToOneField. So to reliably reproduce that the database needs to support concurrency.

SQLite doesn't support concurrent writes. It locks the entire database file during a write.
MySQL was failing with errors like Lock wait timeout exceeded.
PostgreSQL was the third one i tried and it allowed me to reproduce the race condition.

raise
# Re-raise if the object still doesn't exist.
# this wasn't a race condition but an integrity error.
raise e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the correct solution. Note the test in the original ticket is still failing for me and based off the ticket discussion, I expect the solution to be more involved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solution updated, and now passes test (modified to use models in get_or_create) from original ticket description.

Copy link
Member

@charettes charettes Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you given any thoughts on how systematically clearing one-to-one relationship cache will break select_related("parent").get_or_create(parent=parent), how this relates to update_or_create and if something similar should be done with defaults and create_defaults as described in the ticket?

I appreciate that the test are passing but it seems like there are still a few open questions about which approach should be taken.

Copy link
Contributor Author

@Jkhall81 Jkhall81 Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding that pruned comment, I was having git issues. Fixed that.

So instead of systematically clearing the one-to-one relationship cache, it should be done conditionally. Only if the cache looks stale. I can make that change real quick. I'm still getting used to how everything works.

I appreciate the guidance. I'll get that changed pushed shortly.

Those other two issues, update_or_create() and defaults / create_defaults, should those be dealt with here? I can take a closer look at both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like update_or_create uses get_or_create internally:

obj, created = self.select_for_update().get_or_create(
                create_defaults, **kwargs
            )

The cache fix will apply here also.

@Jkhall81 Jkhall81 force-pushed the 36489-fix-get-or-create-cache-bug branch 2 times, most recently from 9ad93a5 to 59b52ab Compare July 30, 2025 16:21
@Jkhall81 Jkhall81 requested a review from sarahboyce July 30, 2025 16:27
Comment on lines 975 to 976
# The get() needs to be targeted at the write database in order
# to avoid potential transaction consistency problems.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you prune this comment?

@Jkhall81 Jkhall81 force-pushed the 36489-fix-get-or-create-cache-bug branch from 59b52ab to 7c81ccc Compare July 30, 2025 18:11
@Jkhall81 Jkhall81 force-pushed the 36489-fix-get-or-create-cache-bug branch from 7c81ccc to 7279535 Compare July 30, 2025 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants