Add ability to disable stale search expiry

This commit is contained in:
James Agnew 2016-11-17 10:01:23 +01:00
parent 391999062e
commit ca9223fb70
5 changed files with 169 additions and 48 deletions

View File

@ -1,11 +1,6 @@
package ca.uhn.fhir.jpa.dao; package ca.uhn.fhir.jpa.dao;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
@ -52,37 +47,39 @@ public class DaoConfig {
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private int myDeferIndexingForCodesystemsOfSize = 2000; private int myDeferIndexingForCodesystemsOfSize = 2000;
private boolean myDeleteStaleSearches = true;
// *** // ***
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR; private long myExpireSearchResultsAfterMillis = DateUtils.MILLIS_PER_HOUR;
private int myHardSearchLimit = 1000; private int myHardSearchLimit = 1000;
private int myHardTagListLimit = 1000; private int myHardTagListLimit = 1000;
private int myIncludeLimit = 2000; private int myIncludeLimit = 2000;
// *** // ***
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private boolean myIndexContainedResources = true; private boolean myIndexContainedResources = true;
private List<IServerInterceptor> myInterceptors; private List<IServerInterceptor> myInterceptors;
// *** // ***
// update setter javadoc if default changes // update setter javadoc if default changes
// *** // ***
private int myMaximumExpansionSize = 5000; private int myMaximumExpansionSize = 5000;
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC; private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
private boolean mySchedulingDisabled; private boolean mySchedulingDisabled;
private boolean mySubscriptionEnabled; private boolean mySubscriptionEnabled;
private long mySubscriptionPollDelay = 1000; private long mySubscriptionPollDelay = 1000;
private Long mySubscriptionPurgeInactiveAfterMillis; private Long mySubscriptionPurgeInactiveAfterMillis;
private Set<String> myTreatBaseUrlsAsLocal = new HashSet<String>(); private Set<String> myTreatBaseUrlsAsLocal = new HashSet<String>();
/** /**
@ -105,6 +102,9 @@ public class DaoConfig {
* number of milliseconds, they will be deleted from the database, and any paging links * number of milliseconds, they will be deleted from the database, and any paging links
* (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour.
* </p> * </p>
* <p>
* @see To disable this feature entirely, see {@link #setExpireSearchResults(boolean)}
* </p>
* *
* @since 1.5 * @since 1.5
*/ */
@ -202,6 +202,18 @@ public class DaoConfig {
return myAllowMultipleDelete; return myAllowMultipleDelete;
} }
/**
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
* <p>
* This feature is useful if you want to define your own process for deleting these (e.g. because
* you are running in a cluster)
* </p>
*/
public boolean isExpireSearchResults() {
return myDeleteStaleSearches;
}
/** /**
* Should contained IDs be indexed the same way that non-contained IDs are (default is * Should contained IDs be indexed the same way that non-contained IDs are (default is
* <code>true</code>) * <code>true</code>)
@ -280,6 +292,18 @@ public class DaoConfig {
myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize; myDeferIndexingForCodesystemsOfSize = theDeferIndexingForCodesystemsOfSize;
} }
/**
* If this is set to <code>false</code> (default is <code>true</code>) the stale search deletion
* task will be disabled (meaning that search results will be retained in the database indefinitely). USE WITH CAUTION.
* <p>
* This feature is useful if you want to define your own process for deleting these (e.g. because
* you are running in a cluster)
* </p>
*/
public void setExpireSearchResults(boolean theDeleteStaleSearches) {
myDeleteStaleSearches = theDeleteStaleSearches;
}
/** /**
* Sets the number of milliseconds that search results for a given client search * Sets the number of milliseconds that search results for a given client search
* should be preserved before being purged from the database. * should be preserved before being purged from the database.
@ -289,7 +313,9 @@ public class DaoConfig {
* number of milliseconds, they will be deleted from the database, and any paging links * number of milliseconds, they will be deleted from the database, and any paging links
* (next/prev links in search response bundles) will become invalid. Defaults to 1 hour. * (next/prev links in search response bundles) will become invalid. Defaults to 1 hour.
* </p> * </p>
* * <p>
* @see To disable this feature entirely, see {@link #setExpireSearchResults(boolean)}
* </p>
* @since 1.5 * @since 1.5
*/ */
public void setExpireSearchResultsAfterMillis(long theExpireSearchResultsAfterMillis) { public void setExpireSearchResultsAfterMillis(long theExpireSearchResultsAfterMillis) {
@ -389,11 +415,11 @@ public class DaoConfig {
} }
mySubscriptionPurgeInactiveAfterMillis = theMillis; mySubscriptionPurgeInactiveAfterMillis = theMillis;
} }
public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) { public void setSubscriptionPurgeInactiveAfterSeconds(int theSeconds) {
setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND); setSubscriptionPurgeInactiveAfterMillis(theSeconds * DateUtils.MILLIS_PER_SECOND);
} }
/** /**
* This setting may be used to advise the server that any references found in * This setting may be used to advise the server that any references found in
* resources that have any of the base URLs given here will be replaced with * resources that have any of the base URLs given here will be replaced with

View File

@ -10,7 +10,7 @@ package ca.uhn.fhir.jpa.search;
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -47,46 +47,47 @@ public class StaleSearchDeletingSvc {
@Autowired @Autowired
private ISearchDao mySearchDao; private ISearchDao mySearchDao;
@Autowired @Autowired
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Autowired @Autowired
private ISearchResultDao mySearchResultDao; private ISearchResultDao mySearchResultDao;
@Autowired @Autowired
private ISearchIncludeDao mySearchIncludeDao; private ISearchIncludeDao mySearchIncludeDao;
@Autowired @Autowired
private PlatformTransactionManager myTransactionManager; private PlatformTransactionManager myTransactionManager;
@Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND) @Scheduled(fixedDelay = 10 * DateUtils.MILLIS_PER_SECOND)
@Transactional(propagation = Propagation.NOT_SUPPORTED) @Transactional(propagation = Propagation.NOT_SUPPORTED)
public synchronized void pollForStaleSearches() { public synchronized void pollForStaleSearches() {
Date cutoff = new Date(System.currentTimeMillis() - myDaoConfig.getExpireSearchResultsAfterMillis()); if (myDaoConfig.isExpireSearchResults()) {
ourLog.debug("Searching for searches which are before {}", cutoff); Date cutoff = new Date(System.currentTimeMillis() - myDaoConfig.getExpireSearchResultsAfterMillis());
ourLog.debug("Searching for searches which are before {}", cutoff);
Collection<Search> toDelete = mySearchDao.findWhereCreatedBefore(cutoff);
if (toDelete.isEmpty()) { Collection<Search> toDelete = mySearchDao.findWhereCreatedBefore(cutoff);
return; if (toDelete.isEmpty()) {
return;
}
TransactionTemplate tt = new TransactionTemplate(myTransactionManager);
for (final Search next : toDelete) {
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
Search searchToDelete = mySearchDao.findOne(next.getId());
ourLog.info("Expiring stale search {} / {}", searchToDelete.getId(), searchToDelete.getUuid());
mySearchIncludeDao.deleteForSearch(searchToDelete.getId());
mySearchResultDao.deleteForSearch(searchToDelete.getId());
mySearchDao.delete(searchToDelete);
}
});
}
ourLog.info("Deleted {} searches, {} remaining", toDelete.size(), mySearchDao.count());
} }
TransactionTemplate tt = new TransactionTemplate(myTransactionManager);
for (final Search next : toDelete) {
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus theArg0) {
Search searchToDelete = mySearchDao.findOne(next.getId());
ourLog.info("Expiring stale search {} / {}", searchToDelete.getId(), searchToDelete.getUuid());
mySearchIncludeDao.deleteForSearch(searchToDelete.getId());
mySearchResultDao.deleteForSearch(searchToDelete.getId());
mySearchDao.delete(searchToDelete);
}
});
}
ourLog.info("Deleted {} searches, {} remaining", toDelete.size(), mySearchDao.count());
} }
} }

View File

@ -3,7 +3,6 @@ package ca.uhn.fhir.jpa.dao.dstu3;
import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsInRelativeOrder; import static org.hamcrest.Matchers.containsInRelativeOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
@ -24,7 +23,6 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hibernate.boot.model.source.spi.IdentifierSourceSimple;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType; import org.hl7.fhir.dstu3.model.Bundle.BundleType;
@ -37,23 +35,24 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
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.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.After;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.dao.SearchParameterMap;
import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum; import ca.uhn.fhir.jpa.dao.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.jpa.entity.*;
import ca.uhn.fhir.jpa.search.StaleSearchDeletingSvc;
import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.MedicationPrescription;
import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.param.*;
@ -1504,6 +1503,80 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
} }
@After
public final void after() {
myDaoConfig.setExpireSearchResults(new DaoConfig().isExpireSearchResults());
myDaoConfig.setExpireSearchResultsAfterMillis(new DaoConfig().getExpireSearchResultsAfterMillis());
}
@Autowired
protected StaleSearchDeletingSvc myStaleSearchDeletingSvc;
@Test
public void testSearchPagesExpiryDisabled() throws Exception {
IIdType pid1;
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().addFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Date between = new Date();
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().addFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
myDaoConfig.setExpireSearchResults(false);
myStaleSearchDeletingSvc.pollForStaleSearches();
Thread.sleep(1500);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), (containsInAnyOrder(pid1, pid2)));
}
@Test
public void testSearchPagesExpiry() throws Exception {
IIdType pid1;
IIdType pid2;
{
Patient patient = new Patient();
patient.addName().addFamily("EXPIRE");
pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
{
Patient patient = new Patient();
patient.addName().addFamily("EXPIRE");
pid2 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless();
}
Thread.sleep(10);
SearchParameterMap params;
params = new SearchParameterMap();
params.add(Patient.SP_FAMILY, new StringParam("EXPIRE"));
IBundleProvider bundleProvider = myPatientDao.search(params);
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
assertThat(toUnqualifiedVersionlessIds(bundleProvider), containsInAnyOrder(pid1, pid2));
Thread.sleep(1500);
myDaoConfig.setExpireSearchResultsAfterMillis(500);
myStaleSearchDeletingSvc.pollForStaleSearches();
assertThat(toUnqualifiedVersionlessIds(bundleProvider), not(containsInAnyOrder(pid1, pid2)));
}
@Test @Test
public void testSearchStringParamReallyLong() { public void testSearchStringParamReallyLong() {
String methodName = "testSearchStringParamReallyLong"; String methodName = "testSearchStringParamReallyLong";

View File

@ -3,13 +3,18 @@ package ca.uhn.fhir.jpa.provider.dstu3;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import org.apache.catalina.filters.CorsFilter;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport; import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
@ -36,6 +41,7 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.CORSFilter_;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider; import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
@ -114,6 +120,15 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
subsServletHolder.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, WebsocketDstu3Config.class.getName()); subsServletHolder.setInitParameter(ContextLoader.CONFIG_LOCATION_PARAM, WebsocketDstu3Config.class.getName());
proxyHandler.addServlet(subsServletHolder, "/*"); proxyHandler.addServlet(subsServletHolder, "/*");
FilterHolder corsFilterHolder = new FilterHolder();
corsFilterHolder.setHeldClass(CorsFilter.class);
corsFilterHolder.setInitParameter("cors.allowed.origins", "*");
corsFilterHolder.setInitParameter("cors.allowed.methods", "GET,POST,PUT,DELETE,OPTIONS");
corsFilterHolder.setInitParameter("cors.allowed.headers", "X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers");
corsFilterHolder.setInitParameter("cors.exposed.headers", "Location,Content-Location");
corsFilterHolder.setInitParameter("cors.support.credentials", "true");
corsFilterHolder.setInitParameter("cors.logging.enabled", "true");
proxyHandler.addFilter(corsFilterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
server.setHandler(proxyHandler); server.setHandler(proxyHandler);
server.start(); server.start();

View File

@ -19,6 +19,12 @@
(e.g. "StructureDefinition.url"). Thanks (e.g. "StructureDefinition.url"). Thanks
to David Hay for reporting! to David Hay for reporting!
</action> </action>
<action type="add">
Add ability to JPA server for disabling stale search
expiry. This is useful if you are deploying the server
to a cluster.
</action>
</release>
<release version="2.1" date="2016-11-11"> <release version="2.1" date="2016-11-11">
<action type="add"> <action type="add">
STU3 structure definitions have been updated to the STU3 structure definitions have been updated to the