Make cache timeout configurable

This commit is contained in:
James Agnew 2019-10-15 09:34:48 -04:00
parent 48e2960c71
commit 9a050461a8
6 changed files with 214 additions and 114 deletions

View File

@ -36,5 +36,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(value=ElementType.METHOD)
public @interface Metadata {
// nothing for now
long cacheMillis() default 60 * 1000L;
}

View File

@ -2669,15 +2669,11 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
@Test
public void testMetadata() throws Exception {
HttpGet get = new HttpGet(ourServerBase + "/metadata");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
try (CloseableHttpResponse response = ourHttpClient.execute(get)) {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, stringContainsInOrder("THIS IS THE DESC"));
} finally {
response.getEntity().getContent().close();
response.close();
}
}

View File

@ -20,22 +20,14 @@ package ca.uhn.fhir.rest.server.method;
* #L%
*/
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.api.CacheControlDirective;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -44,12 +36,17 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding {
private static final long CACHE_MILLIS = 60 * 1000;
/*
* Note: This caching mechanism should probably be configurable and maybe
* even applicable to other bindings. It's particularly important for this
@ -57,7 +54,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
*/
private final AtomicReference<IBaseResource> myCachedResponse = new AtomicReference<>();
private final AtomicLong myCachedResponseExpires = new AtomicLong(0L);
private long myCacheMillis = 60 * 1000;
ConformanceMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(theMethod.getReturnType(), theMethod, theContext, theProvider);
@ -68,6 +65,35 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
throw new ConfigurationException("Conformance resource provider method '" + theMethod.getName() + "' should return a Conformance resource class, returns: " + theMethod.getReturnType());
}
Metadata metadata = theMethod.getAnnotation(Metadata.class);
if (metadata != null) {
setCacheMillis(metadata.cacheMillis());
}
}
/**
* Returns the number of milliseconds to cache the generated CapabilityStatement for. Default is one minute, and can be
* set to 0 to never cache.
*
* @see #setCacheMillis(long)
* @see Metadata#cacheMillis()
* @since 4.1.0
*/
private long getCacheMillis() {
return myCacheMillis;
}
/**
* Returns the number of milliseconds to cache the generated CapabilityStatement for. Default is one minute, and can be
* set to 0 to never cache.
*
* @see #getCacheMillis()
* @see Metadata#cacheMillis()
* @since 4.1.0
*/
private void setCacheMillis(long theCacheMillis) {
myCacheMillis = theCacheMillis;
}
@Override
@ -116,8 +142,10 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding
if (conf == null) {
conf = (IBaseResource) invokeServerMethod(theServer, theRequest, theMethodParams);
if (myCacheMillis > 0) {
myCachedResponse.set(conf);
myCachedResponseExpires.set(System.currentTimeMillis() + CACHE_MILLIS);
myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis());
}
}
return new SimpleBundleProvider(conf);

View File

@ -1,29 +1,18 @@
package ca.uhn.fhir.rest.server.method;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.junit.Before;
import org.junit.Test;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ -31,40 +20,130 @@ public class ConformanceMethodBindingTest {
private FhirContext fhirContext;
private ConformanceMethodBinding conformanceMethodBinding;
private TestResourceProvider dummy;
@Before
public void setUp() throws NoSuchMethodException {
public void setUp() {
fhirContext = mock(FhirContext.class);
dummy = spy(new TestResourceProvider());
Method method = dummy.getClass().getDeclaredMethod("getServerConformance", HttpServletRequest.class, RequestDetails.class);
conformanceMethodBinding = new ConformanceMethodBinding(method, fhirContext, dummy);
}
private <T> T init(T theCapabilityStatementProvider) throws NoSuchMethodException {
T provider = spy(theCapabilityStatementProvider);
Method method = provider.getClass().getDeclaredMethod("getServerConformance", HttpServletRequest.class, RequestDetails.class);
conformanceMethodBinding = new ConformanceMethodBinding(method, fhirContext, provider);
return provider;
}
@Test
public void invokeServerCached() {
public void invokeServerCached() throws NoSuchMethodException {
TestResourceProvider provider = init(new TestResourceProvider());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(dummy, times(1)).getServerConformance(any(), any());
verify(provider, times(1)).getServerConformance(any(), any());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(dummy, times(1)).getServerConformance(any(), any());
verify(provider, times(1)).getServerConformance(any(), any());
}
@Test
public void invokeServerNotCached_ClientControlled() {
public void invokeServerCacheExpires() throws NoSuchMethodException {
TestResourceProviderSmallCache provider = init(new TestResourceProviderSmallCache());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(1)).getServerConformance(any(), any());
sleepAtLeast(20);
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(2)).getServerConformance(any(), any());
}
@Test
public void invokeServerCacheDisabled() throws NoSuchMethodException {
TestResourceProviderNoCache provider = init(new TestResourceProviderNoCache());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(1)).getServerConformance(any(), any());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(2)).getServerConformance(any(), any());
}
@Test
public void invokeServerCacheDisabledInSuperclass() throws NoSuchMethodException {
TestResourceProviderNoCache2 provider = init(new TestResourceProviderNoCache2());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(1)).getServerConformance(any(), any());
// We currently don't scan the annotation on the superclass...Perhaps we should
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), mock(RequestDetails.class, RETURNS_DEEP_STUBS), new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(provider, times(1)).getServerConformance(any(), any());
}
@Test
public void invokeServerNotCached_ClientControlled() throws NoSuchMethodException {
TestResourceProvider provider = init(new TestResourceProvider());
RequestDetails requestDetails = mock(RequestDetails.class, RETURNS_DEEP_STUBS);
when(requestDetails.getHeaders(Constants.HEADER_CACHE_CONTROL)).thenReturn(Lists.newArrayList(Constants.CACHE_CONTROL_NO_CACHE));
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), requestDetails, new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(dummy, times(1)).getServerConformance(any(), any());
verify(provider, times(1)).getServerConformance(any(), any());
conformanceMethodBinding.invokeServer(mock(IRestfulServer.class, RETURNS_DEEP_STUBS), requestDetails, new Object[]{mock(HttpServletRequest.class), mock(RequestDetails.class)});
verify(dummy, times(2)).getServerConformance(any(), any());
verify(provider, times(2)).getServerConformance(any(), any());
}
class TestResourceProvider {
@SuppressWarnings("unused")
static class TestResourceProvider {
@Metadata
public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
return mock(IBaseConformance.class, RETURNS_DEEP_STUBS);
}
}
@SuppressWarnings("unused")
static class TestResourceProviderSmallCache {
@Metadata(cacheMillis = 10)
public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
return mock(IBaseConformance.class, RETURNS_DEEP_STUBS);
}
}
@SuppressWarnings("unused")
static class TestResourceProviderNoCache {
@Metadata(cacheMillis = 0)
public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
return mock(IBaseConformance.class, RETURNS_DEEP_STUBS);
}
}
@SuppressWarnings("unused")
static class TestResourceProviderNoCache2 extends TestResourceProviderNoCache {
// No @Metadata
@Override
public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) {
return mock(IBaseConformance.class, RETURNS_DEEP_STUBS);
}
}
private static void sleepAtLeast(long theMillis) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() <= start + theMillis) {
try {
long timeSinceStarted = System.currentTimeMillis() - start;
long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
Thread.sleep(timeToSleep);
} catch (InterruptedException theE) {
// ignore
}
}
}
}

View File

@ -1,21 +1,19 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.VersionUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.*;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -28,27 +26,22 @@ import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.VersionUtil;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
public class MetadataConformanceDstu3Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MetadataConformanceDstu3Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu3();
private static int ourPort;
private static Server ourServer;
private static RestfulServer ourServlet;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MetadataConformanceDstu3Test.class);
@Test
public void testSummary() throws Exception {
@ -104,38 +97,55 @@ public class MetadataConformanceDstu3Test {
public void testHttpMethods() throws Exception {
String output;
HttpRequestBase httpPost = new HttpGet("http://localhost:" + ourPort + "/metadata");
CloseableHttpResponse status = ourClient.execute(httpPost);
try {
HttpRequestBase httpOperation = new HttpGet("http://localhost:" + ourPort + "/metadata");
try (CloseableHttpResponse status = ourClient.execute(httpOperation)) {
output = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(output, containsString("<CapabilityStatement"));
assertEquals("HAPI FHIR " + VersionUtil.getVersion() + " REST Server (FHIR Server; FHIR " + FhirVersionEnum.DSTU3.getFhirVersionString() + "/DSTU3)", status.getFirstHeader("X-Powered-By").getValue());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
try {
httpPost = new HttpPost("http://localhost:" + ourPort + "/metadata");
status = ourClient.execute(httpPost);
httpOperation = new HttpOptions("http://localhost:" + ourPort);
try (CloseableHttpResponse status = ourClient.execute(httpOperation)) {
output = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(output, containsString("<CapabilityStatement"));
assertEquals("HAPI FHIR " + VersionUtil.getVersion() + " REST Server (FHIR Server; FHIR " + FhirVersionEnum.DSTU3.getFhirVersionString() + "/DSTU3)", status.getFirstHeader("X-Powered-By").getValue());
}
httpOperation = new HttpPost("http://localhost:" + ourPort + "/metadata");
try (CloseableHttpResponse status = ourClient.execute(httpOperation)) {
output = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals(405, status.getStatusLine().getStatusCode());
assertEquals("<OperationOutcome xmlns=\"http://hl7.org/fhir\"><issue><severity value=\"error\"/><code value=\"processing\"/><diagnostics value=\"/metadata request must use HTTP GET\"/></issue></OperationOutcome>", output);
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
/*
* There is no @read on the RP below, so this should fail. Otherwise it
* would be interpreted as a read on ID "metadata"
*/
try {
httpPost = new HttpGet("http://localhost:" + ourPort + "/Patient/metadata");
status = ourClient.execute(httpPost);
output = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
httpOperation = new HttpGet("http://localhost:" + ourPort + "/Patient/metadata");
try (CloseableHttpResponse status = ourClient.execute(httpOperation)) {
assertEquals(400, status.getStatusLine().getStatusCode());
} finally {
IOUtils.closeQuietly(status.getEntity().getContent());
}
}
@SuppressWarnings("unused")
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<Patient> search(@OptionalParam(name = "foo") StringParam theFoo) {
throw new UnsupportedOperationException();
}
@Validate()
public MethodOutcome validate(@ResourceParam Patient theResource) {
return new MethodOutcome();
}
}
@ -169,23 +179,4 @@ public class MetadataConformanceDstu3Test {
}
@SuppressWarnings("unused")
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Search
public List<Patient> search(@OptionalParam(name="foo") StringParam theFoo) {
throw new UnsupportedOperationException();
}
@Validate()
public MethodOutcome validate(@ResourceParam Patient theResource) {
return new MethodOutcome();
}
}
}

View File

@ -363,6 +363,10 @@
The server CapabilityStatement (/metadata) endpoint now respects the Cache-Control header. Thanks
to Jens Villadsen for the pull request!
</action>
<action type="add">
The @Metadata annotation now has an attribute that can be used to control
the cache timeout
</action>
</release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix">