Providing parameter '_total' when searching may lead to paging issues. (#5455)

* Initial failing test.

* Initial failing test.

* fix and changelog.

* applying spotless check.

* small code refactoring.

* test refactoring

---------

Co-authored-by: peartree <etienne.poirier@smilecdr.com>
This commit is contained in:
Etienne Poirier 2023-11-16 07:18:35 -05:00 committed by GitHub
parent 41d9abf6ac
commit 1412873cac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 65 additions and 16 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 5454
jira: SMILE-7295
title: "Previously, searching with parameter '_total' could influence chunked query resultsets and subsequently, paged results. This is now fixed."

View File

@ -572,12 +572,11 @@ public class JpaConfig {
@Scope("prototype") @Scope("prototype")
public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider( public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider(
RequestDetails theRequest, RequestDetails theRequest,
Search theSearch,
SearchTask theSearchTask, SearchTask theSearchTask,
ISearchBuilder theSearchBuilder, ISearchBuilder theSearchBuilder,
RequestPartitionId theRequestPartitionId) { RequestPartitionId theRequestPartitionId) {
return new PersistedJpaSearchFirstPageBundleProvider( return new PersistedJpaSearchFirstPageBundleProvider(
theSearch, theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId); theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId);
} }
@Bean(name = RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER) @Bean(name = RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER)

View File

@ -151,6 +151,11 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; myRequestPartitionHelperSvc = theRequestPartitionHelperSvc;
} }
@VisibleForTesting
public Search getSearchEntityForTesting() {
return getSearchEntity();
}
protected Search getSearchEntity() { protected Search getSearchEntity() {
return mySearchEntity; return mySearchEntity;
} }

View File

@ -55,14 +55,12 @@ public class PersistedJpaBundleProviderFactory {
public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage( public PersistedJpaSearchFirstPageBundleProvider newInstanceFirstPage(
RequestDetails theRequestDetails, RequestDetails theRequestDetails,
Search theSearch,
SearchTask theTask, SearchTask theTask,
ISearchBuilder theSearchBuilder, ISearchBuilder theSearchBuilder,
RequestPartitionId theRequestPartitionId) { RequestPartitionId theRequestPartitionId) {
return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean( return (PersistedJpaSearchFirstPageBundleProvider) myApplicationContext.getBean(
JpaConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER, JpaConfig.PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER,
theRequestDetails, theRequestDetails,
theSearch,
theTask, theTask,
theSearchBuilder, theSearchBuilder,
theRequestPartitionId); theRequestPartitionId);

View File

@ -21,7 +21,6 @@ package ca.uhn.fhir.jpa.search;
import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.ISearchBuilder;
import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.entity.SearchTypeEnum; import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
@ -52,16 +51,14 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public PersistedJpaSearchFirstPageBundleProvider( public PersistedJpaSearchFirstPageBundleProvider(
Search theSearch,
SearchTask theSearchTask, SearchTask theSearchTask,
ISearchBuilder theSearchBuilder, ISearchBuilder theSearchBuilder,
RequestDetails theRequest, RequestDetails theRequest,
RequestPartitionId theRequestPartitionId) { RequestPartitionId theRequestPartitionId) {
super(theRequest, theSearch.getUuid()); super(theRequest, theSearchTask.getSearch());
assert theSearch.getSearchType() != SearchTypeEnum.HISTORY; assert getSearchEntity().getSearchType() != SearchTypeEnum.HISTORY;
setSearchEntity(theSearch);
mySearchTask = theSearchTask; mySearchTask = theSearchTask;
mySearchBuilder = theSearchBuilder; mySearchBuilder = theSearchBuilder;
super.setRequestPartitionId(theRequestPartitionId); super.setRequestPartitionId(theRequestPartitionId);

View File

@ -110,7 +110,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
private final SearchStrategyFactory mySearchStrategyFactory; private final SearchStrategyFactory mySearchStrategyFactory;
private final ExceptionService myExceptionSvc; private final ExceptionService myExceptionSvc;
private final BeanFactory myBeanFactory; private final BeanFactory myBeanFactory;
private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<>();
private final Consumer<String> myOnRemoveSearchTask = myIdToSearchTask::remove; private final Consumer<String> myOnRemoveSearchTask = myIdToSearchTask::remove;
@ -162,6 +162,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
return myIdToSearchTask.keySet(); return myIdToSearchTask.keySet();
} }
@VisibleForTesting
public void setIdToSearchTaskMapForUnitTests(ConcurrentHashMap<String, SearchTask> theIdToSearchTaskMap) {
myIdToSearchTask = theIdToSearchTaskMap;
}
@VisibleForTesting @VisibleForTesting
public void setLoadingThrottleForUnitTests(Integer theLoadingThrottleForUnitTests) { public void setLoadingThrottleForUnitTests(Integer theLoadingThrottleForUnitTests) {
myLoadingThrottleForUnitTests = theLoadingThrottleForUnitTests; myLoadingThrottleForUnitTests = theLoadingThrottleForUnitTests;
@ -571,7 +576,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc<JpaPid> {
task.call(); task.call();
PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage( PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(
theRequestDetails, theSearch, task, theSb, theRequestPartitionId); theRequestDetails, task, theSb, theRequestPartitionId);
ourLog.debug("Search initial phase completed in {}ms", w.getMillis()); ourLog.debug("Search initial phase completed in {}ms", w.getMillis());
return retVal; return retVal;

View File

@ -293,12 +293,11 @@ public class SearchCoordinatorSvcImplTest extends BaseSearchSvc {
} }
private void initAsyncSearches() { private void initAsyncSearches() {
when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(Search.class), nullable(SearchTask.class), nullable(ISearchBuilder.class), nullable(RequestPartitionId.class))).thenAnswer(t -> { when(myPersistedJpaBundleProviderFactory.newInstanceFirstPage(nullable(RequestDetails.class), nullable(SearchTask.class), nullable(ISearchBuilder.class), nullable(RequestPartitionId.class))).thenAnswer(t -> {
RequestDetails requestDetails = t.getArgument(0, RequestDetails.class); RequestDetails requestDetails = t.getArgument(0, RequestDetails.class);
Search search = t.getArgument(1, Search.class); SearchTask searchTask = t.getArgument(1, SearchTask.class);
SearchTask searchTask = t.getArgument(2, SearchTask.class); ISearchBuilder<JpaPid> searchBuilder = t.getArgument(2, ISearchBuilder.class);
ISearchBuilder<JpaPid> searchBuilder = t.getArgument(3, ISearchBuilder.class); PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(searchTask, searchBuilder, requestDetails, null);
PersistedJpaSearchFirstPageBundleProvider retVal = new PersistedJpaSearchFirstPageBundleProvider(search, searchTask, searchBuilder, requestDetails, null);
retVal.setStorageSettingsForUnitTest(new JpaStorageSettings()); retVal.setStorageSettingsForUnitTest(new JpaStorageSettings());
retVal.setTxServiceForUnitTest(myTransactionService); retVal.setTxServiceForUnitTest(myTransactionService);
retVal.setSearchCoordinatorSvcForUnitTest(mySvc); retVal.setSearchCoordinatorSvcForUnitTest(mySvc);

View File

@ -8,7 +8,9 @@ import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
import ca.uhn.fhir.jpa.search.builder.tasks.SearchTask;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
@ -29,7 +31,9 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.test.utilities.ProxyUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.checkerframework.checker.units.qual.A;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BodyStructure; import org.hl7.fhir.r4.model.BodyStructure;
import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.CodeableConcept;
@ -51,6 +55,8 @@ import org.hl7.fhir.r4.model.UriType;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean; import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean;
@ -58,6 +64,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -102,6 +109,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
public final void after() { public final void after() {
mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null); mySearchCoordinatorSvcImpl.setLoadingThrottleForUnitTests(null);
mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE); mySearchCoordinatorSvcImpl.setSyncSizeForUnitTests(QueryParameterUtils.DEFAULT_SYNC_SIZE);
mySearchCoordinatorSvcImpl.setIdToSearchTaskMapForUnitTests(new ConcurrentHashMap<>());
myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds()); myStorageSettings.setSearchPreFetchThresholds(new JpaStorageSettings().getSearchPreFetchThresholds());
myCaptureQueriesListener.setCaptureQueryStackTrace(false); myCaptureQueriesListener.setCaptureQueryStackTrace(false);
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields()); myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields());
@ -214,6 +222,39 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
} }
@Test
public void testSearchCoordinatorSvc_whenExecutingSearchWithParamTotal_returnsBundleSynchronizedWithBackingSearchCapabilities(){
ArgumentCaptor<String> keyArgumentCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<SearchTask> valueArgumentCaptor = ArgumentCaptor.forClass(SearchTask.class);
ConcurrentHashMap<String, SearchTask> spyingIdToSearchTaskMap = Mockito.spy(new ConcurrentHashMap<>());
mySearchCoordinatorSvcImpl.setIdToSearchTaskMapForUnitTests(spyingIdToSearchTaskMap);
create200Patients();
SearchParameterMap params = new SearchParameterMap();
params.add(Patient.SP_NAME, new StringParam("FAM"));
params.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
// calling dao.search will end up invoking the searchCoordinatorSvc. based on the provided search parameters, the svc
// generates and triggers a searchTask which will create chunked resultsets. the searchTask make use of a searchEntity
// to keep track of search progress and key indicators like the search total count.
PersistedJpaSearchFirstPageBundleProvider results = (PersistedJpaSearchFirstPageBundleProvider) myPatientDao.search(params);
// to return the correct resources through method getResources(), the PersistedJpaSearchFirstPageBundleProvider generated by the
// searchCoordinatorSvc needs to access the same searchEntity that was used by the searchTask. this test ensures that the searchEntity
// operated upon by the searchTask is the same as the searchEntity that is found in the generated PersistedJpaSearchFirstPageBundleProvider.
Mockito.verify(spyingIdToSearchTaskMap, Mockito.times(1)).put(keyArgumentCaptor.capture(), valueArgumentCaptor.capture());
Search bundleProviderSearch = results.getSearchEntityForTesting();
Search backingSearch = valueArgumentCaptor.getValue().getSearch();
assertThat(bundleProviderSearch.getUuid(), equalTo(keyArgumentCaptor.getValue()));
assertThat(bundleProviderSearch.getUuid(), equalTo(backingSearch.getUuid()));
assertThat(bundleProviderSearch.getStatus(), equalTo(backingSearch.getStatus()));
assertThat(bundleProviderSearch.getId(), equalTo(backingSearch.getId()));
}
@Test @Test
public void testFetchTotalAccurateForSlowLoading() { public void testFetchTotalAccurateForSlowLoading() {
create200Patients(); create200Patients();