private ICollection <string> GetDerivedResourceTypes(IReadOnlyCollection <string> resourceTypes)
        {
            var completeResourceList = new HashSet <string>(resourceTypes);

            foreach (var baseResourceType in resourceTypes)
            {
                if (baseResourceType == KnownResourceTypes.Resource)
                {
                    completeResourceList.UnionWith(_modelInfoProvider.GetResourceTypeNames().ToHashSet());

                    // We added all possible resource types, so no need to continue
                    break;
                }

                if (baseResourceType == KnownResourceTypes.DomainResource)
                {
                    var domainResourceChildResourceTypes = _modelInfoProvider.GetResourceTypeNames().ToHashSet();

                    // Remove types that inherit from Resource directly
                    domainResourceChildResourceTypes.Remove(KnownResourceTypes.Binary);
                    domainResourceChildResourceTypes.Remove(KnownResourceTypes.Bundle);
                    domainResourceChildResourceTypes.Remove(KnownResourceTypes.Parameters);

                    completeResourceList.UnionWith(domainResourceChildResourceTypes);
                }
            }

            return(completeResourceList);
        }
        public static MockModelInfoProviderBuilder Create(FhirSpecification version)
        {
            IModelInfoProvider provider = Substitute.For <IModelInfoProvider>();

            provider.Version.Returns(version);

            // Adds normative types by default
            var seenTypes = new HashSet <string>
            {
                "Binary", "Bundle", "CapabilityStatement", "CodeSystem", "Observation", "OperationOutcome", "Patient", "StructureDefinition", "ValueSet",
            };

            provider.GetResourceTypeNames().Returns(_ => seenTypes.Where(x => !string.IsNullOrEmpty(x)).ToArray());
            provider.IsKnownResource(Arg.Any <string>()).Returns(x => provider.GetResourceTypeNames().Contains(x[0]));

            // Simulate inherited behavior
            // Some code depends on "InheritedResource".BaseType
            // This adds the ability to resolve "Resource" as the base type
            provider.GetTypeForFhirType(Arg.Any <string>()).Returns(p => p.ArgAt <string>(0) == "Resource" ? typeof(ResourceObj) : typeof(InheritedResourceObj));
            provider.GetFhirTypeNameForType(Arg.Any <Type>()).Returns(p => p.ArgAt <Type>(0) == typeof(ResourceObj) ? "Resource" : null);

            // IStructureDefinitionSummaryProvider allows the execution of FHIRPath queries
            provider.ToTypedElement(Arg.Any <ISourceNode>())
            .Returns(p => p.ArgAt <ISourceNode>(0).ToTypedElement(new MockStructureDefinitionSummaryProvider(p.ArgAt <ISourceNode>(0), seenTypes)));

            return(new MockModelInfoProviderBuilder(provider, seenTypes));
        }
Beispiel #3
0
        public ICapabilityStatementBuilder PopulateDefaultResourceInteractions()
        {
            foreach (string resource in _modelInfoProvider.GetResourceTypeNames())
            {
                // Parameters is a non-persisted resource used to pass information into and back from an operation.
                if (string.Equals(resource, KnownResourceTypes.Parameters, StringComparison.Ordinal))
                {
                    continue;
                }

                AddResourceInteraction(resource, TypeRestfulInteraction.Create);
                AddResourceInteraction(resource, TypeRestfulInteraction.Read);
                AddResourceInteraction(resource, TypeRestfulInteraction.Vread);
                AddResourceInteraction(resource, TypeRestfulInteraction.HistoryType);
                AddResourceInteraction(resource, TypeRestfulInteraction.HistoryInstance);

                // AuditEvents should not allow Update or Delete
                if (!string.Equals(resource, KnownResourceTypes.AuditEvent, StringComparison.Ordinal))
                {
                    AddResourceInteraction(resource, TypeRestfulInteraction.Update);
                    AddResourceInteraction(resource, TypeRestfulInteraction.Patch);
                    AddResourceInteraction(resource, TypeRestfulInteraction.Delete);
                }

                ApplyToResource(resource, component =>
                {
                    component.Versioning.Add(ResourceVersionPolicy.NoVersion);
                    component.Versioning.Add(ResourceVersionPolicy.Versioned);
                    component.Versioning.Add(ResourceVersionPolicy.VersionedUpdate);

                    // Create is added for every resource above.
                    component.ConditionalCreate = true;

                    // AuditEvent don't allow update, so no conditional update as well.
                    if (!string.Equals(resource, KnownResourceTypes.AuditEvent, StringComparison.Ordinal))
                    {
                        component.ConditionalUpdate = true;

                        component.ConditionalDelete.Add(ConditionalDeleteStatus.NotSupported);
                        component.ConditionalDelete.Add(ConditionalDeleteStatus.Single);
                        component.ConditionalDelete.Add(ConditionalDeleteStatus.Multiple);
                    }

                    component.ReadHistory  = true;
                    component.UpdateCreate = true;
                });
            }

            AddGlobalInteraction(SystemRestfulInteraction.HistorySystem);

            return(this);
        }
Beispiel #4
0
        internal void Build()
        {
            ILookup <string, SearchParameterInfo> searchParametersLookup = ValidateAndGetFlattenedList()
                                                                           .ToLookup(
                entry => entry.ResourceType,
                entry => entry.SearchParameter);

            // Build the inheritance. For example, the _id search parameter is on Resource
            // and should be available to all resources that inherit Resource.
            foreach (string resourceType in _modelInfoProvider.GetResourceTypeNames())
            {
                if (_resourceTypeDictionary.TryGetValue(resourceType, out IDictionary <string, SearchParameterInfo> _))
                {
                    // The list has already been built, move on.
                    continue;
                }

                // Recursively build the search parameter definitions. For example,
                // Appointment inherits from DomainResource, which inherits from Resource
                // and therefore Appointment should include all search parameters DomainResource and Resource supports.
                BuildSearchParameterDefinition(searchParametersLookup, resourceType);
            }

            _initialized = true;
        }
Beispiel #5
0
        internal static void Build(
            IReadOnlyCollection <ITypedElement> searchParameters,
            IDictionary <Uri, SearchParameterInfo> uriDictionary,
            IDictionary <string, IDictionary <string, SearchParameterInfo> > resourceTypeDictionary,
            IModelInfoProvider modelInfoProvider)
        {
            EnsureArg.IsNotNull(searchParameters, nameof(searchParameters));
            EnsureArg.IsNotNull(uriDictionary, nameof(uriDictionary));
            EnsureArg.IsNotNull(resourceTypeDictionary, nameof(resourceTypeDictionary));
            EnsureArg.IsNotNull(modelInfoProvider, nameof(modelInfoProvider));

            ILookup <string, SearchParameterInfo> searchParametersLookup = ValidateAndGetFlattenedList(
                searchParameters,
                uriDictionary,
                modelInfoProvider).ToLookup(
                entry => entry.ResourceType,
                entry => entry.SearchParameter);

            // Build the inheritance. For example, the _id search parameter is on Resource
            // and should be available to all resources that inherit Resource.
            foreach (string resourceType in modelInfoProvider.GetResourceTypeNames())
            {
                // Recursively build the search parameter definitions. For example,
                // Appointment inherits from DomainResource, which inherits from Resource
                // and therefore Appointment should include all search parameters DomainResource and Resource supports.
                BuildSearchParameterDefinition(searchParametersLookup, resourceType, resourceTypeDictionary, modelInfoProvider);
            }
        }
Beispiel #6
0
        public void Build(IListedCapabilityStatement statement)
        {
            foreach (var resource in _modelInfoProvider.GetResourceTypeNames())
            {
                statement.TryAddRestInteraction(resource, TypeRestfulInteraction.HistoryType);
                statement.TryAddRestInteraction(resource, TypeRestfulInteraction.HistoryInstance);
            }

            statement.TryAddRestInteraction(SystemRestfulInteraction.HistorySystem);
        }
        public ICapabilityStatementBuilder AddDefaultResourceInteractions()
        {
            foreach (string resource in _modelInfoProvider.GetResourceTypeNames())
            {
                // Parameters is a non-persisted resource used to pass information into and back from an operation.
                if (string.Equals(resource, KnownResourceTypes.Parameters, StringComparison.Ordinal))
                {
                    continue;
                }

                AddRestInteraction(resource, TypeRestfulInteraction.Create);
                AddRestInteraction(resource, TypeRestfulInteraction.Read);
                AddRestInteraction(resource, TypeRestfulInteraction.Vread);
                AddRestInteraction(resource, TypeRestfulInteraction.HistoryType);
                AddRestInteraction(resource, TypeRestfulInteraction.HistoryInstance);

                // AuditEvents should not allow Update or Delete
                if (!string.Equals(resource, KnownResourceTypes.AuditEvent, StringComparison.Ordinal))
                {
                    AddRestInteraction(resource, TypeRestfulInteraction.Update);
                    AddRestInteraction(resource, TypeRestfulInteraction.Delete);
                }

                UpdateRestResourceComponent(resource, component =>
                {
                    component.Versioning.Add(ResourceVersionPolicy.NoVersion);
                    component.Versioning.Add(ResourceVersionPolicy.Versioned);
                    component.Versioning.Add(ResourceVersionPolicy.VersionedUpdate);

                    component.ReadHistory  = true;
                    component.UpdateCreate = true;
                });
            }

            AddRestInteraction(SystemRestfulInteraction.HistorySystem);

            return(this);
        }
        public void Build(IListedCapabilityStatement statement)
        {
            EnsureArg.IsNotNull(statement, nameof(statement));

            foreach (var resource in _modelInfoProvider.GetResourceTypeNames())
            {
                statement.BuildRestResourceComponent(resource, builder =>
                {
                    builder.AddResourceVersionPolicy(ResourceVersionPolicy.NoVersion);
                    builder.AddResourceVersionPolicy(ResourceVersionPolicy.Versioned);
                    builder.AddResourceVersionPolicy(ResourceVersionPolicy.VersionedUpdate);

                    builder.ReadHistory  = true;
                    builder.UpdateCreate = true;
                });
            }
        }
        /// <inheritdoc />
        public async Task ExecuteAsync(ReindexJobRecord reindexJobRecord, WeakETag weakETag, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(reindexJobRecord, nameof(reindexJobRecord));
            EnsureArg.IsNotNull(weakETag, nameof(weakETag));

            _reindexJobRecord = reindexJobRecord;
            _weakETag         = weakETag;
            var jobSemaphore = new SemaphoreSlim(1, 1);

            var existingFhirRequestContext = _contextAccessor.FhirRequestContext;

            try
            {
                // Add a request context so Datastore consumption can be added
                var fhirRequestContext = new FhirRequestContext(
                    method: OperationsConstants.Reindex,
                    uriString: "$reindex",
                    baseUriString: "$reindex",
                    correlationId: _reindexJobRecord.Id,
                    requestHeaders: new Dictionary <string, StringValues>(),
                    responseHeaders: new Dictionary <string, StringValues>())
                {
                    IsBackgroundTask = true,
                    AuditEventType   = OperationsConstants.Reindex,
                };

                _contextAccessor.FhirRequestContext = fhirRequestContext;

                using (IScoped <IFhirDataStore> store = _fhirDataStoreFactory())
                {
                    var provisionedCapacity = await store.Value.GetProvisionedDataStoreCapacityAsync(cancellationToken);

                    _throttleController.Initialize(_reindexJobRecord, provisionedCapacity);
                }

                if (_reindexJobRecord.Status != OperationStatus.Running ||
                    _reindexJobRecord.StartTime == null)
                {
                    // update job record to running
                    _reindexJobRecord.Status    = OperationStatus.Running;
                    _reindexJobRecord.StartTime = Clock.UtcNow;
                    await UpdateJobAsync(cancellationToken);
                }

                // If we are resuming a job, we can detect that by checking the progress info from the job record.
                // If no queries have been added to the progress then this is a new job
                if (_reindexJobRecord.QueryList?.Count == 0)
                {
                    // Build query based on new search params
                    // Find supported, but not yet searchable params
                    var notYetIndexedParams = _supportedSearchParameterDefinitionManager.GetSearchParametersRequiringReindexing();

                    // if there are not any parameters which are supported but not yet indexed, then we have nothing to do
                    if (!notYetIndexedParams.Any())
                    {
                        _reindexJobRecord.Error.Add(new OperationOutcomeIssue(
                                                        OperationOutcomeConstants.IssueSeverity.Information,
                                                        OperationOutcomeConstants.IssueType.Informational,
                                                        Resources.NoSearchParametersNeededToBeIndexed));
                        _reindexJobRecord.CanceledTime = DateTimeOffset.UtcNow;
                        await CompleteJobAsync(OperationStatus.Canceled, cancellationToken);

                        return;
                    }

                    // From the param list, get the list of necessary resources which should be
                    // included in our query
                    var resourceList = new HashSet <string>();
                    foreach (var param in notYetIndexedParams)
                    {
                        foreach (var baseResourceType in param.BaseResourceTypes)
                        {
                            if (baseResourceType == KnownResourceTypes.Resource)
                            {
                                resourceList.UnionWith(_modelInfoProvider.GetResourceTypeNames().ToHashSet());

                                // We added all possible resource types, so no need to continue
                                break;
                            }

                            if (baseResourceType == KnownResourceTypes.DomainResource)
                            {
                                var domainResourceChildResourceTypes = _modelInfoProvider.GetResourceTypeNames().ToHashSet();

                                // Remove types that inherit from Resource directly
                                domainResourceChildResourceTypes.Remove(KnownResourceTypes.Binary);
                                domainResourceChildResourceTypes.Remove(KnownResourceTypes.Bundle);
                                domainResourceChildResourceTypes.Remove(KnownResourceTypes.Parameters);

                                resourceList.UnionWith(domainResourceChildResourceTypes);
                            }
                            else
                            {
                                resourceList.UnionWith(new[] { baseResourceType });
                            }
                        }
                    }

                    _reindexJobRecord.Resources.AddRange(resourceList);
                    _reindexJobRecord.SearchParams.AddRange(notYetIndexedParams.Select(p => p.Url.ToString()));

                    await CalculateTotalAndResourceCounts(cancellationToken);

                    if (_reindexJobRecord.Count == 0)
                    {
                        _reindexJobRecord.Error.Add(new OperationOutcomeIssue(
                                                        OperationOutcomeConstants.IssueSeverity.Information,
                                                        OperationOutcomeConstants.IssueType.Informational,
                                                        Resources.NoResourcesNeedToBeReindexed));
                        await UpdateParametersAndCompleteJob(cancellationToken);

                        return;
                    }

                    // Generate separate queries for each resource type and add them to query list.
                    foreach (string resourceType in _reindexJobRecord.Resources)
                    {
                        // Checking resource specific counts is a performance improvement,
                        // so if an entry for this resource failed to get added to the count dictionary, run a query anyways
                        if (!_reindexJobRecord.ResourceCounts.ContainsKey(resourceType) || _reindexJobRecord.ResourceCounts[resourceType] > 0)
                        {
                            var query = new ReindexJobQueryStatus(resourceType, continuationToken: null)
                            {
                                LastModified = Clock.UtcNow,
                                Status       = OperationStatus.Queued,
                            };

                            _reindexJobRecord.QueryList.TryAdd(query, 1);
                        }
                    }

                    await UpdateJobAsync(cancellationToken);

                    _throttleController.UpdateDatastoreUsage();
                }

                var queryTasks = new List <Task <ReindexJobQueryStatus> >();
                var queryCancellationTokens = new Dictionary <ReindexJobQueryStatus, CancellationTokenSource>();

                // while not all queries are finished
                while (_reindexJobRecord.QueryList.Keys.Where(q =>
                                                              q.Status == OperationStatus.Queued ||
                                                              q.Status == OperationStatus.Running).Any())
                {
                    if (_reindexJobRecord.QueryList.Keys.Where(q => q.Status == OperationStatus.Queued).Any())
                    {
                        // grab the next query from the list which is labeled as queued and run it
                        var query = _reindexJobRecord.QueryList.Keys.Where(q => q.Status == OperationStatus.Queued).OrderBy(q => q.LastModified).FirstOrDefault();
                        CancellationTokenSource queryTokensSource = new CancellationTokenSource();
                        queryCancellationTokens.TryAdd(query, queryTokensSource);

#pragma warning disable CS4014 // Suppressed as we want to continue execution and begin processing the next query while this continues to run
                        queryTasks.Add(ProcessQueryAsync(query, jobSemaphore, queryTokensSource.Token));
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed

                        _logger.LogInformation($"Reindex job task created {queryTasks.Count} Tasks");
                    }

                    // reset stale queries to pending
                    var staleQueries = _reindexJobRecord.QueryList.Keys.Where(
                        q => q.Status == OperationStatus.Running && q.LastModified < Clock.UtcNow - _reindexJobConfiguration.JobHeartbeatTimeoutThreshold);
                    foreach (var staleQuery in staleQueries)
                    {
                        await jobSemaphore.WaitAsync();

                        try
                        {
                            // if this query has a created task, cancel it
                            if (queryCancellationTokens.TryGetValue(staleQuery, out var tokenSource))
                            {
                                try
                                {
                                    tokenSource.Cancel(false);
                                }
                                catch
                                {
                                    // may throw exception if the task is disposed
                                }
                            }

                            staleQuery.Status = OperationStatus.Queued;
                            await UpdateJobAsync(cancellationToken);
                        }
                        finally
                        {
                            jobSemaphore.Release();
                        }
                    }

                    var averageDbConsumption = _throttleController.UpdateDatastoreUsage();
                    _logger.LogInformation($"Reindex avaerage DB consumption: {averageDbConsumption}");
                    var throttleDelayTime = _throttleController.GetThrottleBasedDelay();
                    _logger.LogInformation($"Reindex throttle delay: {throttleDelayTime}");
                    await Task.Delay(_reindexJobRecord.QueryDelayIntervalInMilliseconds + throttleDelayTime);

                    // Remove all finished tasks from the collections of tasks
                    // and cancellationTokens
                    if (queryTasks.Count >= reindexJobRecord.MaximumConcurrency)
                    {
                        var taskArray = queryTasks.ToArray();
                        Task.WaitAny(taskArray);
                        var finishedTasks = queryTasks.Where(t => t.IsCompleted).ToArray();
                        foreach (var finishedTask in finishedTasks)
                        {
                            queryTasks.Remove(finishedTask);
                            queryCancellationTokens.Remove(await finishedTask);
                        }
                    }

                    // if our received CancellationToken is cancelled we should
                    // pass that cancellation request onto all the cancellationTokens
                    // for the currently executing threads
                    if (cancellationToken.IsCancellationRequested)
                    {
                        foreach (var tokenSource in queryCancellationTokens.Values)
                        {
                            tokenSource.Cancel(false);
                        }
                    }
                }

                Task.WaitAll(queryTasks.ToArray());

                await jobSemaphore.WaitAsync();

                try
                {
                    await CheckJobCompletionStatus(cancellationToken);
                }
                finally
                {
                    jobSemaphore.Release();
                }
            }
            catch (JobConflictException)
            {
                // The reindex job was updated externally.
                _logger.LogInformation("The job was updated by another process.");
            }
            catch (Exception ex)
            {
                await jobSemaphore.WaitAsync();

                try
                {
                    _reindexJobRecord.Error.Add(new OperationOutcomeIssue(
                                                    OperationOutcomeConstants.IssueSeverity.Error,
                                                    OperationOutcomeConstants.IssueType.Exception,
                                                    ex.Message));

                    _reindexJobRecord.FailureCount++;

                    _logger.LogError(ex, "Encountered an unhandled exception. The job failure count increased to {failureCount}.", _reindexJobRecord.FailureCount);

                    await UpdateJobAsync(cancellationToken);

                    if (_reindexJobRecord.FailureCount >= _reindexJobConfiguration.ConsecutiveFailuresThreshold)
                    {
                        await CompleteJobAsync(OperationStatus.Failed, cancellationToken);
                    }
                    else
                    {
                        _reindexJobRecord.Status = OperationStatus.Queued;
                        await UpdateJobAsync(cancellationToken);
                    }
                }
                finally
                {
                    jobSemaphore.Release();
                }
            }
            finally
            {
                jobSemaphore.Dispose();
                _contextAccessor.FhirRequestContext = existingFhirRequestContext;
            }
        }
Beispiel #10
0
 public static string[] GetResourceTypeNames()
 {
     EnsureProvider();
     return(_modelInfoProvider.GetResourceTypeNames());
 }
        private async Task <bool> TryPopulateNewJobFields()
        {
            // Build query based on new search params
            // Find supported, but not yet searchable params
            var notYetIndexedParams = _supportedSearchParameterDefinitionManager.GetSearchParametersRequiringReindexing();

            // if there are not any parameters which are supported but not yet indexed, then we have nothing to do
            if (!notYetIndexedParams.Any())
            {
                _reindexJobRecord.Error.Add(new OperationOutcomeIssue(
                                                OperationOutcomeConstants.IssueSeverity.Information,
                                                OperationOutcomeConstants.IssueType.Informational,
                                                Resources.NoSearchParametersNeededToBeIndexed));
                _reindexJobRecord.CanceledTime = Clock.UtcNow;
                await MoveToFinalStatusAsync(OperationStatus.Canceled);

                return(false);
            }

            // From the param list, get the list of necessary resources which should be
            // included in our query
            var resourceList = new HashSet <string>();

            foreach (var param in notYetIndexedParams)
            {
                foreach (var baseResourceType in param.BaseResourceTypes)
                {
                    if (baseResourceType == KnownResourceTypes.Resource)
                    {
                        resourceList.UnionWith(_modelInfoProvider.GetResourceTypeNames().ToHashSet());

                        // We added all possible resource types, so no need to continue
                        break;
                    }

                    if (baseResourceType == KnownResourceTypes.DomainResource)
                    {
                        var domainResourceChildResourceTypes = _modelInfoProvider.GetResourceTypeNames().ToHashSet();

                        // Remove types that inherit from Resource directly
                        domainResourceChildResourceTypes.Remove(KnownResourceTypes.Binary);
                        domainResourceChildResourceTypes.Remove(KnownResourceTypes.Bundle);
                        domainResourceChildResourceTypes.Remove(KnownResourceTypes.Parameters);

                        resourceList.UnionWith(domainResourceChildResourceTypes);
                    }
                    else
                    {
                        resourceList.UnionWith(new[] { baseResourceType });
                    }
                }
            }

            foreach (var resource in resourceList)
            {
                _reindexJobRecord.Resources.Add(resource);
            }

            foreach (var searchParams in notYetIndexedParams.Select(p => p.Url.ToString()))
            {
                _reindexJobRecord.SearchParams.Add(searchParams);
            }

            await CalculateTotalAndResourceCounts();

            if (_reindexJobRecord.Count == 0)
            {
                _reindexJobRecord.Error.Add(new OperationOutcomeIssue(
                                                OperationOutcomeConstants.IssueSeverity.Information,
                                                OperationOutcomeConstants.IssueType.Informational,
                                                Resources.NoResourcesNeedToBeReindexed));
                await UpdateParametersAndCompleteJob();

                return(false);
            }

            // Generate separate queries for each resource type and add them to query list.
            foreach (string resourceType in _reindexJobRecord.Resources)
            {
                // Checking resource specific counts is a performance improvement,
                // so if an entry for this resource failed to get added to the count dictionary, run a query anyways
                if (!_reindexJobRecord.ResourceCounts.ContainsKey(resourceType) || _reindexJobRecord.ResourceCounts[resourceType] > 0)
                {
                    var query = new ReindexJobQueryStatus(resourceType, continuationToken: null)
                    {
                        LastModified = Clock.UtcNow,
                        Status       = OperationStatus.Queued,
                    };

                    _reindexJobRecord.QueryList.TryAdd(query, 1);
                }
            }

            await UpdateJobAsync();

            _throttleController.UpdateDatastoreUsage();
            return(true);
        }