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 351f4a4ae3 adds tests for tablet adding new file to metadata (#4617) 351f4a4ae3 is described below commit 351f4a4ae3bbb781d80297047daf8623abc44454 Author: Keith Turner <ktur...@apache.org> AuthorDate: Thu May 30 17:12:05 2024 -0400 adds tests for tablet adding new file to metadata (#4617) --- .../org/apache/accumulo/tserver/tablet/Tablet.java | 24 +- .../test/ample/usage/TabletFileUpdateIT.java | 268 +++++++++++++++++++++ 2 files changed, 284 insertions(+), 8 deletions(-) diff --git a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/Tablet.java b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/Tablet.java index 0747a10867..14d9aabe79 100644 --- a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/Tablet.java +++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/Tablet.java @@ -67,6 +67,7 @@ import org.apache.accumulo.core.manager.state.tables.TableState; import org.apache.accumulo.core.metadata.AccumuloTable; import org.apache.accumulo.core.metadata.ReferencedTabletFile; import org.apache.accumulo.core.metadata.StoredTabletFile; +import org.apache.accumulo.core.metadata.TServerInstance; import org.apache.accumulo.core.metadata.schema.Ample; import org.apache.accumulo.core.metadata.schema.Ample.ConditionalResult; import org.apache.accumulo.core.metadata.schema.Ample.ConditionalResult.Status; @@ -111,6 +112,7 @@ import org.apache.zookeeper.KeeperException.NoNodeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; @@ -1343,7 +1345,7 @@ public class Tablet extends TabletBase { /** * Update tablet file data from flush. Returns a StoredTabletFile if there are data entries. */ - public Optional<StoredTabletFile> updateTabletDataFile(long maxCommittedTime, + private Optional<StoredTabletFile> updateTabletDataFile(long maxCommittedTime, ReferencedTabletFile newDatafile, DataFileValue dfv, Set<LogEntry> unusedWalLogs, long flushId, MinorCompactionReason mincReason) { @@ -1353,12 +1355,21 @@ public class Tablet extends TabletBase { // code is locking properly these should not change during this method. var lastTabletMetadata = getMetadata(); + return updateTabletDataFile(getContext().getAmple(), maxCommittedTime, newDatafile, dfv, + unusedWalLogs, flushId, mincReason, tabletServer.getTabletSession(), extent, + lastTabletMetadata, tabletTime, RANDOM.get().nextLong()); + } + + @VisibleForTesting + public static Optional<StoredTabletFile> updateTabletDataFile(Ample ample, long maxCommittedTime, + ReferencedTabletFile newDatafile, DataFileValue dfv, Set<LogEntry> unusedWalLogs, + long flushId, MinorCompactionReason mincReason, TServerInstance tserverInstance, + KeyExtent extent, TabletMetadata lastTabletMetadata, TabletTime tabletTime, long flushNonce) { while (true) { - try (var tabletsMutator = getContext().getAmple().conditionallyMutateTablets()) { + try (var tabletsMutator = ample.conditionallyMutateTablets()) { var expectedLocation = mincReason == MinorCompactionReason.RECOVERY - ? Location.future(tabletServer.getTabletSession()) - : Location.current(tabletServer.getTabletSession()); + ? Location.future(tserverInstance) : Location.current(tserverInstance); var tablet = tabletsMutator.mutateTablet(extent).requireLocation(expectedLocation); @@ -1382,7 +1393,6 @@ public class Tablet extends TabletBase { tablet.putFlushId(flushId); - long flushNonce = RANDOM.get().nextLong(); tablet.putFlushNonce(flushNonce); unusedWalLogs.forEach(tablet::deleteWal); @@ -1391,7 +1401,6 @@ public class Tablet extends TabletBase { // Can not check if the new file exists because of two reasons. First, it could be compacted // away between the write and check. Second, some flushes do not produce a file. tablet.submit(tabletMetadata -> { - // ELASTICITY_TODO need to test this, need a general way of testing these failure checks var persistedNonce = tabletMetadata.getFlushNonce(); if (persistedNonce.isPresent()) { return persistedNonce.getAsLong() == flushNonce; @@ -1400,13 +1409,12 @@ public class Tablet extends TabletBase { }); var result = tabletsMutator.process().get(extent); - if (result.getStatus() == Ample.ConditionalResult.Status.ACCEPTED) { + if (result.getStatus() == Status.ACCEPTED) { return newFile; } else { var updatedTableMetadata = result.readMetadata(); if (setTime && expectedLocation.equals(updatedTableMetadata.getLocation()) && !lastTabletMetadata.getTime().equals(updatedTableMetadata.getTime())) { - // ELASTICITY_TODO need to test this // The update failed because the time changed, so lets try again. log.debug("Failed to add {} to {} because time changed {}!={}, will retry", newFile, extent, lastTabletMetadata.getTime(), updatedTableMetadata.getTime()); diff --git a/test/src/main/java/org/apache/accumulo/test/ample/usage/TabletFileUpdateIT.java b/test/src/main/java/org/apache/accumulo/test/ample/usage/TabletFileUpdateIT.java new file mode 100644 index 0000000000..abc9242b58 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/ample/usage/TabletFileUpdateIT.java @@ -0,0 +1,268 @@ +/* + * 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.test.ample.usage; + +import static org.apache.accumulo.core.client.ConditionalWriter.Status.ACCEPTED; +import static org.apache.accumulo.core.client.ConditionalWriter.Status.UNKNOWN; +import static org.apache.accumulo.test.ample.metadata.ConditionalWriterInterceptor.withStatus; +import static org.apache.accumulo.tserver.tablet.Tablet.updateTabletDataFile; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.accumulo.core.client.Accumulo; +import org.apache.accumulo.core.client.admin.TimeType; +import org.apache.accumulo.core.data.TableId; +import org.apache.accumulo.core.dataImpl.KeyExtent; +import org.apache.accumulo.core.metadata.ReferencedTabletFile; +import org.apache.accumulo.core.metadata.TServerInstance; +import org.apache.accumulo.core.metadata.schema.Ample; +import org.apache.accumulo.core.metadata.schema.DataFileValue; +import org.apache.accumulo.core.metadata.schema.MetadataTime; +import org.apache.accumulo.core.metadata.schema.TabletMetadata.Location; +import org.apache.accumulo.harness.SharedMiniClusterBase; +import org.apache.accumulo.server.tablets.TabletTime; +import org.apache.accumulo.test.ample.metadata.TestAmple; +import org.apache.accumulo.tserver.MinorCompactionReason; +import org.apache.hadoop.fs.Path; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests tablet usage of ample to add a new minor compacted file to tablet. This tests edge cases, + * the normal cases are well tested by many other ITs from simply running Accumulo. + */ +public class TabletFileUpdateIT extends SharedMiniClusterBase { + + @BeforeAll + public static void setup() throws Exception { + SharedMiniClusterBase.startMiniCluster(); + } + + @AfterAll + public static void teardown() { + SharedMiniClusterBase.stopMiniCluster(); + } + + private static final ReferencedTabletFile newFile = + ReferencedTabletFile.of(new Path("file:///accumulo/tables/t-0/b-0/F1.rf")); + private static final DataFileValue dfv1 = new DataFileValue(1000, 100); + private static final TServerInstance tserverInstance = + new TServerInstance("localhost:9997", 0xabcdef123L); + private static final TableId tableId = TableId.of("99"); + private static final KeyExtent extent = new KeyExtent(tableId, null, null); + + /** + * This test ensures that a tablet handles tablet time changing externally (like bulk import could + * change tablet time). + */ + @Test + public void testTimeChanged() throws Exception { + String[] tableNames = getUniqueNames(1); + String metadataTable = tableNames[0]; + + var tabletTime = TabletTime.getInstance(new MetadataTime(1, TimeType.LOGICAL)); + long flushNonce = 42L; + + try (var client = Accumulo.newClient().from(getClientProps()).build()) { + client.tableOperations().create(metadataTable); + TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + Ample testAmple = TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + assertNull(testAmple.readTablet(extent)); + + // create tablet in the fake metadata table + testAmple.mutateTablet(extent).putDirName("dir1").putTime(tabletTime.getMetadataTime()) + .putLocation(Location.current(tserverInstance)).putPrevEndRow(null).mutate(); + + // get the tablets metadata + var lastMetadata = testAmple.readTablet(extent); + + // update the tablets time, so it will differ + testAmple.mutateTablet(extent).putTime(tabletTime.getMetadataTime(tabletTime.getTime() + 2)) + .mutate(); + + tabletTime.updateTimeIfGreater(tabletTime.getTime() + 6); + + updateTabletDataFile(testAmple, tabletTime.getTime(), newFile, dfv1, Set.of(), 7L, + MinorCompactionReason.SYSTEM, tserverInstance, extent, lastMetadata, tabletTime, + flushNonce); + + var updatedMetadata = testAmple.readTablet(extent); + assertEquals(Set.of(newFile.insert()), updatedMetadata.getFiles()); + assertEquals(tabletTime.getMetadataTime(), updatedMetadata.getTime()); + assertEquals(flushNonce, updatedMetadata.getFlushNonce().orElse(-1)); + + client.tableOperations().delete(metadataTable); + } + } + + @Test + public void testNoTimeUpdate() throws Exception { + String[] tableNames = getUniqueNames(1); + String metadataTable = tableNames[0]; + + var tabletTime = TabletTime.getInstance(new MetadataTime(10, TimeType.LOGICAL)); + long flushNonce = 42L; + + try (var client = Accumulo.newClient().from(getClientProps()).build()) { + client.tableOperations().create(metadataTable); + TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + Ample testAmple = TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + assertNull(testAmple.readTablet(extent)); + + // create tablet in the fake metadata table + testAmple.mutateTablet(extent).putDirName("dir1").putTime(tabletTime.getMetadataTime()) + .putLocation(Location.current(tserverInstance)).putPrevEndRow(null).mutate(); + + // get the tablets metadata + var lastMetadata = testAmple.readTablet(extent); + + var maxCommittedTime = tabletTime.getTime() + 2; + + // Update the tablets time, simulating concurrent bulk imports updating the time field + // externally + testAmple.mutateTablet(extent).putTime(tabletTime.getMetadataTime(tabletTime.getTime() + 4)) + .mutate(); + + // Test the case where external bulk imports have pushed tablet time in the metadata table + // higher than what was passed the function. In this case the time should not be updated. + updateTabletDataFile(testAmple, maxCommittedTime, newFile, dfv1, Set.of(), 7L, + MinorCompactionReason.SYSTEM, tserverInstance, extent, lastMetadata, tabletTime, + flushNonce); + + var updatedMetadata = testAmple.readTablet(extent); + assertEquals(Set.of(newFile.insert()), updatedMetadata.getFiles()); + // the time passed to updateTabletDataFile should not have been set because it was lower than + // what is in the metadata table + assertEquals(tabletTime.getMetadataTime(tabletTime.getTime() + 4), updatedMetadata.getTime()); + assertEquals(flushNonce, updatedMetadata.getFlushNonce().orElse(-1)); + + client.tableOperations().delete(metadataTable); + } + } + + /** + * This test ensures that a tablet will not add a new minor compacted file when the tablets + * location is not as expected. + */ + @Test + public void testLocation() throws Exception { + String[] tableNames = getUniqueNames(1); + String metadataTable = tableNames[0]; + + var tabletTime = TabletTime.getInstance(new MetadataTime(1, TimeType.LOGICAL)); + long flushNonce = 42L; + + try (var client = Accumulo.newClient().from(getClientProps()).build()) { + client.tableOperations().create(metadataTable); + TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + Ample testAmple = TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + assertNull(testAmple.readTablet(extent)); + + // create tablet in the fake metadata table + testAmple.mutateTablet(extent).putDirName("dir1").putTime(tabletTime.getMetadataTime()) + .putLocation(Location.current(tserverInstance)).putPrevEndRow(null).mutate(); + + // get the tablets metadata + var lastMetadata = testAmple.readTablet(extent); + + testAmple.mutateTablet(extent).deleteLocation(Location.current(tserverInstance)).mutate(); + + // try locations that differ in type, port, and session + for (var location : List.of(Location.future(tserverInstance), + Location.current(new TServerInstance("localhost:9998", 0xabcdef123L)), + Location.current(new TServerInstance("localhost:9997", 0xabcdef124L)))) { + // set a location on the tablet that will not match + testAmple.mutateTablet(extent).putLocation(location).mutate(); + // should fail to add file to tablet because tablet location is not as expected + assertThrows(IllegalStateException.class, + () -> updateTabletDataFile(testAmple, tabletTime.getTime(), newFile, dfv1, Set.of(), 7L, + MinorCompactionReason.SYSTEM, tserverInstance, extent, lastMetadata, tabletTime, + flushNonce)); + // verify that files was not added + var updatedMetadata = testAmple.readTablet(extent); + assertEquals(Set.of(), updatedMetadata.getFiles()); + testAmple.mutateTablet(extent).deleteLocation(location).mutate(); + } + + client.tableOperations().delete(metadataTable); + } + } + + /** + * This test exercises conditional mutations returning unknown and this causing reading of the + * flush nonce. + */ + @Test + public void testFlushNonce() throws Exception { + String[] tableNames = getUniqueNames(1); + String metadataTable = tableNames[0]; + + var tabletTime = TabletTime.getInstance(new MetadataTime(1, TimeType.LOGICAL)); + long flushNonce = 42L; + + try (var client = Accumulo.newClient().from(getClientProps()).build()) { + client.tableOperations().create(metadataTable); + TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable)); + + // create test ample that will return unknown status for conditional mutations. + Ample testAmple = TestAmple.create(getCluster().getServerContext(), + Map.of(Ample.DataLevel.USER, metadataTable), () -> withStatus(ACCEPTED, UNKNOWN, 1)); + + assertNull(testAmple.readTablet(extent)); + + // create tablet in the fake metadata table + testAmple.mutateTablet(extent).putDirName("dir1").putTime(tabletTime.getMetadataTime()) + .putLocation(Location.current(tserverInstance)).putPrevEndRow(null).mutate(); + + // get the tablets metadata + var lastMetadata = testAmple.readTablet(extent); + + // This should get an unknown status and then check the flush nonce + updateTabletDataFile(testAmple, tabletTime.getTime() + 7, newFile, dfv1, Set.of(), 7L, + MinorCompactionReason.SYSTEM, tserverInstance, extent, lastMetadata, tabletTime, + flushNonce); + + var updatedMetadata = testAmple.readTablet(extent); + assertEquals(Set.of(newFile.insert()), updatedMetadata.getFiles()); + assertEquals(tabletTime.getMetadataTime(8), updatedMetadata.getTime()); + assertEquals(flushNonce, updatedMetadata.getFlushNonce().orElse(-1)); + + client.tableOperations().delete(metadataTable); + } + } +}