Skip to content
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

Merged
merged 23 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@
<artifactId>netty-transport</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>

<!-- Test Dependencies -->
<dependency>
Expand Down
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")
Copy link
Member

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

Copy link
Contributor Author

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.

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();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

107 changes: 107 additions & 0 deletions src/main/java/com/google/firebase/internal/ApacheHttp2Request.java
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));
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
}
Loading