/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.domain.controller.operations.coordination;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DOMAIN_FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DOMAIN_RESULTS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILED;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST_FAILURE_DESCRIPTIONS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESPONSE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER_GROUP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.jboss.as.controller.CompositeOperationHandler;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.domain.controller.ServerIdentity;
import org.jboss.as.domain.controller.logging.DomainControllerLogger;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
/**
* Assembles the overall result for a domain operation from individual host and server results. This handler does
* nothing in {@code execute} except registering a {@link org.jboss.as.controller.OperationContext.ResultHandler}.
* The work is done in the result handler, which processes data gathered by other handlers.
* <p>This handler should be registered before any of the typical kinds of handlers that actually deal with
* the resource and operation indicated by the management operation. This ensures that its result handler
* will execute after any result handler registered by those other handlers, ensuring that this handler will
* see all available data.</p>
*
* @author Brian Stansberry (c) 2011 Red Hat Inc.
*/
class DomainFinalResultHandler implements OperationStepHandler {
private final MultiphaseOverallContext multiphaseContext;
private final HostControllerExecutionSupport executionSupport;
DomainFinalResultHandler(MultiphaseOverallContext multiphaseContext, HostControllerExecutionSupport executionSupport) {
this.multiphaseContext = multiphaseContext;
this.executionSupport = executionSupport;
}
@Override
public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
// Just register a result handler.
context.completeStep(new OperationContext.ResultHandler() {
@Override
public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) {
DomainControllerLogger.HOST_CONTROLLER_LOGGER.tracef("Establishing final response -- result action is %s", resultAction);
// On the way out, fix up the response
final boolean isDomain = isDomainOperation(operation);
boolean shouldContinue = collectDomainFailure(context, isDomain);
shouldContinue = shouldContinue && collectContextFailure(context, isDomain);
if (shouldContinue) {
ModelNode contextResult = context.getResult();
contextResult.setEmptyObject(); // clear out any old data
// Format any local response for easy searching, putting it in the same format
// a slave would send in response to DomainSlaveHandler
ModelNode localDomainFormatted;
if (executionSupport == null) {
localDomainFormatted = new ModelNode();
} else {
ModelNode localResponse = multiphaseContext.getLocalContext().getLocalResponse();
localDomainFormatted = localResponse.clone();
localDomainFormatted.get(RESULT).clear();
ModelNode domainResults = executionSupport.getFormattedDomainResult(localResponse.get(RESULT));
localDomainFormatted.get(RESULT, DOMAIN_RESULTS).set(domainResults);
DomainControllerLogger.HOST_CONTROLLER_LOGGER.tracef("Domain formatted result for local response %s is %s",
localResponse, localDomainFormatted);
}
contextResult.set(getDomainResults(operation, localDomainFormatted));
// If we have server results we know all was ok on the slaves
Map<ServerIdentity, ModelNode> serverResults = multiphaseContext.getServerResults();
if (serverResults.size() > 0) {
populateServerGroupResults(context, serverResults);
// TODO report post-commit failures on slaves (i.e. in OperationContext.ResultHandler impls).
// Consider enabling this. Problem is this results in the op having
// outcome=failed, but really the model and MSC were updated on all HCs and servers
// so the effect is likely much more like outcome=success
// We don't really know what went wrong.
// if (isDomain) {
// // If there were any post-prepare failures on slaves, report them
// populatePostPrepareHCFailures(context);
// }
} else {
shouldContinue = collectHostFailures(context, isDomain);
if (shouldContinue) {
// Just make sure there's an 'undefined' server-groups node
context.getServerResults();
}
}
}
if (!shouldContinue && context.hasResult()) {
context.getResult().setEmptyObject(); // clear out any old data
}
}
});
}
private boolean collectDomainFailure(OperationContext context, final boolean isDomain) {
final ModelNode coordinator = multiphaseContext.getLocalContext().getLocalResponse();
ModelNode domainFailure = null;
if (isDomain && coordinator.has(FAILURE_DESCRIPTION)) {
domainFailure = coordinator.hasDefined(FAILURE_DESCRIPTION) ? coordinator.get(FAILURE_DESCRIPTION) : new ModelNode(DomainControllerLogger.HOST_CONTROLLER_LOGGER.unexplainedFailure());
}
if (domainFailure != null) {
ModelNode fullFailure = new ModelNode();
fullFailure.get(DOMAIN_FAILURE_DESCRIPTION).set(domainFailure);
context.getFailureDescription().set(fullFailure);
return false;
}
return true;
}
private boolean collectContextFailure(OperationContext context, final boolean isDomain) {
// We ignore a context failure description if the request failed on all servers, as the
// DomainRolloutStepHandler would have had to set that to trigger model rollback
// but we still want to record the server results so the user can see the problem
if (!multiphaseContext.isFailureReported() && context.hasFailureDescription()) {
ModelNode formattedFailure = new ModelNode();
if (isDomain) {
ModelNode failure = context.getFailureDescription();
if (failure.isDefined())
formattedFailure.get(DOMAIN_FAILURE_DESCRIPTION).set(failure);
else
formattedFailure.get(DOMAIN_FAILURE_DESCRIPTION).set(DomainControllerLogger.HOST_CONTROLLER_LOGGER.unexplainedFailure());
} else {
ModelNode hostFailureProperty = new ModelNode();
ModelNode contextFailure = context.getFailureDescription();
ModelNode hostFailure = contextFailure.isDefined() ? contextFailure : new ModelNode(DomainControllerLogger.HOST_CONTROLLER_LOGGER.unexplainedFailure());
hostFailureProperty.add(multiphaseContext.getLocalHostInfo().getLocalHostName(), hostFailure);
formattedFailure.get(HOST_FAILURE_DESCRIPTIONS).set(hostFailureProperty);
}
context.getFailureDescription().set(formattedFailure);
return false;
}
return true;
}
private boolean collectHostFailures(final OperationContext context, final boolean isDomain) {
ModelNode hostFailureResults = null;
for (Map.Entry<String, ModelNode> entry : multiphaseContext.getHostControllerPreparedResults().entrySet()) {
ModelNode hostResult = entry.getValue();
if (hostResult.has(FAILURE_DESCRIPTION)) {
if (hostFailureResults == null) {
hostFailureResults = new ModelNode();
}
final ModelNode desc = hostResult.hasDefined(FAILURE_DESCRIPTION) ? hostResult.get(FAILURE_DESCRIPTION) : new ModelNode().set(DomainControllerLogger.HOST_CONTROLLER_LOGGER.unexplainedFailure());
hostFailureResults.get(entry.getKey()).set(desc);
}
}
final ModelNode coordinator = multiphaseContext.getLocalContext().getLocalResponse();
if (!isDomain && coordinator.has(FAILURE_DESCRIPTION)) {
if (hostFailureResults == null) {
hostFailureResults = new ModelNode();
}
final ModelNode desc = coordinator.hasDefined(FAILURE_DESCRIPTION) ? coordinator.get(FAILURE_DESCRIPTION) : new ModelNode().set(DomainControllerLogger.HOST_CONTROLLER_LOGGER.unexplainedFailure());
hostFailureResults.get(multiphaseContext.getLocalHostInfo().getLocalHostName()).set(desc);
}
if (hostFailureResults != null) {
//context.getFailureDescription().get(HOST_FAILURE_DESCRIPTIONS).set(hostFailureResults);
//The following is a workaround for AS7-4597
//DomainRolloutStepHandler.pushToServers() puts in a simple string into the failure description, but that might be a red herring.
//If there is a failure description and it is not of type OBJECT, then let's not set it for now
if (!context.getFailureDescription().isDefined() || context.getFailureDescription().getType() == ModelType.OBJECT) {
ModelNode fullFailure = new ModelNode();
fullFailure.get(HOST_FAILURE_DESCRIPTIONS).set(hostFailureResults);
context.getFailureDescription().set(fullFailure);
} else {
DomainControllerLogger.HOST_CONTROLLER_LOGGER.debugf("Failure description is not of type OBJECT '%s'", context.getFailureDescription());
}
return false;
}
return true;
}
private ModelNode getDomainResults(final ModelNode operation, final ModelNode localDomainFormatted, final String... stepLabels) {
ResponseProvider provider = new ResponseProvider(operation, multiphaseContext.getLocalHostInfo().getLocalHostName());
DomainControllerLogger.HOST_CONTROLLER_LOGGER.tracef("Provider for %s is %s", operation, provider);
ModelNode result = null;
if (!provider.isLeaf()) {
result = new ModelNode();
ModelNode compositeResult;
if (stepLabels.length == 0) {
// Initial request; we're already dealing with the result node
compositeResult = result;
} else {
// Add a wrapper 'response'
// Outcome for composite is 'success' or we would not be here
result.get(OUTCOME).set(SUCCESS);
compositeResult = result.get(RESULT);
}
String[] nextStepLabels = new String[stepLabels.length + 1];
System.arraycopy(stepLabels, 0, nextStepLabels, 0, stepLabels.length);
int i = 1;
for (ModelNode step : provider.getChildren()) {
String childStepLabel = "step-" + i++;
nextStepLabels[stepLabels.length] = childStepLabel;
compositeResult.get(childStepLabel).set(getDomainResults(step, localDomainFormatted, nextStepLabels));
}
} else if (provider.getServer() == null) {
String hostName = provider.getHost();
if (hostName.equals(multiphaseContext.getLocalHostInfo().getLocalHostName())) {
result = getHostControllerResult(localDomainFormatted, stepLabels);
} else {
ModelNode hostFinalResult = multiphaseContext.getHostControllerFinalResults().get(hostName);
if (hostFinalResult != null) {
result = getHostControllerResult(hostFinalResult, stepLabels);
} else {
// Perhaps cancelled; see if there's a prepared result
ModelNode preparedResult = multiphaseContext.getHostControllerPreparedResults().get(hostName);
if (preparedResult != null) {
result = getHostControllerResult(preparedResult, stepLabels);
}
}
}
} else {
result = multiphaseContext.getServerResult(provider.getHost(), provider.getServer(), stepLabels);
}
DomainControllerLogger.HOST_CONTROLLER_LOGGER.tracef("Domain result for %s is %s", operation, result);
return result == null ? new ModelNode() : result;
}
private ModelNode getHostControllerResult(final ModelNode fullResult, final String... stepLabels) {
ModelNode result = null;
if (fullResult != null && fullResult.hasDefined(RESULT, DOMAIN_RESULTS)) {
ModelNode domainResults = fullResult.get(RESULT, DOMAIN_RESULTS);
if (stepLabels.length == 0) {
result = domainResults;
} else {
ModelNode source = domainResults;
for (int i = 0; i < stepLabels.length; i++) {
if (i == 0) {
if (source.hasDefined(stepLabels[i])) {
source = source.get(stepLabels[i]);
} else {
source = new ModelNode();
break;
}
} else {
if (source.hasDefined(RESULT, stepLabels[i])) {
source = source.get(RESULT, stepLabels[i]);
} else {
source = new ModelNode();
break;
}
}
}
result = source;
}
if (result.has(OUTCOME) && !result.hasDefined(OUTCOME)) {
if (result.hasDefined(FAILURE_DESCRIPTION)) {
result.get(OUTCOME).set(FAILED);
} else {
result.get(OUTCOME).set(SUCCESS);
}
}
}
if (DomainControllerLogger.HOST_CONTROLLER_LOGGER.isTraceEnabled()) {
DomainControllerLogger.HOST_CONTROLLER_LOGGER.tracef("Host result from %s at %s is %s",
fullResult, Arrays.asList(stepLabels), result);
}
return result;
}
private void populateServerGroupResults(final OperationContext context, final Map<ServerIdentity, ModelNode> serverResults) {
final Set<String> groupNames = new TreeSet<String>();
final Map<String, Set<HostServer>> groupToServerMap = new HashMap<String, Set<HostServer>>();
for (Map.Entry<ServerIdentity, ModelNode> entry : serverResults.entrySet()) {
final String serverGroup = entry.getKey().getServerGroupName();
groupNames.add(serverGroup);
final String hostName = entry.getKey().getHostName();
final String serverName = entry.getKey().getServerName();
if (!groupToServerMap.containsKey(serverGroup)) {
groupToServerMap.put(serverGroup, new TreeSet<HostServer>());
}
groupToServerMap.get(serverGroup).add(new HostServer(hostName, serverName, entry.getValue()));
}
boolean serverGroupSuccess = false;
ModelNode failureReport = new ModelNode();
for (String groupName : groupNames) {
final ModelNode groupNode = new ModelNode();
boolean groupFailure = multiphaseContext.isServerGroupRollback(groupName);
if (groupFailure) {
// TODO revisit if we should report this for the whole group, since the result might not be accurate
// groupNode.get(ROLLED_BACK).set(true);
} else {
serverGroupSuccess = true;
}
for (HostServer hostServer : groupToServerMap.get(groupName)) {
groupNode.get(HOST, hostServer.hostName, hostServer.serverName, RESPONSE).set(hostServer.result);
if (groupFailure && hostServer.result.hasDefined(OUTCOME)
&& FAILED.equals(hostServer.result.get(OUTCOME).asString())
&& hostServer.result.hasDefined(FAILURE_DESCRIPTION)) {
ModelNode failDesc = hostServer.result.get(FAILURE_DESCRIPTION);
if (!CompositeOperationHandler.getUnexplainedFailureMessage().equals(failDesc.asString())) {
failureReport.get(SERVER_GROUP, groupName, HOST, hostServer.hostName, hostServer.serverName).set(failDesc);
} // else the server just reported an unexplained composite failure.
// We assume it's due to domain-wide rollback, which is not useful information
// so don't include this server in the top level failure description
}
}
context.getServerResults().get(groupName).set(groupNode);
}
if (!serverGroupSuccess) {
if (failureReport.isDefined()) {
ModelNode fullFailure = new ModelNode();
fullFailure.get(DomainControllerLogger.HOST_CONTROLLER_LOGGER.operationFailedOrRolledBackWithCause()).set(failureReport);
context.getFailureDescription().set(fullFailure);
} else {
context.getFailureDescription().set(DomainControllerLogger.HOST_CONTROLLER_LOGGER.operationFailedOrRolledBack());
}
}
}
// See TODO above
// private void populatePostPrepareHCFailures(OperationContext context) {
// ModelNode hostFailureResults = null;
// for (Map.Entry<String, ModelNode> entry : multiphaseContext.getHostControllerFinalResults().entrySet()) {
// ModelNode hostResult = entry.getValue();
// if (hostResult.hasDefined(FAILURE_DESCRIPTION)) {
// if (hostFailureResults == null) {
// hostFailureResults = new ModelNode();
// }
// hostFailureResults.get(entry.getKey()).set(hostResult.get(FAILURE_DESCRIPTION));
// }
// }
//
// if (hostFailureResults != null) {
// //context.getFailureDescription().get(HOST_FAILURE_DESCRIPTIONS).set(hostFailureResults);
//
// //The following is a workaround for AS7-4597
// //DomainRolloutStepHandler.pushToServers() puts in a simple string into the failure description, but that might be a red herring.
// //If there is a failure description and it is not of type OBJECT, then let's not set it for now
// if (!context.getFailureDescription().isDefined() || context.getFailureDescription().getType() == ModelType.OBJECT) {
// ModelNode fullFailure = new ModelNode();
// fullFailure.get(HOST_FAILURE_DESCRIPTIONS).set(hostFailureResults);
// context.getFailureDescription().set(fullFailure);
// } else {
// DomainControllerLogger.HOST_CONTROLLER_LOGGER.debugf("Failure description is not of type OBJECT '%s'", context.getFailureDescription());
// }
// }
// }
private boolean isDomainOperation(final ModelNode operation) {
final PathAddress address = PathAddress.pathAddress(operation.require(OP_ADDR));
return address.size() == 0 || !address.getElement(0).getKey().equals(HOST);
}
private static class HostServer implements Comparable<HostServer> {
private final String hostName;
private final String serverName;
private final ModelNode result;
private HostServer(String hostName, String serverName, ModelNode result) {
this.hostName = hostName;
this.serverName = serverName;
this.result = result;
}
public int compareTo(HostServer hostServer) {
int hostCompare = hostName.compareTo(hostServer.hostName);
if (hostCompare != 0) {
return hostCompare;
}
return serverName.compareTo(hostServer.serverName);
}
}
private static class ResponseProvider {
private final String host;
private final String server;
private final List<ModelNode> children;
private ResponseProvider(final ModelNode operation, final String localHostName) {
boolean composite = COMPOSITE.equals(operation.require(OP).asString());
PathAddress opAddr = PathAddress.pathAddress(operation.get(OP_ADDR));
int addrSize = opAddr.size();
if (addrSize == 0) {
host = localHostName;
server = null;
} else if (HOST.equals(opAddr.getElement(0).getKey()) && !opAddr.getElement(0).isMultiTarget()) {
host = opAddr.getElement(0).getValue();
if (addrSize > 1 && SERVER.equals(opAddr.getElement(1).getKey())
&& !opAddr.getElement(1).isMultiTarget()) {
server = opAddr.getElement(1).getValue();
//composite = composite && addrSize == 2;
} else {
server = null;
}
// 'composite' is false for any op targeted at 'host=x'
// If address isn't host=x/server=y, then it just isn't a true
// composite op, it's some other op named "composite"
// If address *is* host=x/server=y then in this class'
// processing we can treat the result as a leaf node anyway
composite = false;
} else {
// A domain op
host = localHostName;
server = null;
composite = false;
}
if (composite) {
if (operation.hasDefined(STEPS)) {
children = new ArrayList<ModelNode>(operation.require(STEPS).asList());
} else {
// This shouldn't be possible
children = Collections.emptyList();
}
} else {
children = null;
}
}
private String getHost() {
return host;
}
private String getServer() {
return server;
}
private List<ModelNode> getChildren() {
return children;
}
private boolean isLeaf() {
return children == null;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{host=" + host + ", server=" + server + ", children=" + children + "}";
}
}
}