hudson.remoting.Request Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.remoting;
import java.io.IOException;
import java.io.Serializable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Request/response pattern over {@link Channel}, the layer-1 service.
*
*
* This assumes that the receiving side has all the class definitions
* available to de-serialize {@link Request}, just like {@link Command}.
*
* @author Kohsuke Kawaguchi
* @see Response
*/
abstract class Request extends Command {
/**
* Executed on a remote system to perform the task.
*
* @param channel
* The local channel. From the view point of the JVM that
* {@link #call(Channel) made the call}, this channel is
* the remote channel.
* @return
* the return value will be sent back to the calling process.
* @throws EXC
* The exception will be forwarded to the calling process.
* If no checked exception is supposed to be thrown, use {@link RuntimeException}.
*/
protected abstract RSP perform(Channel channel) throws EXC;
/**
* Uniquely identifies this request.
* Used for correlation between request and response.
*/
private final int id;
private volatile Response response;
/**
* If this request performed some I/O back in the caller side during the remote call execution, set to last such
* operation, so that we can block until its completion.
*/
volatile transient Future> lastIo;
/**
* While executing the call this is set to the handle of the execution.
*/
protected volatile transient Future> future;
protected Request() {
synchronized(Request.class) {
id = nextId++;
}
}
/**
* Sends this request to a remote system, and blocks until we receives a response.
*
* @param channel
* The channel from which the request will be sent.
* @throws InterruptedException
* If the thread is interrupted while it's waiting for the call to complete.
* @throws IOException
* If there's an error during the communication.
* @throws RequestAbortedException
* If the channel is terminated while the call is in progress.
* @throws EXC
* If the {@link #perform(Channel)} throws an exception.
*/
public final RSP call(Channel channel) throws EXC, InterruptedException, IOException {
// Channel.send() locks channel, and there are other call sequences
// ( like Channel.terminate()->Request.abort()->Request.onCompleted() )
// that locks channel -> request, so lock objects in the same order
synchronized(channel) {
synchronized(this) {
response=null;
channel.pendingCalls.put(id,this);
channel.send(this);
}
}
try {
synchronized(this) {
// set the thread name to represent the channel we are blocked on,
// so that thread dump would give us more useful information.
Thread t = Thread.currentThread();
final String name = t.getName();
try {
// wait until the response arrives
t.setName(name+" / waiting for "+channel);
while(response==null && !channel.isInClosed())
// I don't know exactly when this can happen, as pendingCalls are cleaned up by Channel,
// but in production I've observed that in rare occasion it can block forever, even after a channel
// is gone. So be defensive against that.
wait(30*1000);
if (response==null)
// channel is closed and we still don't have a response
throw new RequestAbortedException(null);
} finally {
t.setName(name);
}
if (lastIo != null) {
try {
lastIo.get();
} catch (ExecutionException ignore) {
}
}
Object exc = response.exception;
if (exc!=null) {
if(exc instanceof RequestAbortedException) {
// add one more exception, so that stack trace shows both who's waiting for the completion
// and where the connection outage was detected.
exc = new RequestAbortedException((RequestAbortedException)exc);
}
throw (EXC)exc; // some versions of JDK fails to compile this line. If so, upgrade your JDK.
}
return response.returnValue;
}
} catch (InterruptedException e) {
// if we are cancelled, abort the remote computation, too.
// do this outside the "synchronized(this)" block to prevent locking Request and Channel in a wrong order.
synchronized (channel) { // ... so that the close check and send won't be interrupted in the middle by a close
if (!channel.isOutClosed())
channel.send(new Cancel(id)); // only send a cancel if we can, or else ChannelClosedException will mask the original cause
}
throw e;
}
}
/**
* Makes an invocation but immediately returns without waiting for the completion
* (AKA asynchronous invocation.)
*
* @param channel
* The channel from which the request will be sent.
* @return
* The {@link Future} object that can be used to wait for the completion.
* @throws IOException
* If there's an error during the communication.
*/
public final hudson.remoting.Future callAsync(final Channel channel) throws IOException {
response=null;
channel.pendingCalls.put(id,this);
channel.send(this);
return new hudson.remoting.Future() {
private volatile boolean cancelled;
public boolean cancel(boolean mayInterruptIfRunning) {
if (cancelled || isDone()) {
return false;
}
cancelled = true;
if (mayInterruptIfRunning) {
try {
channel.send(new Cancel(id));
} catch (IOException x) {
return false;
}
}
return true;
}
public boolean isCancelled() {
return cancelled;
}
public boolean isDone() {
return isCancelled() || response!=null;
}
public RSP get() throws InterruptedException, ExecutionException {
synchronized(Request.this) {
try {
while(response==null) {
if (isCancelled()) {
throw new CancellationException();
}
Request.this.wait(); // wait until the response arrives
}
} catch (InterruptedException e) {
try {
channel.send(new Cancel(id));
} catch (IOException e1) {
// couldn't cancel. ignore.
}
throw e;
}
if(response.exception!=null)
throw new ExecutionException(response.exception);
return response.returnValue;
}
}
public RSP get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
synchronized(Request.this) {
if(response==null) {
if (isCancelled()) {
throw new CancellationException();
}
Request.this.wait(unit.toMillis(timeout)); // wait until the response arrives
}
if(response==null)
throw new TimeoutException();
if(response.exception!=null)
throw new ExecutionException(response.exception);
return response.returnValue;
}
}
};
}
/**
* Called by the {@link Response} when we received it.
*/
/*package*/ synchronized void onCompleted(Response response) {
this.response = response;
notify();
}
/**
* Aborts the processing. The calling thread will receive an exception.
*/
/*package*/ void abort(IOException e) {
onCompleted(new Response(id,new RequestAbortedException(e)));
}
/**
* Schedules the execution of this request.
*/
protected final void execute(final Channel channel) {
channel.executingCalls.put(id,this);
future = channel.executor.submit(new Runnable() {
public void run() {
try {
Command rsp;
CURRENT.set(Request.this);
try {
RSP r = Request.this.perform(channel);
// normal completion
rsp = new Response(id,r);
} catch (Throwable t) {
// error return
rsp = new Response(id,t);
} finally {
CURRENT.set(null);
}
if(chainCause)
rsp.createdAt.initCause(createdAt);
synchronized (channel) {// expand the synchronization block of the send() method to a check
if(!channel.isOutClosed())
channel.send(rsp);
}
} catch (IOException e) {
// communication error.
// this means the caller will block forever
logger.log(Level.SEVERE, "Failed to send back a reply",e);
} finally {
channel.executingCalls.remove(id);
}
}
});
}
/**
* Next request ID.
*/
private static int nextId=0;
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(Request.class.getName());
/**
* Set to true to chain {@link Command#createdAt} to track request/response relationship.
* This will substantially increase the network traffic, but useful for debugging.
*/
public static boolean chainCause = Boolean.getBoolean(Request.class.getName()+".chainCause");
/**
* Set to the {@link Request} object during {@linkplain #perform(Channel) the execution of the call}.
*/
static ThreadLocal CURRENT = new ThreadLocal();
static int getCurrentRequestId() {
Request r = CURRENT.get();
return r!=null ? r.id : 0;
}
/**
* Interrupts the execution of the remote computation.
*/
private static final class Cancel extends Command {
private final int id;
Cancel(int id) {
this.id = id;
}
protected void execute(Channel channel) {
Request,?> r = channel.executingCalls.get(id);
if(r==null) return; // already completed
Future> f = r.future;
if(f!=null) f.cancel(true);
}
}
}