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

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


The following commit(s) were added to refs/heads/2.1 by this push:
     new 670cd500a4 Consolidate tserver tablet assignment state into 
TabletAssignmentTracker (#6621)
670cd500a4 is described below

commit 670cd500a425224011895d25d8df9ee2ba3213e9
Author: Dom G. <[email protected]>
AuthorDate: Fri Nov 6 15:36:02 2026 -0500

    Consolidate tserver tablet assignment state into TabletAssignmentTracker 
(#6621)
    
    Move the unopened, opening, and online tablet collections out of
    TabletServer into a new TabletAssignmentTracker. All methods synchronize
    on the tracker, so moving a tablet between states is atomic and callers
    no longer lock another class's fields.
    
    Also fixes:
    * the lock-order deadlock in the fixSplit path of AssignmentHandler
    * a livelock when an unload thread was interrupted while waiting for a 
tablet to finish opening
    
    Adds TabletAssignmentTrackerTest.
    
    
    Co-authored-by: Keith Turner <[email protected]>
    Co-authored-by: Claude Opus 5.5
---
 .../apache/accumulo/tserver/AssignmentHandler.java |  81 ++------
 .../org/apache/accumulo/tserver/OnlineTablets.java |  59 ------
 .../accumulo/tserver/TabletAssignmentTracker.java  | 210 +++++++++++++++++++++
 .../accumulo/tserver/TabletClientHandler.java      |  68 +++----
 .../org/apache/accumulo/tserver/TabletServer.java  |  43 ++---
 .../accumulo/tserver/UnloadTabletHandler.java      |  27 +--
 .../tserver/TabletAssignmentTrackerTest.java       | 194 +++++++++++++++++++
 7 files changed, 458 insertions(+), 224 deletions(-)

diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/AssignmentHandler.java
 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/AssignmentHandler.java
index 7daa61a50f..fa482c3f03 100644
--- 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/AssignmentHandler.java
+++ 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/AssignmentHandler.java
@@ -22,7 +22,6 @@ import static java.util.concurrent.TimeUnit.MINUTES;
 import static org.apache.accumulo.server.problems.ProblemType.TABLET_LOAD;
 
 import java.util.Arrays;
-import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 
@@ -68,38 +67,8 @@ class AssignmentHandler implements Runnable {
 
   @Override
   public void run() {
-    synchronized (server.unopenedTablets) {
-      synchronized (server.openingTablets) {
-        synchronized (server.onlineTablets) {
-          // nothing should be moving between sets, do a sanity
-          // check
-          Set<KeyExtent> unopenedOverlapping =
-              KeyExtent.findOverlapping(extent, server.unopenedTablets);
-          Set<KeyExtent> openingOverlapping =
-              KeyExtent.findOverlapping(extent, server.openingTablets);
-          Set<KeyExtent> onlineOverlapping =
-              KeyExtent.findOverlapping(extent, 
server.onlineTablets.snapshot());
-
-          if (openingOverlapping.contains(extent) || 
onlineOverlapping.contains(extent)) {
-            return;
-          }
-
-          if (!unopenedOverlapping.contains(extent)) {
-            log.info("assignment {} no longer in the unopened set", extent);
-            return;
-          }
-
-          if (unopenedOverlapping.size() != 1 || !openingOverlapping.isEmpty()
-              || !onlineOverlapping.isEmpty()) {
-            throw new IllegalStateException(
-                "overlaps assigned " + extent + " " + 
!server.unopenedTablets.contains(extent) + " "
-                    + unopenedOverlapping + " " + openingOverlapping + " " + 
onlineOverlapping);
-          }
-        }
-
-        server.unopenedTablets.remove(extent);
-        server.openingTablets.add(extent);
-      }
+    if (!server.assignmentTracker.unopenedToOpening(extent)) {
+      return;
     }
 
     // check Metadata table before accepting assignment
@@ -115,28 +84,21 @@ class AssignmentHandler implements Runnable {
         KeyExtent fixedExtent =
             ManagerMetadataUtil.fixSplit(server.getContext(), tabletMetadata, 
server.getLock());
 
-        synchronized (server.openingTablets) {
-          server.openingTablets.remove(extent);
-          server.openingTablets.notifyAll();
-          // it expected that the new extent will overlap the old one... if it 
does not, it
-          // should not be added to unopenedTablets
-          if (!KeyExtent.findOverlapping(extent, new 
TreeSet<>(Arrays.asList(fixedExtent)))
-              .contains(fixedExtent)) {
-            throw new IllegalStateException(
-                "Fixed split does not overlap " + extent + " " + fixedExtent);
-          }
-          server.unopenedTablets.add(fixedExtent);
+        // it expected that the new extent will overlap the old one... if it 
does not, it
+        // should not be added to unopenedTablets
+        if (!KeyExtent.findOverlapping(extent, new 
TreeSet<>(Arrays.asList(fixedExtent)))
+            .contains(fixedExtent)) {
+          throw new IllegalStateException(
+              "Fixed split does not overlap " + extent + " " + fixedExtent);
         }
+        server.assignmentTracker.openingToUnopened(extent, fixedExtent);
         // split was rolled back... try again
         new AssignmentHandler(server, fixedExtent).run();
         return;
 
       }
     } catch (Exception e) {
-      synchronized (server.openingTablets) {
-        server.openingTablets.remove(extent);
-        server.openingTablets.notifyAll();
-      }
+      server.assignmentTracker.removeOpening(extent);
       log.warn("Failed to verify tablet " + extent, e);
       server.enqueueManagerMessage(new 
TabletStatusMessage(TabletLoadState.LOAD_FAILURE, extent));
       throw new RuntimeException(e);
@@ -145,10 +107,7 @@ class AssignmentHandler implements Runnable {
     if (!canLoad) {
       log.debug("Reporting tablet {} assignment failure: unable to verify 
Tablet Information",
           extent);
-      synchronized (server.openingTablets) {
-        server.openingTablets.remove(extent);
-        server.openingTablets.notifyAll();
-      }
+      server.assignmentTracker.removeOpening(extent);
       server.enqueueManagerMessage(new 
TabletStatusMessage(TabletLoadState.LOAD_FAILURE, extent));
       return;
     }
@@ -184,14 +143,8 @@ class AssignmentHandler implements Runnable {
           new Assignment(extent, server.getTabletSession(), 
tabletMetadata.getLast());
       TabletStateStore.setLocation(server.getContext(), assignment);
 
-      synchronized (server.openingTablets) {
-        synchronized (server.onlineTablets) {
-          server.openingTablets.remove(extent);
-          server.onlineTablets.put(extent, tablet);
-          server.openingTablets.notifyAll();
-          server.recentlyUnloadedCache.remove(tablet.getExtent());
-        }
-      }
+      server.recentlyUnloadedCache.remove(tablet.getExtent());
+      server.assignmentTracker.openingToOnline(extent, tablet);
       tablet = null; // release this reference
       successful = true;
     } catch (Exception e) {
@@ -209,13 +162,7 @@ class AssignmentHandler implements Runnable {
     if (successful) {
       server.enqueueManagerMessage(new 
TabletStatusMessage(TabletLoadState.LOADED, extent));
     } else {
-      synchronized (server.unopenedTablets) {
-        synchronized (server.openingTablets) {
-          server.openingTablets.remove(extent);
-          server.unopenedTablets.add(extent);
-          server.openingTablets.notifyAll();
-        }
-      }
+      server.assignmentTracker.openingToUnopened(extent, extent);
       log.warn("failed to open tablet {} reporting failure to manager", 
extent);
       server.enqueueManagerMessage(new 
TabletStatusMessage(TabletLoadState.LOAD_FAILURE, extent));
       long reschedule = Math.min((1L << Math.min(32, retryAttempt)) * 1000, 
MINUTES.toMillis(10));
diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/OnlineTablets.java 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/OnlineTablets.java
deleted file mode 100644
index f30254529b..0000000000
--- 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/OnlineTablets.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.accumulo.tserver;
-
-import java.util.Collections;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import org.apache.accumulo.core.dataImpl.KeyExtent;
-import org.apache.accumulo.tserver.tablet.Tablet;
-
-import com.google.common.collect.ImmutableSortedMap;
-
-/*
- * The set of online tablets is frequently read by many threads and 
infrequently updated.  This
- * class exists to create a simple wrapper that keeps an immutable snapshot up 
to date.  Many
- * threads can access the snapshot without interfering with each other.
- */
-public class OnlineTablets {
-  private volatile SortedMap<KeyExtent,Tablet> snapshot = 
Collections.emptySortedMap();
-  private final SortedMap<KeyExtent,Tablet> onlineTablets = new TreeMap<>();
-
-  public synchronized void put(KeyExtent ke, Tablet t) {
-    onlineTablets.put(ke, t);
-    snapshot = ImmutableSortedMap.copyOf(onlineTablets);
-  }
-
-  public synchronized void remove(KeyExtent ke) {
-    onlineTablets.remove(ke);
-    snapshot = ImmutableSortedMap.copyOf(onlineTablets);
-  }
-
-  public synchronized void split(KeyExtent oldTablet, Tablet newTablet1, 
Tablet newTablet2) {
-    onlineTablets.remove(oldTablet);
-    onlineTablets.put(newTablet1.getExtent(), newTablet1);
-    onlineTablets.put(newTablet2.getExtent(), newTablet2);
-    snapshot = ImmutableSortedMap.copyOf(onlineTablets);
-  }
-
-  SortedMap<KeyExtent,Tablet> snapshot() {
-    return snapshot;
-  }
-}
diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletAssignmentTracker.java
 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletAssignmentTracker.java
new file mode 100644
index 0000000000..1efb86dfd8
--- /dev/null
+++ 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletAssignmentTracker.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.tserver;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import org.apache.accumulo.core.dataImpl.KeyExtent;
+import org.apache.accumulo.tserver.tablet.Tablet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSortedMap;
+
+/*
+ * Tracks the tablets assigned to a tablet server as they move from unopened 
to opening to online.
+ * All methods synchronize on this object so that moving a tablet between 
collections is atomic.
+ * It is also used to wake threads in awaitNotOpening, so any change that 
removes a tablet from
+ * opening must call notifyAll.
+ *
+ * The set of online tablets is frequently read by many threads and 
infrequently updated, so an
+ * immutable snapshot of it is kept up to date. Many threads can access the 
snapshot without
+ * interfering with each other.
+ */
+class TabletAssignmentTracker {
+  private static final Logger log = 
LoggerFactory.getLogger(TabletAssignmentTracker.class);
+
+  private final SortedSet<KeyExtent> unopened = new TreeSet<>();
+  private final SortedSet<KeyExtent> opening = new TreeSet<>();
+  private final SortedMap<KeyExtent,Tablet> online = new TreeMap<>();
+  private volatile SortedMap<KeyExtent,Tablet> onlineSnapshot = 
Collections.emptySortedMap();
+
+  static class Overlapping {
+    final Set<KeyExtent> unopened;
+    final Set<KeyExtent> opening;
+    // the tablets are captured under the lock so callers do not need to look 
them up again
+    final Map<KeyExtent,Tablet> online;
+
+    Overlapping(Set<KeyExtent> unopened, Set<KeyExtent> opening, 
Map<KeyExtent,Tablet> online) {
+      this.unopened = Collections.unmodifiableSet(unopened);
+      this.opening = Collections.unmodifiableSet(opening);
+      this.online = Collections.unmodifiableMap(online);
+    }
+
+    boolean isEmpty() {
+      return unopened.isEmpty() && opening.isEmpty() && online.isEmpty();
+    }
+  }
+
+  /**
+   * Adds the extent to the unopened set if it does not overlap any unopened, 
opening, or online
+   * tablet.
+   *
+   * @return the tablets the extent overlaps, empty if the extent was added
+   */
+  synchronized Overlapping addUnopenedIfNoOverlap(KeyExtent extent) {
+    // Checking if the current tablet is in any of the sets
+    // below is not a strong enough check to catch all overlapping tablets
+    // when splits and fix splits are occurring
+    Overlapping overlapping = new 
Overlapping(KeyExtent.findOverlapping(extent, unopened),
+        KeyExtent.findOverlapping(extent, opening), 
KeyExtent.findOverlapping(extent, online)
+            .stream().collect(Collectors.toMap(ke -> ke, online::get)));
+    if (overlapping.isEmpty()) {
+      unopened.add(extent);
+    }
+    return overlapping;
+  }
+
+  /**
+   * Moves the extent from unopened to opening.
+   *
+   * @return false if the extent is already opening or online, or is no longer 
unopened
+   * @throws IllegalStateException if the extent overlaps another unopened, 
opening, or online
+   *         tablet
+   */
+  synchronized boolean unopenedToOpening(KeyExtent extent) {
+    // nothing should be moving between sets, do a sanity check
+    Set<KeyExtent> unopenedOverlapping = KeyExtent.findOverlapping(extent, 
unopened);
+    Set<KeyExtent> openingOverlapping = KeyExtent.findOverlapping(extent, 
opening);
+    Set<KeyExtent> onlineOverlapping = KeyExtent.findOverlapping(extent, 
online);
+
+    if (openingOverlapping.contains(extent) || 
onlineOverlapping.contains(extent)) {
+      return false;
+    }
+
+    if (!unopenedOverlapping.contains(extent)) {
+      log.info("assignment {} no longer in the unopened set", extent);
+      return false;
+    }
+
+    if (unopenedOverlapping.size() != 1 || !openingOverlapping.isEmpty()
+        || !onlineOverlapping.isEmpty()) {
+      throw new IllegalStateException(
+          "overlaps assigned " + extent + " " + !unopened.contains(extent) + " 
"
+              + unopenedOverlapping + " " + openingOverlapping + " " + 
onlineOverlapping);
+    }
+
+    unopened.remove(extent);
+    opening.add(extent);
+    return true;
+  }
+
+  synchronized void openingToOnline(KeyExtent extent, Tablet tablet) {
+    opening.remove(extent);
+    online.put(extent, tablet);
+    onlineSnapshot = ImmutableSortedMap.copyOf(online);
+    notifyAll();
+  }
+
+  /**
+   * Removes the extent from opening and adds unopenedExtent to unopened so 
its assignment can be
+   * retried. The two extents differ when a split was rolled back.
+   */
+  synchronized void openingToUnopened(KeyExtent extent, KeyExtent 
unopenedExtent) {
+    opening.remove(extent);
+    unopened.add(unopenedExtent);
+    notifyAll();
+  }
+
+  synchronized void removeOpening(KeyExtent extent) {
+    opening.remove(extent);
+    notifyAll();
+  }
+
+  synchronized boolean removeUnopened(KeyExtent extent) {
+    return unopened.remove(extent);
+  }
+
+  /**
+   * Blocks until the extent is no longer opening, either because it came 
online or failed to open.
+   */
+  synchronized void awaitNotOpening(KeyExtent extent) {
+    boolean interrupted = false;
+    while (opening.contains(extent)) {
+      try {
+        log.info("Waiting for tablet {} to finish opening before unloading.", 
extent);
+        wait();
+      } catch (InterruptedException e) {
+        // re-interrupting here would make every later wait() throw without 
releasing the monitor
+        interrupted = true;
+      }
+    }
+    if (interrupted) {
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  synchronized void removeOnline(KeyExtent extent) {
+    online.remove(extent);
+    onlineSnapshot = ImmutableSortedMap.copyOf(online);
+  }
+
+  synchronized void split(KeyExtent oldTablet, Tablet newTablet1, Tablet 
newTablet2) {
+    online.remove(oldTablet);
+    online.put(newTablet1.getExtent(), newTablet1);
+    online.put(newTablet2.getExtent(), newTablet2);
+    onlineSnapshot = ImmutableSortedMap.copyOf(online);
+  }
+
+  SortedMap<KeyExtent,Tablet> getOnline() {
+    return onlineSnapshot;
+  }
+
+  /**
+   * Gets a consistent snapshot of the opening and online tablets, this avoids 
missing any tablets
+   * that are moving between them.
+   */
+  synchronized OpeningAndOnlineCompactables getOpeningAndOnline() {
+    return new OpeningAndOnlineCompactables(Set.copyOf(opening), 
onlineSnapshot);
+  }
+
+  synchronized List<KeyExtent> getUnopenedAndOpening() {
+    List<KeyExtent> copy = new ArrayList<>(unopened.size() + opening.size());
+    copy.addAll(unopened);
+    copy.addAll(opening);
+    return Collections.unmodifiableList(copy);
+  }
+
+  synchronized int getOpeningCount() {
+    return opening.size();
+  }
+
+  synchronized int getUnopenedCount() {
+    return unopened.size();
+  }
+}
diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletClientHandler.java
 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletClientHandler.java
index eac87f25f3..d3adc49a97 100644
--- 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletClientHandler.java
+++ 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletClientHandler.java
@@ -1218,53 +1218,33 @@ public class TabletClientHandler implements 
TabletClientService.Iface {
 
     final KeyExtent extent = KeyExtent.fromThrift(textent);
 
-    synchronized (server.unopenedTablets) {
-      synchronized (server.openingTablets) {
-        synchronized (server.onlineTablets) {
-
-          // Checking if the current tablet is in any of the sets
-          // below is not a strong enough check to catch all overlapping 
tablets
-          // when splits and fix splits are occurring
-          Set<KeyExtent> unopenedOverlapping =
-              KeyExtent.findOverlapping(extent, server.unopenedTablets);
-          Set<KeyExtent> openingOverlapping =
-              KeyExtent.findOverlapping(extent, server.openingTablets);
-          Set<KeyExtent> onlineOverlapping =
-              KeyExtent.findOverlapping(extent, server.getOnlineTablets());
-
-          Set<KeyExtent> all = new HashSet<>(
-              unopenedOverlapping.size() + openingOverlapping.size() + 
onlineOverlapping.size(),
-              1.0f);
-          all.addAll(unopenedOverlapping);
-          all.addAll(openingOverlapping);
-          all.addAll(onlineOverlapping);
-
-          if (!all.isEmpty()) {
-
-            // ignore any tablets that have recently split, for error logging
-            for (KeyExtent e2 : onlineOverlapping) {
-              Tablet tablet = server.getOnlineTablet(e2);
-              if (System.currentTimeMillis() - tablet.getSplitCreationTime()
-                  < RECENTLY_SPLIT_MILLIES) {
-                all.remove(e2);
-              }
-            }
-
-            // ignore self, for error logging
-            all.remove(extent);
+    TabletAssignmentTracker.Overlapping overlapping =
+        server.assignmentTracker.addUnopenedIfNoOverlap(extent);
+    if (!overlapping.isEmpty()) {
+      Set<KeyExtent> all = new HashSet<>(
+          overlapping.unopened.size() + overlapping.opening.size() + 
overlapping.online.size(),
+          1.0f);
+      all.addAll(overlapping.unopened);
+      all.addAll(overlapping.opening);
+      all.addAll(overlapping.online.keySet());
+
+      // ignore any tablets that have recently split, for error logging
+      overlapping.online.forEach((e2, tablet) -> {
+        if (System.currentTimeMillis() - tablet.getSplitCreationTime() < 
RECENTLY_SPLIT_MILLIES) {
+          all.remove(e2);
+        }
+      });
 
-            if (!all.isEmpty()) {
-              log.error(
-                  "Tablet {} overlaps a previously assigned tablet, possibly 
due to a recent split. "
-                      + "Overlapping tablets:  Unopened: {}, Opening: {}, 
Online: {}",
-                  extent, unopenedOverlapping, openingOverlapping, 
onlineOverlapping);
-            }
-            return;
-          }
+      // ignore self, for error logging
+      all.remove(extent);
 
-          server.unopenedTablets.add(extent);
-        }
+      if (!all.isEmpty()) {
+        log.error(
+            "Tablet {} overlaps a previously assigned tablet, possibly due to 
a recent split. "
+                + "Overlapping tablets:  Unopened: {}, Opening: {}, Online: 
{}",
+            extent, overlapping.unopened, overlapping.opening, 
overlapping.online.keySet());
       }
+      return;
     }
 
     TabletLogger.loading(extent, server.getTabletSession());
diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServer.java 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServer.java
index bf5c4613ba..62f5aaab43 100644
--- a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServer.java
+++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServer.java
@@ -49,9 +49,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.SortedMap;
-import java.util.SortedSet;
 import java.util.TreeMap;
-import java.util.TreeSet;
 import java.util.UUID;
 import java.util.concurrent.BlockingDeque;
 import java.util.concurrent.ConcurrentHashMap;
@@ -64,7 +62,6 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.LongAdder;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
-import java.util.function.Supplier;
 
 import org.apache.accumulo.core.Constants;
 import org.apache.accumulo.core.client.Durability;
@@ -210,9 +207,7 @@ public class TabletServer extends AbstractServer
   private final AtomicLong flushCounter = new AtomicLong(0);
   private final AtomicLong syncCounter = new AtomicLong(0);
 
-  final OnlineTablets onlineTablets = new OnlineTablets();
-  final SortedSet<KeyExtent> unopenedTablets = 
Collections.synchronizedSortedSet(new TreeSet<>());
-  final SortedSet<KeyExtent> openingTablets = 
Collections.synchronizedSortedSet(new TreeSet<>());
+  final TabletAssignmentTracker assignmentTracker = new 
TabletAssignmentTracker();
   final Map<KeyExtent,Long> recentlyUnloadedCache = 
Collections.synchronizedMap(new LRUMap<>(1000));
 
   final TabletServerResourceManager resourceManager;
@@ -276,7 +271,7 @@ public class TabletServer extends AbstractServer
 
             @Override
             public void run() {
-              Collection<Tablet> tablets = onlineTablets.snapshot().values();
+              Collection<Tablet> tablets = 
assignmentTracker.getOnline().values();
               logBusyTablets(ingestTracker.computeBusiest(tablets), "ingest 
count");
               logBusyTablets(queryTracker.computeBusiest(tablets), "query 
count");
             }
@@ -528,7 +523,7 @@ public class TabletServer extends AbstractServer
     statsKeeper.saveMajorMinorTimes(tablet.getTabletStats());
 
     // lose the reference to the old tablet and open two new ones
-    onlineTablets.split(tablet.getExtent(), newTablets[0], newTablets[1]);
+    assignmentTracker.split(tablet.getExtent(), newTablets[0], newTablets[1]);
 
     // tell the manager
     enqueueManagerMessage(new SplitReportMessage(tablet.getExtent(), 
newTablets[0].getExtent(),
@@ -815,18 +810,8 @@ public class TabletServer extends AbstractServer
     metricsInfo.init(MetricsInfo.serviceTags(context.getInstanceName(), 
getApplicationName(),
         clientAddress, ""));
 
-    Supplier<OpeningAndOnlineCompactables> compactables = () -> {
-      // synchronize on both sets to get a consistent snapshot across both 
sets, this avoids missing
-      // any tablets that are moving between sets
-      synchronized (openingTablets) {
-        synchronized (onlineTablets) {
-          return new OpeningAndOnlineCompactables(Set.copyOf(openingTablets),
-              onlineTablets.snapshot());
-        }
-      }
-    };
-
-    this.compactionManager = new CompactionManager(compactables, getContext(), 
ceMetrics);
+    this.compactionManager =
+        new CompactionManager(assignmentTracker::getOpeningAndOnline, 
getContext(), ceMetrics);
     compactionManager.start();
 
     announceExistence();
@@ -875,7 +860,7 @@ public class TabletServer extends AbstractServer
     long tabletCheckFrequency = 
aconf.getTimeInMillis(Property.TSERV_HEALTH_CHECK_FREQ);
     // Periodically check that metadata of tablets matches what is held in 
memory
     watchCriticalFixedDelay(aconf, tabletCheckFrequency, () -> {
-      final SortedMap<KeyExtent,Tablet> onlineTabletsSnapshot = 
onlineTablets.snapshot();
+      final SortedMap<KeyExtent,Tablet> onlineTabletsSnapshot = 
assignmentTracker.getOnline();
 
       Map<KeyExtent,MetadataUpdateCount> updateCounts = new HashMap<>();
 
@@ -1216,13 +1201,7 @@ public class TabletServer extends AbstractServer
       table.scans.running += mapCounter.getInt(ScanRunState.RUNNING);
     });
 
-    ArrayList<KeyExtent> offlineTabletsCopy = new ArrayList<>();
-    synchronized (this.unopenedTablets) {
-      synchronized (this.openingTablets) {
-        offlineTabletsCopy.addAll(this.unopenedTablets);
-        offlineTabletsCopy.addAll(this.openingTablets);
-      }
-    }
+    List<KeyExtent> offlineTabletsCopy = 
assignmentTracker.getUnopenedAndOpening();
 
     for (KeyExtent extent : offlineTabletsCopy) {
       String tableId = extent.tableId().canonical();
@@ -1315,12 +1294,12 @@ public class TabletServer extends AbstractServer
   }
 
   public SortedMap<KeyExtent,Tablet> getOnlineTablets() {
-    return onlineTablets.snapshot();
+    return assignmentTracker.getOnline();
   }
 
   @Override
   public Tablet getOnlineTablet(KeyExtent extent) {
-    return onlineTablets.snapshot().get(extent);
+    return assignmentTracker.getOnline().get(extent);
   }
 
   @Override
@@ -1338,11 +1317,11 @@ public class TabletServer extends AbstractServer
   }
 
   public int getOpeningCount() {
-    return openingTablets.size();
+    return assignmentTracker.getOpeningCount();
   }
 
   public int getUnopenedCount() {
-    return unopenedTablets.size();
+    return assignmentTracker.getUnopenedCount();
   }
 
   public long getTotalMinorCompactions() {
diff --git 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/UnloadTabletHandler.java
 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/UnloadTabletHandler.java
index 2000f40b99..5c863b8556 100644
--- 
a/server/tserver/src/main/java/org/apache/accumulo/tserver/UnloadTabletHandler.java
+++ 
b/server/tserver/src/main/java/org/apache/accumulo/tserver/UnloadTabletHandler.java
@@ -53,30 +53,13 @@ class UnloadTabletHandler implements Runnable {
   @Override
   public void run() {
 
-    Tablet t = null;
     log.info("Tablet unload for extent {} requested.", extent);
 
-    synchronized (server.unopenedTablets) {
-      if (server.unopenedTablets.contains(extent)) {
-        server.unopenedTablets.remove(extent);
-        return;
-      }
-    }
-    synchronized (server.openingTablets) {
-      while (server.openingTablets.contains(extent)) {
-        try {
-          log.info("Waiting for tablet {} to finish opening before 
unloading.", extent);
-          server.openingTablets.wait();
-        } catch (InterruptedException e) {
-          Thread.currentThread().interrupt();
-        }
-      }
-    }
-    synchronized (server.onlineTablets) {
-      if (server.onlineTablets.snapshot().containsKey(extent)) {
-        t = server.onlineTablets.snapshot().get(extent);
-      }
+    if (server.assignmentTracker.removeUnopened(extent)) {
+      return;
     }
+    server.assignmentTracker.awaitNotOpening(extent);
+    Tablet t = server.getOnlineTablet(extent);
 
     if (t == null) {
       // Tablet has probably been recently unloaded: repeated manager
@@ -106,7 +89,7 @@ class UnloadTabletHandler implements Runnable {
     // stop serving tablet - client will get not serving tablet
     // exceptions
     server.recentlyUnloadedCache.put(extent, System.currentTimeMillis());
-    server.onlineTablets.remove(extent);
+    server.assignmentTracker.removeOnline(extent);
 
     try {
       TServerInstance instance = server.getTabletSession();
diff --git 
a/server/tserver/src/test/java/org/apache/accumulo/tserver/TabletAssignmentTrackerTest.java
 
b/server/tserver/src/test/java/org/apache/accumulo/tserver/TabletAssignmentTrackerTest.java
new file mode 100644
index 0000000000..0c3e73c8f3
--- /dev/null
+++ 
b/server/tserver/src/test/java/org/apache/accumulo/tserver/TabletAssignmentTrackerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.accumulo.tserver;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.core.dataImpl.KeyExtent;
+import org.apache.accumulo.tserver.tablet.Tablet;
+import org.apache.hadoop.io.Text;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+public class TabletAssignmentTrackerTest {
+
+  private static final TableId TID = TableId.of("1");
+  private static final KeyExtent ALL = new KeyExtent(TID, null, null);
+  private static final KeyExtent LOW = new KeyExtent(TID, new Text("m"), null);
+  private static final KeyExtent HIGH = new KeyExtent(TID, null, new 
Text("m"));
+
+  private static Tablet tablet(KeyExtent extent) {
+    Tablet tablet = createMock(Tablet.class);
+    expect(tablet.getExtent()).andReturn(extent).anyTimes();
+    replay(tablet);
+    return tablet;
+  }
+
+  private static void assertState(TabletAssignmentTracker tracker, 
Set<KeyExtent> unopened,
+      Set<KeyExtent> opening, Set<KeyExtent> online) {
+    Set<KeyExtent> unopenedAndOpening = new HashSet<>(unopened);
+    unopenedAndOpening.addAll(opening);
+    assertEquals(unopenedAndOpening, 
Set.copyOf(tracker.getUnopenedAndOpening()));
+    assertEquals(unopened.size(), tracker.getUnopenedCount());
+    assertEquals(opening.size(), tracker.getOpeningCount());
+    assertEquals(opening, tracker.getOpeningAndOnline().opening);
+    assertEquals(online, tracker.getOpeningAndOnline().online.keySet());
+    assertEquals(online, tracker.getOnline().keySet());
+  }
+
+  @Test
+  public void testLifecycle() {
+    var tracker = new TabletAssignmentTracker();
+
+    assertTrue(tracker.addUnopenedIfNoOverlap(ALL).isEmpty());
+    assertState(tracker, Set.of(ALL), Set.of(), Set.of());
+
+    // overlapping assignments are rejected
+    var overlapping = tracker.addUnopenedIfNoOverlap(LOW);
+    assertEquals(Set.of(ALL), overlapping.unopened);
+    assertEquals(Set.of(), overlapping.opening);
+    assertEquals(Map.of(), overlapping.online);
+    assertState(tracker, Set.of(ALL), Set.of(), Set.of());
+
+    assertTrue(tracker.unopenedToOpening(ALL));
+    assertFalse(tracker.unopenedToOpening(ALL));
+    assertState(tracker, Set.of(), Set.of(ALL), Set.of());
+
+    Tablet tablet = tablet(ALL);
+    tracker.openingToOnline(ALL, tablet);
+    assertState(tracker, Set.of(), Set.of(), Set.of(ALL));
+    assertSame(tablet, tracker.getOnline().get(ALL));
+
+    overlapping = tracker.addUnopenedIfNoOverlap(HIGH);
+    assertEquals(Set.of(), overlapping.unopened);
+    assertEquals(Set.of(), overlapping.opening);
+    assertEquals(Map.of(ALL, tablet), overlapping.online);
+
+    tracker.split(ALL, tablet(LOW), tablet(HIGH));
+    assertState(tracker, Set.of(), Set.of(), Set.of(LOW, HIGH));
+
+    tracker.removeOnline(LOW);
+    tracker.removeOnline(HIGH);
+    assertState(tracker, Set.of(), Set.of(), Set.of());
+  }
+
+  @Test
+  public void testRetry() {
+    var tracker = new TabletAssignmentTracker();
+    assertTrue(tracker.addUnopenedIfNoOverlap(ALL).isEmpty());
+    assertTrue(tracker.unopenedToOpening(ALL));
+
+    tracker.openingToUnopened(ALL, LOW);
+    assertState(tracker, Set.of(LOW), Set.of(), Set.of());
+
+    assertTrue(tracker.removeUnopened(LOW));
+    assertFalse(tracker.removeUnopened(LOW));
+    assertState(tracker, Set.of(), Set.of(), Set.of());
+    // assignment was canceled by an unload before it ran
+    assertFalse(tracker.unopenedToOpening(LOW));
+
+    // a rolled back split can put an extent in unopened that overlaps another 
unopened extent
+    assertTrue(tracker.addUnopenedIfNoOverlap(HIGH).isEmpty());
+    assertTrue(tracker.addUnopenedIfNoOverlap(LOW).isEmpty());
+    assertTrue(tracker.unopenedToOpening(LOW));
+    tracker.openingToUnopened(LOW, ALL);
+    assertState(tracker, Set.of(HIGH, ALL), Set.of(), Set.of());
+    assertThrows(IllegalStateException.class, () -> 
tracker.unopenedToOpening(ALL));
+  }
+
+  @Test
+  @Timeout(30)
+  public void testAwaitNotOpening() throws Exception {
+    var tracker = new TabletAssignmentTracker();
+    assertTrue(tracker.addUnopenedIfNoOverlap(ALL).isEmpty());
+    assertTrue(tracker.unopenedToOpening(ALL));
+
+    Thread waiter = new Thread(() -> tracker.awaitNotOpening(ALL));
+    waiter.start();
+    while (waiter.getState() != Thread.State.WAITING) {
+      Thread.sleep(10);
+    }
+
+    tracker.removeOpening(ALL);
+    waiter.join();
+    assertState(tracker, Set.of(), Set.of(), Set.of());
+  }
+
+  @Test
+  // on a livelock removeOpening blocks on the monitor, which only a separate 
thread can time out
+  @Timeout(value = 30, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
+  public void testAwaitNotOpeningInterrupted() throws Exception {
+    var tracker = new TabletAssignmentTracker();
+    assertTrue(tracker.addUnopenedIfNoOverlap(ALL).isEmpty());
+    assertTrue(tracker.unopenedToOpening(ALL));
+
+    var stillInterrupted = new AtomicBoolean();
+    Thread waiter = new Thread(() -> {
+      // interrupt before waiting so the first wait() throws while holding the 
monitor
+      Thread.currentThread().interrupt();
+      tracker.awaitNotOpening(ALL);
+      stillInterrupted.set(Thread.currentThread().isInterrupted());
+    });
+    waiter.setDaemon(true);
+    waiter.start();
+    while (waiter.getState() != Thread.State.WAITING) {
+      Thread.sleep(10);
+    }
+
+    tracker.removeOpening(ALL);
+    waiter.join();
+    assertTrue(stillInterrupted.get());
+    assertState(tracker, Set.of(), Set.of(), Set.of());
+  }
+
+  @Test
+  public void testReturnsAreImmutable() {
+    var tracker = new TabletAssignmentTracker();
+    assertTrue(tracker.addUnopenedIfNoOverlap(LOW).isEmpty());
+    assertTrue(tracker.unopenedToOpening(LOW));
+    tracker.openingToOnline(LOW, tablet(LOW));
+    assertTrue(tracker.addUnopenedIfNoOverlap(HIGH).isEmpty());
+    assertTrue(tracker.unopenedToOpening(HIGH));
+
+    var overlapping = tracker.addUnopenedIfNoOverlap(ALL);
+    assertThrows(UnsupportedOperationException.class, 
overlapping.unopened::clear);
+    assertThrows(UnsupportedOperationException.class, 
overlapping.opening::clear);
+    assertThrows(UnsupportedOperationException.class, 
overlapping.online::clear);
+
+    var openingAndOnline = tracker.getOpeningAndOnline();
+    assertThrows(UnsupportedOperationException.class, 
openingAndOnline.opening::clear);
+    assertThrows(UnsupportedOperationException.class, 
openingAndOnline.online::clear);
+    assertThrows(UnsupportedOperationException.class, () -> 
tracker.getOnline().clear());
+    assertThrows(UnsupportedOperationException.class,
+        () -> tracker.getUnopenedAndOpening().clear());
+  }
+}

Reply via email to