View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.backup.example;
19  
20  import static org.junit.Assert.assertEquals;
21  import static org.junit.Assert.assertFalse;
22  import static org.junit.Assert.assertTrue;
23  import static org.mockito.Mockito.mock;
24  import static org.mockito.Mockito.when;
25  
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.concurrent.CountDownLatch;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.hadoop.conf.Configuration;
34  import org.apache.hadoop.fs.FileStatus;
35  import org.apache.hadoop.fs.FileSystem;
36  import org.apache.hadoop.fs.Path;
37  import org.apache.hadoop.hbase.ChoreService;
38  import org.apache.hadoop.hbase.HBaseTestingUtility;
39  import org.apache.hadoop.hbase.HColumnDescriptor;
40  import org.apache.hadoop.hbase.HConstants;
41  import org.apache.hadoop.hbase.master.cleaner.CleanerChore;
42  import org.apache.hadoop.hbase.testclassification.MediumTests;
43  import org.apache.hadoop.hbase.Stoppable;
44  import org.apache.hadoop.hbase.client.ClusterConnection;
45  import org.apache.hadoop.hbase.client.ConnectionFactory;
46  import org.apache.hadoop.hbase.client.Put;
47  import org.apache.hadoop.hbase.master.cleaner.BaseHFileCleanerDelegate;
48  import org.apache.hadoop.hbase.master.cleaner.HFileCleaner;
49  import org.apache.hadoop.hbase.regionserver.CompactedHFilesDischarger;
50  import org.apache.hadoop.hbase.regionserver.HRegion;
51  import org.apache.hadoop.hbase.regionserver.Region;
52  import org.apache.hadoop.hbase.regionserver.RegionServerServices;
53  import org.apache.hadoop.hbase.regionserver.Store;
54  import org.apache.hadoop.hbase.util.Bytes;
55  import org.apache.hadoop.hbase.util.FSUtils;
56  import org.apache.hadoop.hbase.util.HFileArchiveUtil;
57  import org.apache.hadoop.hbase.util.StoppableImplementation;
58  import org.apache.hadoop.hbase.zookeeper.ZKUtil;
59  import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
60  import org.apache.zookeeper.KeeperException;
61  import org.junit.After;
62  import org.junit.AfterClass;
63  import org.junit.BeforeClass;
64  import org.junit.Test;
65  import org.junit.experimental.categories.Category;
66  import org.mockito.Mockito;
67  import org.mockito.invocation.InvocationOnMock;
68  import org.mockito.stubbing.Answer;
69  
70  /**
71   * Spin up a small cluster and check that the hfiles of region are properly long-term archived as
72   * specified via the {@link ZKTableArchiveClient}.
73   */
74  @Category(MediumTests.class)
75  public class TestZooKeeperTableArchiveClient {
76  
77    private static final Log LOG = LogFactory.getLog(TestZooKeeperTableArchiveClient.class);
78    private static final HBaseTestingUtility UTIL = HBaseTestingUtility.createLocalHTU();
79    private static final String STRING_TABLE_NAME = "test";
80    private static final byte[] TEST_FAM = Bytes.toBytes("fam");
81    private static final byte[] TABLE_NAME = Bytes.toBytes(STRING_TABLE_NAME);
82    private static ZKTableArchiveClient archivingClient;
83    private final List<Path> toCleanup = new ArrayList<Path>();
84    private static ClusterConnection CONNECTION;
85    private static RegionServerServices rss;
86  
87    /**
88     * Setup the config for the cluster
89     */
90    @BeforeClass
91    public static void setupCluster() throws Exception {
92      setupConf(UTIL.getConfiguration());
93      UTIL.startMiniZKCluster();
94      CONNECTION = (ClusterConnection)ConnectionFactory.createConnection(UTIL.getConfiguration());
95      archivingClient = new ZKTableArchiveClient(UTIL.getConfiguration(), CONNECTION);
96      // make hfile archiving node so we can archive files
97      ZooKeeperWatcher watcher = UTIL.getZooKeeperWatcher();
98      String archivingZNode = ZKTableArchiveClient.getArchiveZNode(UTIL.getConfiguration(), watcher);
99      ZKUtil.createWithParents(watcher, archivingZNode);
100     rss = mock(RegionServerServices.class);
101   }
102 
103   private static void setupConf(Configuration conf) {
104     // only compact with 3 files
105     conf.setInt("hbase.hstore.compaction.min", 3);
106   }
107 
108   @After
109   public void tearDown() throws Exception {
110     try {
111       FileSystem fs = UTIL.getTestFileSystem();
112       // cleanup each of the files/directories registered
113       for (Path file : toCleanup) {
114       // remove the table and archive directories
115         FSUtils.delete(fs, file, true);
116       }
117     } catch (IOException e) {
118       LOG.warn("Failure to delete archive directory", e);
119     } finally {
120       toCleanup.clear();
121     }
122     // make sure that backups are off for all tables
123     archivingClient.disableHFileBackup();
124   }
125 
126   @AfterClass
127   public static void cleanupTest() throws Exception {
128     try {
129       CONNECTION.close();
130       UTIL.shutdownMiniZKCluster();
131     } catch (Exception e) {
132       LOG.warn("problem shutting down cluster", e);
133     }
134   }
135 
136   /**
137    * Test turning on/off archiving
138    */
139   @Test (timeout=300000)
140   public void testArchivingEnableDisable() throws Exception {
141     // 1. turn on hfile backups
142     LOG.debug("----Starting archiving");
143     archivingClient.enableHFileBackupAsync(TABLE_NAME);
144     assertTrue("Archving didn't get turned on", archivingClient
145         .getArchivingEnabled(TABLE_NAME));
146 
147     // 2. Turn off archiving and make sure its off
148     archivingClient.disableHFileBackup();
149     assertFalse("Archving didn't get turned off.", archivingClient.getArchivingEnabled(TABLE_NAME));
150 
151     // 3. Check enable/disable on a single table
152     archivingClient.enableHFileBackupAsync(TABLE_NAME);
153     assertTrue("Archving didn't get turned on", archivingClient
154         .getArchivingEnabled(TABLE_NAME));
155 
156     // 4. Turn off archiving and make sure its off
157     archivingClient.disableHFileBackup(TABLE_NAME);
158     assertFalse("Archving didn't get turned off for " + STRING_TABLE_NAME,
159       archivingClient.getArchivingEnabled(TABLE_NAME));
160   }
161 
162   @Test (timeout=300000)
163   public void testArchivingOnSingleTable() throws Exception {
164     createArchiveDirectory();
165     FileSystem fs = UTIL.getTestFileSystem();
166     Path archiveDir = getArchiveDir();
167     Path tableDir = getTableDir(STRING_TABLE_NAME);
168     toCleanup.add(archiveDir);
169     toCleanup.add(tableDir);
170 
171     Configuration conf = UTIL.getConfiguration();
172     // setup the delegate
173     Stoppable stop = new StoppableImplementation();
174     CleanerChore.initChorePool(conf);
175     HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop);
176     List<BaseHFileCleanerDelegate> cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner);
177     final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
178 
179     // create the region
180     HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM);
181     HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd);
182     List<Region> regions = new ArrayList<Region>();
183     regions.add(region);
184     when(rss.getOnlineRegions()).thenReturn(regions);
185     final CompactedHFilesDischarger compactionCleaner =
186         new CompactedHFilesDischarger(100, stop, rss, false);
187     loadFlushAndCompact(region, TEST_FAM);
188     compactionCleaner.chore();
189     // get the current hfiles in the archive directory
190     List<Path> files = getAllFiles(fs, archiveDir);
191     if (files == null) {
192       FSUtils.logFileSystemState(fs, UTIL.getDataTestDir(), LOG);
193       throw new RuntimeException("Didn't archive any files!");
194     }
195     CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size());
196 
197     runCleaner(cleaner, finished, stop);
198 
199     // know the cleaner ran, so now check all the files again to make sure they are still there
200     List<Path> archivedFiles = getAllFiles(fs, archiveDir);
201     assertEquals("Archived files changed after running archive cleaner.", files, archivedFiles);
202 
203     // but we still have the archive directory
204     assertTrue(fs.exists(HFileArchiveUtil.getArchivePath(UTIL.getConfiguration())));
205   }
206 
207   /**
208    * Test archiving/cleaning across multiple tables, where some are retained, and others aren't
209    * @throws Exception on failure
210    */
211   @Test (timeout=300000)
212   public void testMultipleTables() throws Exception {
213     createArchiveDirectory();
214     String otherTable = "otherTable";
215 
216     FileSystem fs = UTIL.getTestFileSystem();
217     Path archiveDir = getArchiveDir();
218     Path tableDir = getTableDir(STRING_TABLE_NAME);
219     Path otherTableDir = getTableDir(otherTable);
220 
221     // register cleanup for the created directories
222     toCleanup.add(archiveDir);
223     toCleanup.add(tableDir);
224     toCleanup.add(otherTableDir);
225     Configuration conf = UTIL.getConfiguration();
226     // setup the delegate
227     Stoppable stop = new StoppableImplementation();
228     final ChoreService choreService = new ChoreService("TEST_SERVER_NAME");
229     CleanerChore.initChorePool(conf);
230     HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop);
231     List<BaseHFileCleanerDelegate> cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner);
232     final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
233     // create the region
234     HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM);
235     HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd);
236     List<Region> regions = new ArrayList<Region>();
237     regions.add(region);
238     when(rss.getOnlineRegions()).thenReturn(regions);
239     final CompactedHFilesDischarger compactionCleaner =
240         new CompactedHFilesDischarger(100, stop, rss, false);
241     loadFlushAndCompact(region, TEST_FAM);
242     compactionCleaner.chore();
243     // create the another table that we don't archive
244     hcd = new HColumnDescriptor(TEST_FAM);
245     HRegion otherRegion = UTIL.createTestRegion(otherTable, hcd);
246     regions = new ArrayList<Region>();
247     regions.add(otherRegion);
248     when(rss.getOnlineRegions()).thenReturn(regions);
249     final CompactedHFilesDischarger compactionCleaner1 = new CompactedHFilesDischarger(100, stop,
250         rss, false);
251     loadFlushAndCompact(otherRegion, TEST_FAM);
252     compactionCleaner1.chore();
253     // get the current hfiles in the archive directory
254     // Should  be archived
255     List<Path> files = getAllFiles(fs, archiveDir);
256     if (files == null) {
257       FSUtils.logFileSystemState(fs, archiveDir, LOG);
258       throw new RuntimeException("Didn't load archive any files!");
259     }
260 
261     // make sure we have files from both tables
262     int initialCountForPrimary = 0;
263     int initialCountForOtherTable = 0;
264     for (Path file : files) {
265       String tableName = file.getParent().getParent().getParent().getName();
266       // check to which table this file belongs
267       if (tableName.equals(otherTable)) initialCountForOtherTable++;
268       else if (tableName.equals(STRING_TABLE_NAME)) initialCountForPrimary++;
269     }
270 
271     assertTrue("Didn't archive files for:" + STRING_TABLE_NAME, initialCountForPrimary > 0);
272     assertTrue("Didn't archive files for:" + otherTable, initialCountForOtherTable > 0);
273 
274     // run the cleaners, checking for each of the directories + files (both should be deleted and
275     // need to be checked) in 'otherTable' and the files (which should be retained) in the 'table'
276     CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size() + 3);
277     // run the cleaner
278     choreService.scheduleChore(cleaner);
279     // wait for the cleaner to check all the files
280     finished.await();
281     // stop the cleaner
282     stop.stop("");
283 
284     // know the cleaner ran, so now check all the files again to make sure they are still there
285     List<Path> archivedFiles = getAllFiles(fs, archiveDir);
286     int archivedForPrimary = 0;
287     for(Path file: archivedFiles) {
288       String tableName = file.getParent().getParent().getParent().getName();
289       // ensure we don't have files from the non-archived table
290       assertFalse("Have a file from the non-archived table: " + file, tableName.equals(otherTable));
291       if (tableName.equals(STRING_TABLE_NAME)) archivedForPrimary++;
292     }
293 
294     assertEquals("Not all archived files for the primary table were retained.", initialCountForPrimary,
295       archivedForPrimary);
296 
297     // but we still have the archive directory
298     assertTrue("Archive directory was deleted via archiver", fs.exists(archiveDir));
299   }
300 
301 
302   private void createArchiveDirectory() throws IOException {
303     //create the archive and test directory
304     FileSystem fs = UTIL.getTestFileSystem();
305     Path archiveDir = getArchiveDir();
306     fs.mkdirs(archiveDir);
307   }
308 
309   private Path getArchiveDir() throws IOException {
310     return new Path(UTIL.getDataTestDir(), HConstants.HFILE_ARCHIVE_DIRECTORY);
311   }
312 
313   private Path getTableDir(String tableName) throws IOException {
314     Path testDataDir = UTIL.getDataTestDir();
315     FSUtils.setRootDir(UTIL.getConfiguration(), testDataDir);
316     return new Path(testDataDir, tableName);
317   }
318 
319   private HFileCleaner setupAndCreateCleaner(Configuration conf, FileSystem fs, Path archiveDir,
320       Stoppable stop) {
321     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
322       LongTermArchivingHFileCleaner.class.getCanonicalName());
323     return new HFileCleaner(1000, stop, conf, fs, archiveDir);
324   }
325 
326   /**
327    * Start archiving table for given hfile cleaner
328    * @param tableName table to archive
329    * @param cleaner cleaner to check to make sure change propagated
330    * @return underlying {@link LongTermArchivingHFileCleaner} that is managing archiving
331    * @throws IOException on failure
332    * @throws KeeperException on failure
333    */
334   private List<BaseHFileCleanerDelegate> turnOnArchiving(String tableName, HFileCleaner cleaner)
335       throws IOException, KeeperException {
336     // turn on hfile retention
337     LOG.debug("----Starting archiving for table:" + tableName);
338     archivingClient.enableHFileBackupAsync(Bytes.toBytes(tableName));
339     assertTrue("Archving didn't get turned on", archivingClient.getArchivingEnabled(tableName));
340 
341     // wait for the archiver to get the notification
342     List<BaseHFileCleanerDelegate> cleaners = cleaner.getDelegatesForTesting();
343     LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
344     while (!delegate.archiveTracker.keepHFiles(STRING_TABLE_NAME)) {
345       // spin until propagation - should be fast
346     }
347     return cleaners;
348   }
349 
350   /**
351    * Spy on the {@link LongTermArchivingHFileCleaner} to ensure we can catch when the cleaner has
352    * seen all the files
353    * @return a {@link CountDownLatch} to wait on that releases when the cleaner has been called at
354    *         least the expected number of times.
355    */
356   private CountDownLatch setupCleanerWatching(LongTermArchivingHFileCleaner cleaner,
357       List<BaseHFileCleanerDelegate> cleaners, final int expected) {
358     // replace the cleaner with one that we can can check
359     BaseHFileCleanerDelegate delegateSpy = Mockito.spy(cleaner);
360     final int[] counter = new int[] { 0 };
361     final CountDownLatch finished = new CountDownLatch(1);
362     Mockito.doAnswer(new Answer<Iterable<FileStatus>>() {
363 
364       @Override
365       public Iterable<FileStatus> answer(InvocationOnMock invocation) throws Throwable {
366         counter[0]++;
367         LOG.debug(counter[0] + "/ " + expected + ") Wrapping call to getDeletableFiles for files: "
368             + invocation.getArguments()[0]);
369 
370         @SuppressWarnings("unchecked")
371         Iterable<FileStatus> ret = (Iterable<FileStatus>) invocation.callRealMethod();
372         if (counter[0] >= expected) finished.countDown();
373         return ret;
374       }
375     }).when(delegateSpy).getDeletableFiles(Mockito.anyListOf(FileStatus.class));
376     cleaners.set(0, delegateSpy);
377 
378     return finished;
379   }
380 
381   /**
382    * Get all the files (non-directory entries) in the file system under the passed directory
383    * @param dir directory to investigate
384    * @return all files under the directory
385    */
386   private List<Path> getAllFiles(FileSystem fs, Path dir) throws IOException {
387     FileStatus[] files = FSUtils.listStatus(fs, dir, null);
388     if (files == null) {
389       LOG.warn("No files under:" + dir);
390       return null;
391     }
392 
393     List<Path> allFiles = new ArrayList<Path>();
394     for (FileStatus file : files) {
395       if (file.isDirectory()) {
396         List<Path> subFiles = getAllFiles(fs, file.getPath());
397         if (subFiles != null) allFiles.addAll(subFiles);
398         continue;
399       }
400       allFiles.add(file.getPath());
401     }
402     return allFiles;
403   }
404 
405   private void loadFlushAndCompact(Region region, byte[] family) throws IOException {
406     // create two hfiles in the region
407     createHFileInRegion(region, family);
408     createHFileInRegion(region, family);
409 
410     Store s = region.getStore(family);
411     int count = s.getStorefilesCount();
412     assertTrue("Don't have the expected store files, wanted >= 2 store files, but was:" + count,
413       count >= 2);
414 
415     // compact the two files into one file to get files in the archive
416     LOG.debug("Compacting stores");
417     region.compact(true);
418   }
419 
420   /**
421    * Create a new hfile in the passed region
422    * @param region region to operate on
423    * @param columnFamily family for which to add data
424    * @throws IOException
425    */
426   private void createHFileInRegion(Region region, byte[] columnFamily) throws IOException {
427     // put one row in the region
428     Put p = new Put(Bytes.toBytes("row"));
429     p.add(columnFamily, Bytes.toBytes("Qual"), Bytes.toBytes("v1"));
430     region.put(p);
431     // flush the region to make a store file
432     region.flush(true);
433   }
434 
435   /**
436    * @param cleaner
437    */
438   private void runCleaner(HFileCleaner cleaner, CountDownLatch finished, Stoppable stop)
439       throws InterruptedException {
440     final ChoreService choreService = new ChoreService("CLEANER_SERVER_NAME");
441     // run the cleaner
442     choreService.scheduleChore(cleaner);
443     // wait for the cleaner to check all the files
444     finished.await();
445     // stop the cleaner
446     stop.stop("");
447   }
448 }