Make sure we reuse searches

This commit is contained in:
James Agnew 2017-05-09 11:47:05 +02:00
parent 262e39e57a
commit 579e5da5ff
8 changed files with 2722 additions and 31 deletions

View File

@ -383,6 +383,9 @@ public class SearchParameterMap extends LinkedHashMap<String, List<List<? extend
b.append(getCount());
}
if (b.length() == 0) {
b.append('?');
}
return b.toString();
}

View File

@ -343,6 +343,7 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
List<DeleteConflict> deleteConflicts = new ArrayList<DeleteConflict>();
Map<BundleEntryComponent, ResourceTable> entriesToProcess = new IdentityHashMap<BundleEntryComponent, ResourceTable>();
Set<ResourceTable> nonUpdatedEntities = new HashSet<ResourceTable>();
Set<String> conditionalRequestUrls = new HashSet<String>();
/*
* Loop through the request and process any entries of type
@ -412,6 +413,10 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
entriesToProcess.put(nextRespEntry, outcome.getEntity());
if (outcome.getCreated() == false) {
nonUpdatedEntities.add(outcome.getEntity());
} else {
if (isNotBlank(matchUrl)) {
conditionalRequestUrls.add(matchUrl);
}
}
break;
@ -462,7 +467,11 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao<Bundle, Meta> {
outcome = resourceDao.update(res, null, false, theRequestDetails);
} else {
res.setId((String) null);
outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false, theRequestDetails);
String matchUrl = parts.getResourceType() + '?' + parts.getParams();
outcome = resourceDao.update(res, matchUrl, false, theRequestDetails);
if (Boolean.TRUE.equals(outcome.getCreated())) {
conditionalRequestUrls.add(matchUrl);
}
}
handleTransactionCreateOrUpdateOutcome(idSubstitutions, idToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res);

View File

@ -273,7 +273,8 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
public PersistedJpaBundleProvider doInTransaction(TransactionStatus theStatus) {
Search searchToUse = null;
Collection<Search> candidates = mySearchDao.find(resourceType, queryString.hashCode(), createdCutoff);
int hashCode = queryString.hashCode();
Collection<Search> candidates = mySearchDao.find(resourceType, hashCode, createdCutoff);
for (Search nextCandidateSearch : candidates) {
if (queryString.equals(nextCandidateSearch.getSearchQueryString())) {
searchToUse = nextCandidateSearch;

View File

@ -12,7 +12,7 @@ import java.util.Date;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -22,18 +22,17 @@ import java.util.Date;
* #L%
*/
public class StopWatch {
private long myStarted = System.currentTimeMillis();
/**
* Constructor
*/
public StopWatch() {
super();
}
/**
* Constructor
*/
@ -47,13 +46,13 @@ public class StopWatch {
myStarted = now;
return retVal;
}
public long getMillis() {
long now = System.currentTimeMillis();
long retVal = now - myStarted;
return retVal;
}
public long getMillis(Date theNow) {
long retVal = theNow.getTime() - myStarted;
return retVal;
@ -62,9 +61,40 @@ public class StopWatch {
public Date getStartedDate() {
return new Date(myStarted);
}
public double getMillisPerOperation(int theNumOperations) {
return ((double)getMillis()) / Math.max(1.0, theNumOperations);
/**
* Formats value in the format hh:mm:ss.SSSS
*/
@Override
public String toString() {
return formatMillis(getMillis());
}
static public String formatMillis(long val) {
StringBuilder buf = new StringBuilder(20);
append(buf, "", 2, ((val % 3600000) / 60000));
append(buf, ":", 2, ((val % 60000) / 1000));
append(buf, ".", 3, (val % 1000));
return buf.toString();
}
/** Append a right-aligned and zero-padded numeric value to a `StringBuilder`. */
static private void append(StringBuilder tgt, String pfx, int dgt, long val) {
tgt.append(pfx);
if (dgt > 1) {
int pad = (dgt - 1);
for (long xa = val; xa > 9 && pad > 0; xa /= 10) {
pad--;
}
for (int xa = 0; xa < pad; xa++) {
tgt.append('0');
}
}
tgt.append(val);
}
public double getMillisPerOperation(int theNumOperations) {
return ((double) getMillis()) / Math.max(1.0, theNumOperations);
}
}

View File

@ -84,6 +84,17 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete());
}
@Test
public void testTransactionWithMultiBundle() throws IOException {
String inputBundleString = loadClasspath("/batch-error.xml");
Bundle bundle = myFhirCtx.newXmlParser().parseResource(Bundle.class, inputBundleString);
Bundle resp = mySystemDao.transaction(mySrd, bundle);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus());
}
@SuppressWarnings("unchecked")
private <T extends org.hl7.fhir.dstu3.model.Resource> T find(Bundle theBundle, Class<T> theType, int theIndex) {
int count = 0;
@ -396,30 +407,30 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
public void testTransactionWithPostDoesntUpdate() throws Exception {
// First bundle (name is Joshua)
String input = IOUtils.toString(getClass().getResource("/dstu3-post1.xml"), StandardCharsets.UTF_8);
Bundle request = myFhirCtx.newXmlParser().parseResource(Bundle.class, input);
Bundle response = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(response));
assertEquals(1, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertEquals("1", response.getEntry().get(0).getResponse().getEtag());
String id = response.getEntry().get(0).getResponse().getLocation();
// Now the second (name is Adam, shouldn't get used)
input = IOUtils.toString(getClass().getResource("/dstu3-post2.xml"), StandardCharsets.UTF_8);
request = myFhirCtx.newXmlParser().parseResource(Bundle.class, input);
response = mySystemDao.transaction(mySrd, request);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(response));
assertEquals(1, response.getEntry().size());
assertEquals("200 OK", response.getEntry().get(0).getResponse().getStatus());
assertEquals("1", response.getEntry().get(0).getResponse().getEtag());
String id2 = response.getEntry().get(0).getResponse().getLocation();
assertEquals(id, id2);
Patient patient = myPatientDao.read(new IdType(id), mySrd);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient));
assertEquals("Joshua", patient.getNameFirstRep().getGivenAsSingleString());
@ -2196,6 +2207,147 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertThat(o2.getSubject().getReferenceElement().getValue(), endsWith("Patient/" + p1.getIdElement().getIdPart()));
}
@Test
public void testTransactionDoubleConditionalCreateOnlyCreatesOne() {
Bundle inputBundle = new Bundle();
inputBundle.setType(Bundle.BundleType.TRANSACTION);
Encounter enc1 = new Encounter();
enc1.addIdentifier().setSystem("urn:foo").setValue("12345");
inputBundle
.addEntry()
.setResource(enc1)
.getRequest()
.setMethod(HTTPVerb.POST)
.setIfNoneExist("Encounter?identifier=urn:foo|12345");
Encounter enc2 = new Encounter();
enc2.addIdentifier().setSystem("urn:foo").setValue("12345");
inputBundle
.addEntry()
.setResource(enc2)
.getRequest()
.setMethod(HTTPVerb.POST)
.setIfNoneExist("Encounter?identifier=urn:foo|12345");
try {
mySystemDao.transaction(mySrd, inputBundle);
fail();
} catch (InvalidRequestException e) {
assertEquals("Unable to process Transaction - Request would cause multiple resources to match URL: \"Encounter?identifier=urn:foo|12345\". Does transaction request contain duplicates?", e.getMessage());
}
}
@Test
public void testTransactionDoubleConditionalUpdateOnlyCreatesOne() {
Bundle inputBundle = new Bundle();
inputBundle.setType(Bundle.BundleType.TRANSACTION);
Encounter enc1 = new Encounter();
enc1.addIdentifier().setSystem("urn:foo").setValue("12345");
inputBundle
.addEntry()
.setResource(enc1)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Encounter?identifier=urn:foo|12345");
Encounter enc2 = new Encounter();
enc2.addIdentifier().setSystem("urn:foo").setValue("12345");
inputBundle
.addEntry()
.setResource(enc2)
.getRequest()
.setMethod(HTTPVerb.PUT)
.setUrl("Encounter?identifier=urn:foo|12345");
Bundle response = mySystemDao.transaction(mySrd, inputBundle);
IBundleProvider found = myEncounterDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertEquals(1, found.size().intValue());
}
@Test
public void testCircularCreateAndDelete() {
Encounter enc = new Encounter();
enc.setId(IdType.newRandomUuid());
Condition cond = new Condition();
cond.setId(IdType.newRandomUuid());
EpisodeOfCare ep = new EpisodeOfCare();
ep.setId(IdType.newRandomUuid());
enc.getEpisodeOfCareFirstRep().setReference(ep.getId());
cond.getContext().setReference(enc.getId());
ep.getDiagnosisFirstRep().getCondition().setReference(cond.getId());
Bundle inputBundle = new Bundle();
inputBundle.setType(Bundle.BundleType.TRANSACTION);
inputBundle
.addEntry()
.setResource(ep)
.setFullUrl(ep.getId())
.getRequest().setMethod(HTTPVerb.POST);
inputBundle
.addEntry()
.setResource(cond)
.setFullUrl(cond.getId())
.getRequest().setMethod(HTTPVerb.POST);
inputBundle
.addEntry()
.setResource(enc)
.setFullUrl(enc.getId())
.getRequest().setMethod(HTTPVerb.POST);
Bundle resp = mySystemDao.transaction(mySrd, inputBundle);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
IdType epId = new IdType(resp.getEntry().get(0).getResponse().getLocation());
IdType condId = new IdType(resp.getEntry().get(1).getResponse().getLocation());
IdType encId = new IdType(resp.getEntry().get(2).getResponse().getLocation());
// Make sure a single one can't be deleted
try {
myEncounterDao.delete(encId);
fail();
} catch (ResourceVersionConflictException e) {
// good
}
/*
* Now delete all 3 by transaction
*/
inputBundle = new Bundle();
inputBundle.setType(Bundle.BundleType.TRANSACTION);
inputBundle
.addEntry()
.getRequest().setMethod(HTTPVerb.DELETE)
.setUrl(epId.toUnqualifiedVersionless().getValue());
inputBundle
.addEntry()
.getRequest().setMethod(HTTPVerb.DELETE)
.setUrl(encId.toUnqualifiedVersionless().getValue());
inputBundle
.addEntry()
.getRequest().setMethod(HTTPVerb.DELETE)
.setUrl(condId.toUnqualifiedVersionless().getValue());
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(inputBundle));
resp = mySystemDao.transaction(mySrd, inputBundle);
ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(resp));
// They should now be deleted
try {
myEncounterDao.read(encId.toUnqualifiedVersionless());
fail();
} catch (ResourceGoneException e) {
// good
}
}
@AfterClass
public static void afterClassClearContext() {

View File

@ -2859,6 +2859,53 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
assertNotEquals(uuid1, uuid3);
}
@Test
public void testSearchReusesResultsEnabledNoParams() throws Exception {
List<IBaseResource> resources = new ArrayList<IBaseResource>();
for (int i = 0; i < 50; i++) {
Organization org = new Organization();
org.setName("HELLO");
resources.add(org);
}
ourClient.transaction().withResources(resources).prettyPrint().encodedXml().execute();
myDaoConfig.setReuseCachedSearchResultsForMillis(100000L);
Bundle result1 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
final String uuid1 = toSearchUuidFromLinkNext(result1);
Search search1 = newTxTemplate().execute(new TransactionCallback<Search>() {
@Override
public Search doInTransaction(TransactionStatus theStatus) {
return mySearchEntityDao.findByUuid(uuid1);
}
});
Date lastReturned1 = search1.getSearchLastReturned();
Bundle result2 = ourClient
.search()
.forResource("Organization")
.returnBundle(Bundle.class)
.execute();
final String uuid2 = toSearchUuidFromLinkNext(result2);
Search search2 = newTxTemplate().execute(new TransactionCallback<Search>() {
@Override
public Search doInTransaction(TransactionStatus theStatus) {
return mySearchEntityDao.findByUuid(uuid2);
}
});
Date lastReturned2 = search2.getSearchLastReturned();
assertTrue(lastReturned2.getTime() > lastReturned1.getTime());
assertEquals(uuid1, uuid2);
}
/**
* See #316
*/

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.util;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import java.util.Date;
@ -10,26 +11,39 @@ import org.junit.Test;
public class StopWatchTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StopWatchTest.class);
@Test
public void testStopwatch() throws Exception {
StopWatch sw = new StopWatch();
Thread.sleep(100);
assertThat(sw.getMillis(new Date()), greaterThan(10L));
assertThat(sw.getMillis(), greaterThan(10L));
assertThat(sw.getStartedDate().getTime(), lessThan(System.currentTimeMillis()));
}
@Test
public void testStopwatchWithDate() throws Exception {
StopWatch sw = new StopWatch(new Date());
Thread.sleep(100);
assertThat(sw.getMillis(new Date()), greaterThan(10L));
assertThat(sw.getMillis(), greaterThan(10L));
assertThat(sw.getStartedDate().getTime(), lessThan(System.currentTimeMillis()));
}
@Test
public void testStopwatchWithDate() throws Exception {
StopWatch sw = new StopWatch(new Date());
Thread.sleep(100);
assertThat(sw.getMillis(new Date()), greaterThan(10L));
assertThat(sw.getMillis(), greaterThan(10L));
assertThat(sw.getStartedDate().getTime(), lessThan(System.currentTimeMillis()));
}
@Test
public void testToString() throws Exception {
StopWatch sw = new StopWatch();
Thread.sleep(100);
String string = sw.toString();
ourLog.info(string);
assertThat(string, startsWith("00:00"));
}
}

File diff suppressed because it is too large Load Diff