Example #1
0
        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;
                    }
                }
            }
        }