View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.constraint;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.DataOutputStream;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Comparator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Map.Entry;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.hbase.classification.InterfaceAudience;
35  import org.apache.hadoop.conf.Configuration;
36  import org.apache.hadoop.hbase.HTableDescriptor;
37  import org.apache.hadoop.hbase.util.Bytes;
38  import org.apache.hadoop.hbase.util.Pair;
39  
40  /**
41   * Utilities for adding/removing constraints from a table.
42   * <p>
43   * Constraints can be added on table load time, via the {@link HTableDescriptor}.
44   * <p>
45   * NOTE: this class is NOT thread safe. Concurrent setting/enabling/disabling of
46   * constraints can cause constraints to be run at incorrect times or not at all.
47   */
48  @InterfaceAudience.Private
49  public final class Constraints {
50    private static final int DEFAULT_PRIORITY = -1;
51  
52    private Constraints() {
53    }
54  
55    private static final Log LOG = LogFactory.getLog(Constraints.class);
56    private static final String CONSTRAINT_HTD_KEY_PREFIX = "constraint $";
57    private static final Pattern CONSTRAINT_HTD_ATTR_KEY_PATTERN = Pattern
58        .compile(CONSTRAINT_HTD_KEY_PREFIX, Pattern.LITERAL);
59  
60    // configuration key for if the constraint is enabled
61    private static final String ENABLED_KEY = "_ENABLED";
62    // configuration key for the priority
63    private static final String PRIORITY_KEY = "_PRIORITY";
64  
65    // smallest priority a constraiNt can have
66    private static final long MIN_PRIORITY = 0L;
67    // ensure a priority less than the smallest we could intentionally set
68    private static final long UNSET_PRIORITY = MIN_PRIORITY - 1;
69  
70    private static String COUNTER_KEY = "hbase.constraint.counter";
71  
72    /**
73     * Enable constraints on a table.
74     * <p>
75     * Currently, if you attempt to add a constraint to the table, then
76     * Constraints will automatically be turned on.
77     * 
78     * @param desc
79     *          table description to add the processor
80     * @throws IOException
81     *           If the {@link ConstraintProcessor} CP couldn't be added to the
82     *           table.
83     */
84    public static void enable(HTableDescriptor desc) throws IOException {
85      // if the CP has already been loaded, do nothing
86      String clazz = ConstraintProcessor.class.getName();
87      if (desc.hasCoprocessor(clazz)) {
88        return;
89      }
90  
91      // add the constrain processor CP to the table
92      desc.addCoprocessor(clazz);
93    }
94  
95    /**
96     * Turn off processing constraints for a given table, even if constraints have
97     * been turned on or added.
98     * 
99     * @param desc
100    *          {@link HTableDescriptor} where to disable {@link Constraint
101    *          Constraints}.
102    */
103   public static void disable(HTableDescriptor desc) {
104     desc.removeCoprocessor(ConstraintProcessor.class.getName());
105   }
106 
107   /**
108    * Remove all {@link Constraint Constraints} that have been added to the table
109    * and turn off the constraint processing.
110    * <p>
111    * All {@link Configuration Configurations} and their associated
112    * {@link Constraint} are removed.
113    * 
114    * @param desc
115    *          {@link HTableDescriptor} to remove {@link Constraint Constraints}
116    *          from.
117    */
118   public static void remove(HTableDescriptor desc) {
119     // disable constraints
120     disable(desc);
121 
122     // remove all the constraint settings
123     List<Bytes> keys = new ArrayList<Bytes>();
124     // loop through all the key, values looking for constraints
125     for (Map.Entry<Bytes, Bytes> e : desc
126         .getValues().entrySet()) {
127       String key = Bytes.toString((e.getKey().get()));
128       String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
129       if (className.length == 2) {
130         keys.add(e.getKey());
131       }
132     }
133     // now remove all the keys we found
134     for (Bytes key : keys) {
135       desc.remove(key);
136     }
137   }
138 
139   /**
140    * Check to see if the Constraint is currently set.
141    * 
142    * @param desc
143    *          {@link HTableDescriptor} to check
144    * @param clazz
145    *          {@link Constraint} class to check for.
146    * @return <tt>true</tt> if the {@link Constraint} is present, even if it is
147    *         disabled. <tt>false</tt> otherwise.
148    */
149   public static boolean has(HTableDescriptor desc,
150       Class<? extends Constraint> clazz) {
151     return getKeyValueForClass(desc, clazz) != null;
152   }
153 
154   /**
155    * Get the kv {@link Entry} in the descriptor for the specified class
156    * 
157    * @param desc {@link HTableDescriptor} to read
158    * @param clazz To search for
159    * @return The {@link Pair} of {@literal <key, value>} in the table, if that class is
160    *         present. {@code NULL} otherwise.
161    */
162   private static Pair<String, String> getKeyValueForClass(
163       HTableDescriptor desc, Class<? extends Constraint> clazz) {
164     // get the serialized version of the constraint
165     String key = serializeConstraintClass(clazz);
166     String value = desc.getValue(key);
167 
168     return value == null ? null : new Pair<String, String>(key, value);
169   }
170 
171   /**
172    * Add configuration-less constraints to the table.
173    * <p>
174    * This will overwrite any configuration associated with the previous
175    * constraint of the same class.
176    * <p>
177    * Each constraint, when added to the table, will have a specific priority,
178    * dictating the order in which the {@link Constraint} will be run. A
179    * {@link Constraint} earlier in the list will be run before those later in
180    * the list. The same logic applies between two Constraints over time (earlier
181    * added is run first on the regionserver).
182    * 
183    * @param desc
184    *          {@link HTableDescriptor} to add {@link Constraint Constraints}
185    * @param constraints
186    *          {@link Constraint Constraints} to add. All constraints are
187    *          considered automatically enabled on add
188    * @throws IOException
189    *           If constraint could not be serialized/added to table
190    */
191   public static void add(HTableDescriptor desc,
192       Class<? extends Constraint>... constraints) throws IOException {
193     // make sure constraints are enabled
194     enable(desc);
195     long priority = getNextPriority(desc);
196 
197     // store each constraint
198     for (Class<? extends Constraint> clazz : constraints) {
199       addConstraint(desc, clazz, null, priority++);
200     }
201     updateLatestPriority(desc, priority);
202   }
203 
204   /**
205    * Add constraints and their associated configurations to the table.
206    * <p>
207    * Adding the same constraint class twice will overwrite the first
208    * constraint's configuration
209    * <p>
210    * Each constraint, when added to the table, will have a specific priority,
211    * dictating the order in which the {@link Constraint} will be run. A
212    * {@link Constraint} earlier in the list will be run before those later in
213    * the list. The same logic applies between two Constraints over time (earlier
214    * added is run first on the regionserver).
215    * 
216    * @param desc
217    *          {@link HTableDescriptor} to add a {@link Constraint}
218    * @param constraints
219    *          {@link Pair} of a {@link Constraint} and its associated
220    *          {@link Configuration}. The Constraint will be configured on load
221    *          with the specified configuration.All constraints are considered
222    *          automatically enabled on add
223    * @throws IOException
224    *           if any constraint could not be deserialized. Assumes if 1
225    *           constraint is not loaded properly, something has gone terribly
226    *           wrong and that all constraints need to be enforced.
227    */
228   public static void add(HTableDescriptor desc,
229       Pair<Class<? extends Constraint>, Configuration>... constraints)
230       throws IOException {
231     enable(desc);
232     long priority = getNextPriority(desc);
233     for (Pair<Class<? extends Constraint>, Configuration> pair : constraints) {
234       addConstraint(desc, pair.getFirst(), pair.getSecond(), priority++);
235     }
236     updateLatestPriority(desc, priority);
237   }
238 
239   /**
240    * Add a {@link Constraint} to the table with the given configuration
241    * <p>
242    * Each constraint, when added to the table, will have a specific priority,
243    * dictating the order in which the {@link Constraint} will be run. A
244    * {@link Constraint} added will run on the regionserver before those added to
245    * the {@link HTableDescriptor} later.
246    * 
247    * @param desc
248    *          table descriptor to the constraint to
249    * @param constraint
250    *          to be added
251    * @param conf
252    *          configuration associated with the constraint
253    * @throws IOException
254    *           if any constraint could not be deserialized. Assumes if 1
255    *           constraint is not loaded properly, something has gone terribly
256    *           wrong and that all constraints need to be enforced.
257    */
258   public static void add(HTableDescriptor desc,
259       Class<? extends Constraint> constraint, Configuration conf)
260       throws IOException {
261     enable(desc);
262     long priority = getNextPriority(desc);
263     addConstraint(desc, constraint, conf, priority++);
264 
265     updateLatestPriority(desc, priority);
266   }
267 
268   /**
269    * Write the raw constraint and configuration to the descriptor.
270    * <p>
271    * This method takes care of creating a new configuration based on the passed
272    * in configuration and then updating that with enabled and priority of the
273    * constraint.
274    * <p>
275    * When a constraint is added, it is automatically enabled.
276    */
277   private static void addConstraint(HTableDescriptor desc,
278       Class<? extends Constraint> clazz, Configuration conf, long priority)
279       throws IOException {
280     writeConstraint(desc, serializeConstraintClass(clazz),
281         configure(conf, true, priority));
282   }
283 
284   /**
285    * Setup the configuration for a constraint as to whether it is enabled and
286    * its priority
287    * 
288    * @param conf
289    *          on which to base the new configuration
290    * @param enabled
291    *          <tt>true</tt> if it should be run
292    * @param priority
293    *          relative to other constraints
294    * @return a new configuration, storable in the {@link HTableDescriptor}
295    */
296   private static Configuration configure(Configuration conf, boolean enabled,
297       long priority) {
298     // create the configuration to actually be stored
299     // clone if possible, but otherwise just create an empty configuration
300     Configuration toWrite = conf == null ? new Configuration()
301         : new Configuration(conf);
302 
303     // update internal properties
304     toWrite.setBooleanIfUnset(ENABLED_KEY, enabled);
305 
306     // set if unset long
307     if (toWrite.getLong(PRIORITY_KEY, UNSET_PRIORITY) == UNSET_PRIORITY) {
308       toWrite.setLong(PRIORITY_KEY, priority);
309     }
310 
311     return toWrite;
312   }
313 
314   /**
315    * Just write the class to a String representation of the class as a key for
316    * the {@link HTableDescriptor}
317    * 
318    * @param clazz
319    *          Constraint class to convert to a {@link HTableDescriptor} key
320    * @return key to store in the {@link HTableDescriptor}
321    */
322   private static String serializeConstraintClass(
323       Class<? extends Constraint> clazz) {
324     String constraintClazz = clazz.getName();
325     return CONSTRAINT_HTD_KEY_PREFIX + constraintClazz;
326   }
327 
328   /**
329    * Write the given key and associated configuration to the
330    * {@link HTableDescriptor}
331    */
332   private static void writeConstraint(HTableDescriptor desc, String key,
333       Configuration conf) throws IOException {
334     // store the key and conf in the descriptor
335     desc.setValue(key, serializeConfiguration(conf));
336   }
337 
338   /**
339    * Write the configuration to a String
340    * 
341    * @param conf
342    *          to write
343    * @return String representation of that configuration
344    * @throws IOException
345    */
346   private static String serializeConfiguration(Configuration conf)
347       throws IOException {
348     // write the configuration out to the data stream
349     ByteArrayOutputStream bos = new ByteArrayOutputStream();
350     DataOutputStream dos = new DataOutputStream(bos);
351     conf.writeXml(dos);
352     dos.flush();
353     byte[] data = bos.toByteArray();
354     return Bytes.toString(data);
355   }
356 
357   /**
358    * Read the {@link Configuration} stored in the byte stream.
359    * 
360    * @param bytes
361    *          to read from
362    * @return A valid configuration
363    */
364   private static Configuration readConfiguration(byte[] bytes)
365       throws IOException {
366     ByteArrayInputStream is = new ByteArrayInputStream(bytes);
367     Configuration conf = new Configuration(false);
368     conf.addResource(is);
369     return conf;
370   }
371 
372   /**
373    * Read in the configuration from the String encoded configuration
374    * 
375    * @param bytes
376    *          to read from
377    * @return A valid configuration
378    * @throws IOException
379    *           if the configuration could not be read
380    */
381   private static Configuration readConfiguration(String bytes)
382       throws IOException {
383     return readConfiguration(Bytes.toBytes(bytes));
384   }
385 
386   private static long getNextPriority(HTableDescriptor desc) {
387     String value = desc.getValue(COUNTER_KEY);
388 
389     long priority;
390     // get the current priority
391     if (value == null) {
392       priority = MIN_PRIORITY;
393     } else {
394       priority = Long.parseLong(value) + 1;
395     }
396 
397     return priority;
398   }
399 
400   private static void updateLatestPriority(HTableDescriptor desc, long priority) {
401     // update the max priority
402     desc.setValue(COUNTER_KEY, Long.toString(priority));
403   }
404 
405   /**
406    * Update the configuration for the {@link Constraint}; does not change the
407    * order in which the constraint is run.
408    * 
409    * @param desc
410    *          {@link HTableDescriptor} to update
411    * @param clazz
412    *          {@link Constraint} to update
413    * @param configuration
414    *          to update the {@link Constraint} with.
415    * @throws IOException
416    *           if the Constraint was not stored correctly
417    * @throws IllegalArgumentException
418    *           if the Constraint was not present on this table.
419    */
420   public static void setConfiguration(HTableDescriptor desc,
421       Class<? extends Constraint> clazz, Configuration configuration)
422       throws IOException, IllegalArgumentException {
423     // get the entry for this class
424     Pair<String, String> e = getKeyValueForClass(desc, clazz);
425 
426     if (e == null) {
427       throw new IllegalArgumentException("Constraint: " + clazz.getName()
428           + " is not associated with this table.");
429     }
430 
431     // clone over the configuration elements
432     Configuration conf = new Configuration(configuration);
433 
434     // read in the previous info about the constraint
435     Configuration internal = readConfiguration(e.getSecond());
436 
437     // update the fields based on the previous settings
438     conf.setIfUnset(ENABLED_KEY, internal.get(ENABLED_KEY));
439     conf.setIfUnset(PRIORITY_KEY, internal.get(PRIORITY_KEY));
440 
441     // update the current value
442     writeConstraint(desc, e.getFirst(), conf);
443   }
444 
445   /**
446    * Remove the constraint (and associated information) for the table
447    * descriptor.
448    * 
449    * @param desc
450    *          {@link HTableDescriptor} to modify
451    * @param clazz
452    *          {@link Constraint} class to remove
453    */
454   public static void remove(HTableDescriptor desc,
455       Class<? extends Constraint> clazz) {
456     String key = serializeConstraintClass(clazz);
457     desc.remove(key);
458   }
459 
460   /**
461    * Enable the given {@link Constraint}. Retains all the information (e.g.
462    * Configuration) for the {@link Constraint}, but makes sure that it gets
463    * loaded on the table.
464    * 
465    * @param desc
466    *          {@link HTableDescriptor} to modify
467    * @param clazz
468    *          {@link Constraint} to enable
469    * @throws IOException
470    *           If the constraint cannot be properly deserialized
471    */
472   public static void enableConstraint(HTableDescriptor desc,
473       Class<? extends Constraint> clazz) throws IOException {
474     changeConstraintEnabled(desc, clazz, true);
475   }
476 
477   /**
478    * Disable the given {@link Constraint}. Retains all the information (e.g.
479    * Configuration) for the {@link Constraint}, but it just doesn't load the
480    * {@link Constraint} on the table.
481    * 
482    * @param desc
483    *          {@link HTableDescriptor} to modify
484    * @param clazz
485    *          {@link Constraint} to disable.
486    * @throws IOException
487    *           if the constraint cannot be found
488    */
489   public static void disableConstraint(HTableDescriptor desc,
490       Class<? extends Constraint> clazz) throws IOException {
491     changeConstraintEnabled(desc, clazz, false);
492   }
493 
494   /**
495    * Change the whether the constraint (if it is already present) is enabled or
496    * disabled.
497    */
498   private static void changeConstraintEnabled(HTableDescriptor desc,
499       Class<? extends Constraint> clazz, boolean enabled) throws IOException {
500     // get the original constraint
501     Pair<String, String> entry = getKeyValueForClass(desc, clazz);
502     if (entry == null) {
503       throw new IllegalArgumentException("Constraint: " + clazz.getName()
504           + " is not associated with this table. You can't enable it!");
505     }
506 
507     // create a new configuration from that conf
508     Configuration conf = readConfiguration(entry.getSecond());
509 
510     // set that it is enabled
511     conf.setBoolean(ENABLED_KEY, enabled);
512 
513     // write it back out
514     writeConstraint(desc, entry.getFirst(), conf);
515   }
516 
517   /**
518    * Check to see if the given constraint is enabled.
519    * 
520    * @param desc
521    *          {@link HTableDescriptor} to check.
522    * @param clazz
523    *          {@link Constraint} to check for
524    * @return <tt>true</tt> if the {@link Constraint} is present and enabled.
525    *         <tt>false</tt> otherwise.
526    * @throws IOException
527    *           If the constraint has improperly stored in the table
528    */
529   public static boolean enabled(HTableDescriptor desc,
530       Class<? extends Constraint> clazz) throws IOException {
531     // get the kv
532     Pair<String, String> entry = getKeyValueForClass(desc, clazz);
533     // its not enabled so just return false. In fact, its not even present!
534     if (entry == null) {
535       return false;
536     }
537 
538     // get the info about the constraint
539     Configuration conf = readConfiguration(entry.getSecond());
540 
541     return conf.getBoolean(ENABLED_KEY, false);
542   }
543 
544   /**
545    * Get the constraints stored in the table descriptor
546    * 
547    * @param desc
548    *          To read from
549    * @param classloader
550    *          To use when loading classes. If a special classloader is used on a
551    *          region, for instance, then that should be the classloader used to
552    *          load the constraints. This could also apply to unit-testing
553    *          situation, where want to ensure that class is reloaded or not.
554    * @return List of configured {@link Constraint Constraints}
555    * @throws IOException
556    *           if any part of reading/arguments fails
557    */
558   static List<? extends Constraint> getConstraints(HTableDescriptor desc,
559       ClassLoader classloader) throws IOException {
560     List<Constraint> constraints = new ArrayList<Constraint>();
561     // loop through all the key, values looking for constraints
562     for (Map.Entry<Bytes, Bytes> e : desc
563         .getValues().entrySet()) {
564       // read out the constraint
565       String key = Bytes.toString(e.getKey().get()).trim();
566       String[] className = CONSTRAINT_HTD_ATTR_KEY_PATTERN.split(key);
567       if (className.length == 2) {
568         key = className[1];
569         if (LOG.isDebugEnabled()) {
570           LOG.debug("Loading constraint:" + key);
571         }
572 
573         // read in the rest of the constraint
574         Configuration conf;
575         try {
576           conf = readConfiguration(e.getValue().get());
577         } catch (IOException e1) {
578           // long that we don't have a valid configuration stored, and move on.
579           LOG.warn("Corrupted configuration found for key:" + key
580               + ",  skipping it.");
581           continue;
582         }
583         // if it is not enabled, skip it
584         if (!conf.getBoolean(ENABLED_KEY, false)) {
585           if (LOG.isDebugEnabled())
586             LOG.debug("Constraint: " + key + " is DISABLED - skipping it");
587           // go to the next constraint
588           continue;
589         }
590 
591         try {
592           // add the constraint, now that we expect it to be valid.
593           Class<? extends Constraint> clazz = classloader.loadClass(key)
594               .asSubclass(Constraint.class);
595           Constraint constraint = clazz.newInstance();
596           constraint.setConf(conf);
597           constraints.add(constraint);
598         } catch (ClassNotFoundException e1) {
599           throw new IOException(e1);
600         } catch (InstantiationException e1) {
601           throw new IOException(e1);
602         } catch (IllegalAccessException e1) {
603           throw new IOException(e1);
604         }
605       }
606     }
607     // sort them, based on the priorities
608     Collections.sort(constraints, constraintComparator);
609     return constraints;
610   }
611 
612   private static final Comparator<Constraint> constraintComparator = new Comparator<Constraint>() {
613     @Override
614     public int compare(Constraint c1, Constraint c2) {
615       // compare the priorities of the constraints stored in their configuration
616       return Long.valueOf(c1.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY))
617           .compareTo(c2.getConf().getLong(PRIORITY_KEY, DEFAULT_PRIORITY));
618     }
619   };
620 
621 }