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

DomGarguilo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/main by this push:
     new a5979677bc Properly close thread pools (#6480)
a5979677bc is described below

commit a5979677bc3fb9e6eff41623465e4e3bd19cb4c1
Author: Dom G. <[email protected]>
AuthorDate: Mon Jul 20 10:24:35 2026 -0400

    Properly close thread pools (#6480)
    
    * close thread pools that were not being closed and also move existing/new 
thread pool shutdown calls into a finally block
---
 .../core/clientImpl/ClientTabletCacheImplTest.java | 190 +++++-----
 .../apache/accumulo/core/crypto/CryptoTest.java    |  58 +--
 .../core/file/rfile/bcfile/CompressionTest.java    | 161 ++++-----
 .../core/util/CompletableFutureUtilTest.java       |  72 ++--
 .../server/tablets/UniqueNameAllocatorTest.java    |  56 +--
 .../compaction/queue/CompactionJobQueuesTest.java  |  86 ++---
 .../accumulo/tserver/AssignmentWatcherTest.java    |  29 +-
 .../accumulo/test/CountNameNodeOpsBulkIT.java      | 118 +++---
 .../java/org/apache/accumulo/test/LargeReadIT.java |  87 ++---
 .../accumulo/test/MultipleManagerFateIT.java       |   4 +-
 .../org/apache/accumulo/test/ScanServerIT.java     |   5 +-
 .../accumulo/test/ScanServerMaxLatencyIT.java      |   6 +-
 .../accumulo/test/TableConfigurationUpdateIT.java  |  13 +-
 .../apache/accumulo/test/TableOperationsIT.java    |  94 ++---
 .../accumulo/test/UniqueNameAllocatorIT.java       |  95 ++---
 .../org/apache/accumulo/test/ZombieScanIT.java     |   8 +-
 .../test/conf/PropStoreConfigIT_SimpleSuite.java   | 277 +++++++-------
 .../accumulo/test/fate/meta/ZooMutatorIT.java      |   8 +-
 .../test/functional/AddSplitIT_SimpleSuite.java    |  62 ++--
 .../apache/accumulo/test/functional/BulkNewIT.java | 112 +++---
 .../accumulo/test/functional/CompactionIT.java     | 122 ++++---
 .../test/functional/ConcurrentDeleteTableIT.java   |  23 +-
 .../ConcurrentTableNameOperationsIT.java           | 400 +++++++++++----------
 .../accumulo/test/functional/FateStarvationIT.java |  54 +--
 .../test/functional/FunctionalTestUtils.java       |  41 ++-
 .../test/functional/ManagerAssignmentIT.java       |  72 ++--
 .../apache/accumulo/test/functional/ScanIdIT.java  |  14 +-
 .../apache/accumulo/test/functional/SplitIT.java   | 109 +++---
 .../accumulo/test/functional/WriteLotsIT.java      |  31 +-
 29 files changed, 1252 insertions(+), 1155 deletions(-)

diff --git 
a/core/src/test/java/org/apache/accumulo/core/clientImpl/ClientTabletCacheImplTest.java
 
b/core/src/test/java/org/apache/accumulo/core/clientImpl/ClientTabletCacheImplTest.java
index a2cf16c176..9b1cb1adec 100644
--- 
a/core/src/test/java/org/apache/accumulo/core/clientImpl/ClientTabletCacheImplTest.java
+++ 
b/core/src/test/java/org/apache/accumulo/core/clientImpl/ClientTabletCacheImplTest.java
@@ -1984,69 +1984,73 @@ public class ClientTabletCacheImplTest {
     setLocation(tservers, "tserver3", mte2, ke3, "tserver9");
 
     var executor = Executors.newCachedThreadPool();
-    final int lookupCount = 128;
-    final int roundCount = 8;
-    final int numTasks = roundCount * lookupCount;
-
-    List<Future<CachedTablet>> futures = new ArrayList<>(numTasks);
-    CountDownLatch startLatch = new CountDownLatch(32); // start a portion of 
threads at once
-    assertTrue(numTasks >= startLatch.getCount(),
-        "Not enough tasks to satisfy latch count - deadlock risk");
-
-    // multiple rounds to increase the chance of contention
-    for (int round = 0; round < roundCount; round++) {
+    try {
+      final int lookupCount = 128;
+      final int roundCount = 8;
+      final int numTasks = roundCount * lookupCount;
+
+      List<Future<CachedTablet>> futures = new ArrayList<>(numTasks);
+      CountDownLatch startLatch = new CountDownLatch(32); // start a portion 
of threads at once
+      assertTrue(numTasks >= startLatch.getCount(),
+          "Not enough tasks to satisfy latch count - deadlock risk");
+
+      // multiple rounds to increase the chance of contention
+      for (int round = 0; round < roundCount; round++) {
+
+        // start a bunch of threads all trying to lookup data in the cache
+        // should see exactly 2 threads doing metadata lookups at a time
+        List<String> rowsToLookup = new ArrayList<>(lookupCount);
+
+        for (int i = 0; i < lookupCount; i++) {
+          String lookup = (char) ('a' + (i % 26)) + "";
+          rowsToLookup.add(lookup);
+        }
 
-      // start a bunch of threads all trying to lookup data in the cache
-      // should see exactly 2 threads doing metadata lookups at a time
-      List<String> rowsToLookup = new ArrayList<>(lookupCount);
+        Collections.shuffle(rowsToLookup, RANDOM.get());
 
-      for (int i = 0; i < lookupCount; i++) {
-        String lookup = (char) ('a' + (i % 26)) + "";
-        rowsToLookup.add(lookup);
+        for (var lookup : rowsToLookup) {
+          var future = executor.submit(() -> {
+            startLatch.countDown();
+            startLatch.await();
+            if (RANDOM.get().nextInt(10) < 3) {
+              Thread.yield();
+            }
+            var loc = metaCache.findTablet(context, new Text(lookup), false, 
LocationNeed.REQUIRED);
+            if (lookup.compareTo("m") <= 0) {
+              assertEquals("tserver7", loc.getTserverLocation().orElseThrow());
+            } else if (lookup.compareTo("q") <= 0) {
+              assertEquals("tserver8", loc.getTserverLocation().orElseThrow());
+            } else {
+              assertEquals("tserver9", loc.getTserverLocation().orElseThrow());
+            }
+            return loc;
+          });
+          futures.add(future);
+        }
       }
+      assertEquals(numTasks, futures.size());
 
-      Collections.shuffle(rowsToLookup, RANDOM.get());
-
-      for (var lookup : rowsToLookup) {
-        var future = executor.submit(() -> {
-          startLatch.countDown();
-          startLatch.await();
-          if (RANDOM.get().nextInt(10) < 3) {
-            Thread.yield();
-          }
-          var loc = metaCache.findTablet(context, new Text(lookup), false, 
LocationNeed.REQUIRED);
-          if (lookup.compareTo("m") <= 0) {
-            assertEquals("tserver7", loc.getTserverLocation().orElseThrow());
-          } else if (lookup.compareTo("q") <= 0) {
-            assertEquals("tserver8", loc.getTserverLocation().orElseThrow());
-          } else {
-            assertEquals("tserver9", loc.getTserverLocation().orElseThrow());
-          }
-          return loc;
-        });
-        futures.add(future);
+      for (var future : futures) {
+        assertNotNull(future.get());
       }
-    }
-    assertEquals(numTasks, futures.size());
 
-    for (var future : futures) {
-      assertNotNull(future.get());
+      assertTrue(sawTwoActive.get(), "Expected to see exactly two lookups.");
+      // The second metadata tablet (mte2) contains two user tablets (ke2 and 
ke3). Depending on
+      // which of these two user tablets is looked up in the metadata table 
first will see a total
+      // of 2 or 3 lookups. If the location of ke2 is looked up first then it 
will get the locations
+      // of ke2 and ke3 from mte2 and put them in the cache. If the location 
of ke3 is looked up
+      // first then it will only get the location of ke3 from mte2 and not ke2.
+      assertTrue(lookups.size() == 2 || lookups.size() == 3,
+          "Expected 2 or 3 lookups, got " + lookups.size() + " : " + lookups);
+      assertEquals(1,
+          lookups.stream().filter(metadataExtent -> 
metadataExtent.equals(mte1)).count(),
+          lookups::toString);
+      var mte2Lookups =
+          lookups.stream().filter(metadataExtent -> 
metadataExtent.equals(mte2)).count();
+      assertTrue(mte2Lookups == 1 || mte2Lookups == 2, lookups::toString);
+    } finally {
+      executor.shutdown();
     }
-
-    assertTrue(sawTwoActive.get(), "Expected to see exactly two lookups.");
-    // The second metadata tablet (mte2) contains two user tablets (ke2 and 
ke3). Depending on which
-    // of these two user tablets is looked up in the metadata table first will 
see a total of 2 or 3
-    // lookups. If the location of ke2 is looked up first then it will get the 
locations of ke2 and
-    // ke3 from mte2 and put them in the cache. If the location of ke3 is 
looked up first then it
-    // will only get the location of ke3 from mte2 and not ke2.
-    assertTrue(lookups.size() == 2 || lookups.size() == 3,
-        "Expected 2 or 3 lookups, got " + lookups.size() + " : " + lookups);
-    assertEquals(1, lookups.stream().filter(metadataExtent -> 
metadataExtent.equals(mte1)).count(),
-        lookups::toString);
-    var mte2Lookups =
-        lookups.stream().filter(metadataExtent -> 
metadataExtent.equals(mte2)).count();
-    assertTrue(mte2Lookups == 1 || mte2Lookups == 2, lookups::toString);
-    executor.shutdown();
   }
 
   @Test
@@ -2097,51 +2101,55 @@ public class ClientTabletCacheImplTest {
     metaCache.invalidateCache(ke2);
 
     var executor = Executors.newCachedThreadPool();
+    try {
 
-    // acquire this lock to simulate a metadata table lookup blocking
-    lookupLock.lock();
+      // acquire this lock to simulate a metadata table lookup blocking
+      lookupLock.lock();
 
-    // verify test assumption
-    assertEquals(0, lookupLock.getQueueLength());
+      // verify test assumption
+      assertEquals(0, lookupLock.getQueueLength());
 
-    // start a background task that will read from the metadata table
-    var lookupFuture = executor
-        .submit(() -> metaCache.findTablet(context, new Text("h"), false, 
LocationNeed.REQUIRED)
-            .getTserverLocation().orElseThrow());
+      // start a background task that will read from the metadata table
+      var lookupFuture = executor
+          .submit(() -> metaCache.findTablet(context, new Text("h"), false, 
LocationNeed.REQUIRED)
+              .getTserverLocation().orElseThrow());
 
-    // wait for the background thread to get stuck waiting on the lock
-    while (lookupLock.getQueueLength() == 0) {
-      Thread.sleep(5);
-    }
+      // wait for the background thread to get stuck waiting on the lock
+      while (lookupLock.getQueueLength() == 0) {
+        Thread.sleep(5);
+      }
 
-    // The lookup task should be blocked trying to get location from the 
metadata table
-    assertFalse(lookupFuture.isDone());
-    assertEquals(1, lookupLock.getQueueLength());
+      // The lookup task should be blocked trying to get location from the 
metadata table
+      assertFalse(lookupFuture.isDone());
+      assertEquals(1, lookupLock.getQueueLength());
 
-    // should be able to get the tablet locations that are still in the cache 
w/o blocking
-    assertEquals("tserver3",
-        metaCache.findTablet(context, new Text("a"), false, 
LocationNeed.REQUIRED)
-            .getTserverLocation().orElseThrow());
-    assertEquals("tserver5",
-        metaCache.findTablet(context, new Text("n"), false, 
LocationNeed.REQUIRED)
-            .getTserverLocation().orElseThrow());
-    assertEquals("tserver6",
-        metaCache.findTablet(context, new Text("s"), false, 
LocationNeed.REQUIRED)
-            .getTserverLocation().orElseThrow());
+      // should be able to get the tablet locations that are still in the 
cache w/o blocking
+      assertEquals("tserver3",
+          metaCache.findTablet(context, new Text("a"), false, 
LocationNeed.REQUIRED)
+              .getTserverLocation().orElseThrow());
+      assertEquals("tserver5",
+          metaCache.findTablet(context, new Text("n"), false, 
LocationNeed.REQUIRED)
+              .getTserverLocation().orElseThrow());
+      assertEquals("tserver6",
+          metaCache.findTablet(context, new Text("s"), false, 
LocationNeed.REQUIRED)
+              .getTserverLocation().orElseThrow());
 
-    // The lookup task should still be blocked
-    assertFalse(lookupFuture.isDone());
-    assertEquals(1, lookupLock.getQueueLength());
+      // The lookup task should still be blocked
+      assertFalse(lookupFuture.isDone());
+      assertEquals(1, lookupLock.getQueueLength());
 
-    // allow the metadata lookup running in background thread to proceed.
-    lookupLock.unlock();
+      // allow the metadata lookup running in background thread to proceed.
+      lookupLock.unlock();
 
-    // The future should be able to run and complete
-    assertEquals("tserver4", lookupFuture.get());
+      // The future should be able to run and complete
+      assertEquals("tserver4", lookupFuture.get());
 
-    // verify test assumptions
-    assertTrue(lookupFuture.isDone());
-    assertEquals(0, lookupLock.getQueueLength());
+      // verify test assumptions
+      assertTrue(lookupFuture.isDone());
+      assertEquals(0, lookupLock.getQueueLength());
+    } finally {
+      executor.shutdown();
+    }
   }
 
   @Test
diff --git a/core/src/test/java/org/apache/accumulo/core/crypto/CryptoTest.java 
b/core/src/test/java/org/apache/accumulo/core/crypto/CryptoTest.java
index b7833bedeb..668df2a26b 100644
--- a/core/src/test/java/org/apache/accumulo/core/crypto/CryptoTest.java
+++ b/core/src/test/java/org/apache/accumulo/core/crypto/CryptoTest.java
@@ -560,34 +560,38 @@ public class CryptoTest {
     byte[] cipherText = out.toByteArray();
 
     var executor = Executors.newCachedThreadPool();
+    try {
+
+      final int numTasks = 32;
+      List<Future<Boolean>> verifyFutures = new ArrayList<>(numTasks);
+      CountDownLatch startLatch = new CountDownLatch(numTasks);
+      assertTrue(numTasks >= startLatch.getCount(),
+          "Not enough tasks to satisfy latch count - deadlock risk");
+
+      FileDecrypter decrypter = cs.getFileDecrypter(new 
CryptoEnvironmentImpl(scope, null, params));
+
+      // verify that each input stream returned by decrypter.decryptStream() 
is independent when
+      // used by multiple threads
+      for (int i = 0; i < numTasks; i++) {
+        var future = executor.submit(() -> {
+          startLatch.countDown();
+          startLatch.await();
+          try (ByteArrayInputStream in = new ByteArrayInputStream(cipherText);
+              DataInputStream decrypted = new 
DataInputStream(decrypter.decryptStream(in))) {
+            byte[] dataRead = new byte[plainText.length];
+            decrypted.readFully(dataRead);
+            return Arrays.equals(plainText, dataRead);
+          }
+        });
+        verifyFutures.add(future);
+      }
+      assertEquals(numTasks, verifyFutures.size());
 
-    final int numTasks = 32;
-    List<Future<Boolean>> verifyFutures = new ArrayList<>(numTasks);
-    CountDownLatch startLatch = new CountDownLatch(numTasks);
-    assertTrue(numTasks >= startLatch.getCount(),
-        "Not enough tasks to satisfy latch count - deadlock risk");
-
-    FileDecrypter decrypter = cs.getFileDecrypter(new 
CryptoEnvironmentImpl(scope, null, params));
-
-    // verify that each input stream returned by decrypter.decryptStream() is 
independent when used
-    // by multiple threads
-    for (int i = 0; i < numTasks; i++) {
-      var future = executor.submit(() -> {
-        startLatch.countDown();
-        startLatch.await();
-        try (ByteArrayInputStream in = new ByteArrayInputStream(cipherText);
-            DataInputStream decrypted = new 
DataInputStream(decrypter.decryptStream(in))) {
-          byte[] dataRead = new byte[plainText.length];
-          decrypted.readFully(dataRead);
-          return Arrays.equals(plainText, dataRead);
-        }
-      });
-      verifyFutures.add(future);
-    }
-    assertEquals(numTasks, verifyFutures.size());
-
-    for (var future : verifyFutures) {
-      assertTrue(future.get());
+      for (var future : verifyFutures) {
+        assertTrue(future.get());
+      }
+    } finally {
+      executor.shutdownNow();
     }
   }
 
diff --git 
a/core/src/test/java/org/apache/accumulo/core/file/rfile/bcfile/CompressionTest.java
 
b/core/src/test/java/org/apache/accumulo/core/file/rfile/bcfile/CompressionTest.java
index ccbb1f0974..c4c2f9ca30 100644
--- 
a/core/src/test/java/org/apache/accumulo/core/file/rfile/bcfile/CompressionTest.java
+++ 
b/core/src/test/java/org/apache/accumulo/core/file/rfile/bcfile/CompressionTest.java
@@ -18,7 +18,6 @@
  */
 package org.apache.accumulo.core.file.rfile.bcfile;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -198,33 +197,31 @@ public class CompressionTest {
 
         final int numTasks = 32;
         ExecutorService service = Executors.newFixedThreadPool(numTasks);
-
-        ArrayList<Future<Boolean>> results = new ArrayList<>(numTasks);
-        CountDownLatch startLatch = new CountDownLatch(numTasks);
-        assertTrue(numTasks >= startLatch.getCount(),
-            "Not enough tasks/threads to satisfy latch count - deadlock risk");
-
-        for (int i = 0; i < numTasks; i++) {
-          results.add(service.submit(() -> {
-            startLatch.countDown();
-            startLatch.await();
-            assertNotNull(al.getCodec(), al + " should not be null");
-            return true;
-          }));
-        }
-        assertEquals(numTasks, results.size());
-
-        service.shutdown();
-
-        assertNotNull(codec, al + " should not be null");
-
-        while (!service.awaitTermination(1, SECONDS)) {
-          // wait
-        }
-
-        for (Future<Boolean> result : results) {
-          assertTrue(result.get(),
-              al + " resulted in a failed call to getcodec within the thread 
pool");
+        try {
+
+          ArrayList<Future<Boolean>> results = new ArrayList<>(numTasks);
+          CountDownLatch startLatch = new CountDownLatch(numTasks);
+          assertTrue(numTasks >= startLatch.getCount(),
+              "Not enough tasks/threads to satisfy latch count - deadlock 
risk");
+
+          for (int i = 0; i < numTasks; i++) {
+            results.add(service.submit(() -> {
+              startLatch.countDown();
+              startLatch.await();
+              assertNotNull(al.getCodec(), al + " should not be null");
+              return true;
+            }));
+          }
+          assertEquals(numTasks, results.size());
+
+          assertNotNull(codec, al + " should not be null");
+
+          for (Future<Boolean> result : results) {
+            assertTrue(result.get(),
+                al + " resulted in a failed call to getcodec within the thread 
pool");
+          }
+        } finally {
+          service.shutdown();
         }
         somethingGotTested = true;
       }
@@ -247,32 +244,30 @@ public class CompressionTest {
 
         final int numTasks = 32;
         ExecutorService service = Executors.newFixedThreadPool(numTasks);
-
-        ArrayList<Future<Boolean>> results = new ArrayList<>(numTasks);
-        CountDownLatch startLatch = new CountDownLatch(numTasks);
-        assertTrue(numTasks >= startLatch.getCount(),
-            "Not enough tasks/threads to satisfy latch count - deadlock risk");
-
-        for (int i = 0; i < numTasks; i++) {
-
-          results.add(service.submit(() -> {
-            startLatch.countDown();
-            startLatch.await();
-            assertNotNull(al.getCodec(), al + " should have a non-null codec");
-            return true;
-          }));
-        }
-        assertEquals(numTasks, results.size());
-
-        service.shutdown();
-
-        while (!service.awaitTermination(1, SECONDS)) {
-          // wait
-        }
-
-        for (Future<Boolean> result : results) {
-          assertTrue(result.get(),
-              al + " resulted in a failed call to getcodec within the thread 
pool");
+        try {
+
+          ArrayList<Future<Boolean>> results = new ArrayList<>(numTasks);
+          CountDownLatch startLatch = new CountDownLatch(numTasks);
+          assertTrue(numTasks >= startLatch.getCount(),
+              "Not enough tasks/threads to satisfy latch count - deadlock 
risk");
+
+          for (int i = 0; i < numTasks; i++) {
+
+            results.add(service.submit(() -> {
+              startLatch.countDown();
+              startLatch.await();
+              assertNotNull(al.getCodec(), al + " should have a non-null 
codec");
+              return true;
+            }));
+          }
+          assertEquals(numTasks, results.size());
+
+          for (Future<Boolean> result : results) {
+            assertTrue(result.get(),
+                al + " resulted in a failed call to getcodec within the thread 
pool");
+          }
+        } finally {
+          service.shutdown();
         }
         somethingGotTested = true;
       }
@@ -295,37 +290,37 @@ public class CompressionTest {
 
         final int numTasks = 32;
         ExecutorService service = Executors.newFixedThreadPool(numTasks);
-
-        ArrayList<Callable<Integer>> list = new ArrayList<>(numTasks);
-
-        CountDownLatch startLatch = new CountDownLatch(numTasks);
-        assertTrue(numTasks >= startLatch.getCount(),
-            "Not enough tasks/threads to satisfy latch count - deadlock risk");
-
-        // keep track of the system's identity hashcodes.
-
-        for (int i = 0; i < numTasks; i++) {
-          list.add(() -> {
-            startLatch.countDown();
-            startLatch.await();
-            CompressionCodec codec = al.getCodec();
-            assertNotNull(codec, al + " resulted in a non-null codec");
-            return System.identityHashCode(codec);
-          });
-        }
-        assertEquals(numTasks, list.size());
-
-        final HashSet<Integer> hashCodes = new HashSet<>();
-        for (Future<Integer> result : service.invokeAll(list)) {
-          hashCodes.add(result.get());
+        try {
+
+          ArrayList<Callable<Integer>> list = new ArrayList<>(numTasks);
+
+          CountDownLatch startLatch = new CountDownLatch(numTasks);
+          assertTrue(numTasks >= startLatch.getCount(),
+              "Not enough tasks/threads to satisfy latch count - deadlock 
risk");
+
+          // keep track of the system's identity hashcodes.
+
+          for (int i = 0; i < numTasks; i++) {
+            list.add(() -> {
+              startLatch.countDown();
+              startLatch.await();
+              CompressionCodec codec = al.getCodec();
+              assertNotNull(codec, al + " resulted in a non-null codec");
+              return System.identityHashCode(codec);
+            });
+          }
+          assertEquals(numTasks, list.size());
+
+          final HashSet<Integer> hashCodes = new HashSet<>();
+          for (Future<Integer> result : service.invokeAll(list)) {
+            hashCodes.add(result.get());
+          }
+          assertEquals(1, hashCodes.size(), al + " created too many codecs");
+
+        } finally {
+          service.shutdown();
         }
-        assertEquals(1, hashCodes.size(), al + " created too many codecs");
-
-        service.shutdown();
 
-        while (!service.awaitTermination(1, SECONDS)) {
-          // wait
-        }
         somethingGotTested = true;
       }
     }
diff --git 
a/core/src/test/java/org/apache/accumulo/core/util/CompletableFutureUtilTest.java
 
b/core/src/test/java/org/apache/accumulo/core/util/CompletableFutureUtilTest.java
index 2e719540b6..deaa61a014 100644
--- 
a/core/src/test/java/org/apache/accumulo/core/util/CompletableFutureUtilTest.java
+++ 
b/core/src/test/java/org/apache/accumulo/core/util/CompletableFutureUtilTest.java
@@ -62,40 +62,44 @@ public class CompletableFutureUtilTest {
   @Test
   public void testIterateUntil() throws Exception {
     ExecutorService es = Executors.newFixedThreadPool(1);
-    Function<Integer,CompletableFuture<Integer>> step =
-        n -> CompletableFuture.supplyAsync(() -> n - 1, es);
-    Predicate<Integer> isDone = n -> n == 0;
-    // The call stack should overflow before 10,000 calls, so this
-    // effectively tests whether iterateUntil avoids stack overflows
-    // when given async futures.
-    for (int n : new int[] {0, 1, 2, 3, 100, 10_000}) {
-      assertEquals(0, CompletableFutureUtil.iterateUntil(step, isDone, 
n).get());
-    }
-    // Test throwing an exception in the step function.
-    {
-      Function<Integer,CompletableFuture<Integer>> badStep = n -> {
-        throw new RuntimeException();
-      };
-      assertThrows(ExecutionException.class,
-          () -> CompletableFutureUtil.iterateUntil(badStep, isDone, 
100).get());
-    }
-    // Test throwing an exception in the future returned by the step
-    // function.
-    {
-      Function<Integer,CompletableFuture<Integer>> badStep =
-          n -> CompletableFuture.supplyAsync(() -> {
-            throw new RuntimeException();
-          }, es);
-      assertThrows(ExecutionException.class,
-          () -> CompletableFutureUtil.iterateUntil(badStep, isDone, 
100).get());
-    }
-    // Test throwing an exception in the predicate.
-    {
-      Predicate<Integer> badIsDone = n -> {
-        throw new RuntimeException();
-      };
-      assertThrows(ExecutionException.class,
-          () -> CompletableFutureUtil.iterateUntil(step, badIsDone, 
100).get());
+    try {
+      Function<Integer,CompletableFuture<Integer>> step =
+          n -> CompletableFuture.supplyAsync(() -> n - 1, es);
+      Predicate<Integer> isDone = n -> n == 0;
+      // The call stack should overflow before 10,000 calls, so this
+      // effectively tests whether iterateUntil avoids stack overflows
+      // when given async futures.
+      for (int n : new int[] {0, 1, 2, 3, 100, 10_000}) {
+        assertEquals(0, CompletableFutureUtil.iterateUntil(step, isDone, 
n).get());
+      }
+      // Test throwing an exception in the step function.
+      {
+        Function<Integer,CompletableFuture<Integer>> badStep = n -> {
+          throw new RuntimeException();
+        };
+        assertThrows(ExecutionException.class,
+            () -> CompletableFutureUtil.iterateUntil(badStep, isDone, 
100).get());
+      }
+      // Test throwing an exception in the future returned by the step
+      // function.
+      {
+        Function<Integer,CompletableFuture<Integer>> badStep =
+            n -> CompletableFuture.supplyAsync(() -> {
+              throw new RuntimeException();
+            }, es);
+        assertThrows(ExecutionException.class,
+            () -> CompletableFutureUtil.iterateUntil(badStep, isDone, 
100).get());
+      }
+      // Test throwing an exception in the predicate.
+      {
+        Predicate<Integer> badIsDone = n -> {
+          throw new RuntimeException();
+        };
+        assertThrows(ExecutionException.class,
+            () -> CompletableFutureUtil.iterateUntil(step, badIsDone, 
100).get());
+      }
+    } finally {
+      es.shutdown();
     }
   }
 }
diff --git 
a/server/base/src/test/java/org/apache/accumulo/server/tablets/UniqueNameAllocatorTest.java
 
b/server/base/src/test/java/org/apache/accumulo/server/tablets/UniqueNameAllocatorTest.java
index 6120cf6310..6707061be8 100644
--- 
a/server/base/src/test/java/org/apache/accumulo/server/tablets/UniqueNameAllocatorTest.java
+++ 
b/server/base/src/test/java/org/apache/accumulo/server/tablets/UniqueNameAllocatorTest.java
@@ -64,37 +64,41 @@ public class UniqueNameAllocatorTest {
   public void testConcurrent() throws Exception {
     NameIterator iter = new NameIterator(0, 10_000);
     var executor = Executors.newCachedThreadPool();
+    try {
 
-    // start 100 threads each consuming 100 names
-    List<Future<String[]>> futures = new ArrayList<>(100);
-    for (int i = 0; i < 100; i++) {
-      var future = executor.submit(() -> {
-        String[] names = new String[100];
-        for (int j = 0; j < 100; j++) {
-          assertTrue(iter.hasNext());
-          names[j] = iter.next();
-        }
-        return names;
-      });
-      futures.add(future);
-    }
+      // start 100 threads each consuming 100 names
+      List<Future<String[]>> futures = new ArrayList<>(100);
+      for (int i = 0; i < 100; i++) {
+        var future = executor.submit(() -> {
+          String[] names = new String[100];
+          for (int j = 0; j < 100; j++) {
+            assertTrue(iter.hasNext());
+            names[j] = iter.next();
+          }
+          return names;
+        });
+        futures.add(future);
+      }
 
-    // verify 10_000 unique names were generated
-    TreeSet<String> allNames = new TreeSet<>();
-    for (var future : futures) {
-      String[] names = future.get();
-      for (String name : names) {
-        assertTrue(allNames.add(name));
+      // verify 10_000 unique names were generated
+      TreeSet<String> allNames = new TreeSet<>();
+      for (var future : futures) {
+        String[] names = future.get();
+        for (String name : names) {
+          assertTrue(allNames.add(name));
+        }
       }
-    }
 
-    // everything should be consumed from the iterator
-    assertFalse(iter.hasNext());
+      // everything should be consumed from the iterator
+      assertFalse(iter.hasNext());
 
-    // verify order of names
-    long expected = 0;
-    for (String name : allNames) {
-      assertEquals(expected++, Long.parseLong(name, 36));
+      // verify order of names
+      long expected = 0;
+      for (String name : allNames) {
+        assertEquals(expected++, Long.parseLong(name, 36));
+      }
+    } finally {
+      executor.shutdown();
     }
   }
 }
diff --git 
a/server/manager/src/test/java/org/apache/accumulo/manager/compaction/queue/CompactionJobQueuesTest.java
 
b/server/manager/src/test/java/org/apache/accumulo/manager/compaction/queue/CompactionJobQueuesTest.java
index d34cc2666d..016a784704 100644
--- 
a/server/manager/src/test/java/org/apache/accumulo/manager/compaction/queue/CompactionJobQueuesTest.java
+++ 
b/server/manager/src/test/java/org/apache/accumulo/manager/compaction/queue/CompactionJobQueuesTest.java
@@ -270,55 +270,57 @@ public class CompactionJobQueuesTest {
         Stream.of("G1", "G2", 
"G3").map(ResourceGroupId::of).toArray(ResourceGroupId[]::new);
 
     var executor = Executors.newFixedThreadPool(groups.length);
+    try {
 
-    List<Future<Integer>> futures = new ArrayList<>();
+      List<Future<Integer>> futures = new ArrayList<>();
 
-    AtomicBoolean stop = new AtomicBoolean(false);
+      AtomicBoolean stop = new AtomicBoolean(false);
 
-    // create a background thread per a group that polls jobs for the group
-    for (var group : groups) {
-      var future = executor.submit(() -> {
-        int seen = 0;
-        while (!stop.get()) {
-          var job = jobQueues.poll(group);
-          if (job != null) {
-            seen++;
+      // create a background thread per a group that polls jobs for the group
+      for (var group : groups) {
+        var future = executor.submit(() -> {
+          int seen = 0;
+          while (!stop.get()) {
+            var job = jobQueues.poll(group);
+            if (job != null) {
+              seen++;
+            }
           }
-        }
-
-        // After stop was set, nothing should be added to queues anymore. 
Drain anything that is
-        // present and then exit.
-        while (jobQueues.poll(group) != null) {
-          seen++;
-        }
-
-        return seen;
-      });
-      futures.add(future);
-    }
-
-    // Add jobs to queues spread across the groups. While these are being 
added the background
-    // threads should concurrently empty queues causing them to be deleted.
-    for (int i = 0; i < numToAdd; i++) {
-      // Create unique exents because re-adding the same extent will clobber 
any jobs already in the
-      // queue for that extent which could throw off the counts
-      KeyExtent extent = new KeyExtent(TableId.of("1"), new Text(i + "z"), new 
Text(i + "a"));
-      jobQueues.add(extent, List.of(newJob((short) (i % 31), i, groups[i % 
groups.length])));
-    }
 
-    // Cause the background threads to exit after polling all data
-    stop.set(true);
+          // After stop was set, nothing should be added to queues anymore. 
Drain anything that is
+          // present and then exit.
+          while (jobQueues.poll(group) != null) {
+            seen++;
+          }
 
-    // Count the total jobs seen by background threads
-    int totalSeen = 0;
-    for (var future : futures) {
-      totalSeen += future.get();
+          return seen;
+        });
+        futures.add(future);
+      }
+
+      // Add jobs to queues spread across the groups. While these are being 
added the background
+      // threads should concurrently empty queues causing them to be deleted.
+      for (int i = 0; i < numToAdd; i++) {
+        // Create unique exents because re-adding the same extent will clobber 
any jobs already in
+        // the queue for that extent which could throw off the counts
+        KeyExtent extent = new KeyExtent(TableId.of("1"), new Text(i + "z"), 
new Text(i + "a"));
+        jobQueues.add(extent, List.of(newJob((short) (i % 31), i, groups[i % 
groups.length])));
+      }
+
+      // Cause the background threads to exit after polling all data
+      stop.set(true);
+
+      // Count the total jobs seen by background threads
+      int totalSeen = 0;
+      for (var future : futures) {
+        totalSeen += future.get();
+      }
+
+      // The background threads should have seen every job that was added
+      assertEquals(numToAdd, totalSeen);
+    } finally {
+      executor.shutdown();
     }
-
-    executor.shutdown();
-
-    // The background threads should have seen every job that was added
-    assertEquals(numToAdd, totalSeen);
   }
 
   @Test
diff --git 
a/server/tserver/src/test/java/org/apache/accumulo/tserver/AssignmentWatcherTest.java
 
b/server/tserver/src/test/java/org/apache/accumulo/tserver/AssignmentWatcherTest.java
index 4a90cca43f..c0bdc1d1d1 100644
--- 
a/server/tserver/src/test/java/org/apache/accumulo/tserver/AssignmentWatcherTest.java
+++ 
b/server/tserver/src/test/java/org/apache/accumulo/tserver/AssignmentWatcherTest.java
@@ -40,21 +40,26 @@ public class AssignmentWatcherTest {
   @Test
   public void testAssignmentWarning() {
     var e = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
-    var c = new 
ConfigurationCopy(Map.of(Property.TSERV_ASSIGNMENT_DURATION_WARNING.getKey(), 
"0"));
-    ServerContext context = createMock(ServerContext.class);
-    expect(context.getScheduledExecutor()).andReturn(e);
-    expect(context.getConfiguration()).andReturn(c);
+    try {
+      var c =
+          new 
ConfigurationCopy(Map.of(Property.TSERV_ASSIGNMENT_DURATION_WARNING.getKey(), 
"0"));
+      ServerContext context = createMock(ServerContext.class);
+      expect(context.getScheduledExecutor()).andReturn(e);
+      expect(context.getConfiguration()).andReturn(c);
 
-    ActiveAssignmentRunnable task = createMock(ActiveAssignmentRunnable.class);
-    expect(task.getException()).andReturn(new Exception("Assignment warning 
happened"));
+      ActiveAssignmentRunnable task = 
createMock(ActiveAssignmentRunnable.class);
+      expect(task.getException()).andReturn(new Exception("Assignment warning 
happened"));
 
-    var assignments = Map.of(new KeyExtent(TableId.of("1"), null, null),
-        new RunnableStartedAt(task, System.currentTimeMillis()));
-    var watcher = new AssignmentWatcher(context, assignments);
+      var assignments = Map.of(new KeyExtent(TableId.of("1"), null, null),
+          new RunnableStartedAt(task, System.currentTimeMillis()));
+      var watcher = new AssignmentWatcher(context, assignments);
 
-    replay(context, task);
-    watcher.run();
-    verify(context, task);
+      replay(context, task);
+      watcher.run();
+      verify(context, task);
+    } finally {
+      e.shutdownNow();
+    }
   }
 
 }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java 
b/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
index 8deb58a6a4..4ca0835868 100644
--- a/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/CountNameNodeOpsBulkIT.java
@@ -18,7 +18,6 @@
  */
 package org.apache.accumulo.test;
 
-import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.apache.accumulo.core.util.LazySingletons.GSON;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -124,66 +123,69 @@ public class CountNameNodeOpsBulkIT extends 
ConfigurableMacBase {
       fs.mkdirs(base);
 
       ExecutorService es = Executors.newFixedThreadPool(5);
-      List<Future<String>> futures = new ArrayList<>();
-      for (int i = 0; i < 10; i++) {
-        final int which = i;
-        futures.add(es.submit(() -> {
-          Path files = new Path(base, "files" + which);
-          fs.mkdirs(files);
-          for (int i1 = 0; i1 < 100; i1++) {
-            FileSKVWriter writer = 
FileOperations.getInstance().newWriterBuilder()
-                .forFile(
-                    UnreferencedTabletFile.of(fs,
-                        new Path(files + "/bulk_" + i1 + "." + 
RFile.EXTENSION)),
-                    fs, fs.getConf(), NoCryptoServiceFactory.NONE)
-                
.withTableConfiguration(DefaultConfiguration.getInstance()).build();
-            writer.startDefaultLocalityGroup();
-            for (int j = 0x100; j < 0xfff; j += 3) {
-              writer.append(new Key(Integer.toHexString(j)), new Value());
+      try {
+        List<Future<String>> futures = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+          final int which = i;
+          futures.add(es.submit(() -> {
+            Path files = new Path(base, "files" + which);
+            fs.mkdirs(files);
+            for (int i1 = 0; i1 < 100; i1++) {
+              FileSKVWriter writer = 
FileOperations.getInstance().newWriterBuilder()
+                  .forFile(
+                      UnreferencedTabletFile.of(fs,
+                          new Path(files + "/bulk_" + i1 + "." + 
RFile.EXTENSION)),
+                      fs, fs.getConf(), NoCryptoServiceFactory.NONE)
+                  
.withTableConfiguration(DefaultConfiguration.getInstance()).build();
+              writer.startDefaultLocalityGroup();
+              for (int j = 0x100; j < 0xfff; j += 3) {
+                writer.append(new Key(Integer.toHexString(j)), new Value());
+              }
+              writer.close();
             }
-            writer.close();
+            return files.toString();
+          }));
+        }
+        List<String> dirs = new ArrayList<>();
+        for (Future<String> f : futures) {
+          dirs.add(f.get());
+        }
+        log.info("Importing");
+        long startOps = getStat(getStats(), "FileInfoOps");
+        long now = System.currentTimeMillis();
+        List<Future<Object>> errs = new ArrayList<>();
+        for (String dir : dirs) {
+          errs.add(es.submit(() -> {
+            c.tableOperations().importDirectory(dir).to(tableName).load();
+            return null;
+          }));
+        }
+        for (Future<Object> err : errs) {
+          err.get();
+        }
+        log.info(
+            String.format("Completed in %.2f seconds", 
(System.currentTimeMillis() - now) / 1000.));
+        Thread.sleep(SECONDS.toMillis(30));
+        Map<?,?> map = getStats();
+        map.forEach((k, v) -> {
+          try {
+            if (v != null && Double.parseDouble(v.toString()) > 0.0) {
+              log.debug("{}:{}", k, v);
+            }
+          } catch (NumberFormatException e) {
+            // only looking for numbers
           }
-          return files.toString();
-        }));
-      }
-      List<String> dirs = new ArrayList<>();
-      for (Future<String> f : futures) {
-        dirs.add(f.get());
-      }
-      log.info("Importing");
-      long startOps = getStat(getStats(), "FileInfoOps");
-      long now = System.currentTimeMillis();
-      List<Future<Object>> errs = new ArrayList<>();
-      for (String dir : dirs) {
-        errs.add(es.submit(() -> {
-          c.tableOperations().importDirectory(dir).to(tableName).load();
-          return null;
-        }));
-      }
-      for (Future<Object> err : errs) {
-        err.get();
+        });
+        long getFileInfoOpts = getStat(map, "FileInfoOps") - startOps;
+        log.info("New bulk import used {} opts, vs old using 2060", 
getFileInfoOpts);
+        // counts for old bulk import:
+        // Expected number of FileInfoOps was between 1000 and 2100
+        // new bulk import is way better :)
+        assertEquals(20, getFileInfoOpts, "unexpected number of FileInfoOps");
+
+      } finally {
+        es.shutdown();
       }
-      es.shutdown();
-      es.awaitTermination(2, MINUTES);
-      log.info(
-          String.format("Completed in %.2f seconds", 
(System.currentTimeMillis() - now) / 1000.));
-      Thread.sleep(SECONDS.toMillis(30));
-      Map<?,?> map = getStats();
-      map.forEach((k, v) -> {
-        try {
-          if (v != null && Double.parseDouble(v.toString()) > 0.0) {
-            log.debug("{}:{}", k, v);
-          }
-        } catch (NumberFormatException e) {
-          // only looking for numbers
-        }
-      });
-      long getFileInfoOpts = getStat(map, "FileInfoOps") - startOps;
-      log.info("New bulk import used {} opts, vs old using 2060", 
getFileInfoOpts);
-      // counts for old bulk import:
-      // Expected number of FileInfoOps was between 1000 and 2100
-      // new bulk import is way better :)
-      assertEquals(20, getFileInfoOpts, "unexpected number of FileInfoOps");
     }
   }
 }
diff --git a/test/src/main/java/org/apache/accumulo/test/LargeReadIT.java 
b/test/src/main/java/org/apache/accumulo/test/LargeReadIT.java
index b3f0cdc305..750da22d3c 100644
--- a/test/src/main/java/org/apache/accumulo/test/LargeReadIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/LargeReadIT.java
@@ -127,52 +127,57 @@ public class LargeReadIT extends AccumuloClusterHarness {
 
       final int numTasks = 64;
       var executor = Executors.newFixedThreadPool(numTasks);
-      CountDownLatch startLatch = new CountDownLatch(numTasks);
-      assertTrue(numTasks >= startLatch.getCount(),
-          "Not enough tasks/threads to satisfy latch count - deadlock risk");
-      Callable<Long> scanTask = () -> {
-        startLatch.countDown();
-        startLatch.await();
-        try (var scanner = client.createScanner(tableName)) {
-          scannerConfigurer.accept(scanner);
-          return scanner.stream().count();
+      try {
+        CountDownLatch startLatch = new CountDownLatch(numTasks);
+        assertTrue(numTasks >= startLatch.getCount(),
+            "Not enough tasks/threads to satisfy latch count - deadlock risk");
+        Callable<Long> scanTask = () -> {
+          startLatch.countDown();
+          startLatch.await();
+          try (var scanner = client.createScanner(tableName)) {
+            scannerConfigurer.accept(scanner);
+            return scanner.stream().count();
+          }
+        };
+
+        // Run lots of concurrent task that should only read the small data, 
if they read the big
+        // column family then it will exceed the tablet server memory and 
cause it to die and the
+        // test to timeout.
+        var tasks =
+            Stream.iterate(scanTask, t -> t).limit(numTasks * 
5).collect(Collectors.toList());
+        assertEquals(numTasks * 5, tasks.size());
+        for (var future : executor.invokeAll(tasks)) {
+          assertEquals(100, future.get());
         }
-      };
-
-      // Run lots of concurrent task that should only read the small data, if 
they read the big
-      // column family then it will exceed the tablet server memory and cause 
it to die and the test
-      // to timeout.
-      var tasks = Stream.iterate(scanTask, t -> t).limit(numTasks * 
5).collect(Collectors.toList());
-      assertEquals(numTasks * 5, tasks.size());
-      for (var future : executor.invokeAll(tasks)) {
-        assertEquals(100, future.get());
-      }
 
-      // expected data to be in memory for this part of the test, so verify 
that
-      var ctx = getCluster().getServerContext();
-      try (var tablets = 
ctx.getAmple().readTablets().forTable(ctx.getTableId(tableName))
-          .fetch(TabletMetadata.ColumnType.FILES).build()) {
-        assertEquals(0, tablets.stream().count());
-      }
+        // expected data to be in memory for this part of the test, so verify 
that
+        var ctx = getCluster().getServerContext();
+        try (var tablets = 
ctx.getAmple().readTablets().forTable(ctx.getTableId(tableName))
+            .fetch(TabletMetadata.ColumnType.FILES).build()) {
+          assertEquals(0, tablets.stream().count());
+        }
 
-      // flush data and verify
-      client.tableOperations().flush(tableName, null, null, true);
-      try (var tablets = 
ctx.getAmple().readTablets().forTable(ctx.getTableId(tableName))
-          .fetch(TabletMetadata.ColumnType.FILES).build()) {
-        assertEquals(1, tablets.stream().count());
-      }
+        // flush data and verify
+        client.tableOperations().flush(tableName, null, null, true);
+        try (var tablets = 
ctx.getAmple().readTablets().forTable(ctx.getTableId(tableName))
+            .fetch(TabletMetadata.ColumnType.FILES).build()) {
+          assertEquals(1, tablets.stream().count());
+        }
 
-      // Run the scans again, reading from files instead of in memory map... 
verify the large data
-      // is not brought into memory from the file, which would kill the tablet 
server. The test was
-      // created because of a bug in the native map code, but can also check 
the rfile code for a
-      // similar problem.
-      for (var future : executor.invokeAll(tasks)) {
-        assertEquals(100, future.get());
-      }
+        // Run the scans again, reading from files instead of in memory map... 
verify the large data
+        // is not brought into memory from the file, which would kill the 
tablet server. The test
+        // was created because of a bug in the native map code, but can also 
check the rfile code
+        // for a similar problem.
+        for (var future : executor.invokeAll(tasks)) {
+          assertEquals(100, future.get());
+        }
 
-      // this test assumes the first key in the tablet is the big one, verify 
this assumption
-      try (var scanner = client.createScanner(tableName)) {
-        assertEquals(10_000_000, 
scanner.iterator().next().getValue().getSize());
+        // this test assumes the first key in the tablet is the big one, 
verify this assumption
+        try (var scanner = client.createScanner(tableName)) {
+          assertEquals(10_000_000, 
scanner.iterator().next().getValue().getSize());
+        }
+      } finally {
+        executor.shutdown();
       }
     }
   }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/MultipleManagerFateIT.java 
b/test/src/main/java/org/apache/accumulo/test/MultipleManagerFateIT.java
index dce79c9953..a15b1777f6 100644
--- a/test/src/main/java/org/apache/accumulo/test/MultipleManagerFateIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/MultipleManagerFateIT.java
@@ -245,10 +245,10 @@ public class MultipleManagerFateIT extends 
ConfigurableMacBase {
         // Check that each background thread made a least one loop over all 
its table operations.
         assertTrue(loops > 0);
       }
+    } finally {
+      executor.shutdown();
     }
 
-    executor.shutdown();
-
   }
 
   private static void waitToSeeManagers(ClientContext context, int 
expectedManagers,
diff --git a/test/src/main/java/org/apache/accumulo/test/ScanServerIT.java 
b/test/src/main/java/org/apache/accumulo/test/ScanServerIT.java
index 0c3140c38d..e82299c576 100644
--- a/test/src/main/java/org/apache/accumulo/test/ScanServerIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/ScanServerIT.java
@@ -320,8 +320,9 @@ public class ScanServerIT extends SharedMiniClusterBase {
           var e = assertThrows(ExecutionException.class, () -> f.get());
           assertTrue(e.getCause() instanceof AssertionError);
         });
-      } // when the scanner is closed, all open sessions should be closed
-      executor.shutdown();
+      } finally { // when the scanner is closed, all open sessions should be 
closed
+        executor.shutdown();
+      }
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/ScanServerMaxLatencyIT.java 
b/test/src/main/java/org/apache/accumulo/test/ScanServerMaxLatencyIT.java
index 62d67a99fa..b92556991b 100644
--- a/test/src/main/java/org/apache/accumulo/test/ScanServerMaxLatencyIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/ScanServerMaxLatencyIT.java
@@ -150,14 +150,14 @@ public class ScanServerMaxLatencyIT extends 
ConfigurableMacBase {
       // The write task should still be running unless they experienced an 
exception.
       assertTrue(futures.stream().noneMatch(Future::isDone));
 
-      executor.shutdownNow();
-      executor.awaitTermination(600, TimeUnit.SECONDS);
-
       assertEquals(-1, readMaxElapsed(client, EVENTUAL, table2));
       // Now that nothing is writing its expected that max read by an 
immediate scan will see any
       // data an eventual scan would see.
       assertTrue(
           readMaxElapsed(client, IMMEDIATE, table1) >= readMaxElapsed(client, 
EVENTUAL, table1));
+    } finally {
+      executor.shutdownNow();
+      executor.awaitTermination(600, TimeUnit.SECONDS);
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java 
b/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
index 27899f38d9..23fd5a4908 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
@@ -78,14 +78,15 @@ public class TableConfigurationUpdateIT extends 
AccumuloClusterHarness {
       ExecutorService svc = Executors.newFixedThreadPool(numThreads);
       CountDownLatch countDown = new CountDownLatch(numThreads);
       ArrayList<Future<Exception>> futures = new ArrayList<>(numThreads);
-
-      for (int i = 0; i < numThreads; i++) {
-        futures.add(svc.submit(new TableConfRunner(randomMax, iterations, 
tableConf, countDown)));
+      try {
+        for (int i = 0; i < numThreads; i++) {
+          futures.add(svc.submit(new TableConfRunner(randomMax, iterations, 
tableConf, countDown)));
+        }
+      } finally {
+        svc.shutdown();
+        assertTrue(svc.awaitTermination(60, TimeUnit.MINUTES));
       }
 
-      svc.shutdown();
-      assertTrue(svc.awaitTermination(60, TimeUnit.MINUTES));
-
       for (Future<Exception> fut : futures) {
         Exception e = fut.get();
         if (e != null) {
diff --git a/test/src/main/java/org/apache/accumulo/test/TableOperationsIT.java 
b/test/src/main/java/org/apache/accumulo/test/TableOperationsIT.java
index 21c835b0d3..610b2b7449 100644
--- a/test/src/main/java/org/apache/accumulo/test/TableOperationsIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/TableOperationsIT.java
@@ -212,44 +212,46 @@ public class TableOperationsIT extends 
AccumuloClusterHarness {
     final int initialTableSize = 
accumuloClient.tableOperations().list().size();
     final int numTasks = 10;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
+    try {
 
-    for (String tablename : getUniqueNames(30)) {
-      CountDownLatch startSignal = new CountDownLatch(numTasks);
-      List<Future<Boolean>> futureList = new ArrayList<>(numTasks);
-
-      for (int j = 0; j < numTasks; j++) {
-        Future<Boolean> future = pool.submit(() -> {
-          boolean result;
-          try {
-            startSignal.countDown();
-            startSignal.await();
-            accumuloClient.tableOperations().create(tablename);
-            result = true;
-          } catch (TableExistsException e) {
-            result = false;
-          }
-          return result;
-        });
-        futureList.add(future);
-      }
+      for (String tablename : getUniqueNames(30)) {
+        CountDownLatch startSignal = new CountDownLatch(numTasks);
+        List<Future<Boolean>> futureList = new ArrayList<>(numTasks);
+
+        for (int j = 0; j < numTasks; j++) {
+          Future<Boolean> future = pool.submit(() -> {
+            boolean result;
+            try {
+              startSignal.countDown();
+              startSignal.await();
+              accumuloClient.tableOperations().create(tablename);
+              result = true;
+            } catch (TableExistsException e) {
+              result = false;
+            }
+            return result;
+          });
+          futureList.add(future);
+        }
 
-      int taskSucceeded = 0;
-      int taskFailed = 0;
-      for (Future<Boolean> result : futureList) {
-        if (result.get() == true) {
-          taskSucceeded++;
-        } else {
-          taskFailed++;
+        int taskSucceeded = 0;
+        int taskFailed = 0;
+        for (Future<Boolean> result : futureList) {
+          if (result.get() == true) {
+            taskSucceeded++;
+          } else {
+            taskFailed++;
+          }
         }
+
+        assertEquals(1, taskSucceeded);
+        assertEquals(9, taskFailed);
       }
 
-      assertEquals(1, taskSucceeded);
-      assertEquals(9, taskFailed);
+      assertEquals(30, accumuloClient.tableOperations().list().size() - 
initialTableSize);
+    } finally {
+      pool.shutdown();
     }
-
-    assertEquals(30, accumuloClient.tableOperations().list().size() - 
initialTableSize);
-
-    pool.shutdown();
   }
 
   @Test
@@ -913,27 +915,29 @@ public class TableOperationsIT extends 
AccumuloClusterHarness {
     Set<TableId> hash = new HashSet<>();
 
     ExecutorService pool = Executors.newFixedThreadPool(64);
+    try {
 
-    for (int i = 0; i < 1000; i++) {
-      int finalI = i;
+      for (int i = 0; i < 1000; i++) {
+        int finalI = i;
 
-      Future<TableId> future = pool.submit(() -> {
-        TableId tableId = null;
+        Future<TableId> future = pool.submit(() -> {
+          TableId tableId = null;
 
-        tableId = Utils.getNextId("Testing" + finalI, getServerContext(), 
TableId::of);
+          tableId = Utils.getNextId("Testing" + finalI, getServerContext(), 
TableId::of);
 
-        return tableId;
-      });
+          return tableId;
+        });
 
-      futureList.add(future);
-    }
+        futureList.add(future);
+      }
 
-    for (Future<TableId> tab : futureList) {
-      hash.add(tab.get());
+      for (Future<TableId> tab : futureList) {
+        hash.add(tab.get());
+      }
+    } finally {
+      pool.shutdown();
     }
 
-    pool.shutdown();
-
     assertEquals(1000, hash.size());
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/UniqueNameAllocatorIT.java 
b/test/src/main/java/org/apache/accumulo/test/UniqueNameAllocatorIT.java
index facbe5e1b5..5d8087b831 100644
--- a/test/src/main/java/org/apache/accumulo/test/UniqueNameAllocatorIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/UniqueNameAllocatorIT.java
@@ -69,53 +69,58 @@ class UniqueNameAllocatorIT extends SharedMiniClusterBase {
     Set<String> namesSeen = ConcurrentHashMap.newKeySet();
 
     var executorService = Executors.newCachedThreadPool();
-    final int numLargeTasks = 64;
-    final int numSmallTasks = 10;
-    final int numTasks = numLargeTasks + numSmallTasks;
-    List<Future<Integer>> futures = new ArrayList<>(numTasks);
-    // start a portion of threads at the same time
-    CountDownLatch startLatch = new CountDownLatch(32);
-    assertTrue(numTasks >= startLatch.getCount(),
-        "Not enough tasks to satisfy latch count - deadlock risk");
-
-    // create threads that are allocating large random chunks
-    for (int i = 0; i < numLargeTasks; i++) {
-      var future = executorService.submit(() -> {
-        startLatch.countDown();
-        startLatch.await();
-        int added = 0;
-        while (namesSeen.size() < 1_000_000) {
-          var allocator = allocators[random.nextInt(allocators.length)];
-          int needed = Math.max(1, random.nextInt(999));
-          allocate(allocator, needed, namesSeen);
-          added += needed;
-        }
-        return added;
-      });
-      futures.add(future);
-    }
+    try {
+      final int numLargeTasks = 64;
+      final int numSmallTasks = 10;
+      final int numTasks = numLargeTasks + numSmallTasks;
+      List<Future<Integer>> futures = new ArrayList<>(numTasks);
+      // start a portion of threads at the same time
+      CountDownLatch startLatch = new CountDownLatch(32);
+      assertTrue(numTasks >= startLatch.getCount(),
+          "Not enough tasks to satisfy latch count - deadlock risk");
 
-    // create threads that are always allocating a small amount
-    for (int i = 1; i <= numSmallTasks; i++) {
-      int needed = i;
-      var future = executorService.submit(() -> {
-        startLatch.countDown();
-        startLatch.await();
-        int added = 0;
-        while (namesSeen.size() < 1_000_000) {
-          var allocator = allocators[random.nextInt(allocators.length)];
-          allocate(allocator, needed, namesSeen);
-          added += needed;
-        }
-        return added;
-      });
-      futures.add(future);
-    }
-    assertEquals(numTasks, futures.size());
+      // create threads that are allocating large random chunks
+      for (int i = 0; i < numLargeTasks; i++) {
+        var future = executorService.submit(() -> {
+          startLatch.countDown();
+          startLatch.await();
+          int added = 0;
+          while (namesSeen.size() < 1_000_000) {
+            var allocator = allocators[random.nextInt(allocators.length)];
+            int needed = Math.max(1, random.nextInt(999));
+            allocate(allocator, needed, namesSeen);
+            added += needed;
+          }
+          return added;
+        });
+        futures.add(future);
+      }
+
+      // create threads that are always allocating a small amount
+      for (int i = 1; i <= numSmallTasks; i++) {
+        int needed = i;
+        var future = executorService.submit(() -> {
+          startLatch.countDown();
+          startLatch.await();
+          int added = 0;
+          while (namesSeen.size() < 1_000_000) {
+            var allocator = allocators[random.nextInt(allocators.length)];
+            allocate(allocator, needed, namesSeen);
+            added += needed;
+          }
+          return added;
+        });
+        futures.add(future);
+      }
+      assertEquals(numTasks, futures.size());
+
+      for (var future : futures) {
+        // expect all threads to add some names
+        assertTrue(future.get() > 0);
+      }
 
-    for (var future : futures) {
-      // expect all threads to add some names
-      assertTrue(future.get() > 0);
+    } finally {
+      executorService.shutdownNow();
     }
 
     assertThrows(IllegalArgumentException.class, () -> 
allocators[0].getNextNames(-1));
diff --git a/test/src/main/java/org/apache/accumulo/test/ZombieScanIT.java 
b/test/src/main/java/org/apache/accumulo/test/ZombieScanIT.java
index 80559f2415..60283f7423 100644
--- a/test/src/main/java/org/apache/accumulo/test/ZombieScanIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/ZombieScanIT.java
@@ -149,6 +149,7 @@ public class ZombieScanIT extends ConfigurableMacBase {
 
     String table = getUniqueNames(1)[0];
 
+    var executor = Executors.newCachedThreadPool();
     try (AccumuloClient c = 
Accumulo.newClient().from(getClientProperties()).build()) {
 
       var splits = new TreeSet<Text>();
@@ -170,8 +171,6 @@ public class ZombieScanIT extends ConfigurableMacBase {
       // the in memory map it will wait for 15 seconds for the scan
       c.tableOperations().flush(table, null, null, true);
 
-      var executor = Executors.newCachedThreadPool();
-
       // start two zombie scans that should never return using a normal scanner
       List<Future<String>> futures = new ArrayList<>();
       for (var row : List.of("2", "4")) {
@@ -252,6 +251,7 @@ public class ZombieScanIT extends ConfigurableMacBase {
       // simple enough to include
       assertValidScanIds(c);
 
+    } finally {
       executor.shutdownNow();
     }
 
@@ -273,12 +273,11 @@ public class ZombieScanIT extends ConfigurableMacBase {
 
     final ServerType serverType = consistency == IMMEDIATE ? TABLET_SERVER : 
SCAN_SERVER;
 
+    var executor = Executors.newCachedThreadPool();
     try (AccumuloClient c = 
Accumulo.newClient().from(getClientProperties()).build()) {
 
       c.tableOperations().create(table);
 
-      var executor = Executors.newCachedThreadPool();
-
       // start four stuck scans that should never return data
       List<Future<String>> futures = new ArrayList<>();
       for (var row : List.of("2", "4")) {
@@ -337,6 +336,7 @@ public class ZombieScanIT extends ConfigurableMacBase {
 
       assertEquals(6, countActiveScans(c, serverType, table));
 
+    } finally {
       executor.shutdownNow();
     }
   }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT_SimpleSuite.java
 
b/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT_SimpleSuite.java
index 3039cbb3ed..c1bdbdbe9a 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT_SimpleSuite.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT_SimpleSuite.java
@@ -601,157 +601,158 @@ public class PropStoreConfigIT_SimpleSuite extends 
SharedMiniClusterBase {
   private static void runConcurrentPropsModificationTest(PropertyShim 
propShim) throws Exception {
     final int numTasks = 4;
     ExecutorService executor = Executors.newFixedThreadPool(numTasks);
-    CountDownLatch startLatch = new CountDownLatch(numTasks);
-    assertTrue(numTasks >= startLatch.getCount(),
-        "Not enough tasks/threads to satisfy latch count - deadlock risk");
-    var tasks = new ArrayList<Callable<Void>>(numTasks);
+    try {
+      CountDownLatch startLatch = new CountDownLatch(numTasks);
+      assertTrue(numTasks >= startLatch.getCount(),
+          "Not enough tasks/threads to satisfy latch count - deadlock risk");
+      var tasks = new ArrayList<Callable<Void>>(numTasks);
+
+      final int iterations = 151;
+
+      tasks.add(() -> {
+        startLatch.countDown();
+        startLatch.await();
+        for (int i = 0; i < iterations; i++) {
+
+          Map<String,String> prevProps = null;
+          if (i % 10 == 0) {
+            prevProps = propShim.getProperties();
+          }
+
+          Map<String,String> acceptedProps = 
propShim.modifyProperties(tableProps -> {
+            int A = 
Integer.parseInt(tableProps.getOrDefault("general.custom.A", "0"));
+            int B = 
Integer.parseInt(tableProps.getOrDefault("general.custom.B", "0"));
+            int C = 
Integer.parseInt(tableProps.getOrDefault("general.custom.C", "0"));
+            int D = 
Integer.parseInt(tableProps.getOrDefault("general.custom.D", "0"));
+
+            tableProps.put("general.custom.A", A + 2 + "");
+            tableProps.put("general.custom.B", B + 3 + "");
+            tableProps.put("general.custom.C", C + 5 + "");
+            tableProps.put("general.custom.D", D + 7 + "");
+          });
+
+          if (prevProps != null) {
+            var beforeA = 
Integer.parseInt(prevProps.getOrDefault("general.custom.A", "0"));
+            var beforeB = 
Integer.parseInt(prevProps.getOrDefault("general.custom.B", "0"));
+            var beforeC = 
Integer.parseInt(prevProps.getOrDefault("general.custom.C", "0"));
+            var beforeD = 
Integer.parseInt(prevProps.getOrDefault("general.custom.D", "0"));
+
+            var afterA = 
Integer.parseInt(acceptedProps.get("general.custom.A"));
+            var afterB = 
Integer.parseInt(acceptedProps.get("general.custom.B"));
+            var afterC = 
Integer.parseInt(acceptedProps.get("general.custom.C"));
+            var afterD = 
Integer.parseInt(acceptedProps.get("general.custom.D"));
+
+            // because there are other thread possibly making changes since 
reading prevProps, can
+            // only do >= as opposed to == check. Should at a minimum see the 
changes made by this
+            // thread.
+            assertTrue(afterA >= beforeA + 2);
+            assertTrue(afterB >= beforeB + 3);
+            assertTrue(afterC >= beforeC + 5);
+            assertTrue(afterD >= beforeD + 7);
+          }
+        }
+        return null;
+      });
 
-    final int iterations = 151;
+      tasks.add(() -> {
+        startLatch.countDown();
+        startLatch.await();
+        for (int i = 0; i < iterations; i++) {
+          propShim.modifyProperties(tableProps -> {
+            int B = 
Integer.parseInt(tableProps.getOrDefault("general.custom.B", "0"));
+            int C = 
Integer.parseInt(tableProps.getOrDefault("general.custom.C", "0"));
+
+            tableProps.put("general.custom.B", B + 11 + "");
+            tableProps.put("general.custom.C", C + 13 + "");
+          });
+        }
+        return null;
+      });
 
-    tasks.add(() -> {
-      startLatch.countDown();
-      startLatch.await();
-      for (int i = 0; i < iterations; i++) {
+      tasks.add(() -> {
+        startLatch.countDown();
+        startLatch.await();
+        for (int i = 0; i < iterations; i++) {
+          propShim.modifyProperties(tableProps -> {
+            int B = 
Integer.parseInt(tableProps.getOrDefault("general.custom.B", "0"));
 
-        Map<String,String> prevProps = null;
-        if (i % 10 == 0) {
-          prevProps = propShim.getProperties();
+            tableProps.put("general.custom.B", B + 17 + "");
+          });
         }
+        return null;
+      });
 
-        Map<String,String> acceptedProps = 
propShim.modifyProperties(tableProps -> {
-          int A = Integer.parseInt(tableProps.getOrDefault("general.custom.A", 
"0"));
-          int B = Integer.parseInt(tableProps.getOrDefault("general.custom.B", 
"0"));
-          int C = Integer.parseInt(tableProps.getOrDefault("general.custom.C", 
"0"));
-          int D = Integer.parseInt(tableProps.getOrDefault("general.custom.D", 
"0"));
-
-          tableProps.put("general.custom.A", A + 2 + "");
-          tableProps.put("general.custom.B", B + 3 + "");
-          tableProps.put("general.custom.C", C + 5 + "");
-          tableProps.put("general.custom.D", D + 7 + "");
-        });
-
-        if (prevProps != null) {
-          var beforeA = 
Integer.parseInt(prevProps.getOrDefault("general.custom.A", "0"));
-          var beforeB = 
Integer.parseInt(prevProps.getOrDefault("general.custom.B", "0"));
-          var beforeC = 
Integer.parseInt(prevProps.getOrDefault("general.custom.C", "0"));
-          var beforeD = 
Integer.parseInt(prevProps.getOrDefault("general.custom.D", "0"));
-
-          var afterA = Integer.parseInt(acceptedProps.get("general.custom.A"));
-          var afterB = Integer.parseInt(acceptedProps.get("general.custom.B"));
-          var afterC = Integer.parseInt(acceptedProps.get("general.custom.C"));
-          var afterD = Integer.parseInt(acceptedProps.get("general.custom.D"));
-
-          // because there are other thread possibly making changes since 
reading prevProps, can
-          // only do >= as opposed to == check. Should at a minimum see the 
changes made by this
-          // thread.
-          assertTrue(afterA >= beforeA + 2);
-          assertTrue(afterB >= beforeB + 3);
-          assertTrue(afterC >= beforeC + 5);
-          assertTrue(afterD >= beforeD + 7);
+      tasks.add(() -> {
+        startLatch.countDown();
+        startLatch.await();
+        for (int i = 0; i < iterations; i++) {
+          propShim.modifyProperties(tableProps -> {
+            int E = 
Integer.parseInt(tableProps.getOrDefault("general.custom.E", "0"));
+            tableProps.put("general.custom.E", E + 19 + "");
+          });
         }
-      }
-      return null;
-    });
-
-    tasks.add(() -> {
-      startLatch.countDown();
-      startLatch.await();
-      for (int i = 0; i < iterations; i++) {
-        propShim.modifyProperties(tableProps -> {
-          int B = Integer.parseInt(tableProps.getOrDefault("general.custom.B", 
"0"));
-          int C = Integer.parseInt(tableProps.getOrDefault("general.custom.C", 
"0"));
-
-          tableProps.put("general.custom.B", B + 11 + "");
-          tableProps.put("general.custom.C", C + 13 + "");
-        });
-      }
-      return null;
-    });
+        return null;
+      });
 
-    tasks.add(() -> {
-      startLatch.countDown();
-      startLatch.await();
-      for (int i = 0; i < iterations; i++) {
-        propShim.modifyProperties(tableProps -> {
-          int B = Integer.parseInt(tableProps.getOrDefault("general.custom.B", 
"0"));
+      assertEquals(numTasks, tasks.size());
 
-          tableProps.put("general.custom.B", B + 17 + "");
-        });
+      // run all of the above task concurrently
+      for (Future<Void> future : executor.invokeAll(tasks)) {
+        // see if there were any exceptions in the background thread and wait 
for it to finish
+        future.get();
       }
-      return null;
-    });
 
-    tasks.add(() -> {
-      startLatch.countDown();
-      startLatch.await();
-      for (int i = 0; i < iterations; i++) {
-        propShim.modifyProperties(tableProps -> {
-          int E = Integer.parseInt(tableProps.getOrDefault("general.custom.E", 
"0"));
-          tableProps.put("general.custom.E", E + 19 + "");
-        });
-      }
-      return null;
-    });
+      Map<String,String> expected = new HashMap<>();
+
+      // determine the expected sum for all the additions done by the separate 
threads for each
+      // property
+      expected.put("general.custom.A", iterations * 2 + "");
+      expected.put("general.custom.B", iterations * (3 + 11 + 17) + "");
+      expected.put("general.custom.C", iterations * (5 + 13) + "");
+      expected.put("general.custom.D", iterations * 7 + "");
+      expected.put("general.custom.E", iterations * 19 + "");
+
+      final var IS_NOT_CUSTOM_TABLE_PROP =
+          
Pattern.compile("general[.]custom[.][ABCDEF]").asMatchPredicate().negate();
+      Wait.waitFor(() -> {
+        var tableProps = new HashMap<>(propShim.getProperties());
+        tableProps.keySet().removeIf(IS_NOT_CUSTOM_TABLE_PROP);
+        boolean equal = expected.equals(tableProps);
+        if (!equal) {
+          log.info(
+              "Waiting for properties to converge. Actual:" + tableProps + " 
Expected:" + expected);
+        }
+        return equal;
+      });
 
-    assertEquals(numTasks, tasks.size());
+      // now that there are not other thread modifying properties, make a 
modification to check that
+      // the returned map is exactly as expected.
+      Map<String,String> acceptedProps = propShim.modifyProperties(tableProps 
-> {
+        int A = Integer.parseInt(tableProps.getOrDefault("general.custom.A", 
"0"));
+        int B = Integer.parseInt(tableProps.getOrDefault("general.custom.B", 
"0"));
+        int C = Integer.parseInt(tableProps.getOrDefault("general.custom.C", 
"0"));
+        int D = Integer.parseInt(tableProps.getOrDefault("general.custom.D", 
"0"));
+
+        tableProps.put("general.custom.A", A + 2 + "");
+        tableProps.put("general.custom.B", B + 3 + "");
+        tableProps.put("general.custom.C", C + 5 + "");
+        tableProps.put("general.custom.D", D + 7 + "");
+      });
 
-    // run all of the above task concurrently
-    for (Future<Void> future : executor.invokeAll(tasks)) {
-      // see if there were any exceptions in the background thread and wait 
for it to finish
-      future.get();
+      var afterA = Integer.parseInt(acceptedProps.get("general.custom.A"));
+      var afterB = Integer.parseInt(acceptedProps.get("general.custom.B"));
+      var afterC = Integer.parseInt(acceptedProps.get("general.custom.C"));
+      var afterD = Integer.parseInt(acceptedProps.get("general.custom.D"));
+      var afterE = Integer.parseInt(acceptedProps.get("general.custom.E"));
+
+      assertEquals(iterations * 2 + 2, afterA);
+      assertEquals(iterations * (3 + 11 + 17) + 3, afterB);
+      assertEquals(iterations * (5 + 13) + 5, afterC);
+      assertEquals(iterations * 7 + 7, afterD);
+      assertEquals(iterations * 19, afterE);
+    } finally {
+      executor.shutdown();
     }
-
-    Map<String,String> expected = new HashMap<>();
-
-    // determine the expected sum for all the additions done by the separate 
threads for each
-    // property
-    expected.put("general.custom.A", iterations * 2 + "");
-    expected.put("general.custom.B", iterations * (3 + 11 + 17) + "");
-    expected.put("general.custom.C", iterations * (5 + 13) + "");
-    expected.put("general.custom.D", iterations * 7 + "");
-    expected.put("general.custom.E", iterations * 19 + "");
-
-    final var IS_NOT_CUSTOM_TABLE_PROP =
-        
Pattern.compile("general[.]custom[.][ABCDEF]").asMatchPredicate().negate();
-    Wait.waitFor(() -> {
-      var tableProps = new HashMap<>(propShim.getProperties());
-      tableProps.keySet().removeIf(IS_NOT_CUSTOM_TABLE_PROP);
-      boolean equal = expected.equals(tableProps);
-      if (!equal) {
-        log.info(
-            "Waiting for properties to converge. Actual:" + tableProps + " 
Expected:" + expected);
-      }
-      return equal;
-    });
-
-    // now that there are not other thread modifying properties, make a 
modification to check that
-    // the returned map
-    // is exactly as expected.
-    Map<String,String> acceptedProps = propShim.modifyProperties(tableProps -> 
{
-      int A = Integer.parseInt(tableProps.getOrDefault("general.custom.A", 
"0"));
-      int B = Integer.parseInt(tableProps.getOrDefault("general.custom.B", 
"0"));
-      int C = Integer.parseInt(tableProps.getOrDefault("general.custom.C", 
"0"));
-      int D = Integer.parseInt(tableProps.getOrDefault("general.custom.D", 
"0"));
-
-      tableProps.put("general.custom.A", A + 2 + "");
-      tableProps.put("general.custom.B", B + 3 + "");
-      tableProps.put("general.custom.C", C + 5 + "");
-      tableProps.put("general.custom.D", D + 7 + "");
-    });
-
-    var afterA = Integer.parseInt(acceptedProps.get("general.custom.A"));
-    var afterB = Integer.parseInt(acceptedProps.get("general.custom.B"));
-    var afterC = Integer.parseInt(acceptedProps.get("general.custom.C"));
-    var afterD = Integer.parseInt(acceptedProps.get("general.custom.D"));
-    var afterE = Integer.parseInt(acceptedProps.get("general.custom.E"));
-
-    assertEquals(iterations * 2 + 2, afterA);
-    assertEquals(iterations * (3 + 11 + 17) + 3, afterB);
-    assertEquals(iterations * (5 + 13) + 5, afterC);
-    assertEquals(iterations * 7 + 7, afterD);
-    assertEquals(iterations * 19, afterE);
-
-    executor.shutdown();
   }
 
   @Test
diff --git 
a/test/src/main/java/org/apache/accumulo/test/fate/meta/ZooMutatorIT.java 
b/test/src/main/java/org/apache/accumulo/test/fate/meta/ZooMutatorIT.java
index f0ad481e6b..d689db45fa 100644
--- a/test/src/main/java/org/apache/accumulo/test/fate/meta/ZooMutatorIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/fate/meta/ZooMutatorIT.java
@@ -86,12 +86,11 @@ public class ZooMutatorIT extends WithTestNames {
     if (!Files.isDirectory(newFolder)) {
       Files.createDirectories(newFolder);
     }
+    final int numTasks = 16;
+    var executor = Executors.newFixedThreadPool(numTasks);
     try (var testZk = new ZooKeeperTestingServer(newFolder.toFile()); var zk = 
testZk.newClient()) {
       var zrw = zk.asReaderWriter();
 
-      final int numTasks = 16;
-      var executor = Executors.newFixedThreadPool(numTasks);
-
       String initialData = hash("Accumulo Zookeeper Mutator test data") + " 0";
 
       List<Future<List<Integer>>> futures = new ArrayList<>(numTasks);
@@ -131,7 +130,6 @@ public class ZooMutatorIT extends WithTestNames {
           countCounts.put(count, countCounts.getOrDefault(count, 0) + 1);
         }
       }
-      executor.shutdown();
 
       byte[] actual = zrw.getData("/test-zm");
       int settledCount = getCount(actual);
@@ -149,6 +147,8 @@ public class ZooMutatorIT extends WithTestNames {
 
       assertEquals(settledCount + 1, countCounts.size());
       assertEquals(expected, new String(actual, UTF_8));
+    } finally {
+      executor.shutdown();
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/AddSplitIT_SimpleSuite.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/AddSplitIT_SimpleSuite.java
index 52cd4550f8..b85679b115 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/AddSplitIT_SimpleSuite.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/AddSplitIT_SimpleSuite.java
@@ -291,7 +291,6 @@ public class AddSplitIT_SimpleSuite extends 
SharedMiniClusterBase {
   @Test
   public void concurrentAddSplitTest() throws Exception {
     var threads = 10;
-    var service = Executors.newFixedThreadPool(threads);
     var latch = new CountDownLatch(threads);
 
     String tableName = getUniqueNames(1)[0];
@@ -311,37 +310,40 @@ public class AddSplitIT_SimpleSuite extends 
SharedMiniClusterBase {
       var commonSplits = commonBuilder.build();
       allSplits.putAll(commonSplits);
 
-      // Spin up 10 threads and concurrently submit all 50 existing splits
-      // as well as 50 unique splits, this should create a collision with fate
-      // and cause retries
-      for (int i = 1; i <= threads; i++) {
-        var start = i * 100;
-        service.execute(() -> {
-          // add the 50 common splits
-          SortedMap<Text,TabletMergeability> splits = new 
TreeMap<>(commonSplits);
-          // create 50 unique splits
-          for (int j = start; j < start + 50; j++) {
-            splits.put(new Text(String.format("%09d", j)),
-                TabletMergeability.after(Duration.ofHours(1 + j)));
-          }
-          // make sure all splits are captured
-          allSplits.putAll(splits);
-          // Wait for all 10 threads to be ready before calling putSplits()
-          // to increase the chance of collisions
-          latch.countDown();
-          try {
-            latch.await();
-            c.tableOperations().putSplits(tableName, splits);
-          } catch (Exception e) {
-            throw new RuntimeException(e);
-          }
-        });
+      var service = Executors.newFixedThreadPool(threads);
+      try {
+        // Spin up 10 threads and concurrently submit all 50 existing splits
+        // as well as 50 unique splits, this should create a collision with 
fate
+        // and cause retries
+        for (int i = 1; i <= threads; i++) {
+          var start = i * 100;
+          service.execute(() -> {
+            // add the 50 common splits
+            SortedMap<Text,TabletMergeability> splits = new 
TreeMap<>(commonSplits);
+            // create 50 unique splits
+            for (int j = start; j < start + 50; j++) {
+              splits.put(new Text(String.format("%09d", j)),
+                  TabletMergeability.after(Duration.ofHours(1 + j)));
+            }
+            // make sure all splits are captured
+            allSplits.putAll(splits);
+            // Wait for all 10 threads to be ready before calling putSplits()
+            // to increase the chance of collisions
+            latch.countDown();
+            try {
+              latch.await();
+              c.tableOperations().putSplits(tableName, splits);
+            } catch (Exception e) {
+              throw new RuntimeException(e);
+            }
+          });
+        }
+      } finally {
+        // Wait for all 10 threads to finish
+        service.shutdown();
+        assertTrue(service.awaitTermination(2, TimeUnit.MINUTES));
       }
 
-      // Wait for all 10 threads to finish
-      service.shutdown();
-      assertTrue(service.awaitTermination(2, TimeUnit.MINUTES));
-
       // Verify we have 550 splits and then all splits are correctly set
       assertEquals(50 + (threads * 50), allSplits.size());
       TableId id = TableId.of(c.tableOperations().tableIdMap().get(tableName));
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/BulkNewIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/BulkNewIT.java
index 36ed26a3fb..474b8edc11 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/BulkNewIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/BulkNewIT.java
@@ -470,41 +470,45 @@ public class BulkNewIT extends SharedMiniClusterBase {
       // Start a second bulk import in background thread because it is 
expected this bulk import
       // will hang because tablets are over the pause file limit.
       ExecutorService executor = Executors.newFixedThreadPool(1);
-      var future = executor.submit(() -> {
-        
client.tableOperations().importDirectory(dir2).to(tableName).tableTime(true).load();
-        return null;
-      });
-
-      // sleep a bit to give the bulk import a chance to run
-      UtilWaitThread.sleep(3000);
-      // bulk import should not have gone through it should be pausing because 
the tablet have too
-      // many files
-      assertFalse(future.isDone());
-      verifyData(client, tableName, 0, 179, false);
+      try {
+        var future = executor.submit(() -> {
+          
client.tableOperations().importDirectory(dir2).to(tableName).tableTime(true).load();
+          return null;
+        });
 
-      // Before the bulk import runs no tablets should have loaded flags set
-      assertEquals(Map.of("0060", 0, "0120", 0, "null", 0), 
countLoaded(client, tableName));
-      // compacting the first tablet should allow the import on that tablet to 
proceed
-      client.tableOperations().compact(tableName,
-          new CompactionConfig().setWait(true).setEndRow(new Text("0060")));
-      // Wait for the first tablets data to be updated by bulk import.
-      Wait.waitFor(
-          () -> Map.of("0060", 7, "0120", 0, "null", 
0).equals(countLoaded(client, tableName)));
-
-      // The bulk imports on the other tablets should not have gone through, 
verify their data was
-      // not updated. Spot check a few rows in the other two tablets. The 
first tablet may or may
-      // not be updated on the tablet server at this point, so can not look at 
its data.
-      assertEquals(61L, readRowValue(client, tableName, 61));
-      assertEquals(100L, readRowValue(client, tableName, 100));
-      assertEquals(140L, readRowValue(client, tableName, 140));
-
-      // compact the entire table, should allow all bulk imports to go through
-      client.tableOperations().compact(tableName, new 
CompactionConfig().setWait(true));
-      // wait for bulk import to complete
-      future.get();
-      // verify the values were updated by the bulk import that was paused
-      verifyData(client, tableName, 0, 179, 1000, false);
-      assertEquals(Map.of("0060", 0, "0120", 0, "null", 0), 
countLoaded(client, tableName));
+        // sleep a bit to give the bulk import a chance to run
+        UtilWaitThread.sleep(3000);
+        // bulk import should not have gone through it should be pausing 
because the tablet have too
+        // many files
+        assertFalse(future.isDone());
+        verifyData(client, tableName, 0, 179, false);
+
+        // Before the bulk import runs no tablets should have loaded flags set
+        assertEquals(Map.of("0060", 0, "0120", 0, "null", 0), 
countLoaded(client, tableName));
+        // compacting the first tablet should allow the import on that tablet 
to proceed
+        client.tableOperations().compact(tableName,
+            new CompactionConfig().setWait(true).setEndRow(new Text("0060")));
+        // Wait for the first tablets data to be updated by bulk import.
+        Wait.waitFor(
+            () -> Map.of("0060", 7, "0120", 0, "null", 
0).equals(countLoaded(client, tableName)));
+
+        // The bulk imports on the other tablets should not have gone through, 
verify their data was
+        // not updated. Spot check a few rows in the other two tablets. The 
first tablet may or may
+        // not be updated on the tablet server at this point, so can not look 
at its data.
+        assertEquals(61L, readRowValue(client, tableName, 61));
+        assertEquals(100L, readRowValue(client, tableName, 100));
+        assertEquals(140L, readRowValue(client, tableName, 140));
+
+        // compact the entire table, should allow all bulk imports to go 
through
+        client.tableOperations().compact(tableName, new 
CompactionConfig().setWait(true));
+        // wait for bulk import to complete
+        future.get();
+        // verify the values were updated by the bulk import that was paused
+        verifyData(client, tableName, 0, 179, 1000, false);
+        assertEquals(Map.of("0060", 0, "0120", 0, "null", 0), 
countLoaded(client, tableName));
+      } finally {
+        executor.shutdown();
+      }
     }
   }
 
@@ -1200,7 +1204,6 @@ public class BulkNewIT extends SharedMiniClusterBase {
       c.tableOperations().addSplits(tableName, splits);
 
       final int numTasks = 16;
-      var executor = Executors.newFixedThreadPool(numTasks);
       var futures = new ArrayList<Future<?>>();
       // wait for a portion of the tasks to be ready
       CountDownLatch startLatch = new CountDownLatch(numTasks);
@@ -1212,27 +1215,30 @@ public class BulkNewIT extends SharedMiniClusterBase {
       var imports = IntStream.range(2, 
8999).boxed().collect(Collectors.toList());
       // The order in which imports are added to the load plan should not 
matter so test that.
       Collections.shuffle(imports);
-      for (var data : imports) {
-        String filename = "f" + data + ".";
-        loadPlanBuilder.loadFileTo(filename + RFile.EXTENSION, 
RangeType.TABLE, row(data - 1),
-            row(data));
-        var future = executor.submit(() -> {
-          startLatch.countDown();
-          startLatch.await();
-          writeData(fs, dir + "/" + filename, aconf, data, data);
-          return null;
-        });
-        futures.add(future);
-        rowsExpected.add(row(data));
-      }
-      assertEquals(imports.size(), futures.size());
+      var executor = Executors.newFixedThreadPool(numTasks);
+      try {
+        for (var data : imports) {
+          String filename = "f" + data + ".";
+          loadPlanBuilder.loadFileTo(filename + RFile.EXTENSION, 
RangeType.TABLE, row(data - 1),
+              row(data));
+          var future = executor.submit(() -> {
+            startLatch.countDown();
+            startLatch.await();
+            writeData(fs, dir + "/" + filename, aconf, data, data);
+            return null;
+          });
+          futures.add(future);
+          rowsExpected.add(row(data));
+        }
+        assertEquals(imports.size(), futures.size());
 
-      for (var future : futures) {
-        future.get();
+        for (var future : futures) {
+          future.get();
+        }
+      } finally {
+        executor.shutdown();
       }
 
-      executor.shutdown();
-
       var loadPlan = loadPlanBuilder.build();
 
       
c.tableOperations().importDirectory(dir).to(tableName).plan(loadPlan).load();
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/CompactionIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/CompactionIT.java
index 2e6fbfa7d9..000ffd26e0 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/CompactionIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/CompactionIT.java
@@ -286,26 +286,29 @@ public class CompactionIT extends CompactionITBase {
       final int THREADS = 5;
       for (int count = 0; count < THREADS; count++) {
         ExecutorService executor = Executors.newFixedThreadPool(THREADS);
-        final int span = 500000 / 59;
-        for (int i = 0; i < 500000; i += 500000 / 59) {
-          final int finalI = i;
-          Runnable r = () -> {
-            try {
-              VerifyParams params = new VerifyParams(getClientProps(), 
tableName, span);
-              params.startRow = finalI;
-              params.random = 56;
-              params.dataSize = 50;
-              params.cols = 1;
-              VerifyIngest.verifyIngest(c, params);
-            } catch (Exception ex) {
-              log.warn("Got exception verifying data", ex);
-              fail.set(true);
-            }
-          };
-          executor.execute(r);
+        try {
+          final int span = 500000 / 59;
+          for (int i = 0; i < 500000; i += 500000 / 59) {
+            final int finalI = i;
+            Runnable r = () -> {
+              try {
+                VerifyParams params = new VerifyParams(getClientProps(), 
tableName, span);
+                params.startRow = finalI;
+                params.random = 56;
+                params.dataSize = 50;
+                params.cols = 1;
+                VerifyIngest.verifyIngest(c, params);
+              } catch (Exception ex) {
+                log.warn("Got exception verifying data", ex);
+                fail.set(true);
+              }
+            };
+            executor.execute(r);
+          }
+        } finally {
+          executor.shutdown();
+          executor.awaitTermination(defaultTimeout().toSeconds(), SECONDS);
         }
-        executor.shutdown();
-        executor.awaitTermination(defaultTimeout().toSeconds(), SECONDS);
         assertFalse(fail.get(),
             "Failed to successfully run all threads, Check the test output for 
error");
       }
@@ -1070,56 +1073,59 @@ public class CompactionIT extends CompactionITBase {
       client.tableOperations().setProperty(table, 
Property.TABLE_MAJC_RATIO.getKey(), "1");
 
       // start a bunch of compactions in the background
-      var executor = Executors.newCachedThreadPool();
       final int numTasks = 20;
       List<Future<?>> futures = new ArrayList<>(numTasks);
       CountDownLatch startLatch = new CountDownLatch(numTasks);
       assertTrue(numTasks >= startLatch.getCount(),
           "Not enough tasks/threads to satisfy latch count - deadlock risk");
-      // start user compactions on a subset of the tables tablets, system 
compactions should attempt
-      // to run on all tablets. With concurrency should get a mix.
-      for (int i = 1; i < numTasks + 1; i++) {
-        var startRow = new Text(String.format("r:%04d", i - 1));
-        var endRow = new Text(String.format("r:%04d", i));
-        final CompactionConfig config = new CompactionConfig();
-        config.setWait(true);
-        config.setStartRow(startRow);
-        config.setEndRow(endRow);
-        futures.add(executor.submit(() -> {
-          startLatch.countDown();
-          startLatch.await();
-          client.tableOperations().compact(table, config);
-          return null;
-        }));
-      }
-      assertEquals(numTasks, futures.size());
+      var executor = Executors.newCachedThreadPool();
+      try {
+        // start user compactions on a subset of the tables tablets, system 
compactions should
+        // attempt to run on all tablets. With concurrency should get a mix.
+        for (int i = 1; i < numTasks + 1; i++) {
+          var startRow = new Text(String.format("r:%04d", i - 1));
+          var endRow = new Text(String.format("r:%04d", i));
+          final CompactionConfig config = new CompactionConfig();
+          config.setWait(true);
+          config.setStartRow(startRow);
+          config.setEndRow(endRow);
+          futures.add(executor.submit(() -> {
+            startLatch.countDown();
+            startLatch.await();
+            client.tableOperations().compact(table, config);
+            return null;
+          }));
+        }
+        assertEquals(numTasks, futures.size());
 
-      log.debug("Waiting for offline");
-      // take tablet offline while there are concurrent compactions
-      client.tableOperations().offline(table, true);
+        log.debug("Waiting for offline");
+        // take tablet offline while there are concurrent compactions
+        client.tableOperations().offline(table, true);
 
-      // grab a snapshot of all the tablets files after waiting for offline, 
do not expect any
-      // tablets files to change at this point
-      var files1 = getFiles(ctx, tableId);
+        // grab a snapshot of all the tablets files after waiting for offline, 
do not expect any
+        // tablets files to change at this point
+        var files1 = getFiles(ctx, tableId);
 
-      // wait for the background compactions
-      log.debug("Waiting for futures");
-      for (var future : futures) {
-        try {
-          future.get();
-        } catch (ExecutionException ee) {
-          // its ok if some of the compactions fail because the table was 
concurrently taken offline
-          assertTrue(ee.getMessage().contains("is offline"));
+        // wait for the background compactions
+        log.debug("Waiting for futures");
+        for (var future : futures) {
+          try {
+            future.get();
+          } catch (ExecutionException ee) {
+            // its ok if some of the compactions fail because the table was 
concurrently taken
+            // offline
+            assertTrue(ee.getMessage().contains("is offline"));
+          }
         }
-      }
 
-      // grab a second snapshot of the tablets files after all the background 
operations completed
-      var files2 = getFiles(ctx, tableId);
+        // grab a second snapshot of the tablets files after all the 
background operations completed
+        var files2 = getFiles(ctx, tableId);
 
-      // do not expect the files to have changed after the offline operation 
returned.
-      assertEquals(files1, files2);
-
-      executor.shutdown();
+        // do not expect the files to have changed after the offline operation 
returned.
+        assertEquals(files1, files2);
+      } finally {
+        executor.shutdown();
+      }
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentDeleteTableIT.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentDeleteTableIT.java
index 275223b192..64de2ab6cd 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentDeleteTableIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentDeleteTableIT.java
@@ -65,13 +65,13 @@ public class ConcurrentDeleteTableIT extends 
AccumuloClusterHarness {
 
   @Test
   public void testConcurrentDeleteTablesOps() throws Exception {
-    try (AccumuloClient c = 
Accumulo.newClient().from(getClientProps()).build()) {
+    int numDeleteOps = 20;
 
-      String[] tables = getUniqueNames(NUM_TABLES);
+    ExecutorService es = Executors.newFixedThreadPool(numDeleteOps);
 
-      int numDeleteOps = 20;
+    try (AccumuloClient c = 
Accumulo.newClient().from(getClientProps()).build()) {
 
-      ExecutorService es = Executors.newFixedThreadPool(numDeleteOps);
+      String[] tables = getUniqueNames(NUM_TABLES);
 
       int count = 0;
       for (final String table : tables) {
@@ -119,19 +119,21 @@ public class ConcurrentDeleteTableIT extends 
AccumuloClusterHarness {
         FunctionalTestUtils.assertNoDanglingFateLocks(getCluster());
       }
 
-      es.shutdown();
+    } finally {
+      es.shutdownNow();
     }
   }
 
   @Test
   public void testConcurrentFateOpsWithDelete() throws Exception {
-    try (AccumuloClient c = 
Accumulo.newClient().from(getClientProps()).build()) {
 
-      String[] tables = getUniqueNames(NUM_TABLES);
+    int numOperations = 8;
+
+    ExecutorService es = Executors.newFixedThreadPool(numOperations);
 
-      int numOperations = 8;
+    try (AccumuloClient c = 
Accumulo.newClient().from(getClientProps()).build()) {
 
-      ExecutorService es = Executors.newFixedThreadPool(numOperations);
+      String[] tables = getUniqueNames(NUM_TABLES);
 
       int count = 0;
       for (final String table : tables) {
@@ -227,7 +229,8 @@ public class ConcurrentDeleteTableIT extends 
AccumuloClusterHarness {
         FunctionalTestUtils.assertNoDanglingFateLocks(getCluster());
       }
 
-      es.shutdown();
+    } finally {
+      es.shutdownNow();
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentTableNameOperationsIT.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentTableNameOperationsIT.java
index 571fbbd334..ea5642ac5a 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentTableNameOperationsIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/ConcurrentTableNameOperationsIT.java
@@ -88,31 +88,33 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = 16;
     final int numIterations = 8;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
+    try {
+
+      for (String targetTableName : getUniqueNames(numIterations)) {
+        List<String> sourceTableNames = new ArrayList<>();
+        for (int i = 0; i < numTasks; i++) {
+          String sourceTable = targetTableName + "_source_" + i;
+          client.tableOperations().create(sourceTable);
+          sourceTableNames.add(sourceTable);
+        }
 
-    for (String targetTableName : getUniqueNames(numIterations)) {
-      List<String> sourceTableNames = new ArrayList<>();
-      for (int i = 0; i < numTasks; i++) {
-        String sourceTable = targetTableName + "_source_" + i;
-        client.tableOperations().create(sourceTable);
-        sourceTableNames.add(sourceTable);
-      }
-
-      int tableCountBefore = client.tableOperations().list().size();
+        int tableCountBefore = client.tableOperations().list().size();
 
-      int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
-        client.tableOperations().clone(sourceTableNames.get(index), 
targetTableName, true, Map.of(),
-            Set.of());
-        return true;
-      });
+        int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
+          client.tableOperations().clone(sourceTableNames.get(index), 
targetTableName, true,
+              Map.of(), Set.of());
+          return true;
+        });
 
-      assertEquals(1, successCount, "Expected only one clone operation to 
succeed");
-      assertTrue(client.tableOperations().exists(targetTableName),
-          "Expected target table " + targetTableName + " to exist");
-      assertEquals(tableCountBefore + 1, 
client.tableOperations().list().size(),
-          "Expected only one new table after clone");
+        assertEquals(1, successCount, "Expected only one clone operation to 
succeed");
+        assertTrue(client.tableOperations().exists(targetTableName),
+            "Expected target table " + targetTableName + " to exist");
+        assertEquals(tableCountBefore + 1, 
client.tableOperations().list().size(),
+            "Expected only one new table after clone");
+      }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   /**
@@ -123,29 +125,31 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = 16;
     final int numIterations = 10;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
+    try {
+
+      for (String targetTableName : getUniqueNames(numIterations)) {
+        List<String> sourceTableNames = new ArrayList<>();
+        for (int i = 0; i < numTasks; i++) {
+          String sourceTable = targetTableName + "_rename_source_" + i;
+          client.tableOperations().create(sourceTable);
+          sourceTableNames.add(sourceTable);
+        }
 
-    for (String targetTableName : getUniqueNames(numIterations)) {
-      List<String> sourceTableNames = new ArrayList<>();
-      for (int i = 0; i < numTasks; i++) {
-        String sourceTable = targetTableName + "_rename_source_" + i;
-        client.tableOperations().create(sourceTable);
-        sourceTableNames.add(sourceTable);
-      }
-
-      int tableCountBefore = client.tableOperations().list().size();
+        int tableCountBefore = client.tableOperations().list().size();
 
-      int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
-        client.tableOperations().rename(sourceTableNames.get(index), 
targetTableName);
-        return true;
-      });
+        int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
+          client.tableOperations().rename(sourceTableNames.get(index), 
targetTableName);
+          return true;
+        });
 
-      assertEquals(1, successCount, "Expected only one rename operation to 
succeed");
-      assertTrue(client.tableOperations().exists(targetTableName),
-          "Expected target table " + targetTableName + " to exist");
-      assertEquals(tableCountBefore, client.tableOperations().list().size());
+        assertEquals(1, successCount, "Expected only one rename operation to 
succeed");
+        assertTrue(client.tableOperations().exists(targetTableName),
+            "Expected target table " + targetTableName + " to exist");
+        assertEquals(tableCountBefore, client.tableOperations().list().size());
+      }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   /**
@@ -157,36 +161,38 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = 16;
     final int numIterations = 4;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
-    String[] targetTableNames = getUniqueNames(numIterations);
-    var ntc = new NewTableConfiguration().createOffline();
-
-    for (String importTableName : targetTableNames) {
-      // Create separate source tables and export directories for each thread
-      List<String> exportDirs = new ArrayList<>(numTasks);
-      for (int i = 0; i < numTasks; i++) {
-        String sourceTableName = importTableName + "_export_source_" + i;
-        client.tableOperations().create(sourceTableName, ntc);
-        String exportDir = getCluster().getTemporaryPath() + "/export_" + 
sourceTableName;
-        client.tableOperations().exportTable(sourceTableName, exportDir);
-        exportDirs.add(exportDir);
-      }
+    try {
+      String[] targetTableNames = getUniqueNames(numIterations);
+      var ntc = new NewTableConfiguration().createOffline();
+
+      for (String importTableName : targetTableNames) {
+        // Create separate source tables and export directories for each thread
+        List<String> exportDirs = new ArrayList<>(numTasks);
+        for (int i = 0; i < numTasks; i++) {
+          String sourceTableName = importTableName + "_export_source_" + i;
+          client.tableOperations().create(sourceTableName, ntc);
+          String exportDir = getCluster().getTemporaryPath() + "/export_" + 
sourceTableName;
+          client.tableOperations().exportTable(sourceTableName, exportDir);
+          exportDirs.add(exportDir);
+        }
 
-      int tableCountBefore = client.tableOperations().list().size();
+        int tableCountBefore = client.tableOperations().list().size();
 
-      // All threads attempt to import to the same target table name
-      int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
-        client.tableOperations().importTable(importTableName, 
exportDirs.get(index));
-        return true;
-      });
+        // All threads attempt to import to the same target table name
+        int successCount = runConcurrentTableOperation(pool, numTasks, (index) 
-> {
+          client.tableOperations().importTable(importTableName, 
exportDirs.get(index));
+          return true;
+        });
 
-      assertEquals(1, successCount, "Expected only one import operation to 
succeed");
-      assertTrue(client.tableOperations().exists(importTableName),
-          "Expected import table " + importTableName + " to exist");
-      assertEquals(tableCountBefore + 1, 
client.tableOperations().list().size(),
-          "Expected +1 table count for import operation");
+        assertEquals(1, successCount, "Expected only one import operation to 
succeed");
+        assertTrue(client.tableOperations().exists(importTableName),
+            "Expected import table " + importTableName + " to exist");
+        assertEquals(tableCountBefore + 1, 
client.tableOperations().list().size(),
+            "Expected +1 table count for import operation");
+      }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   /**
@@ -199,94 +205,96 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = operationsPerType * 3;
     final int numIterations = 4;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
-    String[] expectedTableNames = getUniqueNames(numIterations);
-
-    for (String targetTableName : expectedTableNames) {
-      List<String> cloneSourceTables = new ArrayList<>();
-      List<String> renameSourceTables = new ArrayList<>();
-      for (int i = 0; i < operationsPerType; i++) {
-        String cloneSource = targetTableName + "_clone_src_" + i;
-        client.tableOperations().create(cloneSource);
-        cloneSourceTables.add(cloneSource);
-
-        String renameSource = targetTableName + "_rename_src_" + i;
-        client.tableOperations().create(renameSource);
-        renameSourceTables.add(renameSource);
-      }
+    try {
+      String[] expectedTableNames = getUniqueNames(numIterations);
+
+      for (String targetTableName : expectedTableNames) {
+        List<String> cloneSourceTables = new ArrayList<>();
+        List<String> renameSourceTables = new ArrayList<>();
+        for (int i = 0; i < operationsPerType; i++) {
+          String cloneSource = targetTableName + "_clone_src_" + i;
+          client.tableOperations().create(cloneSource);
+          cloneSourceTables.add(cloneSource);
+
+          String renameSource = targetTableName + "_rename_src_" + i;
+          client.tableOperations().create(renameSource);
+          renameSourceTables.add(renameSource);
+        }
 
-      int tableCountBefore = client.tableOperations().list().size();
-
-      List<Future<Boolean>> futures = new ArrayList<>();
-      CountDownLatch startSignal = new CountDownLatch(numTasks);
-      AtomicReference<String> successfulOperation = new AtomicReference<>();
-
-      for (int i = 0; i < operationsPerType; i++) {
-        futures.add(pool.submit(() -> {
-          try {
-            startSignal.countDown();
-            startSignal.await();
-            client.tableOperations().create(targetTableName);
-            successfulOperation.set("create");
-            return true;
-          } catch (TableExistsException e) {
-            return false;
-          }
-        }));
-
-        final int index = i;
-
-        futures.add(pool.submit(() -> {
-          try {
-            startSignal.countDown();
-            startSignal.await();
-            client.tableOperations().rename(renameSourceTables.get(index), 
targetTableName);
-            successfulOperation.set("rename");
-            return true;
-          } catch (TableExistsException e) {
-            return false;
-          }
-        }));
-
-        futures.add(pool.submit(() -> {
-          try {
-            startSignal.countDown();
-            startSignal.await();
-            client.tableOperations().clone(cloneSourceTables.get(index), 
targetTableName, true,
-                Map.of(), Set.of());
-            successfulOperation.set("clone");
-            return true;
-          } catch (TableExistsException e) {
-            return false;
-          }
-        }));
-      }
+        int tableCountBefore = client.tableOperations().list().size();
+
+        List<Future<Boolean>> futures = new ArrayList<>();
+        CountDownLatch startSignal = new CountDownLatch(numTasks);
+        AtomicReference<String> successfulOperation = new AtomicReference<>();
+
+        for (int i = 0; i < operationsPerType; i++) {
+          futures.add(pool.submit(() -> {
+            try {
+              startSignal.countDown();
+              startSignal.await();
+              client.tableOperations().create(targetTableName);
+              successfulOperation.set("create");
+              return true;
+            } catch (TableExistsException e) {
+              return false;
+            }
+          }));
+
+          final int index = i;
+
+          futures.add(pool.submit(() -> {
+            try {
+              startSignal.countDown();
+              startSignal.await();
+              client.tableOperations().rename(renameSourceTables.get(index), 
targetTableName);
+              successfulOperation.set("rename");
+              return true;
+            } catch (TableExistsException e) {
+              return false;
+            }
+          }));
+
+          futures.add(pool.submit(() -> {
+            try {
+              startSignal.countDown();
+              startSignal.await();
+              client.tableOperations().clone(cloneSourceTables.get(index), 
targetTableName, true,
+                  Map.of(), Set.of());
+              successfulOperation.set("clone");
+              return true;
+            } catch (TableExistsException e) {
+              return false;
+            }
+          }));
+        }
 
-      assertEquals(numTasks, futures.size(),
-          "Actual created task count did not match expected count");
+        assertEquals(numTasks, futures.size(),
+            "Actual created task count did not match expected count");
 
-      int successCount = 0;
-      for (Future<Boolean> future : futures) {
-        if (future.get()) {
-          successCount++;
+        int successCount = 0;
+        for (Future<Boolean> future : futures) {
+          if (future.get()) {
+            successCount++;
+          }
         }
-      }
 
-      assertEquals(1, successCount, "Expected only one operation to succeed");
+        assertEquals(1, successCount, "Expected only one operation to 
succeed");
 
-      int tableCountAfter = client.tableOperations().list().size();
-      assertTrue(client.tableOperations().exists(targetTableName),
-          "Expected target table " + targetTableName + " to exist");
+        int tableCountAfter = client.tableOperations().list().size();
+        assertTrue(client.tableOperations().exists(targetTableName),
+            "Expected target table " + targetTableName + " to exist");
 
-      String operation = successfulOperation.get();
-      if ("create".equals(operation) || "clone".equals(operation)) {
-        assertEquals(tableCountBefore + 1, tableCountAfter,
-            "Expected +1 table count for " + operation);
-      } else if ("rename".equals(operation)) {
-        assertEquals(tableCountBefore, tableCountAfter, "Expected same table 
count for rename");
+        String operation = successfulOperation.get();
+        if ("create".equals(operation) || "clone".equals(operation)) {
+          assertEquals(tableCountBefore + 1, tableCountAfter,
+              "Expected +1 table count for " + operation);
+        } else if ("rename".equals(operation)) {
+          assertEquals(tableCountBefore, tableCountAfter, "Expected same table 
count for rename");
+        }
       }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   /**
@@ -298,30 +306,32 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = 16;
     final int numIterations = 16;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
-    String[] targetNamespaceNames = getUniqueNames(numIterations);
+    try {
+      String[] targetNamespaceNames = getUniqueNames(numIterations);
 
-    for (String namespaceName : targetNamespaceNames) {
-      Set<String> namespacesBefore = client.namespaceOperations().list();
+      for (String namespaceName : targetNamespaceNames) {
+        Set<String> namespacesBefore = client.namespaceOperations().list();
 
-      int successCount = runConcurrentNamespaceOperation(pool, numTasks, 
(index) -> {
-        client.namespaceOperations().create(namespaceName);
-        return true;
-      });
+        int successCount = runConcurrentNamespaceOperation(pool, numTasks, 
(index) -> {
+          client.namespaceOperations().create(namespaceName);
+          return true;
+        });
 
-      assertEquals(1, successCount, "Expected only one create operation to 
succeed");
-      assertTrue(client.namespaceOperations().exists(namespaceName),
-          "Expected namespace " + namespaceName + " to exist");
+        assertEquals(1, successCount, "Expected only one create operation to 
succeed");
+        assertTrue(client.namespaceOperations().exists(namespaceName),
+            "Expected namespace " + namespaceName + " to exist");
 
-      Set<String> namespacesAfter = client.namespaceOperations().list();
-      Set<String> newNamespaces = new HashSet<>(namespacesAfter);
-      newNamespaces.removeAll(namespacesBefore);
-      assertEquals(Set.of(namespaceName), newNamespaces,
-          "Expected exactly one new namespace: " + namespaceName);
+        Set<String> namespacesAfter = client.namespaceOperations().list();
+        Set<String> newNamespaces = new HashSet<>(namespacesAfter);
+        newNamespaces.removeAll(namespacesBefore);
+        assertEquals(Set.of(namespaceName), newNamespaces,
+            "Expected exactly one new namespace: " + namespaceName);
 
-      client.namespaceOperations().delete(namespaceName);
+        client.namespaceOperations().delete(namespaceName);
+      }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   /**
@@ -333,45 +343,47 @@ public class ConcurrentTableNameOperationsIT extends 
SharedMiniClusterBase {
     final int numTasks = 16;
     final int numIterations = 8;
     ExecutorService pool = Executors.newFixedThreadPool(numTasks);
-    String[] targetNamespaceNames = getUniqueNames(numIterations);
-
-    for (String targetNamespaceName : targetNamespaceNames) {
-      // multiple source namespaces for rename ops
-      List<String> sourceNamespaces = new ArrayList<>();
-      for (int i = 0; i < numTasks; i++) {
-        String sourceNamespace = targetNamespaceName + "_source_" + i;
-        client.namespaceOperations().create(sourceNamespace);
-        sourceNamespaces.add(sourceNamespace);
-      }
+    try {
+      String[] targetNamespaceNames = getUniqueNames(numIterations);
+
+      for (String targetNamespaceName : targetNamespaceNames) {
+        // multiple source namespaces for rename ops
+        List<String> sourceNamespaces = new ArrayList<>();
+        for (int i = 0; i < numTasks; i++) {
+          String sourceNamespace = targetNamespaceName + "_source_" + i;
+          client.namespaceOperations().create(sourceNamespace);
+          sourceNamespaces.add(sourceNamespace);
+        }
 
-      Set<String> namespacesBefore = client.namespaceOperations().list();
+        Set<String> namespacesBefore = client.namespaceOperations().list();
 
-      int successCount = runConcurrentNamespaceOperation(pool, numTasks, 
(index) -> {
-        client.namespaceOperations().rename(sourceNamespaces.get(index), 
targetNamespaceName);
-        return true;
-      });
+        int successCount = runConcurrentNamespaceOperation(pool, numTasks, 
(index) -> {
+          client.namespaceOperations().rename(sourceNamespaces.get(index), 
targetNamespaceName);
+          return true;
+        });
 
-      assertEquals(1, successCount, "Expected only one rename operation to 
succeed");
-      assertTrue(client.namespaceOperations().exists(targetNamespaceName),
-          "Expected target namespace " + targetNamespaceName + " to exist");
+        assertEquals(1, successCount, "Expected only one rename operation to 
succeed");
+        assertTrue(client.namespaceOperations().exists(targetNamespaceName),
+            "Expected target namespace " + targetNamespaceName + " to exist");
 
-      Set<String> namespacesAfter = client.namespaceOperations().list();
-      assertEquals(namespacesBefore.size(), namespacesAfter.size(),
-          "Expected same namespace count (rename operation)");
-      assertTrue(namespacesAfter.contains(targetNamespaceName),
-          "Expected target namespace in final list");
+        Set<String> namespacesAfter = client.namespaceOperations().list();
+        assertEquals(namespacesBefore.size(), namespacesAfter.size(),
+            "Expected same namespace count (rename operation)");
+        assertTrue(namespacesAfter.contains(targetNamespaceName),
+            "Expected target namespace in final list");
 
-      for (String sourceNamespace : sourceNamespaces) {
-        if (client.namespaceOperations().exists(sourceNamespace)) {
-          client.namespaceOperations().delete(sourceNamespace);
+        for (String sourceNamespace : sourceNamespaces) {
+          if (client.namespaceOperations().exists(sourceNamespace)) {
+            client.namespaceOperations().delete(sourceNamespace);
+          }
+        }
+        if (client.namespaceOperations().exists(targetNamespaceName)) {
+          client.namespaceOperations().delete(targetNamespaceName);
         }
       }
-      if (client.namespaceOperations().exists(targetNamespaceName)) {
-        client.namespaceOperations().delete(targetNamespaceName);
-      }
+    } finally {
+      pool.shutdown();
     }
-
-    pool.shutdown();
   }
 
   private int runConcurrentTableOperation(ExecutorService pool, int numTasks,
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/FateStarvationIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/FateStarvationIT.java
index d80be33af9..de043de912 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/FateStarvationIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/FateStarvationIT.java
@@ -89,31 +89,35 @@ public class FateStarvationIT extends 
AccumuloClusterHarness {
       int numTasks = 100;
       List<Future<?>> futures = new ArrayList<>(numTasks);
       var executor = Executors.newCachedThreadPool();
-      // wait for a portion of the tasks to be ready
-      CountDownLatch startLatch = new CountDownLatch(32);
-      assertTrue(numTasks >= startLatch.getCount(),
-          "Not enough tasks to satisfy latch count - deadlock risk");
-
-      for (int i = 0; i < numTasks; i++) {
-        int idx1 = RANDOM.get().nextInt(splits.size() - 1);
-        int idx2 = RANDOM.get().nextInt(splits.size() - (idx1 + 1)) + idx1 + 1;
-
-        var future = executor.submit(() -> {
-          startLatch.countDown();
-          startLatch.await();
-          c.tableOperations().compact(tableName, splits.get(idx1), 
splits.get(idx2), false, true);
-          return null;
-        });
-
-        futures.add(future);
-      }
-      assertEquals(numTasks, futures.size());
-
-      log.debug("Started compactions");
-
-      // wait for all compactions to complete
-      for (var future : futures) {
-        future.get();
+      try {
+        // wait for a portion of the tasks to be ready
+        CountDownLatch startLatch = new CountDownLatch(32);
+        assertTrue(numTasks >= startLatch.getCount(),
+            "Not enough tasks to satisfy latch count - deadlock risk");
+
+        for (int i = 0; i < numTasks; i++) {
+          int idx1 = RANDOM.get().nextInt(splits.size() - 1);
+          int idx2 = RANDOM.get().nextInt(splits.size() - (idx1 + 1)) + idx1 + 
1;
+
+          var future = executor.submit(() -> {
+            startLatch.countDown();
+            startLatch.await();
+            c.tableOperations().compact(tableName, splits.get(idx1), 
splits.get(idx2), false, true);
+            return null;
+          });
+
+          futures.add(future);
+        }
+        assertEquals(numTasks, futures.size());
+
+        log.debug("Started compactions");
+
+        // wait for all compactions to complete
+        for (var future : futures) {
+          future.get();
+        }
+      } finally {
+        executor.shutdown();
       }
 
       FunctionalTestUtils.assertNoDanglingFateLocks(getCluster());
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/FunctionalTestUtils.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/FunctionalTestUtils.java
index cf2460035f..1b94f9116a 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/FunctionalTestUtils.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/FunctionalTestUtils.java
@@ -155,27 +155,30 @@ public class FunctionalTestUtils {
   public static void createRFiles(final AccumuloClient c, final FileSystem fs, 
String path,
       int rows, int splits, int threads) throws Exception {
     fs.delete(new Path(path), true);
-    ExecutorService threadPool = Executors.newFixedThreadPool(threads);
     final AtomicBoolean fail = new AtomicBoolean(false);
-    for (int i = 0; i < rows; i += rows / splits) {
-      TestIngest.IngestParams params = new 
TestIngest.IngestParams(c.properties());
-      params.outputFile = String.format("%s/mf%s", path, i);
-      params.random = 56;
-      params.timestamp = 1;
-      params.dataSize = 50;
-      params.rows = rows / splits;
-      params.startRow = i;
-      params.cols = 1;
-      threadPool.execute(() -> {
-        try {
-          TestIngest.ingest(c, fs, params);
-        } catch (Exception e) {
-          fail.set(true);
-        }
-      });
+    ExecutorService threadPool = Executors.newFixedThreadPool(threads);
+    try {
+      for (int i = 0; i < rows; i += rows / splits) {
+        TestIngest.IngestParams params = new 
TestIngest.IngestParams(c.properties());
+        params.outputFile = String.format("%s/mf%s", path, i);
+        params.random = 56;
+        params.timestamp = 1;
+        params.dataSize = 50;
+        params.rows = rows / splits;
+        params.startRow = i;
+        params.cols = 1;
+        threadPool.execute(() -> {
+          try {
+            TestIngest.ingest(c, fs, params);
+          } catch (Exception e) {
+            fail.set(true);
+          }
+        });
+      }
+    } finally {
+      threadPool.shutdown();
+      threadPool.awaitTermination(1, TimeUnit.HOURS);
     }
-    threadPool.shutdown();
-    threadPool.awaitTermination(1, TimeUnit.HOURS);
     assertFalse(fail.get());
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/ManagerAssignmentIT.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/ManagerAssignmentIT.java
index b28ef8d3d9..aa660d7ed1 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/ManagerAssignmentIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/ManagerAssignmentIT.java
@@ -510,44 +510,48 @@ public class ManagerAssignmentIT extends 
SharedMiniClusterBase {
     };
 
     ExecutorService service = Executors.newFixedThreadPool(10);
-    for (int i = 0; i < 10; i++) {
-      service.execute(task);
-    }
-
-    // Wait until all threads are reading some data
-    latch.await();
-
-    // getClusterControl().stopAllServers(ServerType.TABLET_SERVER)
-    // could potentially send a kill -9 to the process. Shut the tablet
-    // servers down in a more graceful way.
-    final Map<String,Map<KeyExtent,List<Range>>> binnedRanges = new 
HashMap<>();
-    ((ClientContext) 
client).getTabletLocationCache(tid).binRanges((ClientContext) client,
-        Collections.singletonList(TabletsSection.getRange()), binnedRanges);
-    binnedRanges.keySet().forEach((location) -> {
-      HostAndPort address = HostAndPort.fromString(location);
-      String addressWithSession = address.toString();
-      var zLockPath = getCluster().getServerContext().getServerPaths()
-          .createTabletServerPath(ResourceGroupId.DEFAULT, address);
-      long sessionId =
-          
ServiceLock.getSessionId(getCluster().getServerContext().getZooCache(), 
zLockPath);
-      if (sessionId != 0) {
-        addressWithSession = address + "[" + Long.toHexString(sessionId) + "]";
+    try {
+      for (int i = 0; i < 10; i++) {
+        service.execute(task);
       }
 
-      final String finalAddress = addressWithSession;
-      System.out.println("Attempting to shutdown TabletServer at: " + address);
-      try {
-        ThriftClientTypes.MANAGER.executeVoid((ClientContext) client,
-            c -> c.shutdownTabletServer(TraceUtil.traceInfo(),
-                getCluster().getServerContext().rpcCreds(), finalAddress, 
false));
-      } catch (AccumuloException | AccumuloSecurityException e) {
-        fail("Error shutting down TabletServer", e);
-      }
+      // Wait until all threads are reading some data
+      latch.await();
+
+      // getClusterControl().stopAllServers(ServerType.TABLET_SERVER)
+      // could potentially send a kill -9 to the process. Shut the tablet
+      // servers down in a more graceful way.
+      final Map<String,Map<KeyExtent,List<Range>>> binnedRanges = new 
HashMap<>();
+      ((ClientContext) 
client).getTabletLocationCache(tid).binRanges((ClientContext) client,
+          Collections.singletonList(TabletsSection.getRange()), binnedRanges);
+      binnedRanges.keySet().forEach((location) -> {
+        HostAndPort address = HostAndPort.fromString(location);
+        String addressWithSession = address.toString();
+        var zLockPath = getCluster().getServerContext().getServerPaths()
+            .createTabletServerPath(ResourceGroupId.DEFAULT, address);
+        long sessionId =
+            
ServiceLock.getSessionId(getCluster().getServerContext().getZooCache(), 
zLockPath);
+        if (sessionId != 0) {
+          addressWithSession = address + "[" + Long.toHexString(sessionId) + 
"]";
+        }
 
-    });
+        final String finalAddress = addressWithSession;
+        System.out.println("Attempting to shutdown TabletServer at: " + 
address);
+        try {
+          ThriftClientTypes.MANAGER.executeVoid((ClientContext) client,
+              c -> c.shutdownTabletServer(TraceUtil.traceInfo(),
+                  getCluster().getServerContext().rpcCreds(), finalAddress, 
false));
+        } catch (AccumuloException | AccumuloSecurityException e) {
+          fail("Error shutting down TabletServer", e);
+        }
 
-    Wait.waitFor(
-        () -> 
client.instanceOperations().getServers(ServerId.Type.TABLET_SERVER).size() == 
0);
+      });
+
+      Wait.waitFor(
+          () -> 
client.instanceOperations().getServers(ServerId.Type.TABLET_SERVER).size() == 
0);
+    } finally {
+      service.shutdownNow();
+    }
 
     // restart the tablet server for the other tests. Need to call 
stopAllServers
     // to clear out the process list because we shutdown the TabletServer 
outside
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/ScanIdIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/ScanIdIT.java
index c9c0d33d22..837a1202b9 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/ScanIdIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/ScanIdIT.java
@@ -63,6 +63,8 @@ import org.apache.accumulo.core.security.Authorizations;
 import org.apache.accumulo.core.security.ColumnVisibility;
 import org.apache.accumulo.test.harness.AccumuloClusterHarness;
 import org.apache.hadoop.io.Text;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -93,10 +95,20 @@ public class ScanIdIT extends AccumuloClusterHarness {
   private static final int NUM_BATCH_SCANNERS = 1;
   private static final int NUM_TOTAL_SCANNERS = NUM_SINGLE_SCANNERS + 
NUM_BATCH_SCANNERS;
   private static final int NUM_DATA_ROWS = 100;
-  private static final ExecutorService pool = 
Executors.newFixedThreadPool(NUM_TOTAL_SCANNERS);
+  private static ExecutorService pool;
   private static final AtomicBoolean testInProgress = new AtomicBoolean(true);
   private static final Map<Integer,Value> resultsByWorker = new 
ConcurrentHashMap<>();
 
+  @BeforeAll
+  public static void setup() {
+    pool = Executors.newFixedThreadPool(NUM_TOTAL_SCANNERS);
+  }
+
+  @AfterAll
+  public static void teardown() {
+    pool.shutdownNow();
+  }
+
   @Override
   protected Duration defaultTimeout() {
     return Duration.ofMinutes(2);
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/SplitIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/SplitIT.java
index a63050583d..aba4391e2a 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/SplitIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/SplitIT.java
@@ -385,68 +385,69 @@ public class SplitIT extends AccumuloClusterHarness {
 
       log.debug("Creating futures that add random splits to the table");
       ExecutorService es = Executors.newFixedThreadPool(10);
-      final int totalFutures = 100;
-      final int splitsPerFuture = 4;
-      final Set<Text> totalSplits = new ConcurrentSkipListSet<>();
-      List<Callable<Void>> tasks = new ArrayList<>(totalFutures);
-      for (int i = 0; i < totalFutures; i++) {
-        final Pair<Integer,Integer> splitBounds = 
getRandomSplitBounds(numRows);
-        final TreeSet<Text> splits = 
TestIngest.getSplitPoints(splitBounds.getFirst().longValue(),
-            splitBounds.getSecond().longValue(), splitsPerFuture);
-        tasks.add(() -> {
-          c.tableOperations().addSplits(tableName, splits);
-          totalSplits.addAll(splits);
-          return null;
-        });
-      }
+      try {
+        final int totalFutures = 100;
+        final int splitsPerFuture = 4;
+        final Set<Text> totalSplits = new ConcurrentSkipListSet<>();
+        List<Callable<Void>> tasks = new ArrayList<>(totalFutures);
+        for (int i = 0; i < totalFutures; i++) {
+          final Pair<Integer,Integer> splitBounds = 
getRandomSplitBounds(numRows);
+          final TreeSet<Text> splits = 
TestIngest.getSplitPoints(splitBounds.getFirst().longValue(),
+              splitBounds.getSecond().longValue(), splitsPerFuture);
+          tasks.add(() -> {
+            c.tableOperations().addSplits(tableName, splits);
+            totalSplits.addAll(splits);
+            return null;
+          });
+        }
 
-      log.debug("Submitting futures");
-      List<Future<Void>> futures =
-          tasks.parallelStream().map(es::submit).collect(Collectors.toList());
+        log.debug("Submitting futures");
+        List<Future<Void>> futures =
+            
tasks.parallelStream().map(es::submit).collect(Collectors.toList());
 
-      Set<Text> splitsAfterOffline = null;
-      if (offlineTable) {
-        // run offline concurrently with split operation
-        c.tableOperations().offline(tableName, true);
-        splitsAfterOffline = 
Set.copyOf(c.tableOperations().listSplits(tableName));
-      }
+        Set<Text> splitsAfterOffline = null;
+        if (offlineTable) {
+          // run offline concurrently with split operation
+          c.tableOperations().offline(tableName, true);
+          splitsAfterOffline = 
Set.copyOf(c.tableOperations().listSplits(tableName));
+        }
 
-      log.debug("Waiting for futures to complete");
-      for (Future<?> f : futures) {
-        try {
-          f.get();
-        } catch (ExecutionException ee) {
-          if (offlineTable && ee.getMessage().contains("is offline")) {
-            // Some exceptions are expected when concurrently taking the table 
offline.
-            log.debug(ee.getMessage());
-          } else {
-            throw ee;
+        log.debug("Waiting for futures to complete");
+        for (Future<?> f : futures) {
+          try {
+            f.get();
+          } catch (ExecutionException ee) {
+            if (offlineTable && ee.getMessage().contains("is offline")) {
+              // Some exceptions are expected when concurrently taking the 
table offline.
+              log.debug(ee.getMessage());
+            } else {
+              throw ee;
+            }
           }
         }
-      }
-
-      if (offlineTable) {
-        // The splits seen immediately after offline() call should not change 
after all the futures
-        // complete. This ensures that nothing changes in the tablet after the 
offline+wait call
-        // returns.
-        assertEquals(splitsAfterOffline, new 
HashSet<>(c.tableOperations().listSplits(tableName)),
-            "Splits changed after offline");
-
-        // table will be scanned for verification, so bring it online
-        c.tableOperations().online(tableName);
-      } else {
-        assertFalse(totalSplits.isEmpty());
-      }
-
-      log.debug("Checking that {} splits were created ", totalSplits.size());
-      assertEquals(totalSplits, new 
HashSet<>(c.tableOperations().listSplits(tableName)),
-          "Did not see expected splits");
 
-      log.debug("Verifying {} rows ingested into {}", numRows, tableName);
-      VerifyIngest.verifyIngest(c, params);
+        if (offlineTable) {
+          // The splits seen immediately after offline() call should not 
change after all the
+          // futures complete. This ensures that nothing changes in the tablet 
after the
+          // offline+wait call returns.
+          assertEquals(splitsAfterOffline, new 
HashSet<>(c.tableOperations().listSplits(tableName)),
+              "Splits changed after offline");
+
+          // table will be scanned for verification, so bring it online
+          c.tableOperations().online(tableName);
+        } else {
+          assertFalse(totalSplits.isEmpty());
+        }
 
-      es.shutdown();
+        log.debug("Checking that {} splits were created ", totalSplits.size());
+        assertEquals(totalSplits, new 
HashSet<>(c.tableOperations().listSplits(tableName)),
+            "Did not see expected splits");
 
+        log.debug("Verifying {} rows ingested into {}", numRows, tableName);
+        VerifyIngest.verifyIngest(c, params);
+      } finally {
+        es.shutdown();
+      }
     }
   }
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/WriteLotsIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/WriteLotsIT.java
index de8fe05aa8..ce75154c72 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/WriteLotsIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/WriteLotsIT.java
@@ -54,21 +54,24 @@ public class WriteLotsIT extends AccumuloClusterHarness {
       final int THREADS = 5;
       ThreadPoolExecutor tpe = new ThreadPoolExecutor(0, THREADS, 0, 
TimeUnit.SECONDS,
           new ArrayBlockingQueue<>(THREADS));
-      for (int i = 0; i < THREADS; i++) {
-        final int index = i;
-        Runnable r = () -> {
-          try {
-            IngestParams ingestParams = new IngestParams(getClientProps(), 
tableName, 10_000);
-            ingestParams.startRow = index * 10000;
-            TestIngest.ingest(c, ingestParams);
-          } catch (Exception ex) {
-            ref.set(ex);
-          }
-        };
-        tpe.execute(r);
+      try {
+        for (int i = 0; i < THREADS; i++) {
+          final int index = i;
+          Runnable r = () -> {
+            try {
+              IngestParams ingestParams = new IngestParams(getClientProps(), 
tableName, 10_000);
+              ingestParams.startRow = index * 10000;
+              TestIngest.ingest(c, ingestParams);
+            } catch (Exception ex) {
+              ref.set(ex);
+            }
+          };
+          tpe.execute(r);
+        }
+      } finally {
+        tpe.shutdown();
+        tpe.awaitTermination(90, TimeUnit.SECONDS);
       }
-      tpe.shutdown();
-      tpe.awaitTermination(90, TimeUnit.SECONDS);
       if (ref.get() != null) {
         throw ref.get();
       }

Reply via email to