/*
* 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.future;
import com.biasedbit.http.client.connection.Connection;
import lombok.SneakyThrows;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="http://biasedbit.com/">Bruno de Carvalho</a>
*/
public class DefaultRequestFuture<T>
implements RequestFuture<T> {
// internal vars --------------------------------------------------------------------------------------------------
private final long creation = System.nanoTime();
private T result;
private HttpResponse response;
private boolean done;
private List<RequestFutureListener<T>> listeners;
private Throwable cause;
private int waiters;
private Connection connection;
private long executionStart = -1;
private long executionEnd = -1;
// RequestFuture ----------------------------------------------------------------------------------------------
@Override public T getProcessedResult() { return result; }
@Override public HttpResponse getResponse() { return response; }
@Override public HttpResponseStatus getStatus() {
if (response == null) return null;
else return response.getStatus();
}
@Override public int getResponseStatusCode() {
if (response == null) return -1;
else return response.getStatus().getCode();
}
@Override public boolean hasSuccessfulResponse() {
int code = getResponseStatusCode();
return (code >= 200) && (code <= 299);
}
@Override public void markExecutionStart() { executionStart = System.nanoTime(); }
@Override public long getExecutionTime() {
if (done) return executionStart == -1 ? 0 : (executionEnd - executionStart) / 1000000;
else return -1;
}
@Override public long getExistenceTime() {
if (done) return (executionEnd - creation) / 1000000;
else return (System.nanoTime() - creation) / 1000000;
}
@Override public boolean isDone() { return done; }
@Override public boolean isSuccessful() { return cause == null; }
@Override public boolean isCancelled() { return cause == CANCELLED; }
@Override public Throwable getCause() { return cause; }
@Override public boolean cancel() {
synchronized (this) {
if (done) return false;
executionEnd = System.nanoTime();
cause = CANCELLED;
done = true;
// The connection must be killed in order for the request to effectively be cancelled
if (connection != null) connection.terminate(CANCELLED);
if (waiters > 0) notifyAll();
}
notifyListeners();
return true;
}
@Override public void addListener(RequestFutureListener<T> listener) {
synchronized (this) {
if (done) {
notifyListener(listener);
} else {
if (listeners == null) listeners = new ArrayList<>(1);
listeners.add(listener);
}
}
}
@Override public void removeListener(RequestFutureListener<T> listener) {
synchronized (this) {
if (done) return;
if (listeners != null) listeners.remove(listener);
}
}
@Override public RequestFuture<T> await()
throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
synchronized (this) {
while (!done) {
waiters++;
try {
wait();
} finally {
waiters--;
}
}
}
return this;
}
@Override public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return await0(unit.toNanos(timeout), true);
}
@Override public boolean await(long timeoutMillis)
throws InterruptedException {
return await0(TimeUnit.MILLISECONDS.toNanos(timeoutMillis), true);
}
@Override public RequestFuture<T> awaitUninterruptibly() {
boolean interrupted = false;
synchronized (this) {
while (!done) {
waiters++;
try {
wait();
} catch (InterruptedException e) {
interrupted = true;
} finally {
waiters--;
}
}
}
// Preserve interruption.
if (interrupted) Thread.currentThread().interrupt();
return this;
}
@SneakyThrows(InterruptedException.class)
@Override public boolean awaitUninterruptibly(long timeout, TimeUnit unit) {
return await0(unit.toNanos(timeout), false);
}
@SneakyThrows(InterruptedException.class)
@Override public boolean awaitUninterruptibly(long timeoutMillis) {
return await0(TimeUnit.MILLISECONDS.toNanos(timeoutMillis), false);
}
// interface ------------------------------------------------------------------------------------------------------
// TODO review this
public void attachConnection(Connection connection) { this.connection = connection; }
public boolean finishedSuccessfully(T processedResponse, HttpResponse response) {
synchronized (this) {
if (done) return false;
executionEnd = System.nanoTime();
done = true;
result = processedResponse;
this.response = response;
if (waiters > 0) notifyAll();
}
notifyListeners();
return true;
}
public boolean failedWithCause(Throwable cause) {
synchronized (this) {
if (done) return false; // Allow only once.
executionEnd = System.nanoTime();
this.cause = cause;
done = true;
if (connection != null) connection.terminate(cause);
if (waiters > 0) notifyAll();
}
notifyListeners();
return true;
}
public boolean failedWhileProcessingResponse(Throwable cause, HttpResponse response) {
synchronized (this) {
if (done) return false;
executionEnd = System.nanoTime();
this.response = response;
this.cause = cause;
done = true;
if (waiters > 0) notifyAll();
}
notifyListeners();
return true;
}
// private helpers ------------------------------------------------------------------------------------------------
private void notifyListeners() {
// This method doesn't need synchronization because:
// 1) This method is always called after synchronized (this) block.
// Hence any listener list modification happens-before this method.
// 2) This method is called only when 'done' is true. Once 'done'
// becomes true, the listener list is never modified - see add/removeListener()
if (listeners == null) return; // Not testing for isEmpty since removing listeners is quite rare...
for (RequestFutureListener<T> listener : listeners) notifyListener(listener);
}
private void notifyListener(RequestFutureListener<T> listener) {
try {
listener.operationComplete(this);
} catch (Throwable t) {
System.err.println("An exception was thrown by an instance of " + listener.getClass().getSimpleName());
t.printStackTrace();
}
}
private boolean await0(long timeoutNanos, boolean interruptable)
throws InterruptedException {
// Implementation straight out of Netty's own DefaultChannelFuture
if (interruptable && Thread.interrupted()) throw new InterruptedException();
long startTime = timeoutNanos <= 0 ? 0 : System.nanoTime();
long waitTime = timeoutNanos;
boolean interrupted = false;
try {
synchronized (this) {
if (done) return true;
else if (waitTime <= 0) return done;
waiters++;
try {
while (true) {
try {
wait(waitTime / 1000000, (int) (waitTime % 1000000));
} catch (InterruptedException e) {
if (interruptable) throw e;
else interrupted = true;
}
if (done) {
return true;
} else {
waitTime = timeoutNanos - (System.nanoTime() - startTime);
if (waitTime <= 0) return done;
}
}
} finally {
waiters--;
}
}
} finally {
if (interrupted) Thread.currentThread().interrupt();
}
}
// object overrides -----------------------------------------------------------------------------------------------
@Override public String toString() {
StringBuilder builder = new StringBuilder()
.append("RequestFuture{")
.append("existenceTime=").append(getExistenceTime())
.append(", executionTime=").append(getExecutionTime());
if (!isDone()) builder.append(", inProgress");
else if (isSuccessful()) builder.append(", succeeded (code ").append(response.getStatus().getCode()).append(')');
else builder.append(", failed (").append(cause).append(')');
return builder.append('}').toString();
}
}