HBASE-9510 Namespace operations should throw clean exceptions

git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1523902 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael Stack 2013-09-17 05:43:06 +00:00
parent 866a2c6c42
commit 040b53e09a
3 changed files with 167 additions and 24 deletions

View File

@ -51,22 +51,21 @@ import org.apache.hadoop.hbase.Abortable;
import org.apache.hadoop.hbase.Chore; import org.apache.hadoop.hbase.Chore;
import org.apache.hadoop.hbase.ClusterId; import org.apache.hadoop.hbase.ClusterId;
import org.apache.hadoop.hbase.ClusterStatus; import org.apache.hadoop.hbase.ClusterStatus;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.HBaseIOException; import org.apache.hadoop.hbase.HBaseIOException;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.constraint.ConstraintException;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.HColumnDescriptor; import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.HealthCheckChore; import org.apache.hadoop.hbase.HealthCheckChore;
import org.apache.hadoop.hbase.MasterNotRunningException; import org.apache.hadoop.hbase.MasterNotRunningException;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.NamespaceNotFoundException;
import org.apache.hadoop.hbase.PleaseHoldException; import org.apache.hadoop.hbase.PleaseHoldException;
import org.apache.hadoop.hbase.Server; import org.apache.hadoop.hbase.Server;
import org.apache.hadoop.hbase.ServerLoad; import org.apache.hadoop.hbase.ServerLoad;
import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableDescriptors; import org.apache.hadoop.hbase.TableDescriptors;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.TableNotDisabledException; import org.apache.hadoop.hbase.TableNotDisabledException;
import org.apache.hadoop.hbase.TableNotFoundException; import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.UnknownRegionException; import org.apache.hadoop.hbase.UnknownRegionException;
@ -218,8 +217,11 @@ import org.apache.hadoop.metrics.util.MBeanUtil;
import org.apache.hadoop.net.DNS; import org.apache.hadoop.net.DNS;
import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import com.google.protobuf.RpcCallback; import com.google.protobuf.RpcCallback;
@ -366,7 +368,7 @@ MasterServices, Server {
/** The health check chore. */ /** The health check chore. */
private HealthCheckChore healthCheckChore; private HealthCheckChore healthCheckChore;
/** /**
* is in distributedLogReplay mode. When true, SplitLogWorker directly replays WAL edits to newly * is in distributedLogReplay mode. When true, SplitLogWorker directly replays WAL edits to newly
* assigned region servers instead of creating recovered.edits files. * assigned region servers instead of creating recovered.edits files.
@ -489,7 +491,7 @@ MasterServices, Server {
} }
} }
distributedLogReplay = this.conf.getBoolean(HConstants.DISTRIBUTED_LOG_REPLAY_KEY, distributedLogReplay = this.conf.getBoolean(HConstants.DISTRIBUTED_LOG_REPLAY_KEY,
HConstants.DEFAULT_DISTRIBUTED_LOG_REPLAY_CONFIG); HConstants.DEFAULT_DISTRIBUTED_LOG_REPLAY_CONFIG);
} }
@ -902,7 +904,7 @@ MasterServices, Server {
status.setStatus("Starting namespace manager"); status.setStatus("Starting namespace manager");
initNamespace(); initNamespace();
} }
if (this.cpHost != null) { if (this.cpHost != null) {
try { try {
this.cpHost.preMasterInitialization(); this.cpHost.preMasterInitialization();
@ -1361,6 +1363,7 @@ MasterServices, Server {
return !isStopped(); return !isStopped();
} }
@Override
public IsMasterRunningResponse isMasterRunning(RpcController c, IsMasterRunningRequest req) public IsMasterRunningResponse isMasterRunning(RpcController c, IsMasterRunningRequest req)
throws ServiceException { throws ServiceException {
return IsMasterRunningResponse.newBuilder().setIsMasterRunning(isMasterRunning()).build(); return IsMasterRunningResponse.newBuilder().setIsMasterRunning(isMasterRunning()).build();
@ -1718,9 +1721,7 @@ MasterServices, Server {
} }
String namespace = hTableDescriptor.getTableName().getNamespaceAsString(); String namespace = hTableDescriptor.getTableName().getNamespaceAsString();
if (getNamespaceDescriptor(namespace) == null) { getNamespaceDescriptor(namespace); // ensure namespace exists
throw new ConstraintException("Namespace " + namespace + " does not exist");
}
HRegionInfo[] newRegions = getHRegionInfos(hTableDescriptor, splitKeys); HRegionInfo[] newRegions = getHRegionInfos(hTableDescriptor, splitKeys);
checkInitialized(); checkInitialized();
@ -2101,6 +2102,7 @@ MasterServices, Server {
} }
} }
Collections.sort(backupMasters, new Comparator<ServerName>() { Collections.sort(backupMasters, new Comparator<ServerName>() {
@Override
public int compare(ServerName s1, ServerName s2) { public int compare(ServerName s1, ServerName s2) {
return s1.getServerName().compareTo(s2.getServerName()); return s1.getServerName().compareTo(s2.getServerName());
}}); }});
@ -2208,6 +2210,7 @@ MasterServices, Server {
this.zooKeeper.reconnectAfterExpiration(); this.zooKeeper.reconnectAfterExpiration();
Callable<Boolean> callable = new Callable<Boolean> () { Callable<Boolean> callable = new Callable<Boolean> () {
@Override
public Boolean call() throws InterruptedException, public Boolean call() throws InterruptedException,
IOException, KeeperException { IOException, KeeperException {
MonitoredTask status = MonitoredTask status =
@ -2383,6 +2386,7 @@ MasterServices, Server {
return this.stopped; return this.stopped;
} }
@Override
public boolean isAborted() { public boolean isAborted() {
return this.abort; return this.abort;
} }
@ -2414,6 +2418,7 @@ MasterServices, Server {
* *
* @return true if master is ready to go, false if not. * @return true if master is ready to go, false if not.
*/ */
@Override
public boolean isInitialized() { public boolean isInitialized() {
return initialized; return initialized;
} }
@ -2423,6 +2428,7 @@ MasterServices, Server {
* assignMeta to prevent processing of ServerShutdownHandler. * assignMeta to prevent processing of ServerShutdownHandler.
* @return true if assignMeta has completed; * @return true if assignMeta has completed;
*/ */
@Override
public boolean isServerShutdownHandlerEnabled() { public boolean isServerShutdownHandlerEnabled() {
return this.serverShutdownHandlerEnabled; return this.serverShutdownHandlerEnabled;
} }
@ -2521,6 +2527,7 @@ MasterServices, Server {
* @return GetTableDescriptorsResponse * @return GetTableDescriptorsResponse
* @throws ServiceException * @throws ServiceException
*/ */
@Override
public GetTableDescriptorsResponse getTableDescriptors( public GetTableDescriptorsResponse getTableDescriptors(
RpcController controller, GetTableDescriptorsRequest req) throws ServiceException { RpcController controller, GetTableDescriptorsRequest req) throws ServiceException {
List<HTableDescriptor> descriptors = new ArrayList<HTableDescriptor>(); List<HTableDescriptor> descriptors = new ArrayList<HTableDescriptor>();
@ -2589,6 +2596,7 @@ MasterServices, Server {
* @return GetTableNamesResponse * @return GetTableNamesResponse
* @throws ServiceException * @throws ServiceException
*/ */
@Override
public GetTableNamesResponse getTableNames( public GetTableNamesResponse getTableNames(
RpcController controller, GetTableNamesRequest req) throws ServiceException { RpcController controller, GetTableNamesRequest req) throws ServiceException {
try { try {
@ -3030,6 +3038,7 @@ MasterServices, Server {
return org.apache.commons.lang.StringUtils.isNotBlank(healthScriptLocation); return org.apache.commons.lang.StringUtils.isNotBlank(healthScriptLocation);
} }
@Override
public void createNamespace(NamespaceDescriptor descriptor) throws IOException { public void createNamespace(NamespaceDescriptor descriptor) throws IOException {
TableName.isLegalNamespaceName(Bytes.toBytes(descriptor.getName())); TableName.isLegalNamespaceName(Bytes.toBytes(descriptor.getName()));
if (cpHost != null) { if (cpHost != null) {
@ -3044,6 +3053,7 @@ MasterServices, Server {
} }
} }
@Override
public void modifyNamespace(NamespaceDescriptor descriptor) throws IOException { public void modifyNamespace(NamespaceDescriptor descriptor) throws IOException {
TableName.isLegalNamespaceName(Bytes.toBytes(descriptor.getName())); TableName.isLegalNamespaceName(Bytes.toBytes(descriptor.getName()));
if (cpHost != null) { if (cpHost != null) {
@ -3058,6 +3068,7 @@ MasterServices, Server {
} }
} }
@Override
public void deleteNamespace(String name) throws IOException { public void deleteNamespace(String name) throws IOException {
if (cpHost != null) { if (cpHost != null) {
if (cpHost.preDeleteNamespace(name)) { if (cpHost.preDeleteNamespace(name)) {
@ -3071,19 +3082,29 @@ MasterServices, Server {
} }
} }
@Override
public NamespaceDescriptor getNamespaceDescriptor(String name) throws IOException { public NamespaceDescriptor getNamespaceDescriptor(String name) throws IOException {
return tableNamespaceManager.get(name); NamespaceDescriptor nsd = tableNamespaceManager.get(name);
if (nsd == null) {
throw new NamespaceNotFoundException(name);
}
return nsd;
} }
@Override
public List<NamespaceDescriptor> listNamespaceDescriptors() throws IOException { public List<NamespaceDescriptor> listNamespaceDescriptors() throws IOException {
return Lists.newArrayList(tableNamespaceManager.list()); return Lists.newArrayList(tableNamespaceManager.list());
} }
@Override
public List<HTableDescriptor> listTableDescriptorsByNamespace(String name) throws IOException { public List<HTableDescriptor> listTableDescriptorsByNamespace(String name) throws IOException {
getNamespaceDescriptor(name); // check that namespace exists
return Lists.newArrayList(tableDescriptors.getByNamespace(name).values()); return Lists.newArrayList(tableDescriptors.getByNamespace(name).values());
} }
@Override
public List<TableName> listTableNamesByNamespace(String name) throws IOException { public List<TableName> listTableNamesByNamespace(String name) throws IOException {
getNamespaceDescriptor(name); // check that namespace exists
List<TableName> tableNames = Lists.newArrayList(); List<TableName> tableNames = Lists.newArrayList();
for (HTableDescriptor descriptor: tableDescriptors.getByNamespace(name).values()) { for (HTableDescriptor descriptor: tableDescriptors.getByNamespace(name).values()) {
tableNames.add(descriptor.getTableName()); tableNames.add(descriptor.getTableName());

View File

@ -33,6 +33,8 @@ import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionInfo; import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.NamespaceDescriptor; import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.NamespaceExistException;
import org.apache.hadoop.hbase.NamespaceNotFoundException;
import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.ZKNamespaceManager; import org.apache.hadoop.hbase.ZKNamespaceManager;
import org.apache.hadoop.hbase.catalog.MetaReader; import org.apache.hadoop.hbase.catalog.MetaReader;
@ -113,14 +115,14 @@ public class TableNamespaceManager {
nsTable = new HTable(conf, TableName.NAMESPACE_TABLE_NAME); nsTable = new HTable(conf, TableName.NAMESPACE_TABLE_NAME);
zkNamespaceManager = new ZKNamespaceManager(masterServices.getZooKeeper()); zkNamespaceManager = new ZKNamespaceManager(masterServices.getZooKeeper());
zkNamespaceManager.start(); zkNamespaceManager.start();
if (get(nsTable, NamespaceDescriptor.DEFAULT_NAMESPACE.getName()) == null) { if (get(nsTable, NamespaceDescriptor.DEFAULT_NAMESPACE.getName()) == null) {
create(nsTable, NamespaceDescriptor.DEFAULT_NAMESPACE); create(nsTable, NamespaceDescriptor.DEFAULT_NAMESPACE);
} }
if (get(nsTable, NamespaceDescriptor.SYSTEM_NAMESPACE.getName()) == null) { if (get(nsTable, NamespaceDescriptor.SYSTEM_NAMESPACE.getName()) == null) {
create(nsTable, NamespaceDescriptor.SYSTEM_NAMESPACE); create(nsTable, NamespaceDescriptor.SYSTEM_NAMESPACE);
} }
ResultScanner scanner = nsTable.getScanner(HTableDescriptor.NAMESPACE_FAMILY_INFO_BYTES); ResultScanner scanner = nsTable.getScanner(HTableDescriptor.NAMESPACE_FAMILY_INFO_BYTES);
try { try {
for(Result result : scanner) { for(Result result : scanner) {
@ -160,7 +162,7 @@ public class TableNamespaceManager {
public synchronized void update(NamespaceDescriptor ns) throws IOException { public synchronized void update(NamespaceDescriptor ns) throws IOException {
HTable table = getNamespaceTable(); HTable table = getNamespaceTable();
if (get(table, ns.getName()) == null) { if (get(table, ns.getName()) == null) {
throw new ConstraintException("Namespace "+ns.getName()+" does not exist"); throw new NamespaceNotFoundException(ns.getName());
} }
upsert(table, ns); upsert(table, ns);
} }
@ -179,7 +181,7 @@ public class TableNamespaceManager {
private void create(HTable table, NamespaceDescriptor ns) throws IOException { private void create(HTable table, NamespaceDescriptor ns) throws IOException {
if (get(table, ns.getName()) != null) { if (get(table, ns.getName()) != null) {
throw new ConstraintException("Namespace "+ns.getName()+" already exists"); throw new NamespaceExistException(ns.getName());
} }
FileSystem fs = masterServices.getMasterFileSystem().getFileSystem(); FileSystem fs = masterServices.getMasterFileSystem().getFileSystem();
fs.mkdirs(FSUtils.getNamespaceDir( fs.mkdirs(FSUtils.getNamespaceDir(
@ -203,6 +205,9 @@ public class TableNamespaceManager {
} }
public synchronized void remove(String name) throws IOException { public synchronized void remove(String name) throws IOException {
if (get(name) == null) {
throw new NamespaceNotFoundException(name);
}
if (NamespaceDescriptor.RESERVED_NAMESPACES.contains(name)) { if (NamespaceDescriptor.RESERVED_NAMESPACES.contains(name)) {
throw new ConstraintException("Reserved namespace "+name+" cannot be removed."); throw new ConstraintException("Reserved namespace "+name+" cannot be removed.");
} }
@ -210,7 +215,7 @@ public class TableNamespaceManager {
try { try {
tableCount = masterServices.listTableDescriptorsByNamespace(name).size(); tableCount = masterServices.listTableDescriptorsByNamespace(name).size();
} catch (FileNotFoundException fnfe) { } catch (FileNotFoundException fnfe) {
throw new ConstraintException("namespace " + name + " does not exist"); throw new NamespaceNotFoundException(name);
} }
if (tableCount > 0) { if (tableCount > 0) {
throw new ConstraintException("Only empty namespaces can be removed. " + throw new ConstraintException("Only empty namespaces can be removed. " +

View File

@ -18,7 +18,17 @@
*/ */
package org.apache.hadoop.hbase; package org.apache.hadoop.hbase;
import com.google.common.collect.Sets; import static org.junit.Assert.assertEquals;
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 static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.Callable;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FileSystem;
@ -33,7 +43,9 @@ import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.FSUtils; import org.apache.hadoop.hbase.util.FSUtils;
import org.apache.hadoop.hbase.zookeeper.ZKUtil; import org.apache.hadoop.hbase.zookeeper.ZKUtil;
import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher; import org.apache.hadoop.hbase.zookeeper.ZooKeeperWatcher;
import com.google.common.collect.Sets;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Ignore; import org.junit.Ignore;
@ -117,8 +129,8 @@ public class TestNamespace {
} }
//verify system tables aren't listed //verify system tables aren't listed
assertEquals(0, admin.listTables().length); assertEquals(0, admin.listTables().length);
//Try creating default and system namespaces. //Try creating default and system namespaces.
boolean exceptionCaught = false; boolean exceptionCaught = false;
try { try {
admin.createNamespace(NamespaceDescriptor.DEFAULT_NAMESPACE); admin.createNamespace(NamespaceDescriptor.DEFAULT_NAMESPACE);
@ -139,7 +151,7 @@ public class TestNamespace {
assertTrue(exceptionCaught); assertTrue(exceptionCaught);
} }
} }
@Test @Test
public void testDeleteReservedNS() throws Exception { public void testDeleteReservedNS() throws Exception {
boolean exceptionCaught = false; boolean exceptionCaught = false;
@ -192,7 +204,7 @@ public class TestNamespace {
LOG.info(testName); LOG.info(testName);
byte[] tableName = Bytes.toBytes("my_table"); byte[] tableName = Bytes.toBytes("my_table");
byte[] tableNameFoo = Bytes.toBytes(nsName+".my_table"); byte[] tableNameFoo = Bytes.toBytes(nsName+":my_table");
//create namespace and verify //create namespace and verify
admin.createNamespace(NamespaceDescriptor.create(nsName).build()); admin.createNamespace(NamespaceDescriptor.create(nsName).build());
TEST_UTIL.createTable(tableName, Bytes.toBytes(nsName)); TEST_UTIL.createTable(tableName, Bytes.toBytes(nsName));
@ -219,8 +231,8 @@ public class TestNamespace {
desc.addFamily(colDesc); desc.addFamily(colDesc);
try { try {
admin.createTable(desc); admin.createTable(desc);
fail("Expected no namespace constraint exception"); fail("Expected no namespace exists exception");
} catch (ConstraintException ex) { } catch (NamespaceNotFoundException ex) {
} }
//create table and in new namespace //create table and in new namespace
admin.createNamespace(NamespaceDescriptor.create(nsName).build()); admin.createNamespace(NamespaceDescriptor.create(nsName).build());
@ -262,7 +274,7 @@ public class TestNamespace {
HColumnDescriptor colDesc = new HColumnDescriptor("cf1"); HColumnDescriptor colDesc = new HColumnDescriptor("cf1");
desc.addFamily(colDesc); desc.addFamily(colDesc);
admin.createTable(desc); admin.createTable(desc);
assertTrue(admin.listTables().length == 1); assertTrue(admin.listTables().length == 1);
admin.disableTable(desc.getTableName()); admin.disableTable(desc.getTableName());
admin.deleteTable(desc.getTableName()); admin.deleteTable(desc.getTableName());
} }
@ -307,4 +319,109 @@ public class TestNamespace {
ZooKeeperWatcher.namespaceZNode).size()); ZooKeeperWatcher.namespaceZNode).size());
} }
@Test(timeout = 60000)
public void testNamespaceOperations() throws IOException {
admin.createNamespace(NamespaceDescriptor.create(prefix + "ns1").build());
admin.createNamespace(NamespaceDescriptor.create(prefix + "ns2").build());
// create namespace that already exists
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
admin.createNamespace(NamespaceDescriptor.create(prefix + "ns1").build());
return null;
}
}, NamespaceExistException.class);
// create a table in non-existing namespace
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
HTableDescriptor htd = new HTableDescriptor(TableName.valueOf("non_existing_namespace", "table1"));
htd.addFamily(new HColumnDescriptor("family1"));
admin.createTable(htd);
return null;
}
}, NamespaceNotFoundException.class);
// get descriptor for existing namespace
admin.getNamespaceDescriptor(prefix + "ns1");
// get descriptor for non-existing namespace
runWithExpectedException(new Callable<NamespaceDescriptor>() {
@Override
public NamespaceDescriptor call() throws Exception {
return admin.getNamespaceDescriptor("non_existing_namespace");
}
}, NamespaceNotFoundException.class);
// delete descriptor for existing namespace
admin.deleteNamespace(prefix + "ns2");
// delete descriptor for non-existing namespace
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
admin.deleteNamespace("non_existing_namespace");
return null;
}
}, NamespaceNotFoundException.class);
// modify namespace descriptor for existing namespace
NamespaceDescriptor ns1 = admin.getNamespaceDescriptor(prefix + "ns1");
ns1.setConfiguration("foo", "bar");
admin.modifyNamespace(ns1);
// modify namespace descriptor for non-existing namespace
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
admin.modifyNamespace(NamespaceDescriptor.create("non_existing_namespace").build());
return null;
}
}, NamespaceNotFoundException.class);
// get table descriptors for existing namespace
HTableDescriptor htd = new HTableDescriptor(TableName.valueOf(prefix + "ns1", "table1"));
htd.addFamily(new HColumnDescriptor("family1"));
admin.createTable(htd);
HTableDescriptor[] htds = admin.listTableDescriptorsByNamespace(prefix + "ns1");
assertNotNull("Should have not returned null", htds);
assertEquals("Should have returned non-empty array", 1, htds.length);
// get table descriptors for non-existing namespace
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
admin.listTableDescriptorsByNamespace("non_existing_namespace");
return null;
}
}, NamespaceNotFoundException.class);
// get table names for existing namespace
TableName[] tableNames = admin.listTableNamesByNamespace(prefix + "ns1");
assertNotNull("Should have not returned null", tableNames);
assertEquals("Should have returned non-empty array", 1, tableNames.length);
// get table names for non-existing namespace
runWithExpectedException(new Callable<Void>() {
@Override
public Void call() throws Exception {
admin.listTableNamesByNamespace("non_existing_namespace");
return null;
}
}, NamespaceNotFoundException.class);
}
private static <V, E> void runWithExpectedException(Callable<V> callable, Class<E> exceptionClass) {
try {
callable.call();
} catch(Exception ex) {
Assert.assertEquals(exceptionClass, ex.getClass());
return;
}
fail("Should have thrown exception " + exceptionClass);
}
} }