Skip to content

Commit

Permalink
Provide common HTTP Service interface for all API variants. (#2456)
Browse files Browse the repository at this point in the history
Motivation:

Right now four different Service API variants are provided, all
with their async and sync listen* overloads. If the service
type is known at compile time those overloads suffice, but in
situations where the type is only determined at runtime (for
example when DI is used) the user needs to write verbose code
that can be provided by ServiceTalk and improve the developer
experience.

Modifications:

This changeset introduces a new "Service" interface in the
http package and it now acts as a parent for the four different
service implementations. The HttpServerBuilder gets two more
overloads (blocking and non-blocking) to accept the Service
type and then internally performs the runtime type checking
and delegates to the appropriate methods.

Result:

Better developer experience when the service type is not known
at compile time, for example when dependency injection is used.
  • Loading branch information
daschl authored Dec 14, 2022
1 parent 5117e36 commit 5941982
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* The equivalent of {@link HttpService} but with synchronous/blocking APIs instead of asynchronous APIs.
*/
@FunctionalInterface
public interface BlockingHttpService extends HttpExecutionStrategyInfluencer, GracefulAutoCloseable {
public interface BlockingHttpService extends HttpServiceBase, GracefulAutoCloseable {
/**
* Handles a single HTTP request.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* The equivalent of {@link StreamingHttpService} but with synchronous/blocking APIs instead of asynchronous APIs.
*/
@FunctionalInterface
public interface BlockingStreamingHttpService extends HttpExecutionStrategyInfluencer, GracefulAutoCloseable {
public interface BlockingStreamingHttpService extends HttpServiceBase, GracefulAutoCloseable {
/**
* Handles a single HTTP request.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,27 @@ HttpServerBuilder appendServiceFilter(Predicate<StreamingHttpRequest> predicate,
*/
HttpServerBuilder executionStrategy(HttpExecutionStrategy strategy);

/**
* Starts this server and returns the {@link HttpServerContext} after the server has been successfully started.
* <p>
* If the underlying protocol (e.g. TCP) supports it this will result in a socket bind/listen on {@code address}.
* <p>
* Note that this method is generic in the sense that it accepts all HTTP {@link HttpServiceBase} implementations
* to be passed in, namely {@link StreamingHttpService}, {@link HttpService}, {@link BlockingStreamingHttpService}
* and {@link BlockingHttpService}. It is especially useful when Dependency Injection is used and the type of
* service is not known at compile time.
*
* @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 IllegalArgumentException if an unsupported {@link HttpServiceBase} type is being provided.
* @throws Exception if the server could not be started.
*/
default HttpServerContext listenServiceAndAwait(HttpServiceBase service) throws Exception {
return blockingInvocation(listenService(service));
}

/**
* Starts this server and returns the {@link HttpServerContext} after the server has been successfully started.
* <p>
Expand Down Expand Up @@ -374,6 +395,36 @@ default HttpServerContext listenBlockingStreamingAndAwait(BlockingStreamingHttpS
return blockingInvocation(listenBlockingStreaming(service));
}

/**
* Starts this server and returns the {@link HttpServerContext} after the server has been successfully started.
* <p>
* If the underlying protocol (e.g. TCP) supports it this will result in a socket bind/listen on {@code address}.
* <p>
* Note that this method is generic in the sense that it accepts all HTTP {@link HttpServiceBase} implementations
* to be passed in, namely {@link StreamingHttpService}, {@link HttpService}, {@link BlockingStreamingHttpService}
* and {@link BlockingHttpService}. It is especially useful when Dependency Injection is used and the type of
* service is not known at compile time.
*
* @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 Single} that completes when the server is successfully started or terminates with an error if
* the server could not be started.
* @throws IllegalArgumentException if an unsupported {@link HttpServiceBase} type is being provided.
*/
default Single<HttpServerContext> listenService(final HttpServiceBase service) {
if (service instanceof HttpService) {
return listen((HttpService) service);
} else if (service instanceof StreamingHttpService) {
return listenStreaming((StreamingHttpService) service);
} else if (service instanceof BlockingHttpService) {
return listenBlocking((BlockingHttpService) service);
} else if (service instanceof BlockingStreamingHttpService) {
return listenBlockingStreaming((BlockingStreamingHttpService) service);
} else {
return Single.failed(new IllegalArgumentException("Unsupported service type: " + service));
}
}

/**
* Starts this server and returns the {@link HttpServerContext} after the server has been successfully started.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* Same as {@link StreamingHttpService} but that accepts {@link HttpRequest} and returns {@link HttpResponse}.
*/
@FunctionalInterface
public interface HttpService extends AsyncCloseable, HttpExecutionStrategyInfluencer {
public interface HttpService extends AsyncCloseable, HttpServiceBase {
/**
* Handles a single HTTP request.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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;

/**
* Parent interface for all available HTTP services.
*
* @see HttpService
* @see StreamingHttpService
* @see BlockingHttpService
* @see BlockingStreamingHttpService
*/
public interface HttpServiceBase extends HttpExecutionStrategyInfluencer {
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* A service contract for the HTTP protocol.
*/
@FunctionalInterface
public interface StreamingHttpService extends AsyncCloseable, HttpExecutionStrategyInfluencer {
public interface StreamingHttpService extends AsyncCloseable, HttpServiceBase {
/**
* Handles a single HTTP request.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.concurrent.api.Single;
import io.servicetalk.http.api.BlockingHttpService;
import io.servicetalk.http.api.BlockingStreamingHttpService;
import io.servicetalk.http.api.HttpExecutionStrategy;
import io.servicetalk.http.api.HttpServerContext;
import io.servicetalk.http.api.HttpService;
import io.servicetalk.http.api.HttpServiceBase;
import io.servicetalk.http.api.StreamingHttpService;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class HttpServiceTest {

@ParameterizedTest(name = "{displayName} [{index}] {0}")
@MethodSource("serviceProvider")
void supportsHttpServiceVariantAtRuntime(HttpServiceBase service) throws Exception {
assertNotNull(HttpServers.forAddress(localAddress(0)).listenServiceAndAwait(service));
}

@Test
void failsOnUnknownService() {
assertThrows(IllegalArgumentException.class, () -> {
HttpServiceBase service = new HttpServiceBase() {
@Override
public HttpExecutionStrategy requiredOffloads() {
return HttpServiceBase.super.requiredOffloads();
}
};

try (HttpServerContext ctx = HttpServers.forAddress(localAddress(0)).listenServiceAndAwait(service)) {
ctx.closeGracefully();
}
});
}

static Stream<Arguments> serviceProvider() {
return Stream.of(
arguments(named("BlockingHttpService",
(BlockingHttpService) (ctx, request, responseFactory) -> responseFactory.ok())),
arguments(named("BlockingStreamingHttpService",
(BlockingStreamingHttpService) (ctx, request, response) -> { })),
arguments(named("HttpService",
(HttpService) (ctx, request, responseFactory) ->
Single.succeeded(responseFactory.ok()))),
arguments(named("StreamingHttpService",
(StreamingHttpService) (ctx, request, responseFactory) ->
Single.succeeded(responseFactory.ok())))
);
}
}

0 comments on commit 5941982

Please sign in to comment.