/*
* Copyright (c) 2009 - 2017 Deutsches Elektronen-Synchroton,
* Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library 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 Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.xdr;
import com.google.common.base.Throwables;
import java.io.EOFException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetSocketAddress;
import java.nio.channels.CompletionHandler;
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
public class RpcCall {
private final static Logger _log = LoggerFactory.getLogger(RpcCall.class);
private final static Random RND = new Random();
/**
* XID number generator
*/
private final AtomicInteger xidGenerator = new AtomicInteger(RND.nextInt());
private int _xid;
/**
* Supported RPC protocol version
*/
private final static int RPCVERS = 2;
/**
* RPC program number
*/
private int _prog;
/**
* RPC program version number
*/
private int _version;
/**
* RPC program procedure number
*/
private int _proc;
/**
* RPC protocol version number
*/
private int _rpcvers;
/**
* Authentication credential.
*/
private RpcAuth _cred;
/**
* RPC call transport.
*/
private final XdrTransport _transport;
/**
* Call body.
*/
private final Xdr _xdr;
/**
* Object used to synchronize access to sendListeners.
*/
private final Object _listenerLock = new Object();
/**
* The {link CompletionHandler} which is used to notify all registered
* completion listeners.
*/
private class NotifyListenersCompletionHandler implements CompletionHandler<Integer, InetSocketAddress> {
@Override
public void completed(Integer result, InetSocketAddress attachment) {
synchronized (_listenerLock) {
if (_sendListeners != null) {
_sendListeners
.parallelStream()
.forEach(l -> l.completed(result, attachment));
}
if (_sendOnceListeners != null) {
_sendOnceListeners
.parallelStream()
.forEach(l -> l.completed(result, attachment));
_sendOnceListeners = null;
}
}
}
@Override
public void failed(Throwable t, InetSocketAddress attachment) {
_log.error("Failed to send RPC to {} : {}", attachment, t.getMessage());
synchronized (_listenerLock) {
if (_sendListeners != null) {
_sendListeners
.parallelStream()
.forEach(l -> l.failed(t, attachment));
}
if (_sendOnceListeners != null) {
_sendOnceListeners
.parallelStream()
.forEach(l -> l.failed(t, attachment));
_sendOnceListeners = null;
}
}
}
}
/**
* A {@link List} of registered {@link CompletionHandler} to be notified when
* send request complete.
*/
private List<CompletionHandler<Integer, InetSocketAddress>> _sendListeners;
/**
* A {@link List} of registered {@link CompletionHandler} to be notified
* when send request complete. The listeners will be removed from the list
* after notification.
*/
private List<CompletionHandler<Integer, InetSocketAddress>> _sendOnceListeners;
private final CompletionHandler<Integer, InetSocketAddress> _sendNotificationHandler
= new NotifyListenersCompletionHandler();
public RpcCall(int prog, int ver, RpcAuth cred, XdrTransport transport) {
this(prog, ver, cred, new Xdr(Xdr.INITIAL_XDR_SIZE), transport);
}
public RpcCall(int prog, int ver, RpcAuth cred, Xdr xdr, XdrTransport transport) {
_prog = prog;
_version = ver;
_cred = cred;
_transport = transport;
_xdr = xdr;
_proc = 0;
}
public RpcCall(int xid, Xdr xdr, XdrTransport transport) {
_xid = xid;
_xdr = xdr;
_transport = transport;
}
public RpcCall(int xid, int prog, int ver, int proc, RpcAuth cred, Xdr xdr, XdrTransport transport) {
_xid = xid;
_prog = prog;
_version = ver;
_proc = proc;
_cred = cred;
_xdr = xdr;
_transport = transport;
_rpcvers = RPCVERS;
}
/**
* Accept message. Have to be called prior processing RPC call.
* @throws IOException
* @throws OncRpcException
*/
public void accept() throws IOException, OncRpcException {
_rpcvers = _xdr.xdrDecodeInt();
if (_rpcvers != RPCVERS) {
throw new RpcMismatchReply(_rpcvers, 2);
}
_prog = _xdr.xdrDecodeInt();
_version = _xdr.xdrDecodeInt();
_proc = _xdr.xdrDecodeInt();
_cred = RpcCredential.decode(_xdr);
}
/**
* Get RPC call program number.
*
* @return version number
*/
public int getProgram() {
return _prog;
}
/**
* @return the RPC call program version
*/
public int getProgramVersion() {
return _version;
}
/**
* @return the RPC call program procedure
*/
public int getProcedure() {
return _proc;
}
public RpcAuth getCredential() {
return _cred;
}
/**
* Get RPC {@XdrTransport} used by this call.
* @return transport
*/
public XdrTransport getTransport() {
return _transport;
}
/**
* Get xid associated with this rpc message.
*/
public int getXid() {
return _xid;
}
/**
* Get {@link Xdr} stream used by this message.
* @return xdr stream
*/
public Xdr getXdr() {
return _xdr;
}
@Override
public String toString() {
return String.format("RPCv%d call: program=%d, version=%d, procedure=%d",
_rpcvers, _prog, _version, _proc);
}
/**
* Reject the request with given status. The call can be rejected for two
* reasons: either the server is not running a compatible version of the
* RPC protocol (RPC_MISMATCH), or the server rejects the identity of the
* caller (AUTH_ERROR).
*
* @see RpcRejectStatus
* @param status
* @param reason
*/
public void reject(int status, XdrAble reason) {
XdrEncodingStream xdr = _xdr;
try {
RpcMessage replyMessage = new RpcMessage(_xid, RpcMessageType.REPLY);
xdr.beginEncoding();
replyMessage.xdrEncode(_xdr);
xdr.xdrEncodeInt(RpcReplyStatus.MSG_DENIED);
xdr.xdrEncodeInt(status);
reason.xdrEncode(_xdr);
xdr.endEncoding();
_transport.send((Xdr)xdr, _transport.getRemoteSocketAddress(), _sendNotificationHandler);
} catch (OncRpcException e) {
_log.warn("Xdr exception: ", e);
} catch (IOException e) {
_log.error("Failed send reply: ", e);
}
}
/**
* Send accepted reply to the client.
*
* @param reply
*/
public void reply(XdrAble reply) {
acceptedReply(RpcAccepsStatus.SUCCESS, reply);
}
public void acceptedReply(int state, XdrAble reply) {
XdrEncodingStream xdr = _xdr;
try {
RpcMessage replyMessage = new RpcMessage(_xid, RpcMessageType.REPLY);
xdr.beginEncoding();
replyMessage.xdrEncode(_xdr);
xdr.xdrEncodeInt(RpcReplyStatus.MSG_ACCEPTED);
_cred.getVerifier().xdrEncode(xdr);
xdr.xdrEncodeInt(state);
reply.xdrEncode(xdr);
xdr.endEncoding();
_transport.send((Xdr)xdr, _transport.getRemoteSocketAddress(), _sendNotificationHandler);
} catch (OncRpcException e) {
_log.warn("Xdr exception: ", e);
} catch (IOException e) {
_log.error("Failed send reply: ", e);
}
}
/**
* Retrieves the parameters sent within an ONC/RPC call message.
*
* @param args the call argument do decode
* @throws OncRpcException
*/
public void retrieveCall(XdrAble args) throws OncRpcException, IOException {
args.xdrDecode(_xdr);
_xdr.endDecoding();
}
/**
* Reply to client with error program version mismatch.
* Accepted message sent.
*
* @param min minimal supported version
* @param max maximal supported version
*/
public void failProgramMismatch(int min, int max) {
acceptedReply(RpcAccepsStatus.PROG_MISMATCH, new MismatchInfo(min, max));
}
/**
* Reply to client with error program unavailable.
* Accepted message sent.
*/
public void failProgramUnavailable() {
acceptedReply(RpcAccepsStatus.PROG_UNAVAIL, XdrVoid.XDR_VOID);
}
/**
* Reply to client with error procedure unavailable.
*/
public void failProcedureUnavailable() {
acceptedReply(RpcAccepsStatus.PROC_UNAVAIL, XdrVoid.XDR_VOID);
}
/**
* Reply to client with error garbage args.
*/
public void failRpcGarbage() {
acceptedReply(RpcAccepsStatus.GARBAGE_ARGS, XdrVoid.XDR_VOID);
}
/**
* Reply to client with error system error.
*/
public void failRpcSystem() {
acceptedReply(RpcAccepsStatus.SYSTEM, XdrVoid.XDR_VOID);
}
/**
* Send asynchronous RPC request to a remove server.
*
* This method initiates an asynchronous RPC request. The handler parameter
* is a completion handler that is invoked when the RPC operation completes
* (or fails/times-out). The result passed to the completion handler is the
* RPC result returned by server.
*
* @param procedure The number of the procedure.
* @param args The argument of the procedure.
* @param callback The completion handler.
* @param timeoutValue timeout value. 0 means no timeout
* @param timeoutUnits units for timeout value
* @param auth auth to use for the call
* @throws OncRpcException
* @throws IOException
* @since 2.4.0
*/
public void call(int procedure, XdrAble args, CompletionHandler<RpcReply, XdrTransport> callback, long timeoutValue, TimeUnit timeoutUnits, RpcAuth auth)
throws IOException {
callInternal(procedure, args, callback, timeoutValue, timeoutUnits, auth);
}
/**
* convenience version of {@link #call(int, XdrAble, CompletionHandler, long, TimeUnit, RpcAuth)} with no auth
*/
public void call(int procedure, XdrAble args, CompletionHandler<RpcReply, XdrTransport> callback, long timeoutValue, TimeUnit timeoutUnits)
throws IOException {
callInternal(procedure, args, callback, timeoutValue, timeoutUnits, null);
}
/**
* convenience version of {@link #call(int, XdrAble, CompletionHandler, long, TimeUnit, RpcAuth)} with no timeout
*/
public void call(int procedure, XdrAble args, CompletionHandler<RpcReply, XdrTransport> callback, RpcAuth auth)
throws IOException {
callInternal(procedure, args, callback, 0, null, auth);
}
/**
* convenience version of {@link #call(int, XdrAble, CompletionHandler, long, TimeUnit, RpcAuth)} with no timeout or auth
*/
public void call(int procedure, XdrAble args, CompletionHandler<RpcReply, XdrTransport> callback)
throws IOException {
callInternal(procedure, args, callback, 0, null, null);
}
/**
* executes an RPC. returns the (internally generated) xid for the call
* @param procedure The number of the procedure.
* @param args The argument of the procedure.
* @param callback The completion handler.
* @param timeoutValue timeout value. 0 means no timeout
* @param timeoutUnits units for timeout value
* @param auth auth to use for this call. null for constructor-provided default
* @return the xid for the call
* @throws OncRpcException
* @throws IOException
*/
private int callInternal(int procedure, XdrAble args, CompletionHandler<RpcReply, XdrTransport> callback,
long timeoutValue, TimeUnit timeoutUnits, RpcAuth auth)
throws IOException {
int xid = nextXid();
Xdr xdr = new Xdr(Xdr.INITIAL_XDR_SIZE);
xdr.beginEncoding();
RpcMessage rpcMessage = new RpcMessage(xid, RpcMessageType.CALL);
rpcMessage.xdrEncode(xdr);
xdr.xdrEncodeInt(RPCVERS);
xdr.xdrEncodeInt(_prog);
xdr.xdrEncodeInt(_version);
xdr.xdrEncodeInt(procedure);
if (auth != null) {
auth.xdrEncode(xdr);
} else {
_cred.xdrEncode(xdr);
}
args.xdrEncode(xdr);
xdr.endEncoding();
ReplyQueue replyQueue = _transport.getReplyQueue();
if (callback != null) {
replyQueue.registerKey(xid, _transport.getLocalSocketAddress(), callback, timeoutValue, timeoutUnits);
} else {
//no handler, so we wont get any errors if connection was dropped. have to check.
if (!_transport.isOpen()) {
throw new EOFException("XdrTransport is not open");
}
}
_transport.send(xdr, _transport.getRemoteSocketAddress(), new NotifyListenersCompletionHandler() {
@Override
public void failed(Throwable t, InetSocketAddress attachment) {
super.failed(t, attachment);
if (callback != null) {
replyQueue.get(xid);
callback.failed(t, _transport);
}
}
});
return xid;
}
/**
* Send asynchronous RPC request to a remove server.
*
* This method initiates an asynchronous RPC request. The method behaves in
* exactly the same manner as the {@link #call(int, XdrAble, CompletionHandler, long, TimeUnit)}
* method except that instead of specifying a completion handler, this method
* returns a Future representing the pending result. The Future's get method
* returns the RPC reply responded by server.
*
* @param <T> The result type of RPC call.
* @param procedure The number of the procedure.
* @param args The argument of the procedure.
* @param type The expected type of the reply
* @param auth auth to use for the call
* @return A Future representing the result of the operation.
* @throws OncRpcException
* @throws IOException
* @since 2.4.0
*/
public <T extends XdrAble> Future<T> call(int procedure, XdrAble args, final Class<T> type, final RpcAuth auth)
throws IOException {
try {
T result = type.newInstance();
return getCallFuture(procedure, args, result, 0, null, auth);
} catch (InstantiationException | IllegalAccessException e) {
// this exceptions point to bugs
throw new RuntimeException("Failed to create in instance of " + type, e);
}
}
/**
* convenience version of {@link #call(int, XdrAble, Class, RpcAuth)} with no auth
*/
public <T extends XdrAble> Future<T> call(int procedure, XdrAble args, final Class<T> type)
throws IOException {
return call(procedure, args, type, null);
}
/**
* Send call to remove RPC server.
*
* @param procedure the number of the procedure.
* @param args the argument of the procedure.
* @param result the result of the procedure
* @param timeoutValue timeout value. 0 means no timeout
* @param timeoutUnits units for timeout value
* @param auth auth to use for the call
* @throws OncRpcException
* @throws IOException
*/
public void call(int procedure, XdrAble args, XdrAble result, long timeoutValue, TimeUnit timeoutUnits, RpcAuth auth)
throws IOException, TimeoutException {
try {
Future<XdrAble> future = getCallFuture(procedure, args, result, timeoutValue, timeoutUnits, auth);
future.get();
} catch (InterruptedException e) {
// workaround missing chained constructor
IOException ioe = new InterruptedIOException(e.getMessage());
ioe.initCause(e);
throw ioe;
} catch (ExecutionException e) {
Throwable t = Throwables.getRootCause(e);
Throwables.throwIfInstanceOf(t, OncRpcException.class);
Throwables.throwIfInstanceOf(t, IOException.class);
Throwables.throwIfInstanceOf(t, TimeoutException.class);
throw new IOException(t);
}
}
/**
* convenience version of {@link #call(int, XdrAble, XdrAble, long, TimeUnit, RpcAuth)} with default auth
*/
public void call(int procedure, XdrAble args, XdrAble result, long timeoutValue, TimeUnit timeoutUnits)
throws IOException, TimeoutException {
call(procedure, args, result, timeoutValue, timeoutUnits, null);
}
/**
* convenience version of {@link #call(int, XdrAble, XdrAble, long, TimeUnit, RpcAuth)} with no timeout
*/
public void call(int procedure, XdrAble args, XdrAble result, RpcAuth auth)
throws IOException {
try {
call(procedure, args, result, 0, null, auth);
} catch (TimeoutException e) {
throw new IllegalStateException(e); //theoretically impossible
}
}
/**
* convenience version of {@link #call(int, XdrAble, XdrAble, long, TimeUnit, RpcAuth)} with no timeout or auth
*/
public void call(int procedure, XdrAble args, XdrAble result)
throws IOException {
try {
call(procedure, args, result, 0, null, null);
} catch (TimeoutException e) {
throw new IllegalStateException(e); //theoretically impossible
}
}
private <T extends XdrAble> Future<T> getCallFuture(int procedure, XdrAble args, final T result, long timeoutValue, TimeUnit timeoutUnits, RpcAuth auth)
throws IOException {
final CompletableFuture<T> future = new CompletableFuture<>();
CompletionHandler<RpcReply, XdrTransport> callback = new CompletionHandler<RpcReply, XdrTransport>() {
@Override
public void completed(RpcReply reply, XdrTransport attachment) {
try {
reply.getReplyResult(result);
future.complete(result);
} catch (IOException e) {
failed(e, attachment);
}
}
@Override
public void failed(Throwable exc, XdrTransport attachment) {
future.completeExceptionally(exc);
}
};
int xid = callInternal(procedure, args, callback, timeoutValue, timeoutUnits, auth);
//wrap the future if no timeout provided up-front to properly un-register
//the handler if a timeout is later provided to Future.get()
return timeoutValue > 0 ? future : new TimeoutAwareFuture<>(future, xid);
}
private class TimeoutAwareFuture<T> implements Future<T> {
private final Future<T> delegate;
private final int xid;
public TimeoutAwareFuture(Future<T> delegate, int xid) {
this.delegate = delegate;
this.xid = xid;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
try {
return delegate.cancel(mayInterruptIfRunning);
} finally {
if (mayInterruptIfRunning) {
unregisterXid();
}
}
}
@Override
public boolean isCancelled() {
return delegate.isCancelled();
}
@Override
public boolean isDone() {
return delegate.isDone();
}
@Override
public T get() throws InterruptedException, ExecutionException {
try {
return delegate.get();
} finally {
unregisterXid();
}
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
try {
return delegate.get(timeout, unit);
} finally {
unregisterXid();
}
}
private void unregisterXid() {
_transport.getReplyQueue().get(xid); //make sure its removed from the reply queue
}
}
private int nextXid() {
return xidGenerator.incrementAndGet();
}
/**
* Register {@link CompletionHandler} to receive notification when message
* send is complete. NOTICE: when processing rpc call on the server side
* the @{code registerSendListener} has the same effect as {@link #registerSendOnceListener}
* as a new instance of {@link RpcCall} is used to process the request.
* @param listener the message sent listener
*/
public void registerSendListener(CompletionHandler<Integer, InetSocketAddress> listener) {
synchronized (_listenerLock) {
if (_sendListeners == null) {
_sendListeners = new ArrayList<>();
}
_sendListeners.add(listener);
}
}
/**
* Register {@link CompletionHandler} to receive notification when message
* send is complete. The listener will be removed after next send event.
*
* @param listener the message sent listener
*/
public void registerSendOnceListener(CompletionHandler<Integer, InetSocketAddress> listener) {
synchronized (_listenerLock) {
if (_sendOnceListeners == null) {
_sendOnceListeners = new ArrayList<>();
}
_sendOnceListeners.add(listener);
}
}
}