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;
+ }
+}