public ExportFileManagerTests() { _exportDestinationClient = Substitute.For <IExportDestinationClient>(); _exportDestinationClient .CreateFileAsync(Arg.Any <string>(), Arg.Any <CancellationToken>()) .Returns <Uri>(callInfo => new Uri("https://localhost/" + callInfo.ArgAt <string>(0))); _exportJobConfigurationFormat = $"{ExportFormatTags.ResourceName}"; _exportJobRecord = new ExportJobRecord( new Uri("https://localhost/$export"), ExportJobType.All, _exportJobConfigurationFormat, resourceType: null, filters: null, "hash", rollingFileSizeInMB: 1); _exportFileManager = new ExportFileManager(_exportJobRecord, _exportDestinationClient); }
public async Task GivenStorageAccountUriChanged_WhenExecuted_ThenRecordsAreSentToOldStorageAccount() { var exportJobRecordWithChangedConnection = new ExportJobRecord( new Uri("https://localhost/ExportJob/"), "Patient", "hash", since: PartialDateTime.MinValue, storageAccountConnectionHash: Microsoft.Health.Core.Extensions.StringExtensions.ComputeHash(_exportJobConfiguration.StorageAccountConnection), storageAccountUri: "origionalUri"); SetupExportJobRecordAndOperationDataStore(exportJobRecordWithChangedConnection); ExportJobConfiguration configurationWithUri = new ExportJobConfiguration(); configurationWithUri.StorageAccountUri = "newUri"; IExportDestinationClient mockDestinationClient = Substitute.For <IExportDestinationClient>(); ExportJobConfiguration capturedConfiguration = null; mockDestinationClient.ConnectAsync( Arg.Do <ExportJobConfiguration>(arg => capturedConfiguration = arg), Arg.Any <CancellationToken>(), Arg.Any <string>()) .Returns(x => { return(Task.CompletedTask); }); var exportJobTask = new ExportJobTask( () => _fhirOperationDataStore.CreateMockScope(), Options.Create(_exportJobConfiguration), () => _searchService.CreateMockScope(), _resourceToByteArraySerializer, mockDestinationClient, NullLogger <ExportJobTask> .Instance); await exportJobTask.ExecuteAsync(_exportJobRecord, _weakETag, _cancellationToken); Assert.Equal(exportJobRecordWithChangedConnection.StorageAccountUri, capturedConfiguration.StorageAccountUri); }
public ExportJobTask( Func <IScoped <IFhirOperationDataStore> > fhirOperationDataStoreFactory, IOptions <ExportJobConfiguration> exportJobConfiguration, Func <IScoped <ISearchService> > searchServiceFactory, IResourceToByteArraySerializer resourceToByteArraySerializer, IExportDestinationClient exportDestinationClient, ILogger <ExportJobTask> logger) { EnsureArg.IsNotNull(fhirOperationDataStoreFactory, nameof(fhirOperationDataStoreFactory)); EnsureArg.IsNotNull(exportJobConfiguration?.Value, nameof(exportJobConfiguration)); EnsureArg.IsNotNull(searchServiceFactory, nameof(searchServiceFactory)); EnsureArg.IsNotNull(resourceToByteArraySerializer, nameof(resourceToByteArraySerializer)); EnsureArg.IsNotNull(exportDestinationClient, nameof(exportDestinationClient)); EnsureArg.IsNotNull(logger, nameof(logger)); _fhirOperationDataStoreFactory = fhirOperationDataStoreFactory; _exportJobConfiguration = exportJobConfiguration.Value; _searchServiceFactory = searchServiceFactory; _resourceToByteArraySerializer = resourceToByteArraySerializer; _exportDestinationClient = exportDestinationClient; _logger = logger; }
public async Task GivenConnectingToDestinationFails_WhenExecuted_ThenJobStatusShouldBeUpdatedToFailed() { // Setup export destination client. string connectionFailure = "failedToConnectToDestination"; IExportDestinationClient mockExportDestinationClient = Substitute.For <IExportDestinationClient>(); mockExportDestinationClient.ConnectAsync(Arg.Any <CancellationToken>(), Arg.Any <string>()) .Returns <Task>(x => throw new DestinationConnectionException(connectionFailure, HttpStatusCode.BadRequest)); var exportJobTask = new ExportJobTask( () => _fhirOperationDataStore.CreateMockScope(), Options.Create(_exportJobConfiguration), () => _searchService.CreateMockScope(), _resourceToByteArraySerializer, mockExportDestinationClient, NullLogger <ExportJobTask> .Instance); await exportJobTask.ExecuteAsync(_exportJobRecord, _weakETag, _cancellationToken); Assert.NotNull(_lastExportJobOutcome); Assert.Equal(OperationStatus.Failed, _lastExportJobOutcome.JobRecord.Status); Assert.Equal(connectionFailure, _lastExportJobOutcome.JobRecord.FailureDetails.FailureReason); Assert.Equal(HttpStatusCode.BadRequest, _lastExportJobOutcome.JobRecord.FailureDetails.FailureStatusCode); }
public ExportJobTask( Func <IScoped <IFhirOperationDataStore> > fhirOperationDataStoreFactory, IOptions <ExportJobConfiguration> exportJobConfiguration, Func <IScoped <ISearchService> > searchServiceFactory, IGroupMemberExtractor groupMemberExtractor, IResourceToByteArraySerializer resourceToByteArraySerializer, IExportDestinationClient exportDestinationClient, IResourceDeserializer resourceDeserializer, IScoped <IAnonymizerFactory> anonymizerFactory, IMediator mediator, IFhirRequestContextAccessor contextAccessor, ILogger <ExportJobTask> logger) { EnsureArg.IsNotNull(fhirOperationDataStoreFactory, nameof(fhirOperationDataStoreFactory)); EnsureArg.IsNotNull(exportJobConfiguration?.Value, nameof(exportJobConfiguration)); EnsureArg.IsNotNull(searchServiceFactory, nameof(searchServiceFactory)); EnsureArg.IsNotNull(groupMemberExtractor, nameof(groupMemberExtractor)); EnsureArg.IsNotNull(resourceToByteArraySerializer, nameof(resourceToByteArraySerializer)); EnsureArg.IsNotNull(exportDestinationClient, nameof(exportDestinationClient)); EnsureArg.IsNotNull(resourceDeserializer, nameof(resourceDeserializer)); EnsureArg.IsNotNull(mediator, nameof(mediator)); EnsureArg.IsNotNull(contextAccessor, nameof(contextAccessor)); EnsureArg.IsNotNull(logger, nameof(logger)); _fhirOperationDataStoreFactory = fhirOperationDataStoreFactory; _exportJobConfiguration = exportJobConfiguration.Value; _searchServiceFactory = searchServiceFactory; _groupMemberExtractor = groupMemberExtractor; _resourceToByteArraySerializer = resourceToByteArraySerializer; _resourceDeserializer = resourceDeserializer; _exportDestinationClient = exportDestinationClient; _anonymizerFactory = anonymizerFactory; _mediator = mediator; _contextAccessor = contextAccessor; _logger = logger; }
/// <inheritdoc /> public async Task ExecuteAsync(ExportJobRecord exportJobRecord, WeakETag weakETag, CancellationToken cancellationToken) { EnsureArg.IsNotNull(exportJobRecord, nameof(exportJobRecord)); _exportJobRecord = exportJobRecord; _weakETag = weakETag; try { // Get destination type from secret store. DestinationInfo destinationInfo = await GetDestinationInfo(cancellationToken); // Connect to the destination using appropriate client. _exportDestinationClient = _exportDestinationClientFactory.Create(destinationInfo.DestinationType); await _exportDestinationClient.ConnectAsync(destinationInfo.DestinationConnectionString, cancellationToken, _exportJobRecord.Id); // TODO: For now, always restart from the beginning. We will support resume in another work item. _exportJobRecord.Progress = new ExportJobProgress(continuationToken: null, page: 0); ExportJobProgress progress = _exportJobRecord.Progress; // Current page will be used to organize a set of search results into a group so that they can be committed together. uint currentBatchId = progress.Page; // The first item is placeholder for continuation token so that it can be updated efficiently later. var queryParameters = new Tuple <string, string>[] { null, Tuple.Create(KnownQueryParameterNames.Count, _exportJobConfiguration.MaximumNumberOfResourcesPerQuery.ToString(CultureInfo.InvariantCulture)), Tuple.Create(KnownQueryParameterNames.LastUpdated, $"le{_exportJobRecord.QueuedTime.ToString("o", CultureInfo.InvariantCulture)}"), }; // Process the export if: // 1. There is continuation token, which means there is more resource to be exported. // 2. There is no continuation token but the page is 0, which means it's the initial export. while (progress.ContinuationToken != null || progress.Page == 0) { // Commit the changes if necessary. if (progress.Page != 0 && progress.Page % _exportJobConfiguration.NumberOfPagesPerCommit == 0) { await _exportDestinationClient.CommitAsync(cancellationToken); // Update the job record. await UpdateJobRecord(_exportJobRecord, cancellationToken); currentBatchId = progress.Page; } // Set the continuation token. queryParameters[0] = Tuple.Create(KnownQueryParameterNames.ContinuationToken, progress.ContinuationToken); SearchResult searchResult = await _searchService.SearchAsync(_exportJobRecord.ResourceType, queryParameters, cancellationToken); foreach (ResourceWrapper resourceWrapper in searchResult.Results) { await ProcessResourceWrapperAsync(resourceWrapper, currentBatchId, cancellationToken); } if (searchResult.ContinuationToken == null) { // No more continuation token, we are done. break; } // Update the job record. progress.UpdateContinuationToken(searchResult.ContinuationToken); } // Commit one last time for any pending changes. await _exportDestinationClient.CommitAsync(cancellationToken); _exportJobRecord.Output.AddRange(_resourceTypeToFileInfoMapping.Values); _logger.LogTrace("Successfully completed the job."); await UpdateJobStatus(OperationStatus.Completed, updateEndTimestamp : true, cancellationToken); try { // Best effort to delete the secret. If it fails to delete, then move on. await _secretStore.DeleteSecretAsync(_exportJobRecord.SecretName, cancellationToken); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to delete the secret."); } } catch (JobConflictException) { // The job was updated by another process. _logger.LogWarning("The job was updated by another process."); // TODO: We will want to get the latest and merge the results without updating the status. return; } catch (Exception ex) { // The job has encountered an error it cannot recover from. // Try to update the job to failed state. _logger.LogError(ex, "Encountered an unhandled exception. The job will be marked as failed."); await UpdateJobStatus(OperationStatus.Failed, updateEndTimestamp : true, cancellationToken); } }