/* * JBoss, Home of Professional Open Source. * Copyright 2012, 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.controller.remote; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CANCELLED; 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.OUTCOME; import static org.jboss.as.controller.logging.ControllerLogger.MGMT_OP_LOGGER; import static org.jboss.as.controller.logging.ControllerLogger.ROOT_LOGGER; import java.io.DataInput; import java.io.IOException; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collections; import java.util.concurrent.CountDownLatch; import org.jboss.as.controller.AccessAuditContext; import org.jboss.as.controller.ModelController; import org.jboss.as.controller.client.Operation; import org.jboss.as.controller.client.OperationMessageHandler; import org.jboss.as.controller.client.OperationResponse; import org.jboss.as.controller.client.impl.ModelControllerProtocol; import org.jboss.as.controller.logging.ControllerLogger; import org.jboss.as.protocol.StreamUtils; import org.jboss.as.protocol.mgmt.ActiveOperation; import org.jboss.as.protocol.mgmt.FlushableDataOutput; import org.jboss.as.protocol.mgmt.ManagementChannelAssociation; import org.jboss.as.protocol.mgmt.ManagementProtocol; import org.jboss.as.protocol.mgmt.ManagementProtocolHeader; import org.jboss.as.protocol.mgmt.ManagementRequestContext; import org.jboss.as.protocol.mgmt.ManagementRequestContext.AsyncTask; import org.jboss.as.protocol.mgmt.ManagementRequestHandler; import org.jboss.as.protocol.mgmt.ManagementRequestHandlerFactory; import org.jboss.as.protocol.mgmt.ManagementRequestHeader; import org.jboss.as.protocol.mgmt.ManagementResponseHeader; import org.jboss.as.protocol.mgmt.ProtocolUtils; import org.jboss.dmr.ModelNode; import org.wildfly.security.auth.server.SecurityIdentity; /** * The transactional request handler for a remote {@link TransactionalProtocolClient}. * * @author <a href="kabir.khan@jboss.com">Kabir Khan</a> * @author Emanuel Muckenhuber * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class TransactionalProtocolOperationHandler implements ManagementRequestHandlerFactory { private final ModelController controller; private final ManagementChannelAssociation channelAssociation; private final ResponseAttachmentInputStreamSupport responseAttachmentSupport; public TransactionalProtocolOperationHandler(final ModelController controller, final ManagementChannelAssociation channelAssociation, final ResponseAttachmentInputStreamSupport responseAttachmentSupport) { this.controller = controller; this.channelAssociation = channelAssociation; this.responseAttachmentSupport = responseAttachmentSupport; } @Override public ManagementRequestHandler<?, ?> resolveHandler(RequestHandlerChain handlers, ManagementRequestHeader request) { switch(request.getOperationId()) { case ModelControllerProtocol.EXECUTE_TX_REQUEST: { // Initialize the request context final ExecuteRequestContext executeRequestContext = new ExecuteRequestContext(responseAttachmentSupport); try { executeRequestContext.operation = handlers.registerActiveOperation(request.getBatchId(), executeRequestContext, executeRequestContext); } catch (IllegalStateException ise) { // WFLY-3381 Unusual case where the initial request lost a race with a COMPLETE_TX_REQUEST carrying a cancellation return new AbortOperationHandler(true); } return new ExecuteRequestHandler(); } case ModelControllerProtocol.COMPLETE_TX_REQUEST: { final ExecuteRequestContext executeRequestContext = new ExecuteRequestContext(responseAttachmentSupport); try { executeRequestContext.operation = handlers.registerActiveOperation(request.getBatchId(), executeRequestContext, executeRequestContext); // WLFY-3381 Unusual case where the initial request must have lost a race with a COMPLETE_TX_REQUEST carrying a cancellation return new AbortOperationHandler(false); } catch (IllegalStateException ise) { // Expected case -- not a normal commit/rollback or one where a COMPLETE_TX_REQUEST with a cancel // won a race with the initial request } return new CompleteTxOperationHandler(); } case ModelControllerProtocol.GET_CHUNKED_INPUTSTREAM_REQUEST: { // initialize the operation ctx before executing the request handler handlers.registerActiveOperation(request.getBatchId(), null); return responseAttachmentSupport.getReadHandler(); } case ModelControllerProtocol.CLOSE_INPUTSTREAM_REQUEST: { // initialize the operation ctx before executing the request handler handlers.registerActiveOperation(request.getBatchId(), null); return responseAttachmentSupport.getCloseHandler(); } } return handlers.resolveNext(); } /** * The request handler for requests from {@link org.jboss.as.controller.remote.TransactionalProtocolClient#execute}. */ private class ExecuteRequestHandler implements ManagementRequestHandler<Void, ExecuteRequestContext> { @Override public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<Void> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException { ControllerLogger.MGMT_OP_LOGGER.tracef("Handling transactional ExecuteRequest for %d", context.getOperationId()); final ExecutableRequest executableRequest = ExecutableRequest.parse(input, channelAssociation); final PrivilegedAction<Void> action = new PrivilegedAction<Void>() { @Override public Void run() { doExecute(executableRequest.operation, executableRequest.attachmentsLength, context); return null; } }; // Set the response information and execute the operation final ExecuteRequestContext executeRequestContext = context.getAttachment(); executeRequestContext.initialize(context); @SuppressWarnings("deprecation") AsyncTask<TransactionalProtocolOperationHandler.ExecuteRequestContext> task = new ManagementRequestContext.MultipleResponseAsyncTask<TransactionalProtocolOperationHandler.ExecuteRequestContext>() { @Override public void execute(final ManagementRequestContext<ExecuteRequestContext> context) throws Exception { AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { // TODO Elytron - Inflow the remote address. AccessAuditContext.doAs(executableRequest.securityIdentity, null, action); return null; } }); } @Override public ManagementProtocolHeader getCurrentRequestHeader() { ManagementRequestContext current = executeRequestContext.responseChannel; return current == null ? null : current.getRequestHeader(); } }; context.executeAsync(task); } protected void doExecute(final ModelNode operation, final int attachmentsLength, final ManagementRequestContext<ExecuteRequestContext> context) { ControllerLogger.MGMT_OP_LOGGER.tracef("Executing transactional ExecuteRequest for %d", context.getOperationId()); final ExecuteRequestContext executeRequestContext = context.getAttachment(); // Set the response information executeRequestContext.initialize(context); final Integer batchId = executeRequestContext.getOperationId(); final OperationMessageHandlerProxy messageHandlerProxy = new OperationMessageHandlerProxy(channelAssociation, batchId); final ProxyOperationTransactionControl control = new ProxyOperationTransactionControl(executeRequestContext); final OperationAttachmentsProxy attachmentsProxy = OperationAttachmentsProxy.create(operation, channelAssociation, batchId, attachmentsLength); final OperationResponse result; try { // Execute the operation result = internalExecute(attachmentsProxy, context, messageHandlerProxy, control); } catch (Throwable t) { final ModelNode failure = new ModelNode(); failure.get(OUTCOME).set(FAILED); failure.get(FAILURE_DESCRIPTION).set(t.getClass().getName() + ":" + t.getMessage()); executeRequestContext.failed(failure); attachmentsProxy.shutdown(); ControllerLogger.MGMT_OP_LOGGER.unexpectedOperationExecutionException(t, Collections.singletonList(operation)); return; } // At this point the transactional request either failed prior to preparing the transaction, // or it has completed. if (!executeRequestContext.prepared) { // If internalExecute did not result in a prepare, it failed executeRequestContext.failed(result.getResponseNode()); } else { executeRequestContext.completed(result); } } } private static class ExecutableRequest { private final ModelNode operation; private final int attachmentsLength; private final SecurityIdentity securityIdentity; private ExecutableRequest(ModelNode operation, int attachmentsLength, SecurityIdentity securityIdentity) { this.operation = operation; this.attachmentsLength = attachmentsLength; this.securityIdentity = securityIdentity; } static ExecutableRequest parse(DataInput input, ManagementChannelAssociation channelAssociation) throws IOException { final ModelNode operation = new ModelNode(); ProtocolUtils.expectHeader(input, ModelControllerProtocol.PARAM_OPERATION); operation.readExternal(input); ProtocolUtils.expectHeader(input, ModelControllerProtocol.PARAM_INPUTSTREAMS_LENGTH); final int attachmentsLength = input.readInt(); // TODO Elytron At this point we should be inflowing the identity of the user that has triggered this operation. return new ExecutableRequest(operation, attachmentsLength, channelAssociation.getChannel().getConnection().getLocalIdentity()); } } /** * Subclasses can override this method to determine how to execute the method, e.g. attach to an existing operation or not * * @param operation the operation being executed * @param messageHandler the operation message handler proxy * @param control the operation transaction control * @return the result of the executed operation */ protected OperationResponse internalExecute(final Operation operation, final ManagementRequestContext<?> context, final OperationMessageHandler messageHandler, final ModelController.OperationTransactionControl control) { // Execute the operation return controller.execute( operation, messageHandler, control); } /** * The request handler for requests from {@link org.jboss.as.controller.remote.TransactionalProtocolClientImpl.CompleteTxRequest} */ private class CompleteTxOperationHandler implements ManagementRequestHandler<Void, ExecuteRequestContext> { @Override public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<Void> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException { final ExecuteRequestContext executeRequestContext = context.getAttachment(); final byte commitOrRollback = input.readByte(); // Complete transaction, either commit or rollback executeRequestContext.completeTx(context, commitOrRollback == ModelControllerProtocol.PARAM_COMMIT); } } private class AbortOperationHandler implements ManagementRequestHandler<Void, ExecuteRequestContext> { private final boolean forExecuteTxRequest; private AbortOperationHandler(boolean forExecuteTxRequest) { this.forExecuteTxRequest = forExecuteTxRequest; } @Override public void handleRequest(final DataInput input, final ActiveOperation.ResultHandler<Void> resultHandler, final ManagementRequestContext<ExecuteRequestContext> context) throws IOException { if (forExecuteTxRequest) { try { // Read and discard the input ExecutableRequest.parse(input, channelAssociation); } finally { ControllerLogger.MGMT_OP_LOGGER.tracef("aborting (cancel received before request) for %d", context.getOperationId()); ModelNode response = new ModelNode(); response.get(OUTCOME).set(CANCELLED); context.getAttachment().initialize(context); context.getAttachment().failed(response); } } else { // This was a COMPLETE_TX_REQUEST that came in before the original EXECUTE_TX_REQUEST final byte commitOrRollback = input.readByte(); if (commitOrRollback == ModelControllerProtocol.PARAM_COMMIT) { // a cancel would not use PARAM_COMMIT; this was a request that didn't match any existing op // Likely the request was cancelled and removed but the commit message was in process throw ControllerLogger.MGMT_OP_LOGGER.responseHandlerNotFound(context.getOperationId()); } // else this was a cancel request. Do nothing and wait for the initial operation request to come in // and see the pre-existing ActiveOperation and then call this with forExecuteTxRequest=true } } } /** * OperationTransactionControl that handle operationPrepared by signalling the ExecutionRequestContext * associated with the active operation. */ private static class ProxyOperationTransactionControl implements ModelController.OperationTransactionControl { private final ExecuteRequestContext requestContext; ProxyOperationTransactionControl(ExecuteRequestContext requestContext) { this.requestContext = requestContext; } @Override public void operationPrepared(final ModelController.OperationTransaction transaction, final ModelNode result) { requestContext.prepare(transaction, result); try { // Wait for the commit or rollback message requestContext.txCompletedLatch.await(); } catch (InterruptedException e) { // requestContext.getResultHandler().failed(e); ROOT_LOGGER.tracef("Clearing interrupted status from client request %d", requestContext.getOperationId()); Thread.currentThread().interrupt(); } } } /** * Attachment to a ManagementRequestContext that handlers dealing with the various requests * associated with a single overall operation can use to coordinate execution. */ private static class ExecuteRequestContext implements ActiveOperation.CompletedCallback<Void> { /** The operation being executed */ private ActiveOperation<Void, ExecuteRequestContext> operation; /** * Whether the prepare method has been invoked. Once the initial response to the remote side goes out, * then {@code responseChannel} will be set to null */ private boolean prepared; /** * True if we get a compleTx call before the prepare method is invoked. * This would mean the op was cancelled on the remote side. */ private boolean rollbackOnPrepare; /** * The tx provided to prepare. */ private ModelController.OperationTransaction activeTx; /** * Not null if we owe the remote side a response; null if the currently expected response has gone out. * Use this to track whether a response has been sent to the remote side. */ private ManagementRequestContext<ExecuteRequestContext> responseChannel; /** * The thread that calls the prepare method will block on this so it eventually must be tripped once prepare is called */ private final CountDownLatch txCompletedLatch = new CountDownLatch(1); /** * Set to true once the prepare method has been invoked and a completeTx call is received. * Used to detect a second completeTx call coming in to cancel the op the final response goes out. */ private boolean txCompleted; /** * A final response to the op that was passed to complete after the op had sent a prepare message * to the remote side but before any completeTx message was received. This indicates a failure or * local cancellation while waiting for completeTx. The final response is cached in this field * so it can be sent to the client once the completeTx call comes in. Until that happens there * is no responseChannel available to send the response, as the prepare message has completed the * request/response pair from the initial request */ private OperationResponse postPrepareRaceResponse; /** Support object for managing any streams associated with the response */ final ResponseAttachmentInputStreamSupport streamSupport; ExecuteRequestContext(final ResponseAttachmentInputStreamSupport streamSupport) { this.streamSupport = streamSupport; } Integer getOperationId() { return operation.getOperationId(); } ActiveOperation.ResultHandler<Void> getResultHandler() { return operation.getResultHandler(); } @Override public void completed(Void result) { // } @Override public synchronized void failed(Exception e) { if(prepared) { final ModelController.OperationTransaction transaction = activeTx; activeTx = null; if(transaction != null) { try { transaction.rollback(); } finally { txCompletedLatch.countDown(); } } } else if (responseChannel != null) { rollbackOnPrepare = true; // Failed in a step before prepare, send error response final String message = e.getMessage() != null ? e.getMessage() : "failure before rollback " + e.getClass().getName(); final ModelNode response = new ModelNode(); response.get(OUTCOME).set(FAILED); response.get(FAILURE_DESCRIPTION).set(message); ControllerLogger.MGMT_OP_LOGGER.tracef("sending pre-prepare failed response for %d --- interrupted: %s", getOperationId(), (Object) Thread.currentThread().isInterrupted()); try { sendResponse(responseChannel, ModelControllerProtocol.PARAM_OPERATION_FAILED, response); responseChannel = null; } catch (IOException ignored) { ControllerLogger.MGMT_OP_LOGGER.failedSendingFailedResponse(ignored, response, getOperationId()); } } } @Override public void cancelled() { // } /** * Initializes this object with a reference to the {@code ManagementRequestContext} it can * use for executing asynchronous tasks and sending messages to the remote client. * @param context the ManagementRequestContext */ synchronized void initialize(final ManagementRequestContext<ExecuteRequestContext> context) { assert ! prepared; assert activeTx == null; this.responseChannel = context; ControllerLogger.MGMT_OP_LOGGER.tracef("Initialized for %d", getOperationId()); } /** Signal from ProxyOperationTransactionControl that the operation is prepared */ synchronized void prepare(final ModelController.OperationTransaction tx, final ModelNode result) { assert !prepared; prepared = true; if(rollbackOnPrepare) { try { tx.rollback(); ControllerLogger.MGMT_OP_LOGGER.tracef("rolled back on prepare for %d --- interrupted: %s", getOperationId(), (Object) Thread.currentThread().isInterrupted()); } finally { txCompletedLatch.countDown(); } // no response to remote side here; the response will go out when this thread executing // the now rolled-back op returns in ExecuteRequestHandler.doExecute } else { assert activeTx == null; assert responseChannel != null; activeTx = tx; ControllerLogger.MGMT_OP_LOGGER.tracef("sending prepared response for %d --- interrupted: %s", getOperationId(), (Object) Thread.currentThread().isInterrupted()); try { sendResponse(responseChannel, ModelControllerProtocol.PARAM_OPERATION_PREPARED, result); responseChannel = null; // we've now sent a response to the original request, so we can't use this one further } catch (IOException e) { getResultHandler().failed(e); // this will eventually call back into failed(e) above and roll back the tx } } } /** Invoked when we receive a ModelControllerProtocol.COMPLETE_TX_REQUEST request, either a tx commit/rollback or a cancel */ synchronized void completeTx(final ManagementRequestContext<ExecuteRequestContext> context, final boolean commit) { if (!prepared) { assert !commit; // can only be cancel before it's prepared ControllerLogger.MGMT_OP_LOGGER.tracef("completeTx (cancel unprepared) for %d", getOperationId()); rollbackOnPrepare = true; cancel(context); // response is sent when the cancalled op results in the thead returning in ExecuteRequestHandler.doExecute } else if (txCompleted) { // A 2nd call means a cancellation from the remote side after the tx was committed/rolled back // This would usually mean the completion of the request is hanging for some reason ControllerLogger.MGMT_OP_LOGGER.tracef("completeTx (post-commit cancel) for %d", getOperationId()); cancel(context); } else if (postPrepareRaceResponse == null) { txCompleted = true; if (activeTx != null) { try { assert responseChannel == null; responseChannel = context; ControllerLogger.MGMT_OP_LOGGER.tracef("completeTx (%s) for %d", commit, getOperationId()); if (commit) { activeTx.commit(); } else { activeTx.rollback(); } } finally { txCompletedLatch.countDown(); } } // else when the prepare call came in rollbackOnPrepare was true. That means this was // a 2nd cancellation request. We already cancelled in the if (!prepared) block above // when the first request came in and doing it again will do nothing, so ignore this. } else { assert responseChannel == null; responseChannel = context; ControllerLogger.MGMT_OP_LOGGER.tracef("completeTx (%s) for %d received after a post-prepare response " + "had already been cached; sending the cached response", commit, getOperationId()); completed(postPrepareRaceResponse); } } /** * Handles sending a failure response to the remote client. If operation has already been prepared * this method simply delegates to {@link #completed(OperationResponse)}. * @param response the failure response ModelNode to send */ synchronized void failed(final ModelNode response) { if(prepared) { // in case commit or rollback throws an exception, to conform with the API we still send an operation-completed message completed(OperationResponse.Factory.createSimple(response)); } else { // Failure before prepare. So send a response to the original request assert responseChannel != null; ControllerLogger.MGMT_OP_LOGGER.tracef("sending pre-prepare failed response for %d --- interrupted: %s", getOperationId(), (Object) Thread.currentThread().isInterrupted()); try { sendResponse(responseChannel, ModelControllerProtocol.PARAM_OPERATION_FAILED, response); responseChannel = null; // we've now sent a response to the original request, so we can't use this one further } catch (IOException e) { ControllerLogger.MGMT_OP_LOGGER.failedSendingFailedResponse(e, response, getOperationId()); } finally { getResultHandler().done(null); } } } /** * Sends the final response to the remote client after the prepare phase has been executed. * This should be called whether the outcome was successful or not. */ synchronized void completed(final OperationResponse response) { assert prepared; if (responseChannel != null) { // Normal case, where a COMPLETE_TX_REQUEST came in after the prepare() call and // established a new responseChannel so the client can correlate the response ControllerLogger.MGMT_OP_LOGGER.tracef("sending completed response %s for %d --- interrupted: %s", response.getResponseNode(), getOperationId(), Thread.currentThread().isInterrupted()); streamSupport.registerStreams(operation.getOperationId(), response.getInputStreams()); try { sendResponse(responseChannel, ModelControllerProtocol.PARAM_OPERATION_COMPLETED, response.getResponseNode()); responseChannel = null; // we've now sent a response to the COMPLETE_TX_REQUEST, so we can't use this one further } catch (IOException e) { ControllerLogger.MGMT_OP_LOGGER.failedSendingCompletedResponse(e, response.getResponseNode(), getOperationId()); } finally { getResultHandler().done(null); } } else { // We were cancelled or somehow failed after sending our prepare() message but before we got a COMPLETE_TX_REQUEST. // The client will not be able to deal with any response until it sends a COMPLETE_TX_REQUEST // (which is why we null out responseChannel in prepare()). So, just cache this response // so we can send it in completeTx when the COMPLETE_TX_REQUEST comes in. assert postPrepareRaceResponse == null; // else we got called twice locally! ControllerLogger.MGMT_OP_LOGGER.tracef("received a post-prepare response for %d but no " + "COMPLETE_TX_REQUEST has been received; caching the response", getOperationId()); postPrepareRaceResponse = response; } } /** Asynchronously invokes cancel on the result handler for the operation */ private void cancel(final ManagementRequestContext<ExecuteRequestContext> context) { context.executeAsync(new ManagementRequestContext.AsyncTask<ExecuteRequestContext>() { @Override public void execute(ManagementRequestContext<ExecuteRequestContext> executeRequestContextManagementRequestContext) throws Exception { operation.getResultHandler().cancel(); } }, false); } } /** * Send an operation response. * * @param context the request context * @param responseType the response type * @param response the operation response * @throws java.io.IOException for any error */ static void sendResponse(final ManagementRequestContext<ExecuteRequestContext> context, final byte responseType, final ModelNode response) throws IOException { // WFLY-3090 Protect the communication channel from getting closed due to administrative // cancellation of the management op by using a separate thread to send final CountDownLatch latch = new CountDownLatch(1); final IOExceptionHolder exceptionHolder = new IOExceptionHolder(); boolean accepted = context.executeAsync(new AsyncTask<TransactionalProtocolOperationHandler.ExecuteRequestContext>() { @Override public void execute(final ManagementRequestContext<ExecuteRequestContext> context) throws Exception { FlushableDataOutput output = null; try { MGMT_OP_LOGGER.tracef("Transmitting response for %d", context.getOperationId()); final ManagementResponseHeader header = ManagementResponseHeader.create(context.getRequestHeader()); output = context.writeMessage(header); // response type output.writeByte(responseType); // operation result response.writeExternal(output); // response end output.writeByte(ManagementProtocol.RESPONSE_END); output.close(); } catch (IOException toCache) { exceptionHolder.exception = toCache; } finally { StreamUtils.safeClose(output); latch.countDown(); } } }, false); if (accepted) { try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (exceptionHolder.exception != null) { throw exceptionHolder.exception; } } } private static class IOExceptionHolder { private IOException exception; } }