-
Notifications
You must be signed in to change notification settings - Fork 269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add HTTP/2 enabled transport as default transport #979
Changes from 6 commits
e0f3e21
93016eb
1541ad0
d78632c
81246d9
a86c325
ff1e4fb
36927d9
ed88849
21bfa8a
f9e8576
3368eb2
5797b48
0c37ba4
1881f28
74c57b7
ceac56c
18365a7
92d3112
73e1355
165ff59
11aab96
be9e869
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package com.google.firebase.internal; | ||
|
||
import com.google.api.client.util.StreamingContent; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.nio.ByteBuffer; | ||
import java.util.Set; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
import org.apache.hc.core5.http.ContentType; | ||
import org.apache.hc.core5.http.nio.AsyncEntityProducer; | ||
import org.apache.hc.core5.http.nio.DataStreamChannel; | ||
|
||
@SuppressWarnings("deprecation") | ||
public class ApacheHttp2AsyncEntityProducer implements AsyncEntityProducer { | ||
private final ByteBuffer bytebuf; | ||
private ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||
private final ContentType contentType; | ||
private final long contentLength; | ||
private final String contentEncoding; | ||
private final CompletableFuture<Void> writeFuture; | ||
private final AtomicReference<Exception> exception; | ||
|
||
public ApacheHttp2AsyncEntityProducer(StreamingContent content, ContentType contentType, | ||
String contentEncoding, long contentLength, CompletableFuture<Void> writeFuture) { | ||
this.writeFuture = writeFuture; | ||
|
||
if (content != null) { | ||
try { | ||
content.writeTo(baos); | ||
} catch (IOException e) { | ||
writeFuture.completeExceptionally(e); | ||
} | ||
} | ||
this.bytebuf = ByteBuffer.wrap(baos.toByteArray()); | ||
this.contentType = contentType; | ||
this.contentLength = contentLength; | ||
this.contentEncoding = contentEncoding; | ||
this.exception = new AtomicReference<>(); | ||
} | ||
|
||
public ApacheHttp2AsyncEntityProducer(ApacheHttp2Request request, | ||
CompletableFuture<Void> writeFuture) { | ||
this( | ||
request.getStreamingContent(), | ||
ContentType.parse(request.getContentType()), | ||
request.getContentEncoding(), | ||
request.getContentLength(), | ||
writeFuture); | ||
} | ||
|
||
@Override | ||
public boolean isRepeatable() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public String getContentType() { | ||
return contentType != null ? contentType.toString() : null; | ||
} | ||
|
||
@Override | ||
public long getContentLength() { | ||
return contentLength; | ||
} | ||
|
||
@Override | ||
public int available() { | ||
return Integer.MAX_VALUE; | ||
} | ||
|
||
@Override | ||
public String getContentEncoding() { | ||
return contentEncoding; | ||
} | ||
|
||
@Override | ||
public boolean isChunked() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public Set<String> getTrailerNames() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public void produce(DataStreamChannel channel) throws IOException { | ||
if (bytebuf.hasRemaining()) { | ||
channel.write(bytebuf); | ||
} | ||
if (!bytebuf.hasRemaining()) { | ||
channel.endStream(); | ||
writeFuture.complete(null); | ||
} | ||
} | ||
|
||
@Override | ||
public void failed(Exception cause) { | ||
if (exception.compareAndSet(null, cause)) { | ||
releaseResources(); | ||
writeFuture.completeExceptionally(cause); | ||
} | ||
} | ||
|
||
public final Exception getException() { | ||
return exception.get(); | ||
} | ||
|
||
@Override | ||
public void releaseResources() { | ||
bytebuf.clear(); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: new line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package com.google.firebase.internal; | ||
|
||
import com.google.api.client.http.LowLevelHttpRequest; | ||
import com.google.api.client.http.LowLevelHttpResponse; | ||
|
||
import java.io.IOException; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.TimeoutException; | ||
|
||
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; | ||
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; | ||
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; | ||
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer; | ||
import org.apache.hc.client5.http.config.RequestConfig; | ||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; | ||
import org.apache.hc.core5.concurrent.FutureCallback; | ||
import org.apache.hc.core5.http.nio.support.BasicRequestProducer; | ||
import org.apache.hc.core5.util.Timeout; | ||
|
||
final class ApacheHttp2Request extends LowLevelHttpRequest { | ||
private final CloseableHttpAsyncClient httpAsyncClient; | ||
private final SimpleRequestBuilder requestBuilder; | ||
private SimpleHttpRequest request; | ||
private final RequestConfig.Builder requestConfig; | ||
private int writeTimeout; | ||
|
||
ApacheHttp2Request( | ||
CloseableHttpAsyncClient httpAsyncClient, SimpleRequestBuilder requestBuilder) { | ||
this.httpAsyncClient = httpAsyncClient; | ||
this.requestBuilder = requestBuilder; | ||
this.writeTimeout = 0; | ||
|
||
this.requestConfig = RequestConfig.custom() | ||
.setRedirectsEnabled(false); | ||
} | ||
|
||
@Override | ||
public void addHeader(String name, String value) { | ||
requestBuilder.addHeader(name, value); | ||
} | ||
|
||
@Override | ||
@SuppressWarnings("deprecation") | ||
public void setTimeout(int connectionTimeout, int readTimeout) throws IOException { | ||
requestConfig | ||
.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout)) | ||
.setResponseTimeout(Timeout.ofMilliseconds(readTimeout)); | ||
} | ||
|
||
@Override | ||
public void setWriteTimeout(int writeTimeout) throws IOException { | ||
this.writeTimeout = writeTimeout; | ||
} | ||
|
||
@Override | ||
public LowLevelHttpResponse execute() throws IOException { | ||
// Set request configs | ||
requestBuilder.setRequestConfig(requestConfig.build()); | ||
|
||
// Build request | ||
request = requestBuilder.build(); | ||
|
||
// Make Producer | ||
CompletableFuture<Void> writeFuture = new CompletableFuture<>(); | ||
ApacheHttp2AsyncEntityProducer entityProducer = | ||
new ApacheHttp2AsyncEntityProducer(this, writeFuture); | ||
|
||
// Execute | ||
final CompletableFuture<SimpleHttpResponse> responseFuture = new CompletableFuture<>(); | ||
try { | ||
httpAsyncClient.execute( | ||
new BasicRequestProducer(request, entityProducer), | ||
SimpleResponseConsumer.create(), | ||
new FutureCallback<SimpleHttpResponse>() { | ||
@Override | ||
public void completed(final SimpleHttpResponse response) { | ||
responseFuture.complete(response); | ||
} | ||
|
||
@Override | ||
public void failed(final Exception exception) { | ||
responseFuture.completeExceptionally(exception); | ||
} | ||
|
||
@Override | ||
public void cancelled() { | ||
responseFuture.cancel(false); | ||
} | ||
}); | ||
|
||
if (writeTimeout != 0) { | ||
writeFuture.get(writeTimeout, TimeUnit.MILLISECONDS); | ||
} | ||
|
||
final SimpleHttpResponse response = responseFuture.get(); | ||
return new ApacheHttp2Response(request, response); | ||
} catch (InterruptedException e) { | ||
throw new IOException("Request Interrupted", e); | ||
} catch (ExecutionException e) { | ||
throw new IOException("Exception in request", e); | ||
} catch (TimeoutException e) { | ||
throw new IOException("Timed out", e); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package com.google.firebase.internal; | ||
|
||
import com.google.api.client.http.LowLevelHttpResponse; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
|
||
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; | ||
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; | ||
import org.apache.hc.core5.http.Header; | ||
|
||
public class ApacheHttp2Response extends LowLevelHttpResponse { | ||
|
||
private final SimpleHttpResponse response; | ||
private final Header[] allHeaders; | ||
|
||
ApacheHttp2Response(SimpleHttpRequest request, SimpleHttpResponse response) { | ||
this.response = response; | ||
allHeaders = response.getHeaders(); | ||
} | ||
|
||
@Override | ||
public int getStatusCode() { | ||
return response.getCode(); | ||
} | ||
|
||
@Override | ||
public InputStream getContent() throws IOException { | ||
return new ByteArrayInputStream(response.getBodyBytes()); | ||
} | ||
|
||
@Override | ||
public String getContentEncoding() { | ||
Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); | ||
if (contentEncodingHeader == null) { | ||
return null; | ||
} | ||
return contentEncodingHeader.getValue(); | ||
} | ||
|
||
@Override | ||
public long getContentLength() { | ||
return response.getBodyText().length(); | ||
} | ||
|
||
@Override | ||
public String getContentType() { | ||
return response.getContentType().toString(); | ||
} | ||
|
||
@Override | ||
public String getReasonPhrase() { | ||
return response.getReasonPhrase(); | ||
} | ||
|
||
@Override | ||
public String getStatusLine() { | ||
return response.toString(); | ||
} | ||
|
||
public String getHeaderValue(String name) { | ||
Header header = response.getLastHeader(name); | ||
if (header == null) { | ||
return null; | ||
} | ||
return header.getValue(); | ||
} | ||
|
||
@Override | ||
public String getHeaderValue(int index) { | ||
return allHeaders[index].getValue(); | ||
} | ||
|
||
@Override | ||
public int getHeaderCount() { | ||
return allHeaders.length; | ||
} | ||
|
||
@Override | ||
public String getHeaderName(int index) { | ||
return allHeaders[index].getName(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package com.google.firebase.internal; | ||
|
||
import com.google.api.client.http.HttpTransport; | ||
|
||
import java.io.IOException; | ||
import java.net.ProxySelector; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import org.apache.hc.client5.http.async.HttpAsyncClient; | ||
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; | ||
import org.apache.hc.client5.http.config.TlsConfig; | ||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; | ||
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; | ||
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; | ||
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; | ||
import org.apache.hc.core5.http.config.Http1Config; | ||
import org.apache.hc.core5.http2.HttpVersionPolicy; | ||
import org.apache.hc.core5.http2.config.H2Config; | ||
import org.apache.hc.core5.util.TimeValue; | ||
|
||
public final class ApacheHttp2Transport extends HttpTransport { | ||
|
||
public final CloseableHttpAsyncClient httpAsyncClient; | ||
|
||
public ApacheHttp2Transport() { | ||
this(newDefaultHttpAsyncClient()); | ||
} | ||
|
||
public ApacheHttp2Transport(CloseableHttpAsyncClient httpAsyncClient) { | ||
this.httpAsyncClient = httpAsyncClient; | ||
httpAsyncClient.start(); | ||
} | ||
|
||
public static CloseableHttpAsyncClient newDefaultHttpAsyncClient() { | ||
return defaultHttpAsyncClientBuilder().build(); | ||
} | ||
|
||
public static HttpAsyncClientBuilder defaultHttpAsyncClientBuilder() { | ||
PoolingAsyncClientConnectionManager connectionManager = | ||
new PoolingAsyncClientConnectionManager(); | ||
connectionManager.setMaxTotal(100); | ||
connectionManager.setDefaultMaxPerRoute(100); | ||
connectionManager.closeIdle(TimeValue.of(30, TimeUnit.SECONDS)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a note on how we decided on these limits? 100 max connections and 30 seconds timeout? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, I changed the settings here to match the google api client's apache http transport and added a comment stating that. Also correctly limited the max concurrent streams to 100 and added a comment on the reasoning for this. Also added more additional comments throughout this file. |
||
connectionManager.setDefaultTlsConfig( | ||
TlsConfig.custom().setVersionPolicy(HttpVersionPolicy.NEGOTIATE).build()); | ||
|
||
return HttpAsyncClientBuilder.create() | ||
.setH2Config(H2Config.DEFAULT) | ||
.setHttp1Config(Http1Config.DEFAULT) | ||
.setConnectionManager(connectionManager) | ||
.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault())) | ||
.disableRedirectHandling() | ||
.disableAutomaticRetries(); | ||
} | ||
|
||
@Override | ||
public boolean supportsMethod(String method) { | ||
return true; | ||
} | ||
|
||
@Override | ||
protected ApacheHttp2Request buildRequest(String method, String url) { | ||
SimpleRequestBuilder requestBuilder = SimpleRequestBuilder.create(method).setUri(url); | ||
return new ApacheHttp2Request(httpAsyncClient, requestBuilder); | ||
} | ||
|
||
@Override | ||
public void shutdown() throws IOException { | ||
httpAsyncClient.close(); | ||
} | ||
|
||
public HttpAsyncClient getHttpClient() { | ||
return httpAsyncClient; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which deprecation is this? Maybe we should add a note
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to the google api client's
StreamingContent
. Removed the suppression to match other files where we don't suppress this warning.