View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   * http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.coprocessor;
21  
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.Comparator;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  import java.util.TreeSet;
30  import java.util.UUID;
31  import java.util.concurrent.ConcurrentSkipListSet;
32  import java.util.concurrent.ExecutorService;
33  import java.util.concurrent.atomic.AtomicInteger;
34  
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.hadoop.hbase.classification.InterfaceAudience;
38  import org.apache.hadoop.hbase.classification.InterfaceStability;
39  import org.apache.hadoop.conf.Configuration;
40  import org.apache.hadoop.fs.Path;
41  import org.apache.hadoop.hbase.Abortable;
42  import org.apache.hadoop.hbase.Coprocessor;
43  import org.apache.hadoop.hbase.CoprocessorEnvironment;
44  import org.apache.hadoop.hbase.DoNotRetryIOException;
45  import org.apache.hadoop.hbase.HBaseInterfaceAudience;
46  import org.apache.hadoop.hbase.HConstants;
47  import org.apache.hadoop.hbase.TableName;
48  import org.apache.hadoop.hbase.client.HTable;
49  import org.apache.hadoop.hbase.client.HTableWrapper;
50  import org.apache.hadoop.hbase.client.Table;
51  import org.apache.hadoop.hbase.util.CoprocessorClassLoader;
52  import org.apache.hadoop.hbase.util.SortedList;
53  import org.apache.hadoop.hbase.util.VersionInfo;
54
55  /**
56   * Provides the common setup framework and runtime services for coprocessor
57   * invocation from HBase services.
58   * @param <E> the specific environment extension that a concrete implementation
59   * provides
60   */
61  @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.COPROC)
62  @InterfaceStability.Evolving
63  public abstract class CoprocessorHost<E extends CoprocessorEnvironment> {
64    public static final String REGION_COPROCESSOR_CONF_KEY =
65        "hbase.coprocessor.region.classes";
66    public static final String REGIONSERVER_COPROCESSOR_CONF_KEY =
67        "hbase.coprocessor.regionserver.classes";
68    public static final String USER_REGION_COPROCESSOR_CONF_KEY =
69        "hbase.coprocessor.user.region.classes";
70    public static final String MASTER_COPROCESSOR_CONF_KEY =
71        "hbase.coprocessor.master.classes";
72    public static final String WAL_COPROCESSOR_CONF_KEY =
73      "hbase.coprocessor.wal.classes";
74    public static final String ABORT_ON_ERROR_KEY = "hbase.coprocessor.abortonerror";
75    public static final boolean DEFAULT_ABORT_ON_ERROR = true;
76    public static final String COPROCESSORS_ENABLED_CONF_KEY = "hbase.coprocessor.enabled";
77    public static final boolean DEFAULT_COPROCESSORS_ENABLED = true;
78    public static final String USER_COPROCESSORS_ENABLED_CONF_KEY =
79      "hbase.coprocessor.user.enabled";
80    public static final boolean DEFAULT_USER_COPROCESSORS_ENABLED = true;
81
82    private static final Log LOG = LogFactory.getLog(CoprocessorHost.class);
83    protected Abortable abortable;
84    /** Ordered set of loaded coprocessors with lock */
85    protected SortedList<E> coprocessors =
86        new SortedList<E>(new EnvironmentPriorityComparator());
87    protected Configuration conf;
88    // unique file prefix to use for local copies of jars when classloading
89    protected String pathPrefix;
90    protected AtomicInteger loadSequence = new AtomicInteger();
91
92    public CoprocessorHost(Abortable abortable) {
93      this.abortable = abortable;
94      this.pathPrefix = UUID.randomUUID().toString();
95    }
96
97    /**
98     * Not to be confused with the per-object _coprocessors_ (above),
99     * coprocessorNames is static and stores the set of all coprocessors ever
100    * loaded by any thread in this JVM. It is strictly additive: coprocessors are
101    * added to coprocessorNames, by loadInstance() but are never removed, since
102    * the intention is to preserve a history of all loaded coprocessors for
103    * diagnosis in case of server crash (HBASE-4014).
104    */
105   private static Set<String> coprocessorNames =
106       Collections.synchronizedSet(new HashSet<String>());
107
108   public static Set<String> getLoadedCoprocessors() {
109     synchronized (coprocessorNames) {
110       return new HashSet(coprocessorNames);
111     }
112   }
113
114   /**
115    * Used to create a parameter to the HServerLoad constructor so that
116    * HServerLoad can provide information about the coprocessors loaded by this
117    * regionserver.
118    * (HBASE-4070: Improve region server metrics to report loaded coprocessors
119    * to master).
120    */
121   public Set<String> getCoprocessors() {
122     Set<String> returnValue = new TreeSet<String>();
123     for (CoprocessorEnvironment e: coprocessors) {
124       returnValue.add(e.getInstance().getClass().getSimpleName());
125     }
126     return returnValue;
127   }
128
129   /**
130    * Load system coprocessors once only. Read the class names from configuration.
131    * Called by constructor.
132    */
133   protected void loadSystemCoprocessors(Configuration conf, String confKey) {
134     boolean coprocessorsEnabled = conf.getBoolean(COPROCESSORS_ENABLED_CONF_KEY,
135       DEFAULT_COPROCESSORS_ENABLED);
136     if (!coprocessorsEnabled) {
137       return;
138     }
139
140     Class<?> implClass = null;
141
142     // load default coprocessors from configure file
143     String[] defaultCPClasses = conf.getStrings(confKey);
144     if (defaultCPClasses == null || defaultCPClasses.length == 0)
145       return;
146
147     int priority = Coprocessor.PRIORITY_SYSTEM;
148     for (String className : defaultCPClasses) {
149       className = className.trim();
150       if (findCoprocessor(className) != null) {
151         // If already loaded will just continue
152         LOG.warn("Attempted duplicate loading of " + className + "; skipped");
153         continue;
154       }
155       ClassLoader cl = this.getClass().getClassLoader();
156       Thread.currentThread().setContextClassLoader(cl);
157       try {
158         implClass = cl.loadClass(className);
159         // Add coprocessors as we go to guard against case where a coprocessor is specified twice
160         // in the configuration
161         this.coprocessors.add(loadInstance(implClass, priority, conf));
162         LOG.info("System coprocessor " + className + " was loaded " +
163             "successfully with priority (" + priority + ").");
164         ++priority;
165       } catch (Throwable t) {
166         // We always abort if system coprocessors cannot be loaded
167         abortServer(className, t);
168       }
169     }
170   }
171
172   /**
173    * Load a coprocessor implementation into the host
174    * @param path path to implementation jar
175    * @param className the main class name
176    * @param priority chaining priority
177    * @param conf configuration for coprocessor
178    * @throws java.io.IOException Exception
179    */
180   public E load(Path path, String className, int priority,
181       Configuration conf) throws IOException {
182     String[] includedClassPrefixes = null;
183     if (conf.get(HConstants.CP_HTD_ATTR_INCLUSION_KEY) != null){
184       String prefixes = conf.get(HConstants.CP_HTD_ATTR_INCLUSION_KEY);
185       includedClassPrefixes = prefixes.split(";");
186     }
187     return load(path, className, priority, conf, includedClassPrefixes);
188   }
189
190   /**
191    * Load a coprocessor implementation into the host
192    * @param path path to implementation jar
193    * @param className the main class name
194    * @param priority chaining priority
195    * @param conf configuration for coprocessor
196    * @param includedClassPrefixes class name prefixes to include
197    * @throws java.io.IOException Exception
198    */
199   public E load(Path path, String className, int priority,
200       Configuration conf, String[] includedClassPrefixes) throws IOException {
201     Class<?> implClass = null;
202     LOG.debug("Loading coprocessor class " + className + " with path " +
203         path + " and priority " + priority);
204
205     ClassLoader cl = null;
206     if (path == null) {
207       try {
208         implClass = getClass().getClassLoader().loadClass(className);
209       } catch (ClassNotFoundException e) {
210         throw new IOException("No jar path specified for " + className);
211       }
212     } else {
213       cl = CoprocessorClassLoader.getClassLoader(
214         path, getClass().getClassLoader(), pathPrefix, conf);
215       try {
216         implClass = ((CoprocessorClassLoader)cl).loadClass(className, includedClassPrefixes);
217       } catch (ClassNotFoundException e) {
218         throw new IOException("Cannot load external coprocessor class " + className, e);
219       }
220     }
221
222     //load custom code for coprocessor
223     Thread currentThread = Thread.currentThread();
224     ClassLoader hostClassLoader = currentThread.getContextClassLoader();
225     try{
226       // switch temporarily to the thread classloader for custom CP
227       currentThread.setContextClassLoader(cl);
228       E cpInstance = loadInstance(implClass, priority, conf);
229       return cpInstance;
230     } finally {
231       // restore the fresh (host) classloader
232       currentThread.setContextClassLoader(hostClassLoader);
233     }
234   }
235
236   /**
237    * @param implClass Implementation class
238    * @param priority priority
239    * @param conf configuration
240    * @throws java.io.IOException Exception
241    */
242   public void load(Class<?> implClass, int priority, Configuration conf)
243       throws IOException {
244     E env = loadInstance(implClass, priority, conf);
245     coprocessors.add(env);
246   }
247
248   /**
249    * @param implClass Implementation class
250    * @param priority priority
251    * @param conf configuration
252    * @throws java.io.IOException Exception
253    */
254   public E loadInstance(Class<?> implClass, int priority, Configuration conf)
255       throws IOException {
256     if (!Coprocessor.class.isAssignableFrom(implClass)) {
257       throw new IOException("Configured class " + implClass.getName() + " must implement "
258           + Coprocessor.class.getName() + " interface ");
259     }
260
261     // create the instance
262     Coprocessor impl;
263     Object o = null;
264     try {
265       o = implClass.newInstance();
266       impl = (Coprocessor)o;
267     } catch (InstantiationException e) {
268       throw new IOException(e);
269     } catch (IllegalAccessException e) {
270       throw new IOException(e);
271     }
272     // create the environment
273     E env = createEnvironment(implClass, impl, priority, loadSequence.incrementAndGet(), conf);
274     if (env instanceof Environment) {
275       ((Environment)env).startup();
276     }
277     // HBASE-4014: maintain list of loaded coprocessors for later crash analysis
278     // if server (master or regionserver) aborts.
279     coprocessorNames.add(implClass.getName());
280     return env;
281   }
282
283   /**
284    * Called when a new Coprocessor class is loaded
285    */
286   public abstract E createEnvironment(Class<?> implClass, Coprocessor instance,
287       int priority, int sequence, Configuration conf);
288
289   public void shutdown(CoprocessorEnvironment e) {
290     if (e instanceof Environment) {
291       if (LOG.isDebugEnabled()) {
292         LOG.debug("Stop coprocessor " + e.getInstance().getClass().getName());
293       }
294       ((Environment)e).shutdown();
295     } else {
296       LOG.warn("Shutdown called on unknown environment: "+
297           e.getClass().getName());
298     }
299   }
300
301   /**
302    * Find a coprocessor implementation by class name
303    * @param className the class name
304    * @return the coprocessor, or null if not found
305    */
306   public Coprocessor findCoprocessor(String className) {
307     for (E env: coprocessors) {
308       if (env.getInstance().getClass().getName().equals(className) ||
309           env.getInstance().getClass().getSimpleName().equals(className)) {
310         return env.getInstance();
311       }
312     }
313     return null;
314   }
315
316   /**
317    * Find list of coprocessors that extend/implement the given class/interface
318    * @param cls the class/interface to look for
319    * @return the list of coprocessors, or null if not found
320    */
321   public <T extends Coprocessor> List<T> findCoprocessors(Class<T> cls) {
322     ArrayList<T> ret = new ArrayList<T>();
323
324     for (E env: coprocessors) {
325       Coprocessor cp = env.getInstance();
326
327       if(cp != null) {
328         if (cls.isAssignableFrom(cp.getClass())) {
329           ret.add((T)cp);
330         }
331       }
332     }
333     return ret;
334   }
335
336   /**
337    * Find list of CoprocessorEnvironment that extend/implement the given class/interface
338    * @param cls the class/interface to look for
339    * @return the list of CoprocessorEnvironment, or null if not found
340    */
341   public List<CoprocessorEnvironment> findCoprocessorEnvironment(Class<?> cls) {
342     ArrayList<CoprocessorEnvironment> ret = new ArrayList<CoprocessorEnvironment>();
343
344     for (E env: coprocessors) {
345       Coprocessor cp = env.getInstance();
346
347       if(cp != null) {
348         if (cls.isAssignableFrom(cp.getClass())) {
349           ret.add(env);
350         }
351       }
352     }
353     return ret;
354   }
355
356   /**
357    * Find a coprocessor environment by class name
358    * @param className the class name
359    * @return the coprocessor, or null if not found
360    */
361   public CoprocessorEnvironment findCoprocessorEnvironment(String className) {
362     for (E env: coprocessors) {
363       if (env.getInstance().getClass().getName().equals(className) ||
364           env.getInstance().getClass().getSimpleName().equals(className)) {
365         return env;
366       }
367     }
368     return null;
369   }
370
371   /**
372    * Retrieves the set of classloaders used to instantiate Coprocessor classes defined in external
373    * jar files.
374    * @return A set of ClassLoader instances
375    */
376   Set<ClassLoader> getExternalClassLoaders() {
377     Set<ClassLoader> externalClassLoaders = new HashSet<ClassLoader>();
378     final ClassLoader systemClassLoader = this.getClass().getClassLoader();
379     for (E env : coprocessors) {
380       ClassLoader cl = env.getInstance().getClass().getClassLoader();
381       if (cl != systemClassLoader){
382         //do not include system classloader
383         externalClassLoaders.add(cl);
384       }
385     }
386     return externalClassLoaders;
387   }
388
389   /**
390    * Environment priority comparator.
391    * Coprocessors are chained in sorted order.
392    */
393   static class EnvironmentPriorityComparator
394       implements Comparator<CoprocessorEnvironment> {
395     @Override
396     public int compare(final CoprocessorEnvironment env1,
397         final CoprocessorEnvironment env2) {
398       if (env1.getPriority() < env2.getPriority()) {
399         return -1;
400       } else if (env1.getPriority() > env2.getPriority()) {
401         return 1;
402       }
403       if (env1.getLoadSequence() < env2.getLoadSequence()) {
404         return -1;
405       } else if (env1.getLoadSequence() > env2.getLoadSequence()) {
406         return 1;
407       }
408       return 0;
409     }
410   }
411
412   /**
413    * Encapsulation of the environment of each coprocessor
414    */
415   public static class Environment implements CoprocessorEnvironment {
416
417     /** The coprocessor */
418     public Coprocessor impl;
419     /** Chaining priority */
420     protected int priority = Coprocessor.PRIORITY_USER;
421     /** Current coprocessor state */
422     Coprocessor.State state = Coprocessor.State.UNINSTALLED;
423     /** Accounting for tables opened by the coprocessor */
424     protected List<Table> openTables =
425       Collections.synchronizedList(new ArrayList<Table>());
426     private int seq;
427     private Configuration conf;
428     private ClassLoader classLoader;
429
430     /**
431      * Constructor
432      * @param impl the coprocessor instance
433      * @param priority chaining priority
434      */
435     public Environment(final Coprocessor impl, final int priority,
436         final int seq, final Configuration conf) {
437       this.impl = impl;
438       this.classLoader = impl.getClass().getClassLoader();
439       this.priority = priority;
440       this.state = Coprocessor.State.INSTALLED;
441       this.seq = seq;
442       this.conf = conf;
443     }
444
445     /** Initialize the environment */
446     public void startup() throws IOException {
447       if (state == Coprocessor.State.INSTALLED ||
448           state == Coprocessor.State.STOPPED) {
449         state = Coprocessor.State.STARTING;
450         Thread currentThread = Thread.currentThread();
451         ClassLoader hostClassLoader = currentThread.getContextClassLoader();
452         try {
453           currentThread.setContextClassLoader(this.getClassLoader());
454           impl.start(this);
455           state = Coprocessor.State.ACTIVE;
456         } finally {
457           currentThread.setContextClassLoader(hostClassLoader);
458         }
459       } else {
460         LOG.warn("Not starting coprocessor "+impl.getClass().getName()+
461             " because not inactive (state="+state.toString()+")");
462       }
463     }
464
465     /** Clean up the environment */
466     protected void shutdown() {
467       if (state == Coprocessor.State.ACTIVE) {
468         state = Coprocessor.State.STOPPING;
469         Thread currentThread = Thread.currentThread();
470         ClassLoader hostClassLoader = currentThread.getContextClassLoader();
471         try {
472           currentThread.setContextClassLoader(this.getClassLoader());
473           impl.stop(this);
474           state = Coprocessor.State.STOPPED;
475         } catch (IOException ioe) {
476           LOG.error("Error stopping coprocessor "+impl.getClass().getName(), ioe);
477         } finally {
478           currentThread.setContextClassLoader(hostClassLoader);
479         }
480       } else {
481         LOG.warn("Not stopping coprocessor "+impl.getClass().getName()+
482             " because not active (state="+state.toString()+")");
483       }
484       synchronized (openTables) {
485         // clean up any table references
486         for (Table table: openTables) {
487           try {
488             ((HTableWrapper)table).internalClose();
489           } catch (IOException e) {
490             // nothing can be done here
491             LOG.warn("Failed to close " +
492                 table.getName(), e);
493           }
494         }
495       }
496     }
497
498     @Override
499     public Coprocessor getInstance() {
500       return impl;
501     }
502
503     @Override
504     public ClassLoader getClassLoader() {
505       return classLoader;
506     }
507
508     @Override
509     public int getPriority() {
510       return priority;
511     }
512
513     @Override
514     public int getLoadSequence() {
515       return seq;
516     }
517
518     /** @return the coprocessor environment version */
519     @Override
520     public int getVersion() {
521       return Coprocessor.VERSION;
522     }
523
524     /** @return the HBase release */
525     @Override
526     public String getHBaseVersion() {
527       return VersionInfo.getVersion();
528     }
529
530     @Override
531     public Configuration getConfiguration() {
532       return conf;
533     }
534
535     /**
536      * Open a table from within the Coprocessor environment
537      * @param tableName the table name
538      * @return an interface for manipulating the table
539      * @exception java.io.IOException Exception
540      */
541     @Override
542     public Table getTable(TableName tableName) throws IOException {
543       return this.getTable(tableName, null);
544     }
545
546     /**
547      * Open a table from within the Coprocessor environment
548      * @param tableName the table name
549      * @return an interface for manipulating the table
550      * @exception java.io.IOException Exception
551      */
552     @Override
553     public Table getTable(TableName tableName, ExecutorService pool) throws IOException {
554       return HTableWrapper.createWrapper(openTables, tableName, this, pool);
555     }
556   }
557
558   protected void abortServer(final CoprocessorEnvironment environment, final Throwable e) {
559     abortServer(environment.getInstance().getClass().getName(), e);
560   }
561
562   protected void abortServer(final String coprocessorName, final Throwable e) {
563     String message = "The coprocessor " + coprocessorName + " threw " + e.toString();
564     LOG.error(message, e);
565     if (abortable != null) {
566       abortable.abort(message, e);
567     } else {
568       LOG.warn("No available Abortable, process was not aborted");
569     }
570   }
571
572   /**
573    * This is used by coprocessor hooks which are declared to throw IOException
574    * (or its subtypes). For such hooks, we should handle throwable objects
575    * depending on the Throwable's type. Those which are instances of
576    * IOException should be passed on to the client. This is in conformance with
577    * the HBase idiom regarding IOException: that it represents a circumstance
578    * that should be passed along to the client for its own handling. For
579    * example, a coprocessor that implements access controls would throw a
580    * subclass of IOException, such as AccessDeniedException, in its preGet()
581    * method to prevent an unauthorized client's performing a Get on a particular
582    * table.
583    * @param env Coprocessor Environment
584    * @param e Throwable object thrown by coprocessor.
585    * @exception IOException Exception
586    */
587   protected void handleCoprocessorThrowable(final CoprocessorEnvironment env, final Throwable e)
588       throws IOException {
589     if (e instanceof IOException) {
590       throw (IOException)e;
591     }
592     // If we got here, e is not an IOException. A loaded coprocessor has a
593     // fatal bug, and the server (master or regionserver) should remove the
594     // faulty coprocessor from its set of active coprocessors. Setting
595     // 'hbase.coprocessor.abortonerror' to true will cause abortServer(),
596     // which may be useful in development and testing environments where
597     // 'failing fast' for error analysis is desired.
598     if (env.getConfiguration().getBoolean(ABORT_ON_ERROR_KEY, DEFAULT_ABORT_ON_ERROR)) {
599       // server is configured to abort.
600       abortServer(env, e);
601     } else {
602       LOG.error("Removing coprocessor '" + env.toString() + "' from " +
603           "environment because it threw:  " + e,e);
604       coprocessors.remove(env);
605       try {
606         shutdown(env);
607       } catch (Exception x) {
608         LOG.error("Uncaught exception when shutting down coprocessor '"
609             + env.toString() + "'", x);
610       }
611       throw new DoNotRetryIOException("Coprocessor: '" + env.toString() +
612           "' threw: '" + e + "' and has been removed from the active " +
613           "coprocessor set.", e);
614     }
615   }
616
617   /**
618    * Used to gracefully handle fallback to deprecated methods when we
619    * evolve coprocessor APIs.
620    *
621    * When a particular Coprocessor API is updated to change methods, hosts can support fallback
622    * to the deprecated API by using this method to determine if an instance implements the new API.
623    * In the event that said support is partial, then in the face of a runtime issue that prevents
624    * proper operation {@link #legacyWarning(Class, String)} should be used to let operators know.
625    *
626    * For examples of this in action, see the implementation of
627    * <ul>
628    *   <li>{@link org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost}
629    *   <li>{@link org.apache.hadoop.hbase.regionserver.wal.WALCoprocessorHost}
630    * </ul>
631    *
632    * @param clazz Coprocessor you wish to evaluate
633    * @param methodName the name of the non-deprecated method version
634    * @param parameterTypes the Class of the non-deprecated method's arguments in the order they are
635    *     declared.
636    */
637   @InterfaceAudience.Private
638   protected static boolean useLegacyMethod(final Class<? extends Coprocessor> clazz,
639       final String methodName, final Class<?>... parameterTypes) {
640     boolean useLegacy;
641     // Use reflection to see if they implement the non-deprecated version
642     try {
643       clazz.getDeclaredMethod(methodName, parameterTypes);
644       LOG.debug("Found an implementation of '" + methodName + "' that uses updated method " +
645           "signature. Skipping legacy support for invocations in '" + clazz +"'.");
646       useLegacy = false;
647     } catch (NoSuchMethodException exception) {
648       useLegacy = true;
649     } catch (SecurityException exception) {
650       LOG.warn("The Security Manager denied our attempt to detect if the coprocessor '" + clazz +
651           "' requires legacy support; assuming it does. If you get later errors about legacy " +
652           "coprocessor use, consider updating your security policy to allow access to the package" +
653           " and declared members of your implementation.");
654       LOG.debug("Details of Security Manager rejection.", exception);
655       useLegacy = true;
656     }
657     return useLegacy;
658   }
659
660   /**
661    * Used to limit legacy handling to once per Coprocessor class per classloader.
662    */
663   private static final Set<Class<? extends Coprocessor>> legacyWarning =
664       new ConcurrentSkipListSet<Class<? extends Coprocessor>>(
665           new Comparator<Class<? extends Coprocessor>>() {
666             @Override
667             public int compare(Class<? extends Coprocessor> c1, Class<? extends Coprocessor> c2) {
668               if (c1.equals(c2)) {
669                 return 0;
670               }
671               return c1.getName().compareTo(c2.getName());
672             }
673           });
674
675   /**
676    * limits the amount of logging to once per coprocessor class.
677    * Used in concert with {@link #useLegacyMethod(Class, String, Class[])} when a runtime issue
678    * prevents properly supporting the legacy version of a coprocessor API.
679    * Since coprocessors can be in tight loops this serves to limit the amount of log spam we create.
680    */
681   @InterfaceAudience.Private
682   protected void legacyWarning(final Class<? extends Coprocessor> clazz, final String message) {
683     if(legacyWarning.add(clazz)) {
684       LOG.error("You have a legacy coprocessor loaded and there are events we can't map to the " +
685           " deprecated API. Your coprocessor will not see these events.  Please update '" + clazz +
686           "'. Details of the problem: " + message);
687     }
688   }
689 }