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.DirScanPool;
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    private static DirScanPool POOL;
87  
88    /**
89     * Setup the config for the cluster
90     */
91    @BeforeClass
92    public static void setupCluster() throws Exception {
93      setupConf(UTIL.getConfiguration());
94      UTIL.startMiniZKCluster();
95      CONNECTION = (ClusterConnection)ConnectionFactory.createConnection(UTIL.getConfiguration());
96      archivingClient = new ZKTableArchiveClient(UTIL.getConfiguration(), CONNECTION);
97      // make hfile archiving node so we can archive files
98      ZooKeeperWatcher watcher = UTIL.getZooKeeperWatcher();
99      String archivingZNode = ZKTableArchiveClient.getArchiveZNode(UTIL.getConfiguration(), watcher);
100     ZKUtil.createWithParents(watcher, archivingZNode);
101     rss = mock(RegionServerServices.class);
102     POOL= new DirScanPool(UTIL.getConfiguration());
103   }
104 
105   private static void setupConf(Configuration conf) {
106     // only compact with 3 files
107     conf.setInt("hbase.hstore.compaction.min", 3);
108   }
109 
110   @After
111   public void tearDown() throws Exception {
112     try {
113       FileSystem fs = UTIL.getTestFileSystem();
114       // cleanup each of the files/directories registered
115       for (Path file : toCleanup) {
116       // remove the table and archive directories
117         FSUtils.delete(fs, file, true);
118       }
119     } catch (IOException e) {
120       LOG.warn("Failure to delete archive directory", e);
121     } finally {
122       toCleanup.clear();
123     }
124     // make sure that backups are off for all tables
125     archivingClient.disableHFileBackup();
126   }
127 
128   @AfterClass
129   public static void cleanupTest() throws Exception {
130     CONNECTION.close();
131     UTIL.shutdownMiniZKCluster();
132     POOL.shutdownNow();
133   }
134 
135   /**
136    * Test turning on/off archiving
137    */
138   @Test (timeout=300000)
139   public void testArchivingEnableDisable() throws Exception {
140     // 1. turn on hfile backups
141     LOG.debug("----Starting archiving");
142     archivingClient.enableHFileBackupAsync(TABLE_NAME);
143     assertTrue("Archving didn't get turned on", archivingClient
144         .getArchivingEnabled(TABLE_NAME));
145 
146     // 2. Turn off archiving and make sure its off
147     archivingClient.disableHFileBackup();
148     assertFalse("Archving didn't get turned off.", archivingClient.getArchivingEnabled(TABLE_NAME));
149 
150     // 3. Check enable/disable on a single table
151     archivingClient.enableHFileBackupAsync(TABLE_NAME);
152     assertTrue("Archving didn't get turned on", archivingClient
153         .getArchivingEnabled(TABLE_NAME));
154 
155     // 4. Turn off archiving and make sure its off
156     archivingClient.disableHFileBackup(TABLE_NAME);
157     assertFalse("Archving didn't get turned off for " + STRING_TABLE_NAME,
158       archivingClient.getArchivingEnabled(TABLE_NAME));
159   }
160 
161   @Test (timeout=300000)
162   public void testArchivingOnSingleTable() throws Exception {
163     createArchiveDirectory();
164     FileSystem fs = UTIL.getTestFileSystem();
165     Path archiveDir = getArchiveDir();
166     Path tableDir = getTableDir(STRING_TABLE_NAME);
167     toCleanup.add(archiveDir);
168     toCleanup.add(tableDir);
169 
170     Configuration conf = UTIL.getConfiguration();
171     // setup the delegate
172     Stoppable stop = new StoppableImplementation();
173     HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop);
174     List<BaseHFileCleanerDelegate> cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner);
175     final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
176 
177     // create the region
178     HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM);
179     HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd);
180     List<Region> regions = new ArrayList<Region>();
181     regions.add(region);
182     when(rss.getOnlineRegions()).thenReturn(regions);
183     final CompactedHFilesDischarger compactionCleaner =
184         new CompactedHFilesDischarger(100, stop, rss, false);
185     loadFlushAndCompact(region, TEST_FAM);
186     compactionCleaner.chore();
187     // get the current hfiles in the archive directory
188     List<Path> files = getAllFiles(fs, archiveDir);
189     if (files == null) {
190       FSUtils.logFileSystemState(fs, UTIL.getDataTestDir(), LOG);
191       throw new RuntimeException("Didn't archive any files!");
192     }
193     CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size());
194 
195     runCleaner(cleaner, finished, stop);
196 
197     // know the cleaner ran, so now check all the files again to make sure they are still there
198     List<Path> archivedFiles = getAllFiles(fs, archiveDir);
199     assertEquals("Archived files changed after running archive cleaner.", files, archivedFiles);
200 
201     // but we still have the archive directory
202     assertTrue(fs.exists(HFileArchiveUtil.getArchivePath(UTIL.getConfiguration())));
203   }
204 
205   /**
206    * Test archiving/cleaning across multiple tables, where some are retained, and others aren't
207    * @throws Exception on failure
208    */
209   @Test (timeout=300000)
210   public void testMultipleTables() throws Exception {
211     createArchiveDirectory();
212     String otherTable = "otherTable";
213 
214     FileSystem fs = UTIL.getTestFileSystem();
215     Path archiveDir = getArchiveDir();
216     Path tableDir = getTableDir(STRING_TABLE_NAME);
217     Path otherTableDir = getTableDir(otherTable);
218 
219     // register cleanup for the created directories
220     toCleanup.add(archiveDir);
221     toCleanup.add(tableDir);
222     toCleanup.add(otherTableDir);
223     Configuration conf = UTIL.getConfiguration();
224     // setup the delegate
225     Stoppable stop = new StoppableImplementation();
226     final ChoreService choreService = new ChoreService("TEST_SERVER_NAME");
227     HFileCleaner cleaner = setupAndCreateCleaner(conf, fs, archiveDir, stop);
228     List<BaseHFileCleanerDelegate> cleaners = turnOnArchiving(STRING_TABLE_NAME, cleaner);
229     final LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
230     // create the region
231     HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAM);
232     HRegion region = UTIL.createTestRegion(STRING_TABLE_NAME, hcd);
233     List<Region> regions = new ArrayList<Region>();
234     regions.add(region);
235     when(rss.getOnlineRegions()).thenReturn(regions);
236     final CompactedHFilesDischarger compactionCleaner =
237         new CompactedHFilesDischarger(100, stop, rss, false);
238     loadFlushAndCompact(region, TEST_FAM);
239     compactionCleaner.chore();
240     // create the another table that we don't archive
241     hcd = new HColumnDescriptor(TEST_FAM);
242     HRegion otherRegion = UTIL.createTestRegion(otherTable, hcd);
243     regions = new ArrayList<Region>();
244     regions.add(otherRegion);
245     when(rss.getOnlineRegions()).thenReturn(regions);
246     final CompactedHFilesDischarger compactionCleaner1 = new CompactedHFilesDischarger(100, stop,
247         rss, false);
248     loadFlushAndCompact(otherRegion, TEST_FAM);
249     compactionCleaner1.chore();
250     // get the current hfiles in the archive directory
251     // Should  be archived
252     List<Path> files = getAllFiles(fs, archiveDir);
253     if (files == null) {
254       FSUtils.logFileSystemState(fs, archiveDir, LOG);
255       throw new RuntimeException("Didn't load archive any files!");
256     }
257 
258     // make sure we have files from both tables
259     int initialCountForPrimary = 0;
260     int initialCountForOtherTable = 0;
261     for (Path file : files) {
262       String tableName = file.getParent().getParent().getParent().getName();
263       // check to which table this file belongs
264       if (tableName.equals(otherTable)) initialCountForOtherTable++;
265       else if (tableName.equals(STRING_TABLE_NAME)) initialCountForPrimary++;
266     }
267 
268     assertTrue("Didn't archive files for:" + STRING_TABLE_NAME, initialCountForPrimary > 0);
269     assertTrue("Didn't archive files for:" + otherTable, initialCountForOtherTable > 0);
270 
271     // run the cleaners, checking for each of the directories + files (both should be deleted and
272     // need to be checked) in 'otherTable' and the files (which should be retained) in the 'table'
273     CountDownLatch finished = setupCleanerWatching(delegate, cleaners, files.size() + 3);
274     // run the cleaner
275     choreService.scheduleChore(cleaner);
276     // wait for the cleaner to check all the files
277     finished.await();
278     // stop the cleaner
279     stop.stop("");
280 
281     // know the cleaner ran, so now check all the files again to make sure they are still there
282     List<Path> archivedFiles = getAllFiles(fs, archiveDir);
283     int archivedForPrimary = 0;
284     for(Path file: archivedFiles) {
285       String tableName = file.getParent().getParent().getParent().getName();
286       // ensure we don't have files from the non-archived table
287       assertFalse("Have a file from the non-archived table: " + file, tableName.equals(otherTable));
288       if (tableName.equals(STRING_TABLE_NAME)) archivedForPrimary++;
289     }
290 
291     assertEquals("Not all archived files for the primary table were retained.", initialCountForPrimary,
292       archivedForPrimary);
293 
294     // but we still have the archive directory
295     assertTrue("Archive directory was deleted via archiver", fs.exists(archiveDir));
296   }
297 
298 
299   private void createArchiveDirectory() throws IOException {
300     //create the archive and test directory
301     FileSystem fs = UTIL.getTestFileSystem();
302     Path archiveDir = getArchiveDir();
303     fs.mkdirs(archiveDir);
304   }
305 
306   private Path getArchiveDir() throws IOException {
307     return new Path(UTIL.getDataTestDir(), HConstants.HFILE_ARCHIVE_DIRECTORY);
308   }
309 
310   private Path getTableDir(String tableName) throws IOException {
311     Path testDataDir = UTIL.getDataTestDir();
312     FSUtils.setRootDir(UTIL.getConfiguration(), testDataDir);
313     return new Path(testDataDir, tableName);
314   }
315 
316   private HFileCleaner setupAndCreateCleaner(Configuration conf, FileSystem fs, Path archiveDir,
317       Stoppable stop) {
318     conf.setStrings(HFileCleaner.MASTER_HFILE_CLEANER_PLUGINS,
319       LongTermArchivingHFileCleaner.class.getCanonicalName());
320     return new HFileCleaner(1000, stop, conf, fs, archiveDir, POOL);
321   }
322 
323   /**
324    * Start archiving table for given hfile cleaner
325    * @param tableName table to archive
326    * @param cleaner cleaner to check to make sure change propagated
327    * @return underlying {@link LongTermArchivingHFileCleaner} that is managing archiving
328    * @throws IOException on failure
329    * @throws KeeperException on failure
330    */
331   private List<BaseHFileCleanerDelegate> turnOnArchiving(String tableName, HFileCleaner cleaner)
332       throws IOException, KeeperException {
333     // turn on hfile retention
334     LOG.debug("----Starting archiving for table:" + tableName);
335     archivingClient.enableHFileBackupAsync(Bytes.toBytes(tableName));
336     assertTrue("Archving didn't get turned on", archivingClient.getArchivingEnabled(tableName));
337 
338     // wait for the archiver to get the notification
339     List<BaseHFileCleanerDelegate> cleaners = cleaner.getDelegatesForTesting();
340     LongTermArchivingHFileCleaner delegate = (LongTermArchivingHFileCleaner) cleaners.get(0);
341     while (!delegate.archiveTracker.keepHFiles(STRING_TABLE_NAME)) {
342       // spin until propagation - should be fast
343     }
344     return cleaners;
345   }
346 
347   /**
348    * Spy on the {@link LongTermArchivingHFileCleaner} to ensure we can catch when the cleaner has
349    * seen all the files
350    * @return a {@link CountDownLatch} to wait on that releases when the cleaner has been called at
351    *         least the expected number of times.
352    */
353   private CountDownLatch setupCleanerWatching(LongTermArchivingHFileCleaner cleaner,
354       List<BaseHFileCleanerDelegate> cleaners, final int expected) {
355     // replace the cleaner with one that we can can check
356     BaseHFileCleanerDelegate delegateSpy = Mockito.spy(cleaner);
357     final int[] counter = new int[] { 0 };
358     final CountDownLatch finished = new CountDownLatch(1);
359     Mockito.doAnswer(new Answer<Iterable<FileStatus>>() {
360 
361       @Override
362       public Iterable<FileStatus> answer(InvocationOnMock invocation) throws Throwable {
363         counter[0]++;
364         LOG.debug(counter[0] + "/ " + expected + ") Wrapping call to getDeletableFiles for files: "
365             + invocation.getArguments()[0]);
366 
367         @SuppressWarnings("unchecked")
368         Iterable<FileStatus> ret = (Iterable<FileStatus>) invocation.callRealMethod();
369         if (counter[0] >= expected) finished.countDown();
370         return ret;
371       }
372     }).when(delegateSpy).getDeletableFiles(Mockito.anyListOf(FileStatus.class));
373     cleaners.set(0, delegateSpy);
374 
375     return finished;
376   }
377 
378   /**
379    * Get all the files (non-directory entries) in the file system under the passed directory
380    * @param dir directory to investigate
381    * @return all files under the directory
382    */
383   private List<Path> getAllFiles(FileSystem fs, Path dir) throws IOException {
384     FileStatus[] files = FSUtils.listStatus(fs, dir, null);
385     if (files == null) {
386       LOG.warn("No files under:" + dir);
387       return null;
388     }
389 
390     List<Path> allFiles = new ArrayList<Path>();
391     for (FileStatus file : files) {
392       if (file.isDirectory()) {
393         List<Path> subFiles = getAllFiles(fs, file.getPath());
394         if (subFiles != null) allFiles.addAll(subFiles);
395         continue;
396       }
397       allFiles.add(file.getPath());
398     }
399     return allFiles;
400   }
401 
402   private void loadFlushAndCompact(Region region, byte[] family) throws IOException {
403     // create two hfiles in the region
404     createHFileInRegion(region, family);
405     createHFileInRegion(region, family);
406 
407     Store s = region.getStore(family);
408     int count = s.getStorefilesCount();
409     assertTrue("Don't have the expected store files, wanted >= 2 store files, but was:" + count,
410       count >= 2);
411 
412     // compact the two files into one file to get files in the archive
413     LOG.debug("Compacting stores");
414     region.compact(true);
415   }
416 
417   /**
418    * Create a new hfile in the passed region
419    * @param region region to operate on
420    * @param columnFamily family for which to add data
421    * @throws IOException
422    */
423   private void createHFileInRegion(Region region, byte[] columnFamily) throws IOException {
424     // put one row in the region
425     Put p = new Put(Bytes.toBytes("row"));
426     p.add(columnFamily, Bytes.toBytes("Qual"), Bytes.toBytes("v1"));
427     region.put(p);
428     // flush the region to make a store file
429     region.flush(true);
430   }
431 
432   /**
433    * @param cleaner
434    */
435   private void runCleaner(HFileCleaner cleaner, CountDownLatch finished, Stoppable stop)
436       throws InterruptedException {
437     final ChoreService choreService = new ChoreService("CLEANER_SERVER_NAME");
438     // run the cleaner
439     choreService.scheduleChore(cleaner);
440     // wait for the cleaner to check all the files
441     finished.await();
442     // stop the cleaner
443     stop.stop("");
444   }
445 }