diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionCheckable.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionCheckable.java new file mode 100644 index 00000000000..c3975553863 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionCheckable.java @@ -0,0 +1,44 @@ +/** + * 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.server.errorhandling; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Check for errors to a given process. + * @param Type of error that this throws if it finds an error + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public interface ExceptionCheckable { + + /** + * Checks to see if any process to which the exception checker is bound has created an error that + * would cause a failure. + * @throws E if there has been an error, allowing a fail-fast mechanism + */ + public void failOnError() throws E; + + /** + * Non-exceptional form of {@link #failOnError()}. Checks to see if any process to which the + * exception checkers is bound has created an error that would cause a failure. + * @return true if there has been an error,false otherwise + */ + public boolean checkForError(); +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionListener.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionListener.java new file mode 100644 index 00000000000..8a074db55df --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionListener.java @@ -0,0 +1,40 @@ +/** + * 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.server.errorhandling; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Listen for errors on a process or operation + * @param Type of exception that is expected + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public interface ExceptionListener { + + /** + * Receive an error. + *

+ * Implementers must ensure that this method is thread-safe. + * @param message reason for the error + * @param e exception causing the error + * @param info general information about the error + */ + public void receiveError(String message, E e, Object... info); +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionVisitor.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionVisitor.java new file mode 100644 index 00000000000..6b9218e6550 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/ExceptionVisitor.java @@ -0,0 +1,41 @@ +/** + * 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.server.errorhandling; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.impl.ExceptionOrchestrator; + +/** + * Simple visitor interface to update an error listener with an error notification + * @see ExceptionOrchestrator + * @param Type of listener to update + */ +@InterfaceAudience.Public +@InterfaceStability.Unstable +public interface ExceptionVisitor { + + /** + * Visit the listener with the given error, possibly transforming or ignoring the error + * @param listener listener to update + * @param message error message + * @param e exception that caused the error + * @param info general information about the error + */ + public void visit(T listener, String message, Exception e, Object... info); +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/FaultInjector.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/FaultInjector.java new file mode 100644 index 00000000000..01b64fd8cbf --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/FaultInjector.java @@ -0,0 +1,51 @@ +/** + * 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.server.errorhandling; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.impl.ExceptionOrchestratorFactory; +import org.apache.hadoop.hbase.util.Pair; + +/** + * Inject faults when classes check to see if an error occurs. + *

+ * Can be added to any monitoring via + * {@link ExceptionOrchestratorFactory#addFaultInjector(FaultInjector)} + * @see ExceptionListener + * @see ExceptionCheckable + * @param Type of exception that the corresponding {@link ExceptionListener} is expecting + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public interface FaultInjector { + + /** + * Called by the specified class whenever checking for process errors. Care needs to be taken when + * using fault injectors to pass the correct size array back or the received error in the listener + * could not receive the correct number of argument and throw an error. + *

+ * Note that every time the fault injector is called it does not necessarily need to inject a + * fault, but only when the fault is desired. + * @param trace full stack trace of the call to check for an error + * @return the information about the fault that should be returned if there was a fault (expected + * exception to throw and generic error information) or null if no fault should + * be injected. + */ + public Pair injectFault(StackTraceElement[] trace); +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/Name.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/Name.java new file mode 100644 index 00000000000..a8c921079e8 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/Name.java @@ -0,0 +1,52 @@ +/** + * 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.server.errorhandling; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Base class for an object with a name. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class Name { + + private String name; + + public Name(String name) { + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Get the name of the class that should be used for logging + * @return {@link String} prefix for logging + */ + public String getNamePrefixForLog() { + return name != null ? "(" + name + ")" : ""; + } + + @Override + public String toString() { + return this.name; + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/OperationAttemptTimer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/OperationAttemptTimer.java new file mode 100644 index 00000000000..0f9fb8de493 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/OperationAttemptTimer.java @@ -0,0 +1,129 @@ +/** + * 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.server.errorhandling; + +import java.util.Timer; +import java.util.TimerTask; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.exception.OperationAttemptTimeoutException; +import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; + +/** + * Time a given process/operation and report a failure if the elapsed time exceeds the max allowed + * time. + *

+ * The timer won't start tracking time until calling {@link #start()}. If {@link #complete()} or + * {@link #trigger()} is called before {@link #start()}, calls to {@link #start()} will fail. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class OperationAttemptTimer { + + private static final Log LOG = LogFactory.getLog(OperationAttemptTimer.class); + + private final long maxTime; + private volatile boolean complete; + private final Timer timer; + private final TimerTask timerTask; + private long start = -1; + + /** + * Create a generic timer for a task/process. + * @param listener listener to notify if the process times out + * @param maxTime max allowed running time for the process. Timer starts on calls to + * {@link #start()} + * @param info information about the process to pass along if the timer expires + */ + @SuppressWarnings("rawtypes") + public OperationAttemptTimer(final ExceptionListener listener, final long maxTime, + final Object... info) { + this.maxTime = maxTime; + timer = new Timer(); + timerTask = new TimerTask() { + @SuppressWarnings("unchecked") + @Override + public void run() { + // ensure we don't run this task multiple times + synchronized (this) { + // quick exit if we already marked the task complete + if (OperationAttemptTimer.this.complete) return; + // mark the task is run, to avoid repeats + OperationAttemptTimer.this.complete = true; + } + long end = EnvironmentEdgeManager.currentTimeMillis(); + listener.receiveError("Timeout elapsed!", new OperationAttemptTimeoutException(start, end, + maxTime), info); + } + }; + } + + /** + * For all time forward, do not throw an error because the process has completed. + */ + public void complete() { + // warn if the timer is already marked complete. This isn't going to be thread-safe, but should + // be good enough and its not worth locking just for a warning. + if (this.complete) { + LOG.warn("Timer already marked completed, ignoring!"); + return; + } + LOG.debug("Marking timer as complete - no error notifications will be received for this timer."); + synchronized (this.timerTask) { + this.complete = true; + } + this.timer.cancel(); + } + + /** + * Start a timer to fail a process if it takes longer than the expected time to complete. + *

+ * Non-blocking. + * @throws IllegalStateException if the timer has already been marked done via {@link #complete()} + * or {@link #trigger()} + */ + public synchronized void start() throws IllegalStateException { + if (this.start >= 0) { + LOG.warn("Timer already started, can't be started again. Ignoring second request."); + return; + } + LOG.debug("Scheduling process timer to run in: " + maxTime + " ms"); + timer.schedule(timerTask, maxTime); + this.start = EnvironmentEdgeManager.currentTimeMillis(); + } + + /** + * Trigger the timer immediately. + *

+ * Exposed for testing. + */ + public void trigger() { + synchronized (timerTask) { + if (this.complete) { + LOG.warn("Timer already completed, not triggering."); + return; + } + LOG.debug("Triggering timer immediately!"); + this.timer.cancel(); + this.timerTask.run(); + } + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/OperationAttemptTimeoutException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/OperationAttemptTimeoutException.java new file mode 100644 index 00000000000..89fe626043f --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/OperationAttemptTimeoutException.java @@ -0,0 +1,43 @@ +/** + * 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.server.errorhandling.exception; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.OperationAttemptTimer; + +/** + * Exception for a timeout of a task. + * @see OperationAttemptTimer + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +@SuppressWarnings("serial") +public class OperationAttemptTimeoutException extends Exception { + + /** + * Exception indicating that an operation attempt has timed out + * @param start time the operation started (ms since epoch) + * @param end time the timeout was triggered (ms since epoch) + * @param allowed max allow amount of time for the operation to complete (ms) + */ + public OperationAttemptTimeoutException(long start, long end, long allowed) { + super("Timeout elapsed! Start:" + start + ", End:" + end + ", diff:" + (end - start) + ", max:" + + allowed + " ms"); + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/UnknownErrorException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/UnknownErrorException.java new file mode 100644 index 00000000000..822349abfa9 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/exception/UnknownErrorException.java @@ -0,0 +1,32 @@ +/** + * 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.server.errorhandling.exception; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.impl.ExceptionSnare; + +/** + * Exception when an {@link ExceptionSnare} doens't have an Exception when it receives an + * error. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +@SuppressWarnings("serial") +public class UnknownErrorException extends RuntimeException { +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcher.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcher.java new file mode 100644 index 00000000000..f0f2a2e3022 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcher.java @@ -0,0 +1,104 @@ +/** + * 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.server.errorhandling.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.Abortable; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionCheckable; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; + +/** + * The dispatcher acts as a central point of control of error handling. Any exceptions from the + * dispatcher get passed directly to the listeners. Likewise, any errors from the listeners get + * passed to the dispatcher and then back to any listeners. + *

+ * This is useful, for instance, for informing multiple process in conjunction with an + * {@link Abortable} + *

+ * This is different than an {@link ExceptionOrchestrator} as it will only propagate an error + * once to all listeners; its single use, just like an {@link ExceptionSnare}. For example, + * if an error is passed to this then that error will be passed to all listeners, but a + * second error passed to {@link #receiveError(String, Exception, Object...)} will be ignored. This + * is particularly useful to help avoid accidentally having infinite loops when passing errors. + *

+ * @param generic exception listener type to update + * @param Type of {@link Exception} to throw when calling {@link #failOnError()} + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class ExceptionDispatcher extends ExceptionOrchestrator implements + ExceptionListener, ExceptionCheckable { + private static final Log LOG = LogFactory.getLog(ExceptionDispatcher.class); + protected final ExceptionVisitor visitor; + private final ExceptionSnare snare = new ExceptionSnare(); + + public ExceptionDispatcher(String name, ExceptionVisitor visitor) { + super(name); + this.visitor = visitor; + } + + public ExceptionDispatcher(ExceptionVisitor visitor) { + this("single-error-dispatcher", visitor); + } + + public ExceptionDispatcher() { + this(null); + } + + @Override + public synchronized void receiveError(String message, E e, Object... info) { + // if we already have an error, then ignore it + if (snare.checkForError()) return; + + LOG.debug(name.getNamePrefixForLog() + "Accepting received error:" + message); + // mark that we got the error + snare.receiveError(message, e, info); + + // notify all the listeners + super.receiveError(message, e, info); + } + + @Override + public void failOnError() throws E { + snare.failOnError(); + } + + @Override + public boolean checkForError() { + return snare.checkForError(); + } + + public ExceptionVisitor getDefaultVisitor() { + return this.visitor; + } + + /** + * Add a typed error listener that will be visited by the {@link ExceptionVisitor}, passed in the + * constructor, when receiving errors. + * @param errorable listener for error notifications + */ + public void addErrorListener(T errorable) { + if (this.visitor == null) throw new UnsupportedOperationException("No error visitor for " + + errorable + ", can't add it to the listeners"); + addErrorListener(this.visitor, errorable); + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcherFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcherFactory.java new file mode 100644 index 00000000000..8158b0055ba --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionDispatcherFactory.java @@ -0,0 +1,57 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.List; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; +import org.apache.hadoop.hbase.server.errorhandling.FaultInjector; + +/** + * Generic error dispatcher factory that just creates an error dispatcher on request (potentially + * wrapping with an error injector via the {@link ExceptionOrchestratorFactory}). + * @param Type of generic error listener the dispatchers should handle + * @see ExceptionOrchestratorFactory + */ +@InterfaceAudience.Public +@InterfaceStability.Unstable +public class ExceptionDispatcherFactory extends + ExceptionOrchestratorFactory, T> { + + /** + * @param visitor to use when building an error handler via {@link #createErrorHandler()}. + */ + public ExceptionDispatcherFactory(ExceptionVisitor visitor) { + super(visitor); + } + + @Override + protected ExceptionDispatcher buildErrorHandler(ExceptionVisitor visitor) { + return new ExceptionDispatcher(visitor); + } + + @Override + protected ExceptionDispatcher wrapWithInjector( + ExceptionDispatcher dispatcher, + List> injectors) { + return new InjectingExceptionDispatcher, T, Exception>(dispatcher, + injectors); + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestrator.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestrator.java new file mode 100644 index 00000000000..8b610af9034 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestrator.java @@ -0,0 +1,123 @@ +/** + * 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.server.errorhandling.impl; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; +import org.apache.hadoop.hbase.server.errorhandling.Name; +import org.apache.hadoop.hbase.util.Pair; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ListMultimap; + +/** + * The orchestrator acts as a central point of control of error handling. Any exceptions passed to + * this get passed directly to the listeners. + *

+ * Any exception listener added will only be weakly referenced, so you must keep a reference + * to it if you want to use it other places. This allows minimal effort error monitoring, allowing + * you to register an error listener and then not worry about having to unregister the listener. + *

+ * A single {@link ExceptionOrchestrator} should be used for each set of operation attempts (e.g. + * one parent operation with child operations, potentially multiple levels deep) to monitor. This + * allows for a single source of truth for exception dispatch between all the interested operation + * attempts. + * @param Type of {@link Exception} to expect when receiving errors + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class ExceptionOrchestrator implements ExceptionListener { + + private static final Log LOG = LogFactory.getLog(ExceptionOrchestrator.class); + protected final Name name; + + protected final ListMultimap, WeakReference> listeners = ArrayListMultimap + .create(); + + /** Error visitor for framework listeners */ + final ForwardingErrorVisitor genericVisitor = new ForwardingErrorVisitor(); + + public ExceptionOrchestrator() { + this("generic-error-dispatcher"); + } + + public ExceptionOrchestrator(String name) { + this.name = new Name(name); + } + + public Name getName() { + return this.name; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public synchronized void receiveError(String message, E e, Object... info) { + // update all the listeners with the passed error + LOG.debug(name.getNamePrefixForLog() + " Recieved error, notifying listeners..."); + List, WeakReference>> toRemove = new ArrayList, WeakReference>>(); + for (Entry, WeakReference> entry : listeners.entries()) { + Object o = entry.getValue().get(); + if (o == null) { + // if the listener doesn't have a reference, then drop it from the list + // need to copy this over b/c guava is finicky with the entries + toRemove.add(new Pair, WeakReference>(entry.getKey(), entry + .getValue())); + continue; + } + // otherwise notify the listener that we had a failure + ((ExceptionVisitor) entry.getKey()).visit(o, message, e, info); + } + + // cleanup all visitors that aren't referenced anymore + if (toRemove.size() > 0) LOG.debug(name.getNamePrefixForLog() + " Cleaning up entries."); + for (Pair, WeakReference> entry : toRemove) { + this.listeners.remove(entry.getFirst(), entry.getSecond()); + } + } + + /** + * Listen for failures to a given process + * @param visitor pass error notifications onto the typed listener, possibly transforming or + * ignore the error notification + * @param errorable listener for the errors + */ + public synchronized void addErrorListener(ExceptionVisitor visitor, L errorable) { + this.listeners.put(visitor, new WeakReference(errorable)); + } + + /** + * A simple error visitor that just forwards the received error to a generic listener. + */ + private class ForwardingErrorVisitor implements ExceptionVisitor> { + + @Override + @SuppressWarnings("unchecked") + public void visit(ExceptionListener listener, String message, Exception e, Object... info) { + listener.receiveError(message, (E) e, info); + } + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestratorFactory.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestratorFactory.java new file mode 100644 index 00000000000..d045d7be066 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionOrchestratorFactory.java @@ -0,0 +1,115 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionCheckable; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; +import org.apache.hadoop.hbase.server.errorhandling.FaultInjector; + +/** + * Error factory that produces an {@link ExceptionOrchestrator}, potentially wrapped with a + * {@link FaultInjector}. + * @param type for {@link ExceptionOrchestrator} that should be used + * @param Type of error listener that the dispatcher from this factory can communicate + */ +@InterfaceAudience.Public +@InterfaceStability.Unstable +public abstract class ExceptionOrchestratorFactory, T> { + private static final List> faults = new ArrayList>(); + + /** + * Add a fault injector that will run on checks of the {@link ExceptionCheckable} generated by + * this factory. To ensure that faults are injected, this must be called before the the handler is + * created via {@link #createErrorHandler()}. + *

+ * Exposed for TESTING. + * @param injector fault injector to add + * @param type of exception that will be thrown on checks of + * {@link ExceptionCheckable#failOnError()} from created exception monitors + */ + public static void addFaultInjector(FaultInjector injector) { + faults.add(injector); + } + + /** + * Complement to {@link #addFaultInjector(FaultInjector)} - removes any existing fault injectors + * set for the factory. + *

+ * Exposed for TESTING. + */ + public static void clearFaults() { + faults.clear(); + } + + protected final ExceptionVisitor visitor; + + /** + * @param visitor to use when building an error handler via {@link #createErrorHandler()}. + */ + public ExceptionOrchestratorFactory(ExceptionVisitor visitor) { + this.visitor = visitor; + } + + /** + * Create a dispatcher with a specific visitor + * @param visitor visitor to pass on error notifications to bound error listeners + * @return an error dispatcher that is passes on errors to all listening objects + */ + public final D createErrorHandler(ExceptionVisitor visitor) { + D handler = buildErrorHandler(visitor); + // wrap with a fault injector, if we need to + if (faults.size() > 0) { + return wrapWithInjector(handler, faults); + } + return handler; + } + + /** + * Create a dispatcher with a specific visitor. Uses the default visitor passed in the constructor + * @return an error dispatcher that is passes on errors to all listening objects + */ + public final D createErrorHandler() { + return createErrorHandler(this.visitor); + } + + /** + * Build an error handler. This will be wrapped via + * {@link #wrapWithInjector(ErrorMonitorable, List)} if there are fault injectors present. + * @return an error handler + */ + protected abstract D buildErrorHandler(ExceptionVisitor visitor); + + /** + * Wrap the built error handler with an error injector. Subclasses should override if they need + * custom error injection. Generally, this will just wrap calls to <D> by first checking the + * {@link #faults} that were dynamically injected and then, if the {@link FaultInjector} didn't + * inject a fault, that actual methods are called. + *

+ * This method will only be called if there are fault injectors present. Otherwise, the handler + * will just be built via {@link #buildErrorHandler(ExceptionVisitor)}. + * @param delegate built delegate to wrap with injector checking + * @param injectors injectors that should be checked + * @return a <D> that also does {@link FaultInjector} checking + */ + protected abstract D wrapWithInjector(D delegate, List> injectors); +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionSnare.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionSnare.java new file mode 100644 index 00000000000..334ba0070d8 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionSnare.java @@ -0,0 +1,96 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionCheckable; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.Name; +import org.apache.hadoop.hbase.server.errorhandling.exception.UnknownErrorException; + +/** + * Simple exception handler that keeps track of whether of its failure state, and the exception that + * should be thrown based on the received error. + *

+ * Ensures that an exception is not propagated if an error has already been received, ensuring that + * you don't have infinite error propagation. + *

+ * You can think of it like a 'one-time-use' {@link ExceptionCheckable}, that once it receives an + * error will not listen to any new error updates. + *

+ * Thread-safe. + * @param Type of exception to throw when calling {@link #failOnError()} + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class ExceptionSnare implements ExceptionCheckable, + ExceptionListener { + + private static final Log LOG = LogFactory.getLog(ExceptionSnare.class); + private boolean error = false; + protected E exception; + protected Name name; + + /** + * Create an exception snare with a generic error name + */ + public ExceptionSnare() { + this.name = new Name("generic-error-snare"); + } + + @Override + public void failOnError() throws E { + if (checkForError()) { + if (exception == null) throw new UnknownErrorException(); + throw exception; + } + } + + @Override + public boolean checkForError() { + return this.error; + } + + @Override + public void receiveError(String message, E e, Object... info) { + LOG.error(name.getNamePrefixForLog() + "Got an error:" + message + ", info:" + + Arrays.toString(info)); + receiveInternalError(e); + } + + /** + * Receive an error notification from internal sources. Can be used by subclasses to set an error. + *

+ * This method may be called concurrently, so precautions must be taken to not clobber yourself, + * either making the method synchronized, synchronizing on this of calling this + * method. + * @param e exception that caused the error (can be null). + */ + protected synchronized void receiveInternalError(E e) { + // if we already got the error or we received the error fail fast + if (this.error) return; + // store the error since we haven't seen it before + this.error = true; + this.exception = e; + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/InjectingExceptionDispatcher.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/InjectingExceptionDispatcher.java new file mode 100644 index 00000000000..8e40ea5a212 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/InjectingExceptionDispatcher.java @@ -0,0 +1,92 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionCheckable; +import org.apache.hadoop.hbase.server.errorhandling.FaultInjector; +import org.apache.hadoop.hbase.server.errorhandling.impl.delegate.DelegatingExceptionDispatcher; +import org.apache.hadoop.hbase.util.Pair; +import org.apache.jasper.compiler.ErrorDispatcher; + +/** + * {@link ErrorDispatcher} that delegates calls for all methods, but wraps exception checking to + * allow the fault injectors to have a chance to inject a fault into the running process + * @param {@link ExceptionOrchestrator} to wrap for fault checking + * @param type of generic error listener that should be notified + * @param exception to be thrown on checks of {@link #failOnError()} + */ +@InterfaceAudience.Private +@InterfaceStability.Unstable +public class InjectingExceptionDispatcher, T, E extends Exception> extends + DelegatingExceptionDispatcher { + + private final List> faults; + + /** + * Wrap an exception handler with one that will inject faults on calls to {@link #checkForError()} + * . + * @param delegate base exception handler to wrap + * @param faults injectors to run each time there is a check for an error + */ + @SuppressWarnings("unchecked") + public InjectingExceptionDispatcher(D delegate, List> faults) { + super(delegate); + // since we don't know the type of fault injector, we need to convert it. + // this is only used in tests, so throwing a class-cast here isn't too bad. + this.faults = new ArrayList>(faults.size()); + for (FaultInjector fault : faults) { + this.faults.add((FaultInjector) fault); + } + } + + @Override + public void failOnError() throws E { + // first fail if there is already an error + delegate.failOnError(); + // then check for an error via the update mechanism + if (this.checkForError()) delegate.failOnError(); + } + + /** + * Use the injectors to possibly inject an error into the delegate. Should call + * {@link ExceptionCheckable#checkForError()} or {@link ExceptionCheckable#failOnError()} after calling + * this method on return of true. + * @return true if an error found via injector or in the delegate, false + * otherwise + */ + @Override + public boolean checkForError() { + // if there are fault injectors, run them + if (faults.size() > 0) { + // get the caller of this method. Should be the direct calling class + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + for (FaultInjector injector : faults) { + Pair info = injector.injectFault(trace); + if (info != null) { + delegate.receiveError("Injected fail", info.getFirst(), info.getSecond()); + } + } + } + return delegate.checkForError(); + } +} \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/delegate/DelegatingExceptionDispatcher.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/delegate/DelegatingExceptionDispatcher.java new file mode 100644 index 00000000000..3babdf6c2ac --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/server/errorhandling/impl/delegate/DelegatingExceptionDispatcher.java @@ -0,0 +1,71 @@ +/** + * 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.server.errorhandling.impl.delegate; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; +import org.apache.hadoop.hbase.server.errorhandling.impl.ExceptionDispatcher; + +/** + * Helper class for exception handler factories. + * @param Type of delegate to use + * @param type of generic error listener to update + * @param exception to expect for errors + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class DelegatingExceptionDispatcher, T, E extends Exception> + extends ExceptionDispatcher { + + protected final D delegate; + public DelegatingExceptionDispatcher(D delegate) { + super("delegate - " + delegate.getName(), delegate.getDefaultVisitor()); + this.delegate = delegate; + } + + @Override + public ExceptionVisitor getDefaultVisitor() { + return delegate.getDefaultVisitor(); + } + + @Override + public void receiveError(String message, E e, Object... info) { + delegate.receiveError(message, e, info); + } + + @Override + public void addErrorListener(ExceptionVisitor visitor, L errorable) { + delegate.addErrorListener(visitor, errorable); + } + + @Override + public void failOnError() throws E { + delegate.failOnError(); + } + + @Override + public boolean checkForError() { + return delegate.checkForError(); + } + + @Override + public void addErrorListener(T errorable) { + delegate.addErrorListener(errorable); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionForTesting.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionForTesting.java new file mode 100644 index 00000000000..f03216eb955 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionForTesting.java @@ -0,0 +1,28 @@ +/** + * 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.server.errorhandling.impl; + +/** + * Exception thrown from the test + */ +@SuppressWarnings("serial") +public class ExceptionForTesting extends Exception { + public ExceptionForTesting(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionTestingUtils.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionTestingUtils.java new file mode 100644 index 00000000000..362b3e70db7 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/ExceptionTestingUtils.java @@ -0,0 +1,38 @@ +/** + * 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.server.errorhandling.impl; + +/** + * Utility class for testing error propagation + */ +public class ExceptionTestingUtils { + + /** + * Determine if the stack trace contains the given calling class + * @param stack trace to examine + * @param clazz Class to search for + * @return true if the stack contains the calling class + */ + public static boolean stackContainsClass(StackTraceElement[] stack, Class clazz) { + String name = clazz.getName(); + for (StackTraceElement elem : stack) { + if (elem.getClassName().equals(name)) return true; + } + return false; + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/FaultInjectionPolicy.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/FaultInjectionPolicy.java new file mode 100644 index 00000000000..13414c983bc --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/FaultInjectionPolicy.java @@ -0,0 +1,157 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.hbase.util.Pair; + +/** + * Decoratable policy for if a fault should be injected for a given stack trace. + *

+ * Passed in policies are combined with the current policy via a {@link PolicyCombination}. + *

+ * Using {@link PolicyCombination#AND} means that all policies must agree to inject a fault + * (including the current implemented) before a fault is injected. + *

+ * Using {@link PolicyCombination#OR} means that if any of the policies may assert that a + * fault be injected, in which case all remaining injectors will be ignored. + *

+ * Order of operations occurs in reverse order of the operations added via + * {@link #and(FaultInjectionPolicy)} or {@link #or(FaultInjectionPolicy)}. For example, if this is + * the default policy 'a', which we {@link #and(FaultInjectionPolicy)} with 'b' and then 'c', we get + * the following policy chain: + *

+ * a && (b && c) + *

+ * Similarly, if this is the default policy 'a', which we {@link #or(FaultInjectionPolicy)} with 'b' + * and then 'c', we get the following policy chain: + *

+ * a || (b || c). + *

+ * Naturally, more complex policies can then be built using this style. Suppose we have policy A, + * which is actually the 'and' of two policies, a and b: + *

+ * A = a && b + *

+ * and similarly we also have B which is an 'or' of c and d: + *

+ * B = c || d + *

+ * then we could combine the two by calling A {@link #and(FaultInjectionPolicy)} B, to get: + *

+ * A && B = (a && b) && (c || d) + */ +public class FaultInjectionPolicy { + + public enum PolicyCombination { + AND, OR; + /** + * Apply the combination to the policy outputs + * @param current current policy value + * @param next next policy to value to consider + * @return true if the logical combination is valid, false otherwise + */ + public boolean apply(boolean current, boolean next) { + switch (this) { + case AND: + return current && next; + case OR: + return current || next; + default: + throw new IllegalArgumentException("Unrecognized policy!" + this); + } + } + } + + private List> policies = new ArrayList>(); + + /** + * And the current chain with another policy. + *

+ * For example, if this is the default policy 'a', which we {@link #and(FaultInjectionPolicy)} + * with 'b' and then 'c', we get the following policy chain: + *

+ * a && (b && c) + * @param policy policy to logical AND with the current policies + * @return this for chaining + */ + public FaultInjectionPolicy and(FaultInjectionPolicy policy) { + return addPolicy(PolicyCombination.AND, policy); + } + + /** + * And the current chain with another policy. + *

+ * For example, if this is the default policy 'a', which we {@link #or(FaultInjectionPolicy)} with + * 'b' and then 'c', we get the following policy chain: + *

+ * a || (b || c) + * @param policy policy to logical OR with the current policies + * @return this for chaining + */ + public FaultInjectionPolicy or(FaultInjectionPolicy policy) { + return addPolicy(PolicyCombination.OR, policy); + } + + private FaultInjectionPolicy addPolicy(PolicyCombination combinator, FaultInjectionPolicy policy) { + policies.add(new Pair(combinator, policy)); + return this; + } + + /** + * Check to see if this, or any of the policies this decorates, find that a fault should be + * injected . + * @param stack + * @return true if a fault should be injected, false otherwise. + */ + public final boolean shouldFault(StackTraceElement[] stack) { + boolean current = checkForFault(stack); + return eval(current, policies, stack); + } + + /** + * @param current + * @param policies2 + * @param stack + * @return + */ + private boolean eval(boolean current, + List> policies, StackTraceElement[] stack) { + // base condition: if there are no more to evaluate, the comparison is the last + if (policies.size() == 0) return current; + + // otherwise we have to evaluate the rest of chain + Pair policy = policies.get(0); + boolean next = policy.getSecond().shouldFault(stack); + return policy.getFirst() + .apply(current, eval(next, policies.subList(1, policies.size()), stack)); + } + + /** + * Check to see if we should generate a fault for the given stacktrace. + *

+ * Subclass hook for providing custom fault checking behavior + * @param stack + * @return if a fault should be injected for this error check request. false by default + */ + protected boolean checkForFault(StackTraceElement[] stack) { + return false; + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/PoliciedFaultInjector.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/PoliciedFaultInjector.java new file mode 100644 index 00000000000..ac427afa8c3 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/PoliciedFaultInjector.java @@ -0,0 +1,58 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.server.errorhandling.FaultInjector; +import org.apache.hadoop.hbase.util.Pair; + +/** + * Fault injector that can take a policy for when to inject a fault + * @param type of exception that should be returned + */ +public abstract class PoliciedFaultInjector implements FaultInjector { + + private static final Log LOG = LogFactory.getLog(PoliciedFaultInjector.class); + private FaultInjectionPolicy policy; + + public PoliciedFaultInjector(FaultInjectionPolicy policy) { + this.policy = policy; + } + + @Override + public final Pair injectFault(StackTraceElement[] trace) { + + if (policy.shouldFault(trace)) { + return this.getInjectedError(trace); + } + LOG.debug("NOT injecting fault, stack:" + Arrays.toString(Arrays.copyOfRange(trace, 3, 6))); + return null; + } + + /** + * Get the error that should be returned to the caller when the {@link FaultInjectionPolicy} + * determines we need to inject a fault + * @param trace trace for which the {@link FaultInjectionPolicy} specified we should have an error + * @return the information about the fault that should be returned if there was a fault, null + * otherwise + */ + protected abstract Pair getInjectedError(StackTraceElement[] trace); +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/SimpleErrorListener.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/SimpleErrorListener.java new file mode 100644 index 00000000000..7d2c8fd1b25 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/SimpleErrorListener.java @@ -0,0 +1,37 @@ +/** + * 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.server.errorhandling.impl; + +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; + +/** + * Simple error listener that can be checked to see if it received an error ({@link #error}) and the + * information about the error received ({@link #info}). + */ +@SuppressWarnings("javadoc") +public class SimpleErrorListener implements ExceptionListener { + + public boolean error = false; + public Object[] info = null; + + @Override + public void receiveError(String message, Exception e, Object... info) { + this.error = true; + this.info = info; + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestExceptionOrchestrator.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestExceptionOrchestrator.java new file mode 100644 index 00000000000..573e39b2fcc --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestExceptionOrchestrator.java @@ -0,0 +1,139 @@ +/** + * 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.server.errorhandling.impl; + +import java.util.Arrays; + +import org.apache.hadoop.hbase.SmallTests; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.OperationAttemptTimer; +import org.apache.hadoop.hbase.server.errorhandling.exception.OperationAttemptTimeoutException; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; + +/** + * Test that we propagate errors through an orchestrator as expected + */ +@Category(SmallTests.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class TestExceptionOrchestrator { + + @Test + public void testErrorPropagation() { + + ExceptionListener listener1 = Mockito.mock(ExceptionListener.class); + ExceptionListener listener2 = Mockito.mock(ExceptionListener.class); + + ExceptionOrchestrator orchestrator = new ExceptionOrchestrator(); + + // add the listeners + orchestrator.addErrorListener(orchestrator.genericVisitor, listener1); + orchestrator.addErrorListener(orchestrator.genericVisitor, listener2); + + // create an artificial error + String message = "Some error"; + Object[] info = new Object[] { "info1" }; + Exception e = new ExceptionForTesting("error"); + + orchestrator.receiveError(message, e, info); + + // make sure the listeners got the error + Mockito.verify(listener1, Mockito.times(1)).receiveError(message, e, info); + Mockito.verify(listener2, Mockito.times(1)).receiveError(message, e, info); + + // push another error, which should be passed to listeners + message = "another error"; + e = new ExceptionForTesting("hello"); + info[0] = "info2"; + orchestrator.receiveError(message, e, info); + Mockito.verify(listener1, Mockito.times(1)).receiveError(message, e, info); + Mockito.verify(listener2, Mockito.times(1)).receiveError(message, e, info); + + // now create a timer and check for that error + info[0] = "timer"; + OperationAttemptTimer timer = new OperationAttemptTimer(orchestrator, 1000, info); + timer.start(); + timer.trigger(); + // make sure that we got the timer error + Mockito.verify(listener1, Mockito.times(1)).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class), + Mockito.argThat(new VarArgMatcher(Object.class, info))); + Mockito.verify(listener2, Mockito.times(1)).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class), + Mockito.argThat(new VarArgMatcher(Object.class, info))); + } + + /** + * Matcher that matches var-args elements + * @param Type of args to match + */ + private static class VarArgMatcher extends BaseMatcher { + + private T[] expected; + private Class clazz; + private String reason; + + /** + * Setup the matcher to expect args of the given type + * @param clazz type of args to expect + * @param expected expected arguments + */ + public VarArgMatcher(Class clazz, T... expected) { + this.expected = expected; + this.clazz = clazz; + } + + @Override + public boolean matches(Object arg0) { + // null check early exit + if (expected == null && arg0 == null) return true; + + // single arg matching + if (clazz.isAssignableFrom(arg0.getClass())) { + if (expected.length == 1) { + if (arg0.equals(expected[0])) return true; + reason = "single argument received, but didn't match argument"; + } else { + reason = "single argument received, but expected array of args, size = " + + expected.length; + } + } else if (arg0.getClass().isArray()) { + // array matching + try { + T[] arg = (T[]) arg0; + if (Arrays.equals(expected, arg)) return true; + reason = "Array of args didn't match expected"; + } catch (Exception e) { + reason = "Exception while matching arguments:" + e.getMessage(); + } + } else reason = "Objet wasn't the same as passed class or not an array"; + + // nothing worked - fail + return false; + } + + @Override + public void describeTo(Description arg0) { + arg0.appendText(reason); + } + + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjecting.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjecting.java new file mode 100644 index 00000000000..f236f6d57f3 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjecting.java @@ -0,0 +1,95 @@ +/** + * 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.server.errorhandling.impl; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.SmallTests; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionCheckable; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionVisitor; +import org.apache.hadoop.hbase.server.errorhandling.FaultInjector; +import org.apache.hadoop.hbase.util.Pair; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; + +/** + * Test that we can correctly inject faults for testing + */ +@Category(SmallTests.class) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class TestFaultInjecting { + + private static final Log LOG = LogFactory.getLog(TestFaultInjecting.class); + public static final ExceptionVisitor VISITOR = new ExceptionVisitor() { + + @Override + public void visit(ExceptionListener listener, String message, Exception e, Object... info) { + listener.receiveError(message, e, info); + } + }; + + @Test + public void testSimpleFaultInjection() { + ExceptionDispatcherFactory factory = Mockito + .spy(new ExceptionDispatcherFactory(TestFaultInjecting.VISITOR)); + ExceptionDispatcher dispatcher = new ExceptionDispatcher(); + Mockito.when(factory.buildErrorHandler(VISITOR)).thenReturn(dispatcher); + String info = "info"; + ExceptionOrchestratorFactory.addFaultInjector(new StringFaultInjector(info)); + ExceptionCheckable monitor = factory.createErrorHandler(); + // make sure we wrap the dispatcher with the fault injection + assertNotSame(dispatcher, monitor); + + // test that we actually inject a fault + assertTrue("Monitor didn't get an injected error", monitor.checkForError()); + try { + monitor.failOnError(); + fail("Monitor didn't get an exception from the fault injected in the factory."); + } catch (ExceptionForTesting e) { + LOG.debug("Correctly got an exception from the test!"); + } catch (Exception e) { + fail("Got an unexpected exception:" + e); + } + } + + /** + * Fault injector that will always throw a string error + */ + public static class StringFaultInjector implements FaultInjector { + private final String info; + + public StringFaultInjector(String info) { + this.info = info; + } + + @Override + public Pair injectFault(StackTraceElement[] trace) { + if (ExceptionTestingUtils.stackContainsClass(trace, TestFaultInjecting.class)) { + return new Pair(new ExceptionForTesting( + "injected!"), new String[] { info }); + } + return null; + } + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjectionPolicies.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjectionPolicies.java new file mode 100644 index 00000000000..e2813099643 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestFaultInjectionPolicies.java @@ -0,0 +1,95 @@ +/** + * 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.server.errorhandling.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.apache.hadoop.hbase.SmallTests; +import org.apache.hadoop.hbase.util.Pair; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +/** + * Test the fault injection policies and combinations + */ +@Category(SmallTests.class) +public class TestFaultInjectionPolicies { + + @Test + public void testAndCombination() { + FaultInjectionPolicy alwaysFalse = new FaultInjectionPolicy(); + assertFalse("Default policy isn't false", alwaysFalse.shouldFault(null)); + + FaultInjectionPolicy alwaysTrue = new AlwaysTrue(); + FaultInjectionPolicy andTrue = new AlwaysTrue().or(alwaysTrue).or(alwaysTrue); + assertTrue("And True isn't always returning true", andTrue.shouldFault(null)); + + FaultInjectionPolicy andFalse = new FaultInjectionPolicy().and(alwaysTrue); + assertFalse("false AND true", andFalse.shouldFault(null)); + assertFalse("true AND false", alwaysTrue.and(alwaysFalse).shouldFault(null)); + assertFalse("true AND (false AND true)", + new AlwaysTrue().and(new FaultInjectionPolicy().and(new AlwaysTrue())).shouldFault(null)); + assertFalse("(true AND false AND true)", + new AlwaysTrue().and(new FaultInjectionPolicy()).and(new AlwaysTrue()).shouldFault(null)); + } + + @Test + public void testORCombination() { + FaultInjectionPolicy alwaysTrue = new AlwaysTrue(); + + FaultInjectionPolicy andTrue = new AlwaysTrue().or(alwaysTrue).or(alwaysTrue); + assertTrue("OR True isn't always returning true", andTrue.shouldFault(null)); + + FaultInjectionPolicy andFalse = new FaultInjectionPolicy().or(alwaysTrue); + assertTrue("Combination of true OR false should be true", andFalse.shouldFault(null)); + assertTrue("Combining multiple ands isn't correct", + new FaultInjectionPolicy().or(andTrue).or(andFalse).shouldFault(null)); + } + + @Test + public void testMixedAndOr() { + assertTrue("true AND (false OR true)", + new AlwaysTrue().and(new FaultInjectionPolicy().or(new AlwaysTrue())).shouldFault(null)); + assertTrue("(true AND false) OR true", + new AlwaysTrue().or(new AlwaysTrue().and(new FaultInjectionPolicy())).shouldFault(null)); + assertFalse( + "(true AND false) OR false", + new FaultInjectionPolicy().or(new AlwaysTrue().and(new FaultInjectionPolicy())).shouldFault( + null)); + } + + private static class AlwaysTrue extends FaultInjectionPolicy { + + protected boolean checkForFault(StackTraceElement[] stack) { + return true; + } + } + + public static class SimplePolicyFaultInjector extends PoliciedFaultInjector { + + public SimplePolicyFaultInjector(FaultInjectionPolicy policy) { + super(policy); + } + + @Override + protected Pair getInjectedError(StackTraceElement[] trace) { + return new Pair(new RuntimeException("error"), null); + } + } +} \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestOperationAttemptTimer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestOperationAttemptTimer.java new file mode 100644 index 00000000000..8b252d89086 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestOperationAttemptTimer.java @@ -0,0 +1,97 @@ +/** + * 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.server.errorhandling.impl; + +import static org.junit.Assert.fail; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.SmallTests; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.OperationAttemptTimer; +import org.apache.hadoop.hbase.server.errorhandling.exception.OperationAttemptTimeoutException; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; + +/** + * Test the {@link OperationAttemptTimer} to ensure we fulfill contracts + */ +@Category(SmallTests.class) +@SuppressWarnings("unchecked") +public class TestOperationAttemptTimer { + + private static final Log LOG = LogFactory.getLog(TestOperationAttemptTimer.class); + + @Test(timeout = 1000) + public void testTimerTrigger() { + final long time = 10000000; + ExceptionListener listener = Mockito.mock(ExceptionListener.class); + OperationAttemptTimer timer = new OperationAttemptTimer(listener, time); + timer.start(); + timer.trigger(); + Mockito.verify(listener, Mockito.times(1)).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class)); + } + + @Test + public void testTimerPassesOnErrorInfo() { + final long time = 10; + ExceptionListener listener = Mockito.mock(ExceptionListener.class); + final Object[] data = new Object[] { "data" }; + OperationAttemptTimer timer = new OperationAttemptTimer(listener, time, data); + timer.start(); + timer.trigger(); + Mockito.verify(listener).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class), Mockito.eq(data[0])); + } + + @Test(timeout = 1000) + public void testStartAfterComplete() throws InterruptedException { + final long time = 10; + ExceptionListener listener = Mockito.mock(ExceptionListener.class); + OperationAttemptTimer timer = new OperationAttemptTimer(listener, time); + timer.complete(); + try { + timer.start(); + fail("Timer should fail to start after complete."); + } catch (IllegalStateException e) { + LOG.debug("Correctly failed timer: " + e.getMessage()); + } + Thread.sleep(time + 1); + Mockito.verifyZeroInteractions(listener); + } + + @Test(timeout = 1000) + public void testStartAfterTrigger() throws InterruptedException { + final long time = 10; + ExceptionListener listener = Mockito.mock(ExceptionListener.class); + OperationAttemptTimer timer = new OperationAttemptTimer(listener, time); + timer.trigger(); + try { + timer.start(); + fail("Timer should fail to start after complete."); + } catch (IllegalStateException e) { + LOG.debug("Correctly failed timer: " + e.getMessage()); + } + Thread.sleep(time * 2); + Mockito.verify(listener, Mockito.times(1)).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class)); + Mockito.verifyNoMoreInteractions(listener); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestSingleExceptionDispatcher.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestSingleExceptionDispatcher.java new file mode 100644 index 00000000000..1c4b93ce936 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/server/errorhandling/impl/TestSingleExceptionDispatcher.java @@ -0,0 +1,113 @@ +/** + * 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.server.errorhandling.impl; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hbase.SmallTests; +import org.apache.hadoop.hbase.server.errorhandling.ExceptionListener; +import org.apache.hadoop.hbase.server.errorhandling.OperationAttemptTimer; +import org.apache.hadoop.hbase.server.errorhandling.exception.OperationAttemptTimeoutException; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; + +/** + * Test using the single error dispatcher + */ +@SuppressWarnings("unchecked") +@Category(SmallTests.class) +public class TestSingleExceptionDispatcher { + + private static final Log LOG = LogFactory.getLog(TestSingleExceptionDispatcher.class); + @Test + public void testErrorPropagation() { + ExceptionListener listener1 = Mockito.mock(ExceptionListener.class); + ExceptionListener listener2 = Mockito.mock(ExceptionListener.class); + + ExceptionDispatcher, Exception> monitor = new ExceptionDispatcher, Exception>(); + + // add the listeners + monitor.addErrorListener(monitor.genericVisitor, listener1); + monitor.addErrorListener(monitor.genericVisitor, listener2); + + // create an artificial error + String message = "Some error"; + Exception expected = new ExceptionForTesting("error"); + Object info = "info1"; + monitor.receiveError(message, expected, info); + + // make sure the listeners got the error + Mockito.verify(listener1).receiveError(message, expected, info); + Mockito.verify(listener2).receiveError(message, expected, info); + + // make sure that we get an exception + try { + monitor.failOnError(); + fail("Monitor should have thrown an exception after getting error."); + } catch (Exception e) { + assertTrue("Got an unexpected exception:" + e, e instanceof ExceptionForTesting); + LOG.debug("Got the testing exception!"); + } + // push another error, but this shouldn't be passed to the listeners + monitor.receiveError("another error", new ExceptionForTesting("hello"), + "shouldn't be found"); + // make sure we don't re-propagate the error + Mockito.verifyNoMoreInteractions(listener1, listener2); + } + + @Test + public void testSingleDispatcherWithTimer() { + ExceptionListener listener1 = Mockito.mock(ExceptionListener.class); + ExceptionListener listener2 = Mockito.mock(ExceptionListener.class); + + ExceptionDispatcher, Exception> monitor = new ExceptionDispatcher, Exception>(); + + // add the listeners + monitor.addErrorListener(monitor.genericVisitor, listener1); + monitor.addErrorListener(monitor.genericVisitor, listener2); + + Object info = "message"; + OperationAttemptTimer timer = new OperationAttemptTimer(monitor, 1000, info); + timer.start(); + timer.trigger(); + + assertTrue("Monitor didn't get timeout", monitor.checkForError()); + + // verify that that we propagated the error + Mockito.verify(listener1).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class), Mockito.eq(info)); + Mockito.verify(listener2).receiveError(Mockito.anyString(), + Mockito.any(OperationAttemptTimeoutException.class), Mockito.eq(info)); + } + + @Test + public void testAddListenerWithoutVisitor() { + SimpleErrorListener listener = new SimpleErrorListener(); + ExceptionDispatcher, Exception> monitor = new ExceptionDispatcher, Exception>(); + try { + monitor.addErrorListener(listener); + fail("Monitor needs t have a visitor for adding generically typed listeners"); + } catch (UnsupportedOperationException e) { + LOG.debug("Correctly failed to add listener without visitor: " + e.getMessage()); + } + } +} \ No newline at end of file