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

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


The following commit(s) were added to refs/heads/elasticity by this push:
     new 79337889f7 ensure only single location is set in conditional location 
check (#4620)
79337889f7 is described below

commit 79337889f7ab4a0040973a26e8709c09b053c280
Author: Keith Turner <ktur...@apache.org>
AuthorDate: Thu May 30 15:29:17 2024 -0400

    ensure only single location is set in conditional location check (#4620)
    
    Accumulo's metadata schema for locations allows multiple locations
    to be set for a tablet even though only one is expected.  The
    conditional mutation check for a location would succeed if the
    required location was present, even if other locations were also
    present.  Updated the check to make it more strict and only pass
    if the required location is present AND no other locations are
    present.
    
    This update removes a specialized iterator for locations and uses
    the more general SetEncodingIterator which allows treating a column
    family like a map for comparison purposes.  Using this we can easily
    check the more strict requirements using well tested code.
    
    Added to ITs related to location conditional check to exercise the
    situation of multiple locations being set on a tablet.
---
 .../core/metadata/schema/TabletMutatorBase.java    | 13 +++
 .../metadata/ConditionalTabletMutatorImpl.java     | 55 ++++++++++--
 .../metadata/iterators/LocationExistsIterator.java | 68 ---------------
 .../metadata/iterators/SetEncodingIterator.java    | 11 ++-
 .../metadata/iterators/TabletExistsIterator.java   |  2 +-
 .../test/functional/AmpleConditionalWriterIT.java  | 99 ++++++++++++++++++++++
 6 files changed, 169 insertions(+), 79 deletions(-)

diff --git 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMutatorBase.java
 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMutatorBase.java
index ab0d6490a3..25db5e1c16 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMutatorBase.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/metadata/schema/TabletMutatorBase.java
@@ -165,6 +165,19 @@ public abstract class TabletMutatorBase<T extends 
Ample.TabletUpdates<T>>
     }
   }
 
+  protected Text getLocationFamilyText(LocationType type) {
+    switch (type) {
+      case CURRENT:
+        return CurrentLocationColumnFamily.NAME;
+      case FUTURE:
+        return FutureLocationColumnFamily.NAME;
+      case LAST:
+        return LastLocationColumnFamily.NAME;
+      default:
+        throw new IllegalArgumentException();
+    }
+  }
+
   @Override
   public T putLocation(Location location) {
     Preconditions.checkState(updatesEnabled, "Cannot make updates after 
calling mutate.");
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/ConditionalTabletMutatorImpl.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/ConditionalTabletMutatorImpl.java
index 208fe4954c..33f746c599 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/ConditionalTabletMutatorImpl.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/ConditionalTabletMutatorImpl.java
@@ -34,6 +34,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 import org.apache.accumulo.core.client.IteratorSetting;
 import org.apache.accumulo.core.client.admin.TabletAvailability;
@@ -54,12 +55,12 @@ import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.Us
 import org.apache.accumulo.core.metadata.schema.TabletMetadata;
 import org.apache.accumulo.core.metadata.schema.TabletMetadata.ColumnType;
 import org.apache.accumulo.core.metadata.schema.TabletMetadata.Location;
+import org.apache.accumulo.core.metadata.schema.TabletMetadata.LocationType;
 import org.apache.accumulo.core.metadata.schema.TabletMutatorBase;
 import org.apache.accumulo.core.metadata.schema.TabletOperationId;
 import org.apache.accumulo.core.tabletserver.log.LogEntry;
 import org.apache.accumulo.core.util.Pair;
 import org.apache.accumulo.server.ServerContext;
-import org.apache.accumulo.server.metadata.iterators.LocationExistsIterator;
 import org.apache.accumulo.server.metadata.iterators.PresentIterator;
 import org.apache.accumulo.server.metadata.iterators.SetEncodingIterator;
 import org.apache.accumulo.server.metadata.iterators.TabletExistsIterator;
@@ -101,21 +102,57 @@ public class ConditionalTabletMutatorImpl extends 
TabletMutatorBase<Ample.Condit
   @Override
   public Ample.ConditionalTabletMutator requireAbsentLocation() {
     Preconditions.checkState(updatesEnabled, "Cannot make updates after 
calling mutate.");
-    IteratorSetting is = new IteratorSetting(INITIAL_ITERATOR_PRIO, 
LocationExistsIterator.class);
-    Condition c = new Condition("", "").setIterators(is);
-    mutation.addCondition(c);
+
+    // It is not expected the encoder will actually be called, so throw an 
exception if it is.
+    Function<Location,byte[]> encoder = l -> {
+      throw new UnsupportedOperationException();
+    };
+
+    // The column families for each location type should conceptually be an 
empty set, so create
+    // conditions that check for this.
+    var condition1 = SetEncodingIterator.createCondition(Set.of(), encoder,
+        getLocationFamilyText(LocationType.FUTURE));
+    var condition2 = SetEncodingIterator.createCondition(Set.of(), encoder,
+        getLocationFamilyText(LocationType.CURRENT));
+
+    // Add the conditions for both location column families, both conditions 
must be met for the
+    // mutation to be applied.
+    mutation.addCondition(condition1);
+    mutation.addCondition(condition2);
+
     return this;
   }
 
   @Override
   public Ample.ConditionalTabletMutator requireLocation(Location location) {
     Preconditions.checkState(updatesEnabled, "Cannot make updates after 
calling mutate.");
-    Preconditions.checkArgument(location.getType() == 
TabletMetadata.LocationType.FUTURE
-        || location.getType() == TabletMetadata.LocationType.CURRENT);
+    Preconditions.checkArgument(
+        location.getType() == LocationType.FUTURE || location.getType() == 
LocationType.CURRENT);
     sawOperationRequirement = true;
-    Condition c = new Condition(getLocationFamily(location.getType()), 
location.getSession())
-        .setValue(location.getHostPort());
-    mutation.addCondition(c);
+
+    Function<Location,Pair<byte[],byte[]>> encoder =
+        l -> new Pair<>(location.getSession().getBytes(UTF_8),
+            location.getHostPort().getBytes(UTF_8));
+
+    // The location column family can have multiple column qualifiers set. 
When requiring a location
+    // we want to check the location is set AND that no other location 
qualifiers are set on the
+    // column family. So the condition should conceptually check that the 
column family is a map of
+    // size one with only our expected location set in the map.
+    var condition1 = 
SetEncodingIterator.createConditionWithVal(Set.of(location), encoder,
+        getLocationFamilyText(location.getType()));
+
+    // Conceptually the column family for the other location type should be an 
empty map, so create
+    // a condition that checks this.
+    var otherLocType =
+        location.getType() == LocationType.CURRENT ? LocationType.FUTURE : 
LocationType.CURRENT;
+    var condition2 = SetEncodingIterator.createConditionWithVal(Set.of(), 
encoder,
+        getLocationFamilyText(otherLocType));
+
+    // Add the conditions for both location column families, both conditions 
must be met for the
+    // mutation to be applied.
+    mutation.addCondition(condition1);
+    mutation.addCondition(condition2);
+
     return this;
   }
 
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/LocationExistsIterator.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/LocationExistsIterator.java
deleted file mode 100644
index bd4396b33b..0000000000
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/LocationExistsIterator.java
+++ /dev/null
@@ -1,68 +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.server.metadata.iterators;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Set;
-
-import org.apache.accumulo.core.data.ArrayByteSequence;
-import org.apache.accumulo.core.data.ByteSequence;
-import org.apache.accumulo.core.data.Key;
-import org.apache.accumulo.core.data.PartialKey;
-import org.apache.accumulo.core.data.Range;
-import org.apache.accumulo.core.iterators.WrappingIterator;
-import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection;
-import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.CurrentLocationColumnFamily;
-import 
org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.FutureLocationColumnFamily;
-import org.apache.hadoop.io.Text;
-
-import com.google.common.base.Preconditions;
-
-/**
- * A specialized iterator used for conditional mutations to check if a 
location is present in a
- * tablet.
- */
-public class LocationExistsIterator extends WrappingIterator {
-  private static final Collection<ByteSequence> LOC_FAMS =
-      Set.of(new ArrayByteSequence(FutureLocationColumnFamily.STR_NAME),
-          new ArrayByteSequence(CurrentLocationColumnFamily.STR_NAME));
-
-  @Override
-  public void seek(Range range, Collection<ByteSequence> columnFamilies, 
boolean inclusive)
-      throws IOException {
-
-    Text tabletRow = getTabletRow(range);
-    Key startKey = new Key(tabletRow, FutureLocationColumnFamily.NAME);
-    Key endKey =
-        new Key(tabletRow, 
CurrentLocationColumnFamily.NAME).followingKey(PartialKey.ROW_COLFAM);
-
-    Range r = new Range(startKey, true, endKey, false);
-
-    super.seek(r, LOC_FAMS, true);
-  }
-
-  static Text getTabletRow(Range range) {
-    var row = range.getStartKey().getRow();
-    // expecting this range to cover a single metadata row, so validate the 
range meets expectations
-    TabletsSection.validateRow(row);
-    Preconditions.checkArgument(row.equals(range.getEndKey().getRow()));
-    return range.getStartKey().getRow();
-  }
-}
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/SetEncodingIterator.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/SetEncodingIterator.java
index facd747b47..b0456afd32 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/SetEncodingIterator.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/SetEncodingIterator.java
@@ -42,6 +42,7 @@ import org.apache.accumulo.core.data.Range;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.iterators.IteratorEnvironment;
 import org.apache.accumulo.core.iterators.SortedKeyValueIterator;
+import org.apache.accumulo.core.metadata.schema.MetadataSchema;
 import org.apache.accumulo.core.util.Pair;
 import org.apache.accumulo.server.metadata.ConditionalTabletMutatorImpl;
 import org.apache.hadoop.io.Text;
@@ -73,11 +74,19 @@ public class SetEncodingIterator implements 
SortedKeyValueIterator<Key,Value> {
   private Value topValue = null;
   private boolean concat = false;
 
+  static Text getTabletRow(Range range) {
+    var row = range.getStartKey().getRow();
+    // expecting this range to cover a single metadata row, so validate the 
range meets expectations
+    MetadataSchema.TabletsSection.validateRow(row);
+    Preconditions.checkArgument(row.equals(range.getEndKey().getRow()));
+    return range.getStartKey().getRow();
+  }
+
   @Override
   public void seek(Range range, Collection<ByteSequence> columnFamilies, 
boolean inclusive)
       throws IOException {
 
-    Text tabletRow = LocationExistsIterator.getTabletRow(range);
+    Text tabletRow = getTabletRow(range);
     Text family = range.getStartKey().getColumnFamily();
 
     Preconditions.checkArgument(
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/TabletExistsIterator.java
 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/TabletExistsIterator.java
index 4b9672f8dc..a258f19e6d 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/TabletExistsIterator.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/metadata/iterators/TabletExistsIterator.java
@@ -37,7 +37,7 @@ public class TabletExistsIterator extends WrappingIterator {
   public void seek(Range range, Collection<ByteSequence> columnFamilies, 
boolean inclusive)
       throws IOException {
 
-    Text tabletRow = LocationExistsIterator.getTabletRow(range);
+    Text tabletRow = SetEncodingIterator.getTabletRow(range);
 
     Range r = new Range(tabletRow, true, tabletRow, false);
 
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/AmpleConditionalWriterIT.java
 
b/test/src/main/java/org/apache/accumulo/test/functional/AmpleConditionalWriterIT.java
index 3c51f2db47..6e7df27e2b 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/functional/AmpleConditionalWriterIT.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/functional/AmpleConditionalWriterIT.java
@@ -164,6 +164,7 @@ public class AmpleConditionalWriterIT extends 
AccumuloClusterHarness {
 
       assertEquals(Location.future(ts1), 
context.getAmple().readTablet(e1).getLocation());
 
+      // test require absent with a future location set
       ctmi = new ConditionalTabletsMutatorImpl(context);
       ctmi.mutateTablet(e1).requireAbsentOperation().requireAbsentLocation()
           .putLocation(Location.future(ts2)).submit(tm -> false);
@@ -188,6 +189,15 @@ public class AmpleConditionalWriterIT extends 
AccumuloClusterHarness {
 
       assertEquals(Location.current(ts1), 
context.getAmple().readTablet(e1).getLocation());
 
+      // test require absent with a current location set
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      ctmi.mutateTablet(e1).requireAbsentOperation().requireAbsentLocation()
+          .putLocation(Location.future(ts2)).submit(tm -> false);
+      results = ctmi.process();
+      assertEquals(Status.REJECTED, results.get(e1).getStatus());
+
+      assertEquals(Location.current(ts1), 
context.getAmple().readTablet(e1).getLocation());
+
       try (TabletsMetadata tablets =
           context.getAmple().readTablets().forTable(tid).filter(new 
HasCurrentFilter()).build()) {
         List<KeyExtent> actual =
@@ -220,6 +230,95 @@ public class AmpleConditionalWriterIT extends 
AccumuloClusterHarness {
       assertEquals(Status.ACCEPTED, results.get(e1).getStatus());
 
       assertNull(context.getAmple().readTablet(e1).getLocation());
+
+      // Set two current locations, this puts the tablet in a bad state as its 
only expected that
+      // single location should be set
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      
ctmi.mutateTablet(e1).requireAbsentOperation().putLocation(Location.current(ts1))
+          .putLocation(Location.current(ts2)).submit(tm -> false);
+      results = ctmi.process();
+      assertEquals(Status.ACCEPTED, results.get(e1).getStatus());
+
+      // When a tablet has two locations reading it should throw an exception
+      assertThrows(IllegalStateException.class, () -> 
context.getAmple().readTablet(e1));
+
+      // Try to update the tablet requiring one of the locations that is set 
on the tablet. Even
+      // though the required location exists, the presence of the other 
location in the tablet
+      // metadata should cause the update to fail.
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      
ctmi.mutateTablet(e1).requireAbsentOperation().requireLocation(Location.current(ts1))
+          
.deleteLocation(Location.current(ts1)).deleteLocation(Location.current(ts2))
+          .submit(tm -> false);
+      results = ctmi.process();
+      var finalResult1 = results.get(e1);
+      // The update should be rejected because of the two locations. When a 
conditional mutation is
+      // rejected an attempt is made to read the tablet metadata and examine. 
This read of the
+      // tablet metadata will fail because the tablet has two locations.
+      assertThrows(IllegalStateException.class, finalResult1::getStatus);
+
+      // tablet should still have two location set, so reading it should fail
+      assertThrows(IllegalStateException.class, () -> 
context.getAmple().readTablet(e1));
+
+      // Requiring an absent location should fail when two locations are set
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      ctmi.mutateTablet(e1).requireAbsentOperation().requireAbsentLocation()
+          
.deleteLocation(Location.current(ts1)).deleteLocation(Location.current(ts2))
+          .submit(tm -> false);
+      results = ctmi.process();
+      var finalResult2 = results.get(e1);
+      assertThrows(IllegalStateException.class, finalResult2::getStatus);
+
+      // tablet should still have two location set, so reading it should fail
+      assertThrows(IllegalStateException.class, () -> 
context.getAmple().readTablet(e1));
+
+      // Change the tablet to have a futre and current location set.
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      
ctmi.mutateTablet(e1).requireAbsentOperation().deleteLocation(Location.current(ts1))
+          .putLocation(Location.future(ts1)).submit(tm -> false);
+      results = ctmi.process();
+      assertEquals(Status.ACCEPTED, results.get(e1).getStatus());
+
+      // tablet should still have two location set, so reading it should fail
+      assertThrows(IllegalStateException.class, () -> 
context.getAmple().readTablet(e1));
+
+      // Test requiring different locations. Some of the required locations 
are actually set and
+      // some are not. All should fail because the tablet has multiple 
locations set and/or the
+      // required location does not exist.
+      for (var loc : List.of(Location.current(ts1), Location.current(ts2), 
Location.future(ts1),
+          Location.future(ts2))) {
+        ctmi = new ConditionalTabletsMutatorImpl(context);
+        ctmi.mutateTablet(e1).requireAbsentOperation().requireLocation(loc)
+            
.deleteLocation(Location.future(ts1)).deleteLocation(Location.current(ts2))
+            .submit(tm -> false);
+        results = ctmi.process();
+        var finalResult3 = results.get(e1);
+        // tablet should still have two location set, so reading it should fail
+        assertThrows(IllegalStateException.class, finalResult3::getStatus);
+      }
+
+      // Requiring an absent location should fail when a future and current 
location are set
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      ctmi.mutateTablet(e1).requireAbsentOperation().requireAbsentLocation()
+          
.deleteLocation(Location.current(ts1)).deleteLocation(Location.current(ts2))
+          .submit(tm -> false);
+      results = ctmi.process();
+      var finalResult4 = results.get(e1);
+      assertThrows(IllegalStateException.class, finalResult4::getStatus);
+
+      // tablet should still have two location set, so reading it should fail
+      assertThrows(IllegalStateException.class, () -> 
context.getAmple().readTablet(e1));
+
+      // Delete one of the locations w/o any location requirements, this 
should succeed.
+      ctmi = new ConditionalTabletsMutatorImpl(context);
+      
ctmi.mutateTablet(e1).requireAbsentOperation().deleteLocation(Location.future(ts1))
+          .submit(tm -> false);
+      results = ctmi.process();
+      assertEquals(Status.ACCEPTED, results.get(e1).getStatus());
+
+      // This check validates the expected state of the tablet as some of the 
previous test were
+      // catching an exception and making assumption about the state of the 
tablet metadata based on
+      // the fact that an exception was thrown.
+      assertEquals(Location.current(ts2), 
context.getAmple().readTablet(e1).getLocation());
     }
   }
 

Reply via email to