diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 2d1906a4adc2..0d180fbe9790 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -180,6 +180,15 @@ task japicmp(type: me.champeau.gradle.japicmp.JapicmpTask, dependsOn: 'jar') { 'okhttp3.MediaType#charset(java.nio.charset.Charset)', 'okhttp3.MediaType#subtype()', 'okhttp3.MediaType#type()', + 'okhttp3.RealCall#getResponseWithInterceptorChain()', + 'okhttp3.RealCall#redactedUrl()', + 'okhttp3.RealCall#toLoggableString()', + 'okhttp3.RealCall$AsyncCall#callsPerHost()', + 'okhttp3.RealCall$AsyncCall#executeOn(java.util.concurrent.ExecutorService)', + 'okhttp3.RealCall$AsyncCall#get()', + 'okhttp3.RealCall$AsyncCall#host()', + 'okhttp3.RealCall$AsyncCall#request()', + 'okhttp3.RealCall$AsyncCall#reuseCallsPerHostFrom(okhttp3.RealCall$AsyncCall)', 'okhttp3.TlsVersion#javaName()', ] } diff --git a/okhttp/src/main/java/okhttp3/RealCall.java b/okhttp/src/main/java/okhttp3/RealCall.java deleted file mode 100644 index bb108764daad..000000000000 --- a/okhttp/src/main/java/okhttp3/RealCall.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (C) 2014 Square, Inc. - * - * 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 okhttp3; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicInteger; -import okhttp3.internal.NamedRunnable; -import okhttp3.internal.cache.CacheInterceptor; -import okhttp3.internal.connection.ConnectInterceptor; -import okhttp3.internal.connection.Transmitter; -import okhttp3.internal.http.BridgeInterceptor; -import okhttp3.internal.http.CallServerInterceptor; -import okhttp3.internal.http.RealInterceptorChain; -import okhttp3.internal.http.RetryAndFollowUpInterceptor; -import okhttp3.internal.platform.Platform; -import okio.Timeout; - -import static okhttp3.internal.Util.closeQuietly; -import static okhttp3.internal.platform.Platform.INFO; - -final class RealCall implements Call { - final OkHttpClient client; - - /** - * There is a cycle between the {@link Call} and {@link Transmitter} that makes this awkward. - * This is set after immediately after creating the call instance. - */ - private Transmitter transmitter; - - /** The application's original request unadulterated by redirects or auth headers. */ - final Request originalRequest; - final boolean forWebSocket; - - // Guarded by this. - private boolean executed; - - private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { - this.client = client; - this.originalRequest = originalRequest; - this.forWebSocket = forWebSocket; - } - - static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { - // Safely publish the Call instance to the EventListener. - RealCall call = new RealCall(client, originalRequest, forWebSocket); - call.transmitter = new Transmitter(client, call); - return call; - } - - @Override public Request request() { - return originalRequest; - } - - @Override public Response execute() throws IOException { - synchronized (this) { - if (executed) throw new IllegalStateException("Already Executed"); - executed = true; - } - transmitter.timeoutEnter(); - transmitter.callStart(); - try { - client.dispatcher().executed(this); - return getResponseWithInterceptorChain(); - } finally { - client.dispatcher().finished(this); - } - } - - @Override public void enqueue(Callback responseCallback) { - synchronized (this) { - if (executed) throw new IllegalStateException("Already Executed"); - executed = true; - } - transmitter.callStart(); - client.dispatcher().enqueue(new AsyncCall(responseCallback)); - } - - @Override public void cancel() { - transmitter.cancel(); - } - - @Override public Timeout timeout() { - return transmitter.timeout(); - } - - @Override public synchronized boolean isExecuted() { - return executed; - } - - @Override public boolean isCanceled() { - return transmitter.isCanceled(); - } - - @SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state. - @Override public RealCall clone() { - return RealCall.newRealCall(client, originalRequest, forWebSocket); - } - - final class AsyncCall extends NamedRunnable { - private final Callback responseCallback; - private volatile AtomicInteger callsPerHost = new AtomicInteger(0); - - AsyncCall(Callback responseCallback) { - super("OkHttp %s", redactedUrl()); - this.responseCallback = responseCallback; - } - - AtomicInteger callsPerHost() { - return callsPerHost; - } - - void reuseCallsPerHostFrom(AsyncCall other) { - this.callsPerHost = other.callsPerHost; - } - - String host() { - return originalRequest.url().host(); - } - - Request request() { - return originalRequest; - } - - RealCall get() { - return RealCall.this; - } - - /** - * Attempt to enqueue this async call on {@code executorService}. This will attempt to clean up - * if the executor has been shut down by reporting the call as failed. - */ - void executeOn(ExecutorService executorService) { - assert (!Thread.holdsLock(client.dispatcher())); - boolean success = false; - try { - executorService.execute(this); - success = true; - } catch (RejectedExecutionException e) { - InterruptedIOException ioException = new InterruptedIOException("executor rejected"); - ioException.initCause(e); - transmitter.noMoreExchanges(ioException); - responseCallback.onFailure(RealCall.this, ioException); - } finally { - if (!success) { - client.dispatcher().finished(this); // This call is no longer running! - } - } - } - - @Override protected void execute() { - boolean signalledCallback = false; - transmitter.timeoutEnter(); - try { - Response response = getResponseWithInterceptorChain(); - signalledCallback = true; - responseCallback.onResponse(RealCall.this, response); - } catch (IOException e) { - if (signalledCallback) { - // Do not signal the callback twice! - Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e); - } else { - responseCallback.onFailure(RealCall.this, e); - } - } finally { - client.dispatcher().finished(this); - } - } - } - - /** - * Returns a string that describes this call. Doesn't include a full URL as that might contain - * sensitive information. - */ - String toLoggableString() { - return (isCanceled() ? "canceled " : "") - + (forWebSocket ? "web socket" : "call") - + " to " + redactedUrl(); - } - - String redactedUrl() { - return originalRequest.url().redact(); - } - - Response getResponseWithInterceptorChain() throws IOException { - // Build a full stack of interceptors. - List interceptors = new ArrayList<>(); - interceptors.addAll(client.interceptors()); - interceptors.add(new RetryAndFollowUpInterceptor(client)); - interceptors.add(new BridgeInterceptor(client.cookieJar())); - interceptors.add(new CacheInterceptor(client.internalCache())); - interceptors.add(new ConnectInterceptor(client)); - if (!forWebSocket) { - interceptors.addAll(client.networkInterceptors()); - } - interceptors.add(new CallServerInterceptor(forWebSocket)); - - Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, - originalRequest, this, client.connectTimeoutMillis(), - client.readTimeoutMillis(), client.writeTimeoutMillis()); - - boolean calledNoMoreExchanges = false; - try { - Response response = chain.proceed(originalRequest); - if (transmitter.isCanceled()) { - closeQuietly(response); - throw new IOException("Canceled"); - } - return response; - } catch (IOException e) { - calledNoMoreExchanges = true; - throw transmitter.noMoreExchanges(e); - } finally { - if (!calledNoMoreExchanges) { - transmitter.noMoreExchanges(null); - } - } - } -} diff --git a/okhttp/src/main/java/okhttp3/RealCall.kt b/okhttp/src/main/java/okhttp3/RealCall.kt new file mode 100644 index 000000000000..24138df61f7e --- /dev/null +++ b/okhttp/src/main/java/okhttp3/RealCall.kt @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * 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 okhttp3 + +import okhttp3.internal.NamedRunnable +import okhttp3.internal.Util.closeQuietly +import okhttp3.internal.cache.CacheInterceptor +import okhttp3.internal.connection.ConnectInterceptor +import okhttp3.internal.connection.Transmitter +import okhttp3.internal.http.BridgeInterceptor +import okhttp3.internal.http.CallServerInterceptor +import okhttp3.internal.http.RealInterceptorChain +import okhttp3.internal.http.RetryAndFollowUpInterceptor +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.Platform.Companion.INFO +import okio.Timeout +import java.io.IOException +import java.io.InterruptedIOException +import java.util.ArrayList +import java.util.concurrent.ExecutorService +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicInteger + +internal class RealCall private constructor( + val client: OkHttpClient, + /** The application's original request unadulterated by redirects or auth headers. */ + val originalRequest: Request, + // TODO(egorand): Remove @JvmField once Dispatcher.java is converted to Kotlin + @JvmField val forWebSocket: Boolean +) : Call { + /** + * There is a cycle between the [Call] and [Transmitter] that makes this awkward. + * This is set after immediately after creating the call instance. + */ + private lateinit var transmitter: Transmitter + + // Guarded by this. + @get:Synchronized override var isExecuted: Boolean = false + + override val isCanceled: Boolean + get() = transmitter.isCanceled + + override fun request(): Request = originalRequest + + override fun execute(): Response { + synchronized(this) { + check(!isExecuted) { "Already Executed" } + isExecuted = true + } + transmitter.timeoutEnter() + transmitter.callStart() + try { + client.dispatcher().executed(this) + return getResponseWithInterceptorChain() + } finally { + client.dispatcher().finished(this) + } + } + + override fun enqueue(responseCallback: Callback) { + synchronized(this) { + check(!isExecuted) { "Already Executed" } + isExecuted = true + } + transmitter.callStart() + client.dispatcher().enqueue(AsyncCall(responseCallback)) + } + + override fun cancel() { + transmitter.cancel() + } + + override fun timeout(): Timeout = transmitter.timeout() + + @SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state. + override fun clone(): RealCall { + return RealCall.newRealCall(client, originalRequest, forWebSocket) + } + + internal inner class AsyncCall( + private val responseCallback: Callback + ) : NamedRunnable("OkHttp %s", redactedUrl()) { + @Volatile private var callsPerHost = AtomicInteger(0) + + fun callsPerHost(): AtomicInteger = callsPerHost + + fun reuseCallsPerHostFrom(other: AsyncCall) { + this.callsPerHost = other.callsPerHost + } + + fun host(): String = originalRequest.url().host() + + fun request(): Request = originalRequest + + fun get(): RealCall = this@RealCall + + /** + * Attempt to enqueue this async call on `executorService`. This will attempt to clean up + * if the executor has been shut down by reporting the call as failed. + */ + fun executeOn(executorService: ExecutorService) { + assert(!Thread.holdsLock(client.dispatcher())) + var success = false + try { + executorService.execute(this) + success = true + } catch (e: RejectedExecutionException) { + val ioException = InterruptedIOException("executor rejected") + ioException.initCause(e) + transmitter.noMoreExchanges(ioException) + responseCallback.onFailure(this@RealCall, ioException) + } finally { + if (!success) { + client.dispatcher().finished(this) // This call is no longer running! + } + } + } + + override fun execute() { + var signalledCallback = false + transmitter.timeoutEnter() + try { + val response = getResponseWithInterceptorChain() + signalledCallback = true + responseCallback.onResponse(this@RealCall, response) + } catch (e: IOException) { + if (signalledCallback) { + // Do not signal the callback twice! + Platform.get().log(INFO, "Callback failure for ${toLoggableString()}", e) + } else { + responseCallback.onFailure(this@RealCall, e) + } + } finally { + client.dispatcher().finished(this) + } + } + } + + /** + * Returns a string that describes this call. Doesn't include a full URL as that might contain + * sensitive information. + */ + fun toLoggableString(): String { + return ((if (isCanceled) "canceled " else "") + + (if (forWebSocket) "web socket" else "call") + + " to " + redactedUrl()) + } + + fun redactedUrl(): String = originalRequest.url().redact() + + @Throws(IOException::class) + fun getResponseWithInterceptorChain(): Response { + // Build a full stack of interceptors. + val interceptors = ArrayList() + interceptors.addAll(client.interceptors()) + interceptors.add(RetryAndFollowUpInterceptor(client)) + interceptors.add(BridgeInterceptor(client.cookieJar())) + interceptors.add(CacheInterceptor(client.internalCache())) + interceptors.add(ConnectInterceptor(client)) + if (!forWebSocket) { + interceptors.addAll(client.networkInterceptors()) + } + interceptors.add(CallServerInterceptor(forWebSocket)) + + val chain = RealInterceptorChain(interceptors, transmitter, null, 0, + originalRequest, this, client.connectTimeoutMillis(), + client.readTimeoutMillis(), client.writeTimeoutMillis()) + + var calledNoMoreExchanges = false + try { + val response = chain.proceed(originalRequest) + if (transmitter.isCanceled) { + closeQuietly(response) + throw IOException("Canceled") + } + return response + } catch (e: IOException) { + calledNoMoreExchanges = true + throw transmitter.noMoreExchanges(e) as Throwable + } finally { + if (!calledNoMoreExchanges) { + transmitter.noMoreExchanges(null) + } + } + } + + companion object { + // TODO(egorand): Remove @JvmStatic once OkHttpClient.java is converted to Kotlin + @JvmStatic fun newRealCall( + client: OkHttpClient, + originalRequest: Request, + forWebSocket: Boolean + ): RealCall { + // Safely publish the Call instance to the EventListener. + return RealCall(client, originalRequest, forWebSocket).apply { + transmitter = Transmitter(client, this) + } + } + } +}