Skip to content

Commit

Permalink
[RESTEASY-2646]:Fix match cache in RootNode grows infinitely
Browse files Browse the repository at this point in the history
  • Loading branch information
ronsigal committed Jul 20, 2020
1 parent 87cbec1 commit dd255d6
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package org.jboss.resteasy.core.registry;

import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResourceInvoker;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -23,6 +26,20 @@ public class RootNode
protected int size = 0;
protected MultivaluedMap<String, MethodExpression> bounded = new MultivaluedHashMap<String, MethodExpression>();
protected ConcurrentHashMap<MatchCache.Key, MatchCache> cache = new ConcurrentHashMap<>();
private static int CACHE_SIZE = 2048;
private static boolean CACHE = true;
static
{
if (System.getSecurityManager() == null) {
CACHE = Boolean.parseBoolean(System.getProperty(ResteasyContextParameters.RESTEASY_MATCH_CACHE_ENABLED, "true"));
CACHE_SIZE = Integer.getInteger(ResteasyContextParameters.RESTEASY_MATCH_CACHE_SIZE, 2048);
} else {
CACHE = AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
Boolean.parseBoolean(System.getProperty(ResteasyContextParameters.RESTEASY_MATCH_CACHE_ENABLED, "true")));
CACHE_SIZE = AccessController.doPrivileged((PrivilegedAction<Integer>) () ->
Integer.getInteger(ResteasyContextParameters.RESTEASY_MATCH_CACHE_SIZE, 2048));
}
}

public int getSize()
{
Expand All @@ -42,11 +59,9 @@ public MultivaluedMap<String, ResourceInvoker> getBounded()
return rtn;
}

private static boolean CACHE = true;

public ResourceInvoker match(HttpRequest request, int start)
{
if (!CACHE) {
if (!CACHE || (request.getHttpHeaders().getMediaType() !=null && !request.getHttpHeaders().getMediaType().getParameters().isEmpty())) {
return root.match(request, start).invoker;
}
MatchCache.Key key = new MatchCache.Key(request, start);
Expand All @@ -59,6 +74,9 @@ public ResourceInvoker match(HttpRequest request, int start)
if (match.match != null && match.match.expression.getNumGroups() == 0 && match.invoker instanceof ResourceMethodInvoker) {
//System.out.println("*** caching: " + key.method + " " + key.path);
match.match = null;
if (cache.size() >= CACHE_SIZE) {
cache.clear();
}
cache.putIfAbsent(key, match);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ public interface ResteasyContextParameters {

// Added for quarkus. Initial use switch from warning msg to exception message.
String RESTEASY_FAIL_FAST_ON_MULTIPLE_RESOURCES_MATCHING = "resteasy.fail.fast.on.multiple.resources.matching";
String RESTEASY_MATCH_CACHE_ENABLED = "resteasy.match.cache.enabled";
String RESTEASY_MATCH_CACHE_SIZE = "resteasy.match.cache.size";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.jboss.resteasy.core.registry;

import org.jboss.resteasy.core.InjectorFactoryImpl;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.providerfactory.ResteasyProviderFactoryImpl;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.InjectorFactory;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.spi.metadata.DefaultResourceClass;
import org.jboss.resteasy.spi.metadata.DefaultResourceMethod;
import org.jboss.resteasy.spi.metadata.ResourceBuilder;
import org.jboss.resteasy.spi.metadata.ResourceClass;
import org.jboss.resteasy.spi.metadata.ResourceMethod;
import org.junit.Test;

import javax.ws.rs.core.MediaType;
import java.lang.reflect.Method;

import static org.junit.Assert.assertEquals;

public class RootNodeCacheSizeTest {

@Test
public void testRootNodeCacheSize() throws Exception {
MyRootNode rootNode = new MyRootNode();
rootNode.adjustRoot();

for (int i = 0; i < 2050; i++) {
rootNode.match(MockHttpRequest.get("" + i).contentType(MediaType.TEXT_PLAIN_TYPE), 0);
}

// Default in RootNode is CACHE_SIZE = 2048;
assertEquals("Cache is expected to be cleared when size exceeded 2048 items", 2, rootNode.cacheSize());
for (int i = 0; i < 10; i++) {
rootNode.match(MockHttpRequest.get("" + i).contentType(MediaType.valueOf("text/html;boundary=from" + i)), 0);
}
//MediaType with parameters won't be cached
assertEquals("Unexpected cache item", 2, rootNode.cacheSize());
}

public class MyRootNode extends RootNode {
public int cacheSize() {
return cache.size();
}
public void adjustRoot() {
root = new MySegmentNode("");
}
}
public class MySegmentNode extends SegmentNode {
public MySegmentNode(final String segment) {
super(segment);
}
public String foo() {
return "foo";
}
@Override
public MatchCache match(HttpRequest request, int start) {

// Create sample ResourceMethodInvoker
Class<?> clazz = MySegmentNode.class;
Method method = null;
try {
method = MySegmentNode.class.getMethod("foo");
} catch (NoSuchMethodException e) {
// ignore, method foo is defined
}
ResourceClass resourceClass = new DefaultResourceClass(MySegmentNode.class, "path");
ResourceMethod resourceMethod = new DefaultResourceMethod(resourceClass, method, method);
ResteasyProviderFactory providerFactory = new ResteasyProviderFactoryImpl();
InjectorFactory injectorFactory = new InjectorFactoryImpl();
ResourceBuilder resourceBuilder = new ResourceBuilder();
POJOResourceFactory resourceFactory = new POJOResourceFactory(resourceBuilder, clazz);
ResourceMethodInvoker resourceMethodInvoker = new ResourceMethodInvoker(resourceMethod, injectorFactory, resourceFactory, providerFactory);

MatchCache match = new MatchCache();
match.match = new SegmentNode.Match(new MyMethodExpression(), null);
match.invoker = resourceMethodInvoker;
// returned match needs to be compliant with this if clause from RootNode
// if (match.match != null &&
// match.match.expression.getNumGroups() == 0 &&
// match.invoker instanceof ResourceMethodInvoker) {
return match;
}
}
public class MyMethodExpression extends MethodExpression {

public MyMethodExpression() {
super(null, "foo", null);
}

@Override
public int getNumGroups() {
return 0;
}
}
}

0 comments on commit dd255d6

Please sign in to comment.