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 <[email protected]>
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());
}
}