/** * 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.constraint; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.HTableDescriptor; import org.apache.hadoop.hbase.io.ImmutableBytesWritable; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Pair; /** * Utilities for adding/removing constraints from a table. * <p> * Constraints can be added on table load time, via the {@link HTableDescriptor}. * <p> * NOTE: this class is NOT thread safe. Concurrent setting/enabling/disabling of * constraints can cause constraints to be run at incorrect times or not at all. */ @InterfaceAudience.Private public final class Constraints { private static final int DEFAULT_PRIORITY = -1; private Constraints() { } private static final Log LOG = LogFactory.getLog(Constraints.class); private static final String CONSTRAINT_HTD_KEY_PREFIX = "constraint $"; private static final Pattern CONSTRAINT_HTD_ATTR_KEY_PATTERN = Pattern .compile(CONSTRAINT_HTD_KEY_PREFIX, Pattern.LITERAL); // configuration key for if the constraint is enabled private static final String ENABLED_KEY = "_ENABLED"; // configuration key for the priority private static final String PRIORITY_KEY = "_PRIORITY"; // smallest priority a constraiNt can have private static final long MIN_PRIORITY = 0L; // ensure a priority less than the smallest we could intentionally set private static final long UNSET_PRIORITY = MIN_PRIORITY - 1; private static String COUNTER_KEY = "hbase.constraint.counter"; /** * Enable constraints on a table. * <p> * Currently, if you attempt to add a constraint to the table, then * Constraints will automatically be turned on. * * @param desc * table description to add the processor * @throws IOException * If the {@link ConstraintProcessor} CP couldn't be added to the * table. */ public static void enable(HTableDescriptor desc) throws IOException { // if the CP has already been loaded, do nothing String clazz = ConstraintProcessor.class.getName(); if (desc.hasCoprocessor(clazz)) { return; } // add the constrain processor CP to the table desc.addCoprocessor(clazz); } /** * Turn off processing constraints for a given table, even if constraints have * been turned on or added. * * @param desc * {@link HTableDescriptor} where to disable {@link Constraint * Constraints}. */ public static void disable(HTableDescriptor desc) { desc.removeCoprocessor(ConstraintProcessor.class.getName()); } /** * Remove all {@link Constraint Constraints} that have been added to the table * and turn off the constraint processing. * <p> * All {@link Configuration Configurations} and their associated * {@link Constraint} are removed. * * @param desc * {@link HTableDescriptor} to remove {@link Constraint Constraints} * from. */ public static void remove(HTableDescriptor desc) { // disable constraints disable(desc); // remove all the constraint settings List<ImmutableBytesWritable> keys = new ArrayList<ImmutableBytesWritable>(); // loop through all the key, values looking for constraints for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e : desc .getValues().entrySet()) { String key = Bytes.toString((e.getKey().get())); String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key); if (className.length == 2) { keys.add(e.getKey()); } } // now remove all the keys we found for (ImmutableBytesWritable key : keys) { desc.remove(key.get()); } } /** * Check to see if the Constraint is currently set. * * @param desc * {@link HTableDescriptor} to check * @param clazz * {@link Constraint} class to check for. * @return <tt>true</tt> if the {@link Constraint} is present, even if it is * disabled. <tt>false</tt> otherwise. */ public static boolean has(HTableDescriptor desc, Class<? extends Constraint> clazz) { return getKeyValueForClass(desc, clazz) != null; } /** * Get the kv {@link Entry} in the descriptor for the specified class * * @param desc * {@link HTableDescriptor} to read * @param clazz * to search for * @return the {@link Pair} of <key, value> in the table, if that class is * present. <tt>null</tt> otherwise. */ private static Pair<String, String> getKeyValueForClass( HTableDescriptor desc, Class<? extends Constraint> clazz) { // get the serialized version of the constraint String key = serializeConstraintClass(clazz); String value = desc.getValue(key); return value == null ? null : new Pair<String, String>(key, value); } /** * Add configuration-less constraints to the table. * <p> * This will overwrite any configuration associated with the previous * constraint of the same class. * <p> * Each constraint, when added to the table, will have a specific priority, * dictating the order in which the {@link Constraint} will be run. A * {@link Constraint} earlier in the list will be run before those later in * the list. The same logic applies between two Constraints over time (earlier * added is run first on the regionserver). * * @param desc * {@link HTableDescriptor} to add {@link Constraint Constraints} * @param constraints * {@link Constraint Constraints} to add. All constraints are * considered automatically enabled on add * @throws IOException * If constraint could not be serialized/added to table */ public static void add(HTableDescriptor desc, Class<? extends Constraint>... constraints) throws IOException { // make sure constraints are enabled enable(desc); long priority = getNextPriority(desc); // store each constraint for (Class<? extends Constraint> clazz : constraints) { addConstraint(desc, clazz, null, priority++); } updateLatestPriority(desc, priority); } /** * Add constraints and their associated configurations to the table. * <p> * Adding the same constraint class twice will overwrite the first * constraint's configuration * <p> * Each constraint, when added to the table, will have a specific priority, * dictating the order in which the {@link Constraint} will be run. A * {@link Constraint} earlier in the list will be run before those later in * the list. The same logic applies between two Constraints over time (earlier * added is run first on the regionserver). * * @param desc * {@link HTableDescriptor} to add a {@link Constraint} * @param constraints * {@link Pair} of a {@link Constraint} and its associated * {@link Configuration}. The Constraint will be configured on load * with the specified configuration.All constraints are considered * automatically enabled on add * @throws IOException * if any constraint could not be deserialized. Assumes if 1 * constraint is not loaded properly, something has gone terribly * wrong and that all constraints need to be enforced. */ public static void add(HTableDescriptor desc, Pair<Class<? extends Constraint>, Configuration>... constraints) throws IOException { enable(desc); long priority = getNextPriority(desc); for (Pair<Class<? extends Constraint>, Configuration> pair : constraints) { addConstraint(desc, pair.getFirst(), pair.getSecond(), priority++); } updateLatestPriority(desc, priority); } /** * Add a {@link Constraint} to the table with the given configuration * <p> * Each constraint, when added to the table, will have a specific priority, * dictating the order in which the {@link Constraint} will be run. A * {@link Constraint} added will run on the regionserver before those added to * the {@link HTableDescriptor} later. * * @param desc * table descriptor to the constraint to * @param constraint * to be added * @param conf * configuration associated with the constraint * @throws IOException * if any constraint could not be deserialized. Assumes if 1 * constraint is not loaded properly, something has gone terribly * wrong and that all constraints need to be enforced. */ public static void add(HTableDescriptor desc, Class<? extends Constraint> constraint, Configuration conf) throws IOException { enable(desc); long priority = getNextPriority(desc); addConstraint(desc, constraint, conf, priority++); updateLatestPriority(desc, priority); } /** * Write the raw constraint and configuration to the descriptor. * <p> * This method takes care of creating a new configuration based on the passed * in configuration and then updating that with enabled and priority of the * constraint. * <p> * When a constraint is added, it is automatically enabled. */ private static void addConstraint(HTableDescriptor desc, Class<? extends Constraint> clazz, Configuration conf, long priority) throws IOException { writeConstraint(desc, serializeConstraintClass(clazz), configure(conf, true, priority)); } /** * Setup the configuration for a constraint as to whether it is enabled and * its priority * * @param conf * on which to base the new configuration * @param enabled * <tt>true</tt> if it should be run * @param priority * relative to other constraints * @returns a new configuration, storable in the {@link HTableDescriptor} */ private static Configuration configure(Configuration conf, boolean enabled, long priority) { // create the configuration to actually be stored // clone if possible, but otherwise just create an empty configuration Configuration toWrite = conf == null ? new Configuration() : new Configuration(conf); // update internal properties toWrite.setBooleanIfUnset(ENABLED_KEY, enabled); // set if unset long if (toWrite.getLong(PRIORITY_KEY, UNSET_PRIORITY) == UNSET_PRIORITY) { toWrite.setLong(PRIORITY_KEY, priority); } return toWrite; } /** * Just write the class to a String representation of the class as a key for * the {@link HTableDescriptor} * * @param clazz * Constraint class to convert to a {@link HTableDescriptor} key * @return key to store in the {@link HTableDescriptor} */ private static String serializeConstraintClass( Class<? extends Constraint> clazz) { String constraintClazz = clazz.getName(); return CONSTRAINT_HTD_KEY_PREFIX + constraintClazz; } /** * Write the given key and associated configuration to the * {@link HTableDescriptor} */ private static void writeConstraint(HTableDescriptor desc, String key, Configuration conf) throws IOException { // store the key and conf in the descriptor desc.setValue(key, serializeConfiguration(conf)); } /** * Write the configuration to a String * * @param conf * to write * @return String representation of that configuration * @throws IOException */ private static String serializeConfiguration(Configuration conf) throws IOException { // write the configuration out to the data stream ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); conf.writeXml(dos); dos.flush(); byte[] data = bos.toByteArray(); return Bytes.toString(data); } /** * Read the {@link Configuration} stored in the byte stream. * * @param bytes * to read from * @return A valid configuration */ private static Configuration readConfiguration(byte[] bytes) throws IOException { ByteArrayInputStream is = new ByteArrayInputStream(bytes); Configuration conf = new Configuration(false); conf.addResource(is); return conf; } /** * Read in the configuration from the String encoded configuration * * @param bytes * to read from * @return A valid configuration * @throws IOException * if the configuration could not be read */ private static Configuration readConfiguration(String bytes) throws IOException { return readConfiguration(Bytes.toBytes(bytes)); } private static long getNextPriority(HTableDescriptor desc) { String value = desc.getValue(COUNTER_KEY); long priority; // get the current priority if (value == null) { priority = MIN_PRIORITY; } else { priority = Long.parseLong(value) + 1; } return priority; } private static void updateLatestPriority(HTableDescriptor desc, long priority) { // update the max priority desc.setValue(COUNTER_KEY, Long.toString(priority)); } /** * Update the configuration for the {@link Constraint}; does not change the * order in which the constraint is run. * * @param desc * {@link HTableDescriptor} to update * @param clazz * {@link Constraint} to update * @param configuration * to update the {@link Constraint} with. * @throws IOException * if the Constraint was not stored correctly * @throws IllegalArgumentException * if the Constraint was not present on this table. */ public static void setConfiguration(HTableDescriptor desc, Class<? extends Constraint> clazz, Configuration configuration) throws IOException, IllegalArgumentException { // get the entry for this class Pair<String, String> e = getKeyValueForClass(desc, clazz); if (e == null) { throw new IllegalArgumentException("Constraint: " + clazz.getName() + " is not associated with this table."); } // clone over the configuration elements Configuration conf = new Configuration(configuration); // read in the previous info about the constraint Configuration internal = readConfiguration(e.getSecond()); // update the fields based on the previous settings conf.setIfUnset(ENABLED_KEY, internal.get(ENABLED_KEY)); conf.setIfUnset(PRIORITY_KEY, internal.get(PRIORITY_KEY)); // update the current value writeConstraint(desc, e.getFirst(), conf); } /** * Remove the constraint (and associated information) for the table * descriptor. * * @param desc * {@link HTableDescriptor} to modify * @param clazz * {@link Constraint} class to remove */ public static void remove(HTableDescriptor desc, Class<? extends Constraint> clazz) { String key = serializeConstraintClass(clazz); desc.remove(key); } /** * Enable the given {@link Constraint}. Retains all the information (e.g. * Configuration) for the {@link Constraint}, but makes sure that it gets * loaded on the table. * * @param desc * {@link HTableDescriptor} to modify * @param clazz * {@link Constraint} to enable * @throws IOException * If the constraint cannot be properly deserialized */ public static void enableConstraint(HTableDescriptor desc, Class<? extends Constraint> clazz) throws IOException { changeConstraintEnabled(desc, clazz, true); } /** * Disable the given {@link Constraint}. Retains all the information (e.g. * Configuration) for the {@link Constraint}, but it just doesn't load the * {@link Constraint} on the table. * * @param desc * {@link HTableDescriptor} to modify * @param clazz * {@link Constraint} to disable. * @throws IOException * if the constraint cannot be found */ public static void disableConstraint(HTableDescriptor desc, Class<? extends Constraint> clazz) throws IOException { changeConstraintEnabled(desc, clazz, false); } /** * Change the whether the constraint (if it is already present) is enabled or * disabled. */ private static void changeConstraintEnabled(HTableDescriptor desc, Class<? extends Constraint> clazz, boolean enabled) throws IOException { // get the original constraint Pair<String, String> entry = getKeyValueForClass(desc, clazz); if (entry == null) { throw new IllegalArgumentException("Constraint: " + clazz.getName() + " is not associated with this table. You can't enable it!"); } // create a new configuration from that conf Configuration conf = readConfiguration(entry.getSecond()); // set that it is enabled conf.setBoolean(ENABLED_KEY, enabled); // write it back out writeConstraint(desc, entry.getFirst(), conf); } /** * Check to see if the given constraint is enabled. * * @param desc * {@link HTableDescriptor} to check. * @param clazz * {@link Constraint} to check for * @return <tt>true</tt> if the {@link Constraint} is present and enabled. * <tt>false</tt> otherwise. * @throws IOException * If the constraint has improperly stored in the table */ public static boolean enabled(HTableDescriptor desc, Class<? extends Constraint> clazz) throws IOException { // get the kv Pair<String, String> entry = getKeyValueForClass(desc, clazz); // its not enabled so just return false. In fact, its not even present! if (entry == null) { return false; } // get the info about the constraint Configuration conf = readConfiguration(entry.getSecond()); return conf.getBoolean(ENABLED_KEY, false); } /** * Get the constraints stored in the table descriptor * * @param desc * To read from * @param classloader * To use when loading classes. If a special classloader is used on a * region, for instance, then that should be the classloader used to * load the constraints. This could also apply to unit-testing * situation, where want to ensure that class is reloaded or not. * @return List of configured {@link Constraint Constraints} * @throws IOException * if any part of reading/arguments fails */ static List<? extends Constraint> getConstraints(HTableDescriptor desc, ClassLoader classloader) throws IOException { List<Constraint> constraints = new ArrayList<Constraint>(); // loop through all the key, values looking for constraints for (Map.Entry<ImmutableBytesWritable, ImmutableBytesWritable> e : desc .getValues().entrySet()) { // read out the constraint String key = Bytes.toString(e.getKey().get()).trim(); String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key); if (className.length == 2) { key = className[1]; if (LOG.isDebugEnabled()) { LOG.debug("Loading constraint:" + key); } // read in the rest of the constraint Configuration conf; try { conf = readConfiguration(e.getValue().get()); } catch (IOException e1) { // long that we don't have a valid configuration stored, and move on. LOG.warn("Corrupted configuration found for key:" + key + ", skipping it."); continue; } // if it is not enabled, skip it if (!conf.getBoolean(ENABLED_KEY, false)) { if (LOG.isDebugEnabled()) LOG.debug("Constraint: " + key + " is DISABLED - skipping it"); // go to the next constraint continue; } try { // add the constraint, now that we expect it to be valid. Class<? extends Constraint> clazz = classloader.loadClass(key) .asSubclass(Constraint.class); Constraint constraint = clazz.newInstance(); constraint.setConf(conf); constraints.add(constraint); } catch (ClassNotFoundException e1) { throw new IOException(e1); } catch (InstantiationException e1) { throw new IOException(e1); } catch (IllegalAccessException e1) { throw new IOException(e1); } } } // sort them, based on the priorities Collections.sort(constraints, constraintComparator); return constraints; } private static final Comparator<Constraint> constraintComparator = new Comparator<Constraint>() { @Override public int compare(Constraint c1, Constraint c2) { // compare the priorities of the constraints stored in their configuration return Long.valueOf(c1.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY)) .compareTo(c2.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY)); } }; }