コード例 #1
0
        public async Task <GetExportResponse> Handle(GetExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            ExportJobOutcome outcome = await _fhirOperationDataStore.GetExportJobByIdAsync(request.JobId, cancellationToken);

            // We have an existing job. We will determine the response based on the status of the export operation.
            GetExportResponse exportResponse;

            if (outcome.JobRecord.Status.IsFinished())
            {
                var jobResult = new ExportJobResult(
                    outcome.JobRecord.QueuedTime,
                    outcome.JobRecord.RequestUri,
                    requiresAccessToken: false,
                    outcome.JobRecord.Output.Values.OrderBy(x => x.Type, StringComparer.Ordinal).ToList(),
                    outcome.JobRecord.Error);

                exportResponse = new GetExportResponse(HttpStatusCode.OK, jobResult);
            }
            else
            {
                exportResponse = new GetExportResponse(HttpStatusCode.Accepted);
            }

            return(exportResponse);
        }
コード例 #2
0
        public async Task <CancelExportResponse> Handle(CancelExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            if (_authorizationService.CheckAccess(DataActions.Export) != DataActions.Export)
            {
                throw new UnauthorizedFhirActionException();
            }

            return(await _retryPolicy.ExecuteAsync(async() =>
            {
                ExportJobOutcome outcome = await _fhirOperationDataStore.GetExportJobByIdAsync(request.JobId, cancellationToken);

                // If the job is already completed for any reason, return conflict status.
                if (outcome.JobRecord.Status.IsFinished())
                {
                    return new CancelExportResponse(HttpStatusCode.Conflict);
                }

                // Try to cancel the job.
                outcome.JobRecord.Status = OperationStatus.Canceled;
                outcome.JobRecord.CanceledTime = Clock.UtcNow;

                outcome.JobRecord.FailureDetails = new ExportJobFailureDetails(Resources.UserRequestedCancellation, HttpStatusCode.NoContent);

                await _fhirOperationDataStore.UpdateExportJobAsync(outcome.JobRecord, outcome.ETag, cancellationToken);

                return new CancelExportResponse(HttpStatusCode.Accepted);
            }));
        }
コード例 #3
0
        public async Task GivenThereIsARunningJob_WhenSimultaneousUpdateCallsOccur_ThenJobConflictExceptionShouldBeThrown()
        {
            ExportJobOutcome runningJobOutcome = await CreateRunningJob();

            var completionSource = new TaskCompletionSource <bool>();

            Task <ExportJobOutcome>[] tasks = new[]
            {
                WaitAndUpdateExportJobAsync(runningJobOutcome),
                WaitAndUpdateExportJobAsync(runningJobOutcome),
                WaitAndUpdateExportJobAsync(runningJobOutcome),
            };

            completionSource.SetResult(true);

            await Assert.ThrowsAsync <JobConflictException>(() => Task.WhenAll(tasks));

            async Task <ExportJobOutcome> WaitAndUpdateExportJobAsync(ExportJobOutcome jobOutcome)
            {
                await completionSource.Task;

                jobOutcome.JobRecord.Status = OperationStatus.Completed;
                return(await _operationDataStore.UpdateExportJobAsync(jobOutcome.JobRecord, jobOutcome.ETag, CancellationToken.None));
            }
        }
コード例 #4
0
        public async Task <CreateExportResponse> Handle(CreateExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            if (await _authorizationService.CheckAccess(DataActions.Export) != DataActions.Export)
            {
                throw new UnauthorizedFhirActionException();
            }

            IReadOnlyCollection <KeyValuePair <string, string> > requestorClaims = _claimsExtractor.Extract()?
                                                                                   .OrderBy(claim => claim.Key, StringComparer.Ordinal).ToList();

            // Compute the hash of the job.
            var hashObject = new
            {
                request.RequestUri,
                RequestorClaims = requestorClaims,
            };

            string hash = JsonConvert.SerializeObject(hashObject).ComputeHash();

            // Check to see if a matching job exists or not. If a matching job exists, we will return that instead.
            // Otherwise, we will create a new export job. This will be a best effort since the likelihood of this happen should be small.
            ExportJobOutcome outcome = await _fhirOperationDataStore.GetExportJobByHashAsync(hash, cancellationToken);

            if (outcome == null)
            {
                var jobRecord = new ExportJobRecord(request.RequestUri, request.ResourceType, hash, requestorClaims, request.Since);

                outcome = await _fhirOperationDataStore.CreateExportJobAsync(jobRecord, cancellationToken);
            }

            return(new CreateExportResponse(outcome.JobRecord.Id));
        }
コード例 #5
0
        public ExportJobTaskTests()
        {
            _cancellationToken = _cancellationTokenSource.Token;
            _exportJobRecord   = new ExportJobRecord(
                new Uri("https://localhost/ExportJob/"),
                "Patient",
                "hash");

            _fhirOperationDataStore.UpdateExportJobAsync(_exportJobRecord, _weakETag, _cancellationToken).Returns(x =>
            {
                _lastExportJobOutcome = new ExportJobOutcome(_exportJobRecord, _weakETag);

                return(_lastExportJobOutcome);
            });

            _secretStore.GetSecretAsync(Arg.Any <string>(), _cancellationToken).Returns(x => new SecretWrapper(x.ArgAt <string>(0), "{\"destinationType\": \"in-memory\"}"));

            _exportDestinationClientFactory.Create("in-memory").Returns(_inMemoryDestinationClient);

            _resourceToByteArraySerializer.Serialize(Arg.Any <ResourceWrapper>()).Returns(x => Encoding.UTF8.GetBytes(x.ArgAt <ResourceWrapper>(0).ResourceId));

            _exportJobTask = new ExportJobTask(
                () => _fhirOperationDataStore.CreateMockScope(),
                _secretStore,
                Options.Create(_exportJobConfiguration),
                () => _searchService.CreateMockScope(),
                _resourceToByteArraySerializer,
                _exportDestinationClientFactory,
                NullLogger <ExportJobTask> .Instance);
        }
コード例 #6
0
ファイル: ExportJobTask.cs プロジェクト: jbuiss0n/fhir-server
        private async Task UpdateJobRecord(ExportJobRecord jobRecord, CancellationToken cancellationToken)
        {
            ExportJobOutcome updatedExportJobOutcome = await _fhirOperationDataStore.UpdateExportJobAsync(jobRecord, _weakETag, cancellationToken);

            _exportJobRecord = updatedExportJobOutcome.JobRecord;
            _weakETag        = updatedExportJobOutcome.ETag;
        }
コード例 #7
0
        public async Task <ExportJobOutcome> GetExportJobAsync(string jobId, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNullOrWhiteSpace(jobId);

            try
            {
                DocumentResponse <CosmosExportJobRecordWrapper> cosmosExportJobRecord = await _documentClient.ReadDocumentAsync <CosmosExportJobRecordWrapper>(
                    UriFactory.CreateDocumentUri(_cosmosDataStoreConfiguration.DatabaseId, _collectionConfiguration.CollectionId, jobId),
                    new RequestOptions { PartitionKey = new PartitionKey(CosmosDbExportConstants.ExportJobPartitionKey) },
                    cancellationToken);

                var eTagHeaderValue = cosmosExportJobRecord.ResponseHeaders["ETag"];
                var outcome         = new ExportJobOutcome(cosmosExportJobRecord.Document.JobRecord, WeakETag.FromVersionId(eTagHeaderValue));

                return(outcome);
            }
            catch (DocumentClientException dce)
            {
                if (dce.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new JobNotFoundException(string.Format(Core.Resources.JobNotFound, jobId));
                }

                _logger.LogError(dce, "Unhandled Document Client Exception");
                throw;
            }
        }
コード例 #8
0
        public async Task <ExportJobOutcome> GetExportJobByIdAsync(string id, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNullOrWhiteSpace(id, nameof(id));

            try
            {
                DocumentResponse <CosmosExportJobRecordWrapper> cosmosExportJobRecord = await _documentClientScope.Value.ReadDocumentAsync <CosmosExportJobRecordWrapper>(
                    UriFactory.CreateDocumentUri(DatabaseId, CollectionId, id),
                    new RequestOptions { PartitionKey = new PartitionKey(CosmosDbExportConstants.ExportJobPartitionKey) },
                    cancellationToken);

                var outcome = new ExportJobOutcome(cosmosExportJobRecord.Document.JobRecord, WeakETag.FromVersionId(cosmosExportJobRecord.Document.ETag));

                return(outcome);
            }
            catch (DocumentClientException dce)
            {
                if (dce.StatusCode == HttpStatusCode.TooManyRequests)
                {
                    throw new RequestRateExceededException(dce.RetryAfter);
                }
                else if (dce.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new JobNotFoundException(string.Format(Core.Resources.JobNotFound, id));
                }

                _logger.LogError(dce, "Failed to get an export job by id.");
                throw;
            }
        }
コード例 #9
0
        public async Task <GetExportResponse> Handle(GetExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            ExportJobOutcome outcome = await _fhirDataStore.GetExportJobAsync(request.JobId, cancellationToken);

            // We have an existing job. We will determine the response based on the status of the export operation.
            GetExportResponse exportResponse;

            if (outcome.JobRecord.Status == OperationStatus.Completed)
            {
                var jobResult = new ExportJobResult(
                    outcome.JobRecord.QueuedTime,
                    outcome.JobRecord.RequestUri,
                    requiresAccessToken: false,
                    outcome.JobRecord.Output,
                    outcome.JobRecord.Errors);

                exportResponse = new GetExportResponse(HttpStatusCode.OK, jobResult);
            }
            else
            {
                exportResponse = new GetExportResponse(HttpStatusCode.Accepted);
            }

            return(exportResponse);
        }
コード例 #10
0
        public async Task <GetExportResponse> Handle(GetExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            ExportJobOutcome outcome = await _fhirOperationDataStore.GetExportJobByIdAsync(request.JobId, cancellationToken);

            // We have an existing job. We will determine the response based on the status of the export operation.
            GetExportResponse exportResponse;

            if (outcome.JobRecord.Status == OperationStatus.Completed)
            {
                var jobResult = new ExportJobResult(
                    outcome.JobRecord.QueuedTime,
                    outcome.JobRecord.RequestUri,
                    requiresAccessToken: false,
                    outcome.JobRecord.Output.Values.OrderBy(x => x.Type, StringComparer.Ordinal).ToList(),
                    outcome.JobRecord.Error);

                exportResponse = new GetExportResponse(HttpStatusCode.OK, jobResult);
            }
            else if (outcome.JobRecord.Status == OperationStatus.Failed || outcome.JobRecord.Status == OperationStatus.Canceled)
            {
                throw new OperationFailedException(
                          string.Format(Resources.OperationFailed, OperationsConstants.Export, outcome.JobRecord.FailureDetails.FailureReason),
                          outcome.JobRecord.FailureDetails.FailureStatusCode);
            }
            else
            {
                exportResponse = new GetExportResponse(HttpStatusCode.Accepted);
            }

            return(exportResponse);
        }
コード例 #11
0
        public async Task <ExportJobOutcome> GetExportJobByIdAsync(string id, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNullOrWhiteSpace(id, nameof(id));

            try
            {
                ItemResponse <CosmosExportJobRecordWrapper> cosmosExportJobRecord = await _containerScope.Value.ReadItemAsync <CosmosExportJobRecordWrapper>(
                    id,
                    new PartitionKey(CosmosDbExportConstants.ExportJobPartitionKey),
                    cancellationToken : cancellationToken);

                var outcome = new ExportJobOutcome(cosmosExportJobRecord.Resource.JobRecord, WeakETag.FromVersionId(cosmosExportJobRecord.Resource.ETag));

                return(outcome);
            }
            catch (CosmosException dce)
            {
                if (dce.IsRequestRateExceeded())
                {
                    throw;
                }

                if (dce.StatusCode == HttpStatusCode.NotFound)
                {
                    throw new JobNotFoundException(string.Format(Core.Resources.JobNotFound, id));
                }

                _logger.LogError(dce, "Failed to get an export job by id.");
                throw;
            }
        }
コード例 #12
0
        public async Task GivenAFhirMediator_WhenCancelingExistingExportJobThatHasAlreadyCompleted_ThenConflictStatusCodeShouldBeReturned(OperationStatus operationStatus)
        {
            ExportJobOutcome outcome = await SetupAndExecuteCancelExportAsync(operationStatus, HttpStatusCode.Conflict);

            Assert.Equal(operationStatus, outcome.JobRecord.Status);
            Assert.Null(outcome.JobRecord.CanceledTime);
        }
コード例 #13
0
        public async Task <CreateExportResponse> Handle(CreateExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            if (await _authorizationService.CheckAccess(DataActions.Export, cancellationToken) != DataActions.Export)
            {
                throw new UnauthorizedFhirActionException();
            }

            IReadOnlyCollection <KeyValuePair <string, string> > requestorClaims = _claimsExtractor.Extract()?
                                                                                   .OrderBy(claim => claim.Key, StringComparer.Ordinal).ToList();

            // Compute the hash of the job.
            var hashObject = new
            {
                request.RequestUri,
                RequestorClaims = requestorClaims,
            };

            string hash = JsonConvert.SerializeObject(hashObject).ComputeHash();

            string storageAccountConnectionHash = string.IsNullOrEmpty(_exportJobConfiguration.StorageAccountConnection) ?
                                                  string.Empty :
                                                  StringExtensions.ComputeHash(_exportJobConfiguration.StorageAccountConnection);

            // Check to see if a matching job exists or not. If a matching job exists, we will return that instead.
            // Otherwise, we will create a new export job. This will be a best effort since the likelihood of this happen should be small.
            ExportJobOutcome outcome = await _fhirOperationDataStore.GetExportJobByHashAsync(hash, cancellationToken);

            var filters = ParseFilter(request.Filters);

            ExportJobFormatConfiguration formatConfiguration = ParseFormat(request.FormatName, request.ContainerName != null);

            if (outcome == null)
            {
                var jobRecord = new ExportJobRecord(
                    request.RequestUri,
                    request.RequestType,
                    formatConfiguration.Format,
                    request.ResourceType,
                    filters,
                    hash,
                    _exportJobConfiguration.RollingFileSizeInMB,
                    requestorClaims,
                    request.Since,
                    request.GroupId,
                    storageAccountConnectionHash,
                    _exportJobConfiguration.StorageAccountUri,
                    request.AnonymizationConfigurationLocation,
                    request.AnonymizationConfigurationFileETag,
                    _exportJobConfiguration.MaximumNumberOfResourcesPerQuery,
                    _exportJobConfiguration.NumberOfPagesPerCommit,
                    request.ContainerName);

                outcome = await _fhirOperationDataStore.CreateExportJobAsync(jobRecord, cancellationToken);
            }

            return(new CreateExportResponse(outcome.JobRecord.Id));
        }
コード例 #14
0
        public async Task GivenAMatchingJob_WhenGettingByHash_ThenTheMatchingJobShouldBeReturned()
        {
            var jobRecord = await InsertNewExportJobRecordAsync();

            ExportJobOutcome outcome = await _operationDataStore.GetExportJobByHashAsync(jobRecord.Hash, CancellationToken.None);

            Assert.Equal(jobRecord.Id, outcome?.JobRecord?.Id);
        }
コード例 #15
0
        public async Task GivenNoMatchingJob_WhenGettingByHash_ThenNoMatchingJobShouldBeReturned()
        {
            var jobRecord = await InsertNewExportJobRecordAsync();

            ExportJobOutcome outcome = await _operationDataStore.GetExportJobByHashAsync("test", CancellationToken.None);

            Assert.Null(outcome);
        }
コード例 #16
0
        private async Task UpdateJobStatus(OperationStatus operationStatus, CancellationToken cancellationToken)
        {
            _exportJobRecord.Status = operationStatus;

            ExportJobOutcome updatedExportJobOutcome = await _fhirOperationDataStore.UpdateExportJobAsync(_exportJobRecord, _weakETag, cancellationToken);

            _weakETag = updatedExportJobOutcome.ETag;
        }
コード例 #17
0
        public async void GivenAFhirMediator_WhenSavingAnExportJobSucceeds_ThenResponseShouldBeSuccess()
        {
            var exportOutcome = new ExportJobOutcome(new ExportJobRecord(new Uri(RequestUrl)), WeakETag.FromVersionId("eTag"));

            _fhirDataStore.CreateExportJobAsync(Arg.Any <ExportJobRecord>(), Arg.Any <CancellationToken>()).Returns(exportOutcome);

            var outcome = await _mediator.ExportAsync(new Uri(RequestUrl));

            Assert.NotEmpty(outcome.JobId);
        }
コード例 #18
0
        private async Task <ExportJobRecord> InsertNewExportJobRecordAsync(Action <ExportJobRecord> jobRecordCustomizer = null)
        {
            var jobRecord = new ExportJobRecord(_exportRequest.RequestUri, "Patient", "hash");

            jobRecordCustomizer?.Invoke(jobRecord);

            ExportJobOutcome result = await _operationDataStore.CreateExportJobAsync(jobRecord, CancellationToken.None);

            return(result.JobRecord);
        }
コード例 #19
0
        private async Task UpdateJobRecordAsync(CancellationToken cancellationToken)
        {
            using (IScoped <IFhirOperationDataStore> fhirOperationDataStore = _fhirOperationDataStoreFactory())
            {
                ExportJobOutcome updatedExportJobOutcome = await fhirOperationDataStore.Value.UpdateExportJobAsync(_exportJobRecord, _weakETag, cancellationToken);

                _exportJobRecord = updatedExportJobOutcome.JobRecord;
                _weakETag        = updatedExportJobOutcome.ETag;
            }
        }
コード例 #20
0
        private async Task <ExportJobRecord> InsertNewExportJobRecordAsync(Action <ExportJobRecord> jobRecordCustomizer = null)
        {
            var jobRecord = new ExportJobRecord(new Uri($"http://localhost"), "hash");

            jobRecordCustomizer?.Invoke(jobRecord);

            ExportJobOutcome result = await _operationDataStore.CreateExportJobAsync(jobRecord, CancellationToken.None);

            return(result.JobRecord);
        }
コード例 #21
0
        private async Task <ExportJobOutcome> SetupAndExecuteCancelExportAsync(OperationStatus operationStatus, HttpStatusCode expectedStatusCode)
        {
            ExportJobOutcome outcome = SetupExportJob(operationStatus);

            CancelExportResponse response = await _mediator.CancelExportAsync(JobId, _cancellationToken);

            Assert.NotNull(response);
            Assert.Equal(expectedStatusCode, response.StatusCode);

            return(outcome);
        }
コード例 #22
0
        public async Task GivenANewExportRequest_WhenCreatingExportJob_ThenGetsJobCreated()
        {
            var jobRecord = new ExportJobRecord(new Uri("http://localhost/ExportJob"), "hash");

            ExportJobOutcome outcome = await _operationDataStore.CreateExportJobAsync(jobRecord, CancellationToken.None);

            Assert.NotNull(outcome);
            Assert.NotNull(outcome.JobRecord);
            Assert.NotEmpty(outcome.JobRecord.Id);
            Assert.NotNull(outcome.ETag);
        }
コード例 #23
0
        public async Task GivenAnExportJobRecord_WhenExecuted_ThenTheExportJobRecordShouldBeUpdated()
        {
            ExportJobOutcome job = await CreateAndExecuteCreateExportJobAsync();

            await _exportJobTask.ExecuteAsync(job.JobRecord, job.ETag, CancellationToken.None);

            ExportJobOutcome actual = await _fhirOperationDataStore.GetExportJobByIdAsync(job.JobRecord.Id, CancellationToken.None);

            Assert.NotNull(actual);
            Assert.Equal(OperationStatus.Completed, actual.JobRecord.Status);
        }
コード例 #24
0
        public async Task GivenANonexistentJob_WhenUpdatingTheJob_ThenJobNotFoundExceptionShouldBeThrown()
        {
            ExportJobOutcome jobOutcome = await CreateRunningJob();

            ExportJobRecord job        = jobOutcome.JobRecord;
            WeakETag        jobVersion = jobOutcome.ETag;

            await _testHelper.DeleteExportJobRecordAsync(job.Id);

            await Assert.ThrowsAsync <JobNotFoundException>(() => _operationDataStore.UpdateExportJobAsync(job, jobVersion, CancellationToken.None));
        }
コード例 #25
0
        public async Task GivenANewExportRequest_WhenCreatingExportJob_ThenGetsJobCreated()
        {
            var jobRecord = new ExportJobRecord(_exportRequest.RequestUri, _exportRequest.RequestType, ExportFormatTags.ResourceName, _exportRequest.ResourceType, null, "hash", rollingFileSizeInMB: 64);

            ExportJobOutcome outcome = await _operationDataStore.CreateExportJobAsync(jobRecord, CancellationToken.None);

            Assert.NotNull(outcome);
            Assert.NotNull(outcome.JobRecord);
            Assert.NotEmpty(outcome.JobRecord.Id);
            Assert.NotNull(outcome.ETag);
        }
コード例 #26
0
        public async Task GivenThereIsNoRunningJob_WhenExecuted_ThenATaskShouldBeCreated()
        {
            ExportJobOutcome job = CreateExportJobOutcome();

            SetupOperationDataStore(job);

            _cancellationTokenSource.CancelAfter(DefaultJobPollingFrequency);

            await _exportJobWorker.ExecuteAsync(_cancellationToken);

            _exportJobTaskFactory().Received(1);
        }
コード例 #27
0
        public async Task <CreateExportResponse> Handle(CreateExportRequest request, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(request, nameof(request));

            // TODO: Later we will add some logic here that will check whether a duplicate job already exists
            // and handle it accordingly. For now we just assume all export jobs are unique and create a new one.

            var jobRecord           = new ExportJobRecord(request.RequestUri);
            ExportJobOutcome result = await _fhirDataStore.CreateExportJobAsync(jobRecord, cancellationToken);

            // If job creation had failed we would have thrown an exception.
            return(new CreateExportResponse(jobRecord.Id));
        }
コード例 #28
0
        public async Task GivenThereIsRunningJobThatExpired_WhenAcquiringJobs_ThenTheExpiredJobShouldBeReturned()
        {
            ExportJobOutcome jobOutcome = await CreateRunningJob();

            await Task.Delay(1200);

            IReadOnlyCollection <ExportJobOutcome> expiredJobs = await AcquireExportJobsAsync(jobHeartbeatTimeoutThreshold : TimeSpan.FromSeconds(1));

            Assert.NotNull(expiredJobs);
            Assert.Collection(
                expiredJobs,
                expiredJobOutcome => ValidateExportJobOutcome(jobOutcome.JobRecord, expiredJobOutcome.JobRecord));
        }
コード例 #29
0
        private void SetupExportJobRecordAndOperationDataStore(ExportJobRecord exportJobRecord = null)
        {
            _exportJobRecord = exportJobRecord ?? new ExportJobRecord(
                new Uri("https://localhost/ExportJob/"),
                "Patient",
                "hash");

            _fhirOperationDataStore.UpdateExportJobAsync(_exportJobRecord, _weakETag, _cancellationToken).Returns(x =>
            {
                _lastExportJobOutcome = new ExportJobOutcome(_exportJobRecord, _weakETag);

                return(_lastExportJobOutcome);
            });
        }
コード例 #30
0
        public async Task GivenTheNumberOfRunningJobExceedsThreshold_WhenExecuted_ThenATaskShouldNotBeCreated()
        {
            ExportJobOutcome job = CreateExportJobOutcome();

            SetupOperationDataStore(job);

            _task.ExecuteAsync(job.JobRecord, job.ETag, _cancellationToken).Returns(Task.Run(async() => { await Task.Delay(1000); }));

            _cancellationTokenSource.CancelAfter(DefaultJobPollingFrequency * 2);

            await _exportJobWorker.ExecuteAsync(_cancellationToken);

            _exportJobTaskFactory.Received(1);
        }