/*
* Copyright 2013 BiasedBit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.biasedbit.http.client.timeout;
import com.biasedbit.http.client.future.RequestFuture;
import com.biasedbit.http.client.future.RequestFutureListener;
import com.biasedbit.http.client.util.RequestContext;
import lombok.RequiredArgsConstructor;
import org.jboss.netty.util.internal.ExecutorUtil;
import java.util.concurrent.*;
/**
* Basic implementation of a {@link TimeoutController} that launches a thread with a {@link TimeoutChecker}.
* <p/>
* The {@link TimeoutChecker} will bind itself as a listener to the request future. It will block on a latch until
* either the provided timeout expires or the operation completes.
* <p/>
* If the executor queues the execution of the {@link TimeoutChecker}, it is very likely that when the task starts, the
* operation is already complete. In this case, the checker will terminate immediately.
* <h2>Precision vs resource consumption</h1>
* Precision is how fast a request is cancelled when it's execution times out.
* This timer can consume more resources (unless its configured to run a single thread), but is potentially more precise
* than {@link HashedWheelTimeoutController}.
* <p/>
* <strong>The more threads it has, the more precise it will be, but also more resources it will consume.</strong>
* Conversely, the less threads it has, the less precise it will be.
* <p/>
* <div class="note">
* <div class="header>Important note:</div>
* If an instance is {@linkplain #BasicTimeoutController(java.util.concurrent.Executor) created with an external
* {@code Executor}}, then this manager <strong>will not</strong> terminate the executor.
* <p/>
* If an instance is created with the constructor {@link #BasicTimeoutController(int)}, it will terminate the thread
* pool when {@link #terminate()} is called.
* </div>
*
* @author <a href="http://biasedbit.com/">Bruno de Carvalho</a>
*/
public class BasicTimeoutController
implements TimeoutController {
// properties -----------------------------------------------------------------------------------------------------
private final Executor executor;
// internal vars --------------------------------------------------------------------------------------------------
private final boolean internalExecutor;
// constructors ---------------------------------------------------------------------------------------------------
public BasicTimeoutController(int maxThreads) {
if (maxThreads <= 0) executor = Executors.newCachedThreadPool();
else executor = Executors.newFixedThreadPool(maxThreads);
internalExecutor = true;
}
public BasicTimeoutController(Executor executor) {
this.executor = executor;
internalExecutor = false;
}
// TimeoutManager -------------------------------------------------------------------------------------------------
@Override public boolean init() { return true; }
@Override public void terminate() { if (internalExecutor) ExecutorUtil.terminate(executor); }
@Override public void controlTimeout(RequestContext context) { executor.execute(new TimeoutChecker(context)); }
/**
* Checks if a given request times out before its execution completes.
* <p/>
* When the {@link #run()} method is called, the timeout checker binds itself as a listener to the provided {@link
* com.biasedbit.http.client.util.RequestContext}.
* <p/>
* The motive behind using a {@code TimeoutChecker} instead of having some internal thread in each connection that
* shares state variables is that on some rare occasions, while executing runs with client and server on the same
* host, the responseses are actually received faster than the timeout checker thread is launched. If this happens,
* it means that the {@link com.biasedbit.http.client.connection.Connection} will probably have accepted
* another request and the timeout checker will actually be timing out a different request than the one originally
* intended.
* <p/>
* Having a per-request checker does hurt performance a bit but it avoids the aforementioned problem. Besides, this
* small performance slowdown only occurs when the request-response cycle takes 0.01~ to complete which, in real
* scenarios, is extremely unlikely to ever happen.
*/
@RequiredArgsConstructor
public static class TimeoutChecker
implements Runnable,
RequestFutureListener {
// internal vars ----------------------------------------------------------------------------------------------
private final RequestContext request;
private final CountDownLatch latch = new CountDownLatch(1);
// Runnable ---------------------------------------------------------------------------------------------------
@SuppressWarnings({"unchecked"})
@Override public void run() {
request.getFuture().addListener(this);
// If operation completed meanwhile, operationComplete() will have been called, the latch will have been
// counted down to zero and latch.await() will return immediately with true so everything's perfect even in
// that twisted scenario.
try {
// If explicitly released before timing out, nothing to do, request already executed or failed.
if (latch.await(request.getTimeout(), TimeUnit.MILLISECONDS)) return;
} catch (InterruptedException ignored) { /* ignored */ }
// Request timed out...
// If the future has already been unlocked, then no harm is done by calling this method as it won't produce
// any side effects.
request.getFuture().failedWithCause(RequestFuture.TIMED_OUT);
}
// RequestFutureListener ----------------------------------------------------------------------------------
@Override public void operationComplete(RequestFuture requestFuture)
throws Exception {
latch.countDown();
}
}
}