Skip to content

Commit

Permalink
Add guardrails for UniqueViolation caused by parallel requests
Browse files Browse the repository at this point in the history
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
1 parent 070da38 commit c6dd3ff
Showing 1 changed file with 43 additions and 9 deletions.
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)
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

0 comments on commit c6dd3ff

Please sign in to comment.