diff --git a/api/src/main/java/io/grpc/ManagedChannelRegistry.java b/api/src/main/java/io/grpc/ManagedChannelRegistry.java index 677856ed8d8..52a19d55ecb 100644 --- a/api/src/main/java/io/grpc/ManagedChannelRegistry.java +++ b/api/src/main/java/io/grpc/ManagedChannelRegistry.java @@ -145,6 +145,11 @@ static List> getHardCodedClasses() { } catch (ClassNotFoundException e) { logger.log(Level.FINE, "Unable to find NettyChannelProvider", e); } + try { + list.add(Class.forName("io.grpc.netty.UdsNettyChannelProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Unable to find UdsNettyChannelProvider", e); + } return Collections.unmodifiableList(list); } diff --git a/build.gradle b/build.gradle index f982f600bbe..bb36575570b 100644 --- a/build.gradle +++ b/build.gradle @@ -176,6 +176,8 @@ subprojects { netty: "io.netty:netty-codec-http2:[${nettyVersion}]", netty_epoll: "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64", + netty_epoll_common: "io.netty:netty-transport-native-epoll:${nettyVersion}", + netty_unix_common: "io.netty:netty-transport-native-unix-common:${nettyVersion}", netty_epoll_arm64: "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64", netty_proxy_handler: "io.netty:netty-handler-proxy:${nettyVersion}", diff --git a/netty/BUILD.bazel b/netty/BUILD.bazel index 668abc3ca30..c1768b61e75 100644 --- a/netty/BUILD.bazel +++ b/netty/BUILD.bazel @@ -20,6 +20,7 @@ java_library( "@io_netty_netty_codec_http2//jar", "@io_netty_netty_codec_socks//jar", "@io_netty_netty_common//jar", + "@io_netty_netty_transport_native_unix_common//jar", "@io_netty_netty_handler//jar", "@io_netty_netty_handler_proxy//jar", "@io_netty_netty_resolver//jar", diff --git a/netty/build.gradle b/netty/build.gradle index 5fdc8f20f08..78fb3b4e356 100644 --- a/netty/build.gradle +++ b/netty/build.gradle @@ -21,13 +21,15 @@ dependencies { implementation libraries.netty_proxy_handler, libraries.guava, libraries.errorprone, - libraries.perfmark + libraries.perfmark, + libraries.netty_unix_common // Tests depend on base class defined by core module. testImplementation project(':grpc-core').sourceSets.test.output, project(':grpc-api').sourceSets.test.output, project(':grpc-testing'), - project(':grpc-testing-proto') + project(':grpc-testing-proto'), + libraries.netty_epoll_common testRuntimeOnly libraries.netty_tcnative, libraries.conscrypt, libraries.netty_epoll diff --git a/netty/src/main/java/io/grpc/netty/UdsNameResolver.java b/netty/src/main/java/io/grpc/netty/UdsNameResolver.java new file mode 100644 index 00000000000..8fa8ea06250 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/UdsNameResolver.java @@ -0,0 +1,66 @@ +/* + * 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.netty; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Preconditions; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.netty.channel.unix.DomainSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class UdsNameResolver extends NameResolver { + private NameResolver.Listener2 listener; + private final String authority; + + UdsNameResolver(String authority, String targetPath) { + checkArgument(authority == null, "non-null authority not supported"); + this.authority = targetPath; + } + + @Override + public String getServiceAuthority() { + return this.authority; + } + + @Override + public void start(Listener2 listener) { + Preconditions.checkState(this.listener == null, "already started"); + this.listener = checkNotNull(listener, "listener"); + resolve(); + } + + @Override + public void refresh() { + resolve(); + } + + private void resolve() { + ResolutionResult.Builder resolutionResultBuilder = ResolutionResult.newBuilder(); + List servers = new ArrayList<>(1); + servers.add(new EquivalentAddressGroup(new DomainSocketAddress(authority))); + resolutionResultBuilder.setAddresses(Collections.unmodifiableList(servers)); + listener.onResult(resolutionResultBuilder.build()); + } + + @Override + public void shutdown() {} +} diff --git a/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java b/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java new file mode 100644 index 00000000000..ffc07ff6ecb --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/UdsNameResolverProvider.java @@ -0,0 +1,71 @@ +/* + * 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.netty; + +import com.google.common.base.Preconditions; +import io.grpc.Internal; +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.netty.channel.unix.DomainSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; + +@Internal +public final class UdsNameResolverProvider extends NameResolverProvider { + + private static final String SCHEME = "unix"; + + @Override + public UdsNameResolver newNameResolver(URI targetUri, NameResolver.Args args) { + if (SCHEME.equals(targetUri.getScheme())) { + return new UdsNameResolver(targetUri.getAuthority(), getTargetPathFromUri(targetUri)); + } else { + return null; + } + } + + static String getTargetPathFromUri(URI targetUri) { + Preconditions.checkArgument(SCHEME.equals(targetUri.getScheme()), "scheme must be " + SCHEME); + String targetPath = targetUri.getPath(); + if (targetPath == null) { + targetPath = Preconditions.checkNotNull(targetUri.getSchemeSpecificPart(), "targetPath"); + } + return targetPath; + } + + @Override + public String getDefaultScheme() { + return SCHEME; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 3; + } + + @Override + protected Collection> getProducedSocketAddressTypes() { + return Collections.singleton(DomainSocketAddress.class); + } +} diff --git a/netty/src/main/java/io/grpc/netty/UdsNettyChannelProvider.java b/netty/src/main/java/io/grpc/netty/UdsNettyChannelProvider.java new file mode 100644 index 00000000000..271f056d21f --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/UdsNettyChannelProvider.java @@ -0,0 +1,94 @@ +/* + * 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. + */ + +package io.grpc.netty; + +import io.grpc.CallCredentials; +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.Internal; +import io.grpc.ManagedChannelProvider; +import io.grpc.internal.SharedResourcePool; +import io.netty.channel.unix.DomainSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; + +/** Provider for {@link NettyChannelBuilder} instances for UDS channels. */ +@Internal +public final class UdsNettyChannelProvider extends ManagedChannelProvider { + + @Override + public boolean isAvailable() { + return (Utils.EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE != null); + } + + @Override + public int priority() { + return 3; + } + + @Override + public NettyChannelBuilder builderForAddress(String name, int port) { + throw new UnsupportedOperationException("host:port not supported"); + } + + @Override + public NettyChannelBuilder builderForTarget(String target) { + ChannelCredentials creds = InsecureChannelCredentials.create(); + ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(creds); + if (result.error != null) { + throw new RuntimeException(result.error); + } + return getNettyChannelBuilder(target, creds, null, result.negotiator); + } + + @Override + public NewChannelBuilderResult newChannelBuilder(String target, ChannelCredentials creds) { + ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(creds); + if (result.error != null) { + return NewChannelBuilderResult.error(result.error); + } + return NewChannelBuilderResult.channelBuilder( + getNettyChannelBuilder(target, creds, result.callCredentials, result.negotiator)); + } + + private static NettyChannelBuilder getNettyChannelBuilder( + String target, + ChannelCredentials creds, + CallCredentials callCredentials, + ProtocolNegotiator.ClientFactory negotiator) { + if (Utils.EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE == null) { + throw new IllegalStateException("Epoll is not available"); + } + String targetPath = UdsNameResolverProvider.getTargetPathFromUri(URI.create(target)); + NettyChannelBuilder builder = + new NettyChannelBuilder( + new DomainSocketAddress(targetPath), creds, callCredentials, negotiator); + builder = + builder + .eventLoopGroupPool( + SharedResourcePool.forResource(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP)) + .channelType(Utils.EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE); + return builder; + } + + @Override + protected Collection> getSupportedSocketAddressTypes() { + return Collections.singleton(DomainSocketAddress.class); + } +} diff --git a/netty/src/main/java/io/grpc/netty/Utils.java b/netty/src/main/java/io/grpc/netty/Utils.java index c2f2fa4a7bf..58712096cc2 100644 --- a/netty/src/main/java/io/grpc/netty/Utils.java +++ b/netty/src/main/java/io/grpc/netty/Utils.java @@ -89,6 +89,7 @@ class Utils { = new DefaultEventLoopGroupResource(1, "grpc-nio-boss-ELG", EventLoopGroupType.NIO); public static final Resource NIO_WORKER_EVENT_LOOP_GROUP = new DefaultEventLoopGroupResource(0, "grpc-nio-worker-ELG", EventLoopGroupType.NIO); + public static final Resource DEFAULT_BOSS_EVENT_LOOP_GROUP; public static final Resource DEFAULT_WORKER_EVENT_LOOP_GROUP; @@ -104,6 +105,7 @@ private static final class ByteBufAllocatorPreferHeapHolder { public static final ChannelFactory DEFAULT_SERVER_CHANNEL_FACTORY; public static final Class DEFAULT_CLIENT_CHANNEL_TYPE; + public static final Class EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE; @Nullable private static final Constructor EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR; @@ -112,6 +114,7 @@ private static final class ByteBufAllocatorPreferHeapHolder { // Decide default channel types and EventLoopGroup based on Epoll availability if (isEpollAvailable()) { DEFAULT_CLIENT_CHANNEL_TYPE = epollChannelType(); + EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = epollDomainSocketChannelType(); DEFAULT_SERVER_CHANNEL_FACTORY = new ReflectiveChannelFactory<>(epollServerChannelType()); EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = epollEventLoopGroupConstructor(); DEFAULT_BOSS_EVENT_LOOP_GROUP @@ -122,6 +125,7 @@ private static final class ByteBufAllocatorPreferHeapHolder { logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause()); DEFAULT_SERVER_CHANNEL_FACTORY = nioServerChannelFactory(); DEFAULT_CLIENT_CHANNEL_TYPE = NioSocketChannel.class; + EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = null; DEFAULT_BOSS_EVENT_LOOP_GROUP = NIO_BOSS_EVENT_LOOP_GROUP; DEFAULT_WORKER_EVENT_LOOP_GROUP = NIO_WORKER_EVENT_LOOP_GROUP; EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = null; @@ -326,6 +330,17 @@ private static Class epollChannelType() { } } + // Must call when epoll is available + private static Class epollDomainSocketChannelType() { + try { + Class channelType = Class + .forName("io.netty.channel.epoll.EpollDomainSocketChannel").asSubclass(Channel.class); + return channelType; + } catch (ClassNotFoundException e) { + throw new RuntimeException("Cannot load EpollDomainSocketChannel", e); + } + } + // Must call when epoll is available private static Constructor epollEventLoopGroupConstructor() { try { diff --git a/netty/src/main/resources/META-INF/services/io.grpc.ManagedChannelProvider b/netty/src/main/resources/META-INF/services/io.grpc.ManagedChannelProvider index ebd1bcdf024..e7b37ea49ac 100644 --- a/netty/src/main/resources/META-INF/services/io.grpc.ManagedChannelProvider +++ b/netty/src/main/resources/META-INF/services/io.grpc.ManagedChannelProvider @@ -1 +1,2 @@ io.grpc.netty.NettyChannelProvider +io.grpc.netty.UdsNettyChannelProvider diff --git a/netty/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/netty/src/main/resources/META-INF/services/io.grpc.NameResolverProvider new file mode 100644 index 00000000000..ec775013c1e --- /dev/null +++ b/netty/src/main/resources/META-INF/services/io.grpc.NameResolverProvider @@ -0,0 +1 @@ +io.grpc.netty.UdsNameResolverProvider diff --git a/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java b/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java new file mode 100644 index 00000000000..6a329c8fc68 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/UdsNameResolverProviderTest.java @@ -0,0 +1,124 @@ +/* + * 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. + */ + +package io.grpc.netty; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.netty.channel.unix.DomainSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link UdsNameResolverProvider}. */ +@RunWith(JUnit4.class) +public class UdsNameResolverProviderTest { + + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock + private NameResolver.Listener2 mockListener; + + @Captor + private ArgumentCaptor resultCaptor; + + UdsNameResolverProvider udsNameResolverProvider = new UdsNameResolverProvider(); + + + @Test + public void testUnixRelativePath() { + UdsNameResolver udsNameResolver = + udsNameResolverProvider.newNameResolver(URI.create("unix:sock.sock"), null); + assertThat(udsNameResolver).isNotNull(); + udsNameResolver.start(mockListener); + verify(mockListener).onResult(resultCaptor.capture()); + NameResolver.ResolutionResult result = resultCaptor.getValue(); + List list = result.getAddresses(); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + EquivalentAddressGroup eag = list.get(0); + assertThat(eag).isNotNull(); + List addresses = eag.getAddresses(); + assertThat(addresses).hasSize(1); + assertThat(addresses.get(0)).isInstanceOf(DomainSocketAddress.class); + DomainSocketAddress domainSocketAddress = (DomainSocketAddress) addresses.get(0); + assertThat(domainSocketAddress.path()).isEqualTo("sock.sock"); + } + + @Test + public void testUnixAbsolutePath() { + UdsNameResolver udsNameResolver = + udsNameResolverProvider.newNameResolver(URI.create("unix:/sock.sock"), null); + assertThat(udsNameResolver).isNotNull(); + udsNameResolver.start(mockListener); + verify(mockListener).onResult(resultCaptor.capture()); + NameResolver.ResolutionResult result = resultCaptor.getValue(); + List list = result.getAddresses(); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + EquivalentAddressGroup eag = list.get(0); + assertThat(eag).isNotNull(); + List addresses = eag.getAddresses(); + assertThat(addresses).hasSize(1); + assertThat(addresses.get(0)).isInstanceOf(DomainSocketAddress.class); + DomainSocketAddress domainSocketAddress = (DomainSocketAddress) addresses.get(0); + assertThat(domainSocketAddress.path()).isEqualTo("/sock.sock"); + } + + @Test + public void testUnixAbsoluteAlternatePath() { + UdsNameResolver udsNameResolver = + udsNameResolverProvider.newNameResolver(URI.create("unix:///sock.sock"), null); + assertThat(udsNameResolver).isNotNull(); + udsNameResolver.start(mockListener); + verify(mockListener).onResult(resultCaptor.capture()); + NameResolver.ResolutionResult result = resultCaptor.getValue(); + List list = result.getAddresses(); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + EquivalentAddressGroup eag = list.get(0); + assertThat(eag).isNotNull(); + List addresses = eag.getAddresses(); + assertThat(addresses).hasSize(1); + assertThat(addresses.get(0)).isInstanceOf(DomainSocketAddress.class); + DomainSocketAddress domainSocketAddress = (DomainSocketAddress) addresses.get(0); + assertThat(domainSocketAddress.path()).isEqualTo("/sock.sock"); + } + + @Test + public void testUnixPathWithAuthority() { + try { + udsNameResolverProvider.newNameResolver(URI.create("unix://localhost/sock.sock"), null); + fail("exception expected"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("non-null authority not supported"); + } + } +} diff --git a/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java b/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java new file mode 100644 index 00000000000..8eb010e23e5 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/UdsNameResolverTest.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package io.grpc.netty; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.netty.channel.unix.DomainSocketAddress; +import java.net.SocketAddress; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link UdsNameResolver}. */ +@RunWith(JUnit4.class) +public class UdsNameResolverTest { + + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); + + @Mock + private NameResolver.Listener2 mockListener; + + @Captor + private ArgumentCaptor resultCaptor; + + private UdsNameResolver udsNameResolver; + + @Test + public void testValidTargetPath() { + udsNameResolver = new UdsNameResolver(null, "sock.sock"); + udsNameResolver.start(mockListener); + verify(mockListener).onResult(resultCaptor.capture()); + NameResolver.ResolutionResult result = resultCaptor.getValue(); + List list = result.getAddresses(); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + EquivalentAddressGroup eag = list.get(0); + assertThat(eag).isNotNull(); + List addresses = eag.getAddresses(); + assertThat(addresses).hasSize(1); + assertThat(addresses.get(0)).isInstanceOf(DomainSocketAddress.class); + DomainSocketAddress domainSocketAddress = (DomainSocketAddress) addresses.get(0); + assertThat(domainSocketAddress.path()).isEqualTo("sock.sock"); + assertThat(udsNameResolver.getServiceAuthority()).isEqualTo("sock.sock"); + } + + @Test + public void testNonNullAuthority() { + try { + udsNameResolver = new UdsNameResolver("authority", "sock.sock"); + fail("exception expected"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("non-null authority not supported"); + } + } +} diff --git a/netty/src/test/java/io/grpc/netty/UdsNettyChannelProviderTest.java b/netty/src/test/java/io/grpc/netty/UdsNettyChannelProviderTest.java new file mode 100644 index 00000000000..047c1018dab --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/UdsNettyChannelProviderTest.java @@ -0,0 +1,197 @@ +/* + * 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. + */ + +package io.grpc.netty; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import io.grpc.Grpc; +import io.grpc.InsecureChannelCredentials; +import io.grpc.InternalServiceProviders; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.ManagedChannelProvider; +import io.grpc.ManagedChannelProvider.NewChannelBuilderResult; +import io.grpc.ManagedChannelRegistryAccessor; +import io.grpc.TlsChannelCredentials; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerDomainSocketChannel; +import io.netty.channel.unix.DomainSocketAddress; +import java.io.IOException; +import org.junit.After; +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link UdsNettyChannelProvider}. */ +@RunWith(JUnit4.class) +public class UdsNettyChannelProviderTest { + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Rule + public final GrpcCleanupRule cleanupRule = new GrpcCleanupRule(); + + + private UdsNettyChannelProvider provider = new UdsNettyChannelProvider(); + + private EventLoopGroup elg; + private EventLoopGroup boss; + + @After + public void tearDown() { + if (elg != null) { + elg.shutdownGracefully(); + } + if (boss != null) { + boss.shutdownGracefully(); + } + } + + @Test + public void provided() { + for (ManagedChannelProvider current + : InternalServiceProviders.getCandidatesViaServiceLoader( + ManagedChannelProvider.class, getClass().getClassLoader())) { + if (current instanceof UdsNettyChannelProvider) { + return; + } + } + fail("ServiceLoader unable to load UdsNettyChannelProvider"); + } + + @Test + public void providedHardCoded() { + for (Class current : ManagedChannelRegistryAccessor.getHardCodedClasses()) { + if (current == UdsNettyChannelProvider.class) { + return; + } + } + fail("Hard coded unable to load UdsNettyChannelProvider"); + } + + @Test + public void basicMethods() { + Assume.assumeTrue(provider.isAvailable()); + assertEquals(3, provider.priority()); + } + + @Test + public void builderForTarget() { + Assume.assumeTrue(Utils.isEpollAvailable()); + assertThat(provider.builderForTarget("unix:sock.sock")).isInstanceOf(NettyChannelBuilder.class); + } + + @Test + public void builderForTarget_badScheme() { + Assume.assumeTrue(Utils.isEpollAvailable()); + try { + provider.builderForTarget("dns:sock.sock"); + fail("exception expected"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("scheme must be unix"); + } + } + + @Test + public void newChannelBuilder_success() { + Assume.assumeTrue(Utils.isEpollAvailable()); + NewChannelBuilderResult result = + provider.newChannelBuilder("unix:sock.sock", TlsChannelCredentials.create()); + assertThat(result.getChannelBuilder()).isInstanceOf(NettyChannelBuilder.class); + } + + @Test + public void newChannelBuilder_badScheme() { + Assume.assumeTrue(Utils.isEpollAvailable()); + try { + provider.newChannelBuilder("dns:sock.sock", InsecureChannelCredentials.create()); + fail("exception expected"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("scheme must be unix"); + } + } + + @Test + public void managedChannelRegistry_newChannelBuilder() { + Assume.assumeTrue(Utils.isEpollAvailable()); + ManagedChannelBuilder managedChannelBuilder + = Grpc.newChannelBuilder("unix:///sock.sock", InsecureChannelCredentials.create()); + assertThat(managedChannelBuilder).isNotNull(); + ManagedChannel channel = managedChannelBuilder.build(); + assertThat(channel).isNotNull(); + assertThat(channel.authority()).isEqualTo("/sock.sock"); + channel.shutdownNow(); + } + + @Test + public void udsClientServerTestUsingProvider() throws IOException { + Assume.assumeTrue(Utils.isEpollAvailable()); + String socketPath = tempFolder.getRoot().getAbsolutePath() + "/test.socket"; + createUdsServer(socketPath); + ManagedChannelBuilder channelBuilder = + Grpc.newChannelBuilder("unix://" + socketPath, InsecureChannelCredentials.create()); + SimpleServiceGrpc.SimpleServiceBlockingStub stub = + SimpleServiceGrpc.newBlockingStub(cleanupRule.register(channelBuilder.build())); + assertThat(unaryRpc("buddy", stub)).isEqualTo("Hello buddy"); + } + + /** Say hello to server. */ + private static String unaryRpc( + String requestMessage, SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub) { + SimpleRequest request = SimpleRequest.newBuilder().setRequestMessage(requestMessage).build(); + SimpleResponse response = blockingStub.unaryRpc(request); + return response.getResponseMessage(); + } + + private void createUdsServer(String name) throws IOException { + elg = new EpollEventLoopGroup(); + boss = new EpollEventLoopGroup(1); + cleanupRule.register( + NettyServerBuilder.forAddress(new DomainSocketAddress(name)) + .bossEventLoopGroup(boss) + .workerEventLoopGroup(elg) + .channelType(EpollServerDomainSocketChannel.class) + .addService(new SimpleServiceImpl()) + .directExecutor() + .build() + .start()); + } + + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + + @Override + public void unaryRpc(SimpleRequest req, StreamObserver responseObserver) { + SimpleResponse response = + SimpleResponse.newBuilder() + .setResponseMessage("Hello " + req.getRequestMessage()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + } +}