def test_release_mirror(initialized_db): """ Mirrors that are SYNC_NOW, regardless of starting time. """ disable_existing_mirrors() mirror, repo = create_mirror_repo_robot(["updated", "created"], repo_name="first") # mysql rounds the milliseconds on update so force that to happen now query = RepoMirrorConfig.update( sync_start_date=mirror.sync_start_date).where( RepoMirrorConfig.id == mirror.id) query.execute() mirror = RepoMirrorConfig.get_by_id(mirror.id) original_sync_start_date = mirror.sync_start_date assert mirror.sync_retries_remaining == 3 mirror = release_mirror(mirror, RepoMirrorStatus.FAIL) assert mirror.sync_retries_remaining == 2 assert mirror.sync_start_date == original_sync_start_date mirror = release_mirror(mirror, RepoMirrorStatus.FAIL) assert mirror.sync_retries_remaining == 1 assert mirror.sync_start_date == original_sync_start_date mirror = release_mirror(mirror, RepoMirrorStatus.FAIL) assert mirror.sync_retries_remaining == 3 assert mirror.sync_start_date > original_sync_start_date
def update_with_transaction(mirror, **kwargs): """ Helper function which updates a Repository's RepoMirrorConfig while also rolling its sync_transaction_id for locking purposes. """ # RepoMirrorConfig attributes which can be modified mutable_attributes = ('is_enabled', 'mirror_type', 'external_reference', 'external_registry_username', 'external_registry_password', 'external_registry_config', 'sync_interval', 'sync_start_date', 'sync_expiration_date', 'sync_retries_remaining', 'sync_status', 'sync_transaction_id') # Key-Value map of changes to make filtered_kwargs = { key: kwargs.pop(key) for key in mutable_attributes if key in kwargs } # Roll the sync_transaction_id to a new value filtered_kwargs['sync_transaction_id'] = uuid_generator() # Generate the query to perform the updates query = (RepoMirrorConfig.update(filtered_kwargs).where( RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id, RepoMirrorConfig.id == mirror.id)) # Apply the change(s) and return the object if successful if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) else: return None
def update_sync_status_to_sync_now(mirror): """ This will change the sync status to SYNC_NOW and set the retries remaining to one, if it is less than one. None will be returned in cases where this is not possible, such as if the mirror is in the SYNCING state. """ if mirror.sync_status == RepoMirrorStatus.SYNCING: return None retries = max(mirror.sync_retries_remaining, 1) query = RepoMirrorConfig.update( sync_transaction_id=uuid_generator(), sync_status=RepoMirrorStatus.SYNC_NOW, sync_expiration_date=None, sync_retries_remaining=retries, ).where( RepoMirrorConfig.id == mirror.id, RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id, ) if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) return None
def update_sync_status_to_cancel(mirror): """ If the mirror is SYNCING, it will be force-claimed (ignoring existing transaction id), and the state will set to NEVER_RUN. None will be returned in cases where this is not possible, such as if the mirror is not in the SYNCING state. """ if ( mirror.sync_status != RepoMirrorStatus.SYNCING and mirror.sync_status != RepoMirrorStatus.SYNC_NOW ): return None query = RepoMirrorConfig.update( sync_transaction_id=uuid_generator(), sync_status=RepoMirrorStatus.NEVER_RUN, sync_expiration_date=None, ).where(RepoMirrorConfig.id == mirror.id) if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) return None
def update_sync_status(mirror, sync_status): """ Update the sync status """ query = (RepoMirrorConfig.update( sync_transaction_id=uuid_generator(), sync_status=sync_status).where( RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id, RepoMirrorConfig.id == mirror.id)) if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) return None
def expire_mirror(mirror): """ Set the mirror to synchronize ASAP and reset its failure count. """ # Set the next-sync date to now # TODO: Verify the `where` conditions would not expire a currently syncing mirror. query = (RepoMirrorConfig.update( sync_transaction_id=uuid_generator(), sync_expiration_date=datetime.utcnow(), sync_retries_remaining=MAX_SYNC_RETRIES).where( RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id, RepoMirrorConfig.id == mirror.id, RepoMirrorConfig.state != RepoMirrorStatus.SYNCING)) # Fetch and return the latest updates if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) # Unable to update expiration date. Perhaps another process has claimed it? return None # TODO: Raise some Exception?
def release_mirror(mirror, sync_status): """ Return a mirror to the queue and update its status. Upon success, move next sync to be at the next interval in the future. Failures remain with current date to ensure they are picked up for repeat attempt. After MAX_SYNC_RETRIES, the next sync will be moved ahead as if it were a success. This is to allow a daily sync, for example, to retry the next day. Without this, users would need to manually run syncs to clear failure state. """ if sync_status == RepoMirrorStatus.FAIL: retries = max(0, mirror.sync_retries_remaining - 1) if sync_status == RepoMirrorStatus.SUCCESS or retries < 1: now = datetime.utcnow() delta = now - mirror.sync_start_date delta_seconds = (delta.days * 24 * 60 * 60) + delta.seconds next_start_date = now + timedelta( seconds=mirror.sync_interval - (delta_seconds % mirror.sync_interval)) retries = MAX_SYNC_RETRIES else: next_start_date = mirror.sync_start_date query = RepoMirrorConfig.update( sync_transaction_id=uuid_generator(), sync_status=sync_status, sync_start_date=next_start_date, sync_expiration_date=None, sync_retries_remaining=retries, ).where( RepoMirrorConfig.id == mirror.id, RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id, ) if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) # Unable to release Mirror. Has it been claimed by another process? return None
def claim_mirror(mirror): """ Attempt to create an exclusive lock on the RepoMirrorConfig and return it. If unable to create the lock, `None` will be returned. """ # Attempt to update the RepoMirrorConfig to mark it as "claimed" now = datetime.utcnow() expiration_date = now + timedelta(seconds=MAX_SYNC_DURATION) query = (RepoMirrorConfig.update( sync_status=RepoMirrorStatus.SYNCING, sync_expiration_date=expiration_date, sync_transaction_id=uuid_generator()).where( RepoMirrorConfig.id == mirror.id, RepoMirrorConfig.sync_transaction_id == mirror.sync_transaction_id) ) # If the update was successful, then it was claimed. Return the updated instance. if query.execute(): return RepoMirrorConfig.get_by_id(mirror.id) return None # Another process must have claimed the mirror faster.