ResourceServlet: Stream large files, bypassing the cache

Sufficiently large static files will thrash the cache. Don't bother
even storing them in the cache; stream them directly from their Paths.

Instead of using a content-based hash for the ETag, just use
Last-Modified based on the modified time of the file.

Change-Id: Idc9b5daf9f5da124eb0d6b8b21806866de3d3a6b
This commit is contained in:
Dave Borowitz
2015-11-03 16:11:00 -05:00
parent 712669ac36
commit 74317d4a07

View File

@@ -17,7 +17,9 @@ package com.google.gerrit.httpd.raw;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING; import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
import static com.google.common.net.HttpHeaders.ETAG; import static com.google.common.net.HttpHeaders.ETAG;
import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH; import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
@@ -28,6 +30,7 @@ import com.google.common.base.CharMatcher;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import com.google.gerrit.common.FileUtil;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtexpui.server.CacheHeaders;
@@ -36,14 +39,15 @@ import com.google.gwtjsonrpc.server.RPCServletUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.zip.GZIPOutputStream;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -61,6 +65,8 @@ public abstract class ResourceServlet extends HttpServlet {
private static final Logger log = private static final Logger log =
LoggerFactory.getLogger(ResourceServlet.class); LoggerFactory.getLogger(ResourceServlet.class);
private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
private static final String JS = "application/x-javascript"; private static final String JS = "application/x-javascript";
private static final ImmutableMap<String, String> MIME_TYPES = private static final ImmutableMap<String, String> MIME_TYPES =
ImmutableMap.<String, String> builder() ImmutableMap.<String, String> builder()
@@ -108,20 +114,45 @@ public abstract class ResourceServlet extends HttpServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse rsp) protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
throws IOException { throws IOException {
Resource r; String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
try { if (isUnreasonableName(name)) {
r = getResource(req); notFound(rsp);
} catch (ExecutionException e) { return;
log.warn(String.format( }
"Cannot load static resource %s", Path p = getResourcePath(name);
req.getPathInfo()), e); if (p == null) {
CacheHeaders.setNotCacheable(rsp); notFound(rsp);
rsp.setStatus(SC_INTERNAL_SERVER_ERROR); return;
}
Resource r = cache.getIfPresent(p);
if (r == null && maybeStream(p, req, rsp)) {
return;
}
if (r == null) {
Callable<Resource> loader = newLoader(p);
try {
r = cache.get(p, loader);
if (refresh && r.isStale(p)) {
cache.invalidate(p);
r = cache.get(p, loader);
}
} catch (ExecutionException e) {
log.warn("Cannot load static resource " + req.getPathInfo(), e);
CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
return;
}
}
if (r == Resource.NOT_FOUND) {
notFound(rsp);
return; return;
} }
String e = req.getParameter("e"); String e = req.getParameter("e");
if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) { if (e != null && !r.etag.equals(e)) {
CacheHeaders.setNotCacheable(rsp); CacheHeaders.setNotCacheable(rsp);
rsp.setStatus(SC_NOT_FOUND); rsp.setStatus(SC_NOT_FOUND);
return; return;
@@ -164,30 +195,62 @@ public abstract class ResourceServlet extends HttpServlet {
} }
} }
private Resource getResource(HttpServletRequest req) private static void notFound(HttpServletResponse rsp) {
throws ExecutionException { rsp.setStatus(SC_NOT_FOUND);
String name = CharMatcher.is('/').trimFrom(req.getPathInfo()); CacheHeaders.setNotCacheable(rsp);
if (isUnreasonableName(name)) {
return Resource.NOT_FOUND;
}
Path p = getResourcePath(name);
if (p == null) {
return Resource.NOT_FOUND;
}
Callable<Resource> loader = newLoader(p);
Resource r = cache.get(p, loader);
if (r == Resource.NOT_FOUND) {
return Resource.NOT_FOUND;
}
if (refresh && r.isStale(p)) {
cache.invalidate(p);
r = cache.get(p, loader);
}
return r;
} }
/**
* Maybe stream a path to the response, depending on the properties of the
* file and cache headers in the request.
*
* @param p path to stream
* @param req HTTP request.
* @param rsp HTTP response.
* @return true if the response was written (either the file contents or an
* error); false if the path is too small to stream and should be cached.
*/
private boolean maybeStream(Path p, HttpServletRequest req,
HttpServletResponse rsp) throws IOException {
try {
if (Files.size(p) < CACHE_FILE_SIZE_LIMIT_BYTES) {
return false;
}
} catch (NoSuchFileException e) {
cache.put(p, Resource.NOT_FOUND);
notFound(rsp);
return true;
}
long lastModified = FileUtil.lastModified(p);
if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
rsp.setStatus(SC_NOT_MODIFIED);
return true;
}
if (lastModified > 0) {
rsp.setDateHeader(LAST_MODIFIED, lastModified);
}
if (!CacheHeaders.hasCacheHeader(rsp)) {
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
}
rsp.setContentType(contentType(p.toString()));
OutputStream out = rsp.getOutputStream();
GZIPOutputStream gz = null;
if (RPCServletUtils.acceptsGzipEncoding(req)) {
rsp.setHeader(CONTENT_ENCODING, "gzip");
gz = new GZIPOutputStream(out);
out = gz;
}
Files.copy(p, out);
if (gz != null) {
gz.finish();
}
return true;
}
private static boolean isUnreasonableName(String name) { private static boolean isUnreasonableName(String name) {
return name.length() < 1 return name.length() < 1
|| name.contains("\\") // no windows/dos style paths || name.contains("\\") // no windows/dos style paths
@@ -206,7 +269,7 @@ public abstract class ResourceServlet extends HttpServlet {
Files.getLastModifiedTime(p), Files.getLastModifiedTime(p),
contentType(p.toString()), contentType(p.toString()),
Files.readAllBytes(p)); Files.readAllBytes(p));
} catch (FileNotFoundException e) { } catch (NoSuchFileException e) {
return Resource.NOT_FOUND; return Resource.NOT_FOUND;
} }
} }