diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcClientBuilder.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcClientBuilder.java new file mode 100644 index 0000000000..427973372c --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcClientBuilder.java @@ -0,0 +1,77 @@ +/* + * 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.api; + +import java.time.Duration; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link GrpcClientBuilder} that delegates all methods to another {@link GrpcClientBuilder}. + * + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + */ +public class DelegatingGrpcClientBuilder implements GrpcClientBuilder { + + private GrpcClientBuilder delegate; + + public DelegatingGrpcClientBuilder(final GrpcClientBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link GrpcClientBuilder} delegate. + * + * @return Delegate {@link GrpcClientBuilder}. + */ + protected final GrpcClientBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public GrpcClientBuilder initializeHttp(final HttpInitializer initializer) { + delegate = delegate.initializeHttp(initializer); + return this; + } + + @Override + public GrpcClientBuilder defaultTimeout(final Duration defaultTimeout) { + delegate = delegate.defaultTimeout(defaultTimeout); + return this; + } + + @Override + public > Client build(final GrpcClientFactory clientFactory) { + return delegate.build(clientFactory); + } + + @Override + public > BlockingClient buildBlocking( + final GrpcClientFactory clientFactory) { + return delegate.buildBlocking(clientFactory); + } + + @Override + public MultiClientBuilder buildMulti() { + return delegate.buildMulti(); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcServerBuilder.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcServerBuilder.java new file mode 100644 index 0000000000..14d12e231e --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DelegatingGrpcServerBuilder.java @@ -0,0 +1,86 @@ +/* + * 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.api; + +import io.servicetalk.concurrent.api.Single; + +import java.time.Duration; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link GrpcServerBuilder} that delegates all methods to another {@link GrpcServerBuilder}. + */ +public class DelegatingGrpcServerBuilder implements GrpcServerBuilder { + + private GrpcServerBuilder delegate; + + public DelegatingGrpcServerBuilder(final GrpcServerBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link GrpcServerBuilder} delegate. + * + * @return Delegate {@link GrpcServerBuilder}. + */ + protected final GrpcServerBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public GrpcServerBuilder initializeHttp(final HttpInitializer initializer) { + delegate = delegate.initializeHttp(initializer); + return this; + } + + @Override + public GrpcServerBuilder defaultTimeout(final Duration defaultTimeout) { + delegate = delegate.defaultTimeout(defaultTimeout); + return this; + } + + @Override + public GrpcServerBuilder lifecycleObserver(final GrpcLifecycleObserver lifecycleObserver) { + delegate = delegate.lifecycleObserver(lifecycleObserver); + return this; + } + + @Override + public Single listen(final GrpcBindableService... services) { + return delegate.listen(services); + } + + @Override + public Single listen(final GrpcServiceFactory... serviceFactories) { + return delegate.listen(serviceFactories); + } + + @Override + public GrpcServerContext listenAndAwait(final GrpcServiceFactory... serviceFactories) throws Exception { + return delegate.listenAndAwait(serviceFactories); + } + + @Override + public GrpcServerContext listenAndAwait(final GrpcBindableService... services) throws Exception { + return delegate.listenAndAwait(services); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcProviders.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcProviders.java new file mode 100644 index 0000000000..8b319c5646 --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcProviders.java @@ -0,0 +1,82 @@ +/* + * 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.api; + +import io.servicetalk.http.api.HttpProviders; +import io.servicetalk.http.api.HttpProviders.HttpServerBuilderProvider; +import io.servicetalk.http.api.HttpProviders.SingleAddressHttpClientBuilderProvider; + +import java.net.SocketAddress; +import java.util.ServiceLoader; + +/** + * A holder for all gRPC-specific providers that can be registered using {@link ServiceLoader}. + * + * @see HttpProviders + */ +public final class GrpcProviders { + + private GrpcProviders() { + // No instances. + } + + /** + * Provider for {@link GrpcClientBuilder}. + *

+ * An HTTP layer should use {@link SingleAddressHttpClientBuilderProvider}. + */ + @FunctionalInterface + public interface GrpcClientBuilderProvider { + + /** + * Returns a {@link GrpcClientBuilder} based on the address and pre-initialized {@link GrpcClientBuilder}. + *

+ * This method may return the pre-initialized {@code builder} as-is, or apply custom builder settings before + * returning it, or wrap it ({@link DelegatingGrpcClientBuilder} may be helpful). + * + * @param address a remote address used to create a {@link GrpcClientBuilder}, it can be resolved or unresolved + * based on the factory used + * @param builder pre-initialized {@link GrpcClientBuilder} + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + * @return a {@link GrpcClientBuilder} based on the address and pre-initialized {@link GrpcClientBuilder}. + * @see DelegatingGrpcClientBuilder + */ + GrpcClientBuilder newBuilder(U address, GrpcClientBuilder builder); + } + + /** + * Provider for {@link GrpcServerBuilder}. + *

+ * An HTTP layer should use {@link HttpServerBuilderProvider}. + */ + @FunctionalInterface + public interface GrpcServerBuilderProvider { + + /** + * Returns a {@link GrpcServerBuilder} based on the address and pre-initialized {@link GrpcServerBuilder}. + *

+ * This method may return the pre-initialized {@code builder} as-is, or apply custom builder settings before + * returning it, or wrap it ({@link DelegatingGrpcServerBuilder} may be helpful). + * + * @param address a server address used to create a {@link GrpcServerBuilder} + * @param builder pre-initialized {@link GrpcServerBuilder} + * @return a {@link GrpcServerBuilder} based on the address and pre-initialized{@link GrpcServerBuilder}. + * @see DelegatingGrpcServerBuilder + */ + GrpcServerBuilder newBuilder(SocketAddress address, GrpcServerBuilder builder); + } +} diff --git a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcClientBuilder.java b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcClientBuilder.java index 7e806ac123..4141942f4f 100644 --- a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcClientBuilder.java +++ b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcClientBuilder.java @@ -67,6 +67,7 @@ final class DefaultGrpcClientBuilder implements GrpcClientBuilder { private final Supplier> httpClientBuilderSupplier; + // Do not use this ctor directly, GrpcClients is the entry point for creating a new builder. DefaultGrpcClientBuilder(final Supplier> httpClientBuilderSupplier) { this.httpClientBuilderSupplier = httpClientBuilderSupplier; } diff --git a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcServerBuilder.java b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcServerBuilder.java index 2835aa89c6..9c05026032 100644 --- a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcServerBuilder.java +++ b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/DefaultGrpcServerBuilder.java @@ -91,6 +91,7 @@ final class DefaultGrpcServerBuilder implements GrpcServerBuilder, ServerBinder @Nullable private Duration defaultTimeout; + // Do not use this ctor directly, GrpcServers is the entry point for creating a new builder. DefaultGrpcServerBuilder(final Supplier httpServerBuilderSupplier) { this.httpServerBuilderSupplier = () -> httpServerBuilderSupplier.get() .protocols(h2Default()).allowDropRequestTrailers(true); diff --git a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcClients.java b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcClients.java index c4ff1fbb45..dac9615984 100644 --- a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcClients.java +++ b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcClients.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019 Apple Inc. and the ServiceTalk project authors + * Copyright © 2019, 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. @@ -19,93 +19,138 @@ import io.servicetalk.client.api.ServiceDiscoverer; import io.servicetalk.client.api.ServiceDiscovererEvent; import io.servicetalk.grpc.api.GrpcClientBuilder; +import io.servicetalk.grpc.api.GrpcProviders.GrpcClientBuilderProvider; import io.servicetalk.http.api.HttpHeaderNames; import io.servicetalk.http.netty.HttpClients; import io.servicetalk.transport.api.HostAndPort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.List; import java.util.function.Function; +import static io.servicetalk.utils.internal.ServiceLoaderUtils.loadProviders; + /** * A factory to create gRPC clients. */ public final class GrpcClients { + private static final Logger LOGGER = LoggerFactory.getLogger(GrpcClients.class); + + private static final List PROVIDERS; + private GrpcClients() { // No instances } + static { + final ClassLoader classLoader = GrpcClients.class.getClassLoader(); + PROVIDERS = loadProviders(GrpcClientBuilderProvider.class, classLoader, LOGGER); + } + + private static GrpcClientBuilder applyProviders( + final U address, GrpcClientBuilder builder) { + for (GrpcClientBuilderProvider provider : PROVIDERS) { + builder = provider.newBuilder(address, builder); + } + return builder; + } + /** * Creates a {@link GrpcClientBuilder} for an address with default {@link LoadBalancer} and DNS * {@link ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param host host to connect to, resolved by default using a DNS {@link ServiceDiscoverer}. * @param port port to connect to * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forAddress(final String host, final int port) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forSingleAddress(host, port)); + return forAddress(HostAndPort.of(host, port)); } /** * Creates a {@link GrpcClientBuilder} for an address with default {@link LoadBalancer} and DNS * {@link ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param address the {@code UnresolvedAddress} to connect to, resolved by default using a DNS * {@link ServiceDiscoverer}. * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forAddress(final HostAndPort address) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forSingleAddress(address)); + return applyProviders(address, new DefaultGrpcClientBuilder<>(() -> HttpClients.forSingleAddress(address))); } /** * Creates a {@link GrpcClientBuilder} for the passed {@code serviceName} with default {@link LoadBalancer} and a * DNS {@link ServiceDiscoverer} using SRV record lookups. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param serviceName the service name to query via SRV DNS. * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forServiceAddress(final String serviceName) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forServiceAddress(serviceName)); + return applyProviders(serviceName, + new DefaultGrpcClientBuilder<>(() -> HttpClients.forServiceAddress(serviceName))); } /** * Creates a {@link GrpcClientBuilder} for a resolved address with default {@link LoadBalancer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param host resolved host address to connect to. * @param port port to connect to * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forResolvedAddress(final String host, final int port) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(host, port)); + return forResolvedAddress(HostAndPort.of(host, port)); } /** * Creates a {@link GrpcClientBuilder} for an address with default {@link LoadBalancer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param address the {@code ResolvedAddress} to connect to. * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forResolvedAddress(final HostAndPort address) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address)); + return applyProviders(address, new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address))); } /** * Creates a {@link GrpcClientBuilder} for an address with default {@link LoadBalancer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param address the {@code InetSocketAddress} to connect to. * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forResolvedAddress( final InetSocketAddress address) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address)); + return applyProviders(address, new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address))); } /** * Creates a {@link GrpcClientBuilder} for an address with default {@link LoadBalancer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param address the {@code ResolvedAddress} to connect. This address will also be used for the * {@link HttpHeaderNames#HOST}. @@ -116,14 +161,17 @@ public static GrpcClientBuilder forResolve * if you want to disable this behavior. * @param The type of {@link SocketAddress}. * @return new builder for the address + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forResolvedAddress(final T address) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address)); + return applyProviders(address, new DefaultGrpcClientBuilder<>(() -> HttpClients.forResolvedAddress(address))); } /** * Creates a {@link GrpcClientBuilder} for a custom address type with default {@link LoadBalancer} and user * provided {@link ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link GrpcClientBuilderProvider}. * * @param serviceDiscoverer The {@link ServiceDiscoverer} to resolve addresses of remote servers to connect to. * The lifecycle of the provided {@link ServiceDiscoverer} should be managed by the caller. @@ -131,10 +179,12 @@ public static GrpcClientBuilder forResolvedAddre * @param the type of address before resolution (unresolved address) * @param the type of address after resolution (resolved address) * @return new builder with provided configuration + * @see GrpcClientBuilderProvider */ public static GrpcClientBuilder forAddress(final ServiceDiscoverer> serviceDiscoverer, final U address) { - return new DefaultGrpcClientBuilder<>(() -> HttpClients.forSingleAddress(serviceDiscoverer, address)); + return applyProviders(address, + new DefaultGrpcClientBuilder<>(() -> HttpClients.forSingleAddress(serviceDiscoverer, address))); } } diff --git a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcServers.java b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcServers.java index 09e487483e..c66826ee56 100644 --- a/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcServers.java +++ b/servicetalk-grpc-netty/src/main/java/io/servicetalk/grpc/netty/GrpcServers.java @@ -1,5 +1,5 @@ /* - * Copyright © 2019 Apple Inc. and the ServiceTalk project authors + * Copyright © 2019, 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. @@ -15,37 +15,68 @@ */ package io.servicetalk.grpc.netty; +import io.servicetalk.grpc.api.GrpcProviders.GrpcServerBuilderProvider; import io.servicetalk.grpc.api.GrpcServerBuilder; import io.servicetalk.http.netty.HttpServers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.List; + +import static io.servicetalk.utils.internal.ServiceLoaderUtils.loadProviders; /** * A factory to create gRPC servers. */ public final class GrpcServers { + private static final Logger LOGGER = LoggerFactory.getLogger(GrpcServers.class); + + private static final List PROVIDERS; + + static { + final ClassLoader classLoader = GrpcServers.class.getClassLoader(); + PROVIDERS = loadProviders(GrpcServerBuilderProvider.class, classLoader, LOGGER); + } + private GrpcServers() { // No instances } + private static GrpcServerBuilder applyProviders(final SocketAddress address, GrpcServerBuilder builder) { + for (GrpcServerBuilderProvider provider : PROVIDERS) { + builder = provider.newBuilder(address, builder); + } + return builder; + } + /** * New {@link GrpcServerBuilder} instance. + *

+ * The returned builder can be customized using {@link GrpcServerBuilderProvider}. * - * @param port the listen port for the server. - * @return a new builder. + * @param port the listen port for the server + * @return a new builder + * @see GrpcServerBuilderProvider */ - public static GrpcServerBuilder forPort(int port) { - return new DefaultGrpcServerBuilder(() -> HttpServers.forPort(port)); + public static GrpcServerBuilder forPort(final int port) { + final InetSocketAddress address = new InetSocketAddress(port); + return forAddress(address); } /** * New {@link GrpcServerBuilder} instance. + *

+ * The returned builder can be customized using {@link GrpcServerBuilderProvider}. * - * @param socketAddress the listen address for the server. - * @return a new builder. + * @param address the listen {@link SocketAddress} for the server + * @return a new builder + * @see GrpcServerBuilderProvider */ - public static GrpcServerBuilder forAddress(SocketAddress socketAddress) { - return new DefaultGrpcServerBuilder(() -> HttpServers.forAddress(socketAddress)); + public static GrpcServerBuilder forAddress(final SocketAddress address) { + return applyProviders(address, new DefaultGrpcServerBuilder(() -> HttpServers.forAddress(address))); } } diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcProvidersTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcProvidersTest.java new file mode 100644 index 0000000000..1d95bd685b --- /dev/null +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcProvidersTest.java @@ -0,0 +1,205 @@ +/* + * 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.netty; + +import io.servicetalk.client.api.TransportObserverConnectionFactoryFilter; +import io.servicetalk.grpc.api.BlockingGrpcClient; +import io.servicetalk.grpc.api.DelegatingGrpcClientBuilder; +import io.servicetalk.grpc.api.DelegatingGrpcServerBuilder; +import io.servicetalk.grpc.api.GrpcBindableService; +import io.servicetalk.grpc.api.GrpcClientBuilder; +import io.servicetalk.grpc.api.GrpcClientFactory; +import io.servicetalk.grpc.api.GrpcProviders.GrpcClientBuilderProvider; +import io.servicetalk.grpc.api.GrpcProviders.GrpcServerBuilderProvider; +import io.servicetalk.grpc.api.GrpcServerBuilder; +import io.servicetalk.grpc.api.GrpcServerContext; +import io.servicetalk.grpc.api.GrpcServiceContext; +import io.servicetalk.http.api.HttpProviders.HttpServerBuilderProvider; +import io.servicetalk.http.api.HttpProviders.SingleAddressHttpClientBuilderProvider; +import io.servicetalk.http.api.HttpServerBuilder; +import io.servicetalk.http.api.SingleAddressHttpClientBuilder; +import io.servicetalk.transport.api.HostAndPort; +import io.servicetalk.transport.api.ServerContext; +import io.servicetalk.transport.api.TransportObserver; +import io.servicetalk.transport.netty.internal.NoopTransportObserver; + +import io.grpc.examples.helloworld.Greeter; +import io.grpc.examples.helloworld.Greeter.BlockingGreeterService; +import io.grpc.examples.helloworld.Greeter.ClientFactory; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +@Execution(SAME_THREAD) +class GrpcProvidersTest { + + @BeforeEach + void reset() { + TestGrpcServerBuilderProvider.reset(); + TestGrpcClientBuilderProvider.reset(); + } + + @Test + void testNoProvidersForAddress() throws Exception { + try (ServerContext serverContext = GrpcServers.forAddress(localAddress(0)) + .listenAndAwait(BlockingGreeterServiceImpl.INSTANCE); + Greeter.BlockingGreeterClient client = GrpcClients.forAddress(serverHostAndPort(serverContext)) + .buildBlocking(new ClientFactory())) { + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("foo").build()); + assertThat(reply.getMessage(), is(equalTo("reply to foo"))); + } + } + + @Test + void testGrpcServerBuilderProvider() throws Exception { + final InetSocketAddress serverAddress = localAddress(0); + TestGrpcServerBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (ServerContext serverContext = GrpcServers.forAddress(localAddress(0)) + .listenAndAwait(BlockingGreeterServiceImpl.INSTANCE)) { + assertThat(TestGrpcServerBuilderProvider.BUILD_COUNTER.get(), is(1)); + try (Greeter.BlockingGreeterClient client = GrpcClients.forAddress(serverHostAndPort(serverContext)) + .buildBlocking(new ClientFactory())) { + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("foo").build()); + assertThat(reply.getMessage(), is(equalTo("reply to foo"))); + assertThat(TestGrpcServerBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + @Test + void testGrpcClientBuilderProvider() throws Exception { + try (ServerContext serverContext = GrpcServers.forAddress(localAddress(0)) + .listenAndAwait(BlockingGreeterServiceImpl.INSTANCE)) { + HostAndPort serverAddress = serverHostAndPort(serverContext); + TestGrpcClientBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (Greeter.BlockingGreeterClient client = GrpcClients.forAddress(serverAddress) + .buildBlocking(new ClientFactory())) { + assertThat(TestGrpcClientBuilderProvider.BUILD_COUNTER.get(), is(1)); + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("foo").build()); + assertThat(reply.getMessage(), is(equalTo("reply to foo"))); + assertThat(TestGrpcClientBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + private static final class BlockingGreeterServiceImpl implements BlockingGreeterService { + + static final BlockingGreeterService INSTANCE = new BlockingGreeterServiceImpl(); + + @Override + public HelloReply sayHello(GrpcServiceContext ctx, HelloRequest request) { + return HelloReply.newBuilder().setMessage("reply to " + request.getName()).build(); + } + } + + public static final class TestGrpcServerBuilderProvider implements GrpcServerBuilderProvider, + HttpServerBuilderProvider { + + static final AtomicReference MODIFY_FOR_ADDRESS = new AtomicReference<>(); + static final AtomicInteger BUILD_COUNTER = new AtomicInteger(); + static final AtomicInteger CONNECTION_COUNTER = new AtomicInteger(); + + static void reset() { + MODIFY_FOR_ADDRESS.set(null); + BUILD_COUNTER.set(0); + CONNECTION_COUNTER.set(0); + } + + @Override + public GrpcServerBuilder newBuilder(SocketAddress address, GrpcServerBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + return new DelegatingGrpcServerBuilder(builder) { + + @Override + public GrpcServerContext listenAndAwait(GrpcBindableService... services) throws Exception { + BUILD_COUNTER.incrementAndGet(); + return delegate().listenAndAwait(services); + } + }; + } + return builder; + } + + @Override + public HttpServerBuilder newBuilder(final SocketAddress address, final HttpServerBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + return builder.transportObserver(transportObserver(CONNECTION_COUNTER)); + } + return builder; + } + } + + public static final class TestGrpcClientBuilderProvider implements GrpcClientBuilderProvider, + SingleAddressHttpClientBuilderProvider { + + static final AtomicReference MODIFY_FOR_ADDRESS = new AtomicReference<>(); + static final AtomicInteger BUILD_COUNTER = new AtomicInteger(); + static final AtomicInteger CONNECTION_COUNTER = new AtomicInteger(); + + static void reset() { + MODIFY_FOR_ADDRESS.set(null); + BUILD_COUNTER.set(0); + CONNECTION_COUNTER.set(0); + } + + @Override + public GrpcClientBuilder newBuilder(U address, GrpcClientBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + return new DelegatingGrpcClientBuilder(builder) { + + @Override + public > BlockingClient buildBlocking( + GrpcClientFactory clientFactory) { + BUILD_COUNTER.incrementAndGet(); + return delegate().buildBlocking(clientFactory); + } + }; + } + return builder; + } + + @Override + public SingleAddressHttpClientBuilder newBuilder(U address, + SingleAddressHttpClientBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + return builder.appendConnectionFactoryFilter(new TransportObserverConnectionFactoryFilter<>( + transportObserver(CONNECTION_COUNTER))); + } + return builder; + } + } + + private static TransportObserver transportObserver(AtomicInteger counter) { + return (localAddress, remoteAddress) -> { + counter.incrementAndGet(); + return NoopTransportObserver.NoopConnectionObserver.INSTANCE; + }; + } +} diff --git a/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcClientBuilderProvider b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcClientBuilderProvider new file mode 100644 index 0000000000..895924e0ca --- /dev/null +++ b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcClientBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.grpc.netty.GrpcProvidersTest$TestGrpcClientBuilderProvider diff --git a/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcServerBuilderProvider b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcServerBuilderProvider new file mode 100644 index 0000000000..d2e90d5925 --- /dev/null +++ b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.grpc.api.GrpcProviders$GrpcServerBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.grpc.netty.GrpcProvidersTest$TestGrpcServerBuilderProvider diff --git a/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider new file mode 100644 index 0000000000..d2e90d5925 --- /dev/null +++ b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.grpc.netty.GrpcProvidersTest$TestGrpcServerBuilderProvider diff --git a/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider new file mode 100644 index 0000000000..895924e0ca --- /dev/null +++ b/servicetalk-grpc-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.grpc.netty.GrpcProvidersTest$TestGrpcClientBuilderProvider diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingHttpServerBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingHttpServerBuilder.java new file mode 100644 index 0000000000..27f5f91738 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingHttpServerBuilder.java @@ -0,0 +1,221 @@ +/* + * 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.http.api; + +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.logging.api.LogLevel; +import io.servicetalk.transport.api.ConnectionAcceptorFactory; +import io.servicetalk.transport.api.IoExecutor; +import io.servicetalk.transport.api.ServerSslConfig; +import io.servicetalk.transport.api.TransportObserver; + +import java.net.SocketOption; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link HttpServerBuilder} that delegates all methods to another {@link HttpServerBuilder}. + */ +public class DelegatingHttpServerBuilder implements HttpServerBuilder { + + private HttpServerBuilder delegate; + + /** + * Create a new instance. + * + * @param delegate {@link HttpServerBuilder} to which all methods are delegated. + */ + public DelegatingHttpServerBuilder(final HttpServerBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link HttpServerBuilder} delegate. + * + * @return Delegate {@link HttpServerBuilder}. + */ + protected final HttpServerBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public HttpServerBuilder protocols(final HttpProtocolConfig... protocols) { + delegate = delegate.protocols(protocols); + return this; + } + + @Override + public HttpServerBuilder sslConfig(final ServerSslConfig config) { + delegate = delegate.sslConfig(config); + return this; + } + + @Override + public HttpServerBuilder sslConfig(final ServerSslConfig defaultConfig, final Map sniMap) { + delegate = delegate.sslConfig(defaultConfig, sniMap); + return this; + } + + @Override + public HttpServerBuilder socketOption(final SocketOption option, final T value) { + delegate = delegate.socketOption(option, value); + return this; + } + + @Override + public HttpServerBuilder listenSocketOption(final SocketOption option, final T value) { + delegate = delegate.listenSocketOption(option, value); + return this; + } + + @Override + public HttpServerBuilder enableWireLogging(final String loggerName, final LogLevel logLevel, + final BooleanSupplier logUserData) { + delegate = delegate.enableWireLogging(loggerName, logLevel, logUserData); + return this; + } + + @Override + public HttpServerBuilder transportObserver(final TransportObserver transportObserver) { + delegate = delegate.transportObserver(transportObserver); + return this; + } + + @Override + public HttpServerBuilder lifecycleObserver(final HttpLifecycleObserver lifecycleObserver) { + delegate = delegate.lifecycleObserver(lifecycleObserver); + return this; + } + + @Override + public HttpServerBuilder drainRequestPayloadBody(final boolean enable) { + delegate = delegate.drainRequestPayloadBody(enable); + return this; + } + + @Override + public HttpServerBuilder allowDropRequestTrailers(final boolean allowDrop) { + delegate = delegate.allowDropRequestTrailers(allowDrop); + return this; + } + + @Override + public HttpServerBuilder appendConnectionAcceptorFilter(final ConnectionAcceptorFactory factory) { + delegate = delegate.appendConnectionAcceptorFilter(factory); + return this; + } + + @Override + public HttpServerBuilder appendNonOffloadingServiceFilter(final StreamingHttpServiceFilterFactory factory) { + delegate = delegate.appendNonOffloadingServiceFilter(factory); + return this; + } + + @Override + public HttpServerBuilder appendNonOffloadingServiceFilter(final Predicate predicate, + final StreamingHttpServiceFilterFactory factory) { + delegate = delegate.appendNonOffloadingServiceFilter(predicate, factory); + return this; + } + + @Override + public HttpServerBuilder appendServiceFilter(final StreamingHttpServiceFilterFactory factory) { + delegate = delegate.appendServiceFilter(factory); + return this; + } + + @Override + public HttpServerBuilder appendServiceFilter(final Predicate predicate, + final StreamingHttpServiceFilterFactory factory) { + delegate = delegate.appendServiceFilter(predicate, factory); + return this; + } + + @Override + public HttpServerBuilder ioExecutor(final IoExecutor ioExecutor) { + delegate = delegate.ioExecutor(ioExecutor); + return this; + } + + @Override + public HttpServerBuilder executor(final Executor executor) { + delegate = delegate.executor(executor); + return this; + } + + @Override + public HttpServerBuilder bufferAllocator(final BufferAllocator allocator) { + delegate = delegate.bufferAllocator(allocator); + return this; + } + + @Override + public HttpServerBuilder executionStrategy(final HttpExecutionStrategy strategy) { + delegate = delegate.executionStrategy(strategy); + return this; + } + + @Override + public Single listen(final HttpService service) { + return delegate.listen(service); + } + + @Override + public Single listenStreaming(final StreamingHttpService service) { + return delegate.listenStreaming(service); + } + + @Override + public Single listenBlocking(final BlockingHttpService service) { + return delegate.listenBlocking(service); + } + + @Override + public Single listenBlockingStreaming(final BlockingStreamingHttpService service) { + return delegate.listenBlockingStreaming(service); + } + + @Override + public HttpServerContext listenAndAwait(final HttpService service) throws Exception { + return delegate.listenAndAwait(service); + } + + @Override + public HttpServerContext listenStreamingAndAwait(final StreamingHttpService service) throws Exception { + return delegate.listenStreamingAndAwait(service); + } + + @Override + public HttpServerContext listenBlockingAndAwait(final BlockingHttpService service) throws Exception { + return delegate.listenBlockingAndAwait(service); + } + + @Override + public HttpServerContext listenBlockingStreamingAndAwait(final BlockingStreamingHttpService service) + throws Exception { + return delegate.listenBlockingStreamingAndAwait(service); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingMultiAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingMultiAddressHttpClientBuilder.java new file mode 100644 index 0000000000..5cfabe0528 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingMultiAddressHttpClientBuilder.java @@ -0,0 +1,118 @@ +/* + * 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.http.api; + +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.transport.api.IoExecutor; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link MultiAddressHttpClientBuilder} that delegates all methods to another {@link MultiAddressHttpClientBuilder}. + * + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + */ +public class DelegatingMultiAddressHttpClientBuilder implements MultiAddressHttpClientBuilder { + + private MultiAddressHttpClientBuilder delegate; + + /** + * Create a new instance. + * + * @param delegate {@link MultiAddressHttpClientBuilder} to which all methods are delegated. + */ + public DelegatingMultiAddressHttpClientBuilder(final MultiAddressHttpClientBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link MultiAddressHttpClientBuilder} delegate. + * + * @return Delegate {@link MultiAddressHttpClientBuilder}. + */ + protected final MultiAddressHttpClientBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public MultiAddressHttpClientBuilder ioExecutor(final IoExecutor ioExecutor) { + delegate = delegate.ioExecutor(ioExecutor); + return this; + } + + @Override + public MultiAddressHttpClientBuilder executor(final Executor executor) { + delegate = delegate.executor(executor); + return this; + } + + @Override + public MultiAddressHttpClientBuilder executionStrategy(final HttpExecutionStrategy strategy) { + delegate = delegate.executionStrategy(strategy); + return this; + } + + @Override + public MultiAddressHttpClientBuilder bufferAllocator(final BufferAllocator allocator) { + delegate = delegate.bufferAllocator(allocator); + return this; + } + + @Override + public MultiAddressHttpClientBuilder headersFactory(final HttpHeadersFactory headersFactory) { + delegate = delegate.headersFactory(headersFactory); + return this; + } + + @Override + public MultiAddressHttpClientBuilder initializer(final SingleAddressInitializer initializer) { + delegate = delegate.initializer(initializer); + return this; + } + + @Override + public MultiAddressHttpClientBuilder followRedirects(final RedirectConfig config) { + delegate = delegate.followRedirects(config); + return this; + } + + @Override + public HttpClient build() { + return delegate.build(); + } + + @Override + public StreamingHttpClient buildStreaming() { + return delegate.buildStreaming(); + } + + @Override + public BlockingHttpClient buildBlocking() { + return delegate.buildBlocking(); + } + + @Override + public BlockingStreamingHttpClient buildBlockingStreaming() { + return delegate.buildBlockingStreaming(); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingPartitionedHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingPartitionedHttpClientBuilder.java new file mode 100644 index 0000000000..6072e62825 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingPartitionedHttpClientBuilder.java @@ -0,0 +1,143 @@ +/* + * 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.http.api; + +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.client.api.ServiceDiscoverer; +import io.servicetalk.client.api.partition.PartitionMapFactory; +import io.servicetalk.client.api.partition.PartitionedServiceDiscovererEvent; +import io.servicetalk.concurrent.api.BiIntFunction; +import io.servicetalk.concurrent.api.Completable; +import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.transport.api.IoExecutor; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link PartitionedHttpClientBuilder} that delegates all methods to another {@link PartitionedHttpClientBuilder}. + * + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + */ +public class DelegatingPartitionedHttpClientBuilder implements PartitionedHttpClientBuilder { + + private PartitionedHttpClientBuilder delegate; + + /** + * Create a new instance. + * + * @param delegate {@link PartitionedHttpClientBuilder} to which all methods are delegated. + */ + public DelegatingPartitionedHttpClientBuilder(final PartitionedHttpClientBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link PartitionedHttpClientBuilder} delegate. + * + * @return Delegate {@link PartitionedHttpClientBuilder}. + */ + protected final PartitionedHttpClientBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public PartitionedHttpClientBuilder ioExecutor(final IoExecutor ioExecutor) { + delegate = delegate.ioExecutor(ioExecutor); + return this; + } + + @Override + public PartitionedHttpClientBuilder executor(final Executor executor) { + delegate = delegate.executor(executor); + return this; + } + + @Override + public PartitionedHttpClientBuilder executionStrategy(final HttpExecutionStrategy strategy) { + delegate = delegate.executionStrategy(strategy); + return this; + } + + @Override + public PartitionedHttpClientBuilder bufferAllocator(final BufferAllocator allocator) { + delegate = delegate.bufferAllocator(allocator); + return this; + } + + @Override + public PartitionedHttpClientBuilder headersFactory(final HttpHeadersFactory headersFactory) { + delegate = delegate.headersFactory(headersFactory); + return this; + } + + @Override + public PartitionedHttpClientBuilder serviceDiscoverer( + final ServiceDiscoverer> serviceDiscoverer) { + delegate = delegate.serviceDiscoverer(serviceDiscoverer); + return this; + } + + @Override + public PartitionedHttpClientBuilder retryServiceDiscoveryErrors( + final BiIntFunction retryStrategy) { + delegate = delegate.retryServiceDiscoveryErrors(retryStrategy); + return this; + } + + @Override + public PartitionedHttpClientBuilder serviceDiscoveryMaxQueueSize(final int serviceDiscoveryMaxQueueSize) { + delegate = delegate.serviceDiscoveryMaxQueueSize(serviceDiscoveryMaxQueueSize); + return this; + } + + @Override + public PartitionedHttpClientBuilder partitionMapFactory(final PartitionMapFactory partitionMapFactory) { + delegate = delegate.partitionMapFactory(partitionMapFactory); + return this; + } + + @Override + public PartitionedHttpClientBuilder initializer(final SingleAddressInitializer initializer) { + delegate = delegate.initializer(initializer); + return this; + } + + @Override + public HttpClient build() { + return delegate.build(); + } + + @Override + public StreamingHttpClient buildStreaming() { + return delegate.buildStreaming(); + } + + @Override + public BlockingHttpClient buildBlocking() { + return delegate.buildBlocking(); + } + + @Override + public BlockingStreamingHttpClient buildBlockingStreaming() { + return delegate.buildBlockingStreaming(); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingSingleAddressHttpClientBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingSingleAddressHttpClientBuilder.java new file mode 100644 index 0000000000..87e00b374f --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DelegatingSingleAddressHttpClientBuilder.java @@ -0,0 +1,236 @@ +/* + * 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.http.api; + +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.client.api.ConnectionFactoryFilter; +import io.servicetalk.client.api.ServiceDiscoverer; +import io.servicetalk.client.api.ServiceDiscovererEvent; +import io.servicetalk.concurrent.api.BiIntFunction; +import io.servicetalk.concurrent.api.Completable; +import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.logging.api.LogLevel; +import io.servicetalk.transport.api.ClientSslConfig; +import io.servicetalk.transport.api.IoExecutor; + +import java.net.SocketOption; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Predicate; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link SingleAddressHttpClientBuilder} that delegates all methods to another + * {@link SingleAddressHttpClientBuilder}. + * + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + */ +public class DelegatingSingleAddressHttpClientBuilder implements SingleAddressHttpClientBuilder { + + private SingleAddressHttpClientBuilder delegate; + + /** + * Create a new instance. + * + * @param delegate {@link SingleAddressHttpClientBuilder} to which all methods are delegated. + */ + public DelegatingSingleAddressHttpClientBuilder(final SingleAddressHttpClientBuilder delegate) { + this.delegate = requireNonNull(delegate); + } + + /** + * Returns the {@link SingleAddressHttpClientBuilder} delegate. + * + * @return Delegate {@link SingleAddressHttpClientBuilder}. + */ + protected final SingleAddressHttpClientBuilder delegate() { + return delegate; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "{delegate=" + delegate() + '}'; + } + + @Override + public SingleAddressHttpClientBuilder proxyAddress(final U proxyAddress) { + delegate = delegate.proxyAddress(proxyAddress); + return this; + } + + @Override + public SingleAddressHttpClientBuilder socketOption(final SocketOption option, final T value) { + delegate = delegate.socketOption(option, value); + return this; + } + + @Override + public SingleAddressHttpClientBuilder enableWireLogging(final String loggerName, final LogLevel logLevel, + final BooleanSupplier logUserData) { + delegate = delegate.enableWireLogging(loggerName, logLevel, logUserData); + return this; + } + + @Override + public SingleAddressHttpClientBuilder protocols(final HttpProtocolConfig... protocols) { + delegate = delegate.protocols(protocols); + return this; + } + + @Override + public SingleAddressHttpClientBuilder hostHeaderFallback(final boolean enable) { + delegate = delegate.hostHeaderFallback(enable); + return this; + } + + @Override + public SingleAddressHttpClientBuilder allowDropResponseTrailers(final boolean allowDrop) { + delegate = delegate.allowDropResponseTrailers(allowDrop); + return this; + } + + @Override + public SingleAddressHttpClientBuilder appendConnectionFilter( + final StreamingHttpConnectionFilterFactory factory) { + delegate = delegate.appendConnectionFilter(factory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder appendConnectionFilter( + final Predicate predicate, final StreamingHttpConnectionFilterFactory factory) { + delegate = delegate.appendConnectionFilter(predicate, factory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder ioExecutor(final IoExecutor ioExecutor) { + delegate = delegate.ioExecutor(ioExecutor); + return this; + } + + @Override + public SingleAddressHttpClientBuilder executor(final Executor executor) { + delegate = delegate.executor(executor); + return this; + } + + @Override + public SingleAddressHttpClientBuilder executionStrategy(final HttpExecutionStrategy strategy) { + delegate = delegate.executionStrategy(strategy); + return this; + } + + @Override + public SingleAddressHttpClientBuilder bufferAllocator(final BufferAllocator allocator) { + delegate = delegate.bufferAllocator(allocator); + return this; + } + + @Override + public SingleAddressHttpClientBuilder appendConnectionFactoryFilter( + final ConnectionFactoryFilter factory) { + delegate = delegate.appendConnectionFactoryFilter(factory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder appendClientFilter(final StreamingHttpClientFilterFactory factory) { + delegate = delegate.appendClientFilter(factory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder appendClientFilter(final Predicate predicate, + final StreamingHttpClientFilterFactory factory) { + delegate = delegate.appendClientFilter(predicate, factory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder unresolvedAddressToHost( + final Function unresolvedAddressToHostFunction) { + delegate = delegate.unresolvedAddressToHost(unresolvedAddressToHostFunction); + return this; + } + + @Override + public SingleAddressHttpClientBuilder serviceDiscoverer( + final ServiceDiscoverer> serviceDiscoverer) { + delegate = delegate.serviceDiscoverer(serviceDiscoverer); + return this; + } + + @Override + public SingleAddressHttpClientBuilder retryServiceDiscoveryErrors( + final BiIntFunction retryStrategy) { + delegate = delegate.retryServiceDiscoveryErrors(retryStrategy); + return this; + } + + @Override + public SingleAddressHttpClientBuilder loadBalancerFactory( + final HttpLoadBalancerFactory loadBalancerFactory) { + delegate = delegate.loadBalancerFactory(loadBalancerFactory); + return this; + } + + @Override + public SingleAddressHttpClientBuilder sslConfig(final ClientSslConfig sslConfig) { + delegate = delegate.sslConfig(sslConfig); + return this; + } + + @Override + public SingleAddressHttpClientBuilder inferPeerHost(final boolean shouldInfer) { + delegate = delegate.inferPeerHost(shouldInfer); + return this; + } + + @Override + public SingleAddressHttpClientBuilder inferPeerPort(final boolean shouldInfer) { + delegate = delegate.inferPeerPort(shouldInfer); + return this; + } + + @Override + public SingleAddressHttpClientBuilder inferSniHostname(final boolean shouldInfer) { + delegate = delegate.inferSniHostname(shouldInfer); + return this; + } + + @Override + public HttpClient build() { + return delegate.build(); + } + + @Override + public StreamingHttpClient buildStreaming() { + return delegate.buildStreaming(); + } + + @Override + public BlockingHttpClient buildBlocking() { + return delegate.buildBlocking(); + } + + @Override + public BlockingStreamingHttpClient buildBlockingStreaming() { + return delegate.buildBlockingStreaming(); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProviders.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProviders.java new file mode 100644 index 0000000000..361eb77f9b --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpProviders.java @@ -0,0 +1,97 @@ +/* + * 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.http.api; + +import java.net.SocketAddress; +import java.util.ServiceLoader; + +/** + * A holder for all HTTP-specific providers that can be registered using {@link ServiceLoader}. + */ +public final class HttpProviders { + + private HttpProviders() { + // No instances. + } + + /** + * Provider for {@link SingleAddressHttpClientBuilder}. + */ + @FunctionalInterface + public interface SingleAddressHttpClientBuilderProvider { + + /** + * Returns a {@link SingleAddressHttpClientBuilder} based on the address and pre-initialized + * {@link SingleAddressHttpClientBuilder}. + *

+ * This method may return the pre-initialized {@code builder} as-is, or apply custom builder settings before + * returning it, or wrap it ({@link DelegatingSingleAddressHttpClientBuilder} may be helpful). + * + * @param address a remote address used to create a {@link SingleAddressHttpClientBuilder}, it can be resolved + * or unresolved based on the factory used + * @param builder pre-initialized {@link SingleAddressHttpClientBuilder} + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + * @return a {@link SingleAddressHttpClientBuilder} based on the address and pre-initialized + * {@link SingleAddressHttpClientBuilder}. + * @see DelegatingSingleAddressHttpClientBuilder + */ + SingleAddressHttpClientBuilder newBuilder(U address, SingleAddressHttpClientBuilder builder); + } + + /** + * Provider for {@link MultiAddressHttpClientBuilder}. + */ + @FunctionalInterface + public interface MultiAddressHttpClientBuilderProvider { + + /** + * Returns a {@link MultiAddressHttpClientBuilder} based on the pre-initialized + * {@link MultiAddressHttpClientBuilder}. + *

+ * This method may return the pre-initialized {@code builder} as-is, or apply custom builder settings before + * returning it, or wrap it ({@link DelegatingMultiAddressHttpClientBuilder} may be helpful). + * + * @param builder pre-initialized {@link MultiAddressHttpClientBuilder} + * @param the type of address before resolution (unresolved address) + * @param the type of address after resolution (resolved address) + * @return a {@link MultiAddressHttpClientBuilder} based on the pre-initialized + * {@link MultiAddressHttpClientBuilder}. + * @see DelegatingMultiAddressHttpClientBuilder + */ + MultiAddressHttpClientBuilder newBuilder(MultiAddressHttpClientBuilder builder); + } + + /** + * Provider for {@link HttpServerBuilder}. + */ + @FunctionalInterface + public interface HttpServerBuilderProvider { + + /** + * Returns a {@link HttpServerBuilder} based on the address and pre-initialized {@link HttpServerBuilder}. + *

+ * This method may return the pre-initialized {@code builder} as-is, or apply custom builder settings before + * returning it, or wrap it ({@link DelegatingHttpServerBuilder} may be helpful). + * + * @param address a server address used to create a {@link HttpServerBuilder} + * @param builder pre-initialized {@link HttpServerBuilder} + * @return a {@link HttpServerBuilder} based on the address and pre-initialized{@link HttpServerBuilder}. + * @see DelegatingHttpServerBuilder + */ + HttpServerBuilder newBuilder(SocketAddress address, HttpServerBuilder builder); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpServerBuilder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpServerBuilder.java index cf956c0747..5848a140cc 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpServerBuilder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpServerBuilder.java @@ -334,14 +334,14 @@ default HttpServerContext listenAndAwait(HttpService service) throws Exception { *

* If the underlying protocol (e.g. TCP) supports it this will result in a socket bind/listen on {@code address}. * - * @param handler Service invoked for every request received by this server. The returned {@link HttpServerContext} + * @param service Service invoked for every request received by this server. The returned {@link HttpServerContext} * manages the lifecycle of the {@code service}, ensuring it is closed when the {@link HttpServerContext} is closed. * @return A {@link HttpServerContext} by blocking the calling thread until the server is successfully started or * throws an {@link Exception} if the server could not be started. * @throws Exception if the server could not be started. */ - default HttpServerContext listenStreamingAndAwait(StreamingHttpService handler) throws Exception { - return blockingInvocation(listenStreaming(handler)); + default HttpServerContext listenStreamingAndAwait(StreamingHttpService service) throws Exception { + return blockingInvocation(listenStreaming(service)); } /** @@ -364,14 +364,14 @@ default HttpServerContext listenBlockingAndAwait(BlockingHttpService service) th *

* If the underlying protocol (e.g. TCP) supports it this will result in a socket bind/listen on {@code address}. * - * @param handler Service invoked for every request received by this server. The returned {@link HttpServerContext} + * @param service Service invoked for every request received by this server. The returned {@link HttpServerContext} * manages the lifecycle of the {@code service}, ensuring it is closed when the {@link HttpServerContext} is closed. * @return A {@link HttpServerContext} by blocking the calling thread until the server is successfully started or * throws an {@link Exception} if the server could not be started. * @throws Exception if the server could not be started. */ - default HttpServerContext listenBlockingStreamingAndAwait(BlockingStreamingHttpService handler) throws Exception { - return blockingInvocation(listenBlockingStreaming(handler)); + default HttpServerContext listenBlockingStreamingAndAwait(BlockingStreamingHttpService service) throws Exception { + return blockingInvocation(listenBlockingStreaming(service)); } /** diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultHttpServerBuilder.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultHttpServerBuilder.java index 1dfc7a4ef0..3b71bd246b 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultHttpServerBuilder.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/DefaultHttpServerBuilder.java @@ -84,6 +84,7 @@ final class DefaultHttpServerBuilder implements HttpServerBuilder { private final HttpExecutionContextBuilder executionContextBuilder = new HttpExecutionContextBuilder(); private final SocketAddress address; + // Do not use this ctor directly, HttpServers is the entry point for creating a new builder. DefaultHttpServerBuilder(SocketAddress address) { appendNonOffloadingServiceFilter(ClearAsyncContextHttpServiceFilter.CLEAR_ASYNC_CONTEXT_HTTP_SERVICE_FILTER); this.address = address; diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClients.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClients.java index f33ebe2f5e..d270a42e86 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClients.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpClients.java @@ -27,6 +27,8 @@ import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpHeaderNames; +import io.servicetalk.http.api.HttpProviders.MultiAddressHttpClientBuilderProvider; +import io.servicetalk.http.api.HttpProviders.SingleAddressHttpClientBuilderProvider; import io.servicetalk.http.api.HttpRequestMetaData; import io.servicetalk.http.api.MultiAddressHttpClientBuilder; import io.servicetalk.http.api.MultiAddressHttpClientBuilder.SingleAddressInitializer; @@ -36,9 +38,13 @@ import io.servicetalk.transport.api.HostAndPort; import io.servicetalk.transport.netty.internal.BuilderUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Collection; +import java.util.List; import java.util.function.Function; import static io.servicetalk.concurrent.api.AsyncCloseables.emptyAsyncCloseable; @@ -46,6 +52,7 @@ import static io.servicetalk.http.netty.GlobalDnsServiceDiscoverer.globalDnsServiceDiscoverer; import static io.servicetalk.http.netty.GlobalDnsServiceDiscoverer.globalSrvDnsServiceDiscoverer; import static io.servicetalk.http.netty.GlobalDnsServiceDiscoverer.mappingServiceDiscoverer; +import static io.servicetalk.utils.internal.ServiceLoaderUtils.loadProviders; import static java.util.function.Function.identity; /** @@ -53,10 +60,37 @@ */ public final class HttpClients { + private static final Logger LOGGER = LoggerFactory.getLogger(HttpClients.class); + + private static final List SINGLE_ADDRESS_PROVIDERS; + private static final List MULTI_ADDRESS_PROVIDERS; + + static { + final ClassLoader classLoader = HttpClients.class.getClassLoader(); + SINGLE_ADDRESS_PROVIDERS = loadProviders(SingleAddressHttpClientBuilderProvider.class, classLoader, LOGGER); + MULTI_ADDRESS_PROVIDERS = loadProviders(MultiAddressHttpClientBuilderProvider.class, classLoader, LOGGER); + } + private HttpClients() { // No instances } + private static SingleAddressHttpClientBuilder applyProviders( + final U address, SingleAddressHttpClientBuilder builder) { + for (SingleAddressHttpClientBuilderProvider provider : SINGLE_ADDRESS_PROVIDERS) { + builder = provider.newBuilder(address, builder); + } + return builder; + } + + private static MultiAddressHttpClientBuilder applyProviders( + MultiAddressHttpClientBuilder builder) { + for (MultiAddressHttpClientBuilderProvider provider : MULTI_ADDRESS_PROVIDERS) { + builder = provider.newBuilder(builder); + } + return builder; + } + /** * Creates a {@link MultiAddressHttpClientBuilder} for clients capable of parsing an absolute-form URL, connecting to multiple addresses @@ -65,11 +99,14 @@ private HttpClients() { * When a relative URL is passed in the {@link * StreamingHttpRequest#requestTarget(String)} this client requires a {@link HttpHeaderNames#HOST} present in * order to infer the remote address. + *

+ * The returned builder can be customized using {@link MultiAddressHttpClientBuilderProvider}. * * @return new builder with default configuration + * @see MultiAddressHttpClientBuilderProvider */ public static MultiAddressHttpClientBuilder forMultiAddressUrl() { - return new DefaultMultiAddressUrlHttpClientBuilder(HttpClients::forSingleAddress); + return applyProviders(new DefaultMultiAddressUrlHttpClientBuilder(HttpClients::forSingleAddress)); } /** @@ -80,10 +117,13 @@ public static MultiAddressHttpClientBuilder forM * When a relative URL is passed in the {@link * StreamingHttpRequest#requestTarget(String)} this client requires a {@link HttpHeaderNames#HOST} present in * order to infer the remote address. + *

+ * The returned builder can be customized using {@link MultiAddressHttpClientBuilderProvider}. * * @param serviceDiscoverer The {@link ServiceDiscoverer} to resolve addresses of remote servers to connect to. * The lifecycle of the provided {@link ServiceDiscoverer} should be managed by the caller. * @return new builder with default configuration + * @see MultiAddressHttpClientBuilderProvider * @deprecated Use {@link #forMultiAddressUrl()} to create {@link MultiAddressHttpClientBuilder}, then use * {@link MultiAddressHttpClientBuilder#initializer(SingleAddressInitializer)} to override {@link ServiceDiscoverer} * using {@link SingleAddressHttpClientBuilder#serviceDiscoverer(ServiceDiscoverer)} for all or some of the internal @@ -93,12 +133,15 @@ public static MultiAddressHttpClientBuilder forM public static MultiAddressHttpClientBuilder forMultiAddressUrl( final ServiceDiscoverer> serviceDiscoverer) { - return new DefaultMultiAddressUrlHttpClientBuilder(address -> forSingleAddress(serviceDiscoverer, address)); + return applyProviders( + new DefaultMultiAddressUrlHttpClientBuilder(address -> forSingleAddress(serviceDiscoverer, address))); } /** * Creates a {@link SingleAddressHttpClientBuilder} for an address with default {@link LoadBalancer} and DNS {@link * ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. * * @param host host to connect to, resolved by default using a DNS {@link ServiceDiscoverer}. This will also be * used for the {@link HttpHeaderNames#HOST} together with the {@code port}. Use @@ -106,6 +149,7 @@ public static MultiAddressHttpClientBuilder forM * or {@link SingleAddressHttpClientBuilder#hostHeaderFallback(boolean)} if you want to disable this behavior. * @param port port to connect to * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forSingleAddress( final String host, final int port) { @@ -115,29 +159,37 @@ public static SingleAddressHttpClientBuilder for /** * Creates a {@link SingleAddressHttpClientBuilder} for an address with default {@link LoadBalancer} and DNS {@link * ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. * * @param address the {@code UnresolvedAddress} to connect to, resolved by default using a DNS {@link * ServiceDiscoverer}. This address will also be used for the {@link HttpHeaderNames#HOST}. * Use {@link SingleAddressHttpClientBuilder#unresolvedAddressToHost(Function)} if you want to override that * value or {@link SingleAddressHttpClientBuilder#hostHeaderFallback(boolean)} if you want to disable this behavior. * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forSingleAddress( final HostAndPort address) { - return new DefaultSingleAddressHttpClientBuilder<>(address, globalDnsServiceDiscoverer()); + return applyProviders(address, + new DefaultSingleAddressHttpClientBuilder<>(address, globalDnsServiceDiscoverer())); } /** * Creates a {@link SingleAddressHttpClientBuilder} for the passed {@code serviceName} with default * {@link LoadBalancer} and a DNS {@link ServiceDiscoverer} using * SRV record lookups. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. * * @param serviceName The service name to resolve with SRV DNS. * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forServiceAddress( final String serviceName) { - return new DefaultSingleAddressHttpClientBuilder<>(serviceName, globalSrvDnsServiceDiscoverer()); + return applyProviders(serviceName, + new DefaultSingleAddressHttpClientBuilder<>(serviceName, globalSrvDnsServiceDiscoverer())); } /** @@ -151,8 +203,12 @@ public static SingleAddressHttpClientBuilder forServi * Note, if {@link SingleAddressHttpClientBuilder#proxyAddress(Object) a proxy} is configured for this client, * the proxy address also needs to be already resolved. Otherwise, runtime exceptions will be thrown when * the client is built. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. + * * @param port port to connect to * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forResolvedAddress( final String host, final int port) { @@ -170,16 +226,22 @@ public static SingleAddressHttpClientBuilder for * Note, if {@link SingleAddressHttpClientBuilder#proxyAddress(Object) a proxy} is configured for this client, * the proxy address also needs to be already resolved. Otherwise, runtime exceptions will be thrown when * the client is built. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. + * * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forResolvedAddress( final HostAndPort address) { - return new DefaultSingleAddressHttpClientBuilder<>(address, - mappingServiceDiscoverer(BuilderUtils::toResolvedInetSocketAddress)); + return applyProviders(address, new DefaultSingleAddressHttpClientBuilder<>(address, + mappingServiceDiscoverer(BuilderUtils::toResolvedInetSocketAddress))); } /** * Creates a {@link SingleAddressHttpClientBuilder} for an address with default {@link LoadBalancer}. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. * * @param address the {@code ResolvedAddress} to connect. This address will also be used for the * {@link HttpHeaderNames#HOST}. Use {@link SingleAddressHttpClientBuilder#unresolvedAddressToHost(Function)} @@ -187,14 +249,18 @@ public static SingleAddressHttpClientBuilder for * want to disable this behavior. * @param The type of resolved {@link SocketAddress}. * @return new builder for the address + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forResolvedAddress(final R address) { - return new DefaultSingleAddressHttpClientBuilder<>(address, mappingServiceDiscoverer(identity())); + return applyProviders(address, + new DefaultSingleAddressHttpClientBuilder<>(address, mappingServiceDiscoverer(identity()))); } /** * Creates a {@link SingleAddressHttpClientBuilder} for a custom address type with default {@link LoadBalancer} and * user provided {@link ServiceDiscoverer}. + *

+ * The returned builder can be customized using {@link SingleAddressHttpClientBuilderProvider}. * * @param serviceDiscoverer The {@link ServiceDiscoverer} to resolve addresses of remote servers to connect to. * The lifecycle of the provided {@link ServiceDiscoverer} should be managed by the caller. @@ -205,11 +271,12 @@ public static SingleAddressHttpClientBuilder for * @param the type of address before resolution (unresolved address) * @param the type of address after resolution (resolved address) * @return new builder with provided configuration + * @see SingleAddressHttpClientBuilderProvider */ public static SingleAddressHttpClientBuilder forSingleAddress( final ServiceDiscoverer> serviceDiscoverer, final U address) { - return new DefaultSingleAddressHttpClientBuilder<>(address, serviceDiscoverer); + return applyProviders(address, new DefaultSingleAddressHttpClientBuilder<>(address, serviceDiscoverer)); } /** diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpServers.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpServers.java index f9afd85386..9f49ae5a81 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpServers.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpServers.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018 Apple Inc. and the ServiceTalk project authors + * Copyright © 2018, 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. @@ -15,38 +15,68 @@ */ package io.servicetalk.http.netty; +import io.servicetalk.http.api.HttpProviders.HttpServerBuilderProvider; import io.servicetalk.http.api.HttpServerBuilder; -import io.servicetalk.transport.api.ServerContext; +import io.servicetalk.http.api.HttpServerContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.List; + +import static io.servicetalk.utils.internal.ServiceLoaderUtils.loadProviders; /** - * Factory methods for building HTTP Servers backed by {@link ServerContext}. + * A factory to create {@link HttpServerContext HTTP servers}. */ public final class HttpServers { + private static final Logger LOGGER = LoggerFactory.getLogger(HttpServers.class); + + private static final List PROVIDERS; + + static { + final ClassLoader classLoader = HttpServers.class.getClassLoader(); + PROVIDERS = loadProviders(HttpServerBuilderProvider.class, classLoader, LOGGER); + } + private HttpServers() { // No instances } + private static HttpServerBuilder applyProviders(final SocketAddress address, HttpServerBuilder builder) { + for (HttpServerBuilderProvider provider : PROVIDERS) { + builder = provider.newBuilder(address, builder); + } + return builder; + } + /** * New {@link HttpServerBuilder} instance. + *

+ * The returned builder can be customized using {@link HttpServerBuilderProvider}. * - * @param port The listen port for the server. - * @return a new builder. + * @param port The listen port for the server + * @return a new builder + * @see HttpServerBuilderProvider */ - public static HttpServerBuilder forPort(int port) { - return new DefaultHttpServerBuilder(new InetSocketAddress(port)); + public static HttpServerBuilder forPort(final int port) { + final InetSocketAddress address = new InetSocketAddress(port); + return applyProviders(address, new DefaultHttpServerBuilder(address)); } /** * New {@link HttpServerBuilder} instance. + *

+ * The returned builder can be customized using {@link HttpServerBuilderProvider}. * - * @param socketAddress The listen address for the server. - * @return a new builder. + * @param address The listen {@link SocketAddress} for the server + * @return a new builder + * @see HttpServerBuilderProvider */ - public static HttpServerBuilder forAddress(SocketAddress socketAddress) { - return new DefaultHttpServerBuilder(socketAddress); + public static HttpServerBuilder forAddress(final SocketAddress address) { + return applyProviders(address, new DefaultHttpServerBuilder(address)); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProvidersTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProvidersTest.java new file mode 100644 index 0000000000..7cfffaa57b --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProvidersTest.java @@ -0,0 +1,249 @@ +/* + * 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.http.netty; + +import io.servicetalk.client.api.TransportObserverConnectionFactoryFilter; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.DelegatingHttpServerBuilder; +import io.servicetalk.http.api.DelegatingMultiAddressHttpClientBuilder; +import io.servicetalk.http.api.DelegatingSingleAddressHttpClientBuilder; +import io.servicetalk.http.api.HttpProviders.HttpServerBuilderProvider; +import io.servicetalk.http.api.HttpProviders.MultiAddressHttpClientBuilderProvider; +import io.servicetalk.http.api.HttpProviders.SingleAddressHttpClientBuilderProvider; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.HttpServerBuilder; +import io.servicetalk.http.api.HttpServerContext; +import io.servicetalk.http.api.MultiAddressHttpClientBuilder; +import io.servicetalk.http.api.SingleAddressHttpClientBuilder; +import io.servicetalk.http.api.StreamingHttpClient; +import io.servicetalk.http.api.StreamingHttpService; +import io.servicetalk.transport.api.HostAndPort; +import io.servicetalk.transport.api.ServerContext; +import io.servicetalk.transport.api.TransportObserver; +import io.servicetalk.transport.netty.internal.NoopTransportObserver.NoopConnectionObserver; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static io.servicetalk.http.api.HttpResponseStatus.OK; +import static io.servicetalk.http.netty.TestServiceStreaming.SVC_ECHO; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +@Execution(SAME_THREAD) +class HttpProvidersTest { + + @BeforeEach + void reset() { + TestHttpServerBuilderProvider.reset(); + TestSingleAddressHttpClientBuilderProvider.reset(); + TestMultiAddressHttpClientBuilderProvider.reset(); + } + + @AfterEach + void deactivate() { + TestMultiAddressHttpClientBuilderProvider.ACTIVATED.set(false); + } + + @Test + void testNoProvidersForAddress() throws Exception { + try (ServerContext serverContext = HttpServers.forAddress(localAddress(0)) + .listenStreamingAndAwait(new TestServiceStreaming()); + BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) + .buildBlocking()) { + HttpResponse response = client.request(client.get(SVC_ECHO)); + assertThat(response.status(), is(OK)); + } + assertThat(TestHttpServerBuilderProvider.BUILD_COUNTER.get(), is(0)); + assertThat(TestHttpServerBuilderProvider.CONNECTION_COUNTER.get(), is(0)); + assertThat(TestSingleAddressHttpClientBuilderProvider.BUILD_COUNTER.get(), is(0)); + assertThat(TestSingleAddressHttpClientBuilderProvider.CONNECTION_COUNTER.get(), is(0)); + } + + @Test + void testHttpServerBuilderProvider() throws Exception { + final InetSocketAddress serverAddress = localAddress(0); + TestHttpServerBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (ServerContext serverContext = HttpServers.forAddress(serverAddress) + .listenStreamingAndAwait(new TestServiceStreaming())) { + assertThat(TestHttpServerBuilderProvider.BUILD_COUNTER.get(), is(1)); + try (BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) + .buildBlocking()) { + HttpResponse response = client.request(client.get(SVC_ECHO)); + assertThat(response.status(), is(OK)); + assertThat(TestHttpServerBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + @Test + void testSingleAddressHttpClientBuilderProviderWithHostAndPort() throws Exception { + try (ServerContext serverContext = HttpServers.forAddress(localAddress(0)) + .listenStreamingAndAwait(new TestServiceStreaming())) { + HostAndPort serverAddress = serverHostAndPort(serverContext); + TestSingleAddressHttpClientBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (BlockingHttpClient client = HttpClients.forSingleAddress(serverAddress).buildBlocking()) { + assertThat(TestSingleAddressHttpClientBuilderProvider.BUILD_COUNTER.get(), is(1)); + HttpResponse response = client.request(client.get(SVC_ECHO)); + assertThat(response.status(), is(OK)); + assertThat(TestSingleAddressHttpClientBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + @Test + void testSingleAddressHttpClientBuilderProviderForResolvedAddress() throws Exception { + try (ServerContext serverContext = HttpServers.forAddress(localAddress(0)) + .listenStreamingAndAwait(new TestServiceStreaming())) { + SocketAddress serverAddress = serverContext.listenAddress(); + TestSingleAddressHttpClientBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (BlockingHttpClient client = HttpClients.forResolvedAddress(serverAddress).buildBlocking()) { + assertThat(TestSingleAddressHttpClientBuilderProvider.BUILD_COUNTER.get(), is(1)); + HttpResponse response = client.request(client.get(SVC_ECHO)); + assertThat(response.status(), is(OK)); + assertThat(TestSingleAddressHttpClientBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + @Test + void testMultiAddressHttpClientBuilderProvider() throws Exception { + try (ServerContext serverContext = HttpServers.forAddress(localAddress(0)) + .listenStreamingAndAwait(new TestServiceStreaming())) { + HostAndPort serverAddress = serverHostAndPort(serverContext); + TestSingleAddressHttpClientBuilderProvider.MODIFY_FOR_ADDRESS.set(serverAddress); + try (BlockingHttpClient client = HttpClients.forMultiAddressUrl().buildBlocking()) { + assertThat(TestMultiAddressHttpClientBuilderProvider.BUILD_COUNTER.get(), is(1)); + HttpResponse response = client.request(client.get("http://" + serverAddress + SVC_ECHO)); + assertThat(response.status(), is(OK)); + assertThat(TestSingleAddressHttpClientBuilderProvider.BUILD_COUNTER.get(), is(1)); + assertThat(TestSingleAddressHttpClientBuilderProvider.CONNECTION_COUNTER.get(), is(1)); + } + } + } + + public static final class TestHttpServerBuilderProvider implements HttpServerBuilderProvider { + + static final AtomicReference MODIFY_FOR_ADDRESS = new AtomicReference<>(); + static final AtomicInteger BUILD_COUNTER = new AtomicInteger(); + static final AtomicInteger CONNECTION_COUNTER = new AtomicInteger(); + + static void reset() { + MODIFY_FOR_ADDRESS.set(null); + BUILD_COUNTER.set(0); + CONNECTION_COUNTER.set(0); + } + + @Override + public HttpServerBuilder newBuilder(SocketAddress address, HttpServerBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + return new DelegatingHttpServerBuilder( + builder.transportObserver(transportObserver(CONNECTION_COUNTER))) { + + @Override + public HttpServerContext listenStreamingAndAwait(StreamingHttpService service) + throws Exception { + BUILD_COUNTER.incrementAndGet(); + return delegate().listenStreamingAndAwait(service); + } + }; + } + return builder; + } + } + + public static final class TestSingleAddressHttpClientBuilderProvider + implements SingleAddressHttpClientBuilderProvider { + + static final AtomicReference MODIFY_FOR_ADDRESS = new AtomicReference<>(); + static final AtomicInteger BUILD_COUNTER = new AtomicInteger(); + static final AtomicInteger CONNECTION_COUNTER = new AtomicInteger(); + + static void reset() { + MODIFY_FOR_ADDRESS.set(null); + BUILD_COUNTER.set(0); + CONNECTION_COUNTER.set(0); + } + + @Override + public SingleAddressHttpClientBuilder newBuilder(U address, + SingleAddressHttpClientBuilder builder) { + if (address.equals(MODIFY_FOR_ADDRESS.get())) { + // Test that users can either modify the existing filter or wrap it for additional logic: + return new DelegatingSingleAddressHttpClientBuilder(builder.appendConnectionFactoryFilter( + new TransportObserverConnectionFactoryFilter<>(transportObserver(CONNECTION_COUNTER)))) { + + @Override + public BlockingHttpClient buildBlocking() { + BUILD_COUNTER.incrementAndGet(); + return super.buildBlocking(); + } + + // multi-address and partitioned client builders uses this method: + @Override + public StreamingHttpClient buildStreaming() { + BUILD_COUNTER.incrementAndGet(); + return super.buildStreaming(); + } + }; + } + return builder; + } + } + + public static final class TestMultiAddressHttpClientBuilderProvider + implements MultiAddressHttpClientBuilderProvider { + + // Used to prevent applying this provider for other test classes: + static final AtomicBoolean ACTIVATED = new AtomicBoolean(); + static final AtomicInteger BUILD_COUNTER = new AtomicInteger(); + + static void reset() { + ACTIVATED.set(true); + BUILD_COUNTER.set(0); + } + + @Override + public MultiAddressHttpClientBuilder newBuilder(MultiAddressHttpClientBuilder builder) { + return ACTIVATED.get() ? new DelegatingMultiAddressHttpClientBuilder(builder) { + + @Override + public BlockingHttpClient buildBlocking() { + BUILD_COUNTER.incrementAndGet(); + return delegate().buildBlocking(); + } + } : builder; + } + } + + private static TransportObserver transportObserver(AtomicInteger counter) { + return (localAddress, remoteAddress) -> { + counter.incrementAndGet(); + return NoopConnectionObserver.INSTANCE; + }; + } +} diff --git a/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider new file mode 100644 index 0000000000..08413dc8aa --- /dev/null +++ b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$HttpServerBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.http.netty.HttpProvidersTest$TestHttpServerBuilderProvider diff --git a/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$MultiAddressHttpClientBuilderProvider b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$MultiAddressHttpClientBuilderProvider new file mode 100644 index 0000000000..236eecd767 --- /dev/null +++ b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$MultiAddressHttpClientBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.http.netty.HttpProvidersTest$TestMultiAddressHttpClientBuilderProvider diff --git a/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider new file mode 100644 index 0000000000..64224476c8 --- /dev/null +++ b/servicetalk-http-netty/src/test/resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider @@ -0,0 +1 @@ +io.servicetalk.http.netty.HttpProvidersTest$TestSingleAddressHttpClientBuilderProvider diff --git a/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/ServiceLoaderUtils.java b/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/ServiceLoaderUtils.java new file mode 100644 index 0000000000..2c25e8ae8a --- /dev/null +++ b/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/ServiceLoaderUtils.java @@ -0,0 +1,53 @@ +/* + * 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.utils.internal; + +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + +/** + * {@link ServiceLoader} utilities. + */ +public final class ServiceLoaderUtils { + + private ServiceLoaderUtils() { + // No instances. + } + + /** + * Loads provider classes via {@link ServiceLoader}. + * + * @param clazz interface of abstract class which implementations should be loaded + * @param classLoader {@link ClassLoader} to be searched for provider instances + * @param logger {@link Logger} to use + * @param type of the provider + * @return a list of loaded providers for the specified class + */ + public static List loadProviders(final Class clazz, final ClassLoader classLoader, final Logger logger) { + final List list = new ArrayList<>(0); + for (T provider : ServiceLoader.load(clazz, classLoader)) { + list.add(provider); + } + logger.debug("Registered {} {}(s): {}", list.size(), clazz.getSimpleName(), list); + return list.isEmpty() ? emptyList() : unmodifiableList(list); + } +}