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.regionserver;
19  
20  import static org.junit.Assert.assertTrue;
21  import static org.junit.Assert.fail;
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.concurrent.Callable;
27  
28  import org.apache.commons.lang.exception.ExceptionUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.apache.commons.logging.impl.Log4JLogger;
32  import org.apache.hadoop.conf.Configuration;
33  import org.apache.hadoop.fs.FileSystem;
34  import org.apache.hadoop.fs.Path;
35  import org.apache.hadoop.hbase.Cell;
36  import org.apache.hadoop.hbase.CoordinatedStateManager;
37  import org.apache.hadoop.hbase.HBaseTestingUtility;
38  import org.apache.hadoop.hbase.HConstants;
39  import org.apache.hadoop.hbase.HRegionInfo;
40  import org.apache.hadoop.hbase.HTableDescriptor;
41  import org.apache.hadoop.hbase.HTestConst;
42  import org.apache.hadoop.hbase.KeyValue;
43  import org.apache.hadoop.hbase.KeyValue.KVComparator;
44  import org.apache.hadoop.hbase.TableName;
45  import org.apache.hadoop.hbase.client.Put;
46  import org.apache.hadoop.hbase.client.Result;
47  import org.apache.hadoop.hbase.client.ResultScanner;
48  import org.apache.hadoop.hbase.client.Scan;
49  import org.apache.hadoop.hbase.client.ScannerCallable;
50  import org.apache.hadoop.hbase.client.Table;
51  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ScanRequest;
52  import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.ScanResponse;
53  import org.apache.hadoop.hbase.regionserver.HRegion.RegionScannerImpl;
54  import org.apache.hadoop.hbase.testclassification.MediumTests;
55  import org.apache.hadoop.hbase.util.Bytes;
56  import org.apache.hadoop.hbase.wal.WAL;
57  import org.apache.log4j.Level;
58  import org.junit.After;
59  import org.junit.AfterClass;
60  import org.junit.Before;
61  import org.junit.BeforeClass;
62  import org.junit.Test;
63  import org.junit.experimental.categories.Category;
64  
65  import com.google.protobuf.RpcController;
66  import com.google.protobuf.ServiceException;
67  
68  /**
69   * Here we test to make sure that scans return the expected Results when the server is sending the
70   * Client heartbeat messages. Heartbeat messages are essentially keep-alive messages (they prevent
71   * the scanner on the client side from timing out). A heartbeat message is sent from the server to
72   * the client when the server has exceeded the time limit during the processing of the scan. When
73   * the time limit is reached, the server will return to the Client whatever Results it has
74   * accumulated (potentially empty).
75   */
76  @Category(MediumTests.class)
77  public class TestScannerHeartbeatMessages {
78    private static final Log LOG = LogFactory.getLog(TestScannerHeartbeatMessages.class);
79  
80    private final static HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility();
81  
82    private static Table TABLE = null;
83  
84    /**
85     * Table configuration
86     */
87    private static TableName TABLE_NAME = TableName.valueOf("testScannerHeartbeatMessagesTable");
88  
89    private static int NUM_ROWS = 10;
90    private static byte[] ROW = Bytes.toBytes("testRow");
91    private static byte[][] ROWS = HTestConst.makeNAscii(ROW, NUM_ROWS);
92  
93    private static int NUM_FAMILIES = 3;
94    private static byte[] FAMILY = Bytes.toBytes("testFamily");
95    private static byte[][] FAMILIES = HTestConst.makeNAscii(FAMILY, NUM_FAMILIES);
96  
97    private static int NUM_QUALIFIERS = 3;
98    private static byte[] QUALIFIER = Bytes.toBytes("testQualifier");
99    private static byte[][] QUALIFIERS = HTestConst.makeNAscii(QUALIFIER, NUM_QUALIFIERS);
100 
101   private static int VALUE_SIZE = 128;
102   private static byte[] VALUE = Bytes.createMaxByteArray(VALUE_SIZE);
103 
104   // Time, in milliseconds, that the client will wait for a response from the server before timing
105   // out. This value is used server side to determine when it is necessary to send a heartbeat
106   // message to the client
107   private static int CLIENT_TIMEOUT = 2000;
108 
109   // The server limits itself to running for half of the CLIENT_TIMEOUT value.
110   private static int SERVER_TIME_LIMIT = CLIENT_TIMEOUT / 2;
111 
112   // By default, at most one row's worth of cells will be retrieved before the time limit is reached
113   private static int DEFAULT_ROW_SLEEP_TIME = SERVER_TIME_LIMIT / 2;
114   // By default, at most cells for two column families are retrieved before the time limit is
115   // reached
116   private static int DEFAULT_CF_SLEEP_TIME = DEFAULT_ROW_SLEEP_TIME / NUM_FAMILIES;
117 
118   @BeforeClass
119   public static void setUpBeforeClass() throws Exception {
120     ((Log4JLogger) ScannerCallable.LOG).getLogger().setLevel(Level.ALL);
121     ((Log4JLogger) HeartbeatRPCServices.LOG).getLogger().setLevel(Level.ALL);
122     Configuration conf = TEST_UTIL.getConfiguration();
123 
124     conf.setStrings(HConstants.REGION_IMPL, HeartbeatHRegion.class.getName());
125     conf.setStrings(HConstants.REGION_SERVER_IMPL, HeartbeatHRegionServer.class.getName());
126     conf.setInt(HConstants.HBASE_CLIENT_SCANNER_TIMEOUT_PERIOD, CLIENT_TIMEOUT);
127     conf.setInt(HConstants.HBASE_RPC_TIMEOUT_KEY, CLIENT_TIMEOUT);
128     conf.setInt(HConstants.HBASE_CLIENT_PAUSE, 1);
129 
130     // Check the timeout condition after every cell
131     conf.setLong(StoreScanner.HBASE_CELLS_SCANNED_PER_HEARTBEAT_CHECK, 1);
132     TEST_UTIL.startMiniCluster(1);
133 
134     TABLE = createTestTable(TABLE_NAME, ROWS, FAMILIES, QUALIFIERS, VALUE);
135   }
136 
137   static Table createTestTable(TableName name, byte[][] rows, byte[][] families,
138       byte[][] qualifiers, byte[] cellValue) throws IOException {
139     Table ht = TEST_UTIL.createTable(name, families);
140     List<Put> puts = createPuts(rows, families, qualifiers, cellValue);
141     ht.put(puts);
142 
143     return ht;
144   }
145 
146   /**
147    * Make puts to put the input value into each combination of row, family, and qualifier
148    * @param rows
149    * @param families
150    * @param qualifiers
151    * @param value
152    * @return
153    * @throws IOException
154    */
155   static ArrayList<Put> createPuts(byte[][] rows, byte[][] families, byte[][] qualifiers,
156       byte[] value) throws IOException {
157     Put put;
158     ArrayList<Put> puts = new ArrayList<>();
159 
160     for (int row = 0; row < rows.length; row++) {
161       put = new Put(rows[row]);
162       for (int fam = 0; fam < families.length; fam++) {
163         for (int qual = 0; qual < qualifiers.length; qual++) {
164           KeyValue kv = new KeyValue(rows[row], families[fam], qualifiers[qual], qual, value);
165           put.add(kv);
166         }
167       }
168       puts.add(put);
169     }
170 
171     return puts;
172   }
173 
174   @AfterClass
175   public static void tearDownAfterClass() throws Exception {
176     TEST_UTIL.deleteTable(TABLE_NAME);
177     TEST_UTIL.shutdownMiniCluster();
178   }
179 
180   @Before
181   public void setupBeforeTest() throws Exception {
182     disableSleeping();
183   }
184 
185   @After
186   public void teardownAfterTest() throws Exception {
187     disableSleeping();
188   }
189 
190   /**
191    * Test a variety of scan configurations to ensure that they return the expected Results when
192    * heartbeat messages are necessary. These tests are accumulated under one test case to ensure
193    * that they don't run in parallel. If the tests ran in parallel, they may conflict with each
194    * other due to changing static variables
195    */
196   @Test
197   public void testScannerHeartbeatMessages() throws Exception {
198     testImportanceOfHeartbeats(testHeartbeatBetweenRows());
199     testImportanceOfHeartbeats(testHeartbeatBetweenColumnFamilies());
200   }
201 
202   /**
203    * Run the test callable when heartbeats are enabled/disabled. We expect all tests to only pass
204    * when heartbeat messages are enabled (otherwise the test is pointless). When heartbeats are
205    * disabled, the test should throw an exception.
206    * @param testCallable
207    * @throws InterruptedException
208    */
209   public void testImportanceOfHeartbeats(Callable<Void> testCallable) throws InterruptedException {
210     HeartbeatRPCServices.heartbeatsEnabled = true;
211 
212     try {
213       testCallable.call();
214     } catch (Exception e) {
215       fail("Heartbeat messages are enabled, exceptions should NOT be thrown. Exception trace:"
216           + ExceptionUtils.getStackTrace(e));
217     }
218 
219     HeartbeatRPCServices.heartbeatsEnabled = false;
220     try {
221       testCallable.call();
222     } catch (Exception e) {
223       return;
224     } finally {
225       HeartbeatRPCServices.heartbeatsEnabled = true;
226     }
227     fail("Heartbeats messages are disabled, an exception should be thrown. If an exception "
228         + " is not thrown, the test case is not testing the importance of heartbeat messages");
229   }
230 
231   /**
232    * Test the case that the time limit for the scan is reached after each full row of cells is
233    * fetched.
234    * @throws Exception
235    */
236   public Callable<Void> testHeartbeatBetweenRows() throws Exception {
237     return new Callable<Void>() {
238 
239       @Override
240       public Void call() throws Exception {
241         // Configure the scan so that it can read the entire table in a single RPC. We want to test
242         // the case where a scan stops on the server side due to a time limit
243         Scan scan = new Scan();
244         scan.setMaxResultSize(Long.MAX_VALUE);
245         scan.setCaching(Integer.MAX_VALUE);
246 
247         testEquivalenceOfScanWithHeartbeats(scan, DEFAULT_ROW_SLEEP_TIME, -1, false);
248         return null;
249       }
250     };
251   }
252 
253   /**
254    * Test the case that the time limit for scans is reached in between column families
255    * @throws Exception
256    */
257   public Callable<Void> testHeartbeatBetweenColumnFamilies() throws Exception {
258     return new Callable<Void>() {
259       @Override
260       public Void call() throws Exception {
261         // Configure the scan so that it can read the entire table in a single RPC. We want to test
262         // the case where a scan stops on the server side due to a time limit
263         Scan baseScan = new Scan();
264         baseScan.setMaxResultSize(Long.MAX_VALUE);
265         baseScan.setCaching(Integer.MAX_VALUE);
266 
267         // Copy the scan before each test. When a scan object is used by a scanner, some of its
268         // fields may be changed such as start row
269         Scan scanCopy = new Scan(baseScan);
270         testEquivalenceOfScanWithHeartbeats(scanCopy, -1, DEFAULT_CF_SLEEP_TIME, false);
271         scanCopy = new Scan(baseScan);
272         testEquivalenceOfScanWithHeartbeats(scanCopy, -1, DEFAULT_CF_SLEEP_TIME, true);
273         return null;
274       }
275     };
276   }
277 
278   /**
279    * Test the equivalence of a scan versus the same scan executed when heartbeat messages are
280    * necessary
281    * @param scan The scan configuration being tested
282    * @param rowSleepTime The time to sleep between fetches of row cells
283    * @param cfSleepTime The time to sleep between fetches of column family cells
284    * @param sleepBeforeCf set to true when column family sleeps should occur before the cells for
285    *          that column family are fetched
286    * @throws Exception
287    */
288   public void testEquivalenceOfScanWithHeartbeats(final Scan scan, int rowSleepTime,
289       int cfSleepTime, boolean sleepBeforeCf) throws Exception {
290     disableSleeping();
291     final ResultScanner scanner = TABLE.getScanner(scan);
292     final ResultScanner scannerWithHeartbeats = TABLE.getScanner(scan);
293 
294     Result r1 = null;
295     Result r2 = null;
296 
297     while ((r1 = scanner.next()) != null) {
298       // Enforce the specified sleep conditions during calls to the heartbeat scanner
299       configureSleepTime(rowSleepTime, cfSleepTime, sleepBeforeCf);
300       r2 = scannerWithHeartbeats.next();
301       disableSleeping();
302 
303       assertTrue(r2 != null);
304       try {
305         Result.compareResults(r1, r2);
306       } catch (Exception e) {
307         fail(e.getMessage());
308       }
309     }
310 
311     assertTrue(scannerWithHeartbeats.next() == null);
312     scanner.close();
313     scannerWithHeartbeats.close();
314   }
315 
316   /**
317    * Helper method for setting the time to sleep between rows and column families. If a sleep time
318    * is negative then that sleep will be disabled
319    * @param rowSleepTime
320    * @param cfSleepTime
321    */
322   private static void configureSleepTime(int rowSleepTime, int cfSleepTime, boolean sleepBeforeCf) {
323     HeartbeatHRegion.sleepBetweenRows = rowSleepTime > 0;
324     HeartbeatHRegion.rowSleepTime = rowSleepTime;
325 
326     HeartbeatHRegion.sleepBetweenColumnFamilies = cfSleepTime > 0;
327     HeartbeatHRegion.columnFamilySleepTime = cfSleepTime;
328     HeartbeatHRegion.sleepBeforeColumnFamily = sleepBeforeCf;
329   }
330 
331   /**
332    * Disable the sleeping mechanism server side.
333    */
334   private static void disableSleeping() {
335     HeartbeatHRegion.sleepBetweenRows = false;
336     HeartbeatHRegion.sleepBetweenColumnFamilies = false;
337   }
338 
339   /**
340    * Custom HRegionServer instance that instantiates {@link HeartbeatRPCServices} in place of
341    * {@link RSRpcServices} to allow us to toggle support for heartbeat messages
342    */
343   private static class HeartbeatHRegionServer extends HRegionServer {
344     public HeartbeatHRegionServer(Configuration conf) throws IOException, InterruptedException {
345       super(conf);
346     }
347 
348     public HeartbeatHRegionServer(Configuration conf, CoordinatedStateManager csm)
349         throws IOException, InterruptedException {
350       super(conf, csm);
351     }
352 
353     @Override
354     protected RSRpcServices createRpcServices() throws IOException {
355       return new HeartbeatRPCServices(this);
356     }
357   }
358 
359   /**
360    * Custom RSRpcServices instance that allows heartbeat support to be toggled
361    */
362   private static class HeartbeatRPCServices extends RSRpcServices {
363     private static boolean heartbeatsEnabled = true;
364 
365     public HeartbeatRPCServices(HRegionServer rs) throws IOException {
366       super(rs);
367     }
368 
369     @Override
370     public ScanResponse scan(RpcController controller, ScanRequest request) 
371         throws ServiceException {
372       ScanRequest.Builder builder = ScanRequest.newBuilder(request);
373       builder.setClientHandlesHeartbeats(heartbeatsEnabled);
374       return super.scan(controller, builder.build());
375     }
376   }
377 
378   /**
379    * Custom HRegion class that instantiates {@link RegionScanner}s with configurable sleep times
380    * between fetches of row Results and/or column family cells. Useful for emulating an instance
381    * where the server is taking a long time to process a client's scan request
382    */
383   private static class HeartbeatHRegion extends HRegion {
384     // Row sleeps occur AFTER each row worth of cells is retrieved.
385     private static int rowSleepTime = DEFAULT_ROW_SLEEP_TIME;
386     private static boolean sleepBetweenRows = false;
387 
388     // The sleep for column families can be initiated before or after we fetch the cells for the
389     // column family. If the sleep occurs BEFORE then the time limits will be reached inside
390     // StoreScanner while we are fetching individual cells. If the sleep occurs AFTER then the time
391     // limit will be reached inside RegionScanner after all the cells for a column family have been
392     // retrieved.
393     private static boolean sleepBeforeColumnFamily = false;
394     private static int columnFamilySleepTime = DEFAULT_CF_SLEEP_TIME;
395     private static boolean sleepBetweenColumnFamilies = false;
396 
397     public HeartbeatHRegion(Path tableDir, WAL wal, FileSystem fs, Configuration confParam,
398         HRegionInfo regionInfo, HTableDescriptor htd, RegionServerServices rsServices) {
399       super(tableDir, wal, fs, confParam, regionInfo, htd, rsServices);
400     }
401 
402     public HeartbeatHRegion(HRegionFileSystem fs, WAL wal, Configuration confParam,
403         HTableDescriptor htd, RegionServerServices rsServices) {
404       super(fs, wal, confParam, htd, rsServices);
405     }
406 
407     private static void columnFamilySleep() {
408       if (HeartbeatHRegion.sleepBetweenColumnFamilies) {
409         try {
410           Thread.sleep(HeartbeatHRegion.columnFamilySleepTime);
411         } catch (InterruptedException e) {
412         }
413       }
414     }
415 
416     private static void rowSleep() {
417       try {
418         if (HeartbeatHRegion.sleepBetweenRows) {
419           Thread.sleep(HeartbeatHRegion.rowSleepTime);
420         }
421       } catch (InterruptedException e) {
422       }
423     }
424 
425     // Instantiate the custom heartbeat region scanners
426     @Override
427     protected RegionScanner instantiateRegionScanner(Scan scan,
428         List<KeyValueScanner> additionalScanners) throws IOException {
429       if (scan.isReversed()) {
430         if (scan.getFilter() != null) {
431           scan.getFilter().setReversed(true);
432         }
433         return new HeartbeatReversedRegionScanner(scan, additionalScanners, this);
434       }
435       return new HeartbeatRegionScanner(scan, additionalScanners, this);
436     }
437   }
438 
439   /**
440    * Custom ReversedRegionScanner that can be configured to sleep between retrievals of row Results
441    * and/or column family cells
442    */
443   private static class HeartbeatReversedRegionScanner extends ReversedRegionScannerImpl {
444     HeartbeatReversedRegionScanner(Scan scan, List<KeyValueScanner> additionalScanners,
445         HRegion region) throws IOException {
446       super(scan, additionalScanners, region);
447     }
448 
449     @Override
450     public boolean nextRaw(List<Cell> outResults, ScannerContext context)
451         throws IOException {
452       boolean moreRows = super.nextRaw(outResults, context);
453       HeartbeatHRegion.rowSleep();
454       return moreRows;
455     }
456 
457     @Override
458     protected void initializeKVHeap(List<KeyValueScanner> scanners,
459         List<KeyValueScanner> joinedScanners, HRegion region) throws IOException {
460       this.storeHeap = new HeartbeatReversedKVHeap(scanners, region.getComparator());
461       if (!joinedScanners.isEmpty()) {
462         this.joinedHeap = new HeartbeatReversedKVHeap(joinedScanners, region.getComparator());
463       }
464     }
465   }
466 
467   /**
468    * Custom RegionScanner that can be configured to sleep between retrievals of row Results and/or
469    * column family cells
470    */
471   private static class HeartbeatRegionScanner extends RegionScannerImpl {
472     HeartbeatRegionScanner(Scan scan, List<KeyValueScanner> additionalScanners, HRegion region)
473         throws IOException {
474       region.super(scan, additionalScanners, region);
475     }
476 
477     @Override
478     public boolean nextRaw(List<Cell> outResults, ScannerContext context)
479         throws IOException {
480       boolean moreRows = super.nextRaw(outResults, context);
481       HeartbeatHRegion.rowSleep();
482       return moreRows;
483     }
484 
485     @Override
486     protected void initializeKVHeap(List<KeyValueScanner> scanners,
487         List<KeyValueScanner> joinedScanners, HRegion region) throws IOException {
488       this.storeHeap = new HeartbeatKVHeap(scanners, region.getComparator());
489       if (!joinedScanners.isEmpty()) {
490         this.joinedHeap = new HeartbeatKVHeap(joinedScanners, region.getComparator());
491       }
492     }
493   }
494 
495   /**
496    * Custom KV Heap that can be configured to sleep/wait in between retrievals of column family
497    * cells. Useful for testing
498    */
499   private static final class HeartbeatKVHeap extends KeyValueHeap {
500     public HeartbeatKVHeap(List<? extends KeyValueScanner> scanners, KVComparator comparator)
501         throws IOException {
502       super(scanners, comparator);
503     }
504 
505     HeartbeatKVHeap(List<? extends KeyValueScanner> scanners, KVScannerComparator comparator)
506         throws IOException {
507       super(scanners, comparator);
508     }
509 
510     @Override
511     public boolean next(List<Cell> result, ScannerContext context)
512         throws IOException {
513       if (HeartbeatHRegion.sleepBeforeColumnFamily) HeartbeatHRegion.columnFamilySleep();
514       boolean moreRows = super.next(result, context);
515       if (!HeartbeatHRegion.sleepBeforeColumnFamily) HeartbeatHRegion.columnFamilySleep();
516       return moreRows;
517     }
518   }
519 
520   /**
521    * Custom reversed KV Heap that can be configured to sleep in between retrievals of column family
522    * cells.
523    */
524   private static final class HeartbeatReversedKVHeap extends ReversedKeyValueHeap {
525     public HeartbeatReversedKVHeap(List<? extends KeyValueScanner> scanners, 
526         KVComparator comparator) throws IOException {
527       super(scanners, comparator);
528     }
529 
530     @Override
531     public boolean next(List<Cell> result, ScannerContext context)
532         throws IOException {
533       if (HeartbeatHRegion.sleepBeforeColumnFamily) HeartbeatHRegion.columnFamilySleep();
534       boolean moreRows = super.next(result, context);
535       if (!HeartbeatHRegion.sleepBeforeColumnFamily) HeartbeatHRegion.columnFamilySleep();
536       return moreRows;
537     }
538   }
539 }