You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2018/09/18 18:13:35 UTC

[tika] 02/11: TIKA-2725 -- further cleanups and merge conflicts

This is an automated email from the ASF dual-hosted git repository.

tallison pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tika.git

commit f10c8fe884987920c5332885f414ecd8c06ddc05
Author: TALLISON <ta...@apache.org>
AuthorDate: Tue Sep 11 16:16:34 2018 -0400

    TIKA-2725 -- further cleanups and merge conflicts
---
 .../tika/server/FileCountExceededException.java    |   9 -
 .../java/org/apache/tika/server/ServerStatus.java  |  80 +++---
 .../apache/tika/server/ServerStatusWatcher.java    | 134 ++++++++--
 .../org/apache/tika/server/ServerTimeouts.java     | 106 ++++++++
 .../java/org/apache/tika/server/TikaServerCli.java | 199 +++++++--------
 .../org/apache/tika/server/TikaServerWatchDog.java | 233 +++++++++++++++++
 .../tika/server/resource/DetectorResource.java     |  10 +
 .../apache/tika/server/resource/TikaResource.java  |  23 +-
 .../apache/tika/server/resource/TikaVersion.java   |   1 +
 .../apache/tika/server/resource/TikaWelcome.java   |   5 +
 .../tika/server/resource/TranslateResource.java    |  19 +-
 .../java/org/apache/tika/server/CXFTestBase.java   |   2 +-
 .../apache/tika/server/DetectorResourceTest.java   |   2 +-
 .../apache/tika/server/ServerIntegrationTest.java  |  73 ------
 .../org/apache/tika/server/ServerStatusTest.java   |  16 +-
 .../org/apache/tika/server/StackTraceOffTest.java  |   2 +-
 .../org/apache/tika/server/StackTraceTest.java     |   2 +-
 .../tika/server/TikaServerIntegrationTest.java     | 276 +++++++++++++++++++++
 .../org/apache/tika/server/TikaWelcomeTest.java    |   2 +-
 .../apache/tika/server/TranslateResourceTest.java  |   2 +-
 .../src/test/resources/mock/heavy_hand_100.xml     |  25 ++
 .../src/test/resources/mock/heavy_hang_30000.xml   |  25 ++
 tika-server/src/test/resources/mock/real_oom.xml   |  24 ++
 .../src/test/resources/mock/system_exit.xml        |  25 ++
 .../src/test/resources/mock/thread_interrupt.xml   |  25 ++
 25 files changed, 1062 insertions(+), 258 deletions(-)

diff --git a/tika-server/src/main/java/org/apache/tika/server/FileCountExceededException.java b/tika-server/src/main/java/org/apache/tika/server/FileCountExceededException.java
deleted file mode 100644
index 9920556..0000000
--- a/tika-server/src/main/java/org/apache/tika/server/FileCountExceededException.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.apache.tika.server;
-
-/**
- * Exception thrown by ServerStatusWatcher if tika-server exceeds
- * the maximum number of files to process.
- */
-public class FileCountExceededException extends Exception {
-
-}
diff --git a/tika-server/src/main/java/org/apache/tika/server/ServerStatus.java b/tika-server/src/main/java/org/apache/tika/server/ServerStatus.java
index 861007d..ac5fed4 100644
--- a/tika-server/src/main/java/org/apache/tika/server/ServerStatus.java
+++ b/tika-server/src/main/java/org/apache/tika/server/ServerStatus.java
@@ -16,53 +16,72 @@
  */
 package org.apache.tika.server;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 public class ServerStatus {
 
-    enum STATUS {
-        OPEN(0),
+    enum DIRECTIVES {
+        PING((byte)0),
+        PING_ACTIVE_SERVER_TASKS((byte)1),
+        SHUTDOWN((byte)2);
+
+        private final byte b;
+        DIRECTIVES(byte b) {
+            this.b = b;
+        }
+        byte getByte() { return b;}
+    }
+
+    public enum STATUS {
+        OPERATING(0),
         HIT_MAX(1),
         TIMEOUT(2),
         ERROR(3),
-        PARENT_REQUESTED_SHUTDOWN(4);
+        PARENT_REQUESTED_SHUTDOWN(4),
+        PARENT_EXCEPTION(5);
 
         private final int shutdownCode;
+
+        static STATUS lookup(int i) {
+            STATUS[] values = STATUS.values();
+            if (i < 0 || i >= values.length) {
+                throw new ArrayIndexOutOfBoundsException(i +
+                        " is not acceptable for an array of length "+values.length);
+            }
+            return STATUS.values()[i];
+        }
+
         STATUS(int shutdownCode) {
             this.shutdownCode = shutdownCode;
         }
         int getShutdownCode() {
             return shutdownCode;
         }
+        byte getByte() { return (byte) shutdownCode;}
+
     }
-    enum TASK {
+    public enum TASK {
         PARSE,
-        UNZIP,
         DETECT,
-        METADATA
+        TRANSLATE
     };
+    private static final Logger LOG = LoggerFactory.getLogger(ServerStatus.class);
 
-    private final int maxFilesToProcess;
-    private AtomicInteger counter = new AtomicInteger(0);
-    private Map<Integer, TaskStatus> tasks = new HashMap<>();
+    private AtomicLong counter = new AtomicLong(0);
+    private Map<Long, TaskStatus> tasks = new HashMap<>();
 
-    private STATUS status = STATUS.OPEN;
-    public ServerStatus(int maxFilesToProcess) {
-        this.maxFilesToProcess = maxFilesToProcess;
-    }
-    public synchronized int start(TASK task, String fileName) throws FileCountExceededException {
-        int i = counter.incrementAndGet();
-        if (i == Integer.MAX_VALUE ||
-                (maxFilesToProcess > 0 && i >= maxFilesToProcess)) {
-            setStatus(STATUS.HIT_MAX);
-            throw new FileCountExceededException();
-        }
-        tasks.put(i, new TaskStatus(task, Instant.now(), fileName));
-        return i;
+    private STATUS status = STATUS.OPERATING;
+
+    public synchronized long start(TASK task, String fileName) {
+        long taskId = counter.incrementAndGet();
+        tasks.put(taskId, new TaskStatus(task, Instant.now(), fileName));
+        return taskId;
     }
 
     /**
@@ -71,7 +90,7 @@ public class ServerStatus {
      * @param taskId
      * @throws IllegalArgumentException if there is no task by that taskId in the collection
      */
-    public synchronized void complete(int taskId) throws IllegalArgumentException {
+    public synchronized void complete(long taskId) throws IllegalArgumentException {
         TaskStatus status = tasks.remove(taskId);
         if (status == null) {
             throw new IllegalArgumentException("TaskId is not in map:"+taskId);
@@ -86,13 +105,18 @@ public class ServerStatus {
         return status;
     }
 
-    public synchronized Map<Integer, TaskStatus> getTasks() {
-        Map<Integer, TaskStatus> ret = new HashMap<>();
+    public synchronized Map<Long, TaskStatus> getTasks() {
+        Map<Long, TaskStatus> ret = new HashMap<>();
         ret.putAll(tasks);
         return ret;
     }
 
-    public synchronized int getFilesProcessed() {
+    public synchronized long getFilesProcessed() {
         return counter.get();
     }
+
+    public synchronized boolean isOperating() {
+        return status == STATUS.OPERATING;
+    }
+
 }
diff --git a/tika-server/src/main/java/org/apache/tika/server/ServerStatusWatcher.java b/tika-server/src/main/java/org/apache/tika/server/ServerStatusWatcher.java
index 24b1ddb..8023e94 100644
--- a/tika-server/src/main/java/org/apache/tika/server/ServerStatusWatcher.java
+++ b/tika-server/src/main/java/org/apache/tika/server/ServerStatusWatcher.java
@@ -17,51 +17,110 @@
 
 package org.apache.tika.server;
 
-import org.apache.tika.server.resource.TranslateResource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.concurrent.Callable;
 
 public class ServerStatusWatcher implements Runnable {
 
 
     private static final Logger LOG = LoggerFactory.getLogger(ServerStatusWatcher.class);
-
     private final ServerStatus serverStatus;
-    private final long timeoutMillis;
-    private final long pulseMillis;
+    private final DataInputStream fromParent;
+    private final DataOutputStream toParent;
+    private final long maxFiles;
+    private final ServerTimeouts serverTimeouts;
+
 
-    public ServerStatusWatcher(ServerStatus serverStatus, long timeoutMillis, long pulseMillis) {
+    private volatile Instant lastPing = null;
+
+    public ServerStatusWatcher(ServerStatus serverStatus,
+                               InputStream inputStream, OutputStream outputStream,
+                               long maxFiles,
+                               ServerTimeouts serverTimeouts) {
         this.serverStatus = serverStatus;
-        this.timeoutMillis = timeoutMillis;
-        this.pulseMillis = pulseMillis;
+        this.maxFiles = maxFiles;
+        this.serverTimeouts = serverTimeouts;
+
+        this.fromParent = new DataInputStream(inputStream);
+        this.toParent = new DataOutputStream(outputStream);
+        Thread statusWatcher = new Thread(new StatusWatcher());
+        statusWatcher.setDaemon(true);
+        statusWatcher.start();
     }
 
     @Override
     public void run() {
-        ServerStatus.STATUS status = serverStatus.getStatus();
-        while (status.equals(ServerStatus.STATUS.OPEN)) {
+        //let parent know child is alive
+        try {
+            toParent.writeByte(ServerStatus.STATUS.OPERATING.getByte());
+            toParent.flush();
+        } catch (Exception e) {
+            LOG.warn("Exception writing startup ping to parent", e);
+            serverStatus.setStatus(ServerStatus.STATUS.PARENT_EXCEPTION);
+            shutdown(ServerStatus.STATUS.PARENT_EXCEPTION);
+        }
+
+        byte directive = (byte)-1;
+        while (true) {
             try {
-                Thread.sleep(pulseMillis);
-            } catch (InterruptedException e) {
+                directive = fromParent.readByte();
+                lastPing = Instant.now();
+            } catch (Exception e) {
+                LOG.warn("Exception reading from parent", e);
+                serverStatus.setStatus(ServerStatus.STATUS.PARENT_EXCEPTION);
+                shutdown(ServerStatus.STATUS.PARENT_EXCEPTION);
             }
-            checkForTimeouts();
-            status = serverStatus.getStatus();
+            if (directive == ServerStatus.DIRECTIVES.PING.getByte()) {
+                if (serverStatus.getStatus().equals(ServerStatus.STATUS.OPERATING)) {
+                    checkForHitMaxFiles();
+                    checkForTaskTimeouts();
+                }
+                try {
+                    toParent.writeByte(serverStatus.getStatus().getByte());
+                    toParent.flush();
+                } catch (Exception e) {
+                    LOG.warn("Exception writing to parent", e);
+                    serverStatus.setStatus(ServerStatus.STATUS.PARENT_EXCEPTION);
+                    shutdown(ServerStatus.STATUS.PARENT_EXCEPTION);
+                }
+            } else if (directive == ServerStatus.DIRECTIVES.SHUTDOWN.getByte()) {
+                LOG.info("Parent requested shutdown");
+                serverStatus.setStatus(ServerStatus.STATUS.PARENT_REQUESTED_SHUTDOWN);
+                shutdown(ServerStatus.STATUS.PARENT_REQUESTED_SHUTDOWN);
+            } else if (directive == ServerStatus.DIRECTIVES.PING_ACTIVE_SERVER_TASKS.getByte()) {              try {
+                    toParent.writeInt(serverStatus.getTasks().size());
+                    toParent.flush();
+                } catch (Exception e) {
+                    LOG.warn("Exception writing to parent", e);
+                    serverStatus.setStatus(ServerStatus.STATUS.PARENT_EXCEPTION);
+                    shutdown(ServerStatus.STATUS.PARENT_EXCEPTION);
+                }
+            }
+        }
+    }
+
+    private void checkForHitMaxFiles() {
+        if (maxFiles < 0) {
+            return;
         }
-        if (! status.equals(ServerStatus.STATUS.OPEN)) {
-            LOG.warn("child process shutting down with status: {}", status);
-            System.exit(status.getShutdownCode());
+        long filesProcessed = serverStatus.getFilesProcessed();
+        if (filesProcessed >= maxFiles) {
+            serverStatus.setStatus(ServerStatus.STATUS.HIT_MAX);
         }
     }
 
-    private void checkForTimeouts() {
+    private void checkForTaskTimeouts() {
         Instant now = Instant.now();
         for (TaskStatus status : serverStatus.getTasks().values()) {
-            long millisElapsed = Duration.between(now, status.started).toMillis();
-            if (millisElapsed > timeoutMillis) {
+            long millisElapsed = Duration.between(status.started, now).toMillis();
+            if (millisElapsed > serverTimeouts.getTaskTimeoutMillis()) {
                 serverStatus.setStatus(ServerStatus.STATUS.TIMEOUT);
                 if (status.fileName.isPresent()) {
                     LOG.error("Timeout task {}, millis elapsed {}, file {}",
@@ -73,4 +132,39 @@ public class ServerStatusWatcher implements Runnable {
             }
         }
     }
+
+    private void shutdown(ServerStatus.STATUS status) {
+        LOG.info("Shutting down child process with status: " +status.name());
+        System.exit(status.getShutdownCode());
+    }
+
+    //This is an internal thread that pulses every 100MS
+    //within the child to see if the child should die.
+    private class StatusWatcher implements Runnable {
+
+        @Override
+        public void run() {
+            while (true) {
+                ServerStatus.STATUS currStatus = serverStatus.getStatus();
+
+                if (currStatus != ServerStatus.STATUS.OPERATING) {
+                    LOG.warn("child process observed "+currStatus.name()+ " and is shutting down.");
+                    shutdown(currStatus);
+                }
+
+                if (lastPing != null) {
+                    long elapsed = Duration.between(lastPing, Instant.now()).toMillis();
+                    if (elapsed > serverTimeouts.getPingTimeoutMillis()) {
+                        serverStatus.setStatus(ServerStatus.STATUS.PARENT_EXCEPTION);
+                        shutdown(ServerStatus.STATUS.PARENT_EXCEPTION);
+                    }
+                }
+                try {
+                    Thread.sleep(serverTimeouts.getPingPulseMillis());
+                } catch (InterruptedException e) {
+                    return;
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/tika-server/src/main/java/org/apache/tika/server/ServerTimeouts.java b/tika-server/src/main/java/org/apache/tika/server/ServerTimeouts.java
new file mode 100644
index 0000000..b85d89c
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/ServerTimeouts.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.server;
+
+public class ServerTimeouts {
+
+    /*
+    TODO: integrate these settings:
+     * Number of milliseconds to wait to start child process.
+    public static final long DEFAULT_CHILD_PROCESS_STARTUP_MILLIS = 60000;
+
+     * Maximum number of milliseconds to wait to shutdown child process to allow
+     * for current parses to complete.
+    public static final long DEFAULT_CHILD_PROCESS_SHUTDOWN_MILLIS = 30000;
+
+    private long childProcessStartupMillis = DEFAULT_CHILD_PROCESS_STARTUP_MILLIS;
+
+    private long childProcessShutdownMillis = DEFAULT_CHILD_PROCESS_SHUTDOWN_MILLIS;
+
+     */
+
+
+
+    /**
+     * If the child doesn't receive a ping or the parent doesn't
+     * hear back from a ping in this amount of time, kill and restart the child.
+     */
+    public static final long DEFAULT_PING_TIMEOUT_MILLIS = 30000;
+
+    /**
+     * How often should the parent try to ping the child to check status
+     */
+    public static final long DEFAULT_PING_PULSE_MILLIS = 500;
+
+    /**
+     * Number of milliseconds to wait per server task (parse, detect, unpack, translate,
+     * etc.) before timing out and shutting down the child process.
+     */
+    public static final long DEFAULT_TASK_TIMEOUT_MILLIS = 120000;
+
+    private long taskTimeoutMillis = DEFAULT_TASK_TIMEOUT_MILLIS;
+
+    private long pingTimeoutMillis = DEFAULT_PING_TIMEOUT_MILLIS;
+
+    private long pingPulseMillis = DEFAULT_PING_PULSE_MILLIS;
+
+
+    /**
+     * How long to wait for a task before shutting down the child server process
+     * and restarting it.
+     * @return
+     */
+    public long getTaskTimeoutMillis() {
+        return taskTimeoutMillis;
+    }
+
+    /**
+     *
+     * @param taskTimeoutMillis number of milliseconds to allow per task
+     *                          (parse, detection, unzipping, etc.)
+     */
+    public void setTaskTimeoutMillis(long taskTimeoutMillis) {
+        this.taskTimeoutMillis = taskTimeoutMillis;
+    }
+
+    public long getPingTimeoutMillis() {
+        return pingTimeoutMillis;
+    }
+
+    /**
+     *
+     * @param pingTimeoutMillis if the parent doesn't receive a response
+     *                          in this amount of time, or
+     *                          if the child doesn't receive a ping
+     *                          in this amount of time, restart the child process
+     */
+    public void setPingTimeoutMillis(long pingTimeoutMillis) {
+        this.pingTimeoutMillis = pingTimeoutMillis;
+    }
+
+    public long getPingPulseMillis() {
+        return pingPulseMillis;
+    }
+
+    /**
+     *
+     * @param pingPulseMillis how often to test that the parent and/or child is alive
+     */
+    public void setPingPulseMillis(long pingPulseMillis) {
+        this.pingPulseMillis = pingPulseMillis;
+    }
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java b/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
index af8fd8f..e3242d1 100644
--- a/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
+++ b/tika-server/src/main/java/org/apache/tika/server/TikaServerCli.java
@@ -17,7 +17,7 @@
 
 package org.apache.tika.server;
 
-import java.io.IOException;
+import java.io.ByteArrayInputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -66,11 +66,7 @@ public class TikaServerCli {
 
 
     //used in spawn-child mode
-    private static final long PULSE_MILLIS = 100;
-    private static final int DEFAULT_MAX_FILES = -1;
-    private static final long DEFAULT_TIME_OUT_MS = 60000;
-    private static final long DEFAULT_PULSE_MS = 500;
-    private static Thread SHUTDOWN_HOOK = null;
+    private static final long DEFAULT_MAX_FILES = 100000;
 
 
     public static final int DEFAULT_PORT = 9998;
@@ -86,6 +82,10 @@ public class TikaServerCli {
             "drive or a webpage from your intranet.  See CVE-2015-3271.\n"+
             "Please make sure you know what you are doing.";
 
+    private static final List<String> ONLY_IN_SPAWN_CHILD_MODE =
+            Arrays.asList(new String[] { "taskTimeoutMillis", "taskPulseMillis",
+            "pingTimeoutMillis", "pingPulseMillis", "maxFiles"});
+
     private static Options getOptions() {
         Options options = new Options();
         options.addOption("C", "cors", true, "origin allowed to make CORS requests (default=NONE)\nall allowed if \"all\"");
@@ -100,7 +100,14 @@ public class TikaServerCli {
         options.addOption("enableUnsecureFeatures", false, "this is required to enable fileUrl.");
         options.addOption("enableFileUrl", false, "allows user to pass in fileUrl instead of InputStream.");
         options.addOption("spawnChild", false, "whether or not to spawn a child process for robustness");
-        options.addOption("maxFiles", false, "shutdown server after this many files -- use only in 'spawnChild' mode");
+        options.addOption("taskTimeoutMillis", true, "Only in spawn child mode: how long to wait for a task (e.g. parse) to finish");
+        options.addOption("taskPulseMillis", true, "Only in spawn child mode: how often to check if a task has timed out.");
+        options.addOption("pingTimeoutMillis", true, "Only in spawn child mode: how long to wait to wait for a ping and/or ping response.");
+        options.addOption("pingPulseMillis", true, "Only in spawn child mode: how often to check if a ping has timed out.");
+
+        options.addOption("maxFiles", false, "Only in spawn child mode: shutdown server after this many files -- use only in 'spawnChild' mode");
+        options.addOption("child", false, "this process is a child process -- EXPERT -- " +
+                "should normally only be invoked by parent process");
         return options;
     }
 
@@ -116,106 +123,45 @@ public class TikaServerCli {
     }
 
     private static void execute(String[] args) throws Exception {
-        boolean spawnChild = false;
-        for (int i = 0; i < args.length; i++) {
-            if ("-spawnChild".equals(args[i]) || "--spawnChild".equals(args[i])) {
-                spawnChild = true;
-                break;
-            }
-        }
-        if (spawnChild) {
-            spawnChild(args);
-        } else {
-            executeLegacy(args);
-        }
-    }
+        Options options = getOptions();
 
-    private static void spawnChild(String[] args) throws Exception {
-        Process child = start(args);
-        try {
-            while (true) {
-                Thread.sleep(PULSE_MILLIS);
+        CommandLineParser cliParser = new GnuParser();
 
-                int exitValue = Integer.MAX_VALUE;
-                try {
-                    exitValue = child.exitValue();
-                } catch (IllegalThreadStateException e) {
-                    //process is still running
-                }
-                if (exitValue != Integer.MAX_VALUE) {
-                    if (exitValue != ServerStatus.STATUS.PARENT_REQUESTED_SHUTDOWN.getShutdownCode()) {
-                        LOG.warn("child exited with code ({}) -- restarting, now", Integer.toString(exitValue));
-                        child.destroyForcibly();
-                        child = start(args);
+        //need to strip out -J (child jvm opts) from this parse
+        //they'll be processed correctly in args in the watch dog
+        //and they won't be needed in legacy.
+        CommandLine line = cliParser.parse(options, stripChildArgs(args));
+        if (line.hasOption("spawnChild")) {
+            TikaServerWatchDog watchDog = new TikaServerWatchDog();
+            watchDog.execute(args, configureServerTimeouts(line));
+        } else {
+            if (! line.hasOption("child")) {
+                //make sure the user didn't misunderstand the options
+                for (String childOnly : ONLY_IN_SPAWN_CHILD_MODE) {
+                    if (line.hasOption(childOnly)) {
+                        System.err.println("The option '" + childOnly +
+                                "' can only be used with '-spawnChild'");
+                        usage(options);
                     }
                 }
             }
-        } catch (InterruptedException e) {
-            //interrupted...shutting down
-        } finally {
-            child.destroyForcibly();
-        }
-    }
-
-    private static Process start(String[] args) throws IOException {
-        ProcessBuilder builder = new ProcessBuilder();
-        builder.inheritIO();
-        List<String> argList = new ArrayList<>();
-        List<String> jvmArgs = extractJVMArgs(args);
-        List<String> childArgs = extractArgs(args);
-        argList.add("java");
-            if (! jvmArgs.contains("-cp") && ! jvmArgs.contains("--classpath")) {
-                String cp = System.getProperty("java.class.path");
-                jvmArgs.add("-cp");
-                jvmArgs.add(cp);
-            }
-        argList.addAll(jvmArgs);
-        argList.add("org.apache.tika.server.TikaServerCli");
-        argList.addAll(childArgs);
-
-        builder.command(argList);
-
-        Process process = builder.start();
-
-        if (SHUTDOWN_HOOK != null) {
-            Runtime.getRuntime().removeShutdownHook(SHUTDOWN_HOOK);
+            executeLegacy(line, options);
         }
-        SHUTDOWN_HOOK = new Thread(() -> process.destroy());
-        Runtime.getRuntime().addShutdownHook(SHUTDOWN_HOOK);
-        return process;
     }
 
-    private static List<String> extractArgs(String[] args) {
-        List<String> argList = new ArrayList<>();
+    private static String[] stripChildArgs(String[] args) {
+        List<String> ret = new ArrayList<>();
         for (int i = 0; i < args.length; i++) {
-            if (args[i].startsWith("-J") || args[i].equals("-spawnChild") || args[i].equals("--spawnChild")) {
-                continue;
+            if (! args[i].startsWith("-J")) {
+                ret.add(args[i]);
             }
-            argList.add(args[i]);
         }
-        return argList;
+        return ret.toArray(new String[ret.size()]);
     }
 
-    private static List<String> extractJVMArgs(String[] args) {
-        List<String> jvmArgs = new ArrayList<>();
-        for (int i = 0; i < args.length; i++) {
-            if (args[i].startsWith("-J")) {
-                jvmArgs.add("-"+args[i].substring(2));
-            }
-        }
-        return jvmArgs;
-    }
-
-    private static void executeLegacy(String[] args) throws Exception {
-            Options options = getOptions();
-
-            CommandLineParser cliParser = new GnuParser();
-            CommandLine line = cliParser.parse(options, args);
-
+    private static void executeLegacy(CommandLine line, Options options) throws Exception {
             if (line.hasOption("help")) {
-                HelpFormatter helpFormatter = new HelpFormatter();
-                helpFormatter.printHelp("tikaserver", options);
-                System.exit(-1);
+                usage(options);
             }
 
             String host = DEFAULT_HOST;
@@ -307,30 +253,31 @@ public class TikaServerCli {
                 inputStreamFactory = new DefaultInputStreamFactory();
             }
 
-            int maxFiles = DEFAULT_MAX_FILES;
-            if (line.hasOption("maxFiles")) {
-                maxFiles = Integer.parseInt(line.getOptionValue("maxFiles"));
-            }
+            ServerStatus serverStatus = new ServerStatus();
+            //if this is a child process
+            if (line.hasOption("child")) {
+                long maxFiles = DEFAULT_MAX_FILES;
+                if (line.hasOption("maxFiles")) {
+                    maxFiles = Long.parseLong(line.getOptionValue("maxFiles"));
+                }
 
-            long timeoutMS = DEFAULT_TIME_OUT_MS;
-            if (line.hasOption("timeoutMS")) {
-                timeoutMS = Long.parseLong(line.getOptionValue("timeoutMS"));
+                ServerTimeouts serverTimeouts = configureServerTimeouts(line);
+                Thread serverThread =
+                new Thread(new ServerStatusWatcher(serverStatus, System.in,
+                        System.out, maxFiles, serverTimeouts));
+                serverThread.start();
+                System.setIn(new ByteArrayInputStream(new byte[0]));
+                System.setOut(System.err);
             }
-            long pulseMS = DEFAULT_PULSE_MS;
-            if (line.hasOption("pulseMS")) {
-                pulseMS = Long.parseLong(line.getOptionValue("pulseMS"));
-            }
-            ServerStatus serverStatus = new ServerStatus(maxFiles);
-            new Thread(new ServerStatusWatcher(serverStatus, timeoutMS, pulseMS)).start();
-            TikaResource.init(tika, digester, inputStreamFactory);
+            TikaResource.init(tika, digester, inputStreamFactory, serverStatus);
             JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
 
             List<ResourceProvider> rCoreProviders = new ArrayList<>();
             rCoreProviders.add(new SingletonResourceProvider(new MetadataResource()));
             rCoreProviders.add(new SingletonResourceProvider(new RecursiveMetadataResource()));
-            rCoreProviders.add(new SingletonResourceProvider(new DetectorResource()));
+            rCoreProviders.add(new SingletonResourceProvider(new DetectorResource(serverStatus)));
             rCoreProviders.add(new SingletonResourceProvider(new LanguageResource()));
-            rCoreProviders.add(new SingletonResourceProvider(new TranslateResource()));
+            rCoreProviders.add(new SingletonResourceProvider(new TranslateResource(serverStatus)));
             rCoreProviders.add(new SingletonResourceProvider(new TikaResource()));
             rCoreProviders.add(new SingletonResourceProvider(new UnpackerResource()));
             rCoreProviders.add(new SingletonResourceProvider(new TikaMimeTypes()));
@@ -368,4 +315,38 @@ public class TikaServerCli {
             sf.create();
             LOG.info("Started Apache Tika server at {}", url);
     }
+
+    private static void usage(Options options) {
+        HelpFormatter helpFormatter = new HelpFormatter();
+        helpFormatter.printHelp("tikaserver", options);
+        System.exit(-1);
+    }
+
+    private static ServerTimeouts configureServerTimeouts(CommandLine line) {
+        ServerTimeouts serverTimeouts = new ServerTimeouts();
+        /*TODO -- add these in
+        if (line.hasOption("childProcessStartupMillis")) {
+            serverTimeouts.setChildProcessStartupMillis(
+                    Long.parseLong(line.getOptionValue("childProcessStartupMillis")));
+        }
+        if (line.hasOption("childProcessShutdownMillis")) {
+            serverTimeouts.setChildProcessShutdownMillis(
+                    Long.parseLong(line.getOptionValue("childProcesShutdownMillis")));
+        }*/
+        if (line.hasOption("taskTimeoutMillis")) {
+            serverTimeouts.setTaskTimeoutMillis(
+                    Long.parseLong(line.getOptionValue("taskTimeoutMillis")));
+        }
+        if (line.hasOption("pingTimeoutMillis")) {
+            serverTimeouts.setPingTimeoutMillis(
+                    Long.parseLong(line.getOptionValue("pingTimeoutMillis")));
+        }
+        if (line.hasOption("pingPulseMillis")) {
+            serverTimeouts.setPingPulseMillis(
+                    Long.parseLong(line.getOptionValue("pingPulseMillis")));
+        }
+
+        return serverTimeouts;
+    }
+
 }
diff --git a/tika-server/src/main/java/org/apache/tika/server/TikaServerWatchDog.java b/tika-server/src/main/java/org/apache/tika/server/TikaServerWatchDog.java
new file mode 100644
index 0000000..67007f2
--- /dev/null
+++ b/tika-server/src/main/java/org/apache/tika/server/TikaServerWatchDog.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tika.server;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class TikaServerWatchDog {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TikaServerWatchDog.class);
+    private volatile Instant lastPing = null;
+    private ChildProcess childProcess = null;
+    int restarts = 0;
+
+    public void execute(String[] args, ServerTimeouts serverTimeouts) throws Exception {
+        //if the child thread is in stop-the-world mode, and isn't
+        //responding to the ping, this thread checks to make sure
+        //that the parent ping is sent and received often enough
+        //If it isn't, this force destroys the child process.
+        Thread pingTimer = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                while (true) {
+                    long tmpLastPing = -1L;
+                    try {
+                        //TODO: clean this up with synchronization/locking
+                        //to avoid potential NPE
+                        tmpLastPing = lastPing.toEpochMilli();
+                    } catch (NullPointerException e) {
+
+                    }
+                    if (tmpLastPing > 0) {
+                        long elapsed = Duration.between(Instant.ofEpochMilli(tmpLastPing), Instant.now()).toMillis();
+                        if (elapsed > serverTimeouts.getPingTimeoutMillis()) {
+                            Process processToDestroy = null;
+                            try {
+                                processToDestroy = childProcess.process;
+                            } catch (NullPointerException e) {
+                                //ignore
+                            }
+                            destroyChildForcibly(processToDestroy);
+                        }
+                    }
+                    try {
+                        Thread.sleep(serverTimeouts.getPingPulseMillis());
+                    } catch (InterruptedException e) {
+                        //swallow
+                    }
+                }
+            }
+        }
+        );
+        pingTimer.setDaemon(true);
+        pingTimer.start();
+        try {
+            childProcess = new ChildProcess(args);
+
+            while (true) {
+
+                if (!childProcess.ping()) {
+                    lastPing = null;
+                    childProcess.close();
+                    LOG.info("About to restart the child process");
+                    childProcess = new ChildProcess(args);
+                    LOG.info("Successfully restarted child process -- {} restarts so far)", ++restarts);
+                }
+                Thread.sleep(serverTimeouts.getPingPulseMillis());
+            }
+        } catch (InterruptedException e) {
+            //interrupted...shutting down
+        } finally {
+            if (childProcess != null) {
+                childProcess.close();
+            }
+        }
+    }
+
+    private static List<String> extractArgs(String[] args) {
+        List<String> argList = new ArrayList<>();
+        for (int i = 0; i < args.length; i++) {
+            if (args[i].startsWith("-J") || args[i].equals("-spawnChild") || args[i].equals("--spawnChild")) {
+                continue;
+            }
+            argList.add(args[i]);
+        }
+        return argList;
+    }
+
+    private static List<String> extractJVMArgs(String[] args) {
+        List<String> jvmArgs = new ArrayList<>();
+        for (int i = 0; i < args.length; i++) {
+            if (args[i].startsWith("-J")) {
+                jvmArgs.add("-"+args[i].substring(2));
+            }
+        }
+        return jvmArgs;
+    }
+
+    private class ChildProcess {
+        private Thread SHUTDOWN_HOOK = null;
+
+        Process process;
+        DataInputStream fromChild;
+        DataOutputStream toChild;
+
+
+
+        private ChildProcess(String[] args) throws Exception {
+            this.process = startProcess(args);
+
+            this.fromChild = new DataInputStream(process.getInputStream());
+            this.toChild = new DataOutputStream(process.getOutputStream());
+            byte status = fromChild.readByte();
+            if (status != ServerStatus.STATUS.OPERATING.getByte()) {
+                throw new IOException("bad status from child process: "+
+                        ServerStatus.STATUS.lookup(status));
+            }
+        }
+
+        public boolean ping() {
+            lastPing = Instant.now();
+            try {
+                toChild.writeByte(ServerStatus.DIRECTIVES.PING.getByte());
+                toChild.flush();
+            } catch (Exception e) {
+                LOG.warn("Exception pinging child process", e);
+                return false;
+            }
+            try {
+                byte status = fromChild.readByte();
+                if (status != ServerStatus.STATUS.OPERATING.getByte()) {
+                    LOG.warn("Received status from child: {}",
+                            ServerStatus.STATUS.lookup(status));
+                    return false;
+                }
+            } catch (Exception e) {
+                LOG.warn("Exception receiving status from child", e);
+                return false;
+            }
+            return true;
+        }
+
+        private void close() {
+            try {
+                toChild.writeByte(ServerStatus.DIRECTIVES.SHUTDOWN.getByte());
+                toChild.flush();
+            } catch (Exception e) {
+                LOG.warn("Exception asking child to shutdown", e);
+            }
+            //TODO: add a gracefully timed shutdown routine
+            try {
+                fromChild.close();
+            } catch (Exception e) {
+                LOG.warn("Problem shutting down reader from child", e);
+            }
+
+            try {
+                toChild.close();
+            } catch (Exception e) {
+                LOG.warn("Problem shutting down writer to child", e);
+            }
+            destroyChildForcibly(process);
+        }
+
+        private Process startProcess(String[] args) throws IOException {
+            ProcessBuilder builder = new ProcessBuilder();
+            builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+            List<String> argList = new ArrayList<>();
+            List<String> jvmArgs = extractJVMArgs(args);
+            List<String> childArgs = extractArgs(args);
+            argList.add("java");
+            if (! jvmArgs.contains("-cp") && ! jvmArgs.contains("--classpath")) {
+                String cp = System.getProperty("java.class.path");
+                jvmArgs.add("-cp");
+                jvmArgs.add(cp);
+            }
+            argList.addAll(jvmArgs);
+            argList.add("org.apache.tika.server.TikaServerCli");
+            argList.addAll(childArgs);
+            argList.add("-child");
+
+            builder.command(argList);
+            Process process = builder.start();
+            if (SHUTDOWN_HOOK != null) {
+                Runtime.getRuntime().removeShutdownHook(SHUTDOWN_HOOK);
+            }
+            SHUTDOWN_HOOK = new Thread(() -> process.destroyForcibly());
+            Runtime.getRuntime().addShutdownHook(SHUTDOWN_HOOK);
+
+            return process;
+        }
+    }
+
+    private static synchronized void destroyChildForcibly(Process process) {
+        process = process.destroyForcibly();
+        try {
+            boolean destroyed = process.waitFor(60, TimeUnit.SECONDS);
+            if (! destroyed) {
+                LOG.error("Child process still alive after 60 seconds. " +
+                        "Shutting down the parent.");
+                System.exit(1);
+            }
+
+        } catch (InterruptedException e) {
+            //swallow
+        }
+    }
+
+}
diff --git a/tika-server/src/main/java/org/apache/tika/server/resource/DetectorResource.java b/tika-server/src/main/java/org/apache/tika/server/resource/DetectorResource.java
index 8af3ad4..96b6f91 100644
--- a/tika-server/src/main/java/org/apache/tika/server/resource/DetectorResource.java
+++ b/tika-server/src/main/java/org/apache/tika/server/resource/DetectorResource.java
@@ -31,13 +31,18 @@ import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.mime.MediaType;
+import org.apache.tika.server.ServerStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Path("/detect")
 public class DetectorResource {
     private static final Logger LOG = LoggerFactory.getLogger(DetectorResource.class);
+    private final ServerStatus serverStatus;
 
+    public DetectorResource(ServerStatus serverStatus) {
+        this.serverStatus = serverStatus;
+    }
     @PUT
     @Path("stream")
     @Consumes("*/*")
@@ -55,6 +60,11 @@ public class DetectorResource {
         } catch (IOException e) {
             LOG.warn("Unable to detect MIME type for file. Reason: {}", e.getMessage(), e);
             return MediaType.OCTET_STREAM.toString();
+        } catch (OutOfMemoryError e) {
+            serverStatus.setStatus(ServerStatus.STATUS.ERROR);
+            throw e;
+        } finally {
+            serverStatus.complete(taskId);
         }
     }
 }
diff --git a/tika-server/src/main/java/org/apache/tika/server/resource/TikaResource.java b/tika-server/src/main/java/org/apache/tika/server/resource/TikaResource.java
index 9bd4d50..1f5d8c1 100644
--- a/tika-server/src/main/java/org/apache/tika/server/resource/TikaResource.java
+++ b/tika-server/src/main/java/org/apache/tika/server/resource/TikaResource.java
@@ -41,6 +41,7 @@ import org.apache.tika.sax.BodyContentHandler;
 import org.apache.tika.sax.ExpandedTitleContentHandler;
 import org.apache.tika.sax.RichTextContentHandler;
 import org.apache.tika.server.InputStreamFactory;
+import org.apache.tika.server.ServerStatus;
 import org.apache.tika.server.TikaServerParseException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -93,12 +94,13 @@ public class TikaResource {
     private static TikaConfig tikaConfig;
     private static DigestingParser.Digester digester = null;
     private static InputStreamFactory inputStreamFactory = null;
-
+    private static ServerStatus SERVER_STATUS = null;
     public static void init(TikaConfig config, DigestingParser.Digester digestr,
-                            InputStreamFactory iSF) {
+                            InputStreamFactory iSF, ServerStatus serverStatus) {
         tikaConfig = config;
         digester = digestr;
         inputStreamFactory = iSF;
+        SERVER_STATUS = serverStatus;
     }
 
     static {
@@ -383,6 +385,11 @@ public class TikaResource {
      */
     public static void parse(Parser parser, Logger logger, String path, InputStream inputStream,
                              ContentHandler handler, Metadata metadata, ParseContext parseContext) throws IOException {
+
+        checkIsOperating();
+
+        long taskId = SERVER_STATUS.start(ServerStatus.TASK.PARSE,
+                metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY));
         try {
             parser.parse(inputStream, handler, metadata, parseContext);
         } catch (SAXException e) {
@@ -393,11 +400,22 @@ public class TikaResource {
         } catch (Exception e) {
             logger.warn("{}: Text extraction failed", path, e);
             throw new TikaServerParseException(e);
+        } catch (OutOfMemoryError e) {
+            SERVER_STATUS.setStatus(ServerStatus.STATUS.ERROR);
+            throw e;
         } finally {
+            SERVER_STATUS.complete(taskId);
             inputStream.close();
         }
     }
 
+    public static void checkIsOperating() {
+        //check that server is not in shutdown mode
+        if (! SERVER_STATUS.isOperating()) {
+            throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE);
+        }
+    }
+
     public static void logRequest(Logger logger, UriInfo info, Metadata metadata) {
         if (metadata.get(org.apache.tika.metadata.HttpHeaders.CONTENT_TYPE) == null) {
             logger.info("{} (autodetecting type)", info.getPath());
@@ -409,6 +427,7 @@ public class TikaResource {
     @GET
     @Produces("text/plain")
     public String getMessage() {
+        checkIsOperating();
         return GREETING;
     }
 
diff --git a/tika-server/src/main/java/org/apache/tika/server/resource/TikaVersion.java b/tika-server/src/main/java/org/apache/tika/server/resource/TikaVersion.java
index b695940..a892716 100644
--- a/tika-server/src/main/java/org/apache/tika/server/resource/TikaVersion.java
+++ b/tika-server/src/main/java/org/apache/tika/server/resource/TikaVersion.java
@@ -33,6 +33,7 @@ public class TikaVersion {
     @GET
     @Produces("text/plain")
     public String getVersion() {
+        TikaResource.checkIsOperating();
         return tika.toString();
     }
 }
diff --git a/tika-server/src/main/java/org/apache/tika/server/resource/TikaWelcome.java b/tika-server/src/main/java/org/apache/tika/server/resource/TikaWelcome.java
index f44ff96..3408027 100644
--- a/tika-server/src/main/java/org/apache/tika/server/resource/TikaWelcome.java
+++ b/tika-server/src/main/java/org/apache/tika/server/resource/TikaWelcome.java
@@ -24,6 +24,8 @@ import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -135,6 +137,8 @@ public class TikaWelcome {
     @GET
     @Produces("text/html")
     public String getWelcomeHTML() {
+        TikaResource.checkIsOperating();
+
         StringBuffer h = new StringBuffer();
         String tikaVersion = tika.toString();
 
@@ -190,6 +194,7 @@ public class TikaWelcome {
     @GET
     @Produces("text/plain")
     public String getWelcomePlain() {
+        TikaResource.checkIsOperating();
         StringBuffer text = new StringBuffer();
 
         text.append(tika.toString());
diff --git a/tika-server/src/main/java/org/apache/tika/server/resource/TranslateResource.java b/tika-server/src/main/java/org/apache/tika/server/resource/TranslateResource.java
index 0aba6f9..0417077 100644
--- a/tika-server/src/main/java/org/apache/tika/server/resource/TranslateResource.java
+++ b/tika-server/src/main/java/org/apache/tika/server/resource/TranslateResource.java
@@ -29,6 +29,8 @@ import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.tika.config.LoadErrorHandler;
@@ -37,6 +39,7 @@ import org.apache.tika.exception.TikaException;
 import org.apache.tika.langdetect.OptimaizeLangDetector;
 import org.apache.tika.language.detect.LanguageResult;
 import org.apache.tika.language.translate.Translator;
+import org.apache.tika.server.ServerStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,10 +51,12 @@ public class TranslateResource {
 
 	private static final Logger LOG = LoggerFactory.getLogger(TranslateResource.class);
 
-	public TranslateResource() {
+	private final ServerStatus serverStatus;
+	public TranslateResource(ServerStatus serverStatus) {
 		this.loader = new ServiceLoader(ServiceLoader.class.getClassLoader(),
 				LoadErrorHandler.WARN);
 		this.defaultTranslator = TikaResource.getConfig().getTranslator();
+		this.serverStatus = serverStatus;
 	}
 
 	@PUT
@@ -94,8 +99,16 @@ public class TranslateResource {
 			translate = this.defaultTranslator;
 			LOG.info("Using default translator");
 		}
-
-		return translate.translate(content, sLang, dLang);
+        TikaResource.checkIsOperating();
+		long taskId = serverStatus.start(ServerStatus.TASK.TRANSLATE, null);
+		try {
+			return translate.translate(content, sLang, dLang);
+		} catch (OutOfMemoryError e) {
+		    serverStatus.setStatus(ServerStatus.STATUS.ERROR);
+		    throw e;
+        } finally {
+			serverStatus.complete(taskId);
+		}
 	}
 
 	private Translator byClassName(String className) {
diff --git a/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java b/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
index f851e97..57220d4 100644
--- a/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
+++ b/tika-server/src/test/java/org/apache/tika/server/CXFTestBase.java
@@ -84,7 +84,7 @@ public abstract class CXFTestBase {
         this.tika = new TikaConfig(getClass().getResourceAsStream("tika-config-for-server-tests.xml"));
         TikaResource.init(tika,
                 new CommonsDigester(DIGESTER_READ_LIMIT, "md5,sha1:32"),
-                new DefaultInputStreamFactory());
+                new DefaultInputStreamFactory(), new ServerStatus());
         JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
         setUpResources(sf);
         setUpProviders(sf);
diff --git a/tika-server/src/test/java/org/apache/tika/server/DetectorResourceTest.java b/tika-server/src/test/java/org/apache/tika/server/DetectorResourceTest.java
index 3d4dc1f..5b1e7b7 100644
--- a/tika-server/src/test/java/org/apache/tika/server/DetectorResourceTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/DetectorResourceTest.java
@@ -45,7 +45,7 @@ public class DetectorResourceTest extends CXFTestBase {
     protected void setUpResources(JAXRSServerFactoryBean sf) {
         sf.setResourceClasses(DetectorResource.class);
         sf.setResourceProvider(DetectorResource.class,
-                new SingletonResourceProvider(new DetectorResource()));
+                new SingletonResourceProvider(new DetectorResource(new ServerStatus())));
 
     }
 
diff --git a/tika-server/src/test/java/org/apache/tika/server/ServerIntegrationTest.java b/tika-server/src/test/java/org/apache/tika/server/ServerIntegrationTest.java
deleted file mode 100644
index 8568c6c..0000000
--- a/tika-server/src/test/java/org/apache/tika/server/ServerIntegrationTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.tika.server;
-
-import org.apache.cxf.jaxrs.client.WebClient;
-import org.apache.tika.TikaTest;
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.metadata.OfficeOpenXMLExtended;
-import org.apache.tika.metadata.serialization.JsonMetadataList;
-import org.junit.Test;
-
-import javax.ws.rs.core.Response;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.List;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-
-public class ServerIntegrationTest extends TikaTest {
-    private static final String TEST_RECURSIVE_DOC = "test_recursive_embedded.docx";
-    private static final String META_PATH = "/rmeta";
-    protected static final String endPoint =
-            "http://localhost:" + TikaServerCli.DEFAULT_PORT;
-
-    @Test
-    public void testBasic() throws Exception {
-
-        Thread serverThread = new Thread() {
-            @Override
-            public void run() {
-                TikaServerCli.main(
-                        new String[]{
-                                "-spawnChild", "-p", Integer.toString(TikaServerCli.DEFAULT_PORT)
-                        });
-            }
-        };
-        serverThread.start();
-        //test for the server being available...rather than this sleep call
-        Thread.sleep(20000);
-        Response response = WebClient
-                .create(endPoint + META_PATH)
-                .accept("application/json")
-                .put(ClassLoader
-                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
-        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
-        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
-        assertEquals(12, metadataList.size());
-        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
-        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
-
-        //assertEquals("a38e6c7b38541af87148dee9634cb811", metadataList.get(10).get("X-TIKA:digest:MD5"));
-
-        serverThread.interrupt();
-
-
-    }
-}
diff --git a/tika-server/src/test/java/org/apache/tika/server/ServerStatusTest.java b/tika-server/src/test/java/org/apache/tika/server/ServerStatusTest.java
index 23880ff..39d1583 100644
--- a/tika-server/src/test/java/org/apache/tika/server/ServerStatusTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/ServerStatusTest.java
@@ -33,18 +33,18 @@ public class ServerStatusTest {
 
     @Test(expected = IllegalArgumentException.class)
     public void testBadId() throws Exception {
-        ServerStatus status = new ServerStatus(-1);
+        ServerStatus status = new ServerStatus();
         status.complete(2);
     }
 
     @Test(timeout = 60000)
     public void testBasicMultiThreading() throws Exception {
         //make sure that synchronization is basically working
-        int numThreads = 100;
-        int filesToProcess = 100;
-        ExecutorService service = Executors.newFixedThreadPool(100);
+        int numThreads = 10;
+        int filesToProcess = 20;
+        ExecutorService service = Executors.newFixedThreadPool(numThreads);
         ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<>(service);
-        ServerStatus serverStatus = new ServerStatus(-1);
+        ServerStatus serverStatus = new ServerStatus();
         for (int i = 0; i < numThreads; i++) {
             completionService.submit(new MockTask(serverStatus, filesToProcess));
         }
@@ -78,15 +78,15 @@ public class ServerStatusTest {
             int processed = 0;
             for (int i = 0; i < filesToProcess; i++) {
                 sleepRandom(200);
-                int taskId = serverStatus.start(ServerStatus.TASK.PARSE, null);
+                long taskId = serverStatus.start(ServerStatus.TASK.PARSE, null);
                 sleepRandom(100);
                 serverStatus.complete(taskId);
                 processed++;
                 serverStatus.getStatus();
                 sleepRandom(10);
-                serverStatus.setStatus(ServerStatus.STATUS.OPEN);
+                serverStatus.setStatus(ServerStatus.STATUS.OPERATING);
                 sleepRandom(20);
-                Map<Integer, TaskStatus> tasks = serverStatus.getTasks();
+                Map<Long, TaskStatus> tasks = serverStatus.getTasks();
                 assertNotNull(tasks);
             }
             return processed;
diff --git a/tika-server/src/test/java/org/apache/tika/server/StackTraceOffTest.java b/tika-server/src/test/java/org/apache/tika/server/StackTraceOffTest.java
index 6c86437..d385581 100644
--- a/tika-server/src/test/java/org/apache/tika/server/StackTraceOffTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/StackTraceOffTest.java
@@ -65,7 +65,7 @@ public class StackTraceOffTest extends CXFTestBase {
         List<ResourceProvider> rCoreProviders = new ArrayList<ResourceProvider>();
         rCoreProviders.add(new SingletonResourceProvider(new MetadataResource()));
         rCoreProviders.add(new SingletonResourceProvider(new RecursiveMetadataResource()));
-        rCoreProviders.add(new SingletonResourceProvider(new DetectorResource()));
+        rCoreProviders.add(new SingletonResourceProvider(new DetectorResource(new ServerStatus())));
         rCoreProviders.add(new SingletonResourceProvider(new TikaResource()));
         rCoreProviders.add(new SingletonResourceProvider(new UnpackerResource()));
         sf.setResourceProviders(rCoreProviders);
diff --git a/tika-server/src/test/java/org/apache/tika/server/StackTraceTest.java b/tika-server/src/test/java/org/apache/tika/server/StackTraceTest.java
index 2b76f33..24882f7 100644
--- a/tika-server/src/test/java/org/apache/tika/server/StackTraceTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/StackTraceTest.java
@@ -59,7 +59,7 @@ public class StackTraceTest extends CXFTestBase {
         List<ResourceProvider> rCoreProviders = new ArrayList<ResourceProvider>();
         rCoreProviders.add(new SingletonResourceProvider(new MetadataResource()));
         rCoreProviders.add(new SingletonResourceProvider(new RecursiveMetadataResource()));
-        rCoreProviders.add(new SingletonResourceProvider(new DetectorResource()));
+        rCoreProviders.add(new SingletonResourceProvider(new DetectorResource(new ServerStatus())));
         rCoreProviders.add(new SingletonResourceProvider(new TikaResource()));
         rCoreProviders.add(new SingletonResourceProvider(new UnpackerResource()));
         sf.setResourceProviders(rCoreProviders);
diff --git a/tika-server/src/test/java/org/apache/tika/server/TikaServerIntegrationTest.java b/tika-server/src/test/java/org/apache/tika/server/TikaServerIntegrationTest.java
new file mode 100644
index 0000000..d328711
--- /dev/null
+++ b/tika-server/src/test/java/org/apache/tika/server/TikaServerIntegrationTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.server;
+
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.IOUtils;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.OfficeOpenXMLExtended;
+import org.apache.tika.metadata.serialization.JsonMetadataList;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+public class TikaServerIntegrationTest extends TikaTest {
+
+    private static final String TEST_RECURSIVE_DOC = "test_recursive_embedded.docx";
+    private static final String TEST_OOM = "mock/real_oom.xml";
+    private static final String TEST_SYSTEM_EXIT = "mock/system_exit.xml";
+    private static final String TEST_HEAVY_HANG = "mock/heavy_hang_30000.xml";
+    private static final String TEST_HEAVY_HANG_SHORT = "mock/heavy_hang_100.xml";
+    private static final String META_PATH = "/rmeta";
+
+    //running into conflicts on 9998 with the CXFTestBase tests
+    //TODO: figure out why?!
+    private static final String INTEGRATION_TEST_PORT = "9999";
+
+    protected static final String endPoint =
+            "http://localhost:" + INTEGRATION_TEST_PORT;
+
+    @Test
+    public void testBasic() throws Exception {
+
+        Thread serverThread = new Thread() {
+            @Override
+            public void run() {
+                TikaServerCli.main(
+                        new String[]{
+                                "-spawnChild",
+                                "-p", INTEGRATION_TEST_PORT
+                        });
+            }
+        };
+        serverThread.start();
+        awaitServerStartup();
+
+        Response response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
+        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(12, metadataList.size());
+        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
+        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
+
+        //assertEquals("a38e6c7b38541af87148dee9634cb811", metadataList.get(10).get("X-TIKA:digest:MD5"));
+
+        serverThread.interrupt();
+
+
+    }
+
+    @Test
+    public void testOOM() throws Exception {
+
+        Thread serverThread = new Thread() {
+            @Override
+            public void run() {
+                TikaServerCli.main(
+                        new String[]{
+                                "-spawnChild", "-JXmx512m",
+                                "-p", INTEGRATION_TEST_PORT
+                        });
+            }
+        };
+        serverThread.start();
+        awaitServerStartup();
+        Response response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_OOM));
+        //give some time for the server to crash/kill itself
+        Thread.sleep(2000);
+        awaitServerStartup();
+
+        response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
+        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(12, metadataList.size());
+        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
+        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
+
+        serverThread.interrupt();
+    }
+
+    @Test
+    public void testSystemExit() throws Exception {
+
+        Thread serverThread = new Thread() {
+            @Override
+            public void run() {
+                TikaServerCli.main(
+                        new String[]{
+                                "-spawnChild",
+                                "-p", INTEGRATION_TEST_PORT
+                        });
+            }
+        };
+        serverThread.start();
+        awaitServerStartup();
+        Response response = null;
+        try {
+            response = WebClient
+                    .create(endPoint + META_PATH)
+                    .accept("application/json")
+                    .put(ClassLoader
+                            .getSystemResourceAsStream(TEST_SYSTEM_EXIT));
+        } catch (Exception e) {
+            //sys exit causes catchable problems for the client
+        }
+        //give some time for the server to crash/kill itself
+        Thread.sleep(2000);
+
+        awaitServerStartup();
+
+        response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
+
+        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(12, metadataList.size());
+        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
+        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
+
+        serverThread.interrupt();
+
+
+    }
+
+    @Test
+    public void testTimeoutOk() throws Exception {
+        //test that there's enough time for this file.
+        Thread serverThread = new Thread() {
+            @Override
+            public void run() {
+                TikaServerCli.main(
+                        new String[]{
+                                "-spawnChild", "-p", INTEGRATION_TEST_PORT,
+                                "-taskTimeoutMillis", "10000", "-taskPulseMillis", "500",
+                                "-pingPulseMillis", "500"
+                        });
+            }
+        };
+        serverThread.start();
+        awaitServerStartup();
+        Response response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_HEAVY_HANG_SHORT));
+        awaitServerStartup();
+
+        response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
+        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(12, metadataList.size());
+        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
+        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
+
+        serverThread.interrupt();
+
+
+    }
+
+    @Test
+    public void testTimeout() throws Exception {
+
+        Thread serverThread = new Thread() {
+            @Override
+            public void run() {
+                TikaServerCli.main(
+                        new String[]{
+                                "-spawnChild", "-p", INTEGRATION_TEST_PORT,
+                                "-taskTimeoutMillis", "10000", "-taskPulseMillis", "500",
+                                "-pingPulseMillis", "500"
+                        });
+            }
+        };
+        serverThread.start();
+        awaitServerStartup();
+        Response response = null;
+        try {
+            response = WebClient
+                    .create(endPoint + META_PATH)
+                    .accept("application/json")
+                    .put(ClassLoader
+                            .getSystemResourceAsStream(TEST_HEAVY_HANG));
+        } catch (Exception e) {
+            //catchable exception when server shuts down.
+        }
+        awaitServerStartup();
+
+        response = WebClient
+                .create(endPoint + META_PATH)
+                .accept("application/json")
+                .put(ClassLoader
+                        .getSystemResourceAsStream(TEST_RECURSIVE_DOC));
+        Reader reader = new InputStreamReader((InputStream) response.getEntity(), UTF_8);
+        List<Metadata> metadataList = JsonMetadataList.fromJson(reader);
+        assertEquals(12, metadataList.size());
+        assertEquals("Microsoft Office Word", metadataList.get(0).get(OfficeOpenXMLExtended.APPLICATION));
+        assertContains("plundered our seas", metadataList.get(6).get("X-TIKA:content"));
+
+        serverThread.interrupt();
+
+
+    }
+
+    private void awaitServerStartup() throws Exception {
+
+        Instant started = Instant.now();
+        long elapsed = Duration.between(started, Instant.now()).toMillis();
+        while (elapsed < 30000) {
+            try {
+                Response response = WebClient
+                        .create(endPoint + "/tika")
+                        .accept("text/plain")
+                        .get();
+                if (response.getStatus() == 200) {
+                    return;
+                }
+            } catch (javax.ws.rs.ProcessingException e) {
+            }
+            Thread.sleep(1000);
+            elapsed = Duration.between(started, Instant.now()).toMillis();
+        }
+
+    }
+}
diff --git a/tika-server/src/test/java/org/apache/tika/server/TikaWelcomeTest.java b/tika-server/src/test/java/org/apache/tika/server/TikaWelcomeTest.java
index 27376ff..a52a79a 100644
--- a/tika-server/src/test/java/org/apache/tika/server/TikaWelcomeTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/TikaWelcomeTest.java
@@ -45,7 +45,7 @@ public class TikaWelcomeTest extends CXFTestBase {
         List<ResourceProvider> rpsCore =
 	    new ArrayList<ResourceProvider>();
 	rpsCore.add(new SingletonResourceProvider(new TikaVersion()));
-	rpsCore.add(new SingletonResourceProvider(new DetectorResource()));
+	rpsCore.add(new SingletonResourceProvider(new DetectorResource(new ServerStatus())));
 	rpsCore.add(new SingletonResourceProvider(new MetadataResource()));
         List<ResourceProvider> all = new ArrayList<ResourceProvider>(rpsCore);
         all.add(new SingletonResourceProvider(new TikaWelcome(rpsCore)));
diff --git a/tika-server/src/test/java/org/apache/tika/server/TranslateResourceTest.java b/tika-server/src/test/java/org/apache/tika/server/TranslateResourceTest.java
index 3cc7be4..c52db65 100644
--- a/tika-server/src/test/java/org/apache/tika/server/TranslateResourceTest.java
+++ b/tika-server/src/test/java/org/apache/tika/server/TranslateResourceTest.java
@@ -47,7 +47,7 @@ public class TranslateResourceTest extends CXFTestBase {
 	protected void setUpResources(JAXRSServerFactoryBean sf) {
 		sf.setResourceClasses(TranslateResource.class);
 		sf.setResourceProvider(TranslateResource.class,
-				new SingletonResourceProvider(new TranslateResource()));
+				new SingletonResourceProvider(new TranslateResource(new ServerStatus())));
 
 	}
 
diff --git a/tika-server/src/test/resources/mock/heavy_hand_100.xml b/tika-server/src/test/resources/mock/heavy_hand_100.xml
new file mode 100644
index 0000000..f1f5b67
--- /dev/null
+++ b/tika-server/src/test/resources/mock/heavy_hand_100.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <metadata action="add" name="author">Nikolai Lobachevsky</metadata>
+    <write element="p">some content</write>
+    <hang millis="30000" heavy="true" pulse_millis="100" />
+</mock>
\ No newline at end of file
diff --git a/tika-server/src/test/resources/mock/heavy_hang_30000.xml b/tika-server/src/test/resources/mock/heavy_hang_30000.xml
new file mode 100644
index 0000000..f1f5b67
--- /dev/null
+++ b/tika-server/src/test/resources/mock/heavy_hang_30000.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <metadata action="add" name="author">Nikolai Lobachevsky</metadata>
+    <write element="p">some content</write>
+    <hang millis="30000" heavy="true" pulse_millis="100" />
+</mock>
\ No newline at end of file
diff --git a/tika-server/src/test/resources/mock/real_oom.xml b/tika-server/src/test/resources/mock/real_oom.xml
new file mode 100644
index 0000000..168751a
--- /dev/null
+++ b/tika-server/src/test/resources/mock/real_oom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <metadata action="add" name="author">Nikolai Lobachevsky</metadata>
+    <oom/>
+</mock>
\ No newline at end of file
diff --git a/tika-server/src/test/resources/mock/system_exit.xml b/tika-server/src/test/resources/mock/system_exit.xml
new file mode 100644
index 0000000..75d1d3b
--- /dev/null
+++ b/tika-server/src/test/resources/mock/system_exit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <metadata action="add" name="author">Nikolai Lobachevsky</metadata>
+    <write element="p">some content</write>
+    <system_exit />
+</mock>
\ No newline at end of file
diff --git a/tika-server/src/test/resources/mock/thread_interrupt.xml b/tika-server/src/test/resources/mock/thread_interrupt.xml
new file mode 100644
index 0000000..3e54512
--- /dev/null
+++ b/tika-server/src/test/resources/mock/thread_interrupt.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<mock>
+    <metadata action="add" name="author">Nikolai Lobachevsky</metadata>
+    <write element="p">some content</write>
+    <thread_interrupt />
+</mock>
\ No newline at end of file