/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* Licensed 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 co.cask.cdap.data2.dataset2.lib.hbase;
import co.cask.cdap.api.dataset.DatasetAdmin;
import co.cask.cdap.common.utils.ProjectInfo;
import co.cask.cdap.data2.util.TableId;
import co.cask.cdap.data2.util.hbase.HBaseTableUtil;
import co.cask.cdap.data2.util.hbase.HTableDescriptorBuilder;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.Coprocessor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.TableNotEnabledException;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.twill.filesystem.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Base class for writing HBase EntityAdmin.
*/
public abstract class AbstractHBaseDataSetAdmin implements DatasetAdmin {
/**
* This will only be set by the upgrade tool to force upgrade of all HBase coprocessors after an HBase upgrade.
*/
public static final String SYSTEM_PROPERTY_FORCE_HBASE_UPGRADE = "cdap.force.hbase.upgrade";
private static final Logger LOG = LoggerFactory.getLogger(AbstractHBaseDataSetAdmin.class);
// Property key in the coprocessor for storing version of the coprocessor.
private static final String CDAP_VERSION = "cdap.version";
// Function to convert Class into class Name
private static final Function<Class<?>, String> CLASS_TO_NAME = new Function<Class<?>, String>() {
@Override
public String apply(Class<?> input) {
return input.getName();
}
};
protected final TableId tableId;
protected final Configuration hConf;
protected final HBaseTableUtil tableUtil;
private HBaseAdmin admin;
protected AbstractHBaseDataSetAdmin(TableId tableId, Configuration hConf, HBaseTableUtil tableUtil) {
this.tableId = tableId;
this.hConf = hConf;
this.tableUtil = tableUtil;
}
@Override
public void upgrade() throws IOException {
upgradeTable(Boolean.valueOf(System.getProperty(SYSTEM_PROPERTY_FORCE_HBASE_UPGRADE)));
}
@Override
public boolean exists() throws IOException {
return tableUtil.tableExists(getAdmin(), tableId);
}
@Override
public void truncate() throws IOException {
tableUtil.truncateTable(getAdmin(), tableId);
}
@Override
public void drop() throws IOException {
tableUtil.dropTable(getAdmin(), tableId);
}
@Override
public void close() throws IOException {
if (admin != null) {
admin.close();
}
}
/**
* Performs upgrade on a given HBase table. It will be upgraded if either its spec has
* changed since the HBase table was created or upgraded, or if the CDAP version recorded
* in the HTable descriptor
*
* @param force forces upgrade regardless of whether the table needs upgrade.
* @throws IOException If upgrade failed.
*/
public void upgradeTable(boolean force) throws IOException {
HTableDescriptor tableDescriptor = tableUtil.getHTableDescriptor(getAdmin(), tableId);
// Upgrade any table properties if necessary
boolean needUpgrade = upgradeTable(tableDescriptor) || force;
// Get the cdap version from the table
ProjectInfo.Version version = getVersion(tableDescriptor);
if (!needUpgrade && version.compareTo(ProjectInfo.getVersion()) >= 0) {
// If neither the table spec nor the cdap version have changed, no need to upgrade
LOG.info("Table '{}' has not changed and its version '{}' is same or greater " +
"than current CDAP version '{}'", tableId, version, ProjectInfo.getVersion());
return;
}
// create a new descriptor for the table upgrade
HTableDescriptorBuilder newDescriptor = tableUtil.buildHTableDescriptor(tableDescriptor);
// Generate the coprocessor jar
CoprocessorJar coprocessorJar = createCoprocessorJar();
Location jarLocation = coprocessorJar.getJarLocation();
// Check if coprocessor upgrade is needed
Map<String, HBaseTableUtil.CoprocessorInfo> coprocessorInfo = HBaseTableUtil.getCoprocessorInfo(tableDescriptor);
// For all required coprocessors, check if they've need to be upgraded.
for (Class<? extends Coprocessor> coprocessor : coprocessorJar.getCoprocessors()) {
HBaseTableUtil.CoprocessorInfo info = coprocessorInfo.get(coprocessor.getName());
if (info != null) {
// The same coprocessor has been configured, check by the file name hash to see if they are the same.
if (!jarLocation.getName().equals(info.getPath().getName())) {
// Remove old one and add the new one.
newDescriptor.removeCoprocessor(info.getClassName());
addCoprocessor(newDescriptor, coprocessor, jarLocation, coprocessorJar.getPriority(coprocessor));
}
} else {
// The coprocessor is missing from the table, add it.
addCoprocessor(newDescriptor, coprocessor, jarLocation, coprocessorJar.getPriority(coprocessor));
}
}
// Removes all old coprocessors
Set<String> coprocessorNames = ImmutableSet.copyOf(Iterables.transform(coprocessorJar.coprocessors, CLASS_TO_NAME));
for (String remove : Sets.difference(coprocessorInfo.keySet(), coprocessorNames)) {
newDescriptor.removeCoprocessor(remove);
}
setVersion(newDescriptor);
LOG.info("Upgrading table '{}'...", tableId);
boolean enableTable = false;
try {
tableUtil.disableTable(getAdmin(), tableId);
enableTable = true;
} catch (TableNotEnabledException e) {
LOG.debug("Table '{}' was not enabled before upgrade and will not be enabled after upgrade.", tableId);
}
tableUtil.modifyTable(getAdmin(), newDescriptor.build());
if (enableTable) {
tableUtil.enableTable(getAdmin(), tableId);
}
LOG.info("Table '{}' upgrade completed.", tableId);
}
public static void setVersion(HTableDescriptorBuilder tableDescriptor) {
tableDescriptor.setValue(CDAP_VERSION, ProjectInfo.getVersion().toString());
}
public static ProjectInfo.Version getVersion(HTableDescriptor tableDescriptor) {
String value = tableDescriptor.getValue(CDAP_VERSION);
return new ProjectInfo.Version(value);
}
protected void addCoprocessor(HTableDescriptorBuilder tableDescriptor, Class<? extends Coprocessor> coprocessor,
Location jarFile, Integer priority) throws IOException {
if (priority == null) {
priority = Coprocessor.PRIORITY_USER;
}
tableDescriptor.addCoprocessor(coprocessor.getName(), new Path(jarFile.toURI().getPath()), priority, null);
}
protected abstract CoprocessorJar createCoprocessorJar() throws IOException;
/**
* Modifies the table descriptor for upgrade.
*
* @return true if the table descriptor is modified.
*/
protected abstract boolean upgradeTable(HTableDescriptor tableDescriptor);
/**
* Holder for coprocessor information.
*/
// todo: make protected, after CDAP-1193 is fixed
public static final class CoprocessorJar {
public static final CoprocessorJar EMPTY = new CoprocessorJar(ImmutableList.<Class<? extends Coprocessor>>of(),
null);
private final List<Class<? extends Coprocessor>> coprocessors;
private final Location jarLocation;
private final Map<Class<? extends Coprocessor>, Integer> priorities = Maps.newHashMap();
public CoprocessorJar(Iterable<? extends Class<? extends Coprocessor>> coprocessors, Location jarLocation) {
this.coprocessors = ImmutableList.copyOf(coprocessors);
// set coprocessor loading order to match iteration order
int priority = Coprocessor.PRIORITY_USER;
for (Class<? extends Coprocessor> cpClass : coprocessors) {
priorities.put(cpClass, priority++);
}
this.jarLocation = jarLocation;
}
public void setPriority(Class<? extends Coprocessor> cpClass, int priority) {
priorities.put(cpClass, priority);
}
public Integer getPriority(Class<? extends Coprocessor> cpClass) {
return priorities.get(cpClass);
}
public Iterable<? extends Class<? extends Coprocessor>> getCoprocessors() {
return coprocessors;
}
public Location getJarLocation() {
return jarLocation;
}
public boolean isEmpty() {
return coprocessors.isEmpty();
}
public int size() {
return coprocessors.size();
}
}
protected HBaseAdmin getAdmin() throws IOException {
if (admin == null) {
admin = new HBaseAdmin(hConf);
}
return admin;
}
}