Browse Source

ZOOKEEPER-3620: Allow to override calls to System.exit in server side code

- Introduce a way to override calls to System.exit
- enable DM_EXIT spotbugs rule

see https://issues.apache.org/jira/browse/ZOOKEEPER-3620 for more context.

Author: Enrico Olivelli <enrico.olivelli@diennea.com>
Author: Enrico Olivelli <eolivelli@apache.org>

Reviewers: andor@apache.org

Closes #1147 from eolivelli/fix/ZOOKEEPER-3620-no-systemexit and squashes the following commits:

a234f85f1 [Enrico Olivelli] Fix checkstyle
4c4fec49e [Enrico Olivelli] Fix spotbugs warning
ae339b724 [Enrico Olivelli] Revert changes to VerGen.java
0e5ee07c5 [Enrico Olivelli] Enable DM_EXIT spotbugs rule for the full code base
b05a4bf38 [Enrico Olivelli] ZOOKEEPER-3620 Allow to override calls to System.exit in server side code: - Use a common utility to call System.exit - Override calls to System.exit to a NO-OP function in tests
Enrico Olivelli 5 years ago
parent
commit
8e8905069f
20 changed files with 139 additions and 41 deletions
  1. 0 3
      excludeFindBugsFilter.xml
  2. 4 3
      zookeeper-server/src/main/java/org/apache/zookeeper/Version.java
  3. 3 2
      zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java
  4. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java
  5. 4 3
      zookeeper-server/src/main/java/org/apache/zookeeper/server/LogFormatter.java
  6. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/PurgeTxnLog.java
  7. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/RequestThrottler.java
  8. 5 3
      zookeeper-server/src/main/java/org/apache/zookeeper/server/SnapshotFormatter.java
  9. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java
  10. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java
  11. 7 6
      zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java
  12. 3 2
      zookeeper-server/src/main/java/org/apache/zookeeper/server/persistence/TxnLogToolkit.java
  13. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/FollowerZooKeeperServer.java
  14. 3 3
      zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/Learner.java
  15. 2 1
      zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumCnxManager.java
  16. 7 6
      zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java
  17. 3 2
      zookeeper-server/src/main/java/org/apache/zookeeper/server/util/LogChopper.java
  18. 77 0
      zookeeper-server/src/main/java/org/apache/zookeeper/util/ServiceUtils.java
  19. 3 1
      zookeeper-server/src/main/java/org/apache/zookeeper/version/util/VerGen.java
  20. 6 0
      zookeeper-server/src/test/java/org/apache/zookeeper/ZKTestCase.java

+ 0 - 3
excludeFindBugsFilter.xml

@@ -7,8 +7,5 @@
     <!-- this problem is to be addressed in ZOOKEEPER-3227 -->
     <Bug pattern="DM_DEFAULT_ENCODING"/>
 
-    <!-- not really a problem -->
-    <Bug pattern="DM_EXIT"/>
-
 </FindBugsFilter>
 

+ 4 - 3
zookeeper-server/src/main/java/org/apache/zookeeper/Version.java

@@ -20,6 +20,7 @@ package org.apache.zookeeper;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.apache.zookeeper.server.ExitCode;
+import org.apache.zookeeper.util.ServiceUtils;
 
 public class Version implements org.apache.zookeeper.version.Info {
 
@@ -48,7 +49,7 @@ public class Version implements org.apache.zookeeper.version.Info {
         System.out.print("Usage:\tjava -cp ... org.apache.zookeeper.Version "
                          + "[--full | --short | --revision],\n\tPrints --full version "
                          + "info if no arg specified.");
-        System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+        ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
     }
 
     /**
@@ -68,7 +69,7 @@ public class Version implements org.apache.zookeeper.version.Info {
         }
         if (args.length == 0 || (args.length == 1 && args[0].equals("--full"))) {
             System.out.println(getFullVersion());
-            System.exit(ExitCode.EXECUTION_FINISHED.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.EXECUTION_FINISHED.getValue());
         }
         if (args[0].equals("--short")) {
             System.out.println(getVersion());
@@ -77,7 +78,7 @@ public class Version implements org.apache.zookeeper.version.Info {
         } else {
             printUsage();
         }
-        System.exit(ExitCode.EXECUTION_FINISHED.getValue());
+        ServiceUtils.requestSystemExit(ExitCode.EXECUTION_FINISHED.getValue());
     }
 
 }

+ 3 - 2
zookeeper-server/src/main/java/org/apache/zookeeper/ZooKeeperMain.java

@@ -66,6 +66,7 @@ import org.apache.zookeeper.cli.SyncCommand;
 import org.apache.zookeeper.cli.VersionCommand;
 import org.apache.zookeeper.client.ZKClientConfig;
 import org.apache.zookeeper.server.ExitCode;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -353,7 +354,7 @@ public class ZooKeeperMain {
             // Command line args non-null.  Run what was passed.
             processCmd(cl);
         }
-        System.exit(exitCode);
+        ServiceUtils.requestSystemExit(exitCode);
     }
 
     public void executeLine(String line) throws CliException, InterruptedException, IOException {
@@ -396,7 +397,7 @@ public class ZooKeeperMain {
 
         if (cmd.equals("quit")) {
             zk.close();
-            System.exit(exitCode);
+            ServiceUtils.requestSystemExit(exitCode);
         } else if (cmd.equals("redo") && args.length >= 2) {
             Integer i = Integer.decode(args[1]);
             if (commandCount <= i || i < 0) { // don't allow redoing this redo

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/DataTree.java

@@ -78,6 +78,7 @@ import org.apache.zookeeper.txn.SetACLTxn;
 import org.apache.zookeeper.txn.SetDataTxn;
 import org.apache.zookeeper.txn.Txn;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -301,7 +302,7 @@ public class DataTree {
             childWatches = WatchManagerFactory.createWatchManager();
         } catch (Exception e) {
             LOG.error("Unexpected exception when creating WatchManager, exiting abnormally", e);
-            System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
         }
     }
 

+ 4 - 3
zookeeper-server/src/main/java/org/apache/zookeeper/server/LogFormatter.java

@@ -33,6 +33,7 @@ import org.apache.zookeeper.server.persistence.FileHeader;
 import org.apache.zookeeper.server.persistence.FileTxnLog;
 import org.apache.zookeeper.server.util.SerializeUtils;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,13 +52,13 @@ public class LogFormatter {
     public static void main(String[] args) throws Exception {
         if (args.length != 1) {
             System.err.println("USAGE: LogFormatter log_file");
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
 
         String error = ZKUtil.validateFileInput(args[0]);
         if (null != error) {
             System.err.println(error);
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
 
         FileInputStream fis = new FileInputStream(args[0]);
@@ -67,7 +68,7 @@ public class LogFormatter {
 
         if (fhdr.getMagic() != FileTxnLog.TXNLOG_MAGIC) {
             System.err.println("Invalid magic number for " + args[0]);
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
         System.out.println("ZooKeeper Transactional Log File with dbid "
                            + fhdr.getDbid()

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/PurgeTxnLog.java

@@ -30,6 +30,7 @@ import java.util.Set;
 import org.apache.yetus.audience.InterfaceAudience;
 import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
 import org.apache.zookeeper.server.persistence.Util;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -230,7 +231,7 @@ public class PurgeTxnLog {
 
     private static void printUsageThenExit() {
         printUsage();
-        System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+        ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
     }
 
 }

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/RequestThrottler.java

@@ -20,6 +20,7 @@ package org.apache.zookeeper.server;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.util.concurrent.LinkedBlockingQueue;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -257,7 +258,7 @@ public class RequestThrottler extends ZooKeeperCriticalThread {
         } catch (InterruptedException e) {
             LOG.warn("Interrupted while waiting for {} to finish", this);
             //TODO apply ZOOKEEPER-575 and remove this line.
-            System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
         }
     }
 

+ 5 - 3
zookeeper-server/src/main/java/org/apache/zookeeper/server/SnapshotFormatter.java

@@ -35,6 +35,7 @@ import org.apache.zookeeper.data.StatPersisted;
 import org.apache.zookeeper.server.persistence.FileSnap;
 import org.apache.zookeeper.server.persistence.SnapStream;
 import org.apache.zookeeper.server.persistence.Util;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.json.simple.JSONValue;
 
 /**
@@ -72,18 +73,19 @@ public class SnapshotFormatter {
             System.err.println("USAGE: SnapshotFormatter [-d|-json] snapshot_file");
             System.err.println("       -d dump the data for each znode");
             System.err.println("       -json dump znode info in json format");
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
+            return;
         }
 
         String error = ZKUtil.validateFileInput(snapshotFile);
         if (null != error) {
             System.err.println(error);
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
 
         if (dumpData && dumpJson) {
             System.err.println("Cannot specify both data dump (-d) and json mode (-json) in same call");
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
 
         new SnapshotFormatter().run(snapshotFile, dumpData, dumpJson);

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/TraceFormatter.java

@@ -25,6 +25,7 @@ import java.nio.channels.FileChannel;
 import java.text.DateFormat;
 import java.util.Date;
 import org.apache.zookeeper.ZooDefs.OpCode;
+import org.apache.zookeeper.util.ServiceUtils;
 
 public class TraceFormatter {
 
@@ -35,7 +36,7 @@ public class TraceFormatter {
     public static void main(String[] args) throws IOException {
         if (args.length != 1) {
             System.err.println("USAGE: TraceFormatter trace_file");
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         }
         FileChannel fc = new FileInputStream(args[0]).getChannel();
         while (true) {

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServer.java

@@ -80,6 +80,7 @@ import org.apache.zookeeper.server.util.OSMXBean;
 import org.apache.zookeeper.server.util.RequestPathMetricsCollector;
 import org.apache.zookeeper.txn.CreateSessionTxn;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -511,7 +512,7 @@ public class ZooKeeperServer implements SessionExpirer, ServerStats.Provider {
             LOG.error("Severe unrecoverable error, exiting", e);
             // This is a severe error that we cannot recover from,
             // so we need to exit
-            System.exit(ExitCode.TXNLOG_ERROR_TAKING_SNAPSHOT.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.TXNLOG_ERROR_TAKING_SNAPSHOT.getValue());
         }
         long elapsed = Time.currentElapsedTime() - start;
         LOG.info("Snapshot taken in {} ms", elapsed);

+ 7 - 6
zookeeper-server/src/main/java/org/apache/zookeeper/server/ZooKeeperServerMain.java

@@ -35,6 +35,7 @@ import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
 import org.apache.zookeeper.server.persistence.FileTxnSnapLog.DatadirException;
 import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
 import org.apache.zookeeper.server.util.JvmPauseMonitor;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -69,29 +70,29 @@ public class ZooKeeperServerMain {
             LOG.info(USAGE);
             System.err.println(USAGE);
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         } catch (ConfigException e) {
             LOG.error("Invalid config, exiting abnormally", e);
             System.err.println("Invalid config, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         } catch (DatadirException e) {
             LOG.error("Unable to access datadir, exiting abnormally", e);
             System.err.println("Unable to access datadir, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.UNABLE_TO_ACCESS_DATADIR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNABLE_TO_ACCESS_DATADIR.getValue());
         } catch (AdminServerException e) {
             LOG.error("Unable to start AdminServer, exiting abnormally", e);
             System.err.println("Unable to start AdminServer, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.ERROR_STARTING_ADMIN_SERVER.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.ERROR_STARTING_ADMIN_SERVER.getValue());
         } catch (Exception e) {
             LOG.error("Unexpected exception, exiting abnormally", e);
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
         }
         LOG.info("Exiting normally");
-        System.exit(ExitCode.EXECUTION_FINISHED.getValue());
+        ServiceUtils.requestSystemExit(ExitCode.EXECUTION_FINISHED.getValue());
     }
 
     protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {

+ 3 - 2
zookeeper-server/src/main/java/org/apache/zookeeper/server/persistence/TxnLogToolkit.java

@@ -58,6 +58,7 @@ import org.apache.zookeeper.txn.MultiTxn;
 import org.apache.zookeeper.txn.SetDataTxn;
 import org.apache.zookeeper.txn.Txn;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 
 public class TxnLogToolkit implements Closeable {
 
@@ -126,7 +127,7 @@ public class TxnLogToolkit implements Closeable {
             printHelpAndExit(e.getExitCode(), e.getOptions());
         } catch (TxnLogToolkitException e) {
             System.err.println(e.getMessage());
-            System.exit(e.getExitCode());
+            ServiceUtils.requestSystemExit(e.getExitCode());
         }
     }
 
@@ -424,7 +425,7 @@ public class TxnLogToolkit implements Closeable {
     private static void printHelpAndExit(int exitCode, Options options) {
         HelpFormatter help = new HelpFormatter();
         help.printHelp(120, "TxnLogToolkit [-dhrvc] <txn_log_file_name> (-z <zxid>)", "", options, "");
-        System.exit(exitCode);
+        ServiceUtils.requestSystemExit(exitCode);
     }
 
     private void printStat() {

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/FollowerZooKeeperServer.java

@@ -34,6 +34,7 @@ import org.apache.zookeeper.server.SyncRequestProcessor;
 import org.apache.zookeeper.server.ZKDatabase;
 import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -100,7 +101,7 @@ public class FollowerZooKeeperServer extends LearnerZooKeeperServer {
         if (firstElementZxid != zxid) {
             LOG.error("Committing zxid 0x" + Long.toHexString(zxid)
                       + " but next pending txn 0x" + Long.toHexString(firstElementZxid));
-            System.exit(ExitCode.UNMATCHED_TXN_COMMIT.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNMATCHED_TXN_COMMIT.getValue());
         }
         Request request = pendingTxns.remove();
         request.logLatency(ServerMetrics.getMetrics().COMMIT_PROPAGATION_LATENCY);

+ 3 - 3
zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/Learner.java

@@ -52,6 +52,7 @@ import org.apache.zookeeper.server.util.SerializeUtils;
 import org.apache.zookeeper.server.util.ZxidUtils;
 import org.apache.zookeeper.txn.SetDataTxn;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -455,14 +456,13 @@ public class Learner {
                 if (!truncated) {
                     // not able to truncate the log
                     LOG.error("Not able to truncate the log 0x{}", Long.toHexString(qp.getZxid()));
-                    System.exit(ExitCode.QUORUM_PACKET_ERROR.getValue());
+                    ServiceUtils.requestSystemExit(ExitCode.QUORUM_PACKET_ERROR.getValue());
                 }
                 zk.getZKDatabase().setlastProcessedZxid(qp.getZxid());
 
             } else {
                 LOG.error("Got unexpected packet from leader: {}, exiting ... ", LearnerHandler.packetToString(qp));
-                System.exit(ExitCode.QUORUM_PACKET_ERROR.getValue());
-
+                ServiceUtils.requestSystemExit(ExitCode.QUORUM_PACKET_ERROR.getValue());
             }
             zk.getZKDatabase().initConfigInZKDatabase(self.getQuorumVerifier());
             zk.createSessionTracker();

+ 2 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumCnxManager.java

@@ -55,6 +55,7 @@ import org.apache.zookeeper.server.quorum.auth.QuorumAuthServer;
 import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier;
 import org.apache.zookeeper.server.util.ConfigUtils;
 import org.apache.zookeeper.util.CircularBlockingQueue;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -825,7 +826,7 @@ public class QuorumCnxManager {
         private static final int DEFAULT_PORT_BIND_MAX_RETRY = 3;
 
         private final int portBindMaxRetry;
-        private Runnable socketBindErrorHandler = () -> System.exit(ExitCode.UNABLE_TO_BIND_QUORUM_PORT.getValue());
+        private Runnable socketBindErrorHandler = () -> ServiceUtils.requestSystemExit(ExitCode.UNABLE_TO_BIND_QUORUM_PORT.getValue());
         volatile ServerSocket ss = null;
 
         public Listener() {

+ 7 - 6
zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/QuorumPeerMain.java

@@ -38,6 +38,7 @@ import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
 import org.apache.zookeeper.server.persistence.FileTxnSnapLog.DatadirException;
 import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
 import org.apache.zookeeper.server.util.JvmPauseMonitor;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -92,29 +93,29 @@ public class QuorumPeerMain {
             LOG.info(USAGE);
             System.err.println(USAGE);
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         } catch (ConfigException e) {
             LOG.error("Invalid config, exiting abnormally", e);
             System.err.println("Invalid config, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.INVALID_INVOCATION.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.INVALID_INVOCATION.getValue());
         } catch (DatadirException e) {
             LOG.error("Unable to access datadir, exiting abnormally", e);
             System.err.println("Unable to access datadir, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.UNABLE_TO_ACCESS_DATADIR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNABLE_TO_ACCESS_DATADIR.getValue());
         } catch (AdminServerException e) {
             LOG.error("Unable to start AdminServer, exiting abnormally", e);
             System.err.println("Unable to start AdminServer, exiting abnormally");
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.ERROR_STARTING_ADMIN_SERVER.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.ERROR_STARTING_ADMIN_SERVER.getValue());
         } catch (Exception e) {
             LOG.error("Unexpected exception, exiting abnormally", e);
             ZKAuditProvider.addServerStartFailureAuditLog();
-            System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
+            ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
         }
         LOG.info("Exiting normally");
-        System.exit(ExitCode.EXECUTION_FINISHED.getValue());
+        ServiceUtils.requestSystemExit(ExitCode.EXECUTION_FINISHED.getValue());
     }
 
     protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {

+ 3 - 2
zookeeper-server/src/main/java/org/apache/zookeeper/server/util/LogChopper.java

@@ -36,6 +36,7 @@ import org.apache.zookeeper.server.ExitCode;
 import org.apache.zookeeper.server.persistence.FileHeader;
 import org.apache.zookeeper.server.persistence.FileTxnLog;
 import org.apache.zookeeper.txn.TxnHeader;
+import org.apache.zookeeper.util.ServiceUtils;
 
 /**
  * this class will chop the log at the specified zxid
@@ -49,7 +50,7 @@ public class LogChopper {
             System.out.println("Usage: LogChopper zxid_to_chop_to txn_log_to_chop chopped_filename");
             System.out.println("    this program will read the txn_log_to_chop file and copy all the transactions");
             System.out.println("    from it up to (and including) the given zxid into chopped_filename.");
-            System.exit(rc.getValue());
+            ServiceUtils.requestSystemExit(rc.getValue());
         }
         String txnLog = args[1];
         String choppedLog = args[2];
@@ -63,7 +64,7 @@ public class LogChopper {
         } catch (Exception e) {
             System.out.println("Got exception: " + e.getMessage());
         }
-        System.exit(rc.getValue());
+        ServiceUtils.requestSystemExit(rc.getValue());
     }
 
     public static boolean chop(InputStream is, OutputStream os, long zxid) throws IOException {

+ 77 - 0
zookeeper-server/src/main/java/org/apache/zookeeper/util/ServiceUtils.java

@@ -0,0 +1,77 @@
+/*
+ * 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.zookeeper.util;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.Objects;
+import java.util.function.Consumer;
+import org.apache.zookeeper.server.ExitCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utilities for service management.
+ */
+public abstract class ServiceUtils {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ServiceUtils.class);
+
+    private ServiceUtils() {
+    }
+
+    /**
+     * Default strategy for shutting down the JVM.
+     */
+    @SuppressFBWarnings("DM_EXIT")
+    public static final Consumer<Integer> SYSTEM_EXIT = (code) -> {
+        LOG.error("Exiting JVM with code {}", code);
+        System.exit(code);
+    };
+
+    /**
+     * No-op strategy, useful for tests.
+     */
+    public static final Consumer<Integer> LOG_ONLY = (code) -> {
+        LOG.error("Fatal error, JVM should exit with code {}. "
+                + "Actually System.exit is disabled", code);
+    };
+
+    private static Consumer<Integer> systemExitProcedure = SYSTEM_EXIT;
+
+    /**
+     * Override system callback. Useful for preventing the JVM to exit in tests
+     * or in applications that are running an in-process ZooKeeper server.
+     *
+     * @param systemExitProcedure
+     */
+    public static void setSystemExitProcedure(Consumer<Integer> systemExitProcedure) {
+        Objects.requireNonNull(systemExitProcedure);
+        ServiceUtils.systemExitProcedure = systemExitProcedure;
+    }
+
+    /**
+     * Force shutdown of the JVM using System.exit.
+     *
+     * @param code the exit code
+     * @see ExitCode
+     */
+    public static void requestSystemExit(int code) {
+        systemExitProcedure.accept(code);
+    }
+
+}

+ 3 - 1
zookeeper-server/src/main/java/org/apache/zookeeper/version/util/VerGen.java

@@ -18,6 +18,7 @@
 
 package org.apache.zookeeper.version.util;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
@@ -25,6 +26,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.zookeeper.server.ExitCode;
 
+@SuppressFBWarnings("DM_EXIT")
 public class VerGen {
 
     private static final String PACKAGE_NAME = "org.apache.zookeeper.version";
@@ -123,7 +125,7 @@ public class VerGen {
             w.write("}\n");
         } catch (IOException e) {
             System.out.println("Unable to generate version.VersionInfoMain file: " + e.getMessage());
-            System.exit(1);
+            System.exit(ExitCode.UNEXPECTED_ERROR.getValue());
         }
     }
 

+ 6 - 0
zookeeper-server/src/test/java/org/apache/zookeeper/ZKTestCase.java

@@ -22,6 +22,7 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import java.io.File;
 import java.time.LocalDateTime;
+import org.apache.zookeeper.util.ServiceUtils;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.rules.TestWatcher;
@@ -42,6 +43,11 @@ public class ZKTestCase {
     protected static final File testBaseDir = new File(System.getProperty("build.test.dir", "build"));
     private static final Logger LOG = LoggerFactory.getLogger(ZKTestCase.class);
 
+    static {
+        // Disable System.exit in tests.
+        ServiceUtils.setSystemExitProcedure(ServiceUtils.LOG_ONLY);
+    }
+
     private String testName;
 
     protected String getTestName() {