diff --git a/library/common/jni/jni_interface.cc b/library/common/jni/jni_interface.cc index f9671d7403..64b95e6278 100644 --- a/library/common/jni/jni_interface.cc +++ b/library/common/jni/jni_interface.cc @@ -945,3 +945,11 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_registerStringAccessor(JNIEnv* env->DeleteLocalRef(jcls_JvmStringAccessorContext); return result; } + +extern "C" JNIEXPORT void JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_drainConnections(JNIEnv* env, + jclass, // class + jlong engine) { + jni_log("[Envoy]", "drainConnections"); + drain_connections(engine); +} diff --git a/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java b/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java index dbd6b0ef2a..b6d0639450 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java +++ b/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java @@ -82,4 +82,9 @@ public int recordHistogramValue(String elements, Map tags, int v public int registerStringAccessor(String accessorName, EnvoyStringAccessor accessor) { return envoyEngine.registerStringAccessor(accessorName, accessor); } + + @Override + public void drainConnections() { + envoyEngine.drainConnections(); + } } diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java index f60736589f..bbed4c6278 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java @@ -111,4 +111,9 @@ int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyConfigurat * This is a noop if called before the underlying EnvoyEngine has started. */ void flushStats(); + + /** + * Drain all connections owned by this Engine. + */ + void drainConnections(); } diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index a5c5ff7221..8ae7e648ed 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -189,4 +189,9 @@ public int recordHistogramValue(String elements, Map tags, int v public int registerStringAccessor(String accessor_name, EnvoyStringAccessor accessor) { return JniLibrary.registerStringAccessor(accessor_name, new JvmStringAccessorContext(accessor)); } + + @Override + public void drainConnections() { + JniLibrary.drainConnections(engineHandle); + } } diff --git a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index ea2672efb6..1fdd792b71 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -285,4 +285,9 @@ protected static native int recordHistogramValue(long engine, String elements, b */ protected static native int registerStringAccessor(String accessorName, JvmStringAccessorContext context); + + /** + * Drain all connections owned by this Engine. + */ + protected static native int drainConnections(long engine); } diff --git a/library/kotlin/io/envoyproxy/envoymobile/Engine.kt b/library/kotlin/io/envoyproxy/envoymobile/Engine.kt index 08622d75a2..7a1e040be3 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/Engine.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/Engine.kt @@ -27,4 +27,9 @@ interface Engine { * This is a noop if called before the underlying EnvoyEngine has started. */ fun flushStats() + + /** + * Drain all connections owned by this Engine. + */ + fun drainConnections() } diff --git a/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt b/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt index 15d13e1360..ce6d402830 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt @@ -47,4 +47,8 @@ class EngineImpl constructor( override fun flushStats() { envoyEngine.flushStats() } + + override fun drainConnections() { + envoyEngine.drainConnections() + } } diff --git a/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt b/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt index 13c3009d30..94291e6d5b 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt @@ -42,4 +42,6 @@ internal class MockEnvoyEngine : EnvoyEngine { override fun registerStringAccessor(accessorName: String, accessor: EnvoyStringAccessor): Int = 0 override fun flushStats() = Unit + + override fun drainConnections() = Unit } diff --git a/library/objective-c/EnvoyEngine.h b/library/objective-c/EnvoyEngine.h index 2f831dc58a..ec5c75169c 100644 --- a/library/objective-c/EnvoyEngine.h +++ b/library/objective-c/EnvoyEngine.h @@ -466,6 +466,8 @@ extern const int kEnvoyFailure; - (void)terminate; +- (void)drainConnections; + @end #pragma mark - EnvoyLogger diff --git a/library/objective-c/EnvoyEngineImpl.m b/library/objective-c/EnvoyEngineImpl.m index 6d459137dc..da2aa506cb 100644 --- a/library/objective-c/EnvoyEngineImpl.m +++ b/library/objective-c/EnvoyEngineImpl.m @@ -544,6 +544,10 @@ - (void)terminate { terminate_engine(_engineHandle); } +- (void)drainConnections { + drain_connections(_engineHandle); +} + #pragma mark - Private - (void)startObservingLifecycleNotifications { diff --git a/library/swift/Engine.swift b/library/swift/Engine.swift index 802f0832dc..51b127b31d 100644 --- a/library/swift/Engine.swift +++ b/library/swift/Engine.swift @@ -17,4 +17,7 @@ public protocol Engine: AnyObject { /// Terminates the running engine. func terminate() + + /// Drain all connections owned by this Engine. + func drainConnections() } diff --git a/library/swift/EngineImpl.swift b/library/swift/EngineImpl.swift index 0eed6c76c7..4ee8109490 100644 --- a/library/swift/EngineImpl.swift +++ b/library/swift/EngineImpl.swift @@ -65,4 +65,8 @@ extension EngineImpl: Engine { func terminate() { self.engine.terminate() } + + func drainConnections() { + self.engine.drainConnections() + } } diff --git a/library/swift/mocks/MockEnvoyEngine.swift b/library/swift/mocks/MockEnvoyEngine.swift index 1b43e55db4..195d06439d 100644 --- a/library/swift/mocks/MockEnvoyEngine.swift +++ b/library/swift/mocks/MockEnvoyEngine.swift @@ -91,4 +91,6 @@ extension MockEnvoyEngine: EnvoyEngine { } func terminate() {} + + func drainConnections() {} } diff --git a/test/kotlin/integration/BUILD b/test/kotlin/integration/BUILD index 66ceb9dbb9..04873ecc20 100644 --- a/test/kotlin/integration/BUILD +++ b/test/kotlin/integration/BUILD @@ -71,6 +71,20 @@ envoy_mobile_jni_kt_test( ], ) +envoy_mobile_jni_kt_test( + name = "drain_connections_test", + srcs = [ + "DrainConnectionsTest.kt", + ], + native_deps = [ + "//library/common/jni:libjava_jni_lib.so", + "//library/common/jni:java_jni_lib.jnilib", + ], + deps = [ + "//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib", + ], +) + envoy_mobile_jni_kt_test( name = "grpc_receive_error_test", srcs = [ diff --git a/test/kotlin/integration/DrainConnectionsTest.kt b/test/kotlin/integration/DrainConnectionsTest.kt new file mode 100644 index 0000000000..597c659120 --- /dev/null +++ b/test/kotlin/integration/DrainConnectionsTest.kt @@ -0,0 +1,118 @@ +package test.kotlin.integration + +import io.envoyproxy.envoymobile.Custom +import io.envoyproxy.envoymobile.EngineBuilder +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.UpstreamHttpProtocol +import io.envoyproxy.envoymobile.engine.JniLibrary +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.fail +import org.junit.Test + +private val apiListenerType = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.EnvoyMobileHttpConnectionManager" +private val assertionFilterType = "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion" +private val config = +""" +static_resources: + listeners: + - name: base_api_listener + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + api_listener: + api_listener: + "@type": $apiListenerType + config: + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + http_filters: + - name: envoy.filters.http.assertion + typed_config: + "@type": $assertionFilterType + match_config: + http_request_headers_match: + headers: + - name: ":authority" + exact_match: example.com + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +""" + +class DrainConnectionsTest { + + init { + JniLibrary.loadTestLibrary() + } + + @Test + fun `successful request after connection drain`() { + val headersExpectation = CountDownLatch(2) + + val engine = EngineBuilder(Custom(config)).build() + val client = engine.streamClient() + + val requestHeaders = RequestHeadersBuilder( + method = RequestMethod.GET, + scheme = "https", + authority = "example.com", + path = "/test" + ) + .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2) + .build() + + var resultHeaders1: ResponseHeaders? = null + var resultEndStream1: Boolean? = null + client.newStreamPrototype() + .setOnResponseHeaders { responseHeaders, endStream, _ -> + resultHeaders1 = responseHeaders + resultEndStream1 = endStream + headersExpectation.countDown() + } + .setOnError { _, _ -> fail("Unexpected error") } + .start() + .sendHeaders(requestHeaders, true) + + headersExpectation.await(10, TimeUnit.SECONDS) + + engine.drainConnections() + + var resultHeaders2: ResponseHeaders? = null + var resultEndStream2: Boolean? = null + client.newStreamPrototype() + .setOnResponseHeaders { responseHeaders, endStream, _ -> + resultHeaders2 = responseHeaders + resultEndStream2 = endStream + headersExpectation.countDown() + } + .setOnError { _, _ -> fail("Unexpected error") } + .start() + .sendHeaders(requestHeaders, true) + + headersExpectation.await(10, TimeUnit.SECONDS) + + engine.terminate() + + assertThat(headersExpectation.count).isEqualTo(0) + assertThat(resultHeaders1!!.httpStatus).isEqualTo(200) + assertThat(resultEndStream1).isTrue() + assertThat(resultHeaders2!!.httpStatus).isEqualTo(200) + assertThat(resultEndStream2).isTrue() + } +} diff --git a/test/swift/integration/BUILD b/test/swift/integration/BUILD index d4a3bf7cdc..fe06ba1f80 100644 --- a/test/swift/integration/BUILD +++ b/test/swift/integration/BUILD @@ -12,6 +12,16 @@ envoy_mobile_swift_test( ], ) +envoy_mobile_swift_test( + name = "drain_connections_test", + srcs = [ + "DrainConnectionsTest.swift", + ], + deps = [ + "//library/objective-c:envoy_engine_objc_lib", + ], +) + envoy_mobile_swift_test( name = "direct_response_contains_headers_integration_test", srcs = [ diff --git a/test/swift/integration/DrainConnectionsTest.swift b/test/swift/integration/DrainConnectionsTest.swift new file mode 100644 index 0000000000..ce19948275 --- /dev/null +++ b/test/swift/integration/DrainConnectionsTest.swift @@ -0,0 +1,101 @@ +import Envoy +import EnvoyEngine +import Foundation +import XCTest + +final class DrainConnectionsTest: XCTestCase { + func testDrainConnections() { + // swiftlint:disable:next line_length + let emhcmType = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.EnvoyMobileHttpConnectionManager" + // swiftlint:disable:next line_length + let assertionFilterType = "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion" + let config = +""" +static_resources: + listeners: + - name: base_api_listener + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + api_listener: + api_listener: + "@type": \(emhcmType) + config: + stat_prefix: hcm + route_config: + name: api_router + virtual_hosts: + - name: api + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + http_filters: + - name: envoy.filters.http.assertion + typed_config: + "@type": \(assertionFilterType) + match_config: + http_request_headers_match: + headers: + - name: ":authority" + exact_match: example.com + - name: envoy.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +""" + let engine = EngineBuilder(yaml: config) + .addLogLevel(.debug) + .build() + + let client = engine + .streamClient() + + let requestHeaders = RequestHeadersBuilder(method: .get, scheme: "https", + authority: "example.com", path: "/test") + .addUpstreamHttpProtocol(.http2) + .build() + + let expectation1 = + self.expectation(description: "Run called with expected http status first request") + + client + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, endStream, _ in + XCTAssertEqual(200, responseHeaders.httpStatus) + XCTAssertTrue(endStream) + expectation1.fulfill() + } + .setOnError { _, _ in + XCTFail("Unexpected error") + } + .start() + .sendHeaders(requestHeaders, endStream: true) + + XCTAssertEqual(XCTWaiter.wait(for: [expectation1], timeout: 1), .completed) + + engine.drainConnections() + + let expectation2 = + self.expectation(description: "Run called with expected http status first request") + + client + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, endStream, _ in + XCTAssertEqual(200, responseHeaders.httpStatus) + XCTAssertTrue(endStream) + expectation2.fulfill() + } + .setOnError { _, _ in + XCTFail("Unexpected error") + } + .start() + .sendHeaders(requestHeaders, endStream: true) + + XCTAssertEqual(XCTWaiter.wait(for: [expectation2], timeout: 1), .completed) + } +}