/**
* Copyright (c) 2010 Yahoo! Inc. All rights reserved.
* Licensed 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. See accompanying LICENSE file.
*/
package org.apache.oozie.action;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.conf.Configuration;
import org.apache.oozie.client.WorkflowAction;
import org.apache.oozie.client.WorkflowJob;
import org.apache.oozie.util.ELEvaluator;
import org.apache.oozie.util.ParamChecker;
import org.apache.oozie.util.XLog;
import org.apache.oozie.service.HadoopAccessorException;
import org.apache.oozie.service.Services;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.LinkedHashMap;
/**
* Base action executor class. <p/> All the action executors should extend this class.
*/
public abstract class ActionExecutor {
/**
* Configuration prefix for action executor (sub-classes) properties.
*/
public static final String CONF_PREFIX = "oozie.action.";
/**
* Error code used by {@link #convertException} when there is not register error information for an exception.
*/
public static final String ERROR_OTHER = "OTHER";
private static class ErrorInfo {
ActionExecutorException.ErrorType errorType;
String errorCode;
private ErrorInfo(ActionExecutorException.ErrorType errorType, String errorCode) {
this.errorType = errorType;
this.errorCode = errorCode;
}
}
private static boolean initMode = false;
private static Map<String, Map<Class, ErrorInfo>> ERROR_INFOS = new HashMap<String, Map<Class, ErrorInfo>>();
/**
* Context information passed to the ActionExecutor methods.
*/
public interface Context {
/**
* Create the callback URL for the action.
*
* @param externalStatusVar variable for the caller to inject the external status.
* @return the callback URL.
*/
public String getCallbackUrl(String externalStatusVar);
/**
* Return a proto configuration for actions with auth properties already set.
*
* @return a proto configuration for actions with auth properties already set.
*/
public Configuration getProtoActionConf();
/**
* Return the workflow job.
*
* @return the workflow job.
*/
public WorkflowJob getWorkflow();
/**
* Return an ELEvaluator with the context injected.
*
* @return configured ELEvaluator.
*/
public ELEvaluator getELEvaluator();
/**
* Set a workflow action variable. <p/> Convenience method that prefixes the variable name with the action name
* plus a '.'.
*
* @param name variable name.
* @param value variable value, <code>null</code> removes the variable.
*/
public void setVar(String name, String value);
/**
* Get a workflow action variable. <p/> Convenience method that prefixes the variable name with the action name
* plus a '.'.
*
* @param name variable name.
* @return the variable value, <code>null</code> if not set.
*/
public String getVar(String name);
/**
* Set the action tracking information for an successfully started action.
*
* @param externalId the action external ID.
* @param trackerUri the action tracker URI.
* @param consoleUrl the action console URL.
*/
void setStartData(String externalId, String trackerUri, String consoleUrl);
/**
* Set the action execution completion information for an action. The action status is set to {@link
* org.apache.oozie.client.WorkflowAction.Status#DONE}
*
* @param externalStatus the action external end status.
* @param actionData the action data on completion, <code>null</code> if none.
*/
void setExecutionData(String externalStatus, Properties actionData);
/**
* Set the action end completion information for a completed action.
*
* @param status the action end status, it can be {@link org.apache.oozie.client.WorkflowAction.Status#OK} or
* {@link org.apache.oozie.client.WorkflowAction.Status#ERROR}.
* @param signalValue the action external end status.
*/
void setEndData(WorkflowAction.Status status, String signalValue);
/**
* Return if the executor invocation is a retry or not.
*
* @return if the executor invocation is a retry or not.
*/
boolean isRetry();
/**
* Sets the external status for the action in context.
*
* @param externalStatus the external status.
*/
void setExternalStatus(String externalStatus);
/**
* Get the Action Recovery ID.
*
* @return recovery ID.
*/
String getRecoveryId();
/*
* @return the path that will be used to store action specific data
* @throws IOException @throws URISyntaxException @throws HadoopAccessorException
*/
public Path getActionDir() throws HadoopAccessorException, IOException, URISyntaxException;
/**
* @return filesystem handle for the application deployment fs.
* @throws IOException
* @throws URISyntaxException
* @throws HadoopAccessorException
*/
public FileSystem getAppFileSystem() throws HadoopAccessorException, IOException, URISyntaxException;
public void setErrorInfo(String str, String exMsg);
}
/**
* Define the default maximum number of retry attempts for transient errors (total attempts = 1 + MAX_RETRIES).
*/
public static final int MAX_RETRIES = 3;
/**
* Define the default inteval in seconds between retries.
*/
public static final long RETRY_INTERVAL = 60;
private String type;
private int maxRetries;
private long retryInterval;
/**
* Create an action executor with default retry parameters.
*
* @param type action executor type.
*/
protected ActionExecutor(String type) {
this(type, MAX_RETRIES, RETRY_INTERVAL);
}
/**
* Create an action executor.
*
* @param type action executor type.
* @param retryAttempts retry attempts.
* @param retryInterval retry interval, in seconds.
*/
protected ActionExecutor(String type, int retryAttempts, long retryInterval) {
this.type = ParamChecker.notEmpty(type, "type");
this.maxRetries = retryAttempts;
this.retryInterval = retryInterval;
}
/**
* Clear all init settings for all action types.
*/
public static void resetInitInfo() {
if (!initMode) {
throw new IllegalStateException("Error, action type info locked");
}
ERROR_INFOS.clear();
}
/**
* Enable action type initialization.
*/
public static void enableInit() {
initMode = true;
}
/**
* Disable action type initialization.
*/
public static void disableInit() {
initMode = false;
}
/**
* Invoked once at system initialization time. <p/> It can be used to register error information for the expected
* exceptions. Exceptions should be register from subclasses to superclasses to ensure proper detection, same thing
* that it is done in a normal catch. <p/> This method should invoke the {@link #registerError} method to register
* all its possible errors. <p/> Subclasses overriding must invoke super.
*/
public void initActionType() {
ERROR_INFOS.put(getType(), new LinkedHashMap<Class, ErrorInfo>());
}
/**
* Return the system ID, this ID is defined in Oozie configuration.
*
* @return the system ID.
*/
public String getOozieSystemId() {
return Services.get().getSystemId();
}
/**
* Return the runtime directory of the Oozie instance. <p/> The directory is created under TMP and it is always a
* new directory per system initialization.
*
* @return the runtime directory of the Oozie instance.
*/
public String getOozieRuntimeDir() {
return Services.get().getRuntimeDir();
}
/**
* Return Oozie configuration. <p/> This is useful for actions that need access to configuration properties.
*
* @return Oozie configuration.
*/
public Configuration getOozieConf() {
return Services.get().getConf();
}
/**
* Register error handling information for an exception.
*
* @param exClass excpetion class name (to work in case of a particular exception not being in the classpath, needed
* to be able to handle multiple version of Hadoop or other JARs used by executors with the same codebase).
* @param errorType error type for the exception.
* @param errorCode error code for the exception.
*/
protected void registerError(String exClass, ActionExecutorException.ErrorType errorType, String errorCode) {
if (!initMode) {
throw new IllegalStateException("Error, action type info locked");
}
try {
Class klass = Thread.currentThread().getContextClassLoader().loadClass(exClass);
Map<Class, ErrorInfo> executorErrorInfo = ERROR_INFOS.get(getType());
executorErrorInfo.put(klass, new ErrorInfo(errorType, errorCode));
}
catch (ClassNotFoundException ex) {
XLog.getLog(getClass()).warn(
"Exception [{0}] no in classpath, ActionExecutor [{1}] will handled it as ERROR", exClass,
getType());
}
}
/**
* Return the action executor type.
*
* @return the action executor type.
*/
public String getType() {
return type;
}
/**
* Return the maximum number of retries for the action executor.
*
* @return the maximum number of retries for the action executor.
*/
public int getMaxRetries() {
return maxRetries;
}
/**
* Set the maximum number of retries for the action executor.
*
* @param maxRetries the maximum number of retries.
*/
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
/**
* Return the retry interval for the action executor in seconds.
*
* @return the retry interval for the action executor in seconds.
*/
public long getRetryInterval() {
return retryInterval;
}
/**
* Sets the retry interval for the action executor.
*
* @param retryInterval retry interval in seconds.
*/
public void setRetryInterval(long retryInterval) {
this.retryInterval = retryInterval;
}
/**
* Utility method to handle exceptions in the {@link #start}, {@link #end}, {@link #kill} and {@link #check} methods
* <p/> It uses the error registry to convert exceptions to {@link ActionExecutorException}s.
*
* @param ex exception to convert.
* @return ActionExecutorException converted exception.
*/
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
protected ActionExecutorException convertException(Exception ex) {
if (ex instanceof ActionExecutorException) {
return (ActionExecutorException) ex;
}
for (Map.Entry<Class, ErrorInfo> errorInfo : ERROR_INFOS.get(getType()).entrySet()) {
if (errorInfo.getKey().isInstance(ex)) {
return new ActionExecutorException(errorInfo.getValue().errorType, errorInfo.getValue().errorCode,
"{0}", ex.getMessage(), ex);
}
}
String errorCode = ex.getClass().getName();
errorCode = errorCode.substring(errorCode.lastIndexOf(".") + 1);
return new ActionExecutorException(ActionExecutorException.ErrorType.ERROR, errorCode, "{0}", ex.getMessage(),
ex);
}
/**
* Convenience method that return the signal for an Action based on the action status.
*
* @param status action status.
* @return the action signal.
*/
protected String getActionSignal(WorkflowAction.Status status) {
switch (status) {
case OK:
return "OK";
case ERROR:
case KILLED:
return "ERROR";
default:
throw new IllegalArgumentException("Action status for signal can only be OK or ERROR");
}
}
/**
* Return the path that will be used to store action specific data
*
* @param jobId Worfklow ID
* @param action Action
* @param key An Identifier
* @param temp temp directory flag
* @return A string that has the path
*/
protected String getActionDirPath(String jobId, WorkflowAction action, String key, boolean temp) {
String name = jobId + "/" + action.getName() + "--" + key;
if (temp) {
name += ".temp";
}
return getOozieSystemId() + "/" + name;
}
/**
* Return the path that will be used to store action specific data.
*
* @param jobId Workflow ID
* @param action Action
* @param key An identifier
* @param temp Temp directory flag
* @return Path to the directory
*/
public Path getActionDir(String jobId, WorkflowAction action, String key, boolean temp) {
return new Path(getActionDirPath(jobId, action, key, temp));
}
/**
* Start an action. <p/> The {@link Context#setStartData} method must be called within this method. <p/> If the
* action has completed, the {@link Context#setExecutionData} method must be called within this method.
*
* @param context executor context.
* @param action the action to start.
* @throws ActionExecutorException thrown if the action could not start.
*/
public abstract void start(Context context, WorkflowAction action) throws ActionExecutorException;
/**
* End an action after it has executed. <p/> The {@link Context#setEndData} method must be called within this
* method.
*
* @param context executor context.
* @param action the action to end.
* @throws ActionExecutorException thrown if the action could not end.
*/
public abstract void end(Context context, WorkflowAction action) throws ActionExecutorException;
/**
* Check if an action has completed. This method must be implemented by Async Action Executors. <p/> If the action
* has completed, the {@link Context#setExecutionData} method must be called within this method. <p/> If the action
* has not completed, the {@link Context#setExternalStatus} method must be called within this method.
*
* @param context executor context.
* @param action the action to end.
* @throws ActionExecutorException thrown if the action could not be checked.
*/
public abstract void check(Context context, WorkflowAction action) throws ActionExecutorException;
/**
* Kill an action. <p/> The {@link Context#setEndData} method must be called within this method.
*
* @param context executor context.
* @param action the action to kill.
* @throws ActionExecutorException thrown if the action could not be killed.
*/
public abstract void kill(Context context, WorkflowAction action) throws ActionExecutorException;
/**
* Return if the external status indicates that the action has completed.
*
* @param externalStatus external status to check.
* @return if the external status indicates that the action has completed.
*/
public abstract boolean isCompleted(String externalStatus);
}