diff --git hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveManager.java hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveManager.java new file mode 100644 index 0000000..d5aac50 --- /dev/null +++ hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveManager.java @@ -0,0 +1,168 @@ +/** + * 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.util; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.ZooKeeperConnectionException; +import org.apache.hadoop.hbase.client.HConnection; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; +import org.apache.zookeeper.KeeperException; + +/** + * Client-side manager for which table's hfiles should be preserved for long-term archive. + * @see ZKTableArchiveClient + * @see HFileArchiveTableMonitor + * @see LongTermArchivingHFileCleaner + */ +@InterfaceAudience.Private +class HFileArchiveManager { + + private final String archiveZnode; + private static final Log LOG = LogFactory.getLog(HFileArchiveManager.class); + private final ZooKeeperWatcher zooKeeper; + private volatile boolean stopped = false; + + public HFileArchiveManager(HConnection connection, Configuration conf) + throws ZooKeeperConnectionException, IOException { + this.zooKeeper = new ZooKeeperWatcher(conf, "hfileArchiveManager-on-" + connection.toString(), + connection); + this.archiveZnode = ZKTableArchiveClient.getArchiveZNode(this.zooKeeper.getConfiguration(), + this.zooKeeper); + } + + /** + * Turn on auto-backups of HFiles on the specified table. + *

+ * When HFiles would be deleted from the hfile archive, they are instead preserved. + * @param table name of the table for which to preserve hfiles. + * @return this for chaining. + * @throws KeeperException if we can't reach zookeeper to update the hfile cleaner. + */ + public HFileArchiveManager enableHFileBackup(byte[] table) throws KeeperException { + enable(this.zooKeeper, table); + return this; + } + + /** + * Stop retaining HFiles for the given table in the archive. HFiles will be cleaned up on the next + * pass of the {@link HFileCleaner}, if the HFiles are retained by another cleaner. + * @param table name of the table for which to disable hfile retention. + * @return this for chaining. + * @throws KeeperException if if we can't reach zookeeper to update the hfile cleaner. + */ + public HFileArchiveManager disableHFileBackup(byte[] table) throws KeeperException { + disable(this.zooKeeper, table); + return this; + } + + /** + * Disable long-term archival of all hfiles for all tables in the cluster. + * @return this for chaining. + * @throws IOException if the number of attempts is exceeded + */ + public HFileArchiveManager disableHFileBackup() throws IOException { + LOG.debug("Disabling backups on all tables."); + try { + ZKUtil.deleteNodeRecursively(this.zooKeeper, archiveZnode); + return this; + } catch (KeeperException e) { + throw new IOException("Unexpected ZK exception!", e); + } + } + + /** + * Perform a best effort enable of hfile retention, which relies on zookeeper communicating the // + * * change back to the hfile cleaner. + *

+ * No attempt is made to make sure that backups are successfully created - it is inherently an + * asynchronous operation. + * @param zooKeeper watcher connection to zk cluster + * @param table table name on which to enable archiving + * @throws KeeperException + */ + private void enable(ZooKeeperWatcher zooKeeper, byte[] table) + throws KeeperException { + LOG.debug("Ensuring archiving znode exists"); + ZKUtil.createAndFailSilent(zooKeeper, archiveZnode); + + // then add the table to the list of znodes to archive + String tableNode = this.getTableNode(table); + LOG.debug("Creating: " + tableNode + ", data: []"); + ZKUtil.createSetData(zooKeeper, tableNode, new byte[0]); + } + + /** + * Disable all archiving of files for a given table + *

+ * Inherently an asynchronous operation. + * @param zooKeeper watcher for the ZK cluster + * @param table name of the table to disable + * @throws KeeperException if an unexpected ZK connection issues occurs + */ + private void disable(ZooKeeperWatcher zooKeeper, byte[] table) throws KeeperException { + // ensure the latest state of the archive node is found + zooKeeper.sync(archiveZnode); + + // if the top-level archive node is gone, then we are done + if (ZKUtil.checkExists(zooKeeper, archiveZnode) < 0) { + return; + } + // delete the table node, from the archive + String tableNode = this.getTableNode(table); + // make sure the table is the latest version so the delete takes + zooKeeper.sync(tableNode); + + LOG.debug("Attempting to delete table node:" + tableNode); + ZKUtil.deleteNodeRecursively(zooKeeper, tableNode); + } + + public void stop() { + if (!this.stopped) { + this.stopped = true; + LOG.debug("Stopping HFileArchiveManager..."); + this.zooKeeper.close(); + } + } + + /** + * Check to see if the table is currently marked for archiving + * @param table name of the table to check + * @return true if the archive znode for that table exists, false if not + * @throws KeeperException if an unexpected zookeeper error occurs + */ + public boolean isArchivingEnabled(byte[] table) throws KeeperException { + String tableNode = this.getTableNode(table); + return ZKUtil.checkExists(zooKeeper, tableNode) >= 0; + } + + /** + * Get the zookeeper node associated with archiving the given table + * @param table name of the table to check + * @return znode for the table's archive status + */ + private String getTableNode(byte[] table) { + return ZKUtil.joinZNode(archiveZnode, Bytes.toString(table)); + } +} diff --git hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveTableMonitor.java hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveTableMonitor.java new file mode 100644 index 0000000..75b8acc --- /dev/null +++ hbase-examples/src/main/java/org/apache/hadoop/hbase/util/HFileArchiveTableMonitor.java @@ -0,0 +1,78 @@ +/** + * 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.util; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Monitor the actual tables for which HFiles are archived for long-term retention (always kept + * unless ZK state changes). + *

+ * It is internally synchronized to ensure consistent view of the table state. + */ +public class HFileArchiveTableMonitor { + private static final Log LOG = LogFactory.getLog(HFileArchiveTableMonitor.class); + private final Set archivedTables = new TreeSet(); + + /** + * Set the tables to be archived. Internally adds each table and attempts to + * register it. + *

+ * Note: All previous tables will be removed in favor of these tables. + * @param tables add each of the tables to be archived. + */ + public synchronized void setArchiveTables(List tables) { + archivedTables.clear(); + archivedTables.addAll(tables); + } + + /** + * Add the named table to be those being archived. Attempts to register the + * table + * @param table name of the table to be registered + */ + public synchronized void addTable(String table) { + if (this.shouldArchiveTable(table)) { + LOG.debug("Already archiving table: " + table + ", ignoring it"); + return; + } + archivedTables.add(table); + } + + public synchronized void removeTable(String table) { + archivedTables.remove(table); + } + + public synchronized void clearArchive() { + archivedTables.clear(); + } + + /** + * Determine if the given table should or should not allow its hfiles to be deleted in the archive + * @param tableName name of the table to check + * @return true if its store files should be retained, false otherwise + */ + public synchronized boolean shouldArchiveTable(String tableName) { + return archivedTables.contains(tableName); + } +} diff --git hbase-examples/src/main/java/org/apache/hadoop/hbase/util/LongTermArchivingHFileCleaner.java hbase-examples/src/main/java/org/apache/hadoop/hbase/util/LongTermArchivingHFileCleaner.java new file mode 100644 index 0000000..c77c0d3 --- /dev/null +++ hbase-examples/src/main/java/org/apache/hadoop/hbase/util/LongTermArchivingHFileCleaner.java @@ -0,0 +1,109 @@ +/** + * 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.util; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseInterfaceAudience; +import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate; +import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.zookeeper.KeeperException; + +/** + * {@link BaseHFileCleanerDelegate} that only cleans HFiles that don't belong to a table that is + * currently being archived. + *

+ * This only works properly if the + * {@link org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner} + * is also enabled (it always should be), since it may take a little time + * for the ZK notification to propagate, in which case we may accidentally + * delete some files. + */ +@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG) +public class LongTermArchivingHFileCleaner extends BaseHFileCleanerDelegate { + + private static final Log LOG = LogFactory.getLog(LongTermArchivingHFileCleaner.class); + + TableHFileArchiveTracker archiveTracker; + private FileSystem fs; + + @Override + public boolean isFileDeletable(FileStatus fStat) { + try { + // if its a directory, then it can be deleted + if (fStat.isDirectory()) return true; + + Path file = fStat.getPath(); + // check to see if + FileStatus[] deleteStatus = FSUtils.listStatus(this.fs, file, null); + // if the file doesn't exist, then it can be deleted (but should never + // happen since deleted files shouldn't get passed in) + if (deleteStatus == null) return true; + + // otherwise, we need to check the file's table and see its being archived + Path family = file.getParent(); + Path region = family.getParent(); + Path table = region.getParent(); + + String tableName = table.getName(); + boolean ret = !archiveTracker.keepHFiles(tableName); + LOG.debug("Archiver says to [" + (ret ? "delete" : "keep") + "] files for table:" + tableName); + return ret; + } catch (IOException e) { + LOG.error("Failed to lookup status of:" + fStat.getPath() + ", keeping it just incase.", e); + return false; + } + } + + @Override + public void setConf(Configuration config) { + // setup our own zookeeper connection + // Make my own Configuration. Then I'll have my own connection to zk that + // I can close myself when comes time. + Configuration conf = new Configuration(config); + super.setConf(conf); + try { + this.fs = FileSystem.get(conf); + this.archiveTracker = TableHFileArchiveTracker.create(conf); + this.archiveTracker.start(); + } catch (KeeperException e) { + LOG.error("Error while configuring " + this.getClass().getName(), e); + } catch (IOException e) { + LOG.error("Error while configuring " + this.getClass().getName(), e); + } + } + + @Override + public void stop(String reason) { + if (this.isStopped()) return; + super.stop(reason); + if (this.archiveTracker != null) { + LOG.info("Stopping " + this.archiveTracker); + this.archiveTracker.stop(); + } + + } + +} diff --git hbase-examples/src/main/java/org/apache/hadoop/hbase/util/TableHFileArchiveTracker.java hbase-examples/src/main/java/org/apache/hadoop/hbase/util/TableHFileArchiveTracker.java new file mode 100644 index 0000000..4278040 --- /dev/null +++ hbase-examples/src/main/java/org/apache/hadoop/hbase/util/TableHFileArchiveTracker.java @@ -0,0 +1,267 @@ +/** + * 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.util; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.ZooKeeperConnectionException; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperListener; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; +import org.apache.zookeeper.KeeperException; + +/** + * Track HFile archiving state changes in ZooKeeper. Keeps track of the tables whose HFiles should + * be kept in the archive. + *

+ * {@link TableHFileArchiveTracker#start()} needs to be called to start monitoring for tables to + * archive. + */ +@InterfaceAudience.Private +public class TableHFileArchiveTracker extends ZooKeeperListener { + private static final Log LOG = LogFactory.getLog(TableHFileArchiveTracker.class); + public static final String HFILE_ARCHIVE_ZNODE_PARENT = "hfilearchive"; + private HFileArchiveTableMonitor monitor; + private String archiveHFileZNode; + private boolean stopped = false; + + private TableHFileArchiveTracker(ZooKeeperWatcher watcher, HFileArchiveTableMonitor monitor) { + super(watcher); + watcher.registerListener(this); + this.monitor = monitor; + this.archiveHFileZNode = ZKTableArchiveClient.getArchiveZNode(watcher.getConfiguration(), + watcher); + } + + /** + * Start monitoring for archive updates + * @throws KeeperException on failure to find/create nodes + */ + public void start() throws KeeperException { + // if archiving is enabled, then read in the list of tables to archive + LOG.debug("Starting hfile archive tracker..."); + this.checkEnabledAndUpdate(); + LOG.debug("Finished starting hfile archive tracker!"); + } + + @Override + public void nodeCreated(String path) { + // if it is the archive path + if (!path.startsWith(archiveHFileZNode)) return; + + LOG.debug("Archive node: " + path + " created"); + // since we are already enabled, just update a single table + String table = path.substring(archiveHFileZNode.length()); + + // the top level node has come up, so read in all the tables + if (table.length() == 0) { + + checkEnabledAndUpdate(); + return; + } + // find the table that needs to be archived + try { + addAndReWatchTable(path); + } catch (KeeperException e) { + LOG.warn("Couldn't read zookeeper data for table for path:" + path + + ", not preserving a table.", e); + } + } + + @Override + public void nodeChildrenChanged(String path) { + if (!path.startsWith(archiveHFileZNode)) return; + + LOG.debug("Archive node: " + path + " children changed."); + // a table was added to the archive + try { + updateWatchedTables(); + } catch (KeeperException e) { + LOG.error("Failed to update tables to archive", e); + } + } + + /** + * Add this table to the tracker and then read a watch on that node. + *

+ * Handles situation where table is deleted in the time between the update and resetting the watch + * by deleting the table via {@link #safeStopTrackingTable(String)} + * @param tableZnode full zookeeper path to the table to be added + * @throws KeeperException if an unexpected zk exception occurs + */ + private void addAndReWatchTable(String tableZnode) throws KeeperException { + getMonitor().addTable(ZKUtil.getNodeName(tableZnode)); + // re-add a watch to the table created + // and check to make sure it wasn't deleted + if (!ZKUtil.watchAndCheckExists(watcher, tableZnode)) { + safeStopTrackingTable(tableZnode); + } + } + + /** + * Stop tracking a table. Ensures that the table doesn't exist, but if it does, it attempts to add + * the table back via {@link #addAndReWatchTable(String)} - its a 'safe' removal. + * @param tableZnode full zookeeper path to the table to be added + * @throws KeeperException if an unexpected zk exception occurs + */ + private void safeStopTrackingTable(String tableZnode) throws KeeperException { + getMonitor().removeTable(ZKUtil.getNodeName(tableZnode)); + // if the table exists, then add and rewatch it + if (ZKUtil.checkExists(watcher, tableZnode) >= 0) { + addAndReWatchTable(tableZnode); + } + } + + @Override + public void nodeDeleted(String path) { + if (!path.startsWith(archiveHFileZNode)) return; + + LOG.debug("Archive node: " + path + " deleted"); + String table = path.substring(archiveHFileZNode.length()); + // if we stop archiving all tables + if (table.length() == 0) { + // make sure we have the tracker before deleting the archive + // but if we don't, we don't care about delete + clearTables(); + // watches are one-time events, so we need to renew our subscription to + // the archive node and might as well check to make sure archiving + // didn't come back on at the same time + checkEnabledAndUpdate(); + return; + } + // just stop archiving one table + // note that we don't attempt to add another watch for that table into zk. + // We have no assurances that the table will be archived again (or even + // exists for that matter), so its better not to add unnecessary load to + // zk for watches. If the table is created again, then we will get the + // notification in childrenChanaged. + getMonitor().removeTable(ZKUtil.getNodeName(path)); + } + + /** + * Sets the watch on the top-level archive znode, and then updates the monitor with the current + * tables that should be archived (and ensures that those nodes are watched as well). + */ + private void checkEnabledAndUpdate() { + try { + if (ZKUtil.watchAndCheckExists(watcher, archiveHFileZNode)) { + LOG.debug(archiveHFileZNode + " znode does exist, checking for tables to archive"); + + // update the tables we should backup, to get the most recent state. + // This is safer than also watching for children and then hoping we get + // all the updates as it makes sure we get and watch all the children + updateWatchedTables(); + } else { + LOG.debug("Archiving not currently enabled, waiting"); + } + } catch (KeeperException e) { + LOG.warn("Failed to watch for archiving znode", e); + } + } + + /** + * Read the list of children under the archive znode as table names and then sets those tables to + * the list of tables that we should archive + * @throws KeeperException if there is an unexpected zk exception + */ + private void updateWatchedTables() throws KeeperException { + // get the children and watch for new children + LOG.debug("Updating watches on tables to archive."); + // get the children and add watches for each of the children + List tables = ZKUtil.listChildrenAndWatchThem(watcher, archiveHFileZNode); + LOG.debug("Starting archive for tables:" + tables); + // if archiving is still enabled + if (tables != null && tables.size() > 0) { + getMonitor().setArchiveTables(tables); + } else { + LOG.debug("No tables to archive."); + // only if we currently have a tracker, then clear the archive + clearTables(); + } + } + + /** + * Remove the currently archived tables. + *

+ * Does some intelligent checking to make sure we don't prematurely create an archive tracker. + */ + private void clearTables() { + getMonitor().clearArchive(); + } + + /** + * Determine if the given table should or should not allow its hfiles to be deleted + * @param tableName name of the table to check + * @return true if its store files should be retained, false otherwise + */ + public boolean keepHFiles(String tableName) { + return getMonitor().shouldArchiveTable(tableName); + } + + /** + * @return the tracker for which tables should be archived. + */ + public final HFileArchiveTableMonitor getMonitor() { + return this.monitor; + } + + /** + * Create an archive tracker for the passed in server + * @param conf to read for zookeeper connection information + * @return ZooKeeper tracker to monitor for this server if this server should archive hfiles for a + * given table + * @throws IOException If a unexpected exception occurs + * @throws ZooKeeperConnectionException if we can't reach zookeeper + */ + public static TableHFileArchiveTracker create(Configuration conf) + throws ZooKeeperConnectionException, IOException { + ZooKeeperWatcher zkw = new ZooKeeperWatcher(conf, "hfileArchiveCleaner", null); + return create(zkw, new HFileArchiveTableMonitor()); + } + + /** + * Create an archive tracker with the special passed in table monitor. Should only be used in + * special cases (e.g. testing) + * @param zkw Watcher for the ZooKeeper cluster that we should track + * @param monitor Monitor for which tables need hfile archiving + * @return ZooKeeper tracker to monitor for this server if this server should archive hfiles for a + * given table + */ + private static TableHFileArchiveTracker create(ZooKeeperWatcher zkw, + HFileArchiveTableMonitor monitor) { + return new TableHFileArchiveTracker(zkw, monitor); + } + + public ZooKeeperWatcher getZooKeeperWatcher() { + return this.watcher; + } + + /** + * Stop this tracker and the passed zookeeper + */ + public void stop() { + if (this.stopped) return; + this.stopped = true; + this.watcher.close(); + } +} diff --git hbase-examples/src/main/java/org/apache/hadoop/hbase/util/ZKTableArchiveClient.java hbase-examples/src/main/java/org/apache/hadoop/hbase/util/ZKTableArchiveClient.java new file mode 100644 index 0000000..c6b890e --- /dev/null +++ hbase-examples/src/main/java/org/apache/hadoop/hbase/util/ZKTableArchiveClient.java @@ -0,0 +1,155 @@ +/** + * 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.util; + +import java.io.IOException; + +import org.apache.hadoop.hbase.classification.InterfaceAudience; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.conf.Configured; +import org.apache.hadoop.hbase.client.ClusterConnection; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; +import org.apache.zookeeper.KeeperException; + +/** + * Example class for how to use the table archiving coordinated via zookeeper + */ +@InterfaceAudience.Private +public class ZKTableArchiveClient extends Configured { + + /** Configuration key for the archive node. */ + private static final String ZOOKEEPER_ZNODE_HFILE_ARCHIVE_KEY = "zookeeper.znode.hfile.archive"; + private ClusterConnection connection; + + public ZKTableArchiveClient(Configuration conf, ClusterConnection connection) { + super(conf); + this.connection = connection; + } + + /** + * Turn on backups for all HFiles for the given table. + *

+ * All deleted hfiles are moved to the archive directory under the table directory, rather than + * being deleted. + *

+ * If backups are already enabled for this table, does nothing. + *

+ * If the table does not exist, the archiving the table's hfiles is still enabled as a future + * table with that name may be created shortly. + * @param table name of the table to start backing up + * @throws IOException if an unexpected exception occurs + * @throws KeeperException if zookeeper can't be reached + */ + public void enableHFileBackupAsync(final byte[] table) throws IOException, KeeperException { + createHFileArchiveManager().enableHFileBackup(table).stop(); + } + + /** + * Disable hfile backups for the given table. + *

+ * Previously backed up files are still retained (if present). + *

+ * Asynchronous operation - some extra HFiles may be retained, in the archive directory after + * disable is called, dependent on the latency in zookeeper to the servers. + * @param table name of the table stop backing up + * @throws IOException if an unexpected exception occurs + * @throws KeeperException if zookeeper can't be reached + */ + public void disableHFileBackup(String table) throws IOException, KeeperException { + disableHFileBackup(Bytes.toBytes(table)); + } + + /** + * Disable hfile backups for the given table. + *

+ * Previously backed up files are still retained (if present). + *

+ * Asynchronous operation - some extra HFiles may be retained, in the archive directory after + * disable is called, dependent on the latency in zookeeper to the servers. + * @param table name of the table stop backing up + * @throws IOException if an unexpected exception occurs + * @throws KeeperException if zookeeper can't be reached + */ + public void disableHFileBackup(final byte[] table) throws IOException, KeeperException { + createHFileArchiveManager().disableHFileBackup(table).stop(); + } + + /** + * Disable hfile backups for all tables. + *

+ * Previously backed up files are still retained (if present). + *

+ * Asynchronous operation - some extra HFiles may be retained, in the archive directory after + * disable is called, dependent on the latency in zookeeper to the servers. + * @throws IOException if an unexpected exception occurs + * @throws KeeperException if zookeeper can't be reached + */ + public void disableHFileBackup() throws IOException, KeeperException { + createHFileArchiveManager().disableHFileBackup().stop(); + } + + /** + * Determine if archiving is enabled (but not necessarily fully propagated) for a table + * @param table name of the table to check + * @return true if it is, false otherwise + * @throws IOException if a connection to ZooKeeper cannot be established + * @throws KeeperException + */ + public boolean getArchivingEnabled(byte[] table) throws IOException, KeeperException { + HFileArchiveManager manager = createHFileArchiveManager(); + try { + return manager.isArchivingEnabled(table); + } finally { + manager.stop(); + } + } + + /** + * Determine if archiving is enabled (but not necessarily fully propagated) for a table + * @param table name of the table to check + * @return true if it is, false otherwise + * @throws IOException if an unexpected network issue occurs + * @throws KeeperException if zookeeper can't be reached + */ + public boolean getArchivingEnabled(String table) throws IOException, KeeperException { + return getArchivingEnabled(Bytes.toBytes(table)); + } + + /** + * @return A new {@link HFileArchiveManager} to manage which tables' hfiles should be archived + * rather than deleted. + * @throws KeeperException if we can't reach zookeeper + * @throws IOException if an unexpected network issue occurs + */ + private synchronized HFileArchiveManager createHFileArchiveManager() throws KeeperException, + IOException { + return new HFileArchiveManager(this.connection, this.getConf()); + } + + /** + * @param conf conf to read for the base archive node + * @param zooKeeper zookeeper to used for building the full path + * @return get the znode for long-term archival of a table for + */ + public static String getArchiveZNode(Configuration conf, ZooKeeperWatcher zooKeeper) { + return ZKUtil.joinZNode(zooKeeper.baseZNode, conf.get(ZOOKEEPER_ZNODE_HFILE_ARCHIVE_KEY, + TableHFileArchiveTracker.HFILE_ARCHIVE_ZNODE_PARENT)); + } +} diff --git hbase-examples/src/test/java/org/apache/hadoop/hbase/util/TestZooKeeperTableArchiveClient.java hbase-examples/src/test/java/org/apache/hadoop/hbase/util/TestZooKeeperTableArchiveClient.java new file mode 100644 index 0000000..434f7af --- /dev/null +++ hbase-examples/src/test/java/org/apache/hadoop/hbase/util/TestZooKeeperTableArchiveClient.java @@ -0,0 +1,448 @@ +/** + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.ChoreService; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.Stoppable; +import org.apache.hadoop.hbase.client.ClusterConnection; +import org.apache.hadoop.hbase.client.ConnectionFactory; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate; +import org.apache.hadoop.hbase.master.cleaner.HFileCleaner; +import org.apache.hadoop.hbase.regionserver.CompactedHFilesDischarger; +import org.apache.hadoop.hbase.regionserver.HRegion; +import org.apache.hadoop.hbase.regionserver.Region; +import org.apache.hadoop.hbase.regionserver.RegionServerServices; +import org.apache.hadoop.hbase.regionserver.Store; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiveUtil; +import org.apache.hadoop.hbase.util.LongTermArchivingHFileCleaner; +import org.apache.hadoop.hbase.util.StoppableImplementation; +import org.apache.hadoop.hbase.util.ZKTableArchiveClient; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; +import org.apache.zookeeper.KeeperException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Spin up a small cluster and check that the hfiles of region are properly long-term archived as + * specified via the {@link ZKTableArchiveClient}. + */ +@Category({MiscTests.class, MediumTests.class}) +public class TestZooKeeperTableArchiveClient { + + private static final Log LOG = LogFactory.getLog(TestZooKeeperTableArchiveClient.class); + private static final HBaseTestingUtility UTIL = HBaseTestingUtility.createLocalHTU(); + private static final String STRING_TABLE_NAME = "test"; + private static final byte[] TEST_FAM = Bytes.toBytes("fam"); + private static final byte[] TABLE_NAME = Bytes.toBytes(STRING_TABLE_NAME); + private static ZKTableArchiveClient archivingClient; + private final List toCleanup = new ArrayList(); + private static ClusterConnection CONNECTION; + private static RegionServerServices rss; + + /** + * Setup the config for the cluster + */ + @BeforeClass + public static void setupCluster() throws Exception { + setupConf(UTIL.getConfiguration()); + UTIL.startMiniZKCluster(); + CONNECTION = (ClusterConnection)ConnectionFactory.createConnection(UTIL.getConfiguration()); + archivingClient = new ZKTableArchiveClient(UTIL.getConfiguration(), CONNECTION); + // make hfile archiving node so we can archive files + ZooKeeperWatcher watcher = UTIL.getZooKeeperWatcher(); + String archivingZNode = ZKTableArchiveClient.getArchiveZNode(UTIL.getConfiguration(), watcher); + ZKUtil.createWithParents(watcher, archivingZNode); + rss = mock(RegionServerServices.class); + } + + private static void setupConf(Configuration conf) { + // only compact with 3 files + conf.setInt("hbase.hstore.compaction.min", 3); + } + + @After + public void tearDown() throws Exception { + try { + FileSystem fs = UTIL.getTestFileSystem(); + // cleanup each of the files/directories registered + for (Path file : toCleanup) { + // remove the table and archive directories + FSUtils.delete(fs, file, true); + } + } catch (IOException e) { + LOG.warn("Failure to delete archive directory", e); + } finally { + toCleanup.clear(); + } + // make sure that backups are off for all tables + archivingClient.disableHFileBackup(); + } + + @AfterClass + public static void cleanupTest() throws Exception { + try { + CONNECTION.close(); + UTIL.shutdownMiniZKCluster(); + } catch (Exception e) { + LOG.warn("problem shutting down cluster", e); + } + } + + /** + * Test turning on/off archiving + */ + @Test (timeout=300000) + public void testArchivingEnableDisable() throws Exception { + // 1. turn on hfile backups + LOG.debug("----Starting archiving"); + archivingClient.enableHFileBackupAsync(TABLE_NAME); + assertTrue("Archving didn't get turned on", archivingClient + .getArchivingEnabled(TABLE_NAME)); + + // 2. Turn off archiving and make sure its off + archivingClient.disableHFileBackup(); + assertFalse("Archving didn't get turned off.", archivingClient.getArchivingEnabled(TABLE_NAME)); + + // 3. Check enable/disable on a single table + archivingClient.enableHFileBackupAsync(TABLE_NAME); + assertTrue("Archving didn't get turned on", archivingClient + .getArchivingEnabled(TABLE_NAME)); + + // 4. Turn off archiving and make sure its off + archivingClient.disableHFileBackup(TABLE_NAME); + assertFalse("Archving didn't get turned off for " + STRING_TABLE_NAME, + archivingClient.getArchivingEnabled(TABLE_NAME)); + } + + @Test (timeout=300000) + public void testArchivingOnSingleTable() throws Exception { + createArchiveDirectory(); + FileSystem fs = UTIL.getTestFileSystem(); + Path archiveDir = getArchiveDir(); + Path tableDir = getTableDir(STRING_TABLE_NAME); + toCleanup.add(archiveDir); + toCleanup.add(tableDir); + + Configuration conf = UTIL.getConfiguration(); + // setup the delegate + Stoppable stop = new StoppableImplementation(); + HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop); + List cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner); + final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); + + // create the region + HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM); + HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd); + List regions = new ArrayList(); + regions.add(region); + when(rss.getOnlineRegions()).thenReturn(regions); + final CompactedHFilesDischarger compactionCleaner = + new CompactedHFilesDischarger(100, stop, rss, false); + loadFlushAndCompact(region, TEST_FAM); + compactionCleaner.chore(); + // get the current hfiles in the archive directory + List files = getAllFiles(fs, archiveDir); + if (files == null) { + FSUtils.logFileSystemState(fs, UTIL.getDataTestDir(), LOG); + throw new RuntimeException("Didn't archive any files!"); + } + CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size()); + + runCleaner(cleaner, finished, stop); + + // know the cleaner ran, so now check all the files again to make sure they are still there + List archivedFiles = getAllFiles(fs, archiveDir); + assertEquals("Archived files changed after running archive cleaner.", files, archivedFiles); + + // but we still have the archive directory + assertTrue(fs.exists(HFileArchiveUtil.getArchivePath(UTIL.getConfiguration()))); + } + + /** + * Test archiving/cleaning across multiple tables, where some are retained, and others aren't + * @throws Exception on failure + */ + @Test (timeout=300000) + public void testMultipleTables() throws Exception { + createArchiveDirectory(); + String otherTable = "otherTable"; + + FileSystem fs = UTIL.getTestFileSystem(); + Path archiveDir = getArchiveDir(); + Path tableDir = getTableDir(STRING_TABLE_NAME); + Path otherTableDir = getTableDir(otherTable); + + // register cleanup for the created directories + toCleanup.add(archiveDir); + toCleanup.add(tableDir); + toCleanup.add(otherTableDir); + Configuration conf = UTIL.getConfiguration(); + // setup the delegate + Stoppable stop = new StoppableImplementation(); + final ChoreService choreService = new ChoreService("TEST_SERVER_NAME"); + HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop); + List cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner); + final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); + // create the region + HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM); + HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd); + List regions = new ArrayList(); + regions.add(region); + when(rss.getOnlineRegions()).thenReturn(regions); + final CompactedHFilesDischarger compactionCleaner = + new CompactedHFilesDischarger(100, stop, rss, false); + loadFlushAndCompact(region, TEST_FAM); + compactionCleaner.chore(); + // create the another table that we don't archive + hcd = new HColumnDescriptor(TEST_FAM); + HRegion otherRegion = UTIL.createTestRegion(otherTable, hcd); + regions = new ArrayList(); + regions.add(otherRegion); + when(rss.getOnlineRegions()).thenReturn(regions); + final CompactedHFilesDischarger compactionCleaner1 = new CompactedHFilesDischarger(100, stop, + rss, false); + loadFlushAndCompact(otherRegion, TEST_FAM); + compactionCleaner1.chore(); + // get the current hfiles in the archive directory + // Should be archived + List files = getAllFiles(fs, archiveDir); + if (files == null) { + FSUtils.logFileSystemState(fs, archiveDir, LOG); + throw new RuntimeException("Didn't load archive any files!"); + } + + // make sure we have files from both tables + int initialCountForPrimary = 0; + int initialCountForOtherTable = 0; + for (Path file : files) { + String tableName = file.getParent().getParent().getParent().getName(); + // check to which table this file belongs + if (tableName.equals(otherTable)) initialCountForOtherTable++; + else if (tableName.equals(STRING_TABLE_NAME)) initialCountForPrimary++; + } + + assertTrue("Didn't archive files for:" + STRING_TABLE_NAME, initialCountForPrimary > 0); + assertTrue("Didn't archive files for:" + otherTable, initialCountForOtherTable > 0); + + // run the cleaners, checking for each of the directories + files (both should be deleted and + // need to be checked) in 'otherTable' and the files (which should be retained) in the 'table' + CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size() + 3); + // run the cleaner + choreService.scheduleChore(cleaner); + // wait for the cleaner to check all the files + finished.await(); + // stop the cleaner + stop.stop(""); + + // know the cleaner ran, so now check all the files again to make sure they are still there + List archivedFiles = getAllFiles(fs, archiveDir); + int archivedForPrimary = 0; + for(Path file: archivedFiles) { + String tableName = file.getParent().getParent().getParent().getName(); + // ensure we don't have files from the non-archived table + assertFalse("Have a file from the non-archived table: " + file, tableName.equals(otherTable)); + if (tableName.equals(STRING_TABLE_NAME)) archivedForPrimary++; + } + + assertEquals("Not all archived files for the primary table were retained.", initialCountForPrimary, + archivedForPrimary); + + // but we still have the archive directory + assertTrue("Archive directory was deleted via archiver", fs.exists(archiveDir)); + } + + + private void createArchiveDirectory() throws IOException { + //create the archive and test directory + FileSystem fs = UTIL.getTestFileSystem(); + Path archiveDir = getArchiveDir(); + fs.mkdirs(archiveDir); + } + + private Path getArchiveDir() throws IOException { + return new Path(UTIL.getDataTestDir(), HConstants.HFILE_ARCHIVE_DIRECTORY); + } + + private Path getTableDir(String tableName) throws IOException { + Path testDataDir = UTIL.getDataTestDir(); + FSUtils.setRootDir(UTIL.getConfiguration(), testDataDir); + return new Path(testDataDir, tableName); + } + + private HFileCleaner setupAndCreateCleaner(Configuration conf, FileSystem fs, Path archiveDir, + Stoppable stop) { + conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, + LongTermArchivingHFileCleaner.class.getCanonicalName()); + return new HFileCleaner(1000, stop, conf, fs, archiveDir); + } + + /** + * Start archiving table for given hfile cleaner + * @param tableName table to archive + * @param cleaner cleaner to check to make sure change propagated + * @return underlying {@link LongTermArchivingHFileCleaner} that is managing archiving + * @throws IOException on failure + * @throws KeeperException on failure + */ + private List turnOnArchiving(String tableName, HFileCleaner cleaner) + throws IOException, KeeperException { + // turn on hfile retention + LOG.debug("----Starting archiving for table:" + tableName); + archivingClient.enableHFileBackupAsync(Bytes.toBytes(tableName)); + assertTrue("Archving didn't get turned on", archivingClient.getArchivingEnabled(tableName)); + + // wait for the archiver to get the notification + List cleaners = cleaner.getDelegatesForTesting(); + LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); + while (!delegate.archiveTracker.keepHFiles(STRING_TABLE_NAME)) { + // spin until propagation - should be fast + } + return cleaners; + } + + /** + * Spy on the {@link LongTermArchivingHFileCleaner} to ensure we can catch when the cleaner has + * seen all the files + * @return a {@link CountDownLatch} to wait on that releases when the cleaner has been called at + * least the expected number of times. + */ + private CountDownLatch setupCleanerWatching(LongTermArchivingHFileCleaner cleaner, + List cleaners, final int expected) { + // replace the cleaner with one that we can can check + BaseHFileCleanerDelegate delegateSpy = Mockito.spy(cleaner); + final int[] counter = new int[] { 0 }; + final CountDownLatch finished = new CountDownLatch(1); + Mockito.doAnswer(new Answer>() { + + @Override + public Iterable answer(InvocationOnMock invocation) throws Throwable { + counter[0]++; + LOG.debug(counter[0] + "/ " + expected + ") Wrapping call to getDeletableFiles for files: " + + invocation.getArguments()[0]); + + @SuppressWarnings("unchecked") + Iterable ret = (Iterable) invocation.callRealMethod(); + if (counter[0] >= expected) finished.countDown(); + return ret; + } + }).when(delegateSpy).getDeletableFiles(Mockito.anyListOf(FileStatus.class)); + cleaners.set(0, delegateSpy); + + return finished; + } + + /** + * Get all the files (non-directory entries) in the file system under the passed directory + * @param dir directory to investigate + * @return all files under the directory + */ + private List getAllFiles(FileSystem fs, Path dir) throws IOException { + FileStatus[] files = FSUtils.listStatus(fs, dir, null); + if (files == null) { + LOG.warn("No files under:" + dir); + return null; + } + + List allFiles = new ArrayList(); + for (FileStatus file : files) { + if (file.isDirectory()) { + List subFiles = getAllFiles(fs, file.getPath()); + if (subFiles != null) allFiles.addAll(subFiles); + continue; + } + allFiles.add(file.getPath()); + } + return allFiles; + } + + private void loadFlushAndCompact(Region region, byte[] family) throws IOException { + // create two hfiles in the region + createHFileInRegion(region, family); + createHFileInRegion(region, family); + + Store s = region.getStore(family); + int count = s.getStorefilesCount(); + assertTrue("Don't have the expected store files, wanted >= 2 store files, but was:" + count, + count >= 2); + + // compact the two files into one file to get files in the archive + LOG.debug("Compacting stores"); + region.compact(true); + } + + /** + * Create a new hfile in the passed region + * @param region region to operate on + * @param columnFamily family for which to add data + * @throws IOException + */ + private void createHFileInRegion(Region region, byte[] columnFamily) throws IOException { + // put one row in the region + Put p = new Put(Bytes.toBytes("row")); + p.addColumn(columnFamily, Bytes.toBytes("Qual"), Bytes.toBytes("v1")); + region.put(p); + // flush the region to make a store file + region.flush(true); + } + + /** + * @param cleaner + */ + private void runCleaner(HFileCleaner cleaner, CountDownLatch finished, Stoppable stop) + throws InterruptedException { + final ChoreService choreService = new ChoreService("CLEANER_SERVER_NAME"); + // run the cleaner + choreService.scheduleChore(cleaner); + // wait for the cleaner to check all the files + finished.await(); + // stop the cleaner + stop.stop(""); + } +} \ No newline at end of file diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/HFileArchiver.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/HFileArchiver.java deleted file mode 100644 index d682ccc..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/HFileArchiver.java +++ /dev/null @@ -1,690 +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 - * - * 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.backup; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathFilter; -import org.apache.hadoop.hbase.HRegionInfo; -import org.apache.hadoop.hbase.regionserver.HRegion; -import org.apache.hadoop.hbase.regionserver.StoreFile; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; -import org.apache.hadoop.hbase.util.FSUtils; -import org.apache.hadoop.hbase.util.HFileArchiveUtil; -import org.apache.hadoop.io.MultipleIOException; - -import com.google.common.base.Function; -import com.google.common.base.Preconditions; -import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - -/** - * Utility class to handle the removal of HFiles (or the respective {@link StoreFile StoreFiles}) - * for a HRegion from the {@link FileSystem}. The hfiles will be archived or deleted, depending on - * the state of the system. - */ -public class HFileArchiver { - private static final Log LOG = LogFactory.getLog(HFileArchiver.class); - private static final String SEPARATOR = "."; - - /** Number of retries in case of fs operation failure */ - private static final int DEFAULT_RETRIES_NUMBER = 3; - - private HFileArchiver() { - // hidden ctor since this is just a util - } - - /** - * Cleans up all the files for a HRegion by archiving the HFiles to the - * archive directory - * @param conf the configuration to use - * @param fs the file system object - * @param info HRegionInfo for region to be deleted - * @throws IOException - */ - public static void archiveRegion(Configuration conf, FileSystem fs, HRegionInfo info) - throws IOException { - Path rootDir = FSUtils.getRootDir(conf); - archiveRegion(fs, rootDir, FSUtils.getTableDir(rootDir, info.getTable()), - HRegion.getRegionDir(rootDir, info)); - } - - /** - * Remove an entire region from the table directory via archiving the region's hfiles. - * @param fs {@link FileSystem} from which to remove the region - * @param rootdir {@link Path} to the root directory where hbase files are stored (for building - * the archive path) - * @param tableDir {@link Path} to where the table is being stored (for building the archive path) - * @param regionDir {@link Path} to where a region is being stored (for building the archive path) - * @return true if the region was sucessfully deleted. false if the filesystem - * operations could not complete. - * @throws IOException if the request cannot be completed - */ - public static boolean archiveRegion(FileSystem fs, Path rootdir, Path tableDir, Path regionDir) - throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("ARCHIVING " + regionDir.toString()); - } - - // otherwise, we archive the files - // make sure we can archive - if (tableDir == null || regionDir == null) { - LOG.error("No archive directory could be found because tabledir (" + tableDir - + ") or regiondir (" + regionDir + "was null. Deleting files instead."); - deleteRegionWithoutArchiving(fs, regionDir); - // we should have archived, but failed to. Doesn't matter if we deleted - // the archived files correctly or not. - return false; - } - - // make sure the regiondir lives under the tabledir - Preconditions.checkArgument(regionDir.toString().startsWith(tableDir.toString())); - Path regionArchiveDir = HFileArchiveUtil.getRegionArchiveDir(rootdir, - FSUtils.getTableName(tableDir), - regionDir.getName()); - - FileStatusConverter getAsFile = new FileStatusConverter(fs); - // otherwise, we attempt to archive the store files - - // build collection of just the store directories to archive - Collection toArchive = new ArrayList(); - final PathFilter dirFilter = new FSUtils.DirFilter(fs); - PathFilter nonHidden = new PathFilter() { - @Override - public boolean accept(Path file) { - return dirFilter.accept(file) && !file.getName().toString().startsWith("."); - } - }; - FileStatus[] storeDirs = FSUtils.listStatus(fs, regionDir, nonHidden); - // if there no files, we can just delete the directory and return; - if (storeDirs == null) { - LOG.debug("Region directory (" + regionDir + ") was empty, just deleting and returning!"); - return deleteRegionWithoutArchiving(fs, regionDir); - } - - // convert the files in the region to a File - toArchive.addAll(Lists.transform(Arrays.asList(storeDirs), getAsFile)); - LOG.debug("Archiving " + toArchive); - boolean success = false; - try { - success = resolveAndArchive(fs, regionArchiveDir, toArchive); - } catch (IOException e) { - LOG.error("Failed to archive " + toArchive, e); - success = false; - } - - // if that was successful, then we delete the region - if (success) { - return deleteRegionWithoutArchiving(fs, regionDir); - } - - throw new IOException("Received error when attempting to archive files (" + toArchive - + "), cannot delete region directory. "); - } - - /** - * Remove from the specified region the store files of the specified column family, - * either by archiving them or outright deletion - * @param fs the filesystem where the store files live - * @param conf {@link Configuration} to examine to determine the archive directory - * @param parent Parent region hosting the store files - * @param tableDir {@link Path} to where the table is being stored (for building the archive path) - * @param family the family hosting the store files - * @throws IOException if the files could not be correctly disposed. - */ - public static void archiveFamily(FileSystem fs, Configuration conf, - HRegionInfo parent, Path tableDir, byte[] family) throws IOException { - Path familyDir = new Path(tableDir, new Path(parent.getEncodedName(), Bytes.toString(family))); - FileStatus[] storeFiles = FSUtils.listStatus(fs, familyDir); - if (storeFiles == null) { - LOG.debug("No store files to dispose for region=" + parent.getRegionNameAsString() + - ", family=" + Bytes.toString(family)); - return; - } - - FileStatusConverter getAsFile = new FileStatusConverter(fs); - Collection toArchive = Lists.transform(Arrays.asList(storeFiles), getAsFile); - Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, parent, tableDir, family); - - // do the actual archive - if (!resolveAndArchive(fs, storeArchiveDir, toArchive)) { - throw new IOException("Failed to archive/delete all the files for region:" - + Bytes.toString(parent.getRegionName()) + ", family:" + Bytes.toString(family) - + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); - } - } - - /** - * Remove the store files, either by archiving them or outright deletion - * @param conf {@link Configuration} to examine to determine the archive directory - * @param fs the filesystem where the store files live - * @param regionInfo {@link HRegionInfo} of the region hosting the store files - * @param family the family hosting the store files - * @param compactedFiles files to be disposed of. No further reading of these files should be - * attempted; otherwise likely to cause an {@link IOException} - * @throws IOException if the files could not be correctly disposed. - */ - public static void archiveStoreFiles(Configuration conf, FileSystem fs, HRegionInfo regionInfo, - Path tableDir, byte[] family, Collection compactedFiles) throws IOException { - - // sometimes in testing, we don't have rss, so we need to check for that - if (fs == null) { - LOG.warn("Passed filesystem is null, so just deleting the files without archiving for region:" - + Bytes.toString(regionInfo.getRegionName()) + ", family:" + Bytes.toString(family)); - deleteStoreFilesWithoutArchiving(compactedFiles); - return; - } - - // short circuit if we don't have any files to delete - if (compactedFiles.size() == 0) { - LOG.debug("No store files to dispose, done!"); - return; - } - - // build the archive path - if (regionInfo == null || family == null) throw new IOException( - "Need to have a region and a family to archive from."); - - Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, regionInfo, tableDir, family); - - // make sure we don't archive if we can't and that the archive dir exists - if (!fs.mkdirs(storeArchiveDir)) { - throw new IOException("Could not make archive directory (" + storeArchiveDir + ") for store:" - + Bytes.toString(family) + ", deleting compacted files instead."); - } - - // otherwise we attempt to archive the store files - if (LOG.isDebugEnabled()) LOG.debug("Archiving compacted store files."); - - // Wrap the storefile into a File - StoreToFile getStorePath = new StoreToFile(fs); - Collection storeFiles = Collections2.transform(compactedFiles, getStorePath); - - // do the actual archive - if (!resolveAndArchive(fs, storeArchiveDir, storeFiles)) { - throw new IOException("Failed to archive/delete all the files for region:" - + Bytes.toString(regionInfo.getRegionName()) + ", family:" + Bytes.toString(family) - + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); - } - } - - /** - * Archive the store file - * @param fs the filesystem where the store files live - * @param regionInfo region hosting the store files - * @param conf {@link Configuration} to examine to determine the archive directory - * @param tableDir {@link Path} to where the table is being stored (for building the archive path) - * @param family the family hosting the store files - * @param storeFile file to be archived - * @throws IOException if the files could not be correctly disposed. - */ - public static void archiveStoreFile(Configuration conf, FileSystem fs, HRegionInfo regionInfo, - Path tableDir, byte[] family, Path storeFile) throws IOException { - Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, regionInfo, tableDir, family); - // make sure we don't archive if we can't and that the archive dir exists - if (!fs.mkdirs(storeArchiveDir)) { - throw new IOException("Could not make archive directory (" + storeArchiveDir + ") for store:" - + Bytes.toString(family) + ", deleting compacted files instead."); - } - - // do the actual archive - long start = EnvironmentEdgeManager.currentTime(); - File file = new FileablePath(fs, storeFile); - if (!resolveAndArchiveFile(storeArchiveDir, file, Long.toString(start))) { - throw new IOException("Failed to archive/delete the file for region:" - + regionInfo.getRegionNameAsString() + ", family:" + Bytes.toString(family) - + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); - } - } - - /** - * Archive the given files and resolve any conflicts with existing files via appending the time - * archiving started (so all conflicts in the same group have the same timestamp appended). - *

- * If any of the passed files to archive are directories, archives all the files under that - * directory. Archive directory structure for children is the base archive directory name + the - * parent directory and is built recursively is passed files are directories themselves. - * @param fs {@link FileSystem} on which to archive the files - * @param baseArchiveDir base archive directory to archive the given files - * @param toArchive files to be archived - * @return true on success, false otherwise - * @throws IOException on unexpected failure - */ - private static boolean resolveAndArchive(FileSystem fs, Path baseArchiveDir, - Collection toArchive) throws IOException { - if (LOG.isTraceEnabled()) LOG.trace("Starting to archive " + toArchive); - long start = EnvironmentEdgeManager.currentTime(); - List failures = resolveAndArchive(fs, baseArchiveDir, toArchive, start); - - // notify that some files were not archived. - // We can't delete the files otherwise snapshots or other backup system - // that relies on the archiver end up with data loss. - if (failures.size() > 0) { - LOG.warn("Failed to complete archive of: " + failures + - ". Those files are still in the original location, and they may slow down reads."); - return false; - } - return true; - } - - /** - * Resolve any conflict with an existing archive file via timestamp-append - * renaming of the existing file and then archive the passed in files. - * @param fs {@link FileSystem} on which to archive the files - * @param baseArchiveDir base archive directory to store the files. If any of - * the files to archive are directories, will append the name of the - * directory to the base archive directory name, creating a parallel - * structure. - * @param toArchive files/directories that need to be archvied - * @param start time the archiving started - used for resolving archive - * conflicts. - * @return the list of failed to archive files. - * @throws IOException if an unexpected file operation exception occured - */ - private static List resolveAndArchive(FileSystem fs, Path baseArchiveDir, - Collection toArchive, long start) throws IOException { - // short circuit if no files to move - if (toArchive.size() == 0) return Collections.emptyList(); - - if (LOG.isTraceEnabled()) LOG.trace("moving files to the archive directory: " + baseArchiveDir); - - // make sure the archive directory exists - if (!fs.exists(baseArchiveDir)) { - if (!fs.mkdirs(baseArchiveDir)) { - throw new IOException("Failed to create the archive directory:" + baseArchiveDir - + ", quitting archive attempt."); - } - if (LOG.isTraceEnabled()) LOG.trace("Created archive directory:" + baseArchiveDir); - } - - List failures = new ArrayList(); - String startTime = Long.toString(start); - for (File file : toArchive) { - // if its a file archive it - try { - if (LOG.isTraceEnabled()) LOG.trace("Archiving: " + file); - if (file.isFile()) { - // attempt to archive the file - if (!resolveAndArchiveFile(baseArchiveDir, file, startTime)) { - LOG.warn("Couldn't archive " + file + " into backup directory: " + baseArchiveDir); - failures.add(file); - } - } else { - // otherwise its a directory and we need to archive all files - if (LOG.isTraceEnabled()) LOG.trace(file + " is a directory, archiving children files"); - // so we add the directory name to the one base archive - Path parentArchiveDir = new Path(baseArchiveDir, file.getName()); - // and then get all the files from that directory and attempt to - // archive those too - Collection children = file.getChildren(); - failures.addAll(resolveAndArchive(fs, parentArchiveDir, children, start)); - } - } catch (IOException e) { - LOG.warn("Failed to archive " + file, e); - failures.add(file); - } - } - return failures; - } - - /** - * Attempt to archive the passed in file to the archive directory. - *

- * If the same file already exists in the archive, it is moved to a timestamped directory under - * the archive directory and the new file is put in its place. - * @param archiveDir {@link Path} to the directory that stores the archives of the hfiles - * @param currentFile {@link Path} to the original HFile that will be archived - * @param archiveStartTime time the archiving started, to resolve naming conflicts - * @return true if the file is successfully archived. false if there was a - * problem, but the operation still completed. - * @throws IOException on failure to complete {@link FileSystem} operations. - */ - private static boolean resolveAndArchiveFile(Path archiveDir, File currentFile, - String archiveStartTime) throws IOException { - // build path as it should be in the archive - String filename = currentFile.getName(); - Path archiveFile = new Path(archiveDir, filename); - FileSystem fs = currentFile.getFileSystem(); - - // if the file already exists in the archive, move that one to a timestamped backup. This is a - // really, really unlikely situtation, where we get the same name for the existing file, but - // is included just for that 1 in trillion chance. - if (fs.exists(archiveFile)) { - if (LOG.isDebugEnabled()) { - LOG.debug("File:" + archiveFile + " already exists in archive, moving to " - + "timestamped backup and overwriting current."); - } - - // move the archive file to the stamped backup - Path backedupArchiveFile = new Path(archiveDir, filename + SEPARATOR + archiveStartTime); - if (!fs.rename(archiveFile, backedupArchiveFile)) { - LOG.error("Could not rename archive file to backup: " + backedupArchiveFile - + ", deleting existing file in favor of newer."); - // try to delete the exisiting file, if we can't rename it - if (!fs.delete(archiveFile, false)) { - throw new IOException("Couldn't delete existing archive file (" + archiveFile - + ") or rename it to the backup file (" + backedupArchiveFile - + ") to make room for similarly named file."); - } - } - LOG.debug("Backed up archive file from " + archiveFile); - } - - if (LOG.isTraceEnabled()) { - LOG.trace("No existing file in archive for: " + archiveFile + - ", free to archive original file."); - } - - // at this point, we should have a free spot for the archive file - boolean success = false; - for (int i = 0; !success && i < DEFAULT_RETRIES_NUMBER; ++i) { - if (i > 0) { - // Ensure that the archive directory exists. - // The previous "move to archive" operation has failed probably because - // the cleaner has removed our archive directory (HBASE-7643). - // (we're in a retry loop, so don't worry too much about the exception) - try { - if (!fs.exists(archiveDir)) { - if (fs.mkdirs(archiveDir)) { - LOG.debug("Created archive directory:" + archiveDir); - } - } - } catch (IOException e) { - LOG.warn("Failed to create directory: " + archiveDir, e); - } - } - - try { - success = currentFile.moveAndClose(archiveFile); - } catch (IOException e) { - LOG.warn("Failed to archive " + currentFile + " on try #" + i, e); - success = false; - } - } - - if (!success) { - LOG.error("Failed to archive " + currentFile); - return false; - } - - if (LOG.isDebugEnabled()) { - LOG.debug("Finished archiving from " + currentFile + ", to " + archiveFile); - } - return true; - } - - /** - * Without regard for backup, delete a region. Should be used with caution. - * @param regionDir {@link Path} to the region to be deleted. - * @param fs FileSystem from which to delete the region - * @return true on successful deletion, false otherwise - * @throws IOException on filesystem operation failure - */ - private static boolean deleteRegionWithoutArchiving(FileSystem fs, Path regionDir) - throws IOException { - if (fs.delete(regionDir, true)) { - LOG.debug("Deleted all region files in: " + regionDir); - return true; - } - LOG.debug("Failed to delete region directory:" + regionDir); - return false; - } - - /** - * Just do a simple delete of the given store files - *

- * A best effort is made to delete each of the files, rather than bailing on the first failure. - *

- * This method is preferable to {@link #deleteFilesWithoutArchiving(Collection)} since it consumes - * less resources, but is limited in terms of usefulness - * @param compactedFiles store files to delete from the file system. - * @throws IOException if a file cannot be deleted. All files will be attempted to deleted before - * throwing the exception, rather than failing at the first file. - */ - private static void deleteStoreFilesWithoutArchiving(Collection compactedFiles) - throws IOException { - LOG.debug("Deleting store files without archiving."); - List errors = new ArrayList(0); - for (StoreFile hsf : compactedFiles) { - try { - hsf.deleteReader(); - } catch (IOException e) { - LOG.error("Failed to delete store file:" + hsf.getPath()); - errors.add(e); - } - } - if (errors.size() > 0) { - throw MultipleIOException.createIOException(errors); - } - } - - /** - * Adapt a type to match the {@link File} interface, which is used internally for handling - * archival/removal of files - * @param type to adapt to the {@link File} interface - */ - private static abstract class FileConverter implements Function { - protected final FileSystem fs; - - public FileConverter(FileSystem fs) { - this.fs = fs; - } - } - - /** - * Convert a FileStatus to something we can manage in the archiving - */ - private static class FileStatusConverter extends FileConverter { - public FileStatusConverter(FileSystem fs) { - super(fs); - } - - @Override - public File apply(FileStatus input) { - return new FileablePath(fs, input.getPath()); - } - } - - /** - * Convert the {@link StoreFile} into something we can manage in the archive - * methods - */ - private static class StoreToFile extends FileConverter { - public StoreToFile(FileSystem fs) { - super(fs); - } - - @Override - public File apply(StoreFile input) { - return new FileableStoreFile(fs, input); - } - } - - /** - * Wrapper to handle file operations uniformly - */ - private static abstract class File { - protected final FileSystem fs; - - public File(FileSystem fs) { - this.fs = fs; - } - - /** - * Delete the file - * @throws IOException on failure - */ - abstract void delete() throws IOException; - - /** - * Check to see if this is a file or a directory - * @return true if it is a file, false otherwise - * @throws IOException on {@link FileSystem} connection error - */ - abstract boolean isFile() throws IOException; - - /** - * @return if this is a directory, returns all the children in the - * directory, otherwise returns an empty list - * @throws IOException - */ - abstract Collection getChildren() throws IOException; - - /** - * close any outside readers of the file - * @throws IOException - */ - abstract void close() throws IOException; - - /** - * @return the name of the file (not the full fs path, just the individual - * file name) - */ - abstract String getName(); - - /** - * @return the path to this file - */ - abstract Path getPath(); - - /** - * Move the file to the given destination - * @param dest - * @return true on success - * @throws IOException - */ - public boolean moveAndClose(Path dest) throws IOException { - this.close(); - Path p = this.getPath(); - return FSUtils.renameAndSetModifyTime(fs, p, dest); - } - - /** - * @return the {@link FileSystem} on which this file resides - */ - public FileSystem getFileSystem() { - return this.fs; - } - - @Override - public String toString() { - return this.getClass() + ", file:" + getPath().toString(); - } - } - - /** - * A {@link File} that wraps a simple {@link Path} on a {@link FileSystem}. - */ - private static class FileablePath extends File { - private final Path file; - private final FileStatusConverter getAsFile; - - public FileablePath(FileSystem fs, Path file) { - super(fs); - this.file = file; - this.getAsFile = new FileStatusConverter(fs); - } - - @Override - public void delete() throws IOException { - if (!fs.delete(file, true)) throw new IOException("Failed to delete:" + this.file); - } - - @Override - public String getName() { - return file.getName(); - } - - @Override - public Collection getChildren() throws IOException { - if (fs.isFile(file)) return Collections.emptyList(); - return Collections2.transform(Arrays.asList(fs.listStatus(file)), getAsFile); - } - - @Override - public boolean isFile() throws IOException { - return fs.isFile(file); - } - - @Override - public void close() throws IOException { - // NOOP - files are implicitly closed on removal - } - - @Override - Path getPath() { - return file; - } - } - - /** - * {@link File} adapter for a {@link StoreFile} living on a {@link FileSystem} - * . - */ - private static class FileableStoreFile extends File { - StoreFile file; - - public FileableStoreFile(FileSystem fs, StoreFile store) { - super(fs); - this.file = store; - } - - @Override - public void delete() throws IOException { - file.deleteReader(); - } - - @Override - public String getName() { - return file.getPath().getName(); - } - - @Override - public boolean isFile() { - return true; - } - - @Override - public Collection getChildren() throws IOException { - // storefiles don't have children - return Collections.emptyList(); - } - - @Override - public void close() throws IOException { - file.closeReader(true); - } - - @Override - Path getPath() { - return file.getPath(); - } - } -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveManager.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveManager.java deleted file mode 100644 index 85b1135..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveManager.java +++ /dev/null @@ -1,168 +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 - * - * 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.backup.example; - -import java.io.IOException; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.hbase.classification.InterfaceAudience; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.hbase.ZooKeeperConnectionException; -import org.apache.hadoop.hbase.client.HConnection; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.zookeeper.ZKUtil; -import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; -import org.apache.zookeeper.KeeperException; - -/** - * Client-side manager for which table's hfiles should be preserved for long-term archive. - * @see ZKTableArchiveClient - * @see HFileArchiveTableMonitor - * @see LongTermArchivingHFileCleaner - */ -@InterfaceAudience.Private -class HFileArchiveManager { - - private final String archiveZnode; - private static final Log LOG = LogFactory.getLog(HFileArchiveManager.class); - private final ZooKeeperWatcher zooKeeper; - private volatile boolean stopped = false; - - public HFileArchiveManager(HConnection connection, Configuration conf) - throws ZooKeeperConnectionException, IOException { - this.zooKeeper = new ZooKeeperWatcher(conf, "hfileArchiveManager-on-" + connection.toString(), - connection); - this.archiveZnode = ZKTableArchiveClient.getArchiveZNode(this.zooKeeper.getConfiguration(), - this.zooKeeper); - } - - /** - * Turn on auto-backups of HFiles on the specified table. - *

- * When HFiles would be deleted from the hfile archive, they are instead preserved. - * @param table name of the table for which to preserve hfiles. - * @return this for chaining. - * @throws KeeperException if we can't reach zookeeper to update the hfile cleaner. - */ - public HFileArchiveManager enableHFileBackup(byte[] table) throws KeeperException { - enable(this.zooKeeper, table); - return this; - } - - /** - * Stop retaining HFiles for the given table in the archive. HFiles will be cleaned up on the next - * pass of the {@link HFileCleaner}, if the HFiles are retained by another cleaner. - * @param table name of the table for which to disable hfile retention. - * @return this for chaining. - * @throws KeeperException if if we can't reach zookeeper to update the hfile cleaner. - */ - public HFileArchiveManager disableHFileBackup(byte[] table) throws KeeperException { - disable(this.zooKeeper, table); - return this; - } - - /** - * Disable long-term archival of all hfiles for all tables in the cluster. - * @return this for chaining. - * @throws IOException if the number of attempts is exceeded - */ - public HFileArchiveManager disableHFileBackup() throws IOException { - LOG.debug("Disabling backups on all tables."); - try { - ZKUtil.deleteNodeRecursively(this.zooKeeper, archiveZnode); - return this; - } catch (KeeperException e) { - throw new IOException("Unexpected ZK exception!", e); - } - } - - /** - * Perform a best effort enable of hfile retention, which relies on zookeeper communicating the // - * * change back to the hfile cleaner. - *

- * No attempt is made to make sure that backups are successfully created - it is inherently an - * asynchronous operation. - * @param zooKeeper watcher connection to zk cluster - * @param table table name on which to enable archiving - * @throws KeeperException - */ - private void enable(ZooKeeperWatcher zooKeeper, byte[] table) - throws KeeperException { - LOG.debug("Ensuring archiving znode exists"); - ZKUtil.createAndFailSilent(zooKeeper, archiveZnode); - - // then add the table to the list of znodes to archive - String tableNode = this.getTableNode(table); - LOG.debug("Creating: " + tableNode + ", data: []"); - ZKUtil.createSetData(zooKeeper, tableNode, new byte[0]); - } - - /** - * Disable all archiving of files for a given table - *

- * Inherently an asynchronous operation. - * @param zooKeeper watcher for the ZK cluster - * @param table name of the table to disable - * @throws KeeperException if an unexpected ZK connection issues occurs - */ - private void disable(ZooKeeperWatcher zooKeeper, byte[] table) throws KeeperException { - // ensure the latest state of the archive node is found - zooKeeper.sync(archiveZnode); - - // if the top-level archive node is gone, then we are done - if (ZKUtil.checkExists(zooKeeper, archiveZnode) < 0) { - return; - } - // delete the table node, from the archive - String tableNode = this.getTableNode(table); - // make sure the table is the latest version so the delete takes - zooKeeper.sync(tableNode); - - LOG.debug("Attempting to delete table node:" + tableNode); - ZKUtil.deleteNodeRecursively(zooKeeper, tableNode); - } - - public void stop() { - if (!this.stopped) { - this.stopped = true; - LOG.debug("Stopping HFileArchiveManager..."); - this.zooKeeper.close(); - } - } - - /** - * Check to see if the table is currently marked for archiving - * @param table name of the table to check - * @return true if the archive znode for that table exists, false if not - * @throws KeeperException if an unexpected zookeeper error occurs - */ - public boolean isArchivingEnabled(byte[] table) throws KeeperException { - String tableNode = this.getTableNode(table); - return ZKUtil.checkExists(zooKeeper, tableNode) >= 0; - } - - /** - * Get the zookeeper node associated with archiving the given table - * @param table name of the table to check - * @return znode for the table's archive status - */ - private String getTableNode(byte[] table) { - return ZKUtil.joinZNode(archiveZnode, Bytes.toString(table)); - } -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveTableMonitor.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveTableMonitor.java deleted file mode 100644 index 3258cbb..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/HFileArchiveTableMonitor.java +++ /dev/null @@ -1,78 +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 - * - * 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.backup.example; - -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Monitor the actual tables for which HFiles are archived for long-term retention (always kept - * unless ZK state changes). - *

- * It is internally synchronized to ensure consistent view of the table state. - */ -public class HFileArchiveTableMonitor { - private static final Log LOG = LogFactory.getLog(HFileArchiveTableMonitor.class); - private final Set archivedTables = new TreeSet(); - - /** - * Set the tables to be archived. Internally adds each table and attempts to - * register it. - *

- * Note: All previous tables will be removed in favor of these tables. - * @param tables add each of the tables to be archived. - */ - public synchronized void setArchiveTables(List tables) { - archivedTables.clear(); - archivedTables.addAll(tables); - } - - /** - * Add the named table to be those being archived. Attempts to register the - * table - * @param table name of the table to be registered - */ - public synchronized void addTable(String table) { - if (this.shouldArchiveTable(table)) { - LOG.debug("Already archiving table: " + table + ", ignoring it"); - return; - } - archivedTables.add(table); - } - - public synchronized void removeTable(String table) { - archivedTables.remove(table); - } - - public synchronized void clearArchive() { - archivedTables.clear(); - } - - /** - * Determine if the given table should or should not allow its hfiles to be deleted in the archive - * @param tableName name of the table to check - * @return true if its store files should be retained, false otherwise - */ - public synchronized boolean shouldArchiveTable(String tableName) { - return archivedTables.contains(tableName); - } -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/LongTermArchivingHFileCleaner.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/LongTermArchivingHFileCleaner.java deleted file mode 100644 index 09a6659..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/LongTermArchivingHFileCleaner.java +++ /dev/null @@ -1,109 +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 - * - * 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.backup.example; - -import java.io.IOException; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.hbase.classification.InterfaceAudience; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.hbase.HBaseInterfaceAudience; -import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate; -import org.apache.hadoop.hbase.util.FSUtils; -import org.apache.zookeeper.KeeperException; - -/** - * {@link BaseHFileCleanerDelegate} that only cleans HFiles that don't belong to a table that is - * currently being archived. - *

- * This only works properly if the - * {@link org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner} - * is also enabled (it always should be), since it may take a little time - * for the ZK notification to propagate, in which case we may accidentally - * delete some files. - */ -@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG) -public class LongTermArchivingHFileCleaner extends BaseHFileCleanerDelegate { - - private static final Log LOG = LogFactory.getLog(LongTermArchivingHFileCleaner.class); - - TableHFileArchiveTracker archiveTracker; - private FileSystem fs; - - @Override - public boolean isFileDeletable(FileStatus fStat) { - try { - // if its a directory, then it can be deleted - if (fStat.isDirectory()) return true; - - Path file = fStat.getPath(); - // check to see if - FileStatus[] deleteStatus = FSUtils.listStatus(this.fs, file, null); - // if the file doesn't exist, then it can be deleted (but should never - // happen since deleted files shouldn't get passed in) - if (deleteStatus == null) return true; - - // otherwise, we need to check the file's table and see its being archived - Path family = file.getParent(); - Path region = family.getParent(); - Path table = region.getParent(); - - String tableName = table.getName(); - boolean ret = !archiveTracker.keepHFiles(tableName); - LOG.debug("Archiver says to [" + (ret ? "delete" : "keep") + "] files for table:" + tableName); - return ret; - } catch (IOException e) { - LOG.error("Failed to lookup status of:" + fStat.getPath() + ", keeping it just incase.", e); - return false; - } - } - - @Override - public void setConf(Configuration config) { - // setup our own zookeeper connection - // Make my own Configuration. Then I'll have my own connection to zk that - // I can close myself when comes time. - Configuration conf = new Configuration(config); - super.setConf(conf); - try { - this.fs = FileSystem.get(conf); - this.archiveTracker = TableHFileArchiveTracker.create(conf); - this.archiveTracker.start(); - } catch (KeeperException e) { - LOG.error("Error while configuring " + this.getClass().getName(), e); - } catch (IOException e) { - LOG.error("Error while configuring " + this.getClass().getName(), e); - } - } - - @Override - public void stop(String reason) { - if (this.isStopped()) return; - super.stop(reason); - if (this.archiveTracker != null) { - LOG.info("Stopping " + this.archiveTracker); - this.archiveTracker.stop(); - } - - } - -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/TableHFileArchiveTracker.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/TableHFileArchiveTracker.java deleted file mode 100644 index 33ae82f..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/TableHFileArchiveTracker.java +++ /dev/null @@ -1,267 +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 - * - * 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.backup.example; - -import java.io.IOException; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.hbase.classification.InterfaceAudience; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.hbase.ZooKeeperConnectionException; -import org.apache.hadoop.hbase.zookeeper.ZKUtil; -import org.apache.hadoop.hbase.zookeeper.ZooKeeperListener; -import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; -import org.apache.zookeeper.KeeperException; - -/** - * Track HFile archiving state changes in ZooKeeper. Keeps track of the tables whose HFiles should - * be kept in the archive. - *

- * {@link TableHFileArchiveTracker#start()} needs to be called to start monitoring for tables to - * archive. - */ -@InterfaceAudience.Private -public class TableHFileArchiveTracker extends ZooKeeperListener { - private static final Log LOG = LogFactory.getLog(TableHFileArchiveTracker.class); - public static final String HFILE_ARCHIVE_ZNODE_PARENT = "hfilearchive"; - private HFileArchiveTableMonitor monitor; - private String archiveHFileZNode; - private boolean stopped = false; - - private TableHFileArchiveTracker(ZooKeeperWatcher watcher, HFileArchiveTableMonitor monitor) { - super(watcher); - watcher.registerListener(this); - this.monitor = monitor; - this.archiveHFileZNode = ZKTableArchiveClient.getArchiveZNode(watcher.getConfiguration(), - watcher); - } - - /** - * Start monitoring for archive updates - * @throws KeeperException on failure to find/create nodes - */ - public void start() throws KeeperException { - // if archiving is enabled, then read in the list of tables to archive - LOG.debug("Starting hfile archive tracker..."); - this.checkEnabledAndUpdate(); - LOG.debug("Finished starting hfile archive tracker!"); - } - - @Override - public void nodeCreated(String path) { - // if it is the archive path - if (!path.startsWith(archiveHFileZNode)) return; - - LOG.debug("Archive node: " + path + " created"); - // since we are already enabled, just update a single table - String table = path.substring(archiveHFileZNode.length()); - - // the top level node has come up, so read in all the tables - if (table.length() == 0) { - - checkEnabledAndUpdate(); - return; - } - // find the table that needs to be archived - try { - addAndReWatchTable(path); - } catch (KeeperException e) { - LOG.warn("Couldn't read zookeeper data for table for path:" + path - + ", not preserving a table.", e); - } - } - - @Override - public void nodeChildrenChanged(String path) { - if (!path.startsWith(archiveHFileZNode)) return; - - LOG.debug("Archive node: " + path + " children changed."); - // a table was added to the archive - try { - updateWatchedTables(); - } catch (KeeperException e) { - LOG.error("Failed to update tables to archive", e); - } - } - - /** - * Add this table to the tracker and then read a watch on that node. - *

- * Handles situation where table is deleted in the time between the update and resetting the watch - * by deleting the table via {@link #safeStopTrackingTable(String)} - * @param tableZnode full zookeeper path to the table to be added - * @throws KeeperException if an unexpected zk exception occurs - */ - private void addAndReWatchTable(String tableZnode) throws KeeperException { - getMonitor().addTable(ZKUtil.getNodeName(tableZnode)); - // re-add a watch to the table created - // and check to make sure it wasn't deleted - if (!ZKUtil.watchAndCheckExists(watcher, tableZnode)) { - safeStopTrackingTable(tableZnode); - } - } - - /** - * Stop tracking a table. Ensures that the table doesn't exist, but if it does, it attempts to add - * the table back via {@link #addAndReWatchTable(String)} - its a 'safe' removal. - * @param tableZnode full zookeeper path to the table to be added - * @throws KeeperException if an unexpected zk exception occurs - */ - private void safeStopTrackingTable(String tableZnode) throws KeeperException { - getMonitor().removeTable(ZKUtil.getNodeName(tableZnode)); - // if the table exists, then add and rewatch it - if (ZKUtil.checkExists(watcher, tableZnode) >= 0) { - addAndReWatchTable(tableZnode); - } - } - - @Override - public void nodeDeleted(String path) { - if (!path.startsWith(archiveHFileZNode)) return; - - LOG.debug("Archive node: " + path + " deleted"); - String table = path.substring(archiveHFileZNode.length()); - // if we stop archiving all tables - if (table.length() == 0) { - // make sure we have the tracker before deleting the archive - // but if we don't, we don't care about delete - clearTables(); - // watches are one-time events, so we need to renew our subscription to - // the archive node and might as well check to make sure archiving - // didn't come back on at the same time - checkEnabledAndUpdate(); - return; - } - // just stop archiving one table - // note that we don't attempt to add another watch for that table into zk. - // We have no assurances that the table will be archived again (or even - // exists for that matter), so its better not to add unnecessary load to - // zk for watches. If the table is created again, then we will get the - // notification in childrenChanaged. - getMonitor().removeTable(ZKUtil.getNodeName(path)); - } - - /** - * Sets the watch on the top-level archive znode, and then updates the monitor with the current - * tables that should be archived (and ensures that those nodes are watched as well). - */ - private void checkEnabledAndUpdate() { - try { - if (ZKUtil.watchAndCheckExists(watcher, archiveHFileZNode)) { - LOG.debug(archiveHFileZNode + " znode does exist, checking for tables to archive"); - - // update the tables we should backup, to get the most recent state. - // This is safer than also watching for children and then hoping we get - // all the updates as it makes sure we get and watch all the children - updateWatchedTables(); - } else { - LOG.debug("Archiving not currently enabled, waiting"); - } - } catch (KeeperException e) { - LOG.warn("Failed to watch for archiving znode", e); - } - } - - /** - * Read the list of children under the archive znode as table names and then sets those tables to - * the list of tables that we should archive - * @throws KeeperException if there is an unexpected zk exception - */ - private void updateWatchedTables() throws KeeperException { - // get the children and watch for new children - LOG.debug("Updating watches on tables to archive."); - // get the children and add watches for each of the children - List tables = ZKUtil.listChildrenAndWatchThem(watcher, archiveHFileZNode); - LOG.debug("Starting archive for tables:" + tables); - // if archiving is still enabled - if (tables != null && tables.size() > 0) { - getMonitor().setArchiveTables(tables); - } else { - LOG.debug("No tables to archive."); - // only if we currently have a tracker, then clear the archive - clearTables(); - } - } - - /** - * Remove the currently archived tables. - *

- * Does some intelligent checking to make sure we don't prematurely create an archive tracker. - */ - private void clearTables() { - getMonitor().clearArchive(); - } - - /** - * Determine if the given table should or should not allow its hfiles to be deleted - * @param tableName name of the table to check - * @return true if its store files should be retained, false otherwise - */ - public boolean keepHFiles(String tableName) { - return getMonitor().shouldArchiveTable(tableName); - } - - /** - * @return the tracker for which tables should be archived. - */ - public final HFileArchiveTableMonitor getMonitor() { - return this.monitor; - } - - /** - * Create an archive tracker for the passed in server - * @param conf to read for zookeeper connection information - * @return ZooKeeper tracker to monitor for this server if this server should archive hfiles for a - * given table - * @throws IOException If a unexpected exception occurs - * @throws ZooKeeperConnectionException if we can't reach zookeeper - */ - public static TableHFileArchiveTracker create(Configuration conf) - throws ZooKeeperConnectionException, IOException { - ZooKeeperWatcher zkw = new ZooKeeperWatcher(conf, "hfileArchiveCleaner", null); - return create(zkw, new HFileArchiveTableMonitor()); - } - - /** - * Create an archive tracker with the special passed in table monitor. Should only be used in - * special cases (e.g. testing) - * @param zkw Watcher for the ZooKeeper cluster that we should track - * @param monitor Monitor for which tables need hfile archiving - * @return ZooKeeper tracker to monitor for this server if this server should archive hfiles for a - * given table - */ - private static TableHFileArchiveTracker create(ZooKeeperWatcher zkw, - HFileArchiveTableMonitor monitor) { - return new TableHFileArchiveTracker(zkw, monitor); - } - - public ZooKeeperWatcher getZooKeeperWatcher() { - return this.watcher; - } - - /** - * Stop this tracker and the passed zookeeper - */ - public void stop() { - if (this.stopped) return; - this.stopped = true; - this.watcher.close(); - } -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/ZKTableArchiveClient.java hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/ZKTableArchiveClient.java deleted file mode 100644 index 6f06476..0000000 --- hbase-server/src/main/java/org/apache/hadoop/hbase/backup/example/ZKTableArchiveClient.java +++ /dev/null @@ -1,155 +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 - * - * 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.backup.example; - -import java.io.IOException; - -import org.apache.hadoop.hbase.classification.InterfaceAudience; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.conf.Configured; -import org.apache.hadoop.hbase.client.ClusterConnection; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.zookeeper.ZKUtil; -import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; -import org.apache.zookeeper.KeeperException; - -/** - * Example class for how to use the table archiving coordinated via zookeeper - */ -@InterfaceAudience.Private -public class ZKTableArchiveClient extends Configured { - - /** Configuration key for the archive node. */ - private static final String ZOOKEEPER_ZNODE_HFILE_ARCHIVE_KEY = "zookeeper.znode.hfile.archive"; - private ClusterConnection connection; - - public ZKTableArchiveClient(Configuration conf, ClusterConnection connection) { - super(conf); - this.connection = connection; - } - - /** - * Turn on backups for all HFiles for the given table. - *

- * All deleted hfiles are moved to the archive directory under the table directory, rather than - * being deleted. - *

- * If backups are already enabled for this table, does nothing. - *

- * If the table does not exist, the archiving the table's hfiles is still enabled as a future - * table with that name may be created shortly. - * @param table name of the table to start backing up - * @throws IOException if an unexpected exception occurs - * @throws KeeperException if zookeeper can't be reached - */ - public void enableHFileBackupAsync(final byte[] table) throws IOException, KeeperException { - createHFileArchiveManager().enableHFileBackup(table).stop(); - } - - /** - * Disable hfile backups for the given table. - *

- * Previously backed up files are still retained (if present). - *

- * Asynchronous operation - some extra HFiles may be retained, in the archive directory after - * disable is called, dependent on the latency in zookeeper to the servers. - * @param table name of the table stop backing up - * @throws IOException if an unexpected exception occurs - * @throws KeeperException if zookeeper can't be reached - */ - public void disableHFileBackup(String table) throws IOException, KeeperException { - disableHFileBackup(Bytes.toBytes(table)); - } - - /** - * Disable hfile backups for the given table. - *

- * Previously backed up files are still retained (if present). - *

- * Asynchronous operation - some extra HFiles may be retained, in the archive directory after - * disable is called, dependent on the latency in zookeeper to the servers. - * @param table name of the table stop backing up - * @throws IOException if an unexpected exception occurs - * @throws KeeperException if zookeeper can't be reached - */ - public void disableHFileBackup(final byte[] table) throws IOException, KeeperException { - createHFileArchiveManager().disableHFileBackup(table).stop(); - } - - /** - * Disable hfile backups for all tables. - *

- * Previously backed up files are still retained (if present). - *

- * Asynchronous operation - some extra HFiles may be retained, in the archive directory after - * disable is called, dependent on the latency in zookeeper to the servers. - * @throws IOException if an unexpected exception occurs - * @throws KeeperException if zookeeper can't be reached - */ - public void disableHFileBackup() throws IOException, KeeperException { - createHFileArchiveManager().disableHFileBackup().stop(); - } - - /** - * Determine if archiving is enabled (but not necessarily fully propagated) for a table - * @param table name of the table to check - * @return true if it is, false otherwise - * @throws IOException if a connection to ZooKeeper cannot be established - * @throws KeeperException - */ - public boolean getArchivingEnabled(byte[] table) throws IOException, KeeperException { - HFileArchiveManager manager = createHFileArchiveManager(); - try { - return manager.isArchivingEnabled(table); - } finally { - manager.stop(); - } - } - - /** - * Determine if archiving is enabled (but not necessarily fully propagated) for a table - * @param table name of the table to check - * @return true if it is, false otherwise - * @throws IOException if an unexpected network issue occurs - * @throws KeeperException if zookeeper can't be reached - */ - public boolean getArchivingEnabled(String table) throws IOException, KeeperException { - return getArchivingEnabled(Bytes.toBytes(table)); - } - - /** - * @return A new {@link HFileArchiveManager} to manage which tables' hfiles should be archived - * rather than deleted. - * @throws KeeperException if we can't reach zookeeper - * @throws IOException if an unexpected network issue occurs - */ - private synchronized HFileArchiveManager createHFileArchiveManager() throws KeeperException, - IOException { - return new HFileArchiveManager(this.connection, this.getConf()); - } - - /** - * @param conf conf to read for the base archive node - * @param zooKeeper zookeeper to used for building the full path - * @return get the znode for long-term archival of a table for - */ - public static String getArchiveZNode(Configuration conf, ZooKeeperWatcher zooKeeper) { - return ZKUtil.joinZNode(zooKeeper.baseZNode, conf.get(ZOOKEEPER_ZNODE_HFILE_ARCHIVE_KEY, - TableHFileArchiveTracker.HFILE_ARCHIVE_ZNODE_PARENT)); - } -} diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/master/CatalogJanitor.java hbase-server/src/main/java/org/apache/hadoop/hbase/master/CatalogJanitor.java index b9abc65..04f3d03 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/master/CatalogJanitor.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/master/CatalogJanitor.java @@ -39,13 +39,13 @@ import org.apache.hadoop.hbase.MetaTableAccessor; import org.apache.hadoop.hbase.ScheduledChore; import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.regionserver.HRegionFileSystem; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.hbase.util.PairOfSameType; import org.apache.hadoop.hbase.util.Triple; diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java index 43ae2f8..f9b4ea1 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterFileSystem.java @@ -45,7 +45,6 @@ import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.TableDescriptor; import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.exceptions.DeserializationException; import org.apache.hadoop.hbase.fs.HFileSystem; import org.apache.hadoop.hbase.mob.MobConstants; @@ -58,6 +57,7 @@ import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.FSTableDescriptors; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.ipc.RemoteException; import com.google.common.annotations.VisibleForTesting; diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/DeleteTableProcedure.java hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/DeleteTableProcedure.java index ff22b88..56f606e 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/DeleteTableProcedure.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/DeleteTableProcedure.java @@ -35,7 +35,6 @@ import org.apache.hadoop.hbase.MetaTableAccessor; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableNotDisabledException; import org.apache.hadoop.hbase.TableNotFoundException; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.Delete; @@ -56,6 +55,7 @@ import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos; import org.apache.hadoop.hbase.protobuf.generated.MasterProcedureProtos.DeleteTableState; import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.security.UserGroupInformation; @InterfaceAudience.Private diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/mob/MobUtils.java hbase-server/src/main/java/org/apache/hadoop/hbase/mob/MobUtils.java index 52a19f5..d80abd0 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/mob/MobUtils.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/mob/MobUtils.java @@ -52,7 +52,6 @@ import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.Tag; import org.apache.hadoop.hbase.TagType; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.io.HFileLink; @@ -71,6 +70,7 @@ import org.apache.hadoop.hbase.regionserver.StoreFile; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.hbase.util.ReflectionUtils; import org.apache.hadoop.hbase.util.Threads; diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java index 406850e..9d7f64d 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegion.java @@ -96,7 +96,6 @@ import org.apache.hadoop.hbase.Tag; import org.apache.hadoop.hbase.TagRewriteCell; import org.apache.hadoop.hbase.TagUtil; import org.apache.hadoop.hbase.UnknownScannerException; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.client.Append; import org.apache.hadoop.hbase.client.Delete; @@ -169,6 +168,7 @@ import org.apache.hadoop.hbase.util.Counter; import org.apache.hadoop.hbase.util.EncryptionTest; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.hbase.util.HashedBytes; import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil; diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionFileSystem.java hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionFileSystem.java index 74ff546..6584602 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionFileSystem.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionFileSystem.java @@ -46,12 +46,12 @@ import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.KeyValueUtil; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.fs.HFileSystem; import org.apache.hadoop.hbase.io.Reference; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.FSHDFSUtils; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.hbase.util.ServerRegionReplicaUtil; /** diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/RestoreSnapshotHelper.java hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/RestoreSnapshotHelper.java index 0f14f70..43a4765 100644 --- hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/RestoreSnapshotHelper.java +++ hbase-server/src/main/java/org/apache/hadoop/hbase/snapshot/RestoreSnapshotHelper.java @@ -43,7 +43,6 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.backup.HFileArchiver; import org.apache.hadoop.hbase.MetaTableAccessor; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.errorhandling.ForeignExceptionDispatcher; @@ -59,6 +58,7 @@ import org.apache.hadoop.hbase.regionserver.HRegionFileSystem; import org.apache.hadoop.hbase.regionserver.StoreFileInfo; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiver; import org.apache.hadoop.hbase.util.ModifyRegionUtils; import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.io.IOUtils; diff --git hbase-server/src/main/java/org/apache/hadoop/hbase/util/HFileArchiver.java hbase-server/src/main/java/org/apache/hadoop/hbase/util/HFileArchiver.java new file mode 100644 index 0000000..94da578 --- /dev/null +++ hbase-server/src/main/java/org/apache/hadoop/hbase/util/HFileArchiver.java @@ -0,0 +1,688 @@ +/** + * 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.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathFilter; +import org.apache.hadoop.hbase.HRegionInfo; +import org.apache.hadoop.hbase.regionserver.HRegion; +import org.apache.hadoop.hbase.regionserver.StoreFile; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; +import org.apache.hadoop.io.MultipleIOException; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; + +/** + * Utility class to handle the removal of HFiles (or the respective {@link StoreFile StoreFiles}) + * for a HRegion from the {@link FileSystem}. The hfiles will be archived or deleted, depending on + * the state of the system. + */ +public class HFileArchiver { + private static final Log LOG = LogFactory.getLog(HFileArchiver.class); + private static final String SEPARATOR = "."; + + /** Number of retries in case of fs operation failure */ + private static final int DEFAULT_RETRIES_NUMBER = 3; + + private HFileArchiver() { + // hidden ctor since this is just a util + } + + /** + * Cleans up all the files for a HRegion by archiving the HFiles to the + * archive directory + * @param conf the configuration to use + * @param fs the file system object + * @param info HRegionInfo for region to be deleted + * @throws IOException + */ + public static void archiveRegion(Configuration conf, FileSystem fs, HRegionInfo info) + throws IOException { + Path rootDir = FSUtils.getRootDir(conf); + archiveRegion(fs, rootDir, FSUtils.getTableDir(rootDir, info.getTable()), + HRegion.getRegionDir(rootDir, info)); + } + + /** + * Remove an entire region from the table directory via archiving the region's hfiles. + * @param fs {@link FileSystem} from which to remove the region + * @param rootdir {@link Path} to the root directory where hbase files are stored (for building + * the archive path) + * @param tableDir {@link Path} to where the table is being stored (for building the archive path) + * @param regionDir {@link Path} to where a region is being stored (for building the archive path) + * @return true if the region was sucessfully deleted. false if the filesystem + * operations could not complete. + * @throws IOException if the request cannot be completed + */ + public static boolean archiveRegion(FileSystem fs, Path rootdir, Path tableDir, Path regionDir) + throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("ARCHIVING " + regionDir.toString()); + } + + // otherwise, we archive the files + // make sure we can archive + if (tableDir == null || regionDir == null) { + LOG.error("No archive directory could be found because tabledir (" + tableDir + + ") or regiondir (" + regionDir + "was null. Deleting files instead."); + deleteRegionWithoutArchiving(fs, regionDir); + // we should have archived, but failed to. Doesn't matter if we deleted + // the archived files correctly or not. + return false; + } + + // make sure the regiondir lives under the tabledir + Preconditions.checkArgument(regionDir.toString().startsWith(tableDir.toString())); + Path regionArchiveDir = HFileArchiveUtil.getRegionArchiveDir(rootdir, + FSUtils.getTableName(tableDir), + regionDir.getName()); + + FileStatusConverter getAsFile = new FileStatusConverter(fs); + // otherwise, we attempt to archive the store files + + // build collection of just the store directories to archive + Collection toArchive = new ArrayList(); + final PathFilter dirFilter = new FSUtils.DirFilter(fs); + PathFilter nonHidden = new PathFilter() { + @Override + public boolean accept(Path file) { + return dirFilter.accept(file) && !file.getName().toString().startsWith("."); + } + }; + FileStatus[] storeDirs = FSUtils.listStatus(fs, regionDir, nonHidden); + // if there no files, we can just delete the directory and return; + if (storeDirs == null) { + LOG.debug("Region directory (" + regionDir + ") was empty, just deleting and returning!"); + return deleteRegionWithoutArchiving(fs, regionDir); + } + + // convert the files in the region to a File + toArchive.addAll(Lists.transform(Arrays.asList(storeDirs), getAsFile)); + LOG.debug("Archiving " + toArchive); + boolean success = false; + try { + success = resolveAndArchive(fs, regionArchiveDir, toArchive); + } catch (IOException e) { + LOG.error("Failed to archive " + toArchive, e); + success = false; + } + + // if that was successful, then we delete the region + if (success) { + return deleteRegionWithoutArchiving(fs, regionDir); + } + + throw new IOException("Received error when attempting to archive files (" + toArchive + + "), cannot delete region directory. "); + } + + /** + * Remove from the specified region the store files of the specified column family, + * either by archiving them or outright deletion + * @param fs the filesystem where the store files live + * @param conf {@link Configuration} to examine to determine the archive directory + * @param parent Parent region hosting the store files + * @param tableDir {@link Path} to where the table is being stored (for building the archive path) + * @param family the family hosting the store files + * @throws IOException if the files could not be correctly disposed. + */ + public static void archiveFamily(FileSystem fs, Configuration conf, + HRegionInfo parent, Path tableDir, byte[] family) throws IOException { + Path familyDir = new Path(tableDir, new Path(parent.getEncodedName(), Bytes.toString(family))); + FileStatus[] storeFiles = FSUtils.listStatus(fs, familyDir); + if (storeFiles == null) { + LOG.debug("No store files to dispose for region=" + parent.getRegionNameAsString() + + ", family=" + Bytes.toString(family)); + return; + } + + FileStatusConverter getAsFile = new FileStatusConverter(fs); + Collection toArchive = Lists.transform(Arrays.asList(storeFiles), getAsFile); + Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, parent, tableDir, family); + + // do the actual archive + if (!resolveAndArchive(fs, storeArchiveDir, toArchive)) { + throw new IOException("Failed to archive/delete all the files for region:" + + Bytes.toString(parent.getRegionName()) + ", family:" + Bytes.toString(family) + + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); + } + } + + /** + * Remove the store files, either by archiving them or outright deletion + * @param conf {@link Configuration} to examine to determine the archive directory + * @param fs the filesystem where the store files live + * @param regionInfo {@link HRegionInfo} of the region hosting the store files + * @param family the family hosting the store files + * @param compactedFiles files to be disposed of. No further reading of these files should be + * attempted; otherwise likely to cause an {@link IOException} + * @throws IOException if the files could not be correctly disposed. + */ + public static void archiveStoreFiles(Configuration conf, FileSystem fs, HRegionInfo regionInfo, + Path tableDir, byte[] family, Collection compactedFiles) throws IOException { + + // sometimes in testing, we don't have rss, so we need to check for that + if (fs == null) { + LOG.warn("Passed filesystem is null, so just deleting the files without archiving for region:" + + Bytes.toString(regionInfo.getRegionName()) + ", family:" + Bytes.toString(family)); + deleteStoreFilesWithoutArchiving(compactedFiles); + return; + } + + // short circuit if we don't have any files to delete + if (compactedFiles.size() == 0) { + LOG.debug("No store files to dispose, done!"); + return; + } + + // build the archive path + if (regionInfo == null || family == null) throw new IOException( + "Need to have a region and a family to archive from."); + + Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, regionInfo, tableDir, family); + + // make sure we don't archive if we can't and that the archive dir exists + if (!fs.mkdirs(storeArchiveDir)) { + throw new IOException("Could not make archive directory (" + storeArchiveDir + ") for store:" + + Bytes.toString(family) + ", deleting compacted files instead."); + } + + // otherwise we attempt to archive the store files + if (LOG.isDebugEnabled()) LOG.debug("Archiving compacted store files."); + + // Wrap the storefile into a File + StoreToFile getStorePath = new StoreToFile(fs); + Collection storeFiles = Collections2.transform(compactedFiles, getStorePath); + + // do the actual archive + if (!resolveAndArchive(fs, storeArchiveDir, storeFiles)) { + throw new IOException("Failed to archive/delete all the files for region:" + + Bytes.toString(regionInfo.getRegionName()) + ", family:" + Bytes.toString(family) + + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); + } + } + + /** + * Archive the store file + * @param fs the filesystem where the store files live + * @param regionInfo region hosting the store files + * @param conf {@link Configuration} to examine to determine the archive directory + * @param tableDir {@link Path} to where the table is being stored (for building the archive path) + * @param family the family hosting the store files + * @param storeFile file to be archived + * @throws IOException if the files could not be correctly disposed. + */ + public static void archiveStoreFile(Configuration conf, FileSystem fs, HRegionInfo regionInfo, + Path tableDir, byte[] family, Path storeFile) throws IOException { + Path storeArchiveDir = HFileArchiveUtil.getStoreArchivePath(conf, regionInfo, tableDir, family); + // make sure we don't archive if we can't and that the archive dir exists + if (!fs.mkdirs(storeArchiveDir)) { + throw new IOException("Could not make archive directory (" + storeArchiveDir + ") for store:" + + Bytes.toString(family) + ", deleting compacted files instead."); + } + + // do the actual archive + long start = EnvironmentEdgeManager.currentTime(); + File file = new FileablePath(fs, storeFile); + if (!resolveAndArchiveFile(storeArchiveDir, file, Long.toString(start))) { + throw new IOException("Failed to archive/delete the file for region:" + + regionInfo.getRegionNameAsString() + ", family:" + Bytes.toString(family) + + " into " + storeArchiveDir + ". Something is probably awry on the filesystem."); + } + } + + /** + * Archive the given files and resolve any conflicts with existing files via appending the time + * archiving started (so all conflicts in the same group have the same timestamp appended). + *

+ * If any of the passed files to archive are directories, archives all the files under that + * directory. Archive directory structure for children is the base archive directory name + the + * parent directory and is built recursively is passed files are directories themselves. + * @param fs {@link FileSystem} on which to archive the files + * @param baseArchiveDir base archive directory to archive the given files + * @param toArchive files to be archived + * @return true on success, false otherwise + * @throws IOException on unexpected failure + */ + private static boolean resolveAndArchive(FileSystem fs, Path baseArchiveDir, + Collection toArchive) throws IOException { + if (LOG.isTraceEnabled()) LOG.trace("Starting to archive " + toArchive); + long start = EnvironmentEdgeManager.currentTime(); + List failures = resolveAndArchive(fs, baseArchiveDir, toArchive, start); + + // notify that some files were not archived. + // We can't delete the files otherwise snapshots or other backup system + // that relies on the archiver end up with data loss. + if (failures.size() > 0) { + LOG.warn("Failed to complete archive of: " + failures + + ". Those files are still in the original location, and they may slow down reads."); + return false; + } + return true; + } + + /** + * Resolve any conflict with an existing archive file via timestamp-append + * renaming of the existing file and then archive the passed in files. + * @param fs {@link FileSystem} on which to archive the files + * @param baseArchiveDir base archive directory to store the files. If any of + * the files to archive are directories, will append the name of the + * directory to the base archive directory name, creating a parallel + * structure. + * @param toArchive files/directories that need to be archvied + * @param start time the archiving started - used for resolving archive + * conflicts. + * @return the list of failed to archive files. + * @throws IOException if an unexpected file operation exception occured + */ + private static List resolveAndArchive(FileSystem fs, Path baseArchiveDir, + Collection toArchive, long start) throws IOException { + // short circuit if no files to move + if (toArchive.size() == 0) return Collections.emptyList(); + + if (LOG.isTraceEnabled()) LOG.trace("moving files to the archive directory: " + baseArchiveDir); + + // make sure the archive directory exists + if (!fs.exists(baseArchiveDir)) { + if (!fs.mkdirs(baseArchiveDir)) { + throw new IOException("Failed to create the archive directory:" + baseArchiveDir + + ", quitting archive attempt."); + } + if (LOG.isTraceEnabled()) LOG.trace("Created archive directory:" + baseArchiveDir); + } + + List failures = new ArrayList(); + String startTime = Long.toString(start); + for (File file : toArchive) { + // if its a file archive it + try { + if (LOG.isTraceEnabled()) LOG.trace("Archiving: " + file); + if (file.isFile()) { + // attempt to archive the file + if (!resolveAndArchiveFile(baseArchiveDir, file, startTime)) { + LOG.warn("Couldn't archive " + file + " into backup directory: " + baseArchiveDir); + failures.add(file); + } + } else { + // otherwise its a directory and we need to archive all files + if (LOG.isTraceEnabled()) LOG.trace(file + " is a directory, archiving children files"); + // so we add the directory name to the one base archive + Path parentArchiveDir = new Path(baseArchiveDir, file.getName()); + // and then get all the files from that directory and attempt to + // archive those too + Collection children = file.getChildren(); + failures.addAll(resolveAndArchive(fs, parentArchiveDir, children, start)); + } + } catch (IOException e) { + LOG.warn("Failed to archive " + file, e); + failures.add(file); + } + } + return failures; + } + + /** + * Attempt to archive the passed in file to the archive directory. + *

+ * If the same file already exists in the archive, it is moved to a timestamped directory under + * the archive directory and the new file is put in its place. + * @param archiveDir {@link Path} to the directory that stores the archives of the hfiles + * @param currentFile {@link Path} to the original HFile that will be archived + * @param archiveStartTime time the archiving started, to resolve naming conflicts + * @return true if the file is successfully archived. false if there was a + * problem, but the operation still completed. + * @throws IOException on failure to complete {@link FileSystem} operations. + */ + private static boolean resolveAndArchiveFile(Path archiveDir, File currentFile, + String archiveStartTime) throws IOException { + // build path as it should be in the archive + String filename = currentFile.getName(); + Path archiveFile = new Path(archiveDir, filename); + FileSystem fs = currentFile.getFileSystem(); + + // if the file already exists in the archive, move that one to a timestamped backup. This is a + // really, really unlikely situtation, where we get the same name for the existing file, but + // is included just for that 1 in trillion chance. + if (fs.exists(archiveFile)) { + if (LOG.isDebugEnabled()) { + LOG.debug("File:" + archiveFile + " already exists in archive, moving to " + + "timestamped backup and overwriting current."); + } + + // move the archive file to the stamped backup + Path backedupArchiveFile = new Path(archiveDir, filename + SEPARATOR + archiveStartTime); + if (!fs.rename(archiveFile, backedupArchiveFile)) { + LOG.error("Could not rename archive file to backup: " + backedupArchiveFile + + ", deleting existing file in favor of newer."); + // try to delete the exisiting file, if we can't rename it + if (!fs.delete(archiveFile, false)) { + throw new IOException("Couldn't delete existing archive file (" + archiveFile + + ") or rename it to the backup file (" + backedupArchiveFile + + ") to make room for similarly named file."); + } + } + LOG.debug("Backed up archive file from " + archiveFile); + } + + if (LOG.isTraceEnabled()) { + LOG.trace("No existing file in archive for: " + archiveFile + + ", free to archive original file."); + } + + // at this point, we should have a free spot for the archive file + boolean success = false; + for (int i = 0; !success && i < DEFAULT_RETRIES_NUMBER; ++i) { + if (i > 0) { + // Ensure that the archive directory exists. + // The previous "move to archive" operation has failed probably because + // the cleaner has removed our archive directory (HBASE-7643). + // (we're in a retry loop, so don't worry too much about the exception) + try { + if (!fs.exists(archiveDir)) { + if (fs.mkdirs(archiveDir)) { + LOG.debug("Created archive directory:" + archiveDir); + } + } + } catch (IOException e) { + LOG.warn("Failed to create directory: " + archiveDir, e); + } + } + + try { + success = currentFile.moveAndClose(archiveFile); + } catch (IOException e) { + LOG.warn("Failed to archive " + currentFile + " on try #" + i, e); + success = false; + } + } + + if (!success) { + LOG.error("Failed to archive " + currentFile); + return false; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Finished archiving from " + currentFile + ", to " + archiveFile); + } + return true; + } + + /** + * Without regard for backup, delete a region. Should be used with caution. + * @param regionDir {@link Path} to the region to be deleted. + * @param fs FileSystem from which to delete the region + * @return true on successful deletion, false otherwise + * @throws IOException on filesystem operation failure + */ + private static boolean deleteRegionWithoutArchiving(FileSystem fs, Path regionDir) + throws IOException { + if (fs.delete(regionDir, true)) { + LOG.debug("Deleted all region files in: " + regionDir); + return true; + } + LOG.debug("Failed to delete region directory:" + regionDir); + return false; + } + + /** + * Just do a simple delete of the given store files + *

+ * A best effort is made to delete each of the files, rather than bailing on the first failure. + *

+ * This method is preferable to {@link #deleteFilesWithoutArchiving(Collection)} since it consumes + * less resources, but is limited in terms of usefulness + * @param compactedFiles store files to delete from the file system. + * @throws IOException if a file cannot be deleted. All files will be attempted to deleted before + * throwing the exception, rather than failing at the first file. + */ + private static void deleteStoreFilesWithoutArchiving(Collection compactedFiles) + throws IOException { + LOG.debug("Deleting store files without archiving."); + List errors = new ArrayList(0); + for (StoreFile hsf : compactedFiles) { + try { + hsf.deleteReader(); + } catch (IOException e) { + LOG.error("Failed to delete store file:" + hsf.getPath()); + errors.add(e); + } + } + if (errors.size() > 0) { + throw MultipleIOException.createIOException(errors); + } + } + + /** + * Adapt a type to match the {@link File} interface, which is used internally for handling + * archival/removal of files + * @param type to adapt to the {@link File} interface + */ + private static abstract class FileConverter implements Function { + protected final FileSystem fs; + + public FileConverter(FileSystem fs) { + this.fs = fs; + } + } + + /** + * Convert a FileStatus to something we can manage in the archiving + */ + private static class FileStatusConverter extends FileConverter { + public FileStatusConverter(FileSystem fs) { + super(fs); + } + + @Override + public File apply(FileStatus input) { + return new FileablePath(fs, input.getPath()); + } + } + + /** + * Convert the {@link StoreFile} into something we can manage in the archive + * methods + */ + private static class StoreToFile extends FileConverter { + public StoreToFile(FileSystem fs) { + super(fs); + } + + @Override + public File apply(StoreFile input) { + return new FileableStoreFile(fs, input); + } + } + + /** + * Wrapper to handle file operations uniformly + */ + private static abstract class File { + protected final FileSystem fs; + + public File(FileSystem fs) { + this.fs = fs; + } + + /** + * Delete the file + * @throws IOException on failure + */ + abstract void delete() throws IOException; + + /** + * Check to see if this is a file or a directory + * @return true if it is a file, false otherwise + * @throws IOException on {@link FileSystem} connection error + */ + abstract boolean isFile() throws IOException; + + /** + * @return if this is a directory, returns all the children in the + * directory, otherwise returns an empty list + * @throws IOException + */ + abstract Collection getChildren() throws IOException; + + /** + * close any outside readers of the file + * @throws IOException + */ + abstract void close() throws IOException; + + /** + * @return the name of the file (not the full fs path, just the individual + * file name) + */ + abstract String getName(); + + /** + * @return the path to this file + */ + abstract Path getPath(); + + /** + * Move the file to the given destination + * @param dest + * @return true on success + * @throws IOException + */ + public boolean moveAndClose(Path dest) throws IOException { + this.close(); + Path p = this.getPath(); + return FSUtils.renameAndSetModifyTime(fs, p, dest); + } + + /** + * @return the {@link FileSystem} on which this file resides + */ + public FileSystem getFileSystem() { + return this.fs; + } + + @Override + public String toString() { + return this.getClass() + ", file:" + getPath().toString(); + } + } + + /** + * A {@link File} that wraps a simple {@link Path} on a {@link FileSystem}. + */ + private static class FileablePath extends File { + private final Path file; + private final FileStatusConverter getAsFile; + + public FileablePath(FileSystem fs, Path file) { + super(fs); + this.file = file; + this.getAsFile = new FileStatusConverter(fs); + } + + @Override + public void delete() throws IOException { + if (!fs.delete(file, true)) throw new IOException("Failed to delete:" + this.file); + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public Collection getChildren() throws IOException { + if (fs.isFile(file)) return Collections.emptyList(); + return Collections2.transform(Arrays.asList(fs.listStatus(file)), getAsFile); + } + + @Override + public boolean isFile() throws IOException { + return fs.isFile(file); + } + + @Override + public void close() throws IOException { + // NOOP - files are implicitly closed on removal + } + + @Override + Path getPath() { + return file; + } + } + + /** + * {@link File} adapter for a {@link StoreFile} living on a {@link FileSystem} + * . + */ + private static class FileableStoreFile extends File { + StoreFile file; + + public FileableStoreFile(FileSystem fs, StoreFile store) { + super(fs); + this.file = store; + } + + @Override + public void delete() throws IOException { + file.deleteReader(); + } + + @Override + public String getName() { + return file.getPath().getName(); + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public Collection getChildren() throws IOException { + // storefiles don't have children + return Collections.emptyList(); + } + + @Override + public void close() throws IOException { + file.closeReader(true); + } + + @Override + Path getPath() { + return file.getPath(); + } + } +} diff --git hbase-server/src/test/java/org/apache/hadoop/hbase/backup/TestHFileArchiving.java hbase-server/src/test/java/org/apache/hadoop/hbase/backup/TestHFileArchiving.java deleted file mode 100644 index e30d719..0000000 --- hbase-server/src/test/java/org/apache/hadoop/hbase/backup/TestHFileArchiving.java +++ /dev/null @@ -1,454 +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 - * - * 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.backup; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathFilter; -import org.apache.hadoop.hbase.ChoreService; -import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.HConstants; -import org.apache.hadoop.hbase.Stoppable; -import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.client.Admin; -import org.apache.hadoop.hbase.master.cleaner.HFileCleaner; -import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy; -import org.apache.hadoop.hbase.regionserver.HRegion; -import org.apache.hadoop.hbase.regionserver.HRegionServer; -import org.apache.hadoop.hbase.regionserver.Region; -import org.apache.hadoop.hbase.testclassification.MediumTests; -import org.apache.hadoop.hbase.testclassification.MiscTests; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.util.FSUtils; -import org.apache.hadoop.hbase.util.HFileArchiveTestingUtil; -import org.apache.hadoop.hbase.util.HFileArchiveUtil; -import org.apache.hadoop.hbase.util.StoppableImplementation; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.experimental.categories.Category; - -/** - * Test that the {@link HFileArchiver} correctly removes all the parts of a region when cleaning up - * a region - */ -@Category({MediumTests.class, MiscTests.class}) -public class TestHFileArchiving { - - private static final Log LOG = LogFactory.getLog(TestHFileArchiving.class); - private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); - private static final byte[] TEST_FAM = Bytes.toBytes("fam"); - - /** - * Setup the config for the cluster - */ - @BeforeClass - public static void setupCluster() throws Exception { - setupConf(UTIL.getConfiguration()); - UTIL.startMiniCluster(); - - // We don't want the cleaner to remove files. The tests do that. - UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().cancel(true); - } - - private static void setupConf(Configuration conf) { - // disable the ui - conf.setInt("hbase.regionsever.info.port", -1); - // drop the memstore size so we get flushes - conf.setInt("hbase.hregion.memstore.flush.size", 25000); - // disable major compactions - conf.setInt(HConstants.MAJOR_COMPACTION_PERIOD, 0); - - // prevent aggressive region split - conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY, - ConstantSizeRegionSplitPolicy.class.getName()); - } - - @After - public void tearDown() throws Exception { - // cleanup the archive directory - try { - clearArchiveDirectory(); - } catch (IOException e) { - Assert.fail("Failure to delete archive directory:" + e.getMessage()); - } - } - - @AfterClass - public static void cleanupTest() throws Exception { - try { - UTIL.shutdownMiniCluster(); - } catch (Exception e) { - // NOOP; - } - } - - @Test - public void testRemovesRegionDirOnArchive() throws Exception { - TableName TABLE_NAME = - TableName.valueOf("testRemovesRegionDirOnArchive"); - UTIL.createTable(TABLE_NAME, TEST_FAM); - - final Admin admin = UTIL.getHBaseAdmin(); - - // get the current store files for the region - List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); - // make sure we only have 1 region serving this table - assertEquals(1, servingRegions.size()); - HRegion region = servingRegions.get(0); - - // and load the table - UTIL.loadRegion(region, TEST_FAM); - - // shutdown the table so we can manipulate the files - admin.disableTable(TABLE_NAME); - - FileSystem fs = UTIL.getTestFileSystem(); - - // now attempt to depose the region - Path rootDir = region.getRegionFileSystem().getTableDir().getParent(); - Path regionDir = HRegion.getRegionDir(rootDir, region.getRegionInfo()); - - HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo()); - - // check for the existence of the archive directory and some files in it - Path archiveDir = HFileArchiveTestingUtil.getRegionArchiveDir(UTIL.getConfiguration(), region); - assertTrue(fs.exists(archiveDir)); - - // check to make sure the store directory was copied - // check to make sure the store directory was copied - FileStatus[] stores = fs.listStatus(archiveDir, new PathFilter() { - @Override - public boolean accept(Path p) { - if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) { - return false; - } - return true; - } - }); - assertTrue(stores.length == 1); - - // make sure we archived the store files - FileStatus[] storeFiles = fs.listStatus(stores[0].getPath()); - assertTrue(storeFiles.length > 0); - - // then ensure the region's directory isn't present - assertFalse(fs.exists(regionDir)); - - UTIL.deleteTable(TABLE_NAME); - } - - /** - * Test that the region directory is removed when we archive a region without store files, but - * still has hidden files. - * @throws Exception - */ - @Test - public void testDeleteRegionWithNoStoreFiles() throws Exception { - TableName TABLE_NAME = - TableName.valueOf("testDeleteRegionWithNoStoreFiles"); - UTIL.createTable(TABLE_NAME, TEST_FAM); - - // get the current store files for the region - List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); - // make sure we only have 1 region serving this table - assertEquals(1, servingRegions.size()); - HRegion region = servingRegions.get(0); - - FileSystem fs = region.getRegionFileSystem().getFileSystem(); - - // make sure there are some files in the regiondir - Path rootDir = FSUtils.getRootDir(fs.getConf()); - Path regionDir = HRegion.getRegionDir(rootDir, region.getRegionInfo()); - FileStatus[] regionFiles = FSUtils.listStatus(fs, regionDir, null); - Assert.assertNotNull("No files in the region directory", regionFiles); - if (LOG.isDebugEnabled()) { - List files = new ArrayList(); - for (FileStatus file : regionFiles) { - files.add(file.getPath()); - } - LOG.debug("Current files:" + files); - } - // delete the visible folders so we just have hidden files/folders - final PathFilter dirFilter = new FSUtils.DirFilter(fs); - PathFilter nonHidden = new PathFilter() { - @Override - public boolean accept(Path file) { - return dirFilter.accept(file) && !file.getName().toString().startsWith("."); - } - }; - FileStatus[] storeDirs = FSUtils.listStatus(fs, regionDir, nonHidden); - for (FileStatus store : storeDirs) { - LOG.debug("Deleting store for test"); - fs.delete(store.getPath(), true); - } - - // then archive the region - HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo()); - - // and check to make sure the region directoy got deleted - assertFalse("Region directory (" + regionDir + "), still exists.", fs.exists(regionDir)); - - UTIL.deleteTable(TABLE_NAME); - } - - @Test - public void testArchiveOnTableDelete() throws Exception { - TableName TABLE_NAME = - TableName.valueOf("testArchiveOnTableDelete"); - UTIL.createTable(TABLE_NAME, TEST_FAM); - - List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); - // make sure we only have 1 region serving this table - assertEquals(1, servingRegions.size()); - Region region = servingRegions.get(0); - - // get the parent RS and monitor - HRegionServer hrs = UTIL.getRSForFirstRegionInTable(TABLE_NAME); - FileSystem fs = hrs.getFileSystem(); - - // put some data on the region - LOG.debug("-------Loading table"); - UTIL.loadRegion(region, TEST_FAM); - - // get the hfiles in the region - List regions = hrs.getOnlineRegions(TABLE_NAME); - assertEquals("More that 1 region for test table.", 1, regions.size()); - - region = regions.get(0); - // wait for all the compactions to complete - region.waitForFlushesAndCompactions(); - - // disable table to prevent new updates - UTIL.getHBaseAdmin().disableTable(TABLE_NAME); - LOG.debug("Disabled table"); - - // remove all the files from the archive to get a fair comparison - clearArchiveDirectory(); - - // then get the current store files - byte[][]columns = region.getTableDesc().getFamiliesKeys().toArray(new byte[0][]); - List storeFiles = region.getStoreFileList(columns); - - // then delete the table so the hfiles get archived - UTIL.deleteTable(TABLE_NAME); - LOG.debug("Deleted table"); - - assertArchiveFiles(fs, storeFiles, 30000); - } - - private void assertArchiveFiles(FileSystem fs, List storeFiles, long timeout) throws IOException { - long end = System.currentTimeMillis() + timeout; - Path archiveDir = HFileArchiveUtil.getArchivePath(UTIL.getConfiguration()); - List archivedFiles = new ArrayList(); - - // We have to ensure that the DeleteTableHandler is finished. HBaseAdmin.deleteXXX() can return before all files - // are archived. We should fix HBASE-5487 and fix synchronous operations from admin. - while (System.currentTimeMillis() < end) { - archivedFiles = getAllFileNames(fs, archiveDir); - if (archivedFiles.size() >= storeFiles.size()) { - break; - } - } - - Collections.sort(storeFiles); - Collections.sort(archivedFiles); - - LOG.debug("Store files:"); - for (int i = 0; i < storeFiles.size(); i++) { - LOG.debug(i + " - " + storeFiles.get(i)); - } - LOG.debug("Archive files:"); - for (int i = 0; i < archivedFiles.size(); i++) { - LOG.debug(i + " - " + archivedFiles.get(i)); - } - - assertTrue("Archived files are missing some of the store files!", - archivedFiles.containsAll(storeFiles)); - } - - - /** - * Test that the store files are archived when a column family is removed. - * @throws Exception - */ - @Test - public void testArchiveOnTableFamilyDelete() throws Exception { - TableName TABLE_NAME = - TableName.valueOf("testArchiveOnTableFamilyDelete"); - UTIL.createTable(TABLE_NAME, new byte[][] {TEST_FAM, Bytes.toBytes("fam2")}); - - List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); - // make sure we only have 1 region serving this table - assertEquals(1, servingRegions.size()); - Region region = servingRegions.get(0); - - // get the parent RS and monitor - HRegionServer hrs = UTIL.getRSForFirstRegionInTable(TABLE_NAME); - FileSystem fs = hrs.getFileSystem(); - - // put some data on the region - LOG.debug("-------Loading table"); - UTIL.loadRegion(region, TEST_FAM); - - // get the hfiles in the region - List regions = hrs.getOnlineRegions(TABLE_NAME); - assertEquals("More that 1 region for test table.", 1, regions.size()); - - region = regions.get(0); - // wait for all the compactions to complete - region.waitForFlushesAndCompactions(); - - // disable table to prevent new updates - UTIL.getHBaseAdmin().disableTable(TABLE_NAME); - LOG.debug("Disabled table"); - - // remove all the files from the archive to get a fair comparison - clearArchiveDirectory(); - - // then get the current store files - byte[][]columns = region.getTableDesc().getFamiliesKeys().toArray(new byte[0][]); - List storeFiles = region.getStoreFileList(columns); - - // then delete the table so the hfiles get archived - UTIL.getHBaseAdmin().deleteColumnFamily(TABLE_NAME, TEST_FAM); - - assertArchiveFiles(fs, storeFiles, 30000); - - UTIL.deleteTable(TABLE_NAME); - } - - /** - * Test HFileArchiver.resolveAndArchive() race condition HBASE-7643 - */ - @Test - public void testCleaningRace() throws Exception { - final long TEST_TIME = 20 * 1000; - final ChoreService choreService = new ChoreService("TEST_SERVER_NAME"); - - Configuration conf = UTIL.getMiniHBaseCluster().getMaster().getConfiguration(); - Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace"); - FileSystem fs = UTIL.getTestFileSystem(); - - Path archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY); - Path regionDir = new Path(FSUtils.getTableDir(new Path("./"), - TableName.valueOf("table")), "abcdef"); - Path familyDir = new Path(regionDir, "cf"); - - Path sourceRegionDir = new Path(rootDir, regionDir); - fs.mkdirs(sourceRegionDir); - - Stoppable stoppable = new StoppableImplementation(); - - // The cleaner should be looping without long pauses to reproduce the race condition. - HFileCleaner cleaner = new HFileCleaner(1, stoppable, conf, fs, archiveDir); - try { - choreService.scheduleChore(cleaner); - - // Keep creating/archiving new files while the cleaner is running in the other thread - long startTime = System.currentTimeMillis(); - for (long fid = 0; (System.currentTimeMillis() - startTime) < TEST_TIME; ++fid) { - Path file = new Path(familyDir, String.valueOf(fid)); - Path sourceFile = new Path(rootDir, file); - Path archiveFile = new Path(archiveDir, file); - - fs.createNewFile(sourceFile); - - try { - // Try to archive the file - HFileArchiver.archiveRegion(fs, rootDir, - sourceRegionDir.getParent(), sourceRegionDir); - - // The archiver succeded, the file is no longer in the original location - // but it's in the archive location. - LOG.debug("hfile=" + fid + " should be in the archive"); - assertTrue(fs.exists(archiveFile)); - assertFalse(fs.exists(sourceFile)); - } catch (IOException e) { - // The archiver is unable to archive the file. Probably HBASE-7643 race condition. - // in this case, the file should not be archived, and we should have the file - // in the original location. - LOG.debug("hfile=" + fid + " should be in the source location"); - assertFalse(fs.exists(archiveFile)); - assertTrue(fs.exists(sourceFile)); - - // Avoid to have this file in the next run - fs.delete(sourceFile, false); - } - } - } finally { - stoppable.stop("test end"); - cleaner.cancel(true); - choreService.shutdown(); - fs.delete(rootDir, true); - } - } - - private void clearArchiveDirectory() throws IOException { - UTIL.getTestFileSystem().delete( - new Path(UTIL.getDefaultRootDirPath(), HConstants.HFILE_ARCHIVE_DIRECTORY), true); - } - - /** - * Get the names of all the files below the given directory - * @param fs - * @param archiveDir - * @return - * @throws IOException - */ - private List getAllFileNames(final FileSystem fs, Path archiveDir) throws IOException { - FileStatus[] files = FSUtils.listStatus(fs, archiveDir, new PathFilter() { - @Override - public boolean accept(Path p) { - if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) { - return false; - } - return true; - } - }); - return recurseOnFiles(fs, files, new ArrayList()); - } - - /** Recursively lookup all the file names under the file[] array **/ - private List recurseOnFiles(FileSystem fs, FileStatus[] files, List fileNames) - throws IOException { - if (files == null || files.length == 0) return fileNames; - - for (FileStatus file : files) { - if (file.isDirectory()) { - recurseOnFiles(fs, FSUtils.listStatus(fs, file.getPath(), null), fileNames); - } else fileNames.add(file.getPath().getName()); - } - return fileNames; - } -} diff --git hbase-server/src/test/java/org/apache/hadoop/hbase/backup/example/TestZooKeeperTableArchiveClient.java hbase-server/src/test/java/org/apache/hadoop/hbase/backup/example/TestZooKeeperTableArchiveClient.java deleted file mode 100644 index 64139ee..0000000 --- hbase-server/src/test/java/org/apache/hadoop/hbase/backup/example/TestZooKeeperTableArchiveClient.java +++ /dev/null @@ -1,446 +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 - * - * 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.backup.example; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.hbase.ChoreService; -import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.HColumnDescriptor; -import org.apache.hadoop.hbase.HConstants; -import org.apache.hadoop.hbase.Stoppable; -import org.apache.hadoop.hbase.client.ClusterConnection; -import org.apache.hadoop.hbase.client.ConnectionFactory; -import org.apache.hadoop.hbase.client.Put; -import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate; -import org.apache.hadoop.hbase.master.cleaner.HFileCleaner; -import org.apache.hadoop.hbase.regionserver.CompactedHFilesDischarger; -import org.apache.hadoop.hbase.regionserver.HRegion; -import org.apache.hadoop.hbase.regionserver.Region; -import org.apache.hadoop.hbase.regionserver.RegionServerServices; -import org.apache.hadoop.hbase.regionserver.Store; -import org.apache.hadoop.hbase.testclassification.MediumTests; -import org.apache.hadoop.hbase.testclassification.MiscTests; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.util.FSUtils; -import org.apache.hadoop.hbase.util.HFileArchiveUtil; -import org.apache.hadoop.hbase.util.StoppableImplementation; -import org.apache.hadoop.hbase.zookeeper.ZKUtil; -import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; -import org.apache.zookeeper.KeeperException; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Spin up a small cluster and check that the hfiles of region are properly long-term archived as - * specified via the {@link ZKTableArchiveClient}. - */ -@Category({MiscTests.class, MediumTests.class}) -public class TestZooKeeperTableArchiveClient { - - private static final Log LOG = LogFactory.getLog(TestZooKeeperTableArchiveClient.class); - private static final HBaseTestingUtility UTIL = HBaseTestingUtility.createLocalHTU(); - private static final String STRING_TABLE_NAME = "test"; - private static final byte[] TEST_FAM = Bytes.toBytes("fam"); - private static final byte[] TABLE_NAME = Bytes.toBytes(STRING_TABLE_NAME); - private static ZKTableArchiveClient archivingClient; - private final List toCleanup = new ArrayList(); - private static ClusterConnection CONNECTION; - private static RegionServerServices rss; - - /** - * Setup the config for the cluster - */ - @BeforeClass - public static void setupCluster() throws Exception { - setupConf(UTIL.getConfiguration()); - UTIL.startMiniZKCluster(); - CONNECTION = (ClusterConnection)ConnectionFactory.createConnection(UTIL.getConfiguration()); - archivingClient = new ZKTableArchiveClient(UTIL.getConfiguration(), CONNECTION); - // make hfile archiving node so we can archive files - ZooKeeperWatcher watcher = UTIL.getZooKeeperWatcher(); - String archivingZNode = ZKTableArchiveClient.getArchiveZNode(UTIL.getConfiguration(), watcher); - ZKUtil.createWithParents(watcher, archivingZNode); - rss = mock(RegionServerServices.class); - } - - private static void setupConf(Configuration conf) { - // only compact with 3 files - conf.setInt("hbase.hstore.compaction.min", 3); - } - - @After - public void tearDown() throws Exception { - try { - FileSystem fs = UTIL.getTestFileSystem(); - // cleanup each of the files/directories registered - for (Path file : toCleanup) { - // remove the table and archive directories - FSUtils.delete(fs, file, true); - } - } catch (IOException e) { - LOG.warn("Failure to delete archive directory", e); - } finally { - toCleanup.clear(); - } - // make sure that backups are off for all tables - archivingClient.disableHFileBackup(); - } - - @AfterClass - public static void cleanupTest() throws Exception { - try { - CONNECTION.close(); - UTIL.shutdownMiniZKCluster(); - } catch (Exception e) { - LOG.warn("problem shutting down cluster", e); - } - } - - /** - * Test turning on/off archiving - */ - @Test (timeout=300000) - public void testArchivingEnableDisable() throws Exception { - // 1. turn on hfile backups - LOG.debug("----Starting archiving"); - archivingClient.enableHFileBackupAsync(TABLE_NAME); - assertTrue("Archving didn't get turned on", archivingClient - .getArchivingEnabled(TABLE_NAME)); - - // 2. Turn off archiving and make sure its off - archivingClient.disableHFileBackup(); - assertFalse("Archving didn't get turned off.", archivingClient.getArchivingEnabled(TABLE_NAME)); - - // 3. Check enable/disable on a single table - archivingClient.enableHFileBackupAsync(TABLE_NAME); - assertTrue("Archving didn't get turned on", archivingClient - .getArchivingEnabled(TABLE_NAME)); - - // 4. Turn off archiving and make sure its off - archivingClient.disableHFileBackup(TABLE_NAME); - assertFalse("Archving didn't get turned off for " + STRING_TABLE_NAME, - archivingClient.getArchivingEnabled(TABLE_NAME)); - } - - @Test (timeout=300000) - public void testArchivingOnSingleTable() throws Exception { - createArchiveDirectory(); - FileSystem fs = UTIL.getTestFileSystem(); - Path archiveDir = getArchiveDir(); - Path tableDir = getTableDir(STRING_TABLE_NAME); - toCleanup.add(archiveDir); - toCleanup.add(tableDir); - - Configuration conf = UTIL.getConfiguration(); - // setup the delegate - Stoppable stop = new StoppableImplementation(); - HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop); - List cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner); - final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); - - // create the region - HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM); - HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd); - List regions = new ArrayList(); - regions.add(region); - when(rss.getOnlineRegions()).thenReturn(regions); - final CompactedHFilesDischarger compactionCleaner = - new CompactedHFilesDischarger(100, stop, rss, false); - loadFlushAndCompact(region, TEST_FAM); - compactionCleaner.chore(); - // get the current hfiles in the archive directory - List files = getAllFiles(fs, archiveDir); - if (files == null) { - FSUtils.logFileSystemState(fs, UTIL.getDataTestDir(), LOG); - throw new RuntimeException("Didn't archive any files!"); - } - CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size()); - - runCleaner(cleaner, finished, stop); - - // know the cleaner ran, so now check all the files again to make sure they are still there - List archivedFiles = getAllFiles(fs, archiveDir); - assertEquals("Archived files changed after running archive cleaner.", files, archivedFiles); - - // but we still have the archive directory - assertTrue(fs.exists(HFileArchiveUtil.getArchivePath(UTIL.getConfiguration()))); - } - - /** - * Test archiving/cleaning across multiple tables, where some are retained, and others aren't - * @throws Exception on failure - */ - @Test (timeout=300000) - public void testMultipleTables() throws Exception { - createArchiveDirectory(); - String otherTable = "otherTable"; - - FileSystem fs = UTIL.getTestFileSystem(); - Path archiveDir = getArchiveDir(); - Path tableDir = getTableDir(STRING_TABLE_NAME); - Path otherTableDir = getTableDir(otherTable); - - // register cleanup for the created directories - toCleanup.add(archiveDir); - toCleanup.add(tableDir); - toCleanup.add(otherTableDir); - Configuration conf = UTIL.getConfiguration(); - // setup the delegate - Stoppable stop = new StoppableImplementation(); - final ChoreService choreService = new ChoreService("TEST_SERVER_NAME"); - HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop); - List cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner); - final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); - // create the region - HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM); - HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd); - List regions = new ArrayList(); - regions.add(region); - when(rss.getOnlineRegions()).thenReturn(regions); - final CompactedHFilesDischarger compactionCleaner = - new CompactedHFilesDischarger(100, stop, rss, false); - loadFlushAndCompact(region, TEST_FAM); - compactionCleaner.chore(); - // create the another table that we don't archive - hcd = new HColumnDescriptor(TEST_FAM); - HRegion otherRegion = UTIL.createTestRegion(otherTable, hcd); - regions = new ArrayList(); - regions.add(otherRegion); - when(rss.getOnlineRegions()).thenReturn(regions); - final CompactedHFilesDischarger compactionCleaner1 = new CompactedHFilesDischarger(100, stop, - rss, false); - loadFlushAndCompact(otherRegion, TEST_FAM); - compactionCleaner1.chore(); - // get the current hfiles in the archive directory - // Should be archived - List files = getAllFiles(fs, archiveDir); - if (files == null) { - FSUtils.logFileSystemState(fs, archiveDir, LOG); - throw new RuntimeException("Didn't load archive any files!"); - } - - // make sure we have files from both tables - int initialCountForPrimary = 0; - int initialCountForOtherTable = 0; - for (Path file : files) { - String tableName = file.getParent().getParent().getParent().getName(); - // check to which table this file belongs - if (tableName.equals(otherTable)) initialCountForOtherTable++; - else if (tableName.equals(STRING_TABLE_NAME)) initialCountForPrimary++; - } - - assertTrue("Didn't archive files for:" + STRING_TABLE_NAME, initialCountForPrimary > 0); - assertTrue("Didn't archive files for:" + otherTable, initialCountForOtherTable > 0); - - // run the cleaners, checking for each of the directories + files (both should be deleted and - // need to be checked) in 'otherTable' and the files (which should be retained) in the 'table' - CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size() + 3); - // run the cleaner - choreService.scheduleChore(cleaner); - // wait for the cleaner to check all the files - finished.await(); - // stop the cleaner - stop.stop(""); - - // know the cleaner ran, so now check all the files again to make sure they are still there - List archivedFiles = getAllFiles(fs, archiveDir); - int archivedForPrimary = 0; - for(Path file: archivedFiles) { - String tableName = file.getParent().getParent().getParent().getName(); - // ensure we don't have files from the non-archived table - assertFalse("Have a file from the non-archived table: " + file, tableName.equals(otherTable)); - if (tableName.equals(STRING_TABLE_NAME)) archivedForPrimary++; - } - - assertEquals("Not all archived files for the primary table were retained.", initialCountForPrimary, - archivedForPrimary); - - // but we still have the archive directory - assertTrue("Archive directory was deleted via archiver", fs.exists(archiveDir)); - } - - - private void createArchiveDirectory() throws IOException { - //create the archive and test directory - FileSystem fs = UTIL.getTestFileSystem(); - Path archiveDir = getArchiveDir(); - fs.mkdirs(archiveDir); - } - - private Path getArchiveDir() throws IOException { - return new Path(UTIL.getDataTestDir(), HConstants.HFILE_ARCHIVE_DIRECTORY); - } - - private Path getTableDir(String tableName) throws IOException { - Path testDataDir = UTIL.getDataTestDir(); - FSUtils.setRootDir(UTIL.getConfiguration(), testDataDir); - return new Path(testDataDir, tableName); - } - - private HFileCleaner setupAndCreateCleaner(Configuration conf, FileSystem fs, Path archiveDir, - Stoppable stop) { - conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS, - LongTermArchivingHFileCleaner.class.getCanonicalName()); - return new HFileCleaner(1000, stop, conf, fs, archiveDir); - } - - /** - * Start archiving table for given hfile cleaner - * @param tableName table to archive - * @param cleaner cleaner to check to make sure change propagated - * @return underlying {@link LongTermArchivingHFileCleaner} that is managing archiving - * @throws IOException on failure - * @throws KeeperException on failure - */ - private List turnOnArchiving(String tableName, HFileCleaner cleaner) - throws IOException, KeeperException { - // turn on hfile retention - LOG.debug("----Starting archiving for table:" + tableName); - archivingClient.enableHFileBackupAsync(Bytes.toBytes(tableName)); - assertTrue("Archving didn't get turned on", archivingClient.getArchivingEnabled(tableName)); - - // wait for the archiver to get the notification - List cleaners = cleaner.getDelegatesForTesting(); - LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0); - while (!delegate.archiveTracker.keepHFiles(STRING_TABLE_NAME)) { - // spin until propagation - should be fast - } - return cleaners; - } - - /** - * Spy on the {@link LongTermArchivingHFileCleaner} to ensure we can catch when the cleaner has - * seen all the files - * @return a {@link CountDownLatch} to wait on that releases when the cleaner has been called at - * least the expected number of times. - */ - private CountDownLatch setupCleanerWatching(LongTermArchivingHFileCleaner cleaner, - List cleaners, final int expected) { - // replace the cleaner with one that we can can check - BaseHFileCleanerDelegate delegateSpy = Mockito.spy(cleaner); - final int[] counter = new int[] { 0 }; - final CountDownLatch finished = new CountDownLatch(1); - Mockito.doAnswer(new Answer>() { - - @Override - public Iterable answer(InvocationOnMock invocation) throws Throwable { - counter[0]++; - LOG.debug(counter[0] + "/ " + expected + ") Wrapping call to getDeletableFiles for files: " - + invocation.getArguments()[0]); - - @SuppressWarnings("unchecked") - Iterable ret = (Iterable) invocation.callRealMethod(); - if (counter[0] >= expected) finished.countDown(); - return ret; - } - }).when(delegateSpy).getDeletableFiles(Mockito.anyListOf(FileStatus.class)); - cleaners.set(0, delegateSpy); - - return finished; - } - - /** - * Get all the files (non-directory entries) in the file system under the passed directory - * @param dir directory to investigate - * @return all files under the directory - */ - private List getAllFiles(FileSystem fs, Path dir) throws IOException { - FileStatus[] files = FSUtils.listStatus(fs, dir, null); - if (files == null) { - LOG.warn("No files under:" + dir); - return null; - } - - List allFiles = new ArrayList(); - for (FileStatus file : files) { - if (file.isDirectory()) { - List subFiles = getAllFiles(fs, file.getPath()); - if (subFiles != null) allFiles.addAll(subFiles); - continue; - } - allFiles.add(file.getPath()); - } - return allFiles; - } - - private void loadFlushAndCompact(Region region, byte[] family) throws IOException { - // create two hfiles in the region - createHFileInRegion(region, family); - createHFileInRegion(region, family); - - Store s = region.getStore(family); - int count = s.getStorefilesCount(); - assertTrue("Don't have the expected store files, wanted >= 2 store files, but was:" + count, - count >= 2); - - // compact the two files into one file to get files in the archive - LOG.debug("Compacting stores"); - region.compact(true); - } - - /** - * Create a new hfile in the passed region - * @param region region to operate on - * @param columnFamily family for which to add data - * @throws IOException - */ - private void createHFileInRegion(Region region, byte[] columnFamily) throws IOException { - // put one row in the region - Put p = new Put(Bytes.toBytes("row")); - p.addColumn(columnFamily, Bytes.toBytes("Qual"), Bytes.toBytes("v1")); - region.put(p); - // flush the region to make a store file - region.flush(true); - } - - /** - * @param cleaner - */ - private void runCleaner(HFileCleaner cleaner, CountDownLatch finished, Stoppable stop) - throws InterruptedException { - final ChoreService choreService = new ChoreService("CLEANER_SERVER_NAME"); - // run the cleaner - choreService.scheduleChore(cleaner); - // wait for the cleaner to check all the files - finished.await(); - // stop the cleaner - stop.stop(""); - } -} \ No newline at end of file diff --git hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestHFileArchiving.java hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestHFileArchiving.java new file mode 100644 index 0000000..61f3c52 --- /dev/null +++ hbase-server/src/test/java/org/apache/hadoop/hbase/util/TestHFileArchiving.java @@ -0,0 +1,453 @@ +/** + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathFilter; +import org.apache.hadoop.hbase.ChoreService; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.Stoppable; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Admin; +import org.apache.hadoop.hbase.master.cleaner.HFileCleaner; +import org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy; +import org.apache.hadoop.hbase.regionserver.HRegion; +import org.apache.hadoop.hbase.regionserver.HRegionServer; +import org.apache.hadoop.hbase.regionserver.Region; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileArchiveUtil; +import org.apache.hadoop.hbase.util.HFileArchiver; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Test that the {@link HFileArchiver} correctly removes all the parts of a region when cleaning up + * a region + */ +@Category({MediumTests.class, MiscTests.class}) +public class TestHFileArchiving { + + private static final Log LOG = LogFactory.getLog(TestHFileArchiving.class); + private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); + private static final byte[] TEST_FAM = Bytes.toBytes("fam"); + + /** + * Setup the config for the cluster + */ + @BeforeClass + public static void setupCluster() throws Exception { + setupConf(UTIL.getConfiguration()); + UTIL.startMiniCluster(); + + // We don't want the cleaner to remove files. The tests do that. + UTIL.getMiniHBaseCluster().getMaster().getHFileCleaner().cancel(true); + } + + private static void setupConf(Configuration conf) { + // disable the ui + conf.setInt("hbase.regionsever.info.port", -1); + // drop the memstore size so we get flushes + conf.setInt("hbase.hregion.memstore.flush.size", 25000); + // disable major compactions + conf.setInt(HConstants.MAJOR_COMPACTION_PERIOD, 0); + + // prevent aggressive region split + conf.set(HConstants.HBASE_REGION_SPLIT_POLICY_KEY, + ConstantSizeRegionSplitPolicy.class.getName()); + } + + @After + public void tearDown() throws Exception { + // cleanup the archive directory + try { + clearArchiveDirectory(); + } catch (IOException e) { + Assert.fail("Failure to delete archive directory:" + e.getMessage()); + } + } + + @AfterClass + public static void cleanupTest() throws Exception { + try { + UTIL.shutdownMiniCluster(); + } catch (Exception e) { + // NOOP; + } + } + + @Test + public void testRemovesRegionDirOnArchive() throws Exception { + TableName TABLE_NAME = + TableName.valueOf("testRemovesRegionDirOnArchive"); + UTIL.createTable(TABLE_NAME, TEST_FAM); + + final Admin admin = UTIL.getHBaseAdmin(); + + // get the current store files for the region + List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); + // make sure we only have 1 region serving this table + assertEquals(1, servingRegions.size()); + HRegion region = servingRegions.get(0); + + // and load the table + UTIL.loadRegion(region, TEST_FAM); + + // shutdown the table so we can manipulate the files + admin.disableTable(TABLE_NAME); + + FileSystem fs = UTIL.getTestFileSystem(); + + // now attempt to depose the region + Path rootDir = region.getRegionFileSystem().getTableDir().getParent(); + Path regionDir = HRegion.getRegionDir(rootDir, region.getRegionInfo()); + + HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo()); + + // check for the existence of the archive directory and some files in it + Path archiveDir = HFileArchiveTestingUtil.getRegionArchiveDir(UTIL.getConfiguration(), region); + assertTrue(fs.exists(archiveDir)); + + // check to make sure the store directory was copied + // check to make sure the store directory was copied + FileStatus[] stores = fs.listStatus(archiveDir, new PathFilter() { + @Override + public boolean accept(Path p) { + if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) { + return false; + } + return true; + } + }); + assertTrue(stores.length == 1); + + // make sure we archived the store files + FileStatus[] storeFiles = fs.listStatus(stores[0].getPath()); + assertTrue(storeFiles.length > 0); + + // then ensure the region's directory isn't present + assertFalse(fs.exists(regionDir)); + + UTIL.deleteTable(TABLE_NAME); + } + + /** + * Test that the region directory is removed when we archive a region without store files, but + * still has hidden files. + * @throws Exception + */ + @Test + public void testDeleteRegionWithNoStoreFiles() throws Exception { + TableName TABLE_NAME = + TableName.valueOf("testDeleteRegionWithNoStoreFiles"); + UTIL.createTable(TABLE_NAME, TEST_FAM); + + // get the current store files for the region + List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); + // make sure we only have 1 region serving this table + assertEquals(1, servingRegions.size()); + HRegion region = servingRegions.get(0); + + FileSystem fs = region.getRegionFileSystem().getFileSystem(); + + // make sure there are some files in the regiondir + Path rootDir = FSUtils.getRootDir(fs.getConf()); + Path regionDir = HRegion.getRegionDir(rootDir, region.getRegionInfo()); + FileStatus[] regionFiles = FSUtils.listStatus(fs, regionDir, null); + Assert.assertNotNull("No files in the region directory", regionFiles); + if (LOG.isDebugEnabled()) { + List files = new ArrayList(); + for (FileStatus file : regionFiles) { + files.add(file.getPath()); + } + LOG.debug("Current files:" + files); + } + // delete the visible folders so we just have hidden files/folders + final PathFilter dirFilter = new FSUtils.DirFilter(fs); + PathFilter nonHidden = new PathFilter() { + @Override + public boolean accept(Path file) { + return dirFilter.accept(file) && !file.getName().toString().startsWith("."); + } + }; + FileStatus[] storeDirs = FSUtils.listStatus(fs, regionDir, nonHidden); + for (FileStatus store : storeDirs) { + LOG.debug("Deleting store for test"); + fs.delete(store.getPath(), true); + } + + // then archive the region + HFileArchiver.archiveRegion(UTIL.getConfiguration(), fs, region.getRegionInfo()); + + // and check to make sure the region directoy got deleted + assertFalse("Region directory (" + regionDir + "), still exists.", fs.exists(regionDir)); + + UTIL.deleteTable(TABLE_NAME); + } + + @Test + public void testArchiveOnTableDelete() throws Exception { + TableName TABLE_NAME = + TableName.valueOf("testArchiveOnTableDelete"); + UTIL.createTable(TABLE_NAME, TEST_FAM); + + List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); + // make sure we only have 1 region serving this table + assertEquals(1, servingRegions.size()); + Region region = servingRegions.get(0); + + // get the parent RS and monitor + HRegionServer hrs = UTIL.getRSForFirstRegionInTable(TABLE_NAME); + FileSystem fs = hrs.getFileSystem(); + + // put some data on the region + LOG.debug("-------Loading table"); + UTIL.loadRegion(region, TEST_FAM); + + // get the hfiles in the region + List regions = hrs.getOnlineRegions(TABLE_NAME); + assertEquals("More that 1 region for test table.", 1, regions.size()); + + region = regions.get(0); + // wait for all the compactions to complete + region.waitForFlushesAndCompactions(); + + // disable table to prevent new updates + UTIL.getHBaseAdmin().disableTable(TABLE_NAME); + LOG.debug("Disabled table"); + + // remove all the files from the archive to get a fair comparison + clearArchiveDirectory(); + + // then get the current store files + byte[][]columns = region.getTableDesc().getFamiliesKeys().toArray(new byte[0][]); + List storeFiles = region.getStoreFileList(columns); + + // then delete the table so the hfiles get archived + UTIL.deleteTable(TABLE_NAME); + LOG.debug("Deleted table"); + + assertArchiveFiles(fs, storeFiles, 30000); + } + + private void assertArchiveFiles(FileSystem fs, List storeFiles, long timeout) throws IOException { + long end = System.currentTimeMillis() + timeout; + Path archiveDir = HFileArchiveUtil.getArchivePath(UTIL.getConfiguration()); + List archivedFiles = new ArrayList(); + + // We have to ensure that the DeleteTableHandler is finished. HBaseAdmin.deleteXXX() can return before all files + // are archived. We should fix HBASE-5487 and fix synchronous operations from admin. + while (System.currentTimeMillis() < end) { + archivedFiles = getAllFileNames(fs, archiveDir); + if (archivedFiles.size() >= storeFiles.size()) { + break; + } + } + + Collections.sort(storeFiles); + Collections.sort(archivedFiles); + + LOG.debug("Store files:"); + for (int i = 0; i < storeFiles.size(); i++) { + LOG.debug(i + " - " + storeFiles.get(i)); + } + LOG.debug("Archive files:"); + for (int i = 0; i < archivedFiles.size(); i++) { + LOG.debug(i + " - " + archivedFiles.get(i)); + } + + assertTrue("Archived files are missing some of the store files!", + archivedFiles.containsAll(storeFiles)); + } + + + /** + * Test that the store files are archived when a column family is removed. + * @throws Exception + */ + @Test + public void testArchiveOnTableFamilyDelete() throws Exception { + TableName TABLE_NAME = + TableName.valueOf("testArchiveOnTableFamilyDelete"); + UTIL.createTable(TABLE_NAME, new byte[][] {TEST_FAM, Bytes.toBytes("fam2")}); + + List servingRegions = UTIL.getHBaseCluster().getRegions(TABLE_NAME); + // make sure we only have 1 region serving this table + assertEquals(1, servingRegions.size()); + Region region = servingRegions.get(0); + + // get the parent RS and monitor + HRegionServer hrs = UTIL.getRSForFirstRegionInTable(TABLE_NAME); + FileSystem fs = hrs.getFileSystem(); + + // put some data on the region + LOG.debug("-------Loading table"); + UTIL.loadRegion(region, TEST_FAM); + + // get the hfiles in the region + List regions = hrs.getOnlineRegions(TABLE_NAME); + assertEquals("More that 1 region for test table.", 1, regions.size()); + + region = regions.get(0); + // wait for all the compactions to complete + region.waitForFlushesAndCompactions(); + + // disable table to prevent new updates + UTIL.getHBaseAdmin().disableTable(TABLE_NAME); + LOG.debug("Disabled table"); + + // remove all the files from the archive to get a fair comparison + clearArchiveDirectory(); + + // then get the current store files + byte[][]columns = region.getTableDesc().getFamiliesKeys().toArray(new byte[0][]); + List storeFiles = region.getStoreFileList(columns); + + // then delete the table so the hfiles get archived + UTIL.getHBaseAdmin().deleteColumnFamily(TABLE_NAME, TEST_FAM); + + assertArchiveFiles(fs, storeFiles, 30000); + + UTIL.deleteTable(TABLE_NAME); + } + + /** + * Test HFileArchiver.resolveAndArchive() race condition HBASE-7643 + */ + @Test + public void testCleaningRace() throws Exception { + final long TEST_TIME = 20 * 1000; + final ChoreService choreService = new ChoreService("TEST_SERVER_NAME"); + + Configuration conf = UTIL.getMiniHBaseCluster().getMaster().getConfiguration(); + Path rootDir = UTIL.getDataTestDirOnTestFS("testCleaningRace"); + FileSystem fs = UTIL.getTestFileSystem(); + + Path archiveDir = new Path(rootDir, HConstants.HFILE_ARCHIVE_DIRECTORY); + Path regionDir = new Path(FSUtils.getTableDir(new Path("./"), + TableName.valueOf("table")), "abcdef"); + Path familyDir = new Path(regionDir, "cf"); + + Path sourceRegionDir = new Path(rootDir, regionDir); + fs.mkdirs(sourceRegionDir); + + Stoppable stoppable = new StoppableImplementation(); + + // The cleaner should be looping without long pauses to reproduce the race condition. + HFileCleaner cleaner = new HFileCleaner(1, stoppable, conf, fs, archiveDir); + try { + choreService.scheduleChore(cleaner); + + // Keep creating/archiving new files while the cleaner is running in the other thread + long startTime = System.currentTimeMillis(); + for (long fid = 0; (System.currentTimeMillis() - startTime) < TEST_TIME; ++fid) { + Path file = new Path(familyDir, String.valueOf(fid)); + Path sourceFile = new Path(rootDir, file); + Path archiveFile = new Path(archiveDir, file); + + fs.createNewFile(sourceFile); + + try { + // Try to archive the file + HFileArchiver.archiveRegion(fs, rootDir, + sourceRegionDir.getParent(), sourceRegionDir); + + // The archiver succeded, the file is no longer in the original location + // but it's in the archive location. + LOG.debug("hfile=" + fid + " should be in the archive"); + assertTrue(fs.exists(archiveFile)); + assertFalse(fs.exists(sourceFile)); + } catch (IOException e) { + // The archiver is unable to archive the file. Probably HBASE-7643 race condition. + // in this case, the file should not be archived, and we should have the file + // in the original location. + LOG.debug("hfile=" + fid + " should be in the source location"); + assertFalse(fs.exists(archiveFile)); + assertTrue(fs.exists(sourceFile)); + + // Avoid to have this file in the next run + fs.delete(sourceFile, false); + } + } + } finally { + stoppable.stop("test end"); + cleaner.cancel(true); + choreService.shutdown(); + fs.delete(rootDir, true); + } + } + + private void clearArchiveDirectory() throws IOException { + UTIL.getTestFileSystem().delete( + new Path(UTIL.getDefaultRootDirPath(), HConstants.HFILE_ARCHIVE_DIRECTORY), true); + } + + /** + * Get the names of all the files below the given directory + * @param fs + * @param archiveDir + * @return + * @throws IOException + */ + private List getAllFileNames(final FileSystem fs, Path archiveDir) throws IOException { + FileStatus[] files = FSUtils.listStatus(fs, archiveDir, new PathFilter() { + @Override + public boolean accept(Path p) { + if (p.getName().contains(HConstants.RECOVERED_EDITS_DIR)) { + return false; + } + return true; + } + }); + return recurseOnFiles(fs, files, new ArrayList()); + } + + /** Recursively lookup all the file names under the file[] array **/ + private List recurseOnFiles(FileSystem fs, FileStatus[] files, List fileNames) + throws IOException { + if (files == null || files.length == 0) return fileNames; + + for (FileStatus file : files) { + if (file.isDirectory()) { + recurseOnFiles(fs, FSUtils.listStatus(fs, file.getPath(), null), fileNames); + } else fileNames.add(file.getPath().getName()); + } + return fileNames; + } +}