package org.dcache.cells;
import com.google.common.base.Function;
import com.google.common.collect.ObjectArrays;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.RateLimiter;
import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.TimeoutCacheException;
import diskCacheV111.vehicles.Message;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageSender;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;
import org.dcache.util.CacheExceptionFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Stub class for common cell communication patterns. An instance
* of the template class encapsulates properties such as the
* destination and timeout for communication.
*
* Operations are aware of the dCache Message class and the
* CacheException class, and are able to interpret dCache error
* messages.
*/
public class CellStub
implements CellMessageSender
{
private CellEndpoint _endpoint;
private CellPath _destination;
private long _timeout = 30000;
private TimeUnit _timeoutUnit = MILLISECONDS;
private CellEndpoint.SendFlag[] _flags = {};
private volatile Semaphore _concurrency = new UnlimitedSemaphore();
private volatile RateLimiter _rateLimiter = RateLimiter.create(Double.POSITIVE_INFINITY);
public CellStub()
{
}
public CellStub(CellEndpoint endpoint)
{
setCellEndpoint(endpoint);
}
public CellStub(CellEndpoint endpoint, CellPath destination)
{
this(endpoint);
setDestinationPath(destination);
}
public CellStub(CellEndpoint endpoint, CellPath destination, long timeout)
{
this(endpoint, destination, timeout, MILLISECONDS);
}
public CellStub(CellEndpoint endpoint, CellPath destination, long timeout, TimeUnit unit)
{
this(endpoint, destination);
setTimeout(timeout);
setTimeoutUnit(unit);
}
@Override
public void setCellEndpoint(CellEndpoint endpoint)
{
_endpoint = endpoint;
}
public void setDestination(String destination)
{
setDestinationPath(new CellPath(destination));
}
public void setDestinationPath(CellPath destination)
{
_destination = destination;
}
public CellPath getDestinationPath()
{
return _destination;
}
/**
* Sets the communication timeout of the stub.
* @param timeout the timeout in milliseconds
*/
public void setTimeout(long timeout)
{
_timeout = timeout;
}
/**
* Returns the communication timeout of the stub.
*/
public long getTimeout()
{
return _timeout;
}
public void setTimeoutUnit(TimeUnit unit)
{
_timeoutUnit = unit;
}
public TimeUnit getTimeoutUnit()
{
return _timeoutUnit;
}
/**
* Returns the communication timeout in milliseconds of the stub.
*/
public long getTimeoutInMillis()
{
return _timeoutUnit.toMillis(_timeout);
}
public void setFlags(CellEndpoint.SendFlag... flags)
{
_flags = flags;
}
/**
* Sets a limit on the number of concurrent requests to issue.
*
* Once the limit is reached, further attempts to send a message will block
* until a reply is received for an earlier request or the earlier request
* times out.
*
* Notification requests (those for which no reply is expected) are not subject
* to the limit.
*
* Note that setting the limit will reset the counter, temporarily allowing a
* higher number of outstanding requests.
*
* @param limit maximum number of concurrent requests
*/
public void setConcurrencyLimit(Integer limit)
{
_concurrency = (limit == null) ? new UnlimitedSemaphore() : new Semaphore(limit);
}
/**
* Sets a limit on the request rate.
*
* Places an upper bound on the number of requests issues per second on this
* CellStub.
*
* In contrast to limiting the number of concurrent requests, the rate limiter
* also applies to notification messages.
*
* @param requestsPerSecond maximum requests per second
*/
public void setRate(Double requestsPerSecond)
{
/* We create a new rate limiter as it seems setting the rate to infinity once causes
* an infinite number of permits to accumulate that will never be spent, thus
* making it impossible to lower the rate. Should probably be filed as a bug against
* Guava.
*/
setRateLimiter(RateLimiter.create((requestsPerSecond == null) ? Double.POSITIVE_INFINITY : requestsPerSecond));
}
public Double getRate()
{
return _rateLimiter.getRate();
}
/**
* Sets a limiter on the request rate.
*
* @param rateLimiter rate limiter
*/
public void setRateLimiter(RateLimiter rateLimiter)
{
_rateLimiter = checkNotNull(rateLimiter);
}
public RateLimiter getRateLimiter()
{
return _rateLimiter;
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the same type as the message
* object that was sent, and the return code of that message is
* expected to be zero. If either is not the case, an exception is
* thrown.
*
* @param msg the message object to send
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T extends Message> T sendAndWait(T msg, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return getMessage(send(msg, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the same type as the message
* object that was sent, and the return code of that message is
* expected to be zero. If either is not the case, an exception is
* thrown.
*
* @param msg the message object to send
* @param timeout in milliseconds to wait for a reply
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T extends Message> T sendAndWait(T msg, long timeout, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return getMessage(send(msg, timeout, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the same type as the message
* object that was sent, and the return code of that message is
* expected to be zero. If either is not the case, an exception is
* thrown.
*
* @param path the destination cell
* @param msg the message object to send
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T extends Message> T sendAndWait(CellPath path, T msg, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return getMessage(send(path, msg, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the specified type. If this is
* not the case, an exception is thrown.
*
* @param msg the message object to send
* @param type the expected type of the reply
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T> T sendAndWait(Serializable msg, Class<T> type, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return get(send(msg, type, flags));
}
public <T> T sendAndWait(CellPath path, Serializable msg, Class<T> type, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return get(send(path, msg, type, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the specified type. If this is
* not the case, an exception is thrown.
*
* @param msg the message object to send
* @param type the expected type of the reply
* @param timeout the time to wait for the reply
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T> T sendAndWait(Serializable msg, Class<T> type, long timeout, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return get(send(msg, type, timeout, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the same type as the message
* object that was sent, and the return code of that message is
* expected to be zero. If either is not the case, an exception is
* thrown.
*
* @param path the destination cell
* @param msg the message object to send
* @param timeout the time to wait for the reply
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T extends Message> T sendAndWait(CellPath path, T msg, long timeout, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return getMessage(send(path, msg, timeout, flags));
}
/**
* Sends a message and waits for the reply. The reply is expected
* to contain a message object of the specified type. If this is
* not the case, an exception is thrown.
*
* @param path the destination cell
* @param msg the message object to send
* @param type the expected type of the reply
* @param timeout the time to wait for the reply
* @param flags flags affecting how the message is sent
* @return the message object from the reply
* @throws InterruptedException If the thread is interrupted
* @throws NoRouteToCellException If the message could not be sent.
* @throws TimeoutCacheException If a timeout occurred.
* @throws CacheException If the object in the reply was of the
* wrong type or if the service return a failure.
*/
public <T> T sendAndWait(CellPath path, Serializable msg, Class<T> type, long timeout, CellEndpoint.SendFlag... flags)
throws CacheException, InterruptedException, NoRouteToCellException
{
return get(send(path, msg, type, timeout, flags));
}
public <T extends Message> ListenableFuture<T> send(T message, CellEndpoint.SendFlag... flags)
{
return send(_destination, message, flags);
}
public <T extends Message> ListenableFuture<T> send(T message, long timeout, CellEndpoint.SendFlag... flags)
{
return send(_destination, message, timeout, flags);
}
public <T extends Message> ListenableFuture<T> send(CellPath destination, T message, CellEndpoint.SendFlag... flags)
{
return send(destination, message, getTimeoutInMillis(), flags);
}
@SuppressWarnings("unchecked")
public <T extends Message> ListenableFuture<T> send(CellPath destination, T message, long timeout, CellEndpoint.SendFlag... flags)
{
message.setReplyRequired(true);
return send(destination, message, (Class<T>) message.getClass(), timeout, flags);
}
public <T> ListenableFuture<T> send(Serializable message, Class<T> type, CellEndpoint.SendFlag... flags)
{
return send(_destination, message, type, flags);
}
public <T> ListenableFuture<T> send(
CellPath destination, Serializable message, Class<T> type, CellEndpoint.SendFlag... flags)
{
return send(destination, message, type, getTimeoutInMillis(), flags);
}
public <T> ListenableFuture<T> send(
Serializable message, Class<T> type, long timeout, CellEndpoint.SendFlag... flags)
{
return send(_destination, message, type, timeout, flags);
}
public <T> ListenableFuture<T> send(
CellPath destination, Serializable message, Class<T> type, long timeout, CellEndpoint.SendFlag... flags)
{
CellMessage envelope = new CellMessage(checkNotNull(destination), checkNotNull(message));
Semaphore concurrency = _concurrency;
CallbackFuture<T> future = new CallbackFuture<>(type, concurrency);
concurrency.acquireUninterruptibly();
_rateLimiter.acquire();
_endpoint.sendMessage(envelope, future, MoreExecutors.directExecutor(), timeout, mergeFlags(_flags, flags));
return future;
}
private CellEndpoint.SendFlag[] mergeFlags(CellEndpoint.SendFlag[] a, CellEndpoint.SendFlag[] b)
{
return (a.length == 0) ? b : (b.length == 0) ? a : ObjectArrays.concat(a, b, CellEndpoint.SendFlag.class);
}
/**
* Sends <code>message</code> to <code>destination</code>.
*/
public void notify(Serializable message)
{
if (_destination == null) {
throw new IllegalStateException("Destination must be specified");
}
notify(_destination, message);
}
/**
* Sends <code>message</code> to <code>destination</code>.
*/
public void notify(CellPath destination, Serializable message)
{
_rateLimiter.acquire();
_endpoint.sendMessage(new CellMessage(destination, message));
}
@Override
public String toString()
{
CellPath path = getDestinationPath();
return (path != null) ? path.toString() : super.toString();
}
/**
* Registers a callback to be run when the {@code Future}'s computation is
* {@linkplain java.util.concurrent.Future#isDone() complete} or, if the
* computation is already complete, immediately.
*
* <p>There is no guaranteed ordering of execution of callbacks, but any
* callback added through this method is guaranteed to be called once the
* computation is complete.
*
* <p>Note: For fast, lightweight listeners that would be safe to execute in
* any thread, consider {@link MoreExecutors#directExecutor}. For heavier
* listeners, {@code directExecutor()} carries some caveats. See {@link
* ListenableFuture#addListener} for details.
*
* <p>In is important the the executor isn't blocked by tasks waiting for
* the callback; such tasks could lead to a deadlock.
*
* <p>If not using {@code directExecutor()}, it is advisable to use a
* CDC preserving executor.
*
* @param future The future attach the callback to.
* @param callback The callback to invoke when {@code future} is completed.
* @param executor The executor to run {@code callback} when the future
* completes.
* @see com.google.common.util.concurrent.Futures#addCallback
* @see ListenableFuture#addListener
*/
public static <T extends Message> void addCallback(
final ListenableFuture<T> future, final MessageCallback<? super T> callback, Executor executor)
{
future.addListener(() -> {
try {
T reply = getUninterruptibly(future);
callback.setReply(reply);
if (reply.getReturnCode() != 0) {
callback.failure(reply.getReturnCode(), reply.getErrorObject());
} else {
callback.success();
}
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof TimeoutCacheException) {
callback.timeout(cause.getMessage());
} else if (cause instanceof CacheException) {
CacheException cacheException = (CacheException) cause;
callback.failure(cacheException.getRc(), cacheException.getMessage());
} else if (cause instanceof NoRouteToCellException) {
callback.noroute(((NoRouteToCellException) cause).getDestinationPath());
} else {
callback.failure(CacheException.UNEXPECTED_SYSTEM_EXCEPTION, cause);
}
}
}, executor);
}
/**
* Returns the result of {@link java.util.concurrent.Future#get()}, converting most exceptions
* and error conditions to {@link CacheException} or {@link NoRouteToCellException}.
*
* <p>Like {@link #get}, but also checks the return code of the Message reply. If non-zero, it
* is rethrown as a CacheException matching the return code.
*
* @see CellStub#get
*/
public static <T extends Message> T getMessage(Future<T> future)
throws CacheException, InterruptedException, NoRouteToCellException
{
T reply = get(future);
if (reply.getReturnCode() != 0) {
throw CacheExceptionFactory.exceptionOf(reply);
}
return reply;
}
/**
* Returns a new {@code ListenableFuture} whose result is asynchronously
* derived from the message result of the given {@code Future}. More precisely,
* the returned {@code Future} takes its result from a {@code Future} produced
* by applying the given {@code Function} to the result of the original
* {@code Future}.
*
* If the original {@code Future} returns a message indicating an error, the
* returned {@code Future} will fail with the corresponding CacheException. This
* distinguishes this method from {@link Futures#transform}.
*/
public static <T extends Message, V> ListenableFuture<V> transform(
ListenableFuture<T> future, Function<? super T, ? extends V> f)
{
return Futures.transformAsync(future,
msg -> {
if (msg.getReturnCode() != 0) {
throw CacheExceptionFactory.exceptionOf(msg);
}
return Futures.immediateFuture(f.apply(msg));
});
}
/**
* Returns a new {@code ListenableFuture} whose result is asynchronously
* derived from the message result of the given {@code Future}. More precisely,
* the returned {@code Future} takes its result from a {@code Future} produced
* by applying the given {@code AsyncFunction} to the result of the original
* {@code Future}.
*
* If the original {@code Future} returns a message indicating an error, the
* returned {@code Future} will fail with the corresponding CacheException. This
* distinguishes this method from {@link Futures#transform}.
*/
public static <T extends Message, V> ListenableFuture<V> transformAsync(
ListenableFuture<T> future, AsyncFunction<? super T, V> f)
{
return Futures.transformAsync(future,
msg -> {
if (msg.getReturnCode() != 0) {
throw CacheExceptionFactory.exceptionOf(msg);
}
return f.apply(msg);
});
}
/**
* Returns the result of {@link java.util.concurrent.Future#get()}, converting most exceptions
* to a {@link CacheException} or {@link NoRouteToCellException}.
*
* <p>Exceptions from {@code Future.get} are treated as follows:
* <ul>
* <li>Any {@link ExecutionException} has its <i>cause</i> unwrapped. Any CacheException or
* NoRouteToCellException is propagated untouched. Other exceptions are wrapped in a
* CacheException with error code UNEXPECTED_SYSTEM_EXCEPTION.
* <li>Any {@link InterruptedException} is propagated untouched.
* <li>Any {@link java.util.concurrent.CancellationException} is propagated untouched, as is any
* other {@link RuntimeException}.
* </ul>
*/
public static <T> T get(Future<T> future)
throws CacheException, InterruptedException, NoRouteToCellException
{
try {
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof CacheException) {
throw (CacheException) cause;
} else if (cause instanceof NoRouteToCellException) {
throw (NoRouteToCellException) cause;
} else {
throw new CacheException(CacheException.UNEXPECTED_SYSTEM_EXCEPTION, cause.getMessage(), cause);
}
}
}
/**
* Adapter class to turn a CellMessageAnswerable callback into a ListenableFuture.
*/
private static class CallbackFuture<T> extends FutureCellMessageAnswerable<T>
{
private final Semaphore _concurrency;
CallbackFuture(Class<? extends T> type, Semaphore concurrency)
{
super(type);
_concurrency = concurrency;
}
@Override
protected boolean set(T value)
{
boolean result = super.set(value);
if (result) {
_concurrency.release();
}
return result;
}
@Override
protected boolean setException(Throwable throwable)
{
boolean result = super.setException(throwable);
if (result) {
_concurrency.release();
}
return result;
}
}
/** NOP semaphore. Internal to CellStub; not a complete implementation. */
private static class UnlimitedSemaphore extends Semaphore
{
private static final long serialVersionUID = -9165031174889707659L;
UnlimitedSemaphore()
{
super(0);
}
@Override
public void acquire() throws InterruptedException
{
}
@Override
public void acquireUninterruptibly()
{
}
@Override
public void release()
{
}
}
}