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