Skip to content

Commit 2e0e1b0

Browse files
committed
HBASE-22578 HFileCleaner should not delete empty ns/table directories used for user san snapshot feature
1 parent b20044c commit 2e0e1b0

File tree

4 files changed

+210
-6
lines changed

4 files changed

+210
-6
lines changed

hbase-server/src/main/java/org/apache/hadoop/hbase/master/cleaner/CleanerChore.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,26 @@ private boolean checkAndDeleteFiles(List<FileStatus> files) {
407407
return deleteFiles(filesToDelete) == files.size();
408408
}
409409

410+
/**
411+
* Check if a empty directory with no subdirs or subfiles can be deleted
412+
* @param dir Path of the directory
413+
* @return True if the directory can be deleted, otherwise false
414+
*/
415+
private boolean isEmptyDirDeletable(Path dir) {
416+
for (T cleaner : cleanersChain) {
417+
if (cleaner.isStopped() || this.getStopper().isStopped()) {
418+
LOG.warn("A file cleaner {} is stopped, won't delete the empty directory {}",
419+
this.getName(), dir);
420+
return false;
421+
}
422+
if (!cleaner.isEmptyDirDeletable(dir)) {
423+
// If one of the cleaner need the empty directory, skip delete it
424+
return false;
425+
}
426+
}
427+
return true;
428+
}
429+
410430
/**
411431
* Delete the given files
412432
* @param filesToDelete files to delete
@@ -513,9 +533,9 @@ protected Boolean compute() {
513533
allSubdirsDeleted = deleteAction(() -> getCleanResult(tasks), "subdirs");
514534
}
515535

516-
boolean result = allFilesDeleted && allSubdirsDeleted;
517-
// if and only if files and subdirs under current dir are deleted successfully, and
518-
// it is not the root dir, then task will try to delete it.
536+
boolean result = allFilesDeleted && allSubdirsDeleted && isEmptyDirDeletable(dir);
537+
// if and only if files and subdirs under current dir are deleted successfully, and the empty
538+
// directory can be deleted, and it is not the root dir then task will try to delete it.
519539
if (result && !root) {
520540
result &= deleteAction(() -> fs.delete(dir, false), "dir");
521541
}

hbase-server/src/main/java/org/apache/hadoop/hbase/master/cleaner/FileCleanerDelegate.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
*/
1818
package org.apache.hadoop.hbase.master.cleaner;
1919

20-
import org.apache.yetus.audience.InterfaceAudience;
20+
import java.util.Map;
21+
22+
import org.apache.hadoop.fs.Path;
2123
import org.apache.hadoop.conf.Configurable;
2224
import org.apache.hadoop.fs.FileStatus;
2325
import org.apache.hadoop.hbase.Stoppable;
24-
25-
import java.util.Map;
26+
import org.apache.yetus.audience.InterfaceAudience;
2627

2728
/**
2829
* General interface for cleaning files from a folder (generally an archive or
@@ -50,4 +51,13 @@ public interface FileCleanerDelegate extends Configurable, Stoppable {
5051
*/
5152
default void preClean() {
5253
}
54+
55+
/**
56+
* Check if a empty directory with no subdirs or subfiles can be deleted
57+
* @param dir Path of the directory
58+
* @return True if the directory can be deleted, otherwise false
59+
*/
60+
default boolean isEmptyDirDeletable(Path dir) {
61+
return true;
62+
}
5363
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.hbase.security.access;
19+
20+
import java.io.IOException;
21+
import java.util.Map;
22+
23+
import org.apache.hadoop.conf.Configuration;
24+
import org.apache.hadoop.fs.FileStatus;
25+
import org.apache.hadoop.fs.Path;
26+
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
27+
import org.apache.hadoop.hbase.HConstants;
28+
import org.apache.hadoop.hbase.MetaTableAccessor;
29+
import org.apache.hadoop.hbase.TableName;
30+
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
31+
import org.apache.hadoop.hbase.master.HMaster;
32+
import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate;
33+
import org.apache.yetus.audience.InterfaceAudience;
34+
import org.apache.yetus.audience.InterfaceStability;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
37+
38+
import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
39+
40+
/**
41+
* Implementation of a file cleaner that checks if a empty directory with no subdirs and subfiles is
42+
* deletable when user scan snapshot feature is enabled
43+
*/
44+
@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
45+
@InterfaceStability.Evolving
46+
public class SnapshotScannerHDFSAclCleaner extends BaseHFileCleanerDelegate {
47+
private static final Logger LOG = LoggerFactory.getLogger(SnapshotScannerHDFSAclCleaner.class);
48+
49+
private HMaster master;
50+
private boolean userScanSnapshotEnabled = false;
51+
52+
@Override
53+
public void init(Map<String, Object> params) {
54+
if (params != null && params.containsKey(HMaster.MASTER)) {
55+
this.master = (HMaster) params.get(HMaster.MASTER);
56+
}
57+
}
58+
59+
@Override
60+
public void setConf(Configuration conf) {
61+
super.setConf(conf);
62+
userScanSnapshotEnabled = isUserScanSnapshotEnabled(conf);
63+
}
64+
65+
@Override
66+
protected boolean isFileDeletable(FileStatus fStat) {
67+
// This plugin does not handle the file deletions, so return true by default
68+
return true;
69+
}
70+
71+
@Override
72+
public boolean isEmptyDirDeletable(Path dir) {
73+
if (userScanSnapshotEnabled) {
74+
/*
75+
* If user scan snapshot feature is enabled(see HBASE-21995), then when namespace or table
76+
* exists, the archive namespace or table directories should not be deleted because the HDFS
77+
* acls are set at these directories; the archive data directory should not be deleted because
78+
* the HDFS acls of global permission is set at this directory.
79+
*/
80+
return isEmptyArchiveDirDeletable(dir);
81+
}
82+
return true;
83+
}
84+
85+
private boolean isUserScanSnapshotEnabled(Configuration conf) {
86+
String masterCoprocessors = conf.get(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY);
87+
return conf.getBoolean(SnapshotScannerHDFSAclHelper.USER_SCAN_SNAPSHOT_ENABLE, false)
88+
&& masterCoprocessors.contains(SnapshotScannerHDFSAclController.class.getName())
89+
&& masterCoprocessors.contains(AccessController.class.getName());
90+
}
91+
92+
private boolean isEmptyArchiveDirDeletable(Path dir) {
93+
try {
94+
if (isArchiveDataDir(dir)) {
95+
return false;
96+
} else if (isArchiveNamespaceDir(dir) && namespaceExists(dir.getName())) {
97+
return false;
98+
} else if (isArchiveTableDir(dir)) {
99+
return !tableExists(TableName.valueOf(dir.getParent().getName(), dir.getName()));
100+
}
101+
return true;
102+
} catch (IOException e) {
103+
LOG.warn("Check if empty dir {} is deletable error", dir, e);
104+
return false;
105+
}
106+
}
107+
108+
@VisibleForTesting
109+
static boolean isArchiveDataDir(Path path) {
110+
if (path != null && path.getName().equals(HConstants.BASE_NAMESPACE_DIR)) {
111+
Path parent = path.getParent();
112+
return parent != null && parent.getName().equals(HConstants.HFILE_ARCHIVE_DIRECTORY);
113+
}
114+
return false;
115+
}
116+
117+
@VisibleForTesting
118+
static boolean isArchiveNamespaceDir(Path path) {
119+
return path != null && isArchiveDataDir(path.getParent());
120+
}
121+
122+
@VisibleForTesting
123+
static boolean isArchiveTableDir(Path path) {
124+
return path != null && isArchiveNamespaceDir(path.getParent());
125+
}
126+
127+
private boolean namespaceExists(String namespace) throws IOException {
128+
return master != null && master.listNamespaces().contains(namespace);
129+
}
130+
131+
private boolean tableExists(TableName tableName) throws IOException {
132+
return master != null && MetaTableAccessor.tableExists(master.getConnection(), tableName);
133+
}
134+
}

hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestSnapshotScannerHDFSAclController.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import static org.apache.hadoop.hbase.security.access.Permission.Action.READ;
2222
import static org.apache.hadoop.hbase.security.access.Permission.Action.WRITE;
2323
import static org.junit.Assert.assertEquals;
24+
import static org.junit.Assert.assertFalse;
25+
import static org.junit.Assert.assertTrue;
2426

2527
import java.io.IOException;
2628
import java.security.PrivilegedExceptionAction;
@@ -46,10 +48,12 @@
4648
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
4749
import org.apache.hadoop.hbase.client.TableSnapshotScanner;
4850
import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
51+
import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
4952
import org.apache.hadoop.hbase.security.User;
5053
import org.apache.hadoop.hbase.testclassification.LargeTests;
5154
import org.apache.hadoop.hbase.testclassification.SecurityTests;
5255
import org.apache.hadoop.hbase.util.Bytes;
56+
import org.apache.hadoop.hbase.util.HFileArchiveUtil;
5357
import org.junit.AfterClass;
5458
import org.junit.BeforeClass;
5559
import org.junit.ClassRule;
@@ -94,6 +98,9 @@ public static void setupBeforeClass() throws Exception {
9498
conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
9599
conf.get(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY) + ","
96100
+ SnapshotScannerHDFSAclController.class.getName());
101+
// set hfile cleaner plugin
102+
conf.set(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
103+
SnapshotScannerHDFSAclCleaner.class.getName());
97104

98105
TEST_UTIL.startMiniCluster();
99106
admin = TEST_UTIL.getAdmin();
@@ -546,6 +553,39 @@ public void testDeleteNamespace() throws Exception {
546553
}
547554
}
548555

556+
@Test
557+
public void testCleanArchiveTableDir() throws Exception {
558+
final String grantUserName = name.getMethodName();
559+
User grantUser = User.createUserForTesting(conf, grantUserName, new String[] {});
560+
String namespace = name.getMethodName();
561+
TableName table = TableName.valueOf(namespace, "t1");
562+
String snapshot = namespace + "t1";
563+
564+
TestHDFSAclHelper.createTableAndPut(TEST_UTIL, table);
565+
admin.snapshot(snapshot, table);
566+
TestHDFSAclHelper.grantOnTable(TEST_UTIL, grantUserName, table, READ);
567+
TestHDFSAclHelper.canUserScanSnapshot(TEST_UTIL, grantUser, snapshot, 6);
568+
569+
// HFileCleaner will not delete archive table directory even if it's a empty directory
570+
HFileCleaner cleaner = TEST_UTIL.getHBaseCluster().getMaster().getHFileCleaner();
571+
cleaner.choreForTesting();
572+
Path archiveTableDir = HFileArchiveUtil.getTableArchivePath(rootDir, table);
573+
assertTrue(fs.exists(archiveTableDir));
574+
575+
// delete table and grant user can scan snapshot
576+
admin.disableTable(table);
577+
admin.deleteTable(table);
578+
TestHDFSAclHelper.canUserScanSnapshot(TEST_UTIL, grantUser, snapshot, 6);
579+
580+
// Check SnapshotScannerHDFSAclCleaner method
581+
assertTrue(SnapshotScannerHDFSAclCleaner.isArchiveTableDir(archiveTableDir));
582+
assertTrue(SnapshotScannerHDFSAclCleaner.isArchiveNamespaceDir(archiveTableDir.getParent()));
583+
assertTrue(
584+
SnapshotScannerHDFSAclCleaner.isArchiveDataDir(archiveTableDir.getParent().getParent()));
585+
assertFalse(SnapshotScannerHDFSAclCleaner
586+
.isArchiveDataDir(archiveTableDir.getParent().getParent().getParent()));
587+
}
588+
549589
@Test
550590
public void testGrantMobTable() throws Exception {
551591
final String grantUserName = name.getMethodName();

0 commit comments

Comments
 (0)