/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.core.pc.operation;
import java.util.EnumSet;
import java.util.TimerTask;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.clientapi.agent.configuration.ConfigurationUtility;
import org.rhq.core.clientapi.server.operation.OperationServerService;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.operation.OperationDefinition;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.util.exception.ExceptionPackage;
import org.rhq.core.util.exception.Severity;
/**
* The runnable that is actually responsible for invoking an operation on a plugin's {@link OperationFacet}.
*
* @author John Mazzitelli
*/
public class OperationInvocation implements Runnable {
private final Log log = LogFactory.getLog(OperationInvocation.class);
/**
* Indicates the current status of this invocation (QUEUED, RUNNING or FINISHED). A invocation may have additional
* indicators: CANCELED (if it was told to stop) and TIMED_OUT (if it was told to stop due to a time-out).
*/
public enum Status {
QUEUED, RUNNING, FINISHED, CANCELED, TIMED_OUT
}
private final int resourceId;
private final long invocationTime;
private final TimerTask timerTask;
private final Configuration parameterConfig;
private final String jobId;
private final String operationName;
private final OperationFacet operationComponent;
private final OperationServerService operationServerService;
private final OperationThreadPoolGateway operationThreadPoolGateway;
private final OperationDefinition operationDefinition;
private final EnumSet<Status> status;
private Thread operationThread;
public OperationInvocation(int resourceId, long invocationTime, TimerTask timerTask, Configuration parameterConfig,
String jobId, String operationName, OperationFacet operationComponent,
OperationServerService operationServerService, OperationThreadPoolGateway operationThreadPoolGateway,
OperationDefinition operationDefinition) {
this.resourceId = resourceId;
this.invocationTime = invocationTime;
this.timerTask = timerTask;
this.parameterConfig = parameterConfig;
this.jobId = jobId;
this.operationName = operationName;
this.operationComponent = operationComponent;
this.operationServerService = operationServerService;
this.operationThreadPoolGateway = operationThreadPoolGateway;
this.operationDefinition = operationDefinition;
this.status = EnumSet.of(Status.QUEUED);
this.operationThread = null; // will be non-null when in running state
}
/**
* Identifies the resource that this invocation will operate on.
*
* @return the resource's ID
*/
public int getResourceId() {
return resourceId;
}
/**
* Returns the job ID that identifies this specific operation invocation.
*
* @return unique job identification string
*/
public String getJobId() {
return jobId;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("OperationInvocation: ");
str.append("resource-id=[" + resourceId);
str.append("], job-id=[" + jobId);
str.append("], op-name=[" + operationName);
str.append("], status=[" + getStatus());
str.append("]");
return str.toString();
}
public EnumSet<Status> getStatus() {
synchronized (status) {
return EnumSet.copyOf(status);
}
}
/**
* Flags this operation as being canceled. This will also interrupt the thread running the operation, if it is in
* the running state. Note that if the operation has already completed, this method does nothing since it is too
* late to cancel the operation.
*
* @return the current state of the operation when it was canceled
*/
public EnumSet<Status> markAsCanceled() {
synchronized (status) {
EnumSet<Status> interruptedStatus = EnumSet.copyOf(status);
if (!status.contains(Status.FINISHED)) {
status.add(Status.CANCELED);
if (operationThread != null) {
operationThread.interrupt();
}
}
return interruptedStatus;
}
}
/**
* Flags this operation as being canceled due to a time out. This will also interrupt the thread running the
* operation, if it is in the running state. Note that if the operation has already completed, this method does
* nothing since it is too late to time out the operation.
*/
public void markAsTimedOut() {
synchronized (status) {
if (!status.contains(Status.FINISHED)) {
status.add(Status.TIMED_OUT);
markAsCanceled();
}
}
}
/**
* Flags this operation as running within the given thread. If this operation was prematurely timed out or canceled,
* this will return <code>false</code> indicating to the caller that the operation should abort and not run. <code>
* true</code> is returned if the operation can move forward and begin to execute.
*
* @param thread the thread in which this operation is running in
*
* @return <code>false</code> if the operation was prematurely canceled/timed out; <code>true</code> if the
* operation can continue to execute
*/
private boolean markAsRunning(Thread thread) {
synchronized (status) {
operationThread = thread;
status.remove(Status.QUEUED);
status.add(Status.RUNNING);
return !status.contains(Status.CANCELED);
}
}
/**
* Flags this operation as being finished (it either completed normally or it was canceled or it timed out).
*/
private void markAsFinished() {
synchronized (status) {
operationThread = null;
status.remove(Status.QUEUED);
status.remove(Status.RUNNING);
status.add(Status.FINISHED);
}
}
/**
* This actually invokes the plugin's operation facet and executes the operation. If it does not finish before the
* timeout expires, the timer task will {@link #markAsTimedOut()} interrupt this thread}. This thread will also be
* interrupted if the operation was {@link #markAsCanceled() canceled}.
*
* @see java.lang.Runnable#run()
*/
public void run() {
Configuration result = null;
String errorMessage = null;
Throwable failure = null;
long finishedTime;
try {
boolean canContinue = markAsRunning(Thread.currentThread());
// We are at the point of no return now. If someone tries to cancel us, its up to
// the plugin writer of the operation facet to handle the interrupt exception, if applicable.
if (canContinue) {
// call the plugin component's operation facet to actually execute the operation
Configuration parameters = (parameterConfig != null) ? parameterConfig : new Configuration();
OperationResult opResult = operationComponent.invokeOperation(operationName, parameters);
// allow a plugin to return a null (aka void) result set
result = (opResult != null) ? opResult.getComplexResults() : null;
if (result != null) {
if (this.operationDefinition != null) {
if (this.operationDefinition.getResultsConfigurationDefinition() != null) {
// Normalize the result Configuration.
ConfigurationUtility.normalizeConfiguration(result, operationDefinition
.getResultsConfigurationDefinition());
// TODO: Validate the result Configuration?
} else if (!result.getProperties().isEmpty()) {
log.error("Plugin error: Operation [" + this.operationDefinition.getName()
+ "] is defined as returning no results, but it returned non-null results: "
+ result.toString(true));
result = null; // Don't return results that the GUI won't be able to display anyway.
}
}
errorMessage = opResult.getErrorMessage();
}
} else {
failure = new InterruptedException("Operation was aborted before it started.");
}
} catch (Throwable t) {
failure = t;
} finally {
// IT IS IMPORTANT THAT THIS FINALLY CLAUSE NOT THROW EXCEPTIONS. EVERYTHING IN HERE
// MUST EXECUTE PROPERLY OR ELSE THE OPERATION MANAGER WILL NOT EXECUTE OPERATIONS PROPERLY
// Note: if the timer triggers in the nanoseconds between the above and our mark here
// then the timer task will erroneously cause us to send out a timeout message to the server. I know of no
// way to prevent this right now. This will be a rare occurrence, but not an impossibility.
markAsFinished();
timerTask.cancel();
// remember the time we finished - to ensure our times are in order,
// be sure we mark this time before we allow the gateway to submit the next operation to the thread pool
finishedTime = System.currentTimeMillis();
// before taking the time to send the request up to the server, let's allow the next
// operation in the queue to get executed right now. Make sure this always is called,
// otherwise, no other operations on the resource will be allowed
operationThreadPoolGateway.operationCompleted(this);
}
// if we have a server that we need to tell, notify it of the results/failure/cancellation/timeout
if (operationServerService != null) {
if (failure == null) {
// Note that even if the operation was canceled and/or timed out, we may still get here.
// This happens if either the plugin quickly finished before we received the order to cancel
// or the plugin ignored the order to cancel (i.e. the thread interrupt) and finished doing
// what it was doing anyway. In either case, the operation really did succeed (it was not
// canceled) so we need to indicate this via calling operationSucceeded
if (errorMessage == null) {
try {
operationServerService.operationSucceeded(jobId, result, invocationTime, finishedTime);
} catch (Throwable t) {
log.error("Failed to send operation succeeded message to server. resource=[" + resourceId
+ "], operation=[" + operationName + "], jobId=[" + jobId + "]", t);
}
} else {
ExceptionPackage errorResults = new ExceptionPackage(Severity.Severe, new Exception(errorMessage));
try {
operationServerService.operationFailed(jobId, result, errorResults, invocationTime,
finishedTime);
} catch (Throwable t) {
log.error("Failed to send operation failed message to server. resource=[" + resourceId
+ "], operation=[" + operationName + "], jobId=[" + jobId + "]", t);
}
}
} else {
if (status.contains(Status.TIMED_OUT)) {
try {
operationServerService.operationTimedOut(jobId, invocationTime, finishedTime);
} catch (Throwable t) {
log.error("Failed to send operation timed out message to server. resource=[" + resourceId
+ "], operation=[" + operationName + "], jobId=[" + jobId + "]", t);
}
} else {
ExceptionPackage errorResults;
if (status.contains(Status.CANCELED)) {
errorResults = new ExceptionPackage(Severity.Info, new Exception("Canceled", failure));
} else {
errorResults = new ExceptionPackage(Severity.Severe, failure);
}
try {
if (status.contains(Status.CANCELED)) {
operationServerService.operationCanceled(jobId, null, errorResults, invocationTime,
finishedTime);
} else {
operationServerService.operationFailed(jobId, null, errorResults, invocationTime,
finishedTime);
}
} catch (Throwable t) {
log.error("Failed to send operation failed message to server. resource=[" + resourceId
+ "], operation=[" + operationName + "], jobId=[" + jobId + "], operation-error=["
+ errorResults.toString() + "]", t);
}
}
}
}
return;
}
}