Skip to content
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

Add guardrails for UniqueViolation caused by parallel requests #592

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add guardrails for UniqueViolation caused by parallel requests
Sometime, when multiple requests are submitted with the same
parameters, it can cause a UniqueViolation in the database.
Example, when requests A and B started, a value wasn't present so they
both decide to add a value X. A's addition is successful while when
B tries to add it, a UniqueViolation is triggered since A already
added it.

Refers to CLOUDDST-20863
  • Loading branch information
yashvardhannanavati committed Nov 28, 2023
commit 8b53a427aa32801a7d4231dd3700b80fe3cb0b29
52 changes: 43 additions & 9 deletions iib/web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,24 @@ def get_or_create(cls, pull_specification: str) -> Image:
f'Image {pull_specification} should have a tag or a digest specified.'
)

image = cls.query.filter_by(pull_specification=pull_specification).first()
# cls.query triggers an auto-flush of the session by default. So if there are
# multiple requests with same parameters submitted to IIB, call to query pre-maturely
# flushes the contents of the session not allowing our handlers to resolve conflicts.
# https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.params.autoflush
with db.session.no_autoflush:
image = cls.query.filter_by(pull_specification=pull_specification).first()

if not image:
image = Image(pull_specification=pull_specification)
db.session.add(image)
try:
db.session.commit()
# This is a SAVEPOINT so that the rest of the session is not rolled back when
# adding the image conflicts with an already existing row added by another request
# with similar pullspecs is submitted at the same time. When the context manager
# completes, the objects local to it are committed. If an error is raised, it
# rolls back objects local to it while keeping the parent session unaffected.
# https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#using-savepoint
with db.session.begin_nested():
db.session.add(image)
yashvardhannanavati marked this conversation as resolved.
Show resolved Hide resolved
except sqlalchemy.exc.IntegrityError:
current_app.logger.info(
'Image pull specification is already in database. "%s"', pull_specification
Expand Down Expand Up @@ -287,12 +299,23 @@ def get_or_create(cls, name: str) -> Operator:
added to the database session, but not committed, if it was created
:rtype: Operator
"""
operator = cls.query.filter_by(name=name).first()
# cls.query triggers an auto-flush of the session by default. So if there are
# multiple requests with same parameters submitted to IIB, call to query pre-maturely
# flushes the contents of the session not allowing our handlers to resolve conflicts.
# https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.params.autoflush
with db.session.no_autoflush:
operator = cls.query.filter_by(name=name).first()
if not operator:
operator = Operator(name=name)
db.session.add(operator)
try:
db.session.commit()
# This is a SAVEPOINT so that the rest of the session is not rolled back when
# adding the image conflicts with an already existing row added by another request
# with similar pullspecs is submitted at the same time. When the context manager
# completes, the objects local to it are committed. If an error is raised, it
# rolls back objects local to it while keeping the parent session unaffected.
# https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#using-savepoint
with db.session.begin_nested():
db.session.add(operator)
except sqlalchemy.exc.IntegrityError:
current_app.logger.info('Operators is already in database. "%s"', name)
operator = cls.query.filter_by(name=name).first()
Expand Down Expand Up @@ -1721,12 +1744,23 @@ def get_or_create(cls, username: str) -> User:
added to the database session, but not committed, if it was created
:rtype: User
"""
user = cls.query.filter_by(username=username).first()
# cls.query triggers an auto-flush of the session by default. So if there are
# multiple requests with same parameters submitted to IIB, call to query pre-maturely
# flushes the contents of the session not allowing our handlers to resolve conflicts.
# https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.params.autoflush
with db.session.no_autoflush:
user = cls.query.filter_by(username=username).first()
if not user:
user = User(username=username)
db.session.add(user)
try:
db.session.commit()
# This is a SAVEPOINT so that the rest of the session is not rolled back when
# adding the image conflicts with an already existing row added by another request
# with similar pullspecs is submitted at the same time. When the context manager
# completes, the objects local to it are committed. If an error is raised, it
# rolls back objects local to it while keeping the parent session unaffected.
# https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#using-savepoint
with db.session.begin_nested():
db.session.add(user)
except sqlalchemy.exc.IntegrityError:
current_app.logger.info('User is already in database. "%s"', username)
user = cls.query.filter_by(username=username).first()
Expand Down