diff --git a/atst/models/utils.py b/atst/models/utils.py index e7b69032..bde09c91 100644 --- a/atst/models/utils.py +++ b/atst/models/utils.py @@ -7,12 +7,22 @@ from atst.domain.exceptions import ClaimFailedException @contextmanager def claim_for_update(resource, minutes=30): + """ + Claim a mutually exclusive expiring hold on a resource. + Uses the database as a central source of time in case the server clocks have drifted. + + Args: + resource: A SQLAlchemy model instance with a `claimed_until` attribute. + minutes: The maximum amount of time, in minutes, to hold the claim. + """ Model = resource.__class__ claim_until = func.now() + func.cast( sql.functions.concat(minutes, " MINUTES"), Interval ) + # Optimistically query for and update the resource in question. If it's + # already claimed, `rows_updated` will be 0 and we can give up. rows_updated = ( db.session.query(Model) .filter( @@ -26,11 +36,14 @@ def claim_for_update(resource, minutes=30): if rows_updated < 1: raise ClaimFailedException(resource) + # Fetch the claimed resource claimed = db.session.query(Model).filter_by(id=resource.id).one() try: + # Give the resource to the caller. yield claimed finally: + # Release the claim. db.session.query(Model).filter(Model.id == resource.id).filter( Model.claimed_until != None ).update({"claimed_until": None}, synchronize_session="fetch")