Skip to content

Commit

Permalink
mobile: improved HTTP/3 cronet testing (#35600)
Browse files Browse the repository at this point in the history
This adds testing of doing retries post HTTP/3 handshake, and resetting
brokenness on networ kchange.

Risk Level: n/a (adds a stat to tracking)
Testing: yes
Docs Changes: no
Release Notes: no

---------

Signed-off-by: Alyssa Wilk <[email protected]>
  • Loading branch information
alyssawilk authored Aug 6, 2024
1 parent 56be4f4 commit a8ec87b
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 32 deletions.
1 change: 1 addition & 0 deletions mobile/library/cc/engine_builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ std::unique_ptr<envoy::config::bootstrap::v3::Bootstrap> EngineBuilder::generate
list->add_patterns()->set_prefix("cluster.base.upstream_cx_");
list->add_patterns()->set_prefix("cluster.stats.upstream_cx_");
list->add_patterns()->set_exact("cluster.base.http2.keepalive_timeout");
list->add_patterns()->set_exact("cluster.base.upstream_http3_broken");
list->add_patterns()->set_exact("cluster.stats.http2.keepalive_timeout");
list->add_patterns()->set_prefix("http.hcm.downstream_rq_");
list->add_patterns()->set_prefix("http.hcm.decompressor.");
Expand Down
18 changes: 16 additions & 2 deletions mobile/test/common/integration/test_server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,22 @@ void TestServer::start(TestServerType type, int port) {
}
}

upstream_ = std::make_unique<AutonomousUpstream>(std::move(factory), port_, version_,
upstream_config_, true);
// We have series of Cronvoy tests which don't bind to port 0, and often hit
// port conflicts with other processes using 127.0.0.1. Default non-apple
// builds to 127.0.0.1 (this fails on iOS and probably OSX with Can't assign
// requested address)
#if !defined(__APPLE__)
if (version_ == Network::Address::IpVersion::v4) {
#else
if (false) {
#endif
auto address = Network::Utility::parseInternetAddressNoThrow("127.0.0.3", port_);
upstream_ =
std::make_unique<AutonomousUpstream>(std::move(factory), address, upstream_config_, true);
} else {
upstream_ = std::make_unique<AutonomousUpstream>(std::move(factory), port_, version_,
upstream_config_, true);
}

// Legacy behavior for cronet tests.
if (type == TestServerType::HTTP3) {
Expand Down
220 changes: 190 additions & 30 deletions mobile/test/java/org/chromium/net/CronetHttp3Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import static org.chromium.net.testing.CronetTestRule.getContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import io.envoyproxy.envoymobile.engine.types.EnvoyNetworkType;
import org.chromium.net.impl.CronvoyUrlRequestContext;
import io.envoyproxy.envoymobile.engine.EnvoyEngine;
import org.chromium.net.impl.CronvoyLogger;
import androidx.test.core.app.ApplicationProvider;
import org.chromium.net.testing.TestUploadDataProvider;
import androidx.test.filters.SmallTest;
import org.chromium.net.impl.CronvoyUrlRequestContext;
import org.chromium.net.impl.NativeCronvoyEngineBuilderImpl;
Expand All @@ -28,7 +31,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;

/**
* Test CronetEngine with production HTTP/3 logic
*/
Expand All @@ -51,6 +53,8 @@ public class CronetHttp3Test {
private CronvoyLogger logger;
// The engine for this test.
private CronvoyUrlRequestContext cronvoyEngine;
// A URL which will point to the IP and port of the test servers.
private String testServerUrl;

@BeforeClass
public static void loadJniLibrary() {
Expand All @@ -72,6 +76,7 @@ public void setUp(boolean setUpLogging) throws Exception {
http2TestServer = HttpTestServerFactory.start(
HttpTestServerFactory.Type.HTTP2_WITH_TLS, http3TestServer.getPort(), headers,
"This is a simple text file served by QUIC.\n", Collections.emptyMap());
testServerUrl = "https://" + http2TestServer.getAddress() + "/";

// Optionally, set up logging. This will slow down the tests a bit but make debugging much
// easier.
Expand All @@ -83,57 +88,212 @@ public void log(int logLevel, String message) {
}
};
}

// Set up the Envoy engine.
NativeCronvoyEngineBuilderImpl nativeCronetEngineBuilder =
new NativeCronvoyEngineBuilderImpl(ApplicationProvider.getApplicationContext());
nativeCronetEngineBuilder.addRuntimeGuard("reset_brokenness_on_nework_change", true);
if (setUpLogging) {
nativeCronetEngineBuilder.setLogger(logger);
nativeCronetEngineBuilder.setLogLevel(EnvoyEngine.LogLevel.TRACE);
}
// Make sure the handshake will work despite lack of real certs.
nativeCronetEngineBuilder.setMockCertVerifierForTesting();
cronvoyEngine = new CronvoyUrlRequestContext(nativeCronetEngineBuilder);
}

@After
public void tearDown() throws Exception {
// Shut down Envoy and the test servers.
cronvoyEngine.shutdown();
http2TestServer.shutdown();
http3TestServer.shutdown();
if (http3TestServer != null) {
http3TestServer.shutdown();
}
}

private TestUrlRequestCallback doBasicGetRequest() {
TestUrlRequestCallback callback = new TestUrlRequestCallback();
UrlRequest.Builder urlRequestBuilder =
cronvoyEngine.newUrlRequestBuilder(testServerUrl, callback, callback.getExecutor());
urlRequestBuilder.build().start();
callback.blockForDone();
return callback;
}

// Sets up a basic POST request with 4 byte body, set idempotent.
private TestUrlRequestCallback doBasicPostRequest() {
TestUrlRequestCallback callback = new TestUrlRequestCallback();
ExperimentalUrlRequest.Builder urlRequestBuilder =
cronvoyEngine.newUrlRequestBuilder(testServerUrl, callback, callback.getExecutor());
urlRequestBuilder.addHeader("content-type", "text");
urlRequestBuilder.setHttpMethod("POST");
urlRequestBuilder.setIdempotency(ExperimentalUrlRequest.Builder.IDEMPOTENT);
TestUploadDataProvider dataProvider = new TestUploadDataProvider(
TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor());
dataProvider.addRead("test".getBytes());
urlRequestBuilder.setUploadDataProvider(dataProvider, callback.getExecutor());
urlRequestBuilder.build().start();
callback.blockForDone();
return callback;
}

void doInitialHttp2Request() {
// Do a request to https://127.0.0.1:test_server_port/
TestUrlRequestCallback callback = doBasicGetRequest();

// Make sure the request succeeded. It should go out over HTTP/2 as it's the first
// request and HTTP/3 support is not established.
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("h2", callback.mResponseInfo.getNegotiatedProtocol());
}

@Test
@SmallTest
@Feature({"Cronet"})
public void testInitEngineAndStartRequest() throws Exception {
public void basicHttp3Get() throws Exception {
// Ideally we could override this from the command line but that's TBD.
setUp(printEnvoyLogs);

// Set up the Envoy engine.
NativeCronvoyEngineBuilderImpl nativeCronetEngineBuilder =
new NativeCronvoyEngineBuilderImpl(ApplicationProvider.getApplicationContext());
if (printEnvoyLogs) {
nativeCronetEngineBuilder.setLogger(logger);
nativeCronetEngineBuilder.setLogLevel(EnvoyEngine.LogLevel.TRACE);
}
// Make sure the handshake will work despite lack of real certs.
nativeCronetEngineBuilder.setMockCertVerifierForTesting();
cronvoyEngine = new CronvoyUrlRequestContext(nativeCronetEngineBuilder);
// Do the initial HTTP/2 request to get the alt-svc response.
doInitialHttp2Request();

// Do a request to https://127.0.0.1:test_server_port/
TestUrlRequestCallback callback1 = new TestUrlRequestCallback();
String newUrl = "https://" + http2TestServer.getAddress() + "/";
// Set up a second request, which will hopefully go out over HTTP/3 due to alt-svc
// advertisement.
TestUrlRequestCallback callback = doBasicGetRequest();

// Verify the second request used HTTP/3
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("h3", callback.mResponseInfo.getNegotiatedProtocol());
}

@Test
@SmallTest
@Feature({"Cronet"})
public void failToHttp2() throws Exception {
// Ideally we could override this from the command line but that's TBD.
setUp(printEnvoyLogs);

// Do the initial HTTP/2 request to get the alt-svc response.
doInitialHttp2Request();

// Set up a second request, which will hopefully go out over HTTP/3 due to alt-svc
// advertisement.
TestUrlRequestCallback getCallback = doBasicGetRequest();

// Verify the second request used HTTP/3
assertEquals(200, getCallback.mResponseInfo.getHttpStatusCode());
assertEquals("h3", getCallback.mResponseInfo.getNegotiatedProtocol());

// Now stop the HTTP/3 server.
http3TestServer.shutdown();
http3TestServer = null;

// The next request will fail on HTTP2 but should succeed on HTTP/2 despite having a body.
TestUrlRequestCallback postCallback = doBasicPostRequest();
assertEquals(200, postCallback.mResponseInfo.getHttpStatusCode());
assertEquals("h2", postCallback.mResponseInfo.getNegotiatedProtocol());
}

@Test
@SmallTest
@Feature({"Cronet"})
public void testNoRetryPostAfterHandshake() throws Exception {
setUp(printEnvoyLogs);

// Do the initial HTTP/2 request to get the alt-svc response.
doInitialHttp2Request();

// Set up a second request, which will hopefully go out over HTTP/3 due to alt-svc
// advertisement.
TestUrlRequestCallback callback = new TestUrlRequestCallback();
UrlRequest.Builder urlRequestBuilder =
cronvoyEngine.newUrlRequestBuilder(newUrl, callback1, callback1.getExecutor());
cronvoyEngine.newUrlRequestBuilder(testServerUrl, callback, callback.getExecutor());
// Set the upstream to reset after the request.
urlRequestBuilder.addHeader("reset_after_request", "yes");
urlRequestBuilder.addHeader("content-type", "text");
urlRequestBuilder.setHttpMethod("POST");
TestUploadDataProvider dataProvider = new TestUploadDataProvider(
TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor());
dataProvider.addRead("test".getBytes());
urlRequestBuilder.setUploadDataProvider(dataProvider, callback.getExecutor());

urlRequestBuilder.build().start();
callback1.blockForDone();
callback.blockForDone();

// Make sure the request succeeded. It should go out over HTTP/2 as it's the first
// request and HTTP/3 support is not established.
assertEquals(200, callback1.mResponseInfo.getHttpStatusCode());
assertEquals("h2", callback1.mResponseInfo.getNegotiatedProtocol());
// Both HTTP/3 and HTTP/2 servers will reset after the request.
assertTrue(callback.mOnErrorCalled);
// There are 2 requests - the initial HTTP/2 alt-svc request and the HTTP/3 request.
// By default, POST requests will not retry.
String stats = cronvoyEngine.getEnvoyEngine().dumpStats();
assertTrue(stats.contains("cluster.base.upstream_rq_total: 2"));
}

// Set up to use HTTP/3, then force HTTP/3 to fail post-handshake. The request should
// be retried on HTTP/2 and HTTP/3 will be marked broken.
public void retryPostHandshake() throws Exception {
// Do the initial HTTP/2 request to get the alt-svc response.
doInitialHttp2Request();

// Set up a second request, which will hopefully go out over HTTP/3 due to alt-svc
// advertisement.
TestUrlRequestCallback callback2 = new TestUrlRequestCallback();
UrlRequest.Builder urlRequestBuilder2 =
cronvoyEngine.newUrlRequestBuilder(newUrl, callback2, callback2.getExecutor());
urlRequestBuilder2.build().start();
callback2.blockForDone();
TestUrlRequestCallback callback = new TestUrlRequestCallback();
ExperimentalUrlRequest.Builder urlRequestBuilder =
cronvoyEngine.newUrlRequestBuilder(testServerUrl, callback, callback.getExecutor());
urlRequestBuilder.addHeader("reset_after_request", "yes");
urlRequestBuilder.addHeader("content-type", "text");
urlRequestBuilder.setHttpMethod("POST");
TestUploadDataProvider dataProvider = new TestUploadDataProvider(
TestUploadDataProvider.SuccessCallbackMode.SYNC, callback.getExecutor());
dataProvider.addRead("test".getBytes());
urlRequestBuilder.setUploadDataProvider(dataProvider, callback.getExecutor());
// Set the request to be idempotent so Envoy knows it's safe to retry post-handshake
urlRequestBuilder.setIdempotency(ExperimentalUrlRequest.Builder.IDEMPOTENT);

// Verify the second request used HTTP/3
assertEquals(200, callback2.mResponseInfo.getHttpStatusCode());
assertEquals("h3", callback2.mResponseInfo.getNegotiatedProtocol());
urlRequestBuilder.build().start();
callback.blockForDone();

String stats = cronvoyEngine.getEnvoyEngine().dumpStats();

// Both HTTP/3 and HTTP/2 servers will reset after the request.
assertTrue(callback.mOnErrorCalled);
// Unlike testNoRetryPostPostHandshake there will be 3 requests - the initial HTTP/2 alt-svc
// request, the HTTP/3 request, and the HTTP/2 retry.
assertTrue(stats.contains("cluster.base.upstream_rq_total: 3"));
assertTrue(stats.contains("cluster.base.upstream_rq_retry: 1"));
// Because H/3 was disallowed on the final retry and TCP connected, H/3 gets marked as broken.
assertTrue(stats.contains("cluster.base.upstream_http3_broken: 1"));
}

@Test
@SmallTest
@Feature({"Cronet"})
public void testRetryPostHandshake() throws Exception {
setUp(printEnvoyLogs);

retryPostHandshake();
}

@Test
@SmallTest
@Feature({"Cronet"})
public void networkChangeAffectsBrokenness() throws Exception {
setUp(printEnvoyLogs);

// Set HTTP/3 to be marked as broken.
retryPostHandshake();

// From prior calls, there was one HTTP/3 connection established.
String preStats = cronvoyEngine.getEnvoyEngine().dumpStats();
assertTrue(preStats.contains("cluster.base.upstream_cx_http3_total: 1"));

// This should change QUIC brokenness to "failed recently".
cronvoyEngine.getEnvoyEngine().setPreferredNetwork(EnvoyNetworkType.WLAN);

// The next request may go out over HTTP/2 or HTTP/3 (depends on who wins the race)
// but HTTP/3 will be tried.
doBasicGetRequest();
String postStats = cronvoyEngine.getEnvoyEngine().dumpStats();
assertTrue(postStats.contains("cluster.base.upstream_cx_http3_total: 2"));
}
}

0 comments on commit a8ec87b

Please sign in to comment.