Merge "Update bucklets"
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index bed3fe7..e1c6da3 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -768,6 +768,17 @@
   imageLimit = 256K
 ```
 
+### Parsing timeout
+
+Parsing Markdown can be expensive so this implementation places
+a default upper bound of 2 seconds on running time per document.
+This is measured in wall clock time from the start of the request.
+
+```
+[markdown]
+  parseTimeout = 2s
+```
+
 ### Google Analytics
 
 [Google Analytics](https://www.google.com/analytics/) can be
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
index 7013918..91795bb 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
@@ -31,6 +31,7 @@
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.joda.time.Duration;
 import org.pegdown.ast.RootNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -89,10 +90,13 @@
 
   SanitizedContent render() {
     try {
+      Duration parseTimeout = ConfigUtil.getDuration(cfg, "markdown", null,
+          "parseTimeout", Duration.standardSeconds(2));
       int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
       byte[] raw = reader.open(readmeId, Constants.OBJ_BLOB).getCachedBytes(inputLimit);
       String md = RawParseUtils.decode(raw);
-      RootNode root = GitilesMarkdown.parseFile(view, readmePath, md);
+      RootNode root =
+          GitilesMarkdown.parseFile(parseTimeout, view, readmePath, md);
       if (root == null) {
         return null;
       }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
index 7b7f974..59b8b5c 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
@@ -29,6 +29,7 @@
 import com.google.common.hash.Hashing;
 import com.google.common.net.HttpHeaders;
 import com.google.gitiles.BaseServlet;
+import com.google.gitiles.ConfigUtil;
 import com.google.gitiles.FormatType;
 import com.google.gitiles.GitilesAccess;
 import com.google.gitiles.GitilesView;
@@ -47,6 +48,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.joda.time.Duration;
 import org.pegdown.ast.RootNode;
 
 import java.io.IOException;
@@ -108,10 +110,12 @@
         return;
       }
 
+      Duration parseTimeout = ConfigUtil.getDuration(cfg, "markdown", null,
+          "parseTimeout", Duration.standardSeconds(2));
       view = view.toBuilder().setPathPart(srcmd.path).build();
       int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
       RootNode doc = GitilesMarkdown.parseFile(
-          view, srcmd.path,
+          parseTimeout, view, srcmd.path,
           srcmd.read(rw.getObjectReader(), inputLimit));
       if (doc == null) {
         res.sendRedirect(GitilesView.show().copyFrom(view).toUrl());
@@ -121,7 +125,7 @@
       RootNode nav = null;
       if (navmd != null) {
         nav = GitilesMarkdown.parseFile(
-            view, navmd.path,
+            parseTimeout, view, navmd.path,
             navmd.read(rw.getObjectReader(), inputLimit));
         if (nav == null) {
           res.setStatus(SC_INTERNAL_SERVER_ERROR);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
index dcef245..91168b8 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Throwables;
 import com.google.gitiles.GitilesView;
 
+import org.joda.time.Duration;
 import org.parboiled.Rule;
 import org.parboiled.common.Factory;
 import org.parboiled.errors.ParserRuntimeException;
@@ -50,20 +51,22 @@
   // this impacting the rendered formatting.
   private static final int MD_OPTIONS = (ALL | SUPPRESS_ALL_HTML) & ~(HARDWRAPS);
 
-  public static RootNode parseFile(GitilesView view, String path, String md) {
+  public static RootNode parseFile(Duration parseTimeout, GitilesView view,
+      String path, String md) {
     if (md == null) {
       return null;
     }
 
     try {
       try {
-        return newParser().parseMarkdown(md.toCharArray());
+        return newParser(parseTimeout).parseMarkdown(md.toCharArray());
       } catch (ParserRuntimeException e) {
         Throwables.propagateIfInstanceOf(e.getCause(), ParsingTimeoutException.class);
         throw e;
       }
     } catch (ParsingTimeoutException e) {
-      log.error("timeout rendering {}/{} at {}",
+      log.error("timeout {} ms rendering {}/{} at {}",
+          parseTimeout.getMillis(),
           view.getRepositoryName(),
           path,
           view.getRevision().getName());
@@ -71,17 +74,19 @@
     }
   }
 
-  private static PegDownProcessor newParser() {
+  private static PegDownProcessor newParser(Duration parseDeadline) {
     PegDownPlugins plugins = new PegDownPlugins.Builder()
-        .withPlugin(GitilesMarkdown.class)
+        .withPlugin(GitilesMarkdown.class, parseDeadline)
         .build();
-    return new PegDownProcessor(MD_OPTIONS, plugins);
+    return new PegDownProcessor(MD_OPTIONS, parseDeadline.getMillis(), plugins);
   }
 
+  private final Duration parseTimeout;
   private PegDownProcessor parser;
 
-  GitilesMarkdown() {
-    super(MD_OPTIONS, 2000L, DefaultParseRunnerProvider);
+  GitilesMarkdown(Duration parseTimeout) {
+    super(MD_OPTIONS, parseTimeout.getMillis(), DefaultParseRunnerProvider);
+    this.parseTimeout = parseTimeout;
   }
 
   @Override
@@ -235,7 +240,7 @@
     // use its existing parsing rules. Recurse manually for inner text
     // parsing within a block.
     if (parser == null) {
-      parser = newParser();
+      parser = newParser(parseTimeout);
     }
     return parser.parseMarkdown(body.getChars()).getChildren();
   }