This is an automated email from the ASF dual-hosted git repository. dlmarion 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 e3e4c26e65 Attempt to prevent half-closed Tablet due to failing minc (#3677) e3e4c26e65 is described below commit e3e4c26e653eaa4e324f603db07f2e00ba50bf75 Author: Dave Marion <dlmar...@apache.org> AuthorDate: Fri Aug 11 17:20:27 2023 -0400 Attempt to prevent half-closed Tablet due to failing minc (#3677) This change adds some logic to prevent NPE's, add some minor logging changes, and catches Exception instead of IOException in MinorCompactionTask. The uncaught exceptions in MinorCompactionTask were causing the minor compaction thread to die preventing future minor compactions from starting. This commit also adds 4 tests for different failure scenarios that were seen when configuring an invalid context or a bad iterator. Fixes #3674 Co-authored-by: Ed Coleman <edcole...@apache.org> Co-authored-by: dtspence <dtspe...@users.noreply.github.com> Co-authored-by: Christopher Tubbs <ctubb...@apache.org> Co-authored-by: Keith Turner <ktur...@apache.org> --- .../core/conf/ConfigurationTypeHelper.java | 2 +- .../accumulo/server/conf/TableConfiguration.java | 8 + .../accumulo/tserver/TabletClientHandler.java | 3 + .../accumulo/tserver/tablet/CompactableImpl.java | 7 + .../tserver/tablet/MinorCompactionTask.java | 9 +- .../org/apache/accumulo/tserver/tablet/Tablet.java | 3 + .../test/functional/HalfClosedTablet2IT.java | 121 +++++++++ .../test/functional/HalfClosedTabletIT.java | 282 +++++++++++++++++++++ 8 files changed, 430 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/conf/ConfigurationTypeHelper.java b/core/src/main/java/org/apache/accumulo/core/conf/ConfigurationTypeHelper.java index 8600cccaa9..8893ae8ce9 100644 --- a/core/src/main/java/org/apache/accumulo/core/conf/ConfigurationTypeHelper.java +++ b/core/src/main/java/org/apache/accumulo/core/conf/ConfigurationTypeHelper.java @@ -174,7 +174,7 @@ public class ConfigurationTypeHelper { try { instance = getClassInstance(context, clazzName, base); } catch (RuntimeException | IOException | ReflectiveOperationException e) { - log.warn("Failed to load class {} in classloader context {}", clazzName, context, e); + log.error("Failed to load class {} in classloader context {}", clazzName, context, e); } if (instance == null && defaultInstance != null) { diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java index b793719cb2..138bcf2ce7 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java +++ b/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java @@ -178,6 +178,14 @@ public class TableConfiguration extends ZooBasedConfiguration { CompactionDispatcher newDispatcher = Property.createTableInstanceFromPropertyName(conf, Property.TABLE_COMPACTION_DISPATCHER, CompactionDispatcher.class, null); + if (newDispatcher == null) { + // return early to prevent NPE + log.error( + "Null returned for compaction dispatcher for table: {}. Did not return default value, check server log.", + tableId); + return null; + } + Map<String,String> opts = conf.getAllPropertiesWithPrefixStripped(Property.TABLE_COMPACTION_DISPATCHER_OPTS); 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 c557905f7b..92c06c85e1 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 @@ -1009,6 +1009,9 @@ public class TabletClientHandler implements TabletClientService.Iface { } catch (IOException e) { log.warn("Failed to split " + keyExtent, e); throw new RuntimeException(e); + } catch (RuntimeException re) { + log.warn("Failed to split " + keyExtent, re); + throw re; } } } diff --git a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/CompactableImpl.java b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/CompactableImpl.java index f020657bed..852ba4d638 100644 --- a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/CompactableImpl.java +++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/CompactableImpl.java @@ -1473,6 +1473,13 @@ public class CompactableImpl implements Compactable { try { var dispatcher = tablet.getTableConfiguration().getCompactionDispatcher(); + if (dispatcher == null) { + log.error( + "Failed to dispatch compaction {} kind:{} hints:{}, falling back to {} service. Unable to instantiate dispatcher plugin. Check server log.", + getExtent(), kind, debugHints, CompactionServicesConfig.DEFAULT_SERVICE); + return CompactionServicesConfig.DEFAULT_SERVICE; + } + Map<String,String> tmpHints = Map.of(); if (kind == CompactionKind.USER) { diff --git a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/MinorCompactionTask.java b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/MinorCompactionTask.java index 32c4ee7a87..f190d79460 100644 --- a/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/MinorCompactionTask.java +++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/tablet/MinorCompactionTask.java @@ -18,8 +18,6 @@ */ package org.apache.accumulo.tserver.tablet; -import java.io.IOException; - import org.apache.accumulo.core.metadata.TabletFile; import org.apache.accumulo.core.metadata.schema.DataFileValue; import org.apache.accumulo.core.trace.TraceUtil; @@ -90,8 +88,11 @@ class MinorCompactionTask implements Runnable { tablet.getTabletServer().minorCompactionStarted(commitSession, commitSession.getWALogSeq() + 1, newFile.getMetaInsert()); break; - } catch (IOException e) { - // An IOException could have occurred while creating the new file + } catch (Exception e) { + // Catching Exception here rather than something more specific as we can't allow the + // MinorCompactionTask to exit and the thread to die. Tablet.minorCompact *must* be + // called and *must* complete else no future minor compactions can be performed on + // this Tablet. if (newFile == null) { log.warn("Failed to create new file for minor compaction {}", e.getMessage(), e); } else { 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 d0d71bf722..31e9df9b89 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 @@ -1594,6 +1594,9 @@ public class Tablet extends TabletBase { } catch (IllegalStateException ise) { log.debug("File {} not splitting : {}", extent, ise.getMessage()); return null; + } catch (RuntimeException re) { + log.debug("File {} not splitting : {}", extent, re.getMessage()); + throw re; } // obtain this info outside of synch block since it will involve opening diff --git a/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTablet2IT.java b/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTablet2IT.java new file mode 100644 index 0000000000..187feff222 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTablet2IT.java @@ -0,0 +1,121 @@ +/* + * 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.functional; + +import java.io.File; + +import org.apache.accumulo.core.client.Accumulo; +import org.apache.accumulo.core.conf.Property; +import org.apache.accumulo.core.data.Mutation; +import org.apache.accumulo.core.data.TableId; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.spi.fs.DelegatingChooser; +import org.apache.accumulo.core.spi.fs.PreferredVolumeChooser; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.SharedMiniClusterBase; +import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl; +import org.apache.accumulo.test.util.Wait; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +// This covers issues like that reported in https://github.com/apache/accumulo/issues/3674 +// where a failing minor compaction leaves the Tablet in a half-closed state that prevents it +// from unloading and the TServer from shutting down normally. +// This test recreates that scenario by setting an invalid context and verifies that the +// tablet can recover and unload after the context is set correctly. +public class HalfClosedTablet2IT extends SharedMiniClusterBase { + + public static class HalfClosedTablet2ITConfiguration implements MiniClusterConfigurationCallback { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + cfg.setNumTservers(1); + cfg.setProperty(Property.GENERAL_VOLUME_CHOOSER, DelegatingChooser.class.getName()); + cfg.setProperty("general.custom.volume.chooser.default", + PreferredVolumeChooser.class.getName()); + cfg.setProperty("general.custom.volume.preferred.default", + new File(cfg.getDir().getAbsolutePath(), "/accumulo").toURI().toString()); + } + } + + @BeforeAll + public static void startup() throws Exception { + SharedMiniClusterBase.startMiniClusterWithConfig(new HalfClosedTablet2ITConfiguration()); + } + + @AfterAll + public static void shutdown() throws Exception { + SharedMiniClusterBase.stopMiniCluster(); + } + + @Test + public void testInvalidContextCausesVolumeChooserFailure() throws Exception { + + // In this scenario an invalid context causes the VolumeChooser impl to not + // be loaded, which causes the MinorCompactionTask to fail to create an + // output file. This failure previously caused the minor compaction thread to die. + + String tableName = getUniqueNames(1)[0]; + try (final var client = Accumulo.newClient().from(getClientProps()).build()) { + + final var tops = client.tableOperations(); + tops.create(tableName); + TableId tableId = TableId.of(tops.tableIdMap().get(tableName)); + + try (final var bw = client.createBatchWriter(tableName)) { + final var m1 = new Mutation("a"); + final var m2 = new Mutation("b"); + m1.put(new Text("cf"), new Text(), new Value()); + m2.put(new Text("cf"), new Text(), new Value()); + bw.addMutation(m1); + bw.addMutation(m2); + } + + HalfClosedTabletIT.setInvalidClassLoaderContextPropertyWithoutValidation( + getCluster().getServerContext(), tableId); + + // Need to wait for TabletServer to pickup configuration change + Thread.sleep(3000); + + tops.flush(tableName); + + // minc should fail until invalid context is removed, so there should be no files + FunctionalTestUtils.checkRFiles(client, tableName, 1, 1, 0, 0); + + HalfClosedTabletIT.removeInvalidClassLoaderContextProperty(client, tableName); + + // Minc should have completed successfully + Wait.waitFor(() -> HalfClosedTabletIT.tabletHasExpectedRFiles(client, tableName, 1, 1, 1, 1), + 340_000); + + // offline the table which will unload the tablets. If the context property is not + // removed above, then this test will fail because the tablets will not be able to be + // unloaded + tops.offline(tableName); + + Wait.waitFor(() -> HalfClosedTabletIT.countHostedTablets(client, tableId) == 0L, 340_000); + + } + + } + +} diff --git a/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTabletIT.java b/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTabletIT.java new file mode 100644 index 0000000000..8f6d907e96 --- /dev/null +++ b/test/src/main/java/org/apache/accumulo/test/functional/HalfClosedTabletIT.java @@ -0,0 +1,282 @@ +/* + * 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.functional; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.accumulo.core.client.Accumulo; +import org.apache.accumulo.core.client.AccumuloClient; +import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; +import org.apache.accumulo.core.client.BatchWriter; +import org.apache.accumulo.core.client.IteratorSetting; +import org.apache.accumulo.core.client.admin.CompactionConfig; +import org.apache.accumulo.core.clientImpl.ClientContext; +import org.apache.accumulo.core.conf.Property; +import org.apache.accumulo.core.data.Mutation; +import org.apache.accumulo.core.data.TableId; +import org.apache.accumulo.core.data.Value; +import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope; +import org.apache.accumulo.core.metadata.schema.TabletMetadata.ColumnType; +import org.apache.accumulo.core.metadata.schema.TabletsMetadata; +import org.apache.accumulo.core.util.UtilWaitThread; +import org.apache.accumulo.harness.MiniClusterConfigurationCallback; +import org.apache.accumulo.harness.SharedMiniClusterBase; +import org.apache.accumulo.miniclusterImpl.MiniAccumuloConfigImpl; +import org.apache.accumulo.server.ServerContext; +import org.apache.accumulo.server.conf.store.TablePropKey; +import org.apache.accumulo.test.util.Wait; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.Sets; + +// This IT tests the cases seen in https://github.com/apache/accumulo/issues/3674 +// where a failing minor compaction causes a Tablet.initiateClose to leave the +// Tablet in a half-closed state. The half-closed Tablet cannot be unloaded and +// the TabletServer cannot be shutdown normally. Because the minor compaction has +// been failing the Tablet needs to be recovered when it's ultimately re-assigned. +// +public class HalfClosedTabletIT extends SharedMiniClusterBase { + + public static class HalfClosedTabletITConfiguration implements MiniClusterConfigurationCallback { + + @Override + public void configureMiniCluster(MiniAccumuloConfigImpl cfg, Configuration coreSite) { + cfg.setNumTservers(1); + } + + } + + @BeforeAll + public static void startup() throws Exception { + SharedMiniClusterBase.startMiniClusterWithConfig(new HalfClosedTabletITConfiguration()); + } + + @AfterAll + public static void shutdown() throws Exception { + SharedMiniClusterBase.stopMiniCluster(); + } + + @Test + public void testSplitWithInvalidContext() throws Exception { + + // In this scenario the table has been mis-configured with an invalid context name. + // The minor compaction task is failing because classes like the volume chooser or + // user iterators cannot be loaded. The user calls Tablet.split which calls initiateClose. + // This test ensures that the Tablet can still be unloaded normally by taking if offline + // after the split call with an invalid context. The context property is removed after the + // split call below to get the minor compaction task to succeed on a subsequent run. Because + // the minor compaction task backs off when retrying, this could take some time. + + String tableName = getUniqueNames(1)[0]; + + try (final var client = Accumulo.newClient().from(getClientProps()).build()) { + final var tops = client.tableOperations(); + tops.create(tableName); + TableId tableId = TableId.of(tops.tableIdMap().get(tableName)); + try (final var bw = client.createBatchWriter(tableName)) { + final var m1 = new Mutation("a"); + final var m2 = new Mutation("b"); + m1.put(new Text("cf"), new Text(), new Value()); + m2.put(new Text("cf"), new Text(), new Value()); + bw.addMutation(m1); + bw.addMutation(m2); + } + + setInvalidClassLoaderContextPropertyWithoutValidation(getCluster().getServerContext(), + tableId); + + // Need to wait for TabletServer to pickup configuration change + Thread.sleep(3000); + + Thread configFixer = new Thread(() -> { + UtilWaitThread.sleep(3000); + removeInvalidClassLoaderContextProperty(client, tableName); + }); + + long t1 = System.nanoTime(); + configFixer.start(); + + // The split will probably start running w/ bad config that will cause it to get stuck. + // However once the config is fixed by the background thread it should continue. + tops.addSplits(tableName, Sets.newTreeSet(List.of(new Text("b")))); + + long t2 = System.nanoTime(); + // expect that split took at least 3 seconds because that is the time it takes to fix the + // config + assertTrue(TimeUnit.NANOSECONDS.toMillis(t2 - t1) >= 3000); + + // offline the table which will unload the tablets. If the context property is not + // removed above, then this test will fail because the tablets will not be able to be + // unloaded + tops.offline(tableName); + + Wait.waitFor(() -> countHostedTablets(client, tableId) == 0L, 340_000); + } + } + + @Test + public void testIteratorThrowingTransientError() throws Exception { + + // In this scenario a minc iterator throws an error some number of time, then + // succeeds. We want to verify that the minc is being retried and the tablet + // can be closed. + + try (AccumuloClient c = Accumulo.newClient().from(getClientProps()).build()) { + + String tableName = getUniqueNames(1)[0]; + final var tops = c.tableOperations(); + + tops.create(tableName); + final var tid = TableId.of(tops.tableIdMap().get(tableName)); + + try (BatchWriter bw = c.createBatchWriter(tableName)) { + Mutation m = new Mutation(new Text("r1")); + m.put("acf", tableName, "1"); + bw.addMutation(m); + } + + IteratorSetting setting = new IteratorSetting(50, "error", ErrorThrowingIterator.class); + setting.addOption(ErrorThrowingIterator.TIMES, "3"); + c.tableOperations().attachIterator(tableName, setting, EnumSet.of(IteratorScope.minc)); + c.tableOperations().compact(tableName, new CompactionConfig().setWait(true).setFlush(true)); + + // Taking the table offline should succeed normally + tops.offline(tableName); + + // Minc should have completed successfully + Wait.waitFor(() -> tabletHasExpectedRFiles(c, tableName, 1, 1, 1, 1), 340_000); + + Wait.waitFor(() -> countHostedTablets(c, tid) == 0L, 340_000); + + } + } + + // Note that these tests can talk several minutes each because by the time the test + // code changes the configuration, the minc has failed so many times that the minc + // is waiting for a few minutes before trying again. For example, I saw this backoff + // timing: + // + // DEBUG: MinC failed sleeping 169 ms before retrying + // DEBUG: MinC failed sleeping 601 ms before retrying + // DEBUG: MinC failed sleeping 2004 ms before retrying + // DEBUG: MinC failed sleeping 11891 ms before retrying + // DEBUG: MinC failed sleeping 43156 ms before retrying + // DEBUG: MinC failed sleeping 179779 ms before retrying + @Test + public void testBadIteratorOnStack() throws Exception { + + // In this scenario the table is using an iterator for minc that is throwing an exception. + // This test ensures that the Tablet can still be unloaded normally by taking if offline + // after the bad iterator has been removed from the minc configuration. + + try (AccumuloClient c = Accumulo.newClient().from(getClientProps()).build()) { + + String tableName = getUniqueNames(1)[0]; + final var tops = c.tableOperations(); + + tops.create(tableName); + final var tid = TableId.of(tops.tableIdMap().get(tableName)); + + IteratorSetting is = new IteratorSetting(30, BadIterator.class); + c.tableOperations().attachIterator(tableName, is, EnumSet.of(IteratorScope.minc)); + + try (BatchWriter bw = c.createBatchWriter(tableName)) { + Mutation m = new Mutation(new Text("r1")); + m.put("acf", tableName, "1"); + bw.addMutation(m); + } + + c.tableOperations().flush(tableName, null, null, false); + + UtilWaitThread.sleepUninterruptibly(5, TimeUnit.SECONDS); + + // minc should fail, so there should be no files + FunctionalTestUtils.checkRFiles(c, tableName, 1, 1, 0, 0); + + // tell the server to take the table offline + tops.offline(tableName); + + // The offine operation should not be able to complete because the tablet can not minor + // compact, give the offline operation a bit of time to attempt to complete even though it + // should never be able to complete. + UtilWaitThread.sleepUninterruptibly(5, TimeUnit.SECONDS); + + assertTrue(countHostedTablets(c, tid) > 0); + + // minc should fail, so there should be no files + FunctionalTestUtils.checkRFiles(c, tableName, 1, 1, 0, 0); + + // remove the bad iterator. The failing minc task is in a backoff retry loop + // and should pick up this change on the next try + c.tableOperations().removeIterator(tableName, BadIterator.class.getSimpleName(), + EnumSet.of(IteratorScope.minc)); + + // Minc should have completed successfully + Wait.waitFor(() -> tabletHasExpectedRFiles(c, tableName, 1, 1, 1, 1), 340_000); + + // The previous operation to offline the table should be able to succeed after the minor + // compaction completed + Wait.waitFor(() -> countHostedTablets(c, tid) == 0L, 340_000); + } + } + + public static void setInvalidClassLoaderContextPropertyWithoutValidation(ServerContext context, + TableId tableId) { + TablePropKey key = TablePropKey.of(context, tableId); + context.getPropStore().putAll(key, + Map.of(Property.TABLE_CLASSLOADER_CONTEXT.getKey(), "invalid")); + } + + public static void removeInvalidClassLoaderContextProperty(AccumuloClient client, + String tableName) { + try { + client.tableOperations().removeProperty(tableName, + Property.TABLE_CLASSLOADER_CONTEXT.getKey()); + } catch (AccumuloException | AccumuloSecurityException e) { + throw new RuntimeException(e); + } + } + + public static boolean tabletHasExpectedRFiles(AccumuloClient c, String tableName, int minTablets, + int maxTablets, int minRFiles, int maxRFiles) { + try { + FunctionalTestUtils.checkRFiles(c, tableName, minTablets, maxTablets, minRFiles, maxRFiles); + return true; + } catch (Exception e) { + return false; + } + } + + public static long countHostedTablets(AccumuloClient c, TableId tid) { + try (TabletsMetadata tm = ((ClientContext) c).getAmple().readTablets().forTable(tid) + .fetch(ColumnType.LOCATION).build()) { + return tm.stream().count(); + } + } +}