Merging master into working branch to resolve conflicts.

# Conflicts:
#	src/changes/changes.xml
This commit is contained in:
Diederik Muylwyk 2019-09-26 20:19:22 -04:00
commit 3e173bb07c
20 changed files with 269 additions and 157 deletions

View File

@ -17,5 +17,3 @@ http://hapi.fhir.org/
This project is Open Source, licensed under the Apache Software License 2.0. This project is Open Source, licensed under the Apache Software License 2.0.
Please see [this wiki page](https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help) for information on where to get help with HAPI FHIR. Please see [Smile CDR](https://smilecdr.com) for information on commercial support. Please see [this wiki page](https://github.com/jamesagnew/hapi-fhir/wiki/Getting-Help) for information on where to get help with HAPI FHIR. Please see [Smile CDR](https://smilecdr.com) for information on commercial support.
---

View File

@ -1,7 +1,4 @@
# Starter pipeline # HAPI FHIR Build Pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
variables: variables:
MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository
@ -13,7 +10,6 @@ trigger:
pool: pool:
vmImage: 'ubuntu-latest' vmImage: 'ubuntu-latest'
jobs: jobs:
- job: Build - job: Build
timeoutInMinutes: 360 timeoutInMinutes: 360
@ -23,37 +19,37 @@ jobs:
key: maven key: maven
path: $(MAVEN_CACHE_FOLDER) path: $(MAVEN_CACHE_FOLDER)
displayName: Cache Maven local repo displayName: Cache Maven local repo
- task: Maven@3 - task: Maven@3
inputs: inputs:
#mavenPomFile: 'pom.xml' goals: 'clean install'
goals: 'clean install' # Optional # These are Maven CLI options (and show up in the build logs) - "-nsu"=Don't update snapshots. We can remove this when Maven OSS is more healthy
options: '-P ALLMODULES,JACOCO' options: '-P ALLMODULES,JACOCO -nsu'
#publishJUnitResults: true # These are JVM options (and don't show up in the build logs)
#testResultsFiles: '**/surefire-reports/TEST-*.xml' # Required when publishJUnitResults == True mavenOptions: '-Xmx2048m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS -Duser.timezone=America/Toronto'
#testRunTitle: # Optional
#codeCoverageToolOption: 'None' # Optional. Options: none, cobertura, jaCoCo. Enabling code coverage inserts the `clean` goal into the Maven goals list when Maven runs.
#codeCoverageClassFilter: # Optional. Comma-separated list of filters to include or exclude classes from collecting code coverage. For example: +:com.*,+:org.*,-:my.app*.*
#codeCoverageClassFilesDirectories: # Optional
#codeCoverageSourceDirectories: # Optional
#codeCoverageFailIfEmpty: false # Optional
#javaHomeOption: 'JDKVersion' # Options: jDKVersion, path
#jdkVersionOption: 'default' # Optional. Options: default, 1.11, 1.10, 1.9, 1.8, 1.7, 1.6
#jdkDirectory: # Required when javaHomeOption == Path
#jdkArchitectureOption: 'x64' # Optional. Options: x86, x64
#mavenVersionOption: 'Default' # Options: default, path
#mavenDirectory: # Required when mavenVersionOption == Path
#mavenSetM2Home: false # Required when mavenVersionOption == Path
mavenOptions: '-Xmx2048m $(MAVEN_OPTS) -Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss,SSS' # Optional
#mavenAuthenticateFeed: false
#effectivePomSkip: false
#sonarQubeRunAnalysis: false
#sqMavenPluginVersionChoice: 'latest' # Required when sonarQubeRunAnalysis == True# Options: latest, pom
#checkStyleRunAnalysis: false # Optional
#pmdRunAnalysis: false # Optional
#findBugsRunAnalysis: false # Optional
- script: bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) - script: bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN)
displayName: 'codecov' displayName: 'codecov'
# Potential Additional Maven3 Options:
#publishJUnitResults: true
#testResultsFiles: '**/surefire-reports/TEST-*.xml' # Required when publishJUnitResults == True
#testRunTitle: # Optional
#codeCoverageToolOption: 'None' # Optional. Options: none, cobertura, jaCoCo. Enabling code coverage inserts the `clean` goal into the Maven goals list when Maven runs.
#codeCoverageClassFilter: # Optional. Comma-separated list of filters to include or exclude classes from collecting code coverage. For example: +:com.*,+:org.*,-:my.app*.*
#codeCoverageClassFilesDirectories: # Optional
#codeCoverageSourceDirectories: # Optional
#codeCoverageFailIfEmpty: false # Optional
#javaHomeOption: 'JDKVersion' # Options: jDKVersion, path
#jdkVersionOption: 'default' # Optional. Options: default, 1.11, 1.10, 1.9, 1.8, 1.7, 1.6
#jdkDirectory: # Required when javaHomeOption == Path
#jdkArchitectureOption: 'x64' # Optional. Options: x86, x64
#mavenVersionOption: 'Default' # Options: default, path
#mavenDirectory: # Required when mavenVersionOption == Path
#mavenSetM2Home: false # Required when mavenVersionOption == Path
#mavenAuthenticateFeed: false
#effectivePomSkip: false
#sonarQubeRunAnalysis: false
#sqMavenPluginVersionChoice: 'latest' # Required when sonarQubeRunAnalysis == True# Options: latest, pom
#checkStyleRunAnalysis: false # Optional
#pmdRunAnalysis: false # Optional
#findBugsRunAnalysis: false # Optional

View File

@ -52,15 +52,6 @@ public class GsonStructure implements JsonLikeStructure {
super(); super();
} }
public GsonStructure (JsonObject json) {
super();
setNativeObject(json);
}
public GsonStructure (JsonArray json) {
super();
setNativeArray(json);
}
public void setNativeObject (JsonObject json) { public void setNativeObject (JsonObject json) {
this.rootType = ROOT_TYPE.OBJECT; this.rootType = ROOT_TYPE.OBJECT;
this.nativeRoot = json; this.nativeRoot = json;
@ -111,8 +102,7 @@ public class GsonStructure implements JsonLikeStructure {
if (nextInt == '{') { if (nextInt == '{') {
JsonObject root = gson.fromJson(pbr, JsonObject.class); JsonObject root = gson.fromJson(pbr, JsonObject.class);
setNativeObject(root); setNativeObject(root);
} else } else if (nextInt == '[') {
if (nextInt == '[') {
JsonArray root = gson.fromJson(pbr, JsonArray.class); JsonArray root = gson.fromJson(pbr, JsonArray.class);
setNativeArray(root); setNativeArray(root);
} }

View File

@ -108,9 +108,13 @@ public class OperationOutcomeUtil {
if (theOutcome == null) { if (theOutcome == null) {
return false; return false;
} }
return getIssueCount(theCtx, theOutcome) > 0;
}
public static int getIssueCount(FhirContext theCtx, IBaseOperationOutcome theOutcome) {
RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome);
BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue");
return issueChild.getAccessor().getValues(theOutcome).size() > 0; return issueChild.getAccessor().getValues(theOutcome).size();
} }
public static IBaseOperationOutcome newInstance(FhirContext theCtx) { public static IBaseOperationOutcome newInstance(FhirContext theCtx) {
@ -152,5 +156,4 @@ public class OperationOutcomeUtil {
locationChild.getMutator().addValue(theIssue, locationElem); locationChild.getMutator().addValue(theIssue, locationElem);
} }
} }
} }

View File

@ -53,21 +53,6 @@ public class Search implements ICachedSearchDetails, Serializable {
private static final int FAILURE_MESSAGE_LENGTH = 500; private static final int FAILURE_MESSAGE_LENGTH = 500;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final Logger ourLog = LoggerFactory.getLogger(Search.class); private static final Logger ourLog = LoggerFactory.getLogger(Search.class);
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myLastUpdatedHigh", myLastUpdatedHigh)
.append("myLastUpdatedLow", myLastUpdatedLow)
.append("myNumFound", myNumFound)
.append("myNumBlocked", myNumBlocked)
.append("myStatus", myStatus)
.append("myTotalCount", myTotalCount)
.append("myUuid", myUuid)
.append("myVersion", myVersion)
.toString();
}
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED", nullable = false, updatable = false) @Column(name = "CREATED", nullable = false, updatable = false)
private Date myCreated; private Date myCreated;
@ -139,6 +124,20 @@ public class Search implements ICachedSearchDetails, Serializable {
super(); super();
} }
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myLastUpdatedHigh", myLastUpdatedHigh)
.append("myLastUpdatedLow", myLastUpdatedLow)
.append("myNumFound", myNumFound)
.append("myNumBlocked", myNumBlocked)
.append("myStatus", myStatus)
.append("myTotalCount", myTotalCount)
.append("myUuid", myUuid)
.append("myVersion", myVersion)
.toString();
}
public int getNumBlocked() { public int getNumBlocked() {
return myNumBlocked != null ? myNumBlocked : 0; return myNumBlocked != null ? myNumBlocked : 0;
} }
@ -351,8 +350,8 @@ public class Search implements ICachedSearchDetails, Serializable {
return myVersion; return myVersion;
} }
public SearchParameterMap getSearchParameterMap() { public Optional<SearchParameterMap> getSearchParameterMap() {
return SerializationUtils.deserialize(mySearchParameterMap); return Optional.ofNullable(mySearchParameterMap).map(t -> SerializationUtils.deserialize(mySearchParameterMap));
} }
public void setSearchParameterMap(SearchParameterMap theSearchParameterMap) { public void setSearchParameterMap(SearchParameterMap theSearchParameterMap) {

View File

@ -173,4 +173,11 @@ public class TermCodeSystemVersion implements Serializable {
"Version ID exceeds maximum length (" + MAX_VERSION_LENGTH + "): " + length(theCodeSystemDisplayName)); "Version ID exceeds maximum length (" + MAX_VERSION_LENGTH + "): " + length(theCodeSystemDisplayName));
myCodeSystemDisplayName = theCodeSystemDisplayName; myCodeSystemDisplayName = theCodeSystemDisplayName;
} }
public TermConcept addConcept() {
TermConcept concept = new TermConcept();
concept.setCodeSystemVersion(this);
getConcepts().add(concept);
return concept;
}
} }

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface ISearchCoordinatorSvc { public interface ISearchCoordinatorSvc {
@ -37,4 +38,10 @@ public interface ISearchCoordinatorSvc {
IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, @Nullable RequestDetails theRequestDetails); IBundleProvider registerSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, @Nullable RequestDetails theRequestDetails);
/**
* Fetch the total number of search results for the given currently executing search, if one is currently executing and
* the total is known. Will return empty otherwise
*/
Optional<Integer> getSearchTotal(String theUuid);
} }

View File

@ -191,12 +191,12 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
if (mySearchEntity == null) { if (mySearchEntity == null) {
ensureDependenciesInjected(); ensureDependenciesInjected();
Optional<Search> search = mySearchCacheSvc.fetchByUuid(myUuid); Optional<Search> searchOpt = mySearchCacheSvc.fetchByUuid(myUuid);
if (!search.isPresent()) { if (!searchOpt.isPresent()) {
return false; return false;
} }
setSearchEntity(search.get()); setSearchEntity(searchOpt.get());
ourLog.trace("Retrieved search with version {} and total {}", mySearchEntity.getVersion(), mySearchEntity.getTotalCount()); ourLog.trace("Retrieved search with version {} and total {}", mySearchEntity.getVersion(), mySearchEntity.getTotalCount());
@ -292,10 +292,16 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity); SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearchEntity);
Integer size = mySearchEntity.getTotalCount(); Integer size = mySearchEntity.getTotalCount();
if (size == null) { if (size != null) {
return null; return Math.max(0, size);
} }
return Math.max(0, size);
if (mySearchEntity.getSearchType() == SearchTypeEnum.HISTORY) {
return null;
} else {
return mySearchCoordinatorSvc.getSearchTotal(myUuid).orElse(null);
}
} }
// Note: Leave as protected, HSPC depends on this // Note: Leave as protected, HSPC depends on this

View File

@ -62,6 +62,8 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch); SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
mySearchTask.awaitInitialSync();
ourLog.trace("Fetching search resource PIDs from task: {}", mySearchTask.getClass()); ourLog.trace("Fetching search resource PIDs from task: {}", mySearchTask.getClass());
final List<Long> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex); final List<Long> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex);
ourLog.trace("Done fetching search resource PIDs"); ourLog.trace("Done fetching search resource PIDs");

View File

@ -92,7 +92,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public static final int DEFAULT_SYNC_SIZE = 250; public static final int DEFAULT_SYNC_SIZE = 250;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchCoordinatorSvcImpl.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchCoordinatorSvcImpl.class);
private final ConcurrentHashMap<String, BaseTask> myIdToSearchTask = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, SearchTask> myIdToSearchTask = new ConcurrentHashMap<>();
@Autowired @Autowired
private FhirContext myContext; private FhirContext myContext;
@Autowired @Autowired
@ -151,7 +151,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
@Override @Override
public void cancelAllActiveSearches() { public void cancelAllActiveSearches() {
for (BaseTask next : myIdToSearchTask.values()) { for (SearchTask next : myIdToSearchTask.values()) {
next.requestImmediateAbort(); next.requestImmediateAbort();
try { try {
next.getCompletionLatch().await(30, TimeUnit.SECONDS); next.getCompletionLatch().await(30, TimeUnit.SECONDS);
@ -171,17 +171,35 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// If we're actively searching right now, don't try to do anything until at least one batch has been
// persisted in the DB
SearchTask searchTask = myIdToSearchTask.get(theUuid);
if (searchTask != null) {
searchTask.awaitInitialSync();
}
ourLog.trace("About to start looking for resources {}-{}", theFrom, theTo);
Search search; Search search;
StopWatch sw = new StopWatch(); StopWatch sw = new StopWatch();
while (true) { while (true) {
if (myNeverUseLocalSearchForUnitTests == false) { if (myNeverUseLocalSearchForUnitTests == false) {
BaseTask task = myIdToSearchTask.get(theUuid); if (searchTask != null) {
if (task != null) {
ourLog.trace("Local search found"); ourLog.trace("Local search found");
List<Long> resourcePids = task.getResourcePids(theFrom, theTo); List<Long> resourcePids = searchTask.getResourcePids(theFrom, theTo);
if (resourcePids != null) { if (resourcePids != null) {
return resourcePids; ourLog.trace("Local search returned {} pids, wanted {}-{} - Search: {}", resourcePids.size(), theFrom, theTo, searchTask.getSearch());
/*
* Generally, if a search task is open, the fastest possible thing is to just return its results. This
* will work most of the time, but can fail if the task hit a search threshold and the client is requesting
* results beyond that threashold. In that case, we'll keep going below, since that will trigger another
* task.
*/
if ((searchTask.getSearch().getNumFound() - searchTask.getSearch().getNumBlocked()) >= theTo || resourcePids.size() == (theTo - theFrom)) {
return resourcePids;
}
} }
} }
} }
@ -189,18 +207,18 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
search = mySearchCacheSvc search = mySearchCacheSvc
.fetchByUuid(theUuid) .fetchByUuid(theUuid)
.orElseThrow(() -> { .orElseThrow(() -> {
ourLog.debug("Client requested unknown paging ID[{}]", theUuid); ourLog.trace("Client requested unknown paging ID[{}]", theUuid);
String msg = myContext.getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", theUuid); String msg = myContext.getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", theUuid);
return new ResourceGoneException(msg); return new ResourceGoneException(msg);
}); });
verifySearchHasntFailedOrThrowInternalErrorException(search); verifySearchHasntFailedOrThrowInternalErrorException(search);
if (search.getStatus() == SearchStatusEnum.FINISHED) { if (search.getStatus() == SearchStatusEnum.FINISHED) {
ourLog.debug("Search entity marked as finished with {} results", search.getNumFound()); ourLog.trace("Search entity marked as finished with {} results", search.getNumFound());
break; break;
} }
if (search.getNumFound() >= theTo) { if (search.getNumFound() >= theTo) {
ourLog.debug("Search entity has {} results so far", search.getNumFound()); ourLog.trace("Search entity has {} results so far", search.getNumFound());
break; break;
} }
@ -212,11 +230,12 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
// If the search was saved in "pass complete mode" it's probably time to // If the search was saved in "pass complete mode" it's probably time to
// start a new pass // start a new pass
if (search.getStatus() == SearchStatusEnum.PASSCMPLET) { if (search.getStatus() == SearchStatusEnum.PASSCMPLET) {
ourLog.trace("Going to try to start next search");
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search); Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search);
if (newSearch.isPresent()) { if (newSearch.isPresent()) {
search = newSearch.get(); search = newSearch.get();
String resourceType = search.getResourceType(); String resourceType = search.getResourceType();
SearchParameterMap params = search.getSearchParameterMap(); SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType); IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(resourceType);
SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequestDetails); SearchContinuationTask task = new SearchContinuationTask(search, resourceDao, params, resourceType, theRequestDetails);
myIdToSearchTask.put(search.getUuid(), task); myIdToSearchTask.put(search.getUuid(), task);
@ -231,10 +250,11 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
} }
ourLog.trace("Finished looping");
List<Long> pids = mySearchResultCacheSvc.fetchResultPids(search, theFrom, theTo); List<Long> pids = mySearchResultCacheSvc.fetchResultPids(search, theFrom, theTo);
ourLog.trace("Fetched {} results", pids.size());
return pids; return pids;
} }
@ -290,6 +310,38 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
@Override
public Optional<Integer> getSearchTotal(String theUuid) {
SearchTask task = myIdToSearchTask.get(theUuid);
if (task != null) {
return Optional.ofNullable(task.awaitInitialSync());
}
/*
* In case there is no running search, if the total is listed as accurate we know one is coming
* so let's wait a bit for it to show up
*/
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Optional<Search> search = mySearchCacheSvc.fetchByUuid(theUuid);
if (search.isPresent()) {
Optional<SearchParameterMap> searchParameterMap = search.get().getSearchParameterMap();
if (searchParameterMap.isPresent() && searchParameterMap.get().getSearchTotalMode() == SearchTotalModeEnum.ACCURATE) {
for (int i = 0; i < 10; i++) {
if (search.isPresent()) {
verifySearchHasntFailedOrThrowInternalErrorException(search.get());
if (search.get().getTotalCount() != null) {
return Optional.of(search.get().getTotalCount());
}
}
search = mySearchCacheSvc.fetchByUuid(theUuid);
}
}
}
return Optional.empty();
}
@NotNull @NotNull
private IBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString) { private IBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString) {
StopWatch w = new StopWatch(); StopWatch w = new StopWatch();
@ -516,7 +568,20 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myInterceptorBroadcaster = theInterceptorBroadcaster; myInterceptorBroadcaster = theInterceptorBroadcaster;
} }
public abstract class BaseTask implements Callable<Void> { /**
* A search task is a Callable task that runs in
* a thread pool to handle an individual search. One instance
* is created for any requested search and runs from the
* beginning to the end of the search.
* <p>
* Understand:
* This class executes in its own thread separate from the
* web server client thread that made the request. We do that
* so that we can return to the client as soon as possible,
* but keep the search going in the background (and have
* the next page of results ready to go when the client asks).
*/
public class SearchTask implements Callable<Void> {
private final SearchParameterMap myParams; private final SearchParameterMap myParams;
private final IDao myCallingDao; private final IDao myCallingDao;
private final String myResourceType; private final String myResourceType;
@ -538,7 +603,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
/** /**
* Constructor * Constructor
*/ */
protected BaseTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest) { protected SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest) {
mySearch = theSearch; mySearch = theSearch;
myCallingDao = theCallingDao; myCallingDao = theCallingDao;
myParams = theParams; myParams = theParams;
@ -549,6 +614,29 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
myRequest = theRequest; myRequest = theRequest;
} }
/**
* This method is called by the server HTTP thread, and
* will block until at least one page of results have been
* fetched from the DB, and will never block after that.
*/
Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync");
do {
try {
if (getInitialCollectionLatch().await(250, TimeUnit.MILLISECONDS)) {
break;
}
} catch (InterruptedException e) {
// Shouldn't happen
Thread.currentThread().interrupt();
throw new InternalErrorException(e);
}
} while (getSearch().getStatus() == SearchStatusEnum.LOADING);
ourLog.trace("Initial sync completed");
return getSearch().getTotalCount();
}
protected Search getSearch() { protected Search getSearch() {
return mySearch; return mySearch;
} }
@ -1010,7 +1098,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
public class SearchContinuationTask extends BaseTask { public class SearchContinuationTask extends SearchTask {
public SearchContinuationTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest) { public SearchContinuationTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequest) {
super(theSearch, theCallingDao, theParams, theResourceType, theRequest); super(theSearch, theCallingDao, theParams, theResourceType, theRequest);
@ -1041,51 +1129,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
} }
/**
* A search task is a Callable task that runs in
* a thread pool to handle an individual search. One instance
* is created for any requested search and runs from the
* beginning to the end of the search.
* <p>
* Understand:
* This class executes in its own thread separate from the
* web server client thread that made the request. We do that
* so that we can return to the client as soon as possible,
* but keep the search going in the background (and have
* the next page of results ready to go when the client asks).
*/
class SearchTask extends BaseTask {
/**
* Constructor
*/
SearchTask(Search theSearch, IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails) {
super(theSearch, theCallingDao, theParams, theResourceType, theRequestDetails);
}
/**
* This method is called by the server HTTP thread, and
* will block until at least one page of results have been
* fetched from the DB, and will never block after that.
*/
Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync");
do {
try {
if (getInitialCollectionLatch().await(250, TimeUnit.MILLISECONDS)) {
break;
}
} catch (InterruptedException e) {
// Shouldn't happen
throw new InternalErrorException(e);
}
} while (getSearch().getStatus() == SearchStatusEnum.LOADING);
ourLog.trace("Initial sync completed");
return getSearch().getTotalCount();
}
}
public static void populateSearchEntity(SearchParameterMap theParams, String theResourceType, String theSearchUuid, String theQueryString, Search theSearch) { public static void populateSearchEntity(SearchParameterMap theParams, String theResourceType, String theSearchUuid, String theQueryString, Search theSearch) {
theSearch.setDeleted(false); theSearch.setDeleted(false);

View File

@ -58,9 +58,6 @@ public class DatabaseSearchResultCacheSvcImpl implements ISearchResultCacheSvc {
ourLog.debug("fetchResultPids for range {}-{} returned {} pids", theFrom, theTo, retVal.size()); ourLog.debug("fetchResultPids for range {}-{} returned {} pids", theFrom, theTo, retVal.size());
// FIXME: should we remove the blocked number from this message?
Validate.isTrue((theSearch.getNumFound() - theSearch.getNumBlocked()) < theTo || retVal.size() == (theTo - theFrom), "Failed to find results in cache, requested %d - %d and got %d with total found=%d and blocked %s", theFrom, theTo, retVal.size(), theSearch.getNumFound(), theSearch.getNumBlocked());
return new ArrayList<>(retVal); return new ArrayList<>(retVal);
} }

View File

@ -270,7 +270,7 @@ public class HapiTerminologySvcR5 extends BaseHapiTerminologySvcImpl implements
public IValidationSupport.CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) { public IValidationSupport.CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) {
TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return txTemplate.execute(t-> { return txTemplate.execute(t -> {
Optional<TermConcept> codeOpt = findCode(theCodeSystem, theCode); Optional<TermConcept> codeOpt = findCode(theCodeSystem, theCode);
if (codeOpt.isPresent()) { if (codeOpt.isPresent()) {
TermConcept code = codeOpt.get(); TermConcept code = codeOpt.get();

View File

@ -367,6 +367,11 @@ public abstract class BaseJpaR4Test extends BaseJpaTest {
myInterceptorRegistry.registerInterceptor(myPerformanceTracingLoggingInterceptor); myInterceptorRegistry.registerInterceptor(myPerformanceTracingLoggingInterceptor);
} }
@Before
public void beforeUnregisterAllSubscriptions() {
mySubscriptionRegistry.unregisterAllSubscriptions();
}
@Before @Before
public void beforeFlushFT() { public void beforeFlushFT() {
runInTransaction(() -> { runInTransaction(() -> {

View File

@ -120,20 +120,24 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
myPatientDao.update(p); myPatientDao.update(p);
myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190)); myDaoConfig.setSearchPreFetchThresholds(Arrays.asList(20, 50, 190));
SearchParameterMap params;
IBundleProvider results;
String uuid;
List<String> ids;
// Search with count only // Search with count only
SearchParameterMap params = new SearchParameterMap(); params = new SearchParameterMap();
params.add(Patient.SP_NAME, new StringParam("FAM")); params.add(Patient.SP_NAME, new StringParam("FAM"));
params.setSummaryMode((SummaryEnum.COUNT)); params.setSummaryMode((SummaryEnum.COUNT));
IBundleProvider results = myPatientDao.search(params); results = myPatientDao.search(params);
String uuid = results.getUuid(); uuid = results.getUuid();
ourLog.info("** Search returned UUID: {}", uuid); ourLog.info("** Search returned UUID: {}", uuid);
assertEquals(201, results.size().intValue()); assertEquals(201, results.size().intValue());
List<String> ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true); ids = toUnqualifiedVersionlessIdValues(results, 0, 10, true);
assertThat(ids, empty()); assertThat(ids, empty());
assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue()); assertEquals(201, myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size().intValue());
// Seach with total explicitly requested // Search with total explicitly requested
params = new SearchParameterMap(); params = new SearchParameterMap();
params.add(Patient.SP_NAME, new StringParam("FAM")); params.add(Patient.SP_NAME, new StringParam("FAM"));
params.setSearchTotalMode(SearchTotalModeEnum.ACCURATE); params.setSearchTotalMode(SearchTotalModeEnum.ACCURATE);
@ -213,7 +217,6 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals("Patient/PT00000", ids.get(0)); assertEquals("Patient/PT00000", ids.get(0));
assertEquals("Patient/PT00009", ids.get(9)); assertEquals("Patient/PT00009", ids.get(9));
await().until(() -> myDatabaseBackedPagingProvider.retrieveResultList(null, uuid).size() != null);
results = myDatabaseBackedPagingProvider.retrieveResultList(null, uuid); results = myDatabaseBackedPagingProvider.retrieveResultList(null, uuid);
Integer resultsSize = results.size(); Integer resultsSize = results.size();
assertEquals(200, resultsSize.intValue()); assertEquals(200, resultsSize.intValue());
@ -354,6 +357,15 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
* 20 should be prefetched since that's the initial page size * 20 should be prefetched since that's the initial page size
*/ */
await().until(()->{
return runInTransaction(()->{
return mySearchEntityDao
.findByUuidAndFetchIncludes(uuid)
.orElseThrow(() -> new InternalErrorException(""))
.getStatus() == SearchStatusEnum.PASSCMPLET;
});
});
runInTransaction(() -> { runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException("")); Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException(""));
assertEquals(20, search.getNumFound()); assertEquals(20, search.getNumFound());
@ -636,6 +648,10 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
assertEquals("Patient/PT00000", ids.get(0)); assertEquals("Patient/PT00000", ids.get(0));
assertEquals(1, ids.size()); assertEquals(1, ids.size());
await().until(()-> runInTransaction(()-> mySearchEntityDao
.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException(""))
.getStatus() == SearchStatusEnum.FINISHED));
runInTransaction(() -> { runInTransaction(() -> {
Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException("")); Search search = mySearchEntityDao.findByUuidAndFetchIncludes(uuid).orElseThrow(() -> new InternalErrorException(""));
assertEquals(SearchStatusEnum.FINISHED, search.getStatus()); assertEquals(SearchStatusEnum.FINISHED, search.getStatus());
@ -778,6 +794,7 @@ public class FhirResourceDaoR4SearchOptimizedTest extends BaseJpaR4Test {
search.getResources(0, 20); search.getResources(0, 20);
ourLog.info("** Done retrieving resources"); ourLog.info("** Done retrieving resources");
await().until(()->myCaptureQueriesListener.countSelectQueries() == 4);
myCaptureQueriesListener.logSelectQueriesForCurrentThread(); myCaptureQueriesListener.logSelectQueriesForCurrentThread();
assertEquals(4, myCaptureQueriesListener.countSelectQueries()); assertEquals(4, myCaptureQueriesListener.countSelectQueries());

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.dao.r4; package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.term.IHapiTerminologySvc;
import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.ValidationModeEnum;
@ -8,10 +10,12 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.IValidatorModule;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource; 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.hapi.validation.FhirInstanceValidator; import org.hl7.fhir.r4.hapi.validation.FhirInstanceValidator;
@ -28,16 +32,18 @@ import org.springframework.test.util.AopTestUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat; import static org.junit.Assert.*;
import static org.junit.Assert.fail;
public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ValidateTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4ValidateTest.class);
@Autowired @Autowired
private IValidatorModule myValidatorModule; private IValidatorModule myValidatorModule;
@Autowired
private IHapiTerminologySvc myTerminologySvc;
@Test @Test
public void testValidateStructureDefinition() throws Exception { public void testValidateStructureDefinition() throws Exception {
@ -106,6 +112,43 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test {
assertThat(ooString, containsString("Element '/f:Observation.device': minimum required = 1, but only found 0")); assertThat(ooString, containsString("Element '/f:Observation.device': minimum required = 1, but only found 0"));
} }
@Test
public void testValidateUsingExternallyDefinedCode() {
CodeSystem codeSystem = new CodeSystem();
codeSystem.setUrl("http://foo");
codeSystem.setContent(CodeSystem.CodeSystemContentMode.NOTPRESENT);
IIdType csId = myCodeSystemDao.create(codeSystem).getId();
TermCodeSystemVersion csv = new TermCodeSystemVersion();
csv.addConcept().setCode("bar").setDisplay("Bar Code");
myTerminologySvc.storeNewCodeSystemVersion(codeSystem, csv, mySrd, Collections.emptyList(), Collections.emptyList());
// Validate a resource containing this codesystem in a field with an extendable binding
Patient patient = new Patient();
patient.getText().setStatus(Narrative.NarrativeStatus.GENERATED).setDivAsString("<div>hello</div>");
patient
.addIdentifier()
.setSystem("http://example.com")
.setValue("12345")
.getType()
.addCoding()
.setSystem("http://foo")
.setCode("bar");
MethodOutcome outcome = myPatientDao.validate(patient, null, encode(patient), EncodingEnum.JSON, ValidationModeEnum.CREATE, null, mySrd);
IBaseOperationOutcome oo = outcome.getOperationOutcome();
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo));
// It would be ok for this to produce 0 issues, or just an information message too
assertEquals(1, OperationOutcomeUtil.getIssueCount(myFhirCtx, oo));
assertEquals("None of the codes provided are in the value set http://hl7.org/fhir/ValueSet/identifier-type (http://hl7.org/fhir/ValueSet/identifier-type, and a code should come from this value set unless it has no suitable code) (codes = http://foo#bar)", OperationOutcomeUtil.getFirstIssueDetails(myFhirCtx, oo));
}
private String encode(Patient thePatient) {
return myFhirCtx.newJsonParser().encodeResourceToString(thePatient);
}
private OperationOutcome doTestValidateResourceContainingProfileDeclaration(String methodName, EncodingEnum enc) throws IOException { private OperationOutcome doTestValidateResourceContainingProfileDeclaration(String methodName, EncodingEnum enc) throws IOException {
Bundle vss = loadResourceFromClasspath(Bundle.class, "/org/hl7/fhir/r4/model/valueset/valuesets.xml"); Bundle vss = loadResourceFromClasspath(Bundle.class, "/org/hl7/fhir/r4/model/valueset/valuesets.xml");
myValueSetDao.update((ValueSet) findResourceByIdInBundle(vss, "observation-status"), mySrd); myValueSetDao.update((ValueSet) findResourceByIdInBundle(vss, "observation-status"), mySrd);

View File

@ -293,6 +293,7 @@ public class SearchCoordinatorSvcImplTest {
myExpectedNumberOfSearchBuildersCreated = 4; myExpectedNumberOfSearchBuildersCreated = 4;
} }
@Test @Test
public void testAsyncSearchSmallResultSetSameCoordinator() { public void testAsyncSearchSmallResultSetSameCoordinator() {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();

View File

@ -98,11 +98,9 @@ public class ValidationSupportChain implements IValidationSupport {
@Override @Override
public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) { public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) {
for (IValidationSupport next : myChain) { for (IValidationSupport next : myChain) {
if (next.isCodeSystemSupported(theCtx, theSystem)) { CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem);
CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem); if (retVal != null) {
if (retVal != null) { return retVal;
return retVal;
}
} }
} }
return null; return null;

View File

@ -77,11 +77,9 @@ public class ValidationSupportChain implements IValidationSupport {
@Override @Override
public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) { public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) {
for (IValidationSupport next : myChain) { for (IValidationSupport next : myChain) {
if (next.isCodeSystemSupported(theCtx, theSystem)) { CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem);
CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem); if (retVal != null) {
if (retVal != null) { return retVal;
return retVal;
}
} }
} }
return null; return null;

View File

@ -75,11 +75,9 @@ public class ValidationSupportChain implements IValidationSupport {
@Override @Override
public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) { public CodeSystem fetchCodeSystem(FhirContext theCtx, String theSystem) {
for (IValidationSupport next : myChain) { for (IValidationSupport next : myChain) {
if (next.isCodeSystemSupported(theCtx, theSystem)) { CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem);
CodeSystem retVal = next.fetchCodeSystem(theCtx, theSystem); if (retVal != null) {
if (retVal != null) { return retVal;
return retVal;
}
} }
} }
return null; return null;

View File

@ -248,6 +248,10 @@
<![CDATA[<code>=</code>]]> or <![CDATA[<code>in</code>]]> operators. At present, the <![CDATA[<code>=</code>]]> or <![CDATA[<code>in</code>]]> operators. At present, the
<![CDATA[<code>ancestor</code>]]> filter can only be used with the <![CDATA[<code>=</code>]]> operator. <![CDATA[<code>ancestor</code>]]> filter can only be used with the <![CDATA[<code>=</code>]]> operator.
</action> </action>
<action type="fix">
The JPA server failed to find codes defined in not-present codesystems in some cases, and reported
that the CodeSystem did not exist. This has been corrected.
</action>
</release> </release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)"> <release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix"> <action type="fix">