/* * Copyright 2011 The Apache Software Foundation * * 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.coprocessor; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.NavigableMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.CoprocessorEnvironment; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HColumnDescriptor; import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HServerAddress; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.MiniHBaseCluster; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.client.HBaseAdmin; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.master.AssignmentManager; import org.apache.hadoop.hbase.master.HMaster; import org.apache.hadoop.hbase.master.MasterCoprocessorHost; import org.apache.hadoop.hbase.regionserver.HRegionServer; import org.apache.hadoop.hbase.util.Bytes; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; /** * Tests invocation of the {@link org.apache.hadoop.hbase.coprocessor.MasterObserver} * interface hooks at all appropriate times during normal HMaster operations. */ public class TestMasterObserver { private static final Log LOG = LogFactory.getLog(TestMasterObserver.class); public static class CPMasterObserver implements MasterObserver { private boolean bypass = false; private boolean preCreateTableCalled; private boolean postCreateTableCalled; private boolean preDeleteTableCalled; private boolean postDeleteTableCalled; private boolean preModifyTableCalled; private boolean postModifyTableCalled; private boolean preAddColumnCalled; private boolean postAddColumnCalled; private boolean preModifyColumnCalled; private boolean postModifyColumnCalled; private boolean preDeleteColumnCalled; private boolean postDeleteColumnCalled; private boolean preEnableTableCalled; private boolean postEnableTableCalled; private boolean preDisableTableCalled; private boolean postDisableTableCalled; private boolean preMoveCalled; private boolean postMoveCalled; private boolean preAssignCalled; private boolean postAssignCalled; private boolean preUnassignCalled; private boolean postUnassignCalled; private boolean preBalanceCalled; private boolean postBalanceCalled; private boolean preBalanceSwitchCalled; private boolean postBalanceSwitchCalled; private boolean preShutdownCalled; private boolean preStopMasterCalled; private boolean postStartMasterCalled; private boolean startCalled; private boolean stopCalled; public void enableBypass(boolean bypass) { this.bypass = bypass; } public void resetStates() { preCreateTableCalled = false; postCreateTableCalled = false; preDeleteTableCalled = false; postDeleteTableCalled = false; preModifyTableCalled = false; postModifyTableCalled = false; preAddColumnCalled = false; postAddColumnCalled = false; preModifyColumnCalled = false; postModifyColumnCalled = false; preDeleteColumnCalled = false; postDeleteColumnCalled = false; preEnableTableCalled = false; postEnableTableCalled = false; preDisableTableCalled = false; postDisableTableCalled = false; preMoveCalled= false; postMoveCalled = false; preAssignCalled = false; postAssignCalled = false; preUnassignCalled = false; postUnassignCalled = false; preBalanceCalled = false; postBalanceCalled = false; preBalanceSwitchCalled = false; postBalanceSwitchCalled = false; } @Override public void preCreateTable(ObserverContext<MasterCoprocessorEnvironment> env, HTableDescriptor desc, HRegionInfo[] regions) throws IOException { if (bypass) { env.bypass(); } preCreateTableCalled = true; } @Override public void postCreateTable(ObserverContext<MasterCoprocessorEnvironment> env, HTableDescriptor desc, HRegionInfo[] regions) throws IOException { postCreateTableCalled = true; } public boolean wasCreateTableCalled() { return preCreateTableCalled && postCreateTableCalled; } public boolean preCreateTableCalledOnly() { return preCreateTableCalled && !postCreateTableCalled; } @Override public void preDeleteTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { if (bypass) { env.bypass(); } preDeleteTableCalled = true; } @Override public void postDeleteTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { postDeleteTableCalled = true; } public boolean wasDeleteTableCalled() { return preDeleteTableCalled && postDeleteTableCalled; } public boolean preDeleteTableCalledOnly() { return preDeleteTableCalled && !postDeleteTableCalled; } @Override public void preModifyTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HTableDescriptor htd) throws IOException { if (bypass) { env.bypass(); } preModifyTableCalled = true; } @Override public void postModifyTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HTableDescriptor htd) throws IOException { postModifyTableCalled = true; } public boolean wasModifyTableCalled() { return preModifyTableCalled && postModifyTableCalled; } public boolean preModifyTableCalledOnly() { return preModifyTableCalled && !postModifyTableCalled; } @Override public void preAddColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HColumnDescriptor column) throws IOException { if (bypass) { env.bypass(); } preAddColumnCalled = true; } @Override public void postAddColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HColumnDescriptor column) throws IOException { postAddColumnCalled = true; } public boolean wasAddColumnCalled() { return preAddColumnCalled && postAddColumnCalled; } public boolean preAddColumnCalledOnly() { return preAddColumnCalled && !postAddColumnCalled; } @Override public void preModifyColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HColumnDescriptor descriptor) throws IOException { if (bypass) { env.bypass(); } preModifyColumnCalled = true; } @Override public void postModifyColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, HColumnDescriptor descriptor) throws IOException { postModifyColumnCalled = true; } public boolean wasModifyColumnCalled() { return preModifyColumnCalled && postModifyColumnCalled; } public boolean preModifyColumnCalledOnly() { return preModifyColumnCalled && !postModifyColumnCalled; } @Override public void preDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, byte[] c) throws IOException { if (bypass) { env.bypass(); } preDeleteColumnCalled = true; } @Override public void postDeleteColumn(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName, byte[] c) throws IOException { postDeleteColumnCalled = true; } public boolean wasDeleteColumnCalled() { return preDeleteColumnCalled && postDeleteColumnCalled; } public boolean preDeleteColumnCalledOnly() { return preDeleteColumnCalled && !postDeleteColumnCalled; } @Override public void preEnableTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { if (bypass) { env.bypass(); } preEnableTableCalled = true; } @Override public void postEnableTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { postEnableTableCalled = true; } public boolean wasEnableTableCalled() { return preEnableTableCalled && postEnableTableCalled; } public boolean preEnableTableCalledOnly() { return preEnableTableCalled && !postEnableTableCalled; } @Override public void preDisableTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { if (bypass) { env.bypass(); } preDisableTableCalled = true; } @Override public void postDisableTable(ObserverContext<MasterCoprocessorEnvironment> env, byte[] tableName) throws IOException { postDisableTableCalled = true; } public boolean wasDisableTableCalled() { return preDisableTableCalled && postDisableTableCalled; } public boolean preDisableTableCalledOnly() { return preDisableTableCalled && !postDisableTableCalled; } @Override public void preMove(ObserverContext<MasterCoprocessorEnvironment> env, HRegionInfo region, ServerName srcServer, ServerName destServer) throws IOException { if (bypass) { env.bypass(); } preMoveCalled = true; } @Override public void postMove(ObserverContext<MasterCoprocessorEnvironment> env, HRegionInfo region, ServerName srcServer, ServerName destServer) throws IOException { postMoveCalled = true; } public boolean wasMoveCalled() { return preMoveCalled && postMoveCalled; } public boolean preMoveCalledOnly() { return preMoveCalled && !postMoveCalled; } @Override public void preAssign(ObserverContext<MasterCoprocessorEnvironment> env, final HRegionInfo regionInfo) throws IOException { if (bypass) { env.bypass(); } preAssignCalled = true; } @Override public void postAssign(ObserverContext<MasterCoprocessorEnvironment> env, final HRegionInfo regionInfo) throws IOException { postAssignCalled = true; } public boolean wasAssignCalled() { return preAssignCalled && postAssignCalled; } public boolean preAssignCalledOnly() { return preAssignCalled && !postAssignCalled; } @Override public void preUnassign(ObserverContext<MasterCoprocessorEnvironment> env, final HRegionInfo regionInfo, final boolean force) throws IOException { if (bypass) { env.bypass(); } preUnassignCalled = true; } @Override public void postUnassign(ObserverContext<MasterCoprocessorEnvironment> env, final HRegionInfo regionInfo, final boolean force) throws IOException { postUnassignCalled = true; } public boolean wasUnassignCalled() { return preUnassignCalled && postUnassignCalled; } public boolean preUnassignCalledOnly() { return preUnassignCalled && !postUnassignCalled; } @Override public void preBalance(ObserverContext<MasterCoprocessorEnvironment> env) throws IOException { if (bypass) { env.bypass(); } preBalanceCalled = true; } @Override public void postBalance(ObserverContext<MasterCoprocessorEnvironment> env) throws IOException { postBalanceCalled = true; } public boolean wasBalanceCalled() { return preBalanceCalled && postBalanceCalled; } public boolean preBalanceCalledOnly() { return preBalanceCalled && !postBalanceCalled; } @Override public boolean preBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> env, boolean b) throws IOException { if (bypass) { env.bypass(); } preBalanceSwitchCalled = true; return b; } @Override public void postBalanceSwitch(ObserverContext<MasterCoprocessorEnvironment> env, boolean oldValue, boolean newValue) throws IOException { postBalanceSwitchCalled = true; } public boolean wasBalanceSwitchCalled() { return preBalanceSwitchCalled && postBalanceSwitchCalled; } public boolean preBalanceSwitchCalledOnly() { return preBalanceSwitchCalled && !postBalanceSwitchCalled; } @Override public void preShutdown(ObserverContext<MasterCoprocessorEnvironment> env) throws IOException { preShutdownCalled = true; } @Override public void preStopMaster(ObserverContext<MasterCoprocessorEnvironment> env) throws IOException { preStopMasterCalled = true; } @Override public void postStartMaster(ObserverContext<MasterCoprocessorEnvironment> ctx) throws IOException { postStartMasterCalled = true; } public boolean wasStartMasterCalled() { return postStartMasterCalled; } @Override public void start(CoprocessorEnvironment env) throws IOException { startCalled = true; } @Override public void stop(CoprocessorEnvironment env) throws IOException { stopCalled = true; } public boolean wasStarted() { return startCalled; } public boolean wasStopped() { return stopCalled; } } private static HBaseTestingUtility UTIL = new HBaseTestingUtility(); private static byte[] TEST_TABLE = Bytes.toBytes("observed_table"); private static byte[] TEST_FAMILY = Bytes.toBytes("fam1"); private static byte[] TEST_FAMILY2 = Bytes.toBytes("fam2"); @BeforeClass public static void setupBeforeClass() throws Exception { Configuration conf = UTIL.getConfiguration(); conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, CPMasterObserver.class.getName()); // We need more than one data server on this test UTIL.startMiniCluster(2); } @AfterClass public static void tearDownAfterClass() throws Exception { UTIL.shutdownMiniCluster(); } @Test public void testStarted() throws Exception { MiniHBaseCluster cluster = UTIL.getHBaseCluster(); HMaster master = cluster.getMaster(); assertTrue("Master should be active", master.isActiveMaster()); MasterCoprocessorHost host = master.getCoprocessorHost(); assertNotNull("CoprocessorHost should not be null", host); CPMasterObserver cp = (CPMasterObserver)host.findCoprocessor( CPMasterObserver.class.getName()); assertNotNull("CPMasterObserver coprocessor not found or not installed!", cp); // check basic lifecycle assertTrue("MasterObserver should have been started", cp.wasStarted()); assertTrue("postStartMaster() hook should have been called", cp.wasStartMasterCalled()); } @Test public void testTableOperations() throws Exception { MiniHBaseCluster cluster = UTIL.getHBaseCluster(); HMaster master = cluster.getMaster(); MasterCoprocessorHost host = master.getCoprocessorHost(); CPMasterObserver cp = (CPMasterObserver)host.findCoprocessor( CPMasterObserver.class.getName()); cp.enableBypass(true); cp.resetStates(); assertFalse("No table created yet", cp.wasCreateTableCalled()); // create a table HTableDescriptor htd = new HTableDescriptor(TEST_TABLE); htd.addFamily(new HColumnDescriptor(TEST_FAMILY)); HBaseAdmin admin = UTIL.getHBaseAdmin(); admin.createTable(htd); // preCreateTable can't bypass default action. assertTrue("Test table should be created", cp.wasCreateTableCalled()); admin.disableTable(TEST_TABLE); assertTrue(admin.isTableDisabled(TEST_TABLE)); // preDisableTable can't bypass default action. assertTrue("Coprocessor should have been called on table disable", cp.wasDisableTableCalled()); // enable assertFalse(cp.wasEnableTableCalled()); admin.enableTable(TEST_TABLE); assertTrue(admin.isTableEnabled(TEST_TABLE)); // preEnableTable can't bypass default action. assertTrue("Coprocessor should have been called on table enable", cp.wasEnableTableCalled()); admin.disableTable(TEST_TABLE); assertTrue(admin.isTableDisabled(TEST_TABLE)); // modify table htd.setMaxFileSize(512 * 1024 * 1024); admin.modifyTable(TEST_TABLE, htd); // preModifyTable can't bypass default action. assertTrue("Test table should have been modified", cp.wasModifyTableCalled()); // add a column family admin.addColumn(TEST_TABLE, new HColumnDescriptor(TEST_FAMILY2)); assertTrue("New column family shouldn't have been added to test table", cp.preAddColumnCalledOnly()); // modify a column family HColumnDescriptor hcd1 = new HColumnDescriptor(TEST_FAMILY2); hcd1.setMaxVersions(25); admin.modifyColumn(TEST_TABLE, hcd1); assertTrue("Second column family should be modified", cp.preModifyColumnCalledOnly()); // delete table admin.deleteTable(TEST_TABLE); assertFalse("Test table should have been deleted", admin.tableExists(TEST_TABLE)); // preDeleteTable can't bypass default action. assertTrue("Coprocessor should have been called on table delete", cp.wasDeleteTableCalled()); // turn off bypass, run the tests again cp.enableBypass(false); cp.resetStates(); admin.createTable(htd); assertTrue("Test table should be created", cp.wasCreateTableCalled()); // disable assertFalse(cp.wasDisableTableCalled()); admin.disableTable(TEST_TABLE); assertTrue(admin.isTableDisabled(TEST_TABLE)); assertTrue("Coprocessor should have been called on table disable", cp.wasDisableTableCalled()); // modify table htd.setMaxFileSize(512 * 1024 * 1024); admin.modifyTable(TEST_TABLE, htd); assertTrue("Test table should have been modified", cp.wasModifyTableCalled()); // add a column family admin.addColumn(TEST_TABLE, new HColumnDescriptor(TEST_FAMILY2)); assertTrue("New column family should have been added to test table", cp.wasAddColumnCalled()); // modify a column family HColumnDescriptor hcd = new HColumnDescriptor(TEST_FAMILY2); hcd.setMaxVersions(25); admin.modifyColumn(TEST_TABLE, hcd); assertTrue("Second column family should be modified", cp.wasModifyColumnCalled()); // enable assertFalse(cp.wasEnableTableCalled()); admin.enableTable(TEST_TABLE); assertTrue(admin.isTableEnabled(TEST_TABLE)); assertTrue("Coprocessor should have been called on table enable", cp.wasEnableTableCalled()); // disable again admin.disableTable(TEST_TABLE); assertTrue(admin.isTableDisabled(TEST_TABLE)); // delete column assertFalse("No column family deleted yet", cp.wasDeleteColumnCalled()); admin.deleteColumn(TEST_TABLE, TEST_FAMILY2); HTableDescriptor tableDesc = admin.getTableDescriptor(TEST_TABLE); assertNull("'"+Bytes.toString(TEST_FAMILY2)+"' should have been removed", tableDesc.getFamily(TEST_FAMILY2)); assertTrue("Coprocessor should have been called on column delete", cp.wasDeleteColumnCalled()); // delete table assertFalse("No table deleted yet", cp.wasDeleteTableCalled()); admin.deleteTable(TEST_TABLE); assertFalse("Test table should have been deleted", admin.tableExists(TEST_TABLE)); assertTrue("Coprocessor should have been called on table delete", cp.wasDeleteTableCalled()); } @Test public void testRegionTransitionOperations() throws Exception { MiniHBaseCluster cluster = UTIL.getHBaseCluster(); HMaster master = cluster.getMaster(); MasterCoprocessorHost host = master.getCoprocessorHost(); CPMasterObserver cp = (CPMasterObserver)host.findCoprocessor( CPMasterObserver.class.getName()); cp.enableBypass(false); cp.resetStates(); HTable table = UTIL.createTable(TEST_TABLE, TEST_FAMILY); int countOfRegions = UTIL.createMultiRegions(table, TEST_FAMILY); UTIL.waitUntilAllRegionsAssigned(countOfRegions); NavigableMap<HRegionInfo, ServerName> regions = table.getRegionLocations(); Map.Entry<HRegionInfo, ServerName> firstGoodPair = null; for (Map.Entry<HRegionInfo, ServerName> e: regions.entrySet()) { if (e.getValue() != null) { firstGoodPair = e; break; } } assertNotNull("Found a non-null entry", firstGoodPair); LOG.info("Found " + firstGoodPair.toString()); // Try to force a move Collection<ServerName> servers = master.getClusterStatus().getServers(); String destName = null; String firstRegionHostnamePortStr = firstGoodPair.getValue().toString(); LOG.info("firstRegionHostnamePortStr=" + firstRegionHostnamePortStr); boolean found = false; // Find server that is NOT carrying the first region for (ServerName info : servers) { LOG.info("ServerName=" + info); if (!firstRegionHostnamePortStr.equals(info.getHostAndPort())) { destName = info.toString(); found = true; break; } } assertTrue("Found server", found); LOG.info("Found " + destName); master.move(firstGoodPair.getKey().getEncodedNameAsBytes(), Bytes.toBytes(destName)); assertTrue("Coprocessor should have been called on region move", cp.wasMoveCalled()); // make sure balancer is on master.balanceSwitch(true); assertTrue("Coprocessor should have been called on balance switch", cp.wasBalanceSwitchCalled()); // force region rebalancing master.balanceSwitch(false); // move half the open regions from RS 0 to RS 1 HRegionServer rs = cluster.getRegionServer(0); byte[] destRS = Bytes.toBytes(cluster.getRegionServer(1).getServerName().toString()); List<HRegionInfo> openRegions = rs.getOnlineRegions(); int moveCnt = openRegions.size()/2; for (int i=0; i<moveCnt; i++) { HRegionInfo info = openRegions.get(i); if (!info.isMetaTable()) { master.move(openRegions.get(i).getEncodedNameAsBytes(), destRS); } } // wait for assignments to finish AssignmentManager mgr = master.getAssignmentManager(); Collection<AssignmentManager.RegionState> transRegions = mgr.getRegionsInTransition().values(); for (AssignmentManager.RegionState state : transRegions) { mgr.waitOnRegionToClearRegionsInTransition(state.getRegion()); } // now trigger a balance master.balanceSwitch(true); boolean balanceRun = master.balance(); assertTrue("Coprocessor should be called on region rebalancing", cp.wasBalanceCalled()); } }