From 889dbadf329f90a34c7dcb364328f30ec618901c Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Thu, 10 Mar 2022 02:02:35 -0800 Subject: [PATCH 01/14] Add grpc-health service implementaiton Motivation: The gRPC community has defined a health checking standard [1] and API [2]. ServiceTalk should provide a default implementation of this service. [1] https://github.com/grpc/grpc/blob/master/doc/health-checking.md [2] https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto --- servicetalk-grpc-health/build.gradle | 125 +++++++++++ .../gradle/checkstyle/suppressions.xml | 24 ++ .../gradle/spotbugs/main-exclusions.xml | 22 ++ servicetalk-grpc-health/src/README.md | 4 + .../grpc/health/DefaultHealthService.java | 208 ++++++++++++++++++ .../servicetalk/grpc/health/package-info.java | 22 ++ .../grpc/health/DefaultHealthServiceTest.java | 153 +++++++++++++ settings.gradle | 1 + 8 files changed, 559 insertions(+) create mode 100644 servicetalk-grpc-health/build.gradle create mode 100644 servicetalk-grpc-health/gradle/checkstyle/suppressions.xml create mode 100644 servicetalk-grpc-health/gradle/spotbugs/main-exclusions.xml create mode 100644 servicetalk-grpc-health/src/README.md create mode 100644 servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java create mode 100644 servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/package-info.java create mode 100644 servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle new file mode 100644 index 0000000000..ae0c05e360 --- /dev/null +++ b/servicetalk-grpc-health/build.gradle @@ -0,0 +1,125 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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. + */ + +buildscript { + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufGradlePluginVersion" + } +} + +apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" +apply plugin: "com.google.protobuf" + +dependencies { + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-grpc-netty") + implementation project(":servicetalk-grpc-protoc") + implementation project(":servicetalk-grpc-protobuf") + + testImplementation enforcedPlatform("org.junit:junit-bom:$junit5Version") + testImplementation project(":servicetalk-concurrent-api-test") + testImplementation testFixtures(project(":servicetalk-transport-netty-internal")) + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" +} + +// Instead of copy/pasting the .proto files into our repository, fetch them from maven central. +// This will also more likely to raise any API changes earlier so we can adjust. +configurations { + grpcProtos { transitive = false } +} + +dependencies { + grpcProtos "io.grpc:grpc-services:$grpcVersion" +} + +processResources { + duplicatesStrategy = 'include' +} + +var extractedProtoDir = "$buildDir/extracted-protos/main" +var generatedCodeDir = "$projectDir/src/generated" +task unzipGrpcProtos(type: Copy) { + dependsOn processResources + from zipTree(configurations.grpcProtos.singleFile).matching { + include '**/health.proto' + } + into extractedProtoDir + includeEmptyDirs = false + duplicatesStrategy = 'include' + // Rename the java package name to avoid potential classpath conflicts with grpc-java. + filter { line -> line.replace( + 'option java_package = "io.grpc.health.v1";', + 'option java_package = "io.servicetalk.health.v1";') + } +} + +sourceSets { + main { + java { + srcDir generatedCodeDir + } + proto { + srcDir "$extractedProtoDir/main" + } + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protobufVersion" + } + + //// REMOVE if outside of ServiceTalk gradle project + def pluginJar = file("${project.rootProject.rootDir}/servicetalk-grpc-protoc/build" + + "/buildExecutable/servicetalk-grpc-protoc-${project.version}-all.jar") + //// REMOVE if outside of ServiceTalk gradle project + + plugins { + servicetalk_grpc { + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + //// "path" is used only because we want to use the gradle project local version of the plugin. + path = pluginJar.path + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + + // artifact = "io.servicetalk:servicetalk-grpc-protoc:$serviceTalkVersion:all@jar" + } + } + generateProtoTasks { + all().each { task -> + task.dependsOn unzipGrpcProtos + //// REMOVE if outside of ServiceTalk gradle project + task.dependsOn(":servicetalk-grpc-protoc:buildExecutable") // use gradle project local grpc-protoc dependency + + // you may need to manually add the artifact name as an input + task.inputs + .file(pluginJar) + .withNormalizer(ClasspathNormalizer) + .withPropertyName("servicetalkPluginJar") + .withPathSensitivity(PathSensitivity.RELATIVE) + //// REMOVE if outside of ServiceTalk gradle project + + task.plugins { + servicetalk_grpc { + // Need to tell protobuf-gradle-plugin to output in the correct directory if all generated + // code for a single proto goes to a single file (e.g. "java_multiple_files = false" in the .proto). + outputSubDir = "java" + } + } + } + } + generatedFilesBaseDir = generatedCodeDir +} diff --git a/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml b/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml new file mode 100644 index 0000000000..b6e756aa39 --- /dev/null +++ b/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/servicetalk-grpc-health/gradle/spotbugs/main-exclusions.xml b/servicetalk-grpc-health/gradle/spotbugs/main-exclusions.xml new file mode 100644 index 0000000000..e210392660 --- /dev/null +++ b/servicetalk-grpc-health/gradle/spotbugs/main-exclusions.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/servicetalk-grpc-health/src/README.md b/servicetalk-grpc-health/src/README.md new file mode 100644 index 0000000000..0695fd24c6 --- /dev/null +++ b/servicetalk-grpc-health/src/README.md @@ -0,0 +1,4 @@ +## gRPC Health Checking + +This package contains an implementation of the +[gRPC Health Checking v1 Protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). \ No newline at end of file diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java new file mode 100644 index 0000000000..095db61d09 --- /dev/null +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -0,0 +1,208 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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.servicetalk.grpc.health; + +import io.servicetalk.concurrent.PublisherSource; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.grpc.api.GrpcServiceContext; +import io.servicetalk.grpc.api.GrpcStatus; +import io.servicetalk.grpc.api.GrpcStatusCode; +import io.servicetalk.health.v1.Health; +import io.servicetalk.health.v1.HealthCheckRequest; +import io.servicetalk.health.v1.HealthCheckResponse; +import io.servicetalk.health.v1.HealthCheckResponse.ServingStatus; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +import static io.servicetalk.concurrent.api.Processors.newPublisherProcessorDropHeadOnOverflow; +import static io.servicetalk.concurrent.api.SourceAdapters.fromSource; +import static io.servicetalk.grpc.api.GrpcStatusCode.FAILED_PRECONDITION; +import static io.servicetalk.grpc.api.GrpcStatusCode.NOT_FOUND; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVICE_UNKNOWN; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVING; +import static io.servicetalk.health.v1.HealthCheckResponse.newBuilder; +import static java.util.Objects.requireNonNull; + +/** + * Implementation of {@link Health.HealthService} that provides accessors to set/clear status for arbitrary services. + */ +public final class DefaultHealthService implements Health.HealthService { + /** + * The name of the service corresponding to + * the overall health status. + */ + public static final String OVERALL_SERVICE_NAME = ""; + private final Map serviceToStatusMap = new ConcurrentHashMap<>(); + private final Predicate watchAllowed; + private final Lock lock = new ReentrantLock(); + private boolean terminated; + + /** + * Create a new instance. Service {@link #OVERALL_SERVICE_NAME} state is set to {@link ServingStatus#SERVING}. + */ + public DefaultHealthService() { + this(service -> true); + } + + /** + * Create a new instance. Service {@link #OVERALL_SERVICE_NAME} state is set to {@link ServingStatus#SERVING}. + * @param watchAllowed {@link Predicate} that determines if a {@link #watch(GrpcServiceContext, HealthCheckRequest)} + * request that doesn't match an existing service will succeed or fail with + * {@link GrpcStatusCode#FAILED_PRECONDITION}. This can be used to bound memory by restricting watches to expected + * service names. + */ + public DefaultHealthService(Predicate watchAllowed) { + this.watchAllowed = requireNonNull(watchAllowed); + serviceToStatusMap.put(OVERALL_SERVICE_NAME, new HealthValue(SERVING)); + } + + @Override + public Single check(final GrpcServiceContext ctx, final HealthCheckRequest request) { + HealthValue health = serviceToStatusMap.get(request.getService()); + if (health == null) { + return Single.failed(new GrpcStatus(NOT_FOUND, null, "unknown service " + request.getService()) + .asException()); + } + return Single.succeeded(health.last); + } + + @Override + public Publisher watch(final GrpcServiceContext ctx, final HealthCheckRequest request) { + // Try a get first to avoid locking with the assumption that most requests will be to watch existing services. + HealthValue healthValue = serviceToStatusMap.get(request.getService()); + if (healthValue == null) { + if (!watchAllowed.test(request.getService())) { + return Publisher.failed(new GrpcStatus(FAILED_PRECONDITION, null, "watch not allowed for service " + + request.getService()).asException()); + } + lock.lock(); + try { + if (terminated) { + return Publisher.from(newBuilder().setStatus(NOT_SERVING).build()); + } + healthValue = serviceToStatusMap.computeIfAbsent(request.getService(), + service -> new HealthValue(SERVICE_UNKNOWN)); + } finally { + lock.unlock(); + } + } + + return Publisher.from(healthValue.last).concat(healthValue.publisher); + } + + /** + * Updates the status of the server. + * @param service the name of some aspect of the server that is associated with a health status. + * This name can have no relation with the gRPC services that the server is running with. + * It can also be an empty String {@code ""} per the gRPC specification. + * @param status is one of the values {@link ServingStatus#SERVING}, {@link ServingStatus#NOT_SERVING}, + * and {@link ServingStatus#UNKNOWN}. + * @return {@code true} if this change was applied. {@code false} if it was not due to {@link #terminate()}. + */ + public boolean setStatus(String service, ServingStatus status) { + final HealthCheckResponse resp; + final HealthValue healthValue; + lock.lock(); + try { + if (terminated) { + return false; + } + resp = newBuilder().setStatus(status).build(); + healthValue = serviceToStatusMap.computeIfAbsent(service, service2 -> new HealthValue(resp)); + } finally { + lock.unlock(); + } + healthValue.next(resp); + return true; + } + + /** + * Clears the health status record of a service. The health service will respond with NOT_FOUND + * error on checking the status of a cleared service. + * @param service the name of some aspect of the server that is associated with a health status. + * This name can have no relation with the gRPC services that the server is running with. + * It can also be an empty String {@code ""} per the gRPC specification. + * @return {@code true} if this call removed a service. {@code false} if service wasn't found. + */ + public boolean clearStatus(String service) { + final HealthValue healthValue = serviceToStatusMap.remove(service); + if (healthValue != null) { + healthValue.complete(SERVICE_UNKNOWN); + return true; + } + return false; + } + + /** + * All services will be marked as {@link ServingStatus#NOT_SERVING}, and + * future updates to services will be prohibited. This method is meant to be called prior to server shutdown as a + * way to indicate that clients should redirect their traffic elsewhere. + * @return {@code true} if this call terminated this service. {@code false} if it was not due to previous call to + * this method. + */ + public boolean terminate() { + lock.lock(); + try { + if (terminated) { + return false; + } + terminated = true; + } finally { + lock.unlock(); + } + for (final HealthValue healthValue : serviceToStatusMap.values()) { + healthValue.complete(NOT_SERVING); + } + serviceToStatusMap.clear(); + return true; + } + + private static final class HealthValue { + private final PublisherSource.Processor processor; + private final Publisher publisher; + private volatile HealthCheckResponse last; + + private HealthValue(final HealthCheckResponse initialState) { + this.processor = newPublisherProcessorDropHeadOnOverflow(4); + this.publisher = fromSource(processor) + // Allow multiple subscribers to Subscribe to the resulting Publisher. + .multicast(1, false); + this.last = initialState; + } + + private HealthValue(final ServingStatus status) { + this(newBuilder().setStatus(status).build()); + } + + void next(HealthCheckResponse response) { + // Set the status here instead of in an operator because we need the status to be updated regardless if + // anyone is consuming the status. + last = response; + processor.onNext(response); + } + + void complete(ServingStatus status) { + next(newBuilder().setStatus(status).build()); + processor.onComplete(); + } + } +} diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/package-info.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/package-info.java new file mode 100644 index 0000000000..e5c1dde19d --- /dev/null +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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. + */ +/** + * gRPC Health Checking Service. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.grpc.health; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java new file mode 100644 index 0000000000..510f5c04c7 --- /dev/null +++ b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java @@ -0,0 +1,153 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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.servicetalk.grpc.health; + +import io.servicetalk.concurrent.BlockingIterator; +import io.servicetalk.grpc.api.GrpcStatusException; +import io.servicetalk.grpc.netty.GrpcClients; +import io.servicetalk.grpc.netty.GrpcServers; +import io.servicetalk.health.v1.Health; +import io.servicetalk.health.v1.HealthCheckRequest; +import io.servicetalk.health.v1.HealthCheckResponse; +import io.servicetalk.health.v1.HealthCheckResponse.ServingStatus; +import io.servicetalk.transport.api.ServerContext; + +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; + +import static io.servicetalk.grpc.api.GrpcStatusCode.FAILED_PRECONDITION; +import static io.servicetalk.grpc.api.GrpcStatusCode.NOT_FOUND; +import static io.servicetalk.grpc.health.DefaultHealthService.OVERALL_SERVICE_NAME; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVICE_UNKNOWN; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVING; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class DefaultHealthServiceTest { + private static final String UNKNOWN_SERVICE_NAME = "unknown"; + + @Test + void defaultCheck() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(client.check(newRequest(OVERALL_SERVICE_NAME)).getStatus(), equalTo(SERVING)); + + assertThat(service.setStatus(OVERALL_SERVICE_NAME, NOT_SERVING), equalTo(true)); + assertThat(client.check(newRequest(OVERALL_SERVICE_NAME)).getStatus(), equalTo(NOT_SERVING)); + } + } + } + + @Test + void statusChangeCheck() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + String serviceName = "service"; + ServingStatus serviceStatus = NOT_SERVING; + service.setStatus(serviceName, serviceStatus); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(client.check(newRequest(serviceName)).getStatus(), equalTo(serviceStatus)); + } + } + } + + @Test + void notFoundCheck() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(assertThrows(GrpcStatusException.class, + () -> client.check(newRequest(UNKNOWN_SERVICE_NAME))).status().code(), + equalTo(NOT_FOUND)); + } + } + } + + @Test + void defaultWatch() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + BlockingIterator itr = client.watch(newRequest(OVERALL_SERVICE_NAME)).iterator(); + assertThat(itr.next().getStatus(), equalTo(SERVING)); + + assertThat(service.setStatus(OVERALL_SERVICE_NAME, NOT_SERVING), equalTo(true)); + assertThat(itr.next().getStatus(), equalTo(NOT_SERVING)); + } + } + } + + @Test + void clearWatch() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(service.clearStatus(OVERALL_SERVICE_NAME), equalTo(true)); + BlockingIterator itr = client.watch(newRequest(OVERALL_SERVICE_NAME)).iterator(); + assertThat(itr.next().getStatus(), equalTo(SERVICE_UNKNOWN)); + + assertThat(service.setStatus(OVERALL_SERVICE_NAME, SERVING), equalTo(true)); + assertThat(itr.next().getStatus(), equalTo(SERVING)); + } + } + } + + @Test + void terminateWatchCheck() throws Exception { + DefaultHealthService service = new DefaultHealthService(); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(service.terminate(), equalTo(true)); + BlockingIterator itr = client.watch(newRequest(OVERALL_SERVICE_NAME)).iterator(); + assertThat(itr.next().getStatus(), equalTo(NOT_SERVING)); + + assertThat(service.setStatus(OVERALL_SERVICE_NAME, SERVING), equalTo(false)); + assertThat(service.clearStatus(OVERALL_SERVICE_NAME), equalTo(false)); + assertThat(assertThrows(GrpcStatusException.class, + () -> client.check(newRequest(OVERALL_SERVICE_NAME))).status().code(), + equalTo(NOT_FOUND)); + } + } + } + + @Test + void watchPredicateFails() throws Exception { + DefaultHealthService service = new DefaultHealthService(name -> false); + try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { + try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( + (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { + assertThat(assertThrows(GrpcStatusException.class, + () -> client.watch(newRequest(UNKNOWN_SERVICE_NAME)).iterator().next()).status().code(), + equalTo(FAILED_PRECONDITION)); + } + } + } + + private static HealthCheckRequest newRequest(String service) { + return HealthCheckRequest.newBuilder().setService(service).build(); + } +} diff --git a/settings.gradle b/settings.gradle index 40f166e56c..eb5b2d780e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -66,6 +66,7 @@ include "servicetalk-annotations", "servicetalk-examples:http:timeout", "servicetalk-gradle-plugin-internal", "servicetalk-grpc-api", + "servicetalk-grpc-health", "servicetalk-grpc-internal", "servicetalk-grpc-netty", "servicetalk-grpc-protobuf", From d3b836bd8c28d74604cd4a2e943be03161f9af1f Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 09:36:20 -0700 Subject: [PATCH 02/14] add example, fix exclusions of checkstyle --- .../ROOT/pages/_partials/nav-versioned.adoc | 1 + .../docs/modules/ROOT/pages/grpc/index.adoc | 18 ++++- servicetalk-examples/grpc/health/README.adoc | 3 + servicetalk-examples/grpc/health/build.gradle | 81 +++++++++++++++++++ .../grpc/health/HealthClientExample.java | 58 +++++++++++++ .../grpc/health/HealthServerExample.java | 41 ++++++++++ .../examples/grpc/health/package-info.java | 19 +++++ .../health/src/main/proto/helloworld.proto | 37 +++++++++ .../grpc/health/src/main/resources/log4j2.xml | 35 ++++++++ .../checkstyle/global-suppressions.xml | 3 + servicetalk-grpc-health/build.gradle | 8 +- .../gradle/checkstyle/suppressions.xml | 24 ------ .../grpc/health/DefaultHealthService.java | 2 +- settings.gradle | 2 + 14 files changed, 303 insertions(+), 29 deletions(-) create mode 100644 servicetalk-examples/grpc/health/README.adoc create mode 100644 servicetalk-examples/grpc/health/build.gradle create mode 100644 servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java create mode 100644 servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java create mode 100644 servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/package-info.java create mode 100644 servicetalk-examples/grpc/health/src/main/proto/helloworld.proto create mode 100644 servicetalk-examples/grpc/health/src/main/resources/log4j2.xml delete mode 100644 servicetalk-grpc-health/gradle/checkstyle/suppressions.xml diff --git a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc index 22ff8df5a5..b6c5e97460 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc @@ -21,6 +21,7 @@ ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Compression[Compression] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Deadlines[Deadlines] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Observer[Observer] +** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Health[Health] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#errors[Application Errors] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#execution-strategy[Execution Strategy] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#route-guide[Route Guide] diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index 444225dc5f..42946865c0 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -106,15 +106,12 @@ Extends the blocking "Hello World" example to demonstrate configuration of debug * link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/debugging/DebuggingClient.java[DebuggingClient] – Sends hello requests to the server and receives a greeting response. - [#Observer] == Observer This example demonstrates the following: - Use of link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcLifecycleObserver.java[GrpcLifecycleObserver] to log a summary of each request/response. -Using the following classes: - - link:{source-root}/servicetalk-examples/grpc/observer/src/main/java/io/servicetalk/examples/grpc/observer/LifecycleObserverServer.java[LifecycleObserverServer] - A server that installs a link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcLifecycleObserver.java[GrpcLifecycleObserver] on the server builder. @@ -122,6 +119,21 @@ on the server builder. link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcLifecycleObserver.java[GrpcLifecycleObserver] on via a client filter on the client builder. +[#Health] +== Health +This example demonstrates the following: +- Use of +link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. + +Using the following classes: + +Using the following classes: +* link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java[HealthServerExample] a server +that installs link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to +a simple "hello world" service. +* link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java[HealthClientExample] a client +that calls the server and prints results. + [#errors] == Application Errors The gRPC protocol supports propagating application level errors, and also provides serialization/deserialization of diff --git a/servicetalk-examples/grpc/health/README.adoc b/servicetalk-examples/grpc/health/README.adoc new file mode 100644 index 0000000000..84d18e7ea5 --- /dev/null +++ b/servicetalk-examples/grpc/health/README.adoc @@ -0,0 +1,3 @@ +== ServiceTalk gRPC Health Example + +Extends "Hello World" ServiceTalk gRPC example to demonstrate default health service. \ No newline at end of file diff --git a/servicetalk-examples/grpc/health/build.gradle b/servicetalk-examples/grpc/health/build.gradle new file mode 100644 index 0000000000..1a0faedb7d --- /dev/null +++ b/servicetalk-examples/grpc/health/build.gradle @@ -0,0 +1,81 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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. + */ + +buildscript { + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufGradlePluginVersion" + } +} + +apply plugin: "java" +apply plugin: "com.google.protobuf" +apply from: "../../gradle/idea.gradle" + +dependencies { + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-grpc-netty") + implementation project(":servicetalk-grpc-protoc") + implementation project(":servicetalk-grpc-protobuf") + implementation project(":servicetalk-grpc-health") + + implementation "org.slf4j:slf4j-api:$slf4jVersion" + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protobufVersion" + } + + //// REMOVE if outside of ServiceTalk gradle project + def pluginJar = file("${project.rootProject.rootDir}/servicetalk-grpc-protoc/build" + + "/buildExecutable/servicetalk-grpc-protoc-${project.version}-all.jar") + //// REMOVE if outside of ServiceTalk gradle project + + plugins { + servicetalk_grpc { + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + //// "path" is used only because we want to use the gradle project local version of the plugin. + path = pluginJar.path + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + + // artifact = "io.servicetalk:servicetalk-grpc-protoc:$serviceTalkVersion:all@jar" + } + } + generateProtoTasks { + all().each { task -> + //// REMOVE if outside of ServiceTalk gradle project + task.dependsOn(":servicetalk-grpc-protoc:buildExecutable") // use gradle project local grpc-protoc dependency + + // you may need to manually add the artifact name as an input + task.inputs + .file(pluginJar) + .withNormalizer(ClasspathNormalizer) + .withPropertyName("servicetalkPluginJar") + .withPathSensitivity(PathSensitivity.RELATIVE) + //// REMOVE if outside of ServiceTalk gradle project + + task.plugins { + servicetalk_grpc { + // Need to tell protobuf-gradle-plugin to output in the correct directory if all generated + // code for a single proto goes to a single file (e.g. "java_multiple_files = false" in the .proto). + outputSubDir = "java" + } + } + } + } + generatedFilesBaseDir = "$buildDir/generated/sources/proto" +} diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java new file mode 100644 index 0000000000..c55fd530c1 --- /dev/null +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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.servicetalk.examples.grpc.health; + +import io.servicetalk.grpc.api.GrpcStatusException; +import io.servicetalk.grpc.netty.GrpcClients; +import io.servicetalk.health.v1.Health; +import io.servicetalk.health.v1.Health.BlockingHealthClient; +import io.servicetalk.health.v1.HealthCheckRequest; + +import io.grpc.examples.health.Greeter; +import io.grpc.examples.health.Greeter.BlockingGreeterClient; +import io.grpc.examples.health.HelloRequest; +import io.grpc.examples.health.Greeter; + +/** + * Extends the async "Hello World" example to demonstrate health service usage. + */ +public final class HealthClientExample { + public static void main(String... args) throws Exception { + final String serviceName = "World"; + try (Greeter.BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) + .buildBlocking(new Greeter.ClientFactory()); + BlockingHealthClient healthClient = GrpcClients.forAddress("localhost", 8080) + .buildBlocking(new Health.ClientFactory())) { + // Check health before + checkHealth(healthClient, serviceName); + + System.out.println("Response=" + client.sayHello(HelloRequest.newBuilder().setName("World").build()) + .getMessage()); + + // Check the health after to observe it changed. + checkHealth(healthClient, serviceName); + } + } + + private static void checkHealth(BlockingHealthClient healthClient, String serviceName) throws Exception { + try { + System.out.println("Service '" + serviceName + "' health=" + + healthClient.check(HealthCheckRequest.newBuilder().setService(serviceName).build()).getStatus()); + } catch (GrpcStatusException e) { + System.out.println("Service '" + serviceName + "' health exception=" + e); + } + } +} diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java new file mode 100644 index 0000000000..e983413b43 --- /dev/null +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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.servicetalk.examples.grpc.health; + +import io.servicetalk.grpc.health.DefaultHealthService; +import io.servicetalk.grpc.netty.GrpcServers; + +import io.grpc.examples.health.Greeter.GreeterService; +import io.grpc.examples.health.HelloReply; + +import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVING; + +/** + * A simple extension of the gRPC "Hello World" example which demonstrates {@link DefaultHealthService}. + */ +public final class HealthServerExample { + public static void main(String... args) throws Exception { + DefaultHealthService healthService = new DefaultHealthService(); + GrpcServers.forPort(8080) + .listenAndAwait(healthService, (GreeterService) (ctx, request) -> { + // For demonstration purposes, just use the name as a service and mark it as SERVING. + healthService.setStatus(request.getName(), SERVING); + return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); + }) + .awaitShutdown(); + } +} diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/package-info.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/package-info.java new file mode 100644 index 0000000000..3e3cd1b129 --- /dev/null +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.examples.grpc.health; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-examples/grpc/health/src/main/proto/helloworld.proto b/servicetalk-examples/grpc/health/src/main/proto/helloworld.proto new file mode 100644 index 0000000000..2390d6a016 --- /dev/null +++ b/servicetalk-examples/grpc/health/src/main/proto/helloworld.proto @@ -0,0 +1,37 @@ +// Copyright 2015 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.health"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/servicetalk-examples/grpc/health/src/main/resources/log4j2.xml b/servicetalk-examples/grpc/health/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..0e67e5569a --- /dev/null +++ b/servicetalk-examples/grpc/health/src/main/resources/log4j2.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/servicetalk-gradle-plugin-internal/src/main/resources/io/servicetalk/gradle/plugin/internal/checkstyle/global-suppressions.xml b/servicetalk-gradle-plugin-internal/src/main/resources/io/servicetalk/gradle/plugin/internal/checkstyle/global-suppressions.xml index ca95408766..045c77d5ac 100644 --- a/servicetalk-gradle-plugin-internal/src/main/resources/io/servicetalk/gradle/plugin/internal/checkstyle/global-suppressions.xml +++ b/servicetalk-gradle-plugin-internal/src/main/resources/io/servicetalk/gradle/plugin/internal/checkstyle/global-suppressions.xml @@ -43,4 +43,7 @@ + + + diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index ae0c05e360..ca1e35290a 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -52,6 +52,8 @@ processResources { var extractedProtoDir = "$buildDir/extracted-protos/main" var generatedCodeDir = "$projectDir/src/generated" +var generatedJavaPkg = "io.servicetalk.health.v1"; + task unzipGrpcProtos(type: Copy) { dependsOn processResources from zipTree(configurations.grpcProtos.singleFile).matching { @@ -63,10 +65,14 @@ task unzipGrpcProtos(type: Copy) { // Rename the java package name to avoid potential classpath conflicts with grpc-java. filter { line -> line.replace( 'option java_package = "io.grpc.health.v1";', - 'option java_package = "io.servicetalk.health.v1";') + "option java_package = \"$generatedJavaPkg\";") } } +javadoc { + options.addStringOption("exclude", generatedJavaPkg) +} + sourceSets { main { java { diff --git a/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml b/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml deleted file mode 100644 index b6e756aa39..0000000000 --- a/servicetalk-grpc-health/gradle/checkstyle/suppressions.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java index 095db61d09..5fe511d1ab 100644 --- a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -79,7 +79,7 @@ public DefaultHealthService(Predicate watchAllowed) { public Single check(final GrpcServiceContext ctx, final HealthCheckRequest request) { HealthValue health = serviceToStatusMap.get(request.getService()); if (health == null) { - return Single.failed(new GrpcStatus(NOT_FOUND, null, "unknown service " + request.getService()) + return Single.failed(new GrpcStatus(NOT_FOUND, null, "unknown service: " + request.getService()) .asException()); } return Single.succeeded(health.last); diff --git a/settings.gradle b/settings.gradle index eb5b2d780e..ac2e54cfc5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -48,6 +48,7 @@ include "servicetalk-annotations", "servicetalk-examples:grpc:debugging", "servicetalk-examples:grpc:execution-strategy", "servicetalk-examples:grpc:observer", + "servicetalk-examples:grpc:health", "servicetalk-examples:http:helloworld", "servicetalk-examples:http:mutual-tls", "servicetalk-examples:http:redirects", @@ -114,6 +115,7 @@ project(":servicetalk-examples:grpc:debugging").name = "servicetalk-examples-grp project(":servicetalk-examples:grpc:errors").name = "servicetalk-examples-grpc-errors" project(":servicetalk-examples:grpc:execution-strategy").name = "servicetalk-examples-grpc-execution-strategy" project(":servicetalk-examples:grpc:observer").name = "servicetalk-examples-grpc-observer" +project(":servicetalk-examples:grpc:health").name = "servicetalk-examples-grpc-health" project(":servicetalk-examples:http:http2").name = "servicetalk-examples-http-http2" project(":servicetalk-examples:http:helloworld").name = "servicetalk-examples-http-helloworld" project(":servicetalk-examples:http:debugging").name = "servicetalk-examples-http-debugging" From e79e4e037ff8197bf83f3ba0f406315455aa109a Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 09:38:17 -0700 Subject: [PATCH 03/14] fix docs --- servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index 42946865c0..ea8c9e4492 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -112,6 +112,8 @@ This example demonstrates the following: - Use of link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcLifecycleObserver.java[GrpcLifecycleObserver] to log a summary of each request/response. +Using the following classes: + - link:{source-root}/servicetalk-examples/grpc/observer/src/main/java/io/servicetalk/examples/grpc/observer/LifecycleObserverServer.java[LifecycleObserverServer] - A server that installs a link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcLifecycleObserver.java[GrpcLifecycleObserver] on the server builder. @@ -125,8 +127,6 @@ This example demonstrates the following: - Use of link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. -Using the following classes: - Using the following classes: * link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java[HealthServerExample] a server that installs link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to From 4023cbb0f78b8e6ca139241d32783c5bde1528a4 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 09:41:03 -0700 Subject: [PATCH 04/14] fix dangling links in docs --- servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index ea8c9e4492..54442ae06c 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -128,10 +128,10 @@ This example demonstrates the following: link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. Using the following classes: -* link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java[HealthServerExample] a server +* link:{source-root}/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java[HealthServerExample] a server that installs link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. -* link:{source-root}/servicetalk-examples/grpc/debugging/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java[HealthClientExample] a client +* link:{source-root}/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java[HealthClientExample] a client that calls the server and prints results. [#errors] From 6cfe5d6bb0d4b09ed534631511586ad4e6d78118 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 09:44:48 -0700 Subject: [PATCH 05/14] example client import cleanup --- .../servicetalk/examples/grpc/health/HealthClientExample.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java index c55fd530c1..074703bd92 100644 --- a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java @@ -32,10 +32,10 @@ public final class HealthClientExample { public static void main(String... args) throws Exception { final String serviceName = "World"; - try (Greeter.BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) + try (BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new Greeter.ClientFactory()); BlockingHealthClient healthClient = GrpcClients.forAddress("localhost", 8080) - .buildBlocking(new Health.ClientFactory())) { + .buildBlocking(new Health.ClientFactory())) { // Check health before checkHealth(healthClient, serviceName); From cfdd0d68814165e0c08ffb6f92002806e18616f9 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 10:58:29 -0700 Subject: [PATCH 06/14] exclude generated code from pmd --- .../gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy index c79873776d..4601bf1939 100644 --- a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy +++ b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy @@ -252,6 +252,12 @@ final class ServiceTalkLibraryPlugin extends ServiceTalkCorePlugin { incrementalAnalysis = true ruleSets = [] ruleSetConfig = resources.text.fromString(getClass().getResourceAsStream("pmd/basic.xml").text) + + pmdMain { + excludes = [ + '**/src/generated/**' + ] + } } tasks.withType(Pmd).all { From 8968cc245872390d806ca20c6304f17261fc165b Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 12:38:15 -0700 Subject: [PATCH 07/14] exclude javadoc for generated code take 2 --- servicetalk-grpc-health/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index ca1e35290a..0c34878ab1 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -70,7 +70,7 @@ task unzipGrpcProtos(type: Copy) { } javadoc { - options.addStringOption("exclude", generatedJavaPkg) + options.addBooleanOption("exclude $generatedJavaPkg", true) } sourceSets { From 21f066ab32e9f8fad6fcb9a9b9c8397da174dfb6 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 12:54:04 -0700 Subject: [PATCH 08/14] review comments --- .../docs/modules/ROOT/pages/grpc/index.adoc | 2 +- servicetalk-grpc-health/build.gradle | 2 +- .../grpc/health/DefaultHealthService.java | 4 +++- .../grpc/health/DefaultHealthServiceTest.java | 19 ++++++++++++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index 54442ae06c..725ed32877 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -132,7 +132,7 @@ Using the following classes: that installs link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. * link:{source-root}/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java[HealthClientExample] a client -that calls the server and prints results. +that calls the "hello world" server, the "health check" server, and prints results. [#errors] == Application Errors diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index 0c34878ab1..b261b6994d 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -70,7 +70,7 @@ task unzipGrpcProtos(type: Copy) { } javadoc { - options.addBooleanOption("exclude $generatedJavaPkg", true) + options.addBooleanOption("exclude \'$generatedJavaPkg\'", true) } sourceSets { diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java index 5fe511d1ab..ff4dcdb6ab 100644 --- a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -43,7 +43,9 @@ import static java.util.Objects.requireNonNull; /** - * Implementation of {@link Health.HealthService} that provides accessors to set/clear status for arbitrary services. + * Implementation of {@link Health.HealthService} which targets + * gRPC health checking that provides + * accessors to set/clear status for arbitrary services. */ public final class DefaultHealthService implements Health.HealthService { /** diff --git a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java index 510f5c04c7..b9b551b0ae 100644 --- a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java +++ b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java @@ -16,6 +16,7 @@ package io.servicetalk.grpc.health; import io.servicetalk.concurrent.BlockingIterator; +import io.servicetalk.grpc.api.GrpcStatusCode; import io.servicetalk.grpc.api.GrpcStatusException; import io.servicetalk.grpc.netty.GrpcClients; import io.servicetalk.grpc.netty.GrpcServers; @@ -29,8 +30,10 @@ import java.net.InetSocketAddress; +import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.grpc.api.GrpcStatusCode.FAILED_PRECONDITION; import static io.servicetalk.grpc.api.GrpcStatusCode.NOT_FOUND; +import static io.servicetalk.grpc.api.GrpcStatusCode.UNKNOWN; import static io.servicetalk.grpc.health.DefaultHealthService.OVERALL_SERVICE_NAME; import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.NOT_SERVING; import static io.servicetalk.health.v1.HealthCheckResponse.ServingStatus.SERVICE_UNKNOWN; @@ -135,14 +138,24 @@ void terminateWatchCheck() throws Exception { } @Test - void watchPredicateFails() throws Exception { - DefaultHealthService service = new DefaultHealthService(name -> false); + void watchPredicateFalse() throws Exception { + watchFailure(new DefaultHealthService(name -> false), FAILED_PRECONDITION); + } + + @Test + void watchPredicateThrows() throws Exception { + watchFailure(new DefaultHealthService(name -> { + throw DELIBERATE_EXCEPTION; + }), UNKNOWN); + } + + private static void watchFailure(DefaultHealthService service, GrpcStatusCode expectedCode) throws Exception { try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { assertThat(assertThrows(GrpcStatusException.class, () -> client.watch(newRequest(UNKNOWN_SERVICE_NAME)).iterator().next()).status().code(), - equalTo(FAILED_PRECONDITION)); + equalTo(expectedCode)); } } } From 20c801efa2e1c8ee840e1f3487aa020c80d3c703 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 13:05:39 -0700 Subject: [PATCH 09/14] add concurrent-internal test fixture --- servicetalk-grpc-health/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index b261b6994d..f392769364 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -32,6 +32,7 @@ dependencies { testImplementation enforcedPlatform("org.junit:junit-bom:$junit5Version") testImplementation project(":servicetalk-concurrent-api-test") testImplementation testFixtures(project(":servicetalk-transport-netty-internal")) + testImplementation testFixtures(project(":servicetalk-concurrent-internal")) testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.hamcrest:hamcrest:$hamcrestVersion" } From 6b92fc374ab96674510f5bc28bdb1fa6c02b53d3 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 16:16:40 -0700 Subject: [PATCH 10/14] try exclude javadoc at root --- gradle.properties | 4 ++-- .../plugin/internal/ServiceTalkLibraryPlugin.groovy | 1 + servicetalk-grpc-health/build.gradle | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8814d8ad09..c1d1b8ce25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,8 +15,8 @@ # # build configuration -org.gradle.parallel=true -org.gradle.caching=true +#org.gradle.parallel=true +#org.gradle.caching=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xms2g -Xmx4g -dsa -da -ea:io.servicetalk... -XX:+HeapDumpOnOutOfMemoryError diff --git a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy index 4601bf1939..1208d420d6 100644 --- a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy +++ b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy @@ -68,6 +68,7 @@ final class ServiceTalkLibraryPlugin extends ServiceTalkCorePlugin { options.addBooleanOption("Xwerror", true) options.addBooleanOption("Xdoclint:all,-missing", true) options.addBooleanOption("protected", true) + options.addBooleanOption("exclude 'io.servicetalk.health.v1'", true) } def sourcesJar = createSourcesJarTask(project, sourceSets.main) diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index f392769364..2e7ad494ef 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -70,10 +70,6 @@ task unzipGrpcProtos(type: Copy) { } } -javadoc { - options.addBooleanOption("exclude \'$generatedJavaPkg\'", true) -} - sourceSets { main { java { @@ -85,6 +81,10 @@ sourceSets { } } +javadoc { + options.addBooleanOption("exclude \'$generatedJavaPkg\'", true) +} + protobuf { protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" From 5d935bbadc86658a97c6bd7085946ea9badbd97c Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 17:18:34 -0700 Subject: [PATCH 11/14] review comments --- gradle.properties | 4 ++-- .../docs/modules/ROOT/pages/_partials/nav-versioned.adoc | 2 +- .../docs/modules/ROOT/pages/grpc/index.adoc | 5 +++-- .../examples/grpc/health/HealthClientExample.java | 4 ++-- .../plugin/internal/ServiceTalkLibraryPlugin.groovy | 1 - servicetalk-grpc-health/build.gradle | 9 +++++---- .../io/servicetalk/grpc/health/DefaultHealthService.java | 4 ++-- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/gradle.properties b/gradle.properties index c1d1b8ce25..8814d8ad09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,8 +15,8 @@ # # build configuration -#org.gradle.parallel=true -#org.gradle.caching=true +org.gradle.parallel=true +org.gradle.caching=true org.gradle.configureondemand=true org.gradle.jvmargs=-Xms2g -Xmx4g -dsa -da -ea:io.servicetalk... -XX:+HeapDumpOnOutOfMemoryError diff --git a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc index b6c5e97460..1994cbcb2f 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc @@ -21,7 +21,7 @@ ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Compression[Compression] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Deadlines[Deadlines] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Observer[Observer] -** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Health[Health] +** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Health[Health Checking] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#errors[Application Errors] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#execution-strategy[Execution Strategy] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#route-guide[Route Guide] diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index 725ed32877..961770886c 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -122,10 +122,11 @@ link:{source-root}/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/Gr on via a client filter on the client builder. [#Health] -== Health +== Health Checking This example demonstrates the following: - Use of -link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] in addition to a simple "hello world" service. +link:{source-root}/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java[DefaultHealthService] +which implements link:https://github.com/grpc/grpc/blob/master/doc/health-checking.md[gRPC health checking] paired with a simple "hello world" service. Using the following classes: * link:{source-root}/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java[HealthServerExample] a server diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java index 074703bd92..8734c66d26 100644 --- a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java @@ -16,6 +16,7 @@ package io.servicetalk.examples.grpc.health; import io.servicetalk.grpc.api.GrpcStatusException; +import io.servicetalk.grpc.health.DefaultHealthService; import io.servicetalk.grpc.netty.GrpcClients; import io.servicetalk.health.v1.Health; import io.servicetalk.health.v1.Health.BlockingHealthClient; @@ -24,10 +25,9 @@ import io.grpc.examples.health.Greeter; import io.grpc.examples.health.Greeter.BlockingGreeterClient; import io.grpc.examples.health.HelloRequest; -import io.grpc.examples.health.Greeter; /** - * Extends the async "Hello World" example to demonstrate health service usage. + * Extends the async "Hello World" example to demonstrate {@link DefaultHealthService} usage. */ public final class HealthClientExample { public static void main(String... args) throws Exception { diff --git a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy index 1208d420d6..4601bf1939 100644 --- a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy +++ b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkLibraryPlugin.groovy @@ -68,7 +68,6 @@ final class ServiceTalkLibraryPlugin extends ServiceTalkCorePlugin { options.addBooleanOption("Xwerror", true) options.addBooleanOption("Xdoclint:all,-missing", true) options.addBooleanOption("protected", true) - options.addBooleanOption("exclude 'io.servicetalk.health.v1'", true) } def sourcesJar = createSourcesJarTask(project, sourceSets.main) diff --git a/servicetalk-grpc-health/build.gradle b/servicetalk-grpc-health/build.gradle index 2e7ad494ef..607681f121 100644 --- a/servicetalk-grpc-health/build.gradle +++ b/servicetalk-grpc-health/build.gradle @@ -24,6 +24,7 @@ apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" apply plugin: "com.google.protobuf" dependencies { + api project(":servicetalk-grpc-api") // generated code is exposed implementation project(":servicetalk-annotations") implementation project(":servicetalk-grpc-netty") implementation project(":servicetalk-grpc-protoc") @@ -51,9 +52,9 @@ processResources { duplicatesStrategy = 'include' } -var extractedProtoDir = "$buildDir/extracted-protos/main" -var generatedCodeDir = "$projectDir/src/generated" -var generatedJavaPkg = "io.servicetalk.health.v1"; +def extractedProtoDir = "$buildDir/extracted-protos/main" +def generatedCodeDir = "$projectDir/src/generated" +def generatedJavaPkg = "io.servicetalk.health.v1"; task unzipGrpcProtos(type: Copy) { dependsOn processResources @@ -82,7 +83,7 @@ sourceSets { } javadoc { - options.addBooleanOption("exclude \'$generatedJavaPkg\'", true) + exclude "**/${generatedJavaPkg.replace('.', '/')}/**" } protobuf { diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java index ff4dcdb6ab..081533ed16 100644 --- a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -102,7 +102,7 @@ public Publisher watch(final GrpcServiceContext ctx, final return Publisher.from(newBuilder().setStatus(NOT_SERVING).build()); } healthValue = serviceToStatusMap.computeIfAbsent(request.getService(), - service -> new HealthValue(SERVICE_UNKNOWN)); + __ -> new HealthValue(SERVICE_UNKNOWN)); } finally { lock.unlock(); } @@ -129,7 +129,7 @@ public boolean setStatus(String service, ServingStatus status) { return false; } resp = newBuilder().setStatus(status).build(); - healthValue = serviceToStatusMap.computeIfAbsent(service, service2 -> new HealthValue(resp)); + healthValue = serviceToStatusMap.computeIfAbsent(service, __ -> new HealthValue(resp)); } finally { lock.unlock(); } From 267d50aa937bcaff869031f4d1e4ea584cc88a8c Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 17:24:08 -0700 Subject: [PATCH 12/14] example local vars --- .../examples/grpc/health/HealthClientExample.java | 11 +++++++---- .../examples/grpc/health/HealthServerExample.java | 11 ++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java index 8734c66d26..9374600fbb 100644 --- a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthClientExample.java @@ -21,9 +21,11 @@ import io.servicetalk.health.v1.Health; import io.servicetalk.health.v1.Health.BlockingHealthClient; import io.servicetalk.health.v1.HealthCheckRequest; +import io.servicetalk.health.v1.HealthCheckResponse; import io.grpc.examples.health.Greeter; import io.grpc.examples.health.Greeter.BlockingGreeterClient; +import io.grpc.examples.health.HelloReply; import io.grpc.examples.health.HelloRequest; /** @@ -39,8 +41,8 @@ public static void main(String... args) throws Exception { // Check health before checkHealth(healthClient, serviceName); - System.out.println("Response=" + client.sayHello(HelloRequest.newBuilder().setName("World").build()) - .getMessage()); + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("World").build()); + System.out.println("HelloReply=" + reply.getMessage()); // Check the health after to observe it changed. checkHealth(healthClient, serviceName); @@ -49,8 +51,9 @@ public static void main(String... args) throws Exception { private static void checkHealth(BlockingHealthClient healthClient, String serviceName) throws Exception { try { - System.out.println("Service '" + serviceName + "' health=" + - healthClient.check(HealthCheckRequest.newBuilder().setService(serviceName).build()).getStatus()); + HealthCheckResponse response = healthClient.check( + HealthCheckRequest.newBuilder().setService(serviceName).build()); + System.out.println("Service '" + serviceName + "' health=" + response.getStatus()); } catch (GrpcStatusException e) { System.out.println("Service '" + serviceName + "' health exception=" + e); } diff --git a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java index e983413b43..b1bfa01814 100644 --- a/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java +++ b/servicetalk-examples/grpc/health/src/main/java/io/servicetalk/examples/grpc/health/HealthServerExample.java @@ -30,12 +30,13 @@ public final class HealthServerExample { public static void main(String... args) throws Exception { DefaultHealthService healthService = new DefaultHealthService(); + GreeterService greeterService = (ctx, request) -> { + // For demonstration purposes, just use the name as a service and mark it as SERVING. + healthService.setStatus(request.getName(), SERVING); + return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); + }; GrpcServers.forPort(8080) - .listenAndAwait(healthService, (GreeterService) (ctx, request) -> { - // For demonstration purposes, just use the name as a service and mark it as SERVING. - healthService.setStatus(request.getName(), SERVING); - return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); - }) + .listenAndAwait(healthService, greeterService) .awaitShutdown(); } } From 2864c52bc4639300b464316fbcb9e08195615927 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 17:34:58 -0700 Subject: [PATCH 13/14] terminate to return NOT_SERVING instead of NOT_FOUND --- .../servicetalk/grpc/health/DefaultHealthService.java | 1 - .../grpc/health/DefaultHealthServiceTest.java | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java index 081533ed16..df44b51b91 100644 --- a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -174,7 +174,6 @@ public boolean terminate() { for (final HealthValue healthValue : serviceToStatusMap.values()) { healthValue.complete(NOT_SERVING); } - serviceToStatusMap.clear(); return true; } diff --git a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java index b9b551b0ae..bf143eecfa 100644 --- a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java +++ b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java @@ -124,12 +124,18 @@ void terminateWatchCheck() throws Exception { try (ServerContext serverCtx = GrpcServers.forAddress(localAddress(0)).listenAndAwait(service)) { try (Health.BlockingHealthClient client = GrpcClients.forResolvedAddress( (InetSocketAddress) serverCtx.listenAddress()).buildBlocking(new Health.ClientFactory())) { - assertThat(service.terminate(), equalTo(true)); BlockingIterator itr = client.watch(newRequest(OVERALL_SERVICE_NAME)).iterator(); + assertThat(itr.next().getStatus(), equalTo(SERVING)); + assertThat(client.check(newRequest(OVERALL_SERVICE_NAME)).getStatus(), equalTo(SERVING)); + + assertThat(service.terminate(), equalTo(true)); + assertThat(itr.next().getStatus(), equalTo(NOT_SERVING)); + assertThat(itr.hasNext(), equalTo(false)); + assertThat(client.check(newRequest(OVERALL_SERVICE_NAME)).getStatus(), equalTo(NOT_SERVING)); assertThat(service.setStatus(OVERALL_SERVICE_NAME, SERVING), equalTo(false)); - assertThat(service.clearStatus(OVERALL_SERVICE_NAME), equalTo(false)); + assertThat(service.clearStatus(OVERALL_SERVICE_NAME), equalTo(true)); assertThat(assertThrows(GrpcStatusException.class, () -> client.check(newRequest(OVERALL_SERVICE_NAME))).status().code(), equalTo(NOT_FOUND)); From 2d5dfadc37832c4a9438a8ee00c33931f68f91a8 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Mon, 14 Mar 2022 18:13:10 -0700 Subject: [PATCH 14/14] clarify assumptions about processor --- .../grpc/health/DefaultHealthService.java | 15 ++++++++++----- .../grpc/health/DefaultHealthServiceTest.java | 2 ++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java index df44b51b91..2ae60ed5fc 100644 --- a/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java +++ b/servicetalk-grpc-health/src/main/java/io/servicetalk/grpc/health/DefaultHealthService.java @@ -15,7 +15,7 @@ */ package io.servicetalk.grpc.health; -import io.servicetalk.concurrent.PublisherSource; +import io.servicetalk.concurrent.PublisherSource.Processor; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; import io.servicetalk.grpc.api.GrpcServiceContext; @@ -148,7 +148,7 @@ public boolean setStatus(String service, ServingStatus status) { public boolean clearStatus(String service) { final HealthValue healthValue = serviceToStatusMap.remove(service); if (healthValue != null) { - healthValue.complete(SERVICE_UNKNOWN); + healthValue.completeMultipleTerminalSafe(SERVICE_UNKNOWN); return true; } return false; @@ -172,13 +172,13 @@ public boolean terminate() { lock.unlock(); } for (final HealthValue healthValue : serviceToStatusMap.values()) { - healthValue.complete(NOT_SERVING); + healthValue.completeMultipleTerminalSafe(NOT_SERVING); } return true; } private static final class HealthValue { - private final PublisherSource.Processor processor; + private final Processor processor; private final Publisher publisher; private volatile HealthCheckResponse last; @@ -201,7 +201,12 @@ void next(HealthCheckResponse response) { processor.onNext(response); } - void complete(ServingStatus status) { + /** + * This method is safe to invoke multiple times. Safety is currently provided by default {@link Processor} + * implementations. + * @param status The last status to set. + */ + void completeMultipleTerminalSafe(ServingStatus status) { next(newBuilder().setStatus(status).build()); processor.onComplete(); } diff --git a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java index bf143eecfa..9a9de0ebb8 100644 --- a/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java +++ b/servicetalk-grpc-health/src/test/java/io/servicetalk/grpc/health/DefaultHealthServiceTest.java @@ -135,6 +135,8 @@ void terminateWatchCheck() throws Exception { assertThat(client.check(newRequest(OVERALL_SERVICE_NAME)).getStatus(), equalTo(NOT_SERVING)); assertThat(service.setStatus(OVERALL_SERVICE_NAME, SERVING), equalTo(false)); + + // Clear after terminate verifies that multiple termination doesn't cause issues. assertThat(service.clearStatus(OVERALL_SERVICE_NAME), equalTo(true)); assertThat(assertThrows(GrpcStatusException.class, () -> client.check(newRequest(OVERALL_SERVICE_NAME))).status().code(),