001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.coprocessor; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertTrue; 022 023import java.io.IOException; 024import java.util.Collections; 025import java.util.Optional; 026import java.util.concurrent.ExecutorService; 027import java.util.concurrent.SynchronousQueue; 028import java.util.concurrent.ThreadPoolExecutor; 029import java.util.concurrent.TimeUnit; 030import org.apache.hadoop.hbase.HBaseClassTestRule; 031import org.apache.hadoop.hbase.HBaseTestingUtility; 032import org.apache.hadoop.hbase.HColumnDescriptor; 033import org.apache.hadoop.hbase.HTableDescriptor; 034import org.apache.hadoop.hbase.TableName; 035import org.apache.hadoop.hbase.client.Admin; 036import org.apache.hadoop.hbase.client.Durability; 037import org.apache.hadoop.hbase.client.Put; 038import org.apache.hadoop.hbase.client.Result; 039import org.apache.hadoop.hbase.client.ResultScanner; 040import org.apache.hadoop.hbase.client.Scan; 041import org.apache.hadoop.hbase.client.Table; 042import org.apache.hadoop.hbase.testclassification.CoprocessorTests; 043import org.apache.hadoop.hbase.testclassification.MediumTests; 044import org.apache.hadoop.hbase.util.Threads; 045import org.apache.hadoop.hbase.wal.WALEdit; 046import org.junit.After; 047import org.junit.AfterClass; 048import org.junit.BeforeClass; 049import org.junit.ClassRule; 050import org.junit.Test; 051import org.junit.experimental.categories.Category; 052 053/** 054 * Test that a coprocessor can open a connection and write to another table, inside a hook. 055 */ 056@Category({CoprocessorTests.class, MediumTests.class}) 057public class TestOpenTableInCoprocessor { 058 059 @ClassRule 060 public static final HBaseClassTestRule CLASS_RULE = 061 HBaseClassTestRule.forClass(TestOpenTableInCoprocessor.class); 062 063 private static final TableName otherTable = TableName.valueOf("otherTable"); 064 private static final TableName primaryTable = TableName.valueOf("primary"); 065 private static final byte[] family = new byte[] { 'f' }; 066 067 private static boolean[] completed = new boolean[1]; 068 /** 069 * Custom coprocessor that just copies the write to another table. 070 */ 071 public static class SendToOtherTableCoprocessor implements RegionCoprocessor, RegionObserver { 072 073 @Override 074 public Optional<RegionObserver> getRegionObserver() { 075 return Optional.of(this); 076 } 077 078 @Override 079 public void prePut(final ObserverContext<RegionCoprocessorEnvironment> e, final Put put, 080 final WALEdit edit, final Durability durability) throws IOException { 081 try (Table table = e.getEnvironment().getConnection(). 082 getTable(otherTable)) { 083 table.put(put); 084 completed[0] = true; 085 } 086 } 087 088 } 089 090 private static boolean[] completedWithPool = new boolean[1]; 091 /** 092 * Coprocessor that creates an HTable with a pool to write to another table 093 */ 094 public static class CustomThreadPoolCoprocessor implements RegionCoprocessor, RegionObserver { 095 096 /** 097 * Get a pool that has only ever one thread. A second action added to the pool (running 098 * concurrently), will cause an exception. 099 * @return 100 */ 101 private ExecutorService getPool() { 102 int maxThreads = 1; 103 long keepAliveTime = 60; 104 ThreadPoolExecutor pool = 105 new ThreadPoolExecutor(1, maxThreads, keepAliveTime, TimeUnit.SECONDS, 106 new SynchronousQueue<>(), Threads.newDaemonThreadFactory("hbase-table")); 107 pool.allowCoreThreadTimeOut(true); 108 return pool; 109 } 110 111 @Override 112 public Optional<RegionObserver> getRegionObserver() { 113 return Optional.of(this); 114 } 115 116 @Override 117 public void prePut(final ObserverContext<RegionCoprocessorEnvironment> e, final Put put, 118 final WALEdit edit, final Durability durability) throws IOException { 119 try (Table table = e.getEnvironment().getConnection().getTable(otherTable, getPool())) { 120 Put p = new Put(new byte[]{'a'}); 121 p.addColumn(family, null, new byte[]{'a'}); 122 try { 123 table.batch(Collections.singletonList(put), null); 124 } catch (InterruptedException e1) { 125 throw new IOException(e1); 126 } 127 completedWithPool[0] = true; 128 } 129 } 130 } 131 132 private static HBaseTestingUtility UTIL = new HBaseTestingUtility(); 133 134 @BeforeClass 135 public static void setupCluster() throws Exception { 136 UTIL.startMiniCluster(); 137 } 138 139 @After 140 public void cleanupTestTable() throws Exception { 141 UTIL.getAdmin().disableTable(primaryTable); 142 UTIL.getAdmin().deleteTable(primaryTable); 143 144 UTIL.getAdmin().disableTable(otherTable); 145 UTIL.getAdmin().deleteTable(otherTable); 146 147 } 148 149 @AfterClass 150 public static void teardownCluster() throws Exception { 151 UTIL.shutdownMiniCluster(); 152 } 153 154 @Test 155 public void testCoprocessorCanCreateConnectionToRemoteTable() throws Throwable { 156 runCoprocessorConnectionToRemoteTable(SendToOtherTableCoprocessor.class, completed); 157 } 158 159 @Test 160 public void testCoprocessorCanCreateConnectionToRemoteTableWithCustomPool() throws Throwable { 161 runCoprocessorConnectionToRemoteTable(CustomThreadPoolCoprocessor.class, completedWithPool); 162 } 163 164 private void runCoprocessorConnectionToRemoteTable(Class clazz, boolean[] completeCheck) 165 throws Throwable { 166 // Check if given class implements RegionObserver. 167 assert(RegionObserver.class.isAssignableFrom(clazz)); 168 HTableDescriptor primary = new HTableDescriptor(primaryTable); 169 primary.addFamily(new HColumnDescriptor(family)); 170 // add our coprocessor 171 primary.addCoprocessor(clazz.getName()); 172 173 HTableDescriptor other = new HTableDescriptor(otherTable); 174 other.addFamily(new HColumnDescriptor(family)); 175 176 177 Admin admin = UTIL.getAdmin(); 178 admin.createTable(primary); 179 admin.createTable(other); 180 181 Table table = UTIL.getConnection().getTable(TableName.valueOf("primary")); 182 Put p = new Put(new byte[] { 'a' }); 183 p.addColumn(family, null, new byte[]{'a'}); 184 table.put(p); 185 table.close(); 186 187 Table target = UTIL.getConnection().getTable(otherTable); 188 assertTrue("Didn't complete update to target table!", completeCheck[0]); 189 assertEquals("Didn't find inserted row", 1, getKeyValueCount(target)); 190 target.close(); 191 } 192 193 /** 194 * Count the number of keyvalue in the table. Scans all possible versions 195 * @param table table to scan 196 * @return number of keyvalues over all rows in the table 197 * @throws IOException 198 */ 199 private int getKeyValueCount(Table table) throws IOException { 200 Scan scan = new Scan(); 201 scan.setMaxVersions(Integer.MAX_VALUE - 1); 202 203 ResultScanner results = table.getScanner(scan); 204 int count = 0; 205 for (Result res : results) { 206 count += res.listCells().size(); 207 System.out.println(count + ") " + res); 208 } 209 results.close(); 210 211 return count; 212 } 213}