/** * * 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.regionserver; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.hbase.HBaseTestCase; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HColumnDescriptor; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.SmallTests; import org.apache.hadoop.hbase.UnknownScannerException; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.filter.Filter; import org.apache.hadoop.hbase.filter.InclusiveStopFilter; import org.apache.hadoop.hbase.filter.PrefixFilter; import org.apache.hadoop.hbase.filter.WhileMatchFilter; import org.apache.hadoop.hbase.util.Bytes; import org.junit.experimental.categories.Category; /** * Test of a long-lived scanner validating as we go. */ @Category(SmallTests.class) public class TestScanner extends HBaseTestCase { private final Log LOG = LogFactory.getLog(this.getClass()); private static final byte [] FIRST_ROW = HConstants.EMPTY_START_ROW; private static final byte [][] COLS = { HConstants.CATALOG_FAMILY }; private static final byte [][] EXPLICIT_COLS = { HConstants.REGIONINFO_QUALIFIER, HConstants.SERVER_QUALIFIER, // TODO ryan //HConstants.STARTCODE_QUALIFIER }; static final HTableDescriptor TESTTABLEDESC = new HTableDescriptor("testscanner"); static { TESTTABLEDESC.addFamily( new HColumnDescriptor(HConstants.CATALOG_FAMILY) // Ten is an arbitrary number. Keep versions to help debugging. .setMaxVersions(10) .setBlockCacheEnabled(false) .setBlocksize(8 * 1024) ); } /** HRegionInfo for root region */ public static final HRegionInfo REGION_INFO = new HRegionInfo(TESTTABLEDESC.getName(), HConstants.EMPTY_BYTE_ARRAY, HConstants.EMPTY_BYTE_ARRAY); private static final byte [] ROW_KEY = REGION_INFO.getRegionName(); private static final long START_CODE = Long.MAX_VALUE; private HRegion r; private HRegionIncommon region; private byte[] firstRowBytes, secondRowBytes, thirdRowBytes; final private byte[] col1, col2; public TestScanner() { super(); firstRowBytes = START_KEY_BYTES; secondRowBytes = START_KEY_BYTES.clone(); // Increment the least significant character so we get to next row. secondRowBytes[START_KEY_BYTES.length - 1]++; thirdRowBytes = START_KEY_BYTES.clone(); thirdRowBytes[START_KEY_BYTES.length - 1] += 2; col1 = Bytes.toBytes("column1"); col2 = Bytes.toBytes("column2"); } /** * Test basic stop row filter works. * @throws Exception */ public void testStopRow() throws Exception { byte [] startrow = Bytes.toBytes("bbb"); byte [] stoprow = Bytes.toBytes("ccc"); try { this.r = createNewHRegion(TESTTABLEDESC, null, null); addContent(this.r, HConstants.CATALOG_FAMILY); List<KeyValue> results = new ArrayList<KeyValue>(); // Do simple test of getting one row only first. Scan scan = new Scan(Bytes.toBytes("abc"), Bytes.toBytes("abd")); scan.addFamily(HConstants.CATALOG_FAMILY); InternalScanner s = r.getScanner(scan); int count = 0; while (s.next(results)) { count++; } s.close(); assertEquals(0, count); // Now do something a bit more imvolved. scan = new Scan(startrow, stoprow); scan.addFamily(HConstants.CATALOG_FAMILY); s = r.getScanner(scan); count = 0; KeyValue kv = null; results = new ArrayList<KeyValue>(); for (boolean first = true; s.next(results);) { kv = results.get(0); if (first) { assertTrue(Bytes.BYTES_COMPARATOR.compare(startrow, kv.getRow()) == 0); first = false; } count++; } assertTrue(Bytes.BYTES_COMPARATOR.compare(stoprow, kv.getRow()) > 0); // We got something back. assertTrue(count > 10); s.close(); } finally { HRegion.closeHRegion(this.r); } } void rowPrefixFilter(Scan scan) throws IOException { List<KeyValue> results = new ArrayList<KeyValue>(); scan.addFamily(HConstants.CATALOG_FAMILY); InternalScanner s = r.getScanner(scan); boolean hasMore = true; while (hasMore) { hasMore = s.next(results); for (KeyValue kv : results) { assertEquals((byte)'a', kv.getRow()[0]); assertEquals((byte)'b', kv.getRow()[1]); } results.clear(); } s.close(); } void rowInclusiveStopFilter(Scan scan, byte[] stopRow) throws IOException { List<KeyValue> results = new ArrayList<KeyValue>(); scan.addFamily(HConstants.CATALOG_FAMILY); InternalScanner s = r.getScanner(scan); boolean hasMore = true; while (hasMore) { hasMore = s.next(results); for (KeyValue kv : results) { assertTrue(Bytes.compareTo(kv.getRow(), stopRow) <= 0); } results.clear(); } s.close(); } public void testFilters() throws IOException { try { this.r = createNewHRegion(TESTTABLEDESC, null, null); addContent(this.r, HConstants.CATALOG_FAMILY); byte [] prefix = Bytes.toBytes("ab"); Filter newFilter = new PrefixFilter(prefix); Scan scan = new Scan(); scan.setFilter(newFilter); rowPrefixFilter(scan); byte[] stopRow = Bytes.toBytes("bbc"); newFilter = new WhileMatchFilter(new InclusiveStopFilter(stopRow)); scan = new Scan(); scan.setFilter(newFilter); rowInclusiveStopFilter(scan, stopRow); } finally { HRegion.closeHRegion(this.r); } } /** * Test that closing a scanner while a client is using it doesn't throw * NPEs but instead a UnknownScannerException. HBASE-2503 * @throws Exception */ public void testRaceBetweenClientAndTimeout() throws Exception { try { this.r = createNewHRegion(TESTTABLEDESC, null, null); addContent(this.r, HConstants.CATALOG_FAMILY); Scan scan = new Scan(); InternalScanner s = r.getScanner(scan); List<KeyValue> results = new ArrayList<KeyValue>(); try { s.next(results); s.close(); s.next(results); fail("We don't want anything more, we should be failing"); } catch (UnknownScannerException ex) { // ok! return; } } finally { HRegion.closeHRegion(this.r); } } /** The test! * @throws IOException */ public void testScanner() throws IOException { try { r = createNewHRegion(TESTTABLEDESC, null, null); region = new HRegionIncommon(r); // Write information to the meta table Put put = new Put(ROW_KEY, System.currentTimeMillis()); put.add(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER, REGION_INFO.toByteArray()); region.put(put); // What we just committed is in the memstore. Verify that we can get // it back both with scanning and get scan(false, null); getRegionInfo(); // Close and re-open r.close(); r = openClosedRegion(r); region = new HRegionIncommon(r); // Verify we can get the data back now that it is on disk. scan(false, null); getRegionInfo(); // Store some new information String address = HConstants.LOCALHOST_IP + ":" + HBaseTestingUtility.randomFreePort(); put = new Put(ROW_KEY, System.currentTimeMillis()); put.add(HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER, Bytes.toBytes(address)); // put.add(HConstants.COL_STARTCODE, Bytes.toBytes(START_CODE)); region.put(put); // Validate that we can still get the HRegionInfo, even though it is in // an older row on disk and there is a newer row in the memstore scan(true, address.toString()); getRegionInfo(); // flush cache region.flushcache(); // Validate again scan(true, address.toString()); getRegionInfo(); // Close and reopen r.close(); r = openClosedRegion(r); region = new HRegionIncommon(r); // Validate again scan(true, address.toString()); getRegionInfo(); // Now update the information again address = "bar.foo.com:4321"; put = new Put(ROW_KEY, System.currentTimeMillis()); put.add(HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER, Bytes.toBytes(address)); region.put(put); // Validate again scan(true, address.toString()); getRegionInfo(); // flush cache region.flushcache(); // Validate again scan(true, address.toString()); getRegionInfo(); // Close and reopen r.close(); r = openClosedRegion(r); region = new HRegionIncommon(r); // Validate again scan(true, address.toString()); getRegionInfo(); } finally { // clean up HRegion.closeHRegion(r); } } /** Compare the HRegionInfo we read from HBase to what we stored */ private void validateRegionInfo(byte [] regionBytes) throws IOException { HRegionInfo info = HRegionInfo.parseFromOrNull(regionBytes); assertEquals(REGION_INFO.getRegionId(), info.getRegionId()); assertEquals(0, info.getStartKey().length); assertEquals(0, info.getEndKey().length); assertEquals(0, Bytes.compareTo(info.getRegionName(), REGION_INFO.getRegionName())); //assertEquals(0, info.getTableDesc().compareTo(REGION_INFO.getTableDesc())); } /** Use a scanner to get the region info and then validate the results */ private void scan(boolean validateStartcode, String serverName) throws IOException { InternalScanner scanner = null; Scan scan = null; List<KeyValue> results = new ArrayList<KeyValue>(); byte [][][] scanColumns = { COLS, EXPLICIT_COLS }; for(int i = 0; i < scanColumns.length; i++) { try { scan = new Scan(FIRST_ROW); for (int ii = 0; ii < EXPLICIT_COLS.length; ii++) { scan.addColumn(COLS[0], EXPLICIT_COLS[ii]); } scanner = r.getScanner(scan); while (scanner.next(results)) { assertTrue(hasColumn(results, HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER)); byte [] val = getColumn(results, HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER).getValue(); validateRegionInfo(val); if(validateStartcode) { // assertTrue(hasColumn(results, HConstants.CATALOG_FAMILY, // HConstants.STARTCODE_QUALIFIER)); // val = getColumn(results, HConstants.CATALOG_FAMILY, // HConstants.STARTCODE_QUALIFIER).getValue(); assertNotNull(val); assertFalse(val.length == 0); long startCode = Bytes.toLong(val); assertEquals(START_CODE, startCode); } if(serverName != null) { assertTrue(hasColumn(results, HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER)); val = getColumn(results, HConstants.CATALOG_FAMILY, HConstants.SERVER_QUALIFIER).getValue(); assertNotNull(val); assertFalse(val.length == 0); String server = Bytes.toString(val); assertEquals(0, server.compareTo(serverName)); } } } finally { InternalScanner s = scanner; scanner = null; if(s != null) { s.close(); } } } } private boolean hasColumn(final List<KeyValue> kvs, final byte [] family, final byte [] qualifier) { for (KeyValue kv: kvs) { if (kv.matchingFamily(family) && kv.matchingQualifier(qualifier)) { return true; } } return false; } private KeyValue getColumn(final List<KeyValue> kvs, final byte [] family, final byte [] qualifier) { for (KeyValue kv: kvs) { if (kv.matchingFamily(family) && kv.matchingQualifier(qualifier)) { return kv; } } return null; } /** Use get to retrieve the HRegionInfo and validate it */ private void getRegionInfo() throws IOException { Get get = new Get(ROW_KEY); get.addColumn(HConstants.CATALOG_FAMILY, HConstants.REGIONINFO_QUALIFIER); Result result = region.get(get); byte [] bytes = result.value(); validateRegionInfo(bytes); } /** * Tests to do a sync flush during the middle of a scan. This is testing the StoreScanner * update readers code essentially. This is not highly concurrent, since its all 1 thread. * HBase-910. * @throws Exception */ public void testScanAndSyncFlush() throws Exception { this.r = createNewHRegion(TESTTABLEDESC, null, null); HRegionIncommon hri = new HRegionIncommon(r); try { LOG.info("Added: " + addContent(hri, Bytes.toString(HConstants.CATALOG_FAMILY), Bytes.toString(HConstants.REGIONINFO_QUALIFIER))); int count = count(hri, -1, false); assertEquals(count, count(hri, 100, false)); // do a sync flush. } catch (Exception e) { LOG.error("Failed", e); throw e; } finally { HRegion.closeHRegion(this.r); } } /** * Tests to do a concurrent flush (using a 2nd thread) while scanning. This tests both * the StoreScanner update readers and the transition from memstore -> snapshot -> store file. * * @throws Exception */ public void testScanAndRealConcurrentFlush() throws Exception { this.r = createNewHRegion(TESTTABLEDESC, null, null); HRegionIncommon hri = new HRegionIncommon(r); try { LOG.info("Added: " + addContent(hri, Bytes.toString(HConstants.CATALOG_FAMILY), Bytes.toString(HConstants.REGIONINFO_QUALIFIER))); int count = count(hri, -1, false); assertEquals(count, count(hri, 100, true)); // do a true concurrent background thread flush } catch (Exception e) { LOG.error("Failed", e); throw e; } finally { HRegion.closeHRegion(this.r); } } /** * Make sure scanner returns correct result when we run a major compaction * with deletes. * * @throws Exception */ @SuppressWarnings("deprecation") public void testScanAndConcurrentMajorCompact() throws Exception { HTableDescriptor htd = createTableDescriptor(getName()); this.r = createNewHRegion(htd, null, null); HRegionIncommon hri = new HRegionIncommon(r); try { addContent(hri, Bytes.toString(fam1), Bytes.toString(col1), firstRowBytes, secondRowBytes); addContent(hri, Bytes.toString(fam2), Bytes.toString(col1), firstRowBytes, secondRowBytes); Delete dc = new Delete(firstRowBytes); /* delete column1 of firstRow */ dc.deleteColumns(fam1, col1); r.delete(dc, true); r.flushcache(); addContent(hri, Bytes.toString(fam1), Bytes.toString(col1), secondRowBytes, thirdRowBytes); addContent(hri, Bytes.toString(fam2), Bytes.toString(col1), secondRowBytes, thirdRowBytes); r.flushcache(); InternalScanner s = r.getScanner(new Scan()); // run a major compact, column1 of firstRow will be cleaned. r.compactStores(true); List<KeyValue> results = new ArrayList<KeyValue>(); s.next(results); // make sure returns column2 of firstRow assertTrue("result is not correct, keyValues : " + results, results.size() == 1); assertTrue(Bytes.BYTES_COMPARATOR.compare(firstRowBytes, results.get(0) .getRow()) == 0); assertTrue(Bytes.BYTES_COMPARATOR.compare(fam2, results.get(0) .getFamily()) == 0); results = new ArrayList<KeyValue>(); s.next(results); // get secondRow assertTrue(results.size() == 2); assertTrue(Bytes.BYTES_COMPARATOR.compare(secondRowBytes, results.get(0) .getRow()) == 0); assertTrue(Bytes.BYTES_COMPARATOR.compare(fam1, results.get(0) .getFamily()) == 0); assertTrue(Bytes.BYTES_COMPARATOR.compare(fam2, results.get(1) .getFamily()) == 0); } finally { HRegion.closeHRegion(this.r); } } /* * @param hri Region * @param flushIndex At what row we start the flush. * @param concurrent if the flush should be concurrent or sync. * @return Count of rows found. * @throws IOException */ private int count(final HRegionIncommon hri, final int flushIndex, boolean concurrent) throws IOException { LOG.info("Taking out counting scan"); ScannerIncommon s = hri.getScanner(HConstants.CATALOG_FAMILY, EXPLICIT_COLS, HConstants.EMPTY_START_ROW, HConstants.LATEST_TIMESTAMP); List<KeyValue> values = new ArrayList<KeyValue>(); int count = 0; boolean justFlushed = false; while (s.next(values)) { if (justFlushed) { LOG.info("after next() just after next flush"); justFlushed=false; } count++; if (flushIndex == count) { LOG.info("Starting flush at flush index " + flushIndex); Thread t = new Thread() { public void run() { try { hri.flushcache(); LOG.info("Finishing flush"); } catch (IOException e) { LOG.info("Failed flush cache"); } } }; if (concurrent) { t.start(); // concurrently flush. } else { t.run(); // sync flush } LOG.info("Continuing on after kicking off background flush"); justFlushed = true; } } s.close(); LOG.info("Found " + count + " items"); return count; } }