ServiceCaller.java |
/* * $Id: ServiceCaller.java,v 1.82 2012/04/16 19:40:39 agoubard Exp $ * * See the COPYRIGHT file for redistribution and use restrictions. */ package org.xins.common.service; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.xins.common.Log; import org.xins.common.MandatoryArgumentChecker; import org.xins.common.TimeOutController; import org.xins.common.TimeOutException; import org.xins.common.Utils; /** * Abstraction of a service caller for a TCP-based service. Service caller * implementations can be used to perform a call to a service, and potentially * fail-over to other back-ends if one is not available. * * <a name="section-descriptors"></a> * <h2>Descriptors</h2> * * <p>A service caller has a link to a {@link Descriptor} instance, which * describes which back-ends to call. A <code>Descriptor</code> describes * either a single back-end or a group of back-ends. A single back-end is * represented by a {@link TargetDescriptor} instance, while a groups of * back-ends is represented by a {@link GroupDescriptor} instance. Both are * subclasses of class <code>Descriptor</code>. * * <p>There is only one type of target descriptor, but there are * different types of group descriptor: * * <ul> * <li><em>ordered</em>: underlying descriptors are iterated over in * sequential order; * <li><em>random</em>: underlying descriptors are iterated over in random * order. * </ul> * * <p>Note that group descriptors may contain other group descriptors. * * <a name="section-timeouts"></a> * <h2>Time-outs</h2> * * <p>Target descriptors support three kinds of time-out: * * <ul> * <li><em>total time-out</em>: limits the duration of a call, * including connection time, time used to send the request, time used to * receive the response, etcetera; * <li><em>connection time-out</em>: limits the time for attempting to * establish a connection; * <li><em>socket time-out</em>: limits the time for attempting to receive * data on a socket. * </ul> * * <a name="section-lbfo"></a> * <h2>Load-balancing and fail-over</h2> * * Service callers can help in evenly distributing processing across * available resources. This load-balancing is achieved by using a group * descriptor which iterates over the underlying descriptors in a * <em>random</em> order. * * <p>Unlike load-balancing, fail-over allows the detection of a failure and * the migration of the processing to a similar, redundant back-end. This can * be achieved using any type of group descriptor (either <em>ordered</em> or * <em>random</em>). * * <p>Not all calls should be retried on a different back-end. For example, if * a call fails because the back-end indicates the request is considered * incorrect, then it may be considered unappropriate to try other back-ends. * The {@link #shouldFailOver(CallRequest,CallConfig,CallExceptionList) * shouldFailOver} method determines whether a failed call will be retried. * * <p>Consider the following hypothetical scenario. A company has two data * centers, a primary site and a secondary backup site. The primary site has * 3 back-ends running an <em>eshop</em> service, while the hot backup site * has only 2 such back-ends. The back-ends at the primary site should always * be preferred over the back-ends at the backup site. At each site, load * should be evenly distributed among the available back-ends within that * site. * * <p>Such a scenario can be converted to a descriptor configuration such as * the following: * * <ul> * <li>the service caller uses a group descriptor called <em>All</em> of type * <em>ordered</em>. This group descriptor contains 2 other group * descriptors: <em>MainSite</em> and <em>BackupSite</em>. * <li>the group descriptor <em>MainSite</em> is of type <em>random</em> and * contains 3 target descriptors, called <em>Main1</em>, * <em>Main2</em> and <em>Main3</em>. * <li>the group descriptor <em>BackupSite</em> is also of type * <em>random</em> and contains 2 target descriptors, called * <em>Backup1</em> and <em>Backup2</em>. * </ul> * * <p>Now if the service caller performs a call, it will first randomly select * one of <em>Main1</em>, <em>Main2</em> and <em>Main3</em>. If the call * fails and fail-over is considered allowable, it will retry the call with * one of the other back-ends in the <em>MainSite</em> group. If none of the * back-ends in the <em>MainSite</em> group succeeds, it will randomly select * back-ends from the <em>BackupSite</em> group until the call has succeeded * or until all back-ends were tried. * * <a name="section-callconfig"></a> * <h2>Call configuration</h2> * * <p>Some aspects of a call can be configured using a {@link CallConfig} * object. For example, the <code>CallConfig</code> base class indicates * whether fail-over is unconditionally allowed. Like this, some aspects of * the behaviour of the caller can be tweaked. * * <p>There are different places where a <code>CallConfig</code> can be * applied: * * <ul> * <li>associated with a <code>ServiceCaller</code>; * <li>associated with a <code>CallRequest</code>; * <li>passed with the call method. * </ul> * * <p>First of all, each <code>ServiceCaller</code> instance will have a * fall-back <code>CallConfig</code>. * * <p>Secondly, a {@link CallRequest} instance may have a * <code>CallConfig</code> associated with it as well. If it does, then this * overrides the one on the <code>ServiceCaller</code> instance. * * <p>Finally, a <code>CallConfig</code> can be passed as an argument to the * call method. If it is, then this overrides any other settings. * * <a name="section-implementations"></a> * <h2>Subclass implementations</h2> * * <p>This class is abstract and is intended to be have service-specific * subclasses, e.g. for HTTP, FTP, JDBC, etc. * * <p>Normally, a subclass should be stick to the following rules: * * <ol> * <li>There should be a constructor that accepts only a {@link Descriptor} * object. This constructor should call * <code>super(descriptor, null)</code>. * This descriptor should document the same exceptions as the * {@link #ServiceCaller(Descriptor,CallConfig)} constructor. * <li>There should be a constructor that accepts both a * {@link Descriptor} and a service-specific call config object * (derived from {@link CallConfig}). This constructor should call * <code>super(descriptor, callConfig)</code>. * This descriptor should document the same exceptions as the * {@link #ServiceCaller(Descriptor,CallConfig)} constructor. * <li>The method {@link #isProtocolSupportedImpl(String)} should be * implemented. * <li>There should be a <code>call</code> method that accepts only a * service-specific request object (derived from {@link CallRequest}). * It should call * {@link #doCall(CallRequest,CallConfig) doCall}<code>(request, null)</code>. * <li>There should be a <code>call</code> method that accepts both a * service-specific request object (derived from {@link CallRequest}). * and a service-specific call config object (derived from * {@link CallConfig}). It should call * {@link #doCall(CallRequest,CallConfig) doCall}<code>(request, callConfig)</code>. * <li>The method * {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)} must * be implemented as specified. * <li>The {@link #createCallResult(CallRequest,TargetDescriptor,long,CallExceptionList,Object) createCallResult} * method must be implemented as specified. * <li>To control when fail-over is applied, the method * {@link #shouldFailOver(CallRequest,CallConfig,CallExceptionList)} * may also be implemented. The implementation can assume that * the passed {@link CallRequest} object is an instance of the * service-specific call request class and that the passed * {@link CallConfig} object is an instance of the service-specific * call config class. * </ol> * * @version $Revision: 1.82 $ $Date: 2012/04/16 19:40:39 $ * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a> * * @since XINS 1.0.0 */ public abstract class ServiceCaller { /** * The descriptor for this service. Can be <code>null</code>. */ private Descriptor _descriptor; /** * The fall-back call config object for this service caller. Can only be * <code>null</code> if this is an old-style service caller. */ private CallConfig _callConfig; /** * Constructs a new <code>ServiceCaller</code> with the specified * <code>CallConfig</code>. * * <p>The descriptor is not mandatory. However, no calls can be made with * this service caller until the descriptor is set. * * @param descriptor * the descriptor of the service, or <code>null</code>. * * @param callConfig * the {@link CallConfig} object, or <code>null</code> if the default * should be used. * * @throws UnsupportedProtocolException * if <code>descriptor</code> is or contains a {@link TargetDescriptor} * with an unsupported protocol (<em>since XINS 1.2.0</em>). * * @since XINS 1.1.0 */ protected ServiceCaller(Descriptor descriptor, CallConfig callConfig) throws UnsupportedProtocolException { // Store information setDescriptor(descriptor); // If no CallConfig is specified, then use a default one if (callConfig == null) { // Call getDefaultCallConfig() to get the default config... try { callConfig = getDefaultCallConfig(); // ...it should not throw any exception... } catch (Throwable t) { throw Utils.logProgrammingError(t); } // ...and it should never return null. if (callConfig == null) { throw Utils.logProgrammingError("Method returned null, although that is disallowed by the ServiceCaller.getDefaultCallConfig() contract."); } } // Set call configuration _callConfig = callConfig; } /** * Asserts that the specified target descriptor is considered acceptable * for this service caller. If not, an exception is thrown. * * @param target * the {@link TargetDescriptor} to test, should not be * <code>null</code>. * * @throws IllegalArgumentException * if <code>target == null</code>. * * @throws UnsupportedProtocolException * if the protocol in the target descriptor is unsupported. * * @since XINS 1.2.0 */ public final void testTargetDescriptor(TargetDescriptor target) throws IllegalArgumentException, UnsupportedProtocolException { // Check preconditions MandatoryArgumentChecker.check("target", target); try { if (! isProtocolSupported(target.getProtocol())) { throw new UnsupportedProtocolException(target); } } catch (UnsupportedOperationException exception) { // ignore } } /** * Checks if the specified protocol is supported (wrapper method). The * protocol is the part in a URL before the string <code>"://"</code>). * * <p>For example: * * <ul> * <li>in the URL <code>"http://www.google.nl"</code>, the protocol is * <code>"http"</code>; * * <li>in the URL <code>"jdbc:mysql://we.are.the.b.org/mydb/"</code>, * the protocol is <code>"jdbc:mysql"</code>. * </ul> * * <p>This method first checks the argument. If it is <code>null</code>, * then an exception is thrown. Otherwise, the result of a call to * {@link #isProtocolSupportedImpl(String)} is returned, passing the * supplied protocol, but in lowercase. This method may then throw an * {@link UnsupportedOperationException} if it is not implemented (default * behavior). * * @param protocol * the protocol, should not be <code>null</code>. * * @return * <code>true</code> if the specified protocol is supported, or * <code>false</code> if it is not. * * @throws IllegalArgumentException * if <code>protocol == null</code>. * * @throws UnsupportedOperationException * if this method is not implemented (probably because this * <code>ServiceCaller</code> implementation was originally written with * XINS 1.0.x or XINS 1.1.x) * * @since XINS 1.2.0 */ public final boolean isProtocolSupported(String protocol) throws IllegalArgumentException, UnsupportedOperationException { // Check preconditions MandatoryArgumentChecker.check("protocol", protocol); return isProtocolSupportedImpl(protocol.toLowerCase()); } /** * Checks if the specified protocol is supported (implementation method). * The protocol is the part in a URL before the string <code>"://"</code>). * * <p>This method should only ever be called from the * {@link #isProtocolSupported(String)} method. * * <p>The implementation of this method in class <code>ServiceCaller</code> * throws an {@link UnsupportedOperationException}. * * @param protocol * the protocol, guaranteed not to be <code>null</code> and guaranteed * to be in lower case. * * @return * <code>true</code> if the specified protocol is supported, or * <code>false</code> if it is not. * * @throws UnsupportedOperationException * if this method is not implemented (probably because this * <code>ServiceCaller</code> implementation was originally written with * XINS 1.0.x or XINS 1.1.x) * * @since XINS 1.2.0 */ protected boolean isProtocolSupportedImpl(String protocol) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Sets the descriptor. * * @param descriptor * the descriptor for this service, or <code>null</code>. * * @throws UnsupportedProtocolException * if <code>descriptor</code> is or contains a {@link TargetDescriptor} * with an unsupported protocol. * * @since XINS 1.2.0 */ public void setDescriptor(Descriptor descriptor) throws UnsupportedProtocolException { // Test the protocol for all TargetDescriptors if (descriptor != null) { for (TargetDescriptor target : descriptor) { testTargetDescriptor(target); } } // Store it _descriptor = descriptor; } /** * Returns the descriptor. If the descriptor is currently unset, then * <code>null</code> is returned. * * <p><em>Since XINS 1.2.0, this method may return <code>null</code>.</em> * * @return * the descriptor for this service, or <code>null</code> if it is * currently unset. */ public final Descriptor getDescriptor() { return _descriptor; } /** * Sets the <code>CallConfig</code> associated with this service caller. * * <p>This method should only be called on new-style (XINS 1.1) service * callers that used the {@link #ServiceCaller(Descriptor,CallConfig)} * constructor. * * @param config * the fall-back {@link CallConfig} object for this service caller, * cannot be <code>null</code>. * * @throws IllegalArgumentException * if <code>config == null</code>. * * @since XINS 1.2.0 */ protected final void setCallConfig(CallConfig config) throws IllegalArgumentException { // Check argument MandatoryArgumentChecker.check("config", config); _callConfig = config; } /** * Returns the <code>CallConfig</code> associated with this service caller. * * @return * the fall-back {@link CallConfig} object for this service caller, * never <code>null</code>. * * @since XINS 1.1.0 */ public final CallConfig getCallConfig() { return _callConfig; } /** * Returns a default <code>CallConfig</code> object. This method is called * by the <code>ServiceCaller</code> constructor if no * <code>CallConfig</code> object was given. * * <p>Subclasses that support the new service calling framework (introduced * in XINS 1.1.0) <em>must</em> override this method to return a more * suitable <code>CallConfig</code> instance. * * <p>This method should never be called by subclasses. * * @return * a new, appropriate, {@link CallConfig} instance, never * <code>null</code>. * * @since XINS 1.1.0 */ protected abstract CallConfig getDefaultCallConfig(); /** * Attempts to execute the specified call request on one of the target * services, with the specified call configuration. During the execution, * {@link TargetDescriptor Target descriptors} will be picked and passed to * {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)} until there * is one that succeeds, as long as fail-over can be done (according to * {@link #shouldFailOver(CallRequest,CallConfig,CallExceptionList)}). * * <p>If one of the calls succeeds, then the result is returned. If * none succeeds or if fail-over should not be done, then a * {@link CallException} is thrown. * * <p>Subclasses that want to use this method <em>must</em> implement * {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)}. That * method is called for each call attempt to a specific service target * (represented by a {@link TargetDescriptor}). * * @param request * the call request, not <code>null</code>. * * @param callConfig * the call configuration, or <code>null</code> if the one defined for * the call request should be used if specified, or otherwise the * fall-back call configuration associated with this * <code>ServiceCaller</code> (see {@link #getCallConfig()}). * * @return * a combination of the call result and a link to the * {@link TargetDescriptor target} that returned this result, if and * only if one of the calls succeeded, could be <code>null</code>. * * @throws IllegalArgumentException * if <code>request == null</code>. * * @throws IllegalStateException * if the descriptor is currently unset (<em>since XINS 1.2.0</em>). * * @throws CallException * if all call attempts failed. * * @since XINS 1.1.0 */ protected final CallResult doCall(CallRequest request, CallConfig callConfig) throws IllegalArgumentException, IllegalStateException, CallException { // Check preconditions MandatoryArgumentChecker.check("request", request); // Determine descriptor Descriptor descriptor = _descriptor; if (descriptor == null) { throw new IllegalStateException("Descriptor is currently unset."); } // Determine what config to use. The argument has priority, then the one // associated with the request and the fall-back is the one associated // with this service caller. if (callConfig == null) { callConfig = request.getCallConfig(); if (callConfig == null) { callConfig = _callConfig; } } // Keep a reference to the most recent CallException since // setNext(CallException) needs to be called on it to make it link to // the next one (if there is one) CallException lastException = null; // Maintain the list of CallExceptions // // This is needed if a successful result (a CallResult object) is // returned, since it will contain references to the exceptions as well; // // Note that this object is lazily initialized because this code is // performance- and memory-optimized for the successful case List<CallException> exceptions = null; // Iterate over all targets Iterator<TargetDescriptor> iterator = descriptor.iterator(); // There should be at least one target if (! iterator.hasNext()) { throw Utils.logProgrammingError("Descriptor returns no target descriptors."); } // Loop over all TargetDescriptors boolean shouldContinue = true; while (shouldContinue) { // Get a reference to the next TargetDescriptor TargetDescriptor target = iterator.next(); // Call using this target Log.log_1309(target.getURL()); Object result = null; boolean succeeded = false; long start = System.currentTimeMillis(); try { // Attempt the call result = doCallImpl(request, callConfig, target); succeeded = true; Log.log_1301(target.getURL()); // If the call to the target fails, store the exception and try the next } catch (Throwable exception) { Log.log_1302(target.getURL()); long duration = System.currentTimeMillis() - start; // If the caught exception is not a CallException, then // encapsulate it in one CallException currentException; if (exception instanceof CallException) { currentException = (CallException) exception; } else { currentException = new UnexpectedExceptionCallException(request, target, duration, null, exception); } // Link the previous exception (if there is one) to this one if (lastException != null) { lastException.setNext(currentException); } // Now set this exception as the most recent CallException lastException = currentException; // If this is the first exception being caught, then lazily // initialize the CallExceptionList and keep a reference to the // first exception if (exceptions == null) { exceptions = new ArrayList<CallException>(); } // Store the failure exceptions.add(currentException); // Determine whether fail-over is allowed and whether we have // another target to fail-over to boolean failOver = shouldFailOver(request, callConfig, exceptions); boolean haveNext = iterator.hasNext(); // No more targets and no fail-over if (!haveNext && !failOver) { Log.log_1304(); shouldContinue = false; // No more targets but fail-over would be allowed } else if (!haveNext) { Log.log_1305(); shouldContinue = false; // More targets available but fail-over is not allowed } else if (!failOver) { Log.log_1306(); shouldContinue = false; // More targets available and fail-over is allowed } else { Log.log_1307(); shouldContinue = true; } } // The call succeeded if (succeeded) { long duration = System.currentTimeMillis() - start; return createCallResult(request, target, duration, exceptions, result); } } // Loop ended, call failed completely Log.log_1303(); // Get the first exception from the list, this one should be thrown CallException first = exceptions.get(0); throw first; } /** * Calls the specified target using the specified subject. This method must * be implemented by subclasses. It is called as soon as a target is * selected to be called. If the call fails, then a {@link CallException} * should be thrown. If the call succeeds, then the call result should be * returned from this method. * * <p>Subclasses that want to use {@link #doCall(CallRequest,CallConfig)} * <em>must</em> implement this method. * * @param request * the call request to be executed, never <code>null</code>. * * @param callConfig * the call config to be used, never <code>null</code>; this is * determined by {@link #doCall(CallRequest,CallConfig)} and is * guaranteed not to be <code>null</code>. * * @param target * the target to call, cannot be <code>null</code>. * * @return * the result, if and only if the call succeeded, could be * <code>null</code>. * * @throws ClassCastException * if the specified <code>request</code> object is not <code>null</code> * and not an instance of an expected subclass of class * {@link CallRequest}. * * @throws IllegalArgumentException * if <code>target == null || request == null</code>. * * @throws CallException * if the call to the specified target failed. * * @since XINS 1.1.0 */ public abstract Object doCallImpl(CallRequest request, CallConfig callConfig, TargetDescriptor target) throws ClassCastException, IllegalArgumentException, CallException; /** * Constructs an appropriate <code>CallResult</code> object for a * successful call attempt. This method is called from * {@link #doCall(CallRequest,CallConfig)}. * * @param request * the {@link CallRequest} that was to be executed, never * <code>null</code> when called from {@link #doCall(CallRequest,CallConfig)}. * * @param succeededTarget * the {@link TargetDescriptor} for the service that was successfully * called, never <code>null</code> when called from * {@link #doCall(CallRequest,CallConfig)}. * * @param duration * the call duration in milliseconds, guaranteed to be a non-negative * number when called from {@link #doCall(CallRequest,CallConfig)}. * * @param exceptions * the list of {@link CallException} instances, or <code>null</code> if * there were no call failures. * * @param result * the result from the call, which is the object returned by * {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)}, can be * <code>null</code>. * * @return * a {@link CallResult} instance, never <code>null</code>. * * @throws ClassCastException * if <code>request</code> and/or <code>result</code> are not of the * correct class. */ protected abstract CallResult createCallResult(CallRequest request, TargetDescriptor succeededTarget, long duration, List<CallException> exceptions, Object result) throws ClassCastException; /** * Runs the specified task. If the task does not finish within the total * time-out period, then the thread executing it is interrupted using the * {@link Thread#interrupt()} method and a {@link TimeOutException} is * thrown. * * @param task * the task to run, cannot be <code>null</code>. * * @param descriptor * the descriptor for the target on which the task is executed, cannot * be <code>null</code>. * * @throws IllegalArgumentException * if <code>task == null || descriptor == null</code>. * * @throws IllegalThreadStateException * if <code>descriptor.getTotalTimeOut() > 0</code> and the task is a * {@link Thread} which is already started. * * @throws SecurityException * if the task did not finish within the total time-out period, but the * interruption of the thread was disallowed (see * {@link Thread#interrupt()}). * * @throws TimeOutException * if the task did not finish within the total time-out period and was * interrupted. */ protected final void controlTimeOut(Runnable task, TargetDescriptor descriptor) throws IllegalArgumentException, IllegalThreadStateException, SecurityException, TimeOutException { // Check preconditions MandatoryArgumentChecker.check("task", task, "descriptor", descriptor); // Determine the total time-out int totalTimeOut = descriptor.getTotalTimeOut(); // If there is no total time-out, then execute the task on this thread if (totalTimeOut < 1) { task.run(); // Otherwise a time-out controller will be used } else { TimeOutController.execute(task, totalTimeOut); } } /** * Determines whether a call should fail-over to the next selected target * based on a request, call configuration and exception list. * This method should only be called from * {@link #doCall(CallRequest,CallConfig)}. * * <p>This method is typically overridden by subclasses. Usually, a * subclass first calls this method in the superclass, and if that returns * <code>false</code> it does some additional checks, otherwise * <code>true</code> is immediately returned. * * <p>The implementation of this method in class {@link ServiceCaller} * returns <code>true</code> if and only if at least one of the following * conditions is true: * * <ul> * <li><code>callConfig.{@link CallConfig#isFailOverAllowed() * isFailOverAllowed()}</code> * <li><code>exception instanceof {@link ConnectionCallException}</code> * </ul> * * @param request * the request for the call, as passed to {@link #doCall(CallRequest,CallConfig)}, * should not be <code>null</code>. * * @param callConfig * the call config that is currently in use, never <code>null</code>. * * @param exceptions * the current list of {@link CallException}s; never * <code>null</code>; get the most recent one by calling * <code>exceptions.</code>. * * @return * <code>true</code> if the call should fail-over to the next target, or * <code>false</code> if it should not. * * @since XINS 1.1.0 */ protected boolean shouldFailOver(CallRequest request, CallConfig callConfig, List<CallException> exceptions) { MandatoryArgumentChecker.check("request", request, "callConfig", callConfig, "exceptions", exceptions); // Determine if fail-over is applicable boolean should = callConfig.isFailOverAllowed() || exceptions.get(exceptions.size() - 1).isFailOverAllowed(); return should; } }