From 1c31d7d364b8996801265f5611b6568d45595184 Mon Sep 17 00:00:00 2001 From: Peng Lu Date: Wed, 10 Jun 2026 19:02:37 +0800 Subject: [PATCH 1/2] FNFE may occur when executing `hbase snapshot info --list-snapshots` --- .../hadoop/hbase/snapshot/SnapshotInfo.java | 3 + .../master/snapshot/TestSnapshotInfo.java | 104 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java index 151319e165a2..c74494867d8c 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java @@ -628,6 +628,9 @@ public static List getSnapshotList(final Configuration conf Path rootDir = CommonFSUtils.getRootDir(conf); FileSystem fs = FileSystem.get(rootDir.toUri(), conf); Path snapshotDir = SnapshotDescriptionUtils.getSnapshotsDir(rootDir); + if (!fs.exists(snapshotDir)) { + return Collections.emptyList(); + } FileStatus[] snapshots = fs.listStatus(snapshotDir, new SnapshotDescriptionUtils.CompletedSnaphotDirectoriesFilter(fs)); List snapshotLists = new ArrayList<>(snapshots.length); diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java new file mode 100644 index 000000000000..46489040d068 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java @@ -0,0 +1,104 @@ +/* + * 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 + * + * http://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.hadoop.hbase.master.snapshot; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.List; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseTestingUtil; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Admin; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; +import org.apache.hadoop.hbase.client.SnapshotDescription; +import org.apache.hadoop.hbase.client.TableDescriptorBuilder; +import org.apache.hadoop.hbase.snapshot.SnapshotDescriptionUtils; +import org.apache.hadoop.hbase.snapshot.SnapshotInfo; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.util.Bytes; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +@Tag(MasterTests.TAG) +@Tag(MediumTests.TAG) +public class TestSnapshotInfo { + private final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil(); + + private Path rootDir; + private FileSystem fs; + private Configuration conf; + private Admin admin; + private String currentTestName; + + @BeforeEach + public void setup(TestInfo testInfo) throws Exception { + TEST_UTIL.startMiniCluster(1); + rootDir = TEST_UTIL.getDefaultRootDirPath(); + fs = TEST_UTIL.getTestFileSystem(); + conf = TEST_UTIL.getConfiguration(); + admin = TEST_UTIL.getAdmin(); + currentTestName = testInfo.getTestMethod().get().getName(); + } + + @AfterEach + public void tearDown() throws IOException { + admin.close(); + TEST_UTIL.shutdownMiniCluster(); + TEST_UTIL.getTestFileSystem().delete(TEST_UTIL.getDataTestDir(), true); + } + + @Test + public void testGetSnapshotList() throws IOException { + Path snapshotDir = SnapshotDescriptionUtils.getSnapshotsDir(rootDir); + + // HBase cluster has just started, .hbase-snapshot directory doesn't exist + assertFalse(fs.exists(snapshotDir)); + List snapshotDescList = SnapshotInfo.getSnapshotList(conf); + assertTrue(snapshotDescList.isEmpty()); + + TableName tableName = TableName.valueOf(currentTestName); + TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tableName); + ColumnFamilyDescriptor columnFamilyDescriptor = + ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("info")).build(); + tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor); + admin.createTable(tableDescriptorBuilder.build()); + assertTrue(admin.tableExists(tableName)); + String snapshotName = "snapshot_" + currentTestName; + admin.snapshot(snapshotName, tableName); + + // .hbase-snapshot directory exists after snapshotting a table + assertTrue(fs.exists(snapshotDir)); + snapshotDescList = SnapshotInfo.getSnapshotList(conf); + assertFalse(snapshotDescList.isEmpty()); + + // Deleting snapshot and cluster has no snapshots, .hbase-snapshot directory would still exist + admin.deleteSnapshot(snapshotName); + assertTrue(fs.exists(snapshotDir)); + snapshotDescList = SnapshotInfo.getSnapshotList(conf); + assertTrue(snapshotDescList.isEmpty()); + } +} From 86b7a13e02310a38edcca7c5f717dd12088693ec Mon Sep 17 00:00:00 2001 From: Peng Lu Date: Sat, 13 Jun 2026 00:23:14 +0800 Subject: [PATCH 2/2] Update code based the feedback --- .../java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java | 6 +++--- .../hadoop/hbase/master/snapshot/TestSnapshotInfo.java | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java index c74494867d8c..0a57aeb4c02e 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/SnapshotInfo.java @@ -628,11 +628,11 @@ public static List getSnapshotList(final Configuration conf Path rootDir = CommonFSUtils.getRootDir(conf); FileSystem fs = FileSystem.get(rootDir.toUri(), conf); Path snapshotDir = SnapshotDescriptionUtils.getSnapshotsDir(rootDir); - if (!fs.exists(snapshotDir)) { + FileStatus[] snapshots = CommonFSUtils.listStatus(fs, snapshotDir, + new SnapshotDescriptionUtils.CompletedSnaphotDirectoriesFilter(fs)); + if (snapshots == null) { return Collections.emptyList(); } - FileStatus[] snapshots = fs.listStatus(snapshotDir, - new SnapshotDescriptionUtils.CompletedSnaphotDirectoriesFilter(fs)); List snapshotLists = new ArrayList<>(snapshots.length); for (FileStatus snapshotDirStat : snapshots) { SnapshotProtos.SnapshotDescription snapshotDesc = diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java index 46489040d068..3fb997907aa4 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/snapshot/TestSnapshotInfo.java @@ -98,6 +98,7 @@ public void testGetSnapshotList() throws IOException { // Deleting snapshot and cluster has no snapshots, .hbase-snapshot directory would still exist admin.deleteSnapshot(snapshotName); assertTrue(fs.exists(snapshotDir)); + assertTrue(fs.exists(new Path(snapshotDir, SnapshotDescriptionUtils.SNAPSHOT_TMP_DIR_NAME))); snapshotDescList = SnapshotInfo.getSnapshotList(conf); assertTrue(snapshotDescList.isEmpty()); }