Skip to content

Commit

Permalink
kotlin: add cancel stream test and fix custom yaml path (#1424)
Browse files Browse the repository at this point in the history
Adding a cancel stream test to mirror swift's cancel stream test: https://github.com/envoyproxy/envoy-mobile/blob/102b40d0da174200b4308a2677f6788a1df2a0aa/test/swift/integration/CancelStreamTest.swift

Found that the custom yaml configuration also has a bug in it where it does not appropriately register platform and native filters when a custom yaml is used. Luckily this is the first usage of the custom yaml. The JNI structure is different from the objective-c structure but I think I was able to mimic it as close as possible by just requiring an `EnvoyConfiguration`.

The API changes are here: https://github.com/envoyproxy/envoy-mobile/pull/1424/files#diff-c4d0f50e81c31efc078d029708e09a001ea3e466c6b38141c027fad21c6b70abR30-R34

Signed-off-by: Alan Chiu <[email protected]>

For an explanation of how to fill out the fields, please see the relevant section
in [PULL_REQUESTS.md](https://github.com/envoyproxy/envoy/blob/master/PULL_REQUESTS.md)

Description: kotlin: add cancel stream test and fix custom yaml path
Risk Level: low
Testing: integration test
Docs Changes: n/a
Release Notes: n/a
[Optional Fixes #Issue]
[Optional Deprecated:]
  • Loading branch information
Alan Chiu authored May 5, 2021
1 parent b56e17c commit 7c15bb6
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ public EnvoyHTTPStream startStream(EnvoyHTTPCallbacks callbacks) {
return envoyEngine.startStream(callbacks);
}

@Override
public int runWithConfig(String configurationYAML, String logLevel) {
public int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyConfiguration,
String logLevel) {
// re-enable lifecycle-based stat flushing when https://github.com/lyft/envoy-mobile/issues/748
// gets fixed. AndroidAppLifecycleMonitor monitor = new AndroidAppLifecycleMonitor();
// application.registerActivityLifecycleCallbacks(monitor);
return envoyEngine.runWithConfig(configurationYAML, logLevel);
return envoyEngine.runWithTemplate(configurationYAML, envoyConfiguration, logLevel);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public class EnvoyConfiguration {
* @param appId the App ID of the App using this Envoy Client.
* @param virtualClusters the JSON list of virtual cluster configs.
* @param nativeFilterChain the configuration for native filters.
* @param httpPlatformFilterFactories the configuration for platform filters.
* @param stringAccesssors platform string accessors to register.
* @param httpPlatformFilterFactories the configuration for platform filters.
* @param stringAccessors platform string accessors to register.
*/
public EnvoyConfiguration(String statsDomain, int connectTimeoutSeconds, int dnsRefreshSeconds,
int dnsFailureRefreshSecondsBase, int dnsFailureRefreshSecondsMax,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package io.envoyproxy.envoymobile.engine;

import io.envoyproxy.envoymobile.engine.types.EnvoyHTTPCallbacks;
import io.envoyproxy.envoymobile.engine.types.EnvoyLogger;
import io.envoyproxy.envoymobile.engine.types.EnvoyOnEngineRunning;
import io.envoyproxy.envoymobile.engine.types.EnvoyStringAccessor;

import java.util.Map;
Expand All @@ -25,11 +23,15 @@ public interface EnvoyEngine {
/**
* Run the Envoy engine with the provided yaml string and log level.
*
* The envoyConfiguration is used to resolve the configurationYAML.
*
* @param configurationYAML The configuration yaml with which to start Envoy.
* @param envoyConfiguration The EnvoyConfiguration used to start Envoy.
* @param logLevel The log level to use when starting Envoy.
* @return A status indicating if the action was successful.
*/
int runWithConfig(String configurationYAML, String logLevel);
int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyConfiguration,
String logLevel);

/**
* Run the Envoy engine with the provided EnvoyConfiguration and log level.
Expand Down
47 changes: 35 additions & 12 deletions library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,32 @@ public void terminate() {
/**
* Run the Envoy engine with the provided yaml string and log level.
*
* The envoyConfiguration is used to resolve the configurationYAML.
*
* @param configurationYAML The configuration yaml with which to start Envoy.
* @param logLevel The log level to use when starting Envoy.
* @param envoyConfiguration The EnvoyConfiguration used to start Envoy.
* @param logLevel The log level to use when starting Envoy.
* @return A status indicating if the action was successful.
*/
@Override
public int runWithConfig(String configurationYAML, String logLevel) {
try {
return JniLibrary.runEngine(this.engineHandle, configurationYAML, logLevel);
} catch (Throwable throwable) {
// TODO: Need to have a way to log the exception somewhere.
return ENVOY_FAILURE;
public int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyConfiguration,
String logLevel) {
for (EnvoyHTTPFilterFactory filterFactory : envoyConfiguration.httpPlatformFilterFactories) {
JniLibrary.registerFilterFactory(filterFactory.getFilterName(),
new JvmFilterFactoryContext(filterFactory));
}

for (Map.Entry<String, EnvoyStringAccessor> entry :
envoyConfiguration.stringAccessors.entrySet()) {
JniLibrary.registerStringAccessor(entry.getKey(),
new JvmStringAccessorContext(entry.getValue()));
}

return runWithResolvedYAML(
envoyConfiguration.resolveTemplate(configurationYAML, JniLibrary.statsSinkTemplateString(),
JniLibrary.platformFilterTemplateString(),
JniLibrary.nativeFilterTemplateString()),
logLevel);
}

/**
Expand All @@ -84,11 +98,20 @@ public int runWithConfig(EnvoyConfiguration envoyConfiguration, String logLevel)
new JvmStringAccessorContext(entry.getValue()));
}

return runWithConfig(envoyConfiguration.resolveTemplate(
JniLibrary.templateString(), JniLibrary.statsSinkTemplateString(),
JniLibrary.platformFilterTemplateString(),
JniLibrary.nativeFilterTemplateString()),
logLevel);
return runWithResolvedYAML(
envoyConfiguration.resolveTemplate(
JniLibrary.templateString(), JniLibrary.statsSinkTemplateString(),
JniLibrary.platformFilterTemplateString(), JniLibrary.nativeFilterTemplateString()),
logLevel);
}

private int runWithResolvedYAML(String configurationYAML, String logLevel) {
try {
return JniLibrary.runEngine(this.engineHandle, configurationYAML, logLevel);
} catch (Throwable throwable) {
// TODO: Need to have a way to log the exception somewhere.
return ENVOY_FAILURE;
}
}

/**
Expand Down
12 changes: 11 additions & 1 deletion library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,17 @@ open class EngineBuilder(
fun build(): Engine {
return when (configuration) {
is Custom -> {
EngineImpl(engineType(), configuration.yaml, logLevel)
EngineImpl(
engineType(),
EnvoyConfiguration(
statsDomain, connectTimeoutSeconds,
dnsRefreshSeconds, dnsFailureRefreshSecondsBase, dnsFailureRefreshSecondsMax,
statsFlushSeconds, appVersion, appId, virtualClusters, nativeFilterChain,
platformFilterChain, stringAccessors
),
configuration.yaml,
logLevel
)
}
is Standard -> {
EngineImpl(
Expand Down
14 changes: 4 additions & 10 deletions library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import io.envoyproxy.envoymobile.engine.EnvoyEngine
/**
* An implementation of {@link Engine}.
*/
class EngineImpl internal constructor(
class EngineImpl constructor(
internal val envoyEngine: EnvoyEngine,
internal val envoyConfiguration: EnvoyConfiguration?,
internal val envoyConfiguration: EnvoyConfiguration,
internal val configurationYAML: String?,
internal val logLevel: LogLevel
) : Engine {
Expand All @@ -22,17 +22,11 @@ class EngineImpl internal constructor(
logLevel: LogLevel = LogLevel.INFO
) : this(envoyEngine, envoyConfiguration, null, logLevel)

constructor(
envoyEngine: EnvoyEngine,
configurationYAML: String,
logLevel: LogLevel = LogLevel.INFO
) : this(envoyEngine, null, configurationYAML, logLevel)

init {
streamClient = StreamClientImpl(envoyEngine)
pulseClient = PulseClientImpl(envoyEngine)
if (envoyConfiguration == null) {
envoyEngine.runWithConfig(configurationYAML, logLevel.level)
if (configurationYAML != null) {
envoyEngine.runWithTemplate(configurationYAML, envoyConfiguration, logLevel.level)
} else {
envoyEngine.runWithConfig(envoyConfiguration, logLevel.level)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import io.envoyproxy.envoymobile.engine.types.EnvoyStringAccessor
internal class MockEnvoyEngine : EnvoyEngine {
override fun runWithConfig(envoyConfiguration: EnvoyConfiguration?, logLevel: String?): Int = 0

override fun runWithConfig(configurationYAML: String?, logLevel: String?): Int = 0
override fun runWithTemplate(
configurationYAML: String,
envoyConfiguration: EnvoyConfiguration,
logLevel: String
): Int = 0

override fun startStream(callbacks: EnvoyHTTPCallbacks?): EnvoyHTTPStream = MockEnvoyHTTPStream(callbacks!!)

Expand Down
14 changes: 14 additions & 0 deletions test/kotlin/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ envoy_mobile_jni_kt_test(
"//library/kotlin/io/envoyproxy/envoymobile:envoy_interfaces_lib",
],
)

envoy_mobile_jni_kt_test(
name = "cancel_stream_test",
srcs = [
"CancelStreamTest.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",
],
)
157 changes: 157 additions & 0 deletions test/kotlin/integration/CancelStreamTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package test.kotlin.integration

import io.envoyproxy.envoymobile.Custom
import io.envoyproxy.envoymobile.EngineBuilder
import io.envoyproxy.envoymobile.EnvoyError
import io.envoyproxy.envoymobile.FilterDataStatus
import io.envoyproxy.envoymobile.FilterHeadersStatus
import io.envoyproxy.envoymobile.FilterTrailersStatus
import io.envoyproxy.envoymobile.RequestHeadersBuilder
import io.envoyproxy.envoymobile.RequestMethod
import io.envoyproxy.envoymobile.ResponseFilter
import io.envoyproxy.envoymobile.ResponseHeaders
import io.envoyproxy.envoymobile.ResponseTrailers
import io.envoyproxy.envoymobile.UpstreamHttpProtocol
import io.envoyproxy.envoymobile.engine.JniLibrary
import java.nio.ByteBuffer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

private const val hcmType =
"type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
private const val pbfType = "type.googleapis.com/envoymobile.extensions.filters.http.platform_bridge.PlatformBridge"
private const val filterName = "cancel_validation_filter"
private const val config =
"""
static_resources:
listeners:
- name: fake_remote_listener
address:
socket_address: { protocol: TCP, address: 127.0.0.1, port_value: 10101 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": $hcmType
stat_prefix: remote_hcm
route_config:
name: remote_route
virtual_hosts:
- name: remote_service
domains: ["*"]
routes:
- match: { prefix: "/" }
direct_response: { status: 200 }
http_filters:
- name: envoy.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: base_api_listener
address:
socket_address: { protocol: TCP, address: 0.0.0.0, port_value: 10000 }
api_listener:
api_listener:
"@type": $hcmType
stat_prefix: api_hcm
route_config:
name: api_router
virtual_hosts:
- name: api
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: fake_remote }
http_filters:
- name: envoy.filters.http.platform_bridge
typed_config:
"@type": $pbfType
platform_filter_name: $filterName
- name: envoy.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: fake_remote
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: fake_remote
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 10101 }
"""

class CancelStreamTest {

init {
JniLibrary.loadTestLibrary()
}

private val filterExpectation = CountDownLatch(1)
private val runExpectation = CountDownLatch(1)

class CancelValidationFilter(
private val latch: CountDownLatch
) : ResponseFilter {
override fun onResponseHeaders(headers: ResponseHeaders, endStream: Boolean): FilterHeadersStatus<ResponseHeaders> {
return FilterHeadersStatus.Continue(headers)
}

override fun onResponseData(body: ByteBuffer, endStream: Boolean): FilterDataStatus<ResponseHeaders> {
return FilterDataStatus.Continue(body)
}

override fun onResponseTrailers(trailers: ResponseTrailers): FilterTrailersStatus<ResponseHeaders, ResponseTrailers> {
return FilterTrailersStatus.Continue(trailers)
}

override fun onError(error: EnvoyError) {}

override fun onCancel() {
latch.countDown()
}
}

@Test
fun `cancel stream calls onCancel callback`() {
val engine = EngineBuilder(Custom(config))
.addPlatformFilter(
name = "cancel_validation_filter",
factory = { CancelValidationFilter(filterExpectation) }
)
.setOnEngineRunning {}
.build()

val client = engine.streamClient()

val requestHeaders = RequestHeadersBuilder(
method = RequestMethod.GET,
scheme = "https",
authority = "example.com",
path = "/test"
)
.addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2)
.build()

client.newStreamPrototype()
.setOnCancel {
runExpectation.countDown()
}
.start(Executors.newSingleThreadExecutor())
.sendHeaders(requestHeaders, false)
.cancel()

filterExpectation.await(10, TimeUnit.SECONDS)
runExpectation.await(10, TimeUnit.SECONDS)

engine.terminate()

assertThat(filterExpectation.count).isEqualTo(0)
assertThat(runExpectation.count).isEqualTo(0)
}
}
5 changes: 3 additions & 2 deletions test/swift/integration/CancelStreamTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ final class CancelStreamTests: XCTestCase {
let hcmType = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
// swiftlint:disable:next line_length
let pbfType = "type.googleapis.com/envoymobile.extensions.filters.http.platform_bridge.PlatformBridge"
let filterName = "cancel_validation_filter"
let config =
"""
static_resources:
Expand Down Expand Up @@ -53,7 +54,7 @@ final class CancelStreamTests: XCTestCase {
- name: envoy.filters.http.platform_bridge
typed_config:
"@type": \(pbfType)
platform_filter_name: cancel_validation_filter
platform_filter_name: \(filterName)
- name: envoy.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Expand Down Expand Up @@ -102,7 +103,7 @@ final class CancelStreamTests: XCTestCase {
let client = EngineBuilder(yaml: config)
.addLogLevel(.trace)
.addPlatformFilter(
name: "cancel_validation_filter",
name: filterName,
factory: { CancelValidationFilter(expectation: filterExpectation) }
)
.build()
Expand Down

0 comments on commit 7c15bb6

Please sign in to comment.