diff --git a/README.md b/README.md index 610c637892b..9ebc824c8da 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ gRPC-Java - An RPC library and framework ======================================== -gRPC-Java works with JDK 7. gRPC-Java clients are supported on Android API +gRPC-Java works with JDK 8. gRPC-Java clients are supported on Android API levels 19 and up (KitKat and later). Deploying gRPC servers on an Android device is not supported. @@ -31,8 +31,8 @@ For a guided tour, take a look at the [quick start guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC basics](https://grpc.io/docs/languages/java/basics). -The [examples](https://github.com/grpc/grpc-java/tree/v1.43.1/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.43.1/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.45.0/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.45.0/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -43,18 +43,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.43.1 + 1.45.0 runtime io.grpc grpc-protobuf - 1.43.1 + 1.45.0 io.grpc grpc-stub - 1.43.1 + 1.45.0 org.apache.tomcat @@ -66,23 +66,23 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: Or for Gradle with non-Android, add to your dependencies: ```gradle -runtimeOnly 'io.grpc:grpc-netty-shaded:1.43.1' -implementation 'io.grpc:grpc-protobuf:1.43.1' -implementation 'io.grpc:grpc-stub:1.43.1' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.45.0' +implementation 'io.grpc:grpc-protobuf:1.45.0' +implementation 'io.grpc:grpc-stub:1.45.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and `grpc-protobuf-lite` instead of `grpc-protobuf`: ```gradle -implementation 'io.grpc:grpc-okhttp:1.43.1' -implementation 'io.grpc:grpc-protobuf-lite:1.43.1' -implementation 'io.grpc:grpc-stub:1.43.1' +implementation 'io.grpc:grpc-okhttp:1.45.0' +implementation 'io.grpc:grpc-protobuf-lite:1.45.0' +implementation 'io.grpc:grpc-stub:1.45.0' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.43.1 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.45.0 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -112,9 +112,9 @@ For protobuf-based codegen integrated with the Maven build system, you can use protobuf-maven-plugin 0.6.1 - com.google.protobuf:protoc:3.19.1:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.19.2:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.43.1:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.45.0:exe:${os.detected.classifier} @@ -140,11 +140,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.19.1" + artifact = "com.google.protobuf:protoc:3.19.2" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.43.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' } } generateProtoTasks { @@ -173,11 +173,11 @@ plugins { protobuf { protoc { - artifact = "com.google.protobuf:protoc:3.19.1" + artifact = "com.google.protobuf:protoc:3.19.2" } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.43.1' + artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' } } generateProtoTasks { diff --git a/RELEASING.md b/RELEASING.md index f2e37f312b4..b8682961234 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -85,7 +85,7 @@ would be used to create all `v1.7` tags (e.g. `v1.7.0`, `v1.7.1`). $ echo "## gRPC Java $MAJOR.$MINOR.0 Release Notes" && echo && \ git shortlog "$(git merge-base upstream/v$MAJOR.$((MINOR-1)).x upstream/v$MAJOR.$MINOR.x)"..upstream/v$MAJOR.$MINOR.x | cat && \ echo && echo && echo "Backported commits in previous release:" && \ - git cherry -v v$MAJOR.$((MINOR-1)).0 upstream/v$MAJOR.$MINOR.x | grep ^- + git log --oneline "$(git merge-base v$MAJOR.$((MINOR-1)).0 upstream/v$MAJOR.$MINOR.x)"..v$MAJOR.$((MINOR-1)).0^ ``` Tagging the Release @@ -133,7 +133,7 @@ Tagging the Release $ git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT" ``` 6. Go through PR review and push the release tag and updated release branch to - GitHub: + GitHub (DO NOT click the merge button on the GitHub page): ```bash $ git checkout v$MAJOR.$MINOR.x diff --git a/alts/build.gradle b/alts/build.gradle index 8c467f51c12..a056799d4f0 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -9,9 +9,6 @@ plugins { description = "gRPC: ALTS" -sourceCompatibility = 1.7 -targetCompatibility = 1.7 - evaluationDependsOn(project(':grpc-core').path) dependencies { @@ -65,14 +62,14 @@ javadoc { } jar { - // Must use a different classifier to avoid conflicting with shadowJar - classifier = 'original' + // Must use a different archiveClassifier to avoid conflicting with shadowJar + archiveClassifier = 'original' } // We want to use grpc-netty-shaded instead of grpc-netty. But we also want our // source to work with Bazel, so we rewrite the code as part of the build. shadowJar { - classifier = null + archiveClassifier = null dependencies { exclude(dependency {true}) } diff --git a/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java b/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java index 4164560e7a0..e2e7d4046fb 100644 --- a/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java +++ b/alts/src/main/java/io/grpc/alts/internal/ChannelCrypterNetty.java @@ -21,8 +21,8 @@ import java.util.List; /** - * A @{code ChannelCrypterNetty} performs stateful encryption and decryption of independent input - * and output streams. Both decrypt and encrypt gather their input from a list of Netty @{link + * A {@code ChannelCrypterNetty} performs stateful encryption and decryption of independent input + * and output streams. Both decrypt and encrypt gather their input from a list of Netty {@link * ByteBuf} instances. * *

Note that we provide implementations of this interface that provide integrity only and diff --git a/alts/src/test/java/io/grpc/alts/internal/TsiTest.java b/alts/src/test/java/io/grpc/alts/internal/TsiTest.java index f677a44a754..f75fb7fcee0 100644 --- a/alts/src/test/java/io/grpc/alts/internal/TsiTest.java +++ b/alts/src/test/java/io/grpc/alts/internal/TsiTest.java @@ -34,13 +34,13 @@ import java.util.List; import javax.crypto.AEADBadTagException; -/** Utility class that provides tests for implementations of @{link TsiHandshaker}. */ +/** Utility class that provides tests for implementations of {@link TsiHandshaker}. */ public final class TsiTest { private static final String DECRYPTION_FAILURE_RE = "Tag mismatch!|BAD_DECRYPT"; private TsiTest() {} - /** A @{code TsiHandshaker} pair for running tests. */ + /** A {@code TsiHandshaker} pair for running tests. */ public static class Handshakers { private final TsiHandshaker client; private final TsiHandshaker server; diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index b18d84e2625..e40507e6a3b 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -76,8 +76,8 @@ dependencies { compileOnly libraries.javax_annotation - androidTestImplementation 'androidx.test:rules:1.1.0-alpha1' - androidTestImplementation 'androidx.test:runner:1.1.0-alpha1' + androidTestImplementation 'androidx.test.ext:junit:1.1.3', + 'androidx.test:runner:1.4.0' } // Checkstyle doesn't run automatically with android @@ -102,6 +102,10 @@ tasks.withType(JavaCompile) { "-Xlint:-cast" ] appendToProperty(it.options.errorprone.excludedPaths, ".*/R.java", "|") + appendToProperty( + it.options.errorprone.excludedPaths, + ".*/src/generated/.*", + "|") // Reuses source code from grpc-interop-testing, which targets Java 7 (no method references) options.errorprone.check("UnnecessaryAnonymousClass", CheckSeverity.OFF) } diff --git a/android-interop-testing/src/androidTest/AndroidManifest.xml b/android-interop-testing/src/androidTest/AndroidManifest.xml index 916a1c04332..9c59d201eff 100644 --- a/android-interop-testing/src/androidTest/AndroidManifest.xml +++ b/android-interop-testing/src/androidTest/AndroidManifest.xml @@ -6,8 +6,8 @@ android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="io.grpc.android.integrationtest" /> - - + diff --git a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/InteropInstrumentationTest.java b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/InteropInstrumentationTest.java index abcf9e08593..5d76f1c0a6e 100644 --- a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/InteropInstrumentationTest.java +++ b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/InteropInstrumentationTest.java @@ -16,11 +16,12 @@ package io.grpc.android.integrationtest; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; import android.util.Log; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.gms.common.GooglePlayServicesNotAvailableException; import com.google.android.gms.common.GooglePlayServicesRepairableException; import com.google.android.gms.security.ProviderInstaller; @@ -59,7 +60,7 @@ public void setUp() throws Exception { if (useTls) { try { - ProviderInstaller.installIfNeeded(InstrumentationRegistry.getTargetContext()); + ProviderInstaller.installIfNeeded(ApplicationProvider.getApplicationContext()); } catch (GooglePlayServicesRepairableException e) { // The provider is helpful, but it is possible to succeed without it. // Hope that the system-provided libraries are new enough. @@ -110,7 +111,7 @@ public void onComplete(String result) { }; InputStream testCa; if (useTestCa) { - testCa = InstrumentationRegistry.getTargetContext().getResources().openRawResource(R.raw.ca); + testCa = ApplicationProvider.getApplicationContext().getResources().openRawResource(R.raw.ca); } else { testCa = null; } diff --git a/android/build.gradle b/android/build.gradle index b0916f9e04c..7e32ac5ca2f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -59,12 +59,12 @@ task javadocs(type: Javadoc) { } task javadocJar(type: Jar, dependsOn: javadocs) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadocs.destinationDir } task sourcesJar(type: Jar) { - classifier = 'sources' + archiveClassifier = 'sources' from android.sourceSets.main.java.srcDirs } diff --git a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java index 8c69ca68b5f..dadab1c830c 100644 --- a/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java +++ b/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java @@ -27,6 +27,7 @@ import android.util.Log; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.InlineMe; import io.grpc.CallOptions; import io.grpc.ClientCall; import io.grpc.ConnectivityState; @@ -90,6 +91,9 @@ public static AndroidChannelBuilder forAddress(String name, int port) { */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6043") @Deprecated + @InlineMe( + replacement = "AndroidChannelBuilder.usingBuilder(builder)", + imports = "io.grpc.android.AndroidChannelBuilder") public static AndroidChannelBuilder fromBuilder(ManagedChannelBuilder builder) { return usingBuilder(builder); } @@ -293,6 +297,13 @@ private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback public void onAvailable(Network network) { delegate.enterIdle(); } + + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + if (!blocked) { + delegate.enterIdle(); + } + } } /** Respond to network changes. Only used on API levels < 24. */ diff --git a/api/build.gradle b/api/build.gradle index 1348e49ad60..9f5e6163153 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -12,9 +12,9 @@ evaluationDependsOn(project(':grpc-context').path) dependencies { api project(':grpc-context'), - libraries.jsr305 - implementation libraries.guava, + libraries.jsr305, libraries.errorprone + implementation libraries.guava testImplementation project(':grpc-context').sourceSets.test.output, project(':grpc-testing'), @@ -25,7 +25,7 @@ dependencies { jmh project(':grpc-core') signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } javadoc { diff --git a/api/src/jmh/java/io/grpc/StatusBenchmark.java b/api/src/jmh/java/io/grpc/StatusBenchmark.java index 846682c3298..e3f087c6204 100644 --- a/api/src/jmh/java/io/grpc/StatusBenchmark.java +++ b/api/src/jmh/java/io/grpc/StatusBenchmark.java @@ -16,7 +16,7 @@ package io.grpc; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -57,7 +57,7 @@ public byte[] messageEncodeEscape() { @OutputTimeUnit(TimeUnit.NANOSECONDS) public String messageDecodePlain() { return Status.MESSAGE_KEY.parseBytes( - "Unexpected RST in stream".getBytes(StandardCharsets.US_ASCII)); + "Unexpected RST in stream".getBytes(Charset.forName("US-ASCII"))); } /** @@ -68,7 +68,7 @@ public String messageDecodePlain() { @OutputTimeUnit(TimeUnit.NANOSECONDS) public String messageDecodeEscape() { return Status.MESSAGE_KEY.parseBytes( - "Some Error%10Wasabi and Horseradish are the same".getBytes(StandardCharsets.US_ASCII)); + "Some Error%10Wasabi and Horseradish are the same".getBytes(Charset.forName("US-ASCII"))); } /** @@ -88,7 +88,7 @@ public byte[] codeEncode() { @BenchmarkMode(Mode.SampleTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public Status codeDecode() { - return Status.CODE_KEY.parseBytes("15".getBytes(StandardCharsets.US_ASCII)); + return Status.CODE_KEY.parseBytes("15".getBytes(Charset.forName("US-ASCII"))); } } diff --git a/api/src/main/java/io/grpc/Attributes.java b/api/src/main/java/io/grpc/Attributes.java index b70cc2bc8c3..26c2a72f909 100644 --- a/api/src/main/java/io/grpc/Attributes.java +++ b/api/src/main/java/io/grpc/Attributes.java @@ -46,11 +46,13 @@ @Immutable public final class Attributes { - private final Map, Object> data; + private final IdentityHashMap, Object> data; - public static final Attributes EMPTY = new Attributes(Collections., Object>emptyMap()); + private static final IdentityHashMap, Object> EMPTY_MAP = + new IdentityHashMap, Object>(); + public static final Attributes EMPTY = new Attributes(EMPTY_MAP); - private Attributes(Map, Object> data) { + private Attributes(IdentityHashMap, Object> data) { assert data != null; this.data = data; } @@ -212,14 +214,14 @@ public int hashCode() { */ public static final class Builder { private Attributes base; - private Map, Object> newdata; + private IdentityHashMap, Object> newdata; private Builder(Attributes base) { assert base != null; this.base = base; } - private Map, Object> data(int size) { + private IdentityHashMap, Object> data(int size) { if (newdata == null) { newdata = new IdentityHashMap<>(size); } @@ -241,7 +243,7 @@ public Builder set(Key key, T value) { @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5777") public Builder discard(Key key) { if (base.data.containsKey(key)) { - Map, Object> newBaseData = new IdentityHashMap<>(base.data); + IdentityHashMap, Object> newBaseData = new IdentityHashMap<>(base.data); newBaseData.remove(key); base = new Attributes(newBaseData); } diff --git a/api/src/main/java/io/grpc/CallOptions.java b/api/src/main/java/io/grpc/CallOptions.java index c7504a6506a..5c05d5b7bd7 100644 --- a/api/src/main/java/io/grpc/CallOptions.java +++ b/api/src/main/java/io/grpc/CallOptions.java @@ -358,6 +358,11 @@ public T getOption(Key key) { return key.defaultValue; } + /** + * Returns the executor override to use for this specific call, or {@code null} if there is no + * override. The executor is only for servicing this one call, so is not safe to use after + * {@link ClientCall.Listener#onClose}. + */ @Nullable public Executor getExecutor() { return executor; diff --git a/api/src/main/java/io/grpc/DecompressorRegistry.java b/api/src/main/java/io/grpc/DecompressorRegistry.java index b2669d3c0fc..ea0433d8d0a 100644 --- a/api/src/main/java/io/grpc/DecompressorRegistry.java +++ b/api/src/main/java/io/grpc/DecompressorRegistry.java @@ -20,7 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Joiner; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; @@ -85,7 +85,7 @@ private DecompressorRegistry(Decompressor d, boolean advertised, DecompressorReg decompressors = Collections.unmodifiableMap(newDecompressors); advertisedDecompressors = ACCEPT_ENCODING_JOINER.join(getAdvertisedMessageEncodings()) - .getBytes(StandardCharsets.US_ASCII); + .getBytes(Charset.forName("US-ASCII")); } private DecompressorRegistry() { diff --git a/api/src/main/java/io/grpc/InternalManagedChannelProvider.java b/api/src/main/java/io/grpc/InternalManagedChannelProvider.java new file mode 100644 index 00000000000..076b5464b7e --- /dev/null +++ b/api/src/main/java/io/grpc/InternalManagedChannelProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import io.grpc.ManagedChannelProvider.NewChannelBuilderResult; + +/** Internal accessor for {@link ManagedChannelProvider}. */ +@Internal +public final class InternalManagedChannelProvider { + + private InternalManagedChannelProvider() { + } + + public static ManagedChannelBuilder builderForAddress(ManagedChannelProvider provider, + String name, int port) { + return provider.builderForAddress(name, port); + } + + public static ManagedChannelBuilder builderForTarget(ManagedChannelProvider provider, + String target) { + return provider.builderForTarget(target); + } + + public static NewChannelBuilderResult newChannelBuilder(ManagedChannelProvider provider, + String target, ChannelCredentials creds) { + return provider.newChannelBuilder(target, creds); + } +} diff --git a/api/src/main/java/io/grpc/InternalMetadata.java b/api/src/main/java/io/grpc/InternalMetadata.java index cb98ad752b2..2823882952f 100644 --- a/api/src/main/java/io/grpc/InternalMetadata.java +++ b/api/src/main/java/io/grpc/InternalMetadata.java @@ -20,7 +20,6 @@ import io.grpc.Metadata.AsciiMarshaller; import io.grpc.Metadata.BinaryStreamMarshaller; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; /** * Internal {@link Metadata} accessor. This is intended for use by io.grpc.internal, and the @@ -43,7 +42,7 @@ public interface TrustedAsciiMarshaller extends Metadata.TrustedAsciiMarshall * Copy of StandardCharsets, which is only available on Java 1.7 and above. */ @Internal - public static final Charset US_ASCII = StandardCharsets.US_ASCII; + public static final Charset US_ASCII = Charset.forName("US-ASCII"); /** * An instance of base64 encoder that omits padding. diff --git a/api/src/main/java/io/grpc/InternalServerProvider.java b/api/src/main/java/io/grpc/InternalServerProvider.java new file mode 100644 index 00000000000..d8c1d77fe76 --- /dev/null +++ b/api/src/main/java/io/grpc/InternalServerProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import io.grpc.ServerProvider.NewServerBuilderResult; + +/** Internal accessor for {@link ServerProvider}. */ +@Internal +public final class InternalServerProvider { + + private InternalServerProvider() { + } + + public static ServerBuilder builderForPort(ServerProvider provider, int port) { + return provider.builderForPort(port); + } + + public static NewServerBuilderResult newServerBuilderForPort(ServerProvider provider, int port, + ServerCredentials creds) { + return provider.newServerBuilderForPort(port, creds); + } +} diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java index f4c05aa6a64..78b35a14e38 100644 --- a/api/src/main/java/io/grpc/NameResolver.java +++ b/api/src/main/java/io/grpc/NameResolver.java @@ -21,6 +21,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import com.google.errorprone.annotations.InlineMe; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -204,6 +205,10 @@ public abstract static class Listener2 implements Listener { */ @Override @Deprecated + @InlineMe( + replacement = "this.onResult(ResolutionResult.newBuilder().setAddresses(servers)" + + ".setAttributes(attributes).build())", + imports = "io.grpc.NameResolver.ResolutionResult") public final void onAddresses( List servers, @ResolutionResultAttr Attributes attributes) { // TODO(jihuncho) need to promote Listener2 if we want to use ConfigOrError diff --git a/api/src/test/java/io/grpc/StatusTest.java b/api/src/test/java/io/grpc/StatusTest.java index aca5dc8ee66..9abeba436f7 100644 --- a/api/src/test/java/io/grpc/StatusTest.java +++ b/api/src/test/java/io/grpc/StatusTest.java @@ -21,8 +21,6 @@ import io.grpc.Status.Code; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -30,7 +28,7 @@ /** Unit tests for {@link Status}. */ @RunWith(JUnit4.class) public class StatusTest { - private final Charset ascii = StandardCharsets.US_ASCII; + private final Charset ascii = Charset.forName("US-ASCII"); @Test public void verifyExceptionMessage() { diff --git a/auth/build.gradle b/auth/build.gradle index 791402fce59..233de359b49 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -14,5 +14,5 @@ dependencies { testImplementation project(':grpc-testing'), libraries.google_auth_oauth2_http signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } diff --git a/authz/build.gradle b/authz/build.gradle new file mode 100644 index 00000000000..f6110a5850c --- /dev/null +++ b/authz/build.gradle @@ -0,0 +1,77 @@ +plugins { + id "java-library" + id "maven-publish" + + id "com.github.johnrengelman.shadow" + id "com.google.protobuf" + id "ru.vyarus.animalsniffer" +} + +description = "gRPC: Authorization" + +dependencies { + implementation project(':grpc-protobuf'), + project(':grpc-core') + + annotationProcessor libraries.autovalue + compileOnly libraries.javax_annotation + + testImplementation project(':grpc-testing'), + project(':grpc-testing-proto') + testImplementation (libraries.guava_testlib) { + exclude group: 'junit', module: 'junit' + } + + def xdsDependency = implementation project(':grpc-xds') + shadow configurations.implementation.getDependencies().minus([xdsDependency]) + shadow project(path: ':grpc-xds', configuration: 'shadow') + + signature "org.codehaus.mojo.signature:java17:1.0@signature" +} + +jar { + classifier = 'original' +} + +// TODO(ashithasantosh): Remove javadoc exclusion on adding authorization +// interceptor implementations. +javadoc { + exclude "io/grpc/authz/*" +} + +shadowJar { + classifier = null + dependencies { + exclude(dependency {true}) + } + relocate 'io.grpc.xds', 'io.grpc.xds.shaded.io.grpc.xds' + relocate 'udpa.annotations', 'io.grpc.xds.shaded.udpa.annotations' + relocate 'com.github.udpa', 'io.grpc.xds.shaded.com.github.udpa' + relocate 'envoy.annotations', 'io.grpc.xds.shaded.envoy.annotations' + relocate 'io.envoyproxy', 'io.grpc.xds.shaded.io.envoyproxy' + relocate 'com.google.api.expr', 'io.grpc.xds.shaded.com.google.api.expr' +} + +publishing { + publications { + maven(MavenPublication) { + // We want this to throw an exception if it isn't working + def originalJar = artifacts.find { dep -> dep.classifier == 'original'} + artifacts.remove(originalJar) + + pom.withXml { + def dependenciesNode = new Node(null, 'dependencies') + project.configurations.shadow.allDependencies.each { dep -> + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', dep.group) + dependencyNode.appendNode('artifactId', dep.name) + dependencyNode.appendNode('version', dep.version) + dependencyNode.appendNode('scope', 'compile') + } + asNode().dependencies[0].replaceNode(dependenciesNode) + } + } + } +} + +[publishMavenPublicationToMavenRepository]*.onlyIf {false} diff --git a/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java new file mode 100644 index 00000000000..1637af737ad --- /dev/null +++ b/authz/src/main/java/io/grpc/authz/AuthorizationPolicyTranslator.java @@ -0,0 +1,198 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.authz; + +import com.google.common.collect.ImmutableList; +import io.envoyproxy.envoy.config.rbac.v3.Permission; +import io.envoyproxy.envoy.config.rbac.v3.Policy; +import io.envoyproxy.envoy.config.rbac.v3.Principal; +import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated; +import io.envoyproxy.envoy.config.rbac.v3.RBAC; +import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.type.matcher.v3.PathMatcher; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import io.grpc.internal.JsonParser; +import io.grpc.internal.JsonUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. + */ +class AuthorizationPolicyTranslator { + private static final ImmutableList UNSUPPORTED_HEADERS = ImmutableList.of( + "host", "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", + "te", "trailer", "transfer-encoding", "upgrade"); + + private static StringMatcher getStringMatcher(String value) { + if (value.equals("*")) { + return StringMatcher.newBuilder().setSafeRegex( + RegexMatcher.newBuilder().setRegex(".+").build()).build(); + } else if (value.startsWith("*")) { + return StringMatcher.newBuilder().setSuffix(value.substring(1)).build(); + } else if (value.endsWith("*")) { + return StringMatcher.newBuilder().setPrefix(value.substring(0, value.length() - 1)).build(); + } + return StringMatcher.newBuilder().setExact(value).build(); + } + + private static Principal parseSource(Map source) { + List principalsList = JsonUtil.getListOfStrings(source, "principals"); + if (principalsList == null || principalsList.isEmpty()) { + return Principal.newBuilder().setAny(true).build(); + } + Principal.Set.Builder principalsSet = Principal.Set.newBuilder(); + for (String principal: principalsList) { + principalsSet.addIds( + Principal.newBuilder().setAuthenticated( + Authenticated.newBuilder().setPrincipalName( + getStringMatcher(principal)).build()).build()); + } + return Principal.newBuilder().setOrIds(principalsSet.build()).build(); + } + + private static Permission parseHeader(Map header) throws IllegalArgumentException { + String key = JsonUtil.getString(header, "key"); + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("\"key\" is absent or empty"); + } + if (key.charAt(0) == ':' + || key.startsWith("grpc-") + || UNSUPPORTED_HEADERS.contains(key.toLowerCase())) { + throw new IllegalArgumentException(String.format("Unsupported \"key\" %s", key)); + } + List valuesList = JsonUtil.getListOfStrings(header, "values"); + if (valuesList == null || valuesList.isEmpty()) { + throw new IllegalArgumentException("\"values\" is absent or empty"); + } + Permission.Set.Builder orSet = Permission.Set.newBuilder(); + for (String value: valuesList) { + orSet.addRules( + Permission.newBuilder().setHeader( + HeaderMatcher.newBuilder() + .setName(key) + .setStringMatch(getStringMatcher(value)).build()).build()); + } + return Permission.newBuilder().setOrRules(orSet.build()).build(); + } + + private static Permission parseRequest(Map request) throws IllegalArgumentException { + Permission.Set.Builder andSet = Permission.Set.newBuilder(); + List pathsList = JsonUtil.getListOfStrings(request, "paths"); + if (pathsList != null && !pathsList.isEmpty()) { + Permission.Set.Builder pathsSet = Permission.Set.newBuilder(); + for (String path: pathsList) { + pathsSet.addRules( + Permission.newBuilder().setUrlPath( + PathMatcher.newBuilder().setPath( + getStringMatcher(path)).build()).build()); + } + andSet.addRules(Permission.newBuilder().setOrRules(pathsSet.build()).build()); + } + List> headersList = JsonUtil.getListOfObjects(request, "headers"); + if (headersList != null && !headersList.isEmpty()) { + Permission.Set.Builder headersSet = Permission.Set.newBuilder(); + for (Map header: headersList) { + headersSet.addRules(parseHeader(header)); + } + andSet.addRules(Permission.newBuilder().setAndRules(headersSet.build()).build()); + } + if (andSet.getRulesCount() == 0) { + return Permission.newBuilder().setAny(true).build(); + } + return Permission.newBuilder().setAndRules(andSet.build()).build(); + } + + private static Map parseRules( + List> objects, String name) throws IllegalArgumentException { + Map policies = new LinkedHashMap(); + for (Map object: objects) { + String policyName = JsonUtil.getString(object, "name"); + if (policyName == null || policyName.isEmpty()) { + throw new IllegalArgumentException("rule \"name\" is absent or empty"); + } + List principals = new ArrayList<>(); + Map source = JsonUtil.getObject(object, "source"); + if (source != null) { + principals.add(parseSource(source)); + } else { + principals.add(Principal.newBuilder().setAny(true).build()); + } + List permissions = new ArrayList<>(); + Map request = JsonUtil.getObject(object, "request"); + if (request != null) { + permissions.add(parseRequest(request)); + } else { + permissions.add(Permission.newBuilder().setAny(true).build()); + } + Policy policy = + Policy.newBuilder() + .addAllPermissions(permissions) + .addAllPrincipals(principals) + .build(); + policies.put(name + "_" + policyName, policy); + } + return policies; + } + + /** + * Translates a gRPC authorization policy in JSON string to Envoy RBAC policies. + * On success, will return one of the following - + * 1. One allow RBAC policy or, + * 2. Two RBAC policies, deny policy followed by allow policy. + * If the policy cannot be parsed or is invalid, an exception will be thrown. + */ + public static List translate(String authorizationPolicy) + throws IllegalArgumentException, IOException { + Object jsonObject = JsonParser.parse(authorizationPolicy); + if (!(jsonObject instanceof Map)) { + throw new IllegalArgumentException( + "Authorization policy should be a JSON object. Found: " + + (jsonObject == null ? null : jsonObject.getClass())); + } + @SuppressWarnings("unchecked") + Map json = (Map)jsonObject; + String name = JsonUtil.getString(json, "name"); + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("\"name\" is absent or empty"); + } + List rbacs = new ArrayList<>(); + List> objects = JsonUtil.getListOfObjects(json, "deny_rules"); + if (objects != null && !objects.isEmpty()) { + rbacs.add( + RBAC.newBuilder() + .setAction(Action.DENY) + .putAllPolicies(parseRules(objects, name)) + .build()); + } + objects = JsonUtil.getListOfObjects(json, "allow_rules"); + if (objects == null || objects.isEmpty()) { + throw new IllegalArgumentException("\"allow_rules\" is absent"); + } + rbacs.add( + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putAllPolicies(parseRules(objects, name)) + .build()); + return rbacs; + } +} diff --git a/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java new file mode 100644 index 00000000000..b957bf283e7 --- /dev/null +++ b/authz/src/test/java/io/grpc/authz/AuthorizationPolicyTranslatorTest.java @@ -0,0 +1,574 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.authz; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import io.envoyproxy.envoy.config.rbac.v3.Permission; +import io.envoyproxy.envoy.config.rbac.v3.Policy; +import io.envoyproxy.envoy.config.rbac.v3.Principal; +import io.envoyproxy.envoy.config.rbac.v3.Principal.Authenticated; +import io.envoyproxy.envoy.config.rbac.v3.RBAC; +import io.envoyproxy.envoy.config.rbac.v3.RBAC.Action; +import io.envoyproxy.envoy.config.route.v3.HeaderMatcher; +import io.envoyproxy.envoy.type.matcher.v3.PathMatcher; +import io.envoyproxy.envoy.type.matcher.v3.RegexMatcher; +import io.envoyproxy.envoy.type.matcher.v3.StringMatcher; +import java.io.IOException; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AuthorizationPolicyTranslatorTest { + @Test + public void invalidPolicy() throws Exception { + String policy = "{ \"name\": \"abc\",, }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IOException ioe) { + assertThat(ioe).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON" + + " at line 1 column 18 path $.name"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingAuthorizationPolicyName() throws Exception { + String policy = "{}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"name\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void incorrectAuthorizationPolicyName() throws Exception { + String policy = "{ \"name\": [\"abc\"] }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (ClassCastException cce) { + assertThat(cce).hasMessageThat().isEqualTo( + "value '[abc]' for key 'name' in '{name=[abc]}' is not String"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingAllowRules() throws Exception { + String policy = "{ \"name\": \"authz\" }"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"allow_rules\" is absent"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingRuleName() throws Exception { + String policy = "{" + + " \"name\" : \"abc\" ," + + " \"allow_rules\" : [" + + " {}" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("rule \"name\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingSourceAndRequest() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\" : [" + + " {" + + " \"name\": \"allow_all\"" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(1, rbacs.size()); + RBAC expected_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_all", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder().setAny(true)) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_rbac, rbacs.get(0)); + } + + @Test + public void emptySourceAndRequest() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\" : [" + + " {" + + " \"name\": \"allow_all\"," + + " \"source\": {}," + + " \"request\": {}" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(1, rbacs.size()); + RBAC expected_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_all", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder().setAny(true)) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_rbac, rbacs.get(0)); + } + + @Test + public void incorrectRulesType() throws Exception { + String policy = "{" + + " \"name\" : \"abc\" ," + + " \"allow_rules\" : {}" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (ClassCastException cce) { + assertThat(cce).hasMessageThat().isEqualTo( + "value '{}' for key 'allow_rules' in '{name=abc, allow_rules={}}' is not List"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void parseSourceSuccess() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"deny_rules\": [" + + " {" + + " \"name\": \"deny_users\"," + + " \"source\": {" + + " \"principals\": [" + + " \"spiffe://foo.com\"," + + " \"spiffe://bar*\"," + + " \"*baz\"," + + " \"spiffe://*.com\"" + + " ]" + + " }" + + " }" + + " ]," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_any\"," + + " \"source\": {" + + " \"principals\": [" + + " \"*\"" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(2, rbacs.size()); + RBAC expected_deny_rbac = + RBAC.newBuilder() + .setAction(Action.DENY) + .putPolicies("authz_deny_users", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder() + .setOrIds( + Principal.Set.newBuilder() + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setExact("spiffe://foo.com").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setPrefix("spiffe://bar").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setSuffix("baz").build()).build()).build()) + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setExact("spiffe://*.com").build()).build()).build()) + .build()).build()) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()).build(); + RBAC expected_allow_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_any", + Policy.newBuilder() + .addPrincipals(Principal.newBuilder() + .setOrIds( + Principal.Set.newBuilder() + .addIds(Principal.newBuilder() + .setAuthenticated(Authenticated.newBuilder() + .setPrincipalName(StringMatcher.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder() + .setRegex(".+").build()).build()).build()) + .build()) + .build()).build()) + .addPermissions(Permission.newBuilder().setAny(true)) + .build()).build(); + assertEquals(expected_deny_rbac, rbacs.get(0)); + assertEquals(expected_allow_rbac, rbacs.get(1)); + } + + @Test + public void unsupportedPseudoHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \":method\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" :method"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void unsupportedGrpcPrefixHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"grpc-xxx\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" grpc-xxx"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void unsupportedHostHeaders() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"Host\"," + + " \"values\": [" + + " \"foo\"" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("Unsupported \"key\" Host"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingHeaderKey() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {}" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"key\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void missingHeaderValues() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void emptyHeaderValues() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_dev\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"," + + " \"values\": []" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + try { + AuthorizationPolicyTranslator.translate(policy); + fail("exception expected"); + } catch (IllegalArgumentException iae) { + assertThat(iae).hasMessageThat().isEqualTo("\"values\" is absent or empty"); + } catch (Exception e) { + throw new AssertionError("the test failed ", e); + } + } + + @Test + public void parseRequestSuccess() throws Exception { + String policy = "{" + + " \"name\" : \"authz\" ," + + " \"deny_rules\": [" + + " {" + + " \"name\": \"deny_access\"," + + " \"request\": {" + + " \"paths\": [" + + " \"/pkg.service/foo\"," + + " \"/pkg.service/bar*\"" + + " ]," + + " \"headers\": [" + + " {" + + " \"key\": \"dev-path\"," + + " \"values\": [\"/dev/path/*\"]" + + " }" + + " ]" + + " }" + + " }" + + " ]," + + " \"allow_rules\": [" + + " {" + + " \"name\": \"allow_access1\"," + + " \"request\": {" + + " \"headers\": [" + + " {" + + " \"key\": \"key-1\"," + + " \"values\": [" + + " \"foo\"," + + " \"*bar\"" + + " ]" + + " }," + + " {" + + " \"key\": \"key-2\"," + + " \"values\": [" + + " \"*\"" + + " ]" + + " }" + + " ]" + + " }" + + " }," + + " {" + + " \"name\": \"allow_access2\"," + + " \"request\": {" + + " \"paths\": [" + + " \"*baz\"" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + List rbacs = AuthorizationPolicyTranslator.translate(policy); + assertEquals(2, rbacs.size()); + RBAC expected_deny_rbac = + RBAC.newBuilder() + .setAction(Action.DENY) + .putPolicies("authz_deny_access", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setExact("/pkg.service/foo").build()).build()).build()) + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setPrefix("/pkg.service/bar").build()).build()).build()) + .build()).build()) + .addRules(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("dev-path") + .setStringMatch(StringMatcher.newBuilder() + .setPrefix("/dev/path/").build()) + .build()) + .build()) + .build()).build()) + .build()).build()) + .build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()).build(); + RBAC expected_allow_rbac = + RBAC.newBuilder() + .setAction(Action.ALLOW) + .putPolicies("authz_allow_access1", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-1") + .setStringMatch(StringMatcher.newBuilder() + .setExact("foo").build()) + .build()) + .build()) + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-1") + .setStringMatch(StringMatcher.newBuilder() + .setSuffix("bar").build()) + .build()) + .build()) + .build()).build()) + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setHeader(HeaderMatcher.newBuilder() + .setName("key-2") + .setStringMatch(StringMatcher.newBuilder() + .setSafeRegex(RegexMatcher.newBuilder() + .setRegex(".+").build()).build()) + .build()) + .build()) + .build()).build()).build()).build()).build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()) + .putPolicies("authz_allow_access2", + Policy.newBuilder() + .addPermissions(Permission.newBuilder() + .setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setOrRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder() + .setUrlPath(PathMatcher.newBuilder() + .setPath(StringMatcher.newBuilder() + .setSuffix("baz").build()).build()).build()) + .build()).build()) + .build())) + .addPrincipals(Principal.newBuilder().setAny(true)) + .build()) + .build(); + assertEquals(expected_deny_rbac, rbacs.get(0)); + assertEquals(expected_allow_rbac, rbacs.get(1)); + } +} diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 48b9030714f..c8a8669dd1f 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -27,6 +27,7 @@ dependencies { project(':grpc-stub'), project(':grpc-protobuf'), project(':grpc-testing'), + project(path: ':grpc-xds', configuration: 'shadow'), libraries.hdrhistogram, libraries.netty_tcnative, libraries.netty_epoll, diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/SocketAddressValidator.java b/benchmarks/src/main/java/io/grpc/benchmarks/SocketAddressValidator.java index 6c6d597cd5e..c35cc7e5c38 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/SocketAddressValidator.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/SocketAddressValidator.java @@ -33,6 +33,11 @@ public interface SocketAddressValidator { public boolean isValidSocketAddress(SocketAddress address) { return address instanceof InetSocketAddress; } + + @Override + public boolean isValidSocketAddress(String address) { + return !address.startsWith("unix://"); + } }; /** @@ -43,10 +48,20 @@ public boolean isValidSocketAddress(SocketAddress address) { public boolean isValidSocketAddress(SocketAddress address) { return "DomainSocketAddress".equals(address.getClass().getSimpleName()); } + + @Override + public boolean isValidSocketAddress(String address) { + return address.startsWith("unix://"); + } }; /** * Returns {@code true} if the given address is valid. */ boolean isValidSocketAddress(SocketAddress address); + + /** + * Returns {@code true} if the given address is valid. + */ + boolean isValidSocketAddress(String address); } diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/Transport.java b/benchmarks/src/main/java/io/grpc/benchmarks/Transport.java index 0097bd9dd20..820b3ac1968 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/Transport.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/Transport.java @@ -16,8 +16,6 @@ package io.grpc.benchmarks; -import java.net.SocketAddress; - /** * All of the supported transports. */ @@ -49,7 +47,7 @@ public enum Transport { * * @throws IllegalArgumentException if the given address is invalid for this transport. */ - public void validateSocketAddress(SocketAddress address) { + public void validateSocketAddress(String address) { if (!socketAddressValidator.isValidSocketAddress(address)) { throw new IllegalArgumentException( "Invalid address " + address + " for transport " + this); diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/Utils.java b/benchmarks/src/main/java/io/grpc/benchmarks/Utils.java index 9a3d1f6bc24..8087afbf406 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/Utils.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/Utils.java @@ -18,20 +18,22 @@ import static java.util.concurrent.ForkJoinPool.defaultForkJoinWorkerThreadFactory; +import com.google.common.base.Preconditions; import com.google.common.util.concurrent.UncaughtExceptionHandlers; import com.google.protobuf.ByteString; +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Status; +import io.grpc.TlsChannelCredentials; import io.grpc.benchmarks.proto.Messages; import io.grpc.benchmarks.proto.Messages.Payload; import io.grpc.benchmarks.proto.Messages.SimpleRequest; import io.grpc.benchmarks.proto.Messages.SimpleResponse; -import io.grpc.internal.testing.TestUtils; -import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyChannelBuilder; import io.grpc.okhttp.OkHttpChannelBuilder; -import io.grpc.okhttp.internal.Platform; +import io.grpc.testing.TlsTesting; import io.netty.channel.epoll.EpollDomainSocketChannel; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; @@ -79,15 +81,10 @@ public static boolean parseBoolean(String value) { /** * Parse a {@link SocketAddress} from the given string. */ - public static SocketAddress parseSocketAddress(String value) { + public static SocketAddress parseServerSocketAddress(String value) { if (value.startsWith(UNIX_DOMAIN_SOCKET_PREFIX)) { - // Unix Domain Socket address. - // Create the underlying file for the Unix Domain Socket. - String filePath = value.substring(UNIX_DOMAIN_SOCKET_PREFIX.length()); - File file = new File(filePath); - if (!file.isAbsolute()) { - throw new IllegalArgumentException("File path must be absolute: " + filePath); - } + DomainSocketAddress domainAddress = parseUnixSocketAddress(value); + File file = new File(domainAddress.path()); try { if (file.createNewFile()) { // If this application created the file, delete it when the application exits. @@ -96,8 +93,7 @@ public static SocketAddress parseSocketAddress(String value) { } catch (IOException ex) { throw new RuntimeException(ex); } - // Create the SocketAddress referencing the file. - return new DomainSocketAddress(file); + return domainAddress; } else { // Standard TCP/IP address. String[] parts = value.split(":", 2); @@ -111,37 +107,24 @@ public static SocketAddress parseSocketAddress(String value) { } } - private static OkHttpChannelBuilder newOkHttpClientChannel( - SocketAddress address, boolean tls, boolean testca) { - InetSocketAddress addr = (InetSocketAddress) address; - OkHttpChannelBuilder builder = - OkHttpChannelBuilder.forAddress(addr.getHostName(), addr.getPort()); - if (!tls) { - builder.usePlaintext(); - } else if (testca) { - try { - builder.sslSocketFactory(TestUtils.newSslSocketFactoryForCa( - Platform.get().getProvider(), - TestUtils.loadCert("ca.pem"))); - } catch (Exception e) { - throw new RuntimeException(e); - } + private static DomainSocketAddress parseUnixSocketAddress(String value) { + Preconditions.checkArgument( + value.startsWith(UNIX_DOMAIN_SOCKET_PREFIX), + "Must start with %s: %s", UNIX_DOMAIN_SOCKET_PREFIX, value); + // Unix Domain Socket address. + // Create the underlying file for the Unix Domain Socket. + String filePath = value.substring(UNIX_DOMAIN_SOCKET_PREFIX.length()); + File file = new File(filePath); + if (!file.isAbsolute()) { + throw new IllegalArgumentException("File path must be absolute: " + filePath); } - return builder; + // Create the SocketAddress referencing the file. + return new DomainSocketAddress(file); } - private static NettyChannelBuilder newNettyClientChannel(Transport transport, - SocketAddress address, boolean tls, boolean testca, int flowControlWindow) - throws IOException { - NettyChannelBuilder builder = - NettyChannelBuilder.forAddress(address).flowControlWindow(flowControlWindow); - if (!tls) { - builder.usePlaintext(); - } else if (testca) { - File cert = TestUtils.loadCert("ca.pem"); - builder.sslContext(GrpcSslContexts.forClient().trustManager(cert).build()); - } - + private static NettyChannelBuilder configureNetty( + NettyChannelBuilder builder, Transport transport, int flowControlWindow) { + builder.flowControlWindow(flowControlWindow); DefaultThreadFactory tf = new DefaultThreadFactory("client-elg-", true /*daemon */); switch (transport) { case NETTY_NIO: @@ -194,17 +177,38 @@ public ForkJoinWorkerThread newThread(ForkJoinPool pool) { /** * Create a {@link ManagedChannel} for the given parameters. */ - public static ManagedChannel newClientChannel(Transport transport, SocketAddress address, + public static ManagedChannel newClientChannel(Transport transport, String target, boolean tls, boolean testca, @Nullable String authorityOverride, int flowControlWindow, boolean directExecutor) { + ChannelCredentials credentials; + if (tls) { + if (testca) { + try { + credentials = TlsChannelCredentials.newBuilder() + .trustManager(TlsTesting.loadCert("ca.pem")) + .build(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } else { + credentials = TlsChannelCredentials.create(); + } + } else { + credentials = InsecureChannelCredentials.create(); + } ManagedChannelBuilder builder; if (transport == Transport.OK_HTTP) { - builder = newOkHttpClientChannel(address, tls, testca); + builder = OkHttpChannelBuilder.forTarget(target, credentials) + .flowControlWindow(flowControlWindow); } else { - try { - builder = newNettyClientChannel(transport, address, tls, testca, flowControlWindow); - } catch (Exception e) { - throw new RuntimeException(e); + if (target.startsWith(UNIX_DOMAIN_SOCKET_PREFIX)) { + builder = configureNetty( + NettyChannelBuilder.forAddress(parseUnixSocketAddress(target), credentials), + transport, flowControlWindow); + } else { + builder = configureNetty( + NettyChannelBuilder.forTarget(target, credentials), + transport, flowControlWindow); } } if (authorityOverride != null) { diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadClient.java b/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadClient.java index c25fb38fdec..c9a5812b6a6 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadClient.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadClient.java @@ -82,7 +82,7 @@ class LoadClient { channels[i] = Utils.newClientChannel( Epoll.isAvailable() ? Transport.NETTY_EPOLL : Transport.NETTY_NIO, - Utils.parseSocketAddress(config.getServerTargets(i % config.getServerTargetsCount())), + config.getServerTargets(i % config.getServerTargetsCount()), config.hasSecurityParams(), config.hasSecurityParams() && config.getSecurityParams().getUseTestCa(), config.hasSecurityParams() diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/qps/ClientConfiguration.java b/benchmarks/src/main/java/io/grpc/benchmarks/qps/ClientConfiguration.java index f15779bdabc..3bafdb836ba 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/qps/ClientConfiguration.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/qps/ClientConfiguration.java @@ -28,8 +28,6 @@ import io.grpc.benchmarks.proto.Messages.PayloadType; import io.grpc.internal.testing.TestUtils; import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; @@ -47,7 +45,7 @@ public class ClientConfiguration implements Configuration { String authorityOverride = TestUtils.TEST_SERVER_HOST; boolean useDefaultCiphers; boolean directExecutor; - SocketAddress address; + String target; int channels = 4; int outstandingRpcsPerChannel = 10; int serverPayload; @@ -66,7 +64,7 @@ private ClientConfiguration() { } public ManagedChannel newChannel() throws IOException { - return Utils.newClientChannel(transport, address, tls, testca, authorityOverride, + return Utils.newClientChannel(transport, target, tls, testca, authorityOverride, flowControlWindow, directExecutor); } @@ -106,18 +104,10 @@ protected ClientConfiguration build0(ClientConfiguration config) { throw new IllegalArgumentException( "Transport " + config.transport.name().toLowerCase() + " does not support TLS."); } - - if (config.transport != Transport.OK_HTTP - && config.testca && config.address instanceof InetSocketAddress) { - // Override the socket address with the host from the testca. - InetSocketAddress address = (InetSocketAddress) config.address; - config.address = TestUtils.testServerAddress(address.getHostName(), - address.getPort()); - } } // Verify that the address type is correct for the transport type. - config.transport.validateSocketAddress(config.address); + config.transport.validateSocketAddress(config.target); return config; } @@ -136,7 +126,7 @@ enum ClientParam implements AbstractConfigurationBuilder.Param { + "(unix:///path/to/file), depending on the transport selected.", null, true) { @Override protected void setClientValue(ClientConfiguration config, String value) { - config.address = Utils.parseSocketAddress(value); + config.target = value; } }, CHANNELS("INT", "Number of Channels.", "" + DEFAULT.channels) { diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/qps/ServerConfiguration.java b/benchmarks/src/main/java/io/grpc/benchmarks/qps/ServerConfiguration.java index a8a097303f1..915c1da75eb 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/qps/ServerConfiguration.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/qps/ServerConfiguration.java @@ -142,7 +142,7 @@ enum ServerParam implements AbstractConfigurationBuilder.Param { + "(unix:///path/to/file), depending on the transport selected.", null, true) { @Override protected void setServerValue(ServerConfiguration config, String value) { - SocketAddress address = Utils.parseSocketAddress(value); + SocketAddress address = Utils.parseServerSocketAddress(value); if (address instanceof InetSocketAddress) { InetSocketAddress addr = (InetSocketAddress) address; int port = addr.getPort() == 0 ? Utils.pickUnusedPort() : addr.getPort(); diff --git a/binder/build.gradle b/binder/build.gradle index fa9d0d8cee9..dc3da4c9a72 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -111,12 +111,12 @@ task javadocs(type: Javadoc) { } task javadocJar(type: Jar, dependsOn: javadocs) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadocs.destinationDir } task sourcesJar(type: Jar) { - classifier = 'sources' + archiveClassifier = 'sources' from android.sourceSets.main.java.srcDirs } diff --git a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java index c37ba457b2b..4809a2db43f 100644 --- a/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java +++ b/binder/src/main/java/io/grpc/binder/AndroidComponentAddress.java @@ -69,11 +69,15 @@ public static AndroidComponentAddress forLocalComponent(Context context, Class> ongoingCalls; + @GuardedBy("this") + private final LinkedHashSet callIdsToNotifyWhenReady = new LinkedHashSet<>(); + @GuardedBy("this") protected Attributes attributes; @@ -529,9 +534,18 @@ final void handleAcknowledgedBytes(long numBytes) { logger.log( Level.FINE, "handleAcknowledgedBytes: Transmit Window No-Longer Full. Unblock calls: " + this); - // We're ready again, and need to poke any waiting transactions. - for (Inbound inbound : ongoingCalls.values()) { - inbound.onTransportReady(); + + // The LinkedHashSet contract guarantees that an id already present in this collection will + // not lose its priority if we re-insert it here. + callIdsToNotifyWhenReady.addAll(ongoingCalls.keySet()); + + Iterator i = callIdsToNotifyWhenReady.iterator(); + while (isReady() && i.hasNext()) { + Inbound inbound = ongoingCalls.get(i.next()); + i.remove(); + if (inbound != null) { // Calls can be removed out from under us. + inbound.onTransportReady(); + } } } } diff --git a/binder/src/main/java/io/grpc/binder/internal/Inbound.java b/binder/src/main/java/io/grpc/binder/internal/Inbound.java index f28b9bfb29a..da1a2961546 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Inbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Inbound.java @@ -345,8 +345,8 @@ final synchronized void handleTransaction(Parcel parcel) { int index = parcel.readInt(); boolean hasPrefix = TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_PREFIX); boolean hasMessageData = - (TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_MESSAGE_DATA)); - boolean hasSuffix = (TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_SUFFIX)); + TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_MESSAGE_DATA); + boolean hasSuffix = TransactionUtils.hasFlag(flags, TransactionUtils.FLAG_SUFFIX); if (hasPrefix) { handlePrefix(flags, parcel); onDeliveryState(State.PREFIX_DELIVERED); diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 11480b36a33..650ead9bcdb 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -119,6 +119,7 @@ public synchronized void bind() { state = State.BINDING; Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags); if (!bindResult.isOk()) { + handleBindServiceFailure(sourceContext, this); state = State.UNBOUND; mainThreadExecutor.execute(() -> notifyUnbound(bindResult)); } @@ -142,6 +143,19 @@ private static Status bindInternal( } } + // Over the years, the API contract for Context#bindService() has been inconsistent on the subject + // of error handling. But inspecting recent AOSP implementations shows that, internally, + // bindService() retains a reference to the ServiceConnection when it throws certain Exceptions + // and even when it returns false. To avoid leaks, we *always* call unbindService() in case of + // error and simply ignore any "Service not registered" IAE and other RuntimeExceptions. + private static void handleBindServiceFailure(Context context, ServiceConnection conn) { + try { + context.unbindService(conn); + } catch (RuntimeException e) { + logger.log(Level.FINE, "Could not clean up after bindService() failure.", e); + } + } + @Override @AnyThread public void unbind() { diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 44a3898e532..967e91b820d 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -68,6 +68,9 @@ public void setUp() { shadowApplication = shadowOf(appContext); shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder); + // Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't. + shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false); + binding = newBuilder().build(); shadowOf(getMainLooper()).idle(); } @@ -137,6 +140,7 @@ public void testBindUnbind() throws Exception { assertThat(observer.gotUnboundEvent).isTrue(); assertThat(observer.unboundReason.getCode()).isEqualTo(Code.CANCELLED); assertThat(binding.isSourceContextCleared()).isTrue(); + assertThat(shadowApplication.getBoundServiceConnections()).isEmpty(); } @Test @@ -174,6 +178,7 @@ public void testBindFailure() throws Exception { assertThat(observer.gotUnboundEvent).isTrue(); assertThat(observer.unboundReason.getCode()).isEqualTo(Code.UNIMPLEMENTED); assertThat(binding.isSourceContextCleared()).isTrue(); + assertThat(shadowApplication.getBoundServiceConnections()).isEmpty(); } @Test @@ -187,6 +192,7 @@ public void testBindSecurityException() throws Exception { assertThat(observer.unboundReason.getCode()).isEqualTo(Code.PERMISSION_DENIED); assertThat(observer.unboundReason.getCause()).isEqualTo(securityException); assertThat(binding.isSourceContextCleared()).isTrue(); + assertThat(shadowApplication.getBoundServiceConnections()).isEmpty(); } @Test @@ -257,7 +263,8 @@ private void assertNoLockHeld() { } catch (IllegalMonitorStateException ime) { // Expected. } catch (InterruptedException inte) { - throw new AssertionError("Interrupted exception when we shouldn't have been able to wait.", inte); + throw new AssertionError( + "Interrupted exception when we shouldn't have been able to wait.", inte); } } diff --git a/bom/build.gradle b/bom/build.gradle index 49fcb3cbd90..be2722d4bc0 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -14,7 +14,7 @@ publishing { // Generate bom using subprojects def internalProjects = [ project.name, - 'grpc-binder', + 'grpc-authz', 'grpc-compiler', 'grpc-gae-interop-testing-jdk8', ] diff --git a/build.gradle b/build.gradle index 149ade4ae02..51dbffae770 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.44.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.45.0" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() @@ -56,12 +56,12 @@ subprojects { javaPluginPath = "$rootDir/compiler/build/exe/java_plugin/$protocPluginBaseName$exeSuffix" nettyVersion = '4.1.72.Final' - guavaVersion = '30.1.1-android' - googleauthVersion = '0.22.2' + guavaVersion = '31.0.1-android' + googleauthVersion = '1.4.0' protobufVersion = '3.19.2' protocVersion = protobufVersion opencensusVersion = '0.28.0' - autovalueVersion = '1.7.4' + autovalueVersion = '1.9' configureProtoCompilation = { String generatedSourcePath = "${projectDir}/src/generated" @@ -151,7 +151,7 @@ subprojects { animalsniffer_annotations: "org.codehaus.mojo:animal-sniffer-annotations:1.19", autovalue: "com.google.auto.value:auto-value:${autovalueVersion}", autovalue_annotation: "com.google.auto.value:auto-value-annotations:${autovalueVersion}", - errorprone: "com.google.errorprone:error_prone_annotations:2.9.0", + errorprone: "com.google.errorprone:error_prone_annotations:2.10.0", cronet_api: 'org.chromium.net:cronet-api:92.4515.131', cronet_embedded: 'org.chromium.net:cronet-embedded:92.4515.131', gson: "com.google.code.gson:gson:2.8.9", @@ -176,6 +176,7 @@ subprojects { netty: "io.netty:netty-codec-http2:[${nettyVersion}]", netty_epoll: "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64", + netty_epoll_arm64: "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64", netty_proxy_handler: "io.netty:netty-handler-proxy:${nettyVersion}", // Keep the following references of tcnative version in sync whenever it's updated @@ -239,25 +240,21 @@ subprojects { } } - if (rootProject.properties.get('errorProne', true)) { + if (!project.hasProperty('errorProne') || errorProne.toBoolean()) { dependencies { - errorprone 'com.google.errorprone:error_prone_core:2.4.0' + errorprone 'com.google.errorprone:error_prone_core:2.10.0' errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1' } } else { // Disable Error Prone - allprojects { - afterEvaluate { project -> - project.tasks.withType(JavaCompile) { - options.errorprone.enabled = false - } - } + tasks.withType(JavaCompile) { + options.errorprone.enabled = false } } plugins.withId("java") { - sourceCompatibility = 1.7 - targetCompatibility = 1.7 + sourceCompatibility = 1.8 + targetCompatibility = 1.8 dependencies { testImplementation libraries.junit, @@ -304,7 +301,7 @@ subprojects { maxHeapSize = '1500m' } - if (rootProject.properties.get('errorProne', true)) { + if (!project.hasProperty('errorProne') || errorProne.toBoolean()) { dependencies { annotationProcessor 'com.google.guava:guava-beta-checker:1.0' } @@ -315,6 +312,7 @@ subprojects { options.errorprone.check("UnnecessaryAnonymousClass", CheckSeverity.OFF) // This project targets Java 7 (no time.Duration class) options.errorprone.check("PreferJavaTimeOverload", CheckSeverity.OFF) + options.errorprone.check("JavaUtilDate", CheckSeverity.OFF) // The warning fails to provide a source location options.errorprone.check("MissingSummary", CheckSeverity.OFF) } @@ -323,6 +321,7 @@ subprojects { options.errorprone.check("JdkObsolete", CheckSeverity.OFF) options.errorprone.check("UnnecessaryAnonymousClass", CheckSeverity.OFF) options.errorprone.check("PreferJavaTimeOverload", CheckSeverity.OFF) + options.errorprone.check("JavaUtilDate", CheckSeverity.OFF) } plugins.withId("ru.vyarus.animalsniffer") { diff --git a/buildscripts/kokoro/kokoro.sh b/buildscripts/kokoro/kokoro.sh new file mode 100755 index 00000000000..0b3864b5092 --- /dev/null +++ b/buildscripts/kokoro/kokoro.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +spongify_logs() { + local f + while read -r f; do + mkdir "${f%.xml}" + cp "$f" "${f%.xml}/sponge_log.xml" + done < <(find "${KOKORO_ARTIFACTS_DIR:-.}" -name 'TEST-*.xml') +} diff --git a/buildscripts/kokoro/linux_aarch64.cfg b/buildscripts/kokoro/linux_aarch64.cfg index f91fbe4d702..325d910c5ea 100644 --- a/buildscripts/kokoro/linux_aarch64.cfg +++ b/buildscripts/kokoro/linux_aarch64.cfg @@ -6,7 +6,8 @@ timeout_mins: 60 action { define_artifacts { + regex: "github/grpc-java/**/build/test-results/**/sponge_log.xml" regex: "github/grpc-java/mvn-artifacts/**" regex: "github/grpc-java/artifacts/**" } -} \ No newline at end of file +} diff --git a/buildscripts/kokoro/linux_aarch64.sh b/buildscripts/kokoro/linux_aarch64.sh index 11c3f8ae3a1..f4a1292efb5 100755 --- a/buildscripts/kokoro/linux_aarch64.sh +++ b/buildscripts/kokoro/linux_aarch64.sh @@ -5,6 +5,11 @@ if [[ -f /VERSION ]]; then cat /VERSION fi +readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" + +. "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh +trap spongify_logs EXIT + cd github/grpc-java buildscripts/qemu_helpers/prepare_qemu.sh diff --git a/buildscripts/kokoro/linux_artifacts.cfg b/buildscripts/kokoro/linux_artifacts.cfg index 0938e4bff2e..8691b5ff356 100644 --- a/buildscripts/kokoro/linux_artifacts.cfg +++ b/buildscripts/kokoro/linux_artifacts.cfg @@ -6,6 +6,7 @@ timeout_mins: 60 action { define_artifacts { + regex: "github/grpc-java/**/build/test-results/**/sponge_log.xml" regex: "github/grpc-java/mvn-artifacts/**" regex: "github/grpc-java/artifacts/**" } diff --git a/buildscripts/kokoro/linux_artifacts.sh b/buildscripts/kokoro/linux_artifacts.sh index b4551843c6a..e23b2bcc628 100755 --- a/buildscripts/kokoro/linux_artifacts.sh +++ b/buildscripts/kokoro/linux_artifacts.sh @@ -7,6 +7,9 @@ fi readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" +. "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh +trap spongify_logs EXIT + "$GRPC_JAVA_DIR"/buildscripts/build_docker.sh "$GRPC_JAVA_DIR"/buildscripts/run_in_docker.sh /grpc-java/buildscripts/build_artifacts_in_docker.sh diff --git a/buildscripts/kokoro/macos.cfg b/buildscripts/kokoro/macos.cfg index a6bf290d1ec..f79634e3cf8 100644 --- a/buildscripts/kokoro/macos.cfg +++ b/buildscripts/kokoro/macos.cfg @@ -1,7 +1,7 @@ # Config file for internal CI # Location of the continuous shell script in repository. -build_file: "grpc-java/buildscripts/kokoro/unix.sh" +build_file: "grpc-java/buildscripts/kokoro/macos.sh" timeout_mins: 45 # We had problems with random tests timing out because it took seconds to do @@ -15,7 +15,7 @@ env_vars { # We always build mvn artifacts. action { define_artifacts { - regex: "github/grpc-java/**/build/reports/**" + regex: "github/grpc-java/**/build/test-results/**/sponge_log.xml" regex: "github/grpc-java/mvn-artifacts/**" } } diff --git a/buildscripts/kokoro/macos.sh b/buildscripts/kokoro/macos.sh new file mode 100755 index 00000000000..df52a004f52 --- /dev/null +++ b/buildscripts/kokoro/macos.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -veux -o pipefail + +if [[ -f /VERSION ]]; then + cat /VERSION +fi + +readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" + +. "$GRPC_JAVA_DIR"/buildscripts/kokoro/kokoro.sh +trap spongify_logs EXIT + +"$GRPC_JAVA_DIR"/buildscripts/kokoro/unix.sh diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index 61d65324a07..91ee67f1d98 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -16,10 +16,6 @@ set -exu -o pipefail # It would be nicer to use 'readlink -f' here but osx does not support it. readonly GRPC_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)" -if [[ -f /VERSION ]]; then - cat /VERSION -fi - # cd to the root dir of grpc-java cd $(dirname $0)/../.. @@ -48,7 +44,7 @@ export LD_LIBRARY_PATH=/tmp/protobuf/lib export LDFLAGS=-L/tmp/protobuf/lib export CXXFLAGS="-I/tmp/protobuf/include" -./gradlew clean $GRADLE_FLAGS +./gradlew grpc-compiler:clean $GRADLE_FLAGS if [[ -z "${SKIP_TESTS:-}" ]]; then # Ensure all *.proto changes include *.java generated code @@ -62,7 +58,6 @@ if [[ -z "${SKIP_TESTS:-}" ]]; then # Run tests ./gradlew build :grpc-all:jacocoTestReport $GRADLE_FLAGS pushd examples - ./gradlew clean $GRADLE_FLAGS ./gradlew build $GRADLE_FLAGS # --batch-mode reduces log spam mvn verify --batch-mode diff --git a/buildscripts/kokoro/windows.cfg b/buildscripts/kokoro/windows.cfg index 6b2703f99c9..bdfaa38904f 100644 --- a/buildscripts/kokoro/windows.cfg +++ b/buildscripts/kokoro/windows.cfg @@ -7,7 +7,7 @@ timeout_mins: 45 # We always build mvn artifacts. action { define_artifacts { - regex: "**/build/test-results/**/*.xml" + regex: "github/grpc-java/**/build/test-results/**/sponge_log.xml" regex: "github/grpc-java/mvn-artifacts/**" } } diff --git a/buildscripts/kokoro/windows64.bat b/buildscripts/kokoro/windows64.bat index edcb421fe7d..eaa1fcf845e 100644 --- a/buildscripts/kokoro/windows64.bat +++ b/buildscripts/kokoro/windows64.bat @@ -32,4 +32,4 @@ SET GRADLE_FLAGS=-PtargetArch=%TARGET_ARCH% -PfailOnWarnings=%FAIL_ON_WARNINGS% @rem make sure no daemons have any files open cmd.exe /C "%WORKSPACE%\gradlew.bat --stop" -cmd.exe /C "%WORKSPACE%\gradlew.bat %GRADLE_FLAGS% -Dorg.gradle.parallel=false -PrepositoryDir=%WORKSPACE%\artifacts clean grpc-compiler:build grpc-compiler:publish" || exit /b 1 +cmd.exe /C "%WORKSPACE%\gradlew.bat %GRADLE_FLAGS% -Dorg.gradle.parallel=false -PrepositoryDir=%WORKSPACE%\artifacts grpc-compiler:clean grpc-compiler:build grpc-compiler:publish" || exit /b 1 diff --git a/buildscripts/kokoro/xds_k8s_lb.cfg b/buildscripts/kokoro/xds_k8s_lb.cfg new file mode 100644 index 00000000000..43971896cd4 --- /dev/null +++ b/buildscripts/kokoro/xds_k8s_lb.cfg @@ -0,0 +1,13 @@ +# Config file for internal CI + +# Location of the continuous shell script in repository. +build_file: "grpc-java/buildscripts/kokoro/xds_k8s_lb.sh" +timeout_mins: 180 + +action { + define_artifacts { + regex: "artifacts/**/*sponge_log.xml" + regex: "artifacts/**/*sponge_log.log" + strip_prefix: "artifacts" + } +} diff --git a/buildscripts/kokoro/xds_k8s_lb.sh b/buildscripts/kokoro/xds_k8s_lb.sh new file mode 100755 index 00000000000..22f1e383440 --- /dev/null +++ b/buildscripts/kokoro/xds_k8s_lb.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +set -eo pipefail + +# Constants +readonly GITHUB_REPOSITORY_NAME="grpc-java" +readonly TEST_DRIVER_INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/${TEST_DRIVER_REPO_OWNER:-grpc}/grpc/${TEST_DRIVER_BRANCH:-master}/tools/internal_ci/linux/grpc_xds_k8s_install_test_driver.sh" +## xDS test server/client Docker images +readonly SERVER_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-server" +readonly CLIENT_IMAGE_NAME="gcr.io/grpc-testing/xds-interop/java-client" +readonly FORCE_IMAGE_BUILD="${FORCE_IMAGE_BUILD:-0}" +readonly BUILD_APP_PATH="interop-testing/build/install/grpc-interop-testing" + +####################################### +# Builds the test app using gradle and smoke-checks its binaries +# Globals: +# SRC_DIR +# BUILD_APP_PATH +# Arguments: +# None +# Outputs: +# Writes the output of xds-test-client and xds-test-server --help to stderr +####################################### +build_java_test_app() { + echo "Building Java test app" + cd "${SRC_DIR}" + ./gradlew --no-daemon grpc-interop-testing:installDist -x test \ + -PskipCodegen=true -PskipAndroid=true --console=plain + + # Test-run binaries + run_ignore_exit_code "${SRC_DIR}/${BUILD_APP_PATH}/bin/xds-test-client" --help + run_ignore_exit_code "${SRC_DIR}/${BUILD_APP_PATH}/bin/xds-test-server" --help +} + +####################################### +# Builds test app Docker images and pushes them to GCR +# Globals: +# BUILD_APP_PATH +# SERVER_IMAGE_NAME: Test server Docker image name +# CLIENT_IMAGE_NAME: Test client Docker image name +# GIT_COMMIT: SHA-1 of git commit being built +# Arguments: +# None +# Outputs: +# Writes the output of `gcloud builds submit` to stdout, stderr +####################################### +build_test_app_docker_images() { + echo "Building Java xDS interop test app Docker images" + local docker_dir="${SRC_DIR}/buildscripts/xds-k8s" + local build_dir + build_dir="$(mktemp -d)" + # Copy Docker files, log properties, and the test app to the build dir + cp -v "${docker_dir}/"*.Dockerfile "${build_dir}" + cp -v "${docker_dir}/"*.properties "${build_dir}" + cp -rv "${SRC_DIR}/${BUILD_APP_PATH}" "${build_dir}" + # Pick a branch name for the built image + if [[ -n $KOKORO_JOB_NAME ]]; then + branch_name=$(echo "$KOKORO_JOB_NAME" | sed -E 's|^grpc/java/([^/]+)/.*|\1|') + else + branch_name='experimental' + fi + # Run Google Cloud Build + gcloud builds submit "${build_dir}" \ + --config "${docker_dir}/cloudbuild.yaml" \ + --substitutions "_SERVER_IMAGE_NAME=${SERVER_IMAGE_NAME},_CLIENT_IMAGE_NAME=${CLIENT_IMAGE_NAME},COMMIT_SHA=${GIT_COMMIT},BRANCH_NAME=${branch_name}" + # TODO(sergiitk): extra "cosmetic" tags for versioned branches, e.g. v1.34.x + # TODO(sergiitk): do this when adding support for custom configs per version +} + +####################################### +# Builds test app and its docker images unless they already exist +# Globals: +# SERVER_IMAGE_NAME: Test server Docker image name +# CLIENT_IMAGE_NAME: Test client Docker image name +# GIT_COMMIT: SHA-1 of git commit being built +# FORCE_IMAGE_BUILD +# Arguments: +# None +# Outputs: +# Writes the output to stdout, stderr +####################################### +build_docker_images_if_needed() { + # Check if images already exist + server_tags="$(gcloud_gcr_list_image_tags "${SERVER_IMAGE_NAME}" "${GIT_COMMIT}")" + printf "Server image: %s:%s\n" "${SERVER_IMAGE_NAME}" "${GIT_COMMIT}" + echo "${server_tags:-Server image not found}" + + client_tags="$(gcloud_gcr_list_image_tags "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}")" + printf "Client image: %s:%s\n" "${CLIENT_IMAGE_NAME}" "${GIT_COMMIT}" + echo "${client_tags:-Client image not found}" + + # Build if any of the images are missing, or FORCE_IMAGE_BUILD=1 + if [[ "${FORCE_IMAGE_BUILD}" == "1" || -z "${server_tags}" || -z "${client_tags}" ]]; then + build_java_test_app + build_test_app_docker_images + else + echo "Skipping Java test app build" + fi +} + +####################################### +# Executes the test case +# Globals: +# TEST_DRIVER_FLAGFILE: Relative path to test driver flagfile +# KUBE_CONTEXT: The name of kubectl context with GKE cluster access +# TEST_XML_OUTPUT_DIR: Output directory for the test xUnit XML report +# SERVER_IMAGE_NAME: Test server Docker image name +# CLIENT_IMAGE_NAME: Test client Docker image name +# GIT_COMMIT: SHA-1 of git commit being built +# Arguments: +# Test case name +# Outputs: +# Writes the output of test execution to stdout, stderr +# Test xUnit report to ${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml +####################################### +run_test() { + # Test driver usage: + # https://github.com/grpc/grpc/tree/master/tools/run_tests/xds_k8s_test_driver#basic-usage + local test_name="${1:?Usage: run_test test_name}" + set -x + python -m "tests.${test_name}" \ + --flagfile="${TEST_DRIVER_FLAGFILE}" \ + --kube_context="${KUBE_CONTEXT}" \ + --server_image="${SERVER_IMAGE_NAME}:${GIT_COMMIT}" \ + --client_image="${CLIENT_IMAGE_NAME}:${GIT_COMMIT}" \ + --xml_output_file="${TEST_XML_OUTPUT_DIR}/${test_name}/sponge_log.xml" \ + --force_cleanup + set +x +} + +####################################### +# Main function: provision software necessary to execute tests, and run them +# Globals: +# KOKORO_ARTIFACTS_DIR +# GITHUB_REPOSITORY_NAME +# SRC_DIR: Populated with absolute path to the source repo +# TEST_DRIVER_REPO_DIR: Populated with the path to the repo containing +# the test driver +# TEST_DRIVER_FULL_DIR: Populated with the path to the test driver source code +# TEST_DRIVER_FLAGFILE: Populated with relative path to test driver flagfile +# TEST_XML_OUTPUT_DIR: Populated with the path to test xUnit XML report +# GIT_ORIGIN_URL: Populated with the origin URL of git repo used for the build +# GIT_COMMIT: Populated with the SHA-1 of git commit being built +# GIT_COMMIT_SHORT: Populated with the short SHA-1 of git commit being built +# KUBE_CONTEXT: Populated with name of kubectl context with GKE cluster access +# Arguments: +# None +# Outputs: +# Writes the output of test execution to stdout, stderr +####################################### +main() { + local script_dir + script_dir="$(dirname "$0")" + + # Source the test driver from the master branch. + echo "Sourcing test driver install script from: ${TEST_DRIVER_INSTALL_SCRIPT_URL}" + source /dev/stdin <<< "$(curl -s "${TEST_DRIVER_INSTALL_SCRIPT_URL}")" + + activate_gke_cluster GKE_CLUSTER_PSM_BASIC + + set -x + if [[ -n "${KOKORO_ARTIFACTS_DIR}" ]]; then + kokoro_setup_test_driver "${GITHUB_REPOSITORY_NAME}" + else + local_setup_test_driver "${script_dir}" + fi + build_docker_images_if_needed + # Run tests + cd "${TEST_DRIVER_FULL_DIR}" + run_test api_listener_test +} + +main "$@" diff --git a/compiler/src/test/golden/TestDeprecatedService.java.txt b/compiler/src/test/golden/TestDeprecatedService.java.txt index 68d8e1172a3..eda5a4de6ab 100644 --- a/compiler/src/test/golden/TestDeprecatedService.java.txt +++ b/compiler/src/test/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.44.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.45.0)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/test/golden/TestService.java.txt b/compiler/src/test/golden/TestService.java.txt index 3c6cdc82b51..6bb5aaebfc9 100644 --- a/compiler/src/test/golden/TestService.java.txt +++ b/compiler/src/test/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.44.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.45.0)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/compiler/src/testLite/golden/TestDeprecatedService.java.txt b/compiler/src/testLite/golden/TestDeprecatedService.java.txt index 8dcafe51f24..ab35321fe87 100644 --- a/compiler/src/testLite/golden/TestDeprecatedService.java.txt +++ b/compiler/src/testLite/golden/TestDeprecatedService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.44.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.45.0)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated @java.lang.Deprecated diff --git a/compiler/src/testLite/golden/TestService.java.txt b/compiler/src/testLite/golden/TestService.java.txt index 60ac085f920..e72fa752e52 100644 --- a/compiler/src/testLite/golden/TestService.java.txt +++ b/compiler/src/testLite/golden/TestService.java.txt @@ -8,7 +8,7 @@ import static io.grpc.MethodDescriptor.generateFullMethodName; * */ @javax.annotation.Generated( - value = "by gRPC proto compiler (version 1.44.0-SNAPSHOT)", + value = "by gRPC proto compiler (version 1.45.0)", comments = "Source: grpc/testing/compiler/test.proto") @io.grpc.stub.annotations.GrpcGenerated public final class TestServiceGrpc { diff --git a/context/build.gradle b/context/build.gradle index 35ad0566bb6..5815374aeb3 100644 --- a/context/build.gradle +++ b/context/build.gradle @@ -9,11 +9,17 @@ plugins { description = 'gRPC: Context' +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + dependencies { testImplementation libraries.jsr305 - testImplementation (libraries.guava_testlib) { + // Explicitly choose the guava version to stay Java 7-compatible. The rest of gRPC can move + // forward to Java 8-requiring versions. This is also only used for testing, so is unlikely to + // cause problems. + testImplementation ('com.google.guava:guava-testlib:30.1.1-android') { exclude group: 'junit', module: 'junit' } signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } diff --git a/context/src/main/java/io/grpc/Context.java b/context/src/main/java/io/grpc/Context.java index ad47ca9cb4d..41d3a5c94a6 100644 --- a/context/src/main/java/io/grpc/Context.java +++ b/context/src/main/java/io/grpc/Context.java @@ -1042,7 +1042,7 @@ public Context doAttach(Context toAttach) { * Implements {@link io.grpc.Context#current}. * *

Caution: {@link Context} interprets a return value of {@code null} to mean the same - * thing as {@code Context{@link #ROOT}}. + * thing as {@link Context#ROOT}. * *

See also {@link #doAttach(Context)}. * diff --git a/core/build.gradle b/core/build.gradle index ed8e5400765..bc8231fd9e1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -43,7 +43,7 @@ dependencies { jmh project(':grpc-testing') signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } javadoc { diff --git a/core/src/main/java/io/grpc/internal/AbstractServerImplBuilder.java b/core/src/main/java/io/grpc/internal/AbstractServerImplBuilder.java index d20c60cd446..c1268cfdb09 100644 --- a/core/src/main/java/io/grpc/internal/AbstractServerImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/AbstractServerImplBuilder.java @@ -17,6 +17,7 @@ package io.grpc.internal; import com.google.common.base.MoreObjects; +import com.google.errorprone.annotations.DoNotCall; import io.grpc.BinaryLog; import io.grpc.BindableService; import io.grpc.CompressorRegistry; @@ -53,6 +54,7 @@ protected AbstractServerImplBuilder() {} /** * This method serves to force sub classes to "hide" this static factory. */ + @DoNotCall("Unsupported") public static ServerBuilder forPort(int port) { throw new UnsupportedOperationException("Subclass failed to hide static factory"); } diff --git a/core/src/main/java/io/grpc/internal/ClientCallImpl.java b/core/src/main/java/io/grpc/internal/ClientCallImpl.java index e60f9e54b47..db1a992b968 100644 --- a/core/src/main/java/io/grpc/internal/ClientCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ClientCallImpl.java @@ -53,7 +53,7 @@ import io.perfmark.PerfMark; import io.perfmark.Tag; import java.io.InputStream; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.Locale; import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; @@ -71,7 +71,7 @@ final class ClientCallImpl extends ClientCall { private static final Logger log = Logger.getLogger(ClientCallImpl.class.getName()); private static final byte[] FULL_STREAM_DECOMPRESSION_ENCODINGS - = "gzip".getBytes(StandardCharsets.US_ASCII); + = "gzip".getBytes(Charset.forName("US-ASCII")); private final MethodDescriptor method; private final Tag tag; diff --git a/core/src/main/java/io/grpc/internal/ClientStreamListener.java b/core/src/main/java/io/grpc/internal/ClientStreamListener.java index dfb521f40a3..8db1fbe445f 100644 --- a/core/src/main/java/io/grpc/internal/ClientStreamListener.java +++ b/core/src/main/java/io/grpc/internal/ClientStreamListener.java @@ -57,12 +57,16 @@ enum RpcProgress { */ PROCESSED, /** - * The RPC is not processed by the server's application logic. + * The stream on the wire is created but not processed by the server's application logic. */ REFUSED, /** * The RPC is dropped (by load balancer). */ - DROPPED + DROPPED, + /** + * The stream is closed even before anything leaves the client. + */ + MISCARRIED } } diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index bce1a0f1503..6762c7853fb 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -55,7 +55,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -76,7 +75,7 @@ public final class GrpcUtil { private static final Logger log = Logger.getLogger(GrpcUtil.class.getName()); - public static final Charset US_ASCII = StandardCharsets.US_ASCII; + public static final Charset US_ASCII = Charset.forName("US-ASCII"); /** * {@link io.grpc.Metadata.Key} for the timeout header. @@ -204,7 +203,7 @@ public byte[] parseAsciiString(byte[] serialized) { public static final Splitter ACCEPT_ENCODING_SPLITTER = Splitter.on(',').trimResults(); - private static final String IMPLEMENTATION_VERSION = "1.44.0-SNAPSHOT"; // CURRENT_GRPC_VERSION + private static final String IMPLEMENTATION_VERSION = "1.45.0"; // CURRENT_GRPC_VERSION /** * The default timeout in nanos for a keepalive ping request. diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index fa2bf2e46bc..4f7c8d1f22f 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -772,6 +772,9 @@ private String printShortStatus(Status status) { if (status.getDescription() != null) { buffer.append("(").append(status.getDescription()).append(")"); } + if (status.getCause() != null) { + buffer.append("[").append(status.getCause()).append("]"); + } return buffer.toString(); } diff --git a/core/src/main/java/io/grpc/internal/JndiResourceResolverFactory.java b/core/src/main/java/io/grpc/internal/JndiResourceResolverFactory.java index a13c9fe4665..22e08de9cf4 100644 --- a/core/src/main/java/io/grpc/internal/JndiResourceResolverFactory.java +++ b/core/src/main/java/io/grpc/internal/JndiResourceResolverFactory.java @@ -140,7 +140,7 @@ public List resolveSrv(String host) throws Exception { Level level = Level.WARNING; for (String rawSrv : rawSrvRecords) { try { - String[] parts = whitespace.split(rawSrv); + String[] parts = whitespace.split(rawSrv, 5); Verify.verify(parts.length == 4, "Bad SRV Record: %s", rawSrv); // SRV requires the host name to be absolute if (!parts[3].endsWith(".")) { diff --git a/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java b/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java index 0cc1dd3aaca..3e7dd010e22 100644 --- a/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java +++ b/core/src/main/java/io/grpc/internal/ProxyDetectorImpl.java @@ -202,7 +202,14 @@ public ProxiedSocketAddress proxyFor(SocketAddress targetServerAddress) throws I private ProxiedSocketAddress detectProxy(InetSocketAddress targetAddr) throws IOException { URI uri; - String host = GrpcUtil.getHost(targetAddr); + String host; + try { + host = GrpcUtil.getHost(targetAddr); + } catch (Throwable t) { + // Workaround for Android API levels < 19 if getHostName causes a NetworkOnMainThreadException + log.log(Level.WARNING, "Failed to get host for proxy lookup, proceeding without proxy", t); + return null; + } try { uri = new URI( diff --git a/core/src/main/java/io/grpc/internal/RetriableStream.java b/core/src/main/java/io/grpc/internal/RetriableStream.java index 4afdb3f750e..d0430b5edbd 100644 --- a/core/src/main/java/io/grpc/internal/RetriableStream.java +++ b/core/src/main/java/io/grpc/internal/RetriableStream.java @@ -101,9 +101,12 @@ public void uncaughtException(Thread t, Throwable e) { false, 0); /** - * Either transparent retry happened or reached server's application logic. + * Either non-local transparent retry happened or reached server's application logic. + * + *

Note that local-only transparent retries are unlimited. */ private final AtomicBoolean noMoreTransparentRetry = new AtomicBoolean(); + private final AtomicInteger localOnlyTransparentRetries = new AtomicInteger(); // Used for recording the share of buffer used for the current call out of the channel buffer. // This field would not be necessary if there is no channel buffer limit. @@ -849,10 +852,29 @@ public void run() { } return; } + if (rpcProgress == RpcProgress.MISCARRIED + && localOnlyTransparentRetries.incrementAndGet() > 10_000) { + commitAndRun(substream); + if (state.winningSubstream == substream) { + Status tooManyTransparentRetries = Status.INTERNAL + .withDescription("Too many transparent retries. Might be a bug in gRPC") + .withCause(status.asRuntimeException()); + listenerSerializeExecutor.execute( + new Runnable() { + @Override + public void run() { + isClosed = true; + masterListener.closed(tooManyTransparentRetries, rpcProgress, trailers); + } + }); + } + return; + } if (state.winningSubstream == null) { - if (rpcProgress == RpcProgress.REFUSED - && noMoreTransparentRetry.compareAndSet(false, true)) { + if (rpcProgress == RpcProgress.MISCARRIED + || (rpcProgress == RpcProgress.REFUSED + && noMoreTransparentRetry.compareAndSet(false, true))) { // transparent retry final Substream newSubstream = createSubstream(substream.previousAttemptCount, true); if (isHedging) { diff --git a/core/src/main/java/io/grpc/internal/SubchannelChannel.java b/core/src/main/java/io/grpc/internal/SubchannelChannel.java index a1d454ed2fb..773dcb99dd7 100644 --- a/core/src/main/java/io/grpc/internal/SubchannelChannel.java +++ b/core/src/main/java/io/grpc/internal/SubchannelChannel.java @@ -43,7 +43,7 @@ final class SubchannelChannel extends Channel { Status.UNAVAILABLE.withDescription( "wait-for-ready RPC is not supported on Subchannel.asChannel()"); private static final FailingClientTransport notReadyTransport = - new FailingClientTransport(NOT_READY_ERROR, RpcProgress.REFUSED); + new FailingClientTransport(NOT_READY_ERROR, RpcProgress.MISCARRIED); private final InternalSubchannel subchannel; private final Executor executor; private final ScheduledExecutorService deadlineCancellationExecutor; diff --git a/core/src/test/java/io/grpc/internal/JsonUtilTest.java b/core/src/test/java/io/grpc/internal/JsonUtilTest.java index 960800c97c0..a01f8868220 100644 --- a/core/src/test/java/io/grpc/internal/JsonUtilTest.java +++ b/core/src/test/java/io/grpc/internal/JsonUtilTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -130,4 +131,16 @@ public void getNumber() { assertThat(JsonUtil.getNumberAsInteger(map, "key_nonexistent")).isNull(); assertThat(JsonUtil.getNumberAsLong(map, "key_nonexistent")).isNull(); } + + @Test + public void getObject_mapExplicitNullValue() { + Map mapWithNullValue = Collections.singletonMap("key", null); + try { + JsonUtil.getObject(mapWithNullValue, "key"); + fail("ClassCastException expected"); + } catch (ClassCastException e) { + assertThat(e).hasMessageThat() + .isEqualTo("value 'null' for key 'key' in '{key=null}' is not object"); + } + } } diff --git a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java index 8b851573b21..025c4f23e80 100644 --- a/core/src/test/java/io/grpc/internal/RetriableStreamTest.java +++ b/core/src/test/java/io/grpc/internal/RetriableStreamTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.internal.ClientStreamListener.RpcProgress.DROPPED; +import static io.grpc.internal.ClientStreamListener.RpcProgress.MISCARRIED; import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; import static io.grpc.internal.ClientStreamListener.RpcProgress.REFUSED; import static io.grpc.internal.RetriableStream.GRPC_PREVIOUS_RPC_ATTEMPTS; @@ -1611,7 +1612,7 @@ public void throttleStream_Succeed() { } @Test - public void transparentRetry() { + public void transparentRetry_onlyOnceOnRefused() { ClientStream mockStream1 = mock(ClientStream.class); ClientStream mockStream2 = mock(ClientStream.class); ClientStream mockStream3 = mock(ClientStream.class); @@ -1661,6 +1662,72 @@ public void transparentRetry() { assertEquals(0, fakeClock.numPendingTasks()); } + @Test + public void transparentRetry_unlimitedTimesOnMiscarried() { + ClientStream mockStream1 = mock(ClientStream.class); + ClientStream mockStream2 = mock(ClientStream.class); + ClientStream mockStream3 = mock(ClientStream.class); + InOrder inOrder = inOrder( + retriableStreamRecorder, + mockStream1, mockStream2, mockStream3); + + // start + doReturn(mockStream1).when(retriableStreamRecorder).newSubstream(0); + retriableStream.start(masterListener); + + inOrder.verify(retriableStreamRecorder).newSubstream(0); + ArgumentCaptor sublistenerCaptor1 = + ArgumentCaptor.forClass(ClientStreamListener.class); + inOrder.verify(mockStream1).start(sublistenerCaptor1.capture()); + inOrder.verify(mockStream1).isReady(); + inOrder.verifyNoMoreInteractions(); + + // transparent retry + doReturn(mockStream2).when(retriableStreamRecorder).newSubstream(0); + sublistenerCaptor1.getValue() + .closed(Status.fromCode(NON_RETRIABLE_STATUS_CODE), MISCARRIED, new Metadata()); + + inOrder.verify(retriableStreamRecorder).newSubstream(0); + ArgumentCaptor sublistenerCaptor2 = + ArgumentCaptor.forClass(ClientStreamListener.class); + inOrder.verify(mockStream2).start(sublistenerCaptor2.capture()); + inOrder.verify(mockStream2).isReady(); + inOrder.verifyNoMoreInteractions(); + verify(retriableStreamRecorder, never()).postCommit(); + assertEquals(0, fakeClock.numPendingTasks()); + + // more transparent retry + doReturn(mockStream3).when(retriableStreamRecorder).newSubstream(0); + sublistenerCaptor2.getValue() + .closed(Status.fromCode(NON_RETRIABLE_STATUS_CODE), MISCARRIED, new Metadata()); + + inOrder.verify(retriableStreamRecorder).newSubstream(0); + ArgumentCaptor sublistenerCaptor3 = + ArgumentCaptor.forClass(ClientStreamListener.class); + inOrder.verify(mockStream3).start(sublistenerCaptor3.capture()); + inOrder.verify(mockStream3).isReady(); + inOrder.verifyNoMoreInteractions(); + verify(retriableStreamRecorder, never()).postCommit(); + assertEquals(0, fakeClock.numPendingTasks()); + + ArgumentCaptor sublistenerCaptor = sublistenerCaptor3; + for (int i = 0; i < 9999; i++) { + ClientStream mockStream = mock(ClientStream.class); + doReturn(mockStream).when(retriableStreamRecorder).newSubstream(0); + sublistenerCaptor.getValue() + .closed(Status.fromCode(NON_RETRIABLE_STATUS_CODE), MISCARRIED, new Metadata()); + if (i == 9998) { + verify(retriableStreamRecorder).postCommit(); + verify(masterListener) + .closed(any(Status.class), any(RpcProgress.class), any(Metadata.class)); + } else { + verify(retriableStreamRecorder, never()).postCommit(); + sublistenerCaptor = ArgumentCaptor.forClass(ClientStreamListener.class); + verify(mockStream).start(sublistenerCaptor.capture()); + } + } + } + @Test public void normalRetry_thenNoTransparentRetry_butNormalRetry() { ClientStream mockStream1 = mock(ClientStream.class); diff --git a/cronet/README.md b/cronet/README.md index d53da56a8d7..d8861e5fc4b 100644 --- a/cronet/README.md +++ b/cronet/README.md @@ -26,7 +26,7 @@ In your app module's `build.gradle` file, include a dependency on both `grpc-cro Google Play Services Client Library for Cronet ``` -implementation 'io.grpc:grpc-cronet:1.43.1' +implementation 'io.grpc:grpc-cronet:1.45.0' implementation 'com.google.android.gms:play-services-cronet:16.0.0' ``` diff --git a/cronet/build.gradle b/cronet/build.gradle index e0a0b7b53a7..d66eaf3a182 100644 --- a/cronet/build.gradle +++ b/cronet/build.gradle @@ -70,12 +70,12 @@ task javadocs(type: Javadoc) { } task javadocJar(type: Jar, dependsOn: javadocs) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadocs.destinationDir } task sourcesJar(type: Jar) { - classifier = 'sources' + archiveClassifier = 'sources' from android.sourceSets.main.java.srcDirs } diff --git a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java index 7329ed08c5d..d44b716146e 100644 --- a/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java +++ b/cronet/src/main/java/io/grpc/cronet/CronetClientStream.java @@ -44,7 +44,7 @@ import java.lang.reflect.Method; import java.nio.Buffer; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -408,10 +408,10 @@ private void setGrpcHeaders(BidirectionalStream.Builder builder) { // String and byte array. byte[][] serializedHeaders = TransportFrameUtil.toHttp2Headers(headers); for (int i = 0; i < serializedHeaders.length; i += 2) { - String key = new String(serializedHeaders[i], StandardCharsets.UTF_8); + String key = new String(serializedHeaders[i], Charset.forName("UTF-8")); // TODO(ericgribkoff): log an error or throw an exception if (isApplicationHeader(key)) { - String value = new String(serializedHeaders[i + 1], StandardCharsets.UTF_8); + String value = new String(serializedHeaders[i + 1], Charset.forName("UTF-8")); builder.addHeader(key, value); } } @@ -588,8 +588,8 @@ private void reportHeaders(List> headers, boolean endO byte[][] headerValues = new byte[headerList.size()][]; for (int i = 0; i < headerList.size(); i += 2) { - headerValues[i] = headerList.get(i).getBytes(StandardCharsets.UTF_8); - headerValues[i + 1] = headerList.get(i + 1).getBytes(StandardCharsets.UTF_8); + headerValues[i] = headerList.get(i).getBytes(Charset.forName("UTF-8")); + headerValues[i + 1] = headerList.get(i + 1).getBytes(Charset.forName("UTF-8")); } Metadata metadata = InternalMetadata.newMetadata(TransportFrameUtil.toRawSerializedHeaders(headerValues)); diff --git a/cronet/src/test/java/io/grpc/cronet/CronetClientStreamTest.java b/cronet/src/test/java/io/grpc/cronet/CronetClientStreamTest.java index 9b63601d717..1d17dfe9be5 100644 --- a/cronet/src/test/java/io/grpc/cronet/CronetClientStreamTest.java +++ b/cronet/src/test/java/io/grpc/cronet/CronetClientStreamTest.java @@ -46,7 +46,7 @@ import java.io.ByteArrayInputStream; import java.nio.Buffer; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -168,7 +168,7 @@ public void write() { for (int i = 0; i < 5; ++i) { requests[i] = new String("request" + String.valueOf(i)); buffers[i] = allocator.allocate(requests[i].length()); - buffers[i].write(requests[i].getBytes(StandardCharsets.UTF_8), 0, requests[i].length()); + buffers[i].write(requests[i].getBytes(Charset.forName("UTF-8")), 0, requests[i].length()); // The 3rd and 5th writeFrame calls have flush=true. clientStream.abstractClientStreamSink().writeFrame(buffers[i], false, i == 2 || i == 4, 1); } @@ -245,7 +245,7 @@ public void read() { verify(cronetStream, times(0)).read(isA(ByteBuffer.class)); UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); verify(cronetStream, times(1)).read(isA(ByteBuffer.class)); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class); @@ -261,7 +261,7 @@ public void read() { callback.onReadCompleted( cronetStream, info, - createMessageFrame(new String("response1").getBytes(StandardCharsets.UTF_8)), + createMessageFrame(new String("response1").getBytes(Charset.forName("UTF-8"))), false); // Haven't request any message, so no callback is called here. verify(clientListener, times(0)).messagesAvailable(isA(MessageProducer.class)); @@ -293,7 +293,7 @@ public void streamSucceeded() { CronetWritableBufferAllocator allocator = new CronetWritableBufferAllocator(); String request = new String("request"); WritableBuffer writableBuffer = allocator.allocate(request.length()); - writableBuffer.write(request.getBytes(StandardCharsets.UTF_8), 0, request.length()); + writableBuffer.write(request.getBytes(Charset.forName("UTF-8")), 0, request.length()); clientStream.abstractClientStreamSink().writeFrame(writableBuffer, false, true, 1); ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); verify(cronetStream, times(1)).write(bufferCaptor.capture(), isA(Boolean.class)); @@ -305,14 +305,14 @@ public void streamSucceeded() { clientStream.request(2); UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); verify(cronetStream, times(1)).read(isA(ByteBuffer.class)); // Receive one message callback.onReadCompleted( cronetStream, info, - createMessageFrame(new String("response").getBytes(StandardCharsets.UTF_8)), + createMessageFrame(new String("response").getBytes(Charset.forName("UTF-8"))), false); verify(clientListener, times(1)).messagesAvailable(isA(MessageProducer.class)); verify(cronetStream, times(2)).read(isA(ByteBuffer.class)); @@ -363,7 +363,7 @@ public void streamSucceededWithGrpcError() { clientStream.request(2); UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); verify(cronetStream, times(1)).read(isA(ByteBuffer.class)); @@ -418,7 +418,7 @@ public void streamFailedAfterResponseHeaderReceived() { // Receive response header UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); CronetException exception = mock(CronetException.class); @@ -446,7 +446,7 @@ public void streamFailedAfterTrailerReceived() { // Receive response header UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); // Report trailer but not endOfStream. @@ -478,7 +478,7 @@ public void streamFailedAfterTrailerAndEndOfStreamReceived() { // Receive response header UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); // Report trailer and endOfStream @@ -531,7 +531,7 @@ public void reportTrailersWhenTrailersReceivedBeforeReadClosed() { callback.onStreamReady(cronetStream); UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); // Receive trailer first ((CronetClientStream.BidirectionalStreamCallback) callback) @@ -560,7 +560,7 @@ public void reportTrailersWhenTrailersReceivedAfterReadClosed() { callback.onStreamReady(cronetStream); UrlResponseInfo info = new UrlResponseInfoImpl( - new ArrayList(), 200, "", responseHeader("200"), false, "", ""); + new ArrayList<>(), 200, "", responseHeader("200"), false, "", "", 0); callback.onResponseHeadersReceived(cronetStream, info); // Receive cronet's endOfStream callback.onReadCompleted(cronetStream, null, ByteBuffer.allocate(0), true); @@ -688,7 +688,7 @@ public void getUnaryRequest() { .newBidirectionalStreamBuilder( isA(String.class), isA(BidirectionalStream.Callback.class), isA(Executor.class)); - byte[] msg = "request".getBytes(StandardCharsets.UTF_8); + byte[] msg = "request".getBytes(Charset.forName("UTF-8")); stream.writeMessage(new ByteArrayInputStream(msg)); // We still haven't built the stream or sent anything. verify(cronetStream, times(0)).write(isA(ByteBuffer.class), isA(Boolean.class)); diff --git a/documentation/android-channel-builder.md b/documentation/android-channel-builder.md index 09c0bef179a..2fb3f11d4dc 100644 --- a/documentation/android-channel-builder.md +++ b/documentation/android-channel-builder.md @@ -36,8 +36,8 @@ In your `build.gradle` file, include a dependency on both `grpc-android` and `grpc-okhttp`: ``` -implementation 'io.grpc:grpc-android:1.43.1' -implementation 'io.grpc:grpc-okhttp:1.43.1' +implementation 'io.grpc:grpc-android:1.45.0' +implementation 'io.grpc:grpc-okhttp:1.45.0' ``` You also need permission to access the device's network state in your diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index a71611df7a5..0d190b6f55c 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -34,7 +34,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.19.2' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -54,12 +54,12 @@ dependencies { implementation 'com.android.support:appcompat-v7:27.0.2' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.45.0' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.12' testImplementation 'com.google.truth:truth:1.0.1' - testImplementation 'io.grpc:grpc-testing:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + testImplementation 'io.grpc:grpc-testing:1.45.0' // CURRENT_GRPC_VERSION } diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index cc84e9bcc5c..d4ba7520acc 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.19.2' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'com.android.support:appcompat-v7:27.0.2' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.45.0' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 5a43416a5fb..463f0b4ee28 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -32,7 +32,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.19.2' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -52,8 +52,8 @@ dependencies { implementation 'com.android.support:appcompat-v7:27.0.2' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.45.0' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index ad74597ed27..06e88663bc7 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -33,7 +33,7 @@ android { protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.19.2' } plugins { - grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' // CURRENT_GRPC_VERSION } } generateProtoTasks { @@ -53,8 +53,8 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' // You need to build grpc-java to obtain these libraries below. - implementation 'io.grpc:grpc-okhttp:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-protobuf-lite:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'io.grpc:grpc-stub:1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-okhttp:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-protobuf-lite:1.45.0' // CURRENT_GRPC_VERSION + implementation 'io.grpc:grpc-stub:1.45.0' // CURRENT_GRPC_VERSION implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/build.gradle b/examples/build.gradle index 45648d4af85..76bd1082811 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -14,15 +14,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protobufVersion = '3.19.2' def protocVersion = protobufVersion diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index de4e11fd916..887357c0334 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -15,15 +15,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protocVersion = '3.19.2' dependencies { diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index fdde6e01aff..cd617c03681 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -15,15 +15,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protobufVersion = '3.19.2' def protocVersion = protobufVersion diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 2ccd6654757..273bf4fe640 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,13 +6,13 @@ jar - 1.44.0-SNAPSHOT + 1.45.0 example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.44.0-SNAPSHOT + 1.45.0 3.19.2 1.7 diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index f40528e56ea..bc04e0da888 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -13,15 +13,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protobufVersion = '3.19.2' dependencies { diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 31445d405de..01b58578fba 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,13 +6,13 @@ jar - 1.44.0-SNAPSHOT + 1.45.0 example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.44.0-SNAPSHOT + 1.45.0 3.19.2 1.7 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index 8f962a2ffc8..b1ba3b84662 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -14,15 +14,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protobufVersion = '3.19.2' def protocVersion = protobufVersion diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index f026be38467..7724d7309b0 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,13 +7,13 @@ jar - 1.44.0-SNAPSHOT + 1.45.0 example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.44.0-SNAPSHOT + 1.45.0 3.19.2 3.19.2 diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index b3b8372e1c4..07656db6b59 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -15,15 +15,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def protocVersion = '3.19.2' dependencies { diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 193e05fd812..fb6fe00a0c1 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,13 +6,13 @@ jar - 1.44.0-SNAPSHOT + 1.45.0 example-tls https://github.com/grpc/grpc-java UTF-8 - 1.44.0-SNAPSHOT + 1.45.0 3.19.2 2.0.34.Final diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 2d35f5c40d9..880f8a0014f 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -14,15 +14,15 @@ repositories { mavenLocal() } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 // IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you // are looking at a tagged version of the example and not "master"! // Feel free to delete the comment at the next line. It is just for safely // updating the version in our release process. -def grpcVersion = '1.44.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def grpcVersion = '1.45.0' // CURRENT_GRPC_VERSION def nettyTcNativeVersion = '2.0.31.Final' def protocVersion = '3.19.2' diff --git a/examples/pom.xml b/examples/pom.xml index 525fb616fa0..9a140e482f2 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,13 +6,13 @@ jar - 1.44.0-SNAPSHOT + 1.45.0 examples https://github.com/grpc/grpc-java UTF-8 - 1.44.0-SNAPSHOT + 1.45.0 3.19.2 3.19.2 diff --git a/examples/src/main/java/io/grpc/examples/advanced/JsonMarshaller.java b/examples/src/main/java/io/grpc/examples/advanced/JsonMarshaller.java index fbe3fa50afc..8b3068cc009 100644 --- a/examples/src/main/java/io/grpc/examples/advanced/JsonMarshaller.java +++ b/examples/src/main/java/io/grpc/examples/advanced/JsonMarshaller.java @@ -30,7 +30,6 @@ import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; /** * A {@link Marshaller} for JSON. This marshals in the Protobuf 3 format described here: @@ -59,7 +58,7 @@ public static Marshaller jsonMarshaller(final T defaultIn public static Marshaller jsonMarshaller( final T defaultInstance, final Parser parser, final Printer printer) { - final Charset charset = StandardCharsets.UTF_8; + final Charset charset = Charset.forName("UTF-8"); return new Marshaller() { @Override diff --git a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java index 0794674091f..6e49492da06 100644 --- a/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java +++ b/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java @@ -22,7 +22,7 @@ import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.List; /** @@ -58,7 +58,7 @@ public static URL getDefaultFeaturesFile() { public static List parseFeatures(URL file) throws IOException { InputStream input = file.openStream(); try { - Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8); + Reader reader = new InputStreamReader(input, Charset.forName("UTF-8")); try { FeatureDatabase.Builder database = FeatureDatabase.newBuilder(); JsonFormat.parser().merge(reader, database); diff --git a/googleapis/build.gradle b/googleapis/build.gradle new file mode 100644 index 00000000000..69449e8a02b --- /dev/null +++ b/googleapis/build.gradle @@ -0,0 +1,36 @@ +plugins { + id "java-library" + id "maven-publish" + + id "ru.vyarus.animalsniffer" +} + +description = 'gRPC: googleapis' + +dependencies { + api project(':grpc-api') + implementation project(':grpc-alts'), + project(':grpc-core'), + project(':grpc-xds'), + libraries.guava + testImplementation project(':grpc-core').sourceSets.test.output + + signature "org.codehaus.mojo.signature:java17:1.0@signature" +} + +publishing { + publications { + maven(MavenPublication) { + pom { + withXml { + // Since internal APIs are used, pin the version. + asNode().dependencies.'*'.findAll() { dep -> + dep.artifactId.text() in ['grpc-alts', 'grpc-xds'] + }.each() { core -> + core.version*.value = "[" + core.version.text() + "]" + } + } + } + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolver.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java similarity index 93% rename from xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolver.java rename to googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java index 2845d0a00e8..fb330b82bde 100644 --- a/xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolver.java +++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.xds; +package io.grpc.googleapis; import static com.google.common.base.Preconditions.checkNotNull; @@ -32,7 +32,6 @@ import io.grpc.internal.GrpcUtil; import io.grpc.internal.SharedResourceHolder; import io.grpc.internal.SharedResourceHolder.Resource; -import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; @@ -40,6 +39,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; @@ -70,7 +70,7 @@ final class GoogleCloudToProdNameResolver extends NameResolver { private final String authority; private final SynchronizationContext syncContext; private final Resource executorResource; - private final XdsClientPoolFactory xdsClientPoolFactory; + private final BootstrapSetter bootstrapSetter; private final NameResolver delegate; private final Random rand; private final boolean usingExecutorResource; @@ -84,17 +84,16 @@ final class GoogleCloudToProdNameResolver extends NameResolver { private boolean shutdown; GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource executorResource, - XdsClientPoolFactory xdsClientPoolFactory) { - this(targetUri, args, executorResource, new Random(), xdsClientPoolFactory, + BootstrapSetter bootstrapSetter) { + this(targetUri, args, executorResource, new Random(), bootstrapSetter, NameResolverRegistry.getDefaultRegistry().asFactory()); } @VisibleForTesting GoogleCloudToProdNameResolver(URI targetUri, Args args, Resource executorResource, - Random rand, XdsClientPoolFactory xdsClientPoolFactory, - NameResolver.Factory nameResolverFactory) { + Random rand, BootstrapSetter bootstrapSetter, NameResolver.Factory nameResolverFactory) { this.executorResource = checkNotNull(executorResource, "executorResource"); - this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory"); + this.bootstrapSetter = checkNotNull(bootstrapSetter, "bootstrapSetter"); this.rand = checkNotNull(rand, "rand"); String targetPath = checkNotNull(checkNotNull(targetUri, "targetUri").getPath(), "targetPath"); Preconditions.checkArgument( @@ -159,7 +158,7 @@ public void run() { @Override public void run() { if (!shutdown && finalRawBootstrap != null) { - xdsClientPoolFactory.setBootstrapOverride(finalRawBootstrap); + bootstrapSetter.setBootstrap(finalRawBootstrap); delegate.start(listener); succeeded = true; } @@ -175,7 +174,7 @@ public void run() { private ImmutableMap generateBootstrap(String zone, boolean supportIpv6) { ImmutableMap.Builder nodeBuilder = ImmutableMap.builder(); - nodeBuilder.put("id", "C2P-" + rand.nextInt()); + nodeBuilder.put("id", "C2P-" + (rand.nextInt() & Integer.MAX_VALUE)); if (!zone.isEmpty()) { nodeBuilder.put("locality", ImmutableMap.of("zone", zone)); } @@ -284,4 +283,8 @@ public HttpURLConnection createConnection(String url) throws IOException { interface HttpConnectionProvider { HttpURLConnection createConnection(String url) throws IOException; } + + public interface BootstrapSetter { + void setBootstrap(Map bootstrap); + } } diff --git a/xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolverProvider.java b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java similarity index 75% rename from xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolverProvider.java rename to googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java index e7f9cb45ff8..ac39ab1e625 100644 --- a/xds/src/main/java/io/grpc/xds/GoogleCloudToProdNameResolverProvider.java +++ b/googleapis/src/main/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProvider.java @@ -14,14 +14,16 @@ * limitations under the License. */ -package io.grpc.xds; +package io.grpc.googleapis; import io.grpc.Internal; import io.grpc.NameResolver; import io.grpc.NameResolver.Args; import io.grpc.NameResolverProvider; import io.grpc.internal.GrpcUtil; +import io.grpc.xds.InternalSharedXdsClientPoolProvider; import java.net.URI; +import java.util.Map; /** * A provider for {@link GoogleCloudToProdNameResolver}. @@ -36,7 +38,7 @@ public NameResolver newNameResolver(URI targetUri, Args args) { if (SCHEME.equals(targetUri.getScheme())) { return new GoogleCloudToProdNameResolver( targetUri, args, GrpcUtil.SHARED_CHANNEL_EXECUTOR, - SharedXdsClientPoolProvider.getDefaultProvider()); + new SharedXdsClientPoolProviderBootstrapSetter()); } return null; } @@ -55,4 +57,12 @@ protected boolean isAvailable() { protected int priority() { return 4; } + + private static final class SharedXdsClientPoolProviderBootstrapSetter + implements GoogleCloudToProdNameResolver.BootstrapSetter { + @Override + public void setBootstrap(Map bootstrap) { + InternalSharedXdsClientPoolProvider.setDefaultProviderBootstrapOverride(bootstrap); + } + } } diff --git a/googleapis/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/googleapis/src/main/resources/META-INF/services/io.grpc.NameResolverProvider new file mode 100644 index 00000000000..553e5409180 --- /dev/null +++ b/googleapis/src/main/resources/META-INF/services/io.grpc.NameResolverProvider @@ -0,0 +1 @@ +io.grpc.googleapis.GoogleCloudToProdNameResolverProvider diff --git a/xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverProviderTest.java b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProviderTest.java similarity index 98% rename from xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverProviderTest.java rename to googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProviderTest.java index cc6621028c8..e6ef1696dc0 100644 --- a/xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverProviderTest.java +++ b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverProviderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.xds; +package io.grpc.googleapis; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; diff --git a/xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverTest.java b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java similarity index 89% rename from xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverTest.java rename to googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java index 58be7108405..562798c0183 100644 --- a/xds/src/test/java/io/grpc/xds/GoogleCloudToProdNameResolverTest.java +++ b/googleapis/src/test/java/io/grpc/googleapis/GoogleCloudToProdNameResolverTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.grpc.xds; +package io.grpc.googleapis; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; @@ -33,12 +33,10 @@ import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.SynchronizationContext; +import io.grpc.googleapis.GoogleCloudToProdNameResolver.HttpConnectionProvider; import io.grpc.internal.FakeClock; import io.grpc.internal.GrpcUtil; -import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourceHolder.Resource; -import io.grpc.xds.GoogleCloudToProdNameResolver.HttpConnectionProvider; -import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.HttpURLConnection; @@ -50,7 +48,6 @@ import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nullable; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -88,7 +85,7 @@ public void uncaughtException(Thread t, Throwable e) { .setChannelLogger(mock(ChannelLogger.class)) .build(); private final FakeClock fakeExecutor = new FakeClock(); - private final FakeXdsClientPoolFactory fakeXdsClientPoolFactory = new FakeXdsClientPoolFactory(); + private final FakeBootstrapSetter fakeBootstrapSetter = new FakeBootstrapSetter(); private final Resource fakeExecutorResource = new Resource() { @Override public Executor create() { @@ -104,8 +101,7 @@ public void close(Executor instance) {} @Mock private NameResolver.Listener2 mockListener; - @Mock - private Random mockRandom; + private Random random = new Random(1); @Captor private ArgumentCaptor errorCaptor; private boolean originalIsOnGcp; @@ -114,7 +110,6 @@ public void close(Executor instance) {} @Before public void setUp() { - when(mockRandom.nextInt()).thenReturn(123456789); nsRegistry.register(new FakeNsProvider("dns")); nsRegistry.register(new FakeNsProvider("xds")); originalIsOnGcp = GoogleCloudToProdNameResolver.isOnGcp; @@ -146,7 +141,7 @@ public HttpURLConnection createConnection(String url) throws IOException { } }; resolver = new GoogleCloudToProdNameResolver( - TARGET_URI, args, fakeExecutorResource, mockRandom, fakeXdsClientPoolFactory, + TARGET_URI, args, fakeExecutorResource, random, fakeBootstrapSetter, nsRegistry.asFactory()); resolver.setHttpConnectionProvider(httpConnections); } @@ -180,10 +175,10 @@ public void onGcpAndNoProvidedBootstrapDelegateToXds() { fakeExecutor.runDueTasks(); assertThat(delegatedResolver.keySet()).containsExactly("xds"); verify(Iterables.getOnlyElement(delegatedResolver.values())).start(mockListener); - Map bootstrap = fakeXdsClientPoolFactory.bootstrapRef.get(); + Map bootstrap = fakeBootstrapSetter.bootstrapRef.get(); Map node = (Map) bootstrap.get("node"); assertThat(node).containsExactly( - "id", "C2P-123456789", + "id", "C2P-991614323", "locality", ImmutableMap.of("zone", ZONE), "metadata", ImmutableMap.of("TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE", true)); Map server = Iterables.getOnlyElement( @@ -248,23 +243,13 @@ public String getDefaultScheme() { } } - private static final class FakeXdsClientPoolFactory implements XdsClientPoolFactory { + private static final class FakeBootstrapSetter + implements GoogleCloudToProdNameResolver.BootstrapSetter { private final AtomicReference> bootstrapRef = new AtomicReference<>(); @Override - public void setBootstrapOverride(Map bootstrap) { + public void setBootstrap(Map bootstrap) { bootstrapRef.set(bootstrap); } - - @Override - @Nullable - public ObjectPool get() { - throw new UnsupportedOperationException("Should not be called"); - } - - @Override - public ObjectPool getOrCreate() { - throw new UnsupportedOperationException("Should not be called"); - } } } diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index 8ac50419d5f..914db12e5a8 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -40,7 +40,6 @@ import java.io.File; import java.io.FileInputStream; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -51,7 +50,7 @@ */ public class TestServiceClient { - private static final Charset UTF_8 = StandardCharsets.UTF_8; + private static final Charset UTF_8 = Charset.forName("UTF-8"); /** * The main application allowing this client to be launched from the command line. diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java index fdd3ac63011..208eb40c438 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/CompressionTest.java @@ -52,7 +52,7 @@ import io.grpc.testing.integration.Messages.SimpleResponse; import io.grpc.testing.integration.TestServiceGrpc.TestServiceBlockingStub; import io.grpc.testing.integration.TransportCompressionTest.Fzip; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -312,6 +312,6 @@ public void onHeaders(Metadata headers) { } private static void assertEqualsString(String expected, byte[] actual) { - assertEquals(expected, new String(actual, StandardCharsets.US_ASCII)); + assertEquals(expected, new String(actual, Charset.forName("US-ASCII"))); } } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/NettyFlowControlTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/NettyFlowControlTest.java index 4d7c8bdbca7..c86bd8070a0 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/NettyFlowControlTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/NettyFlowControlTest.java @@ -39,17 +39,13 @@ import io.netty.channel.ChannelHandler; import io.netty.handler.codec.http2.Http2Stream; import io.netty.util.AsciiString; -import io.netty.util.concurrent.DefaultThreadFactory; import java.io.IOException; import java.net.InetSocketAddress; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -80,16 +76,6 @@ public class NettyFlowControlTest { private int proxyPort; private int serverPort; - private static final ThreadPoolExecutor executor = - new ThreadPoolExecutor(1, 10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(), - new DefaultThreadFactory("flowcontrol-test-pool", true)); - - - @AfterClass - public static void shutDownTests() { - executor.shutdown(); - } - @Before public void initTest() { startServer(REGULAR_WINDOW); diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java index 80cf83b0939..229d873571a 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/RetryTest.java @@ -74,7 +74,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -459,33 +458,41 @@ public long nanoTime() { assertRetryStatsRecorded(0, 0, 0); } - @Ignore("flaky because old transportReportStatus() is not completely migrated yet") @Test public void transparentRetryStatsRecorded() throws Exception { startNewServer(); createNewChannel(); - final AtomicBoolean transparentRetryTriggered = new AtomicBoolean(); + final AtomicBoolean originalAttemptFailed = new AtomicBoolean(); class TransparentRetryTriggeringTracer extends ClientStreamTracer { @Override public void streamCreated(Attributes transportAttrs, Metadata metadata) { - if (transparentRetryTriggered.get()) { + if (originalAttemptFailed.get()) { return; } + // Send GOAWAY from server. The client may either receive GOAWAY or create the underlying + // netty stream and write headers first, even we await server termination as below. + // In the latter case, we rerun the test. We can also call localServer.shutdown() to trigger + // GOAWAY, but it takes a lot longer time to gracefully shut down. localServer.shutdownNow(); + try { + localServer.awaitTermination(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AssertionError(e); + } } @Override public void streamClosed(Status status) { - if (transparentRetryTriggered.get()) { + if (originalAttemptFailed.get()) { return; } - transparentRetryTriggered.set(true); + originalAttemptFailed.set(true); try { startNewServer(); channel.resetConnectBackoff(); - channel.getState(true); } catch (Exception e) { throw new AssertionError("local server can not be restarted", e); } @@ -502,13 +509,28 @@ public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata header CallOptions callOptions = CallOptions.DEFAULT .withWaitForReady() .withStreamTracerFactory(new TransparentRetryTracerFactory()); - ClientCall call = channel.newCall(clientStreamingMethod, callOptions); - call.start(mockCallListener, new Metadata()); - assertRpcStartedRecorded(); - assertRpcStatusRecorded(Code.UNAVAILABLE, 0, 0); - assertRpcStartedRecorded(); - call.cancel("cancel", null); - assertRpcStatusRecorded(Code.CANCELLED, 0, 0); + while (true) { + ClientCall call = channel.newCall(clientStreamingMethod, callOptions); + call.start(mockCallListener, new Metadata()); + assertRpcStartedRecorded(); // original attempt + MetricsRecord record = clientStatsRecorder.pollRecord(5, SECONDS); + assertThat(record.getMetricAsLongOrFail(DeprecatedCensusConstants.RPC_CLIENT_FINISHED_COUNT)) + .isEqualTo(1); + TagValue statusTag = record.tags.get(RpcMeasureConstants.GRPC_CLIENT_STATUS); + if (statusTag.asString().equals(Code.UNAVAILABLE.toString())) { + break; + } else { + // Due to race condition, GOAWAY is not received/processed before the stream is closed due + // to connection error. Rerun the test. + assertThat(statusTag.asString()).isEqualTo(Code.UNKNOWN.toString()); + assertRetryStatsRecorded(0, 0, 0); + originalAttemptFailed.set(false); + } + } + assertRpcStartedRecorded(); // retry attempt + ServerCall serverCall = serverCalls.poll(5, SECONDS); + serverCall.close(Status.INVALID_ARGUMENT, new Metadata()); + assertRpcStatusRecorded(Code.INVALID_ARGUMENT, 0, 0); assertRetryStatsRecorded(0, 1, 0); } } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TrafficControlProxy.java b/interop-testing/src/test/java/io/grpc/testing/integration/TrafficControlProxy.java index 19f22cb03f1..a93d51628e4 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TrafficControlProxy.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TrafficControlProxy.java @@ -51,7 +51,7 @@ public final class TrafficControlProxy { private Socket serverSock; private Socket clientSock; private final ThreadPoolExecutor executor = - new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(), + new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(), new DefaultThreadFactory("proxy-pool", true)); /** diff --git a/netty/shaded/build.gradle b/netty/shaded/build.gradle index 6b1dad644d1..e2409a86d99 100644 --- a/netty/shaded/build.gradle +++ b/netty/shaded/build.gradle @@ -18,7 +18,8 @@ sourceSets { testShadow {} } dependencies { implementation project(':grpc-netty') runtimeOnly libraries.netty_tcnative, - libraries.netty_epoll + libraries.netty_epoll, + libraries.netty_epoll_arm64 testShadowImplementation files(shadowJar), project(':grpc-testing-proto'), project(':grpc-testing'), @@ -29,12 +30,12 @@ dependencies { } jar { - // Must use a different classifier to avoid conflicting with shadowJar - classifier = 'original' + // Must use a different archiveClassifier to avoid conflicting with shadowJar + archiveClassifier = 'original' } shadowJar { - classifier = null + archiveClassifier = null dependencies { include(project(':grpc-netty')) include(dependency('io.netty:')) diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java index 459c313eded..25f4f9232cf 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java @@ -58,6 +58,7 @@ protected GrpcHttp2ConnectionHandler( * @deprecated Use the two argument method instead. */ @Deprecated + @SuppressWarnings("InlineMeSuggester") // the caller should consider providing securityInfo public void handleProtocolNegotiationCompleted(Attributes attrs) { handleProtocolNegotiationCompleted(attrs, /*securityInfo=*/ null); } diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 17801afa382..ca029fe7cfc 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -24,6 +24,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.InlineMe; import io.grpc.Attributes; import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; @@ -393,6 +394,7 @@ public NettyChannelBuilder flowControlWindow(int flowControlWindow) { * @deprecated Use {@link #maxInboundMetadataSize} instead */ @Deprecated + @InlineMe(replacement = "this.maxInboundMetadataSize(maxHeaderListSize)") public NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) { return maxInboundMetadataSize(maxHeaderListSize); } diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 6dde8c825ef..80d11e54859 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -535,7 +535,7 @@ private void createStream(CreateStreamCommand command, ChannelPromise promise) // The connection is going away (it is really the GOAWAY case), // just terminate the stream now. command.stream().transportReportStatus( - lifecycleManager.getShutdownStatus(), RpcProgress.REFUSED, true, new Metadata()); + lifecycleManager.getShutdownStatus(), RpcProgress.MISCARRIED, true, new Metadata()); promise.setFailure(lifecycleManager.getShutdownThrowable()); return; } @@ -576,7 +576,7 @@ private void createStream(CreateStreamCommand command, ChannelPromise promise) // This should only be reachable during onGoAwayReceived, as otherwise // getShutdownThrowable() != null command.stream().setNonExistent(); - command.stream().transportReportStatus(s, RpcProgress.REFUSED, true, new Metadata()); + command.stream().transportReportStatus(s, RpcProgress.MISCARRIED, true, new Metadata()); promise.setFailure(s.asRuntimeException()); return; } @@ -635,18 +635,24 @@ public void operationComplete(ChannelFuture future) throws Exception { // Just forward on the success status to the original promise. promise.setSuccess(); } else { - final Throwable cause = future.cause(); + Throwable cause = future.cause(); if (cause instanceof StreamBufferingEncoder.Http2GoAwayException) { StreamBufferingEncoder.Http2GoAwayException e = (StreamBufferingEncoder.Http2GoAwayException) cause; Status status = statusFromH2Error( Status.Code.UNAVAILABLE, "GOAWAY closed buffered stream", e.errorCode(), e.debugData()); - stream.transportReportStatus(status, RpcProgress.REFUSED, true, new Metadata()); - promise.setFailure(status.asRuntimeException()); - } else { - promise.setFailure(cause); + cause = status.asRuntimeException(); + stream.transportReportStatus(status, RpcProgress.MISCARRIED, true, new Metadata()); + } else if (cause instanceof StreamBufferingEncoder.Http2ChannelClosedException) { + Status status = lifecycleManager.getShutdownStatus(); + if (status == null) { + status = Status.UNAVAILABLE.withCause(cause) + .withDescription("Connection closed while stream is buffered"); + } + stream.transportReportStatus(status, RpcProgress.MISCARRIED, true, new Metadata()); } + promise.setFailure(cause); } } }); diff --git a/netty/src/main/java/io/grpc/netty/NettyClientStream.java b/netty/src/main/java/io/grpc/netty/NettyClientStream.java index 0e7e69635a8..9c6c12cc0f6 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientStream.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientStream.java @@ -31,6 +31,7 @@ import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.internal.AbstractClientStream; +import io.grpc.internal.ClientStreamListener.RpcProgress; import io.grpc.internal.Http2ClientStreamTransportState; import io.grpc.internal.StatsTraceContext; import io.grpc.internal.TransportTracer; @@ -152,12 +153,18 @@ public void operationComplete(ChannelFuture future) throws Exception { // Stream creation failed. Close the stream if not already closed. // When the channel is shutdown, the lifecycle manager has a better view of the failure, // especially before negotiation completes (because the negotiator commonly doesn't - // receive the execeptionCaught because NettyClientHandler does not propagate it). + // receive the exceptionCaught because NettyClientHandler does not propagate it). Status s = transportState().handler.getLifecycleManager().getShutdownStatus(); if (s == null) { s = transportState().statusFromFailedFuture(future); } - transportState().transportReportStatus(s, true, new Metadata()); + if (transportState().isNonExistent()) { + transportState().transportReportStatus( + s, RpcProgress.MISCARRIED, true, new Metadata()); + } else { + transportState().transportReportStatus( + s, RpcProgress.PROCESSED, true, new Metadata()); + } } } }; @@ -268,7 +275,7 @@ void setNonExistent() { } boolean isNonExistent() { - return this.id == NON_EXISTENT_ID; + return this.id == NON_EXISTENT_ID || this.id == 0; } /** diff --git a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java index 9b67cd21f19..ff5553eb116 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java @@ -26,6 +26,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.errorprone.annotations.InlineMe; import io.grpc.Attributes; import io.grpc.ExperimentalApi; import io.grpc.Internal; @@ -437,6 +438,7 @@ public NettyServerBuilder flowControlWindow(int flowControlWindow) { * future release. */ @Deprecated + @InlineMe(replacement = "this.maxInboundMessageSize(maxMessageSize)") public NettyServerBuilder maxMessageSize(int maxMessageSize) { return maxInboundMessageSize(maxMessageSize); } @@ -458,6 +460,7 @@ public NettyServerBuilder maxInboundMessageSize(int bytes) { * @deprecated Use {@link #maxInboundMetadataSize} instead */ @Deprecated + @InlineMe(replacement = "this.maxInboundMetadataSize(maxHeaderListSize)") public NettyServerBuilder maxHeaderListSize(int maxHeaderListSize) { return maxInboundMetadataSize(maxHeaderListSize); } diff --git a/netty/src/test/java/io/grpc/netty/KeepAliveEnforcerTest.java b/netty/src/test/java/io/grpc/netty/KeepAliveEnforcerTest.java index 33c40390826..8dfeb990e2b 100644 --- a/netty/src/test/java/io/grpc/netty/KeepAliveEnforcerTest.java +++ b/netty/src/test/java/io/grpc/netty/KeepAliveEnforcerTest.java @@ -32,12 +32,12 @@ public class KeepAliveEnforcerTest { @Test(expected = IllegalArgumentException.class) public void negativeTime() { - new KeepAliveEnforcer(true, -1, TimeUnit.NANOSECONDS); + KeepAliveEnforcer unused = new KeepAliveEnforcer(true, -1, TimeUnit.NANOSECONDS); } @Test(expected = NullPointerException.class) public void nullTimeUnit() { - new KeepAliveEnforcer(true, 1, null); + KeepAliveEnforcer unused = new KeepAliveEnforcer(true, 1, null); } @Test diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index d0d48fe9b48..d47942858a3 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Charsets.UTF_8; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.internal.ClientStreamListener.RpcProgress.MISCARRIED; import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; import static io.grpc.internal.ClientStreamListener.RpcProgress.REFUSED; import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; @@ -378,7 +379,7 @@ public void receivedAbruptGoAwayShouldFailRacingQueuedStreamid() throws Exceptio channelRead(goAwayFrame(0, 8 /* Cancel */, Unpooled.copiedBuffer("this is a test", UTF_8))); ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); - verify(streamListener).closed(captor.capture(), same(REFUSED), + verify(streamListener).closed(captor.capture(), same(MISCARRIED), ArgumentMatchers.notNull()); assertEquals(Status.UNAVAILABLE.getCode(), captor.getValue().getCode()); assertEquals( @@ -471,6 +472,20 @@ public void receivedGoAwayShouldFailBufferedStreams() throws Exception { status.getDescription()); } + @Test + public void channelClosureShouldFailBufferedStreams() throws Exception { + receiveMaxConcurrentStreams(0); + + ChannelFuture future = enqueue(newCreateStreamCommand(grpcHeaders, streamTransportState)); + channel().pipeline().fireChannelInactive(); + + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); + verify(streamListener).closed(captor.capture(), same(MISCARRIED), ArgumentMatchers.notNull()); + assertEquals(Status.UNAVAILABLE.getCode(), captor.getValue().getCode()); + } + @Test public void receivedGoAwayShouldFailNewStreams() throws Exception { // Read a GOAWAY that indicates our stream was never processed by the server. diff --git a/observability/build.gradle b/observability/build.gradle new file mode 100644 index 00000000000..137e5f6978a --- /dev/null +++ b/observability/build.gradle @@ -0,0 +1,40 @@ +plugins { + id "java-library" + id "maven-publish" + + id "com.google.protobuf" + id "ru.vyarus.animalsniffer" +} + +description = "gRPC: Observability" +dependencies { + def cloudLoggingVersion = '3.6.1' + + api project(':grpc-api'), + project(':grpc-alts') + + implementation project(':grpc-protobuf'), + project(':grpc-stub'), + ('com.google.guava:guava:31.0.1-jre'), + ('com.google.errorprone:error_prone_annotations:2.11.0'), + ('com.google.auth:google-auth-library-credentials:1.4.0'), + ('org.checkerframework:checker-qual:3.20.0'), + ('com.google.auto.value:auto-value-annotations:1.9'), + ('com.google.http-client:google-http-client:1.41.0'), + ('com.google.http-client:google-http-client-gson:1.41.0'), + ('com.google.api.grpc:proto-google-common-protos:2.7.1'), + ("com.google.cloud:google-cloud-logging:${cloudLoggingVersion}") + + testImplementation project(':grpc-testing'), + project(':grpc-testing-proto'), + project(':grpc-netty-shaded') + testImplementation (libraries.guava_testlib) { + exclude group: 'junit', module: 'junit' + } + + signature "org.codehaus.mojo.signature:java18:1.0@signature" +} + +configureProtoCompilation() + +[publishMavenPublicationToMavenRepository]*.onlyIf { false } diff --git a/observability/src/main/java/io/grpc/observability/LoggingChannelProvider.java b/observability/src/main/java/io/grpc/observability/LoggingChannelProvider.java new file mode 100644 index 00000000000..5011068c176 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/LoggingChannelProvider.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.ChannelCredentials; +import io.grpc.InternalManagedChannelProvider; +import io.grpc.ManagedChannelBuilder; +import io.grpc.ManagedChannelProvider; +import io.grpc.ManagedChannelRegistry; +import io.grpc.observability.interceptors.InternalLoggingChannelInterceptor; + +/** A channel provider that injects logging interceptor. */ +final class LoggingChannelProvider extends ManagedChannelProvider { + private final ManagedChannelProvider prevProvider; + private final InternalLoggingChannelInterceptor.Factory clientInterceptorFactory; + + private static LoggingChannelProvider instance; + + private LoggingChannelProvider(InternalLoggingChannelInterceptor.Factory factory) { + prevProvider = ManagedChannelProvider.provider(); + clientInterceptorFactory = factory; + } + + static synchronized void init(InternalLoggingChannelInterceptor.Factory factory) { + if (instance != null) { + throw new IllegalStateException("LoggingChannelProvider already initialized!"); + } + instance = new LoggingChannelProvider(factory); + ManagedChannelRegistry.getDefaultRegistry().register(instance); + } + + static synchronized void finish() { + if (instance == null) { + throw new IllegalStateException("LoggingChannelProvider not initialized!"); + } + ManagedChannelRegistry.getDefaultRegistry().deregister(instance); + instance = null; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 6; + } + + private ManagedChannelBuilder addInterceptor(ManagedChannelBuilder builder) { + return builder.intercept(clientInterceptorFactory.create()); + } + + @Override + protected ManagedChannelBuilder builderForAddress(String name, int port) { + return addInterceptor( + InternalManagedChannelProvider.builderForAddress(prevProvider, name, port)); + } + + @Override + protected ManagedChannelBuilder builderForTarget(String target) { + return addInterceptor(InternalManagedChannelProvider.builderForTarget(prevProvider, target)); + } + + @Override + protected NewChannelBuilderResult newChannelBuilder(String target, ChannelCredentials creds) { + NewChannelBuilderResult result = InternalManagedChannelProvider.newChannelBuilder(prevProvider, + target, creds); + ManagedChannelBuilder builder = result.getChannelBuilder(); + if (builder != null) { + return NewChannelBuilderResult.channelBuilder( + addInterceptor(builder)); + } + checkNotNull(result.getError(), "Expected error to be set!"); + return result; + } +} diff --git a/observability/src/main/java/io/grpc/observability/LoggingServerProvider.java b/observability/src/main/java/io/grpc/observability/LoggingServerProvider.java new file mode 100644 index 00000000000..5277bcf572b --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/LoggingServerProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.InternalServerProvider; +import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; +import io.grpc.ServerProvider; +import io.grpc.ServerRegistry; +import io.grpc.observability.interceptors.InternalLoggingServerInterceptor; + +/** A server provider that injects the logging interceptor. */ +final class LoggingServerProvider extends ServerProvider { + private final ServerProvider prevProvider; + private final InternalLoggingServerInterceptor.Factory serverInterceptorFactory; + + private static LoggingServerProvider instance; + + private LoggingServerProvider(InternalLoggingServerInterceptor.Factory factory) { + prevProvider = ServerProvider.provider(); + serverInterceptorFactory = factory; + } + + static synchronized void init(InternalLoggingServerInterceptor.Factory factory) { + if (instance != null) { + throw new IllegalStateException("LoggingServerProvider already initialized!"); + } + instance = new LoggingServerProvider(factory); + ServerRegistry.getDefaultRegistry().register(instance); + } + + static synchronized void finish() { + if (instance == null) { + throw new IllegalStateException("LoggingServerProvider not initialized!"); + } + ServerRegistry.getDefaultRegistry().deregister(instance); + instance = null; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 6; + } + + private ServerBuilder addInterceptor(ServerBuilder builder) { + return builder.intercept(serverInterceptorFactory.create()); + } + + @Override + protected ServerBuilder builderForPort(int port) { + return addInterceptor(InternalServerProvider.builderForPort(prevProvider, port)); + } + + @Override + protected NewServerBuilderResult newServerBuilderForPort(int port, ServerCredentials creds) { + ServerProvider.NewServerBuilderResult result = InternalServerProvider.newServerBuilderForPort( + prevProvider, port, + creds); + ServerBuilder builder = result.getServerBuilder(); + if (builder != null) { + return ServerProvider.NewServerBuilderResult.serverBuilder( + addInterceptor(builder)); + } + checkNotNull(result.getError(), "Expected error to be set!"); + return result; + } +} diff --git a/observability/src/main/java/io/grpc/observability/Observability.java b/observability/src/main/java/io/grpc/observability/Observability.java new file mode 100644 index 00000000000..a7a6b04ad07 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/Observability.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import io.grpc.ExperimentalApi; +import io.grpc.ManagedChannelProvider.ProviderNotFoundException; +import io.grpc.observability.interceptors.InternalLoggingChannelInterceptor; +import io.grpc.observability.interceptors.InternalLoggingServerInterceptor; + +/** The main class for gRPC Observability features. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8869") +public final class Observability { + private static boolean initialized = false; + + /** + * Initialize grpc-observability. + * + * @throws ProviderNotFoundException if no underlying channel/server provider is available. + */ + public static synchronized void grpcInit() { + if (initialized) { + throw new IllegalStateException("Observability already initialized!"); + } + LoggingChannelProvider.init(new InternalLoggingChannelInterceptor.FactoryImpl()); + LoggingServerProvider.init(new InternalLoggingServerInterceptor.FactoryImpl()); + // TODO(sanjaypujare): initialize customTags map + initialized = true; + } + + /** Un-initialize or finish grpc-observability. */ + public static synchronized void grpcFinish() { + if (!initialized) { + throw new IllegalStateException("Observability not initialized!"); + } + LoggingChannelProvider.finish(); + LoggingServerProvider.finish(); + // TODO(sanjaypujare): finish customTags map + initialized = false; + } + + private Observability() { + } +} diff --git a/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingChannelInterceptor.java b/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingChannelInterceptor.java new file mode 100644 index 00000000000..3e535de3816 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingChannelInterceptor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability.interceptors; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Internal; +import io.grpc.MethodDescriptor; + +/** A logging interceptor for {@code LoggingChannelProvider}. */ +@Internal +public final class InternalLoggingChannelInterceptor implements ClientInterceptor { + + public interface Factory { + ClientInterceptor create(); + } + + public static class FactoryImpl implements Factory { + + @Override + public ClientInterceptor create() { + return new InternalLoggingChannelInterceptor(); + } + } + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + // TODO(dnvindhya) implement the interceptor + return null; + } +} diff --git a/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingServerInterceptor.java b/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingServerInterceptor.java new file mode 100644 index 00000000000..17385653121 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/interceptors/InternalLoggingServerInterceptor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability.interceptors; + +import io.grpc.Internal; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** A logging interceptor for {@code LoggingServerProvider}. */ +@Internal +public final class InternalLoggingServerInterceptor implements ServerInterceptor { + + public interface Factory { + ServerInterceptor create(); + } + + public static class FactoryImpl implements Factory { + + @Override + public ServerInterceptor create() { + return new InternalLoggingServerInterceptor(); + } + } + + @Override + public ServerCall.Listener interceptCall(ServerCall call, + Metadata headers, ServerCallHandler next) { + // TODO(dnvindhya) implement the interceptor + return null; + } +} diff --git a/observability/src/main/java/io/grpc/observability/logging/CloudLoggingHandler.java b/observability/src/main/java/io/grpc/observability/logging/CloudLoggingHandler.java new file mode 100644 index 00000000000..f325b9d8383 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/logging/CloudLoggingHandler.java @@ -0,0 +1,181 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability.logging; + +import com.google.cloud.MonitoredResource; +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.Logging; +import com.google.cloud.logging.LoggingOptions; +import com.google.cloud.logging.Payload.JsonPayload; +import com.google.cloud.logging.Severity; +import com.google.common.base.Strings; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.grpc.Internal; +import io.grpc.internal.JsonParser; +import io.grpc.observabilitylog.v1.GrpcLogRecord; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Custom logging handler that outputs logs generated using {@link java.util.logging.Logger} to + * Cloud Logging. + */ +// TODO(vindhyan): replace custom JUL handler with internal sink implementation to eliminate +// JUL dependency +@Internal +public class CloudLoggingHandler extends Handler { + + private static final String DEFAULT_LOG_NAME = "grpc-observability"; + private static final Level DEFAULT_LOG_LEVEL = Level.ALL; + + private final LoggingOptions loggingOptions; + private final Logging loggingClient; + private final Level baseLevel; + private final String cloudLogName; + + /** + * Creates a custom logging handler that publishes message to Cloud logging. Default log level is + * set to Level.FINEST if level is not passed. + */ + public CloudLoggingHandler() { + this(DEFAULT_LOG_LEVEL, null, null); + } + + /** + * Creates a custom logging handler that publishes message to Cloud logging. + * + * @param level set the level for which message levels will be logged by the custom logger + */ + public CloudLoggingHandler(Level level) { + this(level, null, null); + } + + /** + * Creates a custom logging handler that publishes message to Cloud logging. + * + * @param level set the level for which message levels will be logged by the custom logger + * @param logName the name of the log to which log entries are written + */ + public CloudLoggingHandler(Level level, String logName) { + this(level, logName, null); + } + + /** + * Creates a custom logging handler that publishes message to Cloud logging. + * + * @param level set the level for which message levels will be logged by the custom logger + * @param logName the name of the log to which log entries are written + * @param destinationProjectId the value of cloud project id to which logs are sent to by the + * custom logger + */ + public CloudLoggingHandler(Level level, String logName, String destinationProjectId) { + baseLevel = + (level != null) ? (level.equals(DEFAULT_LOG_LEVEL) ? Level.FINEST : level) : Level.FINEST; + setLevel(baseLevel); + cloudLogName = logName != null ? logName : DEFAULT_LOG_NAME; + + // TODO(dnvindhya) read the value from config instead of taking it as an argument + if (Strings.isNullOrEmpty(destinationProjectId)) { + loggingOptions = LoggingOptions.getDefaultInstance(); + } else { + loggingOptions = LoggingOptions.newBuilder().setProjectId(destinationProjectId).build(); + } + loggingClient = loggingOptions.getService(); + } + + @Override + public void publish(LogRecord record) { + if (!(record instanceof LogRecordExtension)) { + throw new IllegalArgumentException("Expected record of type LogRecordExtension"); + } + Level logLevel = record.getLevel(); + GrpcLogRecord protoRecord = ((LogRecordExtension) record).getGrpcLogRecord(); + writeLog(protoRecord, logLevel); + } + + private void writeLog(GrpcLogRecord logProto, Level logLevel) { + if (loggingClient == null) { + throw new IllegalStateException("Logging client not initialized"); + } + try { + Severity cloudLogLevel = getCloudLoggingLevel(logLevel); + Map mapPayload = protoToMapConverter(logProto); + + // TODO(vindhyan): make sure all (int, long) values are not displayed as double + LogEntry grpcLogEntry = + LogEntry.newBuilder(JsonPayload.of(mapPayload)) + .setSeverity(cloudLogLevel) + .setLogName(cloudLogName) + .setResource(MonitoredResource.newBuilder("global").build()) + .build(); + loggingClient.write(Collections.singleton(grpcLogEntry)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private Map protoToMapConverter(GrpcLogRecord logProto) + throws InvalidProtocolBufferException, IOException { + JsonFormat.Printer printer = JsonFormat.printer().preservingProtoFieldNames(); + String recordJson = printer.print(logProto); + return (Map) JsonParser.parse(recordJson); + } + + @Override + public void flush() { + if (loggingClient == null) { + throw new IllegalStateException("Logging client not initialized"); + } + loggingClient.flush(); + } + + @Override + public synchronized void close() throws SecurityException { + if (loggingClient == null) { + throw new IllegalStateException("Logging client not initialized"); + } + try { + loggingClient.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Severity getCloudLoggingLevel(Level recordLevel) { + switch (recordLevel.intValue()) { + case 300: // FINEST + case 400: // FINER + case 500: // FINE + return Severity.DEBUG; + case 700: // CONFIG + case 800: // INFO + return Severity.INFO; + case 900: // WARNING + return Severity.WARNING; + case 1000: // SEVERE + return Severity.ERROR; + default: + return Severity.DEFAULT; + } + } +} diff --git a/observability/src/main/java/io/grpc/observability/logging/LogRecordExtension.java b/observability/src/main/java/io/grpc/observability/logging/LogRecordExtension.java new file mode 100644 index 00000000000..a1220659302 --- /dev/null +++ b/observability/src/main/java/io/grpc/observability/logging/LogRecordExtension.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability.logging; + +import io.grpc.Internal; +import io.grpc.observabilitylog.v1.GrpcLogRecord; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * An extension of java.util.logging.LogRecord which includes gRPC observability logging specific + * fields. + */ +@Internal +public final class LogRecordExtension extends LogRecord { + + private final GrpcLogRecord grpcLogRecord; + + public LogRecordExtension(Level recordLevel, GrpcLogRecord record) { + super(recordLevel, null); + this.grpcLogRecord = record; + } + + public GrpcLogRecord getGrpcLogRecord() { + return grpcLogRecord; + } + + // Adding a serial version UID since base class i.e LogRecord is Serializable + private static final long serialVersionUID = 1L; +} diff --git a/observability/src/main/proto/grpc/observabilitylog/v1/observabilitylog.proto b/observability/src/main/proto/grpc/observabilitylog/v1/observabilitylog.proto new file mode 100644 index 00000000000..d2e72329cde --- /dev/null +++ b/observability/src/main/proto/grpc/observabilitylog/v1/observabilitylog.proto @@ -0,0 +1,178 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package grpc.observabilitylog.v1; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option java_package = "io.grpc.observabilitylog.v1"; +option java_outer_classname = "ObservabilityLogProto"; + +message GrpcLogRecord { + // List of event types + enum EventType { + GRPC_CALL_UNKNOWN = 0; + // Header sent from client to server + GRPC_CALL_REQUEST_HEADER = 1; + // Header sent from server to client + GRPC_CALL_RESPONSE_HEADER = 2; + // Message sent from client to server + GRPC_CALL_REQUEST_MESSAGE = 3; + // Message sent from server to client + GRPC_CALL_RESPONSE_MESSAGE = 4; + // Trailer indicates the end of the gRPC call + GRPC_CALL_TRAILER = 5; + // A signal that client is done sending + GRPC_CALL_HALF_CLOSE = 6; + // A signal that the rpc is canceled + GRPC_CALL_CANCEL = 7; + } + // The entity that generates the log entry + enum EventLogger { + LOGGER_UNKNOWN = 0; + LOGGER_CLIENT = 1; + LOGGER_SERVER = 2; + } + // The log severity level of the log entry + enum LogLevel { + LOG_LEVEL_UNKNOWN = 0; + LOG_LEVEL_TRACE = 1; + LOG_LEVEL_DEBUG = 2; + LOG_LEVEL_INFO = 3; + LOG_LEVEL_WARN = 4; + LOG_LEVEL_ERROR = 5; + LOG_LEVEL_CRITICAL = 6; + } + + // The timestamp of the log event + google.protobuf.Timestamp timestamp = 1; + + // Uniquely identifies a call. The value must not be 0 in order to disambiguate + // from an unset value. + // Each call may have several log entries. They will all have the same rpc_id. + // Nothing is guaranteed about their value other than they are unique across + // different RPCs in the same gRPC process. + uint64 rpc_id = 2; + + EventType event_type = 3; // one of the above EventType enum + EventLogger event_logger = 4; // one of the above EventLogger enum + + // the name of the service + string service_name = 5; + // the name of the RPC method + string method_name = 6; + + LogLevel log_level = 7; // one of the above LogLevel enum + + // Peer address information. On client side, peer is logged on server + // header event or trailer event (if trailer-only). On server side, peer + // is always logged on the client header event. + Address peer_address = 8; + + // the RPC timeout value + google.protobuf.Duration timeout = 11; + + // A single process may be used to run multiple virtual servers with + // different identities. + // The authority is the name of such a server identify. It is typically a + // portion of the URI in the form of or :. + string authority = 12; + + // Size of the message or metadata, depending on the event type, + // regardless of whether the full message or metadata is being logged + // (i.e. could be truncated or omitted). + uint32 payload_size = 13; + + // true if message or metadata field is either truncated or omitted due + // to config options + bool payload_truncated = 14; + + // Used by header event or trailer event + Metadata metadata = 15; + + // The entry sequence ID for this call. The first message has a value of 1, + // to disambiguate from an unset value. The purpose of this field is to + // detect missing entries in environments where durability or ordering is + // not guaranteed. + uint64 sequence_id = 16; + + // Used by message event + bytes message = 17; + + // The gRPC status code + uint32 status_code = 18; + + // The gRPC status message + string status_message = 19; + + // The value of the grpc-status-details-bin metadata key, if any. + // This is always an encoded google.rpc.Status message + bytes status_details = 20; + + // Attributes of the environment generating log record. The purpose of this + // field is to identify the source environment. + EnvironmentTags env_tags = 21; + + // A list of non-gRPC custom values specified by the application + repeated CustomTags custom_tags = 22; + + // A list of metadata pairs + message Metadata { + repeated MetadataEntry entry = 1; + } + + // One metadata key value pair + message MetadataEntry { + string key = 1; + bytes value = 2; + } + + // Address information + message Address { + enum Type { + TYPE_UNKNOWN = 0; + TYPE_IPV4 = 1; // in 1.2.3.4 form + TYPE_IPV6 = 2; // IPv6 canonical form (RFC5952 section 4) + TYPE_UNIX = 3; // UDS string + } + Type type = 1; + string address = 2; + // only for TYPE_IPV4 and TYPE_IPV6 + uint32 ip_port = 3; + } + + // Source Environment information + message EnvironmentTags { + string gcp_project_id = 1; + string gcp_numeric_project_id = 2; + string gce_instance_id = 3; + string gce_instance_hostname = 4; + string gce_instance_zone = 5; + string gke_cluster_uid = 6; + string gke_cluster_name = 7; + string gke_cluster_location = 8; + } + + // Custom key value pair + message CustomTags { + string key = 1; + string value = 2; + } +} diff --git a/observability/src/test/java/io/grpc/observability/LoggingChannelProviderTest.java b/observability/src/test/java/io/grpc/observability/LoggingChannelProviderTest.java new file mode 100644 index 00000000000..639bcbc6d0d --- /dev/null +++ b/observability/src/test/java/io/grpc/observability/LoggingChannelProviderTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.ManagedChannelProvider; +import io.grpc.MethodDescriptor; +import io.grpc.TlsChannelCredentials; +import io.grpc.observability.interceptors.InternalLoggingChannelInterceptor; +import io.grpc.testing.TestMethodDescriptors; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentMatchers; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class LoggingChannelProviderTest { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + + private final MethodDescriptor method = TestMethodDescriptors.voidMethod(); + + @Test + public void initTwiceCausesException() { + ManagedChannelProvider prevProvider = ManagedChannelProvider.provider(); + assertThat(prevProvider).isNotInstanceOf(LoggingChannelProvider.class); + LoggingChannelProvider.init(new InternalLoggingChannelInterceptor.FactoryImpl()); + assertThat(ManagedChannelProvider.provider()).isInstanceOf(LoggingChannelProvider.class); + try { + LoggingChannelProvider.init(new InternalLoggingChannelInterceptor.FactoryImpl()); + fail("should have failed for calling init() again"); + } catch (IllegalStateException e) { + assertThat(e).hasMessageThat().contains("LoggingChannelProvider already initialized!"); + } + LoggingChannelProvider.finish(); + assertThat(ManagedChannelProvider.provider()).isSameInstanceAs(prevProvider); + } + + @Test + public void forTarget_interceptorCalled() { + ClientInterceptor interceptor = mock(ClientInterceptor.class, + delegatesTo(new NoopInterceptor())); + InternalLoggingChannelInterceptor.Factory factory = mock( + InternalLoggingChannelInterceptor.Factory.class); + when(factory.create()).thenReturn(interceptor); + LoggingChannelProvider.init(factory); + ManagedChannelBuilder builder = ManagedChannelBuilder.forTarget("localhost"); + ManagedChannel channel = builder.build(); + CallOptions callOptions = CallOptions.DEFAULT; + + ClientCall unused = channel.newCall(method, callOptions); + verify(interceptor) + .interceptCall(same(method), same(callOptions), ArgumentMatchers.any()); + channel.shutdownNow(); + LoggingChannelProvider.finish(); + } + + @Test + public void forAddress_interceptorCalled() { + ClientInterceptor interceptor = mock(ClientInterceptor.class, + delegatesTo(new NoopInterceptor())); + InternalLoggingChannelInterceptor.Factory factory = mock( + InternalLoggingChannelInterceptor.Factory.class); + when(factory.create()).thenReturn(interceptor); + LoggingChannelProvider.init(factory); + ManagedChannelBuilder builder = ManagedChannelBuilder.forAddress("localhost", 80); + ManagedChannel channel = builder.build(); + CallOptions callOptions = CallOptions.DEFAULT; + + ClientCall unused = channel.newCall(method, callOptions); + verify(interceptor) + .interceptCall(same(method), same(callOptions), ArgumentMatchers.any()); + channel.shutdownNow(); + LoggingChannelProvider.finish(); + } + + @Test + public void newChannelBuilder_interceptorCalled() { + ClientInterceptor interceptor = mock(ClientInterceptor.class, + delegatesTo(new NoopInterceptor())); + InternalLoggingChannelInterceptor.Factory factory = mock( + InternalLoggingChannelInterceptor.Factory.class); + when(factory.create()).thenReturn(interceptor); + LoggingChannelProvider.init(factory); + ManagedChannelBuilder builder = Grpc.newChannelBuilder("localhost", + TlsChannelCredentials.create()); + ManagedChannel channel = builder.build(); + CallOptions callOptions = CallOptions.DEFAULT; + + ClientCall unused = channel.newCall(method, callOptions); + verify(interceptor) + .interceptCall(same(method), same(callOptions), ArgumentMatchers.any()); + channel.shutdownNow(); + LoggingChannelProvider.finish(); + } + + private static class NoopInterceptor implements ClientInterceptor { + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } +} diff --git a/observability/src/test/java/io/grpc/observability/LoggingServerProviderTest.java b/observability/src/test/java/io/grpc/observability/LoggingServerProviderTest.java new file mode 100644 index 00000000000..fd6b60b4738 --- /dev/null +++ b/observability/src/test/java/io/grpc/observability/LoggingServerProviderTest.java @@ -0,0 +1,136 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalAnswers.delegatesTo; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerProvider; +import io.grpc.observability.interceptors.InternalLoggingServerInterceptor; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import java.io.IOException; +import java.util.function.Supplier; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentMatchers; + +@RunWith(JUnit4.class) +public class LoggingServerProviderTest { + @Rule public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); + + @Test + public void initTwiceCausesException() { + ServerProvider prevProvider = ServerProvider.provider(); + assertThat(prevProvider).isNotInstanceOf(LoggingServerProvider.class); + LoggingServerProvider.init(new InternalLoggingServerInterceptor.FactoryImpl()); + assertThat(ServerProvider.provider()).isInstanceOf(ServerProvider.class); + try { + LoggingServerProvider.init(new InternalLoggingServerInterceptor.FactoryImpl()); + fail("should have failed for calling init() again"); + } catch (IllegalStateException e) { + assertThat(e).hasMessageThat().contains("LoggingServerProvider already initialized!"); + } + LoggingServerProvider.finish(); + assertThat(ServerProvider.provider()).isSameInstanceAs(prevProvider); + } + + @Test + public void forPort_interceptorCalled() throws IOException { + serverBuilder_interceptorCalled(() -> ServerBuilder.forPort(0)); + } + + @Test + public void newServerBuilder_interceptorCalled() throws IOException { + serverBuilder_interceptorCalled( + () -> Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create())); + } + + @SuppressWarnings("unchecked") + private void serverBuilder_interceptorCalled(Supplier> serverBuilderSupplier) + throws IOException { + ServerInterceptor interceptor = + mock(ServerInterceptor.class, delegatesTo(new NoopInterceptor())); + InternalLoggingServerInterceptor.Factory factory = mock( + InternalLoggingServerInterceptor.Factory.class); + when(factory.create()).thenReturn(interceptor); + LoggingServerProvider.init(factory); + Server server = serverBuilderSupplier.get().addService(new SimpleServiceImpl()).build().start(); + int port = cleanupRule.register(server).getPort(); + ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", port).usePlaintext() + .build(); + SimpleServiceGrpc.SimpleServiceBlockingStub stub = SimpleServiceGrpc.newBlockingStub( + cleanupRule.register(channel)); + assertThat(unaryRpc("buddy", stub)).isEqualTo("Hello buddy"); + verify(interceptor).interceptCall(any(ServerCall.class), any(Metadata.class), anyCallHandler()); + LoggingServerProvider.finish(); + } + + private ServerCallHandler anyCallHandler() { + return ArgumentMatchers.any(); + } + + private static String unaryRpc( + String requestMessage, SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub) { + SimpleRequest request = SimpleRequest.newBuilder().setRequestMessage(requestMessage).build(); + SimpleResponse response = blockingStub.unaryRpc(request); + return response.getResponseMessage(); + } + + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + + @Override + public void unaryRpc(SimpleRequest req, StreamObserver responseObserver) { + SimpleResponse response = + SimpleResponse.newBuilder() + .setResponseMessage("Hello " + req.getRequestMessage()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + } + + private static class NoopInterceptor implements ServerInterceptor { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } +} diff --git a/observability/src/test/java/io/grpc/observability/ObservabilityTest.java b/observability/src/test/java/io/grpc/observability/ObservabilityTest.java new file mode 100644 index 00000000000..6c71a0e2640 --- /dev/null +++ b/observability/src/test/java/io/grpc/observability/ObservabilityTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.observability; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ObservabilityTest { + + @Test + public void initFinish() { + Observability.grpcInit(); + try { + Observability.grpcInit(); + fail("should have failed for calling grpcInit() again"); + } catch (IllegalStateException e) { + assertThat(e).hasMessageThat().contains("Observability already initialized!"); + } + Observability.grpcFinish(); + try { + Observability.grpcFinish(); + fail("should have failed for calling grpcFinit() on uninitialized"); + } catch (IllegalStateException e) { + assertThat(e).hasMessageThat().contains("Observability not initialized!"); + } + } +} diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 3271118feda..999f21e7c10 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -22,7 +22,7 @@ dependencies { project(':grpc-testing'), project(':grpc-netty') signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } project.sourceSets { diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 121093716db..cf7cae19d8a 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -419,7 +419,7 @@ public OkHttpClientStream newStream( void streamReadyToStart(OkHttpClientStream clientStream) { if (goAwayStatus != null) { clientStream.transportState().transportReportStatus( - goAwayStatus, RpcProgress.REFUSED, true, new Metadata()); + goAwayStatus, RpcProgress.MISCARRIED, true, new Metadata()); } else if (streams.size() >= maxConcurrentStreams) { pendingStreams.add(clientStream); setInUse(clientStream); @@ -811,7 +811,10 @@ public void shutdownNow(Status reason) { } for (OkHttpClientStream stream : pendingStreams) { - stream.transportState().transportReportStatus(reason, true, new Metadata()); + // in cases such as the connection fails to ACK keep-alive, pending streams should have a + // chance to retry and be routed to another connection. + stream.transportState().transportReportStatus( + reason, RpcProgress.MISCARRIED, true, new Metadata()); maybeClearInUse(stream); } pendingStreams.clear(); @@ -894,7 +897,7 @@ private void startGoAway(int lastKnownStreamId, ErrorCode errorCode, Status stat for (OkHttpClientStream stream : pendingStreams) { stream.transportState().transportReportStatus( - status, RpcProgress.REFUSED, true, new Metadata()); + status, RpcProgress.MISCARRIED, true, new Metadata()); maybeClearInUse(stream); } pendingStreams.clear(); diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java index 46a49463e5f..99a9159eab0 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientStreamTest.java @@ -42,7 +42,7 @@ import io.grpc.okhttp.internal.framed.Header; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; @@ -243,7 +243,7 @@ public void getUnaryRequest() throws IOException { eq(false), eq(false), eq(3), eq(0), headersCaptor.capture()); verify(transport, times(0)).streamReadyToStart(isA(OkHttpClientStream.class)); - byte[] msg = "request".getBytes(StandardCharsets.UTF_8); + byte[] msg = "request".getBytes(Charset.forName("UTF-8")); stream.writeMessage(new ByteArrayInputStream(msg)); stream.halfClose(); verify(transport).streamReadyToStart(eq(stream)); diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index b70b832a797..1628df4d3c3 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -18,6 +18,7 @@ import static com.google.common.base.Charsets.UTF_8; import static com.google.common.truth.Truth.assertThat; +import static io.grpc.internal.ClientStreamListener.RpcProgress.MISCARRIED; import static io.grpc.internal.ClientStreamListener.RpcProgress.PROCESSED; import static io.grpc.internal.ClientStreamListener.RpcProgress.REFUSED; import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; @@ -2080,7 +2081,7 @@ public void goAway_streamListenerRpcProgress() throws Exception { listener3.waitUntilStreamClosed(); assertNull(listener1.rpcProgress); assertEquals(REFUSED, listener2.rpcProgress); - assertEquals(REFUSED, listener3.rpcProgress); + assertEquals(MISCARRIED, listener3.rpcProgress); assertEquals(1, activeStreamCount()); assertContainStream(DEFAULT_START_STREAM_ID); @@ -2133,6 +2134,39 @@ public void reset_streamListenerRpcProgress() throws Exception { shutdownAndVerify(); } + @Test + public void shutdownNow_streamListenerRpcProgress() throws Exception { + initTransport(); + setMaxConcurrentStreams(2); + MockStreamListener listener1 = new MockStreamListener(); + MockStreamListener listener2 = new MockStreamListener(); + MockStreamListener listener3 = new MockStreamListener(); + OkHttpClientStream stream1 = + clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); + stream1.start(listener1); + OkHttpClientStream stream2 = + clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); + stream2.start(listener2); + OkHttpClientStream stream3 = + clientTransport.newStream(method, new Metadata(), CallOptions.DEFAULT, tracers); + stream3.start(listener3); + waitForStreamPending(1); + + assertEquals(2, activeStreamCount()); + assertContainStream(DEFAULT_START_STREAM_ID); + assertContainStream(DEFAULT_START_STREAM_ID + 2); + + clientTransport.shutdownNow(Status.INTERNAL); + + listener1.waitUntilStreamClosed(); + listener2.waitUntilStreamClosed(); + listener3.waitUntilStreamClosed(); + + assertEquals(PROCESSED, listener1.rpcProgress); + assertEquals(PROCESSED, listener2.rpcProgress); + assertEquals(MISCARRIED, listener3.rpcProgress); + } + private int activeStreamCount() { return clientTransport.getActiveStreams().length; } diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Util.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Util.java index a961c605656..556d849c705 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Util.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/Util.java @@ -27,7 +27,6 @@ import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -48,7 +47,7 @@ public final class Util { public static final String[] EMPTY_STRING_ARRAY = new String[0]; /** A cheap and type-safe constant for the UTF-8 Charset. */ - public static final Charset UTF_8 = StandardCharsets.UTF_8; + public static final Charset UTF_8 = Charset.forName("UTF-8"); private Util() { } diff --git a/protobuf-lite/build.gradle b/protobuf-lite/build.gradle index 75cca43f0e6..7b58309c414 100644 --- a/protobuf-lite/build.gradle +++ b/protobuf-lite/build.gradle @@ -18,7 +18,7 @@ dependencies { testImplementation project(':grpc-core') signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } compileTestJava { diff --git a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java index 7bd9f4b68e3..da388be0503 100644 --- a/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java +++ b/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java @@ -23,24 +23,19 @@ import com.google.common.base.Converter; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; -import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.SubchannelPicker; -import io.grpc.LoadBalancerProvider; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Metadata; -import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; @@ -51,7 +46,6 @@ import io.grpc.lookup.v1.RouteLookupServiceGrpc.RouteLookupServiceStub; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; import io.grpc.rls.LbPolicyConfiguration.ChildLbStatusListener; -import io.grpc.rls.LbPolicyConfiguration.ChildLoadBalancingPolicy; import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper; import io.grpc.rls.LbPolicyConfiguration.RefCountedChildPolicyWrapperFactory; import io.grpc.rls.LruCache.EvictionListener; @@ -87,13 +81,6 @@ final class CachingRlsLbClient { private static final Converter RESPONSE_CONVERTER = new RouteLookupResponseConverter().reverse(); - // System property to use direct path enabled OobChannel, by default direct path is enabled. - private static final String RLS_ENABLE_OOB_CHANNEL_DIRECTPATH_PROPERTY = - "io.grpc.rls.CachingRlsLbClient.enable_oobchannel_directpath"; - @VisibleForTesting - static boolean enableOobChannelDirectPath = - Boolean.parseBoolean(System.getProperty(RLS_ENABLE_OOB_CHANNEL_DIRECTPATH_PROPERTY, "false")); - // All cache status changes (pending, backoff, success) must be under this lock private final Object lock = new Object(); // LRU cache based on access order (BACKOFF and actual data will be here) @@ -128,17 +115,18 @@ private CachingRlsLbClient(Builder builder) { synchronizationContext = helper.getSynchronizationContext(); lbPolicyConfig = checkNotNull(builder.lbPolicyConfig, "lbPolicyConfig"); RouteLookupConfig rlsConfig = lbPolicyConfig.getRouteLookupConfig(); - maxAgeNanos = rlsConfig.getMaxAgeInNanos(); - staleAgeNanos = rlsConfig.getStaleAgeInNanos(); - callTimeoutNanos = rlsConfig.getLookupServiceTimeoutInNanos(); + maxAgeNanos = rlsConfig.maxAgeInNanos(); + staleAgeNanos = rlsConfig.staleAgeInNanos(); + callTimeoutNanos = rlsConfig.lookupServiceTimeoutInNanos(); timeProvider = checkNotNull(builder.timeProvider, "timeProvider"); throttler = checkNotNull(builder.throttler, "throttler"); linkedHashLruCache = new RlsAsyncLruCache( - rlsConfig.getCacheSizeBytes(), + rlsConfig.cacheSizeBytes(), builder.evictionListener, scheduledExecutorService, - timeProvider); + timeProvider, + lock); logger = helper.getChannelLogger(); String serverHost = null; try { @@ -159,16 +147,16 @@ private CachingRlsLbClient(Builder builder) { // will be looked up differently than the backends; overrideAuthority(helper.getAuthority()) is // called to impose the authority security restrictions. ManagedChannelBuilder rlsChannelBuilder = helper.createResolvingOobChannelBuilder( - rlsConfig.getLookupService(), helper.getUnsafeChannelCredentials()); + rlsConfig.lookupService(), helper.getUnsafeChannelCredentials()); rlsChannelBuilder.overrideAuthority(helper.getAuthority()); - if (enableOobChannelDirectPath) { - Map directPathServiceConfig = - getDirectPathServiceConfig(rlsConfig.getLookupService()); + Map routeLookupChannelServiceConfig = + lbPolicyConfig.getRouteLookupChannelServiceConfig(); + if (routeLookupChannelServiceConfig != null) { logger.log( ChannelLogLevel.DEBUG, - "RLS channel direct path enabled. RLS channel service config: {0}", - directPathServiceConfig); - rlsChannelBuilder.defaultServiceConfig(directPathServiceConfig); + "RLS channel service config: {0}", + routeLookupChannelServiceConfig); + rlsChannelBuilder.defaultServiceConfig(routeLookupChannelServiceConfig); rlsChannelBuilder.disableServiceConfigLookUp(); } rlsChannel = rlsChannelBuilder.build(); @@ -181,25 +169,12 @@ private CachingRlsLbClient(Builder builder) { new ChildLoadBalancerHelperProvider(helper, new SubchannelStateManagerImpl(), rlsPicker); refCountedChildPolicyWrapperFactory = new RefCountedChildPolicyWrapperFactory( - childLbHelperProvider, new BackoffRefreshListener()); + lbPolicyConfig.getLoadBalancingPolicy(), childLbResolvedAddressFactory, + childLbHelperProvider, + new BackoffRefreshListener()); logger.log(ChannelLogLevel.DEBUG, "CachingRlsLbClient created"); } - private static ImmutableMap getDirectPathServiceConfig(String serviceName) { - ImmutableMap pickFirstStrategy = - ImmutableMap.of("pick_first", ImmutableMap.of()); - - ImmutableMap childPolicy = - ImmutableMap.of( - "childPolicy", ImmutableList.of(pickFirstStrategy), - "serviceName", serviceName); - - ImmutableMap grpcLbPolicy = - ImmutableMap.of("grpclb", childPolicy); - - return ImmutableMap.of("loadBalancingConfig", ImmutableList.of(grpcLbPolicy)); - } - @CheckReturnValue private ListenableFuture asyncRlsCall(RouteLookupRequest request) { final SettableFuture response = SettableFuture.create(); @@ -536,39 +511,17 @@ final class DataCacheEntry extends CacheEntry { private final long staleTime; private final ChildPolicyWrapper childPolicyWrapper; + // GuardedBy CachingRlsLbClient.lock DataCacheEntry(RouteLookupRequest request, final RouteLookupResponse response) { super(request); this.response = checkNotNull(response, "response"); // TODO(creamsoup) fallback to other targets if first one is not available childPolicyWrapper = refCountedChildPolicyWrapperFactory - .createOrGet(response.getTargets().get(0)); + .createOrGet(response.targets().get(0)); long now = timeProvider.currentTimeNanos(); expireTime = now + maxAgeNanos; staleTime = now + staleAgeNanos; - - if (childPolicyWrapper.getPicker() != null) { - childPolicyWrapper.refreshState(); - } else { - createChildLbPolicy(); - } - } - - private void createChildLbPolicy() { - ChildLoadBalancingPolicy childPolicy = lbPolicyConfig.getLoadBalancingPolicy(); - LoadBalancerProvider lbProvider = childPolicy.getEffectiveLbProvider(); - ConfigOrError lbConfig = - lbProvider - .parseLoadBalancingPolicyConfig( - childPolicy.getEffectiveChildPolicy(childPolicyWrapper.getTarget())); - - LoadBalancer lb = lbProvider.newLoadBalancer(childPolicyWrapper.getHelper()); - logger.log( - ChannelLogLevel.DEBUG, - "RLS child lb created. config: {0}", - lbConfig.getConfig()); - lb.handleResolvedAddresses(childLbResolvedAddressFactory.create(lbConfig.getConfig())); - lb.requestConnection(); } /** @@ -623,7 +576,7 @@ String getHeaderData() { int getSizeBytes() { // size of strings and java object overhead, actual memory usage is more than this. return - (response.getTargets().get(0).length() + response.getHeaderData().length()) * 2 + 38 * 2; + (response.targets().get(0).length() + response.getHeaderData().length()) * 2 + 38 * 2; } @Override @@ -637,7 +590,9 @@ boolean isStaled(long now) { @Override void cleanup() { - refCountedChildPolicyWrapperFactory.release(childPolicyWrapper); + synchronized (lock) { + refCountedChildPolicyWrapperFactory.release(childPolicyWrapper); + } } @Override @@ -856,14 +811,15 @@ private static final class RlsAsyncLruCache RlsAsyncLruCache(long maxEstimatedSizeBytes, @Nullable EvictionListener evictionListener, - ScheduledExecutorService ses, TimeProvider timeProvider) { + ScheduledExecutorService ses, TimeProvider timeProvider, Object lock) { super( maxEstimatedSizeBytes, new AutoCleaningEvictionListener(evictionListener), 1, TimeUnit.MINUTES, ses, - timeProvider); + timeProvider, + lock); } @Override @@ -924,20 +880,21 @@ final class RlsPicker extends SubchannelPicker { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - String[] methodName = args.getMethodDescriptor().getFullMethodName().split("/", 2); + String serviceName = args.getMethodDescriptor().getServiceName(); + String methodName = args.getMethodDescriptor().getBareMethodName(); RouteLookupRequest request = - requestFactory.create(methodName[0], methodName[1], args.getHeaders()); + requestFactory.create(serviceName, methodName, args.getHeaders()); final CachedRouteLookupResponse response = CachingRlsLbClient.this.get(request); logger.log(ChannelLogLevel.DEBUG, "Got route lookup cache entry for service={0}, method={1}, headers={2}:\n {3}", - new Object[]{methodName[0], methodName[1], args.getHeaders(), response}); + new Object[]{serviceName, methodName, args.getHeaders(), response}); if (response.getHeaderData() != null && !response.getHeaderData().isEmpty()) { Metadata headers = args.getHeaders(); headers.discardAll(RLS_DATA_KEY); headers.put(RLS_DATA_KEY, response.getHeaderData()); } - String defaultTarget = lbPolicyConfig.getRouteLookupConfig().getDefaultTarget(); + String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); boolean hasFallback = defaultTarget != null && !defaultTarget.isEmpty(); if (response.hasData()) { ChildPolicyWrapper childPolicyWrapper = response.getChildPolicyWrapper(); @@ -977,7 +934,7 @@ private PickResult useFallback(PickSubchannelArgs args) { } private void startFallbackChildPolicy() { - String defaultTarget = lbPolicyConfig.getRouteLookupConfig().getDefaultTarget(); + String defaultTarget = lbPolicyConfig.getRouteLookupConfig().defaultTarget(); logger.log(ChannelLogLevel.DEBUG, "starting fallback to {0}", defaultTarget); synchronized (lock) { if (fallbackChildPolicyWrapper != null) { @@ -985,27 +942,9 @@ private void startFallbackChildPolicy() { } fallbackChildPolicyWrapper = refCountedChildPolicyWrapperFactory.createOrGet(defaultTarget); } - LoadBalancerProvider lbProvider = - lbPolicyConfig.getLoadBalancingPolicy().getEffectiveLbProvider(); - final LoadBalancer lb = - lbProvider.newLoadBalancer(fallbackChildPolicyWrapper.getHelper()); - final ConfigOrError lbConfig = - lbProvider - .parseLoadBalancingPolicyConfig( - lbPolicyConfig - .getLoadBalancingPolicy() - .getEffectiveChildPolicy(defaultTarget)); - helper.getSynchronizationContext().execute( - new Runnable() { - @Override - public void run() { - lb.handleResolvedAddresses( - childLbResolvedAddressFactory.create(lbConfig.getConfig())); - lb.requestConnection(); - } - }); } + // GuardedBy CachingRlsLbClient.lock void close() { if (fallbackChildPolicyWrapper != null) { refCountedChildPolicyWrapperFactory.release(fallbackChildPolicyWrapper); @@ -1015,7 +954,7 @@ void close() { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("target", lbPolicyConfig.getRouteLookupConfig().getLookupService()) + .add("target", lbPolicyConfig.getRouteLookupConfig().lookupService()) .toString(); } } diff --git a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java index f54e441ffe5..f40cde9fd81 100644 --- a/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java +++ b/rls/src/main/java/io/grpc/rls/LbPolicyConfiguration.java @@ -22,12 +22,15 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ConnectivityState; +import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.Subchannel; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; +import io.grpc.NameResolver.ConfigOrError; import io.grpc.internal.ObjectPool; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; import io.grpc.rls.RlsProtoData.RouteLookupConfig; @@ -45,11 +48,15 @@ final class LbPolicyConfiguration { private final RouteLookupConfig routeLookupConfig; + @Nullable + private final Map routeLookupChannelServiceConfig; private final ChildLoadBalancingPolicy policy; LbPolicyConfiguration( - RouteLookupConfig routeLookupConfig, ChildLoadBalancingPolicy policy) { + RouteLookupConfig routeLookupConfig, @Nullable Map routeLookupChannelServiceConfig, + ChildLoadBalancingPolicy policy) { this.routeLookupConfig = checkNotNull(routeLookupConfig, "routeLookupConfig"); + this.routeLookupChannelServiceConfig = routeLookupChannelServiceConfig; this.policy = checkNotNull(policy, "policy"); } @@ -57,6 +64,11 @@ RouteLookupConfig getRouteLookupConfig() { return routeLookupConfig; } + @Nullable + Map getRouteLookupChannelServiceConfig() { + return routeLookupChannelServiceConfig; + } + ChildLoadBalancingPolicy getLoadBalancingPolicy() { return policy; } @@ -71,18 +83,20 @@ public boolean equals(Object o) { } LbPolicyConfiguration that = (LbPolicyConfiguration) o; return Objects.equals(routeLookupConfig, that.routeLookupConfig) + && Objects.equals(routeLookupChannelServiceConfig, that.routeLookupChannelServiceConfig) && Objects.equals(policy, that.policy); } @Override public int hashCode() { - return Objects.hash(routeLookupConfig, policy); + return Objects.hash(routeLookupConfig, routeLookupChannelServiceConfig, policy); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("routeLookupConfig", routeLookupConfig) + .add("routeLookupChannelServiceConfig", routeLookupChannelServiceConfig) .add("policy", policy) .toString(); } @@ -191,33 +205,49 @@ public String toString() { /** Factory for {@link ChildPolicyWrapper}. */ static final class RefCountedChildPolicyWrapperFactory { + // GuardedBy CachingRlsLbClient.lock @VisibleForTesting final Map childPolicyMap = new HashMap<>(); private final ChildLoadBalancerHelperProvider childLbHelperProvider; private final ChildLbStatusListener childLbStatusListener; + private final ChildLoadBalancingPolicy childPolicy; + private final ResolvedAddressFactory childLbResolvedAddressFactory; public RefCountedChildPolicyWrapperFactory( + ChildLoadBalancingPolicy childPolicy, + ResolvedAddressFactory childLbResolvedAddressFactory, ChildLoadBalancerHelperProvider childLbHelperProvider, ChildLbStatusListener childLbStatusListener) { + this.childPolicy = checkNotNull(childPolicy, "childPolicy"); + this.childLbResolvedAddressFactory = + checkNotNull(childLbResolvedAddressFactory, "childLbResolvedAddressFactory"); this.childLbHelperProvider = checkNotNull(childLbHelperProvider, "childLbHelperProvider"); this.childLbStatusListener = checkNotNull(childLbStatusListener, "childLbStatusListener"); } + // GuardedBy CachingRlsLbClient.lock ChildPolicyWrapper createOrGet(String target) { // TODO(creamsoup) check if the target is valid or not RefCountedChildPolicyWrapper pooledChildPolicyWrapper = childPolicyMap.get(target); if (pooledChildPolicyWrapper == null) { - ChildPolicyWrapper childPolicyWrapper = - new ChildPolicyWrapper(target, childLbHelperProvider, childLbStatusListener); + ChildPolicyWrapper childPolicyWrapper = new ChildPolicyWrapper( + target, childPolicy, childLbResolvedAddressFactory, childLbHelperProvider, + childLbStatusListener); pooledChildPolicyWrapper = RefCountedChildPolicyWrapper.of(childPolicyWrapper); childPolicyMap.put(target, pooledChildPolicyWrapper); + return pooledChildPolicyWrapper.getObject(); + } else { + ChildPolicyWrapper childPolicyWrapper = pooledChildPolicyWrapper.getObject(); + if (childPolicyWrapper.getPicker() != null) { + childPolicyWrapper.refreshState(); + } + return childPolicyWrapper; } - - return pooledChildPolicyWrapper.getObject(); } + // GuardedBy CachingRlsLbClient.lock void release(ChildPolicyWrapper childPolicyWrapper) { checkNotNull(childPolicyWrapper, "childPolicyWrapper"); String target = childPolicyWrapper.getTarget(); @@ -238,16 +268,36 @@ static final class ChildPolicyWrapper { private final String target; private final ChildPolicyReportingHelper helper; + private final LoadBalancer lb; private volatile SubchannelPicker picker; private ConnectivityState state; public ChildPolicyWrapper( String target, + ChildLoadBalancingPolicy childPolicy, + final ResolvedAddressFactory childLbResolvedAddressFactory, ChildLoadBalancerHelperProvider childLbHelperProvider, ChildLbStatusListener childLbStatusListener) { this.target = target; this.helper = new ChildPolicyReportingHelper(childLbHelperProvider, childLbStatusListener); + LoadBalancerProvider lbProvider = childPolicy.getEffectiveLbProvider(); + final ConfigOrError lbConfig = + lbProvider + .parseLoadBalancingPolicyConfig( + childPolicy.getEffectiveChildPolicy(target)); + this.lb = lbProvider.newLoadBalancer(helper); + helper.getChannelLogger().log( + ChannelLogLevel.DEBUG, "RLS child lb created. config: {0}", lbConfig.getConfig()); + helper.getSynchronizationContext().execute( + new Runnable() { + @Override + public void run() { + lb.handleResolvedAddresses( + childLbResolvedAddressFactory.create(lbConfig.getConfig())); + lb.requestConnection(); + } + }); } String getTarget() { @@ -263,7 +313,25 @@ ChildPolicyReportingHelper getHelper() { } void refreshState() { - helper.updateBalancingState(state, picker); + helper.getSynchronizationContext().execute( + new Runnable() { + @Override + public void run() { + helper.updateBalancingState(state, picker); + } + } + ); + } + + void shutdown() { + helper.getSynchronizationContext().execute( + new Runnable() { + @Override + public void run() { + lb.shutdown(); + } + } + ); } @Override @@ -346,6 +414,7 @@ public ChildPolicyWrapper returnObject(Object object) { long newCnt = refCnt.decrementAndGet(); checkState(newCnt != -1, "Cannot return never pooled childPolicyWrapper"); if (newCnt == 0) { + childPolicyWrapper.shutdown(); childPolicyWrapper = null; } return null; diff --git a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java index 2d60f103a60..9c1a24a0d1e 100644 --- a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java +++ b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java @@ -48,7 +48,7 @@ @ThreadSafe abstract class LinkedHashLruCache implements LruCache { - private final Object lock = new Object(); + private final Object lock; @GuardedBy("lock") private final LinkedHashMap delegate; @@ -64,9 +64,11 @@ abstract class LinkedHashLruCache implements LruCache { int cleaningInterval, TimeUnit cleaningIntervalUnit, ScheduledExecutorService ses, - final TimeProvider timeProvider) { + final TimeProvider timeProvider, + Object lock) { checkState(estimatedMaxSizeBytes > 0, "max estimated cache size should be positive"); this.estimatedMaxSizeBytes = estimatedMaxSizeBytes; + this.lock = checkNotNull(lock, "lock"); this.evictionListener = new SizeHandlingEvictionListener(evictionListener); this.timeProvider = checkNotNull(timeProvider, "timeProvider"); delegate = new LinkedHashMap( @@ -200,14 +202,15 @@ private V invalidate(K key, EvictionType cause) { } @Override - public final void invalidateAll(Iterable keys) { - checkNotNull(keys, "keys"); + public final void invalidateAll() { synchronized (lock) { - for (K key : keys) { - SizedValue existing = delegate.remove(key); - if (existing != null) { - evictionListener.onEviction(key, existing, EvictionType.EXPLICIT); + Iterator> iterator = delegate.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue() != null) { + evictionListener.onEviction(entry.getKey(), entry.getValue(), EvictionType.EXPLICIT); } + iterator.remove(); } } } @@ -291,13 +294,10 @@ private boolean cleanupExpiredEntries(int maxExpiredEntries, long now) { public final void close() { synchronized (lock) { periodicCleaner.stop(); - doClose(); - delegate.clear(); + invalidateAll(); } } - protected void doClose() {} - /** Periodically cleans up the AsyncRequestCache. */ private final class PeriodicCleaner { diff --git a/rls/src/main/java/io/grpc/rls/LruCache.java b/rls/src/main/java/io/grpc/rls/LruCache.java index 6ab5c4bcb46..1ad5a958289 100644 --- a/rls/src/main/java/io/grpc/rls/LruCache.java +++ b/rls/src/main/java/io/grpc/rls/LruCache.java @@ -49,10 +49,10 @@ interface LruCache { V invalidate(K key); /** - * Invalidates cache entries for given keys. This operation will trigger {@link EvictionListener} + * Invalidates cache entries for all keys. This operation will trigger {@link EvictionListener} * with {@link EvictionType#EXPLICIT}. */ - void invalidateAll(Iterable keys); + void invalidateAll(); /** Returns {@code true} if given key is cached. */ @CheckReturnValue diff --git a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java index 289098e2554..2aac96cadcf 100644 --- a/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java +++ b/rls/src/main/java/io/grpc/rls/RlsLoadBalancer.java @@ -56,8 +56,8 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { checkNotNull(lbPolicyConfiguration, "Missing rls lb config"); if (!lbPolicyConfiguration.equals(this.lbPolicyConfiguration)) { boolean needToConnect = this.lbPolicyConfiguration == null - || !this.lbPolicyConfiguration.getRouteLookupConfig().getLookupService().equals( - lbPolicyConfiguration.getRouteLookupConfig().getLookupService()); + || !this.lbPolicyConfiguration.getRouteLookupConfig().lookupService().equals( + lbPolicyConfiguration.getRouteLookupConfig().lookupService()); if (needToConnect) { if (routeLookupClient != null) { routeLookupClient.close(); diff --git a/rls/src/main/java/io/grpc/rls/RlsLoadBalancerProvider.java b/rls/src/main/java/io/grpc/rls/RlsLoadBalancerProvider.java index 3755fe3f77c..54ef848498c 100644 --- a/rls/src/main/java/io/grpc/rls/RlsLoadBalancerProvider.java +++ b/rls/src/main/java/io/grpc/rls/RlsLoadBalancerProvider.java @@ -30,9 +30,9 @@ import java.util.Map; /** - * The provider for the "rls-experimental" balancing policy. This class should not be directly + * The provider for the "rls_experimental" balancing policy. This class should not be directly * referenced in code. The policy should be accessed through {@link - * io.grpc.LoadBalancerRegistry#getProvider} with the name "rls-experimental". + * io.grpc.LoadBalancerRegistry#getProvider} with the name "rls_experimental". */ @Internal public final class RlsLoadBalancerProvider extends LoadBalancerProvider { @@ -49,7 +49,7 @@ public int getPriority() { @Override public String getPolicyName() { - return "rls-experimental"; + return "rls_experimental"; } @Override @@ -62,12 +62,15 @@ public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalanc try { RouteLookupConfig routeLookupConfig = new RouteLookupConfigConverter() .convert(JsonUtil.getObject(rawLoadBalancingConfigPolicy, "routeLookupConfig")); + Map routeLookupChannelServiceConfig = + JsonUtil.getObject(rawLoadBalancingConfigPolicy, "routeLookupChannelServiceConfig"); ChildLoadBalancingPolicy lbPolicy = ChildLoadBalancingPolicy .create( JsonUtil.getString(rawLoadBalancingConfigPolicy, "childPolicyConfigTargetFieldName"), JsonUtil.checkObjectList( checkNotNull(JsonUtil.getList(rawLoadBalancingConfigPolicy, "childPolicy")))); - return ConfigOrError.fromConfig(new LbPolicyConfiguration(routeLookupConfig, lbPolicy)); + return ConfigOrError.fromConfig( + new LbPolicyConfiguration(routeLookupConfig, routeLookupChannelServiceConfig, lbPolicy)); } catch (Exception e) { return ConfigOrError.fromError( Status.INVALID_ARGUMENT diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java index b4d3fcc3b29..14e3e50bd49 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoConverters.java @@ -63,7 +63,8 @@ static final class RouteLookupRequestConverter @Override protected RlsProtoData.RouteLookupRequest doForward(RouteLookupRequest routeLookupRequest) { - return new RlsProtoData.RouteLookupRequest(routeLookupRequest.getKeyMapMap()); + return RlsProtoData.RouteLookupRequest.create( + ImmutableMap.copyOf(routeLookupRequest.getKeyMapMap())); } @Override @@ -71,7 +72,7 @@ protected RouteLookupRequest doBackward(RlsProtoData.RouteLookupRequest routeLoo return RouteLookupRequest.newBuilder() .setTargetType("grpc") - .putAllKeyMap(routeLookupRequest.getKeyMap()) + .putAllKeyMap(routeLookupRequest.keyMap()) .build(); } } @@ -86,15 +87,15 @@ static final class RouteLookupResponseConverter @Override protected RlsProtoData.RouteLookupResponse doForward(RouteLookupResponse routeLookupResponse) { return - new RlsProtoData.RouteLookupResponse( - routeLookupResponse.getTargetsList(), + RlsProtoData.RouteLookupResponse.create( + ImmutableList.copyOf(routeLookupResponse.getTargetsList()), routeLookupResponse.getHeaderData()); } @Override protected RouteLookupResponse doBackward(RlsProtoData.RouteLookupResponse routeLookupResponse) { return RouteLookupResponse.newBuilder() - .addAllTargets(routeLookupResponse.getTargets()) + .addAllTargets(routeLookupResponse.targets()) .setHeaderData(routeLookupResponse.getHeaderData()) .build(); } @@ -108,13 +109,13 @@ static final class RouteLookupConfigConverter @Override protected RouteLookupConfig doForward(Map json) { - List grpcKeyBuilders = + ImmutableList grpcKeyBuilders = GrpcKeyBuilderConverter.covertAll( checkNotNull(JsonUtil.getListOfObjects(json, "grpcKeyBuilders"), "grpcKeyBuilders")); checkArgument(!grpcKeyBuilders.isEmpty(), "must have at least one GrpcKeyBuilder"); Set names = new HashSet<>(); for (GrpcKeyBuilder keyBuilder : grpcKeyBuilders) { - for (Name name : keyBuilder.getNames()) { + for (Name name : keyBuilder.names()) { checkArgument(names.add(name), "duplicate names in grpc_keybuilders: " + name); } } @@ -145,14 +146,15 @@ protected RouteLookupConfig doForward(Map json) { checkArgument(cacheSize > 0, "cacheSize must be positive"); cacheSize = Math.min(cacheSize, MAX_CACHE_SIZE); String defaultTarget = Strings.emptyToNull(JsonUtil.getString(json, "defaultTarget")); - return new RouteLookupConfig( - grpcKeyBuilders, - lookupService, - /* lookupServiceTimeoutInNanos= */ timeout, - /* maxAgeInNanos= */ maxAge, - /* staleAgeInNanos= */ staleAge, - /* cacheSizeBytes= */ cacheSize, - defaultTarget); + return RouteLookupConfig.builder() + .grpcKeyBuilders(grpcKeyBuilders) + .lookupService(lookupService) + .lookupServiceTimeoutInNanos(timeout) + .maxAgeInNanos(maxAge) + .staleAgeInNanos(staleAge) + .cacheSizeBytes(cacheSize) + .defaultTarget(defaultTarget) + .build(); } private static T orDefault(@Nullable T value, T defaultValue) { @@ -169,12 +171,12 @@ protected Map doBackward(RouteLookupConfig routeLookupConfig) { } private static final class GrpcKeyBuilderConverter { - public static List covertAll(List> keyBuilders) { - List keyBuilderList = new ArrayList<>(); + public static ImmutableList covertAll(List> keyBuilders) { + ImmutableList.Builder keyBuilderList = ImmutableList.builder(); for (Map keyBuilder : keyBuilders) { keyBuilderList.add(convert(keyBuilder)); } - return keyBuilderList; + return keyBuilderList.build(); } @SuppressWarnings("unchecked") @@ -184,25 +186,26 @@ static GrpcKeyBuilder convert(Map keyBuilder) { rawRawNames != null && !rawRawNames.isEmpty(), "each keyBuilder must have at least one name"); List> rawNames = JsonUtil.checkObjectList(rawRawNames); - List names = new ArrayList<>(); + ImmutableList.Builder namesBuilder = ImmutableList.builder(); for (Map rawName : rawNames) { String serviceName = JsonUtil.getString(rawName, "service"); checkArgument(!Strings.isNullOrEmpty(serviceName), "service must not be empty or null"); - names.add(new Name(serviceName, JsonUtil.getString(rawName, "method"))); + namesBuilder.add(Name.create(serviceName, JsonUtil.getString(rawName, "method"))); } List rawRawHeaders = JsonUtil.getList(keyBuilder, "headers"); List> rawHeaders = rawRawHeaders == null ? new ArrayList>() : JsonUtil.checkObjectList(rawRawHeaders); - List nameMatchers = new ArrayList<>(); + ImmutableList.Builder nameMatchersBuilder = ImmutableList.builder(); for (Map rawHeader : rawHeaders) { Boolean requiredMatch = JsonUtil.getBoolean(rawHeader, "requiredMatch"); checkArgument( requiredMatch == null || !requiredMatch, "requiredMatch shouldn't be specified for gRPC"); - NameMatcher matcher = new NameMatcher( - JsonUtil.getString(rawHeader, "key"), (List) rawHeader.get("names")); - nameMatchers.add(matcher); + NameMatcher matcher = NameMatcher.create( + JsonUtil.getString(rawHeader, "key"), + ImmutableList.copyOf((List) rawHeader.get("names"))); + nameMatchersBuilder.add(matcher); } ExtraKeys extraKeys = ExtraKeys.DEFAULT; Map rawExtraKeys = @@ -216,8 +219,10 @@ static GrpcKeyBuilder convert(Map keyBuilder) { if (constantKeys == null) { constantKeys = ImmutableMap.of(); } + ImmutableList nameMatchers = nameMatchersBuilder.build(); checkUniqueKey(nameMatchers, constantKeys.keySet()); - return new GrpcKeyBuilder(names, nameMatchers, extraKeys, constantKeys); + return GrpcKeyBuilder.create( + namesBuilder.build(), nameMatchers, extraKeys, ImmutableMap.copyOf(constantKeys)); } } @@ -225,7 +230,7 @@ private static void checkUniqueKey(List nameMatchers, Set c Set keys = new HashSet<>(constantKeys); keys.addAll(EXTRA_KEY_NAMES); for (NameMatcher nameMatcher : nameMatchers) { - keys.add(nameMatcher.getKey()); + keys.add(nameMatcher.key()); } if (keys.size() != nameMatchers.size() + constantKeys.size() + EXTRA_KEY_NAMES.size()) { throw new IllegalArgumentException("keys in KeyBuilder must be unique"); diff --git a/rls/src/main/java/io/grpc/rls/RlsProtoData.java b/rls/src/main/java/io/grpc/rls/RlsProtoData.java index 4709894833c..929b7800759 100644 --- a/rls/src/main/java/io/grpc/rls/RlsProtoData.java +++ b/rls/src/main/java/io/grpc/rls/RlsProtoData.java @@ -16,16 +16,9 @@ package io.grpc.rls; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - import com.google.auto.value.AutoValue; -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -35,140 +28,46 @@ final class RlsProtoData { private RlsProtoData() {} /** A request object sent to route lookup service. */ + @AutoValue @Immutable - static final class RouteLookupRequest { - - private final ImmutableMap keyMap; - - RouteLookupRequest(Map keyMap) { - this.keyMap = ImmutableMap.copyOf(checkNotNull(keyMap, "keyMap")); - } + abstract static class RouteLookupRequest { /** Returns a map of key values extracted via key builders for the gRPC or HTTP request. */ - ImmutableMap getKeyMap() { - return keyMap; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RouteLookupRequest that = (RouteLookupRequest) o; - return Objects.equal(keyMap, that.keyMap); - } - - @Override - public int hashCode() { - return Objects.hashCode(keyMap); - } + abstract ImmutableMap keyMap(); - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("keyMap", keyMap) - .toString(); + static RouteLookupRequest create(ImmutableMap keyMap) { + return new AutoValue_RlsProtoData_RouteLookupRequest(keyMap); } } /** A response from route lookup service. */ + @AutoValue @Immutable - static final class RouteLookupResponse { - - private final ImmutableList targets; - - private final String headerData; - - RouteLookupResponse(List targets, String headerData) { - checkState(targets != null && !targets.isEmpty(), "targets cannot be empty or null"); - this.targets = ImmutableList.copyOf(targets); - this.headerData = checkNotNull(headerData, "headerData"); - } + abstract static class RouteLookupResponse { /** * Returns list of targets. Prioritized list (best one first) of addressable entities to use for * routing, using syntax requested by the request target_type. The targets will be tried in * order until a healthy one is found. */ - ImmutableList getTargets() { - return targets; - } + abstract ImmutableList targets(); /** * Returns optional header data to pass along to AFE in the X-Google-RLS-Data header. Cached * with "target" and sent with all requests that match the request key. Allows the RLS to pass * its work product to the eventual target. */ - String getHeaderData() { - return headerData; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RouteLookupResponse that = (RouteLookupResponse) o; - return java.util.Objects.equals(targets, that.targets) - && java.util.Objects.equals(headerData, that.headerData); - } + abstract String getHeaderData(); - @Override - public int hashCode() { - return java.util.Objects.hash(targets, headerData); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("targets", targets) - .add("headerData", headerData) - .toString(); + static RouteLookupResponse create(ImmutableList targets, String getHeaderData) { + return new AutoValue_RlsProtoData_RouteLookupResponse(targets, getHeaderData); } } /** A config object for gRPC RouteLookupService. */ + @AutoValue @Immutable - static final class RouteLookupConfig { - - private final ImmutableList grpcKeyBuilders; - - private final String lookupService; - - private final long lookupServiceTimeoutInNanos; - - private final long maxAgeInNanos; - - private final long staleAgeInNanos; - - private final long cacheSizeBytes; - - @Nullable - private final String defaultTarget; - - RouteLookupConfig( - List grpcKeyBuilders, - String lookupService, - long lookupServiceTimeoutInNanos, - long maxAgeInNanos, - long staleAgeInNanos, - long cacheSizeBytes, - @Nullable - String defaultTarget) { - this.grpcKeyBuilders = ImmutableList.copyOf(grpcKeyBuilders); - this.lookupService = lookupService; - this.lookupServiceTimeoutInNanos = lookupServiceTimeoutInNanos; - this.maxAgeInNanos = maxAgeInNanos; - this.staleAgeInNanos = staleAgeInNanos; - this.cacheSizeBytes = cacheSizeBytes; - this.defaultTarget = defaultTarget; - } + abstract static class RouteLookupConfig { /** * Returns unordered specifications for constructing keys for gRPC requests. All GrpcKeyBuilders @@ -176,36 +75,25 @@ static final class RouteLookupConfig { * keyed by name. If no GrpcKeyBuilder matches, an empty key_map will be sent to the lookup * service; it should likely reply with a global default route and raise an alert. */ - ImmutableList getGrpcKeyBuilders() { - return grpcKeyBuilders; - } + abstract ImmutableList grpcKeyBuilders(); /** * Returns the name of the lookup service as a gRPC URI. Typically, this will be a subdomain of * the target, such as "lookup.datastore.googleapis.com". */ - String getLookupService() { - return lookupService; - } + abstract String lookupService(); /** Returns the timeout value for lookup service requests. */ - long getLookupServiceTimeoutInNanos() { - return lookupServiceTimeoutInNanos; - } - + abstract long lookupServiceTimeoutInNanos(); /** Returns the maximum age the result will be cached. */ - long getMaxAgeInNanos() { - return maxAgeInNanos; - } + abstract long maxAgeInNanos(); /** * Returns the time when an entry will be in a staled status. When cache is accessed whgen the * entry is in staled status, it will */ - long getStaleAgeInNanos() { - return staleAgeInNanos; - } + abstract long staleAgeInNanos(); /** * Returns a rough indicator of amount of memory to use for the client cache. Some of the data @@ -213,9 +101,7 @@ long getStaleAgeInNanos() { * than this value. If this field is omitted or set to zero, a client default will be used. * The value may be capped to a lower amount based on client configuration. */ - long getCacheSizeBytes() { - return cacheSizeBytes; - } + abstract long cacheSizeBytes(); /** * Returns the default target to use if needed. If nonempty (implies request processing @@ -224,51 +110,30 @@ long getCacheSizeBytes() { * {@literal e.g.} "us_east_1.cloudbigtable.googleapis.com". */ @Nullable - String getDefaultTarget() { - return defaultTarget; - } + abstract String defaultTarget(); - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RouteLookupConfig that = (RouteLookupConfig) o; - return lookupServiceTimeoutInNanos == that.lookupServiceTimeoutInNanos - && maxAgeInNanos == that.maxAgeInNanos - && staleAgeInNanos == that.staleAgeInNanos - && cacheSizeBytes == that.cacheSizeBytes - && Objects.equal(grpcKeyBuilders, that.grpcKeyBuilders) - && Objects.equal(lookupService, that.lookupService) - && Objects.equal(defaultTarget, that.defaultTarget); + static Builder builder() { + return new AutoValue_RlsProtoData_RouteLookupConfig.Builder(); } - @Override - public int hashCode() { - return Objects.hashCode( - grpcKeyBuilders, - lookupService, - lookupServiceTimeoutInNanos, - maxAgeInNanos, - staleAgeInNanos, - cacheSizeBytes, - defaultTarget); - } + @AutoValue.Builder + abstract static class Builder { - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("grpcKeyBuilders", grpcKeyBuilders) - .add("lookupService", lookupService) - .add("lookupServiceTimeoutInNanos", lookupServiceTimeoutInNanos) - .add("maxAgeInNanos", maxAgeInNanos) - .add("staleAgeInNanos", staleAgeInNanos) - .add("cacheSize", cacheSizeBytes) - .add("defaultTarget", defaultTarget) - .toString(); + abstract Builder grpcKeyBuilders(ImmutableList grpcKeyBuilders); + + abstract Builder lookupService(String lookupService); + + abstract Builder lookupServiceTimeoutInNanos(long lookupServiceTimeoutInNanos); + + abstract Builder maxAgeInNanos(long maxAgeInNanos); + + abstract Builder staleAgeInNanos(long staleAgeInNanos); + + abstract Builder cacheSizeBytes(long cacheSizeBytes); + + abstract Builder defaultTarget(@Nullable String defaultTarget); + + abstract RouteLookupConfig build(); } } @@ -277,74 +142,25 @@ public String toString() { * The name must match one of the names listed in the "name" field. If the "required_match" field * is true, one of the specified names must be present for the keybuilder to match. */ + @AutoValue @Immutable - static final class NameMatcher { - - private final String key; - - private final ImmutableList names; - - NameMatcher(String key, List names) { - this.key = checkNotNull(key, "key"); - this.names = ImmutableList.copyOf(checkNotNull(names, "names")); - } + abstract static class NameMatcher { /** The name that will be used in the RLS key_map to refer to this value. */ - String getKey() { - return key; - } + abstract String key(); /** Returns ordered list of names; the first non-empty value will be used. */ - ImmutableList names() { - return names; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NameMatcher matcher = (NameMatcher) o; - return java.util.Objects.equals(key, matcher.key) - && java.util.Objects.equals(names, matcher.names); - } + abstract ImmutableList names(); - @Override - public int hashCode() { - return java.util.Objects.hash(key, names); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("key", key) - .add("names", names) - .toString(); + static NameMatcher create(String key, ImmutableList names) { + return new AutoValue_RlsProtoData_NameMatcher(key, names); } } /** GrpcKeyBuilder is a configuration to construct headers consumed by route lookup service. */ - static final class GrpcKeyBuilder { - - private final ImmutableList names; - - private final ImmutableList headers; - private final ExtraKeys extraKeys; - private final ImmutableMap constantKeys; - - /** Constructor. All args should be nonnull. Headers should head unique keys. */ - GrpcKeyBuilder( - List names, List headers, ExtraKeys extraKeys, - Map constantKeys) { - checkState(names != null && !names.isEmpty(), "names cannot be empty"); - this.names = ImmutableList.copyOf(names); - this.headers = ImmutableList.copyOf(headers); - this.extraKeys = checkNotNull(extraKeys, "extraKeys"); - this.constantKeys = ImmutableMap.copyOf(checkNotNull(constantKeys, "constantKeys")); - } + @AutoValue + @Immutable + abstract static class GrpcKeyBuilder { /** * Returns names. To match, one of the given Name fields must match; the service and method @@ -352,53 +168,23 @@ static final class GrpcKeyBuilder { * package name. The method name may be omitted, in which case any method on the given service * is matched. */ - ImmutableList getNames() { - return names; - } + abstract ImmutableList names(); /** * Returns a list of NameMatchers for header. Extract keys from all listed headers. For gRPC, it * is an error to specify "required_match" on the NameMatcher protos, and we ignore it if set. */ - ImmutableList getHeaders() { - return headers; - } + abstract ImmutableList headers(); - ExtraKeys getExtraKeys() { - return extraKeys; - } + abstract ExtraKeys extraKeys(); - ImmutableMap getConstantKeys() { - return constantKeys; - } + abstract ImmutableMap constantKeys(); - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GrpcKeyBuilder that = (GrpcKeyBuilder) o; - return Objects.equal(names, that.names) && Objects.equal(headers, that.headers) - && Objects.equal(extraKeys, that.extraKeys) - && Objects.equal(constantKeys, that.constantKeys); - } - - @Override - public int hashCode() { - return Objects.hashCode(names, headers, extraKeys, constantKeys); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("names", names) - .add("headers", headers) - .add("extraKeys", extraKeys) - .add("constantKeys", constantKeys) - .toString(); + static GrpcKeyBuilder create( + ImmutableList names, + ImmutableList headers, + ExtraKeys extraKeys, ImmutableMap constantKeys) { + return new AutoValue_RlsProtoData_GrpcKeyBuilder(names, headers, extraKeys, constantKeys); } /** @@ -407,57 +193,23 @@ public String toString() { * required and includes the proto package name. The method name may be omitted, in which case * any method on the given service is matched. */ - static final class Name { - - private final String service; + @AutoValue + @Immutable + abstract static class Name { - @Nullable - private final String method; - - /** The primary constructor. */ - Name(String service, @Nullable String method) { - this.service = service; - this.method = method; - } - - String getService() { - return service; - } + abstract String service(); @Nullable - String getMethod() { - return method; - } + abstract String method(); - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Name name = (Name) o; - return Objects.equal(service, name.service) - && Objects.equal(method, name.method); - } - - @Override - public int hashCode() { - return Objects.hashCode(service, method); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("service", service) - .add("method", method) - .toString(); + static Name create(String service, @Nullable String method) { + return new AutoValue_RlsProtoData_GrpcKeyBuilder_Name(service, method); } } } @AutoValue + @Immutable abstract static class ExtraKeys { static final ExtraKeys DEFAULT = create(null, null, null); diff --git a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java index d3aecabd034..4a32bece2b2 100644 --- a/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java +++ b/rls/src/main/java/io/grpc/rls/RlsRequestFactory.java @@ -50,11 +50,11 @@ final class RlsRequestFactory { private static Map createKeyBuilderTable( RouteLookupConfig config) { Map table = new HashMap<>(); - for (GrpcKeyBuilder grpcKeyBuilder : config.getGrpcKeyBuilders()) { - for (Name name : grpcKeyBuilder.getNames()) { - boolean hasMethod = name.getMethod() == null || name.getMethod().isEmpty(); - String method = hasMethod ? "*" : name.getMethod(); - String path = "/" + name.getService() + "/" + method; + for (GrpcKeyBuilder grpcKeyBuilder : config.grpcKeyBuilders()) { + for (Name name : grpcKeyBuilder.names()) { + boolean hasMethod = name.method() == null || name.method().isEmpty(); + String method = hasMethod ? "*" : name.method(); + String path = "/" + name.service() + "/" + method; table.put(path, grpcKeyBuilder); } } @@ -73,12 +73,12 @@ RouteLookupRequest create(String service, String method, Metadata metadata) { grpcKeyBuilder = keyBuilderTable.get("/" + service + "/*"); } if (grpcKeyBuilder == null) { - return new RouteLookupRequest(ImmutableMap.of()); + return RouteLookupRequest.create(ImmutableMap.of()); } - Map rlsRequestHeaders = - createRequestHeaders(metadata, grpcKeyBuilder.getHeaders()); - ExtraKeys extraKeys = grpcKeyBuilder.getExtraKeys(); - Map constantKeys = grpcKeyBuilder.getConstantKeys(); + ImmutableMap.Builder rlsRequestHeaders = + createRequestHeaders(metadata, grpcKeyBuilder.headers()); + ExtraKeys extraKeys = grpcKeyBuilder.extraKeys(); + Map constantKeys = grpcKeyBuilder.constantKeys(); if (extraKeys.host() != null) { rlsRequestHeaders.put(extraKeys.host(), target); } @@ -89,12 +89,12 @@ RouteLookupRequest create(String service, String method, Metadata metadata) { rlsRequestHeaders.put(extraKeys.method(), method); } rlsRequestHeaders.putAll(constantKeys); - return new RouteLookupRequest(rlsRequestHeaders); + return RouteLookupRequest.create(rlsRequestHeaders.build()); } - private Map createRequestHeaders( + private ImmutableMap.Builder createRequestHeaders( Metadata metadata, List keyBuilder) { - Map rlsRequestHeaders = new HashMap<>(); + ImmutableMap.Builder rlsRequestHeaders = ImmutableMap.builder(); for (NameMatcher nameMatcher : keyBuilder) { String value = null; for (String requestHeaderName : nameMatcher.names()) { @@ -104,7 +104,7 @@ private Map createRequestHeaders( } } if (value != null) { - rlsRequestHeaders.put(nameMatcher.getKey(), value); + rlsRequestHeaders.put(nameMatcher.key(), value); } } return rlsRequestHeaders; diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index c8222a02b8a..588bddcaa61 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static io.grpc.rls.CachingRlsLbClient.RLS_DATA_KEY; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -81,6 +82,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -88,7 +90,6 @@ import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -142,19 +143,12 @@ public void uncaughtException(Thread t, Throwable e) { mock(Helper.class, AdditionalAnswers.delegatesTo(new FakeHelper())); private final FakeThrottler fakeThrottler = new FakeThrottler(); private final LbPolicyConfiguration lbPolicyConfiguration = - new LbPolicyConfiguration(ROUTE_LOOKUP_CONFIG, childLbPolicy); + new LbPolicyConfiguration(ROUTE_LOOKUP_CONFIG, null, childLbPolicy); private CachingRlsLbClient rlsLbClient; - private boolean existingEnableOobChannelDirectPath; private Map rlsChannelServiceConfig; private String rlsChannelOverriddenAuthority; - @Before - public void setUp() throws Exception { - existingEnableOobChannelDirectPath = CachingRlsLbClient.enableOobChannelDirectPath; - CachingRlsLbClient.enableOobChannelDirectPath = false; - } - private void setUpRlsLbClient() { rlsLbClient = CachingRlsLbClient.newBuilder() @@ -171,7 +165,9 @@ private void setUpRlsLbClient() { @After public void tearDown() throws Exception { rlsLbClient.close(); - CachingRlsLbClient.enableOobChannelDirectPath = existingEnableOobChannelDirectPath; + assertWithMessage( + "On client shut down, RlsLoadBalancer must shut down with all its child loadbalancers.") + .that(lbProvider.loadBalancers).isEmpty(); } private CachedRouteLookupResponse getInSyncContext( @@ -192,12 +188,12 @@ public void run() { public void get_noError_lifeCycle() throws Exception { setUpRlsLbClient(); InOrder inOrder = inOrder(evictionListener); - RouteLookupRequest routeLookupRequest = new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( routeLookupRequest, - new RouteLookupResponse(ImmutableList.of("target"), "header"))); + RouteLookupResponse.create(ImmutableList.of("target"), "header"))); // initial request CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); @@ -210,7 +206,7 @@ public void get_noError_lifeCycle() throws Exception { assertThat(resp.hasData()).isTrue(); // cache hit for staled entry - fakeTimeProvider.forwardTime(ROUTE_LOOKUP_CONFIG.getStaleAgeInNanos(), TimeUnit.NANOSECONDS); + fakeTimeProvider.forwardTime(ROUTE_LOOKUP_CONFIG.staleAgeInNanos(), TimeUnit.NANOSECONDS); resp = getInSyncContext(routeLookupRequest); assertThat(resp.hasData()).isTrue(); @@ -226,7 +222,7 @@ public void get_noError_lifeCycle() throws Exception { assertThat(resp.hasData()).isTrue(); // existing cache expired - fakeTimeProvider.forwardTime(ROUTE_LOOKUP_CONFIG.getMaxAgeInNanos(), TimeUnit.NANOSECONDS); + fakeTimeProvider.forwardTime(ROUTE_LOOKUP_CONFIG.maxAgeInNanos(), TimeUnit.NANOSECONDS); resp = getInSyncContext(routeLookupRequest); @@ -239,15 +235,35 @@ public void get_noError_lifeCycle() throws Exception { } @Test - public void rls_overDirectPath() throws Exception { - CachingRlsLbClient.enableOobChannelDirectPath = true; - setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = new RouteLookupRequest(ImmutableMap.of( + public void rls_withCustomRlsChannelServiceConfig() throws Exception { + Map routeLookupChannelServiceConfig = + ImmutableMap.of( + "loadBalancingConfig", + ImmutableList.of(ImmutableMap.of( + "grpclb", + ImmutableMap.of( + "childPolicy", + ImmutableList.of(ImmutableMap.of("pick_first", ImmutableMap.of())), + "serviceName", + "service1")))); + LbPolicyConfiguration lbPolicyConfiguration = new LbPolicyConfiguration( + ROUTE_LOOKUP_CONFIG, routeLookupChannelServiceConfig, childLbPolicy); + rlsLbClient = + CachingRlsLbClient.newBuilder() + .setBackoffProvider(fakeBackoffProvider) + .setResolvedAddressesFactory(resolvedAddressFactory) + .setEvictionListener(evictionListener) + .setHelper(helper) + .setLbPolicyConfig(lbPolicyConfiguration) + .setThrottler(fakeThrottler) + .setTimeProvider(fakeTimeProvider) + .build(); + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( routeLookupRequest, - new RouteLookupResponse(ImmutableList.of("target"), "header"))); + RouteLookupResponse.create(ImmutableList.of("target"), "header"))); // initial request CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); @@ -260,27 +276,18 @@ public void rls_overDirectPath() throws Exception { assertThat(resp.hasData()).isTrue(); assertThat(rlsChannelOverriddenAuthority).isEqualTo("bigtable.googleapis.com:443"); - assertThat(rlsChannelServiceConfig).isEqualTo( - ImmutableMap.of( - "loadBalancingConfig", - ImmutableList.of(ImmutableMap.of( - "grpclb", - ImmutableMap.of( - "childPolicy", - ImmutableList.of(ImmutableMap.of("pick_first", ImmutableMap.of())), - "serviceName", - "service1"))))); + assertThat(rlsChannelServiceConfig).isEqualTo(routeLookupChannelServiceConfig); } @Test public void get_throttledAndRecover() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); rlsServerImpl.setLookupTable( ImmutableMap.of( routeLookupRequest, - new RouteLookupResponse(ImmutableList.of("target"), "header"))); + RouteLookupResponse.create(ImmutableList.of("target"), "header"))); fakeThrottler.nextResult = true; fakeBackoffProvider.nextPolicy = createBackoffPolicy(10, TimeUnit.MILLISECONDS); @@ -318,12 +325,12 @@ public void get_throttledAndRecover() throws Exception { public void get_updatesLbState() throws Exception { setUpRlsLbClient(); InOrder inOrder = inOrder(helper); - RouteLookupRequest routeLookupRequest = new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "service1", "method-key", "create")); rlsServerImpl.setLookupTable( ImmutableMap.of( routeLookupRequest, - new RouteLookupResponse( + RouteLookupResponse.create( ImmutableList.of("primary.cloudbigtable.googleapis.com"), "header-rls-data-value"))); @@ -359,7 +366,7 @@ public void get_updatesLbState() throws Exception { fakeBackoffProvider.nextPolicy = createBackoffPolicy(100, TimeUnit.MILLISECONDS); // try to get invalid RouteLookupRequest invalidRouteLookupRequest = - new RouteLookupRequest(ImmutableMap.of()); + RouteLookupRequest.create(ImmutableMap.of()); CachedRouteLookupResponse errorResp = getInSyncContext(invalidRouteLookupRequest); assertThat(errorResp.isPending()).isTrue(); fakeTimeProvider.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); @@ -385,14 +392,16 @@ public void get_updatesLbState() throws Exception { @Test public void get_childPolicyWrapper_reusedForSameTarget() throws Exception { setUpRlsLbClient(); - RouteLookupRequest routeLookupRequest = new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "bar")); - RouteLookupRequest routeLookupRequest2 = new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest routeLookupRequest2 = RouteLookupRequest.create(ImmutableMap.of( "server", "bigtable.googleapis.com", "service-key", "foo", "method-key", "baz")); rlsServerImpl.setLookupTable( ImmutableMap.of( - routeLookupRequest, new RouteLookupResponse(ImmutableList.of("target"), "header"), - routeLookupRequest2, new RouteLookupResponse(ImmutableList.of("target"), "header2"))); + routeLookupRequest, + RouteLookupResponse.create(ImmutableList.of("target"), "header"), + routeLookupRequest2, + RouteLookupResponse.create(ImmutableList.of("target"), "header2"))); CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); assertThat(resp.isPending()).isTrue(); @@ -418,21 +427,22 @@ routeLookupRequest, new RouteLookupResponse(ImmutableList.of("target"), "header" } private static RouteLookupConfig getRouteLookupConfig() { - return new RouteLookupConfig( - ImmutableList.of( - new GrpcKeyBuilder( - ImmutableList.of(new Name("service1", "create")), + return RouteLookupConfig.builder() + .grpcKeyBuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", "create")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("id", ImmutableList.of("X-Google-Id"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("id", ImmutableList.of("X-Google-Id"))), ExtraKeys.create("server", "service-key", "method-key"), - ImmutableMap.of())), - /* lookupService= */ "service1", - /* lookupServiceTimeoutInNanos= */ TimeUnit.SECONDS.toNanos(2), - /* maxAgeInNanos= */ TimeUnit.SECONDS.toNanos(300), - /* staleAgeInNanos= */ TimeUnit.SECONDS.toNanos(240), - /* cacheSizeBytes= */ 1000, - DEFAULT_TARGET); + ImmutableMap.of()))) + .lookupService("service1") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(2)) + .maxAgeInNanos(TimeUnit.SECONDS.toNanos(300)) + .staleAgeInNanos(TimeUnit.SECONDS.toNanos(240)) + .cacheSizeBytes(1000) + .defaultTarget(DEFAULT_TARGET) + .build(); } private static BackoffPolicy createBackoffPolicy(final long delay, final TimeUnit unit) { @@ -462,6 +472,7 @@ public BackoffPolicy get() { * immediately fails when using the fallback target. */ private static final class TestLoadBalancerProvider extends LoadBalancerProvider { + final Set loadBalancers = new HashSet<>(); @Override public boolean isAvailable() { @@ -486,7 +497,7 @@ public ConfigOrError parseLoadBalancingPolicyConfig( @Override public LoadBalancer newLoadBalancer(final Helper helper) { - return new LoadBalancer() { + LoadBalancer loadBalancer = new LoadBalancer() { @Override public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { @@ -527,8 +538,12 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { @Override public void shutdown() { + loadBalancers.remove(this); } }; + + loadBalancers.add(loadBalancer); + return loadBalancer; } } diff --git a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java index 7c6be039c24..36022cb25e6 100644 --- a/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java +++ b/rls/src/test/java/io/grpc/rls/LbPolicyConfigurationTest.java @@ -18,17 +18,25 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.grpc.ChannelLogger; import io.grpc.ConnectivityState; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancerProvider; import io.grpc.LoadBalancerRegistry; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.SynchronizationContext; import io.grpc.rls.ChildLoadBalancerHelper.ChildLoadBalancerHelperProvider; import io.grpc.rls.LbPolicyConfiguration.ChildLbStatusListener; import io.grpc.rls.LbPolicyConfiguration.ChildLoadBalancingPolicy; @@ -36,23 +44,58 @@ import io.grpc.rls.LbPolicyConfiguration.ChildPolicyWrapper.ChildPolicyReportingHelper; import io.grpc.rls.LbPolicyConfiguration.InvalidChildPolicyConfigException; import io.grpc.rls.LbPolicyConfiguration.RefCountedChildPolicyWrapperFactory; +import java.lang.Thread.UncaughtExceptionHandler; import java.util.Map; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentMatchers; @RunWith(JUnit4.class) public class LbPolicyConfigurationTest { private final Helper helper = mock(Helper.class); + private final LoadBalancerProvider lbProvider = mock(LoadBalancerProvider.class); private final SubchannelStateManager subchannelStateManager = new SubchannelStateManagerImpl(); private final SubchannelPicker picker = mock(SubchannelPicker.class); private final ChildLbStatusListener childLbStatusListener = mock(ChildLbStatusListener.class); + private final ResolvedAddressFactory resolvedAddressFactory = + new ResolvedAddressFactory() { + @Override + public ResolvedAddresses create(Object childLbConfig) { + return ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .build(); + } + }; private final RefCountedChildPolicyWrapperFactory factory = new RefCountedChildPolicyWrapperFactory( + new ChildLoadBalancingPolicy( + "targetFieldName", + ImmutableMap.of("foo", "bar"), + lbProvider), + resolvedAddressFactory, new ChildLoadBalancerHelperProvider(helper, subchannelStateManager, picker), childLbStatusListener); + @Before + public void setUp() { + doReturn(mock(ChannelLogger.class)).when(helper).getChannelLogger(); + doReturn( + new SynchronizationContext( + new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new AssertionError(e); + } + })) + .when(helper).getSynchronizationContext(); + doReturn(mock(LoadBalancer.class)).when(lbProvider).newLoadBalancer(any(Helper.class)); + doReturn(ConfigOrError.fromConfig(new Object())) + .when(lbProvider).parseLoadBalancingPolicyConfig(ArgumentMatchers.>any()); + } + @Test public void childPolicyWrapper_refCounted() { String target = "target"; diff --git a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java index b7254341f64..60266e15998 100644 --- a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java +++ b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java @@ -23,7 +23,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import com.google.common.collect.ImmutableList; import io.grpc.rls.DoNotUseDirectScheduledExecutorService.FakeTimeProvider; import io.grpc.rls.LruCache.EvictionListener; import io.grpc.rls.LruCache.EvictionType; @@ -62,7 +61,8 @@ public void setUp() { 10, TimeUnit.NANOSECONDS, fakeScheduledService, - timeProvider) { + timeProvider, + new Object()) { @Override protected boolean isExpired(Integer key, Entry value, long nowNanos) { return value.expireTime <= nowNanos; @@ -210,7 +210,7 @@ public void invalidateAll() { assertThat(cache.estimatedSize()).isEqualTo(2); - cache.invalidateAll(ImmutableList.of(1, 2)); + cache.invalidateAll(); assertThat(cache.estimatedSize()).isEqualTo(0); } diff --git a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java index bf0bc47fcc4..5dfdf948d4b 100644 --- a/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsLoadBalancerTest.java @@ -118,16 +118,12 @@ public void uncaughtException(Thread t, Throwable e) { private MethodDescriptor fakeSearchMethod; private MethodDescriptor fakeRescueMethod; private RlsLoadBalancer rlsLb; - private boolean existingEnableOobChannelDirectPath; private String defaultTarget = "defaultTarget"; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - existingEnableOobChannelDirectPath = CachingRlsLbClient.enableOobChannelDirectPath; - CachingRlsLbClient.enableOobChannelDirectPath = false; - fakeSearchMethod = MethodDescriptor.newBuilder() .setFullMethodName("com.google/Search") @@ -144,16 +140,16 @@ public void setUp() throws Exception { .build(); fakeRlsServerImpl.setLookupTable( ImmutableMap.of( - new RouteLookupRequest(ImmutableMap.of( + RouteLookupRequest.create(ImmutableMap.of( "server", "fake-bigtable.googleapis.com", "service-key", "com.google", "method-key", "Search")), - new RouteLookupResponse(ImmutableList.of("wilderness"), "where are you?"), - new RouteLookupRequest(ImmutableMap.of( + RouteLookupResponse.create(ImmutableList.of("wilderness"), "where are you?"), + RouteLookupRequest.create(ImmutableMap.of( "server", "fake-bigtable.googleapis.com", "service-key", "com.google", "method-key", "Rescue")), - new RouteLookupResponse(ImmutableList.of("civilization"), "you are safe"))); + RouteLookupResponse.create(ImmutableList.of("civilization"), "you are safe"))); rlsLb = (RlsLoadBalancer) provider.newLoadBalancer(helper); rlsLb.cachingRlsLbClientBuilderProvider = new CachingRlsLbClientBuilderProvider() { @@ -168,7 +164,6 @@ public CachingRlsLbClient.Builder get() { @After public void tearDown() throws Exception { rlsLb.shutdown(); - CachingRlsLbClient.enableOobChannelDirectPath = existingEnableOobChannelDirectPath; } @Test diff --git a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java index 958abea3926..215f8f2ac04 100644 --- a/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsProtoConvertersTest.java @@ -53,7 +53,7 @@ public void convert_toRequestProto() { RlsProtoData.RouteLookupRequest object = converter.convert(proto); - assertThat(object.getKeyMap()).containsExactly("key1", "val1"); + assertThat(object.keyMap()).containsExactly("key1", "val1"); } @Test @@ -61,7 +61,7 @@ public void convert_toRequestObject() { Converter converter = new RouteLookupRequestConverter().reverse(); RlsProtoData.RouteLookupRequest requestObject = - new RlsProtoData.RouteLookupRequest(ImmutableMap.of("key1", "val1")); + RlsProtoData.RouteLookupRequest.create(ImmutableMap.of("key1", "val1")); RouteLookupRequest proto = converter.convert(requestObject); @@ -80,7 +80,7 @@ public void convert_toResponseProto() { RlsProtoData.RouteLookupResponse object = converter.convert(proto); - assertThat(object.getTargets()).containsExactly("target"); + assertThat(object.targets()).containsExactly("target"); assertThat(object.getHeaderData()).isEqualTo("some header data"); } @@ -90,7 +90,7 @@ public void convert_toResponseObject() { new RouteLookupResponseConverter().reverse(); RlsProtoData.RouteLookupResponse object = - new RlsProtoData.RouteLookupResponse(ImmutableList.of("target"), "some header data"); + RlsProtoData.RouteLookupResponse.create(ImmutableList.of("target"), "some header data"); RouteLookupResponse proto = converter.convert(object); @@ -176,34 +176,35 @@ public void convert_jsonRlsConfig() throws IOException { + "}"; RouteLookupConfig expectedConfig = - new RouteLookupConfig( - ImmutableList.of( - new GrpcKeyBuilder( - ImmutableList.of(new Name("service1", "create")), + RouteLookupConfig.builder() + .grpcKeyBuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", "create")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("id", ImmutableList.of("X-Google-Id"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("id", ImmutableList.of("X-Google-Id"))), ExtraKeys.DEFAULT, ImmutableMap.of()), - new GrpcKeyBuilder( - ImmutableList.of(new Name("service1", "*")), + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", "*")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("password", ImmutableList.of("Password"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("password", ImmutableList.of("Password"))), ExtraKeys.DEFAULT, ImmutableMap.of()), - new GrpcKeyBuilder( - ImmutableList.of(new Name("service3", "*")), + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service3", "*")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent"))), ExtraKeys.create("host-key", "service-key", "method-key"), - ImmutableMap.of("constKey1", "value1"))), - /* lookupService= */ "service1", - /* lookupServiceTimeoutInNanos= */ TimeUnit.SECONDS.toNanos(2), - /* maxAgeInNanos= */ TimeUnit.SECONDS.toNanos(300), - /* staleAgeInNanos= */ TimeUnit.SECONDS.toNanos(240), - /* cacheSizeBytes= */ 1000, - /* defaultTarget= */ "us_east_1.cloudbigtable.googleapis.com"); + ImmutableMap.of("constKey1", "value1")))) + .lookupService("service1") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(2)) + .maxAgeInNanos(TimeUnit.SECONDS.toNanos(300)) + .staleAgeInNanos(TimeUnit.SECONDS.toNanos(240)) + .cacheSizeBytes(1000) + .defaultTarget("us_east_1.cloudbigtable.googleapis.com") + .build(); RouteLookupConfigConverter converter = new RouteLookupConfigConverter(); @SuppressWarnings("unchecked") @@ -343,19 +344,20 @@ public void convert_jsonRlsConfig_defaultValues() throws IOException { + "}"; RouteLookupConfig expectedConfig = - new RouteLookupConfig( - ImmutableList.of( - new GrpcKeyBuilder( - ImmutableList.of(new Name("service1", null)), + RouteLookupConfig.builder() + .grpcKeyBuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", null)), ImmutableList.of(), ExtraKeys.DEFAULT, - ImmutableMap.of())), - /* lookupService= */ "service1", - /* lookupServiceTimeoutInNanos= */ TimeUnit.SECONDS.toNanos(10), - /* maxAgeInNanos= */ TimeUnit.MINUTES.toNanos(5), - /* staleAgeInNanos= */ TimeUnit.MINUTES.toNanos(5), - /* cacheSizeBytes= */ 5 * 1024 * 1024, - /* defaultTarget= */ "us_east_1.cloudbigtable.googleapis.com"); + ImmutableMap.of()))) + .lookupService("service1") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(10)) + .maxAgeInNanos(TimeUnit.MINUTES.toNanos(5)) + .staleAgeInNanos(TimeUnit.MINUTES.toNanos(5)) + .cacheSizeBytes(5 * 1024 * 1024) + .defaultTarget("us_east_1.cloudbigtable.googleapis.com") + .build(); RouteLookupConfigConverter converter = new RouteLookupConfigConverter(); @SuppressWarnings("unchecked") @@ -399,21 +401,22 @@ public void convert_jsonRlsConfig_staleAgeCappedByMaxAge() throws IOException { + "}"; RouteLookupConfig expectedConfig = - new RouteLookupConfig( - ImmutableList.of( - new GrpcKeyBuilder( - ImmutableList.of(new Name("service1", "create")), + RouteLookupConfig.builder() + .grpcKeyBuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("service1", "create")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("id", ImmutableList.of("X-Google-Id"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("id", ImmutableList.of("X-Google-Id"))), ExtraKeys.DEFAULT, - ImmutableMap.of())), - /* lookupService= */ "service1", - /* lookupServiceTimeoutInNanos= */ TimeUnit.SECONDS.toNanos(2), - /* maxAgeInNanos= */ TimeUnit.SECONDS.toNanos(300), - /* staleAgeInNanos= */ TimeUnit.SECONDS.toNanos(300), - /* cacheSizeBytes= */ 1000, - /* defaultTarget= */ "us_east_1.cloudbigtable.googleapis.com"); + ImmutableMap.of()))) + .lookupService("service1") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(2)) + .maxAgeInNanos(TimeUnit.SECONDS.toNanos(300)) + .staleAgeInNanos(TimeUnit.SECONDS.toNanos(300)) + .cacheSizeBytes(1000) + .defaultTarget("us_east_1.cloudbigtable.googleapis.com") + .build(); RouteLookupConfigConverter converter = new RouteLookupConfigConverter(); @SuppressWarnings("unchecked") diff --git a/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java b/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java index 9b66944d787..82e8416563f 100644 --- a/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java +++ b/rls/src/test/java/io/grpc/rls/RlsRequestFactoryTest.java @@ -36,40 +36,41 @@ public class RlsRequestFactoryTest { private static final RouteLookupConfig RLS_CONFIG = - new RouteLookupConfig( - ImmutableList.of( - new GrpcKeyBuilder( - ImmutableList.of(new Name("com.google.service1", "Create")), + RouteLookupConfig.builder() + .grpcKeyBuilders(ImmutableList.of( + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("com.google.service1", "Create")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("id", ImmutableList.of("X-Google-Id"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("id", ImmutableList.of("X-Google-Id"))), ExtraKeys.create("server-1", null, null), ImmutableMap.of("const-key-1", "const-value-1")), - new GrpcKeyBuilder( - ImmutableList.of(new Name("com.google.service1", "*")), + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("com.google.service1", "*")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent")), - new NameMatcher("password", ImmutableList.of("Password"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent")), + NameMatcher.create("password", ImmutableList.of("Password"))), ExtraKeys.create(null, "service-2", null), ImmutableMap.of("const-key-2", "const-value-2")), - new GrpcKeyBuilder( - ImmutableList.of(new Name("com.google.service2", "*")), + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("com.google.service2", "*")), ImmutableList.of( - new NameMatcher("password", ImmutableList.of("Password"))), + NameMatcher.create("password", ImmutableList.of("Password"))), ExtraKeys.create(null, "service-3", "method-3"), ImmutableMap.of()), - new GrpcKeyBuilder( - ImmutableList.of(new Name("com.google.service3", "*")), + GrpcKeyBuilder.create( + ImmutableList.of(Name.create("com.google.service3", "*")), ImmutableList.of( - new NameMatcher("user", ImmutableList.of("User", "Parent"))), + NameMatcher.create("user", ImmutableList.of("User", "Parent"))), ExtraKeys.create(null, null, null), - ImmutableMap.of("const-key-4", "const-value-4"))), - /* lookupService= */ "bigtable-rls.googleapis.com", - /* lookupServiceTimeoutInNanos= */ TimeUnit.SECONDS.toNanos(2), - /* maxAgeInNanos= */ TimeUnit.SECONDS.toNanos(300), - /* staleAgeInNanos= */ TimeUnit.SECONDS.toNanos(240), - /* cacheSizeBytes= */ 1000, - /* defaultTarget= */ "us_east_1.cloudbigtable.googleapis.com"); + ImmutableMap.of("const-key-4", "const-value-4")))) + .lookupService("bigtable-rls.googleapis.com") + .lookupServiceTimeoutInNanos(TimeUnit.SECONDS.toNanos(2)) + .maxAgeInNanos(TimeUnit.SECONDS.toNanos(300)) + .staleAgeInNanos(TimeUnit.SECONDS.toNanos(240)) + .cacheSizeBytes(1000) + .defaultTarget("us_east_1.cloudbigtable.googleapis.com") + .build(); private final RlsRequestFactory factory = new RlsRequestFactory( RLS_CONFIG, "bigtable.googleapis.com"); @@ -82,7 +83,7 @@ public void create_pathMatches() { metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); RouteLookupRequest request = factory.create("com.google.service1", "Create", metadata); - assertThat(request.getKeyMap()).containsExactly( + assertThat(request.keyMap()).containsExactly( "user", "test", "id", "123", "server-1", "bigtable.googleapis.com", @@ -98,7 +99,7 @@ public void create_pathFallbackMatches() { RouteLookupRequest request = factory.create("com.google.service1" , "Update", metadata); - assertThat(request.getKeyMap()).containsExactly( + assertThat(request.keyMap()).containsExactly( "user", "test", "password", "hunter2", "service-2", "com.google.service1", @@ -114,7 +115,7 @@ public void create_pathFallbackMatches_optionalHeaderMissing() { RouteLookupRequest request = factory.create("com.google.service1", "Update", metadata); - assertThat(request.getKeyMap()).containsExactly( + assertThat(request.keyMap()).containsExactly( "user", "test", "service-2", "com.google.service1", "const-key-2", "const-value-2"); @@ -128,7 +129,7 @@ public void create_unknownPath() { metadata.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar"); RouteLookupRequest request = factory.create("abc.def.service999", "Update", metadata); - assertThat(request.getKeyMap()).isEmpty(); + assertThat(request.keyMap()).isEmpty(); } @Test @@ -140,7 +141,7 @@ public void create_noMethodInRlsConfig() { RouteLookupRequest request = factory.create("com.google.service3", "Update", metadata); - assertThat(request.getKeyMap()).containsExactly( + assertThat(request.keyMap()).containsExactly( "user", "test", "const-key-4", "const-value-4"); } } diff --git a/services/src/main/java/io/grpc/protobuf/services/BinaryLogProviderImpl.java b/services/src/main/java/io/grpc/protobuf/services/BinaryLogProviderImpl.java index f68d67b27b7..896d92bcd91 100644 --- a/services/src/main/java/io/grpc/protobuf/services/BinaryLogProviderImpl.java +++ b/services/src/main/java/io/grpc/protobuf/services/BinaryLogProviderImpl.java @@ -42,6 +42,7 @@ public BinaryLogProviderImpl() throws IOException { * Deprecated and will be removed in a future version of gRPC. */ @Deprecated + @SuppressWarnings("InlineMeSuggester") // Only called internally; don't care public BinaryLogProviderImpl(BinaryLogSink sink) throws IOException { this(sink, System.getenv("GRPC_BINARY_LOG_CONFIG")); } diff --git a/services/src/test/java/io/grpc/protobuf/services/BinlogHelperTest.java b/services/src/test/java/io/grpc/protobuf/services/BinlogHelperTest.java index 23022192be3..37e503ccd41 100644 --- a/services/src/test/java/io/grpc/protobuf/services/BinlogHelperTest.java +++ b/services/src/test/java/io/grpc/protobuf/services/BinlogHelperTest.java @@ -80,7 +80,6 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -95,7 +94,7 @@ /** Tests for {@link BinlogHelper}. */ @RunWith(JUnit4.class) public final class BinlogHelperTest { - private static final Charset US_ASCII = StandardCharsets.US_ASCII; + private static final Charset US_ASCII = Charset.forName("US-ASCII"); private static final BinlogHelper HEADER_FULL = new Builder().header(Integer.MAX_VALUE).build(); private static final BinlogHelper HEADER_256 = new Builder().header(256).build(); private static final BinlogHelper MSG_FULL = new Builder().msg(Integer.MAX_VALUE).build(); diff --git a/settings.gradle b/settings.gradle index d60cddcbbac..8612f3c03cd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,6 +37,7 @@ include ":grpc-protobuf" include ":grpc-protobuf-lite" include ":grpc-netty" include ":grpc-netty-shaded" +include ":grpc-googleapis" include ":grpc-grpclb" include ":grpc-testing" include ":grpc-testing-proto" @@ -49,6 +50,8 @@ include ":grpc-services" include ":grpc-xds" include ":grpc-bom" include ":grpc-rls" +include ":grpc-authz" +include ":grpc-observability" project(':grpc-api').projectDir = "$rootDir/api" as File project(':grpc-core').projectDir = "$rootDir/core" as File @@ -61,6 +64,7 @@ project(':grpc-protobuf').projectDir = "$rootDir/protobuf" as File project(':grpc-protobuf-lite').projectDir = "$rootDir/protobuf-lite" as File project(':grpc-netty').projectDir = "$rootDir/netty" as File project(':grpc-netty-shaded').projectDir = "$rootDir/netty/shaded" as File +project(':grpc-googleapis').projectDir = "$rootDir/googleapis" as File project(':grpc-grpclb').projectDir = "$rootDir/grpclb" as File project(':grpc-testing').projectDir = "$rootDir/testing" as File project(':grpc-testing-proto').projectDir = "$rootDir/testing-proto" as File @@ -73,6 +77,8 @@ project(':grpc-services').projectDir = "$rootDir/services" as File project(':grpc-xds').projectDir = "$rootDir/xds" as File project(':grpc-bom').projectDir = "$rootDir/bom" as File project(':grpc-rls').projectDir = "$rootDir/rls" as File +project(':grpc-authz').projectDir = "$rootDir/authz" as File +project(':grpc-observability').projectDir = "$rootDir/observability" as File if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) { println '*** Skipping the build of codegen and compilation of proto files because skipCodegen=true' diff --git a/stub/build.gradle b/stub/build.gradle index 7d8040a87c2..2b5a6a4edb6 100644 --- a/stub/build.gradle +++ b/stub/build.gradle @@ -14,7 +14,7 @@ dependencies { testImplementation libraries.truth, project(':grpc-testing') signature "org.codehaus.mojo.signature:java17:1.0@signature" - signature "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4@signature" + signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature" } javadoc { diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index 7456948ddf1..04ed83f083a 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -391,7 +391,6 @@ public void setOnReadyHandler(Runnable onReadyHandler) { this.onReadyHandler = onReadyHandler; } - @Deprecated @Override public void disableAutoInboundFlowControl() { disableAutoRequestWithInitial(1); diff --git a/stub/src/main/java/io/grpc/stub/ServerCalls.java b/stub/src/main/java/io/grpc/stub/ServerCalls.java index 09f86d0364c..83954af9670 100644 --- a/stub/src/main/java/io/grpc/stub/ServerCalls.java +++ b/stub/src/main/java/io/grpc/stub/ServerCalls.java @@ -422,7 +422,6 @@ public void setOnCancelHandler(Runnable onCancelHandler) { this.onCancelHandler = onCancelHandler; } - @Deprecated @Override public void disableAutoInboundFlowControl() { disableAutoRequest(); diff --git a/stub/src/main/java/io/grpc/stub/StreamObserver.java b/stub/src/main/java/io/grpc/stub/StreamObserver.java index cf7cc258961..3a5c756e73e 100644 --- a/stub/src/main/java/io/grpc/stub/StreamObserver.java +++ b/stub/src/main/java/io/grpc/stub/StreamObserver.java @@ -26,8 +26,8 @@ * {@code StreamObserver} and passes it to the GRPC library for receiving. * *

Implementations are not required to be thread-safe (but should be - * thread-compatible). - * Separate {@code StreamObserver}s do + * + * thread-compatible). Separate {@code StreamObserver}s do * not need to be synchronized together; incoming and outgoing directions are independent. * Since individual {@code StreamObserver}s are not thread-safe, if multiple threads will be * writing to a {@code StreamObserver} concurrently, the application must synchronize calls. diff --git a/testing/build.gradle b/testing/build.gradle index a0f31819916..0879eca502a 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -13,7 +13,8 @@ dependencies { api project(':grpc-core'), project(':grpc-stub'), libraries.junit - implementation libraries.opencensus_api + // Only io.grpc.internal.testing.StatsTestUtils depends on opencensus_api, for internal use. + compileOnly libraries.opencensus_api runtimeOnly project(":grpc-context") // Pull in newer version than census-api testImplementation (libraries.mockito) { diff --git a/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java b/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java index 329d051ceb3..47a3416d4d3 100644 --- a/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java +++ b/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java @@ -149,6 +149,7 @@ public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { + firstException = null; try { base.evaluate(); } catch (Throwable t) { @@ -175,6 +176,7 @@ public void evaluate() throws Throwable { * Releases all the registered resources. */ private void teardown() { + stopwatch.reset(); stopwatch.start(); if (firstException == null) { diff --git a/xds/build.gradle b/xds/build.gradle index 16b1c877906..60bd4c7c60d 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -86,7 +86,7 @@ sourceSets { configureProtoCompilation() jar { - classifier = 'original' + archiveClassifier = 'original' } javadoc { @@ -110,7 +110,7 @@ javadoc { def prefixName = 'io.grpc.xds' shadowJar { - classifier = null + archiveClassifier = null dependencies { include(project(':grpc-xds')) } diff --git a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java index 23b14357096..4796f5e0361 100644 --- a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java @@ -22,7 +22,6 @@ import com.google.common.collect.ImmutableMap; import io.grpc.ChannelCredentials; import io.grpc.InsecureChannelCredentials; -import io.grpc.Internal; import io.grpc.InternalLogId; import io.grpc.TlsChannelCredentials; import io.grpc.alts.GoogleDefaultChannelCredentials; @@ -44,8 +43,7 @@ /** * A {@link Bootstrapper} implementation that reads xDS configurations from local file system. */ -@Internal -public class BootstrapperImpl extends Bootstrapper { +class BootstrapperImpl extends Bootstrapper { private static final String BOOTSTRAP_PATH_SYS_ENV_VAR = "GRPC_XDS_BOOTSTRAP"; @VisibleForTesting diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index 7735199134f..7a346e01871 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -32,6 +32,7 @@ import io.grpc.xds.CdsLoadBalancerProvider.CdsConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.XdsClient.CdsResourceWatcher; import io.grpc.xds.XdsClient.CdsUpdate; @@ -190,6 +191,10 @@ private void handleClusterDiscovered() { lbProvider = lbRegistry.getProvider("ring_hash_experimental"); lbConfig = new RingHashConfig(root.result.minRingSize(), root.result.maxRingSize()); } + if (root.result.lbPolicy() == LbPolicy.LEAST_REQUEST) { + lbProvider = lbRegistry.getProvider("least_request_experimental"); + lbConfig = new LeastRequestConfig(root.result.choiceCount()); + } if (lbProvider == null) { lbProvider = lbRegistry.getProvider("round_robin"); lbConfig = null; diff --git a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java index 222191ad55b..95b3bbfcfdf 100644 --- a/xds/src/main/java/io/grpc/xds/ClientXdsClient.java +++ b/xds/src/main/java/io/grpc/xds/ClientXdsClient.java @@ -28,7 +28,10 @@ import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.Any; import com.google.protobuf.Duration; import com.google.protobuf.InvalidProtocolBufferException; @@ -41,6 +44,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.CustomClusterType; import io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions; import io.envoyproxy.envoy.config.core.v3.RoutingPriority; @@ -107,7 +111,6 @@ import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -137,6 +140,8 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res @VisibleForTesting static final long DEFAULT_RING_HASH_LB_POLICY_MAX_RING_SIZE = 8 * 1024 * 1024L; @VisibleForTesting + static final int DEFAULT_LEAST_REQUEST_CHOICE_COUNT = 2; + @VisibleForTesting static final long MAX_RING_HASH_LB_POLICY_RING_SIZE = 8 * 1024 * 1024L; @VisibleForTesting static final String AGGREGATE_CLUSTER_TYPE_NAME = "envoy.clusters.aggregate"; @@ -158,7 +163,11 @@ final class ClientXdsClient extends XdsClient implements XdsResponseHandler, Res static boolean enableRouteLookup = !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_XDS_RLS_LB")) && Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_XDS_RLS_LB")); - + @VisibleForTesting + static boolean enableLeastRequest = + !Strings.isNullOrEmpty(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) + ? Boolean.parseBoolean(System.getenv("GRPC_EXPERIMENTAL_ENABLE_LEAST_REQUEST")) + : Boolean.parseBoolean(System.getProperty("io.grpc.xds.experimentalEnableLeastRequest")); private static final String TYPE_URL_HTTP_CONNECTION_MANAGER_V2 = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2" + ".HttpConnectionManager"; @@ -288,7 +297,12 @@ public void handleLdsResponse( errors.add("LDS response Resource index " + i + " - can't decode Listener: " + e); continue; } - String listenerName = listener.getName(); + if (!isResourceNameValid(listener.getName(), resource.getTypeUrl())) { + errors.add( + "Unsupported resource name: " + listener.getName() + " for type: " + ResourceType.LDS); + continue; + } + String listenerName = canonifyResourceName(listener.getName()); unpackedResources.add(listenerName); // Process Listener into LdsUpdate. @@ -383,7 +397,7 @@ static EnvoyServerProtoData.Listener parseServerSideListener( } } - List filterChains = new ArrayList<>(); + ImmutableList.Builder filterChains = ImmutableList.builder(); Set uniqueSet = new HashSet<>(); for (io.envoyproxy.envoy.config.listener.v3.FilterChain fc : proto.getFilterChainsList()) { filterChains.add( @@ -397,8 +411,8 @@ static EnvoyServerProtoData.Listener parseServerSideListener( null, certProviderInstances, parseHttpFilter); } - return new EnvoyServerProtoData.Listener( - proto.getName(), address, Collections.unmodifiableList(filterChains), defaultFilterChain); + return EnvoyServerProtoData.Listener.create( + proto.getName(), address, filterChains.build(), defaultFilterChain); } @VisibleForTesting @@ -455,7 +469,7 @@ static FilterChain parseFilterChain( FilterChainMatch filterChainMatch = parseFilterChainMatch(proto.getFilterChainMatch()); checkForUniqueness(uniqueSet, filterChainMatch); - return new FilterChain( + return FilterChain.create( proto.getName(), filterChainMatch, httpConnectionManager, @@ -658,18 +672,18 @@ private static List getCrossProduct(FilterChainMatch filterCha private static List expandOnPrefixRange(FilterChainMatch filterChainMatch) { ArrayList expandedList = new ArrayList<>(); - if (filterChainMatch.getPrefixRanges().isEmpty()) { + if (filterChainMatch.prefixRanges().isEmpty()) { expandedList.add(filterChainMatch); } else { - for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.getPrefixRanges()) { - expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), - Arrays.asList(cidrRange), - Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), - Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), - filterChainMatch.getConnectionSourceType(), - Collections.unmodifiableList(filterChainMatch.getSourcePorts()), - Collections.unmodifiableList(filterChainMatch.getServerNames()), - filterChainMatch.getTransportProtocol())); + for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.prefixRanges()) { + expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(), + ImmutableList.of(cidrRange), + filterChainMatch.applicationProtocols(), + filterChainMatch.sourcePrefixRanges(), + filterChainMatch.connectionSourceType(), + filterChainMatch.sourcePorts(), + filterChainMatch.serverNames(), + filterChainMatch.transportProtocol())); } } return expandedList; @@ -679,18 +693,18 @@ private static List expandOnApplicationProtocols( Collection set) { ArrayList expandedList = new ArrayList<>(); for (FilterChainMatch filterChainMatch : set) { - if (filterChainMatch.getApplicationProtocols().isEmpty()) { + if (filterChainMatch.applicationProtocols().isEmpty()) { expandedList.add(filterChainMatch); } else { - for (String applicationProtocol : filterChainMatch.getApplicationProtocols()) { - expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), - Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), - Arrays.asList(applicationProtocol), - Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), - filterChainMatch.getConnectionSourceType(), - Collections.unmodifiableList(filterChainMatch.getSourcePorts()), - Collections.unmodifiableList(filterChainMatch.getServerNames()), - filterChainMatch.getTransportProtocol())); + for (String applicationProtocol : filterChainMatch.applicationProtocols()) { + expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(), + filterChainMatch.prefixRanges(), + ImmutableList.of(applicationProtocol), + filterChainMatch.sourcePrefixRanges(), + filterChainMatch.connectionSourceType(), + filterChainMatch.sourcePorts(), + filterChainMatch.serverNames(), + filterChainMatch.transportProtocol())); } } } @@ -701,18 +715,18 @@ private static List expandOnSourcePrefixRange( Collection set) { ArrayList expandedList = new ArrayList<>(); for (FilterChainMatch filterChainMatch : set) { - if (filterChainMatch.getSourcePrefixRanges().isEmpty()) { + if (filterChainMatch.sourcePrefixRanges().isEmpty()) { expandedList.add(filterChainMatch); } else { - for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.getSourcePrefixRanges()) { - expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), - Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), - Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), - Arrays.asList(cidrRange), - filterChainMatch.getConnectionSourceType(), - Collections.unmodifiableList(filterChainMatch.getSourcePorts()), - Collections.unmodifiableList(filterChainMatch.getServerNames()), - filterChainMatch.getTransportProtocol())); + for (EnvoyServerProtoData.CidrRange cidrRange : filterChainMatch.sourcePrefixRanges()) { + expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(), + filterChainMatch.prefixRanges(), + filterChainMatch.applicationProtocols(), + ImmutableList.of(cidrRange), + filterChainMatch.connectionSourceType(), + filterChainMatch.sourcePorts(), + filterChainMatch.serverNames(), + filterChainMatch.transportProtocol())); } } } @@ -722,18 +736,18 @@ private static List expandOnSourcePrefixRange( private static List expandOnSourcePorts(Collection set) { ArrayList expandedList = new ArrayList<>(); for (FilterChainMatch filterChainMatch : set) { - if (filterChainMatch.getSourcePorts().isEmpty()) { + if (filterChainMatch.sourcePorts().isEmpty()) { expandedList.add(filterChainMatch); } else { - for (Integer sourcePort : filterChainMatch.getSourcePorts()) { - expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), - Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), - Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), - Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), - filterChainMatch.getConnectionSourceType(), - Arrays.asList(sourcePort), - Collections.unmodifiableList(filterChainMatch.getServerNames()), - filterChainMatch.getTransportProtocol())); + for (Integer sourcePort : filterChainMatch.sourcePorts()) { + expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(), + filterChainMatch.prefixRanges(), + filterChainMatch.applicationProtocols(), + filterChainMatch.sourcePrefixRanges(), + filterChainMatch.connectionSourceType(), + ImmutableList.of(sourcePort), + filterChainMatch.serverNames(), + filterChainMatch.transportProtocol())); } } } @@ -743,18 +757,18 @@ private static List expandOnSourcePorts(Collection expandOnServerNames(Collection set) { ArrayList expandedList = new ArrayList<>(); for (FilterChainMatch filterChainMatch : set) { - if (filterChainMatch.getServerNames().isEmpty()) { + if (filterChainMatch.serverNames().isEmpty()) { expandedList.add(filterChainMatch); } else { - for (String serverName : filterChainMatch.getServerNames()) { - expandedList.add(new FilterChainMatch(filterChainMatch.getDestinationPort(), - Collections.unmodifiableList(filterChainMatch.getPrefixRanges()), - Collections.unmodifiableList(filterChainMatch.getApplicationProtocols()), - Collections.unmodifiableList(filterChainMatch.getSourcePrefixRanges()), - filterChainMatch.getConnectionSourceType(), - Collections.unmodifiableList(filterChainMatch.getSourcePorts()), - Arrays.asList(serverName), - filterChainMatch.getTransportProtocol())); + for (String serverName : filterChainMatch.serverNames()) { + expandedList.add(FilterChainMatch.create(filterChainMatch.destinationPort(), + filterChainMatch.prefixRanges(), + filterChainMatch.applicationProtocols(), + filterChainMatch.sourcePrefixRanges(), + filterChainMatch.connectionSourceType(), + filterChainMatch.sourcePorts(), + ImmutableList.of(serverName), + filterChainMatch.transportProtocol())); } } } @@ -764,16 +778,17 @@ private static List expandOnServerNames(Collection prefixRanges = new ArrayList<>(); - List sourcePrefixRanges = new ArrayList<>(); + ImmutableList.Builder prefixRanges = ImmutableList.builder(); + ImmutableList.Builder sourcePrefixRanges = ImmutableList.builder(); try { for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getPrefixRangesList()) { - prefixRanges.add(new CidrRange(range.getAddressPrefix(), range.getPrefixLen().getValue())); + prefixRanges.add( + CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue())); } for (io.envoyproxy.envoy.config.core.v3.CidrRange range : proto.getSourcePrefixRangesList()) { sourcePrefixRanges.add( - new CidrRange(range.getAddressPrefix(), range.getPrefixLen().getValue())); + CidrRange.create(range.getAddressPrefix(), range.getPrefixLen().getValue())); } } catch (UnknownHostException e) { throw new ResourceInvalidException("Failed to create CidrRange", e); @@ -792,14 +807,14 @@ private static FilterChainMatch parseFilterChainMatch( default: throw new ResourceInvalidException("Unknown source-type: " + proto.getSourceType()); } - return new FilterChainMatch( + return FilterChainMatch.create( proto.getDestinationPort().getValue(), - prefixRanges, - proto.getApplicationProtocolsList(), - sourcePrefixRanges, + prefixRanges.build(), + ImmutableList.copyOf(proto.getApplicationProtocolsList()), + sourcePrefixRanges.build(), sourceType, - proto.getSourcePortsList(), - proto.getServerNamesList(), + ImmutableList.copyOf(proto.getSourcePortsList()), + ImmutableList.copyOf(proto.getServerNamesList()), proto.getTransportProtocol()); } @@ -876,9 +891,9 @@ static io.grpc.xds.HttpConnectionManager parseHttpConnectionManager( throw new ResourceInvalidException( "HttpConnectionManager contains invalid RDS: missing config_source"); } - if (!rds.getConfigSource().hasAds()) { + if (!rds.getConfigSource().hasAds() && !rds.getConfigSource().hasSelf()) { throw new ResourceInvalidException( - "HttpConnectionManager contains invalid RDS: must specify ADS"); + "HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource"); } // Collect the RDS resource referenced by this HttpConnectionManager. rdsResources.add(rds.getRouteConfigName()); @@ -1415,7 +1430,13 @@ public void handleRdsResponse( errors.add("RDS response Resource index " + i + " - can't decode RouteConfiguration: " + e); continue; } - String routeConfigName = routeConfig.getName(); + if (!isResourceNameValid(routeConfig.getName(), resource.getTypeUrl())) { + errors.add( + "Unsupported resource name: " + routeConfig.getName() + " for type: " + + ResourceType.RDS); + continue; + } + String routeConfigName = canonifyResourceName(routeConfig.getName()); unpackedResources.add(routeConfigName); // Process RouteConfiguration into RdsUpdate. @@ -1537,7 +1558,12 @@ public void handleCdsResponse( errors.add("CDS response Resource index " + i + " - can't decode Cluster: " + e); continue; } - String clusterName = cluster.getName(); + if (!isResourceNameValid(cluster.getName(), resource.getTypeUrl())) { + errors.add( + "Unsupported resource name: " + cluster.getName() + " for type: " + ResourceType.CDS); + continue; + } + String clusterName = canonifyResourceName(cluster.getName()); // Management server is required to always send newly requested resources, even if they // may have been sent previously (proactively). Thus, client does not need to cache @@ -1614,6 +1640,17 @@ static CdsUpdate processCluster(Cluster cluster, Set retainedEdsResource updateBuilder.ringHashLbPolicy(minRingSize, maxRingSize); } else if (cluster.getLbPolicy() == LbPolicy.ROUND_ROBIN) { updateBuilder.roundRobinLbPolicy(); + } else if (enableLeastRequest && cluster.getLbPolicy() == LbPolicy.LEAST_REQUEST) { + LeastRequestLbConfig lbConfig = cluster.getLeastRequestLbConfig(); + int choiceCount = + lbConfig.hasChoiceCount() + ? lbConfig.getChoiceCount().getValue() + : DEFAULT_LEAST_REQUEST_CHOICE_COUNT; + if (choiceCount < DEFAULT_LEAST_REQUEST_CHOICE_COUNT) { + throw new ResourceInvalidException( + "Cluster " + cluster.getName() + ": invalid least_request_lb_config: " + lbConfig); + } + updateBuilder.leastRequestLbPolicy(choiceCount); } else { throw new ResourceInvalidException( "Cluster " + cluster.getName() + ": unsupported lb policy: " + cluster.getLbPolicy()); @@ -1694,9 +1731,11 @@ private static StructOrError parseNonAggregateCluster( String edsServiceName = null; io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig edsClusterConfig = cluster.getEdsClusterConfig(); - if (!edsClusterConfig.getEdsConfig().hasAds()) { - return StructOrError.fromError("Cluster " + clusterName - + ": field eds_cluster_config must be set to indicate to use EDS over ADS."); + if (!edsClusterConfig.getEdsConfig().hasAds() + && ! edsClusterConfig.getEdsConfig().hasSelf()) { + return StructOrError.fromError( + "Cluster " + clusterName + ": field eds_cluster_config must be set to indicate to use" + + " EDS over ADS or self ConfigSource"); } // If the service_name field is set, that value will be used for the EDS request. if (!edsClusterConfig.getServiceName().isEmpty()) { @@ -1770,7 +1809,12 @@ public void handleEdsResponse( "EDS response Resource index " + i + " - can't decode ClusterLoadAssignment: " + e); continue; } - String clusterName = assignment.getClusterName(); + if (!isResourceNameValid(assignment.getClusterName(), resource.getTypeUrl())) { + errors.add("Unsupported resource name: " + assignment.getClusterName() + " for type: " + + ResourceType.EDS); + continue; + } + String clusterName = canonifyResourceName(assignment.getClusterName()); // Skip information for clusters not requested. // Management server is required to always send newly requested resources, even if they @@ -2028,12 +2072,31 @@ public Collection getSubscribedResources(ServerInfo serverInfo, Resource } @Override - Map getSubscribedResourcesMetadata(ResourceType type) { - Map metadataMap = new HashMap<>(); - for (Map.Entry entry : getSubscribedResourcesMap(type).entrySet()) { - metadataMap.put(entry.getKey(), entry.getValue().metadata); - } - return metadataMap; + ListenableFuture>> + getSubscribedResourcesMetadataSnapshot() { + final SettableFuture>> future = + SettableFuture.create(); + syncContext.execute(new Runnable() { + @Override + public void run() { + // A map from a "resource type" to a map ("resource name": "resource metadata") + ImmutableMap.Builder> metadataSnapshot = + ImmutableMap.builder(); + for (ResourceType type : ResourceType.values()) { + if (type == ResourceType.UNKNOWN) { + continue; + } + ImmutableMap.Builder metadataMap = ImmutableMap.builder(); + for (Map.Entry resourceEntry + : getSubscribedResourcesMap(type).entrySet()) { + metadataMap.put(resourceEntry.getKey(), resourceEntry.getValue().metadata); + } + metadataSnapshot.put(type, metadataMap.build()); + } + future.set(metadataSnapshot.build()); + } + }); + return future; } @Override @@ -2340,10 +2403,6 @@ private final class ResourceSubscriber { ResourceSubscriber(ResourceType type, String resource) { syncContext.throwIfNotInThisSynchronizationContext(); this.type = type; - // TODO(zdapeng): Validate authority in resource URI for new-style resource name - // when parsing XDS response. - // TODO(zdapeng): Canonicalize the resource name by sorting the context params in normal - // lexicographic order. this.resource = resource; this.serverInfo = getServerInfo(resource); // Initialize metadata in UNKNOWN state to cover the case when resource subscriber, diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index e4800cf668c..309daf55a18 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -672,7 +672,7 @@ private static PriorityChildConfig generateDnsBasedPriorityChildConfig( * Generates configs to be used in the priority LB policy for priorities in an EDS cluster. * *

priority LB -> cluster_impl LB (one per priority) -> (weighted_target LB - * -> round_robin (one per locality)) / ring_hash_experimental + * -> round_robin / least_request_experimental (one per locality)) / ring_hash_experimental */ private static Map generateEdsBasedPriorityChildConfigs( String cluster, @Nullable String edsServiceName, @Nullable ServerInfo lrsServerInfo, @@ -684,13 +684,14 @@ private static Map generateEdsBasedPriorityChildCon for (String priority : prioritizedLocalityWeights.keySet()) { PolicySelection leafPolicy = endpointLbPolicy; // Depending on the endpoint-level load balancing policy, different LB hierarchy may be - // created. If the endpoint-level LB policy is round_robin, it creates a two-level LB - // hierarchy: a locality-level LB policy that balances load according to locality weights - // followed by an endpoint-level LB policy that simply rounds robin the endpoints within - // the locality. If the endpoint-level LB policy is ring_hash_experimental, it creates - // a unified LB policy that balances load by weighing the product of each endpoint's weight - // and the weight of the locality it belongs to. - if (endpointLbPolicy.getProvider().getPolicyName().equals("round_robin")) { + // created. If the endpoint-level LB policy is round_robin or least_request_experimental, + // it creates a two-level LB hierarchy: a locality-level LB policy that balances load + // according to locality weights followed by an endpoint-level LB policy that balances load + // between endpoints within the locality. If the endpoint-level LB policy is + // ring_hash_experimental, it creates a unified LB policy that balances load by weighing the + // product of each endpoint's weight and the weight of the locality it belongs to. + if (endpointLbPolicy.getProvider().getPolicyName().equals("round_robin") + || endpointLbPolicy.getProvider().getPolicyName().equals("least_request_experimental")) { Map localityWeights = prioritizedLocalityWeights.get(priority); Map targets = new HashMap<>(); for (Locality locality : localityWeights.keySet()) { diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java index 4aaf0dcadde..6f6f887e925 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancerProvider.java @@ -68,7 +68,8 @@ public LoadBalancer newLoadBalancer(Helper helper) { static final class ClusterResolverConfig { // Ordered list of clusters to be resolved. final List discoveryMechanisms; - // Endpoint-level load balancing policy with config (round_robin or ring_hash_experimental). + // Endpoint-level load balancing policy with config + // (round_robin, least_request_experimental or ring_hash_experimental). final PolicySelection lbPolicy; ClusterResolverConfig(List discoveryMechanisms, PolicySelection lbPolicy) { diff --git a/xds/src/main/java/io/grpc/xds/CsdsService.java b/xds/src/main/java/io/grpc/xds/CsdsService.java index 89147536a6d..edee01f95f1 100644 --- a/xds/src/main/java/io/grpc/xds/CsdsService.java +++ b/xds/src/main/java/io/grpc/xds/CsdsService.java @@ -17,8 +17,10 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Verify.verifyNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.util.Timestamps; import io.envoyproxy.envoy.admin.v3.ClientResourceStatus; import io.envoyproxy.envoy.service.status.v3.ClientConfig; @@ -37,6 +39,9 @@ import io.grpc.xds.XdsClient.ResourceMetadata.UpdateFailureState; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; @@ -101,21 +106,28 @@ public void onCompleted() { private boolean handleRequest( ClientStatusRequest request, StreamObserver responseObserver) { + StatusException error; try { responseObserver.onNext(getConfigDumpForRequest(request)); return true; } catch (StatusException e) { - responseObserver.onError(e); + error = e; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.log(Level.FINE, "Server interrupted while building CSDS config dump", e); + error = Status.ABORTED.withDescription("Thread interrupted").withCause(e).asException(); } catch (Exception e) { logger.log(Level.WARNING, "Unexpected error while building CSDS config dump", e); - responseObserver.onError(new StatusException( - Status.INTERNAL.withDescription("Unexpected internal error").withCause(e))); + error = + Status.INTERNAL.withDescription("Unexpected internal error").withCause(e).asException(); } + + responseObserver.onError(error); return false; } private ClientStatusResponse getConfigDumpForRequest(ClientStatusRequest request) - throws StatusException { + throws StatusException, InterruptedException { if (request.getNodeMatchersCount() > 0) { throw new StatusException( Status.INVALID_ARGUMENT.withDescription("node_matchers not supported")); @@ -140,16 +152,20 @@ private ClientStatusResponse getConfigDumpForRequest(ClientStatusRequest request } @VisibleForTesting - static ClientConfig getClientConfigForXdsClient(XdsClient xdsClient) { + static ClientConfig getClientConfigForXdsClient(XdsClient xdsClient) throws InterruptedException { ClientConfig.Builder builder = ClientConfig.newBuilder() .setNode(xdsClient.getBootstrapInfo().node().toEnvoyProtoNode()); - for (ResourceType type : ResourceType.values()) { - if (type == ResourceType.UNKNOWN) { - continue; - } - Map metadataMap = xdsClient.getSubscribedResourcesMetadata(type); - for (String resourceName : metadataMap.keySet()) { - ResourceMetadata metadata = metadataMap.get(resourceName); + + Map> metadataByType = + awaitSubscribedResourcesMetadata(xdsClient.getSubscribedResourcesMetadataSnapshot()); + + for (Map.Entry> metadataByTypeEntry + : metadataByType.entrySet()) { + ResourceType type = metadataByTypeEntry.getKey(); + Map metadataByResourceName = metadataByTypeEntry.getValue(); + for (Map.Entry metadataEntry : metadataByResourceName.entrySet()) { + String resourceName = metadataEntry.getKey(); + ResourceMetadata metadata = metadataEntry.getValue(); GenericXdsConfig.Builder genericXdsConfigBuilder = GenericXdsConfig.newBuilder() .setTypeUrl(type.typeUrl()) .setName(resourceName) @@ -161,6 +177,7 @@ static ClientConfig getClientConfigForXdsClient(XdsClient xdsClient) { .setXdsConfig(metadata.getRawResource()); } if (metadata.getStatus() == ResourceMetadataStatus.NACKED) { + verifyNotNull(metadata.getErrorState(), "resource %s getErrorState", resourceName); genericXdsConfigBuilder .setErrorState(metadataUpdateFailureStateToProto(metadata.getErrorState())); } @@ -170,6 +187,18 @@ static ClientConfig getClientConfigForXdsClient(XdsClient xdsClient) { return builder.build(); } + private static Map> awaitSubscribedResourcesMetadata( + ListenableFuture>> future) + throws InterruptedException { + try { + // Normally this shouldn't take long, but add some slack for cases like a cold JVM. + return future.get(20, TimeUnit.SECONDS); + } catch (ExecutionException | TimeoutException e) { + // For CSDS' purposes, the exact reason why metadata not loaded isn't important. + throw new RuntimeException(e); + } + } + @VisibleForTesting static ClientResourceStatus metadataStatusToClientStatus(ResourceMetadataStatus status) { switch (status) { diff --git a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java index 09318a8c150..e53439755be 100644 --- a/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java +++ b/xds/src/main/java/io/grpc/xds/EnvoyServerProtoData.java @@ -16,17 +16,14 @@ package io.grpc.xds; -import static com.google.common.base.Preconditions.checkNotNull; - +import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.CommonTlsContext; import io.grpc.Internal; import io.grpc.xds.internal.sds.SslContextProviderSupplier; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Collections; -import java.util.List; import java.util.Objects; import javax.annotation.Nullable; @@ -142,47 +139,16 @@ public int hashCode() { } } - static final class CidrRange { - private final InetAddress addressPrefix; - private final int prefixLen; - - CidrRange(String addressPrefix, int prefixLen) throws UnknownHostException { - this.addressPrefix = InetAddress.getByName(addressPrefix); - this.prefixLen = prefixLen; - } - - public InetAddress getAddressPrefix() { - return addressPrefix; - } - - public int getPrefixLen() { - return prefixLen; - } + @AutoValue + abstract static class CidrRange { - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CidrRange cidrRange = (CidrRange) o; - return prefixLen == cidrRange.prefixLen - && java.util.Objects.equals(addressPrefix, cidrRange.addressPrefix); - } + abstract InetAddress addressPrefix(); - @Override - public int hashCode() { - return java.util.Objects.hash(addressPrefix, prefixLen); - } + abstract int prefixLen(); - @Override - public String toString() { - return "CidrRange{" - + "addressPrefix='" + addressPrefix + '\'' - + ", prefixLen=" + prefixLen - + '}'; + static CidrRange create(String addressPrefix, int prefixLen) throws UnknownHostException { + return new AutoValue_EnvoyServerProtoData_CidrRange( + InetAddress.getByName(addressPrefix), prefixLen); } } @@ -199,261 +165,93 @@ enum ConnectionSourceType { /** * Corresponds to Envoy proto message - * {@link io.envoyproxy.envoy.api.v2.listener.FilterChainMatch}. + * {@link io.envoyproxy.envoy.config.listener.v3.FilterChainMatch}. */ - static final class FilterChainMatch { - private final int destinationPort; - private final List prefixRanges; - private final List applicationProtocols; - private final List sourcePrefixRanges; - private final ConnectionSourceType sourceType; - private final List sourcePorts; - private final List serverNames; - private final String transportProtocol; - - @VisibleForTesting - FilterChainMatch( - int destinationPort, - List prefixRanges, - List applicationProtocols, - List sourcePrefixRanges, - ConnectionSourceType sourceType, - List sourcePorts, - List serverNames, - String transportProtocol) { - this.destinationPort = destinationPort; - this.prefixRanges = Collections.unmodifiableList(prefixRanges); - this.applicationProtocols = Collections.unmodifiableList(applicationProtocols); - this.sourcePrefixRanges = sourcePrefixRanges; - this.sourceType = sourceType; - this.sourcePorts = sourcePorts; - this.serverNames = Collections.unmodifiableList(serverNames); - this.transportProtocol = transportProtocol; - } - - public int getDestinationPort() { - return destinationPort; - } + @AutoValue + abstract static class FilterChainMatch { - public List getPrefixRanges() { - return prefixRanges; - } + abstract int destinationPort(); - public List getApplicationProtocols() { - return applicationProtocols; - } + abstract ImmutableList prefixRanges(); - public List getSourcePrefixRanges() { - return sourcePrefixRanges; - } + abstract ImmutableList applicationProtocols(); - public ConnectionSourceType getConnectionSourceType() { - return sourceType; - } + abstract ImmutableList sourcePrefixRanges(); - public List getSourcePorts() { - return sourcePorts; - } + abstract ConnectionSourceType connectionSourceType(); - public List getServerNames() { - return serverNames; - } + abstract ImmutableList sourcePorts(); - public String getTransportProtocol() { - return transportProtocol; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FilterChainMatch that = (FilterChainMatch) o; - return destinationPort == that.destinationPort - && Objects.equals(prefixRanges, that.prefixRanges) - && Objects.equals(applicationProtocols, that.applicationProtocols) - && Objects.equals(sourcePrefixRanges, that.sourcePrefixRanges) - && sourceType == that.sourceType - && Objects.equals(sourcePorts, that.sourcePorts) - && Objects.equals(serverNames, that.serverNames) - && Objects.equals(transportProtocol, that.transportProtocol); - } + abstract ImmutableList serverNames(); - @Override - public int hashCode() { - return Objects.hash( - destinationPort, - prefixRanges, - applicationProtocols, - sourcePrefixRanges, - sourceType, - sourcePorts, - serverNames, - transportProtocol); - } + abstract String transportProtocol(); - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("destinationPort", destinationPort) - .add("prefixRanges", prefixRanges) - .add("applicationProtocols", applicationProtocols) - .add("sourcePrefixRanges", sourcePrefixRanges) - .add("sourceType", sourceType) - .add("sourcePorts", sourcePorts) - .add("serverNames", serverNames) - .add("transportProtocol", transportProtocol) - .toString(); + public static FilterChainMatch create(int destinationPort, + ImmutableList prefixRanges, + ImmutableList applicationProtocols, ImmutableList sourcePrefixRanges, + ConnectionSourceType connectionSourceType, ImmutableList sourcePorts, + ImmutableList serverNames, String transportProtocol) { + return new AutoValue_EnvoyServerProtoData_FilterChainMatch( + destinationPort, prefixRanges, applicationProtocols, sourcePrefixRanges, + connectionSourceType, sourcePorts, serverNames, transportProtocol); } } /** - * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.api.v2.listener.FilterChain}. + * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.FilterChain}. */ - static final class FilterChain { + @AutoValue + abstract static class FilterChain { + // possibly empty - private final String name; + abstract String name(); + // TODO(sanjaypujare): flatten structure by moving FilterChainMatch class members here. - private final FilterChainMatch filterChainMatch; - private final HttpConnectionManager httpConnectionManager; + abstract FilterChainMatch filterChainMatch(); + + abstract HttpConnectionManager httpConnectionManager(); + @Nullable - private final SslContextProviderSupplier sslContextProviderSupplier; + abstract SslContextProviderSupplier sslContextProviderSupplier(); - FilterChain( + static FilterChain create( String name, FilterChainMatch filterChainMatch, HttpConnectionManager httpConnectionManager, @Nullable DownstreamTlsContext downstreamTlsContext, TlsContextManager tlsContextManager) { - SslContextProviderSupplier sslContextProviderSupplier1 = downstreamTlsContext == null ? null - : new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager); - this.name = checkNotNull(name, "name"); - // TODO(chengyuanzhang): enforce non-null, change tests to use a default/empty - // FilterChainMatch instead of null, as that's how the proto is converted. - this.filterChainMatch = filterChainMatch; - this.sslContextProviderSupplier = sslContextProviderSupplier1; - this.httpConnectionManager = checkNotNull(httpConnectionManager, "httpConnectionManager"); - } - - String getName() { - return name; - } - - public FilterChainMatch getFilterChainMatch() { - return filterChainMatch; - } - - HttpConnectionManager getHttpConnectionManager() { - return httpConnectionManager; - } - - @Nullable - public SslContextProviderSupplier getSslContextProviderSupplier() { - return sslContextProviderSupplier; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FilterChain that = (FilterChain) o; - return Objects.equals(name, that.name) - && Objects.equals(filterChainMatch, that.filterChainMatch) - && Objects.equals(httpConnectionManager, that.httpConnectionManager) - && Objects.equals(sslContextProviderSupplier, that.sslContextProviderSupplier); - } - - @Override - public int hashCode() { - return Objects.hash( + SslContextProviderSupplier sslContextProviderSupplier = + downstreamTlsContext == null + ? null : new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager); + return new AutoValue_EnvoyServerProtoData_FilterChain( name, filterChainMatch, httpConnectionManager, sslContextProviderSupplier); } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", name) - .add("filterChainMatch", filterChainMatch) - .add("httpConnectionManager", httpConnectionManager) - .add("sslContextProviderSupplier", sslContextProviderSupplier) - .toString(); - } } /** - * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.api.v2.Listener} & related - * classes. + * Corresponds to Envoy proto message {@link io.envoyproxy.envoy.config.listener.v3.Listener} and + * related classes. */ - public static final class Listener { - private final String name; - @Nullable - private final String address; - private final List filterChains; - @Nullable - private final FilterChain defaultFilterChain; - - /** Construct a Listener. */ - public Listener(String name, @Nullable String address, - List filterChains, @Nullable FilterChain defaultFilterChain) { - this.name = checkNotNull(name, "name"); - this.address = address; - this.filterChains = Collections.unmodifiableList(checkNotNull(filterChains, "filterChains")); - this.defaultFilterChain = defaultFilterChain; - } + @AutoValue + abstract static class Listener { - public String getName() { - return name; - } + abstract String name(); @Nullable - public String getAddress() { - return address; - } + abstract String address(); - public List getFilterChains() { - return filterChains; - } + abstract ImmutableList filterChains(); @Nullable - public FilterChain getDefaultFilterChain() { - return defaultFilterChain; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Listener listener = (Listener) o; - return Objects.equals(name, listener.name) - && Objects.equals(address, listener.address) - && Objects.equals(filterChains, listener.filterChains) - && Objects.equals(defaultFilterChain, listener.defaultFilterChain); - } + abstract FilterChain defaultFilterChain(); - @Override - public int hashCode() { - return Objects.hash(name, address, filterChains, defaultFilterChain); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", name) - .add("address", address) - .add("filterChains", filterChains) - .add("defaultFilterChain", defaultFilterChain) - .toString(); + static Listener create( + String name, + @Nullable String address, + ImmutableList filterChains, + @Nullable FilterChain defaultFilterChain) { + return new AutoValue_EnvoyServerProtoData_Listener(name, address, filterChains, + defaultFilterChain); } } } diff --git a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java index 5d72151db50..e75440225dc 100644 --- a/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java +++ b/xds/src/main/java/io/grpc/xds/FilterChainMatchingProtocolNegotiators.java @@ -39,7 +39,6 @@ import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.EnvoyServerProtoData.FilterChainMatch; -import io.grpc.xds.FilterChainMatchingProtocolNegotiators.FilterChainMatchingHandler.FilterChainSelector; import io.grpc.xds.XdsServerWrapper.ServerRoutingConfig; import io.grpc.xds.internal.Matchers.CidrMatcher; import io.grpc.xds.internal.sds.SslContextProviderSupplier; @@ -189,7 +188,7 @@ SelectedConfig select(InetSocketAddress localAddr, InetSocketAddress remoteAddr) if (filterChains.size() == 1) { FilterChain selected = Iterables.getOnlyElement(filterChains); return new SelectedConfig( - routingConfigs.get(selected), selected.getSslContextProviderSupplier()); + routingConfigs.get(selected), selected.sslContextProviderSupplier()); } if (defaultRoutingConfig.get() != null) { return new SelectedConfig(defaultRoutingConfig, defaultSslContextProviderSupplier); @@ -202,9 +201,9 @@ private static Collection filterOnApplicationProtocols( Collection filterChains) { ArrayList filtered = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); - if (filterChainMatch.getApplicationProtocols().isEmpty()) { + if (filterChainMatch.applicationProtocols().isEmpty()) { filtered.add(filterChain); } } @@ -216,9 +215,9 @@ private static Collection filterOnTransportProtocol( Collection filterChains) { ArrayList filtered = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); - String transportProtocol = filterChainMatch.getTransportProtocol(); + String transportProtocol = filterChainMatch.transportProtocol(); if (Strings.isNullOrEmpty(transportProtocol) || "raw_buffer".equals(transportProtocol)) { filtered.add(filterChain); } @@ -231,9 +230,9 @@ private static Collection filterOnServerNames( Collection filterChains) { ArrayList filtered = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); - if (filterChainMatch.getServerNames().isEmpty()) { + if (filterChainMatch.serverNames().isEmpty()) { filtered.add(filterChain); } } @@ -245,9 +244,9 @@ private static Collection filterOnDestinationPort( Collection filterChains) { ArrayList filtered = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); - if (filterChainMatch.getDestinationPort() + if (filterChainMatch.destinationPort() == UInt32Value.getDefaultInstance().getValue()) { filtered.add(filterChain); } @@ -260,9 +259,9 @@ private static Collection filterOnSourcePort( ArrayList filteredOnMatch = new ArrayList<>(filterChains.size()); ArrayList filteredOnEmpty = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); - List sourcePortsToMatch = filterChainMatch.getSourcePorts(); + List sourcePortsToMatch = filterChainMatch.sourcePorts(); if (sourcePortsToMatch.isEmpty()) { filteredOnEmpty.add(filterChain); } else if (sourcePortsToMatch.contains(sourcePort)) { @@ -278,9 +277,9 @@ private static Collection filterOnSourceType( InetAddress destAddress) { ArrayList filtered = new ArrayList<>(filterChains.size()); for (FilterChain filterChain : filterChains) { - FilterChainMatch filterChainMatch = filterChain.getFilterChainMatch(); + FilterChainMatch filterChainMatch = filterChain.filterChainMatch(); ConnectionSourceType sourceType = - filterChainMatch.getConnectionSourceType(); + filterChainMatch.connectionSourceType(); boolean matching = false; if (sourceType == ConnectionSourceType.SAME_IP_OR_LOOPBACK) { @@ -305,18 +304,18 @@ private static int getMatchingPrefixLength( boolean isIPv6 = address instanceof Inet6Address; List cidrRanges = forDestination - ? filterChainMatch.getPrefixRanges() - : filterChainMatch.getSourcePrefixRanges(); + ? filterChainMatch.prefixRanges() + : filterChainMatch.sourcePrefixRanges(); int matchingPrefixLength; if (cidrRanges.isEmpty()) { // if there is no CidrRange assume 0-length match matchingPrefixLength = 0; } else { matchingPrefixLength = -1; for (CidrRange cidrRange : cidrRanges) { - InetAddress cidrAddr = cidrRange.getAddressPrefix(); + InetAddress cidrAddr = cidrRange.addressPrefix(); boolean cidrIsIpv6 = cidrAddr instanceof Inet6Address; if (isIPv6 == cidrIsIpv6) { - int prefixLen = cidrRange.getPrefixLen(); + int prefixLen = cidrRange.prefixLen(); CidrMatcher matcher = CidrMatcher.create(cidrAddr, prefixLen); if (matcher.matches(address) && prefixLen > matchingPrefixLength) { matchingPrefixLength = prefixLen; @@ -335,7 +334,7 @@ private static Collection filterOnIpAddress( int topMatchingPrefixLen = -1; for (FilterChain filterChain : filterChains) { int currentMatchingPrefixLen = getMatchingPrefixLength( - filterChain.getFilterChainMatch(), address, forDestination); + filterChain.filterChainMatch(), address, forDestination); if (currentMatchingPrefixLen >= 0) { if (currentMatchingPrefixLen < topMatchingPrefixLen) { diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java new file mode 100644 index 00000000000..114300c9281 --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import io.grpc.Internal; +import java.util.Map; + +/** + * Accessor for global factory for managing XdsClient instance. + */ +@Internal +public final class InternalSharedXdsClientPoolProvider { + // Prevent instantiation + private InternalSharedXdsClientPoolProvider() {} + + public static void setDefaultProviderBootstrapOverride(Map bootstrap) { + SharedXdsClientPoolProvider.getDefaultProvider().setBootstrapOverride(bootstrap); + } +} diff --git a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java new file mode 100644 index 00000000000..584ac2dd16f --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancer.java @@ -0,0 +1,430 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.READY; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.xds.LeastRequestLoadBalancerProvider.DEFAULT_CHOICE_COUNT; +import static io.grpc.xds.LeastRequestLoadBalancerProvider.MAX_CHOICE_COUNT; +import static io.grpc.xds.LeastRequestLoadBalancerProvider.MIN_CHOICE_COUNT; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import io.grpc.Attributes; +import io.grpc.ClientStreamTracer; +import io.grpc.ClientStreamTracer.StreamInfo; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nonnull; + +/** + * A {@link LoadBalancer} that provides least request load balancing based on + * outstanding request counters. + * It works by sampling a number of subchannels and picking the one with the + * fewest amount of outstanding requests. + * The default sampling amount of two is also known as + * the "power of two choices" (P2C). + */ +final class LeastRequestLoadBalancer extends LoadBalancer { + @VisibleForTesting + static final Attributes.Key> STATE_INFO = + Attributes.Key.create("state-info"); + @VisibleForTesting + static final Attributes.Key IN_FLIGHTS = + Attributes.Key.create("in-flights"); + + private final Helper helper; + private final ThreadSafeRandom random; + private final Map subchannels = + new HashMap<>(); + + private ConnectivityState currentState; + private LeastRequestPicker currentPicker = new EmptyPicker(EMPTY_OK); + private int choiceCount = DEFAULT_CHOICE_COUNT; + + LeastRequestLoadBalancer(Helper helper) { + this(helper, ThreadSafeRandomImpl.instance); + } + + @VisibleForTesting + LeastRequestLoadBalancer(Helper helper, ThreadSafeRandom random) { + this.helper = checkNotNull(helper, "helper"); + this.random = checkNotNull(random, "random"); + } + + @Override + public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { + LeastRequestConfig config = + (LeastRequestConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); + // Config may be null if least_request is used outside xDS + if (config != null) { + choiceCount = config.choiceCount; + } + + List servers = resolvedAddresses.getAddresses(); + Set currentAddrs = subchannels.keySet(); + Map latestAddrs = stripAttrs(servers); + Set removedAddrs = setsDifference(currentAddrs, latestAddrs.keySet()); + + for (Map.Entry latestEntry : + latestAddrs.entrySet()) { + EquivalentAddressGroup strippedAddressGroup = latestEntry.getKey(); + EquivalentAddressGroup originalAddressGroup = latestEntry.getValue(); + Subchannel existingSubchannel = subchannels.get(strippedAddressGroup); + if (existingSubchannel != null) { + // EAG's Attributes may have changed. + existingSubchannel.updateAddresses(Collections.singletonList(originalAddressGroup)); + continue; + } + // Create new subchannels for new addresses. + Attributes.Builder subchannelAttrs = Attributes.newBuilder() + .set(STATE_INFO, new Ref<>(ConnectivityStateInfo.forNonError(IDLE))) + // Used to track the in flight requests on this particular subchannel + .set(IN_FLIGHTS, new AtomicInteger(0)); + + final Subchannel subchannel = checkNotNull( + helper.createSubchannel(CreateSubchannelArgs.newBuilder() + .setAddresses(originalAddressGroup) + .setAttributes(subchannelAttrs.build()) + .build()), + "subchannel"); + subchannel.start(new SubchannelStateListener() { + @Override + public void onSubchannelState(ConnectivityStateInfo state) { + processSubchannelState(subchannel, state); + } + }); + subchannels.put(strippedAddressGroup, subchannel); + subchannel.requestConnection(); + } + + ArrayList removedSubchannels = new ArrayList<>(); + for (EquivalentAddressGroup addressGroup : removedAddrs) { + removedSubchannels.add(subchannels.remove(addressGroup)); + } + + // Update the picker before shutting down the subchannels, to reduce the chance of the race + // between picking a subchannel and shutting it down. + updateBalancingState(); + + // Shutdown removed subchannels + for (Subchannel removedSubchannel : removedSubchannels) { + shutdownSubchannel(removedSubchannel); + } + } + + @Override + public void handleNameResolutionError(Status error) { + if (currentState != READY) { + updateBalancingState(TRANSIENT_FAILURE, new EmptyPicker(error)); + } + } + + private void processSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) { + if (subchannels.get(stripAttrs(subchannel.getAddresses())) != subchannel) { + return; + } + if (stateInfo.getState() == TRANSIENT_FAILURE || stateInfo.getState() == IDLE) { + helper.refreshNameResolution(); + } + if (stateInfo.getState() == IDLE) { + subchannel.requestConnection(); + } + Ref subchannelStateRef = getSubchannelStateInfoRef(subchannel); + if (subchannelStateRef.value.getState().equals(TRANSIENT_FAILURE)) { + if (stateInfo.getState().equals(CONNECTING) || stateInfo.getState().equals(IDLE)) { + return; + } + } + subchannelStateRef.value = stateInfo; + updateBalancingState(); + } + + private void shutdownSubchannel(Subchannel subchannel) { + subchannel.shutdown(); + getSubchannelStateInfoRef(subchannel).value = + ConnectivityStateInfo.forNonError(SHUTDOWN); + } + + @Override + public void shutdown() { + for (Subchannel subchannel : getSubchannels()) { + shutdownSubchannel(subchannel); + } + subchannels.clear(); + } + + private static final Status EMPTY_OK = Status.OK.withDescription("no subchannels ready"); + + /** + * Updates picker with the list of active subchannels (state == READY). + */ + @SuppressWarnings("ReferenceEquality") + private void updateBalancingState() { + List activeList = filterNonFailingSubchannels(getSubchannels()); + if (activeList.isEmpty()) { + // No READY subchannels, determine aggregate state and error status + boolean isConnecting = false; + Status aggStatus = EMPTY_OK; + for (Subchannel subchannel : getSubchannels()) { + ConnectivityStateInfo stateInfo = getSubchannelStateInfoRef(subchannel).value; + // This subchannel IDLE is not because of channel IDLE_TIMEOUT, + // in which case LB is already shutdown. + // LRLB will request connection immediately on subchannel IDLE. + if (stateInfo.getState() == CONNECTING || stateInfo.getState() == IDLE) { + isConnecting = true; + } + if (aggStatus == EMPTY_OK || !aggStatus.isOk()) { + aggStatus = stateInfo.getStatus(); + } + } + updateBalancingState(isConnecting ? CONNECTING : TRANSIENT_FAILURE, + // If all subchannels are TRANSIENT_FAILURE, return the Status associated with + // an arbitrary subchannel, otherwise return OK. + new EmptyPicker(aggStatus)); + } else { + updateBalancingState(READY, new ReadyPicker(activeList, choiceCount, random)); + } + } + + private void updateBalancingState(ConnectivityState state, LeastRequestPicker picker) { + if (state != currentState || !picker.isEquivalentTo(currentPicker)) { + helper.updateBalancingState(state, picker); + currentState = state; + currentPicker = picker; + } + } + + /** + * Filters out non-ready subchannels. + */ + private static List filterNonFailingSubchannels( + Collection subchannels) { + List readySubchannels = new ArrayList<>(subchannels.size()); + for (Subchannel subchannel : subchannels) { + if (isReady(subchannel)) { + readySubchannels.add(subchannel); + } + } + return readySubchannels; + } + + /** + * Converts list of {@link EquivalentAddressGroup} to {@link EquivalentAddressGroup} set and + * remove all attributes. The values are the original EAGs. + */ + private static Map stripAttrs( + List groupList) { + Map addrs = new HashMap<>(groupList.size() * 2); + for (EquivalentAddressGroup group : groupList) { + addrs.put(stripAttrs(group), group); + } + return addrs; + } + + private static EquivalentAddressGroup stripAttrs(EquivalentAddressGroup eag) { + return new EquivalentAddressGroup(eag.getAddresses()); + } + + @VisibleForTesting + Collection getSubchannels() { + return subchannels.values(); + } + + private static Ref getSubchannelStateInfoRef( + Subchannel subchannel) { + return checkNotNull(subchannel.getAttributes().get(STATE_INFO), "STATE_INFO"); + } + + private static AtomicInteger getInFlights(Subchannel subchannel) { + return checkNotNull(subchannel.getAttributes().get(IN_FLIGHTS), "IN_FLIGHTS"); + } + + // package-private to avoid synthetic access + static boolean isReady(Subchannel subchannel) { + return getSubchannelStateInfoRef(subchannel).value.getState() == READY; + } + + private static Set setsDifference(Set a, Set b) { + Set aCopy = new HashSet<>(a); + aCopy.removeAll(b); + return aCopy; + } + + // Only subclasses are ReadyPicker or EmptyPicker + private abstract static class LeastRequestPicker extends SubchannelPicker { + abstract boolean isEquivalentTo(LeastRequestPicker picker); + } + + @VisibleForTesting + static final class ReadyPicker extends LeastRequestPicker { + private final List list; // non-empty + private final int choiceCount; + private final ThreadSafeRandom random; + + ReadyPicker(List list, int choiceCount, ThreadSafeRandom random) { + checkArgument(!list.isEmpty(), "empty list"); + this.list = list; + this.choiceCount = choiceCount; + this.random = checkNotNull(random, "random"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + final Subchannel subchannel = nextSubchannel(); + final OutstandingRequestsTracingFactory factory = + new OutstandingRequestsTracingFactory(getInFlights(subchannel)); + return PickResult.withSubchannel(subchannel, factory); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(ReadyPicker.class) + .add("list", list) + .add("choiceCount", choiceCount) + .toString(); + } + + private Subchannel nextSubchannel() { + Subchannel candidate = list.get(random.nextInt(list.size())); + for (int i = 0; i < choiceCount - 1; ++i) { + Subchannel sampled = list.get(random.nextInt(list.size())); + if (getInFlights(sampled).get() < getInFlights(candidate).get()) { + candidate = sampled; + } + } + return candidate; + } + + @VisibleForTesting + List getList() { + return list; + } + + @Override + boolean isEquivalentTo(LeastRequestPicker picker) { + if (!(picker instanceof ReadyPicker)) { + return false; + } + ReadyPicker other = (ReadyPicker) picker; + // the lists cannot contain duplicate subchannels + return other == this + || ((list.size() == other.list.size() && new HashSet<>(list).containsAll(other.list)) + && choiceCount == other.choiceCount); + } + } + + @VisibleForTesting + static final class EmptyPicker extends LeastRequestPicker { + + private final Status status; + + EmptyPicker(@Nonnull Status status) { + this.status = Preconditions.checkNotNull(status, "status"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return status.isOk() ? PickResult.withNoResult() : PickResult.withError(status); + } + + @Override + boolean isEquivalentTo(LeastRequestPicker picker) { + return picker instanceof EmptyPicker && (Objects.equal(status, ((EmptyPicker) picker).status) + || (status.isOk() && ((EmptyPicker) picker).status.isOk())); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(EmptyPicker.class).add("status", status).toString(); + } + } + + /** + * A lighter weight Reference than AtomicReference. + */ + static final class Ref { + T value; + + Ref(T value) { + this.value = value; + } + } + + private static final class OutstandingRequestsTracingFactory extends + ClientStreamTracer.Factory { + private final AtomicInteger inFlights; + + private OutstandingRequestsTracingFactory(AtomicInteger inFlights) { + this.inFlights = checkNotNull(inFlights, "inFlights"); + } + + @Override + public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) { + return new ClientStreamTracer() { + @Override + public void streamCreated(Attributes transportAttrs, Metadata headers) { + inFlights.incrementAndGet(); + } + + @Override + public void streamClosed(Status status) { + inFlights.decrementAndGet(); + } + }; + } + } + + static final class LeastRequestConfig { + final int choiceCount; + + LeastRequestConfig(int choiceCount) { + checkArgument(choiceCount >= MIN_CHOICE_COUNT, "choiceCount <= 1"); + // Even though a choiceCount value larger than 2 is currently considered valid in xDS + // we restrict it to 10 here as specified in "A48: xDS Least Request LB Policy". + this.choiceCount = Math.min(choiceCount, MAX_CHOICE_COUNT); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("choiceCount", choiceCount) + .toString(); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancerProvider.java new file mode 100644 index 00000000000..3abac1d2f0d --- /dev/null +++ b/xds/src/main/java/io/grpc/xds/LeastRequestLoadBalancerProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.Internal; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status; +import io.grpc.internal.JsonUtil; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; +import java.util.Map; + +/** + * Provider for the "least_request_experimental" balancing policy. + */ +@Internal +public final class LeastRequestLoadBalancerProvider extends LoadBalancerProvider { + // Minimum number of choices allowed. + static final int MIN_CHOICE_COUNT = 2; + // Maximum number of choices allowed. + static final int MAX_CHOICE_COUNT = 10; + // Same as ClientXdsClient.DEFAULT_LEAST_REQUEST_CHOICE_COUNT + @VisibleForTesting + static final Integer DEFAULT_CHOICE_COUNT = 2; + + @Override + public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) { + return new LeastRequestLoadBalancer(helper); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public int getPriority() { + return 5; + } + + @Override + public String getPolicyName() { + return "least_request_experimental"; + } + + @Override + public ConfigOrError parseLoadBalancingPolicyConfig(Map rawConfig) { + try { + Integer choiceCount = JsonUtil.getNumberAsInteger(rawConfig, "choiceCount"); + if (choiceCount == null) { + choiceCount = DEFAULT_CHOICE_COUNT; + } + if (choiceCount < MIN_CHOICE_COUNT) { + return ConfigOrError.fromError(Status.INVALID_ARGUMENT.withDescription( + "Invalid 'choiceCount'")); + } + return ConfigOrError.fromConfig(new LeastRequestConfig(choiceCount)); + } catch (RuntimeException e) { + return ConfigOrError.fromError( + Status.fromThrowable(e).withDescription( + "Failed to parse least_request_experimental LB config: " + rawConfig)); + } + } +} diff --git a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java index 80ddfd8a865..0678ffc34b6 100644 --- a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java @@ -41,6 +41,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -66,6 +67,7 @@ final class PriorityLoadBalancer extends LoadBalancer { private List priorityNames; // Config for each priority. private Map priorityConfigs; + @Nullable private String currentPriority; private ConnectivityState currentConnectivityState; private SubchannelPicker currentPicker; @@ -113,7 +115,7 @@ public void handleNameResolutionError(Status error) { } } if (gotoTransientFailure) { - updateOverallState(TRANSIENT_FAILURE, new ErrorPicker(error)); + updateOverallState(null, TRANSIENT_FAILURE, new ErrorPicker(error)); } } @@ -134,14 +136,14 @@ private void tryNextPriority(boolean reportConnecting) { new ChildLbState(priority, priorityConfigs.get(priority).ignoreReresolution); children.put(priority, child); child.updateResolvedAddresses(); - updateOverallState(CONNECTING, BUFFER_PICKER); + updateOverallState(priority, CONNECTING, BUFFER_PICKER); return; // Give priority i time to connect. } ChildLbState child = children.get(priority); child.reactivate(); if (child.connectivityState.equals(READY) || child.connectivityState.equals(IDLE)) { logger.log(XdsLogLevel.DEBUG, "Shifted to priority {0}", priority); - updateOverallState(child.connectivityState, child.picker); + updateOverallState(priority, child.connectivityState, child.picker); for (int j = i + 1; j < priorityNames.size(); j++) { String p = priorityNames.get(j); if (children.containsKey(p)) { @@ -152,20 +154,28 @@ private void tryNextPriority(boolean reportConnecting) { } if (child.failOverTimer != null && child.failOverTimer.isPending()) { if (reportConnecting) { - updateOverallState(CONNECTING, BUFFER_PICKER); + updateOverallState(priority, CONNECTING, BUFFER_PICKER); } return; // Give priority i time to connect. } + if (priority.equals(currentPriority) && child.connectivityState != TRANSIENT_FAILURE) { + // If the current priority is not changed into TRANSIENT_FAILURE, keep using it. + updateOverallState(priority, child.connectivityState, child.picker); + return; + } } // TODO(zdapeng): Include error details of each priority. logger.log(XdsLogLevel.DEBUG, "All priority failed"); String lastPriority = priorityNames.get(priorityNames.size() - 1); SubchannelPicker errorPicker = children.get(lastPriority).picker; - updateOverallState(TRANSIENT_FAILURE, errorPicker); + updateOverallState(lastPriority, TRANSIENT_FAILURE, errorPicker); } - private void updateOverallState(ConnectivityState state, SubchannelPicker picker) { - if (!state.equals(currentConnectivityState) || !picker.equals(currentPicker)) { + private void updateOverallState( + @Nullable String priority, ConnectivityState state, SubchannelPicker picker) { + if (!Objects.equals(priority, currentPriority) || !state.equals(currentConnectivityState) + || !picker.equals(currentPicker)) { + currentPriority = priority; currentConnectivityState = state; currentPicker = picker; helper.updateBalancingState(state, picker); @@ -201,6 +211,7 @@ public void run() { picker = new ErrorPicker( Status.UNAVAILABLE.withDescription("Connection timeout for priority " + priority)); logger.log(XdsLogLevel.DEBUG, "Priority {0} failed over to next", priority); + currentPriority = null; // reset currentPriority to guarantee failover happen tryNextPriority(true); } } @@ -302,7 +313,8 @@ public void run() { return; } if (failOverTimer.isPending()) { - if (newState.equals(READY) || newState.equals(TRANSIENT_FAILURE)) { + if (newState.equals(READY) || newState.equals(IDLE) + || newState.equals(TRANSIENT_FAILURE)) { failOverTimer.cancel(); } } diff --git a/xds/src/main/java/io/grpc/xds/XdsClient.java b/xds/src/main/java/io/grpc/xds/XdsClient.java index 024500253a3..1c231b83a19 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClient.java +++ b/xds/src/main/java/io/grpc/xds/XdsClient.java @@ -17,10 +17,15 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.xds.Bootstrapper.XDSTP_SCHEME; import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; +import com.google.common.net.UrlEscapers; +import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.Any; import io.grpc.Status; import io.grpc.xds.AbstractXdsClient.ResourceType; @@ -31,6 +36,8 @@ import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.LoadStatsManager2.ClusterDropStats; import io.grpc.xds.LoadStatsManager2.ClusterLocalityStats; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -48,6 +55,67 @@ */ abstract class XdsClient { + static boolean isResourceNameValid(String resourceName, String typeUrl) { + checkNotNull(resourceName, "resourceName"); + if (!resourceName.startsWith(XDSTP_SCHEME)) { + return true; + } + URI uri; + try { + uri = new URI(resourceName); + } catch (URISyntaxException e) { + return false; + } + String path = uri.getPath(); + // path must be in the form of /{resource type}/{id/*} + Splitter slashSplitter = Splitter.on('/').omitEmptyStrings(); + if (path == null) { + return false; + } + List pathSegs = slashSplitter.splitToList(path); + if (pathSegs.size() < 2) { + return false; + } + String type = pathSegs.get(0); + if (!type.equals(slashSplitter.splitToList(typeUrl).get(1))) { + return false; + } + return true; + } + + static String canonifyResourceName(String resourceName) { + checkNotNull(resourceName, "resourceName"); + if (!resourceName.startsWith(XDSTP_SCHEME)) { + return resourceName; + } + URI uri = URI.create(resourceName); + String rawQuery = uri.getRawQuery(); + Splitter ampSplitter = Splitter.on('&').omitEmptyStrings(); + if (rawQuery == null) { + return resourceName; + } + List queries = ampSplitter.splitToList(rawQuery); + if (queries.size() < 2) { + return resourceName; + } + List canonicalContextParams = new ArrayList<>(queries.size()); + for (String query : queries) { + canonicalContextParams.add(query); + } + Collections.sort(canonicalContextParams); + String canonifiedQuery = Joiner.on('&').join(canonicalContextParams); + return resourceName.replace(rawQuery, canonifiedQuery); + } + + static String percentEncodePath(String input) { + Iterable pathSegs = Splitter.on('/').split(input); + List encodedSegs = new ArrayList<>(); + for (String pathSeg : pathSegs) { + encodedSegs.add(UrlEscapers.urlPathSegmentEscaper().escape(pathSeg)); + } + return Joiner.on('/').join(encodedSegs); + } + @AutoValue abstract static class LdsUpdate implements ResourceUpdate { // Http level api listener configuration. @@ -119,6 +187,9 @@ abstract static class CdsUpdate implements ResourceUpdate { // Only valid if lbPolicy is "ring_hash_experimental". abstract long maxRingSize(); + // Only valid if lbPolicy is "least_request_experimental". + abstract int choiceCount(); + // Alternative resource name to be used in EDS requests. /// Only valid for EDS cluster. @Nullable @@ -157,6 +228,7 @@ static Builder forAggregate(String clusterName, List prioritizedClusterN .clusterType(ClusterType.AGGREGATE) .minRingSize(0) .maxRingSize(0) + .choiceCount(0) .prioritizedClusterNames(ImmutableList.copyOf(prioritizedClusterNames)); } @@ -168,6 +240,7 @@ static Builder forEds(String clusterName, @Nullable String edsServiceName, .clusterType(ClusterType.EDS) .minRingSize(0) .maxRingSize(0) + .choiceCount(0) .edsServiceName(edsServiceName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) @@ -182,6 +255,7 @@ static Builder forLogicalDns(String clusterName, String dnsHostName, .clusterType(ClusterType.LOGICAL_DNS) .minRingSize(0) .maxRingSize(0) + .choiceCount(0) .dnsHostName(dnsHostName) .lrsServerInfo(lrsServerInfo) .maxConcurrentRequests(maxConcurrentRequests) @@ -193,7 +267,7 @@ enum ClusterType { } enum LbPolicy { - ROUND_ROBIN, RING_HASH + ROUND_ROBIN, RING_HASH, LEAST_REQUEST } // FIXME(chengyuanzhang): delete this after UpstreamTlsContext's toString() is fixed. @@ -205,6 +279,7 @@ public final String toString() { .add("lbPolicy", lbPolicy()) .add("minRingSize", minRingSize()) .add("maxRingSize", maxRingSize()) + .add("choiceCount", choiceCount()) .add("edsServiceName", edsServiceName()) .add("dnsHostName", dnsHostName()) .add("lrsServerInfo", lrsServerInfo()) @@ -233,6 +308,13 @@ Builder ringHashLbPolicy(long minRingSize, long maxRingSize) { return this.lbPolicy(LbPolicy.RING_HASH).minRingSize(minRingSize).maxRingSize(maxRingSize); } + Builder leastRequestLbPolicy(int choiceCount) { + return this.lbPolicy(LbPolicy.LEAST_REQUEST).choiceCount(choiceCount); + } + + // Private, use leastRequestLbPolicy(int). + protected abstract Builder choiceCount(int choiceCount); + // Private, use ringHashLbPolicy(long, long). protected abstract Builder minRingSize(long minRingSize); @@ -494,7 +576,16 @@ TlsContextManager getTlsContextManager() { throw new UnsupportedOperationException(); } - Map getSubscribedResourcesMetadata(ResourceType type) { + /** + * Returns a {@link ListenableFuture} to the snapshot of the subscribed resources as + * they are at the moment of the call. + * + *

The snapshot is a map from the "resource type" to + * a map ("resource name": "resource metadata"). + */ + // Must be synchronized. + ListenableFuture>> + getSubscribedResourcesMetadataSnapshot() { throw new UnsupportedOperationException(); } diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index b6b66327525..ed7b849f8be 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -23,9 +23,9 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; -import com.google.common.net.UrlEscapers; import com.google.gson.Gson; import com.google.protobuf.util.Durations; import io.grpc.Attributes; @@ -47,11 +47,14 @@ import io.grpc.SynchronizationContext; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; +import io.grpc.xds.AbstractXdsClient.ResourceType; import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.BootstrapInfo; +import io.grpc.xds.ClusterSpecifierPlugin.PluginConfig; import io.grpc.xds.Filter.ClientInterceptorBuilder; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; +import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteAction; @@ -70,13 +73,13 @@ import io.grpc.xds.internal.Matchers.FractionMatcher; import io.grpc.xds.internal.Matchers.HeaderMatcher; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -116,7 +119,8 @@ final class XdsNameResolver extends NameResolver { private final FilterRegistry filterRegistry; private final XxHash64 hashFunc = XxHash64.INSTANCE; // Clusters (with reference counts) to which new/existing requests can be/are routed. - private final ConcurrentMap clusterRefs = new ConcurrentHashMap<>(); + // put()/remove() must be called in SyncContext, and get() can be called in any thread. + private final ConcurrentMap clusterRefs = new ConcurrentHashMap<>(); private final ConfigSelector configSelector = new ConfigSelector(); private volatile RoutingConfig routingConfig = RoutingConfig.empty; @@ -125,6 +129,9 @@ final class XdsNameResolver extends NameResolver { private XdsClient xdsClient; private CallCounterProvider callCounterProvider; private ResolveState resolveState; + // Workaround for https://github.com/grpc/grpc-java/issues/8886 . This should be handled in + // XdsClient instead of here. + private boolean receivedConfig; XdsNameResolver( @Nullable String targetAuthority, String name, ServiceConfigParser serviceConfigParser, @@ -187,9 +194,16 @@ public void start(Listener2 listener) { } String replacement = serviceAuthority; if (listenerNameTemplate.startsWith(XDSTP_SCHEME)) { - replacement = UrlEscapers.urlFragmentEscaper().escape(replacement); + replacement = XdsClient.percentEncodePath(replacement); } String ldsResourceName = expandPercentS(listenerNameTemplate, replacement); + if (!XdsClient.isResourceNameValid(ldsResourceName, ResourceType.LDS.typeUrl()) + && !XdsClient.isResourceNameValid(ldsResourceName, ResourceType.LDS.typeUrlV2())) { + listener.onError(Status.INVALID_ARGUMENT.withDescription( + "invalid listener resource URI for service authority: " + serviceAuthority)); + return; + } + ldsResourceName = XdsClient.canonifyResourceName(ldsResourceName); callCounterProvider = SharedCallCounterMap.getInstance(); resolveState = new ResolveState(ldsResourceName); resolveState.start(); @@ -246,31 +260,25 @@ public void shutdown() { "methodConfig", Collections.singletonList(methodConfig.build())); } - @VisibleForTesting - static Map generateServiceConfigWithLoadBalancingConfig(Collection clusters) { - Map childPolicy = new HashMap<>(); - for (String cluster : clusters) { - List>> lbPolicy = - Collections.singletonList( - Collections.singletonMap( - "cds_experimental", Collections.singletonMap("cluster", cluster))); - childPolicy.put(cluster, Collections.singletonMap("lbPolicy", lbPolicy)); - } - return Collections.singletonMap("loadBalancingConfig", - Collections.singletonList( - Collections.singletonMap( - "cluster_manager_experimental", Collections.singletonMap( - "childPolicy", Collections.unmodifiableMap(childPolicy))))); - } - @VisibleForTesting XdsClient getXdsClient() { return xdsClient; } + // called in syncContext private void updateResolutionResult() { - Map rawServiceConfig = - generateServiceConfigWithLoadBalancingConfig(clusterRefs.keySet()); + syncContext.throwIfNotInThisSynchronizationContext(); + + ImmutableMap.Builder childPolicy = new ImmutableMap.Builder<>(); + for (String name : clusterRefs.keySet()) { + Map lbPolicy = clusterRefs.get(name).toLbPolicy(); + childPolicy.put(name, ImmutableMap.of("lbPolicy", ImmutableList.of(lbPolicy))); + } + Map rawServiceConfig = ImmutableMap.of( + "loadBalancingConfig", + ImmutableList.of(ImmutableMap.of( + "cluster_manager_experimental", ImmutableMap.of("childPolicy", childPolicy.build())))); + if (logger.isLoggable(XdsLogLevel.INFO)) { logger.log( XdsLogLevel.INFO, "Generated service config:\n{0}", new Gson().toJson(rawServiceConfig)); @@ -288,6 +296,7 @@ private void updateResolutionResult() { .setServiceConfig(parsedServiceConfig) .build(); listener.onResult(result); + receivedConfig = true; } @VisibleForTesting @@ -421,7 +430,7 @@ public Result selectConfig(PickSubchannelArgs args) { } RouteAction action = selectedRoute.routeAction(); if (action.cluster() != null) { - cluster = action.cluster(); + cluster = prefixedClusterName(action.cluster()); } else if (action.weightedClusters() != null) { int totalWeight = 0; for (ClusterWeight weightedCluster : action.weightedClusters()) { @@ -432,11 +441,14 @@ public Result selectConfig(PickSubchannelArgs args) { for (ClusterWeight weightedCluster : action.weightedClusters()) { accumulator += weightedCluster.weight(); if (select < accumulator) { - cluster = weightedCluster.name(); + cluster = prefixedClusterName(weightedCluster.name()); selectedOverrideConfigs.putAll(weightedCluster.filterConfigOverrides()); break; } } + } else if (action.namedClusterSpecifierPluginConfig() != null) { + cluster = + prefixedClusterSpecifierPluginName(action.namedClusterSpecifierPluginConfig().name()); } } while (!retainCluster(cluster)); Long timeoutNanos = null; @@ -526,10 +538,11 @@ public void onClose(Status status, Metadata trailers) { } private boolean retainCluster(String cluster) { - AtomicInteger refCount = clusterRefs.get(cluster); - if (refCount == null) { + ClusterRefState clusterRefState = clusterRefs.get(cluster); + if (clusterRefState == null) { return false; } + AtomicInteger refCount = clusterRefState.refCount; int count; do { count = refCount.get(); @@ -541,12 +554,12 @@ private boolean retainCluster(String cluster) { } private void releaseCluster(final String cluster) { - int count = clusterRefs.get(cluster).decrementAndGet(); + int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { syncContext.execute(new Runnable() { @Override public void run() { - if (clusterRefs.get(cluster).get() == 0) { + if (clusterRefs.get(cluster).refCount.get() == 0) { clusterRefs.remove(cluster); updateResolutionResult(); } @@ -647,6 +660,14 @@ private static String getHeaderValue(Metadata headers, String headerName) { return values == null ? null : Joiner.on(",").join(values); } + private static String prefixedClusterName(String name) { + return "cluster:" + name; + } + + private static String prefixedClusterSpecifierPluginName(String pluginName) { + return "cluster_specifier_plugin:" + pluginName; + } + private class ResolveState implements LdsResourceWatcher { private final ConfigOrError emptyServiceConfig = serviceConfigParser.parseServiceConfig(Collections.emptyMap()); @@ -698,10 +719,12 @@ public void onError(final Status error) { syncContext.execute(new Runnable() { @Override public void run() { - if (stopped) { + if (stopped || receivedConfig) { return; } - listener.onError(error); + listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription( + String.format("Unable to load LDS %s. xDS server returned: %s: %s.", + ldsResourceName, error.getCode(), error.getDescription()))); } }); } @@ -733,6 +756,7 @@ private void stop() { xdsClient.cancelLdsResourceWatch(ldsResourceName, this); } + // called in syncContext private void updateRoutes(List virtualHosts, long httpMaxStreamDurationNano, @Nullable List filterConfigs) { VirtualHost virtualHost = findVirtualHostForHostName(virtualHosts, ldsResourceName); @@ -747,14 +771,31 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura // Populate all clusters to which requests can be routed to through the virtual host. Set clusters = new HashSet<>(); + // uniqueName -> clusterName + Map clusterNameMap = new HashMap<>(); + // uniqueName -> pluginConfig + Map rlsPluginConfigMap = new HashMap<>(); for (Route route : routes) { RouteAction action = route.routeAction(); + String prefixedName; if (action != null) { if (action.cluster() != null) { - clusters.add(action.cluster()); + prefixedName = prefixedClusterName(action.cluster()); + clusters.add(prefixedName); + clusterNameMap.put(prefixedName, action.cluster()); } else if (action.weightedClusters() != null) { for (ClusterWeight weighedCluster : action.weightedClusters()) { - clusters.add(weighedCluster.name()); + prefixedName = prefixedClusterName(weighedCluster.name()); + clusters.add(prefixedName); + clusterNameMap.put(prefixedName, weighedCluster.name()); + } + } else if (action.namedClusterSpecifierPluginConfig() != null) { + PluginConfig pluginConfig = action.namedClusterSpecifierPluginConfig().config(); + if (pluginConfig instanceof RlsPluginConfig) { + prefixedName = prefixedClusterSpecifierPluginName( + action.namedClusterSpecifierPluginConfig().name()); + clusters.add(prefixedName); + rlsPluginConfigMap.put(prefixedName, (RlsPluginConfig) pluginConfig); } } } @@ -770,9 +811,28 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura existingClusters = clusters; for (String cluster : addedClusters) { if (clusterRefs.containsKey(cluster)) { - clusterRefs.get(cluster).incrementAndGet(); + clusterRefs.get(cluster).refCount.incrementAndGet(); } else { - clusterRefs.put(cluster, new AtomicInteger(1)); + if (clusterNameMap.containsKey(cluster)) { + clusterRefs.put( + cluster, + ClusterRefState.forCluster(new AtomicInteger(1), clusterNameMap.get(cluster))); + } + if (rlsPluginConfigMap.containsKey(cluster)) { + clusterRefs.put( + cluster, + ClusterRefState.forRlsPlugin( + new AtomicInteger(1), rlsPluginConfigMap.get(cluster))); + } + shouldUpdateResult = true; + } + } + for (String cluster : clusters) { + RlsPluginConfig rlsPluginConfig = rlsPluginConfigMap.get(cluster); + if (!Objects.equals(rlsPluginConfig, clusterRefs.get(cluster).rlsPluginConfig)) { + ClusterRefState newClusterRefState = + ClusterRefState.forRlsPlugin(clusterRefs.get(cluster).refCount, rlsPluginConfig); + clusterRefs.put(cluster, newClusterRefState); shouldUpdateResult = true; } } @@ -788,7 +848,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura virtualHost.filterConfigOverrides()); shouldUpdateResult = false; for (String cluster : deletedClusters) { - int count = clusterRefs.get(cluster).decrementAndGet(); + int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { clusterRefs.remove(cluster); shouldUpdateResult = true; @@ -802,7 +862,7 @@ private void updateRoutes(List virtualHosts, long httpMaxStreamDura private void cleanUpRoutes() { if (existingClusters != null) { for (String cluster : existingClusters) { - int count = clusterRefs.get(cluster).decrementAndGet(); + int count = clusterRefs.get(cluster).refCount.decrementAndGet(); if (count == 0) { clusterRefs.remove(cluster); } @@ -811,6 +871,7 @@ private void cleanUpRoutes() { } routingConfig = RoutingConfig.empty; listener.onResult(emptyResult); + receivedConfig = true; } private void cleanUpRouteDiscoveryState() { @@ -858,10 +919,12 @@ public void onError(final Status error) { syncContext.execute(new Runnable() { @Override public void run() { - if (RouteDiscoveryState.this != routeDiscoveryState) { + if (RouteDiscoveryState.this != routeDiscoveryState || receivedConfig) { return; } - listener.onError(error); + listener.onError(Status.UNAVAILABLE.withCause(error.getCause()).withDescription( + String.format("Unable to load RDS %s. xDS server returned: %s: %s.", + resourceName, error.getCode(), error.getDescription()))); } }); } @@ -905,4 +968,45 @@ private RoutingConfig( this.virtualHostOverrideConfig = Collections.unmodifiableMap(virtualHostOverrideConfig); } } + + private static class ClusterRefState { + final AtomicInteger refCount; + @Nullable + final String traditionalCluster; + @Nullable + final RlsPluginConfig rlsPluginConfig; + + private ClusterRefState( + AtomicInteger refCount, @Nullable String traditionalCluster, + @Nullable RlsPluginConfig rlsPluginConfig) { + this.refCount = refCount; + checkArgument(traditionalCluster == null ^ rlsPluginConfig == null, + "There must be exactly one non-null value in traditionalCluster and pluginConfig"); + this.traditionalCluster = traditionalCluster; + this.rlsPluginConfig = rlsPluginConfig; + } + + private Map toLbPolicy() { + if (traditionalCluster != null) { + return ImmutableMap.of("cds_experimental", ImmutableMap.of("cluster", traditionalCluster)); + } else { + ImmutableMap rlsConfig = new ImmutableMap.Builder() + .put("routeLookupConfig", rlsPluginConfig.config()) + .put( + "childPolicy", + ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of()))) + .put("childPolicyConfigTargetFieldName", "cluster") + .build(); + return ImmutableMap.of("rls_experimental", rlsConfig); + } + } + + static ClusterRefState forCluster(AtomicInteger refCount, String name) { + return new ClusterRefState(refCount, name, null); + } + + static ClusterRefState forRlsPlugin(AtomicInteger refCount, RlsPluginConfig rlsPluginConfig) { + return new ClusterRefState(refCount, null, rlsPluginConfig); + } + } } diff --git a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java index 0946f621487..eef4a9c2fb3 100644 --- a/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java +++ b/xds/src/main/java/io/grpc/xds/XdsServerWrapper.java @@ -24,7 +24,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.net.UrlEscapers; import com.google.common.util.concurrent.SettableFuture; import io.grpc.Attributes; import io.grpc.InternalServerInterceptors; @@ -196,7 +195,7 @@ private void internalStart() { } String replacement = listenerAddress; if (listenerTemplate.startsWith(XDSTP_SCHEME)) { - replacement = UrlEscapers.urlFragmentEscaper().escape(replacement); + replacement = XdsClient.percentEncodePath(replacement); } discoveryState = new DiscoveryState(listenerTemplate.replaceAll("%s", replacement)); } @@ -387,8 +386,8 @@ public void run() { releaseSuppliersInFlight(); pendingRds.clear(); } - filterChains = update.listener().getFilterChains(); - defaultFilterChain = update.listener().getDefaultFilterChain(); + filterChains = update.listener().filterChains(); + defaultFilterChain = update.listener().defaultFilterChain(); List allFilterChains = filterChains; if (defaultFilterChain != null) { allFilterChains = new ArrayList<>(filterChains); @@ -396,7 +395,7 @@ public void run() { } Set allRds = new HashSet<>(); for (FilterChain filterChain : allFilterChains) { - HttpConnectionManager hcm = filterChain.getHttpConnectionManager(); + HttpConnectionManager hcm = filterChain.httpConnectionManager(); if (hcm.virtualHosts() == null) { RouteDiscoveryState rdsState = routeDiscoveryStates.get(hcm.rdsName()); if (rdsState == null) { @@ -479,7 +478,7 @@ private void updateSelector() { } FilterChainSelector selector = new FilterChainSelector( Collections.unmodifiableMap(filterChainRouting), - defaultFilterChain == null ? null : defaultFilterChain.getSslContextProviderSupplier(), + defaultFilterChain == null ? null : defaultFilterChain.sslContextProviderSupplier(), defaultFilterChain == null ? new AtomicReference() : generateRoutingConfig(defaultFilterChain)); List toRelease = getSuppliersInUse(); @@ -492,7 +491,7 @@ private void updateSelector() { } private AtomicReference generateRoutingConfig(FilterChain filterChain) { - HttpConnectionManager hcm = filterChain.getHttpConnectionManager(); + HttpConnectionManager hcm = filterChain.httpConnectionManager(); if (hcm.virtualHosts() != null) { ImmutableMap interceptors = generatePerRouteInterceptors( hcm.httpFilterConfigs(), hcm.virtualHosts()); @@ -603,8 +602,8 @@ private List getSuppliersInUse() { FilterChainSelector selector = filterChainSelectorManager.getSelectorToUpdateSelector(); if (selector != null) { for (FilterChain f: selector.getRoutingConfigs().keySet()) { - if (f.getSslContextProviderSupplier() != null) { - toRelease.add(f.getSslContextProviderSupplier()); + if (f.sslContextProviderSupplier() != null) { + toRelease.add(f.sslContextProviderSupplier()); } } SslContextProviderSupplier defaultSupplier = @@ -619,13 +618,13 @@ private List getSuppliersInUse() { private void releaseSuppliersInFlight() { SslContextProviderSupplier supplier; for (FilterChain filterChain : filterChains) { - supplier = filterChain.getSslContextProviderSupplier(); + supplier = filterChain.sslContextProviderSupplier(); if (supplier != null) { supplier.close(); } } if (defaultFilterChain != null - && (supplier = defaultFilterChain.getSslContextProviderSupplier()) != null) { + && (supplier = defaultFilterChain.sslContextProviderSupplier()) != null) { supplier.close(); } } @@ -690,20 +689,20 @@ public void run() { private void updateRdsRoutingConfig() { for (FilterChain filterChain : savedRdsRoutingConfigRef.keySet()) { - if (resourceName.equals(filterChain.getHttpConnectionManager().rdsName())) { + if (resourceName.equals(filterChain.httpConnectionManager().rdsName())) { ServerRoutingConfig updatedRoutingConfig; if (savedVirtualHosts == null) { updatedRoutingConfig = ServerRoutingConfig.FAILING_ROUTING_CONFIG; } else { ImmutableMap updatedInterceptors = generatePerRouteInterceptors( - filterChain.getHttpConnectionManager().httpFilterConfigs(), + filterChain.httpConnectionManager().httpFilterConfigs(), savedVirtualHosts); updatedRoutingConfig = ServerRoutingConfig.create(savedVirtualHosts, updatedInterceptors); } logger.log(Level.FINEST, "Updating filter chain {0} rds routing config: {1}", - new Object[]{filterChain.getName(), updatedRoutingConfig}); + new Object[]{filterChain.name(), updatedRoutingConfig}); savedRdsRoutingConfigRef.get(filterChain).set(updatedRoutingConfig); } } diff --git a/xds/src/main/java/io/grpc/xds/internal/sds/CommonTlsContextUtil.java b/xds/src/main/java/io/grpc/xds/internal/sds/CommonTlsContextUtil.java index 0c28c79ee22..df09e8bb247 100644 --- a/xds/src/main/java/io/grpc/xds/internal/sds/CommonTlsContextUtil.java +++ b/xds/src/main/java/io/grpc/xds/internal/sds/CommonTlsContextUtil.java @@ -26,9 +26,11 @@ public final class CommonTlsContextUtil { private CommonTlsContextUtil() {} static boolean hasCertProviderInstance(CommonTlsContext commonTlsContext) { - return commonTlsContext != null - && (commonTlsContext.hasTlsCertificateCertificateProviderInstance() - || hasCertProviderValidationContext(commonTlsContext)); + if (commonTlsContext == null) { + return false; + } + return hasIdentityCertificateProviderInstance(commonTlsContext) + || hasCertProviderValidationContext(commonTlsContext); } private static boolean hasCertProviderValidationContext(CommonTlsContext commonTlsContext) { @@ -37,6 +39,19 @@ private static boolean hasCertProviderValidationContext(CommonTlsContext commonT commonTlsContext.getCombinedValidationContext(); return combinedCertificateValidationContext.hasValidationContextCertificateProviderInstance(); } + return hasValidationProviderInstance(commonTlsContext); + } + + private static boolean hasIdentityCertificateProviderInstance(CommonTlsContext commonTlsContext) { + return commonTlsContext.hasTlsCertificateProviderInstance() + || commonTlsContext.hasTlsCertificateCertificateProviderInstance(); + } + + private static boolean hasValidationProviderInstance(CommonTlsContext commonTlsContext) { + if (commonTlsContext.hasValidationContext() && commonTlsContext.getValidationContext() + .hasCaCertificateProviderInstance()) { + return true; + } return commonTlsContext.hasValidationContextCertificateProviderInstance(); } diff --git a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider index 7ba3dcf22f5..8e5c2dd1c6a 100644 --- a/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ b/xds/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -4,4 +4,5 @@ io.grpc.xds.WeightedTargetLoadBalancerProvider io.grpc.xds.ClusterManagerLoadBalancerProvider io.grpc.xds.ClusterResolverLoadBalancerProvider io.grpc.xds.ClusterImplLoadBalancerProvider +io.grpc.xds.LeastRequestLoadBalancerProvider io.grpc.xds.RingHashLoadBalancerProvider diff --git a/xds/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/xds/src/main/resources/META-INF/services/io.grpc.NameResolverProvider index c1f2c40e7ee..269cdd38801 100644 --- a/xds/src/main/resources/META-INF/services/io.grpc.NameResolverProvider +++ b/xds/src/main/resources/META-INF/services/io.grpc.NameResolverProvider @@ -1,2 +1 @@ io.grpc.xds.XdsNameResolverProvider -io.grpc.xds.GoogleCloudToProdNameResolverProvider diff --git a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java index 7664660d4c6..78e6d6473ca 100644 --- a/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java +++ b/xds/src/test/java/io/grpc/xds/CdsLoadBalancer2Test.java @@ -48,6 +48,7 @@ import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig; import io.grpc.xds.ClusterResolverLoadBalancerProvider.ClusterResolverConfig.DiscoveryMechanism; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; import io.grpc.xds.XdsClient.CdsUpdate; import io.grpc.xds.internal.sds.CommonTlsContextTestsUtil; @@ -121,6 +122,7 @@ public void setUp() { lbRegistry.register(new FakeLoadBalancerProvider(CLUSTER_RESOLVER_POLICY_NAME)); lbRegistry.register(new FakeLoadBalancerProvider("round_robin")); lbRegistry.register(new FakeLoadBalancerProvider("ring_hash_experimental")); + lbRegistry.register(new FakeLoadBalancerProvider("least_request_experimental")); loadBalancer = new CdsLoadBalancer2(helper, lbRegistry); loadBalancer.handleResolvedAddresses( ResolvedAddresses.newBuilder() @@ -164,7 +166,7 @@ public void discoverTopLevelEdsCluster() { public void discoverTopLevelLogicalDnsCluster() { CdsUpdate update = CdsUpdate.forLogicalDns(CLUSTER, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext) - .roundRobinLbPolicy().build(); + .leastRequestLbPolicy(3).build(); xdsClient.deliverCdsUpdate(CLUSTER, update); assertThat(childBalancers).hasSize(1); FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); @@ -174,7 +176,9 @@ public void discoverTopLevelLogicalDnsCluster() { DiscoveryMechanism instance = Iterables.getOnlyElement(childLbConfig.discoveryMechanisms); assertDiscoveryMechanism(instance, CLUSTER, DiscoveryMechanism.Type.LOGICAL_DNS, null, DNS_HOST_NAME, LRS_SERVER_INFO, 100L, upstreamTlsContext); - assertThat(childLbConfig.lbPolicy.getProvider().getPolicyName()).isEqualTo("round_robin"); + assertThat(childLbConfig.lbPolicy.getProvider().getPolicyName()) + .isEqualTo("least_request_experimental"); + assertThat(((LeastRequestConfig) childLbConfig.lbPolicy.getConfig()).choiceCount).isEqualTo(3); } @Test diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java index 940d96fd10e..2b75c02d4dc 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientDataTest.java @@ -37,6 +37,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType; import io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig.HashFunction; import io.envoyproxy.envoy.config.core.v3.Address; @@ -47,6 +48,7 @@ import io.envoyproxy.envoy.config.core.v3.HttpProtocolOptions; import io.envoyproxy.envoy.config.core.v3.Locality; import io.envoyproxy.envoy.config.core.v3.RuntimeFractionalPercent; +import io.envoyproxy.envoy.config.core.v3.SelfConfigSource; import io.envoyproxy.envoy.config.core.v3.SocketAddress; import io.envoyproxy.envoy.config.core.v3.TrafficDirection; import io.envoyproxy.envoy.config.core.v3.TransportSocket; @@ -109,6 +111,7 @@ import io.grpc.lookup.v1.NameMatcher; import io.grpc.lookup.v1.RouteLookupClusterSpecifier; import io.grpc.lookup.v1.RouteLookupConfig; +import io.grpc.xds.AbstractXdsClient.ResourceType; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.ClientXdsClient.ResourceInvalidException; import io.grpc.xds.ClientXdsClient.StructOrError; @@ -132,6 +135,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -156,6 +160,7 @@ public class ClientXdsClientDataTest { private boolean originalEnableRetry; private boolean originalEnableRbac; private boolean originalEnableRouteLookup; + private boolean originalEnableLeastRequest; @Before public void setUp() { @@ -165,6 +170,8 @@ public void setUp() { assertThat(originalEnableRbac).isTrue(); originalEnableRouteLookup = ClientXdsClient.enableRouteLookup; assertThat(originalEnableRouteLookup).isFalse(); + originalEnableLeastRequest = ClientXdsClient.enableLeastRequest; + assertThat(originalEnableLeastRequest).isFalse(); } @After @@ -172,6 +179,7 @@ public void tearDown() { ClientXdsClient.enableRetry = originalEnableRetry; ClientXdsClient.enableRbac = originalEnableRbac; ClientXdsClient.enableRouteLookup = originalEnableRouteLookup; + ClientXdsClient.enableLeastRequest = originalEnableLeastRequest; } @Test @@ -1552,6 +1560,48 @@ public void parseHttpConnectionManager_pluginNameNotFound() throws Exception { true /* does not matter */); } + @Test + public void parseHttpConnectionManager_validateRdsConfigSource() throws Exception { + ClientXdsClient.enableRouteLookup = true; + Set rdsResources = new HashSet<>(); + + HttpConnectionManager hcm1 = + HttpConnectionManager.newBuilder() + .setRds(Rds.newBuilder() + .setRouteConfigName("rds-config-foo") + .setConfigSource( + ConfigSource.newBuilder().setAds(AggregatedConfigSource.getDefaultInstance()))) + .build(); + ClientXdsClient.parseHttpConnectionManager( + hcm1, rdsResources, filterRegistry, false /* parseHttpFilter */, + true /* does not matter */); + + HttpConnectionManager hcm2 = + HttpConnectionManager.newBuilder() + .setRds(Rds.newBuilder() + .setRouteConfigName("rds-config-foo") + .setConfigSource( + ConfigSource.newBuilder().setSelf(SelfConfigSource.getDefaultInstance()))) + .build(); + ClientXdsClient.parseHttpConnectionManager( + hcm2, rdsResources, filterRegistry, false /* parseHttpFilter */, + true /* does not matter */); + + HttpConnectionManager hcm3 = + HttpConnectionManager.newBuilder() + .setRds(Rds.newBuilder() + .setRouteConfigName("rds-config-foo") + .setConfigSource( + ConfigSource.newBuilder().setPath("foo-path"))) + .build(); + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage( + "HttpConnectionManager contains invalid RDS: must specify ADS or self ConfigSource"); + ClientXdsClient.parseHttpConnectionManager( + hcm3, rdsResources, filterRegistry, false /* parseHttpFilter */, + true /* does not matter */); + } + @Test public void parseClusterSpecifierPlugin_typedStructInTypedExtension() throws Exception { class TestPluginConfig implements PluginConfig { @@ -1667,6 +1717,28 @@ public void parseCluster_ringHashLbPolicy_defaultLbConfig() throws ResourceInval .isEqualTo(ClientXdsClient.DEFAULT_RING_HASH_LB_POLICY_MAX_RING_SIZE); } + @Test + public void parseCluster_leastRequestLbPolicy_defaultLbConfig() throws ResourceInvalidException { + ClientXdsClient.enableLeastRequest = true; + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.LEAST_REQUEST) + .build(); + + CdsUpdate update = ClientXdsClient.processCluster( + cluster, new HashSet(), null, LRS_SERVER_INFO); + assertThat(update.lbPolicy()).isEqualTo(CdsUpdate.LbPolicy.LEAST_REQUEST); + assertThat(update.choiceCount()) + .isEqualTo(ClientXdsClient.DEFAULT_LEAST_REQUEST_CHOICE_COUNT); + } + @Test public void parseCluster_transportSocketMatches_exception() throws ResourceInvalidException { Cluster cluster = Cluster.newBuilder() @@ -1741,6 +1813,79 @@ public void parseCluster_ringHashLbPolicy_invalidRingSizeConfig_tooLargeRingSize ClientXdsClient.processCluster(cluster, new HashSet(), null, LRS_SERVER_INFO); } + @Test + public void parseCluster_leastRequestLbPolicy_invalidChoiceCountConfig_tooSmallChoiceCount() + throws ResourceInvalidException { + ClientXdsClient.enableLeastRequest = true; + Cluster cluster = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.LEAST_REQUEST) + .setLeastRequestLbConfig( + LeastRequestLbConfig.newBuilder() + .setChoiceCount(UInt32Value.newBuilder().setValue(1)) + ) + .build(); + + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage("Cluster cluster-foo.googleapis.com: invalid least_request_lb_config"); + ClientXdsClient.processCluster(cluster, new HashSet(), null, LRS_SERVER_INFO); + } + + @Test + public void parseCluster_validateEdsSourceConfig() throws ResourceInvalidException { + Set retainedEdsResources = new HashSet<>(); + Cluster cluster1 = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setAds(AggregatedConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .build(); + ClientXdsClient.processCluster(cluster1, retainedEdsResources, null, LRS_SERVER_INFO); + + Cluster cluster2 = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setSelf(SelfConfigSource.getDefaultInstance())) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .build(); + ClientXdsClient.processCluster(cluster2, retainedEdsResources, null, LRS_SERVER_INFO); + + Cluster cluster3 = Cluster.newBuilder() + .setName("cluster-foo.googleapis.com") + .setType(DiscoveryType.EDS) + .setEdsClusterConfig( + EdsClusterConfig.newBuilder() + .setEdsConfig( + ConfigSource.newBuilder() + .setPath("foo-path")) + .setServiceName("service-foo.googleapis.com")) + .setLbPolicy(LbPolicy.ROUND_ROBIN) + .build(); + + thrown.expect(ResourceInvalidException.class); + thrown.expectMessage( + "Cluster cluster-foo.googleapis.com: field eds_cluster_config must be set to indicate to" + + " use EDS over ADS or self ConfigSource"); + ClientXdsClient.processCluster(cluster3, retainedEdsResources, null, LRS_SERVER_INFO); + } + @Test public void parseServerSideListener_invalidTrafficDirection() throws ResourceInvalidException { Listener listener = @@ -2035,7 +2180,7 @@ public void parseFilterChain_noName() throws ResourceInvalidException { EnvoyServerProtoData.FilterChain parsedFilterChain2 = ClientXdsClient.parseFilterChain( filterChain2, new HashSet(), null, filterRegistry, null, null, true /* does not matter */); - assertThat(parsedFilterChain1.getName()).isEqualTo(parsedFilterChain2.getName()); + assertThat(parsedFilterChain1.name()).isEqualTo(parsedFilterChain2.name()); } @Test @@ -2429,6 +2574,76 @@ public void validateUpstreamTlsContext_noCommonTlsContext() throws ResourceInval ClientXdsClient.validateUpstreamTlsContext(upstreamTlsContext, null); } + @Test + public void validateResourceName() { + String traditionalResource = "cluster1.google.com"; + assertThat(XdsClient.isResourceNameValid(traditionalResource, ResourceType.CDS.typeUrl())) + .isTrue(); + assertThat(XdsClient.isResourceNameValid(traditionalResource, ResourceType.RDS.typeUrlV2())) + .isTrue(); + + String invalidPath = "xdstp:/abc/efg"; + assertThat(XdsClient.isResourceNameValid(invalidPath, ResourceType.CDS.typeUrl())).isFalse(); + + String invalidPath2 = "xdstp:///envoy.config.route.v3.RouteConfiguration"; + assertThat(XdsClient.isResourceNameValid(invalidPath2, ResourceType.RDS.typeUrl())).isFalse(); + + String typeMatch = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1"; + assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.LDS.typeUrl())).isFalse(); + assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.RDS.typeUrl())).isTrue(); + assertThat(XdsClient.isResourceNameValid(typeMatch, ResourceType.RDS.typeUrlV2())).isFalse(); + } + + @Test + public void canonifyResourceName() { + String traditionalResource = "cluster1.google.com"; + assertThat(XdsClient.canonifyResourceName(traditionalResource)) + .isEqualTo(traditionalResource); + assertThat(XdsClient.canonifyResourceName(traditionalResource)) + .isEqualTo(traditionalResource); + assertThat(XdsClient.canonifyResourceName(traditionalResource)) + .isEqualTo(traditionalResource); + assertThat(XdsClient.canonifyResourceName(traditionalResource)) + .isEqualTo(traditionalResource); + + String withNoQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1"; + assertThat(XdsClient.canonifyResourceName(withNoQueries)).isEqualTo(withNoQueries); + + String withOneQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/foo/route1?name=foo"; + assertThat(XdsClient.canonifyResourceName(withOneQueries)).isEqualTo(withOneQueries); + + String withTwoQueries = "xdstp:///envoy.config.route.v3.RouteConfiguration/id/route1?b=1&a=1"; + String expectedCanonifiedName = + "xdstp:///envoy.config.route.v3.RouteConfiguration/id/route1?a=1&b=1"; + assertThat(XdsClient.canonifyResourceName(withTwoQueries)) + .isEqualTo(expectedCanonifiedName); + } + + /** + * Tests compliance with RFC 3986 section 3.3 + * https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + */ + @Test + public void percentEncodePath() { + String unreserved = "aAzZ09-._~"; + assertThat(XdsClient.percentEncodePath(unreserved)).isEqualTo(unreserved); + + String subDelims = "!$&'(*+,;/="; + assertThat(XdsClient.percentEncodePath(subDelims)).isEqualTo(subDelims); + + String colonAndAt = ":@"; + assertThat(XdsClient.percentEncodePath(colonAndAt)).isEqualTo(colonAndAt); + + String needBeEncoded = "?#[]"; + assertThat(XdsClient.percentEncodePath(needBeEncoded)).isEqualTo("%3F%23%5B%5D"); + + String ipv4 = "0.0.0.0:8080"; + assertThat(XdsClient.percentEncodePath(ipv4)).isEqualTo(ipv4); + + String ipv6 = "[::1]:8080"; + assertThat(XdsClient.percentEncodePath(ipv6)).isEqualTo("%5B::1%5D:8080"); + } + private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters) { return Filter.newBuilder() .setName("envoy.http_connection_manager") diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java index 42cf9baa1d9..ba182be76d1 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientTestBase.java @@ -205,8 +205,8 @@ public long currentTimeNanos() { // CDS test resources. private final Any testClusterRoundRobin = - Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, false, null, - "envoy.transport_sockets.tls", null + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, + null, false, null, "envoy.transport_sockets.tls", null )); // EDS test resources. @@ -258,6 +258,7 @@ public long currentTimeNanos() { private ClientXdsClient xdsClient; private boolean originalEnableFaultInjection; private boolean originalEnableRbac; + private boolean originalEnableLeastRequest; @Before public void setUp() throws IOException { @@ -272,6 +273,8 @@ public void setUp() throws IOException { ClientXdsClient.enableFaultInjection = true; originalEnableRbac = ClientXdsClient.enableRbac; assertThat(originalEnableRbac).isTrue(); + originalEnableLeastRequest = ClientXdsClient.enableLeastRequest; + ClientXdsClient.enableLeastRequest = true; final String serverName = InProcessServerBuilder.generateName(); cleanupRule.register( InProcessServerBuilder @@ -345,6 +348,7 @@ SERVER_URI_EMPTY_AUTHORITY, CHANNEL_CREDENTIALS, useProtocolV3()))))) public void tearDown() { ClientXdsClient.enableFaultInjection = originalEnableFaultInjection; ClientXdsClient.enableRbac = originalEnableRbac; + ClientXdsClient.enableLeastRequest = originalEnableLeastRequest; xdsClient.shutdown(); channel.shutdown(); // channel not owned by XdsClient assertThat(adsEnded.get()).isTrue(); @@ -379,10 +383,23 @@ protected static boolean matchErrorDetail( private void verifySubscribedResourcesMetadataSizes( int ldsSize, int cdsSize, int rdsSize, int edsSize) { - assertThat(xdsClient.getSubscribedResourcesMetadata(LDS)).hasSize(ldsSize); - assertThat(xdsClient.getSubscribedResourcesMetadata(CDS)).hasSize(cdsSize); - assertThat(xdsClient.getSubscribedResourcesMetadata(RDS)).hasSize(rdsSize); - assertThat(xdsClient.getSubscribedResourcesMetadata(EDS)).hasSize(edsSize); + Map> subscribedResourcesMetadata = + awaitSubscribedResourcesMetadata(); + assertThat(subscribedResourcesMetadata.get(LDS)).hasSize(ldsSize); + assertThat(subscribedResourcesMetadata.get(CDS)).hasSize(cdsSize); + assertThat(subscribedResourcesMetadata.get(RDS)).hasSize(rdsSize); + assertThat(subscribedResourcesMetadata.get(EDS)).hasSize(edsSize); + } + + private Map> awaitSubscribedResourcesMetadata() { + try { + return xdsClient.getSubscribedResourcesMetadataSnapshot().get(20, TimeUnit.SECONDS); + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new AssertionError(e); + } } /** Verify the resource requested, but not updated. */ @@ -434,22 +451,20 @@ private void verifyResourceMetadataNacked( private ResourceMetadata verifyResourceMetadata( ResourceType type, String resourceName, Any rawResource, ResourceMetadataStatus status, String versionInfo, long updateTimeNanos, boolean hasErrorState) { - ResourceMetadata resourceMetadata = - xdsClient.getSubscribedResourcesMetadata(type).get(resourceName); - assertThat(resourceMetadata).isNotNull(); + ResourceMetadata metadata = awaitSubscribedResourcesMetadata().get(type).get(resourceName); + assertThat(metadata).isNotNull(); String name = type.toString() + " resource '" + resourceName + "' metadata field "; - assertWithMessage(name + "status").that(resourceMetadata.getStatus()).isEqualTo(status); - assertWithMessage(name + "version").that(resourceMetadata.getVersion()).isEqualTo(versionInfo); - assertWithMessage(name + "rawResource").that(resourceMetadata.getRawResource()) - .isEqualTo(rawResource); - assertWithMessage(name + "updateTimeNanos").that(resourceMetadata.getUpdateTimeNanos()) + assertWithMessage(name + "status").that(metadata.getStatus()).isEqualTo(status); + assertWithMessage(name + "version").that(metadata.getVersion()).isEqualTo(versionInfo); + assertWithMessage(name + "rawResource").that(metadata.getRawResource()).isEqualTo(rawResource); + assertWithMessage(name + "updateTimeNanos").that(metadata.getUpdateTimeNanos()) .isEqualTo(updateTimeNanos); if (hasErrorState) { - assertWithMessage(name + "errorState").that(resourceMetadata.getErrorState()).isNotNull(); + assertWithMessage(name + "errorState").that(metadata.getErrorState()).isNotNull(); } else { - assertWithMessage(name + "errorState").that(resourceMetadata.getErrorState()).isNull(); + assertWithMessage(name + "errorState").that(metadata.getErrorState()).isNull(); } - return resourceMetadata; + return metadata; } /** @@ -745,8 +760,9 @@ public void ldsResourceUpdated() { @Test public void ldsResourceUpdated_withXdstpResourceName() { - String ldsResourceName = - "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1"; + String ldsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1" + : "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1"; DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); assertThat(channelForCustomAuthority).isNotNull(); verifyResourceMetadataRequested(LDS, ldsResourceName); @@ -764,8 +780,9 @@ public void ldsResourceUpdated_withXdstpResourceName() { @Test public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() { - String ldsResourceName = - "xdstp:///envoy.config.listener.v3.Listener/listener1"; + String ldsResourceName = useProtocolV3() + ? "xdstp:///envoy.config.listener.v3.Listener/listener1" + : "xdstp:///envoy.api.v2.Listener/listener1"; DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); assertThat(channelForEmptyAuthority).isNotNull(); verifyResourceMetadataRequested(LDS, ldsResourceName); @@ -781,6 +798,107 @@ public void ldsResourceUpdated_withXdstpResourceName_withEmptyAuthority() { LDS, ldsResourceName, testListenerVhosts, VERSION_1, TIME_INCREMENT); } + @Test + public void ldsResourceUpdated_withXdstpResourceName_witUnorderedContextParams() { + String ldsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1/a?bar=2&foo=1" + : "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1/a?bar=2&foo=1"; + DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + + String ldsResourceNameWithUnorderedContextParams = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1/a?foo=1&bar=2" + : "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1/a?foo=1&bar=2"; + Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener( + ldsResourceNameWithUnorderedContextParams, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequest( + LDS, ldsResourceName, VERSION_1, "0000", NODE); + } + + @Test + public void ldsResourceUpdated_withXdstpResourceName_withWrongType() { + String ldsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/listener1" + : "xdstp://authority.xds.com/envoy.api.v2.Listener/listener1"; + DiscoveryRpcCall call = startResourceWatcher(LDS, ldsResourceName, ldsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + + String ldsResourceNameWithWrongType = + "xdstp://authority.xds.com/envoy.config.route.v3.RouteConfiguration/listener1"; + Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener( + ldsResourceNameWithWrongType, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + call.sendResponse(LDS, testListenerVhosts, VERSION_1, "0000"); + call.verifyRequestNack( + LDS, ldsResourceName, "", "0000", NODE, + ImmutableList.of( + "Unsupported resource name: " + ldsResourceNameWithWrongType + " for type: LDS")); + } + + @Test + public void rdsResourceUpdated_withXdstpResourceName_withWrongType() { + String rdsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.route.v3.RouteConfiguration/route1" + : "xdstp://authority.xds.com/envoy.api.v2.RouteConfiguration/route1"; + DiscoveryRpcCall call = startResourceWatcher(RDS, rdsResourceName, rdsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + + String rdsResourceNameWithWrongType = + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/route1"; + Any testRouteConfig = Any.pack(mf.buildRouteConfiguration( + rdsResourceNameWithWrongType, mf.buildOpaqueVirtualHosts(VHOST_SIZE))); + call.sendResponse(RDS, testRouteConfig, VERSION_1, "0000"); + call.verifyRequestNack( + RDS, rdsResourceName, "", "0000", NODE, + ImmutableList.of( + "Unsupported resource name: " + rdsResourceNameWithWrongType + " for type: RDS")); + } + + @Test + public void cdsResourceUpdated_withXdstpResourceName_withWrongType() { + String cdsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.cluster.v3.Cluster/cluster1" + : "xdstp://authority.xds.com/envoy.api.v2.Cluster/cluster1"; + DiscoveryRpcCall call = startResourceWatcher(CDS, cdsResourceName, cdsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + + String cdsResourceNameWithWrongType = + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/cluster1"; + Any testClusterConfig = Any.pack(mf.buildEdsCluster( + cdsResourceNameWithWrongType, null, "round_robin", null, null, false, null, + "envoy.transport_sockets.tls", null)); + call.sendResponse(CDS, testClusterConfig, VERSION_1, "0000"); + call.verifyRequestNack( + CDS, cdsResourceName, "", "0000", NODE, + ImmutableList.of( + "Unsupported resource name: " + cdsResourceNameWithWrongType + " for type: CDS")); + } + + @Test + public void edsResourceUpdated_withXdstpResourceName_withWrongType() { + String edsResourceName = useProtocolV3() + ? "xdstp://authority.xds.com/envoy.config.endpoint.v3.ClusterLoadAssignment/cluster1" + : "xdstp://authority.xds.com/envoy.api.v2.ClusterLoadAssignment/cluster1"; + DiscoveryRpcCall call = startResourceWatcher(EDS, edsResourceName, edsResourceWatcher); + assertThat(channelForCustomAuthority).isNotNull(); + + String edsResourceNameWithWrongType = + "xdstp://authority.xds.com/envoy.config.listener.v3.Listener/cluster1"; + Any testEdsConfig = Any.pack(mf.buildClusterLoadAssignment( + edsResourceNameWithWrongType, + ImmutableList.of(mf.buildLocalityLbEndpoints( + "region2", "zone2", "subzone2", + mf.buildLbEndpoint("172.44.2.2", 8000, "unknown", 3), 2, 0)), + ImmutableList.of())); + call.sendResponse(EDS, testEdsConfig, VERSION_1, "0000"); + call.verifyRequestNack( + EDS, edsResourceName, "", "0000", NODE, + ImmutableList.of( + "Unsupported resource name: " + edsResourceNameWithWrongType + " for type: EDS")); + } + @Test public void ldsResourceUpdate_withFaultInjection() { Assume.assumeTrue(useProtocolV3()); @@ -1164,10 +1282,10 @@ public void rdsResourcesDeletedByLdsTcpListener() { call.sendResponse(LDS, packedListener, VERSION_1, "0000"); verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); - assertThat(ldsUpdateCaptor.getValue().listener().getFilterChains()).hasSize(1); + assertThat(ldsUpdateCaptor.getValue().listener().filterChains()).hasSize(1); FilterChain parsedFilterChain = Iterables.getOnlyElement( - ldsUpdateCaptor.getValue().listener().getFilterChains()); - assertThat(parsedFilterChain.getHttpConnectionManager().rdsName()).isEqualTo(RDS_RESOURCE); + ldsUpdateCaptor.getValue().listener().filterChains()); + assertThat(parsedFilterChain.httpConnectionManager().rdsName()).isEqualTo(RDS_RESOURCE); verifyResourceMetadataAcked(LDS, LISTENER_RESOURCE, packedListener, VERSION_1, TIME_INCREMENT); verifyResourceMetadataRequested(RDS, RDS_RESOURCE); verifySubscribedResourcesMetadataSizes(1, 0, 1, 0); @@ -1192,10 +1310,10 @@ public void rdsResourcesDeletedByLdsTcpListener() { Any.pack(mf.buildListenerWithFilterChain(LISTENER_RESOURCE, 7000, "0.0.0.0", filterChain)); call.sendResponse(LDS, packedListener, VERSION_2, "0001"); verify(ldsResourceWatcher, times(2)).onChanged(ldsUpdateCaptor.capture()); - assertThat(ldsUpdateCaptor.getValue().listener().getFilterChains()).hasSize(1); + assertThat(ldsUpdateCaptor.getValue().listener().filterChains()).hasSize(1); parsedFilterChain = Iterables.getOnlyElement( - ldsUpdateCaptor.getValue().listener().getFilterChains()); - assertThat(parsedFilterChain.getHttpConnectionManager().virtualHosts()).hasSize(VHOST_SIZE); + ldsUpdateCaptor.getValue().listener().filterChains()); + assertThat(parsedFilterChain.httpConnectionManager().virtualHosts()).hasSize(VHOST_SIZE); verify(rdsResourceWatcher).onResourceDoesNotExist(RDS_RESOURCE); verifyResourceMetadataDoesNotExist(RDS, RDS_RESOURCE); verifyResourceMetadataAcked( @@ -1253,9 +1371,9 @@ public void cdsResourceNotFound() { List clusters = ImmutableList.of( Any.pack(mf.buildEdsCluster("cluster-bar.googleapis.com", null, "round_robin", null, - false, null, "envoy.transport_sockets.tls", null)), + null, false, null, "envoy.transport_sockets.tls", null)), Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, - false, null, "envoy.transport_sockets.tls", null))); + null, false, null, "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1329,13 +1447,13 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { // CDS -> {A, B, C}, version 1 ImmutableMap resourcesV1 = ImmutableMap.of( - "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, false, null, + "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), - "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, false, null, + "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), - "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, false, null, + "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); @@ -1348,7 +1466,7 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { // CDS -> {A, B}, version 2 // Failed to parse endpoint B ImmutableMap resourcesV2 = ImmutableMap.of( - "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, false, null, + "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), "B", Any.pack(mf.buildClusterInvalid("B"))); @@ -1365,10 +1483,10 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid() { // CDS -> {B, C} version 3 ImmutableMap resourcesV3 = ImmutableMap.of( - "B", Any.pack(mf.buildEdsCluster("B", "B.3", "round_robin", null, false, null, + "B", Any.pack(mf.buildEdsCluster("B", "B.3", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), - "C", Any.pack(mf.buildEdsCluster("C", "C.3", "round_robin", null, false, null, + "C", Any.pack(mf.buildEdsCluster("C", "C.3", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null ))); call.sendResponse(CDS, resourcesV3.values().asList(), VERSION_3, "0002"); @@ -1401,13 +1519,13 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscripti // CDS -> {A, B, C}, version 1 ImmutableMap resourcesV1 = ImmutableMap.of( - "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, false, null, + "A", Any.pack(mf.buildEdsCluster("A", "A.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), - "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, false, null, + "B", Any.pack(mf.buildEdsCluster("B", "B.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), - "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, false, null, + "C", Any.pack(mf.buildEdsCluster("C", "C.1", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null ))); call.sendResponse(CDS, resourcesV1.values().asList(), VERSION_1, "0000"); @@ -1433,7 +1551,7 @@ public void cdsResponseErrorHandling_subscribedResourceInvalid_withEdsSubscripti // CDS -> {A, B}, version 2 // Failed to parse endpoint B ImmutableMap resourcesV2 = ImmutableMap.of( - "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, false, null, + "A", Any.pack(mf.buildEdsCluster("A", "A.2", "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null )), "B", Any.pack(mf.buildClusterInvalid("B"))); @@ -1478,13 +1596,40 @@ public void cdsResourceFound() { verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); } + @Test + public void cdsResourceFound_leastRequestLbPolicy() { + DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); + Message leastRequestConfig = mf.buildLeastRequestLbConfig(3); + Any clusterRingHash = Any.pack( + mf.buildEdsCluster(CDS_RESOURCE, null, "least_request_experimental", null, + leastRequestConfig, false, null, "envoy.transport_sockets.tls", null + )); + call.sendResponse(ResourceType.CDS, clusterRingHash, VERSION_1, "0000"); + + // Client sent an ACK CDS request. + call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); + verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); + CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); + assertThat(cdsUpdate.clusterName()).isEqualTo(CDS_RESOURCE); + assertThat(cdsUpdate.clusterType()).isEqualTo(ClusterType.EDS); + assertThat(cdsUpdate.edsServiceName()).isNull(); + assertThat(cdsUpdate.lbPolicy()).isEqualTo(LbPolicy.LEAST_REQUEST); + assertThat(cdsUpdate.choiceCount()).isEqualTo(3); + assertThat(cdsUpdate.lrsServerInfo()).isNull(); + assertThat(cdsUpdate.maxConcurrentRequests()).isNull(); + assertThat(cdsUpdate.upstreamTlsContext()).isNull(); + assertThat(fakeClock.getPendingTasks(CDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); + verifyResourceMetadataAcked(CDS, CDS_RESOURCE, clusterRingHash, VERSION_1, TIME_INCREMENT); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + } + @Test public void cdsResourceFound_ringHashLbPolicy() { DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); Message ringHashConfig = mf.buildRingHashLbConfig("xx_hash", 10L, 100L); Any clusterRingHash = Any.pack( - mf.buildEdsCluster(CDS_RESOURCE, null, "ring_hash_experimental", ringHashConfig, false, - null, "envoy.transport_sockets.tls", null + mf.buildEdsCluster(CDS_RESOURCE, null, "ring_hash_experimental", ringHashConfig, null, + false, null, "envoy.transport_sockets.tls", null )); call.sendResponse(ResourceType.CDS, clusterRingHash, VERSION_1, "0000"); @@ -1512,7 +1657,7 @@ public void cdsResponseWithAggregateCluster() { List candidates = Arrays.asList( "cluster1.googleapis.com", "cluster2.googleapis.com", "cluster3.googleapis.com"); Any clusterAggregate = - Any.pack(mf.buildAggregateCluster(CDS_RESOURCE, "round_robin", null, candidates)); + Any.pack(mf.buildAggregateCluster(CDS_RESOURCE, "round_robin", null, null, candidates)); call.sendResponse(CDS, clusterAggregate, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1531,7 +1676,7 @@ public void cdsResponseWithAggregateCluster() { public void cdsResponseWithCircuitBreakers() { DiscoveryRpcCall call = startResourceWatcher(CDS, CDS_RESOURCE, cdsResourceWatcher); Any clusterCircuitBreakers = Any.pack( - mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, false, null, + mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", mf.buildCircuitBreakers(50, 200))); call.sendResponse(CDS, clusterCircuitBreakers, VERSION_1, "0000"); @@ -1563,15 +1708,15 @@ public void cdsResponseWithUpstreamTlsContext() { // Management server sends back CDS response with UpstreamTlsContext. Any clusterEds = Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", - null, true, + null, null, true, mf.buildUpstreamTlsContext("cert-instance-name", "cert1"), "envoy.transport_sockets.tls", null)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", - "dns-service-bar.googleapis.com", 443, "round_robin", null, false, null, null)), + "dns-service-bar.googleapis.com", 443, "round_robin", null, null,false, null, null)), clusterEds, - Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, false, - null, "envoy.transport_sockets.tls", null))); + Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, + false, null, "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1599,15 +1744,15 @@ public void cdsResponseWithNewUpstreamTlsContext() { // Management server sends back CDS response with UpstreamTlsContext. Any clusterEds = Any.pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", - null, true, + null, null,true, mf.buildNewUpstreamTlsContext("cert-instance-name", "cert1"), "envoy.transport_sockets.tls", null)); List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster("cluster-bar.googleapis.com", - "dns-service-bar.googleapis.com", 443, "round_robin", null, false, null, null)), + "dns-service-bar.googleapis.com", 443, "round_robin", null, null, false, null, null)), clusterEds, - Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, false, - null, "envoy.transport_sockets.tls", null))); + Any.pack(mf.buildEdsCluster("cluster-baz.googleapis.com", null, "round_robin", null, null, + false, null, "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); // Client sent an ACK CDS request. @@ -1634,7 +1779,7 @@ public void cdsResponseErrorHandling_badUpstreamTlsContext() { // Management server sends back CDS response with UpstreamTlsContext. List clusters = ImmutableList.of(Any .pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", - null, true, + null, null, true, mf.buildUpstreamTlsContext(null, null), "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); @@ -1662,7 +1807,7 @@ public void cdsResponseErrorHandling_badTransportSocketName() { // Management server sends back CDS response with UpstreamTlsContext. List clusters = ImmutableList.of(Any .pack(mf.buildEdsCluster(CDS_RESOURCE, "eds-cluster-foo.googleapis.com", "round_robin", - null, true, + null, null, true, mf.buildUpstreamTlsContext("secret1", "cert1"), "envoy.transport_sockets.bad", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); @@ -1726,7 +1871,7 @@ public void cdsResourceUpdated() { int dnsHostPort = 443; Any clusterDns = Any.pack(mf.buildLogicalDnsCluster(CDS_RESOURCE, dnsHostAddr, dnsHostPort, "round_robin", - null, false, null, null)); + null, null, false, null, null)); call.sendResponse(CDS, clusterDns, VERSION_1, "0000"); call.verifyRequest(CDS, CDS_RESOURCE, VERSION_1, "0000", NODE); verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); @@ -1743,7 +1888,7 @@ public void cdsResourceUpdated() { // Updated CDS response. String edsService = "eds-service-bar.googleapis.com"; Any clusterEds = Any.pack( - mf.buildEdsCluster(CDS_RESOURCE, edsService, "round_robin", null, true, null, + mf.buildEdsCluster(CDS_RESOURCE, edsService, "round_robin", null, null, true, null, "envoy.transport_sockets.tls", null )); call.sendResponse(CDS, clusterEds, VERSION_2, "0001"); @@ -1817,9 +1962,9 @@ public void multipleCdsWatchers() { String edsService = "eds-service-bar.googleapis.com"; List clusters = ImmutableList.of( Any.pack(mf.buildLogicalDnsCluster(CDS_RESOURCE, dnsHostAddr, dnsHostPort, "round_robin", - null, false, null, null)), - Any.pack(mf.buildEdsCluster(cdsResourceTwo, edsService, "round_robin", null, true, null, - "envoy.transport_sockets.tls", null))); + null, null, false, null, null)), + Any.pack(mf.buildEdsCluster(cdsResourceTwo, edsService, "round_robin", null, null, true, + null, "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); verify(cdsResourceWatcher).onChanged(cdsUpdateCaptor.capture()); CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); @@ -2080,11 +2225,11 @@ public void edsResourceDeletedByCds() { DiscoveryRpcCall call = resourceDiscoveryCalls.poll(); List clusters = ImmutableList.of( - Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, true, null, + Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, null, true, null, "envoy.transport_sockets.tls", null )), - Any.pack(mf.buildEdsCluster(CDS_RESOURCE, EDS_RESOURCE, "round_robin", null, false, null, - "envoy.transport_sockets.tls", null))); + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, EDS_RESOURCE, "round_robin", null, null, false, + null, "envoy.transport_sockets.tls", null))); call.sendResponse(CDS, clusters, VERSION_1, "0000"); verify(cdsWatcher).onChanged(cdsUpdateCaptor.capture()); CdsUpdate cdsUpdate = cdsUpdateCaptor.getValue(); @@ -2129,9 +2274,9 @@ public void edsResourceDeletedByCds() { verifySubscribedResourcesMetadataSizes(0, 2, 0, 2); clusters = ImmutableList.of( - Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, true, null, + Any.pack(mf.buildEdsCluster(resource, null, "round_robin", null, null, true, null, "envoy.transport_sockets.tls", null)), // no change - Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, false, null, + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, null, false, null, "envoy.transport_sockets.tls", null ))); call.sendResponse(CDS, clusters, VERSION_2, "0001"); @@ -2463,15 +2608,15 @@ public void serverSideListenerFound() { ResourceType.LDS, Collections.singletonList(LISTENER_RESOURCE), "0", "0000", NODE); verify(ldsResourceWatcher).onChanged(ldsUpdateCaptor.capture()); EnvoyServerProtoData.Listener parsedListener = ldsUpdateCaptor.getValue().listener(); - assertThat(parsedListener.getName()).isEqualTo(LISTENER_RESOURCE); - assertThat(parsedListener.getAddress()).isEqualTo("0.0.0.0:7000"); - assertThat(parsedListener.getDefaultFilterChain()).isNull(); - assertThat(parsedListener.getFilterChains()).hasSize(1); - FilterChain parsedFilterChain = Iterables.getOnlyElement(parsedListener.getFilterChains()); - assertThat(parsedFilterChain.getFilterChainMatch().getApplicationProtocols()).isEmpty(); - assertThat(parsedFilterChain.getHttpConnectionManager().rdsName()) + assertThat(parsedListener.name()).isEqualTo(LISTENER_RESOURCE); + assertThat(parsedListener.address()).isEqualTo("0.0.0.0:7000"); + assertThat(parsedListener.defaultFilterChain()).isNull(); + assertThat(parsedListener.filterChains()).hasSize(1); + FilterChain parsedFilterChain = Iterables.getOnlyElement(parsedListener.filterChains()); + assertThat(parsedFilterChain.filterChainMatch().applicationProtocols()).isEmpty(); + assertThat(parsedFilterChain.httpConnectionManager().rdsName()) .isEqualTo("route-foo.googleapis.com"); - assertThat(parsedFilterChain.getHttpConnectionManager().httpFilterConfigs().get(0).filterConfig) + assertThat(parsedFilterChain.httpConnectionManager().httpFilterConfigs().get(0).filterConfig) .isEqualTo(RouterFilter.ROUTER_CONFIG); assertThat(fakeClock.getPendingTasks(LDS_RESOURCE_FETCH_TIMEOUT_TASK_FILTER)).isEmpty(); @@ -2680,20 +2825,24 @@ protected abstract Message buildVirtualHost( protected abstract Message buildClusterInvalid(String name); protected abstract Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, - String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, - @Nullable Message upstreamTlsContext, String transportSocketName, + String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, @Nullable Message circuitBreakers); protected abstract Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, - int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, + int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, + @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers); protected abstract Message buildAggregateCluster(String clusterName, String lbPolicy, - @Nullable Message ringHashLbConfig, List clusters); + @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + List clusters); protected abstract Message buildRingHashLbConfig(String hashFunction, long minRingSize, long maxRingSize); + protected abstract Message buildLeastRequestLbConfig(int choiceCount); + protected abstract Message buildUpstreamTlsContext(String instanceName, String certName); protected abstract Message buildNewUpstreamTlsContext(String instanceName, String certName); diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java index be28f07b73c..29c7fdc4c01 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV2Test.java @@ -36,6 +36,7 @@ import io.envoyproxy.envoy.api.v2.Cluster.DiscoveryType; import io.envoyproxy.envoy.api.v2.Cluster.EdsClusterConfig; import io.envoyproxy.envoy.api.v2.Cluster.LbPolicy; +import io.envoyproxy.envoy.api.v2.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.api.v2.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.api.v2.Cluster.RingHashLbConfig.HashFunction; import io.envoyproxy.envoy.api.v2.ClusterLoadAssignment; @@ -392,10 +393,12 @@ protected Message buildClusterInvalid(String name) { @Override protected Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, - String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, + String lbPolicy, @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, @Nullable Message circuitBreakers) { - Cluster.Builder builder = initClusterBuilder(clusterName, lbPolicy, ringHashLbConfig, + Cluster.Builder builder = initClusterBuilder( + clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, enableLrs, upstreamTlsContext, circuitBreakers); builder.setType(DiscoveryType.EDS); EdsClusterConfig.Builder edsClusterConfigBuilder = EdsClusterConfig.newBuilder(); @@ -410,9 +413,11 @@ protected Message buildEdsCluster(String clusterName, @Nullable String edsServic @Override protected Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, - int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, + int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, + @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers) { - Cluster.Builder builder = initClusterBuilder(clusterName, lbPolicy, ringHashLbConfig, + Cluster.Builder builder = initClusterBuilder( + clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, enableLrs, upstreamTlsContext, circuitBreakers); builder.setType(DiscoveryType.LOGICAL_DNS); builder.setLoadAssignment( @@ -428,7 +433,8 @@ protected Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, @Override protected Message buildAggregateCluster(String clusterName, String lbPolicy, - @Nullable Message ringHashLbConfig, List clusters) { + @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + List clusters) { ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build(); CustomClusterType type = CustomClusterType.newBuilder() @@ -441,6 +447,9 @@ protected Message buildAggregateCluster(String clusterName, String lbPolicy, } else if (lbPolicy.equals("ring_hash_experimental")) { builder.setLbPolicy(LbPolicy.RING_HASH); builder.setRingHashLbConfig((RingHashLbConfig) ringHashLbConfig); + } else if (lbPolicy.equals("least_request_experimental")) { + builder.setLbPolicy(LbPolicy.LEAST_REQUEST); + builder.setLeastRequestLbConfig((LeastRequestLbConfig) leastRequestLbConfig); } else { throw new AssertionError("Invalid LB policy"); } @@ -448,8 +457,9 @@ protected Message buildAggregateCluster(String clusterName, String lbPolicy, } private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, - @Nullable Message ringHashLbConfig, boolean enableLrs, - @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers) { + @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + boolean enableLrs, @Nullable Message upstreamTlsContext, + @Nullable Message circuitBreakers) { Cluster.Builder builder = Cluster.newBuilder(); builder.setName(clusterName); if (lbPolicy.equals("round_robin")) { @@ -457,6 +467,9 @@ private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, } else if (lbPolicy.equals("ring_hash_experimental")) { builder.setLbPolicy(LbPolicy.RING_HASH); builder.setRingHashLbConfig((RingHashLbConfig) ringHashLbConfig); + } else if (lbPolicy.equals("least_request_experimental")) { + builder.setLbPolicy(LbPolicy.LEAST_REQUEST); + builder.setLeastRequestLbConfig((LeastRequestLbConfig) leastRequestLbConfig); } else { throw new AssertionError("Invalid LB policy"); } @@ -493,6 +506,13 @@ protected Message buildRingHashLbConfig(String hashFunction, long minRingSize, return builder.build(); } + @Override + protected Message buildLeastRequestLbConfig(int choiceCount) { + LeastRequestLbConfig.Builder builder = LeastRequestLbConfig.newBuilder(); + builder.setChoiceCount(UInt32Value.newBuilder().setValue(choiceCount)); + return builder.build(); + } + @Override protected Message buildUpstreamTlsContext(String instanceName, String certName) { GrpcService grpcService = diff --git a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java index 69e75292778..6a75d9ab068 100644 --- a/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java +++ b/xds/src/test/java/io/grpc/xds/ClientXdsClientV3Test.java @@ -38,6 +38,7 @@ import io.envoyproxy.envoy.config.cluster.v3.Cluster.DiscoveryType; import io.envoyproxy.envoy.config.cluster.v3.Cluster.EdsClusterConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.LbPolicy; +import io.envoyproxy.envoy.config.cluster.v3.Cluster.LeastRequestLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig; import io.envoyproxy.envoy.config.cluster.v3.Cluster.RingHashLbConfig.HashFunction; import io.envoyproxy.envoy.config.core.v3.Address; @@ -448,10 +449,12 @@ protected Message buildClusterInvalid(String name) { @Override protected Message buildEdsCluster(String clusterName, @Nullable String edsServiceName, - String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, + String lbPolicy, @Nullable Message ringHashLbConfig, + @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, @Nullable Message circuitBreakers) { - Cluster.Builder builder = initClusterBuilder(clusterName, lbPolicy, ringHashLbConfig, + Cluster.Builder builder = initClusterBuilder( + clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, enableLrs, upstreamTlsContext, transportSocketName, circuitBreakers); builder.setType(DiscoveryType.EDS); EdsClusterConfig.Builder edsClusterConfigBuilder = EdsClusterConfig.newBuilder(); @@ -466,9 +469,11 @@ protected Message buildEdsCluster(String clusterName, @Nullable String edsServic @Override protected Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, - int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, boolean enableLrs, + int dnsHostPort, String lbPolicy, @Nullable Message ringHashLbConfig, + @Nullable Message leastRequestLbConfig, boolean enableLrs, @Nullable Message upstreamTlsContext, @Nullable Message circuitBreakers) { - Cluster.Builder builder = initClusterBuilder(clusterName, lbPolicy, ringHashLbConfig, + Cluster.Builder builder = initClusterBuilder( + clusterName, lbPolicy, ringHashLbConfig, leastRequestLbConfig, enableLrs, upstreamTlsContext, "envoy.transport_sockets.tls", circuitBreakers); builder.setType(DiscoveryType.LOGICAL_DNS); builder.setLoadAssignment( @@ -484,7 +489,8 @@ protected Message buildLogicalDnsCluster(String clusterName, String dnsHostAddr, @Override protected Message buildAggregateCluster(String clusterName, String lbPolicy, - @Nullable Message ringHashLbConfig, List clusters) { + @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + List clusters) { ClusterConfig clusterConfig = ClusterConfig.newBuilder().addAllClusters(clusters).build(); CustomClusterType type = CustomClusterType.newBuilder() @@ -497,6 +503,9 @@ protected Message buildAggregateCluster(String clusterName, String lbPolicy, } else if (lbPolicy.equals("ring_hash_experimental")) { builder.setLbPolicy(LbPolicy.RING_HASH); builder.setRingHashLbConfig((RingHashLbConfig) ringHashLbConfig); + } else if (lbPolicy.equals("least_request_experimental")) { + builder.setLbPolicy(LbPolicy.LEAST_REQUEST); + builder.setLeastRequestLbConfig((LeastRequestLbConfig) leastRequestLbConfig); } else { throw new AssertionError("Invalid LB policy"); } @@ -504,8 +513,8 @@ protected Message buildAggregateCluster(String clusterName, String lbPolicy, } private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, - @Nullable Message ringHashLbConfig, boolean enableLrs, - @Nullable Message upstreamTlsContext, String transportSocketName, + @Nullable Message ringHashLbConfig, @Nullable Message leastRequestLbConfig, + boolean enableLrs, @Nullable Message upstreamTlsContext, String transportSocketName, @Nullable Message circuitBreakers) { Cluster.Builder builder = Cluster.newBuilder(); builder.setName(clusterName); @@ -514,6 +523,9 @@ private Cluster.Builder initClusterBuilder(String clusterName, String lbPolicy, } else if (lbPolicy.equals("ring_hash_experimental")) { builder.setLbPolicy(LbPolicy.RING_HASH); builder.setRingHashLbConfig((RingHashLbConfig) ringHashLbConfig); + } else if (lbPolicy.equals("least_request_experimental")) { + builder.setLbPolicy(LbPolicy.LEAST_REQUEST); + builder.setLeastRequestLbConfig((LeastRequestLbConfig) leastRequestLbConfig); } else { throw new AssertionError("Invalid LB policy"); } @@ -550,6 +562,13 @@ protected Message buildRingHashLbConfig(String hashFunction, long minRingSize, return builder.build(); } + @Override + protected Message buildLeastRequestLbConfig(int choiceCount) { + LeastRequestLbConfig.Builder builder = LeastRequestLbConfig.newBuilder(); + builder.setChoiceCount(UInt32Value.newBuilder().setValue(choiceCount)); + return builder.build(); + } + @Override @SuppressWarnings("deprecation") protected Message buildUpstreamTlsContext(String instanceName, String certName) { diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index a85f476ed4b..51a7ce5066b 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -65,6 +65,7 @@ import io.grpc.xds.Endpoints.LbEndpoint; import io.grpc.xds.Endpoints.LocalityLbEndpoints; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.RingHashLoadBalancer.RingHashConfig; @@ -136,6 +137,8 @@ public void uncaughtException(Thread t, Throwable e) { new PolicySelection(new FakeLoadBalancerProvider("round_robin"), null); private final PolicySelection ringHash = new PolicySelection( new FakeLoadBalancerProvider("ring_hash_experimental"), new RingHashConfig(10L, 100L)); + private final PolicySelection leastRequest = new PolicySelection( + new FakeLoadBalancerProvider("least_request_experimental"), new LeastRequestConfig(3)); private final List childBalancers = new ArrayList<>(); private final List resolvers = new ArrayList<>(); private final FakeXdsClient xdsClient = new FakeXdsClient(); @@ -267,6 +270,45 @@ public void edsClustersWithRingHashEndpointLbPolicy() { assertThat(ringHashConfig.maxRingSize).isEqualTo(100L); } + @Test + public void edsClustersWithLeastRequestEndpointLbPolicy() { + ClusterResolverConfig config = new ClusterResolverConfig( + Collections.singletonList(edsDiscoveryMechanism1), leastRequest); + deliverLbConfig(config); + assertThat(xdsClient.watchers.keySet()).containsExactly(EDS_SERVICE_NAME1); + assertThat(childBalancers).isEmpty(); + + // Simple case with one priority and one locality + EquivalentAddressGroup endpoint = makeAddress("endpoint-addr-1"); + LocalityLbEndpoints localityLbEndpoints = + LocalityLbEndpoints.create( + Arrays.asList( + LbEndpoint.create(endpoint, 0 /* loadBalancingWeight */, true)), + 100 /* localityWeight */, 1 /* priority */); + xdsClient.deliverClusterLoadAssignment( + EDS_SERVICE_NAME1, + ImmutableMap.of(locality1, localityLbEndpoints)); + assertThat(childBalancers).hasSize(1); + FakeLoadBalancer childBalancer = Iterables.getOnlyElement(childBalancers); + assertThat(childBalancer.addresses).hasSize(1); + EquivalentAddressGroup addr = childBalancer.addresses.get(0); + assertThat(addr.getAddresses()).isEqualTo(endpoint.getAddresses()); + assertThat(childBalancer.name).isEqualTo(PRIORITY_POLICY_NAME); + PriorityLbConfig priorityLbConfig = (PriorityLbConfig) childBalancer.config; + assertThat(priorityLbConfig.priorities).containsExactly(CLUSTER1 + "[priority1]"); + PriorityChildConfig priorityChildConfig = + Iterables.getOnlyElement(priorityLbConfig.childConfigs.values()); + assertThat(priorityChildConfig.policySelection.getProvider().getPolicyName()) + .isEqualTo(CLUSTER_IMPL_POLICY_NAME); + ClusterImplConfig clusterImplConfig = + (ClusterImplConfig) priorityChildConfig.policySelection.getConfig(); + assertClusterImplConfig(clusterImplConfig, CLUSTER1, EDS_SERVICE_NAME1, LRS_SERVER_INFO, 100L, + tlsContext, Collections.emptyList(), WEIGHTED_TARGET_POLICY_NAME); + WeightedTargetConfig weightedTargetConfig = + (WeightedTargetConfig) clusterImplConfig.childPolicy.getConfig(); + assertThat(weightedTargetConfig.targets.keySet()).containsExactly(locality1.toString()); + } + @Test public void onlyEdsClusters_receivedEndpoints() { ClusterResolverConfig config = new ClusterResolverConfig( diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java index 7c6abee2835..0d929939109 100644 --- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java +++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java @@ -23,7 +23,11 @@ import static io.grpc.xds.AbstractXdsClient.ResourceType.RDS; import static org.junit.Assert.fail; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.Any; import io.envoyproxy.envoy.admin.v3.ClientResourceStatus; import io.envoyproxy.envoy.config.cluster.v3.Cluster; @@ -46,13 +50,15 @@ import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcServerRule; import io.grpc.xds.AbstractXdsClient.ResourceType; +import io.grpc.xds.Bootstrapper.BootstrapInfo; +import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.XdsClient.ResourceMetadata; import io.grpc.xds.XdsClient.ResourceMetadata.ResourceMetadataStatus; import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; -import java.util.Arrays; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; @@ -69,22 +75,12 @@ public class CsdsServiceTest { "projects/42/networks/default/nodes/5c85b298-6f5b-4722-b74a-f7d1f0ccf5ad"; private static final EnvoyProtoData.Node BOOTSTRAP_NODE = EnvoyProtoData.Node.newBuilder().setId(NODE_ID).build(); - private static final XdsClient XDS_CLIENT_NO_RESOURCES = new XdsClient() { - @Override - Bootstrapper.BootstrapInfo getBootstrapInfo() { - return Bootstrapper.BootstrapInfo.builder() - .servers(Arrays.asList( - Bootstrapper.ServerInfo.create( - SERVER_URI, InsecureChannelCredentials.create(), false))) - .node(BOOTSTRAP_NODE) - .build(); - } - - @Override - Map getSubscribedResourcesMetadata(ResourceType type) { - return ImmutableMap.of(); - } - }; + private static final BootstrapInfo BOOTSTRAP_INFO = BootstrapInfo.builder() + .servers(ImmutableList.of( + ServerInfo.create(SERVER_URI, InsecureChannelCredentials.create(), true))) + .node(BOOTSTRAP_NODE) + .build(); + private static final XdsClient XDS_CLIENT_NO_RESOURCES = new FakeXdsClient(); @RunWith(JUnit4.class) public static class ServiceTests { @@ -125,13 +121,15 @@ public void fetchClientConfig_invalidArgument() { } } - /** Unexpected exceptions translated to internal error status. */ + /** Unexpected exceptions translated to Status.Code.INTERNAL. */ @Test public void fetchClientConfig_unexpectedException() { - XdsClient throwingXdsClient = new XdsClient() { + XdsClient throwingXdsClient = new FakeXdsClient() { @Override - Map getSubscribedResourcesMetadata(ResourceType type) { - throw new IllegalArgumentException("IllegalArgumentException"); + ListenableFuture>> + getSubscribedResourcesMetadataSnapshot() { + return Futures.immediateFailedFuture( + new IllegalArgumentException("IllegalArgumentException")); } }; grpcServerRule.getServiceRegistry() @@ -143,6 +141,38 @@ Map getSubscribedResourcesMetadata(ResourceType type) } catch (StatusRuntimeException e) { assertThat(e.getStatus().getCode()).isEqualTo(Code.INTERNAL); assertThat(e.getStatus().getDescription()).isEqualTo("Unexpected internal error"); + // Cause is not propagated. + } + } + + /** Interrupted exceptions translated to Status.Code.ABORTED. */ + @Test + public void fetchClientConfig_interruptedException() { + XdsClient throwingXdsClient = new FakeXdsClient() { + @Override + ListenableFuture>> + getSubscribedResourcesMetadataSnapshot() { + return Futures.submit( + new Callable>>() { + @Override + public Map> call() { + Thread.currentThread().interrupt(); + return null; + } + }, MoreExecutors.directExecutor()); + } + }; + grpcServerRule.getServiceRegistry() + .addService(new CsdsService(new FakeXdsClientPoolFactory(throwingXdsClient))); + + try { + ClientStatusResponse response = csdsStub.fetchClientStatus(REQUEST); + fail("Should've failed, got response: " + response); + } catch (StatusRuntimeException e) { + assertThat(e.getStatus().getCode()).isEqualTo(Code.ABORTED); + assertThat(e.getStatus().getDescription()).isEqualTo("Thread interrupted"); + // Clean the test thread interrupt. + assertThat(Thread.interrupted()).isEqualTo(true); } } @@ -289,34 +319,19 @@ public void metadataStatusToClientStatus() { } @Test - public void getClientConfigForXdsClient_subscribedResourcesToGenericXdsConfig() { - ClientConfig clientConfig = CsdsService.getClientConfigForXdsClient(new XdsClient() { + public void getClientConfigForXdsClient_subscribedResourcesToGenericXdsConfig() + throws InterruptedException { + ClientConfig clientConfig = CsdsService.getClientConfigForXdsClient(new FakeXdsClient() { @Override - Bootstrapper.BootstrapInfo getBootstrapInfo() { - return Bootstrapper.BootstrapInfo.builder() - .servers(Arrays.asList( - Bootstrapper.ServerInfo.create( - SERVER_URI, InsecureChannelCredentials.create(), false))) - .node(BOOTSTRAP_NODE) + protected Map> + getSubscribedResourcesMetadata() { + return new ImmutableMap.Builder>() + .put(LDS, ImmutableMap.of("subscribedResourceName.LDS", METADATA_ACKED_LDS)) + .put(RDS, ImmutableMap.of("subscribedResourceName.RDS", METADATA_ACKED_RDS)) + .put(CDS, ImmutableMap.of("subscribedResourceName.CDS", METADATA_ACKED_CDS)) + .put(EDS, ImmutableMap.of("subscribedResourceName.EDS", METADATA_ACKED_EDS)) .build(); } - - @Override - Map getSubscribedResourcesMetadata(ResourceType type) { - switch (type) { - case LDS: - return ImmutableMap.of("subscribedResourceName." + type.name(), METADATA_ACKED_LDS); - case RDS: - return ImmutableMap.of("subscribedResourceName." + type.name(), METADATA_ACKED_RDS); - case CDS: - return ImmutableMap.of("subscribedResourceName." + type.name(), METADATA_ACKED_CDS); - case EDS: - return ImmutableMap.of("subscribedResourceName." + type.name(), METADATA_ACKED_EDS); - case UNKNOWN: - default: - throw new AssertionError("Unexpected resource name"); - } - } }); verifyClientConfigNode(clientConfig); @@ -355,7 +370,7 @@ Map getSubscribedResourcesMetadata(ResourceType type) } @Test - public void getClientConfigForXdsClient_noSubscribedResources() { + public void getClientConfigForXdsClient_noSubscribedResources() throws InterruptedException { ClientConfig clientConfig = CsdsService.getClientConfigForXdsClient(XDS_CLIENT_NO_RESOURCES); verifyClientConfigNode(clientConfig); verifyClientConfigNoResources(clientConfig); @@ -395,6 +410,23 @@ private static EnumMap mapConfigDumps(ClientConf return xdsConfigMap; } + private static class FakeXdsClient extends XdsClient { + protected Map> getSubscribedResourcesMetadata() { + return ImmutableMap.of(); + } + + @Override + ListenableFuture>> + getSubscribedResourcesMetadataSnapshot() { + return Futures.immediateFuture(getSubscribedResourcesMetadata()); + } + + @Override + BootstrapInfo getBootstrapInfo() { + return BOOTSTRAP_INFO; + } + } + private static class FakeXdsClientPoolFactory implements XdsClientPoolFactory { @Nullable private final XdsClient xdsClient; @@ -424,7 +456,7 @@ public void setBootstrapOverride(Map bootstrap) { } @Override - public ObjectPool getOrCreate() throws XdsInitializationException { + public ObjectPool getOrCreate() { throw new UnsupportedOperationException("Should not be called"); } } diff --git a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java index 32a7bc19b32..a4efb226ce9 100644 --- a/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/FilterChainMatchingProtocolNegotiatorsTest.java @@ -60,7 +60,6 @@ import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -90,6 +89,16 @@ public class FilterChainMatchingProtocolNegotiatorsTest { @Mock private ProtocolNegotiator mockDelegate; private FilterChainSelectorManager selectorManager = new FilterChainSelectorManager(); + private static final EnvoyServerProtoData.FilterChainMatch DEFAULT_FILTER_CHAIN_MATCH = + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); private static final HttpConnectionManager HTTP_CONNECTION_MANAGER = createRds("routing-config"); private static final String LOCAL_IP = "10.1.2.3"; // dest private static final String REMOTE_IP = "10.4.2.3"; // source @@ -188,18 +197,18 @@ public void filterSelectorChange_drainsConnection() { @Test public void singleFilterChainWithoutAlpn() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.DownstreamTlsContext tlsContext = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); - EnvoyServerProtoData.FilterChain filterChain = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch, HTTP_CONNECTION_MANAGER, tlsContext, tlsContextManager); @@ -213,7 +222,7 @@ public void singleFilterChainWithoutAlpn() throws Exception { pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); assertThat(sslSet.isDone()).isTrue(); - assertThat(sslSet.get()).isEqualTo(filterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContext); } @@ -221,28 +230,28 @@ public void singleFilterChainWithoutAlpn() throws Exception { @Test public void singleFilterChainWithAlpn() throws Exception { EnvoyServerProtoData.FilterChainMatch filterChainMatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList("managed-mtls"), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of("managed-mtls"), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.DownstreamTlsContext tlsContext = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); - EnvoyServerProtoData.FilterChain filterChain = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch, HTTP_CONNECTION_MANAGER, tlsContext, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext defaultTlsContext = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, defaultTlsContext, - tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, defaultTlsContext, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChain, randomConfig("no-match")), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -251,7 +260,7 @@ public void singleFilterChainWithAlpn() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(defaultFilterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(defaultTlsContext); } @@ -261,24 +270,24 @@ public void destPortFails_returnDefaultFilterChain() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContextWithDestPort = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchWithDestPort = - new EnvoyServerProtoData.FilterChainMatch( - PORT, - Arrays.asList(), - Arrays.asList("managed-mtls"), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + PORT, + ImmutableList.of(), + ImmutableList.of("managed-mtls"), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainWithDestPort = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchWithDestPort, HTTP_CONNECTION_MANAGER, tlsContextWithDestPort, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChain defaultFilterChain = - new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, + EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, tlsContextForDefaultFilterChain, tlsContextManager); ServerRoutingConfig routingConfig = ServerRoutingConfig.create( @@ -287,7 +296,7 @@ public void destPortFails_returnDefaultFilterChain() throws Exception { selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainWithDestPort, new AtomicReference(routingConfig)), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -297,7 +306,7 @@ public void destPortFails_returnDefaultFilterChain() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(defaultFilterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()) .isSameInstanceAs(tlsContextForDefaultFilterChain); @@ -308,27 +317,27 @@ public void destPrefixRangeMatch() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContextMatch = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.0", 24)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChainWithMatch = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChainWithMatch = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchWithMatch, HTTP_CONNECTION_MANAGER, tlsContextMatch, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, tlsContextForDefaultFilterChain, tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainWithMatch, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("no-match"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("no-match"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -338,7 +347,7 @@ public void destPrefixRangeMatch() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainWithMatch.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainWithMatch.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMatch); } @@ -350,27 +359,27 @@ public void destPrefixRangeMismatch_returnDefaultFilterChain() CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); // 10.2.2.0/24 doesn't match LOCAL_IP EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMismatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.2.2.0", 24)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.2.2.0", 24)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainWithMismatch = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchWithMismatch, HTTP_CONNECTION_MANAGER, tlsContextMismatch, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, tlsContextForDefaultFilterChain, tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainWithMismatch, randomConfig("no-match")), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -381,7 +390,7 @@ public void destPrefixRangeMismatch_returnDefaultFilterChain() pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); assertThat(sslSet.isDone()).isTrue(); - assertThat(sslSet.get()).isEqualTo(defaultFilterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextForDefaultFilterChain); } @@ -393,27 +402,27 @@ public void dest0LengthPrefixRange() CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); // 10.2.2.0/24 doesn't match LOCAL_IP EnvoyServerProtoData.FilterChainMatch filterChainMatch0Length = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.2.2.0", 0)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain0Length = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.2.2.0", 0)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain0Length = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch0Length, HTTP_CONNECTION_MANAGER, tlsContext0Length, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, tlsContextForDefaultFilterChain, tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChain0Length, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), + defaultFilterChain.sslContextProviderSupplier(), new AtomicReference())); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -423,7 +432,7 @@ public void dest0LengthPrefixRange() setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChain0Length.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChain0Length.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContext0Length); } @@ -434,43 +443,44 @@ public void destPrefixRange_moreSpecificWins() EnvoyServerProtoData.DownstreamTlsContext tlsContextLessSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.0", 24)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainLessSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchLessSpecific, HTTP_CONNECTION_MANAGER, tlsContextLessSpecific, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextMoreSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.2", 31)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.2", 31)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainMoreSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchMoreSpecific, HTTP_CONNECTION_MANAGER, tlsContextMoreSpecific, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainLessSpecific, randomConfig("no-match"), filterChainMoreSpecific, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -480,7 +490,7 @@ public void destPrefixRange_moreSpecificWins() setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMoreSpecific); } @@ -491,43 +501,44 @@ public void destPrefixRange_emptyListLessSpecific() EnvoyServerProtoData.DownstreamTlsContext tlsContextLessSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainLessSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchLessSpecific, HTTP_CONNECTION_MANAGER, tlsContextLessSpecific, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextMoreSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("8.0.0.0", 5)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("8.0.0.0", 5)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainMoreSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchMoreSpecific, HTTP_CONNECTION_MANAGER, tlsContextMoreSpecific, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainLessSpecific, randomConfig("no-match"), filterChainMoreSpecific, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -536,7 +547,7 @@ public void destPrefixRange_emptyListLessSpecific() setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMoreSpecific); } @@ -547,42 +558,44 @@ public void destPrefixRangeIpv6_moreSpecificWins() EnvoyServerProtoData.DownstreamTlsContext tlsContextLessSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("FE80:0:0:0:0:0:0:0", 60)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("FE80:0:0:0:0:0:0:0", 60)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainLessSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchLessSpecific, HTTP_CONNECTION_MANAGER, tlsContextLessSpecific, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextMoreSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecific = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("FE80:0000:0000:0000:0202:0:0:0", 80)), - Arrays.asList(), - Arrays.asList(), + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("FE80:0000:0000:0000:0202:0:0:0", 80)), + ImmutableList.of(), + ImmutableList.of(), EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainMoreSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchMoreSpecific, HTTP_CONNECTION_MANAGER, tlsContextMoreSpecific, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainLessSpecific, randomConfig("no-match"), filterChainMoreSpecific, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -594,7 +607,7 @@ public void destPrefixRangeIpv6_moreSpecificWins() 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainMoreSpecific.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMoreSpecific); } @@ -605,45 +618,46 @@ public void destPrefixRange_moreSpecificWith2Wins() EnvoyServerProtoData.DownstreamTlsContext tlsContextMoreSpecificWith2 = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecificWith2 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.1.2.0", 24), - new EnvoyServerProtoData.CidrRange(LOCAL_IP, 32)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.1.2.0", 24), + EnvoyServerProtoData.CidrRange.create(LOCAL_IP, 32)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainMoreSpecificWith2 = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchMoreSpecificWith2, HTTP_CONNECTION_MANAGER, tlsContextMoreSpecificWith2, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextLessSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.2", 31)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.2", 31)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainLessSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchLessSpecific, HTTP_CONNECTION_MANAGER, tlsContextLessSpecific, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainMoreSpecificWith2, noopConfig, filterChainLessSpecific, randomConfig("no-match")), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -653,7 +667,7 @@ filterChainLessSpecific, randomConfig("no-match")), pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); assertThat(sslSet.get()).isEqualTo( - filterChainMoreSpecificWith2.getSslContextProviderSupplier()); + filterChainMoreSpecificWith2.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMoreSpecificWith2); } @@ -663,27 +677,27 @@ public void sourceTypeMismatch_returnDefaultFilterChain() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContextMismatch = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMismatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainWithMismatch = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchWithMismatch, HTTP_CONNECTION_MANAGER, tlsContextMismatch, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER,tlsContextForDefaultFilterChain, - tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, + tlsContextForDefaultFilterChain, tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainWithMismatch, randomConfig("no-match")), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -693,7 +707,7 @@ public void sourceTypeMismatch_returnDefaultFilterChain() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(defaultFilterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextForDefaultFilterChain); } @@ -705,33 +719,33 @@ public void sourceTypeLocal() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContextMatch = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchWithMatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChainWithMatch = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChainWithMatch = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchWithMatch, HTTP_CONNECTION_MANAGER, tlsContextMatch, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, HTTP_CONNECTION_MANAGER, tlsContextForDefaultFilterChain, - tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, + tlsContextForDefaultFilterChain, tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainWithMatch, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); setupChannel(LOCAL_IP, LOCAL_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainWithMatch.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainWithMatch.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMatch); } @@ -745,45 +759,46 @@ public void sourcePrefixRange_moreSpecificWith2Wins() EnvoyServerProtoData.DownstreamTlsContext tlsContextMoreSpecificWith2 = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchMoreSpecificWith2 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.4.2.0", 24), - new EnvoyServerProtoData.CidrRange(REMOTE_IP, 32)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), + EnvoyServerProtoData.CidrRange.create(REMOTE_IP, 32)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainMoreSpecificWith2 = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchMoreSpecificWith2, HTTP_CONNECTION_MANAGER, tlsContextMoreSpecificWith2, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextLessSpecific = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchLessSpecific = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.4.2.2", 31)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainLessSpecific = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchLessSpecific, HTTP_CONNECTION_MANAGER, tlsContextLessSpecific, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainMoreSpecificWith2, noopConfig, filterChainLessSpecific, randomConfig("no-match")), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -791,7 +806,7 @@ filterChainLessSpecific, randomConfig("no-match")), pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); assertThat(sslSet.get()).isEqualTo( - filterChainMoreSpecificWith2.getSslContextProviderSupplier()); + filterChainMoreSpecificWith2.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextMoreSpecificWith2); } @@ -812,42 +827,42 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { EnvoyServerProtoData.DownstreamTlsContext tlsContext1 = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatch1 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.4.2.0", 24), - new EnvoyServerProtoData.CidrRange("192.168.10.2", 32)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain1 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), + EnvoyServerProtoData.CidrRange.create("192.168.10.2", 32)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain1 = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch1, HTTP_CONNECTION_MANAGER, tlsContext1, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContext2 = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatch2 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.4.2.0", 24)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain2 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain2 = EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatch2, HTTP_CONNECTION_MANAGER, tlsContext2, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, null); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, null); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChain1, noopConfig, filterChain2, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -871,45 +886,46 @@ public void sourcePortMatch_exactMatchWinsOverEmptyList() throws Exception { EnvoyServerProtoData.DownstreamTlsContext tlsContextEmptySourcePorts = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT1", "VA1"); EnvoyServerProtoData.FilterChainMatch filterChainMatchEmptySourcePorts = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.4.2.0", 24), - new EnvoyServerProtoData.CidrRange("10.4.2.2", 31)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), + EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainEmptySourcePorts = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatchEmptySourcePorts, HTTP_CONNECTION_MANAGER, tlsContextEmptySourcePorts, tlsContextManager); EnvoyServerProtoData.DownstreamTlsContext tlsContextSourcePortMatch = CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT2", "VA2"); EnvoyServerProtoData.FilterChainMatch filterChainMatchSourcePortMatch = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.4.2.2", 31)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(7000, 15000), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.2", 31)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(7000, 15000), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChainSourcePortMatch = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatchSourcePortMatch, HTTP_CONNECTION_MANAGER, tlsContextSourcePortMatch, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-baz", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-baz", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChainEmptySourcePorts, randomConfig("no-match"), filterChainSourcePortMatch, noopConfig), - defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -918,7 +934,7 @@ public void sourcePortMatch_exactMatchWinsOverEmptyList() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChainSourcePortMatch.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChainSourcePortMatch.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContextSourcePortMatch); } @@ -947,16 +963,16 @@ public void filterChain_5stepMatch() throws Exception { // has dest port and specific prefix ranges: gets eliminated in step 1 EnvoyServerProtoData.FilterChainMatch filterChainMatch1 = - new EnvoyServerProtoData.FilterChainMatch( - PORT, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange(REMOTE_IP, 32)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain1 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + PORT, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create(REMOTE_IP, 32)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain1 = EnvoyServerProtoData.FilterChain.create( "filter-chain-1", filterChainMatch1, HTTP_CONNECTION_MANAGER, tlsContext1, tlsContextManager); @@ -964,94 +980,95 @@ public void filterChain_5stepMatch() throws Exception { // has single prefix range: and less specific source prefix range: gets eliminated in step 4 EnvoyServerProtoData.FilterChainMatch filterChainMatch2 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.0", 30)), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.4.0.0", 16)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain2 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.0.0", 16)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain2 = EnvoyServerProtoData.FilterChain.create( "filter-chain-2", filterChainMatch2, HTTP_CONNECTION_MANAGER, tlsContext2, tlsContextManager); // has prefix ranges with one not matching and source type local: gets eliminated in step 3 EnvoyServerProtoData.FilterChainMatch filterChainMatch3 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList( - new EnvoyServerProtoData.CidrRange("192.168.2.0", 24), - new EnvoyServerProtoData.CidrRange("10.1.2.0", 30)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, - Arrays.asList(), - Arrays.asList(), - null); - EnvoyServerProtoData.FilterChain filterChain3 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("192.168.2.0", 24), + EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.SAME_IP_OR_LOOPBACK, + ImmutableList.of(), + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChain filterChain3 = EnvoyServerProtoData.FilterChain.create( "filter-chain-3", filterChainMatch3, HTTP_CONNECTION_MANAGER, tlsContext3, tlsContextManager); // has prefix ranges with both matching and source type external but non matching source port: // gets eliminated in step 5 EnvoyServerProtoData.FilterChainMatch filterChainMatch4 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.1.0.0", 16), - new EnvoyServerProtoData.CidrRange("10.1.2.0", 30)), - Arrays.asList(), - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.4.2.0", 24)), - EnvoyServerProtoData.ConnectionSourceType.EXTERNAL, - Arrays.asList(16000, 9000), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16), + EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + ImmutableList.of(), + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24)), + EnvoyServerProtoData.ConnectionSourceType.EXTERNAL, + ImmutableList.of(16000, 9000), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChain4 = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-4", filterChainMatch4, HTTP_CONNECTION_MANAGER, tlsContext4, tlsContextManager); // has prefix ranges with both matching and source type external and matching source port: this // gets selected EnvoyServerProtoData.FilterChainMatch filterChainMatch5 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.1.0.0", 16), - new EnvoyServerProtoData.CidrRange("10.1.2.0", 30)), - Arrays.asList(), - Arrays.asList( - new EnvoyServerProtoData.CidrRange("10.4.2.0", 24), - new EnvoyServerProtoData.CidrRange("192.168.2.0", 24)), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(15000, 8000), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16), + EnvoyServerProtoData.CidrRange.create("10.1.2.0", 30)), + ImmutableList.of(), + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.4.2.0", 24), + EnvoyServerProtoData.CidrRange.create("192.168.2.0", 24)), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(15000, 8000), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChain5 = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-5", filterChainMatch5, HTTP_CONNECTION_MANAGER, tlsContext5, tlsContextManager); // has prefix range with prefixLen of 29: gets eliminated in step 2 EnvoyServerProtoData.FilterChainMatch filterChainMatch6 = - new EnvoyServerProtoData.FilterChainMatch( - 0, - Arrays.asList(new EnvoyServerProtoData.CidrRange("10.1.2.0", 29)), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(EnvoyServerProtoData.CidrRange.create("10.1.2.0", 29)), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); EnvoyServerProtoData.FilterChain filterChain6 = - new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain.create( "filter-chain-6", filterChainMatch6, HTTP_CONNECTION_MANAGER, tlsContext6, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-7", null, HTTP_CONNECTION_MANAGER, null, tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-7", DEFAULT_FILTER_CHAIN_MATCH, HTTP_CONNECTION_MANAGER, null, + tlsContextManager); Map> map = new HashMap<>(); map.put(filterChain1, randomConfig("1")); @@ -1061,7 +1078,7 @@ public void filterChain_5stepMatch() throws Exception { map.put(filterChain5, noopConfig); map.put(filterChain6, randomConfig("6")); selectorManager.updateSelector(new FilterChainSelector( - map, defaultFilterChain.getSslContextProviderSupplier(), randomConfig("default"))); + map, defaultFilterChain.sslContextProviderSupplier(), randomConfig("default"))); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -1071,7 +1088,7 @@ public void filterChain_5stepMatch() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(filterChain5.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(filterChain5.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext()).isSameInstanceAs(tlsContext5); } @@ -1087,54 +1104,54 @@ public void filterChainMatch_unsupportedMatchers() throws Exception { CommonTlsContextTestsUtil.buildTestInternalDownstreamTlsContext("CERT3", "ROOTCA"); EnvoyServerProtoData.FilterChainMatch filterChainMatch1 = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0 /* destinationPort */, - Collections.singletonList( - new EnvoyServerProtoData.CidrRange("10.1.0.0", 16)) /* prefixRange */, - Arrays.asList("managed-mtls", "h2") /* applicationProtocol */, - Collections.emptyList() /* sourcePrefixRanges */, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.1.0.0", 16)) /* prefixRange */, + ImmutableList.of("managed-mtls", "h2") /* applicationProtocol */, + ImmutableList.of() /* sourcePrefixRanges */, EnvoyServerProtoData.ConnectionSourceType.ANY /* sourceType */, - Collections.emptyList() /* sourcePorts */, - Arrays.asList("server1", "server2") /* serverNames */, + ImmutableList.of() /* sourcePorts */, + ImmutableList.of("server1", "server2") /* serverNames */, "tls" /* transportProtocol */); EnvoyServerProtoData.FilterChainMatch filterChainMatch2 = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0 /* destinationPort */, - Collections.singletonList( - new EnvoyServerProtoData.CidrRange("10.0.0.0", 8)) /* prefixRange */, - Collections.emptyList() /* applicationProtocol */, - Collections.emptyList() /* sourcePrefixRanges */, + ImmutableList.of( + EnvoyServerProtoData.CidrRange.create("10.0.0.0", 8)) /* prefixRange */, + ImmutableList.of() /* applicationProtocol */, + ImmutableList.of() /* sourcePrefixRanges */, EnvoyServerProtoData.ConnectionSourceType.ANY /* sourceType */, - Collections.emptyList() /* sourcePorts */, - Collections.emptyList() /* serverNames */, + ImmutableList.of() /* sourcePorts */, + ImmutableList.of() /* serverNames */, "" /* transportProtocol */); EnvoyServerProtoData.FilterChainMatch defaultFilterChainMatch = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0 /* destinationPort */, - Collections.emptyList() /* prefixRange */, - Collections.emptyList() /* applicationProtocol */, - Collections.emptyList() /* sourcePrefixRanges */, + ImmutableList.of() /* prefixRange */, + ImmutableList.of() /* applicationProtocol */, + ImmutableList.of() /* sourcePrefixRanges */, EnvoyServerProtoData.ConnectionSourceType.ANY /* sourceType */, - Collections.emptyList() /* sourcePorts */, - Collections.emptyList() /* serverNames */, + ImmutableList.of() /* sourcePorts */, + ImmutableList.of() /* serverNames */, "" /* transportProtocol */); - EnvoyServerProtoData.FilterChain filterChain1 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain1 = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch1, HTTP_CONNECTION_MANAGER, tlsContext1, mock(TlsContextManager.class)); - EnvoyServerProtoData.FilterChain filterChain2 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain2 = EnvoyServerProtoData.FilterChain.create( "filter-chain-bar", filterChainMatch2, HTTP_CONNECTION_MANAGER, tlsContext2, mock(TlsContextManager.class)); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( "filter-chain-baz", defaultFilterChainMatch, HTTP_CONNECTION_MANAGER, tlsContext3, mock(TlsContextManager.class)); selectorManager.updateSelector(new FilterChainSelector( ImmutableMap.of(filterChain1, randomConfig("1"), filterChain2, randomConfig("2")), - defaultFilterChain.getSslContextProviderSupplier(), noopConfig)); + defaultFilterChain.sslContextProviderSupplier(), noopConfig)); FilterChainMatchingHandler filterChainMatchingHandler = new FilterChainMatchingHandler(grpcHandler, selectorManager, mockDelegate); @@ -1143,7 +1160,7 @@ public void filterChainMatch_unsupportedMatchers() throws Exception { setupChannel(LOCAL_IP, REMOTE_IP, 15000, filterChainMatchingHandler); pipeline.fireUserEventTriggered(event); channel.runPendingTasks(); - assertThat(sslSet.get()).isEqualTo(defaultFilterChain.getSslContextProviderSupplier()); + assertThat(sslSet.get()).isEqualTo(defaultFilterChain.sslContextProviderSupplier()); assertThat(routingSettable.get()).isEqualTo(noopConfig); assertThat(sslSet.get().getTlsContext().getCommonTlsContext() .getTlsCertificateCertificateProviderInstance() diff --git a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerProviderTest.java new file mode 100644 index 00000000000..2e8519b150d --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerProviderTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.grpc.InternalServiceProviders; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancerProvider; +import io.grpc.NameResolver.ConfigOrError; +import io.grpc.Status.Code; +import io.grpc.SynchronizationContext; +import io.grpc.internal.JsonParser; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; +import java.io.IOException; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link LeastRequestLoadBalancerProvider}. */ +@RunWith(JUnit4.class) +public class LeastRequestLoadBalancerProviderTest { + private static final String AUTHORITY = "foo.googleapis.com"; + + private final SynchronizationContext syncContext = new SynchronizationContext( + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new AssertionError(e); + } + }); + private final LeastRequestLoadBalancerProvider provider = new LeastRequestLoadBalancerProvider(); + + @Test + public void provided() { + for (LoadBalancerProvider current : InternalServiceProviders.getCandidatesViaServiceLoader( + LoadBalancerProvider.class, getClass().getClassLoader())) { + if (current instanceof LeastRequestLoadBalancerProvider) { + return; + } + } + fail("LeastRequestLoadBalancerProvider not registered"); + } + + @Test + public void providesLoadBalancer() { + Helper helper = mock(Helper.class); + when(helper.getSynchronizationContext()).thenReturn(syncContext); + when(helper.getAuthority()).thenReturn(AUTHORITY); + assertThat(provider.newLoadBalancer(helper)) + .isInstanceOf(LeastRequestLoadBalancer.class); + } + + @Test + public void parseLoadBalancingConfig_valid() throws IOException { + String lbConfig = "{\"choiceCount\" : 3}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + LeastRequestConfig config = (LeastRequestConfig) configOrError.getConfig(); + assertThat(config.choiceCount).isEqualTo(3); + } + + @Test + public void parseLoadBalancingConfig_missingChoiceCount_useDefaults() throws IOException { + String lbConfig = "{}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + LeastRequestConfig config = (LeastRequestConfig) configOrError.getConfig(); + assertThat(config.choiceCount) + .isEqualTo(LeastRequestLoadBalancerProvider.DEFAULT_CHOICE_COUNT); + } + + @Test + public void parseLoadBalancingConfig_invalid_negativeSize() throws IOException { + String lbConfig = "{\"choiceCount\" : -10}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThat(configOrError.getError().getDescription()) + .isEqualTo("Invalid 'choiceCount'"); + } + + @Test + public void parseLoadBalancingConfig_invalid_tooSmallSize() throws IOException { + String lbConfig = "{\"choiceCount\" : 1}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThat(configOrError.getError().getDescription()) + .isEqualTo("Invalid 'choiceCount'"); + } + + @Test + public void parseLoadBalancingConfig_choiceCountCappedAtMax() throws IOException { + String lbConfig = "{\"choiceCount\" : 11}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + LeastRequestConfig config = (LeastRequestConfig) configOrError.getConfig(); + assertThat(config.choiceCount).isEqualTo(LeastRequestLoadBalancerProvider.MAX_CHOICE_COUNT); + } + + @Test + public void parseLoadBalancingConfig_invalidInteger() throws IOException { + Map lbConfig = parseJsonObject("{\"choiceCount\" : \"NaN\"}"); + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(lbConfig); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().getDescription()).isEqualTo( + "Failed to parse least_request_experimental LB config: " + lbConfig); + } + + @SuppressWarnings("unchecked") + private static Map parseJsonObject(String json) throws IOException { + return (Map) JsonParser.parse(json); + } +} diff --git a/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java new file mode 100644 index 00000000000..2d09dbfe1fc --- /dev/null +++ b/xds/src/test/java/io/grpc/xds/LeastRequestLoadBalancerTest.java @@ -0,0 +1,632 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.xds; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; +import static io.grpc.ConnectivityState.READY; +import static io.grpc.ConnectivityState.SHUTDOWN; +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; +import static io.grpc.xds.LeastRequestLoadBalancer.IN_FLIGHTS; +import static io.grpc.xds.LeastRequestLoadBalancer.STATE_INFO; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.grpc.Attributes; +import io.grpc.ClientStreamTracer; +import io.grpc.ClientStreamTracer.StreamInfo; +import io.grpc.ConnectivityState; +import io.grpc.ConnectivityStateInfo; +import io.grpc.EquivalentAddressGroup; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.CreateSubchannelArgs; +import io.grpc.LoadBalancer.Helper; +import io.grpc.LoadBalancer.PickResult; +import io.grpc.LoadBalancer.PickSubchannelArgs; +import io.grpc.LoadBalancer.ResolvedAddresses; +import io.grpc.LoadBalancer.Subchannel; +import io.grpc.LoadBalancer.SubchannelPicker; +import io.grpc.LoadBalancer.SubchannelStateListener; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.xds.LeastRequestLoadBalancer.EmptyPicker; +import io.grpc.xds.LeastRequestLoadBalancer.LeastRequestConfig; +import io.grpc.xds.LeastRequestLoadBalancer.ReadyPicker; +import io.grpc.xds.LeastRequestLoadBalancer.Ref; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** Unit test for {@link LeastRequestLoadBalancer}. */ +@RunWith(JUnit4.class) +public class LeastRequestLoadBalancerTest { + private static final Attributes.Key MAJOR_KEY = Attributes.Key.create("major-key"); + + private LeastRequestLoadBalancer loadBalancer; + private final List servers = Lists.newArrayList(); + private final Map, Subchannel> subchannels = Maps.newLinkedHashMap(); + private final Map subchannelStateListeners = + Maps.newLinkedHashMap(); + private final Attributes affinity = + Attributes.newBuilder().set(MAJOR_KEY, "I got the keys").build(); + + @Captor + private ArgumentCaptor pickerCaptor; + @Captor + private ArgumentCaptor stateCaptor; + @Captor + private ArgumentCaptor createArgsCaptor; + @Mock + private Helper mockHelper; + @Mock + private ThreadSafeRandom mockRandom; + + @Mock // This LoadBalancer doesn't use any of the arg fields, as verified in tearDown(). + private PickSubchannelArgs mockArgs; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + for (int i = 0; i < 3; i++) { + SocketAddress addr = new FakeSocketAddress("server" + i); + EquivalentAddressGroup eag = new EquivalentAddressGroup(addr); + servers.add(eag); + Subchannel sc = mock(Subchannel.class); + subchannels.put(Arrays.asList(eag), sc); + } + + when(mockHelper.createSubchannel(any(CreateSubchannelArgs.class))) + .then(new Answer() { + @Override + public Subchannel answer(InvocationOnMock invocation) throws Throwable { + CreateSubchannelArgs args = (CreateSubchannelArgs) invocation.getArguments()[0]; + final Subchannel subchannel = subchannels.get(args.getAddresses()); + when(subchannel.getAllAddresses()).thenReturn(args.getAddresses()); + when(subchannel.getAttributes()).thenReturn(args.getAttributes()); + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + subchannelStateListeners.put( + subchannel, (SubchannelStateListener) invocation.getArguments()[0]); + return null; + } + }).when(subchannel).start(any(SubchannelStateListener.class)); + return subchannel; + } + }); + loadBalancer = new LeastRequestLoadBalancer(mockHelper, mockRandom); + } + + @After + public void tearDown() throws Exception { + verifyNoMoreInteractions(mockRandom); + verifyNoMoreInteractions(mockArgs); + } + + @Test + public void pickAfterResolved() throws Exception { + final Subchannel readySubchannel = subchannels.values().iterator().next(); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity).build()); + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + + verify(mockHelper, times(3)).createSubchannel(createArgsCaptor.capture()); + List> capturedAddrs = new ArrayList<>(); + for (CreateSubchannelArgs arg : createArgsCaptor.getAllValues()) { + capturedAddrs.add(arg.getAddresses()); + } + + assertThat(capturedAddrs).containsAtLeastElementsIn(subchannels.keySet()); + for (Subchannel subchannel : subchannels.values()) { + verify(subchannel).requestConnection(); + verify(subchannel, never()).shutdown(); + } + + verify(mockHelper, times(2)) + .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + + assertEquals(CONNECTING, stateCaptor.getAllValues().get(0)); + assertEquals(READY, stateCaptor.getAllValues().get(1)); + assertThat(getList(pickerCaptor.getValue())).containsExactly(readySubchannel); + + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void pickAfterResolvedUpdatedHosts() throws Exception { + Subchannel removedSubchannel = mock(Subchannel.class); + Subchannel oldSubchannel = mock(Subchannel.class); + Subchannel newSubchannel = mock(Subchannel.class); + + Attributes.Key key = Attributes.Key.create("check-that-it-is-propagated"); + FakeSocketAddress removedAddr = new FakeSocketAddress("removed"); + EquivalentAddressGroup removedEag = new EquivalentAddressGroup(removedAddr); + FakeSocketAddress oldAddr = new FakeSocketAddress("old"); + EquivalentAddressGroup oldEag1 = new EquivalentAddressGroup(oldAddr); + EquivalentAddressGroup oldEag2 = new EquivalentAddressGroup( + oldAddr, Attributes.newBuilder().set(key, "oldattr").build()); + FakeSocketAddress newAddr = new FakeSocketAddress("new"); + EquivalentAddressGroup newEag = new EquivalentAddressGroup( + newAddr, Attributes.newBuilder().set(key, "newattr").build()); + + subchannels.put(Collections.singletonList(removedEag), removedSubchannel); + subchannels.put(Collections.singletonList(oldEag1), oldSubchannel); + subchannels.put(Collections.singletonList(newEag), newSubchannel); + + List currentServers = Lists.newArrayList(removedEag, oldEag1); + + InOrder inOrder = inOrder(mockHelper); + + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(currentServers).setAttributes(affinity) + .build()); + + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + + deliverSubchannelState(removedSubchannel, ConnectivityStateInfo.forNonError(READY)); + deliverSubchannelState(oldSubchannel, ConnectivityStateInfo.forNonError(READY)); + + inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); + SubchannelPicker picker = pickerCaptor.getValue(); + assertThat(getList(picker)).containsExactly(removedSubchannel, oldSubchannel); + + verify(removedSubchannel, times(1)).requestConnection(); + verify(oldSubchannel, times(1)).requestConnection(); + + assertThat(loadBalancer.getSubchannels()).containsExactly(removedSubchannel, + oldSubchannel); + + // This time with Attributes + List latestServers = Lists.newArrayList(oldEag2, newEag); + + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(latestServers).setAttributes(affinity).build()); + + verify(newSubchannel, times(1)).requestConnection(); + verify(oldSubchannel, times(1)).updateAddresses(Arrays.asList(oldEag2)); + verify(removedSubchannel, times(1)).shutdown(); + + deliverSubchannelState(removedSubchannel, ConnectivityStateInfo.forNonError(SHUTDOWN)); + deliverSubchannelState(newSubchannel, ConnectivityStateInfo.forNonError(READY)); + + assertThat(loadBalancer.getSubchannels()).containsExactly(oldSubchannel, + newSubchannel); + + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture()); + + picker = pickerCaptor.getValue(); + assertThat(getList(picker)).containsExactly(oldSubchannel, newSubchannel); + + // test going from non-empty to empty + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(Collections.emptyList()) + .setAttributes(affinity) + .build()); + + inOrder.verify(mockHelper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); + assertEquals(PickResult.withNoResult(), pickerCaptor.getValue().pickSubchannel(mockArgs)); + + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void pickAfterStateChange() throws Exception { + InOrder inOrder = inOrder(mockHelper); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .build()); + Subchannel subchannel = loadBalancer.getSubchannels().iterator().next(); + Ref subchannelStateInfo = subchannel.getAttributes().get( + STATE_INFO); + + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + assertThat(subchannelStateInfo.value).isEqualTo(ConnectivityStateInfo.forNonError(IDLE)); + + deliverSubchannelState(subchannel, + ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(mockHelper).updateBalancingState(eq(READY), pickerCaptor.capture()); + assertThat(pickerCaptor.getValue()).isInstanceOf(ReadyPicker.class); + assertThat(subchannelStateInfo.value).isEqualTo( + ConnectivityStateInfo.forNonError(READY)); + + Status error = Status.UNKNOWN.withDescription("¯\\_(ツ)_//¯"); + deliverSubchannelState(subchannel, + ConnectivityStateInfo.forTransientFailure(error)); + assertThat(subchannelStateInfo.value.getState()).isEqualTo(TRANSIENT_FAILURE); + assertThat(subchannelStateInfo.value.getStatus()).isEqualTo(error); + inOrder.verify(mockHelper).refreshNameResolution(); + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture()); + assertThat(pickerCaptor.getValue()).isInstanceOf(EmptyPicker.class); + + deliverSubchannelState(subchannel, + ConnectivityStateInfo.forNonError(IDLE)); + inOrder.verify(mockHelper).refreshNameResolution(); + assertThat(subchannelStateInfo.value.getState()).isEqualTo(TRANSIENT_FAILURE); + assertThat(subchannelStateInfo.value.getStatus()).isEqualTo(error); + + verify(subchannel, times(2)).requestConnection(); + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void pickAfterConfigChange() { + final LeastRequestConfig oldConfig = new LeastRequestConfig(4); + final LeastRequestConfig newConfig = new LeastRequestConfig(6); + final Subchannel readySubchannel = subchannels.values().iterator().next(); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity) + .setLoadBalancingPolicyConfig(oldConfig).build()); + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + verify(mockHelper, times(2)) + .updateBalancingState(any(ConnectivityState.class), pickerCaptor.capture()); + + // At this point it should use a ReadyPicker with oldConfig + pickerCaptor.getValue().pickSubchannel(mockArgs); + verify(mockRandom, times(oldConfig.choiceCount)).nextInt(1); + + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity) + .setLoadBalancingPolicyConfig(newConfig).build()); + verify(mockHelper, times(3)) + .updateBalancingState(any(ConnectivityState.class), pickerCaptor.capture()); + + // At this point it should use a ReadyPicker with newConfig + pickerCaptor.getValue().pickSubchannel(mockArgs); + verify(mockRandom, times(oldConfig.choiceCount + newConfig.choiceCount)).nextInt(1); + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void ignoreShutdownSubchannelStateChange() { + InOrder inOrder = inOrder(mockHelper); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .build()); + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + + loadBalancer.shutdown(); + for (Subchannel sc : loadBalancer.getSubchannels()) { + verify(sc).shutdown(); + // When the subchannel is being shut down, a SHUTDOWN connectivity state is delivered + // back to the subchannel state listener. + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(SHUTDOWN)); + } + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void stayTransientFailureUntilReady() { + InOrder inOrder = inOrder(mockHelper); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .build()); + + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + + // Simulate state transitions for each subchannel individually. + for (Subchannel sc : loadBalancer.getSubchannels()) { + Status error = Status.UNKNOWN.withDescription("connection broken"); + deliverSubchannelState( + sc, + ConnectivityStateInfo.forTransientFailure(error)); + inOrder.verify(mockHelper).refreshNameResolution(); + deliverSubchannelState( + sc, + ConnectivityStateInfo.forNonError(CONNECTING)); + Ref scStateInfo = sc.getAttributes().get( + STATE_INFO); + assertThat(scStateInfo.value.getState()).isEqualTo(TRANSIENT_FAILURE); + assertThat(scStateInfo.value.getStatus()).isEqualTo(error); + } + inOrder.verify(mockHelper).updateBalancingState(eq(TRANSIENT_FAILURE), isA(EmptyPicker.class)); + inOrder.verifyNoMoreInteractions(); + + Subchannel subchannel = loadBalancer.getSubchannels().iterator().next(); + deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY)); + Ref subchannelStateInfo = subchannel.getAttributes().get( + STATE_INFO); + assertThat(subchannelStateInfo.value).isEqualTo(ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(mockHelper).updateBalancingState(eq(READY), isA(ReadyPicker.class)); + + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void refreshNameResolutionWhenSubchannelConnectionBroken() { + InOrder inOrder = inOrder(mockHelper); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .build()); + + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + + // Simulate state transitions for each subchannel individually. + for (Subchannel sc : loadBalancer.getSubchannels()) { + verify(sc).requestConnection(); + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(CONNECTING)); + Status error = Status.UNKNOWN.withDescription("connection broken"); + deliverSubchannelState(sc, ConnectivityStateInfo.forTransientFailure(error)); + inOrder.verify(mockHelper).refreshNameResolution(); + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(READY)); + inOrder.verify(mockHelper).updateBalancingState(eq(READY), isA(ReadyPicker.class)); + // Simulate receiving go-away so READY subchannels transit to IDLE. + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(IDLE)); + inOrder.verify(mockHelper).refreshNameResolution(); + verify(sc, times(2)).requestConnection(); + inOrder.verify(mockHelper).updateBalancingState(eq(CONNECTING), isA(EmptyPicker.class)); + } + + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void pickerLeastRequest() throws Exception { + int choiceCount = 2; + // This should add inFlight counters to all subchannels. + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .setLoadBalancingPolicyConfig(new LeastRequestConfig(choiceCount)) + .build()); + + assertEquals(3, loadBalancer.getSubchannels().size()); + + List subchannels = Lists.newArrayList(loadBalancer.getSubchannels()); + + // Make sure all inFlight counters have started at 0 + assertEquals(0, + subchannels.get(0).getAttributes().get(IN_FLIGHTS).get()); + assertEquals(0, + subchannels.get(1).getAttributes().get(IN_FLIGHTS).get()); + assertEquals(0, + subchannels.get(2).getAttributes().get(IN_FLIGHTS).get()); + + for (Subchannel sc : subchannels) { + deliverSubchannelState(sc, ConnectivityStateInfo.forNonError(READY)); + } + + // Capture the active ReadyPicker once all subchannels are READY + verify(mockHelper, times(4)) + .updateBalancingState(any(ConnectivityState.class), pickerCaptor.capture()); + assertThat(pickerCaptor.getValue()).isInstanceOf(ReadyPicker.class); + + ReadyPicker picker = (ReadyPicker) pickerCaptor.getValue(); + + assertThat(picker.getList()).containsExactlyElementsIn(subchannels); + + // Make random return 0, then 2 for the sample indexes. + when(mockRandom.nextInt(subchannels.size())).thenReturn(0, 2); + PickResult pickResult1 = picker.pickSubchannel(mockArgs); + verify(mockRandom, times(choiceCount)).nextInt(subchannels.size()); + assertEquals(subchannels.get(0), pickResult1.getSubchannel()); + // This simulates sending the actual RPC on the picked channel + ClientStreamTracer streamTracer1 = + pickResult1.getStreamTracerFactory() + .newClientStreamTracer(StreamInfo.newBuilder().build(), new Metadata()); + streamTracer1.streamCreated(Attributes.EMPTY, new Metadata()); + assertEquals(1, + pickResult1.getSubchannel().getAttributes().get(IN_FLIGHTS).get()); + + // For the second pick it should pick the one with lower inFlight. + when(mockRandom.nextInt(subchannels.size())).thenReturn(0, 2); + PickResult pickResult2 = picker.pickSubchannel(mockArgs); + // Since this is the second pick we expect the total random samples to be choiceCount * 2 + verify(mockRandom, times(choiceCount * 2)).nextInt(subchannels.size()); + assertEquals(subchannels.get(2), pickResult2.getSubchannel()); + + // For the third pick we unavoidably pick subchannel with index 1. + when(mockRandom.nextInt(subchannels.size())).thenReturn(1, 1); + PickResult pickResult3 = picker.pickSubchannel(mockArgs); + verify(mockRandom, times(choiceCount * 3)).nextInt(subchannels.size()); + assertEquals(subchannels.get(1), pickResult3.getSubchannel()); + + // Finally ensure a finished RPC decreases inFlight + streamTracer1.streamClosed(Status.OK); + assertEquals(0, + pickResult1.getSubchannel().getAttributes().get(IN_FLIGHTS).get()); + } + + @Test + public void pickerEmptyList() throws Exception { + SubchannelPicker picker = new EmptyPicker(Status.UNKNOWN); + + assertEquals(null, picker.pickSubchannel(mockArgs).getSubchannel()); + assertEquals(Status.UNKNOWN, + picker.pickSubchannel(mockArgs).getStatus()); + } + + @Test + public void nameResolutionErrorWithNoChannels() throws Exception { + Status error = Status.NOT_FOUND.withDescription("nameResolutionError"); + loadBalancer.handleNameResolutionError(error); + verify(mockHelper).updateBalancingState(eq(TRANSIENT_FAILURE), pickerCaptor.capture()); + LoadBalancer.PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mockArgs); + assertNull(pickResult.getSubchannel()); + assertEquals(error, pickResult.getStatus()); + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void nameResolutionErrorWithActiveChannels() throws Exception { + int choiceCount = 8; + final Subchannel readySubchannel = subchannels.values().iterator().next(); + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setLoadBalancingPolicyConfig(new LeastRequestConfig(choiceCount)) + .setAddresses(servers).setAttributes(affinity).build()); + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + loadBalancer.handleNameResolutionError(Status.NOT_FOUND.withDescription("nameResolutionError")); + + verify(mockHelper, times(3)).createSubchannel(any(CreateSubchannelArgs.class)); + verify(mockHelper, times(2)) + .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + + Iterator stateIterator = stateCaptor.getAllValues().iterator(); + assertEquals(CONNECTING, stateIterator.next()); + assertEquals(READY, stateIterator.next()); + + LoadBalancer.PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mockArgs); + verify(mockRandom, times(choiceCount)).nextInt(1); + assertEquals(readySubchannel, pickResult.getSubchannel()); + assertEquals(Status.OK.getCode(), pickResult.getStatus().getCode()); + + LoadBalancer.PickResult pickResult2 = pickerCaptor.getValue().pickSubchannel(mockArgs); + verify(mockRandom, times(choiceCount * 2)).nextInt(1); + assertEquals(readySubchannel, pickResult2.getSubchannel()); + verifyNoMoreInteractions(mockHelper); + } + + @Test + public void subchannelStateIsolation() throws Exception { + Iterator subchannelIterator = subchannels.values().iterator(); + Subchannel sc1 = subchannelIterator.next(); + Subchannel sc2 = subchannelIterator.next(); + Subchannel sc3 = subchannelIterator.next(); + + loadBalancer.handleResolvedAddresses( + ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(Attributes.EMPTY) + .build()); + verify(sc1, times(1)).requestConnection(); + verify(sc2, times(1)).requestConnection(); + verify(sc3, times(1)).requestConnection(); + + deliverSubchannelState(sc1, ConnectivityStateInfo.forNonError(READY)); + deliverSubchannelState(sc2, ConnectivityStateInfo.forNonError(READY)); + deliverSubchannelState(sc3, ConnectivityStateInfo.forNonError(READY)); + deliverSubchannelState(sc2, ConnectivityStateInfo.forNonError(IDLE)); + deliverSubchannelState(sc3, ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE)); + + verify(mockHelper, times(6)) + .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + Iterator stateIterator = stateCaptor.getAllValues().iterator(); + Iterator pickers = pickerCaptor.getAllValues().iterator(); + // The picker is incrementally updated as subchannels become READY + assertEquals(CONNECTING, stateIterator.next()); + assertThat(pickers.next()).isInstanceOf(EmptyPicker.class); + assertEquals(READY, stateIterator.next()); + assertThat(getList(pickers.next())).containsExactly(sc1); + assertEquals(READY, stateIterator.next()); + assertThat(getList(pickers.next())).containsExactly(sc1, sc2); + assertEquals(READY, stateIterator.next()); + assertThat(getList(pickers.next())).containsExactly(sc1, sc2, sc3); + // The IDLE subchannel is dropped from the picker, but a reconnection is requested + assertEquals(READY, stateIterator.next()); + assertThat(getList(pickers.next())).containsExactly(sc1, sc3); + verify(sc2, times(2)).requestConnection(); + // The failing subchannel is dropped from the picker, with no requested reconnect + assertEquals(READY, stateIterator.next()); + assertThat(getList(pickers.next())).containsExactly(sc1); + verify(sc3, times(1)).requestConnection(); + assertThat(stateIterator.hasNext()).isFalse(); + assertThat(pickers.hasNext()).isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void readyPicker_emptyList() { + // ready picker list must be non-empty + new ReadyPicker(Collections.emptyList(), 2, mockRandom); + } + + @Test + public void internalPickerComparisons() { + EmptyPicker emptyOk1 = new EmptyPicker(Status.OK); + EmptyPicker emptyOk2 = new EmptyPicker(Status.OK.withDescription("different OK")); + EmptyPicker emptyErr = new EmptyPicker(Status.UNKNOWN.withDescription("¯\\_(ツ)_//¯")); + + Iterator subchannelIterator = subchannels.values().iterator(); + Subchannel sc1 = subchannelIterator.next(); + Subchannel sc2 = subchannelIterator.next(); + ReadyPicker ready1 = new ReadyPicker(Arrays.asList(sc1, sc2), 2, mockRandom); + ReadyPicker ready2 = new ReadyPicker(Arrays.asList(sc1), 2, mockRandom); + ReadyPicker ready3 = new ReadyPicker(Arrays.asList(sc2, sc1), 2, mockRandom); + ReadyPicker ready4 = new ReadyPicker(Arrays.asList(sc1, sc2), 2, mockRandom); + ReadyPicker ready5 = new ReadyPicker(Arrays.asList(sc2, sc1), 2, mockRandom); + ReadyPicker ready6 = new ReadyPicker(Arrays.asList(sc2, sc1), 8, mockRandom); + + assertTrue(emptyOk1.isEquivalentTo(emptyOk2)); + assertFalse(emptyOk1.isEquivalentTo(emptyErr)); + assertFalse(ready1.isEquivalentTo(ready2)); + assertTrue(ready1.isEquivalentTo(ready3)); + assertTrue(ready3.isEquivalentTo(ready4)); + assertTrue(ready4.isEquivalentTo(ready5)); + assertFalse(emptyOk1.isEquivalentTo(ready1)); + assertFalse(ready1.isEquivalentTo(emptyOk1)); + assertFalse(ready5.isEquivalentTo(ready6)); + } + + private static List getList(SubchannelPicker picker) { + return picker instanceof ReadyPicker ? ((ReadyPicker) picker).getList() : + Collections.emptyList(); + } + + private void deliverSubchannelState(Subchannel subchannel, ConnectivityStateInfo newState) { + subchannelStateListeners.get(subchannel).onSubchannelState(newState); + } + + private static class FakeSocketAddress extends SocketAddress { + final String name; + + FakeSocketAddress(String name) { + this.name = name; + } + + @Override + public String toString() { + return "FakeSocketAddress-" + name; + } + } +} diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index a7e2e916b3e..9a23770512c 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; import static io.grpc.ConnectivityState.CONNECTING; +import static io.grpc.ConnectivityState.IDLE; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER; @@ -398,6 +399,202 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { verify(balancer3).shutdown(); } + @Test + public void idleToConnectingDoesNotTriggerFailOver() { + PriorityChildConfig priorityChildConfig0 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig1 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityLbConfig priorityLbConfig = + new PriorityLbConfig( + ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), + ImmutableList.of("p0", "p1")); + priorityLb.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setLoadBalancingPolicyConfig(priorityLbConfig) + .build()); + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + Helper helper0 = Iterables.getOnlyElement(fooHelpers); + + // p0 gets IDLE. + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p0 goes to CONNECTING + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // no failover happened + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + } + + @Test + public void readyToConnectDoesNotFailOverButUpdatesPicker() { + PriorityChildConfig priorityChildConfig0 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig1 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityLbConfig priorityLbConfig = + new PriorityLbConfig( + ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1), + ImmutableList.of("p0", "p1")); + priorityLb.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setLoadBalancingPolicyConfig(priorityLbConfig) + .build()); + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + Helper helper0 = Iterables.getOnlyElement(fooHelpers); + + // p0 gets READY. + final Subchannel subchannel0 = mock(Subchannel.class); + helper0.updateBalancingState( + READY, + new SubchannelPicker() { + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withSubchannel(subchannel0); + } + }); + assertCurrentPickerPicksSubchannel(subchannel0); + + // p0 goes to CONNECTING + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // no failover happened + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + + // resolution update without priority change does not trigger failover + Attributes.Key fooKey = Attributes.Key.create("fooKey"); + priorityLb.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setLoadBalancingPolicyConfig(priorityLbConfig) + .setAttributes(Attributes.newBuilder().set(fooKey, "barVal").build()) + .build()); + + assertCurrentPickerIsBufferPicker(); + + // no failover happened + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + } + + @Test + public void typicalPriorityFailOverFlowWithIdleUpdate() { + PriorityChildConfig priorityChildConfig0 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig1 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig2 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityChildConfig priorityChildConfig3 = + new PriorityChildConfig(new PolicySelection(fooLbProvider, new Object()), true); + PriorityLbConfig priorityLbConfig = + new PriorityLbConfig( + ImmutableMap.of("p0", priorityChildConfig0, "p1", priorityChildConfig1, + "p2", priorityChildConfig2, "p3", priorityChildConfig3), + ImmutableList.of("p0", "p1", "p2", "p3")); + priorityLb.handleResolvedAddresses( + ResolvedAddresses.newBuilder() + .setAddresses(ImmutableList.of()) + .setLoadBalancingPolicyConfig(priorityLbConfig) + .build()); + assertThat(fooBalancers).hasSize(1); + assertThat(fooHelpers).hasSize(1); + LoadBalancer balancer0 = Iterables.getLast(fooBalancers); + Helper helper0 = Iterables.getOnlyElement(fooHelpers); + + // p0 gets IDLE. + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p0 fails over to p1 immediately. + helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.ABORTED)); + assertLatestConnectivityState(CONNECTING); + assertThat(fooBalancers).hasSize(2); + assertThat(fooHelpers).hasSize(2); + LoadBalancer balancer1 = Iterables.getLast(fooBalancers); + + // p1 timeout, and fails over to p2 + fakeClock.forwardTime(10, TimeUnit.SECONDS); + assertLatestConnectivityState(CONNECTING); + assertThat(fooBalancers).hasSize(3); + assertThat(fooHelpers).hasSize(3); + LoadBalancer balancer2 = Iterables.getLast(fooBalancers); + Helper helper2 = Iterables.getLast(fooHelpers); + + // p2 gets IDLE + helper2.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p0 gets back to IDLE + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p2 fails but does not affect overall picker + helper2.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE)); + assertCurrentPickerIsBufferPicker(); + + // p0 fails over to p3 immediately since p1 already timeout and p2 already in TRANSIENT_FAILURE. + helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE)); + assertLatestConnectivityState(CONNECTING); + assertThat(fooBalancers).hasSize(4); + assertThat(fooHelpers).hasSize(4); + LoadBalancer balancer3 = Iterables.getLast(fooBalancers); + Helper helper3 = Iterables.getLast(fooHelpers); + + // p3 timeout then the channel should go to TRANSIENT_FAILURE + fakeClock.forwardTime(10, TimeUnit.SECONDS); + assertCurrentPickerReturnsError(Status.Code.UNAVAILABLE, "timeout"); + + // p3 fails then the picker should have error status updated + helper3.updateBalancingState( + TRANSIENT_FAILURE, new ErrorPicker(Status.DATA_LOSS.withDescription("foo"))); + assertCurrentPickerReturnsError(Status.Code.DATA_LOSS, "foo"); + + // p2 gets back to IDLE + helper2.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p0 gets back to IDLE + helper0.updateBalancingState( + IDLE, + BUFFER_PICKER); + assertCurrentPickerIsBufferPicker(); + + // p0 fails over to p2 and picker is updated to p2's existing picker. + helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE)); + assertCurrentPickerIsBufferPicker(); + + // Deactivate child balancer get deleted. + fakeClock.forwardTime(15, TimeUnit.MINUTES); + verify(balancer0, never()).shutdown(); + verify(balancer1, never()).shutdown(); + verify(balancer2, never()).shutdown(); + verify(balancer3).shutdown(); + } + @Test public void bypassReresolutionRequestsIfConfiged() { PriorityChildConfig priorityChildConfig0 = @@ -472,4 +669,10 @@ private void assertCurrentPickerPicksSubchannel(Subchannel expectedSubchannelToP PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); assertThat(pickResult.getSubchannel()).isEqualTo(expectedSubchannelToPick); } + + private void assertCurrentPickerIsBufferPicker() { + assertLatestConnectivityState(IDLE); + PickResult pickResult = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)); + assertThat(pickResult).isEqualTo(PickResult.withNoResult()); + } } diff --git a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java index a39a5495c09..e7d090b6cd1 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientWrapperForServerSdsTestMisc.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.SettableFuture; import io.grpc.Server; import io.grpc.ServerBuilder; @@ -66,8 +67,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -160,10 +159,10 @@ public void run() { assertThat(ldsWatched).isEqualTo("grpc/server?udpa.resource.listening_address=0.0.0.0:" + PORT); EnvoyServerProtoData.Listener listener = - new EnvoyServerProtoData.Listener( + EnvoyServerProtoData.Listener.create( "listener1", "10.1.2.3", - Collections.emptyList(), + ImmutableList.of(), null); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); xdsClient.ldsWatcher.onChanged(listenerUpdate); @@ -268,7 +267,7 @@ public void releaseOldSupplierOnChangedOnShutdown_verifyClose() throws Exception assertThat(returnedSupplier.getTlsContext()).isSameInstanceAs(tlsContext1); callUpdateSslContext(returnedSupplier); XdsServerTestHelper - .generateListenerUpdate(xdsClient, Arrays.asList(1234), tlsContext2, + .generateListenerUpdate(xdsClient, ImmutableList.of(1234), tlsContext2, tlsContext3, tlsContextManager); returnedSupplier = getSslContextProviderSupplier(selectorManager.getSelectorToUpdateSelector()); assertThat(returnedSupplier.getTlsContext()).isSameInstanceAs(tlsContext2); @@ -379,7 +378,7 @@ public void run() { }); xdsClient.ldsResource.get(5, TimeUnit.SECONDS); XdsServerTestHelper - .generateListenerUpdate(xdsClient, Arrays.asList(), tlsContext, + .generateListenerUpdate(xdsClient, ImmutableList.of(), tlsContext, tlsContextForDefaultFilterChain, tlsContextManager); start.get(5, TimeUnit.SECONDS); InetAddress ipRemoteAddress = InetAddress.getByName("10.4.5.6"); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 7968b7fb366..8cc78b7447c 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -72,11 +72,13 @@ import io.grpc.xds.Bootstrapper.AuthorityInfo; import io.grpc.xds.Bootstrapper.BootstrapInfo; import io.grpc.xds.Bootstrapper.ServerInfo; +import io.grpc.xds.ClusterSpecifierPlugin.NamedPluginConfig; import io.grpc.xds.EnvoyProtoData.Node; import io.grpc.xds.FaultConfig.FaultAbort; import io.grpc.xds.FaultConfig.FaultDelay; import io.grpc.xds.Filter.FilterConfig; import io.grpc.xds.Filter.NamedFilterConfig; +import io.grpc.xds.RouteLookupServiceClusterSpecifierPlugin.RlsPluginConfig; import io.grpc.xds.VirtualHost.Route; import io.grpc.xds.VirtualHost.Route.RouteAction; import io.grpc.xds.VirtualHost.Route.RouteAction.ClusterWeight; @@ -87,6 +89,7 @@ import io.grpc.xds.XdsNameResolverProvider.XdsClientPoolFactory; import io.grpc.xds.internal.Matchers.HeaderMatcher; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -267,12 +270,12 @@ public void resolving_targetAuthorityInAuthoritiesMap() { .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( - "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s", + "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( "td.googleapis.com", InsecureChannelCredentials.create(), true))))) .build(); - expectedLdsResourceName = - "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/%5B::FFFF:129.144.52.38%5D:80"; + expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified resolver = new XdsNameResolver( "xds.authority.com", serviceAuthority, serviceConfigParser, syncContext, scheduler, xdsClientPoolFactory, mockRandom, FilterRegistry.getDefaultRegistry(), null); @@ -428,7 +431,21 @@ public void resolving_encounterErrorLdsWatcherOnly() { verify(mockListener).onError(errorCaptor.capture()); Status error = errorCaptor.getValue(); assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("server unreachable"); + assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY + + ". xDS server returned: UNAVAILABLE: server unreachable."); + } + + @Test + public void resolving_translateErrorLds() { + resolver.start(mockListener); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverError(Status.NOT_FOUND.withDescription("server unreachable")); + verify(mockListener).onError(errorCaptor.capture()); + Status error = errorCaptor.getValue(); + assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY + + ". xDS server returned: NOT_FOUND: server unreachable."); + assertThat(error.getCause()).isNull(); } @Test @@ -438,10 +455,14 @@ public void resolving_encounterErrorLdsAndRdsWatchers() { xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); xdsClient.deliverError(Status.UNAVAILABLE.withDescription("server unreachable")); verify(mockListener, times(2)).onError(errorCaptor.capture()); - for (Status error : errorCaptor.getAllValues()) { - assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); - assertThat(error.getDescription()).isEqualTo("server unreachable"); - } + Status error = errorCaptor.getAllValues().get(0); + assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo("Unable to load LDS " + AUTHORITY + + ". xDS server returned: UNAVAILABLE: server unreachable."); + error = errorCaptor.getAllValues().get(1); + assertThat(error.getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(error.getDescription()).isEqualTo("Unable to load RDS " + RDS_RESOURCE_NAME + + ". xDS server returned: UNAVAILABLE: server unreachable."); } @Test @@ -494,7 +515,7 @@ public void resolved_noTimeout() { verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); - assertCallSelectResult(call1, configSelector, cluster1, null); + assertCallSelectClusterResult(call1, configSelector, cluster1, null); } @Test @@ -513,7 +534,7 @@ public void resolved_fallbackToHttpMaxStreamDurationAsTimeout() { verify(mockListener).onResult(resolutionResultCaptor.capture()); ResolutionResult result = resolutionResultCaptor.getValue(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); - assertCallSelectResult(call1, configSelector, cluster1, 5.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 5.0); } @Test @@ -562,7 +583,7 @@ public void retryPolicyInPerMethodConfigGeneratedByResolverIsValid() { @Test public void resolved_simpleCallSucceeds() { InternalConfigSelector configSelector = resolveToClusters(); - assertCallSelectResult(call1, configSelector, cluster1, 15.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 15.0); testCall.deliverResponseHeaders(); verifyNoMoreInteractions(mockListener); } @@ -756,7 +777,7 @@ public void resolved_rpcHashingByChannelId() { @Test public void resolved_resourceUpdateAfterCallStarted() { InternalConfigSelector configSelector = resolveToClusters(); - assertCallSelectResult(call1, configSelector, cluster1, 15.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 15.0); TestCall firstCall = testCall; reset(mockListener); @@ -784,7 +805,7 @@ public void resolved_resourceUpdateAfterCallStarted() { (Map) result.getServiceConfig().getConfig()); assertThat(result.getAttributes().get(InternalConfigSelector.KEY)) .isSameInstanceAs(configSelector); - assertCallSelectResult(call1, configSelector, "another-cluster", 20.0); + assertCallSelectClusterResult(call1, configSelector, "another-cluster", 20.0); firstCall.deliverErrorStatus(); // completes previous call verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); @@ -824,7 +845,7 @@ public void resolved_resourceUpdatedBeforeCallStarted() { (Map) result.getServiceConfig().getConfig()); assertThat(result.getAttributes().get(InternalConfigSelector.KEY)) .isSameInstanceAs(configSelector); - assertCallSelectResult(call1, configSelector, "another-cluster", 20.0); + assertCallSelectClusterResult(call1, configSelector, "another-cluster", 20.0); verifyNoMoreInteractions(mockListener); } @@ -833,7 +854,7 @@ public void resolved_resourceUpdatedBeforeCallStarted() { @Test public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { InternalConfigSelector configSelector = resolveToClusters(); - assertCallSelectResult(call1, configSelector, cluster1, 15.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 15.0); reset(mockListener); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); @@ -873,13 +894,13 @@ public void resolved_raceBetweenCallAndRepeatedResourceUpdate() { TimeUnit.SECONDS.toNanos(15L), null), ImmutableMap.of()))); verifyNoMoreInteractions(mockListener); // no cluster added/deleted - assertCallSelectResult(call1, configSelector, "another-cluster", 15.0); + assertCallSelectClusterResult(call1, configSelector, "another-cluster", 15.0); } @Test public void resolved_raceBetweenClusterReleasedAndResourceUpdateAddBackAgain() { InternalConfigSelector configSelector = resolveToClusters(); - assertCallSelectResult(call1, configSelector, cluster1, 15.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 15.0); FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); xdsClient.deliverLdsUpdate( Collections.singletonList( @@ -933,8 +954,105 @@ public void resolved_simpleCallSucceeds_routeToWeightedCluster() { Arrays.asList(cluster1, cluster2), (Map) result.getServiceConfig().getConfig()); assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull(); InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); - assertCallSelectResult(call1, configSelector, cluster2, 20.0); - assertCallSelectResult(call1, configSelector, cluster1, 20.0); + assertCallSelectClusterResult(call1, configSelector, cluster2, 20.0); + assertCallSelectClusterResult(call1, configSelector, cluster1, 20.0); + } + + @Test + public void resolved_simpleCallSucceeds_routeToRls() { + when(mockRandom.nextInt(anyInt())).thenReturn(90, 10); + resolver.start(mockListener); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverLdsUpdate( + Collections.singletonList( + Route.forAction( + RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forClusterSpecifierPlugin( + NamedPluginConfig.create( + "rls-plugin-foo", + RlsPluginConfig.create( + ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"))), + Collections.emptyList(), + TimeUnit.SECONDS.toNanos(20L), + null), + ImmutableMap.of()))); + verify(mockListener).onResult(resolutionResultCaptor.capture()); + ResolutionResult result = resolutionResultCaptor.getValue(); + assertThat(result.getAddresses()).isEmpty(); + @SuppressWarnings("unchecked") + Map resultServiceConfig = (Map) result.getServiceConfig().getConfig(); + List> rawLbConfigs = + JsonUtil.getListOfObjects(resultServiceConfig, "loadBalancingConfig"); + Map lbConfig = Iterables.getOnlyElement(rawLbConfigs); + assertThat(lbConfig.keySet()).containsExactly("cluster_manager_experimental"); + Map clusterManagerLbConfig = + JsonUtil.getObject(lbConfig, "cluster_manager_experimental"); + Map expectedRlsLbConfig = ImmutableMap.of( + "routeLookupConfig", + ImmutableMap.of("lookupService", "rls-cbt.googleapis.com"), + "childPolicy", + ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of())), + "childPolicyConfigTargetFieldName", + "cluster"); + Map expectedClusterManagerLbConfig = ImmutableMap.of( + "childPolicy", + ImmutableMap.of( + "cluster_specifier_plugin:rls-plugin-foo", + ImmutableMap.of( + "lbPolicy", + ImmutableList.of(ImmutableMap.of("rls_experimental", expectedRlsLbConfig))))); + assertThat(clusterManagerLbConfig).isEqualTo(expectedClusterManagerLbConfig); + + assertThat(result.getAttributes().get(InternalXdsAttributes.XDS_CLIENT_POOL)).isNotNull(); + InternalConfigSelector configSelector = result.getAttributes().get(InternalConfigSelector.KEY); + assertCallSelectRlsPluginResult( + call1, configSelector, "rls-plugin-foo", 20.0); + + // config changed + xdsClient.deliverLdsUpdate( + Collections.singletonList( + Route.forAction( + RouteMatch.withPathExactOnly(call1.getFullMethodNameForPath()), + RouteAction.forClusterSpecifierPlugin( + NamedPluginConfig.create( + "rls-plugin-foo", + RlsPluginConfig.create( + // changed + ImmutableMap.of("lookupService", "rls-cbt-2.googleapis.com"))), + Collections.emptyList(), + // changed + TimeUnit.SECONDS.toNanos(30L), + null), + ImmutableMap.of()))); + verify(mockListener, times(2)).onResult(resolutionResultCaptor.capture()); + ResolutionResult result2 = resolutionResultCaptor.getValue(); + @SuppressWarnings("unchecked") + Map resultServiceConfig2 = (Map) result2.getServiceConfig().getConfig(); + List> rawLbConfigs2 = + JsonUtil.getListOfObjects(resultServiceConfig2, "loadBalancingConfig"); + Map lbConfig2 = Iterables.getOnlyElement(rawLbConfigs2); + assertThat(lbConfig2.keySet()).containsExactly("cluster_manager_experimental"); + Map clusterManagerLbConfig2 = + JsonUtil.getObject(lbConfig2, "cluster_manager_experimental"); + Map expectedRlsLbConfig2 = ImmutableMap.of( + "routeLookupConfig", + ImmutableMap.of("lookupService", "rls-cbt-2.googleapis.com"), + "childPolicy", + ImmutableList.of(ImmutableMap.of("cds_experimental", ImmutableMap.of())), + "childPolicyConfigTargetFieldName", + "cluster"); + Map expectedClusterManagerLbConfig2 = ImmutableMap.of( + "childPolicy", + ImmutableMap.of( + "cluster_specifier_plugin:rls-plugin-foo", + ImmutableMap.of( + "lbPolicy", + ImmutableList.of(ImmutableMap.of("rls_experimental", expectedRlsLbConfig2))))); + assertThat(clusterManagerLbConfig2).isEqualTo(expectedClusterManagerLbConfig2); + + InternalConfigSelector configSelector2 = result.getAttributes().get(InternalConfigSelector.KEY); + assertCallSelectRlsPluginResult( + call1, configSelector2, "rls-plugin-foo", 30.0); } @SuppressWarnings("unchecked") @@ -945,7 +1063,7 @@ private void assertEmptyResolutionResult() { assertThat((Map) result.getServiceConfig().getConfig()).isEmpty(); } - private void assertCallSelectResult( + private void assertCallSelectClusterResult( CallInfo call, InternalConfigSelector configSelector, String expectedCluster, @Nullable Double expectedTimeoutSec) { Result result = configSelector.selectConfig( @@ -956,7 +1074,7 @@ private void assertCallSelectResult( call.methodDescriptor, CallOptions.DEFAULT, channel); clientCall.start(new NoopClientCallListener(), new Metadata()); assertThat(testCall.callOptions.getOption(XdsNameResolver.CLUSTER_SELECTION_KEY)) - .isEqualTo(expectedCluster); + .isEqualTo("cluster:" + expectedCluster); @SuppressWarnings("unchecked") Map config = (Map) result.getConfig(); if (expectedTimeoutSec != null) { @@ -973,6 +1091,28 @@ private void assertCallSelectResult( } } + private void assertCallSelectRlsPluginResult( + CallInfo call, InternalConfigSelector configSelector, String expectedPluginName, + Double expectedTimeoutSec) { + Result result = configSelector.selectConfig( + new PickSubchannelArgsImpl(call.methodDescriptor, new Metadata(), CallOptions.DEFAULT)); + assertThat(result.getStatus().isOk()).isTrue(); + ClientInterceptor interceptor = result.getInterceptor(); + ClientCall clientCall = interceptor.interceptCall( + call.methodDescriptor, CallOptions.DEFAULT, channel); + clientCall.start(new NoopClientCallListener(), new Metadata()); + assertThat(testCall.callOptions.getOption(XdsNameResolver.CLUSTER_SELECTION_KEY)) + .isEqualTo("cluster_specifier_plugin:" + expectedPluginName); + @SuppressWarnings("unchecked") + Map config = (Map) result.getConfig(); + List> rawMethodConfigs = + JsonUtil.getListOfObjects(config, "methodConfig"); + Map methodConfig = Iterables.getOnlyElement(rawMethodConfigs); + List> methods = JsonUtil.getListOfObjects(methodConfig, "name"); + assertThat(Iterables.getOnlyElement(methods)).isEmpty(); + assertThat(JsonUtil.getString(methodConfig, "timeout")).isEqualTo(expectedTimeoutSec + "s"); + } + @SuppressWarnings("unchecked") private InternalConfigSelector resolveToClusters() { resolver.start(mockListener); @@ -1014,56 +1154,107 @@ private static void assertServiceConfigForLoadBalancingConfig( JsonUtil.getObject(lbConfig, "cluster_manager_experimental"); Map clusterManagerChildLbPolicies = JsonUtil.getObject(clusterManagerLbConfig, "childPolicy"); - assertThat(clusterManagerChildLbPolicies.keySet()).containsExactlyElementsIn(clusters); + List expectedChildLbClusterNames = new ArrayList<>(clusters.size()); for (String cluster : clusters) { - Map childLbConfig = JsonUtil.getObject(clusterManagerChildLbPolicies, cluster); + expectedChildLbClusterNames.add("cluster:" + cluster); + } + assertThat(clusterManagerChildLbPolicies.keySet()) + .containsExactlyElementsIn(expectedChildLbClusterNames); + for (int i = 0; i < clusters.size(); i++) { + Map childLbConfig = + JsonUtil.getObject(clusterManagerChildLbPolicies, expectedChildLbClusterNames.get(i)); assertThat(childLbConfig.keySet()).containsExactly("lbPolicy"); List> childLbConfigValues = JsonUtil.getListOfObjects(childLbConfig, "lbPolicy"); Map cdsLbPolicy = Iterables.getOnlyElement(childLbConfigValues); assertThat(cdsLbPolicy.keySet()).containsExactly("cds_experimental"); assertThat(JsonUtil.getObject(cdsLbPolicy, "cds_experimental")) - .containsExactly("cluster", cluster); + .containsExactly("cluster", clusters.get(i)); } } - @SuppressWarnings("unchecked") @Test - public void generateServiceConfig_forLoadBalancingConfig() throws IOException { - List clusters = Arrays.asList("cluster-foo", "cluster-bar", "cluster-baz"); - String expectedServiceConfigJson = "{\n" - + " \"loadBalancingConfig\": [{\n" - + " \"cluster_manager_experimental\": {\n" - + " \"childPolicy\": {\n" - + " \"cluster-foo\": {\n" - + " \"lbPolicy\": [{\n" - + " \"cds_experimental\": {\n" - + " \"cluster\": \"cluster-foo\"\n" - + " }\n" - + " }]\n" - + " },\n" - + " \"cluster-bar\": {\n" - + " \"lbPolicy\": [{\n" - + " \"cds_experimental\": {\n" - + " \"cluster\": \"cluster-bar\"\n" - + " }\n" - + " }]\n" - + " },\n" - + " \"cluster-baz\": {\n" - + " \"lbPolicy\": [{\n" - + " \"cds_experimental\": {\n" - + " \"cluster\": \"cluster-baz\"\n" - + " }\n" - + " }]\n" - + " }\n" - + " }\n" - + " }\n" - + " }]\n" - + "}"; - Map expectedServiceConfig = - (Map) JsonParser.parse(expectedServiceConfigJson); - assertThat(XdsNameResolver.generateServiceConfigWithLoadBalancingConfig(clusters)) - .isEqualTo(expectedServiceConfig); + public void generateServiceConfig_forClusterManagerLoadBalancingConfig() throws IOException { + Route route1 = Route.forAction( + RouteMatch.withPathExactOnly("HelloService/hi"), + RouteAction.forCluster( + "cluster-foo", Collections.emptyList(), TimeUnit.SECONDS.toNanos(15L), null), + ImmutableMap.of()); + Route route2 = Route.forAction( + RouteMatch.withPathExactOnly("HelloService/hello"), + RouteAction.forWeightedClusters( + ImmutableList.of( + ClusterWeight.create("cluster-bar", 50, ImmutableMap.of()), + ClusterWeight.create("cluster-baz", 50, ImmutableMap.of())), + ImmutableList.of(), + TimeUnit.SECONDS.toNanos(15L), + null), + ImmutableMap.of()); + Map rlsConfig = ImmutableMap.of("lookupService", "rls.bigtable.google.com"); + Route route3 = Route.forAction( + RouteMatch.withPathExactOnly("HelloService/greetings"), + RouteAction.forClusterSpecifierPlugin( + NamedPluginConfig.create("plugin-foo", RlsPluginConfig.create(rlsConfig)), + Collections.emptyList(), + TimeUnit.SECONDS.toNanos(20L), + null), + ImmutableMap.of()); + + resolver.start(mockListener); + FakeXdsClient xdsClient = (FakeXdsClient) resolver.getXdsClient(); + xdsClient.deliverLdsUpdateForRdsName(RDS_RESOURCE_NAME); + VirtualHost virtualHost = + VirtualHost.create("virtualhost", Collections.singletonList(AUTHORITY), + ImmutableList.of(route1, route2, route3), + ImmutableMap.of()); + xdsClient.deliverRdsUpdate(RDS_RESOURCE_NAME, Collections.singletonList(virtualHost)); + + verify(mockListener).onResult(resolutionResultCaptor.capture()); + String expectedServiceConfigJson = + "{\n" + + " \"loadBalancingConfig\": [{\n" + + " \"cluster_manager_experimental\": {\n" + + " \"childPolicy\": {\n" + + " \"cluster:cluster-foo\": {\n" + + " \"lbPolicy\": [{\n" + + " \"cds_experimental\": {\n" + + " \"cluster\": \"cluster-foo\"\n" + + " }\n" + + " }]\n" + + " },\n" + + " \"cluster:cluster-bar\": {\n" + + " \"lbPolicy\": [{\n" + + " \"cds_experimental\": {\n" + + " \"cluster\": \"cluster-bar\"\n" + + " }\n" + + " }]\n" + + " },\n" + + " \"cluster:cluster-baz\": {\n" + + " \"lbPolicy\": [{\n" + + " \"cds_experimental\": {\n" + + " \"cluster\": \"cluster-baz\"\n" + + " }\n" + + " }]\n" + + " },\n" + + " \"cluster_specifier_plugin:plugin-foo\": {\n" + + " \"lbPolicy\": [{\n" + + " \"rls_experimental\": {\n" + + " \"routeLookupConfig\": {\n" + + " \"lookupService\": \"rls.bigtable.google.com\"\n" + + " },\n" + + " \"childPolicy\": [\n" + + " {\"cds_experimental\": {}}\n" + + " ],\n" + + " \"childPolicyConfigTargetFieldName\": \"cluster\"\n" + + " }\n" + + " }]\n" + + " }\n" + + " }\n" + + " }\n" + + " }]\n" + + "}"; + assertThat(resolutionResultCaptor.getValue().getServiceConfig().getConfig()) + .isEqualTo(JsonParser.parse(expectedServiceConfigJson)); } @SuppressWarnings("unchecked") diff --git a/xds/src/test/java/io/grpc/xds/XdsSdsClientServerTest.java b/xds/src/test/java/io/grpc/xds/XdsSdsClientServerTest.java index 579542a2777..36669537255 100644 --- a/xds/src/test/java/io/grpc/xds/XdsSdsClientServerTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsSdsClientServerTest.java @@ -49,6 +49,7 @@ import io.grpc.testing.protobuf.SimpleRequest; import io.grpc.testing.protobuf.SimpleResponse; import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.DownstreamTlsContext; import io.grpc.xds.EnvoyServerProtoData.UpstreamTlsContext; import io.grpc.xds.Filter.FilterConfig; @@ -364,15 +365,15 @@ static EnvoyServerProtoData.Listener buildListener( String name, String address, DownstreamTlsContext tlsContext, TlsContextManager tlsContextManager) { EnvoyServerProtoData.FilterChainMatch filterChainMatch = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - null, - Arrays.asList(), - Arrays.asList(), - null); + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); String fullPath = "/" + SimpleServiceGrpc.SERVICE_NAME + "/" + "UnaryRpc"; RouteMatch routeMatch = RouteMatch.create( @@ -386,11 +387,11 @@ static EnvoyServerProtoData.Listener buildListener( HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), new ArrayList()); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch, httpConnectionManager, tlsContext, tlsContextManager); - EnvoyServerProtoData.Listener listener = - new EnvoyServerProtoData.Listener(name, address, Arrays.asList(defaultFilterChain), null); + EnvoyServerProtoData.Listener listener = EnvoyServerProtoData.Listener.create( + name, address, ImmutableList.of(defaultFilterChain), null); return listener; } diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java index 66b3d00a84b..15868ba414e 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java @@ -18,11 +18,13 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.SettableFuture; import io.grpc.InsecureChannelCredentials; import io.grpc.internal.ObjectPool; import io.grpc.xds.Bootstrapper.BootstrapInfo; +import io.grpc.xds.EnvoyServerProtoData.ConnectionSourceType; import io.grpc.xds.EnvoyServerProtoData.FilterChain; import io.grpc.xds.EnvoyServerProtoData.Listener; import io.grpc.xds.Filter.FilterConfig; @@ -61,13 +63,13 @@ static void generateListenerUpdate(FakeXdsClient xdsClient, EnvoyServerProtoData.DownstreamTlsContext tlsContext, TlsContextManager tlsContextManager) { EnvoyServerProtoData.Listener listener = buildTestListener("listener1", "10.1.2.3", - Arrays.asList(), tlsContext, null, tlsContextManager); + ImmutableList.of(), tlsContext, null, tlsContextManager); LdsUpdate listenerUpdate = LdsUpdate.forTcpListener(listener); xdsClient.deliverLdsUpdate(listenerUpdate); } static void generateListenerUpdate( - FakeXdsClient xdsClient, List sourcePorts, + FakeXdsClient xdsClient, ImmutableList sourcePorts, EnvoyServerProtoData.DownstreamTlsContext tlsContext, EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain, TlsContextManager tlsContextManager) { @@ -78,35 +80,45 @@ static void generateListenerUpdate( } static EnvoyServerProtoData.Listener buildTestListener( - String name, String address, List sourcePorts, + String name, String address, ImmutableList sourcePorts, EnvoyServerProtoData.DownstreamTlsContext tlsContext, EnvoyServerProtoData.DownstreamTlsContext tlsContextForDefaultFilterChain, TlsContextManager tlsContextManager) { EnvoyServerProtoData.FilterChainMatch filterChainMatch1 = - new EnvoyServerProtoData.FilterChainMatch( + EnvoyServerProtoData.FilterChainMatch.create( 0, - Arrays.asList(), - Arrays.asList(), - Arrays.asList(), - null, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ConnectionSourceType.ANY, sourcePorts, - Arrays.asList(), - null); + ImmutableList.of(), + ""); + EnvoyServerProtoData.FilterChainMatch defaultFilterChainMatch = + EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); VirtualHost virtualHost = VirtualHost.create( "virtual-host", Collections.singletonList("auth"), new ArrayList(), ImmutableMap.of()); HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), new ArrayList()); - EnvoyServerProtoData.FilterChain filterChain1 = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain1 = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", filterChainMatch1, httpConnectionManager, tlsContext, tlsContextManager); - EnvoyServerProtoData.FilterChain defaultFilterChain = new EnvoyServerProtoData.FilterChain( - "filter-chain-bar", null, httpConnectionManager, tlsContextForDefaultFilterChain, - tlsContextManager); + EnvoyServerProtoData.FilterChain defaultFilterChain = EnvoyServerProtoData.FilterChain.create( + "filter-chain-bar", defaultFilterChainMatch, httpConnectionManager, + tlsContextForDefaultFilterChain, tlsContextManager); EnvoyServerProtoData.Listener listener = - new EnvoyServerProtoData.Listener( - name, address, Arrays.asList(filterChain1), defaultFilterChain); + EnvoyServerProtoData.Listener.create( + name, address, ImmutableList.of(filterChain1), defaultFilterChain); return listener; } @@ -202,8 +214,8 @@ boolean isShutDown() { void deliverLdsUpdate(List filterChains, FilterChain defaultFilterChain) { - ldsWatcher.onChanged(LdsUpdate.forTcpListener(new Listener( - "listener", "0.0.0.0:1", filterChains, defaultFilterChain))); + ldsWatcher.onChanged(LdsUpdate.forTcpListener(Listener.create( + "listener", "0.0.0.0:1", ImmutableList.copyOf(filterChains), defaultFilterChain))); } void deliverLdsUpdate(LdsUpdate ldsUpdate) { diff --git a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java index f011b789da9..f8b8ca2e105 100644 --- a/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsServerWrapperTest.java @@ -99,8 +99,6 @@ public class XdsServerWrapperTest { @Mock private Server mockServer; @Mock - private static TlsContextManager tlsContextManager; - @Mock private XdsServingStatusListener listener; private FilterChainSelectorManager selectorManager = new FilterChainSelectorManager(); @@ -236,8 +234,8 @@ public void run() { assertThat(xdsClient.ldsResource).isNull(); assertThat(xdsClient.shutdown).isTrue(); verify(mockServer).shutdown(); - assertThat(f0.getSslContextProviderSupplier().isShutdown()).isTrue(); - assertThat(f1.getSslContextProviderSupplier().isShutdown()).isTrue(); + assertThat(f0.sslContextProviderSupplier().isShutdown()).isTrue(); + assertThat(f1.sslContextProviderSupplier().isShutdown()).isTrue(); when(mockServer.isTerminated()).thenReturn(true); when(mockServer.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(true); assertThat(xdsServerWrapper.awaitTermination(5, TimeUnit.SECONDS)).isTrue(); @@ -278,8 +276,8 @@ public void run() { assertThat(xdsClient.ldsResource).isNull(); assertThat(xdsClient.shutdown).isTrue(); verify(mockServer).shutdown(); - assertThat(f0.getSslContextProviderSupplier().isShutdown()).isTrue(); - assertThat(f1.getSslContextProviderSupplier().isShutdown()).isTrue(); + assertThat(f0.sslContextProviderSupplier().isShutdown()).isTrue(); + assertThat(f1.sslContextProviderSupplier().isShutdown()).isTrue(); assertThat(start.isDone()).isFalse(); //shall we set initialStatus when shutdown? } @@ -337,7 +335,7 @@ public void run() { xdsClient.ldsResource.get(5, TimeUnit.SECONDS); when(mockServer.start()).thenThrow(new IOException("error!")); FilterChain filterChain = createFilterChain("filter-chain-1", createRds("rds")); - SslContextProviderSupplier sslSupplier = filterChain.getSslContextProviderSupplier(); + SslContextProviderSupplier sslSupplier = filterChain.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain), null); xdsClient.rdsCount.await(5, TimeUnit.SECONDS); xdsClient.deliverRdsUpdate("rds", @@ -439,9 +437,9 @@ public void run() { ImmutableMap.of()); HttpConnectionManager httpConnectionManager = HttpConnectionManager.forVirtualHosts( 0L, Collections.singletonList(virtualHost), new ArrayList()); - EnvoyServerProtoData.FilterChain filterChain = new EnvoyServerProtoData.FilterChain( + EnvoyServerProtoData.FilterChain filterChain = EnvoyServerProtoData.FilterChain.create( "filter-chain-foo", createMatch(), httpConnectionManager, createTls(), - tlsContextManager); + mock(TlsContextManager.class)); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain), null); start.get(5000, TimeUnit.MILLISECONDS); assertThat(ldsWatched).isEqualTo("grpc/server?udpa.resource.listening_address=0.0.0.0:1"); @@ -511,7 +509,7 @@ public void run() { assertThat(realConfig.virtualHosts()).isEqualTo( Collections.singletonList(createVirtualHost("virtual-host-2"))); assertThat(selectorManager.getSelectorToUpdateSelector().getDefaultSslContextProviderSupplier()) - .isEqualTo(f3.getSslContextProviderSupplier()); + .isEqualTo(f3.sslContextProviderSupplier()); } @Test @@ -559,7 +557,7 @@ public void run() { Collections.singletonList(createVirtualHost("virtual-host-0"))); assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); assertThat(selectorManager.getSelectorToUpdateSelector().getDefaultSslContextProviderSupplier()) - .isSameInstanceAs(f2.getSslContextProviderSupplier()); + .isSameInstanceAs(f2.sslContextProviderSupplier()); EnvoyServerProtoData.FilterChain f3 = createFilterChain("filter-chain-3", createRds("r0")); EnvoyServerProtoData.FilterChain f4 = createFilterChain("filter-chain-4", createRds("r1")); @@ -589,7 +587,7 @@ public void run() { assertThat(realConfig.interceptors()).isEqualTo(ImmutableMap.of()); assertThat(selectorManager.getSelectorToUpdateSelector().getDefaultSslContextProviderSupplier()) - .isSameInstanceAs(f4.getSslContextProviderSupplier()); + .isSameInstanceAs(f4.sslContextProviderSupplier()); verify(mockServer, times(1)).start(); xdsServerWrapper.shutdown(); verify(mockServer, times(1)).shutdown(); @@ -677,7 +675,7 @@ public void run() { verify(listener, times(1)).onNotServing(any(StatusException.class)); verify(mockBuilder, times(1)).build(); FilterChain filterChain0 = createFilterChain("filter-chain-0", createRds("rds")); - SslContextProviderSupplier sslSupplier0 = filterChain0.getSslContextProviderSupplier(); + SslContextProviderSupplier sslSupplier0 = filterChain0.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain0), null); xdsClient.ldsWatcher.onError(Status.INTERNAL); assertThat(selectorManager.getSelectorToUpdateSelector()) @@ -690,7 +688,7 @@ public void run() { when(mockServer.start()).thenThrow(new IOException("error!")) .thenReturn(mockServer); FilterChain filterChain1 = createFilterChain("filter-chain-1", createRds("rds")); - SslContextProviderSupplier sslSupplier1 = filterChain1.getSslContextProviderSupplier(); + SslContextProviderSupplier sslSupplier1 = filterChain1.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain1), null); assertThat(sslSupplier0.isShutdown()).isTrue(); xdsClient.deliverRdsUpdate("rds", @@ -754,7 +752,7 @@ public void run() { .thenThrow(new IOException("error2!")) .thenReturn(mockServer); FilterChain filterChain2 = createFilterChain("filter-chain-2", createRds("rds")); - SslContextProviderSupplier sslSupplier2 = filterChain2.getSslContextProviderSupplier(); + SslContextProviderSupplier sslSupplier2 = filterChain2.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain2), null); xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-1"))); @@ -782,7 +780,7 @@ public void run() { // serving after not serving FilterChain filterChain3 = createFilterChain("filter-chain-2", createRds("rds")); - SslContextProviderSupplier sslSupplier3 = filterChain3.getSslContextProviderSupplier(); + SslContextProviderSupplier sslSupplier3 = filterChain3.sslContextProviderSupplier(); xdsClient.deliverLdsUpdate(Collections.singletonList(filterChain3), null); xdsClient.deliverRdsUpdate("rds", Collections.singletonList(createVirtualHost("virtual-host-1"))); @@ -1189,8 +1187,8 @@ public ServerCall.Listener interceptCall(ServerCallasList(), - Arrays.asList(), - Arrays.asList(), - EnvoyServerProtoData.ConnectionSourceType.ANY, - Arrays.asList(), - Arrays.asList(), - null); + return EnvoyServerProtoData.FilterChainMatch.create( + 0, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + EnvoyServerProtoData.ConnectionSourceType.ANY, + ImmutableList.of(), + ImmutableList.of(), + ""); } private static ServerRoutingConfig createRoutingConfig(String path, String domain, diff --git a/xds/src/test/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactoryTest.java index 06a3198b263..adc96a36336 100644 --- a/xds/src/test/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/sds/ClientSslContextProviderFactoryTest.java @@ -208,6 +208,72 @@ public void createCertProviderClientSslContextProvider_2providers() verifyWatcher(sslContextProvider, watcherCaptor[1]); } + @Test + public void createNewCertProviderClientSslContextProvider_withSans() { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[2]; + createAndRegisterProviderProvider(certificateProviderRegistry, watcherCaptor, "testca", 0); + createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "file_watcher", 1); + + CertificateValidationContext staticCertValidationContext = + CertificateValidationContext.newBuilder() + .addAllMatchSubjectAltNames( + ImmutableSet.of( + StringMatcher.newBuilder().setExact("foo").build(), + StringMatcher.newBuilder().setExact("bar").build())) + .build(); + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( + "gcp_id", + "cert-default", + "file_provider", + "root-default", + /* alpnProtocols= */ null, + staticCertValidationContext); + + Bootstrapper.BootstrapInfo bootstrapInfo = CommonBootstrapperTestUtils.getTestBootstrapInfo(); + clientSslContextProviderFactory = + new ClientSslContextProviderFactory( + bootstrapInfo, certProviderClientSslContextProviderFactory); + SslContextProvider sslContextProvider = + clientSslContextProviderFactory.create(upstreamTlsContext); + assertThat(sslContextProvider).isInstanceOf(CertProviderClientSslContextProvider.class); + verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[1]); + } + + @Test + public void createNewCertProviderClientSslContextProvider_onlyRootCert() { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[1]; + createAndRegisterProviderProvider(certificateProviderRegistry, watcherCaptor, "testca", 0); + CertificateValidationContext staticCertValidationContext = + CertificateValidationContext.newBuilder() + .addAllMatchSubjectAltNames( + ImmutableSet.of( + StringMatcher.newBuilder().setExact("foo").build(), + StringMatcher.newBuilder().setExact("bar").build())) + .build(); + UpstreamTlsContext upstreamTlsContext = + CommonTlsContextTestsUtil.buildNewUpstreamTlsContextForCertProviderInstance( + /* certInstanceName= */ null, + /* certName= */ null, + "gcp_id", + "root-default", + /* alpnProtocols= */ null, + staticCertValidationContext); + + Bootstrapper.BootstrapInfo bootstrapInfo = CommonBootstrapperTestUtils.getTestBootstrapInfo(); + clientSslContextProviderFactory = + new ClientSslContextProviderFactory( + bootstrapInfo, certProviderClientSslContextProviderFactory); + SslContextProvider sslContextProvider = + clientSslContextProviderFactory.create(upstreamTlsContext); + assertThat(sslContextProvider).isInstanceOf(CertProviderClientSslContextProvider.class); + verifyWatcher(sslContextProvider, watcherCaptor[0]); + } + @Test public void createNullCommonTlsContext_exception() throws IOException { clientSslContextProviderFactory = diff --git a/xds/src/test/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactoryTest.java b/xds/src/test/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactoryTest.java index a4bab618a36..7623b614001 100644 --- a/xds/src/test/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/sds/ServerSslContextProviderFactoryTest.java @@ -206,4 +206,41 @@ public void createCertProviderServerSslContextProvider_2providers() verifyWatcher(sslContextProvider, watcherCaptor[0]); verifyWatcher(sslContextProvider, watcherCaptor[1]); } + + @Test + public void createNewCertProviderServerSslContextProvider_withSans() + throws XdsInitializationException { + final CertificateProvider.DistributorWatcher[] watcherCaptor = + new CertificateProvider.DistributorWatcher[2]; + createAndRegisterProviderProvider(certificateProviderRegistry, watcherCaptor, "testca", 0); + createAndRegisterProviderProvider( + certificateProviderRegistry, watcherCaptor, "file_watcher", 1); + CertificateValidationContext staticCertValidationContext = + CertificateValidationContext.newBuilder() + .addAllMatchSubjectAltNames( + ImmutableSet.of( + StringMatcher.newBuilder().setExact("foo").build(), + StringMatcher.newBuilder().setExact("bar").build())) + .build(); + + DownstreamTlsContext downstreamTlsContext = + CommonTlsContextTestsUtil.buildNewDownstreamTlsContextForCertProviderInstance( + "gcp_id", + "cert-default", + "file_provider", + "root-default", + /* alpnProtocols= */ null, + staticCertValidationContext, + /* requireClientCert= */ true); + + Bootstrapper.BootstrapInfo bootstrapInfo = CommonBootstrapperTestUtils.getTestBootstrapInfo(); + serverSslContextProviderFactory = + new ServerSslContextProviderFactory( + bootstrapInfo, certProviderServerSslContextProviderFactory); + SslContextProvider sslContextProvider = + serverSslContextProviderFactory.create(downstreamTlsContext); + assertThat(sslContextProvider).isInstanceOf(CertProviderServerSslContextProvider.class); + verifyWatcher(sslContextProvider, watcherCaptor[0]); + verifyWatcher(sslContextProvider, watcherCaptor[1]); + } }