diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a42532185eb0..9effca68ae03 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -79,7 +79,7 @@ jobs: USE_BAZEL_VERSION: 5.0.0 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Bazel cache uses: actions/cache@v3 diff --git a/README.md b/README.md index fecc05643625..c1f0eae012e2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ gRPC-Java - An RPC library and framework Supported Platforms ------------------- -gRPC-Java supports Java 8 and later. Android minSdkVersion 19 (KitKat) and +gRPC-Java supports Java 8 and later. Android minSdkVersion 21 (Lollipop) and later are supported with [Java 8 language desugaring][android-java-8]. TLS usage on Android typically requires Play Services Dynamic Security Provider. @@ -44,8 +44,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.56.0/examples) and the -[Android example](https://github.com/grpc/grpc-java/tree/v1.56.0/examples/android) +The [examples](https://github.com/grpc/grpc-java/tree/v1.57.2/examples) and the +[Android example](https://github.com/grpc/grpc-java/tree/v1.57.2/examples/android) are standalone projects that showcase the usage of gRPC. Download @@ -56,18 +56,18 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`: io.grpc grpc-netty-shaded - 1.56.0 + 1.57.2 runtime io.grpc grpc-protobuf - 1.56.0 + 1.57.2 io.grpc grpc-stub - 1.56.0 + 1.57.2 org.apache.tomcat @@ -79,18 +79,18 @@ 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.56.0' -implementation 'io.grpc:grpc-protobuf:1.56.0' -implementation 'io.grpc:grpc-stub:1.56.0' +runtimeOnly 'io.grpc:grpc-netty-shaded:1.57.2' +implementation 'io.grpc:grpc-protobuf:1.57.2' +implementation 'io.grpc:grpc-stub:1.57.2' 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.56.0' -implementation 'io.grpc:grpc-protobuf-lite:1.56.0' -implementation 'io.grpc:grpc-stub:1.56.0' +implementation 'io.grpc:grpc-okhttp:1.57.2' +implementation 'io.grpc:grpc-protobuf-lite:1.57.2' +implementation 'io.grpc:grpc-stub:1.57.2' compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ ``` @@ -99,7 +99,7 @@ For [Bazel](https://bazel.build), you can either (with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below). [the JARs]: -https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.56.0 +https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.57.2 Development snapshots are available in [Sonatypes's snapshot repository](https://oss.sonatype.org/content/repositories/snapshots/). @@ -131,7 +131,7 @@ For protobuf-based codegen integrated with the Maven build system, you can use com.google.protobuf:protoc:3.22.3:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.56.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:1.57.2:exe:${os.detected.classifier} @@ -152,7 +152,7 @@ For non-Android protobuf-based codegen integrated with the Gradle build system, you can use [protobuf-gradle-plugin][]: ```gradle plugins { - id 'com.google.protobuf' version '0.9.1' + id 'com.google.protobuf' version '0.9.4' } protobuf { @@ -161,7 +161,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.56.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.57.2' } } generateProtoTasks { @@ -185,7 +185,7 @@ use protobuf-gradle-plugin but specify the 'lite' options: ```gradle plugins { - id 'com.google.protobuf' version '0.9.1' + id 'com.google.protobuf' version '0.9.4' } protobuf { @@ -194,7 +194,7 @@ protobuf { } plugins { grpc { - artifact = 'io.grpc:protoc-gen-grpc-java:1.56.0' + artifact = 'io.grpc:protoc-gen-grpc-java:1.57.2' } } generateProtoTasks { diff --git a/RELEASING.md b/RELEASING.md index 5a9b6d88f26a..203daec4aa60 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -66,7 +66,7 @@ would be used to create all `v1.7` tags (e.g. `v1.7.0`, `v1.7.1`). $(git log --pretty=format:%H --grep "^Start $MAJOR.$((MINOR+1)).0 development cycle$" upstream/master)^ $ git push upstream v$MAJOR.$MINOR.x ``` -5. Continue with Google-internal steps at go/grpc/java/releasing, but stop +5. Continue with Google-internal steps at go/grpc-java/releasing, but stop before `Auto releasing using kokoro`. 6. Create a milestone for the next release. 7. Move items out of the release milestone that didn't make the cut. Issues that @@ -91,7 +91,8 @@ Tagging the Release either be deferred or resolved and the fix backported. Verify there are no [TODO:release blocker][] nor [TODO:backport][] issues (open or closed), or that they are tracking an issue for a different branch. -2. Ensure that Google-internal steps completed at go/grpc/java/releasing#before-tagging-a-release. +2. Ensure that the Google-internal steps + at go/grpc-java/releasing#before-tagging-a-release are completed. 3. For vMajor.Minor.x branch, change `README.md` to refer to the next release version. _Also_ update the version numbers for protoc if the protobuf library version was updated since the last release. @@ -141,39 +142,70 @@ Tagging the Release ``` 7. Close the release milestone. -Build Artifacts ---------------- +8. Trigger build as described in "Auto releasing using kokoro" at + go/grpc-java/releasing. + + It runs three jobs on Kokoro, one on each platform. See their scripts: + `linux_artifacts.sh`, `windows.bat`, and `macos.sh`. The mvn-artifacts/ + outputs of each script is combined into a single folder and then processed + by `upload_artifacts.sh`, which signs the files and uploads to Sonatype. + +9. Once all of the artifacts have been pushed to the staging repository, the + repository should have been closed by `upload_artifacts.sh`. Closing triggers + several sanity checks on the repository. If this completes successfully, the + repository can then be `released`, which will begin the process of pushing + the new artifacts to Maven Central (the staging repository will be destroyed + in the process). You can see the complete process for releasing to Maven + Central on the [OSSRH site](https://central.sonatype.org/pages/releasing-the-deployment.html). + +10. We have containers for each release to detect compatibility regressions with + old releases. Generate one for the new release by following the [GCR image + generation instructions][gcr-image]. Summary: + ```bash + # If you haven't previously configured docker: + gcloud auth configure-docker + + # In main grpc repo, add the new version to matrix + ${EDITOR:-nano -w} tools/interop_matrix/client_matrix.py + tools/interop_matrix/create_matrix_images.py --git_checkout --release=v$MAJOR.$MINOR.$PATCH \ + --upload_images --language java + docker pull gcr.io/grpc-testing/grpc_interop_java:v$MAJOR.$MINOR.$PATCH + docker_image=gcr.io/grpc-testing/grpc_interop_java:v$MAJOR.$MINOR.$PATCH \ + tools/interop_matrix/testcases/java__master + + # Commit the changes + git commit --all -m "[interop] Add grpc-java $MAJOR.$MINOR.$PATCH to client_matrix.py" + + # Create a PR with the `release notes: no` label and run ad-hoc test against your PR + ``` +[gcr-image]: https://github.com/grpc/grpc/blob/master/tools/interop_matrix/README.md#step-by-step-instructions-for-adding-a-gcr-image-for-a-new-release-for-compatibility-test + +11. Update gh-pages with the new Javadoc. Generally the file is on repo1 + 15 minutes after publishing: + + ```bash + git checkout gh-pages + git pull --ff-only upstream gh-pages + rm -r javadoc/ + wget -O grpc-all-javadoc.jar "https://repo1.maven.org/maven2/io/grpc/grpc-all/$MAJOR.$MINOR.$PATCH/grpc-all-$MAJOR.$MINOR.$PATCH-javadoc.jar" + unzip -d javadoc grpc-all-javadoc.jar + patch -p1 < ga.patch + rm grpc-all-javadoc.jar + rm -r javadoc/META-INF/ + git add -A javadoc + git commit -m "Javadoc for $MAJOR.$MINOR.$PATCH" + ``` + + Push gh-pages to the main repository and verify the current version is + [live on grpc.io](https://grpc.io/grpc-java/javadoc/). + +12. Add [Release Notes](https://github.com/grpc/grpc-java/releases) for the new tag. + *Make sure that any backports are reflected in the release notes.* -Trigger build as described in "Auto releasing using kokoro" at -go/grpc/java/releasing. - -It runs three jobs on Kokoro, one on each platform. See their scripts: -`linux_artifacts.sh`, `windows.bat`, and `unix.sh` (called directly for OS X; -called within the Docker environment on Linux). The mvn-artifacts/ outputs of -each script is combined into a single folder and then processed by -`upload_artifacts.sh`, which signs the files and uploads to Sonatype. - -Releasing on Maven Central --------------------------- - -Once all of the artifacts have been pushed to the staging repository, the -repository should have been closed by `upload_artifacts.sh`. Closing triggers -several sanity checks on the repository. If this completes successfully, the -repository can then be `released`, which will begin the process of pushing the -new artifacts to Maven Central (the staging repository will be destroyed in the -process). You can see the complete process for releasing to Maven Central on the -[OSSRH site](https://central.sonatype.org/pages/releasing-the-deployment.html). - -Build interop container image ------------------------------ - -We have containers for each release to detect compatibility regressions with old -releases. Generate one for the new release by following the -[GCR image generation instructions](https://github.com/grpc/grpc/blob/master/tools/interop_matrix/README.md#step-by-step-instructions-for-adding-a-gcr-image-for-a-new-release-for-compatibility-test). Update README.md ---------------- -After waiting ~1 day and verifying that the release appears on [Maven +After waiting ~1 day and verifying that the release is indexed on [Maven Central](https://search.maven.org/search?q=g:io.grpc), cherry-pick the commit that updated the README into the master branch. @@ -183,14 +215,6 @@ $ git cherry-pick v$MAJOR.$MINOR.$PATCH^ $ git push --set-upstream origin bump-readme ``` -NOTE: If you add to your ~/.gitconfig the following, you don't need the -`--set-upstream` - -```text -[push] - autoSetupRemote = true -``` - Create a PR and go through the review process Update version referenced by tutorials @@ -202,35 +226,7 @@ of the grpc.io repository. Notify the Community -------------------- -Finally, document and publicize the release. - -1. Add [Release Notes](https://github.com/grpc/grpc-java/releases) for the new tag. - The description should include any major fixes or features since the last release. - You may choose to add links to bugs, PRs, or commits if appropriate. -2. Post a release announcement to [grpc-io](https://groups.google.com/forum/#!forum/grpc-io) - (`grpc-io@googlegroups.com`). The title should be something that clearly identifies - the release (e.g.`GRPC-Java Released`). - - Note that there may have been backports to the release branch since you - generated the release notes. Please verify that any backports are reflected - in the release notes before sending them out. - -Update Hosted Javadoc ---------------------- - -Now we need to update gh-pages with the new Javadoc: - -```bash -git checkout gh-pages -git pull --ff-only upstream gh-pages -rm -r javadoc/ -wget -O grpc-all-javadoc.jar "http://search.maven.org/remotecontent?filepath=io/grpc/grpc-all/$MAJOR.$MINOR.$PATCH/grpc-all-$MAJOR.$MINOR.$PATCH-javadoc.jar" -unzip -d javadoc grpc-all-javadoc.jar -patch -p1 < ga.patch -rm grpc-all-javadoc.jar -rm -r javadoc/META-INF/ -git add -A javadoc -git commit -m "Javadoc for $MAJOR.$MINOR.$PATCH" -``` - -Push gh-pages to the main repository and verify the current version is [live -on grpc.io](https://grpc.io/grpc-java/javadoc/). +Post a release announcement to [grpc-io](https://groups.google.com/forum/#!forum/grpc-io) +(`grpc-io@googlegroups.com`) with the title `gRPC-Java v$MAJOR.$MINOR.$PATCH +Released`. The email content should link to the GitHub release notes and include +a copy of them. diff --git a/SECURITY.md b/SECURITY.md index 5ced7c8b9a54..4fd052837fc9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -395,7 +395,8 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver 1.48.x-1.49.x | 4.1.77.Final | 2.0.53.Final 1.50.x-1.53.x | 4.1.79.Final | 2.0.54.Final 1.54.x-1.55.x | 4.1.87.Final | 2.0.56.Final -1.56.x- | 4.1.87.Final | 2.0.61.Final +1.56.x | 4.1.87.Final | 2.0.61.Final +1.57.x- | 4.1.93.Final | 2.0.61.Final _(grpc-netty-shaded avoids issues with keeping these versions in sync.)_ diff --git a/all/build.gradle b/all/build.gradle index 100e016aa3cc..dceee473316c 100644 --- a/all/build.gradle +++ b/all/build.gradle @@ -10,7 +10,6 @@ description = "gRPC: All" def subprojects = [ project(':grpc-api'), project(':grpc-auth'), - project(':grpc-context'), project(':grpc-core'), project(':grpc-grpclb'), project(':grpc-netty'), @@ -23,6 +22,7 @@ def subprojects = [ project(':grpc-servlet-jakarta'), project(':grpc-stub'), project(':grpc-testing'), + project(':grpc-util'), project(':grpc-xds'), ] @@ -53,11 +53,10 @@ tasks.named("javadoc").configure { } tasks.named("jacocoTestReport").configure { - dependsOn(subprojects.jacocoTestReport.dependsOn) - dependsOn(project(':grpc-interop-testing').jacocoTestReport.dependsOn) + mustRunAfter(subprojects.jacocoTestReport.mustRunAfter) + mustRunAfter(project(':grpc-interop-testing').jacocoTestReport.mustRunAfter) executionData.from files(subprojects.jacocoTestReport.executionData) .plus(project(':grpc-interop-testing').jacocoTestReport.executionData) - .filter { f -> f.exists() } reports { xml.required = true html.required = true diff --git a/alts/build.gradle b/alts/build.gradle index 4edd3c700e5e..9585eeef7645 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -65,6 +65,9 @@ tasks.named("javadoc").configure { tasks.named("jar").configure { // Must use a different archiveClassifier to avoid conflicting with shadowJar archiveClassifier = 'original' + manifest { + attributes('Automatic-Module-Name': 'io.grpc.alts') + } } // We want to use grpc-netty-shaded instead of grpc-netty. But we also want our diff --git a/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java b/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java index dbe0821abe94..3c59c5b6d091 100644 --- a/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java +++ b/alts/src/main/java/io/grpc/alts/FailingCallCredentials.java @@ -38,7 +38,4 @@ public void applyRequestMetadata( CallCredentials.MetadataApplier applier) { applier.fail(status); } - - @Override - public void thisUsesUnstableApi() {} } diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 3fd23343086f..440254f403ab 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -34,7 +34,10 @@ android { defaultConfig { applicationId "io.grpc.android.integrationtest" - minSdkVersion 19 + // Held back to 20 as Gradle fails to build at the 21 level. This is + // presumably a Gradle bug that can be revisited later. + // Maybe this issue: https://github.com/gradle/gradle/issues/20778 + minSdkVersion 20 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -88,8 +91,7 @@ dependencies { compileOnly libraries.javax.annotation - androidTestImplementation project(':grpc-netty'), - 'androidx.test.ext:junit:1.1.3', + androidTestImplementation 'androidx.test.ext:junit:1.1.3', 'androidx.test:runner:1.4.0' } @@ -112,7 +114,8 @@ import net.ltgt.gradle.errorprone.CheckSeverity tasks.withType(JavaCompile).configureEach { options.compilerArgs += [ - "-Xlint:-cast" + "-Xlint:-cast", + "-Xlint:-deprecation", // https://github.com/grpc/grpc-java/issues/10298 ] appendToProperty(it.options.errorprone.excludedPaths, ".*/R.java", "|") appendToProperty( @@ -121,4 +124,16 @@ tasks.withType(JavaCompile).configureEach { "|") } +afterEvaluate { + // Hack to workaround "Task ':grpc-android-interop-testing:extractIncludeDebugProto' uses this + // output of task ':grpc-context:jar' without declaring an explicit or implicit dependency." The + // issue started when grpc-context became empty. + tasks.named('extractIncludeDebugProto').configure { + dependsOn project(':grpc-context').tasks.named('jar') + } + tasks.named('extractIncludeReleaseProto').configure { + dependsOn project(':grpc-context').tasks.named('jar') + } +} + configureProtoCompilation() diff --git a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java index f002dd291c78..f5e54da5d4e9 100644 --- a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java +++ b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java @@ -22,9 +22,10 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; import io.grpc.Server; import io.grpc.android.UdsChannelBuilder; -import io.grpc.netty.NettyServerBuilder; import io.grpc.testing.integration.TestServiceImpl; import java.io.IOException; import java.util.concurrent.ExecutionException; @@ -68,7 +69,7 @@ public void setUp() throws IOException { // Start local server. server = - NettyServerBuilder.forPort(0) + Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create()) .maxInboundMessageSize(16 * 1024 * 1024) .addService(new TestServiceImpl(serverExecutor)) .build(); diff --git a/android-interop-testing/src/main/AndroidManifest.xml b/android-interop-testing/src/main/AndroidManifest.xml index 250deb087e88..35f3ee33a2b4 100644 --- a/android-interop-testing/src/main/AndroidManifest.xml +++ b/android-interop-testing/src/main/AndroidManifest.xml @@ -1,10 +1,12 @@ - + + Calling {@code cancel(null)} is the same as calling {@link #close}. * * @return {@code true} if this context cancelled the context and notified listeners, - * {@code false} if the context was already cancelled. + * {@code false} if the context was already cancelled. */ @CanIgnoreReturnValue public boolean cancel(Throwable cause) { @@ -999,16 +999,6 @@ public String toString() { * subject to change. */ public abstract static class Storage { - /** - * Unused. - * - * @deprecated This is an old API that is no longer used. - */ - @Deprecated - public void attach(Context toAttach) { - throw new UnsupportedOperationException("Deprecated. Do not call."); - } - /** * Implements {@link io.grpc.Context#attach}. * @@ -1022,13 +1012,7 @@ public void attach(Context toAttach) { * as the {@code toRestore} parameter. {@code null} is a valid return value, but see * caution note. */ - public Context doAttach(Context toAttach) { - // This is a default implementation to help migrate existing Storage implementations that - // have an attach() method but no doAttach() method. - Context current = current(); - attach(toAttach); - return current; - } + public abstract Context doAttach(Context toAttach); /** * Implements {@link io.grpc.Context#detach}. diff --git a/context/src/main/java/io/grpc/Deadline.java b/api/src/context/java/io/grpc/Deadline.java similarity index 100% rename from context/src/main/java/io/grpc/Deadline.java rename to api/src/context/java/io/grpc/Deadline.java diff --git a/context/src/main/java/io/grpc/PersistentHashArrayMappedTrie.java b/api/src/context/java/io/grpc/PersistentHashArrayMappedTrie.java similarity index 100% rename from context/src/main/java/io/grpc/PersistentHashArrayMappedTrie.java rename to api/src/context/java/io/grpc/PersistentHashArrayMappedTrie.java diff --git a/context/src/main/java/io/grpc/ThreadLocalContextStorage.java b/api/src/context/java/io/grpc/ThreadLocalContextStorage.java similarity index 100% rename from context/src/main/java/io/grpc/ThreadLocalContextStorage.java rename to api/src/context/java/io/grpc/ThreadLocalContextStorage.java diff --git a/context/src/jmh/java/io/grpc/AttachDetachBenchmark.java b/api/src/jmh/java/io/grpc/AttachDetachBenchmark.java similarity index 100% rename from context/src/jmh/java/io/grpc/AttachDetachBenchmark.java rename to api/src/jmh/java/io/grpc/AttachDetachBenchmark.java diff --git a/context/src/jmh/java/io/grpc/ReadBenchmark.java b/api/src/jmh/java/io/grpc/ReadBenchmark.java similarity index 100% rename from context/src/jmh/java/io/grpc/ReadBenchmark.java rename to api/src/jmh/java/io/grpc/ReadBenchmark.java diff --git a/context/src/jmh/java/io/grpc/WriteBenchmark.java b/api/src/jmh/java/io/grpc/WriteBenchmark.java similarity index 100% rename from context/src/jmh/java/io/grpc/WriteBenchmark.java rename to api/src/jmh/java/io/grpc/WriteBenchmark.java diff --git a/api/src/main/java/io/grpc/ClientCall.java b/api/src/main/java/io/grpc/ClientCall.java index 2a8716a9249c..df9e15001e16 100644 --- a/api/src/main/java/io/grpc/ClientCall.java +++ b/api/src/main/java/io/grpc/ClientCall.java @@ -270,7 +270,6 @@ public boolean isReady() { * encoding has been negotiated, this is a no-op. By default per-message compression is enabled, * but may not have any effect if compression is not enabled on the call. */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1703") public void setMessageCompression(boolean enabled) { // noop } diff --git a/api/src/main/java/io/grpc/ClientInterceptors.java b/api/src/main/java/io/grpc/ClientInterceptors.java index b6fe077f7449..81d0cf6da247 100644 --- a/api/src/main/java/io/grpc/ClientInterceptors.java +++ b/api/src/main/java/io/grpc/ClientInterceptors.java @@ -236,7 +236,11 @@ public final void start(Listener responseListener, Metadata headers) { // to a NO-OP one to prevent the IllegalStateException. The user will finally get notified // about the error through the listener. delegate = (ClientCall) NOOP_CALL; - responseListener.onClose(Status.fromThrowable(e), new Metadata()); + Metadata trailers = Status.trailersFromThrowable(e); + responseListener.onClose( + Status.fromThrowable(e), + trailers != null ? trailers : new Metadata() + ); } } } diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java index 5617d2798625..d7e3fbb917c8 100644 --- a/api/src/main/java/io/grpc/LoadBalancer.java +++ b/api/src/main/java/io/grpc/LoadBalancer.java @@ -115,6 +115,19 @@ public abstract class LoadBalancer { @NameResolver.ResolutionResultAttr public static final Attributes.Key> ATTR_HEALTH_CHECKING_CONFIG = Attributes.Key.create("internal:health-checking-config"); + + public static final SubchannelPicker EMPTY_PICKER = new SubchannelPicker() { + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withNoResult(); + } + + @Override + public String toString() { + return "EMPTY_PICKER"; + } + }; + private int recursionCount; /** @@ -1398,4 +1411,26 @@ public abstract static class Factory { */ public abstract LoadBalancer newLoadBalancer(Helper helper); } + + public static final class ErrorPicker extends SubchannelPicker { + + private final Status error; + + public ErrorPicker(Status error) { + this.error = checkNotNull(error, "error"); + } + + @Override + public PickResult pickSubchannel(PickSubchannelArgs args) { + return PickResult.withError(error); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("error", error) + .toString(); + } + } + } diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java index 15d2fbdd3175..ecae7b92078d 100644 --- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java +++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java @@ -32,6 +32,11 @@ public abstract class ManagedChannelBuilder> /** * Creates a channel with the target's address and port number. * + *

Note that there is an open JDK bug on {@link java.net.URI} class parsing an ipv6 scope ID: + * bugs.openjdk.org/browse/JDK-8199396. This method is exposed to this bug. If you experience an + * issue, a work-around is to convert the scope ID to its numeric form (e.g. by using + * Inet6Address.getScopeId()) before calling this method. + * * @see #forTarget(String) * @since 1.0.0 */ @@ -70,6 +75,11 @@ public static ManagedChannelBuilder forAddress(String name, int port) { *

  • {@code "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443"}
  • * * + *

    Note that there is an open JDK bug on {@link java.net.URI} class parsing an ipv6 scope ID: + * bugs.openjdk.org/browse/JDK-8199396. This method is exposed to this bug. If you experience an + * issue, a work-around is to convert the scope ID to its numeric form (e.g. by using + * Inet6Address.getScopeId()) before calling this method. + * * @since 1.0.0 */ public static ManagedChannelBuilder forTarget(String target) { @@ -352,6 +362,8 @@ public T maxInboundMetadataSize(int bytes) { * small of a value as necessary. * * @throws UnsupportedOperationException if unsupported + * @see gRFC A8 + * Client-side Keepalive * @since 1.7.0 */ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { @@ -366,6 +378,8 @@ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { *

    This value should be at least multiple times the RTT to allow for lost packets. * * @throws UnsupportedOperationException if unsupported + * @see gRFC A8 + * Client-side Keepalive * @since 1.7.0 */ public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) { @@ -383,6 +397,8 @@ public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) { * * @throws UnsupportedOperationException if unsupported * @see #keepAliveTime(long, TimeUnit) + * @see gRFC A8 + * Client-side Keepalive * @since 1.7.0 */ public T keepAliveWithoutCalls(boolean enable) { @@ -571,10 +587,10 @@ public T proxyDetector(ProxyDetector proxyDetector) { * return o; * }} * + * @return this * @throws IllegalArgumentException When the given serviceConfig is invalid or the current version * of grpc library can not parse it gracefully. The state of the builder is unchanged if * an exception is thrown. - * @return this * @since 1.20.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5189") diff --git a/api/src/main/java/io/grpc/MethodDescriptor.java b/api/src/main/java/io/grpc/MethodDescriptor.java index c85be6b64781..983ec4858c5b 100644 --- a/api/src/main/java/io/grpc/MethodDescriptor.java +++ b/api/src/main/java/io/grpc/MethodDescriptor.java @@ -324,7 +324,6 @@ public InputStream streamResponse(RespT response) { * * @since 1.1.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2592") public Marshaller getRequestMarshaller() { return requestMarshaller; } @@ -334,7 +333,6 @@ public Marshaller getRequestMarshaller() { * * @since 1.1.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2592") public Marshaller getResponseMarshaller() { return responseMarshaller; } diff --git a/api/src/main/java/io/grpc/PartialForwardingServerCall.java b/api/src/main/java/io/grpc/PartialForwardingServerCall.java index 7b760c468496..a7da647308b1 100644 --- a/api/src/main/java/io/grpc/PartialForwardingServerCall.java +++ b/api/src/main/java/io/grpc/PartialForwardingServerCall.java @@ -54,7 +54,6 @@ public boolean isCancelled() { } @Override - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1703") public void setMessageCompression(boolean enabled) { delegate().setMessageCompression(enabled); } diff --git a/api/src/main/java/io/grpc/ServerBuilder.java b/api/src/main/java/io/grpc/ServerBuilder.java index 2df644e61aee..c2ad566f90f3 100644 --- a/api/src/main/java/io/grpc/ServerBuilder.java +++ b/api/src/main/java/io/grpc/ServerBuilder.java @@ -230,14 +230,13 @@ public T useTransportSecurity(InputStream certChain, InputStream privateKey) { /** * Sets the permitted time for new connections to complete negotiation handshakes before being - * killed. + * killed. The default value is 2 minutes. * * @return this * @throws IllegalArgumentException if timeout is negative * @throws UnsupportedOperationException if unsupported * @since 1.8.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/3706") public T handshakeTimeout(long timeout, TimeUnit unit) { throw new UnsupportedOperationException(); } @@ -249,6 +248,8 @@ public T handshakeTimeout(long timeout, TimeUnit unit) { * * @throws IllegalArgumentException if time is not positive * @throws UnsupportedOperationException if unsupported + * @see gRFC A9 + * Server-side Connection Management * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -265,6 +266,8 @@ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { * * @throws IllegalArgumentException if timeout is not positive * @throws UnsupportedOperationException if unsupported + * @see gRFC A9 + * Server-side Connection Management * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -281,6 +284,8 @@ public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) { * * @throws IllegalArgumentException if idle is not positive * @throws UnsupportedOperationException if unsupported + * @see gRFC A9 + * Server-side Connection Management * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -296,6 +301,8 @@ public T maxConnectionIdle(long maxConnectionIdle, TimeUnit timeUnit) { * * @throws IllegalArgumentException if age is not positive * @throws UnsupportedOperationException if unsupported + * @see gRFC A9 + * Server-side Connection Management * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -312,6 +319,8 @@ public T maxConnectionAge(long maxConnectionAge, TimeUnit timeUnit) { * @throws IllegalArgumentException if grace is negative * @throws UnsupportedOperationException if unsupported * @see #maxConnectionAge(long, TimeUnit) + * @see gRFC A9 + * Server-side Connection Management * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -333,6 +342,8 @@ public T maxConnectionAgeGrace(long maxConnectionAgeGrace, TimeUnit timeUnit) { * @throws IllegalArgumentException if time is negative * @throws UnsupportedOperationException if unsupported * @see #permitKeepAliveWithoutCalls(boolean) + * @see gRFC A8 + * Client-side Keepalive * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") @@ -346,6 +357,8 @@ public T permitKeepAliveTime(long keepAliveTime, TimeUnit timeUnit) { * * @throws UnsupportedOperationException if unsupported * @see #permitKeepAliveTime(long, TimeUnit) + * @see gRFC A8 + * Client-side Keepalive * @since 1.47.0 */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/9009") diff --git a/api/src/main/java/io/grpc/ServerCall.java b/api/src/main/java/io/grpc/ServerCall.java index bf0e6da8aef3..7408479a2305 100644 --- a/api/src/main/java/io/grpc/ServerCall.java +++ b/api/src/main/java/io/grpc/ServerCall.java @@ -242,7 +242,6 @@ public Attributes getAttributes() { * * @return the authority string. {@code null} if not available. */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2924") @Nullable public String getAuthority() { return null; diff --git a/api/src/main/java/io/grpc/ServerInterceptors.java b/api/src/main/java/io/grpc/ServerInterceptors.java index b44a7b6c8866..0bc6d07c83c1 100644 --- a/api/src/main/java/io/grpc/ServerInterceptors.java +++ b/api/src/main/java/io/grpc/ServerInterceptors.java @@ -291,23 +291,23 @@ public ServerCall.Listener startCall( final Metadata headers) { final ServerCall unwrappedCall = new PartialForwardingServerCall() { - @Override - protected ServerCall delegate() { - return call; - } + @Override + protected ServerCall delegate() { + return call; + } - @Override - public void sendMessage(ORespT message) { - final InputStream is = originalMethod.streamResponse(message); - final WRespT wrappedMessage = wrappedMethod.parseResponse(is); - delegate().sendMessage(wrappedMessage); - } + @Override + public void sendMessage(ORespT message) { + final InputStream is = originalMethod.streamResponse(message); + final WRespT wrappedMessage = wrappedMethod.parseResponse(is); + delegate().sendMessage(wrappedMessage); + } - @Override - public MethodDescriptor getMethodDescriptor() { - return originalMethod; - } - }; + @Override + public MethodDescriptor getMethodDescriptor() { + return originalMethod; + } + }; final ServerCall.Listener originalListener = originalHandler .startCall(unwrappedCall, headers); diff --git a/api/src/main/java/io/grpc/ServerRegistry.java b/api/src/main/java/io/grpc/ServerRegistry.java index e6a067ce87f6..70fd36573075 100644 --- a/api/src/main/java/io/grpc/ServerRegistry.java +++ b/api/src/main/java/io/grpc/ServerRegistry.java @@ -23,6 +23,7 @@ import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; +import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; @@ -92,7 +93,7 @@ public static synchronized ServerRegistry getDefaultRegistry() { if (instance == null) { List providerList = ServiceProviders.loadAll( ServerProvider.class, - Collections.>emptyList(), + getHardCodedClasses(), ServerProvider.class.getClassLoader(), new ServerPriorityAccessor()); instance = new ServerRegistry(); @@ -119,6 +120,20 @@ ServerProvider provider() { return providers.isEmpty() ? null : providers.get(0); } + @VisibleForTesting + static List> getHardCodedClasses() { + // Class.forName(String) is used to remove the need for ProGuard configuration. Note that + // ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader): + // https://sourceforge.net/p/proguard/bugs/418/ + List> list = new ArrayList<>(); + try { + list.add(Class.forName("io.grpc.okhttp.OkHttpServerProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Unable to find OkHttpServerProvider", e); + } + return Collections.unmodifiableList(list); + } + ServerBuilder newServerBuilderForPort(int port, ServerCredentials creds) { List providers = providers(); if (providers.isEmpty()) { diff --git a/api/src/main/java/io/grpc/Status.java b/api/src/main/java/io/grpc/Status.java index 28388424e176..c5c7eed402ef 100644 --- a/api/src/main/java/io/grpc/Status.java +++ b/api/src/main/java/io/grpc/Status.java @@ -418,7 +418,6 @@ public static Status fromThrowable(Throwable t) { * @return the trailers or {@code null} if not found. */ @Nullable - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") public static Metadata trailersFromThrowable(Throwable t) { Throwable cause = checkNotNull(t, "t"); while (cause != null) { @@ -534,7 +533,6 @@ public StatusRuntimeException asRuntimeException() { * Same as {@link #asRuntimeException()} but includes the provided trailers in the returned * exception. */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4683") public StatusRuntimeException asRuntimeException(@Nullable Metadata trailers) { return new StatusRuntimeException(this, trailers); } diff --git a/context/src/test/java/io/grpc/ContextTest.java b/api/src/test/java/io/grpc/ContextTest.java similarity index 100% rename from context/src/test/java/io/grpc/ContextTest.java rename to api/src/test/java/io/grpc/ContextTest.java diff --git a/context/src/test/java/io/grpc/DeadlineTest.java b/api/src/test/java/io/grpc/DeadlineTest.java similarity index 100% rename from context/src/test/java/io/grpc/DeadlineTest.java rename to api/src/test/java/io/grpc/DeadlineTest.java diff --git a/api/src/test/java/io/grpc/ForwardingServerCallTest.java b/api/src/test/java/io/grpc/ForwardingServerCallTest.java index e6cb63fdd2b8..8030b65ecc7e 100644 --- a/api/src/test/java/io/grpc/ForwardingServerCallTest.java +++ b/api/src/test/java/io/grpc/ForwardingServerCallTest.java @@ -48,7 +48,7 @@ public void setUp() { protected ServerCall delegate() { return serverCall; } - }; + }; } @Test diff --git a/api/src/test/java/io/grpc/MetadataTest.java b/api/src/test/java/io/grpc/MetadataTest.java index 51aecc638e75..073a505c8240 100644 --- a/api/src/test/java/io/grpc/MetadataTest.java +++ b/api/src/test/java/io/grpc/MetadataTest.java @@ -54,16 +54,16 @@ public class MetadataTest { private static final Metadata.BinaryMarshaller FISH_MARSHALLER = new Metadata.BinaryMarshaller() { - @Override - public byte[] toBytes(Fish fish) { - return fish.name.getBytes(UTF_8); - } + @Override + public byte[] toBytes(Fish fish) { + return fish.name.getBytes(UTF_8); + } - @Override - public Fish parseBytes(byte[] serialized) { - return new Fish(new String(serialized, UTF_8)); - } - }; + @Override + public Fish parseBytes(byte[] serialized) { + return new Fish(new String(serialized, UTF_8)); + } + }; private static class FishStreamMarsaller implements Metadata.BinaryStreamMarshaller { @Override @@ -100,16 +100,16 @@ public int read() throws IOException { private static final Metadata.BinaryStreamMarshaller IMMUTABLE_FISH_MARSHALLER = new Metadata.BinaryStreamMarshaller() { - @Override - public InputStream toStream(Fish fish) { - return new FakeFishStream(fish); - } + @Override + public InputStream toStream(Fish fish) { + return new FakeFishStream(fish); + } - @Override - public Fish parseStream(InputStream stream) { - return ((FakeFishStream) stream).fish; - } - }; + @Override + public Fish parseStream(InputStream stream) { + return ((FakeFishStream) stream).fish; + } + }; private static final String LANCE = "lance"; private static final byte[] LANCE_BYTES = LANCE.getBytes(US_ASCII); @@ -313,6 +313,7 @@ public void verifyToString_usingBinary() { } @Test + @SuppressWarnings("StringCaseLocaleUsage") // System locale is exactly what we're testing. public void testKeyCaseHandling() { Locale originalLocale = Locale.getDefault(); Locale.setDefault(new Locale("tr", "TR")); diff --git a/context/src/test/java/io/grpc/PersistentHashArrayMappedTrieTest.java b/api/src/test/java/io/grpc/PersistentHashArrayMappedTrieTest.java similarity index 100% rename from context/src/test/java/io/grpc/PersistentHashArrayMappedTrieTest.java rename to api/src/test/java/io/grpc/PersistentHashArrayMappedTrieTest.java diff --git a/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java b/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java index 2c3bf7e9c818..ab20c1112540 100644 --- a/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java +++ b/api/src/test/java/io/grpc/StatusRuntimeExceptionTest.java @@ -54,15 +54,13 @@ public void extendPreservesStack() { @Test public void extendAndOverridePreservesStack() { final StackTraceElement element = new StackTraceElement("a", "b", "c", 4); - StatusRuntimeException exception = - new StatusRuntimeException(Status.CANCELLED, new Metadata()) { - + StatusRuntimeException error = new StatusRuntimeException(Status.CANCELLED, new Metadata()) { @Override public synchronized Throwable fillInStackTrace() { setStackTrace(new StackTraceElement[]{element}); return this; } }; - assertThat(exception.getStackTrace()).asList().containsExactly(element); + assertThat(error.getStackTrace()).asList().containsExactly(element); } } diff --git a/context/src/test/java/io/grpc/ThreadLocalContextStorageTest.java b/api/src/test/java/io/grpc/ThreadLocalContextStorageTest.java similarity index 100% rename from context/src/test/java/io/grpc/ThreadLocalContextStorageTest.java rename to api/src/test/java/io/grpc/ThreadLocalContextStorageTest.java diff --git a/api/src/testFixtures/java/io/grpc/ServerRegistryAccessor.java b/api/src/testFixtures/java/io/grpc/ServerRegistryAccessor.java new file mode 100644 index 000000000000..b15d4dfde192 --- /dev/null +++ b/api/src/testFixtures/java/io/grpc/ServerRegistryAccessor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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; + +/** Accesses test-only methods of {@link ServerRegistry}. */ +public final class ServerRegistryAccessor { + private ServerRegistryAccessor() {} + + public static Iterable> getHardCodedClasses() { + return ServerRegistry.getHardCodedClasses(); + } +} diff --git a/context/src/testFixtures/java/io/grpc/StaticTestingClassLoader.java b/api/src/testFixtures/java/io/grpc/StaticTestingClassLoader.java similarity index 100% rename from context/src/testFixtures/java/io/grpc/StaticTestingClassLoader.java rename to api/src/testFixtures/java/io/grpc/StaticTestingClassLoader.java diff --git a/context/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java b/api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java similarity index 100% rename from context/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java rename to api/src/testFixtures/java/io/grpc/testing/DeadlineSubject.java diff --git a/auth/build.gradle b/auth/build.gradle index 73c108d5203e..3e9646533eeb 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -7,6 +7,13 @@ plugins { } description = "gRPC: Auth" + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.auth') + } +} + dependencies { api project(':grpc-api'), libraries.google.auth.credentials diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 90251e6692ff..49871e28aa7b 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -18,6 +18,12 @@ configurations { alpnagent } +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.benchmarks') + } +} + dependencies { implementation project(':grpc-core'), project(':grpc-netty'), @@ -99,12 +105,14 @@ def benchmark_worker = tasks.register("benchmark_worker", CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into("bin") { - from(qps_client) - from(openloop_client) - from(qps_server) - from(benchmark_worker) - fileMode = 0755 +application { + applicationDistribution.into("bin") { + from(qps_client) + from(openloop_client) + from(qps_server) + from(benchmark_worker) + fileMode = 0755 + } } publishing { diff --git a/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java b/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java index 3edc92ac02aa..d68e66561a84 100644 --- a/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java +++ b/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java @@ -78,6 +78,7 @@ public enum MessageSize { SMALL(10), MEDIUM(1024), LARGE(65536), JUMBO(1048576); private final int bytes; + MessageSize(int bytes) { this.bytes = bytes; } @@ -94,6 +95,7 @@ public enum FlowWindowSize { SMALL(16383), MEDIUM(65535), LARGE(1048575), JUMBO(8388607); private final int bytes; + FlowWindowSize(int bytes) { this.bytes = bytes; } diff --git a/binder/build.gradle b/binder/build.gradle index 0dd50827699f..0d1c5a033950 100644 --- a/binder/build.gradle +++ b/binder/build.gradle @@ -13,7 +13,7 @@ android { targetCompatibility 1.8 } defaultConfig { - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -75,6 +75,7 @@ tasks.withType(JavaCompile).configureEach { options.compilerArgs += [ "-Xlint:-cast" ] + options.compilerArgs -= ["-Werror"] // https://github.com/grpc/grpc-java/issues/10297 appendToProperty(it.options.errorprone.excludedPaths, ".*/R.java", "|") } diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index dbacf351780c..44454fbf7f17 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -35,6 +35,7 @@ import io.grpc.Status.Code; import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.BindServiceFlags; +import io.grpc.binder.BinderChannelCredentials; import io.grpc.binder.BinderServerBuilder; import io.grpc.binder.HostServices; import io.grpc.binder.InboundParcelablePolicy; @@ -146,7 +147,9 @@ public BinderClientTransportBuilder setSecurityPolicy(SecurityPolicy securityPol public BinderTransport.BinderClientTransport build() { return new BinderTransport.BinderClientTransport( appContext, + BinderChannelCredentials.forDefault(), serverAddress, + null, BindServiceFlags.DEFAULTS, ContextCompat.getMainExecutor(appContext), executorServicePool, diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java index 5a1b302f768e..5140057d72e8 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java @@ -24,6 +24,7 @@ import io.grpc.ServerStreamTracer; import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.BindServiceFlags; +import io.grpc.binder.BinderChannelCredentials; import io.grpc.binder.HostServices; import io.grpc.binder.InboundParcelablePolicy; import io.grpc.binder.SecurityPolicies; @@ -95,7 +96,9 @@ protected ManagedClientTransport newClientTransport(InternalServer server) { AndroidComponentAddress addr = (AndroidComponentAddress) server.getListenSocketAddress(); return new BinderTransport.BinderClientTransport( appContext, + BinderChannelCredentials.forDefault(), addr, + null, BindServiceFlags.DEFAULTS, ContextCompat.getMainExecutor(appContext), executorServicePool, diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 214eb6dc4c5e..e714761312ab 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import android.content.Context; +import android.os.UserHandle; import androidx.core.content.ContextCompat; import com.google.errorprone.annotations.DoNotCall; import io.grpc.ChannelCredentials; @@ -71,7 +72,37 @@ public final class BinderChannelBuilder public static BinderChannelBuilder forAddress( AndroidComponentAddress directAddress, Context sourceContext) { return new BinderChannelBuilder( - checkNotNull(directAddress, "directAddress"), null, sourceContext); + checkNotNull(directAddress, "directAddress"), + null, + sourceContext, + BinderChannelCredentials.forDefault()); + } + + /** + * Creates a channel builder that will bind to a remote Android service with provided + * BinderChannelCredentials. + * + *

    The underlying Android binding will be torn down when the channel becomes idle. This happens + * after 30 minutes without use by default but can be configured via {@link + * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link + * ManagedChannel#enterIdle()}. + * + *

    You the caller are responsible for managing the lifecycle of any channels built by the + * resulting builder. They will not be shut down automatically. + * + * @param directAddress the {@link AndroidComponentAddress} referencing the service to bind to. + * @param sourceContext the context to bind from (e.g. The current Activity or Application). + * @param channelCredentials the arbitrary binder specific channel credentials to be used to + * establish a binder connection. + * @return a new builder + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") + public static BinderChannelBuilder forAddress( + AndroidComponentAddress directAddress, + Context sourceContext, + BinderChannelCredentials channelCredentials) { + return new BinderChannelBuilder( + checkNotNull(directAddress, "directAddress"), null, sourceContext, channelCredentials); } /** @@ -92,7 +123,37 @@ public static BinderChannelBuilder forAddress( * @return a new builder */ public static BinderChannelBuilder forTarget(String target, Context sourceContext) { - return new BinderChannelBuilder(null, checkNotNull(target, "target"), sourceContext); + return new BinderChannelBuilder( + null, + checkNotNull(target, "target"), + sourceContext, + BinderChannelCredentials.forDefault()); + } + + /** + * Creates a channel builder that will bind to a remote Android service, via a string target name + * which will be resolved. + * + *

    The underlying Android binding will be torn down when the channel becomes idle. This happens + * after 30 minutes without use by default but can be configured via {@link + * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link + * ManagedChannel#enterIdle()}. + * + *

    You the caller are responsible for managing the lifecycle of any channels built by the + * resulting builder. They will not be shut down automatically. + * + * @param target A target uri which should resolve into an {@link AndroidComponentAddress} + * referencing the service to bind to. + * @param sourceContext the context to bind from (e.g. The current Activity or Application). + * @param channelCredentials the arbitrary binder specific channel credentials to be used to + * establish a binder connection. + * @return a new builder + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") + public static BinderChannelBuilder forTarget( + String target, Context sourceContext, BinderChannelCredentials channelCredentials) { + return new BinderChannelBuilder( + null, checkNotNull(target, "target"), sourceContext, channelCredentials); } /** @@ -121,12 +182,14 @@ public static BinderChannelBuilder forTarget(String target) { private SecurityPolicy securityPolicy; private InboundParcelablePolicy inboundParcelablePolicy; private BindServiceFlags bindServiceFlags; + @Nullable private UserHandle targetUserHandle; private boolean strictLifecycleManagement; private BinderChannelBuilder( @Nullable AndroidComponentAddress directAddress, @Nullable String target, - Context sourceContext) { + Context sourceContext, + BinderChannelCredentials channelCredentials) { mainThreadExecutor = ContextCompat.getMainExecutor(checkNotNull(sourceContext, "sourceContext")); securityPolicy = SecurityPolicies.internalOnly(); @@ -139,10 +202,12 @@ final class BinderChannelTransportFactoryBuilder public ClientTransportFactory buildClientTransportFactory() { return new TransportFactory( sourceContext, + channelCredentials, mainThreadExecutor, schedulerPool, managedChannelImplBuilder.getOffloadExecutorPool(), securityPolicy, + targetUserHandle, bindServiceFlags, inboundParcelablePolicy); } @@ -216,6 +281,23 @@ public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) { return this; } +/** + * Provides the target {@UserHandle} of the remote Android service. + * + *

    When targetUserHandle is set, Context.bindServiceAsUser will used and additional Android + * permissions will be required. If your usage does not require cross-user communications, please + * do not set this field. It is the caller's responsibility to make sure that it holds the + * corresponding permissions. + * + * @param targetUserHandle the target user to bind into. + * @return this + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") + public BinderChannelBuilder bindAsUser(UserHandle targetUserHandle) { + this.targetUserHandle = targetUserHandle; + return this; + } + /** Sets the policy for inbound parcelable objects. */ public BinderChannelBuilder inboundParcelablePolicy( InboundParcelablePolicy inboundParcelablePolicy) { @@ -245,12 +327,14 @@ public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { /** Creates new binder transports. */ private static final class TransportFactory implements ClientTransportFactory { private final Context sourceContext; + private final BinderChannelCredentials channelCredentials; private final Executor mainThreadExecutor; private final ObjectPool scheduledExecutorPool; private final ObjectPool offloadExecutorPool; private final SecurityPolicy securityPolicy; - private final InboundParcelablePolicy inboundParcelablePolicy; + @Nullable private final UserHandle targetUserHandle; private final BindServiceFlags bindServiceFlags; + private final InboundParcelablePolicy inboundParcelablePolicy; private ScheduledExecutorService executorService; private Executor offloadExecutor; @@ -258,17 +342,21 @@ private static final class TransportFactory implements ClientTransportFactory { TransportFactory( Context sourceContext, + BinderChannelCredentials channelCredentials, Executor mainThreadExecutor, ObjectPool scheduledExecutorPool, ObjectPool offloadExecutorPool, SecurityPolicy securityPolicy, + @Nullable UserHandle targetUserHandle, BindServiceFlags bindServiceFlags, InboundParcelablePolicy inboundParcelablePolicy) { this.sourceContext = sourceContext; + this.channelCredentials = channelCredentials; this.mainThreadExecutor = mainThreadExecutor; this.scheduledExecutorPool = scheduledExecutorPool; this.offloadExecutorPool = offloadExecutorPool; this.securityPolicy = securityPolicy; + this.targetUserHandle = targetUserHandle; this.bindServiceFlags = bindServiceFlags; this.inboundParcelablePolicy = inboundParcelablePolicy; @@ -284,7 +372,9 @@ public ConnectionClientTransport newClientTransport( } return new BinderTransport.BinderClientTransport( sourceContext, + channelCredentials, (AndroidComponentAddress) addr, + targetUserHandle, bindServiceFlags, mainThreadExecutor, scheduledExecutorPool, diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelCredentials.java b/binder/src/main/java/io/grpc/binder/BinderChannelCredentials.java new file mode 100644 index 000000000000..1fa2136e4a56 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/BinderChannelCredentials.java @@ -0,0 +1,70 @@ +/* + * 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.binder; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.ComponentName; +import io.grpc.ChannelCredentials; +import io.grpc.ExperimentalApi; +import javax.annotation.Nullable; + +/** Additional arbitrary arguments to establish a Android binder connection channel. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173") +public final class BinderChannelCredentials extends ChannelCredentials { + + /** + * Creates the default BinderChannelCredentials. + * + * @return a BinderChannelCredentials + */ + public static BinderChannelCredentials forDefault() { + return new BinderChannelCredentials(null); + } + + /** + * Creates a BinderChannelCredentials to be used with DevicePolicyManager API. + * + * @param devicePolicyAdminComponentName the admin component to be specified with + * DevicePolicyManager.bindDeviceAdminServiceAsUser API. + * @return a BinderChannelCredentials + */ + public static BinderChannelCredentials forDevicePolicyAdmin( + ComponentName devicePolicyAdminComponentName) { + return new BinderChannelCredentials(devicePolicyAdminComponentName); + } + + @Nullable private final ComponentName devicePolicyAdminComponentName; + + private BinderChannelCredentials(@Nullable ComponentName devicePolicyAdminComponentName) { + this.devicePolicyAdminComponentName = devicePolicyAdminComponentName; + } + + @Override + public ChannelCredentials withoutBearerTokens() { + return this; + } + + /** + * Returns the admin component to be specified with DevicePolicyManager + * bindDeviceAdminServiceAsUser API. + */ + @Nullable + public ComponentName getDevicePolicyAdminComponentName() { + return devicePolicyAdminComponentName; + } +} diff --git a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java index 1a31ef823d3f..1357fb217ae8 100644 --- a/binder/src/main/java/io/grpc/binder/SecurityPolicies.java +++ b/binder/src/main/java/io/grpc/binder/SecurityPolicies.java @@ -26,6 +26,7 @@ import android.os.Build; import android.os.Build.VERSION; import android.os.Process; +import androidx.annotation.RequiresApi; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; @@ -188,12 +189,12 @@ public Status checkAuthorization(int uid) { * Creates {@link SecurityPolicy} which checks if the app is a device owner app. See * {@link DevicePolicyManager}. */ - public static SecurityPolicy isDeviceOwner(Context applicationContext) { + @androidx.annotation.RequiresApi(18) + public static io.grpc.binder.SecurityPolicy isDeviceOwner(Context applicationContext) { DevicePolicyManager devicePolicyManager = (DevicePolicyManager) applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE); return anyPackageWithUidSatisfies( - applicationContext, - pkg -> VERSION.SDK_INT >= 18 && devicePolicyManager.isDeviceOwnerApp(pkg), + applicationContext, pkg -> devicePolicyManager.isDeviceOwnerApp(pkg), "Rejected by device owner policy. No packages found for UID.", "Rejected by device owner policy"); } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 70b89165174f..cb2fe5be3e5d 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -28,6 +28,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.TransactionTooLargeException; +import android.os.UserHandle; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; import com.google.common.util.concurrent.ListenableFuture; @@ -46,6 +47,7 @@ import io.grpc.StatusException; import io.grpc.binder.AndroidComponentAddress; import io.grpc.binder.BindServiceFlags; +import io.grpc.binder.BinderChannelCredentials; import io.grpc.binder.InboundParcelablePolicy; import io.grpc.binder.SecurityPolicy; import io.grpc.internal.ClientStream; @@ -568,7 +570,9 @@ public static final class BinderClientTransport extends BinderTransport public BinderClientTransport( Context sourceContext, + BinderChannelCredentials channelCredentials, AndroidComponentAddress targetAddress, + @Nullable UserHandle targetUserHandle, BindServiceFlags bindServiceFlags, Executor mainThreadExecutor, ObjectPool executorServicePool, @@ -590,7 +594,9 @@ public BinderClientTransport( new ServiceBinding( mainThreadExecutor, sourceContext, + channelCredentials, targetAddress.asBindIntent(), + targetUserHandle, bindServiceFlags.toInteger(), this); } 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 650ead9bcdbf..32d0e7a4add6 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -16,15 +16,20 @@ package io.grpc.binder.internal; +import static com.google.common.base.Preconditions.checkState; + +import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import android.os.UserHandle; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import io.grpc.Status; +import io.grpc.binder.BinderChannelCredentials; import java.util.concurrent.Executor; import java.util.logging.Level; import java.util.logging.Logger; @@ -56,7 +61,26 @@ private enum State { UNBOUND, } + // Type of the method used when binding the service. + private enum BindMethodType { + BIND_SERVICE("bindService"), + BIND_SERVICE_AS_USER("bindServiceAsUser"), + DEVICE_POLICY_BIND_SEVICE_ADMIN("DevicePolicyManager.bindDeviceAdminServiceAsUser"); + + private final String methodName; + + BindMethodType(String methodName) { + this.methodName = methodName; + } + + public String methodName() { + return methodName; + } + } + + private final BinderChannelCredentials channelCredentials; private final Intent bindIntent; + @Nullable private final UserHandle targetUserHandle; private final int bindFlags; private final Observer observer; private final Executor mainThreadExecutor; @@ -76,7 +100,9 @@ private enum State { ServiceBinding( Executor mainThreadExecutor, Context sourceContext, + BinderChannelCredentials channelCredentials, Intent bindIntent, + @Nullable UserHandle targetUserHandle, int bindFlags, Observer observer) { // We need to synchronize here ensure other threads see all @@ -87,6 +113,8 @@ private enum State { this.observer = observer; this.sourceContext = sourceContext; this.mainThreadExecutor = mainThreadExecutor; + this.channelCredentials = channelCredentials; + this.targetUserHandle = targetUserHandle; state = State.NOT_BINDING; reportedState = State.NOT_BINDING; } @@ -117,7 +145,14 @@ private void notifyUnbound(Status reason) { public synchronized void bind() { if (state == State.NOT_BINDING) { state = State.BINDING; - Status bindResult = bindInternal(sourceContext, bindIntent, this, bindFlags); + Status bindResult = + bindInternal( + sourceContext, + bindIntent, + this, + bindFlags, + channelCredentials, + targetUserHandle); if (!bindResult.isOk()) { handleBindServiceFailure(sourceContext, this); state = State.UNBOUND; @@ -127,19 +162,57 @@ public synchronized void bind() { } private static Status bindInternal( - Context context, Intent bindIntent, ServiceConnection conn, int flags) { + Context context, + Intent bindIntent, + ServiceConnection conn, + int flags, + BinderChannelCredentials channelCredentials, + @Nullable UserHandle targetUserHandle) { + BindMethodType bindMethodType = BindMethodType.BIND_SERVICE; try { - if (!context.bindService(bindIntent, conn, flags)) { + if (targetUserHandle == null) { + checkState( + channelCredentials.getDevicePolicyAdminComponentName() == null, + "BindingChannelCredentials is expected to have null devicePolicyAdmin when" + + " targetUserHandle is not set"); + } else { + if (channelCredentials.getDevicePolicyAdminComponentName() != null) { + bindMethodType = BindMethodType.DEVICE_POLICY_BIND_SEVICE_ADMIN; + } else { + bindMethodType = BindMethodType.BIND_SERVICE_AS_USER; + } + } + boolean bindResult = false; + switch (bindMethodType) { + case BIND_SERVICE: + bindResult = context.bindService(bindIntent, conn, flags); + break; + case BIND_SERVICE_AS_USER: + bindResult = context.bindServiceAsUser(bindIntent, conn, flags, targetUserHandle); + break; + case DEVICE_POLICY_BIND_SEVICE_ADMIN: + DevicePolicyManager devicePolicyManager = + (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); + bindResult = devicePolicyManager.bindDeviceAdminServiceAsUser( + channelCredentials.getDevicePolicyAdminComponentName(), + bindIntent, + conn, + flags, + targetUserHandle); + break; + } + if (!bindResult) { return Status.UNIMPLEMENTED.withDescription( - "bindService(" + bindIntent + ") returned false"); + bindMethodType.methodName() + "(" + bindIntent + ") returned false"); } return Status.OK; } catch (SecurityException e) { - return Status.PERMISSION_DENIED.withCause(e).withDescription( - "SecurityException from bindService"); + return Status.PERMISSION_DENIED + .withCause(e) + .withDescription("SecurityException from " + bindMethodType.methodName()); } catch (RuntimeException e) { return Status.INTERNAL.withCause(e).withDescription( - "RuntimeException from bindService"); + "RuntimeException from " + bindMethodType.methodName()); } } diff --git a/binder/src/test/java/io/grpc/binder/BinderChannelCredentialsTest.java b/binder/src/test/java/io/grpc/binder/BinderChannelCredentialsTest.java new file mode 100644 index 000000000000..d31065dfe52b --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/BinderChannelCredentialsTest.java @@ -0,0 +1,32 @@ +package io.grpc.binder; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class BinderChannelCredentialsTest { + private final Context appContext = ApplicationProvider.getApplicationContext(); + + @Test + public void defaultBinderChannelCredentials() { + BinderChannelCredentials channelCredentials = BinderChannelCredentials.forDefault(); + assertThat(channelCredentials.getDevicePolicyAdminComponentName()).isNull(); + } + + @Test + public void binderChannelCredentialsForDevicePolicyAdmin() { + String deviceAdminClassName = "DevicePolicyAdmin"; + BinderChannelCredentials channelCredentials = + BinderChannelCredentials.forDevicePolicyAdmin( + new ComponentName(appContext, deviceAdminClassName)); + assertThat(channelCredentials.getDevicePolicyAdminComponentName()).isNotNull(); + assertThat(channelCredentials.getDevicePolicyAdminComponentName().getClassName()) + .isEqualTo(deviceAdminClassName); + } +} diff --git a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java index 6a6920905495..eda1b286a722 100644 --- a/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java +++ b/binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java @@ -30,9 +30,6 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.os.Build; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.os.Process; import androidx.test.core.app.ApplicationProvider; import com.google.common.collect.ImmutableList; @@ -333,7 +330,7 @@ public void testHasPermissions_failsIfPackageDoesNotHavePermissions() throws Exc } @Test - @Config(sdk = 18) + @Config(sdk = 19) public void testIsDeviceOwner_succeedsForDeviceOwner() throws Exception { PackageInfo info = newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); @@ -348,7 +345,7 @@ public void testIsDeviceOwner_succeedsForDeviceOwner() throws Exception { } @Test - @Config(sdk = 18) + @Config(sdk = 19) public void testIsDeviceOwner_failsForNotDeviceOwner() throws Exception { PackageInfo info = newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); @@ -361,25 +358,13 @@ public void testIsDeviceOwner_failsForNotDeviceOwner() throws Exception { } @Test - @Config(sdk = 18) + @Config(sdk = 19) public void testIsDeviceOwner_failsWhenNoPackagesForUid() throws Exception { policy = SecurityPolicies.isDeviceOwner(appContext); assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.UNAUTHENTICATED.getCode()); } - @Test - @Config(sdk = 17) - public void testIsDeviceOwner_failsForSdkLevelTooLow() throws Exception { - PackageInfo info = - newBuilder().setPackageName(OTHER_UID_PACKAGE_NAME).setSignatures(SIG2).build(); - - installPackages(OTHER_UID, info); - - policy = SecurityPolicies.isDeviceOwner(appContext); - - assertThat(policy.checkAuthorization(OTHER_UID).getCode()).isEqualTo(Status.PERMISSION_DENIED.getCode()); - } @Test @Config(sdk = 21) 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 967e91b820d6..3ec65624fb8b 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -24,15 +24,21 @@ import static org.robolectric.annotation.LooperMode.Mode.PAUSED; import android.app.Application; +import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.os.Parcel; +import android.os.UserHandle; import androidx.core.content.ContextCompat; import androidx.test.core.app.ApplicationProvider; import io.grpc.Status; import io.grpc.Status.Code; +import io.grpc.binder.BinderChannelCredentials; import io.grpc.binder.internal.Bindable.Observer; +import java.util.Arrays; +import javax.annotation.Nullable; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -44,6 +50,7 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.LooperMode; import org.robolectric.shadows.ShadowApplication; +import org.robolectric.shadows.ShadowDevicePolicyManager; @LooperMode(PAUSED) @RunWith(RobolectricTestRunner.class) @@ -256,6 +263,48 @@ public void testCallsAfterUnbindDontCrash() throws Exception { shadowOf(getMainLooper()).idle(); } + @Test + @Config(sdk = 30) + public void testBindWithTargetUserHandle() throws Exception { + binding = + newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build(); + shadowOf(getMainLooper()).idle(); + + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); + assertThat(observer.gotBoundEvent).isTrue(); + assertThat(observer.binder).isSameInstanceAs(mockBinder); + assertThat(observer.gotUnboundEvent).isFalse(); + assertThat(binding.isSourceContextCleared()).isFalse(); + } + + @Test + @Config(sdk = 30) + public void testBindWithDeviceAdmin() throws Exception { + String deviceAdminClassName = "DevicePolicyAdmin"; + ComponentName adminComponent = new ComponentName(appContext, deviceAdminClassName); + allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0); + binding = + newBuilder() + .setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0)) + .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) + .setChannelCredentials( + BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) + .build(); + shadowOf(getMainLooper()).idle(); + + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); + assertThat(observer.gotBoundEvent).isTrue(); + assertThat(observer.binder).isSameInstanceAs(mockBinder); + assertThat(observer.gotUnboundEvent).isFalse(); + assertThat(binding.isSourceContextCleared()).isFalse(); + } + private void assertNoLockHeld() { try { binding.wait(1); @@ -268,6 +317,28 @@ private void assertNoLockHeld() { } } + private static void allowBindDeviceAdminForUser(Context context, ComponentName admin, int userId) { + ShadowDevicePolicyManager devicePolicyManager = + shadowOf(context.getSystemService(DevicePolicyManager.class)); + devicePolicyManager.setDeviceOwner(admin); + devicePolicyManager.setBindDeviceAdminTargetUsers( + Arrays.asList(UserHandle.getUserHandleForUid(userId))); + shadowOf((DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE)); + devicePolicyManager.setDeviceOwner(admin); + devicePolicyManager.setBindDeviceAdminTargetUsers( + Arrays.asList(generateUserHandle(userId))); + } + + /** Generate UserHandles the hard way. */ + private static UserHandle generateUserHandle(int userId) { + Parcel userParcel = Parcel.obtain(); + userParcel.writeInt(userId); + userParcel.setDataPosition(0); + UserHandle userHandle = new UserHandle(userParcel); + userParcel.recycle(); + return userHandle; + } + private class TestObserver implements Bindable.Observer { public boolean gotBoundEvent; @@ -298,9 +369,11 @@ private static class ServiceBindingBuilder { private Observer observer; private Intent bindIntent = new Intent(); private int bindServiceFlags; + @Nullable private UserHandle targetUserHandle = null; + private BinderChannelCredentials channelCredentials = BinderChannelCredentials.forDefault(); public ServiceBindingBuilder setSourceContext(Context sourceContext) { - this.sourceContext = sourceContext; + this.sourceContext = sourceContext; return this; } @@ -324,11 +397,24 @@ public ServiceBindingBuilder setObserver(Observer observer) { return this; } + public ServiceBindingBuilder setTargetUserHandle(UserHandle targetUserHandle) { + this.targetUserHandle = targetUserHandle; + return this; + } + + public ServiceBindingBuilder setChannelCredentials( + BinderChannelCredentials channelCredentials) { + this.channelCredentials = channelCredentials; + return this; + } + public ServiceBinding build() { return new ServiceBinding( ContextCompat.getMainExecutor(sourceContext), sourceContext, + channelCredentials, bindIntent, + targetUserHandle, bindServiceFlags, observer); } diff --git a/build.gradle b/build.gradle index 42e008e80d66..779f10309697 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ subprojects { apply plugin: "net.ltgt.errorprone" group = "io.grpc" - version = "1.57.0-SNAPSHOT" // CURRENT_GRPC_VERSION + version = "1.59.0-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() @@ -37,6 +37,9 @@ subprojects { "-Xlint:-try" ] it.options.encoding = "UTF-8" + // Avoid Gradle OOM. + // https://docs.gradle.org/current/userguide/performance.html#run_the_compiler_as_a_separate_process + it.options.fork = true if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) { it.options.compilerArgs += ["-Werror"] } @@ -68,11 +71,6 @@ subprojects { } generateProtoTasks { all().each { task -> - // Recompile protos when build.gradle has been changed, because - // it's possible the version of protoc has been changed. - task.inputs.file("${rootProject.projectDir}/build.gradle") - .withPathSensitivity(PathSensitivity.RELATIVE) - .withPropertyName('root build.gradle') if (isAndroid) { task.builtins { java { option 'lite' } @@ -164,7 +162,8 @@ subprojects { checkstyle { configDirectory = file("$rootDir/buildscripts") - toolVersion = libs.checkstyle.get().version + toolVersion = JavaVersion.current().isJava11Compatible() ? libs.checkstyle.get().version : libs.checkstylejava8.get().version + ignoreFailures = false if (rootProject.hasProperty("checkstyle.ignoreFailures")) { ignoreFailures = rootProject.properties["checkstyle.ignoreFailures"].toBoolean() @@ -183,9 +182,6 @@ subprojects { } plugins.withId("java") { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - dependencies { testImplementation libraries.junit, libraries.mockito.core, @@ -241,12 +237,25 @@ subprojects { } } + tasks.withType(JavaCompile).configureEach { + if (JavaVersion.current().isJava9Compatible()) { + options.release = 8 + } else { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + } tasks.named("compileJava").configure { // 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) + + // TODO(https://github.com/grpc/grpc-java/issues/10372): remove when fixed. + if (JavaVersion.current().isJava11Compatible()) { + options.errorprone.check("StringCaseLocaleUsage", CheckSeverity.OFF) + } } tasks.named("compileTestJava").configure { // LinkedList doesn't hurt much in tests and has lots of usages @@ -273,7 +282,7 @@ subprojects { requireUpperBoundDepsMatch(configurations.runtimeClasspath, project) } } - tasks.named('compileJava').configure { + tasks.named('assemble').configure { dependsOn checkUpperBoundDeps } } @@ -281,10 +290,6 @@ subprojects { plugins.withId("me.champeau.jmh") { // invoke jmh on a single benchmark class like so: // ./gradlew -PjmhIncludeSingleClass=StatsTraceContextBenchmark clean :grpc-core:jmh - tasks.named("compileJmhJava").configure { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - } tasks.named("jmh").configure { warmupIterations = 10 iterations = 10 @@ -412,31 +417,25 @@ subprojects { def baselineGrpcVersion = '1.6.1' // Get the baseline version's jar for this subproject - File baselineArtifact = null - // Use a detached configuration, otherwise the current version's jar will take precedence - // over the baseline jar. + configurations { + baselineArtifact + } // A necessary hack, the intuitive thing does NOT work: // https://discuss.gradle.org/t/is-the-default-configuration-leaking-into-independent-configurations/2088/6 def oldGroup = project.group try { project.group = 'virtual_group_for_japicmp' - String depModule = "io.grpc:${project.name}:${baselineGrpcVersion}@jar" - String depJar = "${project.name}-${baselineGrpcVersion}.jar" - Configuration configuration = configurations.detachedConfiguration( - dependencies.create(depModule) - ) - baselineArtifact = files(configuration.files).filter { - it.name.equals(depJar) - }.singleFile + dependencies { + baselineArtifact "io.grpc:${project.name}:${baselineGrpcVersion}@jar" + } } finally { project.group = oldGroup } // Add a japicmp task that compares the current .jar with baseline .jar tasks.register("japicmp", me.champeau.gradle.japicmp.JapicmpTask) { - dependsOn jar - oldClasspath = files(baselineArtifact) - newClasspath = files(jar.archiveFile) + oldClasspath.from configurations.baselineArtifact + newClasspath.from tasks.named("jar") onlyBinaryIncompatibleModified = false // Be quiet about things that did not change onlyModified = true @@ -449,12 +448,10 @@ subprojects { // Also break on source incompatible changes, not just binary. // Eg adding abstract method to public class. - // TODO(zpencer): enable after japicmp-gradle-plugin/pull/14 - // breakOnSourceIncompatibility = true + failOnSourceIncompatibility = true // Ignore any classes or methods marked @ExperimentalApi - // TODO(zpencer): enable after japicmp-gradle-plugin/pull/15 - // annotationExcludes = ['@io.grpc.ExperimentalApi'] + annotationExcludes = ['@io.grpc.ExperimentalApi'] } } } @@ -506,6 +503,11 @@ def requireUpperBoundDepsMatch(Configuration conf, Project project) { + "to diagnose") } result.selected.dependencies.each { + // Category.CATEGORY_ATTRIBUTE is the inappropriate Attribute because it is "desugared". + // https://github.com/gradle/gradle/issues/8854 + Attribute category = Attribute.of('org.gradle.category', String) + if (it.resolvedVariant.attributes.getAttribute(category) != Category.LIBRARY) + return queue.add(new DepAndParents( dep: it, parents: depAndParents.parents + [artifact + ":" + version])) } @@ -570,7 +572,7 @@ tasks.register('checkForUpdates') { if (oldResolved != newResolved) { def oldId = oldResolved.id.componentIdentifier def newId = newResolved.id.componentIdentifier - println("${newId.group}:${newId.module} ${oldId.version} -> ${newId.version}") + println("libs.${name} = ${newId.group}:${newId.module} ${oldId.version} -> ${newId.version}") } } } diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml index a5aded93a80d..960fa162ed1d 100644 --- a/buildscripts/checkstyle.xml +++ b/buildscripts/checkstyle.xml @@ -199,7 +199,7 @@ - + - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT example-debug https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 1.8 1.8 diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 917d5ae0cf1d..d7e6a24a7257 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -1,8 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -15,16 +14,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protobufVersion = '3.24.0' def protocVersion = protobufVersion @@ -67,7 +68,9 @@ task googleAuthClient(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(googleAuthClient) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(googleAuthClient) + fileMode = 0755 + } } diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index ce4bfd7afa90..282ac4a3b8e5 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -6,14 +6,14 @@ jar - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT example-gauth https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 1.8 1.8 diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 5c44087c6bbe..ed5acb81c39e 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -1,8 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' @@ -16,16 +15,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" @@ -61,8 +62,10 @@ task ObservabilityHelloWorldClient(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(ObservabilityHelloWorldServer) - from(ObservabilityHelloWorldClient) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(ObservabilityHelloWorldServer) + from(ObservabilityHelloWorldClient) + fileMode = 0755 + } } diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 6f65a3103c17..9784fbb531a6 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -2,7 +2,7 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. id 'java' - id "com.google.protobuf" version "0.8.17" + id "com.google.protobuf" version "0.9.4" id 'com.google.cloud.tools.jib' version '3.1.4' // For releasing to Docker Hub } @@ -13,16 +13,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protobufVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 5520ffe201a1..a8bc7c485a06 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -6,14 +6,14 @@ jar - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT example-hostname https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 1.8 1.8 diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index f9ebf8509437..bfab22662762 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -1,8 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -14,16 +13,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protobufVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protobufVersion = '3.24.0' def protocVersion = protobufVersion dependencies { @@ -77,8 +78,10 @@ task hellowWorldJwtAuthClient(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(hellowWorldJwtAuthServer) - from(hellowWorldJwtAuthClient) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(hellowWorldJwtAuthServer) + from(hellowWorldJwtAuthClient) + fileMode = 0755 + } } diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 55b01c914b6f..2ba3e0ae9c15 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -7,15 +7,15 @@ jar - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT example-jwt-auth https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 + 3.24.0 1.8 1.8 diff --git a/examples/example-jwt-auth/src/main/java/io/grpc/examples/jwtauth/JwtCredential.java b/examples/example-jwt-auth/src/main/java/io/grpc/examples/jwtauth/JwtCredential.java index 4975b4a0ab32..bf6243224d20 100644 --- a/examples/example-jwt-auth/src/main/java/io/grpc/examples/jwtauth/JwtCredential.java +++ b/examples/example-jwt-auth/src/main/java/io/grpc/examples/jwtauth/JwtCredential.java @@ -60,9 +60,4 @@ public void run() { } }); } - - @Override - public void thisUsesUnstableApi() { - // noop - } } diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index a9a0066d1166..a9a6f7539727 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -1,7 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' @@ -14,11 +13,13 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} -def grpcVersion = '1.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" @@ -55,8 +56,10 @@ task CustomBackendMetricsServer(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(CustomBackendMetricsClient) - from(CustomBackendMetricsServer) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(CustomBackendMetricsClient) + from(CustomBackendMetricsServer) + fileMode = 0755 + } } diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index bda681ece274..3c81364b0793 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -1,7 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' @@ -14,11 +13,13 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} -def grpcVersion = '1.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" @@ -48,7 +49,9 @@ task ReflectionServer(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(ReflectionServer) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(ReflectionServer) + fileMode = 0755 + } } diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index 0ea93dd70083..39d518274ea8 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -1,6 +1,5 @@ plugins { - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'war' @@ -12,11 +11,13 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} -def grpcVersion = '1.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}", diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index ea70f249904c..90c941e66b67 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -1,8 +1,7 @@ plugins { // Provide convenience executables for trying out the examples. id 'application' - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' } @@ -15,16 +14,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" @@ -69,8 +70,10 @@ task helloWorldTlsClient(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(helloWorldTlsServer) - from(helloWorldTlsClient) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(helloWorldTlsServer) + from(helloWorldTlsClient) + fileMode = 0755 + } } diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 3476f8934277..854790546130 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -6,14 +6,14 @@ jar - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT example-tls https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 1.8 1.8 diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index a772edafee02..de449268b9c1 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -1,7 +1,6 @@ plugins { id 'application' // Provide convenience executables for trying out the examples. - // ASSUMES GRADLE 5.6 OR HIGHER. Use plugin version 0.8.10 with earlier gradle versions - id 'com.google.protobuf' version '0.8.17' + id 'com.google.protobuf' version '0.9.4' // Generate IntelliJ IDEA's .idea & .iml project files id 'idea' id 'java' @@ -14,16 +13,18 @@ repositories { mavenLocal() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_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.57.0-SNAPSHOT' // CURRENT_GRPC_VERSION -def protocVersion = '3.22.3' +def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION +def protocVersion = '3.24.0' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" @@ -61,8 +62,10 @@ task xdsHelloWorldServer(type: CreateStartScripts) { classpath = startScripts.classpath } -applicationDistribution.into('bin') { - from(xdsHelloWorldClient) - from(xdsHelloWorldServer) - fileMode = 0755 +application { + applicationDistribution.into('bin') { + from(xdsHelloWorldClient) + from(xdsHelloWorldServer) + fileMode = 0755 + } } diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index 070cb702f09e..db9a6b825d7f 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/pom.xml b/examples/pom.xml index b928e5be0742..8316e9a5d4ee 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,15 +6,15 @@ jar - 1.57.0-SNAPSHOT + 1.59.0-SNAPSHOT examples https://github.com/grpc/grpc-java UTF-8 - 1.57.0-SNAPSHOT - 3.22.3 - 3.22.3 + 1.59.0-SNAPSHOT + 3.24.0 + 3.24.0 1.8 1.8 diff --git a/examples/src/main/java/io/grpc/examples/preserialized/ByteArrayMarshaller.java b/examples/src/main/java/io/grpc/examples/preserialized/ByteArrayMarshaller.java new file mode 100644 index 000000000000..c6f099280f90 --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/preserialized/ByteArrayMarshaller.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.examples.preserialized; + +import com.google.common.io.ByteStreams; +import io.grpc.MethodDescriptor; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A marshaller that produces a byte[] instead of decoding into typical POJOs. It can be used for + * any message type. + */ +final class ByteArrayMarshaller implements MethodDescriptor.Marshaller { + @Override + public byte[] parse(InputStream stream) { + try { + return ByteStreams.toByteArray(stream); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public InputStream stream(byte[] b) { + return new ByteArrayInputStream(b); + } +} diff --git a/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedClient.java b/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedClient.java new file mode 100644 index 000000000000..511e8a177f8a --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedClient.java @@ -0,0 +1,108 @@ +/* + * Copyright 2023 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.examples.preserialized; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; +import io.grpc.StatusRuntimeException; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.stub.ClientCalls; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A client that requests a greeting from a hello-world server, but using a pre-serialized request. + * This is a performance optimization that can be useful if you read the request from on-disk or a + * database where it is already serialized, or if you need to send the same complicated message to + * many servers. The same approach can avoid deserializing responses, to be stored in a database. + * This adjustment is client-side only; the server is unable to detect the difference, so this + * client is fully-compatible with the normal {@link HelloWorldServer}. + */ +public class PreSerializedClient { + private static final Logger logger = Logger.getLogger(PreSerializedClient.class.getName()); + + /** + * Modified sayHello() descriptor with bytes as the request, instead of HelloRequest. By adjusting + * toBuilder() you can choose which of the request and response are bytes. + */ + private static final MethodDescriptor SAY_HELLO + = GreeterGrpc.getSayHelloMethod() + .toBuilder(new ByteArrayMarshaller(), GreeterGrpc.getSayHelloMethod().getResponseMarshaller()) + .build(); + + private final Channel channel; + + /** Construct client for accessing hello-world server using the existing channel. */ + public PreSerializedClient(Channel channel) { + this.channel = channel; + } + + /** Say hello to server. */ + public void greet(String name) { + logger.info("Will try to greet " + name + " ..."); + byte[] request = HelloRequest.newBuilder().setName(name).build().toByteArray(); + HelloReply response; + try { + // Stubs use ClientCalls to send RPCs. Since the generated stub won't have byte[] in its + // method signature, this uses ClientCalls directly. It isn't as convenient, but it behaves + // the same as a normal stub. + response = ClientCalls.blockingUnaryCall(channel, SAY_HELLO, CallOptions.DEFAULT, request); + } catch (StatusRuntimeException e) { + logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus()); + return; + } + logger.info("Greeting: " + response.getMessage()); + } + + /** + * Greet server. If provided, the first element of {@code args} is the name to use in the + * greeting. The second argument is the target server. + */ + public static void main(String[] args) throws Exception { + String user = "world"; + String target = "localhost:50051"; + if (args.length > 0) { + if ("--help".equals(args[0])) { + System.err.println("Usage: [name [target]]"); + System.err.println(""); + System.err.println(" name The name you wish to be greeted by. Defaults to " + user); + System.err.println(" target The server to connect to. Defaults to " + target); + System.exit(1); + } + user = args[0]; + } + if (args.length > 1) { + target = args[1]; + } + + ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()) + .build(); + try { + PreSerializedClient client = new PreSerializedClient(channel); + client.greet(user); + } finally { + channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + } + } +} diff --git a/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedServer.java b/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedServer.java new file mode 100644 index 000000000000..51beca57386c --- /dev/null +++ b/examples/src/main/java/io/grpc/examples/preserialized/PreSerializedServer.java @@ -0,0 +1,164 @@ +/* + * Copyright 2023 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.examples.preserialized; + +import io.grpc.BindableService; +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerCallHandler; +import io.grpc.ServerMethodDefinition; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Server that provides a {@code Greeter} service, but that uses a pre-serialized response. This is + * a performance optimization that can be useful if you read the response from on-disk or a database + * where it is already serialized, or if you need to send the same complicated message to many + * clients. The same approach can avoid deserializing requests, to be stored in a database. This + * adjustment is server-side only; the client is unable to detect the differences, so this server is + * fully-compatible with the normal {@link HelloWorldClient}. + */ +public class PreSerializedServer { + private static final Logger logger = Logger.getLogger(PreSerializedServer.class.getName()); + + private Server server; + + private void start() throws IOException { + int port = 50051; + server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()) + .addService(new GreeterImpl()) + .build() + .start(); + logger.info("Server started, listening on " + port); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + // Use stderr here since the logger may have been reset by its JVM shutdown hook. + System.err.println("*** shutting down gRPC server since JVM is shutting down"); + try { + PreSerializedServer.this.stop(); + } catch (InterruptedException e) { + e.printStackTrace(System.err); + } + System.err.println("*** server shut down"); + } + }); + } + + private void stop() throws InterruptedException { + if (server != null) { + server.shutdown().awaitTermination(30, TimeUnit.SECONDS); + } + } + + /** + * Await termination on the main thread since the grpc library uses daemon threads. + */ + private void blockUntilShutdown() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } + + /** + * Main launches the server from the command line. + */ + public static void main(String[] args) throws IOException, InterruptedException { + final PreSerializedServer server = new PreSerializedServer(); + server.start(); + server.blockUntilShutdown(); + } + + static class GreeterImpl implements GreeterGrpc.AsyncService, BindableService { + + public void byteSayHello(HelloRequest req, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build(); + responseObserver.onNext(reply.toByteArray()); + responseObserver.onCompleted(); + } + + @Override + public ServerServiceDefinition bindService() { + MethodDescriptor sayHello = GreeterGrpc.getSayHelloMethod(); + // Modifying the method descriptor to use bytes as the response, instead of HelloReply. By + // adjusting toBuilder() you can choose which of the request and response are bytes. + MethodDescriptor byteSayHello = sayHello + .toBuilder(sayHello.getRequestMarshaller(), new ByteArrayMarshaller()) + .build(); + // GreeterGrpc.bindService() will bind every service method, including sayHello(). (Although + // Greeter only has one method, this approach would work for any service.) AsyncService + // provides a default implementation of sayHello() that returns UNIMPLEMENTED, and that + // implementation will be used by bindService(). replaceMethod() will rewrite that method to + // use our byte-based method instead. + // + // The generated bindService() uses ServerCalls to make RPC handlers. Since the generated + // bindService() won't expect byte[] in the AsyncService, this uses ServerCalls directly. It + // isn't as convenient, but it behaves the same as a normal RPC handler. + return replaceMethod( + GreeterGrpc.bindService(this), + byteSayHello, + ServerCalls.asyncUnaryCall(this::byteSayHello)); + } + + /** Rewrites the ServerServiceDefinition replacing one method's definition. */ + private static ServerServiceDefinition replaceMethod( + ServerServiceDefinition def, + MethodDescriptor newDesc, + ServerCallHandler newHandler) { + // There are two data structures involved. The first is the "descriptor" which describes the + // service and methods as a schema. This is the same on client and server. The second is the + // "definition" which includes the handlers to execute methods. This is specific to the server + // and is generated by "bind." This adjusts both the descriptor and definition. + + // Descriptor + ServiceDescriptor desc = def.getServiceDescriptor(); + ServiceDescriptor.Builder descBuilder = ServiceDescriptor.newBuilder(desc.getName()) + .setSchemaDescriptor(desc.getSchemaDescriptor()) + .addMethod(newDesc); // Add the modified method + // Copy methods other than the modified one + for (MethodDescriptor md : desc.getMethods()) { + if (newDesc.getFullMethodName().equals(md.getFullMethodName())) { + continue; + } + descBuilder.addMethod(md); + } + + // Definition + ServerServiceDefinition.Builder defBuilder = + ServerServiceDefinition.builder(descBuilder.build()) + .addMethod(newDesc, newHandler); // Add the modified method + // Copy methods other than the modified one + for (ServerMethodDefinition smd : def.getMethods()) { + if (newDesc.getFullMethodName().equals(smd.getMethodDescriptor().getFullMethodName())) { + continue; + } + defBuilder.addMethod(smd); + } + return defBuilder.build(); + } + } +} diff --git a/gae-interop-testing/gae-jdk8/build.gradle b/gae-interop-testing/gae-jdk8/build.gradle index 78cadc60582d..f3ff765ddfb0 100644 --- a/gae-interop-testing/gae-jdk8/build.gradle +++ b/gae-interop-testing/gae-jdk8/build.gradle @@ -90,9 +90,6 @@ appengine { group = 'io.grpc' // Generated output GroupId version = '1.0-SNAPSHOT' // Version in generated output -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - /** Returns the service name. */ String getGaeProject() { def stream = new ByteArrayOutputStream() diff --git a/gcp-observability/build.gradle b/gcp-observability/build.gradle index 9cef17fcd84c..69bff88bf0fd 100644 --- a/gcp-observability/build.gradle +++ b/gcp-observability/build.gradle @@ -19,9 +19,13 @@ tasks.named("compileJava").configure { "|") } -dependencies { - def cloudLoggingVersion = '3.14.5' +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.gcp.observability') + } +} +dependencies { annotationProcessor libraries.auto.value api project(':grpc-api') @@ -32,7 +36,7 @@ dependencies { project(':grpc-census'), libraries.opencensus.contrib.grpc.metrics // Avoid gradle using project dependencies without configuration: shadow - implementation ("com.google.cloud:google-cloud-logging:${cloudLoggingVersion}") { + implementation (libraries.google.cloud.logging) { exclude group: 'io.grpc', module: 'grpc-alts' exclude group: 'io.grpc', module: 'grpc-netty-shaded' exclude group: 'io.grpc', module: 'grpc-xds' @@ -57,13 +61,13 @@ dependencies { project(':grpc-grpclb'), // Align grpc versions project(':grpc-services'), // Align grpc versions libraries.animalsniffer.annotations, // Use our newer version + libraries.auto.value.annotations, // Use our newer version libraries.guava.jre, // Use our newer version libraries.protobuf.java.util, // Use our newer version libraries.re2j, // Use our newer version - libraries.checker.qual, // Explicit dependency to keep in step with version used by guava libraries.j2objc.annotations // Explicit dependency to keep in step with version used by guava - testImplementation testFixtures(project(':grpc-context')), + testImplementation testFixtures(project(':grpc-api')), project(':grpc-testing'), project(':grpc-testing-proto') testImplementation (libraries.guava.testlib) { diff --git a/googleapis/build.gradle b/googleapis/build.gradle index 72f5b9794060..435e552d47d1 100644 --- a/googleapis/build.gradle +++ b/googleapis/build.gradle @@ -7,6 +7,12 @@ plugins { description = 'gRPC: googleapis' +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.googleapis') + } +} + dependencies { api project(':grpc-api') implementation project(path: ':grpc-alts', configuration: 'shadow'), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f3d93ffe8f6..9ed986c9aa2c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,18 +2,28 @@ # Compatibility problem with internal version getting onto 1.5.3. # https://github.com/grpc/grpc-java/pull/9118 googleauth = "1.4.0" -guava = "32.0.1-android" -netty = '4.1.87.Final' +# Update notes / 2023-07-19 sergiitk: +# Couldn't update to 32.1.1 because Guava 32.1.0 broke gradle metadata: +# https://github.com/google/guava/releases/tag/v32.1.0 +# 32.1.1 partially fixed this, but our build still breaks with: +# Could not resolve com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava. +# +# TODO(any release manager): attempt removing runtimeOnly dependencies when guava upgraded: +# - okhttp: errorprone.annotations +# +# Allowed to be different from guava-jre. +guava = '32.0.1-android' +netty = '4.1.93.Final' # Keep the following references of tcnative version in sync whenever it's updated: # SECURITY.md nettytcnative = '2.0.61.Final' opencensus = "0.31.1" -protobuf = "3.22.3" +protobuf = "3.24.0" [libraries] android-annotations = "com.google.android:annotations:4.1.1.4" androidx-annotation = "androidx.annotation:annotation:1.6.0" -androidx-core = "androidx.core:core:1.10.0" +androidx-core = "androidx.core:core:1.10.1" androidx-lifecycle-common = "androidx.lifecycle:lifecycle-common:2.6.1" androidx-lifecycle-service = "androidx.lifecycle:lifecycle-service:2.6.1" androidx-test-core = "androidx.test:core:1.5.0" @@ -21,24 +31,26 @@ androidx-test-ext-junit = "androidx.test.ext:junit:1.1.5" androidx-test-rules = "androidx.test:rules:1.5.0" animalsniffer = "org.codehaus.mojo:animal-sniffer:1.23" animalsniffer-annotations = "org.codehaus.mojo:animal-sniffer-annotations:1.23" -auto-value = "com.google.auto.value:auto-value:1.10.1" -auto-value-annotations = "com.google.auto.value:auto-value-annotations:1.10.1" -checker-qual = "org.checkerframework:checker-qual:3.33.0" -checkstyle = "com.puppycrawl.tools:checkstyle:8.28" +auto-value = "com.google.auto.value:auto-value:1.10.2" +auto-value-annotations = "com.google.auto.value:auto-value-annotations:1.10.2" +checkstyle = "com.puppycrawl.tools:checkstyle:10.12.1" commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:108.5359.79" cronet-embedded = "org.chromium.net:cronet-embedded:108.5359.79" -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.18.0" -errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" -errorprone-core = "com.google.errorprone:error_prone_core:2.18.0" -google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.17.0" +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.20.0" +errorprone-core = "com.google.errorprone:error_prone_core:2.20.0" +google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.22.0" google-auth-credentials = { module = "com.google.auth:google-auth-library-credentials", version.ref = "googleauth" } google-auth-oauth2Http = { module = "com.google.auth:google-auth-library-oauth2-http", version.ref = "googleauth" } +# Release notes: https://cloud.google.com/logging/docs/release-notes +google-cloud-logging = "com.google.cloud:google-cloud-logging:3.15.5" gson = "com.google.code.gson:gson:2.10.1" guava = { module = "com.google.guava:guava", version.ref = "guava" } guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava" } +# JRE version is needed for projects where its a transitive dependency, f.e. gcp-observability. +# May be different from the -android version. guava-jre = "com.google.guava:guava:32.0.1-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.1.12" javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" @@ -46,8 +58,11 @@ j2objc-annotations = " com.google.j2objc:j2objc-annotations:2.8" jetty-alpn-agent = "org.mortbay.jetty.alpn:jetty-alpn-agent:2.0.10" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" -mockito-android = "org.mockito:mockito-android:3.12.4" -mockito-core = "org.mockito:mockito-core:3.12.4" +# Update notes / 2023-07-19 sergiitk: +# Couldn't update to 5.4.0, updated to the last in 4.x line. Version 5.x breaks some tests. +# Error log: https://github.com/grpc/grpc-java/pull/10359#issuecomment-1632834435 +mockito-android = "org.mockito:mockito-android:4.11.0" +mockito-core = "org.mockito:mockito-core:4.11.0" netty-codec-http2 = { module = "io.netty:netty-codec-http2", version.ref = "netty" } netty-handler-proxy = { module = "io.netty:netty-handler-proxy", version.ref = "netty" } netty-tcnative = { module = "io.netty:netty-tcnative-boringssl-static", version.ref = "nettytcnative" } @@ -55,7 +70,7 @@ netty-tcnative-classes = { module = "io.netty:netty-tcnative-classes", version.r netty-transport-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-unix-common = { module = "io.netty:netty-transport-native-unix-common", version.ref = "netty" } okhttp = "com.squareup.okhttp:okhttp:2.7.5" -okio = "com.squareup.okio:okio:1.17.5" +okio = "com.squareup.okio:okio:2.10.0" opencensus-api = { module = "io.opencensus:opencensus-api", version.ref = "opencensus" } opencensus-contrib-grpc-metrics = { module = "io.opencensus:opencensus-contrib-grpc-metrics", version.ref = "opencensus" } opencensus-exporter-stats-stackdriver = { module = "io.opencensus:opencensus-exporter-stats-stackdriver", version.ref = "opencensus" } @@ -68,10 +83,13 @@ protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", versio protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" } protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } re2j = "com.google.re2j:re2j:1.7" -# Compilation failed with 4.10.2 due to native graphics, or something. We don't -# use it, but it seemed the compiler felt it needed the definition -robolectric = "org.robolectric:robolectric:4.9.2" +robolectric = "org.robolectric:robolectric:4.10.3" signature-android = "net.sf.androidscents.signature:android-api-level-19:4.4.2_r4" signature-java = "org.codehaus.mojo.signature:java18:1.0" -# 1.1+ requires Java 8, but we still use Java 7 with grpc-context -truth = "com.google.truth:truth:1.0.1" +truth = "com.google.truth:truth:1.1.5" + +# Do not update: Pinned to the last version supporting Java 8. +# See https://checkstyle.sourceforge.io/releasenotes.html#Release_10.1 +checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3" +# See https://github.com/google/error-prone/releases/tag/v2.11.0 +errorprone-corejava8 = "com.google.errorprone:error_prone_core:2.10.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb702f09e..db9a6b825d7f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/grpclb/BUILD.bazel b/grpclb/BUILD.bazel index 0ca4d695bb40..e82d8022bd2a 100644 --- a/grpclb/BUILD.bazel +++ b/grpclb/BUILD.bazel @@ -14,7 +14,7 @@ java_library( "//api", "//context", "//core:internal", - "//core:util", + "//util", "//stub", "@com_google_code_findbugs_jsr305//jar", "@com_google_guava_guava//jar", diff --git a/grpclb/build.gradle b/grpclb/build.gradle index 7aa2d62b0f41..cea599828f5a 100644 --- a/grpclb/build.gradle +++ b/grpclb/build.gradle @@ -9,6 +9,12 @@ plugins { description = "gRPC: GRPCLB LoadBalancer plugin" +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.grpclb') + } +} + dependencies { implementation project(':grpc-core'), project(':grpc-protobuf'), @@ -19,6 +25,7 @@ dependencies { runtimeOnly libraries.errorprone.annotations compileOnly libraries.javax.annotation testImplementation libraries.truth, + project(':grpc-inprocess'), testFixtures(project(':grpc-core')) signature libraries.signature.java @@ -28,6 +35,7 @@ configureProtoCompilation() tasks.named("javadoc").configure { exclude 'io/grpc/grpclb/Internal*' + exclude 'io/grpc/grpclb/*Provider.java' } tasks.named("jacocoTestReport").configure { diff --git a/grpclb/src/main/java/io/grpc/grpclb/SecretGrpclbNameResolverProvider.java b/grpclb/src/main/java/io/grpc/grpclb/SecretGrpclbNameResolverProvider.java index da5b7c3353e9..3970c281e1be 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/SecretGrpclbNameResolverProvider.java +++ b/grpclb/src/main/java/io/grpc/grpclb/SecretGrpclbNameResolverProvider.java @@ -53,6 +53,9 @@ public static final class Provider extends NameResolverProvider { private static final String SCHEME = "dns"; + private static final boolean IS_ANDROID = InternalServiceProviders + .isAndroid(SecretGrpclbNameResolverProvider.class.getClassLoader()); + @Override public GrpclbNameResolver newNameResolver(URI targetUri, Args args) { if (SCHEME.equals(targetUri.getScheme())) { @@ -68,7 +71,7 @@ public GrpclbNameResolver newNameResolver(URI targetUri, Args args) { args, GrpcUtil.SHARED_CHANNEL_EXECUTOR, Stopwatch.createUnstarted(), - InternalServiceProviders.isAndroid(getClass().getClassLoader())); + IS_ANDROID); } else { return null; } diff --git a/inprocess/BUILD.bazel b/inprocess/BUILD.bazel new file mode 100644 index 000000000000..65f2adceda1d --- /dev/null +++ b/inprocess/BUILD.bazel @@ -0,0 +1,16 @@ +java_library( + name = "inprocess", + srcs = glob([ + "src/main/java/io/grpc/inprocess/*.java", + ]), + visibility = ["//visibility:public"], + deps = [ + "//core:internal", + "//api", + "//context", + "@com_google_code_findbugs_jsr305//jar", + "@com_google_errorprone_error_prone_annotations//jar", + "@com_google_guava_guava//jar", + "@com_google_j2objc_j2objc_annotations//jar", + ], +) diff --git a/inprocess/build.gradle b/inprocess/build.gradle new file mode 100644 index 000000000000..11a2be722cce --- /dev/null +++ b/inprocess/build.gradle @@ -0,0 +1,30 @@ +plugins { + id "java-library" + id "maven-publish" + + id "ru.vyarus.animalsniffer" +} + +description = 'gRPC: Inprocess' + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.inprocess') + } +} + +dependencies { + api project(':grpc-core') + + implementation libraries.guava + testImplementation project(':grpc-testing'), + testFixtures(project(':grpc-core')) + testImplementation libraries.guava.testlib + + signature libraries.signature.java + signature libraries.signature.android +} + +tasks.named("javadoc").configure { + exclude 'io/grpc/inprocess/Internal*' +} diff --git a/core/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java rename to inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java diff --git a/core/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java rename to inprocess/src/main/java/io/grpc/inprocess/InProcessChannelBuilder.java diff --git a/core/src/main/java/io/grpc/inprocess/InProcessServer.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessServer.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InProcessServer.java rename to inprocess/src/main/java/io/grpc/inprocess/InProcessServer.java diff --git a/core/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java rename to inprocess/src/main/java/io/grpc/inprocess/InProcessServerBuilder.java diff --git a/core/src/main/java/io/grpc/inprocess/InProcessSocketAddress.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessSocketAddress.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InProcessSocketAddress.java rename to inprocess/src/main/java/io/grpc/inprocess/InProcessSocketAddress.java diff --git a/core/src/main/java/io/grpc/inprocess/InProcessTransport.java b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java similarity index 99% rename from core/src/main/java/io/grpc/inprocess/InProcessTransport.java rename to inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java index 1c2ac3df22b7..dd1028fde7dd 100644 --- a/core/src/main/java/io/grpc/inprocess/InProcessTransport.java +++ b/inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java @@ -117,7 +117,7 @@ public void uncaughtException(Thread t, Throwable e) { } throw new RuntimeException(e); } - }; + }; @GuardedBy("this") diff --git a/core/src/main/java/io/grpc/inprocess/InternalInProcess.java b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InternalInProcess.java rename to inprocess/src/main/java/io/grpc/inprocess/InternalInProcess.java diff --git a/core/src/main/java/io/grpc/inprocess/InternalInProcessChannelBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcessChannelBuilder.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InternalInProcessChannelBuilder.java rename to inprocess/src/main/java/io/grpc/inprocess/InternalInProcessChannelBuilder.java diff --git a/core/src/main/java/io/grpc/inprocess/InternalInProcessServerBuilder.java b/inprocess/src/main/java/io/grpc/inprocess/InternalInProcessServerBuilder.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/InternalInProcessServerBuilder.java rename to inprocess/src/main/java/io/grpc/inprocess/InternalInProcessServerBuilder.java diff --git a/core/src/main/java/io/grpc/inprocess/package-info.java b/inprocess/src/main/java/io/grpc/inprocess/package-info.java similarity index 100% rename from core/src/main/java/io/grpc/inprocess/package-info.java rename to inprocess/src/main/java/io/grpc/inprocess/package-info.java diff --git a/core/src/test/java/io/grpc/inprocess/AnonymousInProcessSocketAddressTest.java b/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessSocketAddressTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/AnonymousInProcessSocketAddressTest.java rename to inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessSocketAddressTest.java diff --git a/core/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java rename to inprocess/src/test/java/io/grpc/inprocess/AnonymousInProcessTransportTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessChannelBuilderTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessChannelBuilderTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessChannelBuilderTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessChannelBuilderTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessClientTransportFactoryTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessClientTransportFactoryTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessClientTransportFactoryTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessClientTransportFactoryTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessServerBuilderTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessServerBuilderTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessServerBuilderTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessServerBuilderTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessServerTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessServerTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessServerTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessServerTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessSocketAddressTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessSocketAddressTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessSocketAddressTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessSocketAddressTest.java diff --git a/core/src/test/java/io/grpc/inprocess/InProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/InProcessTransportTest.java rename to inprocess/src/test/java/io/grpc/inprocess/InProcessTransportTest.java diff --git a/core/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java b/inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java similarity index 100% rename from core/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java rename to inprocess/src/test/java/io/grpc/inprocess/StandaloneInProcessTransportTest.java diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 96d4ca361441..9967fdfc10d3 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -1,6 +1,6 @@ plugins { id "application" - id "java" + id "java-library" id "maven-publish" id "com.google.protobuf" @@ -21,17 +21,19 @@ dependencies { project(':grpc-googleapis'), project(':grpc-netty'), project(':grpc-okhttp'), - project(':grpc-protobuf'), project(':grpc-rls'), project(':grpc-services'), - project(':grpc-stub'), project(':grpc-testing'), project(path: ':grpc-xds', configuration: 'shadow'), libraries.hdrhistogram, - libraries.junit, libraries.truth, libraries.opencensus.contrib.grpc.metrics, - libraries.google.auth.oauth2Http + libraries.google.auth.oauth2Http, + libraries.guava.jre // Fix checkUpperBoundDeps using -android + api project(':grpc-api'), + project(':grpc-stub'), + project(':grpc-protobuf'), + libraries.junit compileOnly libraries.javax.annotation // TODO(sergiitk): replace with com.google.cloud:google-cloud-logging // Used instead of google-cloud-logging because it's failing @@ -45,8 +47,7 @@ dependencies { libraries.netty.tcnative.classes, project(':grpc-grpclb'), project(':grpc-rls') - testImplementation testFixtures(project(':grpc-context')), - testFixtures(project(':grpc-api')), + testImplementation testFixtures(project(':grpc-api')), testFixtures(project(':grpc-core')), libraries.mockito.core, libraries.okhttp @@ -159,22 +160,24 @@ def xds_federation_test_client = tasks.register("xds_federation_test_client", Cr classpath = startScripts.classpath } -applicationDistribution.into("bin") { - from(test_client) - from(test_server) - from(reconnect_test_client) - from(stresstest_client) - from(http2_client) - from(grpclb_long_lived_affinity_test_client) - from(grpclb_fallback_test_client) - from(xds_test_client) - from(xds_test_server) - from(xds_federation_test_client) - fileMode = 0755 -} +application { + applicationDistribution.into("bin") { + from(test_client) + from(test_server) + from(reconnect_test_client) + from(stresstest_client) + from(http2_client) + from(grpclb_long_lived_affinity_test_client) + from(grpclb_fallback_test_client) + from(xds_test_client) + from(xds_test_server) + from(xds_federation_test_client) + fileMode = 0755 + } -applicationDistribution.into("lib") { - from(configurations.alpnagent) + applicationDistribution.into("lib") { + from(configurations.alpnagent) + } } publishing { diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java index aeb866b35283..e12e4ffa5533 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java @@ -1249,7 +1249,7 @@ public void deadlineInPast() throws Exception { } catch (StatusRuntimeException ex) { assertEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus().getCode()); assertThat(ex.getStatus().getDescription()) - .startsWith("ClientCall started after CallOptions deadline was exceeded"); + .startsWith("ClientCall started after CallOptions deadline was exceeded"); } // CensusStreamTracerModule record final status in the interceptor, thus is guaranteed to be @@ -1282,7 +1282,7 @@ public void deadlineInPast() throws Exception { } catch (StatusRuntimeException ex) { assertEquals(Status.Code.DEADLINE_EXCEEDED, ex.getStatus().getCode()); assertThat(ex.getStatus().getDescription()) - .startsWith("ClientCall started after CallOptions deadline was exceeded"); + .startsWith("ClientCall started after CallOptions deadline was exceeded"); } if (metricsExpected()) { MetricsRecord clientStartRecord = clientStatsRecorder.pollRecord(5, TimeUnit.SECONDS); @@ -2007,18 +2007,21 @@ public Status getStatus() { } private SoakIterationResult performOneSoakIteration( - TestServiceGrpc.TestServiceBlockingStub soakStub) throws Exception { + TestServiceGrpc.TestServiceBlockingStub soakStub, int soakRequestSize, int soakResponseSize) + throws Exception { long startNs = System.nanoTime(); Status status = Status.OK; try { final SimpleRequest request = SimpleRequest.newBuilder() - .setResponseSize(314159) - .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[271828]))) + .setResponseSize(soakResponseSize) + .setPayload( + Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakRequestSize]))) .build(); final SimpleResponse goldenResponse = SimpleResponse.newBuilder() - .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[314159]))) + .setPayload( + Payload.newBuilder().setBody(ByteString.copyFrom(new byte[soakResponseSize]))) .build(); assertResponse(goldenResponse, soakStub.unaryCall(request)); } catch (StatusRuntimeException e) { @@ -2039,7 +2042,9 @@ public void performSoakTest( int maxFailures, int maxAcceptablePerIterationLatencyMs, int minTimeMsBetweenRpcs, - int overallTimeoutSeconds) + int overallTimeoutSeconds, + int soakRequestSize, + int soakResponseSize) throws Exception { int iterationsDone = 0; int totalFailures = 0; @@ -2063,7 +2068,8 @@ public void performSoakTest( .newBlockingStub(soakChannel) .withInterceptors(recordClientCallInterceptor(clientCallCapture)); } - SoakIterationResult result = performOneSoakIteration(soakStub); + SoakIterationResult result = + performOneSoakIteration(soakStub, soakRequestSize, soakResponseSize); SocketAddress peer = clientCallCapture .get().getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); StringBuilder logStr = new StringBuilder( @@ -2124,7 +2130,7 @@ public void performSoakTest( assertTrue(tooManyFailuresErrorMessage, totalFailures <= maxFailures); } - protected static void assertSuccess(StreamRecorder recorder) { + private static void assertSuccess(StreamRecorder recorder) { if (recorder.getError() != null) { throw new AssertionError(recorder.getError()); } diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/StressTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/StressTestClient.java index 91e7e9c6ae3c..d2a5f6474f38 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/StressTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/StressTestClient.java @@ -32,24 +32,22 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.Status; import io.grpc.StatusException; -import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NegotiationType; -import io.grpc.netty.NettyChannelBuilder; +import io.grpc.TlsChannelCredentials; import io.grpc.stub.StreamObserver; import io.grpc.testing.TlsTesting; -import io.netty.handler.ssl.SslContext; import java.io.IOException; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -296,19 +294,9 @@ private List parseServerAddresses(String addressesStr) { List addresses = new ArrayList<>(); for (List namePort : parseCommaSeparatedTuples(addressesStr)) { - InetAddress address; String name = namePort.get(0); int port = Integer.valueOf(namePort.get(1)); - try { - address = InetAddress.getByName(name); - if (serverHostOverride != null) { - // Force the hostname to match the cert the server uses. - address = InetAddress.getByAddress(serverHostOverride, address.getAddress()); - } - } catch (UnknownHostException ex) { - throw new RuntimeException(ex); - } - addresses.add(new InetSocketAddress(address, port)); + addresses.add(new InetSocketAddress(name, port)); } return addresses; @@ -341,19 +329,28 @@ private static List> parseCommaSeparatedTuples(String str) { } private ManagedChannel createChannel(InetSocketAddress address) { - SslContext sslContext = null; - if (useTestCa) { - try { - sslContext = GrpcSslContexts.forClient().trustManager( - TlsTesting.loadCert("ca.pem")).build(); - } catch (Exception ex) { - throw new RuntimeException(ex); + ChannelCredentials channelCredentials; + if (useTls) { + if (useTestCa) { + try { + channelCredentials = TlsChannelCredentials.newBuilder() + .trustManager(TlsTesting.loadCert("ca.pem")) + .build(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } else { + channelCredentials = TlsChannelCredentials.create(); } + } else { + channelCredentials = InsecureChannelCredentials.create(); + } + ManagedChannelBuilder builder = Grpc.newChannelBuilderForAddress( + address.getHostString(), address.getPort(), channelCredentials); + if (serverHostOverride != null) { + builder.overrideAuthority(serverHostOverride); } - return NettyChannelBuilder.forAddress(address) - .negotiationType(useTls ? NegotiationType.TLS : NegotiationType.PLAINTEXT) - .sslContext(sslContext) - .build(); + return builder.build(); } private static String serverAddressesToString(List addresses) { 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 a6e2c4f3bf84..768a32be62c5 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 @@ -97,6 +97,8 @@ public static void main(String[] args) throws Exception { private int soakMinTimeMsBetweenRpcs = 0; private int soakOverallTimeoutSeconds = soakIterations * soakPerIterationMaxAcceptableLatencyMs / 1000; + private int soakRequestSize = 271828; + private int soakResponseSize = 314159; private String additionalMetadata = ""; private static LoadBalancerProvider customBackendMetricsLoadBalancerProvider; @@ -175,6 +177,10 @@ void parseArgs(String[] args) throws Exception { soakMinTimeMsBetweenRpcs = Integer.parseInt(value); } else if ("soak_overall_timeout_seconds".equals(key)) { soakOverallTimeoutSeconds = Integer.parseInt(value); + } else if ("soak_request_size".equals(key)) { + soakRequestSize = Integer.parseInt(value); + } else if ("soak_response_size".equals(key)) { + soakResponseSize = Integer.parseInt(value); } else if ("additional_metadata".equals(key)) { additionalMetadata = value; } else { @@ -247,6 +253,12 @@ void parseArgs(String[] args) throws Exception { + "\n should stop and fail, if the desired number of " + "\n iterations have not yet completed. Default " + c.soakOverallTimeoutSeconds + + "\n --soak_request_size " + + "\n The request size in a soak RPC. Default " + + c.soakRequestSize + + "\n --soak_response_size " + + "\n The response size in a soak RPC. Default " + + c.soakResponseSize + "\n --additional_metadata " + "\n Additional metadata to send in each request, as a " + "\n semicolon-separated list of key:value pairs. Default " @@ -481,7 +493,9 @@ private void runTest(TestCases testCase) throws Exception { soakMaxFailures, soakPerIterationMaxAcceptableLatencyMs, soakMinTimeMsBetweenRpcs, - soakOverallTimeoutSeconds); + soakOverallTimeoutSeconds, + soakRequestSize, + soakResponseSize); break; } @@ -493,7 +507,9 @@ private void runTest(TestCases testCase) throws Exception { soakMaxFailures, soakPerIterationMaxAcceptableLatencyMs, soakMinTimeMsBetweenRpcs, - soakOverallTimeoutSeconds); + soakOverallTimeoutSeconds, + soakRequestSize, + soakResponseSize); break; } diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java index 8f166b6affa7..9b01df0d18d6 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/XdsFederationTestClient.java @@ -74,6 +74,8 @@ public void run() { private int soakPerIterationMaxAcceptableLatencyMs = 1000; private int soakOverallTimeoutSeconds = 10; private int soakMinTimeMsBetweenRpcs = 0; + private int soakRequestSize = 271828; + private int soakResponseSize = 314159; private String testCase = "rpc_soak"; private final ArrayList clients = new ArrayList<>(); @@ -122,6 +124,12 @@ private void parseArgs(String[] args) { case "soak_min_time_ms_between_rpcs": soakMinTimeMsBetweenRpcs = Integer.parseInt(value); break; + case "soak_request_size": + soakRequestSize = Integer.parseInt(value); + break; + case "soak_response_size": + soakResponseSize = Integer.parseInt(value); + break; default: System.err.println("Unknown argument: " + key); usage = true; @@ -175,6 +183,14 @@ private void parseArgs(String[] args) { + "\n channel_soak: sends --soak_iterations RPCs, rebuilding the channel " + "each time." + "\n Default: " + c.testCase + + "\n --soak_request_size " + + "\n The request size in a soak RPC. Default " + + c.soakRequestSize + + "\n" + + " --soak_response_size \n" + + " The response size in a soak RPC. Default" + + " " + + c.soakResponseSize ); System.exit(1); } @@ -249,7 +265,9 @@ public void run() { soakMaxFailures, soakPerIterationMaxAcceptableLatencyMs, soakMinTimeMsBetweenRpcs, - soakOverallTimeoutSeconds); + soakOverallTimeoutSeconds, + soakRequestSize, + soakResponseSize); logger.info("Test case: " + testCase + " done for server: " + serverUri); runSucceeded = true; } catch (Exception e) { diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java index f32d00a42d76..66a3f2da8826 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/StressTestClientTest.java @@ -32,8 +32,6 @@ import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; @@ -103,27 +101,19 @@ public void allCommandlineSwitchesAreSupported() { assertEquals(9090, client.metricsPort()); } - @Test - public void serverHostOverrideShouldBeApplied() { - StressTestClient client = new StressTestClient(); - client.parseArgs(new String[] { - "--server_addresses=localhost:8080", - "--server_host_override=foo.test.google.fr", - }); - - assertEquals("foo.test.google.fr", client.addresses().get(0).getHostName()); - } - @Test public void gaugesShouldBeExported() throws Exception { TestServiceServer server = new TestServiceServer(); - server.parseArgs(new String[]{"--port=" + 0, "--use_tls=false"}); + server.parseArgs(new String[]{"--port=" + 0, "--use_tls=true"}); server.start(); StressTestClient client = new StressTestClient(); client.parseArgs(new String[] {"--test_cases=empty_unary:1", "--server_addresses=localhost:" + server.getPort(), "--metrics_port=" + 0, + "--server_host_override=foo.test.google.fr", + "--use_tls=true", + "--use_test_ca=true", "--num_stubs_per_channel=2"}); client.startMetricsService(); client.runStressTest(); @@ -142,7 +132,7 @@ public void gaugesShouldBeExported() throws Exception { List allGauges = ImmutableList.copyOf(stub.getAllGauges(EmptyMessage.getDefaultInstance())); while (allGauges.size() < gaugeNames.size()) { - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + Thread.sleep(100); allGauges = ImmutableList.copyOf(stub.getAllGauges(EmptyMessage.getDefaultInstance())); } diff --git a/istio-interop-testing/build.gradle b/istio-interop-testing/build.gradle index b4495002af5f..204996ca9d6c 100644 --- a/istio-interop-testing/build.gradle +++ b/istio-interop-testing/build.gradle @@ -26,8 +26,7 @@ dependencies { runtimeOnly libraries.netty.tcnative, libraries.netty.tcnative.classes - testImplementation testFixtures(project(':grpc-context')), - testFixtures(project(':grpc-api')), + testImplementation testFixtures(project(':grpc-api')), testFixtures(project(':grpc-core')), libraries.mockito.core, libraries.junit, diff --git a/java_grpc_library.bzl b/java_grpc_library.bzl index 4c7a7f2791b5..88ad8b889a3a 100644 --- a/java_grpc_library.bzl +++ b/java_grpc_library.bzl @@ -3,6 +3,7 @@ _JavaRpcToolchainInfo = provider( fields = [ "java_toolchain", + "java_plugins", "plugin", "plugin_arg", "protoc", @@ -14,6 +15,7 @@ def _java_rpc_toolchain_impl(ctx): return [ _JavaRpcToolchainInfo( java_toolchain = ctx.attr._java_toolchain, + java_plugins = ctx.attr.java_plugins, plugin = ctx.executable.plugin, plugin_arg = ctx.attr.plugin_arg, protoc = ctx.executable._protoc, @@ -39,6 +41,10 @@ java_rpc_toolchain = rule( default = Label("@com_google_protobuf//:protoc"), executable = True, ), + "java_plugins": attr.label_list( + default = [], + providers = [JavaPluginInfo], + ), "_java_toolchain": attr.label( default = Label("@bazel_tools//tools/jdk:current_java_toolchain"), ), @@ -104,6 +110,7 @@ def _java_rpc_library_impl(ctx): source_jars = [srcjar], output = ctx.outputs.jar, output_source_jar = ctx.outputs.srcjar, + plugins = [plugin[JavaPluginInfo] for plugin in toolchain.java_plugins], deps = [ java_common.make_non_strict(deps_java_info), ] + [dep[JavaInfo] for dep in toolchain.runtime], diff --git a/netty/build.gradle b/netty/build.gradle index 0f22d2774657..3150e8069b5d 100644 --- a/netty/build.gradle +++ b/netty/build.gradle @@ -13,6 +13,12 @@ configurations { alpnagent } +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.netty') + } +} + dependencies { api project(':grpc-core'), libraries.netty.codec.http2 @@ -75,7 +81,10 @@ import net.ltgt.gradle.errorprone.CheckSeverity tasks.named("javadoc").configure { options.links 'http://netty.io/4.1/api/' + exclude 'io/grpc/netty/*Provider.java' + exclude 'io/grpc/netty/GrpcHttp2ConnectionHandler.java' exclude 'io/grpc/netty/Internal*' + exclude 'io/grpc/netty/ProtocolNegotiationEvent.java' } tasks.named("test").configure { diff --git a/netty/shaded/build.gradle b/netty/shaded/build.gradle index 10410c959d65..3e52c3e0d953 100644 --- a/netty/shaded/build.gradle +++ b/netty/shaded/build.gradle @@ -1,9 +1,9 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import org.apache.tools.zip.ZipOutputStream +import org.apache.tools.zip.ZipEntry import org.gradle.api.file.FileTreeElement -import shadow.org.apache.tools.zip.ZipOutputStream -import shadow.org.apache.tools.zip.ZipEntry plugins { id "java" @@ -70,6 +70,9 @@ dependencies { tasks.named("jar").configure { // Must use a different archiveClassifier to avoid conflicting with shadowJar archiveClassifier = 'original' + manifest { + attributes('Automatic-Module-Name': 'io.grpc.netty.shaded') + } } tasks.named("shadowJar").configure { diff --git a/netty/src/jmh/java/io/grpc/netty/MethodDescriptorBenchmark.java b/netty/src/jmh/java/io/grpc/netty/MethodDescriptorBenchmark.java index 921299671811..96844016c5c0 100644 --- a/netty/src/jmh/java/io/grpc/netty/MethodDescriptorBenchmark.java +++ b/netty/src/jmh/java/io/grpc/netty/MethodDescriptorBenchmark.java @@ -38,16 +38,16 @@ public class MethodDescriptorBenchmark { private static final MethodDescriptor.Marshaller marshaller = new MethodDescriptor.Marshaller() { - @Override - public InputStream stream(Void value) { - return new ByteArrayInputStream(new byte[]{}); - } + @Override + public InputStream stream(Void value) { + return new ByteArrayInputStream(new byte[]{}); + } - @Override - public Void parse(InputStream stream) { - return null; - } - }; + @Override + public Void parse(InputStream stream) { + return null; + } + }; MethodDescriptor method = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) diff --git a/netty/src/main/java/io/grpc/netty/NettyServer.java b/netty/src/main/java/io/grpc/netty/NettyServer.java index 4c16ac50a262..fe7913870faf 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServer.java +++ b/netty/src/main/java/io/grpc/netty/NettyServer.java @@ -272,9 +272,7 @@ public void initChannel(Channel ch) { transportListener = listener.transportCreated(transport); } - /** - * Releases the event loop if the channel is "done", possibly due to the channel closing. - */ + /* Releases the event loop if the channel is "done", possibly due to the channel closing. */ final class LoopReleaser implements ChannelFutureListener { private boolean done; diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index fd053125d5c7..bbf2f17748b1 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -512,6 +512,9 @@ private void onDataRead(int streamId, ByteBuf data, int padding, boolean endOfSt flowControlPing().onDataRead(data.readableBytes(), padding); try { NettyServerStream.TransportState stream = serverStream(requireHttp2Stream(streamId)); + if (stream == null) { + return; + } try (TaskCloseable ignore = PerfMark.traceTask("NettyServerHandler.onDataRead")) { PerfMark.attachTag(stream.tag()); stream.inboundDataReceived(data, endOfStream); @@ -679,12 +682,14 @@ void returnProcessedBytes(Http2Stream http2Stream, int bytes) { private void closeStreamWhenDone(ChannelPromise promise, int streamId) throws Http2Exception { final NettyServerStream.TransportState stream = serverStream(requireHttp2Stream(streamId)); - promise.addListener(new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - stream.complete(); - } - }); + if (stream != null) { + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + stream.complete(); + } + }); + } } /** diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 6aa29dea6baf..9cb2c043e541 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -185,8 +185,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return null; } }) - .when(streamListener) - .messagesAvailable(ArgumentMatchers.any()); + .when(streamListener) + .messagesAvailable(ArgumentMatchers.any()); lifecycleManager = new ClientTransportLifecycleManager(listener); // This mocks the keepalive manager only for there's in which we verify it. For other tests diff --git a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java index 61525d8369aa..96551d173a47 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientStreamTest.java @@ -124,8 +124,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return null; } }) - .when(listener) - .messagesAvailable(ArgumentMatchers.any()); + .when(listener) + .messagesAvailable(ArgumentMatchers.any()); } @Override diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 2f92de5a891e..368b0600f9ec 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -179,8 +179,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return null; } }) - .when(streamListener) - .messagesAvailable(any(StreamListener.MessageProducer.class)); + .when(streamListener) + .messagesAvailable(any(StreamListener.MessageProducer.class)); } @Override @@ -634,6 +634,34 @@ public void headersWithMultipleHostsShouldFail() throws Exception { any(ChannelPromise.class)); } + @Test + public void headersWithErrAndEndStreamReturnErrorButNotThrowNpe() throws Exception { + manualSetUp(); + Http2Headers headers = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .add(AsciiString.of("host"), AsciiString.of("example.com")) + .path(new AsciiString("/foo/bar")); + ByteBuf headersFrame = headersFrame(STREAM_ID, headers); + channelRead(headersFrame); + channelRead(emptyGrpcFrame(STREAM_ID, true)); + + Http2Headers responseHeaders = new DefaultHttp2Headers() + .set(InternalStatus.CODE_KEY.name(), String.valueOf(Code.INTERNAL.value())) + .set(InternalStatus.MESSAGE_KEY.name(), "Content-Type is missing from the request") + .status("" + 415) + .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8"); + + verifyWrite() + .writeHeaders( + eq(ctx()), + eq(STREAM_ID), + eq(responseHeaders), + eq(0), + eq(false), + any(ChannelPromise.class)); + + } + @Test public void headersWithAuthorityAndHostUsesAuthority() throws Exception { manualSetUp(); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java index fe9cd8e03a69..35f525e68eac 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerStreamTest.java @@ -93,8 +93,8 @@ public Void answer(InvocationOnMock invocation) throws Throwable { return null; } }) - .when(serverListener) - .messagesAvailable(ArgumentMatchers.any()); + .when(serverListener) + .messagesAvailable(ArgumentMatchers.any()); } @Test diff --git a/okhttp/BUILD.bazel b/okhttp/BUILD.bazel index e550634aca05..30a77b11465f 100644 --- a/okhttp/BUILD.bazel +++ b/okhttp/BUILD.bazel @@ -11,7 +11,7 @@ java_library( deps = [ "//api", "//core:internal", - "//core:util", + "//util", "@com_google_code_findbugs_jsr305//jar", "@com_google_errorprone_error_prone_annotations//jar", "@com_google_guava_guava//jar", diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 37d033350c90..99f799ec97f5 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -8,13 +8,25 @@ plugins { description = "gRPC: OkHttp" +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.okhttp') + } +} + dependencies { - api project(':grpc-core') + api project(':grpc-util') implementation libraries.okio, libraries.guava, libraries.perfmark.api // Make okhttp dependencies compile only compileOnly libraries.okhttp + + // Needed because com.google.guava:guava:32.0.1-android requires + // com.google.errorprone:error_prone_annotations version 2.18.0, while we are running newer. + // TODO(any release manager): remove when guava is updated + runtimeOnly libraries.errorprone.annotations + // Tests depend on base class defined by core module. testImplementation testFixtures(project(':grpc-core')), testFixtures(project(':grpc-api')), @@ -38,6 +50,7 @@ tasks.named("checkstyleMain").configure { tasks.named("javadoc").configure { options.links 'http://square.github.io/okhttp/2.x/okhttp/' exclude 'io/grpc/okhttp/Internal*' + exclude 'io/grpc/okhttp/*Provider.java' exclude 'io/grpc/okhttp/internal/**' } diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java index 11cd177a8407..50de8c7002fa 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientStream.java @@ -321,11 +321,12 @@ public void transportHeadersReceived(List

    headers, boolean endOfStream) * Must be called with holding the transport lock. */ @GuardedBy("lock") - public void transportDataReceived(okio.Buffer frame, boolean endOfStream) { + public void transportDataReceived(okio.Buffer frame, boolean endOfStream, int paddingLen) { // We only support 16 KiB frames, and the max permitted in HTTP/2 is 16 MiB. This is verified // in OkHttp's Http2 deframer. In addition, this code is after the data has been read. int length = (int) frame.size(); - window -= length; + window -= length + paddingLen; + processedWindow -= paddingLen; if (window < 0) { frameWriter.rstStream(id(), ErrorCode.FLOW_CONTROL_ERROR); transport.finishStream( diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java index 6eaaf832a6b0..ea3bf77e990d 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpClientTransport.java @@ -1140,7 +1140,8 @@ public void run() { */ @SuppressWarnings("GuardedBy") @Override - public void data(boolean inFinished, int streamId, BufferedSource in, int length) + public void data(boolean inFinished, int streamId, BufferedSource in, int length, + int paddedLength) throws IOException { logger.logData(OkHttpFrameLogger.Direction.INBOUND, streamId, in.getBuffer(), length, inFinished); @@ -1166,12 +1167,12 @@ public void data(boolean inFinished, int streamId, BufferedSource in, int length synchronized (lock) { // TODO(b/145386688): This access should be guarded by 'stream.transportState().lock'; // instead found: 'OkHttpClientTransport.this.lock' - stream.transportState().transportDataReceived(buf, inFinished); + stream.transportState().transportDataReceived(buf, inFinished, paddedLength - length); } } // connection window update - connectionUnacknowledgedBytesRead += length; + connectionUnacknowledgedBytesRead += paddedLength; if (connectionUnacknowledgedBytesRead >= initialWindowSize * DEFAULT_WINDOW_UPDATE_RATIO) { synchronized (lock) { frameWriter.windowUpdate(0, connectionUnacknowledgedBytesRead); diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java index 45d6b9efc54c..8269a8ddf0fc 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerBuilder.java @@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkArgument; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.DoNotCall; @@ -89,7 +88,7 @@ public final class OkHttpServerBuilder extends ForwardingServerBuilder headerBlock) { * Handle an HTTP2 DATA frame. */ @Override - public void data(boolean inFinished, int streamId, BufferedSource in, int length) + public void data(boolean inFinished, int streamId, BufferedSource in, int length, + int paddedLength) throws IOException { frameLogger.logData( OkHttpFrameLogger.Direction.INBOUND, streamId, in.getBuffer(), length, inFinished); @@ -853,7 +854,7 @@ public void data(boolean inFinished, int streamId, BufferedSource in, int length "Received DATA for half-closed (remote) stream. RFC7540 section 5.1"); return; } - if (stream.inboundWindowAvailable() < length) { + if (stream.inboundWindowAvailable() < paddedLength) { in.skip(length); streamError(streamId, ErrorCode.FLOW_CONTROL_ERROR, "Received DATA size exceeded window size. RFC7540 section 6.9"); @@ -861,11 +862,11 @@ public void data(boolean inFinished, int streamId, BufferedSource in, int length } Buffer buf = new Buffer(); buf.write(in.getBuffer(), length); - stream.inboundDataReceived(buf, length, inFinished); + stream.inboundDataReceived(buf, length, paddedLength - length, inFinished); } // connection window update - connectionUnacknowledgedBytesRead += length; + connectionUnacknowledgedBytesRead += paddedLength; if (connectionUnacknowledgedBytesRead >= config.flowControlWindow * Utils.DEFAULT_WINDOW_UPDATE_RATIO) { synchronized (lock) { @@ -1064,7 +1065,7 @@ private void respondWithHttpError( } streams.put(streamId, stream); if (inFinished) { - stream.inboundDataReceived(new Buffer(), 0, true); + stream.inboundDataReceived(new Buffer(), 0, 0, true); } frameWriter.headers(streamId, headers); outboundFlow.data( @@ -1122,7 +1123,7 @@ public void onPingTimeout() { interface StreamState { /** Must be holding 'lock' when calling. */ - void inboundDataReceived(Buffer frame, int windowConsumed, boolean endOfStream); + void inboundDataReceived(Buffer frame, int dataLength, int paddingLength, boolean endOfStream); /** Must be holding 'lock' when calling. */ boolean hasReceivedEndOfStream(); @@ -1159,12 +1160,12 @@ static class Http2ErrorStreamState implements StreamState, OutboundFlowControlle @Override public void onSentBytes(int frameBytes) {} @Override public void inboundDataReceived( - Buffer frame, int windowConsumed, boolean endOfStream) { + Buffer frame, int dataLength, int paddingLength, boolean endOfStream) { synchronized (lock) { if (endOfStream) { receivedEndOfStream = true; } - window -= windowConsumed; + window -= dataLength + paddingLength; try { frame.skip(frame.size()); // Recycle segments } catch (IOException ex) { diff --git a/okhttp/src/main/resources/META-INF/services/io.grpc.ServerProvider b/okhttp/src/main/resources/META-INF/services/io.grpc.ServerProvider new file mode 100644 index 000000000000..263ea4b68f66 --- /dev/null +++ b/okhttp/src/main/resources/META-INF/services/io.grpc.ServerProvider @@ -0,0 +1 @@ +io.grpc.okhttp.OkHttpServerProvider diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java index 644ba27b50ff..7347399bfe5d 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpClientTransportTest.java @@ -291,14 +291,16 @@ public void close() throws SecurityException { final String message = "Hello Client"; Buffer buffer = createMessageFrame(message); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); assertThat(logs).hasSize(1); log = logs.remove(0); assertThat(log.getMessage()).startsWith(Direction.INBOUND + " DATA: streamId=" + 3); assertThat(log.getLevel()).isEqualTo(Level.FINE); // At most 64 bytes of data frame will be logged. - frameHandler().data(false, 3, createMessageFrame(new String(new char[1000])), 1000); + frameHandler().data(false, 3, createMessageFrame(new String(new char[1000])), + 1000, 1000); assertThat(logs).hasSize(1); log = logs.remove(0); String data = log.getMessage(); @@ -377,7 +379,8 @@ public void maxMessageSizeShouldBeEnforced() throws Exception { // Receive the message. final String message = "Hello Client"; Buffer buffer = createMessageFrame(message); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); listener.waitUntilStreamClosed(); assertEquals(Code.RESOURCE_EXHAUSTED, listener.status.getCode()); @@ -500,7 +503,8 @@ public void readMessages() throws Exception { assertNotNull(listener.headers); for (int i = 0; i < numMessages; i++) { Buffer buffer = createMessageFrame(message + i); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); } frameHandler().headers(true, true, 3, 0, grpcResponseTrailers(), HeadersMode.HTTP_20_HEADERS); listener.waitUntilStreamClosed(); @@ -529,7 +533,8 @@ public void receivedHeadersForInvalidStreamShouldKillConnection() throws Excepti @Test public void receivedDataForInvalidStreamShouldKillConnection() throws Exception { initTransport(); - frameHandler().data(false, 3, createMessageFrame(new String(new char[1000])), 1000); + frameHandler().data(false, 3, createMessageFrame(new String(new char[1000])), + 1000, 1000); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); verify(transportListener).transportShutdown(isA(Status.class)); @@ -551,7 +556,8 @@ public void invalidInboundHeadersCancelStream() throws Exception { HeadersMode.HTTP_20_HEADERS); // Now wait to receive 1000 bytes of data so we can have a better error message before // cancelling the streaam. - frameHandler().data(false, 3, createMessageFrame(new String(new char[1000])), 1000); + frameHandler().data(false, 3, + createMessageFrame(new String(new char[1000])), 1000, 1000); verify(frameWriter, timeout(TIME_OUT_MS)).rstStream(eq(3), eq(ErrorCode.CANCEL)); assertNull(listener.headers); assertEquals(Status.INTERNAL.getCode(), listener.status.getCode()); @@ -622,7 +628,8 @@ public void receiveResetNoError() throws Exception { assertContainStream(3); frameHandler().headers(false, false, 3, 0, grpcResponseHeaders(), HeadersMode.HTTP_20_HEADERS); Buffer buffer = createMessageFrame("a message"); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); frameHandler().headers(true, true, 3, 0, grpcResponseTrailers(), HeadersMode.HTTP_20_HEADERS); frameHandler().rstStream(3, ErrorCode.NO_ERROR); stream.request(1); @@ -762,15 +769,18 @@ public void windowUpdate() throws Exception { int messageLength = INITIAL_WINDOW_SIZE / 4; byte[] fakeMessage = new byte[messageLength]; + int paddingLength = 2; // Stream 1 receives a message - Buffer buffer = createMessageFrame(fakeMessage); + Buffer buffer = createMessageFrame(fakeMessage, paddingLength); int messageFrameLength = (int) buffer.size(); - frameHandler().data(false, 3, buffer, messageFrameLength); + frameHandler().data(false, 3, buffer, messageFrameLength - paddingLength, + messageFrameLength); // Stream 2 receives a message - buffer = createMessageFrame(fakeMessage); - frameHandler().data(false, 5, buffer, messageFrameLength); + buffer = createMessageFrame(fakeMessage, paddingLength); + frameHandler().data(false, 5, buffer, messageFrameLength - paddingLength, + messageFrameLength); verify(frameWriter, timeout(TIME_OUT_MS)) .windowUpdate(eq(0), eq((long) 2 * messageFrameLength)); @@ -778,17 +788,18 @@ public void windowUpdate() throws Exception { // Stream 1 receives another message buffer = createMessageFrame(fakeMessage); - frameHandler().data(false, 3, buffer, messageFrameLength); + messageFrameLength = (int) buffer.size(); + frameHandler().data(false, 3, buffer, messageFrameLength, messageFrameLength); verify(frameWriter, timeout(TIME_OUT_MS)) - .windowUpdate(eq(3), eq((long) 2 * messageFrameLength)); + .windowUpdate(eq(3), eq((long) 2 * messageFrameLength + paddingLength)); // Stream 2 receives another message buffer = createMessageFrame(fakeMessage); - frameHandler().data(false, 5, buffer, messageFrameLength); + frameHandler().data(false, 5, buffer, messageFrameLength, messageFrameLength); verify(frameWriter, timeout(TIME_OUT_MS)) - .windowUpdate(eq(5), eq((long) 2 * messageFrameLength)); + .windowUpdate(eq(5), eq((long) 2 * messageFrameLength + paddingLength)); verify(frameWriter, timeout(TIME_OUT_MS)) .windowUpdate(eq(0), eq((long) 2 * messageFrameLength)); @@ -819,7 +830,8 @@ public void windowUpdateWithInboundFlowControl() throws Exception { frameHandler().headers(false, false, 3, 0, grpcResponseHeaders(), HeadersMode.HTTP_20_HEADERS); Buffer buffer = createMessageFrame(fakeMessage); long messageFrameLength = buffer.size(); - frameHandler().data(false, 3, buffer, (int) messageFrameLength); + frameHandler().data(false, 3, buffer, (int) messageFrameLength, + (int) messageFrameLength); ArgumentCaptor idCaptor = ArgumentCaptor.forClass(Integer.class); verify(frameWriter, timeout(TIME_OUT_MS)).windowUpdate( idCaptor.capture(), eq(messageFrameLength)); @@ -1123,7 +1135,8 @@ public void receiveGoAway() throws Exception { frameHandler().headers(false, false, 3, 0, grpcResponseHeaders(), HeadersMode.HTTP_20_HEADERS); final String receivedMessage = "No, you are fine."; Buffer buffer = createMessageFrame(receivedMessage); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); frameHandler().headers(true, true, 3, 0, grpcResponseTrailers(), HeadersMode.HTTP_20_HEADERS); listener1.waitUntilStreamClosed(); assertEquals(1, listener1.messages.size()); @@ -1154,12 +1167,12 @@ public void streamIdExhausted() throws Exception { assertNotNull(listener.headers); String message = "hello"; Buffer buffer = createMessageFrame(message); - frameHandler().data(false, startId, buffer, (int) buffer.size()); + frameHandler().data(false, startId, buffer, (int) buffer.size(), (int) buffer.size()); getStream(startId).cancel(Status.CANCELLED); // Receives the second message after be cancelled. buffer = createMessageFrame(message); - frameHandler().data(false, startId, buffer, (int) buffer.size()); + frameHandler().data(false, startId, buffer, (int) buffer.size(), (int) buffer.size()); listener.waitUntilStreamClosed(); // Should only have the first message delivered. @@ -1329,7 +1342,7 @@ public void receivingWindowExceeded() throws Exception { byte[] fakeMessage = new byte[messageLength]; Buffer buffer = createMessageFrame(fakeMessage); int messageFrameLength = (int) buffer.size(); - frameHandler().data(false, 3, buffer, messageFrameLength); + frameHandler().data(false, 3, buffer, messageFrameLength, messageFrameLength); listener.waitUntilStreamClosed(); assertEquals(Status.INTERNAL.getCode(), listener.status.getCode()); @@ -1392,7 +1405,8 @@ public void receiveDataWithoutHeader() throws Exception { stream.start(listener); stream.request(1); Buffer buffer = createMessageFrame(new byte[1]); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); // Trigger the failure by a trailer. frameHandler().headers( @@ -1414,11 +1428,13 @@ public void receiveDataWithoutHeaderAndTrailer() throws Exception { stream.start(listener); stream.request(1); Buffer buffer = createMessageFrame(new byte[1]); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); // Trigger the failure by a data frame. buffer = createMessageFrame(new byte[1]); - frameHandler().data(true, 3, buffer, (int) buffer.size()); + frameHandler().data(true, 3, buffer, (int) buffer.size(), + (int) buffer.size()); listener.waitUntilStreamClosed(); assertEquals(Status.INTERNAL.getCode(), listener.status.getCode()); @@ -1436,7 +1452,8 @@ public void receiveLongEnoughDataWithoutHeaderAndTrailer() throws Exception { stream.start(listener); stream.request(1); Buffer buffer = createMessageFrame(new byte[1000]); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); // Once we receive enough detail, we cancel the stream. so we should have sent cancel. verify(frameWriter, timeout(TIME_OUT_MS)).rstStream(eq(3), eq(ErrorCode.CANCEL)); @@ -1459,7 +1476,8 @@ public void receiveDataForUnknownStreamUpdateConnectionWindow() throws Exception Buffer buffer = createMessageFrame( new byte[INITIAL_WINDOW_SIZE / 2 + 1]); - frameHandler().data(false, 3, buffer, (int) buffer.size()); + frameHandler().data(false, 3, buffer, (int) buffer.size(), + (int) buffer.size()); // Should still update the connection window even stream 3 is gone. verify(frameWriter, timeout(TIME_OUT_MS)).windowUpdate(0, HEADER_LENGTH + INITIAL_WINDOW_SIZE / 2 + 1); @@ -1467,7 +1485,8 @@ public void receiveDataForUnknownStreamUpdateConnectionWindow() throws Exception new byte[INITIAL_WINDOW_SIZE / 2 + 1]); // This should kill the connection, since we never created stream 5. - frameHandler().data(false, 5, buffer, (int) buffer.size()); + frameHandler().data(false, 5, buffer, (int) buffer.size(), + (int) buffer.size()); verify(frameWriter, timeout(TIME_OUT_MS)) .goAway(eq(0), eq(ErrorCode.PROTOCOL_ERROR), any(byte[].class)); verify(transportListener).transportShutdown(isA(Status.class)); @@ -2114,10 +2133,15 @@ private static Buffer createMessageFrame(String message) { } private static Buffer createMessageFrame(byte[] message) { + return createMessageFrame(message,0); + } + + private static Buffer createMessageFrame(byte[] message, int paddingLength) { Buffer buffer = new Buffer(); buffer.writeByte(0 /* UNCOMPRESSED */); buffer.writeInt(message.length); buffer.write(message); + buffer.write(new byte[paddingLength]); return buffer; } diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerProviderTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerProviderTest.java new file mode 100644 index 000000000000..93354f01e257 --- /dev/null +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerProviderTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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.okhttp; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import io.grpc.InsecureServerCredentials; +import io.grpc.ServerCredentials; +import io.grpc.ServerProvider; +import io.grpc.ServerRegistryAccessor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link OkHttpServerProvider}. */ +@RunWith(JUnit4.class) +public class OkHttpServerProviderTest { + private OkHttpServerProvider provider = new OkHttpServerProvider(); + + @Test + public void provided() { + assertThat(ServerProvider.provider()).isInstanceOf(OkHttpServerProvider.class); + } + + @Test + public void providedHardCoded() { + assertThat(ServerRegistryAccessor.getHardCodedClasses()).contains(OkHttpServerProvider.class); + } + + @Test + public void basicMethods() { + assertThat(provider.isAvailable()).isTrue(); + assertThat(provider.priority()).isEqualTo(4); + } + + @Test + public void builderIsAOkHttpBuilder() { + assertThrows(UnsupportedOperationException.class, () -> provider.builderForPort(80)); + } + + @Test + public void newServerBuilderForPort_success() { + ServerProvider.NewServerBuilderResult result = + provider.newServerBuilderForPort(80, InsecureServerCredentials.create()); + assertThat(result.getServerBuilder()).isInstanceOf(OkHttpServerBuilder.class); + } + + @Test + public void newServerBuilderForPort_fail() { + ServerProvider.NewServerBuilderResult result = provider.newServerBuilderForPort( + 80, new FakeServerCredentials()); + assertThat(result.getError()).contains("FakeServerCredentials"); + } + + private static final class FakeServerCredentials extends ServerCredentials {} +} diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java index 455908816a8e..5ed8514b85bb 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java @@ -60,6 +60,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.List; @@ -70,6 +71,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import okio.Buffer; +import okio.BufferedSink; import okio.BufferedSource; import okio.ByteString; import okio.Okio; @@ -90,15 +92,19 @@ public class OkHttpServerTransportTest { private static final int TIME_OUT_MS = 2000; private static final int INITIAL_WINDOW_SIZE = 65535; private static final long MAX_CONNECTION_IDLE = TimeUnit.SECONDS.toNanos(1); - + private static final byte FLAG_NONE = 0x0; + private static final byte FLAG_PADDED = 0x8; + private static final byte FLAG_END_STREAM = 0x1; + private static final byte TYPE_DATA = 0x0; private MockServerTransportListener mockTransportListener = new MockServerTransportListener(); private ServerTransportListener transportListener = mock(ServerTransportListener.class, delegatesTo(mockTransportListener)); private OkHttpServerTransport serverTransport; private final ExecutorService threadPool = Executors.newCachedThreadPool(); private final SocketPair socketPair = SocketPair.create(threadPool); - private final FrameWriter clientFrameWriter - = new Http2().newWriter(Okio.buffer(Okio.sink(socketPair.getClientOutputStream())), true); + private final BufferedSink clientWriterSink = Okio.buffer( + Okio.sink(socketPair.getClientOutputStream())); + private final FrameWriter clientFrameWriter = new Http2().newWriter(clientWriterSink, true); private final FrameReader clientFrameReader = new Http2().newReader(Okio.buffer(Okio.source(socketPair.getClientInputStream())), true); private final FrameReader.Handler clientFramesRead = mock(FrameReader.Handler.class); @@ -135,7 +141,8 @@ public void setUp() throws Exception { Buffer buf = new Buffer(); buf.write(in.getBuffer(), length); clientDataFrames.data(outDone, streamId, buf); - })).when(clientFramesRead).data(anyBoolean(), anyInt(), any(BufferedSource.class), anyInt()); + })).when(clientFramesRead).data(anyBoolean(), anyInt(), any(BufferedSource.class), anyInt(), + anyInt()); } @After @@ -379,7 +386,8 @@ public void basicRpc_succeeds() throws Exception { Buffer responseMessageFrame = createMessageFrame("Howdy client"); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) - .data(eq(false), eq(1), any(BufferedSource.class), eq((int) responseMessageFrame.size())); + .data(eq(false), eq(1), any(BufferedSource.class), eq((int) responseMessageFrame.size()), + eq((int) responseMessageFrame.size())); verify(clientDataFrames).data(false, 1, responseMessageFrame); List
    responseTrailers = Arrays.asList( @@ -440,7 +448,8 @@ public void activeRpc_delaysShutdownTermination() throws Exception { Buffer responseMessageFrame = createMessageFrame("Howdy client"); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) - .data(eq(false), eq(1), any(BufferedSource.class), eq((int) responseMessageFrame.size())); + .data(eq(false), eq(1), any(BufferedSource.class), eq((int) responseMessageFrame.size()), + eq((int) responseMessageFrame.size())); verify(clientDataFrames).data(false, 1, responseMessageFrame); pingPong(); assertThat(serverTransport.getActiveStreams().length).isEqualTo(1); @@ -975,7 +984,8 @@ public void httpErrorsAdhereToFlowControl() throws Exception { Buffer responseDataFrame = new Buffer().writeUtf8(errorDescription.substring(0, 1)); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead).data( - eq(false), eq(1), any(BufferedSource.class), eq((int) responseDataFrame.size())); + eq(false), eq(1), any(BufferedSource.class), eq((int) responseDataFrame.size()), + eq((int) responseDataFrame.size())); verify(clientDataFrames).data(false, 1, responseDataFrame); clientFrameWriter.windowUpdate(1, 1000); @@ -984,7 +994,8 @@ public void httpErrorsAdhereToFlowControl() throws Exception { responseDataFrame = new Buffer().writeUtf8(errorDescription.substring(1)); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead).data( - eq(true), eq(1), any(BufferedSource.class), eq((int) responseDataFrame.size())); + eq(true), eq(1), any(BufferedSource.class), eq((int) responseDataFrame.size()), + eq((int) responseDataFrame.size())); verify(clientDataFrames).data(true, 1, responseDataFrame); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); @@ -993,6 +1004,71 @@ public void httpErrorsAdhereToFlowControl() throws Exception { shutdownAndTerminate(/*lastStreamId=*/ 1); } + @Test + public void windowUpdate() throws Exception { + serverBuilder.flowControlWindow(100); + initTransport(); + handshake(); + + List
    headers = Arrays.asList( + HTTP_SCHEME_HEADER, + METHOD_HEADER, + new Header(Header.TARGET_AUTHORITY, "example.com:80"), + new Header(Header.TARGET_PATH, "/com.example/SimpleService.doit"), + CONTENT_TYPE_HEADER, + TE_HEADER, + new Header("some-metadata", "this could be anything")); + clientFrameWriter.headers(1, new ArrayList<>(headers)); + clientFrameWriter.headers(3, new ArrayList<>(headers)); + String message = "Hello Server Pad Me!"; // length = 20, add buffer length = 5 + writeDataDirectly(clientWriterSink, FLAG_NONE, 1, message, 0); + pingPong(); + + MockStreamListener streamListener = mockTransportListener.newStreams.pop(); + MockStreamListener streamListener2 = mockTransportListener.newStreams.pop(); + assertThat(streamListener.stream.getAuthority()).isEqualTo("example.com:80"); + assertThat(streamListener.method).isEqualTo("com.example/SimpleService.doit"); + assertThat(streamListener.headers.get( + Metadata.Key.of("Some-Metadata", Metadata.ASCII_STRING_MARSHALLER))) + .isEqualTo("this could be anything"); + streamListener.stream.request(1); + pingPong(); + assertThat(streamListener.messages.pop()).isEqualTo("Hello Server Pad Me!"); + streamListener.stream.writeHeaders(metadata("User-Data", "best data")); + streamListener.stream.writeMessage(new ByteArrayInputStream("Howdy client".getBytes(UTF_8))); + List
    responseHeaders = Arrays.asList( + new Header(":status", "200"), + CONTENT_TYPE_HEADER, + new Header("user-data", "best data")); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead) + .headers(false, false, 1, -1, responseHeaders, HeadersMode.HTTP_20_HEADERS); + + writeDataDirectly(clientWriterSink, FLAG_PADDED, 1, message, 10); + writeDataDirectly(clientWriterSink, FLAG_PADDED | FLAG_END_STREAM, 3, message, 40); + clientFrameWriter.flush(); + + int expectedConsumed = message.length() + 5; + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).windowUpdate(0, expectedConsumed * 2 + 10); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).windowUpdate(0, expectedConsumed + 40); + streamListener.stream.request(2); + streamListener2.stream.request(1); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).windowUpdate(1, expectedConsumed * 2 + 10); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).windowUpdate(3, expectedConsumed + 40); + + writeDataDirectly(clientWriterSink, FLAG_PADDED | FLAG_END_STREAM, 1, message, 100); + clientFrameWriter.flush(); + assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); + verify(clientFramesRead).rstStream(eq(1), eq(ErrorCode.FLOW_CONTROL_ERROR)); + clientFrameWriter.rstStream(3, ErrorCode.CANCEL); + pingPong(); + shutdownAndTerminate(/*lastStreamId=*/ 3); + } + @Test public void dataForStream0_failsWithGoAway() throws Exception { initTransport(); @@ -1223,6 +1299,32 @@ private static Buffer createMessageFrame(String stringMessage) { return buffer; } + private void writeDataDirectly(BufferedSink sink, int flag, int streamId, String message, + int paddingLength) throws IOException { + Buffer buffer = createMessageFrame(message); + int bufferLengthWithPadding = (int) buffer.size(); + if ((flag & FLAG_PADDED) != 0) { + bufferLengthWithPadding += paddingLength; + } + writeLength(sink, bufferLengthWithPadding); + sink.writeByte(TYPE_DATA); + sink.writeByte(flag & 0xff); + sink.writeInt(streamId & 0x7fffffff); + if ((flag & FLAG_PADDED) != 0) { + sink.writeByte((short)(paddingLength - 1)); + char[] value = new char[paddingLength - 1]; + Arrays.fill(value, '!'); + buffer.write(new String(value).getBytes(UTF_8)); + } + sink.write(buffer, buffer.size()); + } + + private void writeLength(BufferedSink sink, int length) throws IOException { + sink.writeByte((length >>> 16 ) & 0xff); + sink.writeByte((length >>> 8 ) & 0xff); + sink.writeByte(length & 0xff); + } + private Metadata metadata(String... keysAndValues) { Metadata metadata = new Metadata(); assertThat(keysAndValues.length % 2).isEqualTo(0); @@ -1279,7 +1381,8 @@ private void verifyHttpError( Buffer responseDataFrame = new Buffer().writeUtf8(errorDescription); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead).data( - eq(true), eq(streamId), any(BufferedSource.class), eq((int) responseDataFrame.size())); + eq(true), eq(streamId), any(BufferedSource.class), + eq((int) responseDataFrame.size()), eq((int) responseDataFrame.size())); verify(clientDataFrames).data(true, streamId, responseDataFrame); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/FrameReader.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/FrameReader.java index 585ccb15355f..490673eff6a1 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/FrameReader.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/FrameReader.java @@ -32,7 +32,7 @@ public interface FrameReader extends Closeable { boolean nextFrame(Handler handler) throws IOException; interface Handler { - void data(boolean inFinished, int streamId, BufferedSource source, int length) + void data(boolean inFinished, int streamId, BufferedSource source, int length, int paddedLength) throws IOException; /** diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java index 3a8c41c62856..0eb49b9f0762 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java @@ -220,7 +220,7 @@ private List
    readHeaderBlock(int length, short padding, byte flags, int return hpackReader.getAndResetHeaderList(); } - private void readData(Handler handler, int length, byte flags, int streamId) + private void readData(Handler handler, int paddedLength, byte flags, int streamId) throws IOException { // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED boolean inFinished = (flags & FLAG_END_STREAM) != 0; @@ -230,10 +230,10 @@ private void readData(Handler handler, int length, byte flags, int streamId) } short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; - length = lengthWithoutPadding(length, flags, padding); + int length = lengthWithoutPadding(paddedLength, flags, padding); // FIXME: pass padding length to handler because it should be included for flow control - handler.data(inFinished, streamId, source, length); + handler.data(inFinished, streamId, source, length, paddedLength); source.skip(padding); } diff --git a/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java b/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java new file mode 100644 index 000000000000..5631a18515dc --- /dev/null +++ b/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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.okhttp.internal.framed; + +import static io.grpc.okhttp.internal.framed.Http2.FLAG_NONE; +import static io.grpc.okhttp.internal.framed.Http2.FLAG_PADDED; +import static io.grpc.okhttp.internal.framed.Http2.TYPE_DATA; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import okio.Buffer; +import okio.BufferedSink; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class Http2Test { + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + private FrameReader http2FrameReader; + @Mock + private FrameReader.Handler mockHandler; + private final int STREAM_ID = 6; + + @Test + public void dataFrameNoPadding() throws IOException { + Buffer bufferIn = createData(FLAG_NONE, 3239, 0 ); + http2FrameReader = new Http2.Reader(bufferIn, 100, true); + http2FrameReader.nextFrame(mockHandler); + + verify(mockHandler).data(eq(false), eq(STREAM_ID), eq(bufferIn), eq(3239), eq(3239)); + assertEquals(3239, bufferIn.size()); + } + + @Test + public void dataFrameOneLengthPadding() throws IOException { + Buffer bufferIn = createData(FLAG_PADDED, 1876, 0); + http2FrameReader = new Http2.Reader(bufferIn, 100, true); + http2FrameReader.nextFrame(mockHandler); + + verify(mockHandler).data(eq(false), eq(STREAM_ID), eq(bufferIn), eq(1875), eq(1876)); + assertEquals(1876, bufferIn.size()); + } + + @Test + public void dataFramePadding() throws IOException { + Buffer bufferIn = createData(FLAG_PADDED, 2037, 125); + http2FrameReader = new Http2.Reader(bufferIn, 100, true); + http2FrameReader.nextFrame(mockHandler); + + verify(mockHandler).data(eq(false), eq(STREAM_ID), eq(bufferIn), eq(2037 - 126), eq(2037)); + assertEquals(2037 - 125, bufferIn.size()); + } + + private Buffer createData(int flag, int length, int paddingLength) throws IOException { + Buffer sink = new Buffer(); + writeLength(sink, length); + sink.writeByte(TYPE_DATA); + sink.writeByte(flag); + sink.writeInt(STREAM_ID); + if ((flag & FLAG_PADDED) != 0) { + sink.writeByte((short)paddingLength); + } + char[] value = new char[length]; + Arrays.fill(value, '!'); + sink.write(new String(value).getBytes(StandardCharsets.UTF_8)); + return sink; + } + + private void writeLength(BufferedSink sink, int length) throws IOException { + sink.writeByte((length >>> 16 ) & 0xff); + sink.writeByte((length >>> 8 ) & 0xff); + sink.writeByte(length & 0xff); + } +} diff --git a/protobuf-lite/build.gradle b/protobuf-lite/build.gradle index 0ada691913c9..11a49d4816da 100644 --- a/protobuf-lite/build.gradle +++ b/protobuf-lite/build.gradle @@ -21,6 +21,12 @@ dependencies { signature libraries.signature.android } +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.protobuf.lite') + } +} + tasks.named("compileTestJava").configure { options.compilerArgs += [ "-Xlint:-cast" diff --git a/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java b/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java index 211ef5366e9e..a0f50cae8dbc 100644 --- a/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java +++ b/protobuf-lite/src/main/java/io/grpc/protobuf/lite/ProtoLiteUtils.java @@ -69,7 +69,6 @@ public final class ProtoLiteUtils { * * @since 1.0.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1787") public static void setExtensionRegistry(ExtensionRegistryLite newRegistry) { globalRegistry = checkNotNull(newRegistry, "newRegistry"); } diff --git a/protobuf/build.gradle b/protobuf/build.gradle index 47d08e1931fd..8220b7da0df1 100644 --- a/protobuf/build.gradle +++ b/protobuf/build.gradle @@ -9,6 +9,12 @@ plugins { description = 'gRPC: Protobuf' +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.protobuf') + } +} + dependencies { api project(':grpc-api'), libraries.jsr305, diff --git a/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java b/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java index ebc708f522f4..eeef6e3b5830 100644 --- a/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java +++ b/protobuf/src/main/java/io/grpc/protobuf/ProtoUtils.java @@ -43,7 +43,6 @@ public final class ProtoUtils { * * @since 1.16.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1787") public static void setExtensionRegistry(ExtensionRegistry registry) { ProtoLiteUtils.setExtensionRegistry(registry); } diff --git a/repositories.bzl b/repositories.bzl index 2b8a580c9159..d9a383a3e893 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -11,34 +11,34 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # ) IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.android:annotations:4.1.1.4", - "com.google.api.grpc:proto-google-common-protos:2.17.0", + "com.google.api.grpc:proto-google-common-protos:2.22.0", "com.google.auth:google-auth-library-credentials:1.4.0", "com.google.auth:google-auth-library-oauth2-http:1.4.0", - "com.google.auto.value:auto-value-annotations:1.10.1", - "com.google.auto.value:auto-value:1.10.1", + "com.google.auto.value:auto-value-annotations:1.10.2", + "com.google.auto.value:auto-value:1.10.2", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.10.1", - "com.google.errorprone:error_prone_annotations:2.18.0", + "com.google.errorprone:error_prone_annotations:2.20.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:32.0.1-android", "com.google.re2j:re2j:1.7", - "com.google.truth:truth:1.0.1", + "com.google.truth:truth:1.1.5", "com.squareup.okhttp:okhttp:2.7.5", - "com.squareup.okio:okio:1.17.5", - "io.netty:netty-buffer:4.1.87.Final", - "io.netty:netty-codec-http2:4.1.87.Final", - "io.netty:netty-codec-http:4.1.87.Final", - "io.netty:netty-codec-socks:4.1.87.Final", - "io.netty:netty-codec:4.1.87.Final", - "io.netty:netty-common:4.1.87.Final", - "io.netty:netty-handler-proxy:4.1.87.Final", - "io.netty:netty-handler:4.1.87.Final", - "io.netty:netty-resolver:4.1.87.Final", + "com.squareup.okio:okio:2.10.0", + "io.netty:netty-buffer:4.1.93.Final", + "io.netty:netty-codec-http2:4.1.93.Final", + "io.netty:netty-codec-http:4.1.93.Final", + "io.netty:netty-codec-socks:4.1.93.Final", + "io.netty:netty-codec:4.1.93.Final", + "io.netty:netty-common:4.1.93.Final", + "io.netty:netty-handler-proxy:4.1.93.Final", + "io.netty:netty-handler:4.1.93.Final", + "io.netty:netty-resolver:4.1.93.Final", "io.netty:netty-tcnative-boringssl-static:2.0.61.Final", "io.netty:netty-tcnative-classes:2.0.61.Final", - "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.87.Final", - "io.netty:netty-transport-native-unix-common:4.1.87.Final", - "io.netty:netty-transport:4.1.87.Final", + "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.93.Final", + "io.netty:netty-transport-native-unix-common:4.1.93.Final", + "io.netty:netty-transport:4.1.93.Final", "io.opencensus:opencensus-api:0.31.0", "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.26.0", @@ -69,6 +69,7 @@ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = { "io.grpc:grpc-core": "@io_grpc_grpc_java//core:core_maven", "io.grpc:grpc-googleapis": "@io_grpc_grpc_java//googleapis", "io.grpc:grpc-grpclb": "@io_grpc_grpc_java//grpclb", + "io.grpc:grpc-inprocess": "@io_grpc_grpc_java//inprocess", "io.grpc:grpc-netty": "@io_grpc_grpc_java//netty", "io.grpc:grpc-netty-shaded": "@io_grpc_grpc_java//netty:shaded_maven", "io.grpc:grpc-okhttp": "@io_grpc_grpc_java//okhttp", @@ -79,6 +80,7 @@ IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS = { "io.grpc:grpc-stub": "@io_grpc_grpc_java//stub", "io.grpc:grpc-testing": "@io_grpc_grpc_java//testing", "io.grpc:grpc-xds": "@io_grpc_grpc_java//xds:xds_maven", + "io.grpc:grpc-util": "@io_grpc_grpc_java//util", } def grpc_java_repositories(): @@ -141,18 +143,18 @@ def com_google_protobuf(): # This statement defines the @com_google_protobuf repo. http_archive( name = "com_google_protobuf", - sha256 = "5d0f05587aa3ad56079b4c4481dcb462267e5f1075d905c321f8ed6339e74ab0", - strip_prefix = "protobuf-22.3", - urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v22.3/protobuf-22.3.zip"], + sha256 = "5980276108f948e1ada091475549a8c75dc83c193129aab0e986ceaac3e97131", + strip_prefix = "protobuf-24.0", + urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v24.0/protobuf-24.0.zip"], ) def com_google_protobuf_javalite(): # java_lite_proto_library rules implicitly depend on @com_google_protobuf_javalite http_archive( name = "com_google_protobuf_javalite", - sha256 = "5d0f05587aa3ad56079b4c4481dcb462267e5f1075d905c321f8ed6339e74ab0", - strip_prefix = "protobuf-22.3", - urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v22.3/protobuf-22.3.zip"], + sha256 = "5980276108f948e1ada091475549a8c75dc83c193129aab0e986ceaac3e97131", + strip_prefix = "protobuf-24.0", + urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v24.0/protobuf-24.0.zip"], ) def io_grpc_grpc_proto(): diff --git a/rls/BUILD.bazel b/rls/BUILD.bazel index d4af35692409..c67c7cd56bea 100644 --- a/rls/BUILD.bazel +++ b/rls/BUILD.bazel @@ -12,7 +12,7 @@ java_library( "//api", "//core", "//core:internal", - "//core:util", + "//util", "//stub", "@com_google_auto_value_auto_value_annotations//jar", "@com_google_code_findbugs_jsr305//jar", diff --git a/rls/build.gradle b/rls/build.gradle index 453052addf39..b0abb08b4f3c 100644 --- a/rls/build.gradle +++ b/rls/build.gradle @@ -1,5 +1,5 @@ plugins { - id "java" + id "java-library" id "maven-publish" id "com.google.protobuf" id "jacoco" @@ -8,8 +8,14 @@ plugins { description = "gRPC: RouteLookupService Loadbalancing plugin" +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.rls') + } +} + dependencies { - implementation project(':grpc-core'), + implementation project(':grpc-util'), project(':grpc-protobuf'), project(':grpc-stub'), libraries.auto.value.annotations, @@ -39,6 +45,7 @@ tasks.named("javadoc").configure { // Do not publish javadoc since currently there is no public API. failOnError false // no public or protected classes found to document exclude 'io/grpc/lookup/v1/**' + exclude 'io/grpc/rls/*Provider.java' exclude 'io/grpc/rls/internal/**' exclude 'io/grpc/rls/Internal*' } diff --git a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java index 8baba0bb8246..5a4a2dab4525 100644 --- a/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java +++ b/rls/src/main/java/io/grpc/rls/LinkedHashLruCache.java @@ -179,7 +179,6 @@ private SizedValue readInternal(K key) { synchronized (lock) { SizedValue existing = delegate.get(key); if (existing != null && isExpired(key, existing.value, ticker.read())) { - invalidate(key, EvictionType.EXPIRED); return null; } return existing; diff --git a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java index 120a486dec69..51b934e13b76 100644 --- a/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java +++ b/rls/src/test/java/io/grpc/rls/CachingRlsLbClientTest.java @@ -354,12 +354,7 @@ public void get_updatesLbState() throws Exception { assertThat(stateCaptor.getAllValues()) .containsExactly(ConnectivityState.CONNECTING, ConnectivityState.READY); Metadata headers = new Metadata(); - PickResult pickResult = pickerCaptor.getValue().pickSubchannel( - new PickSubchannelArgsImpl( - TestMethodDescriptors.voidMethod().toBuilder().setFullMethodName("service1/create") - .build(), - headers, - CallOptions.DEFAULT)); + PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); assertThat(pickResult.getStatus().isOk()).isTrue(); assertThat(pickResult.getSubchannel()).isNotNull(); assertThat(headers.get(RLS_DATA_KEY)).isEqualTo("header-rls-data-value"); @@ -395,6 +390,57 @@ public void get_updatesLbState() throws Exception { assertThat(fakeThrottler.getNumUnthrottled()).isEqualTo(1); } + @Test + public void timeout_not_changing_picked_subchannel() throws Exception { + setUpRlsLbClient(); + RouteLookupRequest routeLookupRequest = RouteLookupRequest.create(ImmutableMap.of( + "server", "bigtable.googleapis.com", "service-key", "service1", "method-key", "create")); + rlsServerImpl.setLookupTable( + ImmutableMap.of( + routeLookupRequest, + RouteLookupResponse.create( + ImmutableList.of("primary.cloudbigtable.googleapis.com", "target2", "target3"), + "header-rls-data-value"))); + + // valid channel + CachedRouteLookupResponse resp = getInSyncContext(routeLookupRequest); + assertThat(resp.hasData()).isFalse(); + fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + + resp = getInSyncContext(routeLookupRequest); + assertThat(resp.hasData()).isTrue(); + + ArgumentCaptor pickerCaptor = ArgumentCaptor.forClass(SubchannelPicker.class); + ArgumentCaptor stateCaptor = + ArgumentCaptor.forClass(ConnectivityState.class); + verify(helper, times(4)).updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); + + Metadata headers = new Metadata(); + PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); + assertThat(pickResult.getStatus().isOk()).isTrue(); + assertThat(pickResult.getSubchannel().toString()) + .isEqualTo("primary.cloudbigtable.googleapis.com"); + + fakeClock.forwardTime(5, TimeUnit.MINUTES); + PickResult pickResult2 = getPickResultForCreate(pickerCaptor, headers); + assertThat(pickResult2.getSubchannel()).isNull(); + fakeClock.forwardTime(SERVER_LATENCY_MILLIS, TimeUnit.MILLISECONDS); + PickResult pickResult3 = getPickResultForCreate(pickerCaptor, headers); + assertThat(pickResult3.getSubchannel()).isNotNull(); + assertThat(pickResult3.getSubchannel().toString()) + .isEqualTo(pickResult.getSubchannel().toString()); + } + + private static PickResult getPickResultForCreate(ArgumentCaptor pickerCaptor, + Metadata headers) { + return pickerCaptor.getValue().pickSubchannel( + new PickSubchannelArgsImpl( + TestMethodDescriptors.voidMethod().toBuilder().setFullMethodName("service1/create") + .build(), + headers, + CallOptions.DEFAULT)); + } + @Test public void get_withAdaptiveThrottler() throws Exception { AdaptiveThrottler adaptiveThrottler = @@ -440,12 +486,7 @@ public void get_withAdaptiveThrottler() throws Exception { .updateBalancingState(stateCaptor.capture(), pickerCaptor.capture()); Metadata headers = new Metadata(); - PickResult pickResult = pickerCaptor.getValue().pickSubchannel( - new PickSubchannelArgsImpl( - TestMethodDescriptors.voidMethod().toBuilder().setFullMethodName("service1/create") - .build(), - headers, - CallOptions.DEFAULT)); + PickResult pickResult = getPickResultForCreate(pickerCaptor, headers); assertThat(pickResult.getSubchannel()).isNotNull(); assertThat(headers.get(RLS_DATA_KEY)).isEqualTo("header-rls-data-value"); @@ -680,7 +721,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { new SubchannelPicker() { @Override public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withSubchannel(mock(Subchannel.class)); + return PickResult.withSubchannel( + mock(Subchannel.class, config.get("target").toString())); } }); } diff --git a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java index 19b3a012151d..a31f58f5365b 100644 --- a/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java +++ b/rls/src/test/java/io/grpc/rls/LinkedHashLruCacheTest.java @@ -150,13 +150,16 @@ public void eviction_size_shouldEvictAlreadyExpired() { } @Test - public void eviction_get_shouldNotReturnAlreadyExpired() { + public void eviction_cleanupShouldRemoveAlreadyExpired() { for (int i = 1; i <= MAX_SIZE; i++) { // last entry is already expired when added - cache.cache(i, new Entry("Entry" + i, ticker.read() + MAX_SIZE - i)); + cache.cache(i, new Entry("Entry" + i, + ticker.read() + ((MAX_SIZE - i) * TimeUnit.MINUTES.toNanos(1)) + 1)); } assertThat(cache.estimatedSize()).isEqualTo(MAX_SIZE); + + fakeClock.forwardTime(1, TimeUnit.MINUTES); assertThat(cache.read(MAX_SIZE)).isNull(); assertThat(cache.estimatedSize()).isEqualTo(MAX_SIZE - 1); verify(evictionListener).onEviction(eq(MAX_SIZE), any(Entry.class), eq(EvictionType.EXPIRED)); diff --git a/services/BUILD.bazel b/services/BUILD.bazel index bf12df6eeb1b..f4ec14cc255b 100644 --- a/services/BUILD.bazel +++ b/services/BUILD.bazel @@ -154,7 +154,7 @@ java_library( ":_health_java_grpc", "//api", "//core:internal", - "//core:util", + "//util", "@com_google_code_findbugs_jsr305//jar", "@com_google_guava_guava//jar", "@io_grpc_grpc_proto//:health_java_proto", diff --git a/services/build.gradle b/services/build.gradle index b834fcd2d795..3135d9095ea6 100644 --- a/services/build.gradle +++ b/services/build.gradle @@ -16,16 +16,22 @@ tasks.named("compileJava").configure { ] } +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.services') + } +} + dependencies { api project(':grpc-protobuf'), project(':grpc-stub'), - project(':grpc-core') + project(':grpc-util') implementation libraries.protobuf.java.util, libraries.guava.jre // JRE required by protobuf-java-util runtimeOnly libraries.errorprone.annotations, - libraries.j2objc.annotations // Explicit dependency to keep in step with version used by guava - + libraries.j2objc.annotations, // Explicit dependency to keep in step with version used by guava + libraries.gson // to fix checkUpperBoundDeps error here compileOnly libraries.javax.annotation testImplementation project(':grpc-testing'), libraries.netty.transport.epoll, // for DomainSocketAddress @@ -39,6 +45,7 @@ configureProtoCompilation() tasks.named("javadoc").configure { exclude 'io/grpc/services/Internal*.java' exclude 'io/grpc/services/internal/*' + exclude 'io/grpc/protobuf/services/BinaryLogProvider.java' exclude 'io/grpc/protobuf/services/internal/*' } diff --git a/services/src/main/java/io/grpc/services/CallMetricRecorder.java b/services/src/main/java/io/grpc/services/CallMetricRecorder.java index f491fb7542e8..d480f0f4c3b4 100644 --- a/services/src/main/java/io/grpc/services/CallMetricRecorder.java +++ b/services/src/main/java/io/grpc/services/CallMetricRecorder.java @@ -41,6 +41,8 @@ public final class CallMetricRecorder { new AtomicReference<>(); private final AtomicReference> requestCostMetrics = new AtomicReference<>(); + private final AtomicReference> namedMetrics = + new AtomicReference<>(); private double cpuUtilizationMetric = 0; private double applicationUtilizationMetric = 0; private double memoryUtilizationMetric = 0; @@ -127,6 +129,27 @@ public CallMetricRecorder recordRequestCostMetric(String name, double value) { return this; } + /** + * Records an application-specific opaque custom metric measurement. If RPC has already finished, + * this method is no-op. + * + *

    A latter record will overwrite its former name-sakes. + * + * @return this recorder object + */ + public CallMetricRecorder recordNamedMetric(String name, double value) { + if (disabled) { + return this; + } + if (namedMetrics.get() == null) { + // The chance of race of creation of the map should be very small, so it should be fine + // to create these maps that might be discarded. + namedMetrics.compareAndSet(null, new ConcurrentHashMap()); + } + namedMetrics.get().put(name, value); + return this; + } + /** * Records a call metric measurement for CPU utilization in the range [0, inf). Values outside the * valid range are ignored. If RPC has already finished, this method is no-op. @@ -235,12 +258,17 @@ Map finalizeAndDump() { MetricReport finalizeAndDump2() { Map savedRequestCostMetrics = finalizeAndDump(); Map savedUtilizationMetrics = utilizationMetrics.get(); + Map savedNamedMetrics = namedMetrics.get(); if (savedUtilizationMetrics == null) { savedUtilizationMetrics = Collections.emptyMap(); } + if (savedNamedMetrics == null) { + savedNamedMetrics = Collections.emptyMap(); + } return new MetricReport(cpuUtilizationMetric, applicationUtilizationMetric, memoryUtilizationMetric, qps, eps, Collections.unmodifiableMap(savedRequestCostMetrics), - Collections.unmodifiableMap(savedUtilizationMetrics) + Collections.unmodifiableMap(savedUtilizationMetrics), + Collections.unmodifiableMap(savedNamedMetrics) ); } diff --git a/services/src/main/java/io/grpc/services/InternalCallMetricRecorder.java b/services/src/main/java/io/grpc/services/InternalCallMetricRecorder.java index 3b0cbbbda35c..a7ff1e5c32e8 100644 --- a/services/src/main/java/io/grpc/services/InternalCallMetricRecorder.java +++ b/services/src/main/java/io/grpc/services/InternalCallMetricRecorder.java @@ -47,8 +47,9 @@ public static MetricReport finalizeAndDump2(CallMetricRecorder recorder) { public static MetricReport createMetricReport(double cpuUtilization, double applicationUtilization, double memoryUtilization, double qps, double eps, - Map requestCostMetrics, Map utilizationMetrics) { + Map requestCostMetrics, Map utilizationMetrics, + Map namedMetrics) { return new MetricReport(cpuUtilization, applicationUtilization, memoryUtilization, qps, eps, - requestCostMetrics, utilizationMetrics); + requestCostMetrics, utilizationMetrics, namedMetrics); } } diff --git a/services/src/main/java/io/grpc/services/MetricRecorder.java b/services/src/main/java/io/grpc/services/MetricRecorder.java index c585e7b54077..39b0e8df25f2 100644 --- a/services/src/main/java/io/grpc/services/MetricRecorder.java +++ b/services/src/main/java/io/grpc/services/MetricRecorder.java @@ -155,6 +155,6 @@ public void clearEpsMetric() { MetricReport getMetricReport() { return new MetricReport(cpuUtilization, applicationUtilization, memoryUtilization, qps, eps, - Collections.emptyMap(), Collections.unmodifiableMap(metricsData)); + Collections.emptyMap(), Collections.unmodifiableMap(metricsData), Collections.emptyMap()); } } diff --git a/services/src/main/java/io/grpc/services/MetricReport.java b/services/src/main/java/io/grpc/services/MetricReport.java index 35cbfc056bf5..e559f0f00b51 100644 --- a/services/src/main/java/io/grpc/services/MetricReport.java +++ b/services/src/main/java/io/grpc/services/MetricReport.java @@ -35,10 +35,11 @@ public final class MetricReport { private double eps; private Map requestCostMetrics; private Map utilizationMetrics; + private Map namedMetrics; MetricReport(double cpuUtilization, double applicationUtilization, double memoryUtilization, double qps, double eps, Map requestCostMetrics, - Map utilizationMetrics) { + Map utilizationMetrics, Map namedMetrics) { this.cpuUtilization = cpuUtilization; this.applicationUtilization = applicationUtilization; this.memoryUtilization = memoryUtilization; @@ -46,6 +47,7 @@ public final class MetricReport { this.eps = eps; this.requestCostMetrics = checkNotNull(requestCostMetrics, "requestCostMetrics"); this.utilizationMetrics = checkNotNull(utilizationMetrics, "utilizationMetrics"); + this.namedMetrics = checkNotNull(namedMetrics, "namedMetrics"); } public double getCpuUtilization() { @@ -68,6 +70,10 @@ public Map getUtilizationMetrics() { return utilizationMetrics; } + public Map getNamedMetrics() { + return namedMetrics; + } + public double getQps() { return qps; } @@ -84,6 +90,7 @@ public String toString() { .add("memoryUtilization", memoryUtilization) .add("requestCost", requestCostMetrics) .add("utilization", utilizationMetrics) + .add("named", namedMetrics) .add("qps", qps) .add("eps", eps) .toString(); diff --git a/services/src/test/java/io/grpc/services/CallMetricRecorderTest.java b/services/src/test/java/io/grpc/services/CallMetricRecorderTest.java index cb0bfc5d83fc..f60446b1a3a1 100644 --- a/services/src/test/java/io/grpc/services/CallMetricRecorderTest.java +++ b/services/src/test/java/io/grpc/services/CallMetricRecorderTest.java @@ -44,6 +44,9 @@ public void dumpDumpsAllSavedMetricValues() { recorder.recordRequestCostMetric("cost1", 37465.12); recorder.recordRequestCostMetric("cost2", 10293.0); recorder.recordRequestCostMetric("cost3", 1.0); + recorder.recordNamedMetric("named1", 0.2233); + recorder.recordNamedMetric("named2", -1.618); + recorder.recordNamedMetric("named3", 3.1415926535); recorder.recordCpuUtilizationMetric(0.1928); recorder.recordApplicationUtilizationMetric(0.9987); recorder.recordMemoryUtilizationMetric(0.474); @@ -55,6 +58,8 @@ public void dumpDumpsAllSavedMetricValues() { .containsExactly("util1", 0.154353423, "util2", 0.1367, "util3", 0.143734); Truth.assertThat(dump.getRequestCostMetrics()) .containsExactly("cost1", 37465.12, "cost2", 10293.0, "cost3", 1.0); + Truth.assertThat(dump.getNamedMetrics()) + .containsExactly("named1", 0.2233, "named2", -1.618, "named3", 3.1415926535); Truth.assertThat(dump.getCpuUtilization()).isEqualTo(0.1928); Truth.assertThat(dump.getApplicationUtilization()).isEqualTo(0.9987); Truth.assertThat(dump.getMemoryUtilization()).isEqualTo(0.474); @@ -62,6 +67,9 @@ public void dumpDumpsAllSavedMetricValues() { Truth.assertThat(dump.getEps()).isEqualTo(1.618); Truth.assertThat(dump.toString()).contains("eps=1.618"); Truth.assertThat(dump.toString()).contains("applicationUtilization=0.9987"); + Truth.assertThat(dump.toString()).contains("named1=0.2233"); + Truth.assertThat(dump.toString()).contains("named2=-1.618"); + Truth.assertThat(dump.toString()).contains("named3=3.1415926535"); } @Test @@ -71,6 +79,7 @@ public void noMetricsRecordedAfterSnapshot() { recorder.recordUtilizationMetric("cost", 0.154353423); recorder.recordQpsMetric(3.14159); recorder.recordEpsMetric(1.618); + recorder.recordNamedMetric("named1", 2.718); assertThat(recorder.finalizeAndDump()).isEqualTo(initDump); } @@ -121,6 +130,9 @@ public void lastValueWinForMetricsWithSameName() { recorder.recordUtilizationMetric("util1", 0.2837421); recorder.recordMemoryUtilizationMetric(0.93840); recorder.recordUtilizationMetric("util1", 0.843233); + recorder.recordNamedMetric("named1", 0.2233); + recorder.recordNamedMetric("named2", 2.718); + recorder.recordNamedMetric("named1", 3.1415926535); recorder.recordQpsMetric(1928.3); recorder.recordQpsMetric(100.8); recorder.recordEpsMetric(3.14159); @@ -133,6 +145,8 @@ public void lastValueWinForMetricsWithSameName() { Truth.assertThat(dump.getMemoryUtilization()).isEqualTo(0.93840); Truth.assertThat(dump.getUtilizationMetrics()) .containsExactly("util1", 0.843233); + Truth.assertThat(dump.getNamedMetrics()) + .containsExactly("named1", 3.1415926535, "named2", 2.718); Truth.assertThat(dump.getCpuUtilization()).isEqualTo(0); Truth.assertThat(dump.getQps()).isEqualTo(100.8); Truth.assertThat(dump.getEps()).isEqualTo(1.618); diff --git a/servlet/build.gradle b/servlet/build.gradle index f5ef32ae11e1..2b87134ca987 100644 --- a/servlet/build.gradle +++ b/servlet/build.gradle @@ -5,19 +5,8 @@ plugins { description = "gRPC: Servlet" -// javax.servlet-api 4.0 requires a minimum of Java 8, so we might as well use that source level -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - def jettyVersion = '10.0.7' -configurations { - itImplementation.extendsFrom(implementation) - undertowTestImplementation.extendsFrom(itImplementation) - tomcatTestImplementation.extendsFrom(itImplementation) - jettyTestImplementation.extendsFrom(itImplementation) -} - sourceSets { // Create a test sourceset for each classpath - could be simplified if we made new test directories undertowTest {} @@ -29,12 +18,25 @@ sourceSets { } } +configurations { + itImplementation.extendsFrom(implementation) + undertowTestImplementation.extendsFrom(itImplementation) + tomcatTestImplementation.extendsFrom(itImplementation) + jettyTestImplementation.extendsFrom(itImplementation) +} + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.servlet') + } +} + dependencies { api project(':grpc-api') compileOnly 'javax.servlet:javax.servlet-api:4.0.1', libraries.javax.annotation // java 9, 10 needs it - implementation project(':grpc-core'), + implementation project(':grpc-util'), libraries.guava testImplementation 'javax.servlet:javax.servlet-api:4.0.1', @@ -42,7 +44,7 @@ dependencies { itImplementation project(':grpc-servlet'), project(':grpc-netty'), - project(':grpc-core').sourceSets.test.runtimeClasspath, + testFixtures(project(':grpc-core')), libraries.junit itImplementation(project(':grpc-interop-testing')) { // Avoid grpc-netty-shaded dependency @@ -56,11 +58,13 @@ dependencies { jettyTestImplementation "org.eclipse.jetty:jetty-servlet:${jettyVersion}", "org.eclipse.jetty.http2:http2-server:${jettyVersion}", - "org.eclipse.jetty:jetty-client:${jettyVersion}" - project(':grpc-testing') + "org.eclipse.jetty:jetty-client:${jettyVersion}", + project(':grpc-testing'), + libraries.truth, + libraries.protobuf.java } -test { +tasks.named("test").configure { if (JavaVersion.current().isJava9Compatible()) { jvmArgs += [ // required for Lincheck @@ -72,11 +76,11 @@ test { // Set up individual classpaths for each test, to avoid any mismatch, // and ensure they are only used when supported by the current jvm -check.dependsOn(tasks.register('undertowTest', Test) { +def undertowTest = tasks.register('undertowTest', Test) { classpath = sourceSets.undertowTest.runtimeClasspath testClassesDirs = sourceSets.undertowTest.output.classesDirs -}) -check.dependsOn(tasks.register('tomcat9Test', Test) { +} +def tomcat9Test = tasks.register('tomcat9Test', Test) { classpath = sourceSets.tomcatTest.runtimeClasspath testClassesDirs = sourceSets.tomcatTest.output.classesDirs @@ -93,19 +97,30 @@ check.dependsOn(tasks.register('tomcat9Test', Test) { if (JavaVersion.current().isJava9Compatible()) { jvmArgs += ['--add-opens=java.base/java.io=ALL-UNNAMED', '--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED'] } -}) +} + +tasks.named("check").configure { + dependsOn undertowTest, tomcat9Test +} + +tasks.named("jacocoTestReport").configure { + // Must use executionData(Task...) override. The executionData(Object...) override doesn't find + // execution data correctly for tasks. + executionData undertowTest.get(), tomcat9Test.get() +} // Only run these tests if java 11+ is being used if (JavaVersion.current().isJava11Compatible()) { - check.dependsOn(tasks.register('jettyTest', Test) { + def jettyTest = tasks.register('jettyTest', Test) { classpath = sourceSets.jettyTest.runtimeClasspath testClassesDirs = sourceSets.jettyTest.output.classesDirs - }) -} - -jacocoTestReport { - executionData undertowTest, tomcat9Test - if (JavaVersion.current().isJava11Compatible()) { - executionData jettyTest + } + tasks.named("check").configure { + dependsOn jettyTest + } + tasks.named("jacocoTestReport").configure { + // Must use executionData(Task...) override. The executionData(Object...) override doesn't + // find execution data correctly for tasks. + executionData jettyTest.get() } } diff --git a/servlet/jakarta/build.gradle b/servlet/jakarta/build.gradle index 59f5ac78d804..82f11938f832 100644 --- a/servlet/jakarta/build.gradle +++ b/servlet/jakarta/build.gradle @@ -4,17 +4,8 @@ plugins { } description = "gRPC: Jakarta Servlet" -sourceCompatibility = 1.8 -targetCompatibility = 1.8 // Set up classpaths and source directories for different servlet tests -configurations { - itImplementation.extendsFrom(implementation) - jettyTestImplementation.extendsFrom(itImplementation) - tomcatTestImplementation.extendsFrom(itImplementation) - undertowTestImplementation.extendsFrom(itImplementation) -} - sourceSets { undertowTest { java { @@ -36,50 +27,65 @@ sourceSets { } } +configurations { + itImplementation.extendsFrom(implementation) + jettyTestImplementation.extendsFrom(itImplementation) + tomcatTestImplementation.extendsFrom(itImplementation) + undertowTestImplementation.extendsFrom(itImplementation) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + // Mechanically transform sources from grpc-servlet to use the corrected packages def migrate(String name, String inputDir, SourceSet sourceSet) { def outputDir = layout.buildDirectory.dir('generated/sources/jakarta-' + name) - sourceSet.java.srcDir outputDir - return tasks.register('migrateSources' + name.capitalize(), Sync) { task -> + sourceSet.java.srcDir tasks.register('migrateSources' + name.capitalize(), Sync) { task -> into(outputDir) from("$inputDir/io/grpc/servlet") { into('io/grpc/servlet/jakarta') filter { String line -> - line.replaceAll('javax\\.servlet', 'jakarta.servlet') - .replaceAll('io\\.grpc\\.servlet', 'io.grpc.servlet.jakarta') + line.replace('javax.servlet', 'jakarta.servlet') + .replace('io.grpc.servlet', 'io.grpc.servlet.jakarta') } } } } -compileJava.dependsOn migrate('main', '../src/main/java', sourceSets.main) - -sourcesJar.dependsOn migrateSourcesMain +migrate('main', '../src/main/java', sourceSets.main) // Build the set of sourceSets and classpaths to modify, since Jetty 11 requires Java 11 // and must be skipped -compileUndertowTestJava.dependsOn(migrate('undertowTest', '../src/undertowTest/java', sourceSets.undertowTest)) -compileTomcatTestJava.dependsOn(migrate('tomcatTest', '../src/tomcatTest/java', sourceSets.tomcatTest)) +migrate('undertowTest', '../src/undertowTest/java', sourceSets.undertowTest) +migrate('tomcatTest', '../src/tomcatTest/java', sourceSets.tomcatTest) if (JavaVersion.current().isJava11Compatible()) { - compileJettyTestJava.dependsOn(migrate('jettyTest', '../src/jettyTest/java', sourceSets.jettyTest)) + migrate('jettyTest', '../src/jettyTest/java', sourceSets.jettyTest) } // Disable checkstyle for this project, since it consists only of generated code -tasks.withType(Checkstyle) { +tasks.withType(Checkstyle).configureEach { enabled = false } +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.servlet.jakarta') + } +} + dependencies { api project(':grpc-api') compileOnly 'jakarta.servlet:jakarta.servlet-api:5.0.0', libraries.javax.annotation - implementation project(':grpc-core'), + implementation project(':grpc-util'), libraries.guava itImplementation project(':grpc-servlet-jakarta'), project(':grpc-netty'), - project(':grpc-core').sourceSets.test.runtimeClasspath, + testFixtures(project(':grpc-core')), libraries.junit itImplementation(project(':grpc-interop-testing')) { // Avoid grpc-netty-shaded dependency @@ -97,11 +103,11 @@ dependencies { // Set up individual classpaths for each test, to avoid any mismatch, // and ensure they are only used when supported by the current jvm -check.dependsOn(tasks.register('undertowTest', Test) { +def undertowTest = tasks.register('undertowTest', Test) { classpath = sourceSets.undertowTest.runtimeClasspath testClassesDirs = sourceSets.undertowTest.output.classesDirs -}) -check.dependsOn(tasks.register('tomcat10Test', Test) { +} +def tomcat10Test = tasks.register('tomcat10Test', Test) { classpath = sourceSets.tomcatTest.runtimeClasspath testClassesDirs = sourceSets.tomcatTest.output.classesDirs @@ -118,11 +124,20 @@ check.dependsOn(tasks.register('tomcat10Test', Test) { if (JavaVersion.current().isJava9Compatible()) { jvmArgs += ['--add-opens=java.base/java.io=ALL-UNNAMED', '--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED'] } -}) +} + +tasks.named("check").configure { + dependsOn undertowTest, tomcat10Test +} + // Only run these tests if java 11+ is being used if (JavaVersion.current().isJava11Compatible()) { - check.dependsOn(tasks.register('jetty11Test', Test) { + def jetty11Test = tasks.register('jetty11Test', Test) { classpath = sourceSets.jettyTest.runtimeClasspath testClassesDirs = sourceSets.jettyTest.output.classesDirs - }) + } + + tasks.named("check").configure { + dependsOn jetty11Test + } } diff --git a/settings.gradle b/settings.gradle index 2d2c65c89701..8e4e9a0a9287 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,17 +1,26 @@ pluginManagement { plugins { + // https://developer.android.com/build/releases/gradle-plugin id "com.android.application" version "7.4.0" id "com.android.library" version "7.4.0" - id "com.github.johnrengelman.shadow" version "7.1.2" + // https://github.com/johnrengelman/shadow/releases + id "com.github.johnrengelman.shadow" version "8.1.1" id "com.github.kt3k.coveralls" version "2.12.2" - id "com.google.cloud.tools.appengine" version "2.4.4" - id "com.google.cloud.tools.jib" version "3.3.1" + // https://github.com/GoogleCloudPlatform/app-gradle-plugin/releases + id "com.google.cloud.tools.appengine" version "2.4.5" + // https://github.com/GoogleContainerTools/jib/blob/master/jib-gradle-plugin/CHANGELOG.md + id "com.google.cloud.tools.jib" version "3.3.2" id "com.google.osdetector" version "1.7.3" - id "com.google.protobuf" version "0.9.3" + // https://github.com/google/protobuf-gradle-plugin/releases + id "com.google.protobuf" version "0.9.4" + // https://github.com/melix/japicmp-gradle-plugin/blob/master/CHANGELOG.txt id "me.champeau.gradle.japicmp" version "0.4.1" + // https://github.com/melix/jmh-gradle-plugin/releases id "me.champeau.jmh" version "0.7.1" + // https://github.com/tbroyer/gradle-errorprone-plugin/releases id "net.ltgt.errorprone" version "3.1.0" - id "ru.vyarus.animalsniffer" version "1.7.0" + // https://github.com/xvik/gradle-animalsniffer-plugin/releases + id "ru.vyarus.animalsniffer" version "1.7.1" } resolutionStrategy { eachPlugin { @@ -60,6 +69,8 @@ include ":grpc-authz" include ":grpc-gcp-observability" include ":grpc-gcp-observability:interop" include ":grpc-istio-interop-testing" +include ":grpc-inprocess" +include ":grpc-util" project(':grpc-api').projectDir = "$rootDir/api" as File project(':grpc-core').projectDir = "$rootDir/core" as File @@ -91,6 +102,8 @@ project(':grpc-authz').projectDir = "$rootDir/authz" as File project(':grpc-gcp-observability').projectDir = "$rootDir/gcp-observability" as File project(':grpc-gcp-observability:interop').projectDir = "$rootDir/gcp-observability/interop" as File project(':grpc-istio-interop-testing').projectDir = "$rootDir/istio-interop-testing" as File +project(':grpc-inprocess').projectDir = "$rootDir/inprocess" as File +project(':grpc-util').projectDir = "$rootDir/util" 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 16a9ca2d995e..971e476fb33e 100644 --- a/stub/build.gradle +++ b/stub/build.gradle @@ -7,6 +7,13 @@ plugins { } description = "gRPC: Stub" + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.stub') + } +} + dependencies { api project(':grpc-api'), libraries.guava diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index af1d43a47e7a..0eb599d784b5 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -136,9 +136,7 @@ public static StreamObserver asyncBidiStreamingCall( public static RespT blockingUnaryCall(ClientCall call, ReqT req) { try { return getUnchecked(futureUnaryCall(call, req)); - } catch (RuntimeException e) { - throw cancelThrow(call, e); - } catch (Error e) { + } catch (RuntimeException | Error e) { throw cancelThrow(call, e); } } @@ -170,10 +168,7 @@ public static RespT blockingUnaryCall( } executor.shutdown(); return getUnchecked(responseFuture); - } catch (RuntimeException e) { - // Something very bad happened. All bets are off; it may be dangerous to wait for onClose(). - throw cancelThrow(call, e); - } catch (Error e) { + } catch (RuntimeException | Error e) { // Something very bad happened. All bets are off; it may be dangerous to wait for onClose(). throw cancelThrow(call, e); } finally { @@ -212,11 +207,10 @@ public static Iterator blockingServerStreamingCall( */ public static Iterator blockingServerStreamingCall( Channel channel, MethodDescriptor method, CallOptions callOptions, ReqT req) { - ThreadlessExecutor executor = new ThreadlessExecutor(); ClientCall call = channel.newCall(method, - callOptions.withOption(ClientCalls.STUB_TYPE_OPTION, StubType.BLOCKING) - .withExecutor(executor)); - BlockingResponseStream result = new BlockingResponseStream<>(call, executor); + callOptions.withOption(ClientCalls.STUB_TYPE_OPTION, StubType.BLOCKING)); + + BlockingResponseStream result = new BlockingResponseStream<>(call); asyncUnaryRequestCall(call, req, result.listener()); return result; } @@ -361,8 +355,7 @@ private static StatusRuntimeException toStatusRuntimeException(Throwable t) { private static RuntimeException cancelThrow(ClientCall call, Throwable t) { try { call.cancel(null, t); - } catch (Throwable e) { - assert e instanceof RuntimeException || e instanceof Error; + } catch (RuntimeException | Error e) { logger.log(Level.SEVERE, "RuntimeException encountered while closing call", e); } if (t instanceof RuntimeException) { @@ -393,9 +386,7 @@ private static void asyncUnaryRequestCall( try { call.sendMessage(req); call.halfClose(); - } catch (RuntimeException e) { - throw cancelThrow(call, e); - } catch (Error e) { + } catch (RuntimeException | Error e) { throw cancelThrow(call, e); } } @@ -670,20 +661,12 @@ private static final class BlockingResponseStream implements Iterator { private final BlockingQueue buffer = new ArrayBlockingQueue<>(3); private final StartableListener listener = new QueuingListener(); private final ClientCall call; - /** May be null. */ - private final ThreadlessExecutor threadless; // Only accessed when iterating. private Object last; // Non private to avoid synthetic class BlockingResponseStream(ClientCall call) { - this(call, null); - } - - // Non private to avoid synthetic class - BlockingResponseStream(ClientCall call, ThreadlessExecutor threadless) { this.call = call; - this.threadless = threadless; } StartableListener listener() { @@ -693,31 +676,14 @@ StartableListener listener() { private Object waitForNext() { boolean interrupt = false; try { - if (threadless == null) { - while (true) { - try { - return buffer.take(); - } catch (InterruptedException ie) { - interrupt = true; - call.cancel("Thread interrupted", ie); - // Now wait for onClose() to be called, to guarantee BlockingQueue doesn't fill - } - } - } else { - Object next; - while ((next = buffer.poll()) == null) { - try { - threadless.waitAndDrain(); - } catch (InterruptedException ie) { - interrupt = true; - call.cancel("Thread interrupted", ie); - // Now wait for onClose() to be called, so interceptors can clean up - } - } - if (next == this || next instanceof StatusRuntimeException) { - threadless.shutdown(); + while (true) { + try { + return buffer.take(); + } catch (InterruptedException ie) { + interrupt = true; + call.cancel("Thread interrupted", ie); + // Now wait for onClose() to be called, to guarantee BlockingQueue doesn't fill } - return next; } } finally { if (interrupt) { diff --git a/stub/src/main/java/io/grpc/stub/MetadataUtils.java b/stub/src/main/java/io/grpc/stub/MetadataUtils.java index 5395ba9b5e3e..addf54c0f813 100644 --- a/stub/src/main/java/io/grpc/stub/MetadataUtils.java +++ b/stub/src/main/java/io/grpc/stub/MetadataUtils.java @@ -18,12 +18,10 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.errorprone.annotations.InlineMe; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; -import io.grpc.ExperimentalApi; import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; import io.grpc.Metadata; @@ -38,24 +36,6 @@ public final class MetadataUtils { // Prevent instantiation private MetadataUtils() {} - /** - * Attaches a set of request headers to a stub. - * - * @param stub to bind the headers to. - * @param extraHeaders the headers to be passed by each call on the returned stub. - * @return an implementation of the stub with {@code extraHeaders} bound to each call. - * @deprecated Use {@code stub.withInterceptors(newAttachHeadersInterceptor(...))} instead. - */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1789") - @Deprecated - @InlineMe( - replacement = - "stub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(extraHeaders))", - imports = "io.grpc.stub.MetadataUtils") - public static > T attachHeaders(T stub, Metadata extraHeaders) { - return stub.withInterceptors(newAttachHeadersInterceptor(extraHeaders)); - } - /** * Returns a client interceptor that attaches a set of headers to requests. * @@ -97,31 +77,6 @@ public void start(Listener responseListener, Metadata headers) { } } - /** - * Captures the last received metadata for a stub. Useful for testing - * - * @param stub to capture for - * @param headersCapture to record the last received headers - * @param trailersCapture to record the last received trailers - * @return an implementation of the stub that allows to access the last received call's - * headers and trailers via {@code headersCapture} and {@code trailersCapture}. - * @deprecated Use {@code stub.withInterceptors(newCaptureMetadataInterceptor())} instead. - */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1789") - @Deprecated - @InlineMe( - replacement = - "stub.withInterceptors(MetadataUtils.newCaptureMetadataInterceptor(headersCapture," - + " trailersCapture))", - imports = "io.grpc.stub.MetadataUtils") - public static > T captureMetadata( - T stub, - AtomicReference headersCapture, - AtomicReference trailersCapture) { - return stub.withInterceptors( - newCaptureMetadataInterceptor(headersCapture, trailersCapture)); - } - /** * Captures the last received metadata on a channel. Useful for testing. * diff --git a/testing-proto/build.gradle b/testing-proto/build.gradle index 168c059e66c7..e6afce468f06 100644 --- a/testing-proto/build.gradle +++ b/testing-proto/build.gradle @@ -8,6 +8,12 @@ plugins { description = "gRPC: Testing Protos" +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.testing.protobuf') + } +} + dependencies { api project(':grpc-protobuf'), project(':grpc-stub') diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index f1e9a5743ed9..071a9650b130 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -11,8 +11,8 @@ java_library( deps = [ "//api", "//context", - "//core:inprocess", - "//core:util", + "//inprocess", + "//util", "//stub", "@com_google_code_findbugs_jsr305//jar", "@com_google_guava_guava//jar", diff --git a/testing/build.gradle b/testing/build.gradle index a2887eac3c8b..43332ba8a7c0 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -9,12 +9,13 @@ plugins { description = "gRPC: Testing" dependencies { - api project(':grpc-core'), + api project(':grpc-inprocess'), + project(':grpc-util'), project(':grpc-stub'), libraries.junit // 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 + runtimeOnly project(":grpc-api") // Pull in newer version than census-api testImplementation libraries.mockito.core diff --git a/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java b/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java index f518dcb9e529..55188e571492 100644 --- a/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java +++ b/testing/src/main/java/io/grpc/testing/GrpcCleanupRule.java @@ -23,7 +23,6 @@ import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; import com.google.common.collect.Lists; -import io.grpc.ExperimentalApi; import io.grpc.ManagedChannel; import io.grpc.Server; import java.util.ArrayList; @@ -70,7 +69,6 @@ * * @since 1.13.0 */ -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488") @NotThreadSafe public final class GrpcCleanupRule extends ExternalResource { diff --git a/testing/src/main/java/io/grpc/testing/GrpcServerRule.java b/testing/src/main/java/io/grpc/testing/GrpcServerRule.java index 71a4ab24c87f..641c9927d911 100644 --- a/testing/src/main/java/io/grpc/testing/GrpcServerRule.java +++ b/testing/src/main/java/io/grpc/testing/GrpcServerRule.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState; import io.grpc.BindableService; -import io.grpc.ExperimentalApi; import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.ServerServiceDefinition; @@ -48,7 +47,6 @@ *

    An {@link AbstractStub} can be created against this service by using the * {@link ManagedChannel} provided by {@link GrpcServerRule#getChannel()}. */ -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2488") public final class GrpcServerRule extends ExternalResource { private ManagedChannel channel; diff --git a/testing/src/main/java/io/grpc/testing/TestMethodDescriptors.java b/testing/src/main/java/io/grpc/testing/TestMethodDescriptors.java index b83c323cd5cb..5ffc60e369b4 100644 --- a/testing/src/main/java/io/grpc/testing/TestMethodDescriptors.java +++ b/testing/src/main/java/io/grpc/testing/TestMethodDescriptors.java @@ -16,7 +16,6 @@ package io.grpc.testing; -import io.grpc.ExperimentalApi; import io.grpc.MethodDescriptor; import io.grpc.MethodDescriptor.MethodType; import java.io.ByteArrayInputStream; @@ -28,7 +27,6 @@ * * @since 1.1.0 */ -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2600") public final class TestMethodDescriptors { private TestMethodDescriptors() {} @@ -38,7 +36,6 @@ private TestMethodDescriptors() {} * * @since 1.1.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2600") public static MethodDescriptor voidMethod() { return MethodDescriptor.newBuilder() .setType(MethodType.UNARY) @@ -53,7 +50,6 @@ public static MethodDescriptor voidMethod() { * * @since 1.1.0 */ - @ExperimentalApi("https://github.com/grpc/grpc-java/issues/2600") public static MethodDescriptor.Marshaller voidMarshaller() { return new NoopMarshaller(); } diff --git a/util/BUILD.bazel b/util/BUILD.bazel new file mode 100644 index 000000000000..b95e428f435d --- /dev/null +++ b/util/BUILD.bazel @@ -0,0 +1,18 @@ +java_library( + name = "util", + srcs = glob([ + "src/main/java/io/grpc/util/*.java", + ]), + resources = glob([ + "src/main/resources/**", + ]), + visibility = ["//visibility:public"], + deps = [ + "//api", + "//core:internal", + "@com_google_code_findbugs_jsr305//jar", + "@com_google_guava_guava//jar", + "@com_google_j2objc_j2objc_annotations//jar", + "@org_codehaus_mojo_animal_sniffer_annotations//jar", + ], +) diff --git a/util/build.gradle b/util/build.gradle new file mode 100644 index 000000000000..a05c55b27bb2 --- /dev/null +++ b/util/build.gradle @@ -0,0 +1,45 @@ +plugins { + id "java-library" + id "maven-publish" + + id "me.champeau.jmh" + id "ru.vyarus.animalsniffer" +} + +description = 'gRPC: Util' + +tasks.named("jar").configure { + manifest { + attributes('Automatic-Module-Name': 'io.grpc.util') + } +} + +dependencies { + api project(':grpc-core') + + implementation libraries.animalsniffer.annotations, + libraries.guava + testImplementation testFixtures(project(':grpc-api')), + testFixtures(project(':grpc-core')), + project(':grpc-testing') + testImplementation libraries.guava.testlib + + jmh project(':grpc-testing') + + signature libraries.signature.java + signature libraries.signature.android +} + +animalsniffer { + // Don't check sourceSets.jmh + sourceSets = [ + sourceSets.main, + sourceSets.test + ] +} + +tasks.named("javadoc").configure { + exclude 'io/grpc/util/MultiChildLoadBalancer.java' + exclude 'io/grpc/util/OutlierDetectionLoadBalancer*' + exclude 'io/grpc/util/RoundRobinLoadBalancer*' +} diff --git a/core/src/jmh/java/io/grpc/util/HandlerRegistryBenchmark.java b/util/src/jmh/java/io/grpc/util/HandlerRegistryBenchmark.java similarity index 100% rename from core/src/jmh/java/io/grpc/util/HandlerRegistryBenchmark.java rename to util/src/jmh/java/io/grpc/util/HandlerRegistryBenchmark.java diff --git a/core/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java similarity index 100% rename from core/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java rename to util/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java diff --git a/core/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java similarity index 100% rename from core/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java rename to util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java diff --git a/core/src/main/java/io/grpc/util/CertificateUtils.java b/util/src/main/java/io/grpc/util/CertificateUtils.java similarity index 100% rename from core/src/main/java/io/grpc/util/CertificateUtils.java rename to util/src/main/java/io/grpc/util/CertificateUtils.java diff --git a/core/src/main/java/io/grpc/util/ForwardingClientStreamTracer.java b/util/src/main/java/io/grpc/util/ForwardingClientStreamTracer.java similarity index 100% rename from core/src/main/java/io/grpc/util/ForwardingClientStreamTracer.java rename to util/src/main/java/io/grpc/util/ForwardingClientStreamTracer.java diff --git a/core/src/main/java/io/grpc/util/ForwardingLoadBalancer.java b/util/src/main/java/io/grpc/util/ForwardingLoadBalancer.java similarity index 100% rename from core/src/main/java/io/grpc/util/ForwardingLoadBalancer.java rename to util/src/main/java/io/grpc/util/ForwardingLoadBalancer.java diff --git a/core/src/main/java/io/grpc/util/ForwardingLoadBalancerHelper.java b/util/src/main/java/io/grpc/util/ForwardingLoadBalancerHelper.java similarity index 100% rename from core/src/main/java/io/grpc/util/ForwardingLoadBalancerHelper.java rename to util/src/main/java/io/grpc/util/ForwardingLoadBalancerHelper.java diff --git a/core/src/main/java/io/grpc/util/ForwardingSubchannel.java b/util/src/main/java/io/grpc/util/ForwardingSubchannel.java similarity index 100% rename from core/src/main/java/io/grpc/util/ForwardingSubchannel.java rename to util/src/main/java/io/grpc/util/ForwardingSubchannel.java diff --git a/core/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java b/util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java similarity index 100% rename from core/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java rename to util/src/main/java/io/grpc/util/GracefulSwitchLoadBalancer.java diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java new file mode 100644 index 000000000000..be0a23a16422 --- /dev/null +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -0,0 +1,264 @@ +/* + * Copyright 2023 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.util; + +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.TRANSIENT_FAILURE; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.ConnectivityState; +import io.grpc.Internal; +import io.grpc.LoadBalancer; +import io.grpc.LoadBalancerProvider; +import io.grpc.Status; +import io.grpc.SynchronizationContext; +import io.grpc.SynchronizationContext.ScheduledHandle; +import io.grpc.internal.ServiceConfigUtil.PolicySelection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * A base load balancing policy for those policies which has multiple children such as + * ClusterManager or the petiole policies. For internal use only. + */ +@Internal +public abstract class MultiChildLoadBalancer extends LoadBalancer { + + @VisibleForTesting + public static final int DELAYED_CHILD_DELETION_TIME_MINUTES = 15; + private static final Logger logger = Logger.getLogger(MultiChildLoadBalancer.class.getName()); + private final Map childLbStates = new HashMap<>(); + private final Helper helper; + protected final SynchronizationContext syncContext; + private final ScheduledExecutorService timeService; + // Set to true if currently in the process of handling resolved addresses. + private boolean resolvingAddresses; + + protected MultiChildLoadBalancer(Helper helper) { + this.helper = checkNotNull(helper, "helper"); + this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); + this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); + logger.log(Level.FINE, "Created"); + } + + protected SubchannelPicker getInitialPicker() { + return EMPTY_PICKER; + } + + protected SubchannelPicker getErrorPicker(Status error) { + return new ErrorPicker(error); + } + + protected abstract Map getPolicySelectionMap( + ResolvedAddresses resolvedAddresses); + + protected abstract SubchannelPicker getSubchannelPicker( + Map childPickers); + + @Override + public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { + try { + resolvingAddresses = true; + return acceptResolvedAddressesInternal(resolvedAddresses); + } finally { + resolvingAddresses = false; + } + } + + private boolean acceptResolvedAddressesInternal(ResolvedAddresses resolvedAddresses) { + logger.log(Level.FINE, "Received resolution result: {0}", resolvedAddresses); + Map newChildPolicies = getPolicySelectionMap(resolvedAddresses); + for (Map.Entry entry : newChildPolicies.entrySet()) { + final Object key = entry.getKey(); + LoadBalancerProvider childPolicyProvider = entry.getValue().getProvider(); + Object childConfig = entry.getValue().getConfig(); + if (!childLbStates.containsKey(key)) { + childLbStates.put(key, new ChildLbState(key, childPolicyProvider, getInitialPicker())); + } else { + childLbStates.get(key).reactivate(childPolicyProvider); + } + LoadBalancer childLb = childLbStates.get(key).lb; + ResolvedAddresses childAddresses = + resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(childConfig).build(); + childLb.handleResolvedAddresses(childAddresses); + } + for (Object key : childLbStates.keySet()) { + if (!newChildPolicies.containsKey(key)) { + childLbStates.get(key).deactivate(); + } + } + // Must update channel picker before return so that new RPCs will not be routed to deleted + // clusters and resolver can remove them in service config. + updateOverallBalancingState(); + return true; + } + + @Override + public void handleNameResolutionError(Status error) { + logger.log(Level.WARNING, "Received name resolution error: {0}", error); + boolean gotoTransientFailure = true; + for (ChildLbState state : childLbStates.values()) { + if (!state.deactivated) { + gotoTransientFailure = false; + state.lb.handleNameResolutionError(error); + } + } + if (gotoTransientFailure) { + helper.updateBalancingState(TRANSIENT_FAILURE, getErrorPicker(error)); + } + } + + @Override + public void shutdown() { + logger.log(Level.INFO, "Shutdown"); + for (ChildLbState state : childLbStates.values()) { + state.shutdown(); + } + childLbStates.clear(); + } + + private void updateOverallBalancingState() { + ConnectivityState overallState = null; + final Map childPickers = new HashMap<>(); + for (ChildLbState childLbState : childLbStates.values()) { + if (childLbState.deactivated) { + continue; + } + childPickers.put(childLbState.key, childLbState.currentPicker); + overallState = aggregateState(overallState, childLbState.currentState); + } + if (overallState != null) { + helper.updateBalancingState(overallState, getSubchannelPicker(childPickers)); + } + } + + @Nullable + private static ConnectivityState aggregateState( + @Nullable ConnectivityState overallState, ConnectivityState childState) { + if (overallState == null) { + return childState; + } + if (overallState == READY || childState == READY) { + return READY; + } + if (overallState == CONNECTING || childState == CONNECTING) { + return CONNECTING; + } + if (overallState == IDLE || childState == IDLE) { + return IDLE; + } + return overallState; + } + + private final class ChildLbState { + private final Object key; + private final GracefulSwitchLoadBalancer lb; + private LoadBalancerProvider policyProvider; + private ConnectivityState currentState = CONNECTING; + private SubchannelPicker currentPicker; + private boolean deactivated; + @Nullable + ScheduledHandle deletionTimer; + + ChildLbState(Object key, LoadBalancerProvider policyProvider, SubchannelPicker initialPicker) { + this.key = key; + this.policyProvider = policyProvider; + lb = new GracefulSwitchLoadBalancer(new ChildLbStateHelper()); + lb.switchTo(policyProvider); + currentPicker = initialPicker; + } + + void deactivate() { + if (deactivated) { + return; + } + + class DeletionTask implements Runnable { + @Override + public void run() { + shutdown(); + childLbStates.remove(key); + } + } + + deletionTimer = + syncContext.schedule( + new DeletionTask(), + DELAYED_CHILD_DELETION_TIME_MINUTES, + TimeUnit.MINUTES, + timeService); + deactivated = true; + logger.log(Level.FINE, "Child balancer {0} deactivated", key); + } + + void reactivate(LoadBalancerProvider policyProvider) { + if (deletionTimer != null && deletionTimer.isPending()) { + deletionTimer.cancel(); + deactivated = false; + logger.log(Level.FINE, "Child balancer {0} reactivated", key); + } + if (!this.policyProvider.getPolicyName().equals(policyProvider.getPolicyName())) { + Object[] objects = { + key, this.policyProvider.getPolicyName(),policyProvider.getPolicyName()}; + logger.log(Level.FINE, "Child balancer {0} switching policy from {1} to {2}", objects); + lb.switchTo(policyProvider); + this.policyProvider = policyProvider; + } + } + + void shutdown() { + if (deletionTimer != null && deletionTimer.isPending()) { + deletionTimer.cancel(); + } + lb.shutdown(); + logger.log(Level.FINE, "Child balancer {0} deleted", key); + } + + private final class ChildLbStateHelper extends ForwardingLoadBalancerHelper { + + @Override + public void updateBalancingState(final ConnectivityState newState, + final SubchannelPicker newPicker) { + // If we are already in the process of resolving addresses, the overall balancing state + // will be updated at the end of it, and we don't need to trigger that update here. + if (!childLbStates.containsKey(key)) { + return; + } + // Subchannel picker and state are saved, but will only be propagated to the channel + // when the child instance exits deactivated state. + currentState = newState; + currentPicker = newPicker; + if (!deactivated && !resolvingAddresses) { + updateOverallBalancingState(); + } + } + + @Override + protected Helper delegate() { + return helper; + } + } + } +} diff --git a/core/src/main/java/io/grpc/util/MutableHandlerRegistry.java b/util/src/main/java/io/grpc/util/MutableHandlerRegistry.java similarity index 96% rename from core/src/main/java/io/grpc/util/MutableHandlerRegistry.java rename to util/src/main/java/io/grpc/util/MutableHandlerRegistry.java index c31102f4213d..307142b642c1 100644 --- a/core/src/main/java/io/grpc/util/MutableHandlerRegistry.java +++ b/util/src/main/java/io/grpc/util/MutableHandlerRegistry.java @@ -31,13 +31,12 @@ import javax.annotation.concurrent.ThreadSafe; /** - * Default implementation of {@link MutableHandlerRegistry}. + * Default implementation of {@link HandlerRegistry}. * *

    Uses {@link ConcurrentHashMap} to avoid service registration excessively * blocking method lookup. */ @ThreadSafe -@ExperimentalApi("https://github.com/grpc/grpc-java/issues/933") public final class MutableHandlerRegistry extends HandlerRegistry { private final ConcurrentMap services = new ConcurrentHashMap<>(); diff --git a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java similarity index 96% rename from core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java rename to util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java index a1a4b4be2826..bd8825474fa6 100644 --- a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancer.java @@ -394,47 +394,55 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { Subchannel subchannel = pickResult.getSubchannel(); if (subchannel != null) { - return PickResult.withSubchannel(subchannel, - new ResultCountingClientStreamTracerFactory( - subchannel.getAttributes().get(ADDRESS_TRACKER_ATTR_KEY))); + return PickResult.withSubchannel(subchannel, new ResultCountingClientStreamTracerFactory( + subchannel.getAttributes().get(ADDRESS_TRACKER_ATTR_KEY), + pickResult.getStreamTracerFactory())); } return pickResult; } /** - * Builds instances of {@link ResultCountingClientStreamTracer}. + * Builds instances of a {@link ClientStreamTracer} that increments the call count in the + * tracker for each closed stream. */ class ResultCountingClientStreamTracerFactory extends ClientStreamTracer.Factory { private final AddressTracker tracker; - ResultCountingClientStreamTracerFactory(AddressTracker tracker) { - this.tracker = tracker; - } - - @Override - public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) { - return new ResultCountingClientStreamTracer(tracker); - } - } + @Nullable + private final ClientStreamTracer.Factory delegateFactory; - /** - * Counts the results (successful/unsuccessful) of a particular {@link - * OutlierDetectionSubchannel}s streams and increments the counter in the associated {@link - * AddressTracker}. - */ - class ResultCountingClientStreamTracer extends ClientStreamTracer { - - AddressTracker tracker; - - public ResultCountingClientStreamTracer(AddressTracker tracker) { + ResultCountingClientStreamTracerFactory(AddressTracker tracker, + @Nullable ClientStreamTracer.Factory delegateFactory) { this.tracker = tracker; + this.delegateFactory = delegateFactory; } @Override - public void streamClosed(Status status) { - tracker.incrementCallCount(status.isOk()); + public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) { + if (delegateFactory != null) { + ClientStreamTracer delegateTracer = delegateFactory.newClientStreamTracer(info, headers); + return new ForwardingClientStreamTracer() { + @Override + protected ClientStreamTracer delegate() { + return delegateTracer; + } + + @Override + public void streamClosed(Status status) { + tracker.incrementCallCount(status.isOk()); + delegate().streamClosed(status); + } + }; + } else { + return new ClientStreamTracer() { + @Override + public void streamClosed(Status status) { + tracker.incrementCallCount(status.isOk()); + } + }; + } } } } diff --git a/core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java b/util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java similarity index 100% rename from core/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java rename to util/src/main/java/io/grpc/util/OutlierDetectionLoadBalancerProvider.java diff --git a/core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java b/util/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java similarity index 100% rename from core/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java rename to util/src/main/java/io/grpc/util/RoundRobinLoadBalancer.java diff --git a/core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java b/util/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java similarity index 100% rename from core/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java rename to util/src/main/java/io/grpc/util/SecretRoundRobinLoadBalancerProvider.java diff --git a/core/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java b/util/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java similarity index 100% rename from core/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java rename to util/src/main/java/io/grpc/util/TransmitStatusRuntimeExceptionInterceptor.java diff --git a/core/src/main/java/io/grpc/util/package-info.java b/util/src/main/java/io/grpc/util/package-info.java similarity index 100% rename from core/src/main/java/io/grpc/util/package-info.java rename to util/src/main/java/io/grpc/util/package-info.java diff --git a/core/src/bazel-util/resources/META-INF/services/io.grpc.LoadBalancerProvider b/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider similarity index 54% rename from core/src/bazel-util/resources/META-INF/services/io.grpc.LoadBalancerProvider rename to util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider index 4c21dd2e1bfc..1fdd69cb00b6 100644 --- a/core/src/bazel-util/resources/META-INF/services/io.grpc.LoadBalancerProvider +++ b/util/src/main/resources/META-INF/services/io.grpc.LoadBalancerProvider @@ -1 +1,2 @@ io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider +io.grpc.util.OutlierDetectionLoadBalancerProvider diff --git a/core/src/test/java/io/grpc/util/CertificateUtilsTest.java b/util/src/test/java/io/grpc/util/CertificateUtilsTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/CertificateUtilsTest.java rename to util/src/test/java/io/grpc/util/CertificateUtilsTest.java diff --git a/core/src/test/java/io/grpc/util/ForwardingClientStreamTracerTest.java b/util/src/test/java/io/grpc/util/ForwardingClientStreamTracerTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/ForwardingClientStreamTracerTest.java rename to util/src/test/java/io/grpc/util/ForwardingClientStreamTracerTest.java diff --git a/core/src/test/java/io/grpc/util/ForwardingLoadBalancerHelperTest.java b/util/src/test/java/io/grpc/util/ForwardingLoadBalancerHelperTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/ForwardingLoadBalancerHelperTest.java rename to util/src/test/java/io/grpc/util/ForwardingLoadBalancerHelperTest.java diff --git a/core/src/test/java/io/grpc/util/ForwardingLoadBalancerTest.java b/util/src/test/java/io/grpc/util/ForwardingLoadBalancerTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/ForwardingLoadBalancerTest.java rename to util/src/test/java/io/grpc/util/ForwardingLoadBalancerTest.java diff --git a/core/src/test/java/io/grpc/util/ForwardingSubchannelTest.java b/util/src/test/java/io/grpc/util/ForwardingSubchannelTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/ForwardingSubchannelTest.java rename to util/src/test/java/io/grpc/util/ForwardingSubchannelTest.java diff --git a/core/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java b/util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java rename to util/src/test/java/io/grpc/util/GracefulSwitchLoadBalancerTest.java diff --git a/core/src/test/java/io/grpc/util/MutableHandlerRegistryTest.java b/util/src/test/java/io/grpc/util/MutableHandlerRegistryTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/MutableHandlerRegistryTest.java rename to util/src/test/java/io/grpc/util/MutableHandlerRegistryTest.java diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerProviderTest.java b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerProviderTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerProviderTest.java rename to util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerProviderTest.java diff --git a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java similarity index 92% rename from core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java rename to util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java index 18f9bbf549f7..13f13421a1ea 100644 --- a/core/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java +++ b/util/src/test/java/io/grpc/util/OutlierDetectionLoadBalancerTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; @@ -46,6 +47,7 @@ import io.grpc.LoadBalancer.SubchannelPicker; import io.grpc.LoadBalancer.SubchannelStateListener; import io.grpc.LoadBalancerProvider; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.internal.FakeClock; @@ -96,6 +98,10 @@ public class OutlierDetectionLoadBalancerTest { private Helper mockHelper; @Mock private SocketAddress mockSocketAddress; + @Mock + private ClientStreamTracer.Factory mockStreamTracerFactory; + @Mock + private ClientStreamTracer mockStreamTracer; @Captor private ArgumentCaptor connectivityStateCaptor; @@ -193,6 +199,9 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); + when(mockStreamTracerFactory.newClientStreamTracer(any(), + any())).thenReturn(mockStreamTracer); + loadBalancer = new OutlierDetectionLoadBalancer(mockHelper, fakeClock.getTimeProvider()); } @@ -355,6 +364,72 @@ public void delegatePick() throws Exception { readySubchannel); } + /** + * Any ClientStreamTracer.Factory set by the delegate picker should still get used. + */ + @Test + public void delegatePickTracerFactoryPreserved() throws Exception { + OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig.Builder() + .setSuccessRateEjection(new SuccessRateEjection.Builder().build()) + .setChildPolicy(new PolicySelection(fakeLbProvider, null)).build(); + + loadBalancer.acceptResolvedAddresses(buildResolvedAddress(config, servers.get(0))); + + // Make one of the subchannels READY. + final Subchannel readySubchannel = subchannels.values().iterator().next(); + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + + verify(mockHelper, times(2)).updateBalancingState(stateCaptor.capture(), + pickerCaptor.capture()); + + // Make sure that we can pick the single READY subchannel. + SubchannelPicker picker = pickerCaptor.getAllValues().get(1); + PickResult pickResult = picker.pickSubchannel(mock(PickSubchannelArgs.class)); + + // Calls to a stream tracer created with the factory in the result should make it to a stream + // tracer the underlying LB/picker is using. + ClientStreamTracer clientStreamTracer = pickResult.getStreamTracerFactory() + .newClientStreamTracer(ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); + clientStreamTracer.inboundHeaders(); + // The underlying fake LB provider is configured with a factory that returns a mock stream + // tracer. + verify(mockStreamTracer).inboundHeaders(); + } + + /** + * Assure the tracer works even when the underlying LB does not have a tracer to delegate to. + */ + @Test + public void delegatePickTracerFactoryNotSet() throws Exception { + // We set the mock factory to null to indicate that the delegate does not have its own tracer. + mockStreamTracerFactory = null; + + OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig.Builder() + .setSuccessRateEjection(new SuccessRateEjection.Builder().build()) + .setChildPolicy(new PolicySelection(fakeLbProvider, null)).build(); + + loadBalancer.acceptResolvedAddresses(buildResolvedAddress(config, servers.get(0))); + + // Make one of the subchannels READY. + final Subchannel readySubchannel = subchannels.values().iterator().next(); + deliverSubchannelState(readySubchannel, ConnectivityStateInfo.forNonError(READY)); + + verify(mockHelper, times(2)).updateBalancingState(stateCaptor.capture(), + pickerCaptor.capture()); + + // Make sure that we can pick the single READY subchannel. + SubchannelPicker picker = pickerCaptor.getAllValues().get(1); + PickResult pickResult = picker.pickSubchannel(mock(PickSubchannelArgs.class)); + + // With no delegate tracers factory a call to the OD tracer should still work + ClientStreamTracer clientStreamTracer = pickResult.getStreamTracerFactory() + .newClientStreamTracer(ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); + clientStreamTracer.inboundHeaders(); + + // Sanity check to make sure the delegate tracer does not get called. + verifyNoInteractions(mockStreamTracer); + } + /** * The success rate algorithm leaves a healthy set of addresses alone. */ @@ -1121,7 +1196,7 @@ void assertEjectedSubchannels(Set addresses) { } /** Round robin like fake load balancer. */ - private static final class FakeLoadBalancer extends LoadBalancer { + private final class FakeLoadBalancer extends LoadBalancer { private final Helper helper; List subchannelList; @@ -1159,7 +1234,8 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (lastPickIndex < 0 || lastPickIndex > subchannelList.size() - 1) { lastPickIndex = 0; } - return PickResult.withSubchannel(subchannelList.get(lastPickIndex++)); + return PickResult.withSubchannel(subchannelList.get(lastPickIndex++), + mockStreamTracerFactory); } }; helper.updateBalancingState(state, picker); diff --git a/core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java b/util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java rename to util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java diff --git a/core/src/test/java/io/grpc/util/UtilServerInterceptorsTest.java b/util/src/test/java/io/grpc/util/UtilServerInterceptorsTest.java similarity index 100% rename from core/src/test/java/io/grpc/util/UtilServerInterceptorsTest.java rename to util/src/test/java/io/grpc/util/UtilServerInterceptorsTest.java diff --git a/xds/BUILD.bazel b/xds/BUILD.bazel index d2c57fde0155..d3b746e39fad 100644 --- a/xds/BUILD.bazel +++ b/xds/BUILD.bazel @@ -38,7 +38,7 @@ java_library( "//api", "//context", "//core:internal", - "//core:util", + "//util", "//netty", "//stub", "//services:metrics", @@ -145,7 +145,7 @@ java_library( "//api", "//context", "//core:internal", - "//core:util", + "//util", "//protobuf", "//services:metrics", "//services:metrics_internal", diff --git a/xds/build.gradle b/xds/build.gradle index 60e30c5102c5..3f3cf6a0f6e1 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -140,6 +140,9 @@ tasks.named("compileJava").configure { tasks.named("jar").configure { archiveClassifier = 'original' + manifest { + attributes('Automatic-Module-Name': 'io.grpc.xds') + } } tasks.named("sourcesJar").configure { @@ -159,6 +162,7 @@ tasks.named("javadoc").configure { exclude 'io/grpc/xds/FilterChainMatchingProtocolNegotiators.java' exclude 'io/grpc/xds/TlsContextManager.java' exclude 'io/grpc/xds/XdsAttributes.java' + exclude 'io/grpc/xds/XdsCredentialsProvider.java' exclude 'io/grpc/xds/XdsInitializationException.java' exclude 'io/grpc/xds/XdsNameResolverProvider.java' exclude 'io/grpc/xds/internal/**' diff --git a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java index e8a450e8df72..4aa008c112d4 100644 --- a/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/BootstrapperImpl.java @@ -169,7 +169,7 @@ BootstrapInfo bootstrap(Map rawData) throws XdsInitializationExceptio if (rawLocality.containsKey("sub_zone")) { subZone = JsonUtil.getString(rawLocality, "sub_zone"); } - logger.log(XdsLogLevel.INFO, "Locality region: {0}, zone: {0}, subZone: {0}", + logger.log(XdsLogLevel.INFO, "Locality region: {0}, zone: {1}, subZone: {2}", region, zone, subZone); Locality locality = Locality.create(region, zone, subZone); nodeBuilder.setLocality(locality); diff --git a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java index a640bbd78b94..7257fdfd16b2 100644 --- a/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java +++ b/xds/src/main/java/io/grpc/xds/CdsLoadBalancer2.java @@ -39,7 +39,6 @@ import io.grpc.xds.XdsClusterResource.CdsUpdate; import io.grpc.xds.XdsClusterResource.CdsUpdate.ClusterType; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 074bb301b581..95ca1a33e153 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -17,7 +17,6 @@ package io.grpc.xds; import static com.google.common.base.Preconditions.checkNotNull; -import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; @@ -33,6 +32,7 @@ import io.grpc.Status; import io.grpc.internal.ForwardingClientStreamTracer; import io.grpc.internal.ObjectPool; +import io.grpc.services.MetricReport; import io.grpc.util.ForwardingLoadBalancerHelper; import io.grpc.util.ForwardingSubchannel; import io.grpc.util.GracefulSwitchLoadBalancer; @@ -45,8 +45,9 @@ import io.grpc.xds.ThreadSafeRandom.ThreadSafeRandomImpl; import io.grpc.xds.XdsLogger.XdsLogLevel; import io.grpc.xds.XdsNameResolverProvider.CallCounterProvider; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import io.grpc.xds.internal.security.SslContextProviderSupplier; +import io.grpc.xds.orca.OrcaPerRequestUtil; +import io.grpc.xds.orca.OrcaPerRequestUtil.OrcaPerRequestReportListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -170,7 +171,7 @@ public void shutdown() { private final class ClusterImplLbHelper extends ForwardingLoadBalancerHelper { private final AtomicLong inFlights; private ConnectivityState currentState = ConnectivityState.IDLE; - private SubchannelPicker currentPicker = BUFFER_PICKER; + private SubchannelPicker currentPicker = LoadBalancer.EMPTY_PICKER; private List dropPolicies = Collections.emptyList(); private long maxConcurrentRequests = DEFAULT_PER_CLUSTER_MAX_CONCURRENT_REQUESTS; @Nullable @@ -329,7 +330,9 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { if (stats != null) { ClientStreamTracer.Factory tracerFactory = new CountingStreamTracerFactory( stats, inFlights, result.getStreamTracerFactory()); - return PickResult.withSubchannel(result.getSubchannel(), tracerFactory); + ClientStreamTracer.Factory orcaTracerFactory = OrcaPerRequestUtil.getInstance() + .newOrcaClientStreamTracerFactory(tracerFactory, new OrcaPerRpcListener(stats)); + return PickResult.withSubchannel(result.getSubchannel(), orcaTracerFactory); } } return result; @@ -386,4 +389,22 @@ public void streamClosed(Status status) { }; } } + + private static final class OrcaPerRpcListener implements OrcaPerRequestReportListener { + + private final ClusterLocalityStats stats; + + private OrcaPerRpcListener(ClusterLocalityStats stats) { + this.stats = checkNotNull(stats, "stats"); + } + + /** + * Copies {@link MetricReport#getNamedMetrics()} to {@link ClusterLocalityStats} such that it is + * included in the snapshot for the LRS report sent to the LRS server. + */ + @Override + public void onLoadReport(MetricReport report) { + stats.recordBackendLoadMetricStats(report.getNamedMetrics()); + } + } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java index cce32c68246a..a4489204236e 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterManagerLoadBalancer.java @@ -16,266 +16,63 @@ package io.grpc.xds; -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.TRANSIENT_FAILURE; -import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER; - -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import io.grpc.ConnectivityState; import io.grpc.InternalLogId; -import io.grpc.LoadBalancer; -import io.grpc.LoadBalancerProvider; import io.grpc.Status; -import io.grpc.SynchronizationContext; -import io.grpc.SynchronizationContext.ScheduledHandle; import io.grpc.internal.ServiceConfigUtil.PolicySelection; -import io.grpc.util.ForwardingLoadBalancerHelper; -import io.grpc.util.GracefulSwitchLoadBalancer; +import io.grpc.util.MultiChildLoadBalancer; import io.grpc.xds.ClusterManagerLoadBalancerProvider.ClusterManagerConfig; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; /** * The top-level load balancing policy. */ -class ClusterManagerLoadBalancer extends LoadBalancer { - - @VisibleForTesting - static final int DELAYED_CHILD_DELETION_TIME_MINUTES = 15; +class ClusterManagerLoadBalancer extends MultiChildLoadBalancer { - private final Map childLbStates = new HashMap<>(); - private final Helper helper; - private final SynchronizationContext syncContext; - private final ScheduledExecutorService timeService; private final XdsLogger logger; - // Set to true if currently in the process of handling resolved addresses. - private boolean resolvingAddresses; ClusterManagerLoadBalancer(Helper helper) { - this.helper = checkNotNull(helper, "helper"); - this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); - this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); + super(helper); logger = XdsLogger.withLogId( InternalLogId.allocate("cluster_manager-lb", helper.getAuthority())); logger.log(XdsLogLevel.INFO, "Created"); } @Override - public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { - try { - resolvingAddresses = true; - return acceptResolvedAddressesInternal(resolvedAddresses); - } finally { - resolvingAddresses = false; - } - } - - public boolean acceptResolvedAddressesInternal(ResolvedAddresses resolvedAddresses) { - logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); + protected Map getPolicySelectionMap( + ResolvedAddresses resolvedAddresses) { ClusterManagerConfig config = (ClusterManagerConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); - Map newChildPolicies = config.childPolicies; + Map newChildPolicies = new HashMap<>(config.childPolicies); logger.log( XdsLogLevel.INFO, "Received cluster_manager lb config: child names={0}", newChildPolicies.keySet()); - for (Map.Entry entry : newChildPolicies.entrySet()) { - final String name = entry.getKey(); - LoadBalancerProvider childPolicyProvider = entry.getValue().getProvider(); - Object childConfig = entry.getValue().getConfig(); - if (!childLbStates.containsKey(name)) { - childLbStates.put(name, new ChildLbState(name, childPolicyProvider)); - } else { - childLbStates.get(name).reactivate(childPolicyProvider); - } - LoadBalancer childLb = childLbStates.get(name).lb; - ResolvedAddresses childAddresses = - resolvedAddresses.toBuilder().setLoadBalancingPolicyConfig(childConfig).build(); - childLb.handleResolvedAddresses(childAddresses); - } - for (String name : childLbStates.keySet()) { - if (!newChildPolicies.containsKey(name)) { - childLbStates.get(name).deactivate(); - } - } - // Must update channel picker before return so that new RPCs will not be routed to deleted - // clusters and resolver can remove them in service config. - updateOverallBalancingState(); - return true; - } - - @Override - public void handleNameResolutionError(Status error) { - logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); - boolean gotoTransientFailure = true; - for (ChildLbState state : childLbStates.values()) { - if (!state.deactivated) { - gotoTransientFailure = false; - state.lb.handleNameResolutionError(error); - } - } - if (gotoTransientFailure) { - helper.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(error)); - } + return newChildPolicies; } @Override - public void shutdown() { - logger.log(XdsLogLevel.INFO, "Shutdown"); - for (ChildLbState state : childLbStates.values()) { - state.shutdown(); - } - childLbStates.clear(); - } - - private void updateOverallBalancingState() { - ConnectivityState overallState = null; - final Map childPickers = new HashMap<>(); - for (ChildLbState childLbState : childLbStates.values()) { - if (childLbState.deactivated) { - continue; - } - childPickers.put(childLbState.name, childLbState.currentPicker); - overallState = aggregateState(overallState, childLbState.currentState); - } - if (overallState != null) { - SubchannelPicker picker = new SubchannelPicker() { - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - String clusterName = - args.getCallOptions().getOption(XdsNameResolver.CLUSTER_SELECTION_KEY); - SubchannelPicker delegate = childPickers.get(clusterName); - if (delegate == null) { - return - PickResult.withError( - Status.UNAVAILABLE.withDescription("CDS encountered error: unable to find " - + "available subchannel for cluster " + clusterName)); - } - return delegate.pickSubchannel(args); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this).add("pickers", childPickers).toString(); - } - }; - helper.updateBalancingState(overallState, picker); - } - } - - @Nullable - private static ConnectivityState aggregateState( - @Nullable ConnectivityState overallState, ConnectivityState childState) { - if (overallState == null) { - return childState; - } - if (overallState == READY || childState == READY) { - return READY; - } - if (overallState == CONNECTING || childState == CONNECTING) { - return CONNECTING; - } - if (overallState == IDLE || childState == IDLE) { - return IDLE; - } - return overallState; - } - - private final class ChildLbState { - private final String name; - private final GracefulSwitchLoadBalancer lb; - private LoadBalancerProvider policyProvider; - private ConnectivityState currentState = CONNECTING; - private SubchannelPicker currentPicker = BUFFER_PICKER; - private boolean deactivated; - @Nullable - ScheduledHandle deletionTimer; - - ChildLbState(String name, LoadBalancerProvider policyProvider) { - this.name = name; - this.policyProvider = policyProvider; - lb = new GracefulSwitchLoadBalancer(new ChildLbStateHelper()); - lb.switchTo(policyProvider); - } - - void deactivate() { - if (deactivated) { - return; - } - - class DeletionTask implements Runnable { - @Override - public void run() { - shutdown(); - childLbStates.remove(name); - } - } - - deletionTimer = - syncContext.schedule( - new DeletionTask(), - DELAYED_CHILD_DELETION_TIME_MINUTES, - TimeUnit.MINUTES, - timeService); - deactivated = true; - logger.log(XdsLogLevel.DEBUG, "Child balancer {0} deactivated", name); - } - - void reactivate(LoadBalancerProvider policyProvider) { - if (deletionTimer != null && deletionTimer.isPending()) { - deletionTimer.cancel(); - deactivated = false; - logger.log(XdsLogLevel.DEBUG, "Child balancer {0} reactivated", name); - } - if (!this.policyProvider.getPolicyName().equals(policyProvider.getPolicyName())) { - logger.log( - XdsLogLevel.DEBUG, - "Child balancer {0} switching policy from {1} to {2}", - name, this.policyProvider.getPolicyName(), policyProvider.getPolicyName()); - lb.switchTo(policyProvider); - this.policyProvider = policyProvider; - } - } - - void shutdown() { - if (deletionTimer != null && deletionTimer.isPending()) { - deletionTimer.cancel(); - } - lb.shutdown(); - logger.log(XdsLogLevel.DEBUG, "Child balancer {0} deleted", name); - } - - private final class ChildLbStateHelper extends ForwardingLoadBalancerHelper { - + protected SubchannelPicker getSubchannelPicker(Map childPickers) { + return new SubchannelPicker() { @Override - public void updateBalancingState(final ConnectivityState newState, - final SubchannelPicker newPicker) { - // If we are already in the process of resolving addresses, the overall balancing state - // will be updated at the end of it, and we don't need to trigger that update here. - if (!childLbStates.containsKey(name)) { - return; - } - // Subchannel picker and state are saved, but will only be propagated to the channel - // when the child instance exits deactivated state. - currentState = newState; - currentPicker = newPicker; - if (!deactivated && !resolvingAddresses) { - updateOverallBalancingState(); + public PickResult pickSubchannel(PickSubchannelArgs args) { + String clusterName = + args.getCallOptions().getOption(XdsNameResolver.CLUSTER_SELECTION_KEY); + SubchannelPicker childPicker = childPickers.get(clusterName); + if (childPicker == null) { + return + PickResult.withError( + Status.UNAVAILABLE.withDescription("CDS encountered error: unable to find " + + "available subchannel for cluster " + clusterName)); } + return childPicker.pickSubchannel(args); } @Override - protected Helper delegate() { - return helper; + public String toString() { + return MoreObjects.toStringHelper(this).add("pickers", childPickers).toString(); } - } + }; } } diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index 3af58ef93cb5..a7564e89a8cd 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -55,7 +55,6 @@ import io.grpc.xds.XdsClient.ResourceWatcher; import io.grpc.xds.XdsEndpointResource.EdsUpdate; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; diff --git a/xds/src/main/java/io/grpc/xds/ControlPlaneClient.java b/xds/src/main/java/io/grpc/xds/ControlPlaneClient.java index 5a3400c751a4..c919365d0935 100644 --- a/xds/src/main/java/io/grpc/xds/ControlPlaneClient.java +++ b/xds/src/main/java/io/grpc/xds/ControlPlaneClient.java @@ -383,8 +383,9 @@ public boolean isReady() { void start() { AggregatedDiscoveryServiceGrpc.AggregatedDiscoveryServiceStub stub = AggregatedDiscoveryServiceGrpc.newStub(channel); - StreamObserver responseReader = - new ClientResponseObserver() { + + final class AdsClientResponseObserver + implements ClientResponseObserver { @Override public void beforeStart(ClientCallStreamObserver requestStream) { @@ -434,8 +435,9 @@ public void run() { } }); } - }; - requestWriter = stub.streamAggregatedResources(responseReader); + } + + requestWriter = stub.streamAggregatedResources(new AdsClientResponseObserver()); } @Override diff --git a/xds/src/main/java/io/grpc/xds/LoadReportClient.java b/xds/src/main/java/io/grpc/xds/LoadReportClient.java index 9daa440a3dc7..b86c8110f636 100644 --- a/xds/src/main/java/io/grpc/xds/LoadReportClient.java +++ b/xds/src/main/java/io/grpc/xds/LoadReportClient.java @@ -45,6 +45,7 @@ import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -361,7 +362,16 @@ private io.envoyproxy.envoy.config.endpoint.v3.ClusterStats buildClusterStats( .setTotalSuccessfulRequests(upstreamLocalityStats.totalSuccessfulRequests()) .setTotalErrorRequests(upstreamLocalityStats.totalErrorRequests()) .setTotalRequestsInProgress(upstreamLocalityStats.totalRequestsInProgress()) - .setTotalIssuedRequests(upstreamLocalityStats.totalIssuedRequests())); + .setTotalIssuedRequests(upstreamLocalityStats.totalIssuedRequests()) + .addAllLoadMetricStats( + upstreamLocalityStats.loadMetricStatsMap().entrySet().stream().map( + e -> io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats.newBuilder() + .setMetricName(e.getKey()) + .setNumRequestsFinishedWithMetric( + e.getValue().numRequestsFinishedWithMetric()) + .setTotalMetricValue(e.getValue().totalMetricValue()) + .build()) + .collect(Collectors.toList()))); } for (DroppedRequests droppedRequests : stats.droppedRequestsList()) { builder.addDroppedRequests( diff --git a/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java b/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java index 4bd0ba437be1..e51c6eceeb57 100644 --- a/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java +++ b/xds/src/main/java/io/grpc/xds/LoadStatsManager2.java @@ -23,6 +23,7 @@ import com.google.common.base.Supplier; import com.google.common.collect.Sets; import io.grpc.Status; +import io.grpc.xds.Stats.BackendLoadMetricStats; import io.grpc.xds.Stats.ClusterStats; import io.grpc.xds.Stats.DroppedRequests; import io.grpc.xds.Stats.UpstreamLocalityStats; @@ -197,7 +198,7 @@ synchronized List getClusterStatsReports(String cluster) { } UpstreamLocalityStats upstreamLocalityStats = UpstreamLocalityStats.create( locality, snapshot.callsIssued, snapshot.callsSucceeded, snapshot.callsFailed, - snapshot.callsInProgress); + snapshot.callsInProgress, snapshot.loadMetricStatsMap); builder.addUpstreamLocalityStats(upstreamLocalityStats); // Use the max (drops/loads) recording interval as the overall interval for the // cluster's stats. In general, they should be mostly identical. @@ -322,6 +323,7 @@ final class ClusterLocalityStats { private final AtomicLong callsSucceeded = new AtomicLong(); private final AtomicLong callsFailed = new AtomicLong(); private final AtomicLong callsIssued = new AtomicLong(); + private Map loadMetricStatsMap = new HashMap<>(); private ClusterLocalityStats( String clusterName, @Nullable String edsServiceName, Locality locality, @@ -353,6 +355,23 @@ void recordCallFinished(Status status) { } } + /** + * Records all custom named backend load metric stats for per-call load reporting. For each + * metric key {@code name}, creates a new {@link BackendLoadMetricStats} with a finished + * requests counter of 1 and the {@code value} if the key is not present in the map. Otherwise, + * increments the finished requests counter and adds the {@code value} to the existing + * {@link BackendLoadMetricStats}. + */ + synchronized void recordBackendLoadMetricStats(Map namedMetrics) { + namedMetrics.forEach((name, value) -> { + if (!loadMetricStatsMap.containsKey(name)) { + loadMetricStatsMap.put(name, new BackendLoadMetricStats(1, value)); + } else { + loadMetricStatsMap.get(name).addMetricValueAndIncrementRequestsFinished(value); + } + }); + } + /** * Release the hard reference for this stats object (previously obtained via {@link * LoadStatsManager2#getClusterLocalityStats}). The object may still be @@ -367,8 +386,13 @@ void release() { private ClusterLocalityStatsSnapshot snapshot() { long duration = stopwatch.elapsed(TimeUnit.NANOSECONDS); stopwatch.reset().start(); + Map loadMetricStatsMapCopy; + synchronized (this) { + loadMetricStatsMapCopy = Collections.unmodifiableMap(loadMetricStatsMap); + loadMetricStatsMap = new HashMap<>(); + } return new ClusterLocalityStatsSnapshot(callsSucceeded.getAndSet(0), callsInProgress.get(), - callsFailed.getAndSet(0), callsIssued.getAndSet(0), duration); + callsFailed.getAndSet(0), callsIssued.getAndSet(0), duration, loadMetricStatsMapCopy); } } @@ -378,15 +402,18 @@ private static final class ClusterLocalityStatsSnapshot { private final long callsFailed; private final long callsIssued; private final long durationNano; + private final Map loadMetricStatsMap; private ClusterLocalityStatsSnapshot( long callsSucceeded, long callsInProgress, long callsFailed, long callsIssued, - long durationNano) { + long durationNano, Map loadMetricStatsMap) { this.callsSucceeded = callsSucceeded; this.callsInProgress = callsInProgress; this.callsFailed = callsFailed; this.callsIssued = callsIssued; this.durationNano = durationNano; + this.loadMetricStatsMap = Collections.unmodifiableMap( + checkNotNull(loadMetricStatsMap, "loadMetricStatsMap")); } } } diff --git a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java index e833b3777b82..5cf543175656 100644 --- a/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/PriorityLoadBalancer.java @@ -21,7 +21,6 @@ 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; import io.grpc.ConnectivityState; import io.grpc.InternalLogId; @@ -36,7 +35,6 @@ import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -149,7 +147,7 @@ private void tryNextPriority() { ChildLbState child = new ChildLbState(priority, priorityConfigs.get(priority).ignoreReresolution); children.put(priority, child); - updateOverallState(priority, CONNECTING, BUFFER_PICKER); + updateOverallState(priority, CONNECTING, LoadBalancer.EMPTY_PICKER); // Calling the child's updateResolvedAddresses() can result in tryNextPriority() being // called recursively. We need to be sure to be done with processing here before it is // called. @@ -210,7 +208,7 @@ private final class ChildLbState { @Nullable ScheduledHandle deletionTimer; @Nullable String policy; ConnectivityState connectivityState = CONNECTING; - SubchannelPicker picker = BUFFER_PICKER; + SubchannelPicker picker = LoadBalancer.EMPTY_PICKER; ChildLbState(final String priority, boolean ignoreReresolution) { this.priority = priority; diff --git a/xds/src/main/java/io/grpc/xds/RbacFilter.java b/xds/src/main/java/io/grpc/xds/RbacFilter.java index 0f5ac1025bab..6a55f7f193ed 100644 --- a/xds/src/main/java/io/grpc/xds/RbacFilter.java +++ b/xds/src/main/java/io/grpc/xds/RbacFilter.java @@ -59,8 +59,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** RBAC Http filter implementation. */ @@ -117,9 +119,12 @@ static ConfigOrError parseRbacConfig(RBAC rbac) { default: return ConfigOrError.fromError("Unknown rbacConfig action type: " + rbacConfig.getAction()); } - Map policyMap = rbacConfig.getPoliciesMap(); List policyMatchers = new ArrayList<>(); - for (Map.Entry entry: policyMap.entrySet()) { + List> sortedPolicyEntries = rbacConfig.getPoliciesMap().entrySet() + .stream() + .sorted((a,b) -> a.getKey().compareTo(b.getKey())) + .collect(Collectors.toList()); + for (Map.Entry entry: sortedPolicyEntries) { try { Policy policy = entry.getValue(); if (policy.hasCondition() || policy.hasCheckedCondition()) { diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java index 436eca8ec5dc..20a70cb0322f 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancer.java @@ -39,7 +39,6 @@ import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Collections; diff --git a/xds/src/main/java/io/grpc/xds/Stats.java b/xds/src/main/java/io/grpc/xds/Stats.java index 7e5fa8639d4d..5953f088448a 100644 --- a/xds/src/main/java/io/grpc/xds/Stats.java +++ b/xds/src/main/java/io/grpc/xds/Stats.java @@ -18,6 +18,8 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Map; import javax.annotation.Nullable; /** Represents client load stats. */ @@ -101,10 +103,45 @@ abstract static class UpstreamLocalityStats { abstract long totalRequestsInProgress(); + abstract ImmutableMap loadMetricStatsMap(); + static UpstreamLocalityStats create(Locality locality, long totalIssuedRequests, - long totalSuccessfulRequests, long totalErrorRequests, long totalRequestsInProgress) { + long totalSuccessfulRequests, long totalErrorRequests, long totalRequestsInProgress, + Map loadMetricStatsMap) { return new AutoValue_Stats_UpstreamLocalityStats(locality, totalIssuedRequests, - totalSuccessfulRequests, totalErrorRequests, totalRequestsInProgress); + totalSuccessfulRequests, totalErrorRequests, totalRequestsInProgress, + ImmutableMap.copyOf(loadMetricStatsMap)); + } + } + + /** + * Load metric stats for multi-dimensional load balancing. + */ + static final class BackendLoadMetricStats { + + private long numRequestsFinishedWithMetric; + private double totalMetricValue; + + BackendLoadMetricStats(long numRequestsFinishedWithMetric, double totalMetricValue) { + this.numRequestsFinishedWithMetric = numRequestsFinishedWithMetric; + this.totalMetricValue = totalMetricValue; + } + + public long numRequestsFinishedWithMetric() { + return numRequestsFinishedWithMetric; + } + + public double totalMetricValue() { + return totalMetricValue; + } + + /** + * Adds the given {@code metricValue} and increments the number of requests finished counter for + * the existing {@link BackendLoadMetricStats}. + */ + public void addMetricValueAndIncrementRequestsFinished(double metricValue) { + numRequestsFinishedWithMetric += 1; + totalMetricValue += metricValue; } } } diff --git a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java index 48442a84b22d..833683729c22 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedRoundRobinLoadBalancer.java @@ -44,10 +44,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.PriorityQueue; import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -65,7 +65,7 @@ final class WeightedRoundRobinLoadBalancer extends RoundRobinLoadBalancer { private final ScheduledExecutorService timeService; private ScheduledHandle weightUpdateTimer; private final Runnable updateWeightTask; - private final Random random; + private final AtomicInteger sequence; private final long infTime; private final Ticker ticker; @@ -81,7 +81,7 @@ public WeightedRoundRobinLoadBalancer(WrrHelper helper, Ticker ticker, Random ra this.syncContext = checkNotNull(helper.getSynchronizationContext(), "syncContext"); this.timeService = checkNotNull(helper.getScheduledExecutorService(), "timeService"); this.updateWeightTask = new UpdateWeightTask(); - this.random = random; + this.sequence = new AtomicInteger(random.nextInt()); log.log(Level.FINE, "weighted_round_robin LB created"); } @@ -120,7 +120,7 @@ private final class UpdateWeightTask implements Runnable { @Override public void run() { if (currentPicker != null && currentPicker instanceof WeightedRoundRobinPicker) { - ((WeightedRoundRobinPicker)currentPicker).updateWeight(); + ((WeightedRoundRobinPicker) currentPicker).updateWeight(); } weightUpdateTimer = syncContext.schedule(this, config.weightUpdatePeriodNanos, TimeUnit.NANOSECONDS, timeService); @@ -258,7 +258,7 @@ final class WeightedRoundRobinPicker extends RoundRobinPicker { new HashMap<>(); private final boolean enableOobLoadReport; private final float errorUtilizationPenalty; - private volatile EdfScheduler scheduler; + private volatile StaticStrideScheduler scheduler; WeightedRoundRobinPicker(List list, boolean enableOobLoadReport, float errorUtilizationPenalty) { @@ -279,7 +279,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { Subchannel subchannel = list.get(scheduler.pick()); if (!enableOobLoadReport) { return PickResult.withSubchannel(subchannel, - OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( + OrcaPerRequestUtil.getInstance().newOrcaClientStreamTracerFactory( subchannelToReportListenerMap.getOrDefault(subchannel, ((WrrSubchannel) subchannel).new OrcaReportListener(errorUtilizationPenalty)))); } else { @@ -288,27 +288,13 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { } private void updateWeight() { - int weightedChannelCount = 0; - double avgWeight = 0; - for (Subchannel value : list) { - double newWeight = ((WrrSubchannel) value).getWeight(); - if (newWeight > 0) { - avgWeight += newWeight; - weightedChannelCount++; - } - } - EdfScheduler scheduler = new EdfScheduler(list.size(), random); - if (weightedChannelCount >= 1) { - avgWeight /= 1.0 * weightedChannelCount; - } else { - avgWeight = 1; - } + float[] newWeights = new float[list.size()]; for (int i = 0; i < list.size(); i++) { WrrSubchannel subchannel = (WrrSubchannel) list.get(i); double newWeight = subchannel.getWeight(); - scheduler.add(i, newWeight > 0 ? newWeight : avgWeight); + newWeights[i] = newWeight > 0 ? (float) newWeight : 0.0f; } - this.scheduler = scheduler; + this.scheduler = new StaticStrideScheduler(newWeights, sequence); } @Override @@ -340,111 +326,146 @@ public boolean isEquivalentTo(RoundRobinPicker picker) { } } - /** - * The earliest deadline first implementation in which each object is - * chosen deterministically and periodically with frequency proportional to its weight. - * - *

    Specifically, each object added to chooser is given a deadline equal to the multiplicative - * inverse of its weight. The place of each object in its deadline is tracked, and each call to - * choose returns the object with the least remaining time in its deadline. - * (Ties are broken by the order in which the children were added to the chooser.) The deadline - * advances by the multiplicative inverse of the object's weight. - * For example, if items A and B are added with weights 0.5 and 0.2, successive chooses return: + /* + * The Static Stride Scheduler is an implementation of an earliest deadline first (EDF) scheduler + * in which each object's deadline is the multiplicative inverse of the object's weight. + *

    + * The way in which this is implemented is through a static stride scheduler. + * The Static Stride Scheduler works by iterating through the list of subchannel weights + * and using modular arithmetic to proportionally distribute picks, favoring entries + * with higher weights. It is based on the observation that the intended sequence generated + * from an EDF scheduler is a periodic one that can be achieved through modular arithmetic. + * The Static Stride Scheduler is more performant than other implementations of the EDF + * Scheduler, as it removes the need for a priority queue (and thus mutex locks). + *

    + * go/static-stride-scheduler + *

    * *

      - *
    • In the first call, the deadlines are A=2 (1/0.5) and B=5 (1/0.2), so A is returned. - * The deadline of A is updated to 4. - *
    • Next, the remaining deadlines are A=4 and B=5, so A is returned. The deadline of A (2) is - * updated to A=6. - *
    • Remaining deadlines are A=6 and B=5, so B is returned. The deadline of B is updated with - * with B=10. - *
    • Remaining deadlines are A=6 and B=10, so A is returned. The deadline of A is updated with - * A=8. - *
    • Remaining deadlines are A=8 and B=10, so A is returned. The deadline of A is updated with - * A=10. - *
    • Remaining deadlines are A=10 and B=10, so A is returned. The deadline of A is updated - * with A=12. - *
    • Remaining deadlines are A=12 and B=10, so B is returned. The deadline of B is updated - * with B=15. - *
    • etc. - *
    - * - *

    In short: the entry with the highest weight is preferred. - * - *

      - *
    • add() - O(lg n) - *
    • pick() - O(lg n) - *
    - * + *
  • nextSequence() - O(1) + *
  • pick() - O(n) */ @VisibleForTesting - static final class EdfScheduler { - private final PriorityQueue prioQueue; - - /** - * Weights below this value will be upped to this minimum weight. - */ - private static final double MINIMUM_WEIGHT = 0.0001; - - private final Object lock = new Object(); + static final class StaticStrideScheduler { + private final short[] scaledWeights; + private final AtomicInteger sequence; + private static final int K_MAX_WEIGHT = 0xFFFF; + + // Assuming the mean of all known weights is M, StaticStrideScheduler will clamp + // weights bigger than M*kMaxRatio and weights smaller than M*kMinRatio. + // + // This is done as a performance optimization by limiting the number of rounds for picks + // for edge cases where channels have large differences in subchannel weights. + // In this case, without these clips, it would potentially require the scheduler to + // frequently traverse through the entire subchannel list within the pick method. + // + // The current values of 10 and 0.1 were chosen without any experimenting. It should + // decrease the amount of sequences that the scheduler must traverse through in order + // to pick a high weight subchannel in such corner cases. + // But, it also makes WeightedRoundRobin to send slightly more requests to + // potentially very bad tasks (that would have near-zero weights) than zero. + // This is not necessarily a downside, though. Perhaps this is not a problem at + // all, and we can increase this value if needed to save CPU cycles. + private static final double K_MAX_RATIO = 10; + private static final double K_MIN_RATIO = 0.1; + + StaticStrideScheduler(float[] weights, AtomicInteger sequence) { + checkArgument(weights.length >= 1, "Couldn't build scheduler: requires at least one weight"); + int numChannels = weights.length; + int numWeightedChannels = 0; + double sumWeight = 0; + double unscaledMeanWeight; + float unscaledMaxWeight = 0; + for (float weight : weights) { + if (weight > 0) { + sumWeight += weight; + unscaledMaxWeight = Math.max(weight, unscaledMaxWeight); + numWeightedChannels++; + } + } - private final Random random; + // Adjust max value s.t. ratio does not exceed K_MAX_RATIO. This should + // ensure that we on average do at most K_MAX_RATIO rounds for picks. + if (numWeightedChannels > 0) { + unscaledMeanWeight = sumWeight / numWeightedChannels; + unscaledMaxWeight = Math.min(unscaledMaxWeight, (float) (K_MAX_RATIO * unscaledMeanWeight)); + } else { + // Fall back to round robin if all values are non-positives + unscaledMeanWeight = 1; + unscaledMaxWeight = 1; + } - /** - * Use the item's deadline as the order in the priority queue. If the deadlines are the same, - * use the index. Index should be unique. - */ - EdfScheduler(int initialCapacity, Random random) { - this.prioQueue = new PriorityQueue(initialCapacity, (o1, o2) -> { - if (o1.deadline == o2.deadline) { - return Integer.compare(o1.index, o2.index); + // Scales weights s.t. max(weights) == K_MAX_WEIGHT, meanWeight is scaled accordingly. + // Note that, since we cap the weights to stay within K_MAX_RATIO, meanWeight might not + // match the actual mean of the values that end up in the scheduler. + double scalingFactor = K_MAX_WEIGHT / unscaledMaxWeight; + // We compute weightLowerBound and clamp it to 1 from below so that in the + // worst case, we represent tiny weights as 1. + int weightLowerBound = (int) Math.ceil(scalingFactor * unscaledMeanWeight * K_MIN_RATIO); + short[] scaledWeights = new short[numChannels]; + for (int i = 0; i < numChannels; i++) { + if (weights[i] <= 0) { + scaledWeights[i] = (short) Math.round(scalingFactor * unscaledMeanWeight); } else { - return Double.compare(o1.deadline, o2.deadline); + int weight = (int) Math.round(scalingFactor * Math.min(weights[i], unscaledMaxWeight)); + scaledWeights[i] = (short) Math.max(weight, weightLowerBound); } - }); - this.random = random; + } + + this.scaledWeights = scaledWeights; + this.sequence = sequence; } - /** - * Adds the item in the scheduler. This is not thread safe. - * - * @param index The field {@link ObjectState#index} to be added - * @param weight positive weight for the added object - */ - void add(int index, double weight) { - checkArgument(weight > 0.0, "Weights need to be positive."); - ObjectState state = new ObjectState(Math.max(weight, MINIMUM_WEIGHT), index); - // Randomize the initial deadline. - state.deadline = random.nextDouble() * (1 / state.weight); - prioQueue.add(state); + /** Returns the next sequence number and atomically increases sequence with wraparound. */ + private long nextSequence() { + return Integer.toUnsignedLong(sequence.getAndIncrement()); } - /** - * Picks the next WRR object. + /* + * Selects index of next backend server. + *

    + * A 2D array is compactly represented as a function of W(backend), where the row + * represents the generation and the column represents the backend index: + * X(backend,generation) | generation ∈ [0,kMaxWeight). + * Each element in the conceptual array is a boolean indicating whether the backend at + * this index should be picked now. If false, the counter is incremented again, + * and the new element is checked. An atomically incremented counter keeps track of our + * backend and generation through modular arithmetic within the pick() method. + *

    + * Modular arithmetic allows us to evenly distribute picks and skips between + * generations based on W(backend). + * X(backend,generation) = (W(backend) * generation) % kMaxWeight >= kMaxWeight - W(backend) + * If we have the same three backends with weights: + * W(backend) = {2,3,6} scaled to max(W(backend)) = 6, then X(backend,generation) is: + *

    + * B0 B1 B2 + * T T T + * F F T + * F T T + * T F T + * F T T + * F F T + * The sequence of picked backend indices is given by + * walking across and down: {0,1,2,2,1,2,0,2,1,2,2}. + *

    + * To reduce the variance and spread the wasted work among different picks, + * an offset that varies per backend index is also included to the calculation. */ int pick() { - synchronized (lock) { - ObjectState minObject = prioQueue.remove(); - minObject.deadline += 1.0 / minObject.weight; - prioQueue.add(minObject); - return minObject.index; + while (true) { + long sequence = this.nextSequence(); + int backendIndex = (int) (sequence % scaledWeights.length); + long generation = sequence / scaledWeights.length; + int weight = Short.toUnsignedInt(scaledWeights[backendIndex]); + long offset = (long) K_MAX_WEIGHT / 2 * backendIndex; + if ((weight * generation + offset) % K_MAX_WEIGHT < K_MAX_WEIGHT - weight) { + continue; + } + return backendIndex; } } } - /** Holds the state of the object. */ - @VisibleForTesting - static class ObjectState { - private final double weight; - private final int index; - private volatile double deadline; - - ObjectState(double weight, int index) { - this.weight = weight; - this.index = index; - } - } - static final class WeightedRoundRobinLoadBalancerConfig { final long blackoutPeriodNanos; final long weightExpirationPeriodNanos; diff --git a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java index 825e4a8eca0c..596247b8234b 100644 --- a/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WeightedTargetLoadBalancer.java @@ -21,7 +21,6 @@ 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; import com.google.common.collect.ImmutableMap; import io.grpc.ConnectivityState; @@ -34,7 +33,6 @@ import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -159,7 +157,7 @@ private void updateOverallBalancingState() { if (overallState == TRANSIENT_FAILURE) { picker = new WeightedRandomPicker(errorPickers); } else { - picker = XdsSubchannelPickers.BUFFER_PICKER; + picker = LoadBalancer.EMPTY_PICKER; } } else { picker = new WeightedRandomPicker(childPickers); @@ -191,7 +189,7 @@ private static ConnectivityState aggregateState( private final class ChildHelper extends ForwardingLoadBalancerHelper { String name; ConnectivityState currentState = CONNECTING; - SubchannelPicker currentPicker = BUFFER_PICKER; + SubchannelPicker currentPicker = LoadBalancer.EMPTY_PICKER; private ChildHelper(String name) { this.name = name; diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java index b9196492624f..885844f1cbfc 100644 --- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java @@ -32,7 +32,6 @@ import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; import io.grpc.xds.XdsLogger.XdsLogLevel; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java index 9571e11e21bc..73b0a975ff44 100644 --- a/xds/src/main/java/io/grpc/xds/XdsClusterResource.java +++ b/xds/src/main/java/io/grpc/xds/XdsClusterResource.java @@ -241,6 +241,11 @@ private static StructOrError parseNonAggregateCluster( if (!edsClusterConfig.getServiceName().isEmpty()) { edsServiceName = edsClusterConfig.getServiceName(); } + // edsServiceName is required if the CDS resource has an xdstp name. + if ((edsServiceName == null) && clusterName.toLowerCase().startsWith("xdstp:")) { + return StructOrError.fromError( + "EDS service_name must be set when Cluster resource has an xdstp name"); + } return StructOrError.fromStruct(CdsUpdate.forEds( clusterName, edsServiceName, lrsServerInfo, maxConcurrentRequests, upstreamTlsContext, outlierDetection)); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 7f853dcf1ee6..29043b177d97 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -147,7 +147,11 @@ final class XdsNameResolver extends NameResolver { XdsClientPoolFactory xdsClientPoolFactory, ThreadSafeRandom random, FilterRegistry filterRegistry, @Nullable Map bootstrapOverride) { this.targetAuthority = targetAuthority; - serviceAuthority = GrpcUtil.checkAuthority(checkNotNull(name, "name")); + + // The name might have multiple slashes so encode it before verifying. + String authority = GrpcUtil.AuthorityEscaper.encodeAuthority(checkNotNull(name, "name")); + serviceAuthority = GrpcUtil.checkAuthority(authority); + this.overrideAuthority = overrideAuthority; this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser"); this.syncContext = checkNotNull(syncContext, "syncContext"); diff --git a/xds/src/main/java/io/grpc/xds/XdsResourceType.java b/xds/src/main/java/io/grpc/xds/XdsResourceType.java index 86fbf3fd6b99..1bec72d492c7 100644 --- a/xds/src/main/java/io/grpc/xds/XdsResourceType.java +++ b/xds/src/main/java/io/grpc/xds/XdsResourceType.java @@ -58,7 +58,7 @@ abstract class XdsResourceType { static boolean enableWrr = getFlag("GRPC_EXPERIMENTAL_XDS_WRR_LB", true); @VisibleForTesting - static boolean enablePickFirst = getFlag("GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG", false); + static boolean enablePickFirst = getFlag("GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG", true); static final String TYPE_URL_CLUSTER_CONFIG = "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig"; diff --git a/xds/src/main/java/io/grpc/xds/XdsSubchannelPickers.java b/xds/src/main/java/io/grpc/xds/XdsSubchannelPickers.java deleted file mode 100644 index 5c2890c34eac..000000000000 --- a/xds/src/main/java/io/grpc/xds/XdsSubchannelPickers.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2019 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.checkNotNull; - -import com.google.common.base.MoreObjects; -import io.grpc.LoadBalancer.PickResult; -import io.grpc.LoadBalancer.PickSubchannelArgs; -import io.grpc.LoadBalancer.SubchannelPicker; -import io.grpc.Status; - -final class XdsSubchannelPickers { - - private XdsSubchannelPickers() { /* DO NOT CALL ME */ } - - static final SubchannelPicker BUFFER_PICKER = new SubchannelPicker() { - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withNoResult(); - } - - @Override - public String toString() { - return "BUFFER_PICKER"; - } - }; - - static final class ErrorPicker extends SubchannelPicker { - - private final Status error; - - ErrorPicker(Status error) { - this.error = checkNotNull(error, "error"); - } - - @Override - public PickResult pickSubchannel(PickSubchannelArgs args) { - return PickResult.withError(error); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("error", error) - .toString(); - } - } -} diff --git a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProviderRegistry.java b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProviderRegistry.java index 2c320b79964c..f8ced7bb2cc4 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProviderRegistry.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/certprovider/CertificateProviderRegistry.java @@ -59,7 +59,7 @@ public synchronized void register(CertificateProviderProvider certificateProvide * Deregisters a provider. No-op if the provider is not in the registry. * * @param certificateProviderProvider the provider that was added to the registry via - * {@link #register}. + * {@link #register}. */ public synchronized void deregister(CertificateProviderProvider certificateProviderProvider) { checkNotNull(certificateProviderProvider, "certificateProviderProvider"); diff --git a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java index 6e244a438c07..d7696bbd3a63 100644 --- a/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java +++ b/xds/src/main/java/io/grpc/xds/internal/security/trust/CertificateUtils.java @@ -49,9 +49,9 @@ public final class CertificateUtils { private static CertificateFactory factory; private static final Pattern KEY_PATTERN = Pattern.compile( - "-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header - "([a-z0-9+/=\\r\\n]+)" + // Base64 text - "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer + "-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" // Header + + "([a-z0-9+/=\\r\\n]+)" // Base64 text + + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer Pattern.CASE_INSENSITIVE); private static synchronized void initInstance() throws CertificateException { diff --git a/xds/src/main/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptor.java b/xds/src/main/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptor.java index 34922dfc8874..1b767e0303cd 100644 --- a/xds/src/main/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptor.java @@ -120,7 +120,8 @@ private static OrcaLoadReport.Builder fromInternalReport(MetricReport internalRe .setRpsFractional(internalReport.getQps()) .setEps(internalReport.getEps()) .putAllUtilization(internalReport.getUtilizationMetrics()) - .putAllRequestCost(internalReport.getRequestCostMetrics()); + .putAllRequestCost(internalReport.getRequestCostMetrics()) + .putAllNamedMetrics(internalReport.getNamedMetrics()); } /** @@ -133,7 +134,8 @@ private static void mergeMetrics( MetricReport callMetricRecorderReport ) { metricRecorderReportBuilder.putAllUtilization(callMetricRecorderReport.getUtilizationMetrics()) - .putAllRequestCost(callMetricRecorderReport.getRequestCostMetrics()); + .putAllRequestCost(callMetricRecorderReport.getRequestCostMetrics()) + .putAllNamedMetrics(callMetricRecorderReport.getNamedMetrics()); // Overwrite only if the values from the given MetricReport for CallMetricRecorder are set double cpu = callMetricRecorderReport.getCpuUtilization(); if (isReportValueSet(cpu)) { diff --git a/xds/src/main/java/io/grpc/xds/orca/OrcaPerRequestUtil.java b/xds/src/main/java/io/grpc/xds/orca/OrcaPerRequestUtil.java index 97b98cd4a282..814015ba93ea 100644 --- a/xds/src/main/java/io/grpc/xds/orca/OrcaPerRequestUtil.java +++ b/xds/src/main/java/io/grpc/xds/orca/OrcaPerRequestUtil.java @@ -256,7 +256,7 @@ static MetricReport fromOrcaLoadReport(OrcaLoadReport loadReport) { return InternalCallMetricRecorder.createMetricReport(loadReport.getCpuUtilization(), loadReport.getApplicationUtilization(), loadReport.getMemUtilization(), loadReport.getRpsFractional(), loadReport.getEps(), loadReport.getRequestCostMap(), - loadReport.getUtilizationMap()); + loadReport.getUtilizationMap(), loadReport.getNamedMetricsMap()); } /** diff --git a/xds/src/main/java/io/grpc/xds/orca/OrcaServiceImpl.java b/xds/src/main/java/io/grpc/xds/orca/OrcaServiceImpl.java index 60cb3e1089f5..2fe53ab6d1cb 100644 --- a/xds/src/main/java/io/grpc/xds/orca/OrcaServiceImpl.java +++ b/xds/src/main/java/io/grpc/xds/orca/OrcaServiceImpl.java @@ -24,6 +24,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.util.Durations; import io.grpc.BindableService; +import io.grpc.ExperimentalApi; import io.grpc.ServerServiceDefinition; import io.grpc.SynchronizationContext; import io.grpc.services.InternalMetricRecorder; @@ -41,6 +42,7 @@ * Implements a {@link BindableService} that generates Out-Of-Band server metrics. * Register the returned service to the server, then a client can request for periodic load reports. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/9006") public final class OrcaServiceImpl implements BindableService { private static final Logger logger = Logger.getLogger(OrcaServiceImpl.class.getName()); diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 2ab101b730d1..7842967c0a58 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.github.xds.data.orca.v3.OrcaLoadReport; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.grpc.Attributes; @@ -45,6 +46,7 @@ import io.grpc.internal.FakeClock; import io.grpc.internal.ObjectPool; import io.grpc.internal.ServiceConfigUtil.PolicySelection; +import io.grpc.protobuf.ProtoUtils; import io.grpc.xds.Bootstrapper.ServerInfo; import io.grpc.xds.ClusterImplLoadBalancerProvider.ClusterImplConfig; import io.grpc.xds.Endpoints.DropOverload; @@ -88,11 +90,16 @@ public class ClusterImplLoadBalancerTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + private static final double TOLERANCE = 1.0e-10; private static final String AUTHORITY = "api.google.com"; private static final String CLUSTER = "cluster-foo.googleapis.com"; private static final String EDS_SERVICE_NAME = "service.googleapis.com"; private static final ServerInfo LRS_SERVER_INFO = ServerInfo.create("api.google.com", InsecureChannelCredentials.create()); + private static final Metadata.Key ORCA_ENDPOINT_LOAD_METRICS_KEY = + Metadata.Key.of( + "endpoint-load-metrics-bin", + ProtoUtils.metadataMarshaller(OrcaLoadReport.getDefaultInstance())); private final SynchronizationContext syncContext = new SynchronizationContext( new Thread.UncaughtExceptionHandler() { @Override @@ -255,7 +262,21 @@ public void recordLoadStats() { ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); // second RPC call ClientStreamTracer streamTracer3 = result.getStreamTracerFactory().newClientStreamTracer( ClientStreamTracer.StreamInfo.newBuilder().build(), new Metadata()); // third RPC call + // When the trailer contains an ORCA report, the listener callback will be invoked. + Metadata trailersWithOrcaLoadReport1 = new Metadata(); + trailersWithOrcaLoadReport1.put(ORCA_ENDPOINT_LOAD_METRICS_KEY, + OrcaLoadReport.newBuilder().setApplicationUtilization(1.414).setMemUtilization(0.034) + .setRpsFractional(1.414).putNamedMetrics("named1", 3.14159) + .putNamedMetrics("named2", -1.618).build()); + streamTracer1.inboundTrailers(trailersWithOrcaLoadReport1); streamTracer1.streamClosed(Status.OK); + Metadata trailersWithOrcaLoadReport2 = new Metadata(); + trailersWithOrcaLoadReport2.put(ORCA_ENDPOINT_LOAD_METRICS_KEY, + OrcaLoadReport.newBuilder().setApplicationUtilization(0.99).setMemUtilization(0.123) + .setRpsFractional(0.905).putNamedMetrics("named1", 2.718) + .putNamedMetrics("named2", 1.414) + .putNamedMetrics("named3", 0.009).build()); + streamTracer2.inboundTrailers(trailersWithOrcaLoadReport2); streamTracer2.streamClosed(Status.UNAVAILABLE); ClusterStats clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); @@ -266,6 +287,24 @@ public void recordLoadStats() { assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); assertThat(localityStats.totalRequestsInProgress()).isEqualTo(1L); + assertThat(localityStats.loadMetricStatsMap().containsKey("named1")).isTrue(); + assertThat( + localityStats.loadMetricStatsMap().get("named1").numRequestsFinishedWithMetric()).isEqualTo( + 2L); + assertThat(localityStats.loadMetricStatsMap().get("named1").totalMetricValue()).isWithin( + TOLERANCE).of(3.14159 + 2.718); + assertThat(localityStats.loadMetricStatsMap().containsKey("named2")).isTrue(); + assertThat( + localityStats.loadMetricStatsMap().get("named2").numRequestsFinishedWithMetric()).isEqualTo( + 2L); + assertThat(localityStats.loadMetricStatsMap().get("named2").totalMetricValue()).isWithin( + TOLERANCE).of(-1.618 + 1.414); + assertThat(localityStats.loadMetricStatsMap().containsKey("named3")).isTrue(); + assertThat( + localityStats.loadMetricStatsMap().get("named3").numRequestsFinishedWithMetric()).isEqualTo( + 1L); + assertThat(localityStats.loadMetricStatsMap().get("named3").totalMetricValue()).isWithin( + TOLERANCE).of(0.009); streamTracer3.streamClosed(Status.OK); subchannel.shutdown(); // stats recorder released @@ -278,6 +317,7 @@ public void recordLoadStats() { assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); assertThat(localityStats.totalRequestsInProgress()).isEqualTo(0L); + assertThat(localityStats.loadMetricStatsMap().isEmpty()).isTrue(); clusterStats = Iterables.getOnlyElement(loadStatsManager.getClusterStatsReports(CLUSTER)); assertThat(clusterStats.upstreamLocalityStatsList()).isEmpty(); // no longer reported diff --git a/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java index 6cb12550b602..6eab6151477d 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterManagerLoadBalancerTest.java @@ -54,7 +54,6 @@ import io.grpc.internal.ServiceConfigUtil.PolicySelection; import io.grpc.testing.TestMethodDescriptors; import io.grpc.xds.ClusterManagerLoadBalancerProvider.ClusterManagerConfig; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -126,10 +125,14 @@ public void handleResolvedAddressesUpdatesChannelPicker() { assertThat(pickSubchannel(picker, "childA")).isEqualTo(PickResult.withNoResult()); assertThat(pickSubchannel(picker, "childB")).isEqualTo(PickResult.withNoResult()); assertThat(childBalancers).hasSize(2); - FakeLoadBalancer childBalancer1 = childBalancers.get(0); - FakeLoadBalancer childBalancer2 = childBalancers.get(1); - assertThat(childBalancer1.name).isEqualTo("policy_a"); - assertThat(childBalancer2.name).isEqualTo("policy_b"); + assertThat(childBalancers.stream() + .filter(b -> b.name.equals("policy_a")) + .count()).isEqualTo(1); + assertThat(childBalancers.stream() + .filter(b -> b.name.equals("policy_b")) + .count()).isEqualTo(1); + FakeLoadBalancer childBalancer1 = getChildBalancerByName("policy_a"); + FakeLoadBalancer childBalancer2 = getChildBalancerByName("policy_b"); assertThat(childBalancer1.config).isEqualTo(lbConfigInventory.get("childA")); assertThat(childBalancer2.config).isEqualTo(lbConfigInventory.get("childB")); @@ -151,8 +154,7 @@ public void handleResolvedAddressesUpdatesChannelPicker() { assertThat(childBalancer2.shutdown).isFalse(); assertThat(childBalancers).hasSize(3); - FakeLoadBalancer childBalancer3 = childBalancers.get(2); - assertThat(childBalancer3.name).isEqualTo("policy_c"); + FakeLoadBalancer childBalancer3 = getChildBalancerByName("policy_c"); assertThat(childBalancer3.config).isEqualTo(lbConfigInventory.get("childC")); // delayed policy_b deletion @@ -166,8 +168,8 @@ public void updateBalancingStateFromChildBalancers() { deliverResolvedAddresses(ImmutableMap.of("childA", "policy_a", "childB", "policy_b")); assertThat(childBalancers).hasSize(2); - FakeLoadBalancer childBalancer1 = childBalancers.get(0); - FakeLoadBalancer childBalancer2 = childBalancers.get(1); + FakeLoadBalancer childBalancer1 = getChildBalancerByName("policy_a"); + FakeLoadBalancer childBalancer2 = getChildBalancerByName("policy_b"); Subchannel subchannel1 = mock(Subchannel.class); Subchannel subchannel2 = mock(Subchannel.class); childBalancer1.deliverSubchannelState(subchannel1, ConnectivityState.READY); @@ -184,11 +186,20 @@ public void updateBalancingStateFromChildBalancers() { .isEqualTo(subchannel2); } + private FakeLoadBalancer getChildBalancerByName(String name) { + for (FakeLoadBalancer childLb : childBalancers) { + if (childLb.name.equals(name)) { + return childLb; + } + } + return null; + } + @Test public void ignoreBalancingStateUpdateForDeactivatedChildLbs() { deliverResolvedAddresses(ImmutableMap.of("childA", "policy_a", "childB", "policy_b")); deliverResolvedAddresses(ImmutableMap.of("childB", "policy_b")); - FakeLoadBalancer childBalancer1 = childBalancers.get(0); // policy_a (deactivated) + FakeLoadBalancer childBalancer1 = getChildBalancerByName("policy_a"); // policy_a (deactivated) Subchannel subchannel = mock(Subchannel.class); childBalancer1.deliverSubchannelState(subchannel, ConnectivityState.READY); verify(helper, never()).updateBalancingState( @@ -231,8 +242,8 @@ public void handleNameResolutionError_beforeChildLbsInstantiated_returnErrorPick public void handleNameResolutionError_afterChildLbsInstantiated_propagateToChildLbs() { deliverResolvedAddresses(ImmutableMap.of("childA", "policy_a", "childB", "policy_b")); assertThat(childBalancers).hasSize(2); - FakeLoadBalancer childBalancer1 = childBalancers.get(0); - FakeLoadBalancer childBalancer2 = childBalancers.get(1); + FakeLoadBalancer childBalancer1 = getChildBalancerByName("policy_a"); + FakeLoadBalancer childBalancer2 = getChildBalancerByName("policy_b"); clusterManagerLoadBalancer.handleNameResolutionError( Status.UNAVAILABLE.withDescription("resolver error")); assertThat(childBalancer1.upstreamError.getCode()).isEqualTo(Code.UNAVAILABLE); @@ -245,8 +256,8 @@ public void handleNameResolutionError_afterChildLbsInstantiated_propagateToChild public void handleNameResolutionError_notPropagateToDeactivatedChildLbs() { deliverResolvedAddresses(ImmutableMap.of("childA", "policy_a", "childB", "policy_b")); deliverResolvedAddresses(ImmutableMap.of("childB", "policy_b")); - FakeLoadBalancer childBalancer1 = childBalancers.get(0); // policy_a (deactivated) - FakeLoadBalancer childBalancer2 = childBalancers.get(1); // policy_b + FakeLoadBalancer childBalancer1 = getChildBalancerByName("policy_a"); // policy_a (deactivated) + FakeLoadBalancer childBalancer2 = getChildBalancerByName("policy_b"); // policy_b clusterManagerLoadBalancer.handleNameResolutionError( Status.UNKNOWN.withDescription("unknown error")); assertThat(childBalancer1.upstreamError).isNull(); diff --git a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java index 604dd57b5cb5..81a23d2b5fd7 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterResolverLoadBalancerTest.java @@ -153,13 +153,13 @@ public void uncaughtException(Thread t, Throwable e) { private final NameResolverRegistry nsRegistry = new NameResolverRegistry(); private final PolicySelection roundRobin = new PolicySelection( new FakeLoadBalancerProvider("wrr_locality_experimental"), new WrrLocalityConfig( - new PolicySelection(new FakeLoadBalancerProvider("round_robin"), null))); + 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("wrr_locality_experimental"), new WrrLocalityConfig( - new PolicySelection(new FakeLoadBalancerProvider("least_request_experimental"), - new LeastRequestConfig(3)))); + 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(); diff --git a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java index 3c3a11b3cd07..910a9fc32856 100644 --- a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java +++ b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java @@ -35,6 +35,7 @@ import com.google.protobuf.util.Durations; import io.envoyproxy.envoy.config.core.v3.Node; import io.envoyproxy.envoy.config.endpoint.v3.ClusterStats; +import io.envoyproxy.envoy.config.endpoint.v3.EndpointLoadMetricStats; import io.envoyproxy.envoy.config.endpoint.v3.UpstreamLocalityStats; import io.envoyproxy.envoy.service.load_stats.v3.LoadReportingServiceGrpc; import io.envoyproxy.envoy.service.load_stats.v3.LoadStatsRequest; @@ -198,11 +199,15 @@ private void addFakeStatsData() { for (int i = 0; i < 31; i++) { localityStats1.recordCallStarted(); } + localityStats1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 3.14159)); + localityStats1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 1.618)); + localityStats1.recordBackendLoadMetricStats(ImmutableMap.of("named1", -2.718)); ClusterLocalityStats localityStats2 = loadStatsManager.getClusterLocalityStats(CLUSTER2, EDS_SERVICE_NAME2, LOCALITY2); for (int i = 0; i < 45; i++) { localityStats2.recordCallStarted(); } + localityStats2.recordBackendLoadMetricStats(ImmutableMap.of("named2", 1.414)); localityStats2.recordCallFinished(Status.OK); } @@ -245,6 +250,12 @@ public void periodicLoadReporting() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(31L); + assertThat(localityStats.getLoadMetricStatsCount()).isEqualTo(1); + EndpointLoadMetricStats loadMetricStats = Iterables.getOnlyElement( + localityStats.getLoadMetricStatsList()); + assertThat(loadMetricStats.getMetricName()).isEqualTo("named1"); + assertThat(loadMetricStats.getNumRequestsFinishedWithMetric()).isEqualTo(3L); + assertThat(loadMetricStats.getTotalMetricValue()).isEqualTo(3.14159 + 1.618 - 2.718); fakeClock.forwardTime(10L, TimeUnit.SECONDS); verify(requestObserver, times(3)).onNext(requestCaptor.capture()); @@ -263,6 +274,7 @@ public void periodicLoadReporting() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(31L); + assertThat(localityStats.getLoadMetricStatsList()).isEmpty(); // Management server updates the interval of sending load reports, while still asking for // loads to cluster1 only. @@ -287,6 +299,7 @@ public void periodicLoadReporting() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(31L); + assertThat(localityStats.getLoadMetricStatsList()).isEmpty(); // Management server asks to report loads for all clusters. responseObserver.onNext(LoadStatsResponse.newBuilder().setSendAllClusters(true) @@ -309,6 +322,7 @@ public void periodicLoadReporting() { assertThat(localityStats1.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats1.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats1.getTotalRequestsInProgress()).isEqualTo(31L); + assertThat(localityStats1.getLoadMetricStatsList()).isEmpty(); ClusterStats clusterStats2 = findClusterStats(request.getClusterStatsList(), CLUSTER2); assertThat(Durations.toSeconds(clusterStats2.getLoadReportInterval())) .isEqualTo(10L + 10L + 20L + 20L); @@ -326,6 +340,12 @@ public void periodicLoadReporting() { assertThat(localityStats2.getTotalSuccessfulRequests()).isEqualTo(1L); assertThat(localityStats2.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats2.getTotalRequestsInProgress()).isEqualTo(45L - 1L); + assertThat(localityStats2.getLoadMetricStatsCount()).isEqualTo(1); + EndpointLoadMetricStats loadMetricStats2 = Iterables.getOnlyElement( + localityStats2.getLoadMetricStatsList()); + assertThat(loadMetricStats2.getMetricName()).isEqualTo("named2"); + assertThat(loadMetricStats2.getNumRequestsFinishedWithMetric()).isEqualTo(1L); + assertThat(loadMetricStats2.getTotalMetricValue()).isEqualTo(1.414); // Load reports for cluster1 is no longer wanted. responseObserver.onNext(LoadStatsResponse.newBuilder().addClusters(CLUSTER2) @@ -348,6 +368,7 @@ public void periodicLoadReporting() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(44L); + assertThat(localityStats.getLoadMetricStatsList()).isEmpty(); fakeClock.forwardTime(10L, TimeUnit.SECONDS); verify(requestObserver, times(7)).onNext(requestCaptor.capture()); @@ -366,6 +387,7 @@ public void periodicLoadReporting() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(44L); + assertThat(localityStats.getLoadMetricStatsList()).isEmpty(); // Management server asks loads for a cluster that client has no load data. responseObserver.onNext(LoadStatsResponse.newBuilder().addClusters("unknown.googleapis.com") @@ -495,6 +517,12 @@ public void lrsStreamClosedAndRetried() { assertThat(localityStats.getTotalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.getTotalErrorRequests()).isEqualTo(0L); assertThat(localityStats.getTotalRequestsInProgress()).isEqualTo(31L); + assertThat(localityStats.getLoadMetricStatsCount()).isEqualTo(1); + EndpointLoadMetricStats loadMetricStats = Iterables.getOnlyElement( + localityStats.getLoadMetricStatsList()); + assertThat(loadMetricStats.getMetricName()).isEqualTo("named1"); + assertThat(loadMetricStats.getNumRequestsFinishedWithMetric()).isEqualTo(3L); + assertThat(loadMetricStats.getTotalMetricValue()).isEqualTo(3.14159 + 1.618 - 2.718); // Wrapping up verify(backoffPolicyProvider, times(2)).get(); diff --git a/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java b/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java index 0cfb7f46a22a..0389fa74acc6 100644 --- a/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java +++ b/xds/src/test/java/io/grpc/xds/LoadStatsManager2Test.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import io.grpc.Status; import io.grpc.internal.FakeClock; @@ -39,6 +40,7 @@ */ @RunWith(JUnit4.class) public class LoadStatsManager2Test { + private static final double TOLERANCE = 1.0e-10; private static final String CLUSTER_NAME1 = "cluster-foo.googleapis.com"; private static final String CLUSTER_NAME2 = "cluster-bar.googleapis.com"; private static final String EDS_SERVICE_NAME1 = "backend-service-foo.googleapis.com"; @@ -71,12 +73,17 @@ public void recordAndGetReport() { for (int i = 0; i < 19; i++) { loadCounter1.recordCallStarted(); } + loadCounter1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 3.14159)); + loadCounter1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 1.618)); + loadCounter1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 99.0)); + loadCounter1.recordBackendLoadMetricStats(ImmutableMap.of("named1", -97.23, "named2", -2.718)); fakeClock.forwardTime(5L, TimeUnit.SECONDS); dropCounter2.recordDroppedRequest(); loadCounter1.recordCallFinished(Status.OK); for (int i = 0; i < 9; i++) { loadCounter2.recordCallStarted(); } + loadCounter2.recordBackendLoadMetricStats(ImmutableMap.of("named3", 0.0009)); loadCounter2.recordCallFinished(Status.UNAVAILABLE); fakeClock.forwardTime(10L, TimeUnit.SECONDS); loadCounter3.recordCallStarted(); @@ -96,6 +103,18 @@ public void recordAndGetReport() { assertThat(loadStats1.totalSuccessfulRequests()).isEqualTo(1L); assertThat(loadStats1.totalErrorRequests()).isEqualTo(0L); assertThat(loadStats1.totalRequestsInProgress()).isEqualTo(19L - 1L); + assertThat(loadStats1.loadMetricStatsMap().containsKey("named1")).isTrue(); + assertThat(loadStats1.loadMetricStatsMap().containsKey("named2")).isTrue(); + assertThat( + loadStats1.loadMetricStatsMap().get("named1").numRequestsFinishedWithMetric()).isEqualTo( + 4L); + assertThat(loadStats1.loadMetricStatsMap().get("named1").totalMetricValue()).isWithin(TOLERANCE) + .of(3.14159 + 1.618 + 99 - 97.23); + assertThat( + loadStats1.loadMetricStatsMap().get("named2").numRequestsFinishedWithMetric()).isEqualTo( + 1L); + assertThat(loadStats1.loadMetricStatsMap().get("named2").totalMetricValue()).isWithin(TOLERANCE) + .of(-2.718); UpstreamLocalityStats loadStats2 = findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY2); @@ -103,6 +122,12 @@ public void recordAndGetReport() { assertThat(loadStats2.totalSuccessfulRequests()).isEqualTo(0L); assertThat(loadStats2.totalErrorRequests()).isEqualTo(1L); assertThat(loadStats2.totalRequestsInProgress()).isEqualTo(9L - 1L); + assertThat(loadStats2.loadMetricStatsMap().containsKey("named3")).isTrue(); + assertThat( + loadStats2.loadMetricStatsMap().get("named3").numRequestsFinishedWithMetric()).isEqualTo( + 1L); + assertThat(loadStats2.loadMetricStatsMap().get("named3").totalMetricValue()).isWithin(TOLERANCE) + .of(0.0009); ClusterStats stats2 = findClusterStats(allStats, CLUSTER_NAME1, EDS_SERVICE_NAME2); assertThat(stats2.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(5L + 10L)); @@ -121,6 +146,7 @@ public void recordAndGetReport() { assertThat(loadStats3.totalSuccessfulRequests()).isEqualTo(0L); assertThat(loadStats3.totalErrorRequests()).isEqualTo(0L); assertThat(loadStats3.totalRequestsInProgress()).isEqualTo(1L); + assertThat(loadStats3.loadMetricStatsMap()).isEmpty(); fakeClock.forwardTime(3L, TimeUnit.SECONDS); List clusterStatsList = loadStatsManager.getClusterStatsReports(CLUSTER_NAME1); @@ -135,11 +161,13 @@ public void recordAndGetReport() { assertThat(loadStats1.totalSuccessfulRequests()).isEqualTo(0L); assertThat(loadStats1.totalErrorRequests()).isEqualTo(0L); assertThat(loadStats1.totalRequestsInProgress()).isEqualTo(18L); // still in-progress + assertThat(loadStats1.loadMetricStatsMap()).isEmpty(); loadStats2 = findLocalityStats(stats1.upstreamLocalityStatsList(), LOCALITY2); assertThat(loadStats2.totalIssuedRequests()).isEqualTo(0L); assertThat(loadStats2.totalSuccessfulRequests()).isEqualTo(0L); assertThat(loadStats2.totalErrorRequests()).isEqualTo(0L); assertThat(loadStats2.totalRequestsInProgress()).isEqualTo(8L); // still in-progress + assertThat(loadStats2.loadMetricStatsMap()).isEmpty(); stats2 = findClusterStats(clusterStatsList, CLUSTER_NAME1, EDS_SERVICE_NAME2); assertThat(stats2.loadReportIntervalNano()).isEqualTo(TimeUnit.SECONDS.toNanos(3L)); @@ -194,9 +222,12 @@ public void sharedLoadCounterStatsAggregation() { ClusterLocalityStats ref2 = loadStatsManager.getClusterLocalityStats( CLUSTER_NAME1, EDS_SERVICE_NAME1, LOCALITY1); ref1.recordCallStarted(); + ref1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 1.618)); + ref1.recordBackendLoadMetricStats(ImmutableMap.of("named1", 3.14159)); ref1.recordCallFinished(Status.OK); ref2.recordCallStarted(); ref2.recordCallStarted(); + ref2.recordBackendLoadMetricStats(ImmutableMap.of("named1", -1.0, "named2", 2.718)); ref2.recordCallFinished(Status.UNAVAILABLE); ClusterStats stats = Iterables.getOnlyElement( @@ -207,6 +238,18 @@ public void sharedLoadCounterStatsAggregation() { assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); assertThat(localityStats.totalRequestsInProgress()).isEqualTo(1L + 2L - 1L - 1L); + assertThat(localityStats.loadMetricStatsMap().containsKey("named1")).isTrue(); + assertThat(localityStats.loadMetricStatsMap().containsKey("named2")).isTrue(); + assertThat( + localityStats.loadMetricStatsMap().get("named1").numRequestsFinishedWithMetric()).isEqualTo( + 3L); + assertThat(localityStats.loadMetricStatsMap().get("named1").totalMetricValue()).isWithin( + TOLERANCE).of(1.618 + 3.14159 - 1); + assertThat( + localityStats.loadMetricStatsMap().get("named2").numRequestsFinishedWithMetric()).isEqualTo( + 1L); + assertThat(localityStats.loadMetricStatsMap().get("named2").totalMetricValue()).isEqualTo( + 2.718); } @Test @@ -215,6 +258,8 @@ public void loadCounterDelayedDeletionAfterAllInProgressRequestsReported() { CLUSTER_NAME1, EDS_SERVICE_NAME1, LOCALITY1); counter.recordCallStarted(); counter.recordCallStarted(); + counter.recordBackendLoadMetricStats(ImmutableMap.of("named1", 2.718)); + counter.recordBackendLoadMetricStats(ImmutableMap.of("named1", 1.414)); ClusterStats stats = Iterables.getOnlyElement( loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)); @@ -224,6 +269,12 @@ public void loadCounterDelayedDeletionAfterAllInProgressRequestsReported() { assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(0L); assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); assertThat(localityStats.totalRequestsInProgress()).isEqualTo(2L); + assertThat(localityStats.loadMetricStatsMap().containsKey("named1")).isTrue(); + assertThat( + localityStats.loadMetricStatsMap().get("named1").numRequestsFinishedWithMetric()).isEqualTo( + 2L); + assertThat(localityStats.loadMetricStatsMap().get("named1").totalMetricValue()).isEqualTo( + 2.718 + 1.414); // release the counter, but requests still in-flight counter.release(); @@ -234,6 +285,7 @@ public void loadCounterDelayedDeletionAfterAllInProgressRequestsReported() { assertThat(localityStats.totalErrorRequests()).isEqualTo(0L); assertThat(localityStats.totalRequestsInProgress()) .isEqualTo(2L); // retained by in-flight calls + assertThat(localityStats.loadMetricStatsMap().isEmpty()).isTrue(); counter.recordCallFinished(Status.OK); counter.recordCallFinished(Status.UNAVAILABLE); @@ -243,6 +295,7 @@ public void loadCounterDelayedDeletionAfterAllInProgressRequestsReported() { assertThat(localityStats.totalSuccessfulRequests()).isEqualTo(1L); assertThat(localityStats.totalErrorRequests()).isEqualTo(1L); assertThat(localityStats.totalRequestsInProgress()).isEqualTo(0L); + assertThat(localityStats.loadMetricStatsMap().isEmpty()).isTrue(); assertThat(loadStatsManager.getClusterStatsReports(CLUSTER_NAME1)).isEmpty(); } diff --git a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java index a005f40fad7b..ebcd68dc950c 100644 --- a/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/PriorityLoadBalancerTest.java @@ -21,7 +21,7 @@ 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; +import static io.grpc.LoadBalancer.EMPTY_PICKER; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; @@ -41,6 +41,7 @@ import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.ErrorPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; @@ -55,7 +56,6 @@ import io.grpc.internal.TestUtils.StandardLoadBalancerProvider; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig; import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig.PriorityChildConfig; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; @@ -423,13 +423,13 @@ public void idleToConnectingDoesNotTriggerFailOver() { // p0 gets IDLE. helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 goes to CONNECTING helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // no failover happened @@ -459,15 +459,15 @@ public void connectingResetFailOverIfSeenReadyOrIdleSinceTransientFailure() { // p0 gets IDLE. helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 goes to CONNECTING, reset failover timer fakeClock.forwardTime(5, TimeUnit.SECONDS); helper0.updateBalancingState( CONNECTING, - BUFFER_PICKER); - verify(helper, times(2)).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + EMPTY_PICKER); + verify(helper, times(2)).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // failover happens fakeClock.forwardTime(10, TimeUnit.SECONDS); @@ -509,7 +509,7 @@ public PickResult pickSubchannel(PickSubchannelArgs args) { // p0 goes to CONNECTING helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // no failover happened @@ -560,7 +560,7 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { // p0 gets IDLE. helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 fails over to p1 immediately. @@ -581,13 +581,13 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { // p2 gets IDLE helper2.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 gets back to IDLE helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p2 fails but does not affect overall picker @@ -614,13 +614,13 @@ public void typicalPriorityFailOverFlowWithIdleUpdate() { // p2 gets back to IDLE helper2.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 gets back to IDLE helper0.updateBalancingState( IDLE, - BUFFER_PICKER); + EMPTY_PICKER); assertCurrentPickerIsBufferPicker(); // p0 fails over to p2 and picker is updated to p2's existing picker. diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index f86b36df5e8b..29af01b222f2 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import com.google.api.expr.v1alpha1.Expr; +import com.google.common.collect.ImmutableList; import com.google.protobuf.Any; import com.google.protobuf.Message; import com.google.protobuf.UInt32Value; @@ -339,6 +340,22 @@ public void ignoredConfig() { assertThat(result.config).isEqualTo(RbacConfig.create(null)); } + @Test + public void testOrderIndependenceOfPolicies() { + Message rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), true); + ConfigOrError ascFirst = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + + rawProto = buildComplexRbac(ImmutableList.of(1, 2, 3, 4, 5, 6), false); + ConfigOrError ascLast = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + + assertThat(ascFirst.config).isEqualTo(ascLast.config); + + rawProto = buildComplexRbac(ImmutableList.of(6, 5, 4, 3, 2, 1), true); + ConfigOrError decFirst = new RbacFilter().parseFilterConfig(Any.pack(rawProto)); + + assertThat(ascFirst.config).isEqualTo(decFirst.config); + } + private static Metadata metadata(String key, String value) { Metadata metadata = new Metadata(); metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value); @@ -369,11 +386,61 @@ private ConfigOrError parseRaw(List permissionList, private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildRbac( List permissionList, List principalList) { return io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() - .setRules(RBAC.newBuilder().setAction(Action.DENY) - .putPolicies("policy-name", Policy.newBuilder() - .addAllPermissions(permissionList) - .addAllPrincipals(principalList).build()).build()).build(); + .setRules(buildRbacRule("policy-name", Action.DENY, + permissionList, principalList)).build(); + } + + private static RBAC buildRbacRule(String policyName, Action action, + List permissionList, List principalList) { + return RBAC.newBuilder().setAction(action) + .putPolicies(policyName, Policy.newBuilder() + .addAllPermissions(permissionList) + .addAllPrincipals(principalList).build()) + .build(); + } + + private io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC buildComplexRbac( + List ids, boolean listsFirst) { + Policy policy1 = createSimplePolicyUsingLists(0); + + RBAC.Builder ruleBuilder = RBAC.newBuilder().setAction(Action.DENY); + + if (listsFirst) { + ruleBuilder.putPolicies("list-policy", policy1); + } + + String base = "filterConfig\\u003dRbacConfig{authConfig\\u003dAuthConfig{policies\\u003d[Poli" + + "cyMatcher{name\\u003dpsm-interop-authz-policy-20230514-0917-er2uh_td_rbac_rule_"; + + for (Integer id : ids) { + ruleBuilder.putPolicies(base + id, createSimplePolicyUsingLists(id)); + } + + if (!listsFirst) { + ruleBuilder.putPolicies("list-policy", policy1); + } + + return io.envoyproxy.envoy.extensions.filters.http.rbac.v3.RBAC.newBuilder() + .setRules(ruleBuilder.build()).build(); + } + + private static Policy createSimplePolicyUsingLists(int id) { + CidrRange cidrRange = CidrRange.newBuilder().setAddressPrefix("10.10." + id + ".0") + .setPrefixLen(UInt32Value.of(24)).build(); + List permissionList = Arrays.asList( + Permission.newBuilder().setAndRules(Permission.Set.newBuilder() + .addRules(Permission.newBuilder().setDestinationIp(cidrRange).build()) + .addRules(Permission.newBuilder().setDestinationPort(9090).build()).build() + ).build()); + List principalList = Arrays.asList( + Principal.newBuilder().setAndIds(Principal.Set.newBuilder() + .addIds(Principal.newBuilder().setDirectRemoteIp(cidrRange).build()) + .addIds(Principal.newBuilder().setRemoteIp(cidrRange).build()) + .build()).build()); + return Policy.newBuilder() + .addAllPermissions(permissionList) + .addAllPrincipals(principalList).build(); } private ConfigOrError parseOverride(List permissionList, diff --git a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java index daf58a174d99..ac08f69f88cf 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedRoundRobinLoadBalancerTest.java @@ -52,7 +52,7 @@ import io.grpc.internal.FakeClock; import io.grpc.services.InternalCallMetricRecorder; import io.grpc.services.MetricReport; -import io.grpc.xds.WeightedRoundRobinLoadBalancer.EdfScheduler; +import io.grpc.xds.WeightedRoundRobinLoadBalancer.StaticStrideScheduler; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinLoadBalancerConfig; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WeightedRoundRobinPicker; import io.grpc.xds.WeightedRoundRobinLoadBalancer.WrrSubchannel; @@ -175,7 +175,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { } }); wrr = new WeightedRoundRobinLoadBalancer(helper, fakeClock.getDeadlineTicker(), - new FakeRandom()); + new FakeRandom(0)); } @Test @@ -214,13 +214,13 @@ public void wrrLifeCycle() { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); assertThat(weightedPicker.pickSubchannel(mockArgs) - .getSubchannel()).isEqualTo(weightedSubchannel1); + .getSubchannel()).isEqualTo(weightedSubchannel1); assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedConfig = WeightedRoundRobinLoadBalancerConfig.newBuilder() .setWeightUpdatePeriodNanos(500_000_000L) //.5s @@ -260,10 +260,10 @@ public void enableOobLoadReportConfig() { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.9, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.9, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); PickResult pickResult = weightedPicker.pickSubchannel(mockArgs); assertThat(pickResult.getSubchannel()).isEqualTo(weightedSubchannel1); @@ -330,66 +330,69 @@ weightedSubchannel3.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(3); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 10000.0 - subchannel1PickRatio)) - .isAtMost(0.001); + .isLessThan(0.0002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 10000.0 - subchannel2PickRatio )) - .isAtMost(0.001); + .isLessThan(0.0002); assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 10000.0 - subchannel3PickRatio )) - .isAtMost(0.001); + .isLessThan(0.0002); } @Test - public void pickByWeight_LargeWeight() { + public void pickByWeight_largeWeight() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 999, 0, new HashMap<>(), new HashMap<>()); + 0.1, 0, 0.1, 999, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.9, 0, 0.1, 2, 0, new HashMap<>(), new HashMap<>()); + 0.9, 0, 0.1, 2, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 100, 0, new HashMap<>(), new HashMap<>()); - double totalWeight = 999 / 0.1 + 2 / 0.9 + 100 / 0.86; - - pickByWeight(report1, report2, report3, 999 / 0.1 / totalWeight, 2 / 0.9 / totalWeight, - 100 / 0.86 / totalWeight); + 0.86, 0, 0.1, 100, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); + double meanWeight = (999 / 0.1 + 2 / 0.9 + 100 / 0.86) / 3; + double cappedMin = meanWeight * 0.1; // min capped at minRatio * meanWeight + double totalWeight = 999 / 0.1 + cappedMin + cappedMin; + pickByWeight(report1, report2, report3, 999 / 0.1 / totalWeight, cappedMin / totalWeight, + cappedMin / totalWeight); } @Test public void pickByWeight_largeWeight_useApplicationUtilization() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.44, 0.1, 0.1, 999, 0, new HashMap<>(), new HashMap<>()); + 0.44, 0.1, 0.1, 999, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.12, 0.9, 0.1, 2, 0, new HashMap<>(), new HashMap<>()); + 0.12, 0.9, 0.1, 2, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.33, 0.86, 0.1, 100, 0, new HashMap<>(), new HashMap<>()); - double totalWeight = 999 / 0.1 + 2 / 0.9 + 100 / 0.86; - - pickByWeight(report1, report2, report3, 999 / 0.1 / totalWeight, 2 / 0.9 / totalWeight, - 100 / 0.86 / totalWeight); + 0.33, 0.86, 0.1, 100, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); + double meanWeight = (999 / 0.1 + 2 / 0.9 + 100 / 0.86) / 3; + double cappedMin = meanWeight * 0.1; + double totalWeight = 999 / 0.1 + cappedMin + cappedMin; // min capped at minRatio * meanWeight + pickByWeight(report1, report2, report3, 999 / 0.1 / totalWeight, cappedMin / totalWeight, + cappedMin / totalWeight); } @Test public void pickByWeight_largeWeight_withEps_defaultErrorUtilizationPenalty() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 999, 13, new HashMap<>(), new HashMap<>()); + 0.1, 0, 0.1, 999, 13, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.9, 0, 0.1, 2, 1.8, new HashMap<>(), new HashMap<>()); + 0.9, 0, 0.1, 2, 1.8, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 100, 3, new HashMap<>(), new HashMap<>()); - double weight1 = 999 / (0.1 + 13 / 999F * weightedConfig.errorUtilizationPenalty); - double weight2 = 2 / (0.9 + 1.8 / 2F * weightedConfig.errorUtilizationPenalty); - double weight3 = 100 / (0.86 + 3 / 100F * weightedConfig.errorUtilizationPenalty); - double totalWeight = weight1 + weight2 + weight3; - - pickByWeight(report1, report2, report3, weight1 / totalWeight, weight2 / totalWeight, - weight3 / totalWeight); + 0.86, 0, 0.1, 100, 3, new HashMap<>(), new HashMap<>(), new HashMap<>()); + double weight1 = 999 / (0.1 + 13 / 999F * weightedConfig.errorUtilizationPenalty); // ~5609.899 + double weight2 = 2 / (0.9 + 1.8 / 2F * weightedConfig.errorUtilizationPenalty); // ~0.317 + double weight3 = 100 / (0.86 + 3 / 100F * weightedConfig.errorUtilizationPenalty); // ~96.154 + double meanWeight = (weight1 + weight2 + weight3) / 3; + double cappedMin = meanWeight * 0.1; // min capped at minRatio * meanWeight + double totalWeight = weight1 + cappedMin + cappedMin; + pickByWeight(report1, report2, report3, weight1 / totalWeight, cappedMin / totalWeight, + cappedMin / totalWeight); } @Test public void pickByWeight_normalWeight() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.12, 0, 0.1, 22, 0, new HashMap<>(), new HashMap<>()); + 0.12, 0, 0.1, 22, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.28, 0, 0.1, 40, 0, new HashMap<>(), new HashMap<>()); + 0.28, 0, 0.1, 40, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 100, 0, new HashMap<>(), new HashMap<>()); + 0.86, 0, 0.1, 100, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); double totalWeight = 22 / 0.12 + 40 / 0.28 + 100 / 0.86; pickByWeight(report1, report2, report3, 22 / 0.12 / totalWeight, 40 / 0.28 / totalWeight, 100 / 0.86 / totalWeight @@ -399,11 +402,11 @@ public void pickByWeight_normalWeight() { @Test public void pickByWeight_normalWeight_useApplicationUtilization() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.72, 0.12, 0.1, 22, 0, new HashMap<>(), new HashMap<>()); + 0.72, 0.12, 0.1, 22, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.98, 0.28, 0.1, 40, 0, new HashMap<>(), new HashMap<>()); + 0.98, 0.28, 0.1, 40, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.99, 0.86, 0.1, 100, 0, new HashMap<>(), new HashMap<>()); + 0.99, 0.86, 0.1, 100, 0, new HashMap<>(), new HashMap<>(), new HashMap<>()); double totalWeight = 22 / 0.12 + 40 / 0.28 + 100 / 0.86; pickByWeight(report1, report2, report3, 22 / 0.12 / totalWeight, 40 / 0.28 / totalWeight, 100 / 0.86 / totalWeight @@ -413,11 +416,11 @@ public void pickByWeight_normalWeight_useApplicationUtilization() { @Test public void pickByWeight_normalWeight_withEps_defaultErrorUtilizationPenalty() { MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.12, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>()); + 0.12, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.28, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>()); + 0.28, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>()); + 0.86, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>(), new HashMap<>()); double weight1 = 22 / (0.12 + 19.7 / 22F * weightedConfig.errorUtilizationPenalty); double weight2 = 40 / (0.28 + 0.998 / 40F * weightedConfig.errorUtilizationPenalty); double weight3 = 100 / (0.86 + 3.14159 / 100F * weightedConfig.errorUtilizationPenalty); @@ -433,11 +436,11 @@ public void pickByWeight_normalWeight_withEps_customErrorUtilizationPenalty() { .setErrorUtilizationPenalty(1.75F).build(); MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0.12, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>()); + 0.12, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0.28, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>()); + 0.28, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0.86, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>()); + 0.86, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>(), new HashMap<>()); double weight1 = 22 / (0.12 + 19.7 / 22F * weightedConfig.errorUtilizationPenalty); double weight2 = 40 / (0.28 + 0.998 / 40F * weightedConfig.errorUtilizationPenalty); double weight3 = 100 / (0.86 + 3.14159 / 100F * weightedConfig.errorUtilizationPenalty); @@ -453,11 +456,11 @@ public void pickByWeight_avgWeight_zeroCpuUtilization_withEps_customErrorUtiliza .setErrorUtilizationPenalty(1.75F).build(); MetricReport report1 = InternalCallMetricRecorder.createMetricReport( - 0, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>()); + 0, 0, 0.1, 22, 19.7, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report2 = InternalCallMetricRecorder.createMetricReport( - 0, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>()); + 0, 0, 0.1, 40, 0.998, new HashMap<>(), new HashMap<>(), new HashMap<>()); MetricReport report3 = InternalCallMetricRecorder.createMetricReport( - 0, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>()); + 0, 0, 0.1, 100, 3.14159, new HashMap<>(), new HashMap<>(), new HashMap<>()); double avgSubchannelPickRatio = 1.0 / 3; pickByWeight(report1, report2, report3, avgSubchannelPickRatio, avgSubchannelPickRatio, @@ -508,10 +511,10 @@ public void blackoutPeriod() { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -520,8 +523,8 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(2); // within blackout period, fallback to simple round robin - assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)).isAtMost(0.001); - assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)).isAtMost(0.001); + assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)).isLessThan(0.002); + assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)).isLessThan(0.002); assertThat(fakeClock.forwardTime(5, TimeUnit.SECONDS)).isEqualTo(1); pickCount = new HashMap<>(); @@ -532,9 +535,9 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt assertThat(pickCount.size()).isEqualTo(2); // after blackout period assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); } @Test @@ -568,10 +571,10 @@ public void updateWeightTimer() { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(11, TimeUnit.SECONDS)).isEqualTo(1); assertThat(weightedPicker.pickSubchannel(mockArgs) .getSubchannel()).isEqualTo(weightedSubchannel1); @@ -585,14 +588,15 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt assertThat(fakeClock.getPendingTasks().size()).isEqualTo(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); //timer fires, new weight updated assertThat(fakeClock.forwardTime(500, TimeUnit.MILLISECONDS)).isEqualTo(1); assertThat(weightedPicker.pickSubchannel(mockArgs) .getSubchannel()).isEqualTo(weightedSubchannel2); + } @Test @@ -619,10 +623,10 @@ public void weightExpired() { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -631,9 +635,9 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(2); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 2.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 1.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); // weight expired, fallback to simple round robin assertThat(fakeClock.forwardTime(300, TimeUnit.SECONDS)).isEqualTo(1); @@ -644,9 +648,9 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(2); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 0.5)) - .isAtMost(0.001); + .isLessThan(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 0.5)) - .isAtMost(0.001); + .isLessThan(0.002); } @Test @@ -684,7 +688,7 @@ public void rrFallback() { subchannel.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( 0.1, 0, 0.1, qpsByChannel.get(subchannel), 0, - new HashMap<>(), new HashMap<>())); + new HashMap<>(), new HashMap<>(), new HashMap<>())); } assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 1.0 / 2)) .isAtMost(0.1); @@ -700,7 +704,7 @@ subchannel.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoad subchannel.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( 0.1, 0, 0.1, qpsByChannel.get(subchannel), 0, - new HashMap<>(), new HashMap<>())); + new HashMap<>(), new HashMap<>(), new HashMap<>())); fakeClock.forwardTime(50, TimeUnit.MILLISECONDS); } assertThat(pickCount.size()).isEqualTo(2); @@ -738,10 +742,10 @@ public void unknownWeightIsAvgWeight() { WrrSubchannel weightedSubchannel3 = (WrrSubchannel) weightedPicker.getList().get(2); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); assertThat(fakeClock.forwardTime(10, TimeUnit.SECONDS)).isEqualTo(1); Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { @@ -750,12 +754,12 @@ weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalt } assertThat(pickCount.size()).isEqualTo(3); assertThat(Math.abs(pickCount.get(weightedSubchannel1) / 1000.0 - 4.0 / 9)) - .isAtMost(0.001); + .isLessThan(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2) / 1000.0 - 2.0 / 9)) - .isAtMost(0.001); + .isLessThan(0.002); // subchannel3's weight is average of subchannel1 and subchannel2 assertThat(Math.abs(pickCount.get(weightedSubchannel3) / 1000.0 - 3.0 / 9)) - .isAtMost(0.001); + .isLessThan(0.002); } @Test @@ -782,10 +786,10 @@ public void pickFromOtherThread() throws Exception { WrrSubchannel weightedSubchannel2 = (WrrSubchannel) weightedPicker.getList().get(1); weightedSubchannel1.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.1, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); weightedSubchannel2.new OrcaReportListener(weightedConfig.errorUtilizationPenalty).onLoadReport( InternalCallMetricRecorder.createMetricReport( - 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>())); + 0.2, 0, 0.1, 1, 0, new HashMap<>(), new HashMap<>(), new HashMap<>())); CyclicBarrier barrier = new CyclicBarrier(2); Map pickCount = new ConcurrentHashMap<>(); pickCount.put(weightedSubchannel1, new AtomicInteger(0)); @@ -816,50 +820,263 @@ public void run() { assertThat(pickCount.size()).isEqualTo(2); // after blackout period assertThat(Math.abs(pickCount.get(weightedSubchannel1).get() / 2000.0 - 2.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); assertThat(Math.abs(pickCount.get(weightedSubchannel2).get() / 2000.0 - 1.0 / 3)) - .isAtMost(0.001); + .isLessThan(0.002); + } + + @Test(expected = NullPointerException.class) + public void wrrConfig_TimeValueNonNull() { + WeightedRoundRobinLoadBalancerConfig.newBuilder().setBlackoutPeriodNanos((Long) null); + } + + @Test(expected = NullPointerException.class) + public void wrrConfig_BooleanValueNonNull() { + WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport((Boolean) null); + } + + @Test(expected = IllegalArgumentException.class) + public void emptyWeights() { + float[] weights = {}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + sss.pick(); + } + + @Test + public void testPicksEqualsWeights() { + float[] weights = {1.0f, 2.0f, 3.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {1, 2, 3}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testContainsZeroWeightUseMean() { + float[] weights = {3.0f, 0.0f, 1.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {3, 2, 1}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testContainsNegativeWeightUseMean() { + float[] weights = {3.0f, -1.0f, 1.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {3, 2, 1}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllSameWeights() { + float[] weights = {1.0f, 1.0f, 1.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllZeroWeightsIsRoundRobin() { + float[] weights = {0.0f, 0.0f, 0.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); + } + + @Test + public void testAllInvalidWeightsIsRoundRobin() { + float[] weights = {-3.1f, -0.0f, 0.0f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + int[] expectedPicks = new int[] {2, 2, 2}; + int[] picks = new int[3]; + for (int i = 0; i < 6; i++) { + picks[sss.pick()] += 1; + } + assertThat(picks).isEqualTo(expectedPicks); } @Test - public void edfScheduler() { - Random random = new Random(); - double totalWeight = 0; - int capacity = random.nextInt(10) + 1; - double[] weights = new double[capacity]; - EdfScheduler scheduler = new EdfScheduler(capacity, random); - for (int i = 0; i < capacity; i++) { - weights[i] = random.nextDouble(); - scheduler.add(i, weights[i]); - totalWeight += weights[i]; + public void testTwoWeights() { + float[] weights = {1.43f, 2.119f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + double totalWeight = 1.43 + 2.119; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } + for (int i = 0; i < 2; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isLessThan(0.002); + } + } + + @Test + public void testManyWeights() { + float[] weights = {1.3f, 2.5f, 3.23f, 4.11f, 7.001f}; + Random random = new Random(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(random.nextInt())); + double totalWeight = 1.3 + 2.5 + 3.23 + 4.11 + 7.001; Map pickCount = new HashMap<>(); for (int i = 0; i < 1000; i++) { - int result = scheduler.pick(); + int result = sss.pick(); pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); } - for (int i = 0; i < capacity; i++) { - assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight) ) - .isAtMost(0.01); + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isLessThan(0.002); } } @Test - public void edsScheduler_sameWeight() { - EdfScheduler scheduler = new EdfScheduler(2, new FakeRandom()); - scheduler.add(0, 0.5); - scheduler.add(1, 0.5); - assertThat(scheduler.pick()).isEqualTo(0); + public void testMaxClamped() { + float[] weights = {81f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, + 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f}; + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(0)); + int[] picks = new int[weights.length]; + + // max gets clamped to mean*maxRatio = 50 for this set of weights. So if we + // pick 50 + 19 times we should get all possible picks. + for (int i = 1; i < 70; i++) { + picks[sss.pick()] += 1; + } + int[] expectedPicks = new int[] {50, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + assertThat(picks).isEqualTo(expectedPicks); } - @Test(expected = NullPointerException.class) - public void wrrConfig_TimeValueNonNull() { - WeightedRoundRobinLoadBalancerConfig.newBuilder().setBlackoutPeriodNanos((Long) null); + @Test + public void testMinClamped() { + float[] weights = {100f, 1e-10f}; + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(0)); + int[] picks = new int[weights.length]; + + // We pick 201 elements and ensure that the second channel (with epsilon + // weight) also gets picked. The math is: mean value of elements is ~50, so + // the first channel keeps its weight of 100, but the second element's weight + // gets capped from below to 50*0.1 = 5. + for (int i = 0; i < 105; i++) { + picks[sss.pick()] += 1; + } + int[] expectedPicks = new int[] {100, 5}; + assertThat(picks).isEqualTo(expectedPicks); } - @Test(expected = NullPointerException.class) - public void wrrConfig_BooleanValueNonNull() { - WeightedRoundRobinLoadBalancerConfig.newBuilder().setEnableOobLoadReport((Boolean) null); + @Test + public void testDeterministicPicks() { + float[] weights = {2.0f, 3.0f, 6.0f}; + AtomicInteger sequence = new AtomicInteger(0); + VerifyingScheduler sss = new VerifyingScheduler(weights, sequence); + assertThat(sequence.get()).isEqualTo(0); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sequence.get()).isEqualTo(2); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(3); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(6); + assertThat(sss.pick()).isEqualTo(0); + assertThat(sequence.get()).isEqualTo(7); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sequence.get()).isEqualTo(8); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(9); + } + + @Test + public void testImmediateWraparound() { + float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(-1)); + double totalWeight = 15; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isLessThan(0.002); + } + } + + @Test + public void testWraparound() { + float[] weights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + VerifyingScheduler sss = new VerifyingScheduler(weights, new AtomicInteger(-500)); + double totalWeight = 15; + Map pickCount = new HashMap<>(); + for (int i = 0; i < 1000; i++) { + int result = sss.pick(); + pickCount.put(result, pickCount.getOrDefault(result, 0) + 1); + } + for (int i = 0; i < 5; i++) { + assertThat(Math.abs(pickCount.getOrDefault(i, 0) / 1000.0 - weights[i] / totalWeight)) + .isLessThan(0.002); + } + } + + @Test + public void testDeterministicWraparound() { + float[] weights = {2.0f, 3.0f, 6.0f}; + AtomicInteger sequence = new AtomicInteger(-1); + VerifyingScheduler sss = new VerifyingScheduler(weights, sequence); + assertThat(sequence.get()).isEqualTo(-1); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sequence.get()).isEqualTo(2); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(3); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(6); + assertThat(sss.pick()).isEqualTo(0); + assertThat(sequence.get()).isEqualTo(7); + assertThat(sss.pick()).isEqualTo(1); + assertThat(sequence.get()).isEqualTo(8); + assertThat(sss.pick()).isEqualTo(2); + assertThat(sequence.get()).isEqualTo(9); + } + + private static final class VerifyingScheduler { + private final StaticStrideScheduler delegate; + private final int max; + private final AtomicInteger sequence; + + public VerifyingScheduler(float[] weights, AtomicInteger sequence) { + this.delegate = new StaticStrideScheduler(weights, sequence); + this.max = weights.length; + this.sequence = sequence; + } + + public int pick() { + int start = sequence.get(); + int i = delegate.pick(); + assertThat(sequence.get() - start).isAtMost(max); + return i; + } } private static class FakeSocketAddress extends SocketAddress { @@ -875,10 +1092,16 @@ private static class FakeSocketAddress extends SocketAddress { } private static class FakeRandom extends Random { + private int nextInt; + + public FakeRandom(int nextInt) { + this.nextInt = nextInt; + } + @Override - public double nextDouble() { + public int nextInt() { // return constant value to disable init deadline randomization in the scheduler - return 0.322023; + return nextInt; } } } diff --git a/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java index 91ab1e8fac4c..6cec8b0fb6ff 100644 --- a/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WeightedTargetLoadBalancerTest.java @@ -20,7 +20,7 @@ import static io.grpc.ConnectivityState.CONNECTING; import static io.grpc.ConnectivityState.READY; import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; -import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER; +import static io.grpc.LoadBalancer.EMPTY_PICKER; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; @@ -39,6 +39,7 @@ import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.ErrorPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.PickResult; import io.grpc.LoadBalancer.PickSubchannelArgs; @@ -52,7 +53,6 @@ import io.grpc.xds.WeightedRandomPicker.WeightedChildPicker; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; @@ -209,7 +209,7 @@ public void handleResolvedAddresses() { .setAttributes(Attributes.newBuilder().set(fakeKey, fakeValue).build()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) .build()); - verify(helper).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); assertThat(childBalancers).hasSize(4); assertThat(childHelpers).hasSize(4); assertThat(fooLbCreated).isEqualTo(2); @@ -246,7 +246,7 @@ public void handleResolvedAddresses() { .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(newTargets)) .build()); - verify(helper, atLeast(2)).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper, atLeast(2)).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); assertThat(childBalancers).hasSize(5); assertThat(childHelpers).hasSize(5); assertThat(fooLbCreated).isEqualTo(3); // One more foo LB created for target4 @@ -288,7 +288,7 @@ public void handleNameResolutionError() { .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) .build()); - verify(helper).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // Error after child balancers created. weightedTargetLb.handleNameResolutionError(Status.ABORTED); @@ -315,7 +315,7 @@ public void balancingStateUpdatedFromChildBalancers() { .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) .build()); - verify(helper).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // Subchannels to be created for each child balancer. final SubchannelPicker[] subchannelPickers = new SubchannelPicker[]{ @@ -335,7 +335,7 @@ public void balancingStateUpdatedFromChildBalancers() { childHelpers.get(1).updateBalancingState(TRANSIENT_FAILURE, failurePickers[1]); verify(helper, never()).updateBalancingState( eq(TRANSIENT_FAILURE), any(SubchannelPicker.class)); - verify(helper, times(2)).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper, times(2)).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // Another child balancer goes to READY. childHelpers.get(2).updateBalancingState(READY, subchannelPickers[2]); @@ -396,7 +396,7 @@ public void raceBetweenShutdownAndChildLbBalancingStateUpdate() { .setAddresses(ImmutableList.of()) .setLoadBalancingPolicyConfig(new WeightedTargetConfig(targets)) .build()); - verify(helper).updateBalancingState(eq(CONNECTING), eq(BUFFER_PICKER)); + verify(helper).updateBalancingState(eq(CONNECTING), eq(EMPTY_PICKER)); // LB shutdown and subchannel state change can happen simultaneously. If shutdown runs first, // any further balancing state update should be ignored. diff --git a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java index 344876aa348a..bb80635f1bdd 100644 --- a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java @@ -30,6 +30,7 @@ import io.grpc.ConnectivityState; import io.grpc.EquivalentAddressGroup; import io.grpc.LoadBalancer; +import io.grpc.LoadBalancer.ErrorPicker; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancer.ResolvedAddresses; import io.grpc.LoadBalancer.SubchannelPicker; @@ -41,7 +42,6 @@ import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedPolicySelection; import io.grpc.xds.WeightedTargetLoadBalancerProvider.WeightedTargetConfig; import io.grpc.xds.WrrLocalityLoadBalancer.WrrLocalityConfig; -import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; import java.net.SocketAddress; import java.util.Collections; import java.util.List; diff --git a/xds/src/test/java/io/grpc/xds/XdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/XdsClientImplTestBase.java index 56e37e7192f5..c18b324b14c6 100644 --- a/xds/src/test/java/io/grpc/xds/XdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/XdsClientImplTestBase.java @@ -2444,6 +2444,34 @@ public void cdsResponseErrorHandling_badTransportSocketName() { verifyStatusWithNodeId(errorCaptor.getValue(), Code.UNAVAILABLE, errorMsg); } + @Test + public void cdsResponseErrorHandling_xdstpWithoutEdsConfig() { + String cdsResourceName = "xdstp://authority.xds.com/envoy.config.cluster.v3.Cluster/cluster1"; + + final Any testClusterRoundRobin = + Any.pack(mf.buildEdsCluster(cdsResourceName, null, "round_robin", null, + null, false, null, "envoy.transport_sockets.tls", null, null + )); + final Any okClusterRoundRobin = + Any.pack(mf.buildEdsCluster(cdsResourceName, "eds-service-bar.googleapis.com", + "round_robin", null,null, false, null, "envoy.transport_sockets.tls", null, null)); + + + DiscoveryRpcCall call = startResourceWatcher(XdsClusterResource.getInstance(), + cdsResourceName, cdsResourceWatcher); + call.sendResponse(CDS, testClusterRoundRobin, VERSION_1, "0000"); + + List errors = ImmutableList.of("CDS response Cluster " + + "\'xdstp://authority.xds.com/envoy.config.cluster.v3.Cluster/cluster1\' " + + "validation error: EDS service_name must be set when Cluster resource has an xdstp name"); + call.verifyRequest(CDS, cdsResourceName, "", "", NODE); // get this out of the way + call.verifyRequestNack(CDS, cdsResourceName, "", "0000", NODE, errors); + verifySubscribedResourcesMetadataSizes(0, 1, 0, 0); + + call.sendResponse(CDS, okClusterRoundRobin, VERSION_1, "0001"); + call.verifyRequest(CDS, cdsResourceName, VERSION_1, "0001", NODE); + } + @Test @SuppressWarnings("unchecked") public void cachedCdsResource_data() { diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java index 95e3f2f997fb..a216c3de0281 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverProviderTest.java @@ -110,14 +110,19 @@ public void validName_noAuthority() { } @Test - public void invalidName_hostnameContainsUnderscore() { - URI uri = URI.create("xds:///foo_bar.googleapis.com"); - try { - provider.newNameResolver(uri, args); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // Expected - } + public void validName_urlExtractedAuthorityInvalidWithoutEncoding() { + XdsNameResolver resolver = + provider.newNameResolver(URI.create("xds:///1234/path/foo.googleapis.com:8080"), args); + assertThat(resolver).isNotNull(); + assertThat(resolver.getServiceAuthority()).isEqualTo("1234%2Fpath%2Ffoo.googleapis.com:8080"); + } + + @Test + public void validName_urlwithTargetAuthorityAndExtractedAuthorityInvalidWithoutEncoding() { + XdsNameResolver resolver = provider.newNameResolver(URI.create( + "xds://trafficdirector.google.com/1234/path/foo.googleapis.com:8080"), args); + assertThat(resolver).isNotNull(); + assertThat(resolver.getServiceAuthority()).isEqualTo("1234%2Fpath%2Ffoo.googleapis.com:8080"); } @Test diff --git a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java index d7ac5bdd3cb9..c51327dc84d9 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestControlPlaneService.java @@ -122,8 +122,8 @@ public void run() { @Override public StreamObserver streamAggregatedResources( final StreamObserver responseObserver) { - final StreamObserver requestObserver = - new StreamObserver() { + + final class AdsStreamObserver implements StreamObserver { @Override public void onNext(final DiscoveryRequest value) { syncContext.execute(new Runnable() { @@ -176,8 +176,9 @@ public void onCompleted() { xdsNonces.get(type).remove(responseObserver); } } - }; - return requestObserver; + } + + return new AdsStreamObserver(); } //must run in syncContext diff --git a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java index 8a4123d54a3d..753bc967089c 100644 --- a/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java +++ b/xds/src/test/java/io/grpc/xds/internal/security/SecurityProtocolNegotiatorsTest.java @@ -233,7 +233,7 @@ public SocketAddress remoteAddress() { ProtocolNegotiationEvent event = InternalProtocolNegotiationEvent.getDefault(); Attributes attr = InternalProtocolNegotiationEvent.getAttributes(event) .toBuilder().set(ATTR_SERVER_SSL_CONTEXT_PROVIDER_SUPPLIER, - new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager)).build(); + new SslContextProviderSupplier(downstreamTlsContext, tlsContextManager)).build(); pipeline.fireUserEventTriggered(InternalProtocolNegotiationEvent.withAttributes(event, attr)); channelHandlerCtx = pipeline.context(handlerPickerHandler); assertThat(channelHandlerCtx).isNull(); diff --git a/xds/src/test/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptorTest.java b/xds/src/test/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptorTest.java index 00562be3dc52..ec56467e5a5f 100644 --- a/xds/src/test/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/orca/OrcaMetricReportingServerInterceptorTest.java @@ -72,6 +72,7 @@ public class OrcaMetricReportingServerInterceptorTest { private final Map applicationUtilizationMetricsMap = new HashMap<>(); private final Map applicationCostMetrics = new HashMap<>(); + private final Map applicationNamedMetrics = new HashMap<>(); private double cpuUtilizationMetrics = 0; private double applicationUtilizationMetrics = 0; private double memoryUtilizationMetrics = 0; @@ -98,6 +99,9 @@ public void unaryRpc( CallMetricRecorder.getCurrent().recordRequestCostMetric(entry.getKey(), entry.getValue()); } + for (Map.Entry entry : applicationNamedMetrics.entrySet()) { + CallMetricRecorder.getCurrent().recordNamedMetric(entry.getKey(), entry.getValue()); + } CallMetricRecorder.getCurrent().recordCpuUtilizationMetric(cpuUtilizationMetrics); CallMetricRecorder.getCurrent() .recordApplicationUtilizationMetric(applicationUtilizationMetrics); @@ -133,8 +137,8 @@ public void unaryRpc( @Test public void shareCallMetricRecorderInContext() throws IOException { final CallMetricRecorder callMetricRecorder = new CallMetricRecorder(); - ServerStreamTracer.Factory callMetricRecorderSharingStreamTracerFactory = - new ServerStreamTracer.Factory() { + ServerStreamTracer.Factory callMetricRecorderSharingStreamTracerFactory; + callMetricRecorderSharingStreamTracerFactory = new ServerStreamTracer.Factory() { @Override public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) { return new ServerStreamTracer() { @@ -196,6 +200,9 @@ public void responseTrailersContainAllReportedMetricsFromCallMetricRecorder() { applicationUtilizationMetricsMap.put("util1", 0.1082); applicationUtilizationMetricsMap.put("util2", 0.4936); applicationUtilizationMetricsMap.put("util3", 0.5342); + applicationNamedMetrics.put("named1", 0.777); + applicationNamedMetrics.put("named2", 737.747); + applicationNamedMetrics.put("named3", -0.380); cpuUtilizationMetrics = 0.3465; applicationUtilizationMetrics = 0.99887; memoryUtilizationMetrics = 0.764; @@ -209,6 +216,8 @@ public void responseTrailersContainAllReportedMetricsFromCallMetricRecorder() { .containsExactly("util1", 0.1082, "util2", 0.4936, "util3", 0.5342); assertThat(report.getRequestCostMap()) .containsExactly("cost1", 1231.4543, "cost2", 0.1367, "cost3", 7614.145); + assertThat(report.getNamedMetricsMap()) + .containsExactly("named1", 0.777, "named2", 737.747, "named3", -0.380); assertThat(report.getCpuUtilization()).isEqualTo(0.3465); assertThat(report.getApplicationUtilization()).isEqualTo(0.99887); assertThat(report.getMemUtilization()).isEqualTo(0.764); @@ -221,6 +230,9 @@ public void responseTrailersContainMergedMetricsFromCallMetricRecorderAndMetricR applicationUtilizationMetricsMap.put("util1", 0.1482); applicationUtilizationMetricsMap.put("util2", 0.4036); applicationUtilizationMetricsMap.put("util3", 0.5742); + applicationNamedMetrics.put("named1", 0.777); + applicationNamedMetrics.put("named2", 737.747); + applicationNamedMetrics.put("named3", -0.380); cpuUtilizationMetrics = 0.3465; memoryUtilizationMetrics = 0.967; metricRecorder.setApplicationUtilizationMetric(2.718); @@ -240,6 +252,8 @@ public void responseTrailersContainMergedMetricsFromCallMetricRecorderAndMetricR assertThat(report.getUtilizationMap()) .containsExactly("util1", 0.1482, "util2", 0.4036, "util3", 0.5742, "serverUtil1", 0.7467, "serverUtil2", 0.2233); + assertThat(report.getNamedMetricsMap()) + .containsExactly("named1", 0.777, "named2", 737.747, "named3", -0.380); assertThat(report.getRequestCostMap()).isEmpty(); assertThat(report.getCpuUtilization()).isEqualTo(0.3465); assertThat(report.getApplicationUtilization()).isEqualTo(2.718); diff --git a/xds/src/test/java/io/grpc/xds/orca/OrcaPerRequestUtilTest.java b/xds/src/test/java/io/grpc/xds/orca/OrcaPerRequestUtilTest.java index ef06e6fccfec..4d0805029151 100644 --- a/xds/src/test/java/io/grpc/xds/orca/OrcaPerRequestUtilTest.java +++ b/xds/src/test/java/io/grpc/xds/orca/OrcaPerRequestUtilTest.java @@ -124,7 +124,8 @@ static boolean reportEqual(MetricReport a, && a.getQps() == b.getQps() && a.getEps() == b.getEps() && Objects.equal(a.getRequestCostMetrics(), b.getRequestCostMetrics()) - && Objects.equal(a.getUtilizationMetrics(), b.getUtilizationMetrics()); + && Objects.equal(a.getUtilizationMetrics(), b.getUtilizationMetrics()) + && Objects.equal(a.getNamedMetrics(), b.getNamedMetrics()); } /** diff --git a/xds/third_party/envoy/import.sh b/xds/third_party/envoy/import.sh index c4b5a8516f3b..b85b06678001 100755 --- a/xds/third_party/envoy/import.sh +++ b/xds/third_party/envoy/import.sh @@ -13,15 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Update VERSION then in this directory run ./import.sh +# Update VERSION then execute this script set -e -BRANCH=main # import VERSION from the google internal copybara_version.txt for Envoy VERSION=0478eba2a495027bf6ac8e787c42e2f5b9eb553b -GIT_REPO="https://github.com/envoyproxy/envoy.git" -GIT_BASE_DIR=envoy -SOURCE_PROTO_BASE_DIR=envoy/api +DOWNLOAD_URL="https://github.com/envoyproxy/envoy/archive/${VERSION}.tar.gz" +DOWNLOAD_BASE_DIR="envoy-${VERSION}" +SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}/api" TARGET_PROTO_BASE_DIR=src/main/proto # Sorted alphabetically. FILES=( @@ -181,19 +180,13 @@ envoy/type/v3/semantic_version.proto pushd `git rev-parse --show-toplevel`/xds/third_party/envoy -# clone the envoy github repo in a tmp directory +# put the repo in a tmp directory tmpdir="$(mktemp -d)" trap "rm -rf ${tmpdir}" EXIT +curl -Ls "${DOWNLOAD_URL}" | tar xz -C "${tmpdir}" -pushd "${tmpdir}" -git clone -b $BRANCH $GIT_REPO -trap "rm -rf $GIT_BASE_DIR" EXIT -cd "$GIT_BASE_DIR" -git checkout $VERSION -popd - -cp -p "${tmpdir}/${GIT_BASE_DIR}/LICENSE" LICENSE -cp -p "${tmpdir}/${GIT_BASE_DIR}/NOTICE" NOTICE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/LICENSE" LICENSE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/NOTICE" NOTICE rm -rf "${TARGET_PROTO_BASE_DIR}" mkdir -p "${TARGET_PROTO_BASE_DIR}" diff --git a/xds/third_party/googleapis/import.sh b/xds/third_party/googleapis/import.sh index e83536564e16..d51893e23d42 100755 --- a/xds/third_party/googleapis/import.sh +++ b/xds/third_party/googleapis/import.sh @@ -13,14 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Update VERSION then in this directory run ./import.sh +# Update VERSION then execute this script set -e -BRANCH=master VERSION=ca1372c6d7bcb199638ebfdb40d2b2660bab7b88 -GIT_REPO="https://github.com/googleapis/googleapis.git" -GIT_BASE_DIR=googleapis -SOURCE_PROTO_BASE_DIR=googleapis +DOWNLOAD_URL="https://github.com/googleapis/googleapis/archive/${VERSION}.tar.gz" +DOWNLOAD_BASE_DIR="googleapis-${VERSION}" +SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}" TARGET_PROTO_BASE_DIR=src/main/proto # Sorted alphabetically. FILES=( @@ -30,18 +29,12 @@ google/api/expr/v1alpha1/syntax.proto pushd `git rev-parse --show-toplevel`/xds/third_party/googleapis -# clone the googleapis github repo in a tmp directory +# put the repo in a tmp directory tmpdir="$(mktemp -d)" trap "rm -rf ${tmpdir}" EXIT +curl -Ls "${DOWNLOAD_URL}" | tar xz -C "${tmpdir}" -pushd "${tmpdir}" -git clone -b $BRANCH $GIT_REPO -trap "rm -rf $GIT_BASE_DIR" EXIT -cd "$GIT_BASE_DIR" -git checkout $VERSION -popd - -cp -p "${tmpdir}/${GIT_BASE_DIR}/LICENSE" LICENSE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/LICENSE" LICENSE rm -rf "${TARGET_PROTO_BASE_DIR}" mkdir -p "${TARGET_PROTO_BASE_DIR}" diff --git a/xds/third_party/protoc-gen-validate/import.sh b/xds/third_party/protoc-gen-validate/import.sh index 4e30b0e1180a..64c92b16c200 100755 --- a/xds/third_party/protoc-gen-validate/import.sh +++ b/xds/third_party/protoc-gen-validate/import.sh @@ -13,33 +13,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Update GIT_ORIGIN_REV_ID then in this directory run ./import.sh +# Update VERSION then execute this script set -e -BRANCH=main -# import GIT_ORIGIN_REV_ID from one of the google internal CLs -GIT_ORIGIN_REV_ID=dfcdc5ea103dda467963fb7079e4df28debcfd28 -GIT_REPO="https://github.com/envoyproxy/protoc-gen-validate.git" -GIT_BASE_DIR=protoc-gen-validate -SOURCE_PROTO_BASE_DIR=protoc-gen-validate +# import VERSION from one of the google internal CLs +VERSION=dfcdc5ea103dda467963fb7079e4df28debcfd28 +DOWNLOAD_URL="https://github.com/envoyproxy/protoc-gen-validate/archive/${VERSION}.tar.gz" +DOWNLOAD_BASE_DIR="protoc-gen-validate-${VERSION}" +SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}" TARGET_PROTO_BASE_DIR=src/main/proto # Sorted alphabetically. FILES=( validate/validate.proto ) -# clone the protoc-gen-validate github repo in a tmp directory +pushd `git rev-parse --show-toplevel`/xds/third_party/protoc-gen-validate + +# put the repo in a tmp directory tmpdir="$(mktemp -d)" -pushd "${tmpdir}" -rm -rf "$GIT_BASE_DIR" -git clone -b $BRANCH $GIT_REPO -cd "$GIT_BASE_DIR" -git checkout $GIT_ORIGIN_REV_ID -popd +trap "rm -rf ${tmpdir}" EXIT +curl -Ls "${DOWNLOAD_URL}" | tar xz -C "${tmpdir}" -cp -p "${tmpdir}/${GIT_BASE_DIR}/LICENSE" LICENSE -cp -p "${tmpdir}/${GIT_BASE_DIR}/NOTICE" NOTICE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/LICENSE" LICENSE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/NOTICE" NOTICE +rm -rf "${TARGET_PROTO_BASE_DIR}" mkdir -p "${TARGET_PROTO_BASE_DIR}" pushd "${TARGET_PROTO_BASE_DIR}" @@ -51,4 +49,4 @@ do done popd -rm -rf "$tmpdir" +popd diff --git a/xds/third_party/xds/import.sh b/xds/third_party/xds/import.sh index f759cb0d35f7..cda86d0368f0 100755 --- a/xds/third_party/xds/import.sh +++ b/xds/third_party/xds/import.sh @@ -13,15 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Update VERSION then in this directory run ./import.sh +# Update VERSION then execute this script set -e -BRANCH=main # import VERSION from one of the google internal CLs VERSION=e9ce68804cb4e64cab5a52e3c8baf840d4ff87b7 -GIT_REPO="https://github.com/cncf/xds.git" -GIT_BASE_DIR=xds -SOURCE_PROTO_BASE_DIR=xds +DOWNLOAD_URL="https://github.com/cncf/xds/archive/${VERSION}.tar.gz" +DOWNLOAD_BASE_DIR="xds-${VERSION}" +SOURCE_PROTO_BASE_DIR="${DOWNLOAD_BASE_DIR}" TARGET_PROTO_BASE_DIR=src/main/proto # Sorted alphabetically. FILES=( @@ -54,18 +53,12 @@ xds/type/v3/typed_struct.proto pushd `git rev-parse --show-toplevel`/xds/third_party/xds -# clone the xds github repo in a tmp directory +# put the repo in a tmp directory tmpdir="$(mktemp -d)" -trap "rm -rf $tmpdir" EXIT +trap "rm -rf ${tmpdir}" EXIT +curl -Ls "${DOWNLOAD_URL}" | tar xz -C "${tmpdir}" -pushd "${tmpdir}" -git clone -b $BRANCH $GIT_REPO -trap "rm -rf $GIT_BASE_DIR" EXIT -cd "$GIT_BASE_DIR" -git checkout $VERSION -popd - -cp -p "${tmpdir}/${GIT_BASE_DIR}/LICENSE" LICENSE +cp -p "${tmpdir}/${DOWNLOAD_BASE_DIR}/LICENSE" LICENSE rm -rf "${TARGET_PROTO_BASE_DIR}" mkdir -p "${TARGET_PROTO_BASE_DIR}"