public void CanCancelHealthyOngoingImportWithCorrectClaim() { var nonFinishedOrAbortingStatuses = EnumUtil .GetEnumValues <IStatus>() .Where(s => !ImportStatus.IsFinishedOrAbortingState(s)) .ToList(); nonFinishedOrAbortingStatuses.ForEach(state => { var releaseId = Guid.NewGuid(); var dataFileName = "my_data_file.csv"; var importStatusService = new Mock <IImportStatusService>(); importStatusService .Setup(s => s.GetImportStatus(releaseId, dataFileName)) .ReturnsAsync(new ImportStatus { Status = state }); // Assert that users with the CancelAllFileImports claim can cancel a non-finished-or-aborting Import AssertHandlerSucceedsWithCorrectClaims <ReleaseFileImportInfo, CancelSpecificFileImportRequirement>( new CancelSpecificFileImportAuthorizationHandler(importStatusService.Object), new ReleaseFileImportInfo { ReleaseId = releaseId, DataFileName = dataFileName, }, SecurityClaimTypes.CancelAllFileImports); }); }
public async Task UpdateStatus(Guid releaseId, string dataFileName, IStatus newStatus, double percentageComplete = 0) { var import = await GetImport(releaseId, dataFileName); var currentImportStatus = CreateImportStatusFromImportRow(import); var percentageCompleteBefore = import.PercentageComplete; var percentageCompleteAfter = (int)Math.Clamp(percentageComplete, 0, 100); // Ignore updating if already finished if (currentImportStatus.IsFinished()) { _logger.LogWarning( $"Update: {dataFileName} {currentImportStatus.Status} ({percentageCompleteBefore}%) -> " + $"{newStatus} ({percentageCompleteAfter}%) ignored as this import is already finished"); return; } // Ignore updating if already aborting and the new state is not aborting or finishing if (currentImportStatus.IsAborting() && !ImportStatus.IsFinishedOrAbortingState(newStatus)) { _logger.LogWarning( $"Update: {dataFileName} {currentImportStatus.Status} ({percentageCompleteBefore}%) -> " + $"{newStatus} ({percentageCompleteAfter}%) ignored as this import is already aborting or is finished"); return; } // Ignore updates if attempting to downgrade from a normal importing state to a lower normal importing state, // or if the percentage is being set lower or the same as is currently and is the same state if (!ImportStatus.IsFinishedOrAbortingState(newStatus) && (currentImportStatus.Status.CompareTo(newStatus) > 0 || currentImportStatus.Status == newStatus && percentageCompleteBefore > percentageCompleteAfter)) { _logger.LogWarning( $"Update: {dataFileName} {currentImportStatus.Status} ({percentageCompleteBefore}%) -> " + $"{newStatus} ({percentageCompleteAfter}%) ignored"); return; } // Ignore updating to an equal percentage complete (after rounding) at the same status without logging it if (currentImportStatus.Status == newStatus && percentageCompleteBefore == percentageCompleteAfter) { return; } _logger.LogInformation( $"Update: {dataFileName} {currentImportStatus.Status} ({percentageCompleteBefore}%) -> {newStatus} ({percentageCompleteAfter}%)"); import.PercentageComplete = percentageCompleteAfter; import.Status = newStatus; // If this is a status update that MUST be the "winner" when multiple threads are writing to the same Table // Row in storage, set the If-Match header to "*" (via the ETag) to ensure that this update does not check // for other updates between this entity getting retrieved and getting merged. if (ImportStatus.IsFinishedOrAbortingState(newStatus)) { var mergeOperation = TableOperation.Merge(import); mergeOperation.Entity.ETag = "*"; await _table.ExecuteAsync(mergeOperation); } // Otherwise, attempt to update the Import row and if it fails due to another thread updating it first, // simply ignore and continue. else { try { await _table.ExecuteAsync(TableOperation.Replace(import)); } catch (StorageException e) { if (e.RequestInformation.HttpStatusCode == (int)HttpStatusCode.PreconditionFailed) { // If the table row has been updated in another thread subsequent // to being read above then an exception will be thrown - ignore and continue. // A similar approach will be required if optimistic locking is employed when & if // we switch from table storage to using db tables. } else { throw; } } } }