All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.ejb.client.EJBClientInvocationContext Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 33.0.2.Final
Show newest version
/*
 * 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.ejb.client;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static java.lang.Thread.holdsLock;

/**
 * An invocation context for EJB invocations from an EJB client
 *
 * @author David M. Lloyd
 * @author Jaikiran Pai
 */
public final class EJBClientInvocationContext extends Attachable {

    private static final Logs log = Logs.MAIN;

    public static final String PRIVATE_ATTACHMENTS_KEY = "org.jboss.ejb.client.invocation.attachments";

    // Contextual stuff
    private final EJBInvocationHandler invocationHandler;
    private final EJBClientContext ejbClientContext;

    // Invocation data
    private final Object invokedProxy;
    private final Method invokedMethod;
    private final Object[] parameters;

    // Invocation state
    private final Object lock = new Object();
    private EJBReceiverInvocationContext.ResultProducer resultProducer;
    private State state = State.WAITING;
    private AsyncState asyncState = AsyncState.SYNCHRONOUS;
    private Object cachedResult;
    private Map contextData;
    private EJBReceiverInvocationContext receiverInvocationContext;
    // nodes to which invocation shouldn't be forwarded to. This is typically for cases
    // where a invocation failed on a specific node, for this invocation context, due to the deployment
    // being undeployed after the node was selected for handling this invocation but before the client
    // could get a undeploy notification for it
    private Set excludedNodes = new HashSet();

    // Interceptor state
    private final EJBClientInterceptor[] interceptorChain;
    private int interceptorChainIndex;
    private boolean resultDone;

    EJBClientInvocationContext(final EJBInvocationHandler invocationHandler, final EJBClientContext ejbClientContext, final Object invokedProxy, final Method invokedMethod, final Object[] parameters) {
        this.invocationHandler = invocationHandler;
        this.ejbClientContext = ejbClientContext;
        this.invokedProxy = invokedProxy;
        this.invokedMethod = invokedMethod;
        this.parameters = parameters;
        interceptorChain = (EJBClientInterceptor[]) ejbClientContext.getInterceptorChain();
    }

    enum AsyncState {
        SYNCHRONOUS,
        ASYNCHRONOUS,
        ONE_WAY;
    }

    enum State {
        WAITING,
        CANCEL_REQ,
        CANCELLED,
        READY,
        CONSUMING,
        FAILED,
        DONE,
        DISCARDED;
    }

    /**
     * Get a value attached to the proxy.
     *
     * @param key the attachment key
     * @param  the value type
     * @return the value, or {@code null} if there is none
     */
    public  T getProxyAttachment(AttachmentKey key) {
        return invocationHandler.getAttachment(key);
    }

    /**
     * Remove a value attached to the proxy.
     *
     * @param key the attachment key
     * @param  the value type
     * @return the value, or {@code null} if there is none
     */
    public  T removeProxyAttachment(final AttachmentKey key) {
        return invocationHandler.removeAttachment(key);
    }

    EJBInvocationHandler getInvocationHandler() {
        return invocationHandler;
    }

    /**
     * Get the EJB client context associated with this invocation.
     *
     * @return the EJB client context
     */
    public EJBClientContext getClientContext() {
        return ejbClientContext;
    }

    /**
     * Get the context data.  This same data will be made available verbatim to
     * server-side interceptors via the {@code InvocationContext.getContextData()} method, and thus
     * can be used to pass data from the client to the server (as long as all map values are
     * {@link Serializable}).
     *
     * @return the context data
     */
    public Map getContextData() {
        if (contextData == null) {
            return contextData = new LinkedHashMap();
        } else {
            return contextData;
        }
    }

    /**
     * Get the locator for the invocation target.
     *
     * @return the locator
     */
    public EJBLocator getLocator() {
        return invocationHandler.getLocator();
    }

    /**
     * Proceed with sending the request normally.
     *
     * @throws Exception if the request was not successfully sent
     */
    public void sendRequest() throws Exception {
        final int idx = this.interceptorChainIndex++;
        final EJBClientInterceptor[] chain = interceptorChain;
        if (idx > chain.length) {
            throw Logs.MAIN.sendRequestCalledDuringWrongPhase();
        }
        if (chain.length == idx) {
            final EJBReceiverInvocationContext context = receiverInvocationContext;
            if (context == null) {
                throw Logs.MAIN.noReceiverAssociatedWithInvocation();
            }
            context.getEjbReceiverContext().getReceiver().processInvocation(this, context);
        } else {
            chain[idx].handleInvocation(this);
        }
    }

    /**
     * Retry a request which was previously sent but probably failed with a error response.
     * Note that this method will throw an {@link IllegalStateException} if a previous request wasn't completed
     * for this invocation context
     *
     * @throws Exception if the retry request was not successfully sent
     */
    void retryRequest() throws Exception {
        // if a previous request wasn't yet done, then this isn't really a "retry",
        // so we error out
        final int idx = this.interceptorChainIndex;
        final EJBClientInterceptor[] chain = interceptorChain;
        if (idx <= chain.length) {
            throw Logs.MAIN.cannotRetryRequest();
        }
        // reset the interceptor index to the beginning of the chain
        this.interceptorChainIndex = 0;
        // reset the previously set receiver invocation context, since a possibly new receiver
        // will be selected during this retry
        this.receiverInvocationContext = null;
        // send request
        this.sendRequest();
    }

    /**
     * Returns a set of nodes which shouldn't be used for handling the invocation represented by this
     * invocation context. If there are no such nodes, then this method returns an empty set.
     * 

* Typically a node is marked as excluded for a {@link EJBClientInvocationContext invocation context} * if it failed to handle that specific {@link EJBClientInvocationContext invocation}. This can happen, * for example, if a node was selected for invocation handling and was later incapable of handling it * due to the deployment being un-deployed. The invocation in such cases will typically be retried by * marking that node as excluded and selecting a different node (if any) for the retried invocation. * * @return */ Set getExcludedNodes() { return Collections.unmodifiableSet(this.excludedNodes); } /** * Marks the passed nodeName as excluded so that the invocation represented by * this {@link EJBClientInvocationContext invocation context} will leave it out while selecting a suitable node for * handling the invocation * * @param nodeName The name of the node to be excluded */ void markNodeAsExcluded(final String nodeName) { if (nodeName == null || nodeName.trim().isEmpty()) { return; } this.excludedNodes.add(nodeName); } void setReceiverInvocationContext(EJBReceiverInvocationContext context) { receiverInvocationContext = context; } /** * Get the invocation result from this request. The result is not actually acquired unless all interceptors * call this method. Should only be called from {@link EJBClientInterceptor#handleInvocationResult(EJBClientInvocationContext)}. * * @return the invocation result * @throws Exception if the invocation did not succeed */ public Object getResult() throws Exception { final EJBReceiverInvocationContext.ResultProducer resultProducer = this.resultProducer; if (resultDone || resultProducer == null) { throw Logs.MAIN.getResultCalledDuringWrongPhase(); } final int idx = this.interceptorChainIndex++; final EJBClientInterceptor[] chain = interceptorChain; if (idx == 0) try { return chain[idx].handleInvocationResult(this); } finally { resultDone = true; final Affinity weakAffinity = getAttachment(AttachmentKeys.WEAK_AFFINITY); if (weakAffinity != null) { invocationHandler.setWeakAffinity(weakAffinity); } } else try { if (chain.length == idx) { if(System.getSecurityManager() == null) { return resultProducer.getResult(); } else { try { return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public Object run() throws Exception { return resultProducer.getResult(); } }); } catch (PrivilegedActionException e) { throw e.getException(); } } } else { return chain[idx].handleInvocationResult(this); } } finally { resultDone = true; } } /** * Discard the result from this request. Should only be called from {@link EJBClientInterceptor#handleInvocationResult(EJBClientInvocationContext)}. * * @throws IllegalStateException if there is no result to discard */ public void discardResult() throws IllegalStateException { final EJBReceiverInvocationContext.ResultProducer resultProducer = this.resultProducer; if (resultProducer == null) { throw Logs.MAIN.discardResultCalledDuringWrongPhase(); } resultProducer.discardResult(); } void resultReady(EJBReceiverInvocationContext.ResultProducer resultProducer) { synchronized (lock) { switch (state) { case WAITING: case CANCEL_REQ: { this.resultProducer = resultProducer; interceptorChainIndex = 0; state = State.READY; lock.notifyAll(); return; } } } // for whatever reason, we don't care resultProducer.discardResult(); return; } /** * Get the EJB receiver associated with this invocation. * * @return the EJB receiver */ protected EJBReceiver getReceiver() { final EJBReceiverInvocationContext context = receiverInvocationContext; if (context == null) { throw Logs.MAIN.noReceiverAssociatedWithInvocation(); } return context.getEjbReceiverContext().getReceiver(); } /** * Get the invoked proxy object. * * @return the invoked proxy */ public Object getInvokedProxy() { return invokedProxy; } /** * Get the invoked proxy method. * * @return the invoked method */ public Method getInvokedMethod() { return invokedMethod; } /** * Get the invocation method parameters. * * @return the invocation method parameters */ public Object[] getParameters() { return parameters; } /** * Get the invoked view class. * * @return the invoked view class */ public Class getViewClass() { return invocationHandler.getLocator().getViewType(); } Future getFutureResponse() { return new FutureResponse(); } static final Object PROCEED_ASYNC = new Object(); void proceedAsynchronously() { assert !holdsLock(lock); synchronized (lock) { if (asyncState == AsyncState.SYNCHRONOUS) { asyncState = AsyncState.ASYNCHRONOUS; lock.notifyAll(); } } } Object awaitResponse() throws Exception { assert !holdsLock(lock); boolean intr = false; final EJBClientConfiguration ejbClientConfiguration = this.ejbClientContext.getEJBClientConfiguration(); final long invocationTimeout = ejbClientConfiguration == null ? 0 : ejbClientConfiguration.getInvocationTimeout(); try { synchronized (lock) { if (asyncState == AsyncState.ASYNCHRONOUS) { return PROCEED_ASYNC; } else if (asyncState == AsyncState.ONE_WAY) { throw log.oneWayInvocation(); } long remainingWaitTimeout = invocationTimeout; long waitStartTime = System.currentTimeMillis(); while (state == State.WAITING) { try { // if no invocation timeout is configured, then we wait indefinitely if (invocationTimeout <= 0) { lock.wait(); } else { waitStartTime = System.currentTimeMillis(); // we wait for a specific amount of time lock.wait(remainingWaitTimeout); } } catch (InterruptedException e) { intr = true; // if there was a invocation timeout configured and the thread was interrupted // then figure out how long we waited and what remaining time we should wait for // if the result hasn't yet arrived if (invocationTimeout > 0) { final long timeWaitedFor = System.currentTimeMillis() - waitStartTime; // we already waited enough, so setup a result producer which will // let the client know that the invocation timed out if (timeWaitedFor >= remainingWaitTimeout) { // setup a invocation timeout result producer this.resultReady(new InvocationTimeoutResultProducer(invocationTimeout)); break; } else { remainingWaitTimeout = remainingWaitTimeout - timeWaitedFor; } } continue; } if (asyncState == AsyncState.ASYNCHRONOUS) { // It's an asynchronous invocation; proceed asynchronously. return PROCEED_ASYNC; } else if (asyncState == AsyncState.ONE_WAY) { throw log.oneWayInvocation(); } // If the state is still waiting and the invocation timeout was specified, // then it indicates that the Object.wait(timeout) returned due to a timeout. if (state == State.WAITING && invocationTimeout > 0) { // setup a invocation timeout result producer this.resultReady(new InvocationTimeoutResultProducer(invocationTimeout)); break; } } } return getResult(); } finally { if (intr) Thread.currentThread().interrupt(); } } void setDiscardResult() { assert !holdsLock(lock); final EJBReceiverInvocationContext.ResultProducer resultProducer; synchronized (lock) { if (asyncState != AsyncState.ONE_WAY) { asyncState = AsyncState.ONE_WAY; lock.notifyAll(); } if (state != State.DONE) { return; } // result is waiting, discard it state = State.DISCARDED; resultProducer = this.resultProducer; lock.notifyAll(); // fall out of the lock to discard the result } resultProducer.discardResult(); } void cancelled() { assert !holdsLock(lock); synchronized (lock) { switch (state) { case WAITING: case CANCEL_REQ: { state = State.CANCELLED; lock.notifyAll(); break; } } } } void failed(Throwable exception) { assert !holdsLock(lock); synchronized (lock) { switch (state) { case WAITING: case CANCEL_REQ: { state = State.FAILED; cachedResult = exception; lock.notifyAll(); break; } } } } final class FutureResponse implements Future { public boolean cancel(final boolean mayInterruptIfRunning) { assert !holdsLock(lock); synchronized (lock) { if (state != State.WAITING) { return false; } // if we aren't allowed to interrupt a running task, then skip the cancellation if (!mayInterruptIfRunning) { return false; } // at this point the task is running and we are allowed to interrupt it. So issue // a cancel request and change the current state state = State.CANCEL_REQ; } return getReceiver().cancelInvocation(EJBClientInvocationContext.this, receiverInvocationContext); } public boolean isCancelled() { assert !holdsLock(lock); synchronized (lock) { return state == State.CANCELLED; } } public boolean isDone() { assert !holdsLock(lock); synchronized (lock) { switch (state) { case WAITING: { return false; } case CANCEL_REQ: case READY: case FAILED: case CANCELLED: case DONE: case CONSUMING: case DISCARDED: { return true; } default: throw new IllegalStateException(); } } } public Object get() throws InterruptedException, ExecutionException { assert !holdsLock(lock); final EJBReceiverInvocationContext.ResultProducer resultProducer; synchronized (lock) { while (state == State.WAITING || state == State.CANCEL_REQ || state == State.CONSUMING) lock.wait(); switch (state) { case READY: { // Change state to consuming, but don't notify since nobody but us can act on it. // Instead we'll notify after the result is consumed. state = State.CONSUMING; resultProducer = EJBClientInvocationContext.this.resultProducer; // we have to get the result, so break out of here. break; } case FAILED: { throw log.remoteInvFailed((Throwable) cachedResult); } case CANCELLED: { throw log.requestCancelled(); } case DONE: { return cachedResult; } case DISCARDED: { throw log.oneWayInvocation(); } default: throw new IllegalStateException(); } } // extract the result from the producer. Object result; try { result = resultProducer.getResult(); } catch (Exception e) { synchronized (lock) { assert state == State.CONSUMING; state = State.FAILED; cachedResult = e; lock.notifyAll(); } throw log.remoteInvFailed(e); } synchronized (lock) { assert state == State.CONSUMING; state = State.DONE; cachedResult = result; lock.notifyAll(); } return result; } public Object get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { assert !holdsLock(lock); final EJBReceiverInvocationContext.ResultProducer resultProducer; synchronized (lock) { if (state == State.WAITING || state == State.CANCEL_REQ || state == State.CONSUMING) { long now = System.nanoTime(); final long end = Math.max(now, now + unit.toNanos(timeout)); do { final long remaining = end - now; if (remaining <= 0L) { throw log.timedOut(); } // wait at least 1ms long millis = (remaining + 999999L) / 1000000L; lock.wait(millis); now = System.nanoTime(); } while (state == State.WAITING || state == State.CANCEL_REQ || state == State.CONSUMING); } switch (state) { case READY: { // Change state to consuming, but don't notify since nobody but us can act on it. // Instead we'll notify after the result is consumed. state = State.CONSUMING; resultProducer = EJBClientInvocationContext.this.resultProducer; // we have to get the result, so break out of here. break; } case FAILED: { throw log.remoteInvFailed((Throwable) cachedResult); } case CANCELLED: { throw log.requestCancelled(); } case DONE: { return cachedResult; } case DISCARDED: { throw log.oneWayInvocation(); } default: throw new IllegalStateException(); } } // extract the result from the producer. Object result; try { result = resultProducer.getResult(); } catch (Exception e) { synchronized (lock) { assert state == State.CONSUMING; state = State.FAILED; cachedResult = e; lock.notifyAll(); } throw log.remoteInvFailed(e); } synchronized (lock) { assert state == State.CONSUMING; state = State.DONE; cachedResult = result; lock.notifyAll(); } return result; } } protected void finalize() throws Throwable { assert !holdsLock(lock); final EJBReceiverInvocationContext.ResultProducer resultProducer; synchronized (lock) { switch (state) { case WAITING: case CANCEL_REQ: { // the receiver lost its reference to us AND no callers actually seem to care. // There's not a whole lot to do at this point. return; } case READY: { // there's a result waiting. We shall simply discard it (outside of the lock though), to make // sure that the transport layer doesn't have resources (like sockets) which are held up. resultProducer = this.resultProducer; break; } default: { // situation normal, or close enough. return; } } } resultProducer.discardResult(); } /** * A {@link org.jboss.ejb.client.EJBReceiverInvocationContext.ResultProducer} which throws a * {@link TimeoutException} to indicate that the client invocation has timed out waiting for a response */ private class InvocationTimeoutResultProducer implements EJBReceiverInvocationContext.ResultProducer { private final long timeout; InvocationTimeoutResultProducer(final long timeout) { this.timeout = timeout; } @Override public Object getResult() throws Exception { throw new TimeoutException("No invocation response received in " + this.timeout + " milliseconds"); } @Override public void discardResult() { } } }