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); }
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); })); }
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)); } }
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)); }
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); }
private async Task UpdateJobRecord(ExportJobRecord jobRecord, CancellationToken cancellationToken) { ExportJobOutcome updatedExportJobOutcome = await _fhirOperationDataStore.UpdateExportJobAsync(jobRecord, _weakETag, cancellationToken); _exportJobRecord = updatedExportJobOutcome.JobRecord; _weakETag = updatedExportJobOutcome.ETag; }
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; } }
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; } }
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); }
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); }
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; } }
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); }
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)); }
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); }
public async Task GivenNoMatchingJob_WhenGettingByHash_ThenNoMatchingJobShouldBeReturned() { var jobRecord = await InsertNewExportJobRecordAsync(); ExportJobOutcome outcome = await _operationDataStore.GetExportJobByHashAsync("test", CancellationToken.None); Assert.Null(outcome); }
private async Task UpdateJobStatus(OperationStatus operationStatus, CancellationToken cancellationToken) { _exportJobRecord.Status = operationStatus; ExportJobOutcome updatedExportJobOutcome = await _fhirOperationDataStore.UpdateExportJobAsync(_exportJobRecord, _weakETag, cancellationToken); _weakETag = updatedExportJobOutcome.ETag; }
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); }
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); }
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; } }
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); }
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); }
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); }
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); }
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)); }
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); }
public async Task GivenThereIsNoRunningJob_WhenExecuted_ThenATaskShouldBeCreated() { ExportJobOutcome job = CreateExportJobOutcome(); SetupOperationDataStore(job); _cancellationTokenSource.CancelAfter(DefaultJobPollingFrequency); await _exportJobWorker.ExecuteAsync(_cancellationToken); _exportJobTaskFactory().Received(1); }
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)); }
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)); }
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); }); }
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); }