diff --git a/.reviewboardrc b/.reviewboardrc new file mode 100644 index 000000000000..af029b835767 --- /dev/null +++ b/.reviewboardrc @@ -0,0 +1,3 @@ +REVIEWBOARD_URL = 'https://rbcommons.com/s/armeria/' +REPOSITORY = 'armeria' + diff --git a/NOTICE.txt b/NOTICE.txt index b980c5ac51eb..4398caded82a 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -161,6 +161,11 @@ This product depends on Netty, distributed by Netty.io: * License: licenses/LICENSE.netty.al20.txt (Apache License v2.0) * Homepage: http://netty.io/ +This product depends on Reactive Streams API, distributed by Reactive-Streams.org: + + * License: licenses/LICENSE.reactivestreams.cc0.txt + * Homepage: http://www.reactive-streams.org/ + This product depends on Reflections, distributed by ronmamo: * License: licenses/LICENSE.reflections.wtfpl.txt (WTFPL) diff --git a/licenses/LICENSE.reactivestreams.cc0.txt b/licenses/LICENSE.reactivestreams.cc0.txt new file mode 100644 index 000000000000..bb5bc373c41b --- /dev/null +++ b/licenses/LICENSE.reactivestreams.cc0.txt @@ -0,0 +1,9 @@ +Licensed under Public Domain (CC0) + +To the extent possible under law, the person who associated CC0 with +this code has waived all copyright and related or neighboring +rights to this code. + +You should have received a copy of the CC0 legalcode along with this +work. If not, see . + diff --git a/pom.xml b/pom.xml index 0fdb1334a259..3917dfe7724e 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,7 @@ 1.1.7 3.1.2 4.1.0.CR7 + 1.0.0 1.7.21 8.0.33 2.0.1 @@ -139,6 +140,13 @@ 1.1.33.Fork17 + + + org.reactivestreams + reactive-streams + ${reactive-streams.version} + + org.eclipse.jetty.alpn @@ -564,7 +572,7 @@ **/TestUtil* random - ${argLine.alpnAgent} ${argLine.leak} + ${argLine.alpnAgent} ${argLine.leak} -Xmx128m diff --git a/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java b/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java new file mode 100644 index 000000000000..b909b4828f3e --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/AbstractClientFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; + +import com.linecorp.armeria.common.Scheme; + +public abstract class AbstractClientFactory implements ClientFactory { + + protected AbstractClientFactory() {} + + @Override + public final T newClient(String uri, Class clientType, ClientOptionValue... options) { + requireNonNull(uri, "uri"); + requireNonNull(options, "options"); + return newClient(URI.create(uri), clientType, ClientOptions.of(options)); + } + + @Override + public final T newClient(String uri, Class clientType, ClientOptions options) { + requireNonNull(uri, "uri"); + return newClient(URI.create(uri), clientType, options); + } + + @Override + public final T newClient(URI uri, Class clientType, ClientOptionValue... options) { + requireNonNull(options, "options"); + return newClient(uri, clientType, ClientOptions.of(options)); + } + + protected final Scheme validate(URI uri, Class clientType, ClientOptions options) { + requireNonNull(uri, "uri"); + requireNonNull(clientType, "clientType"); + requireNonNull(options, "options"); + + final String scheme = uri.getScheme(); + if (scheme == null) { + throw new IllegalArgumentException("URI with missing scheme: " + uri); + } + + if (uri.getAuthority() == null) { + throw new IllegalArgumentException("URI with missing authority: " + uri); + } + + final Optional parsedSchemeOpt = Scheme.tryParse(scheme); + if (!parsedSchemeOpt.isPresent()) { + throw new IllegalArgumentException("URI with unknown scheme: " + uri); + } + + final Scheme parsedScheme = parsedSchemeOpt.get(); + final Set supportedSchemes = supportedSchemes(); + if (!supportedSchemes.contains(parsedScheme)) { + throw new IllegalArgumentException( + "URI with unsupported scheme: " + uri + " (expected: " + supportedSchemes + ')'); + } + + return parsedScheme; + } + + protected static Endpoint newEndpoint(URI uri) { + return Endpoint.parse(uri.getAuthority()); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java b/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java new file mode 100644 index 000000000000..2b040c48bce4 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/AllInOneClientFactory.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableMap; + +import com.linecorp.armeria.client.http.HttpClientFactory; +import com.linecorp.armeria.client.thrift.ThriftClientFactory; +import com.linecorp.armeria.common.Scheme; + +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; + +public class AllInOneClientFactory extends AbstractClientFactory { + + private static final Logger logger = LoggerFactory.getLogger(AllInOneClientFactory.class); + + static { + if (AllInOneClientFactory.class.getClassLoader() == ClassLoader.getSystemClassLoader()) { + Runtime.getRuntime().addShutdownHook(new Thread(ClientFactory::closeDefault)); + } + } + + private final ClientFactory mainFactory; + private final Map clientFactories; + + public AllInOneClientFactory() { + this(SessionOptions.DEFAULT); + } + + public AllInOneClientFactory(SessionOptions options) { + // TODO(trustin): Allow specifying different options for different session protocols. + // We have only one session protocol at the moment, so this is OK so far. + final HttpClientFactory httpClientFactory = new HttpClientFactory(options); + final ThriftClientFactory thriftClientFactory = new ThriftClientFactory(httpClientFactory); + + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ClientFactory f : Arrays.asList(httpClientFactory, thriftClientFactory)) { + f.supportedSchemes().forEach(s -> builder.put(s, f)); + } + + clientFactories = builder.build(); + mainFactory = httpClientFactory; + } + + @Override + public Set supportedSchemes() { + return clientFactories.keySet(); + } + + @Override + public SessionOptions options() { + return mainFactory.options(); + } + + @Override + public EventLoopGroup eventLoopGroup() { + return mainFactory.eventLoopGroup(); + } + + @Override + public Supplier eventLoopSupplier() { + return mainFactory.eventLoopSupplier(); + } + + @Override + public T newClient(URI uri, Class clientType, ClientOptions options) { + final Scheme scheme = validate(uri, clientType, options); + return clientFactories.get(scheme).newClient(uri, clientType, options); + } + + @Override + public void close() { + // The global default should never be closed. + if (this == ClientFactory.DEFAULT) { + logger.debug("Refusing to close the default {}; must be closed via closeDefault()", + ClientFactory.class.getSimpleName()); + return; + } + + doClose(); + } + + void doClose() { + clientFactories.values().forEach(ClientFactory::close); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/Client.java b/src/main/java/com/linecorp/armeria/client/Client.java index d7fdc9bf11ae..47e52b728685 100644 --- a/src/main/java/com/linecorp/armeria/client/Client.java +++ b/src/main/java/com/linecorp/armeria/client/Client.java @@ -16,76 +16,7 @@ package com.linecorp.armeria.client; -import java.util.function.Function; - -/** - * A set of components required for invoking a remote service. - */ -public interface Client { - - /** - * Creates a new function that decorates a {@link Client} with the specified {@code codecDecorator} - * and {@code invokerDecorator}. - *

- * This factory method is useful when you want to write a reusable factory method that returns - * a decorator function and the returned decorator function is expected to be consumed by - * {@link #decorate(Function)}. For example, this may be a factory which combines multiple decorators, - * any of which could be decorating a codec and/or a handler. - *

- * Consider using {@link #decorateCodec(Function)} or {@link #decorateInvoker(Function)} - * instead for simplicity unless you are writing a factory method of a decorator function. - *

- * If you need a function that decorates only a codec or an invoker, use {@link Function#identity()} for - * the non-decorated property. e.g: - *

{@code
-     * newDecorator(Function.identity(), (handler) -> { ... });
-     * }
- */ - @SuppressWarnings("unchecked") - static - Function newDecorator(Function codecDecorator, Function invokerDecorator) { - return client -> new DecoratingClient(client, codecDecorator, invokerDecorator); - } - - /** - * Returns the {@link ClientCodec}. - */ - ClientCodec codec(); - - /** - * Returns the {@link RemoteInvoker}. - */ - RemoteInvoker invoker(); - - /** - * Creates a new {@link Client} that decorates this {@link Client} with the specified {@code decorator}. - */ - default Client decorate(Function decorator) { - @SuppressWarnings("unchecked") - final Client newClient = decorator.apply(this); - if (newClient == null) { - throw new NullPointerException("decorator.apply() returned null: " + decorator); - } - - return newClient; - } - - /** - * Creates a new {@link Client} that decorates the {@link ClientCodec} of this {@link Client} with the - * specified {@code codecDecorator}. - */ - default - Client decorateCodec(Function codecDecorator) { - return new DecoratingClient(this, codecDecorator, Function.identity()); - } - - /** - * Creates a new {@link Client} that decorates the {@link RemoteInvoker} of this - * {@link Client} with the specified {@code invokerDecorator}. - */ - default - Client decorateInvoker(Function invokerDecorator) { - return new DecoratingClient(this, Function.identity(), invokerDecorator); - } +@FunctionalInterface +public interface Client { + O execute(ClientRequestContext ctx, I req) throws Exception; } diff --git a/src/main/java/com/linecorp/armeria/client/ClientBuilder.java b/src/main/java/com/linecorp/armeria/client/ClientBuilder.java index 3dfba2c1c1e8..f1e1a60b1ce8 100644 --- a/src/main/java/com/linecorp/armeria/client/ClientBuilder.java +++ b/src/main/java/com/linecorp/armeria/client/ClientBuilder.java @@ -17,27 +17,12 @@ import static java.util.Objects.requireNonNull; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Proxy; import java.net.URI; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; -import com.linecorp.armeria.client.routing.EndpointGroup; -import com.linecorp.armeria.client.routing.EndpointGroupInvoker; -import com.linecorp.armeria.client.routing.EndpointGroupUtil; -import org.apache.thrift.protocol.TProtocolFactory; - -import com.linecorp.armeria.client.http.SimpleHttpClientCodec; -import com.linecorp.armeria.client.thrift.ThriftClientCodec; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.SerializationFormat; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.TimeoutPolicy; -import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; - /** * Creates a new client that connects to the specified {@link URI} using the builder pattern. Use the factory * methods in {@link Clients} if you do not have many options to override. @@ -47,8 +32,8 @@ public final class ClientBuilder { private final URI uri; private final Map, ClientOptionValue> options = new LinkedHashMap<>(); - private RemoteInvokerFactory remoteInvokerFactory = RemoteInvokerFactory.DEFAULT; - private Function decorator; + private ClientFactory factory = ClientFactory.DEFAULT; + private ClientDecorationBuilder decoration; /** * Creates a new {@link ClientBuilder} that builds the client that connects to the specified {@code uri}. @@ -65,10 +50,10 @@ public ClientBuilder(URI uri) { } /** - * Sets the {@link RemoteInvokerFactory} of the client. The default is {@link RemoteInvokerFactory#DEFAULT}. + * Sets the {@link ClientFactory} of the client. The default is {@link ClientFactory#DEFAULT}. */ - public ClientBuilder remoteInvokerFactory(RemoteInvokerFactory remoteInvokerFactory) { - this.remoteInvokerFactory = requireNonNull(remoteInvokerFactory, "remoteInvokerFactory"); + public ClientBuilder factory(ClientFactory factory) { + this.factory = requireNonNull(factory, "factory"); return this; } @@ -98,9 +83,9 @@ public ClientBuilder options(ClientOptionValue... options) { throw new NullPointerException("options[" + i + ']'); } - if (o.option() == ClientOption.DECORATOR && decorator != null) { + if (o.option() == ClientOption.DECORATION && decoration != null) { throw new IllegalArgumentException( - "options[" + i + "]: option(" + ClientOption.DECORATOR + + "options[" + i + "]: option(" + ClientOption.DECORATION + ") and decorator() are mutually exclusive."); } @@ -120,171 +105,79 @@ public ClientBuilder option(ClientOption option, T value) { private void validateOption(ClientOption option) { requireNonNull(option, "option"); - if (option == ClientOption.DECORATOR && decorator != null) { + if (option == ClientOption.DECORATION && decoration != null) { throw new IllegalArgumentException( - "option(" + ClientOption.DECORATOR + ") and decorator() are mutually exclusive."); + "option(" + ClientOption.DECORATION + ") and decorator() are mutually exclusive."); } } /** - * Sets the timeout of a socket write attempt in milliseconds. + * Sets the default timeout of a socket write attempt in milliseconds. * - * @param writeTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout. + * @param defaultWriteTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout. */ - public ClientBuilder writeTimeoutMillis(long writeTimeoutMillis) { - return writeTimeout(Duration.ofMillis(writeTimeoutMillis)); + public ClientBuilder defaultWriteTimeoutMillis(long defaultWriteTimeoutMillis) { + return option(ClientOption.DEFAULT_WRITE_TIMEOUT_MILLIS, defaultWriteTimeoutMillis); } /** - * Sets the timeout of a socket write attempt. + * Sets the default timeout of a socket write attempt. * - * @param writeTimeout the timeout. {@code 0} disables the timeout. - */ - public ClientBuilder writeTimeout(Duration writeTimeout) { - return writeTimeout(TimeoutPolicy.ofFixed(requireNonNull(writeTimeout, "writeTimeout"))); - } - - /** - * Sets the {@link TimeoutPolicy} of a socket write attempt. + * @param defaultWriteTimeout the timeout. {@code 0} disables the timeout. */ - public ClientBuilder writeTimeout(TimeoutPolicy writeTimeoutPolicy) { - return option(ClientOption.WRITE_TIMEOUT_POLICY, - requireNonNull(writeTimeoutPolicy, "writeTimeoutPolicy")); + public ClientBuilder defaultWriteTimeout(Duration defaultWriteTimeout) { + return defaultWriteTimeoutMillis(requireNonNull(defaultWriteTimeout, "defaultWriteTimeout").toMillis()); } /** - * Sets the timeout of a response in milliseconds. + * Sets the default timeout of a response in milliseconds. * - * @param responseTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout. + * @param defaultResponseTimeoutMillis the timeout in milliseconds. {@code 0} disables the timeout. */ - public ClientBuilder responseTimeoutMillis(long responseTimeoutMillis) { - return responseTimeout(Duration.ofMillis(responseTimeoutMillis)); + public ClientBuilder defaultResponseTimeoutMillis(long defaultResponseTimeoutMillis) { + return option(ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS, defaultResponseTimeoutMillis); } /** - * Sets the timeout of a response. + * Sets the default timeout of a response. * - * @param responseTimeout the timeout. {@code 0} disables the timeout. - */ - public ClientBuilder responseTimeout(Duration responseTimeout) { - return responseTimeout(TimeoutPolicy.ofFixed(requireNonNull(responseTimeout, "responseTimeout"))); - } - - /** - * Sets the {@link TimeoutPolicy} of a response. + * @param defaultResponseTimeout the timeout. {@code 0} disables the timeout. */ - public ClientBuilder responseTimeout(TimeoutPolicy responseTimeoutPolicy) { - return option(ClientOption.RESPONSE_TIMEOUT_POLICY, - requireNonNull(responseTimeoutPolicy, "responseTimeoutPolicy")); + public ClientBuilder defaultResponseTimeout(Duration defaultResponseTimeout) { + return defaultResponseTimeoutMillis( + requireNonNull(defaultResponseTimeout, "defaultResponseTimeout").toMillis()); } /** * Adds the specified {@code decorator}. */ - public ClientBuilder decorator(Function decorator) { - requireNonNull(decorator, "decorator"); + public ClientBuilder decorator(Class requestType, Class responseType, + Function, ? extends Client> decorator) { - if (options.containsKey(ClientOption.DECORATOR)) { + if (options.containsKey(ClientOption.DECORATION)) { throw new IllegalArgumentException( - "decorator() and option(" + ClientOption.DECORATOR + ") are mutually exclusive."); + "decorator() and option(" + ClientOption.DECORATION + ") are mutually exclusive."); } - if (this.decorator == null) { - this.decorator = decorator; - } else { - this.decorator = this.decorator.andThen(decorator); + if (decoration == null) { + decoration = new ClientDecorationBuilder(); } + decoration.add(requestType, responseType, decorator); return this; } /** - * Adds the specified {@code invokerDecorator} that decorates a {@link RemoteInvoker}. - */ - public ClientBuilder invokerDecorator( - Function invokerDecorator) { - requireNonNull(invokerDecorator, "invokerDecorator"); - - @SuppressWarnings("unchecked") - final Function castInvokerDecorator = - (Function) invokerDecorator; - - return decorator(d -> d.decorateInvoker(castInvokerDecorator)); - } - - /** - * Adds the specified {@code codecDecorator} that decorates an {@link ClientCodec}. - */ - public ClientBuilder codecDecorator( - Function codecDecorator) { - requireNonNull(codecDecorator, "codecDecorator"); - - @SuppressWarnings("unchecked") - final Function castCodecDecorator = - (Function) codecDecorator; - - return decorator(d -> d.decorateCodec(castCodecDecorator)); - } - - /** - * Creates a new client which implements the specified {@code interfaceClass}. + * Creates a new client which implements the specified {@code clientType}. */ @SuppressWarnings("unchecked") - public T build(Class interfaceClass) { - requireNonNull(interfaceClass, "interfaceClass"); - - if (decorator != null) { - options.put(ClientOption.DECORATOR, ClientOption.DECORATOR.newValue(decorator)); - } - - final ClientOptions options = ClientOptions.of(this.options.values()); - - final Client client = options.decorator().apply(newClient(interfaceClass)); - - final InvocationHandler handler = new ClientInvocationHandler( - remoteInvokerFactory.eventLoopGroup(), uri, interfaceClass, client, options); - - return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), - new Class[] { interfaceClass }, - handler); - } - - private Client newClient(Class interfaceClass) { - final Scheme scheme = Scheme.parse(uri.getScheme()); - final SessionProtocol sessionProtocol = scheme.sessionProtocol(); - URI codecURI = this.uri; - - RemoteInvoker remoteInvoker = remoteInvokerFactory.getInvoker(sessionProtocol); - if (remoteInvoker == null) { - throw new IllegalArgumentException("unsupported scheme: " + scheme); - } - - final String groupName = EndpointGroupUtil.getEndpointGroupName(this.uri); - if (groupName != null) { - remoteInvoker = new EndpointGroupInvoker(remoteInvoker, groupName); - // Ensure a valid URL is returned in business logic that accesses it - // through the ServiceInvocationContext by stripping out our custom markers. - codecURI = URI.create(EndpointGroupUtil.removeGroupMark(this.uri)); - } - - final ClientCodec codec = createCodec(codecURI, scheme, interfaceClass); - - return new SimpleClient(codec, remoteInvoker); - } - - private static ClientCodec createCodec(URI uri, Scheme scheme, Class interfaceClass) { - SessionProtocol sessionProtocol = scheme.sessionProtocol(); - SerializationFormat serializationFormat = scheme.serializationFormat(); - if (SerializationFormat.ofThrift().contains(serializationFormat)) { - TProtocolFactory protocolFactory = ThriftProtocolFactories.get(serializationFormat); - return new ThriftClientCodec(uri, interfaceClass, protocolFactory); - } + public T build(Class clientType) { + requireNonNull(clientType, "clientType"); - if (SessionProtocol.ofHttp().contains(sessionProtocol) && - serializationFormat == SerializationFormat.NONE) { - return new SimpleHttpClientCodec(uri.getHost()); + if (decoration != null) { + options.put(ClientOption.DECORATION, ClientOption.DECORATION.newValue(decoration.build())); } - throw new IllegalArgumentException("unsupported scheme:" + scheme); + return factory.newClient(uri, clientType, ClientOptions.of(options.values())); } } diff --git a/src/main/java/com/linecorp/armeria/client/ClientCodec.java b/src/main/java/com/linecorp/armeria/client/ClientCodec.java deleted file mode 100644 index 34c34ba9807a..000000000000 --- a/src/main/java/com/linecorp/armeria/client/ClientCodec.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import java.lang.reflect.Method; -import java.util.Optional; - -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.util.concurrent.Promise; - -/** - * Encodes an invocation into a {@link ByteBuf} and decodes its result ({@link ByteBuf}) into a Java object. - */ -public interface ClientCodec { - - /** - * Invoked when a {@link RemoteInvoker} prepares to perform an invocation. - */ - void prepareRequest(Method method, Object[] args, Promise resultPromise); - - /** - * Encodes a Java method invocation into a {@link ServiceInvocationContext}. - */ - EncodeResult encodeRequest(Channel channel, SessionProtocol sessionProtocol, Method method, Object[] args); - - /** - * Decodes the response bytes into a Java object. - */ - T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse) - throws Exception; - - /** - * Returns {@code true} if the invocation result that will be returned to calling code asynchronously. - */ - boolean isAsyncClient(); - - /** - * The result of {@link #encodeRequest(Channel, SessionProtocol, Method, Object[]) ClientCodec.encodeRequest()}. - */ - interface EncodeResult { - /** - * Returns {@code true} if and only if the encode request has been successful. - */ - boolean isSuccess(); - - /** - * Returns the {@link ServiceInvocationContext} created as the result of - * {@link ClientCodec#decodeResponse(ServiceInvocationContext, ByteBuf, Object) - * ClientCodec.decodeResponse()} - * - * @throws IllegalStateException if the encoding was not successful - */ - ServiceInvocationContext invocationContext(); - - /** - * Returns the content resulting from encoding the request. - * - * @throws IllegalStateException if the encoding was not successful - */ - Object content(); - - /** - * Returns the cause of the encode failure. - * - * @throws IllegalStateException if the encoding is successful and thus there is no error to report - */ - Throwable cause(); - - /** - * Returns the host name of the invocation. - */ - Optional encodedHost(); - - /** - * Returns the path of the invocation. - */ - Optional encodedPath(); - - /** - * Returns the {@link Scheme} of the invocation. - */ - Optional encodedScheme(); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/ClientDecoration.java b/src/main/java/com/linecorp/armeria/client/ClientDecoration.java new file mode 100644 index 000000000000..2617f7a61377 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/ClientDecoration.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +import com.linecorp.armeria.client.ClientDecoration.Entry; + +public final class ClientDecoration implements Iterable { + + public static final ClientDecoration NONE = new ClientDecoration(Collections.emptyList()); + + public static ClientDecoration of( + Class requestType, Class responseType, + Function, ? extends Client> decorator) { + return new ClientDecorationBuilder().add(requestType, responseType, decorator).build(); + } + + private final List entries; + + ClientDecoration(List entries) { + this.entries = Collections.unmodifiableList(entries); + } + + @Override + public Iterator iterator() { + return entries.iterator(); + } + + public Client decorate(Class requestType, Class responseType, Client client) { + for (Entry e : this) { + if (!requestType.isAssignableFrom(e.requestType()) || + !responseType.isAssignableFrom(e.responseType())) { + continue; + } + + final Function, Client> decorator = e.decorator(); + client = decorator.apply(client); + } + + return client; + } + + public static final class Entry { + private final Class requestType; + private final Class responseType; + private final Function> decorator; + + Entry(Class requestType, Class responseType, Function> decorator) { + this.requestType = requestType; + this.responseType = responseType; + this.decorator = decorator; + } + + @SuppressWarnings("unchecked") + public Class requestType() { + return (Class) requestType; + } + + @SuppressWarnings("unchecked") + public Class responseType() { + return (Class) responseType; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Function, Client> decorator() { + return (Function, Client>) (Function) decorator; + } + + @Override + public String toString() { + return '(' + + requestType.getSimpleName() + ", " + + responseType.getSimpleName() + ", " + + decorator + + ')'; + } + } +} diff --git a/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java b/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java new file mode 100644 index 000000000000..f1e9a643c45d --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/ClientDecorationBuilder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import com.linecorp.armeria.client.ClientDecoration.Entry; + +public final class ClientDecorationBuilder { + + private final List entries = new ArrayList<>(); + + public ClientDecorationBuilder add( + Class requestType, Class responseType, + Function, ? extends Client> decorator) { + + requireNonNull(requestType, "requestType"); + requireNonNull(responseType, "responseType"); + requireNonNull(decorator, "decorator"); + + entries.add(new Entry(requestType, responseType, decorator)); + return this; + } + + public ClientDecoration build() { + return new ClientDecoration(entries); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/ClientFactory.java b/src/main/java/com/linecorp/armeria/client/ClientFactory.java new file mode 100644 index 000000000000..6a6879801331 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/ClientFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import java.net.URI; +import java.util.Set; +import java.util.function.Supplier; + +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.common.Scheme; + +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; + +/** + * Creates and manages clients. + * + *

Life cycle of the default {@link ClientFactory}

+ *

+ * {@link Clients} or {@link ClientBuilder} uses {@link #DEFAULT}, the default {@link ClientFactory}, + * unless you specified a {@link ClientFactory} explicitly. Calling {@link #close()} on the default + * {@link ClientFactory} won't terminate its I/O threads and release other related resources unlike + * other {@link ClientFactory} to protect itself from accidental premature termination. + *

+ * Instead, when the current {@link ClassLoader} is {@linkplain ClassLoader#getSystemClassLoader() the system + * class loader}, a {@link Runtime#addShutdownHook(Thread) shutdown hook} is registered so that they are + * released when the JVM exits. + *

+ * If you are in an environment managed by a container or you desire the early termination of the default + * {@link ClientFactory}, use {@link #closeDefault()}. + *

+ */ +public interface ClientFactory extends AutoCloseable { + + /** + * The default {@link ClientFactory} implementation. + */ + ClientFactory DEFAULT = new AllInOneClientFactory(); + + /** + * Closes the default {@link ClientFactory}. + */ + static void closeDefault() { + LoggerFactory.getLogger(ClientFactory.class).debug( + "Closing the default {}", ClientFactory.class.getSimpleName()); + ((AllInOneClientFactory) DEFAULT).doClose(); + } + + Set supportedSchemes(); + + SessionOptions options(); + + /** + * Returns the {@link EventLoopGroup} being used by this {@link ClientFactory}. Can be used to, e.g., + * schedule a periodic task without creating a separate event loop. + */ + EventLoopGroup eventLoopGroup(); + + Supplier eventLoopSupplier(); + + /** + * Creates a new client that connects to the specified {@code uri}. + * + * @param uri the URI of the server endpoint + * @param clientType the type of the new client + * @param options the {@link ClientOptionValue}s + */ + T newClient(String uri, Class clientType, ClientOptionValue... options); + + /** + * Creates a new client that connects to the specified {@code uri}. + * + * @param uri the URI of the server endpoint + * @param clientType the type of the new client + * @param options the {@link ClientOptions} + */ + T newClient(String uri, Class clientType, ClientOptions options); + + /** + * Creates a new client that connects to the specified {@link URI} using the default + * {@link ClientFactory}. + * + * @param uri the URI of the server endpoint + * @param clientType the type of the new client + * @param options the {@link ClientOptionValue}s + */ + T newClient(URI uri, Class clientType, ClientOptionValue... options); + + /** + * Creates a new client that connects to the specified {@link URI} using the default + * {@link ClientFactory}. + * + * @param uri the URI of the server endpoint + * @param clientType the type of the new client + * @param options the {@link ClientOptions} + */ + T newClient(URI uri, Class clientType, ClientOptions options); + + /** + * Closes all clients managed by this factory and shuts down the {@link EventLoopGroup} + * created implicitly by this factory. + */ + @Override + void close(); +} diff --git a/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java b/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java deleted file mode 100644 index a3943301410a..000000000000 --- a/src/main/java/com/linecorp/armeria/client/ClientInvocationHandler.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.UndeclaredThrowableException; -import java.net.URI; -import java.nio.channels.ClosedChannelException; - -import com.linecorp.armeria.common.ServiceInvocationContext; - -import io.netty.channel.EventLoop; -import io.netty.channel.EventLoopGroup; -import io.netty.util.concurrent.Future; - -final class ClientInvocationHandler implements InvocationHandler { - - private static final Object[] NO_ARGS = new Object[0]; - - private final EventLoopGroup eventLoopGroup; - private final URI uri; - private final Class interfaceClass; - private final Client client; - private final ClientOptions options; - - ClientInvocationHandler(EventLoopGroup eventLoopGroup, URI uri, - Class interfaceClass, Client client, ClientOptions options) { - - this.eventLoopGroup = eventLoopGroup; - this.uri = uri; - this.interfaceClass = interfaceClass; - this.client = client; - this.options = options; - } - - EventLoopGroup eventLoopGroup() { - return eventLoopGroup; - } - - URI uri() { - return uri; - } - - Class interfaceClass() { - return interfaceClass; - } - - Client client() { - return client; - } - - private RemoteInvoker invoker() { - return client.invoker(); - } - - private ClientCodec codec() { - return client.codec(); - } - - ClientOptions options() { - return options; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - final Class declaringClass = method.getDeclaringClass(); - if (declaringClass == Object.class) { - // Handle the methods in Object - return invokeObjectMethod(proxy, method, args); - } - - assert declaringClass == interfaceClass; - // Handle the methods in the interface. - return invokeClientMethod(method, args); - } - - private Object invokeObjectMethod(Object proxy, Method method, Object[] args) { - final String methodName = method.getName(); - - switch (methodName) { - case "toString": - return interfaceClass.getSimpleName() + '(' + uri + ')'; - case "hashCode": - return System.identityHashCode(proxy); - case "equals": - return proxy == args[0]; - default: - throw new Error("unknown method: " + methodName); - } - } - - private Object invokeClientMethod(Method method, Object[] args) throws Throwable { - if (args == null) { - args = NO_ARGS; - } - - try { - Future resultFuture = invoker().invoke(eventLoop(), uri, options, codec(), method, args); - if (codec().isAsyncClient()) { - return method.getReturnType().isInstance(resultFuture) ? resultFuture : null; - } else { - return resultFuture.sync().getNow(); - } - } catch (ClosedChannelException ignored) { - throw ClosedSessionException.INSTANCE; - } - } - - private EventLoop eventLoop() { - return ServiceInvocationContext.mapCurrent(ServiceInvocationContext::eventLoop, eventLoopGroup::next); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/ClientOption.java b/src/main/java/com/linecorp/armeria/client/ClientOption.java index 243066e6ee12..8638ec5ef4dd 100644 --- a/src/main/java/com/linecorp/armeria/client/ClientOption.java +++ b/src/main/java/com/linecorp/armeria/client/ClientOption.java @@ -20,10 +20,9 @@ import java.util.function.Function; import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.TimeoutPolicy; +import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.util.AbstractOption; -import io.netty.handler.codec.http.HttpHeaders; import io.netty.util.ConstantPool; /** @@ -40,15 +39,21 @@ protected ClientOption newConstant(int id, String name) { }; /** - * The {@link TimeoutPolicy} for a socket write + * The default timeout of a socket write */ - public static final ClientOption WRITE_TIMEOUT_POLICY = valueOf("WRITE_TIMEOUT_POLICY"); + public static final ClientOption DEFAULT_WRITE_TIMEOUT_MILLIS = + valueOf("DEFAULT_WRITE_TIMEOUT_MILLIS"); /** - * The {@link TimeoutPolicy} for a server reply to a client call. + * The default timeout of a server reply to a client call. */ - public static final ClientOption RESPONSE_TIMEOUT_POLICY = valueOf( - "RESPONSE_TIMEOUT_POLICY"); + public static final ClientOption DEFAULT_RESPONSE_TIMEOUT_MILLIS = + valueOf("DEFAULT_RESPONSE_TIMEOUT_MILLIS"); + + /** + * The default maximum allowed length of a server response. + */ + public static final ClientOption DEFAULT_MAX_RESPONSE_LENGTH = valueOf("DEFAULT_MAX_RESPONSE_LENGTH"); /** * The additional HTTP headers to send with requests. Used only when the underlying @@ -57,10 +62,9 @@ protected ClientOption newConstant(int id, String name) { public static final ClientOption HTTP_HEADERS = valueOf("HTTP_HEADERS"); /** - * The {@link Function} that decorates the client components provided by {@link Client}. + * The {@link Function} that decorates the client components. */ - public static final ClientOption> DECORATOR = valueOf( - "DECORATOR"); + public static final ClientOption DECORATION = valueOf("DECORATION"); /** * Returns the {@link ClientOption} of the specified name. diff --git a/src/main/java/com/linecorp/armeria/client/ClientOptionDerivable.java b/src/main/java/com/linecorp/armeria/client/ClientOptionDerivable.java new file mode 100644 index 000000000000..6b7127b936b0 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/ClientOptionDerivable.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import java.net.URI; +import java.util.Arrays; + +public interface ClientOptionDerivable { + + /** + * Creates a new derived client that connects to the same {@link URI} with the specified {@code client} + * with the specified {@code additionalOptions}. Note that the derived client will use the options of + * the specified {@code client} unless specified in {@code additionalOptions}. + */ + default T withOptions(ClientOptionValue... additionalOptions) { + requireNonNull(additionalOptions, "additionalOptions"); + return withOptions(Arrays.asList(additionalOptions)); + } + + /** + * Creates a new derived client that connects to the same {@link URI} with the specified {@code client} + * with the specified {@code additionalOptions}. Note that the derived client will use the options of + * the specified {@code client} unless specified in {@code additionalOptions}. + */ + T withOptions(Iterable> additionalOptions); +} diff --git a/src/main/java/com/linecorp/armeria/client/ClientOptions.java b/src/main/java/com/linecorp/armeria/client/ClientOptions.java index 30b811b55155..0e2fe2ad3b2e 100644 --- a/src/main/java/com/linecorp/armeria/client/ClientOptions.java +++ b/src/main/java/com/linecorp/armeria/client/ClientOptions.java @@ -16,10 +16,11 @@ package com.linecorp.armeria.client; -import static com.linecorp.armeria.client.ClientOption.DECORATOR; +import static com.linecorp.armeria.client.ClientOption.DECORATION; +import static com.linecorp.armeria.client.ClientOption.DEFAULT_MAX_RESPONSE_LENGTH; +import static com.linecorp.armeria.client.ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS; +import static com.linecorp.armeria.client.ClientOption.DEFAULT_WRITE_TIMEOUT_MILLIS; import static com.linecorp.armeria.client.ClientOption.HTTP_HEADERS; -import static com.linecorp.armeria.client.ClientOption.RESPONSE_TIMEOUT_POLICY; -import static com.linecorp.armeria.client.ClientOption.WRITE_TIMEOUT_POLICY; import static java.util.Objects.requireNonNull; import java.time.Duration; @@ -30,13 +31,11 @@ import java.util.Optional; import java.util.function.Function; -import com.linecorp.armeria.common.TimeoutPolicy; -import com.linecorp.armeria.common.http.ImmutableHttpHeaders; +import com.linecorp.armeria.common.http.DefaultHttpHeaders; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.util.AbstractOptions; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; import io.netty.util.AsciiString; @@ -45,28 +44,22 @@ */ public final class ClientOptions extends AbstractOptions { - private static final TimeoutPolicy DEFAULT_WRITE_TIMEOUT_POLICY = - TimeoutPolicy.ofFixed(Duration.ofSeconds(1)); - private static final TimeoutPolicy DEFAULT_RESPONSE_TIMEOUT_POLICY = - TimeoutPolicy.ofFixed(Duration.ofSeconds(10)); - - private static final ClientOptionValue[] DEFAULT_OPTIONS = { - WRITE_TIMEOUT_POLICY.newValue(DEFAULT_WRITE_TIMEOUT_POLICY), - RESPONSE_TIMEOUT_POLICY.newValue(DEFAULT_RESPONSE_TIMEOUT_POLICY) - }; - - /** - * The default {@link ClientOptions}. - */ - public static final ClientOptions DEFAULT = new ClientOptions(DEFAULT_OPTIONS); + private static final Long DEFAULT_DEFAULT_WRITE_TIMEOUT_MILLIS = Duration.ofSeconds(1).toMillis(); + private static final Long DEFAULT_DEFAULT_RESPONSE_TIMEOUT_MILLIS = Duration.ofSeconds(10).toMillis(); + private static final Long DEFAULT_DEFAULT_MAX_RESPONSE_LENGTH = 10L * 1024 * 1024; // 10 MB @SuppressWarnings("deprecation") private static final Collection BLACKLISTED_HEADER_NAMES = Collections.unmodifiableCollection(Arrays.asList( + HttpHeaderNames.AUTHORITY, HttpHeaderNames.CONNECTION, HttpHeaderNames.HOST, HttpHeaderNames.KEEP_ALIVE, + HttpHeaderNames.METHOD, + HttpHeaderNames.PATH, HttpHeaderNames.PROXY_CONNECTION, + HttpHeaderNames.SCHEME, + HttpHeaderNames.STATUS, HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderNames.UPGRADE, HttpHeaderNames.USER_AGENT, @@ -76,6 +69,19 @@ public final class ClientOptions extends AbstractOptions { ExtensionHeaderNames.STREAM_ID.text(), ExtensionHeaderNames.STREAM_PROMISE_ID.text())); + private static final ClientOptionValue[] DEFAULT_OPTIONS = { + DEFAULT_WRITE_TIMEOUT_MILLIS.newValue(DEFAULT_DEFAULT_WRITE_TIMEOUT_MILLIS), + DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue(DEFAULT_DEFAULT_RESPONSE_TIMEOUT_MILLIS), + DEFAULT_MAX_RESPONSE_LENGTH.newValue(DEFAULT_DEFAULT_MAX_RESPONSE_LENGTH), + DECORATION.newValue(ClientDecoration.NONE), + HTTP_HEADERS.newValue(HttpHeaders.EMPTY_HEADERS) + }; + + /** + * The default {@link ClientOptions}. + */ + public static final ClientOptions DEFAULT = new ClientOptions(DEFAULT_OPTIONS); + /** * Returns the {@link ClientOptions} with the specified {@link ClientOptionValue}s. */ @@ -100,6 +106,7 @@ public static ClientOptions of(Iterable> options) { * @return the merged {@link ClientOptions} */ public static ClientOptions of(ClientOptions baseOptions, ClientOptionValue... options) { + // TODO(trustin): Reduce the cost of creating a derived ClientOptions. requireNonNull(baseOptions, "baseOptions"); requireNonNull(options, "options"); if (options.length == 0) { @@ -114,6 +121,14 @@ public static ClientOptions of(ClientOptions baseOptions, ClientOptionValue.. * @return the merged {@link ClientOptions} */ public static ClientOptions of(ClientOptions baseOptions, Iterable> options) { + // TODO(trustin): Reduce the cost of creating a derived ClientOptions. + requireNonNull(baseOptions, "baseOptions"); + requireNonNull(options, "options"); + return new ClientOptions(baseOptions, options); + } + + public static ClientOptions of(ClientOptions baseOptions, ClientOptions options) { + // TODO(trustin): Reduce the cost of creating a derived ClientOptions. requireNonNull(baseOptions, "baseOptions"); requireNonNull(options, "options"); return new ClientOptions(baseOptions, options); @@ -138,12 +153,13 @@ private static ClientOptionValue filterValue(ClientOptionValue optionV private static HttpHeaders filterHttpHeaders(HttpHeaders headers) { requireNonNull(headers, "headers"); - BLACKLISTED_HEADER_NAMES.stream().filter(headers::contains).anyMatch(h -> { - throw new IllegalArgumentException("unallowed header name: " + h); - }); + for (AsciiString name : BLACKLISTED_HEADER_NAMES) { + if (headers.contains(name)) { + throw new IllegalArgumentException("unallowed header name: " + name); + } + } - // Create an immutable copy to prevent further modification. - return new ImmutableHttpHeaders(new DefaultHttpHeaders(false).add(headers)); + return HttpHeaders.ofImmutable(new DefaultHttpHeaders().add(headers)); } private ClientOptions(ClientOptionValue... options) { @@ -158,6 +174,10 @@ private ClientOptions(ClientOptions clientOptions, Iterable super(ClientOptions::filterValue, clientOptions, options); } + private ClientOptions(ClientOptions clientOptions, ClientOptions options) { + super(clientOptions, options); + } + /** * Returns the value of the specified {@link ClientOption}. * @@ -187,24 +207,32 @@ public Map, ClientOptionValue> asMap() { } /** - * Returns the {@link TimeoutPolicy} for a server reply to a client call. + * Returns the default timeout of a server reply to a client call. + */ + public long defaultResponseTimeoutMillis() { + return getOrElse(DEFAULT_RESPONSE_TIMEOUT_MILLIS, DEFAULT_DEFAULT_RESPONSE_TIMEOUT_MILLIS); + } + + /** + * Returns the default timeout of a socket write. */ - public TimeoutPolicy responseTimeoutPolicy() { - return getOrElse(RESPONSE_TIMEOUT_POLICY, DEFAULT_RESPONSE_TIMEOUT_POLICY); + public long defaultWriteTimeoutMillis() { + return getOrElse(DEFAULT_WRITE_TIMEOUT_MILLIS, DEFAULT_DEFAULT_WRITE_TIMEOUT_MILLIS); } /** - * Returns the {@link TimeoutPolicy} for a socket write. + * Returns the maximum allowed length of a server response. */ - public TimeoutPolicy writeTimeoutPolicy() { - return getOrElse(WRITE_TIMEOUT_POLICY, DEFAULT_WRITE_TIMEOUT_POLICY); + @SuppressWarnings("unchecked") + public long defaultMaxResponseLength() { + return getOrElse(DEFAULT_MAX_RESPONSE_LENGTH, DEFAULT_DEFAULT_MAX_RESPONSE_LENGTH); } /** - * Returns the {@link Function} that decorates the components of a client. + * Returns the {@link Function}s that decorate the components of a client. */ - public Function decorator() { - return getOrElse(DECORATOR, Function.identity()); + public ClientDecoration decoration() { + return getOrElse(DECORATION, ClientDecoration.NONE); } } diff --git a/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java b/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java new file mode 100644 index 000000000000..ea1257ba93bc --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import java.time.Duration; + +import com.linecorp.armeria.common.RequestContext; +import com.linecorp.armeria.common.http.HttpHeaders; + +import io.netty.util.AttributeKey; + +/** + * Provides information about an invocation and related utilities. Every client request has its own + * {@link ClientRequestContext} instance. + */ +public interface ClientRequestContext extends RequestContext { + + AttributeKey HTTP_HEADERS = AttributeKey.valueOf(ClientRequestContext.class, "HTTP_HEADERS"); + + Endpoint endpoint(); + ClientOptions options(); + + long writeTimeoutMillis(); + void setWriteTimeoutMillis(long writeTimeoutMillis); + void setWriteTimeout(Duration writeTimeout); + + long responseTimeoutMillis(); + void setResponseTimeoutMillis(long responseTimeoutMillis); + void setResponseTimeout(Duration responseTimeout); + + long maxResponseLength(); + void setMaxResponseLength(long maxResponseLength); +} diff --git a/src/main/java/com/linecorp/armeria/client/Clients.java b/src/main/java/com/linecorp/armeria/client/Clients.java index e2248f1cafab..f6b3fa7da89b 100644 --- a/src/main/java/com/linecorp/armeria/client/Clients.java +++ b/src/main/java/com/linecorp/armeria/client/Clients.java @@ -15,12 +15,7 @@ */ package com.linecorp.armeria.client; -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Proxy; import java.net.URI; -import java.util.function.Function; /** * Creates a new client that connects to a specified {@link URI}. @@ -29,111 +24,111 @@ public final class Clients { /** * Creates a new client that connects to the specified {@code uri} using the default - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptionValue}s */ - public static T newClient(String uri, Class interfaceClass, ClientOptionValue... options) { - return newClient(RemoteInvokerFactory.DEFAULT, uri, interfaceClass, options); + public static T newClient(String uri, Class clientType, ClientOptionValue... options) { + return newClient(ClientFactory.DEFAULT, uri, clientType, options); } /** * Creates a new client that connects to the specified {@code uri} using the default - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptions} */ - public static T newClient(String uri, Class interfaceClass, ClientOptions options) { - return newClient(RemoteInvokerFactory.DEFAULT, uri, interfaceClass, options); + public static T newClient(String uri, Class clientType, ClientOptions options) { + return newClient(ClientFactory.DEFAULT, uri, clientType, options); } /** * Creates a new client that connects to the specified {@code uri} using an alternative - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * - * @param remoteInvokerFactory an alternative {@link RemoteInvokerFactory} + * @param factory an alternative {@link ClientFactory} * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptionValue}s */ - public static T newClient(RemoteInvokerFactory remoteInvokerFactory, String uri, - Class interfaceClass, ClientOptionValue... options) { + public static T newClient(ClientFactory factory, String uri, + Class clientType, ClientOptionValue... options) { - return new ClientBuilder(uri).remoteInvokerFactory(remoteInvokerFactory).options(options) - .build(interfaceClass); + return new ClientBuilder(uri).factory(factory).options(options) + .build(clientType); } /** * Creates a new client that connects to the specified {@code uri} using an alternative - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * - * @param remoteInvokerFactory an alternative {@link RemoteInvokerFactory} + * @param factory an alternative {@link ClientFactory} * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptions} */ - public static T newClient(RemoteInvokerFactory remoteInvokerFactory, String uri, - Class interfaceClass, ClientOptions options) { - return new ClientBuilder(uri).remoteInvokerFactory(remoteInvokerFactory).options(options) - .build(interfaceClass); + public static T newClient(ClientFactory factory, String uri, + Class clientType, ClientOptions options) { + return new ClientBuilder(uri).factory(factory).options(options) + .build(clientType); } /** * Creates a new client that connects to the specified {@link URI} using the default - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptionValue}s */ - public static T newClient(URI uri, Class interfaceClass, ClientOptionValue... options) { - return newClient(RemoteInvokerFactory.DEFAULT, uri, interfaceClass, options); + public static T newClient(URI uri, Class clientType, ClientOptionValue... options) { + return newClient(ClientFactory.DEFAULT, uri, clientType, options); } /** * Creates a new client that connects to the specified {@link URI} using the default - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptions} */ - public static T newClient(URI uri, Class interfaceClass, ClientOptions options) { - return newClient(RemoteInvokerFactory.DEFAULT, uri, interfaceClass, options); + public static T newClient(URI uri, Class clientType, ClientOptions options) { + return newClient(ClientFactory.DEFAULT, uri, clientType, options); } /** * Creates a new client that connects to the specified {@link URI} using an alternative - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * - * @param remoteInvokerFactory an alternative {@link RemoteInvokerFactory} + * @param factory an alternative {@link ClientFactory} * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptionValue}s */ - public static T newClient(RemoteInvokerFactory remoteInvokerFactory, URI uri, Class interfaceClass, + public static T newClient(ClientFactory factory, URI uri, Class clientType, ClientOptionValue... options) { - return new ClientBuilder(uri).remoteInvokerFactory(remoteInvokerFactory).options(options) - .build(interfaceClass); + return new ClientBuilder(uri).factory(factory).options(options) + .build(clientType); } /** * Creates a new client that connects to the specified {@link URI} using an alternative - * {@link RemoteInvokerFactory}. + * {@link ClientFactory}. * - * @param remoteInvokerFactory an alternative {@link RemoteInvokerFactory} + * @param factory an alternative {@link ClientFactory} * @param uri the URI of the server endpoint - * @param interfaceClass the type of the new client + * @param clientType the type of the new client * @param options the {@link ClientOptions} */ - public static T newClient(RemoteInvokerFactory remoteInvokerFactory, URI uri, Class interfaceClass, + public static T newClient(ClientFactory factory, URI uri, Class clientType, ClientOptions options) { - return new ClientBuilder(uri).remoteInvokerFactory(remoteInvokerFactory).options(options) - .build(interfaceClass); + return new ClientBuilder(uri).factory(factory).options(options) + .build(clientType); } /** @@ -142,9 +137,7 @@ public static T newClient(RemoteInvokerFactory remoteInvokerFactory, URI uri * the specified {@code client} unless specified in {@code additionalOptions}. */ public static T newDerivedClient(T client, ClientOptionValue... additionalOptions) { - requireNonNull(additionalOptions, "additionalOptions"); - // TODO(trustin): Reduce the cost of creating a derived ClientOptions. - return newDerivedClient(client, baseOptions -> ClientOptions.of(baseOptions, additionalOptions)); + return asDerivable(client).withOptions(additionalOptions); } /** @@ -153,44 +146,18 @@ public static T newDerivedClient(T client, ClientOptionValue... additiona * the specified {@code client} unless specified in {@code additionalOptions}. */ public static T newDerivedClient(T client, Iterable> additionalOptions) { - requireNonNull(additionalOptions, "additionalOptions"); - // TODO(trustin): Reduce the cost of creating a derived ClientOptions. - return newDerivedClient(client, baseOptions -> ClientOptions.of(baseOptions, additionalOptions)); + return asDerivable(client).withOptions(additionalOptions); } - private static T newDerivedClient(T client, - Function optionFactory) { - - requireNonNull(client, "client"); - - ClientInvocationHandler parent = null; - try { - InvocationHandler ih = Proxy.getInvocationHandler(client); - if (ih instanceof ClientInvocationHandler) { - parent = (ClientInvocationHandler) ih; - } - } catch (IllegalArgumentException expected) { - // Will reach here when 'client' is not a proxy object. - } - - if (parent == null) { - throw new IllegalArgumentException("not a client: " + client); + private static ClientOptionDerivable asDerivable(T client) { + if (!(client instanceof ClientOptionDerivable)) { + throw new IllegalArgumentException("client does not support derivation: " + client); } - final Class interfaceClass = parent.interfaceClass(); - @SuppressWarnings("unchecked") - final T derived = (T) Proxy.newProxyInstance( - interfaceClass.getClassLoader(), - new Class[] { interfaceClass }, - new ClientInvocationHandler(parent.eventLoopGroup(), - parent.uri(), interfaceClass, parent.client(), - optionFactory.apply(parent.options()))); - - return derived; + final ClientOptionDerivable derivable = (ClientOptionDerivable) client; + return derivable; } private Clients() {} } - - diff --git a/src/main/java/com/linecorp/armeria/client/ClosedSessionException.java b/src/main/java/com/linecorp/armeria/client/ClosedSessionException.java deleted file mode 100644 index 9eca4b7bedf2..000000000000 --- a/src/main/java/com/linecorp/armeria/client/ClosedSessionException.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import com.linecorp.armeria.common.util.Exceptions; - -/** - * A {@link RuntimeException} raised when the connection to the server has been closed unexpectedly. - */ -public class ClosedSessionException extends RuntimeException { - - private static final long serialVersionUID = -78487475521731580L; - - static final ClosedSessionException INSTANCE = Exceptions.clearTrace(new ClosedSessionException()); - - /** - * Creates a new instance. - */ - public ClosedSessionException() {} - - /** - * Creates a new instance with the specified {@code message} and {@code cause}. - */ - public ClosedSessionException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new instance with the specified {@code message}. - */ - public ClosedSessionException(String message) { - super(message); - } - - /** - * Creates a new instance with the specified {@code cause}. - */ - public ClosedSessionException(Throwable cause) { - super(cause); - } - - /** - * Creates a new instance with the specified {@code message}, {@code cause}, suppression enabled or - * disabled, and writable stack trace enabled or disabled. - */ - protected ClosedSessionException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/DecoratingClient.java b/src/main/java/com/linecorp/armeria/client/DecoratingClient.java index 450ff0c64fc6..84b9ffb1abbc 100644 --- a/src/main/java/com/linecorp/armeria/client/DecoratingClient.java +++ b/src/main/java/com/linecorp/armeria/client/DecoratingClient.java @@ -18,49 +18,46 @@ import static java.util.Objects.requireNonNull; -import java.util.function.Function; - /** - * A {@link Client} that decorates another {@link Client}. + * Decorates a {@link Client}. */ -public class DecoratingClient extends SimpleClient { +public class DecoratingClient implements Client { + + private final Client delegate; /** - * Creates a new instance that decorates the specified {@link Client} and its {@link ClientCodec} and - * {@link RemoteInvoker} using the specified {@code codecDecorator} and {@code invokerDecorator}. + * Creates a new instance that decorates the specified {@link Client}. */ - protected - DecoratingClient(Client client, Function codecDecorator, Function invokerDecorator) { - super(decorateCodec(client, codecDecorator), decorateInvoker(client, invokerDecorator)); + protected DecoratingClient(Client delegate) { + this.delegate = requireNonNull(delegate, "delegate"); } - private static - ClientCodec decorateCodec(Client client, Function codecDecorator) { - - requireNonNull(codecDecorator, "codecDecorator"); - - @SuppressWarnings("unchecked") - final T codec = (T) client.codec(); - final U decoratedCodec = codecDecorator.apply(codec); - if (decoratedCodec == null) { - throw new NullPointerException("codecDecorator.apply() returned null: " + codecDecorator); - } - - return decoratedCodec; + /** + * Returns the {@link Client} being decorated. + */ + @SuppressWarnings("unchecked") + protected final > T delegate() { + return (T) delegate; } - private static - RemoteInvoker decorateInvoker(Client client, Function invokerDecorator) { - - requireNonNull(invokerDecorator, "invokerDecorator"); + /** + * Returns the {@link Client} being decorated. + */ + @SuppressWarnings({ "unchecked", "UnusedParameters" }) + protected final , T_I, T_O> T delegate(Class requestType, + Class responseType) { + return (T) delegate; + } - @SuppressWarnings("unchecked") - final T invoker = (T) client.invoker(); - final U decoratedInvoker = invokerDecorator.apply(invoker); - if (decoratedInvoker == null) { - throw new NullPointerException("invokerDecorator.apply() returned null: " + invokerDecorator); - } + @Override + public O execute(ClientRequestContext ctx, I req) throws Exception { + return delegate().execute(ctx, req); + } - return decoratedInvoker; + @Override + public String toString() { + final String simpleName = getClass().getSimpleName(); + final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; + return name + '(' + delegate + ')'; } } diff --git a/src/main/java/com/linecorp/armeria/client/DecoratingClientCodec.java b/src/main/java/com/linecorp/armeria/client/DecoratingClientCodec.java deleted file mode 100644 index 09d49f319349..000000000000 --- a/src/main/java/com/linecorp/armeria/client/DecoratingClientCodec.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; - -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.util.concurrent.Promise; - -/** - * A {@link ClientCodec} that decorates another {@link ClientCodec}. - * - * @see ClientOption#DECORATOR - */ -public abstract class DecoratingClientCodec implements ClientCodec { - - private final ClientCodec delegate; - - /** - * Creates a new instance that decorates the specified {@link ClientCodec}. - */ - protected DecoratingClientCodec(ClientCodec delegate) { - this.delegate = requireNonNull(delegate, "delegate"); - } - - /** - * Returns the {@link ClientCodec} being decorated. - */ - @SuppressWarnings("unchecked") - protected final T delegate() { - return (T) delegate; - } - - @Override - public void prepareRequest(Method method, Object[] args, - Promise resultPromise) { - delegate().prepareRequest(method, args, resultPromise); - } - - @Override - public EncodeResult encodeRequest(Channel channel, SessionProtocol sessionProtocol, Method method, - Object[] args) { - return delegate().encodeRequest(channel, sessionProtocol, method, args); - } - - @Override - public T decodeResponse(ServiceInvocationContext ctx, - ByteBuf content, Object originalResponse) throws Exception { - return delegate().decodeResponse(ctx, content, originalResponse); - } - - @Override - public boolean isAsyncClient() { - return delegate().isAsyncClient(); - } - - @Override - public String toString() { - final String simpleName = getClass().getSimpleName(); - final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; - return name + '(' + delegate() + ')'; - } -} diff --git a/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java b/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java new file mode 100644 index 000000000000..1970f33943d3 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/DecoratingClientFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import java.net.URI; +import java.util.Set; +import java.util.function.Supplier; + +import com.linecorp.armeria.common.Scheme; + +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; + +public class DecoratingClientFactory extends AbstractClientFactory { + + private final ClientFactory delegate; + + protected DecoratingClientFactory(ClientFactory delegate) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + protected ClientFactory delegate() { + return delegate; + } + + @Override + public Set supportedSchemes() { + return delegate().supportedSchemes(); + } + + @Override + public SessionOptions options() { + return delegate().options(); + } + + @Override + public EventLoopGroup eventLoopGroup() { + return delegate().eventLoopGroup(); + } + + @Override + public Supplier eventLoopSupplier() { + return delegate().eventLoopSupplier(); + } + + @Override + public T newClient(URI uri, Class clientType, ClientOptions options) { + return delegate().newClient(uri, clientType, options); + } + + @Override + public void close() { + delegate().close(); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/DecoratingRemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/DecoratingRemoteInvoker.java deleted file mode 100644 index 75ba78cbe963..000000000000 --- a/src/main/java/com/linecorp/armeria/client/DecoratingRemoteInvoker.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.net.URI; - -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; - -/** - * A {@link RemoteInvoker} that decorates another {@link RemoteInvoker}. - * - * @see ClientOption#DECORATOR - */ -public abstract class DecoratingRemoteInvoker implements RemoteInvoker { - - private final RemoteInvoker delegate; - - /** - * Creates a new instance that decorates the specified {@link RemoteInvoker}. - */ - protected DecoratingRemoteInvoker(RemoteInvoker delegate) { - this.delegate = requireNonNull(delegate, "delegate"); - } - - /** - * Returns the {@link RemoteInvoker} being decorated. - */ - @SuppressWarnings("unchecked") - protected final T delegate() { - return (T) delegate; - } - - @Override - public Future invoke(EventLoop eventLoop, URI uri, - ClientOptions options, - ClientCodec codec, Method method, - Object[] args) throws Exception { - - return delegate().invoke(eventLoop, uri, options, codec, method, args); - } - - @Override - public void close() { - delegate().close(); - } - - @Override - public String toString() { - final String simpleName = getClass().getSimpleName(); - final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; - return name + '(' + delegate() + ')'; - } -} diff --git a/src/main/java/com/linecorp/armeria/client/DefaultClientRequestContext.java b/src/main/java/com/linecorp/armeria/client/DefaultClientRequestContext.java new file mode 100644 index 000000000000..147f2dc230a9 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/DefaultClientRequestContext.java @@ -0,0 +1,205 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import com.linecorp.armeria.common.AbstractRequestContext; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.DefaultHttpHeaders; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.logging.DefaultRequestLog; +import com.linecorp.armeria.common.logging.DefaultResponseLog; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.RequestLogBuilder; +import com.linecorp.armeria.common.logging.ResponseLog; +import com.linecorp.armeria.common.logging.ResponseLogBuilder; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; + +/** + * Default {@link ClientRequestContext} implementation. + */ +public final class DefaultClientRequestContext extends AbstractRequestContext implements ClientRequestContext { + + private final EventLoop eventLoop; + private final ClientOptions options; + private final Endpoint endpoint; + + private final DefaultRequestLog requestLog; + private final DefaultResponseLog responseLog; + + private long writeTimeoutMillis; + private long responseTimeoutMillis; + private long maxResponseLength; + + private String strVal; + + /** + * Creates a new instance. + * + * @param sessionProtocol the {@link SessionProtocol} of the invocation + * @param request the request associated with this context + */ + public DefaultClientRequestContext( + EventLoop eventLoop, SessionProtocol sessionProtocol, + Endpoint endpoint, String method, String path, ClientOptions options, Object request) { + + super(sessionProtocol, method, path, request); + + this.eventLoop = eventLoop; + this.options = options; + this.endpoint = endpoint; + + requestLog = new DefaultRequestLog(); + responseLog = new DefaultResponseLog(requestLog); + + writeTimeoutMillis = options.defaultWriteTimeoutMillis(); + responseTimeoutMillis = options.defaultResponseTimeoutMillis(); + maxResponseLength = options.defaultMaxResponseLength(); + + if (SessionProtocol.ofHttp().contains(sessionProtocol)) { + final HttpHeaders headers = options.getOrElse(ClientOption.HTTP_HEADERS, HttpHeaders.EMPTY_HEADERS); + if (!headers.isEmpty()) { + final HttpHeaders headersCopy = new DefaultHttpHeaders(true, headers.size()); + headersCopy.set(headers); + attr(HTTP_HEADERS).set(headersCopy); + } + } + } + + @Override + public EventLoop eventLoop() { + return eventLoop; + } + + @Override + public ClientOptions options() { + return options; + } + + @Override + public Endpoint endpoint() { + return endpoint; + } + + @Override + public long writeTimeoutMillis() { + return writeTimeoutMillis; + } + + @Override + public void setWriteTimeoutMillis(long writeTimeoutMillis) { + if (writeTimeoutMillis < 0) { + throw new IllegalArgumentException( + "writeTimeoutMillis: " + writeTimeoutMillis + " (expected: >= 0)"); + } + this.writeTimeoutMillis = writeTimeoutMillis; + } + + @Override + public void setWriteTimeout(Duration writeTimeout) { + setWriteTimeoutMillis(requireNonNull(writeTimeout, "writeTimeout").toMillis()); + } + + @Override + public long responseTimeoutMillis() { + return responseTimeoutMillis; + } + + @Override + public void setResponseTimeoutMillis(long responseTimeoutMillis) { + if (responseTimeoutMillis < 0) { + throw new IllegalArgumentException( + "responseTimeoutMillis: " + responseTimeoutMillis + " (expected: >= 0)"); + } + this.responseTimeoutMillis = responseTimeoutMillis; + } + + @Override + public void setResponseTimeout(Duration responseTimeout) { + setResponseTimeoutMillis(requireNonNull(responseTimeout, "responseTimeout").toMillis()); + } + + @Override + public long maxResponseLength() { + return maxResponseLength; + } + + @Override + public void setMaxResponseLength(long maxResponseLength) { + this.maxResponseLength = maxResponseLength; + } + + @Override + public RequestLogBuilder requestLogBuilder() { + return requestLog; + } + + @Override + public ResponseLogBuilder responseLogBuilder() { + return responseLog; + } + + @Override + public CompletableFuture awaitRequestLog() { + return requestLog; + } + + @Override + public CompletableFuture awaitResponseLog() { + return responseLog; + } + + @Override + public String toString() { + String strVal = this.strVal; + if (strVal != null) { + return strVal; + } + + final StringBuilder buf = new StringBuilder(96); + + // Prepend the current channel information if available. + final Channel ch = requestLog.channel(); + final boolean hasChannel = ch != null; + if (hasChannel) { + buf.append(ch); + } + + buf.append('[') + .append(sessionProtocol().uriText()) + .append("://") + .append(endpoint.authority()) + .append(path()) + .append('#') + .append(method()) + .append(']'); + + strVal = buf.toString(); + + if (hasChannel) { + this.strVal = strVal; + } + + return strVal; + } +} diff --git a/src/main/java/com/linecorp/armeria/client/Endpoint.java b/src/main/java/com/linecorp/armeria/client/Endpoint.java new file mode 100644 index 000000000000..bfc5aaa7225a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/Endpoint.java @@ -0,0 +1,158 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import static java.util.Objects.requireNonNull; + +import javax.annotation.Nonnull; + +import com.linecorp.armeria.client.routing.EndpointGroupRegistry; + +public final class Endpoint { + + public static Endpoint parse(String authority) { + requireNonNull(authority, "authority"); + if (authority.startsWith("group:")) { + return ofGroup(authority.substring(6)); + } + + final int lastColonIdx = authority.lastIndexOf(':'); + if (lastColonIdx <= 0) { + throw parseFailure(authority); + } + + final String host = authority.substring(0, lastColonIdx); + final int port; + try { + port = Integer.parseInt(authority.substring(lastColonIdx + 1)); + } catch (NumberFormatException ignored) { + throw parseFailure(authority); + } + + if (port <= 0 || port >= 65536) { + throw parseFailure(authority); + } + + return of(host, port); + } + + @Nonnull + private static IllegalArgumentException parseFailure(String authority) { + return new IllegalArgumentException("cannot find 'group:' nor valid ':': " + authority); + } + + public static Endpoint ofGroup(String name) { + requireNonNull(name, "name"); + return new Endpoint(name); + } + + public static Endpoint of(String host, int port) { + return of(host, port, 1000); + } + + // TODO(trustin): Remove weight and make Endpoint a pure endpoint representation. + // We could specify an additional attributes such as weight/priority + // when adding an Endpoint to an EndpointGroup. + + public static Endpoint of(String host, int port, int weight) { + return new Endpoint(host, port, weight); + } + + private final String groupName; + private final String host; + private final int port; + private final int weight; + private String authority; + + private Endpoint(String groupName) { + this.groupName = groupName; + host = null; + port = 0; + weight = 0; + } + + private Endpoint(String host, int port, int weight) { + this.host = host; + this.port = port; + this.weight = weight; + groupName = null; + } + + public boolean isGroup() { + return groupName != null; + } + + public Endpoint resolve() { + if (isGroup()) { + return EndpointGroupRegistry.selectNode(groupName); + } else { + return this; + } + } + + public String groupName() { + ensureGroup(); + return groupName; + } + + public String host() { + ensureSingle(); + return host; + } + + public int port() { + ensureSingle(); + return port; + } + + public int weight() { + ensureSingle(); + return weight; + } + + private void ensureGroup() { + if (!isGroup()) { + throw new IllegalStateException("not a group endpoint"); + } + } + + private void ensureSingle() { + if (isGroup()) { + throw new IllegalStateException("not a host:port endpoint"); + } + } + + public String authority() { + String authority = this.authority; + if (authority != null) { + return authority; + } + + if (isGroup()) { + authority = "group:" + groupName; + } else { + authority = host() + ':' + port(); + } + + return this.authority = authority; + } + + @Override + public String toString() { + return "Endpoint(" + authority() + ')'; + } +} diff --git a/src/main/java/com/linecorp/armeria/client/Http2ClientIdleTimeoutHandler.java b/src/main/java/com/linecorp/armeria/client/Http2ClientIdleTimeoutHandler.java deleted file mode 100644 index f67242c34729..000000000000 --- a/src/main/java/com/linecorp/armeria/client/Http2ClientIdleTimeoutHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import java.util.concurrent.TimeUnit; - -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpMessage; -import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; - -/** - * A {@link HttpClientIdleTimeoutHandler} that ignores the responses received from the upgrade stream. - */ -class Http2ClientIdleTimeoutHandler extends HttpClientIdleTimeoutHandler { - - Http2ClientIdleTimeoutHandler(long idleTimeoutMillis) { - super(idleTimeoutMillis); - } - - Http2ClientIdleTimeoutHandler(long idleTimeout, TimeUnit timeUnit) { - super(idleTimeout, timeUnit); - } - - @Override - boolean isResponseEnd(Object msg) { - if (!(msg instanceof FullHttpResponse)) { - return false; - } - - return !"1".equals(((HttpMessage) msg).headers().get(ExtensionHeaderNames.STREAM_ID.text())); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/HttpClientIdleTimeoutHandler.java b/src/main/java/com/linecorp/armeria/client/HttpClientIdleTimeoutHandler.java deleted file mode 100644 index 6960adc5aeef..000000000000 --- a/src/main/java/com/linecorp/armeria/client/HttpClientIdleTimeoutHandler.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.handler.timeout.IdleStateHandler; - -class HttpClientIdleTimeoutHandler extends IdleStateHandler { - private static final Logger logger = LoggerFactory.getLogger(HttpClientIdleTimeoutHandler.class); - - /** - * The number of requests that are waiting for the responses - */ - protected int pendingResCount; - - HttpClientIdleTimeoutHandler(long idleTimeoutMillis) { - this(idleTimeoutMillis, TimeUnit.MILLISECONDS); - } - - HttpClientIdleTimeoutHandler(long idleTimeout, TimeUnit timeUnit) { - super(0, 0, idleTimeout, timeUnit); - } - - boolean isRequestStart(Object msg) { - return msg instanceof HttpRequest; - } - - boolean isResponseEnd(Object msg) { - return msg instanceof LastHttpContent; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (isResponseEnd(msg)) { - pendingResCount--; - } - super.channelRead(ctx, msg); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (isRequestStart(msg)) { - pendingResCount++; - } - - super.write(ctx, msg, promise); - } - - @Override - protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception { - if (pendingResCount == 0 && evt.isFirst()) { - logger.debug("{} Closing due to idleness", ctx.channel()); - ctx.close(); - } - } -} diff --git a/src/main/java/com/linecorp/armeria/client/HttpRemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/HttpRemoteInvoker.java deleted file mode 100644 index 91b373d5cb2c..000000000000 --- a/src/main/java/com/linecorp/armeria/client/HttpRemoteInvoker.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import static com.linecorp.armeria.common.SessionProtocol.H1; -import static com.linecorp.armeria.common.SessionProtocol.H1C; -import static com.linecorp.armeria.common.SessionProtocol.H2; -import static com.linecorp.armeria.common.SessionProtocol.H2C; -import static com.linecorp.armeria.common.SessionProtocol.HTTP; -import static com.linecorp.armeria.common.SessionProtocol.HTTPS; -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.EnumSet; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.client.ClientCodec.EncodeResult; -import com.linecorp.armeria.client.HttpSessionHandler.Invocation; -import com.linecorp.armeria.client.pool.DefaultKeyedChannelPool; -import com.linecorp.armeria.client.pool.KeyedChannelPool; -import com.linecorp.armeria.client.pool.KeyedChannelPoolHandler; -import com.linecorp.armeria.client.pool.KeyedChannelPoolHandlerAdapter; -import com.linecorp.armeria.client.pool.PoolKey; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; - -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelPromise; -import io.netty.channel.EventLoop; -import io.netty.channel.pool.ChannelHealthChecker; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.FutureListener; -import io.netty.util.concurrent.Promise; -import io.netty.util.internal.OneTimeTask; - -final class HttpRemoteInvoker implements RemoteInvoker { - - private static final Logger logger = LoggerFactory.getLogger(HttpRemoteInvoker.class); - - private static final KeyedChannelPoolHandlerAdapter NOOP_POOL_HANDLER = - new KeyedChannelPoolHandlerAdapter<>(); - - private static final ChannelHealthChecker POOL_HEALTH_CHECKER = - ch -> ch.eventLoop().newSucceededFuture(HttpSessionHandler.get(ch).isActive()); - - - static final Set HTTP_PROTOCOLS = EnumSet.of(H1, H1C, H2, H2C, HTTPS, HTTP); - - final ConcurrentMap> map = new ConcurrentHashMap<>(); - - private final Bootstrap baseBootstrap; - private final RemoteInvokerOptions options; - - HttpRemoteInvoker(Bootstrap baseBootstrap, RemoteInvokerOptions options) { - this.baseBootstrap = requireNonNull(baseBootstrap, "baseBootstrap"); - this.options = requireNonNull(options, "options"); - - assert baseBootstrap.group() == null; - } - - @Override - public Future invoke(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args) throws Exception { - - requireNonNull(uri, "uri"); - requireNonNull(options, "options"); - requireNonNull(codec, "codec"); - requireNonNull(method, "method"); - - final Scheme scheme = Scheme.parse(uri.getScheme()); - final SessionProtocol sessionProtocol = validateSessionProtocol(scheme.sessionProtocol()); - final InetSocketAddress remoteAddress = convertToSocketAddress(uri, sessionProtocol.isTls()); - - final PoolKey poolKey = new PoolKey(remoteAddress, sessionProtocol); - final Future channelFuture = pool(eventLoop).acquire(poolKey); - - final Promise resultPromise = eventLoop.newPromise(); - - codec.prepareRequest(method, args, resultPromise); - if (channelFuture.isSuccess()) { - Channel ch = channelFuture.getNow(); - invoke0(codec, ch, method, args, options, resultPromise, poolKey); - } else { - channelFuture.addListener((Future future) -> { - if (future.isSuccess()) { - Channel ch = future.getNow(); - invoke0(codec, ch, method, args, options, resultPromise, poolKey); - } else { - resultPromise.setFailure(channelFuture.cause()); - } - }); - } - - return resultPromise; - } - - private KeyedChannelPool pool(EventLoop eventLoop) { - KeyedChannelPool pool = map.get(eventLoop); - if (pool != null) { - return pool; - } - - return map.computeIfAbsent(eventLoop, e -> { - final Bootstrap bootstrap = baseBootstrap.clone(); - bootstrap.group(eventLoop); - - Function> factory = new HttpSessionChannelFactory(bootstrap, options); - - final KeyedChannelPoolHandler handler = - options.poolHandlerDecorator().apply(NOOP_POOL_HANDLER); - - eventLoop.terminationFuture().addListener((FutureListener) f -> map.remove(eventLoop)); - - //TODO(inch772) handle options.maxConcurrency(); - return new DefaultKeyedChannelPool<>(eventLoop, factory, - POOL_HEALTH_CHECKER, handler, true); - }); - } - - static void invoke0(ClientCodec codec, Channel channel, - Method method, Object[] args, ClientOptions options, - Promise resultPromise, PoolKey poolKey) { - - final HttpSession session = HttpSessionHandler.get(channel); - final SessionProtocol sessionProtocol = session.protocol(); - if (sessionProtocol == null) { - resultPromise.setFailure(ClosedSessionException.INSTANCE); - return; - } - - final EncodeResult encodeResult = codec.encodeRequest(channel, sessionProtocol, method, args); - if (encodeResult.isSuccess()) { - ServiceInvocationContext ctx = encodeResult.invocationContext(); - Promise responsePromise = channel.eventLoop().newPromise(); - - final Invocation invocation = new Invocation(ctx, options, responsePromise, encodeResult.content()); - //write request - final ChannelFuture writeFuture = writeRequest(channel, invocation, ctx, options); - writeFuture.addListener(fut -> { - if (!fut.isSuccess()) { - ctx.rejectPromise(responsePromise, fut.cause()); - } else { - long responseTimeoutMillis = options.responseTimeoutPolicy().timeout(ctx); - scheduleTimeout(channel, responsePromise, responseTimeoutMillis, false); - } - }); - - //handle response - if (responsePromise.isSuccess()) { - decodeResult(codec, resultPromise, ctx, responsePromise.getNow()); - } else { - responsePromise.addListener((Future future) -> { - if (future.isSuccess()) { - decodeResult(codec, resultPromise, ctx, responsePromise.getNow()); - } else { - ctx.rejectPromise(resultPromise, future.cause()); - } - }); - } - } else { - final Throwable cause = encodeResult.cause(); - if (!resultPromise.tryFailure(cause)) { - logger.warn("Failed to reject an invocation promise ({}) with {}", - resultPromise, cause, cause); - } - } - - if (!session.onRequestSent()) { - // Can't send a request via the current session anymore; do not return the channel to the pool. - return; - } - - // Return the channel to the pool. - final KeyedChannelPool pool = KeyedChannelPool.findPool(channel); - if (sessionProtocol.isMultiplex()) { - pool.release(poolKey, channel); - } else { - resultPromise.addListener(fut -> pool.release(poolKey, channel)); - } - } - - private static void decodeResult(ClientCodec codec, Promise resultPromise, - ServiceInvocationContext ctx, FullHttpResponse response) { - try { - ctx.resolvePromise(resultPromise, codec.decodeResponse(ctx, response.content(), response)); - } catch (Throwable e) { - ctx.rejectPromise(resultPromise, e); - } finally { - ReferenceCountUtil.release(response); - } - } - - private static ChannelFuture writeRequest(Channel channel, Invocation invocation, - ServiceInvocationContext ctx, ClientOptions options) { - final long writeTimeoutMillis = options.writeTimeoutPolicy().timeout(ctx); - final ChannelPromise writePromise = channel.newPromise(); - channel.writeAndFlush(invocation, writePromise); - scheduleTimeout(channel, writePromise, writeTimeoutMillis, true); - return writePromise; - } - - private static void scheduleTimeout( - Channel channel, Promise promise, long timeoutMillis, boolean useWriteTimeoutException) { - final ScheduledFuture timeoutFuture; - if (timeoutMillis > 0) { - timeoutFuture = channel.eventLoop().schedule( - new TimeoutTask(promise, timeoutMillis, useWriteTimeoutException), - timeoutMillis, TimeUnit.MILLISECONDS); - } else { - timeoutFuture = null; - } - - promise.addListener(future -> { - if (timeoutFuture != null) { - timeoutFuture.cancel(false); - } - }); - } - - private static class TimeoutTask extends OneTimeTask { - - private final Promise promise; - private final long timeoutMillis; - private final boolean useWriteTimeoutException; - - private TimeoutTask(Promise promise, long timeoutMillis, boolean useWriteTimeoutException) { - this.promise = promise; - this.timeoutMillis = timeoutMillis; - this.useWriteTimeoutException = useWriteTimeoutException; - } - - @Override - public void run() { - if (useWriteTimeoutException) { - promise.tryFailure(new WriteTimeoutException( - "write timed out after " + timeoutMillis + "ms")); - } else { - promise.tryFailure(new ResponseTimeoutException( - "did not receive a response within " + timeoutMillis + "ms")); - } - } - } - - private static InetSocketAddress convertToSocketAddress(URI uri, boolean useTls) { - int port = uri.getPort(); - if (port < 0) { - port = useTls ? 443 : 80; - } - return InetSocketAddress.createUnresolved(uri.getHost(), port); - } - - private static SessionProtocol validateSessionProtocol(SessionProtocol sessionProtocol) { - requireNonNull(sessionProtocol); - if (!HTTP_PROTOCOLS.contains(sessionProtocol)) { - throw new IllegalArgumentException( - "unsupported session protocol: " + sessionProtocol); - } - return sessionProtocol; - } - - @Override - public void close() { - map.values().forEach(KeyedChannelPool::close); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java b/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java deleted file mode 100644 index 19a2e1212b88..000000000000 --- a/src/main/java/com/linecorp/armeria/client/HttpSessionHandler.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import static java.util.Objects.requireNonNull; - -import java.net.InetSocketAddress; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Collections; -import java.util.Queue; -import java.util.concurrent.ScheduledFuture; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.common.SerializationFormat; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.util.Exceptions; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.channel.Channel; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.DecoderResult; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpStatusClass; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.collection.IntObjectHashMap; -import io.netty.util.collection.IntObjectMap; -import io.netty.util.concurrent.Promise; - -class HttpSessionHandler extends ChannelDuplexHandler implements HttpSession { - - private static final Logger logger = LoggerFactory.getLogger(HttpSessionHandler.class); - - /** - * 2^29 - We could have used 2^30 but this should be large enough. - */ - private static final int MAX_NUM_REQUESTS_SENT = 536870912; - - private static final String ARMERIA_USER_AGENT = "armeria client"; - - static HttpSession get(Channel ch) { - final HttpSessionHandler sessionHandler = ch.pipeline().get(HttpSessionHandler.class); - if (sessionHandler == null) { - return HttpSession.INACTIVE; - } - - return sessionHandler; - } - - private final HttpSessionChannelFactory channelFactory; - private final Promise sessionPromise; - private final ScheduledFuture timeoutFuture; - - /** Whether the current channel is active or not **/ - private volatile boolean active; - - /** The current negotiated {@link SessionProtocol} */ - private SessionProtocol protocol; - - /** - * A queue of pending requests. - * Set to {@link SequentialWaitsHolder} or {@link MultiplexWaitsHolder} later on protocol negotiation. - */ - private WaitsHolder waitsHolder = PreNegotiationWaitsHolder.INSTANCE; - - /** The number of requests sent. Disconnects when it reaches at {@link #MAX_NUM_REQUESTS_SENT}. */ - private int numRequestsSent; - - /** - * {@code true} if the protocol upgrade to HTTP/2 has failed. - * If set to {@code true}, another connection attempt will follow. - */ - private boolean needsRetryWithH1C; - - HttpSessionHandler(HttpSessionChannelFactory channelFactory, - Promise sessionPromise, ScheduledFuture timeoutFuture) { - - this.channelFactory = requireNonNull(channelFactory, "channelFactory"); - this.sessionPromise = requireNonNull(sessionPromise, "sessionPromise"); - this.timeoutFuture = requireNonNull(timeoutFuture, "timeoutFuture"); - } - - @Override - public SessionProtocol protocol() { - return protocol; - } - - @Override - public boolean isActive() { - return active; - } - - @Override - public boolean onRequestSent() { - return !isDisconnectionPending(++numRequestsSent); - } - - @Override - public void retryWithH1C() { - needsRetryWithH1C = true; - } - - private boolean isDisconnectionRequired(FullHttpResponse response) { - if (isDisconnectionPending(numRequestsSent) && waitsHolder.isEmpty()) { - return true; - } - - // HTTP/1 request with the 'Connection: close' header - return !protocol().isMultiplex() && - HttpHeaderValues.CLOSE.contentEqualsIgnoreCase( - response.headers().get(HttpHeaderNames.CONNECTION)); - } - - private static boolean isDisconnectionPending(int numRequestsSent) { - return numRequestsSent >= MAX_NUM_REQUESTS_SENT; - } - - @Override - public void deactivate() { - active = false; - failPendingResponses(ClosedSessionException.INSTANCE); - } - - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - active = ctx.channel().isActive(); - } - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - active = true; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof Http2Settings) { - // Expected - } else if (msg instanceof FullHttpResponse) { - FullHttpResponse response = (FullHttpResponse) msg; - - final Invocation invocation = waitsHolder.poll(response); - - if (invocation != null) { - final ServiceInvocationContext iCtx = invocation.invocationContext(); - final SerializationFormat serializationFormat = iCtx.scheme().serializationFormat(); - try { - final Promise resultPromise = invocation.resultPromise(); - if (HttpStatusClass.SUCCESS == response.status().codeClass() - // No serialization indicates a raw HTTP protocol which should - // have error responses returned. - || serializationFormat == SerializationFormat.NONE) { - iCtx.resolvePromise(resultPromise, response.retain()); - } else { - final DecoderResult decoderResult = response.decoderResult(); - final Throwable cause; - - if (decoderResult.isSuccess()) { - cause = new InvalidResponseException("HTTP Response code: " + response.status()); - } else { - final Throwable decoderCause = decoderResult.cause(); - if (decoderCause instanceof DecoderException) { - cause = decoderCause; - } else { - cause = new DecoderException("protocol violation: " + decoderCause, - decoderCause); - } - } - - iCtx.rejectPromise(resultPromise, cause); - } - } finally { - ReferenceCountUtil.release(msg); - } - } else { - logger.warn("{} Received a response without a matching request: {}", ctx.channel(), msg); - } - - if (isDisconnectionRequired(response)) { - ctx.close(); - } - } else { - try { - final String typeInfo; - if (msg instanceof ByteBuf) { - typeInfo = msg + " HexDump: " + ByteBufUtil.hexDump((ByteBuf) msg); - } else { - typeInfo = String.valueOf(msg); - } - throw new IllegalStateException("unexpected message type: " + typeInfo); - } finally { - ReferenceCountUtil.release(msg); - } - } - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof SessionProtocol) { - assert protocol == null; - assert waitsHolder instanceof PreNegotiationWaitsHolder; - - timeoutFuture.cancel(false); - - // Set the current protocol and its associated WaitsHolder implementation. - final SessionProtocol protocol = (SessionProtocol) evt; - this.protocol = protocol; - waitsHolder = protocol.isMultiplex() ? new MultiplexWaitsHolder() : new SequentialWaitsHolder(); - - if (!sessionPromise.trySuccess(ctx.channel())) { - // Session creation has been failed already; close the connection. - ctx.close(); - } - return; - } - - if (evt instanceof SessionProtocolNegotiationException) { - timeoutFuture.cancel(false); - sessionPromise.tryFailure((SessionProtocolNegotiationException) evt); - ctx.close(); - return; - } - - logger.warn("{} Unexpected user event: {}", ctx.channel(), evt); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - active = false; - - // Protocol upgrade has failed, but needs to retry. - if (needsRetryWithH1C) { - assert waitsHolder.isEmpty(); - timeoutFuture.cancel(false); - channelFactory.connect(ctx.channel().remoteAddress(), SessionProtocol.H1C, sessionPromise); - } else { - // Fail all pending responses. - failPendingResponses(ClosedSessionException.INSTANCE); - - // Cancel the timeout and reject the sessionPromise just in case the connection has been closed - // even before the session protocol negotiation is done. - timeoutFuture.cancel(false); - sessionPromise.tryFailure(ClosedSessionException.INSTANCE); - } - } - - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - Exceptions.logIfUnexpected(logger, ctx.channel(), protocol(), cause); - if (ctx.channel().isActive()) { - ctx.close(); - } - } - - private void failPendingResponses(Throwable e) { - final Collection invocations = waitsHolder.getAll(); - if (!invocations.isEmpty()) { - invocations.forEach(i -> i.invocationContext().rejectPromise(i.resultPromise(), e)); - waitsHolder.clear(); - } - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (msg instanceof Invocation) { - Invocation invocation = (Invocation) msg; - FullHttpRequest request = convertToHttpRequest(invocation); - waitsHolder.put(invocation, request); - ctx.write(request, promise); - } else { - ctx.write(msg, promise); - } - } - - private interface WaitsHolder { - Invocation poll(FullHttpResponse response); - - void put(Invocation invocation, FullHttpRequest request); - - Collection getAll(); - - void clear(); - - int size(); - - boolean isEmpty(); - } - - private static final class PreNegotiationWaitsHolder implements WaitsHolder { - - static final WaitsHolder INSTANCE = new PreNegotiationWaitsHolder(); - - @Override - public Invocation poll(FullHttpResponse response) { - return null; - } - - @Override - public void put(Invocation invocation, FullHttpRequest request) { - throw new IllegalStateException("protocol negotiation not complete"); - } - - @Override - public Collection getAll() { - return Collections.emptyList(); - } - - @Override - public void clear() {} - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return true; - } - } - - private static final class SequentialWaitsHolder implements WaitsHolder { - - private final Queue requestExpectQueue; - - SequentialWaitsHolder() { - requestExpectQueue = new ArrayDeque<>(); - } - - @Override - public Invocation poll(FullHttpResponse response) { - return requestExpectQueue.poll(); - } - - @Override - public void put(Invocation invocation, FullHttpRequest request) { - requestExpectQueue.add(invocation); - } - - @Override - public Collection getAll() { - return requestExpectQueue; - } - - @Override - public void clear() { - requestExpectQueue.clear(); - } - - @Override - public int size() { - return requestExpectQueue.size(); - } - - @Override - public boolean isEmpty() { - return requestExpectQueue.isEmpty(); - } - } - - private static final class MultiplexWaitsHolder implements WaitsHolder { - - private final IntObjectMap resultExpectMap; - private int streamId; - - MultiplexWaitsHolder() { - resultExpectMap = new IntObjectHashMap<>(); - streamId = 1; - } - - @Override - public Invocation poll(FullHttpResponse response) { - int streamID = response.headers().getInt(ExtensionHeaderNames.STREAM_ID.text(), 0); - return resultExpectMap.remove(streamID); - } - - @Override - public void put(Invocation invocation, FullHttpRequest request) { - int streamId = nextStreamID(); - request.headers().add(ExtensionHeaderNames.STREAM_ID.text(), streamIdToString(streamId)); - resultExpectMap.put(streamId, invocation); - } - - @Override - public Collection getAll() { - return resultExpectMap.values(); - } - - @Override - public void clear() { - resultExpectMap.clear(); - } - - @Override - public int size() { - return resultExpectMap.size(); - } - - @Override - public boolean isEmpty() { - return resultExpectMap.isEmpty(); - } - - private static String streamIdToString(int streamID) { - return Integer.toString(streamID); - } - - private int nextStreamID() { - return streamId += 2; - } - } - - private FullHttpRequest convertToHttpRequest(Invocation invocation) { - requireNonNull(invocation, "invocation"); - final ServiceInvocationContext ctx = invocation.invocationContext(); - final FullHttpRequest request; - final Object content = invocation.content(); - - if (content instanceof FullHttpRequest) { - request = (FullHttpRequest) content; - } else if (content instanceof ByteBuf) { - request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, ctx.path(), - (ByteBuf) content); - } else { - throw new IllegalStateException( - "content is not a ByteBuf or FullHttpRequest: " + content.getClass().getName()); - } - - HttpHeaders headers = request.headers(); - - headers.set(HttpHeaderNames.HOST, hostHeader(ctx)); - headers.set(ExtensionHeaderNames.SCHEME.text(), protocol.uriText()); - headers.set(HttpHeaderNames.USER_AGENT, ARMERIA_USER_AGENT); - headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); - - ByteBuf contentBuf = request.content(); - if (contentBuf != null && contentBuf.isReadable()) { - headers.set(HttpHeaderNames.CONTENT_LENGTH, contentBuf.readableBytes()); - } - - invocation.options().get(ClientOption.HTTP_HEADERS).ifPresent(headers::add); - if (ctx.scheme().serializationFormat() != SerializationFormat.NONE) { - //we allow a user can set content type and accept headers - String mimeType = ctx.scheme().serializationFormat().mimeType(); - if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { - headers.set(HttpHeaderNames.CONTENT_TYPE, mimeType); - } - if (!headers.contains(HttpHeaderNames.ACCEPT)) { - headers.set(HttpHeaderNames.ACCEPT, mimeType); - } - } - - return request; - } - - private static String hostHeader(ServiceInvocationContext ctx) { - final int port = ((InetSocketAddress) ctx.remoteAddress()).getPort(); - return HttpHostHeaderUtil.hostHeader(ctx.host(), port, - ctx.scheme().sessionProtocol().isTls()); - } - - static class Invocation { - private final ServiceInvocationContext invocationContext; - private final Promise resultPromise; - private final ClientOptions options; - private final Object content; - - Invocation(ServiceInvocationContext invocationContext, ClientOptions options, - Promise resultPromise, Object content) { - this.invocationContext = invocationContext; - this.resultPromise = resultPromise; - this.options = options; - this.content = content; - } - - ServiceInvocationContext invocationContext() { - return invocationContext; - } - - Promise resultPromise() { - return resultPromise; - } - - Object content() { - return content; - } - - ClientOptions options() { - return options; - } - } -} diff --git a/src/main/java/com/linecorp/armeria/client/RemoteInvokerFactory.java b/src/main/java/com/linecorp/armeria/client/NonDecoratingClientFactory.java similarity index 63% rename from src/main/java/com/linecorp/armeria/client/RemoteInvokerFactory.java rename to src/main/java/com/linecorp/armeria/client/NonDecoratingClientFactory.java index 458becdf0203..f3135ea12c09 100644 --- a/src/main/java/com/linecorp/armeria/client/RemoteInvokerFactory.java +++ b/src/main/java/com/linecorp/armeria/client/NonDecoratingClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -20,8 +20,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; -import java.util.Collections; -import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -29,11 +27,12 @@ import java.util.concurrent.ThreadFactory; import java.util.function.BiConsumer; import java.util.function.Function; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.util.NativeLibraries; import io.netty.bootstrap.Bootstrap; @@ -60,90 +59,43 @@ import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; -/** - * Creates and manages {@link RemoteInvoker}s. - * - *

Life cycle of the default {@link RemoteInvokerFactory}

- *

- * {@link Clients} or {@link ClientBuilder} uses {@link #DEFAULT}, the default {@link RemoteInvokerFactory}, - * unless you specified a {@link RemoteInvokerFactory} explicitly. Calling {@link #close()} on the default - * {@link RemoteInvokerFactory} won't terminate its I/O threads and release other related resources unlike - * other {@link RemoteInvokerFactory} to protect itself from accidental premature termination. - *

- * Instead, when the current {@link ClassLoader} is {@linkplain ClassLoader#getSystemClassLoader() the system - * class loader}, a {@link Runtime#addShutdownHook(Thread) shutdown hook} is registered so that they are - * released when the JVM exits. - *

- * If you are in an environment managed by a container or you desire the early termination of the default - * {@link RemoteInvokerFactory}, use {@link #closeDefault()}. - *

- */ -public final class RemoteInvokerFactory implements AutoCloseable { +public abstract class NonDecoratingClientFactory extends AbstractClientFactory { - private static final Logger logger = LoggerFactory.getLogger(RemoteInvokerFactory.class); + private static final Logger logger = LoggerFactory.getLogger(NonDecoratingClientFactory.class); private enum TransportType { NIO, EPOLL } - /** - * The default {@link RemoteInvokerFactory} implementation. - */ - public static final RemoteInvokerFactory DEFAULT = new RemoteInvokerFactory( - RemoteInvokerOptions.DEFAULT, - type -> { - switch (type) { - case NIO: - return new DefaultThreadFactory("default-armeria-client-nio", true); - case EPOLL: - return new DefaultThreadFactory("default-armeria-client-epoll", true); - default: - throw new Error(); - } - }); - - /** - * Closes the default {@link RemoteInvokerFactory}. - */ - public static void closeDefault() { - logger.debug("Closing the default {}", RemoteInvokerFactory.class.getSimpleName()); - DEFAULT.close0(); - } - - static { - if (RemoteInvokerFactory.class.getClassLoader() == ClassLoader.getSystemClassLoader()) { - Runtime.getRuntime().addShutdownHook(new Thread(RemoteInvokerFactory::closeDefault)); - } - } + private final EventLoopGroup eventLoopGroup; + private final boolean closeEventLoopGroup; + private final SessionOptions options; + private final Bootstrap baseBootstrap; - private static final ThreadFactory DEFAULT_THREAD_FACTORY_NIO = - new DefaultThreadFactory("armeria-client-nio", false); + // FIXME(trustin): Reuse idle connections instead of creating a new connection for every event loop. + private final Supplier eventLoopSupplier = + () -> RequestContext.mapCurrent(RequestContext::eventLoop, () -> eventLoopGroup().next()); - private static final ThreadFactory DEFAULT_THREAD_FACTORY_EPOLL = - new DefaultThreadFactory("armeria-client-epoll", false); - private final EventLoopGroup eventLoopGroup; - private final boolean closeEventLoopGroup; - private final Map remoteInvokers; + protected NonDecoratingClientFactory() { + this(SessionOptions.DEFAULT); + } - /** - * Creates a new instance with the specified {@link RemoteInvokerOptions}. - */ - public RemoteInvokerFactory(RemoteInvokerOptions options) { + protected NonDecoratingClientFactory(SessionOptions options) { this(options, type -> { switch (type) { case NIO: - return DEFAULT_THREAD_FACTORY_NIO; + return new DefaultThreadFactory("armeria-client-nio", false); case EPOLL: - return DEFAULT_THREAD_FACTORY_EPOLL; + return new DefaultThreadFactory("armeria-client-epoll", false); default: throw new Error(); } }); } - private RemoteInvokerFactory(RemoteInvokerOptions options, - Function threadFactoryFactory) { + private NonDecoratingClientFactory(SessionOptions options, + Function threadFactoryFactory) { requireNonNull(options, "options"); requireNonNull(threadFactoryFactory, "threadFactoryFactory"); @@ -169,13 +121,8 @@ private RemoteInvokerFactory(RemoteInvokerOptions options, closeEventLoopGroup = true; } - final EnumMap remoteInvokers = new EnumMap<>(SessionProtocol.class); - final HttpRemoteInvoker remoteInvoker = new HttpRemoteInvoker(baseBootstrap, options); - - SessionProtocol.ofHttp().stream().forEach( - protocol -> remoteInvokers.put(protocol, remoteInvoker)); - - this.remoteInvokers = Collections.unmodifiableMap(remoteInvokers); + this.baseBootstrap = baseBootstrap; + this.options = options; } private static Class channelType() { @@ -192,43 +139,27 @@ private static EventLoopGroup createGroup(Function new NioEventLoopGroup(0, threadFactoryFactory.apply(TransportType.NIO)); } - /** - * Returns the {@link EventLoopGroup} being used by this remote invoker factory. Can be used to, e.g., - * schedule a periodic task without creating a separate event loop. - */ - public EventLoopGroup eventLoopGroup() { + @Override + public final EventLoopGroup eventLoopGroup() { return eventLoopGroup; } - /** - * Returns a {@link RemoteInvoker} that can handle the specified {@link SessionProtocol}. - */ - public RemoteInvoker getInvoker(SessionProtocol sessionProtocol) { - final RemoteInvoker remoteInvoker = remoteInvokers.get(sessionProtocol); - if (remoteInvoker == null) { - throw new IllegalArgumentException("unsupported session protocol: " + sessionProtocol); - } - return remoteInvoker; + @Override + public final SessionOptions options() { + return options; } - /** - * Closes all {@link RemoteInvoker}s managed by this factory and shuts down the {@link EventLoopGroup} - * created implicitly by this factory. - */ - @Override - public void close() { - // The global default should never be closed. - if (this == DEFAULT) { - logger.debug("Refusing to close the default {}; must be closed via closeDefault()", - RemoteInvokerFactory.class.getSimpleName()); - return; - } + protected final Bootstrap baseBootstrap() { + return baseBootstrap; + } - close0(); + @Override + public final Supplier eventLoopSupplier() { + return eventLoopSupplier; } - private void close0() { - remoteInvokers.forEach((k, v) -> v.close()); + @Override + public void close() { if (closeEventLoopGroup) { eventLoopGroup.shutdownGracefully().syncUninterruptibly(); } diff --git a/src/main/java/com/linecorp/armeria/client/RemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/RemoteInvoker.java deleted file mode 100644 index 07c9ea41d257..000000000000 --- a/src/main/java/com/linecorp/armeria/client/RemoteInvoker.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import java.lang.reflect.Method; -import java.net.URI; - -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; - -/** - * Performs a remote invocation to a {@link URI}. - */ -public interface RemoteInvoker extends AutoCloseable { - - /** - * Performs a remote invocation to the specified {@link URI}. - * - * @param eventLoop the {@link EventLoop} to perform the invocation - * @param uri the {@link URI} of the server endpoint - * @param options the {@link ClientOptions} - * @param codec the {@link ClientCodec} - * @param method the original {@link Method} that triggered the remote invocation - * @param args the arguments of the remote invocation - * - * @return the {@link Future} that notifies the result of the remote invocation. - */ - Future invoke(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args) throws Exception; - - /** - * Closes the underlying socket connection and releases its associated resources. - */ - @Override - void close(); -} diff --git a/src/main/java/com/linecorp/armeria/client/ResponseTimeoutException.java b/src/main/java/com/linecorp/armeria/client/ResponseTimeoutException.java index 04559c190b47..b66eb3df7396 100644 --- a/src/main/java/com/linecorp/armeria/client/ResponseTimeoutException.java +++ b/src/main/java/com/linecorp/armeria/client/ResponseTimeoutException.java @@ -17,46 +17,24 @@ package com.linecorp.armeria.client; import com.linecorp.armeria.common.TimeoutException; +import com.linecorp.armeria.common.util.Exceptions; /** * A {@link TimeoutException} raised when a response has not been received from a server within timeout. */ -public class ResponseTimeoutException extends TimeoutException { +public final class ResponseTimeoutException extends TimeoutException { private static final long serialVersionUID = 2556616197251937869L; - /** - * Creates a new instance. - */ - public ResponseTimeoutException() {} + private static final ResponseTimeoutException INSTANCE = + Exceptions.clearTrace(new ResponseTimeoutException()); - /** - * Creates a new instance with the specified {@code message} and {@code cause}. - */ - public ResponseTimeoutException(String message, Throwable cause) { - super(message, cause); + public static ResponseTimeoutException get() { + return Exceptions.isVerbose() ? new ResponseTimeoutException() : INSTANCE; } /** - * Creates a new instance with the specified {@code message}. - */ - public ResponseTimeoutException(String message) { - super(message); - } - - /** - * Creates a new instance with the specified {@code cause}. - */ - public ResponseTimeoutException(Throwable cause) { - super(cause); - } - - /** - * Creates a new instance with the specified {@code message}, {@code cause}, suppression enabled or - * disabled, and writable stack trace enabled or disabled. + * Creates a new instance. */ - protected ResponseTimeoutException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } + private ResponseTimeoutException() {} } diff --git a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOption.java b/src/main/java/com/linecorp/armeria/client/SessionOption.java similarity index 60% rename from src/main/java/com/linecorp/armeria/client/RemoteInvokerOption.java rename to src/main/java/com/linecorp/armeria/client/SessionOption.java index 69229e85d199..28d3c15fed29 100644 --- a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOption.java +++ b/src/main/java/com/linecorp/armeria/client/SessionOption.java @@ -33,87 +33,81 @@ import io.netty.util.ConstantPool; /** - * A {@link RemoteInvoker} option. + * An option that affects the session management of a {@link ClientFactory}. */ -public class RemoteInvokerOption extends AbstractOption { +public class SessionOption extends AbstractOption { @SuppressWarnings("rawtypes") private static final ConstantPool pool = new ConstantPool() { @Override - protected RemoteInvokerOption newConstant(int id, String name) { - return new RemoteInvokerOption<>(id, name); + protected SessionOption newConstant(int id, String name) { + return new SessionOption<>(id, name); } }; /** * The timeout of a socket connection attempt. */ - public static final RemoteInvokerOption CONNECT_TIMEOUT = valueOf("CONNECT_TIMEOUT"); + public static final SessionOption CONNECT_TIMEOUT = valueOf("CONNECT_TIMEOUT"); /** * The idle timeout of a socket connection. The connection is closed if there is no invocation in progress * for this amount of time. */ - public static final RemoteInvokerOption IDLE_TIMEOUT = valueOf("IDLE_TIMEOUT"); - - /** - * The maximum allowed length of the frame (or the content) decoded at the session layer. e.g. the - * content of an HTTP request. - */ - public static final RemoteInvokerOption MAX_FRAME_LENGTH = valueOf("MAX_FRAME_LENGTH"); + public static final SessionOption IDLE_TIMEOUT = valueOf("IDLE_TIMEOUT"); /** * The maximum number of concurrent in-progress invocations. */ - public static final RemoteInvokerOption MAX_CONCURRENCY = valueOf("MAX_CONCURRENCY"); + public static final SessionOption MAX_CONCURRENCY = valueOf("MAX_CONCURRENCY"); /** * The {@link TrustManagerFactory} of a TLS connection. */ - public static final RemoteInvokerOption TRUST_MANAGER_FACTORY = + public static final SessionOption TRUST_MANAGER_FACTORY = valueOf("TRUST_MANAGER_FACTORY"); /** * The {@link AddressResolverGroup} to use to resolve remote addresses into {@link InetSocketAddress}es. */ - public static final RemoteInvokerOption> + public static final SessionOption> ADDRESS_RESOLVER_GROUP = valueOf("ADDRESS_RESOLVER_GROUP"); /** * The {@link EventLoopGroup} that will provide the {@link EventLoop} for I/O and asynchronous invocations. * If unspecified, a new one is created automatically. */ - public static final RemoteInvokerOption EVENT_LOOP_GROUP = valueOf("EVENT_LOOP_GROUP"); + public static final SessionOption EVENT_LOOP_GROUP = valueOf("EVENT_LOOP_GROUP"); /** * The {@link Function} that decorates the {@link KeyedChannelPoolHandler}. */ - public static final RemoteInvokerOption, - KeyedChannelPoolHandler>> POOL_HANDLER_DECORATOR = valueOf("POOL_HANDLER_DECORATOR"); + public static final SessionOption, + KeyedChannelPoolHandler>> POOL_HANDLER_DECORATOR = valueOf("POOL_HANDLER_DECORATOR"); /** * Whether to send an HTTP/2 preface string instead of an HTTP/1 upgrade request to negotiate the protocol * version of a cleartext HTTP connection. */ - public static final RemoteInvokerOption USE_HTTP2_PREFACE = valueOf("USE_HTTP2_PREFACE"); + public static final SessionOption USE_HTTP2_PREFACE = valueOf("USE_HTTP2_PREFACE"); /** - * Returns the {@link RemoteInvokerOption} of the specified name. + * Returns the {@link SessionOption} of the specified name. */ @SuppressWarnings("unchecked") - public static RemoteInvokerOption valueOf(String name) { - return (RemoteInvokerOption) pool.valueOf(name); + public static SessionOption valueOf(String name) { + return (SessionOption) pool.valueOf(name); } - private RemoteInvokerOption(int id, String name) { + private SessionOption(int id, String name) { super(id, name); } /** * Creates a new value of this option. */ - public RemoteInvokerOptionValue newValue(T value) { + public SessionOptionValue newValue(T value) { requireNonNull(value, "value"); - return new RemoteInvokerOptionValue<>(this, value); + return new SessionOptionValue<>(this, value); } } diff --git a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOptionValue.java b/src/main/java/com/linecorp/armeria/client/SessionOptionValue.java similarity index 78% rename from src/main/java/com/linecorp/armeria/client/RemoteInvokerOptionValue.java rename to src/main/java/com/linecorp/armeria/client/SessionOptionValue.java index 0093ab14ce38..b7385634a0de 100644 --- a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOptionValue.java +++ b/src/main/java/com/linecorp/armeria/client/SessionOptionValue.java @@ -18,11 +18,11 @@ import com.linecorp.armeria.common.util.AbstractOptionValue; /** - * A value of a {@link RemoteInvokerOption}. + * A value of a {@link SessionOption}. */ -public class RemoteInvokerOptionValue extends AbstractOptionValue, T> { +public class SessionOptionValue extends AbstractOptionValue, T> { - RemoteInvokerOptionValue(RemoteInvokerOption constant, T value) { + SessionOptionValue(SessionOption constant, T value) { super(constant, value); } } diff --git a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOptions.java b/src/main/java/com/linecorp/armeria/client/SessionOptions.java similarity index 59% rename from src/main/java/com/linecorp/armeria/client/RemoteInvokerOptions.java rename to src/main/java/com/linecorp/armeria/client/SessionOptions.java index 977ef5e86113..708a5215a69c 100644 --- a/src/main/java/com/linecorp/armeria/client/RemoteInvokerOptions.java +++ b/src/main/java/com/linecorp/armeria/client/SessionOptions.java @@ -15,15 +15,14 @@ */ package com.linecorp.armeria.client; -import static com.linecorp.armeria.client.RemoteInvokerOption.ADDRESS_RESOLVER_GROUP; -import static com.linecorp.armeria.client.RemoteInvokerOption.CONNECT_TIMEOUT; -import static com.linecorp.armeria.client.RemoteInvokerOption.EVENT_LOOP_GROUP; -import static com.linecorp.armeria.client.RemoteInvokerOption.IDLE_TIMEOUT; -import static com.linecorp.armeria.client.RemoteInvokerOption.MAX_CONCURRENCY; -import static com.linecorp.armeria.client.RemoteInvokerOption.MAX_FRAME_LENGTH; -import static com.linecorp.armeria.client.RemoteInvokerOption.POOL_HANDLER_DECORATOR; -import static com.linecorp.armeria.client.RemoteInvokerOption.TRUST_MANAGER_FACTORY; -import static com.linecorp.armeria.client.RemoteInvokerOption.USE_HTTP2_PREFACE; +import static com.linecorp.armeria.client.SessionOption.ADDRESS_RESOLVER_GROUP; +import static com.linecorp.armeria.client.SessionOption.CONNECT_TIMEOUT; +import static com.linecorp.armeria.client.SessionOption.EVENT_LOOP_GROUP; +import static com.linecorp.armeria.client.SessionOption.IDLE_TIMEOUT; +import static com.linecorp.armeria.client.SessionOption.MAX_CONCURRENCY; +import static com.linecorp.armeria.client.SessionOption.POOL_HANDLER_DECORATOR; +import static com.linecorp.armeria.client.SessionOption.TRUST_MANAGER_FACTORY; +import static com.linecorp.armeria.client.SessionOption.USE_HTTP2_PREFACE; import static java.util.Objects.requireNonNull; import java.net.InetSocketAddress; @@ -45,15 +44,14 @@ import io.netty.resolver.AddressResolverGroup; /** - * A set of {@link RemoteInvokerOption}s and their respective values. + * A set of {@link SessionOption}s and their respective values. */ -public class RemoteInvokerOptions extends AbstractOptions { +public class SessionOptions extends AbstractOptions { - private static final Logger logger = LoggerFactory.getLogger(RemoteInvokerOptions.class); + private static final Logger logger = LoggerFactory.getLogger(SessionOptions.class); private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofMillis(3200); private static final Duration DEFAULT_IDLE_TIMEOUT = Duration.ofSeconds(10); - private static final int DEFAULT_MAX_FRAME_LENGTH = 10 * 1024 * 1024; // 10 MB private static final Integer DEFAULT_MAX_CONCURRENCY = Integer.MAX_VALUE; private static final Boolean DEFAULT_USE_HTTP2_PREFACE = "true".equals(System.getProperty("com.linecorp.armeria.defaultUseHttp2Preface", "false")); @@ -62,43 +60,40 @@ public class RemoteInvokerOptions extends AbstractOptions { logger.info("defaultUseHttp2Preface: {}", DEFAULT_USE_HTTP2_PREFACE); } - private static final RemoteInvokerOptionValue[] DEFAULT_OPTION_VALUES = { + private static final SessionOptionValue[] DEFAULT_OPTION_VALUES = { CONNECT_TIMEOUT.newValue(DEFAULT_CONNECTION_TIMEOUT), IDLE_TIMEOUT.newValue(DEFAULT_IDLE_TIMEOUT), - MAX_FRAME_LENGTH.newValue(DEFAULT_MAX_FRAME_LENGTH), MAX_CONCURRENCY.newValue(DEFAULT_MAX_CONCURRENCY), USE_HTTP2_PREFACE.newValue(DEFAULT_USE_HTTP2_PREFACE) }; /** - * The default {@link RemoteInvokerOptions}. + * The default {@link SessionOptions}. */ - public static final RemoteInvokerOptions DEFAULT = new RemoteInvokerOptions(DEFAULT_OPTION_VALUES); + public static final SessionOptions DEFAULT = new SessionOptions(DEFAULT_OPTION_VALUES); /** - * Creates a new {@link RemoteInvokerOptions} with the specified {@link RemoteInvokerOptionValue}s. + * Creates a new {@link SessionOptions} with the specified {@link SessionOptionValue}s. */ - public static RemoteInvokerOptions of(RemoteInvokerOptionValue... options) { - return new RemoteInvokerOptions(DEFAULT, options); + public static SessionOptions of(SessionOptionValue... options) { + return new SessionOptions(DEFAULT, options); } /** - * Returns the {@link RemoteInvokerOptions} with the specified {@link RemoteInvokerOptionValue}s. + * Returns the {@link SessionOptions} with the specified {@link SessionOptionValue}s. */ - public static RemoteInvokerOptions of(Iterable> options) { - return new RemoteInvokerOptions(DEFAULT, options); + public static SessionOptions of(Iterable> options) { + return new SessionOptions(DEFAULT, options); } - private static RemoteInvokerOptionValue validateValue(RemoteInvokerOptionValue optionValue) { + private static SessionOptionValue validateValue(SessionOptionValue optionValue) { requireNonNull(optionValue, "value"); - RemoteInvokerOption option = optionValue.option(); + SessionOption option = optionValue.option(); T value = optionValue.value(); if (option == CONNECT_TIMEOUT) { validateConnectionTimeout((Duration) value); - } else if (option == MAX_FRAME_LENGTH) { - validateMaxFrameLength((Integer) value); } else if (option == IDLE_TIMEOUT) { validateIdleTimeout((Duration) value); } else if (option == MAX_CONCURRENCY) { @@ -108,13 +103,6 @@ private static RemoteInvokerOptionValue validateValue(RemoteInvokerOption return optionValue; } - private static int validateMaxFrameLength(int maxFrameLength) { - if (maxFrameLength <= 0) { - throw new IllegalArgumentException("maxFrameLength: " + maxFrameLength + " (expected: > 0)"); - } - return maxFrameLength; - } - private static Duration validateConnectionTimeout(Duration connectionTimeout) { requireNonNull(connectionTimeout, "connectionTimeout"); if (connectionTimeout.isNegative() || connectionTimeout.isZero()) { @@ -140,44 +128,44 @@ private static int validateMaxConcurrency(int maxConcurrency) { return maxConcurrency; } - private RemoteInvokerOptions(RemoteInvokerOptionValue... options) { - super(RemoteInvokerOptions::validateValue, options); + private SessionOptions(SessionOptionValue... options) { + super(SessionOptions::validateValue, options); } - private RemoteInvokerOptions(RemoteInvokerOptions baseOptions, RemoteInvokerOptionValue... options) { - super(RemoteInvokerOptions::validateValue, baseOptions, options); + private SessionOptions(SessionOptions baseOptions, SessionOptionValue... options) { + super(SessionOptions::validateValue, baseOptions, options); } - private RemoteInvokerOptions( - RemoteInvokerOptions baseOptions, Iterable> options) { - super(RemoteInvokerOptions::validateValue, baseOptions, options); + private SessionOptions( + SessionOptions baseOptions, Iterable> options) { + super(SessionOptions::validateValue, baseOptions, options); } /** - * Returns the value of the specified {@link RemoteInvokerOption}. + * Returns the value of the specified {@link SessionOption}. * - * @return the value of the {@link RemoteInvokerOption}, or - * {@link Optional#empty()} if the default value of the specified {@link RemoteInvokerOption} is + * @return the value of the {@link SessionOption}, or + * {@link Optional#empty()} if the default value of the specified {@link SessionOption} is * not available */ - public Optional get(RemoteInvokerOption option) { + public Optional get(SessionOption option) { return get0(option); } /** - * Returns the value of the specified {@link RemoteInvokerOption}. + * Returns the value of the specified {@link SessionOption}. * - * @return the value of the {@link RemoteInvokerOption}, or - * {@code defaultValue} if the specified {@link RemoteInvokerOption} is not set. + * @return the value of the {@link SessionOption}, or + * {@code defaultValue} if the specified {@link SessionOption} is not set. */ - public T getOrElse(RemoteInvokerOption option, T defaultValue) { + public T getOrElse(SessionOption option, T defaultValue) { return getOrElse0(option, defaultValue); } /** - * Converts this {@link RemoteInvokerOptions} to a {@link Map}. + * Converts this {@link SessionOptions} to a {@link Map}. */ - public Map, RemoteInvokerOptionValue> asMap() { + public Map, SessionOptionValue> asMap() { return asMap0(); } @@ -215,10 +203,6 @@ public long idleTimeoutMillis() { return idleTimeout().toMillis(); } - public int maxFrameLength() { - return getOrElse(MAX_FRAME_LENGTH, DEFAULT_MAX_FRAME_LENGTH); - } - public int maxConcurrency() { return getOrElse(MAX_CONCURRENCY, DEFAULT_MAX_CONCURRENCY); } diff --git a/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java b/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java index 56c25cc18977..6d92ba76ddea 100644 --- a/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java +++ b/src/main/java/com/linecorp/armeria/client/SessionProtocolNegotiationException.java @@ -38,7 +38,7 @@ public final class SessionProtocolNegotiationException extends RuntimeException * Creates a new instance with the specified expected {@link SessionProtocol}. */ public SessionProtocolNegotiationException(SessionProtocol expected, @Nullable String reason) { - super("expected: " + requireNonNull(expected, "expected")); + super("expected: " + requireNonNull(expected, "expected") + ", reason: " + reason); this.expected = expected; actual = null; } @@ -50,41 +50,11 @@ public SessionProtocolNegotiationException(SessionProtocol expected, @Nullable SessionProtocol actual, @Nullable String reason) { super("expected: " + requireNonNull(expected, "expected") + - ", actual: " + requireNonNull(actual, "actual")); + ", actual: " + requireNonNull(actual, "actual") + ", reason: " + reason); this.expected = expected; this.actual = actual; } - private static String message(SessionProtocol expected, SessionProtocol actual, String reason) { - requireNonNull(expected, "expected"); - - final StringBuilder buf; - - if (reason != null) { - buf = new StringBuilder(reason.length() + 32); - - buf.append(reason); - buf.append(" (expected: "); - buf.append(expected); - if (actual != null) { - buf.append(", actual: "); - buf.append(actual); - } - buf.append(')'); - } else { - buf = new StringBuilder(32); - - buf.append("expected: "); - buf.append(expected); - if (actual != null) { - buf.append(", actual: "); - buf.append(actual); - } - } - - return buf.toString(); - } - /** * Returns the expected {@link SessionProtocol}. */ diff --git a/src/main/java/com/linecorp/armeria/client/SimpleClient.java b/src/main/java/com/linecorp/armeria/client/SimpleClient.java deleted file mode 100644 index e5022c423d30..000000000000 --- a/src/main/java/com/linecorp/armeria/client/SimpleClient.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; - -import static java.util.Objects.requireNonNull; - -/** - * A simple {@link Client}. - */ -class SimpleClient implements Client { - - private final ClientCodec codec; - private final RemoteInvoker invoker; - - /** - * Creates a new instance with the specified {@link ClientCodec} and {@link RemoteInvoker}. - */ - SimpleClient(ClientCodec codec, RemoteInvoker invoker) { - this.codec = requireNonNull(codec, "codec"); - this.invoker = requireNonNull(invoker, "invoker"); - } - - @Override - public final ClientCodec codec() { - return codec; - } - - @Override - public final RemoteInvoker invoker() { - return invoker; - } - - @Override - public String toString() { - return "Client(" + codec().getClass().getSimpleName() + ", " + invoker().getClass().getSimpleName() + ')'; - } -} diff --git a/src/main/java/com/linecorp/armeria/client/UserClient.java b/src/main/java/com/linecorp/armeria/client/UserClient.java new file mode 100644 index 000000000000..3cf7a4af350a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/UserClient.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client; + +import java.util.function.Function; +import java.util.function.Supplier; + +import com.linecorp.armeria.common.SessionProtocol; + +import io.netty.channel.EventLoop; + +public abstract class UserClient implements ClientOptionDerivable { + + private final Client delegate; + private final Supplier eventLoopSupplier; + private final SessionProtocol sessionProtocol; + private final ClientOptions options; + private final Endpoint endpoint; + + protected UserClient(Client delegate, Supplier eventLoopSupplier, + SessionProtocol sessionProtocol, ClientOptions options, Endpoint endpoint) { + + this.delegate = delegate; + this.eventLoopSupplier = eventLoopSupplier; + this.sessionProtocol = sessionProtocol; + this.options = options; + this.endpoint = endpoint; + } + + protected final Client delegate() { + return delegate; + } + + protected final EventLoop eventLoop() { + return eventLoopSupplier.get(); + } + + protected final SessionProtocol sessionProtocol() { + return sessionProtocol; + } + + protected final ClientOptions options() { + return options; + } + + protected final Endpoint endpoint() { + return endpoint; + } + + protected final O execute(String method, String path, I req, Function fallback) { + return execute(eventLoop(), method, path, req, fallback); + } + + protected final O execute(EventLoop eventLoop, String method, String path, I req, Function fallback) { + try { + final ClientRequestContext ctx = new DefaultClientRequestContext( + eventLoop, sessionProtocol, endpoint, method, path, options, req); + return delegate().execute(ctx, req); + } catch (Throwable cause) { + return fallback.apply(cause); + } + } + + @Override + public final T withOptions(ClientOptionValue... additionalOptions) { + final ClientOptions options = ClientOptions.of(options(), additionalOptions); + return newInstance(delegate(), eventLoopSupplier, sessionProtocol(), options, endpoint()); + } + + @Override + public final T withOptions(Iterable> additionalOptions) { + final ClientOptions options = ClientOptions.of(options(), additionalOptions); + return newInstance(delegate(), eventLoopSupplier, sessionProtocol(), options, endpoint()); + } + + protected abstract T newInstance(Client delegate, Supplier eventLoopSupplier, + SessionProtocol sessionProtocol, ClientOptions options, Endpoint endpoint); +} diff --git a/src/main/java/com/linecorp/armeria/client/WriteTimeoutException.java b/src/main/java/com/linecorp/armeria/client/WriteTimeoutException.java index 6e7e287a973c..83b732b5fb45 100644 --- a/src/main/java/com/linecorp/armeria/client/WriteTimeoutException.java +++ b/src/main/java/com/linecorp/armeria/client/WriteTimeoutException.java @@ -17,46 +17,23 @@ package com.linecorp.armeria.client; import com.linecorp.armeria.common.TimeoutException; +import com.linecorp.armeria.common.util.Exceptions; /** * A {@link TimeoutException} raised when a client failed to send a request to the wire within timeout. */ -public class WriteTimeoutException extends TimeoutException { +public final class WriteTimeoutException extends TimeoutException { private static final long serialVersionUID = 2556616197251937869L; - /** - * Creates a new instance. - */ - public WriteTimeoutException() {} + private static final WriteTimeoutException INSTANCE = Exceptions.clearTrace(new WriteTimeoutException()); - /** - * Creates a new instance with the specified {@code message} and {@code cause}. - */ - public WriteTimeoutException(String message, Throwable cause) { - super(message, cause); + public static WriteTimeoutException get() { + return Exceptions.isVerbose() ? new WriteTimeoutException() : INSTANCE; } /** - * Creates a new instance with the specified {@code message}. - */ - public WriteTimeoutException(String message) { - super(message); - } - - /** - * Creates a new instance with the specified {@code cause}. - */ - public WriteTimeoutException(Throwable cause) { - super(cause); - } - - /** - * Creates a new instance with the specified {@code message}, {@code cause}, suppression enabled or - * disabled, and writable stack trace enabled or disabled. + * Creates a new instance. */ - protected WriteTimeoutException(String message, Throwable cause, boolean enableSuppression, - boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } + private WriteTimeoutException() {} } diff --git a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerClient.java b/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerClient.java index ba1c7c81c10b..022423c5ff35 100644 --- a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerClient.java +++ b/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerClient.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -16,16 +16,26 @@ package com.linecorp.armeria.client.circuitbreaker; +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.DecoratingClient; import com.linecorp.armeria.client.circuitbreaker.KeyedCircuitBreakerMapping.KeySelector; +import com.linecorp.armeria.common.Responses; /** * A {@link Client} decorator that handles failures of remote invocation based on circuit breaker pattern. */ -public final class CircuitBreakerClient extends DecoratingClient { +public final class CircuitBreakerClient extends DecoratingClient { + + private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerClient.class); /** * Creates a new decorator using the specified {@link CircuitBreaker} instance. @@ -35,8 +45,8 @@ public final class CircuitBreakerClient extends DecoratingClient { * * @param circuitBreaker The {@link CircuitBreaker} instance to be used */ - public static Function newDecorator(CircuitBreaker circuitBreaker) { - return newDecorator((eventLoop, uri, options, codec, method, args) -> circuitBreaker); + public static Function, Client> newDecorator(CircuitBreaker circuitBreaker) { + return newDecorator((ctx, req) -> circuitBreaker); } /** @@ -44,7 +54,7 @@ public static Function newDecorator(CircuitBreaker circuitBreake * * @param factory A function that takes a method name and creates a new {@link CircuitBreaker}. */ - public static Function newPerMethodDecorator(Function factory) { + public static Function, Client> newPerMethodDecorator(Function factory) { return newDecorator(new KeyedCircuitBreakerMapping<>(KeySelector.METHOD, factory)); } @@ -53,7 +63,7 @@ public static Function newPerMethodDecorator(Function newPerHostDecorator(Function factory) { + public static Function, Client> newPerHostDecorator(Function factory) { return newDecorator(new KeyedCircuitBreakerMapping<>(KeySelector.HOST, factory)); } @@ -62,7 +72,7 @@ public static Function newPerHostDecorator(Function newPerHostAndMethodDecorator( + public static Function, Client> newPerHostAndMethodDecorator( Function factory) { return newDecorator(new KeyedCircuitBreakerMapping<>(KeySelector.HOST_AND_METHOD, factory)); } @@ -70,12 +80,51 @@ public static Function newPerHostAndMethodDecorator( /** * Creates a new decorator with the specified {@link CircuitBreakerMapping}. */ - public static Function newDecorator(CircuitBreakerMapping mapping) { - return client -> new CircuitBreakerClient(client, mapping); + public static Function, Client> newDecorator(CircuitBreakerMapping mapping) { + return delegate -> new CircuitBreakerClient<>(delegate, mapping); } - CircuitBreakerClient(Client client, CircuitBreakerMapping mapping) { - super(client, Function.identity(), invoker -> new CircuitBreakerRemoteInvoker(invoker, mapping)); + private final CircuitBreakerMapping mapping; + + CircuitBreakerClient(Client delegate, CircuitBreakerMapping mapping) { + super(delegate); + this.mapping = requireNonNull(mapping, "mapping"); } + @Override + public O execute(ClientRequestContext ctx, I req) throws Exception { + + final CircuitBreaker circuitBreaker; + try { + circuitBreaker = mapping.get(ctx, req); + } catch (Throwable t) { + logger.warn("Failed to get a circuit breaker from mapping", t); + return delegate().execute(ctx, req); + } + + if (circuitBreaker.canRequest()) { + final O response; + try { + response = delegate().execute(ctx, req); + } catch (Throwable cause) { + circuitBreaker.onFailure(cause); + throw cause; + } + + final CompletableFuture future = Responses.awaitClose(response); + future.whenComplete((res, cause) -> { + // Report whether the invocation has succeeded or failed. + if (cause == null) { + circuitBreaker.onSuccess(); + } else { + circuitBreaker.onFailure(cause); + } + }); + + return response; + } else { + // the circuit is tripped; raise an exception without delegating. + throw new FailFastException(circuitBreaker); + } + } } diff --git a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMapping.java b/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMapping.java index e24153b6bb46..25a0af82074d 100644 --- a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMapping.java +++ b/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerMapping.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -16,23 +16,16 @@ package com.linecorp.armeria.client.circuitbreaker; -import java.lang.reflect.Method; -import java.net.URI; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.ClientOptions; - -import io.netty.channel.EventLoop; +import com.linecorp.armeria.client.ClientRequestContext; /** * Returns a {@link CircuitBreaker} instance from remote invocation parameters. */ @FunctionalInterface -public interface CircuitBreakerMapping { +public interface CircuitBreakerMapping { /** * Returns the {@link CircuitBreaker} mapped to the given parameters. */ - CircuitBreaker get(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, Method method, - Object[] args) throws Exception; + CircuitBreaker get(ClientRequestContext ctx, I req) throws Exception; } diff --git a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRemoteInvoker.java deleted file mode 100644 index 681e9e37e3d6..000000000000 --- a/src/main/java/com/linecorp/armeria/client/circuitbreaker/CircuitBreakerRemoteInvoker.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.circuitbreaker; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.net.URI; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.ClientOptions; -import com.linecorp.armeria.client.DecoratingRemoteInvoker; -import com.linecorp.armeria.client.RemoteInvoker; - -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.Promise; - -/** - * A {@link DecoratingRemoteInvoker} that deals with failures of remote invocation based on circuit breaker - * pattern. - */ -class CircuitBreakerRemoteInvoker extends DecoratingRemoteInvoker { - - private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerRemoteInvoker.class); - - private final CircuitBreakerMapping mapping; - - /** - * Creates a new instance that decorates the given {@link RemoteInvoker}. - */ - CircuitBreakerRemoteInvoker(RemoteInvoker delegate, CircuitBreakerMapping mapping) { - super(delegate); - this.mapping = requireNonNull(mapping, "mapping"); - } - - @Override - public Future invoke(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args) throws Exception { - - final CircuitBreaker circuitBreaker; - try { - circuitBreaker = mapping.get(eventLoop, uri, options, codec, method, args); - } catch (Throwable t) { - logger.warn("Failed to get a circuit breaker from mapping", t); - return delegate().invoke(eventLoop, uri, options, codec, method, args); - } - - if (circuitBreaker.canRequest()) { - final Future resultFut = delegate().invoke(eventLoop, uri, options, codec, method, args); - resultFut.addListener(future -> { - if (future.isSuccess()) { - // reports success event - circuitBreaker.onSuccess(); - } else { - circuitBreaker.onFailure(future.cause()); - } - }); - return resultFut; - } else { - // the circuit is tripped - - // prepares a failed resultPromise - final Promise resultPromise = eventLoop.newPromise(); - resultPromise.setFailure(new FailFastException(circuitBreaker)); - codec.prepareRequest(method, args, resultPromise); - - // returns immediately without calling succeeding remote invokers - return resultPromise; - } - } - -} diff --git a/src/main/java/com/linecorp/armeria/client/circuitbreaker/FailFastException.java b/src/main/java/com/linecorp/armeria/client/circuitbreaker/FailFastException.java index c9a52ecf7627..4cc06661b41a 100644 --- a/src/main/java/com/linecorp/armeria/client/circuitbreaker/FailFastException.java +++ b/src/main/java/com/linecorp/armeria/client/circuitbreaker/FailFastException.java @@ -5,7 +5,7 @@ * 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 + * 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 diff --git a/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java b/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java index bf22563e6d73..33b07b49fa51 100644 --- a/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java +++ b/src/main/java/com/linecorp/armeria/client/circuitbreaker/KeyedCircuitBreakerMapping.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -18,27 +18,23 @@ import static java.util.Objects.requireNonNull; -import java.lang.reflect.Method; -import java.net.URI; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.ClientOptions; - -import io.netty.channel.EventLoop; +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.RpcRequest; /** * A {@link CircuitBreakerMapping} that binds a {@link CircuitBreaker} to its key. {@link KeySelector} is used * to resolve the key from remote invocation parameters. If there is no circuit breaker bound to the key, * A new one is created by using the given circuit breaker factory. */ -public class KeyedCircuitBreakerMapping implements CircuitBreakerMapping { +public class KeyedCircuitBreakerMapping implements CircuitBreakerMapping { private final ConcurrentMap mapping = new ConcurrentHashMap<>(); - private final KeySelector keySelector; + private final KeySelector keySelector; private final Function factory; @@ -49,15 +45,14 @@ public class KeyedCircuitBreakerMapping implements CircuitBreakerMapping { * @param keySelector A function that returns the key of the given remote invocation parameters. * @param factory A function that takes a key and creates a new {@link CircuitBreaker} for the key. */ - public KeyedCircuitBreakerMapping(KeySelector keySelector, Function factory) { + public KeyedCircuitBreakerMapping(KeySelector keySelector, Function factory) { this.keySelector = requireNonNull(keySelector, "keySelector"); this.factory = requireNonNull(factory, "factory"); } @Override - public CircuitBreaker get(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args) throws Exception { - final K key = keySelector.get(eventLoop, uri, options, codec, method, args); + public CircuitBreaker get(ClientRequestContext ctx, I req) throws Exception { + final K key = keySelector.get(ctx, req); final CircuitBreaker circuitBreaker = mapping.get(key); if (circuitBreaker != null) { return circuitBreaker; @@ -69,31 +64,26 @@ public CircuitBreaker get(EventLoop eventLoop, URI uri, ClientOptions options, C * Returns the mapping key of the given remote invocation parameters. */ @FunctionalInterface - public interface KeySelector { + public interface KeySelector { /** * A {@link KeySelector} that returns remote method name as a key. */ - KeySelector METHOD = - (eventLoop, uri, options, codec, method, args) -> method.getName(); + KeySelector METHOD = + (ctx, req) -> req instanceof RpcRequest ? ((RpcRequest) req).method() : ctx.method(); /** * A {@link KeySelector} that returns a key consisted of remote host name and port number. */ - KeySelector HOST = - (eventLoop, uri, options, codec, method, args) -> - uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ':' + uri.getPort(); + KeySelector HOST = + (ctx, req) -> ctx.endpoint().authority(); /** * A {@link KeySelector} that returns a key consisted of remote host name, port number, and method name. */ - KeySelector HOST_AND_METHOD = - (eventLoop, uri, options, codec, method, args) -> - HOST.get(eventLoop, uri, options, codec, method, args) + '#' + method.getName(); - - K get(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, Method method, - Object[] args) throws Exception; + KeySelector HOST_AND_METHOD = + (ctx, req) -> HOST.get(ctx, req) + '#' + METHOD.get(ctx, req); + K get(ClientRequestContext ctx, I req) throws Exception; } - } diff --git a/src/main/java/com/linecorp/armeria/client/http/DecodedHttpResponse.java b/src/main/java/com/linecorp/armeria/client/http/DecodedHttpResponse.java new file mode 100644 index 000000000000..1be34ef347f0 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/DecodedHttpResponse.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import org.reactivestreams.Subscriber; + +import com.linecorp.armeria.common.http.DefaultHttpResponse; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpObject; +import com.linecorp.armeria.internal.Writability; + +import io.netty.channel.EventLoop; + +final class DecodedHttpResponse extends DefaultHttpResponse { + + private final EventLoop eventLoop; + private Writability writability; + private long writtenBytes; + + DecodedHttpResponse(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + void init(Writability writability) { + this.writability = writability; + } + + long writtenBytes() { + return writtenBytes; + } + + @Override + public void subscribe(Subscriber subscriber) { + subscribe(subscriber, eventLoop); + } + + @Override + public boolean write(HttpObject obj) { + final boolean published = super.write(obj); + if (published && obj instanceof HttpData) { + final int length = ((HttpData) obj).length(); + writability.inc(length); + writtenBytes += length; + } + return published; + } + + @Override + protected void onRemoval(HttpObject obj) { + if (obj instanceof HttpData) { + final int length = ((HttpData) obj).length(); + writability.dec(length); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/DefaultHttpClient.java b/src/main/java/com/linecorp/armeria/client/http/DefaultHttpClient.java new file mode 100644 index 000000000000..bd25c7bc0a01 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/DefaultHttpClient.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.util.function.Supplier; + +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientOptions; +import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.client.UserClient; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.DefaultHttpRequest; +import com.linecorp.armeria.common.http.DefaultHttpResponse; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; + +import io.netty.channel.EventLoop; + +final class DefaultHttpClient extends UserClient implements HttpClient { + + DefaultHttpClient(Client delegate, Supplier eventLoopSupplier, + SessionProtocol sessionProtocol, ClientOptions options, Endpoint endpoint) { + super(delegate, eventLoopSupplier, sessionProtocol, options, endpoint); + } + + // For SimpleHttpClient + EventLoop eventLoop0() { + return eventLoop(); + } + + @Override + public HttpResponse execute(HttpRequest req) { + return execute(eventLoop(), req); + } + + private HttpResponse execute(EventLoop eventLoop, HttpRequest req) { + return execute(eventLoop, req.method().name(), req.path(), req, cause -> { + final DefaultHttpResponse res = new DefaultHttpResponse(); + res.close(cause); + return res; + }); + } + + @Override + public HttpResponse execute(AggregatedHttpMessage aggregatedReq) { + return execute(eventLoop(), aggregatedReq); + } + + HttpResponse execute(EventLoop eventLoop, AggregatedHttpMessage aggregatedReq) { + final HttpHeaders headers = aggregatedReq.headers(); + final DefaultHttpRequest req = new DefaultHttpRequest(headers); + final HttpData content = aggregatedReq.content(); + + // Add content if not empty. + if (!content.isEmpty()) { + headers.setInt(HttpHeaderNames.CONTENT_LENGTH, content.length()); + req.write(content); + } + + // Add trailing headers if not empty. + final HttpHeaders trailingHeaders = aggregatedReq.trailingHeaders(); + if (!trailingHeaders.isEmpty()) { + req.write(trailingHeaders); + } + + req.close(); + return execute(eventLoop, req); + } + + @Override + protected HttpClient newInstance( + Client delegate, Supplier eventLoopSupplier, + SessionProtocol sessionProtocol, ClientOptions options, Endpoint endpoint) { + + return new DefaultHttpClient(delegate, eventLoopSupplier, sessionProtocol, options, endpoint); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/DefaultSimpleHttpClient.java b/src/main/java/com/linecorp/armeria/client/http/DefaultSimpleHttpClient.java new file mode 100644 index 000000000000..cdfb94845bff --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/DefaultSimpleHttpClient.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.util.Arrays; + +import com.linecorp.armeria.client.ClientOptionValue; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpMethod; +import com.linecorp.armeria.common.http.HttpResponse; +import com.linecorp.armeria.internal.http.ArmeriaHttpUtil; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +@SuppressWarnings("deprecation") +final class DefaultSimpleHttpClient implements SimpleHttpClient { + + private final DefaultHttpClient client; + + DefaultSimpleHttpClient(DefaultHttpClient client) { + this.client = client; + } + + @Override + public Future execute(SimpleHttpRequest sReq) { + final EventLoop eventLoop = client.eventLoop0(); + final Promise promise = eventLoop.newPromise(); + try { + final AggregatedHttpMessage aReq = AggregatedHttpMessage.of( + HttpMethod.valueOf(sReq.method().name()), + sReq.uri().getPath(), + HttpData.of(sReq.content())); + + // Convert the headers. + ArmeriaHttpUtil.toArmeria(sReq.headers(), aReq.headers()); + + final HttpResponse res = client.execute(eventLoop, aReq); + res.aggregate().whenComplete((aRes, cause) -> { + if (cause != null) { + promise.setFailure(cause); + } else { + try { + final HttpData aContent = aRes.content(); + final byte[] content; + if (aContent.offset() == 0 && aContent.length() == aContent.array().length) { + content = aContent.array(); + } else { + content = Arrays.copyOfRange(aContent.array(), aContent.offset(), + aContent.length()); + } + + final SimpleHttpResponse sRes = new SimpleHttpResponse( + HttpResponseStatus.valueOf(aRes.status().code()), + ArmeriaHttpUtil.toNettyHttp1(aRes.headers()), + content); + + promise.setSuccess(sRes); + } catch (Throwable t) { + promise.setFailure(t); + } + } + }); + } catch (Throwable t) { + promise.setFailure(t); + } + + return promise; + } + + @Override + public SimpleHttpClient withOptions(Iterable> additionalOptions) { + return new DefaultSimpleHttpClient((DefaultHttpClient) client.withOptions(additionalOptions)); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/Http1ResponseDecoder.java b/src/main/java/com/linecorp/armeria/client/http/Http1ResponseDecoder.java new file mode 100644 index 000000000000..b321d72057a2 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/Http1ResponseDecoder.java @@ -0,0 +1,222 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.common.ContentTooLargeException; +import com.linecorp.armeria.common.ProtocolViolationException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpResponseWriter; +import com.linecorp.armeria.internal.http.ArmeriaHttpUtil; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpStatusClass; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.ReferenceCountUtil; + +final class Http1ResponseDecoder extends HttpResponseDecoder implements ChannelInboundHandler { + + private static final Logger logger = LoggerFactory.getLogger(Http1ResponseDecoder.class); + + private enum State { + NEED_HEADERS, + NEED_INFORMATIONAL_DATA, + NEED_DATA_OR_TRAILING_HEADERS, + DISCARD + } + + /** The request being decoded currently. */ + private HttpResponseWrapper res; + private int resId = 1; + private State state = State.NEED_HEADERS; + + Http1ResponseDecoder(SessionProtocol sessionProtocol) { + super(sessionProtocol); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception {} + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {} + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelUnregistered(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelInactive(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof HttpObject)) { + ctx.fireChannelRead(msg); + return; + } + + try { + switch (state) { + case NEED_HEADERS: + if (msg instanceof HttpResponse) { + final HttpResponse nettyRes = (HttpResponse) msg; + final DecoderResult decoderResult = nettyRes.decoderResult(); + if (!decoderResult.isSuccess()) { + fail(ctx, new ProtocolViolationException(decoderResult.cause())); + return; + } + + if (!HttpUtil.isKeepAlive(nettyRes)) { + disconnectWhenFinished(); + } + + final HttpResponseWrapper res = getResponse(resId); + assert res != null; + this.res = res; + + if (nettyRes.status().codeClass() == HttpStatusClass.INFORMATIONAL) { + state = State.NEED_INFORMATIONAL_DATA; + } else { + state = State.NEED_DATA_OR_TRAILING_HEADERS; + res.scheduleTimeout(ctx); + } + + res.write(ArmeriaHttpUtil.toArmeria(nettyRes)); + } else { + failWithUnexpectedMessageType(ctx, msg); + } + break; + case NEED_INFORMATIONAL_DATA: + if (msg instanceof LastHttpContent) { + state = State.NEED_HEADERS; + } else { + failWithUnexpectedMessageType(ctx, msg); + } + break; + case NEED_DATA_OR_TRAILING_HEADERS: + if (msg instanceof HttpContent) { + final HttpContent content = (HttpContent) msg; + final DecoderResult decoderResult = content.decoderResult(); + if (!decoderResult.isSuccess()) { + fail(ctx, new ProtocolViolationException(decoderResult.cause())); + return; + } + + final ByteBuf data = content.content(); + final int dataLength = data.readableBytes(); + if (dataLength != 0) { + final long maxContentLength = res.maxContentLength(); + if (maxContentLength > 0 && res.writtenBytes() > maxContentLength - dataLength) { + fail(ctx, ContentTooLargeException.get()); + return; + } else { + ArmeriaHttpUtil.writeData(ctx, res, data, writability()); + } + } + + if (msg instanceof LastHttpContent) { + final HttpResponseWriter res = removeResponse(resId++); + assert this.res == res; + this.res = null; + + state = State.NEED_HEADERS; + + final HttpHeaders trailingHeaders = ((LastHttpContent) msg).trailingHeaders(); + if (!trailingHeaders.isEmpty()) { + res.write(ArmeriaHttpUtil.toArmeria(trailingHeaders)); + } + + res.close(); + + if (needsToDisconnect()) { + ctx.close(); + } + } + } else { + failWithUnexpectedMessageType(ctx, msg); + } + break; + case DISCARD: + break; + } + } finally { + ReferenceCountUtil.release(msg); + } + } + + private void failWithUnexpectedMessageType(ChannelHandlerContext ctx, Object msg) { + fail(ctx, new ProtocolViolationException( + "unexpected message type: " + msg.getClass().getName())); + } + + private void fail(ChannelHandlerContext ctx, Throwable cause) { + state = State.DISCARD; + + final HttpResponseWriter res = this.res; + this.res = null; + + if (res != null) { + res.close(cause); + } else { + logger.warn("Unexpected exception:", cause); + } + + ctx.close(); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelReadComplete(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + ctx.fireChannelWritabilityChanged(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + ctx.fireExceptionCaught(cause); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/Http2ClientConnectionHandler.java b/src/main/java/com/linecorp/armeria/client/http/Http2ClientConnectionHandler.java new file mode 100644 index 000000000000..4c8a7f86b2f9 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/Http2ClientConnectionHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import com.linecorp.armeria.internal.http.AbstractHttp2ConnectionHandler; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.Http2ConnectionDecoder; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2Settings; + +final class Http2ClientConnectionHandler extends AbstractHttp2ConnectionHandler { + + private final Http2ResponseDecoder responseDecoder; + + Http2ClientConnectionHandler( + Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, + Http2Settings initialSettings, Http2ResponseDecoder responseDecoder) { + + super(decoder, encoder, initialSettings); + this.responseDecoder = responseDecoder; + connection().addListener(responseDecoder); + decoder().frameListener(responseDecoder); + } + + Http2ResponseDecoder responseDecoder() { + return responseDecoder; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + + // NB: Http2ConnectionHandler does not flush the preface string automatically. + ctx.flush(); + } + + @Override + protected void onCloseRequest(ChannelHandlerContext ctx) throws Exception { + HttpSession.get(ctx.channel()).deactivate(); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/Http2ResponseDecoder.java b/src/main/java/com/linecorp/armeria/client/http/Http2ResponseDecoder.java new file mode 100644 index 000000000000..dcc45d13c90d --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/Http2ResponseDecoder.java @@ -0,0 +1,191 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; + +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.ContentTooLargeException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpResponseWriter; +import com.linecorp.armeria.common.http.HttpStatusClass; +import com.linecorp.armeria.internal.http.ArmeriaHttpUtil; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Flags; +import io.netty.handler.codec.http2.Http2FrameListener; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2Stream; + +final class Http2ResponseDecoder extends HttpResponseDecoder implements Http2Connection.Listener, + Http2FrameListener { + + Http2ResponseDecoder(SessionProtocol sessionProtocol) { + super(sessionProtocol); + } + + @Override + public void onStreamAdded(Http2Stream stream) {} + + @Override + public void onStreamActive(Http2Stream stream) {} + + @Override + public void onStreamHalfClosed(Http2Stream stream) {} + + @Override + public void onStreamClosed(Http2Stream stream) {} + + @Override + public void onStreamRemoved(Http2Stream stream) {} + + @Override + public void onPriorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {} + + @Override + public void onPriorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {} + + @Override + public void onWeightChanged(Http2Stream stream, short oldWeight) {} + + @Override + public void onGoAwaySent(int lastStreamId, long errorCode, ByteBuf debugData) {} + + @Override + public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) {} + + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) { + ctx.fireChannelRead(settings); + } + + @Override + public void onSettingsAckRead(ChannelHandlerContext ctx) {} + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, + boolean endOfStream) throws Http2Exception { + HttpResponseWrapper res = getResponse(id(streamId), endOfStream); + if (res == null) { + throw connectionError(PROTOCOL_ERROR, "received a HEADERS frame for an unknown stream: %d", + streamId); + } + + final HttpHeaders converted = ArmeriaHttpUtil.toArmeria(headers); + if (converted.status().codeClass() != HttpStatusClass.INFORMATIONAL) { + res.scheduleTimeout(ctx); + } + + try { + res.write(converted); + } catch (Throwable t) { + res.close(t); + throw connectionError(INTERNAL_ERROR, t, "failed to consume a HEADERS frame"); + } + + if (endOfStream) { + res.close(); + } + } + + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, + short weight, boolean exclusive, int padding, boolean endOfStream) + throws Http2Exception { + onHeadersRead(ctx, streamId, headers, padding, endOfStream); + } + + @Override + public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) + throws Http2Exception { + + final HttpResponseWrapper res = getResponse(id(streamId), endOfStream); + if (res == null) { + throw connectionError(PROTOCOL_ERROR, "received a DATA frame for an unknown stream: %d", + streamId); + } + + final int dataLength = data.readableBytes(); + final long maxContentLength = res.maxContentLength(); + if (maxContentLength > 0 && res.writtenBytes() > maxContentLength - dataLength) { + res.close(ContentTooLargeException.get()); + throw connectionError(INTERNAL_ERROR, + "content length too large: %d + %d > %d (stream: %d)", + res.writtenBytes(), dataLength, maxContentLength, streamId); + } + + try { + ArmeriaHttpUtil.writeData(ctx, res, data, writability()); + } catch (Throwable t) { + res.close(t); + throw connectionError(INTERNAL_ERROR, t, "failed to consume a DATA frame"); + } + + if (endOfStream) { + res.close(); + } + + // All bytes have been processed. + return dataLength + padding; + } + + @Override + public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { + final HttpResponseWriter res = removeResponse(id(streamId)); + if (res == null) { + throw connectionError(PROTOCOL_ERROR, + "received a RST_STREAM frame for an unknown stream: %d", streamId); + } + + res.close(ClosedSessionException.get()); + } + + @Override + public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, + Http2Headers headers, int padding) {} + + @Override + public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight, + boolean exclusive) {} + + @Override + public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) {} + + @Override + public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) {} + + @Override + public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) {} + + @Override + public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) {} + + @Override + public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, + ByteBuf payload) {} + + private static int id(int streamId) { + return streamId - 1 >>> 1; + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpClient.java b/src/main/java/com/linecorp/armeria/client/http/HttpClient.java new file mode 100644 index 000000000000..4b2a3de36589 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpClient.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.nio.charset.Charset; + +import com.linecorp.armeria.client.ClientOptionDerivable; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.DefaultHttpRequest; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpMethod; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; + +public interface HttpClient extends ClientOptionDerivable { + + HttpResponse execute(HttpRequest req); + + default HttpResponse execute(AggregatedHttpMessage aggregatedReq) { + final HttpHeaders headers = aggregatedReq.headers(); + final DefaultHttpRequest req = new DefaultHttpRequest(headers); + final HttpData content = aggregatedReq.content(); + + // Add content if not empty. + if (!content.isEmpty()) { + headers.setInt(HttpHeaderNames.CONTENT_LENGTH, content.length()); + req.write(content); + } + + // Add trailing headers if not empty. + final HttpHeaders trailingHeaders = aggregatedReq.trailingHeaders(); + if (!trailingHeaders.isEmpty()) { + req.write(trailingHeaders); + } + + req.close(); + return execute(req); + } + + default HttpResponse execute(HttpHeaders headers) { + return execute(AggregatedHttpMessage.of(headers)); + } + + default HttpResponse execute(HttpHeaders headers, HttpData content) { + return execute(AggregatedHttpMessage.of(headers, content)); + } + + default HttpResponse execute(HttpHeaders headers, byte[] content) { + return execute(AggregatedHttpMessage.of(headers, HttpData.of(content))); + } + + default HttpResponse execute(HttpHeaders headers, String content) { + return execute(AggregatedHttpMessage.of(headers, HttpData.ofUtf8(content))); + } + + default HttpResponse execute(HttpHeaders headers, String content, Charset charset) { + return execute(AggregatedHttpMessage.of(headers, HttpData.of(charset, content))); + } + + default HttpResponse options(String path) { + return execute(HttpHeaders.of(HttpMethod.OPTIONS, path)); + } + + default HttpResponse get(String path) { + return execute(HttpHeaders.of(HttpMethod.GET, path)); + } + + default HttpResponse head(String path) { + return execute(HttpHeaders.of(HttpMethod.HEAD, path)); + } + + default HttpResponse post(String path, HttpData content) { + return execute(HttpHeaders.of(HttpMethod.POST, path), content); + } + + default HttpResponse post(String path, byte[] content) { + return execute(HttpHeaders.of(HttpMethod.POST, path), content); + } + + default HttpResponse post(String path, String content) { + return execute(HttpHeaders.of(HttpMethod.POST, path), HttpData.ofUtf8(content)); + } + + default HttpResponse post(String path, String content, Charset charset) { + return execute(HttpHeaders.of(HttpMethod.POST, path), content, charset); + } + + default HttpResponse put(String path, HttpData content) { + return execute(HttpHeaders.of(HttpMethod.PUT, path), content); + } + + default HttpResponse put(String path, byte[] content) { + return execute(HttpHeaders.of(HttpMethod.PUT, path), content); + } + + default HttpResponse put(String path, String content) { + return execute(HttpHeaders.of(HttpMethod.PUT, path), HttpData.ofUtf8(content)); + } + + default HttpResponse put(String path, String content, Charset charset) { + return execute(HttpHeaders.of(HttpMethod.PUT, path), content, charset); + } + + default HttpResponse patch(String path, HttpData content) { + return execute(HttpHeaders.of(HttpMethod.PATCH, path), content); + } + + default HttpResponse patch(String path, byte[] content) { + return execute(HttpHeaders.of(HttpMethod.PATCH, path), content); + } + + default HttpResponse patch(String path, String content) { + return execute(HttpHeaders.of(HttpMethod.PATCH, path), HttpData.ofUtf8(content)); + } + + default HttpResponse patch(String path, String content, Charset charset) { + return execute(HttpHeaders.of(HttpMethod.PATCH, path), content, charset); + } + + default HttpResponse delete(String path) { + return execute(HttpHeaders.of(HttpMethod.DELETE, path)); + } + + default HttpResponse trace(String path) { + return execute(HttpHeaders.of(HttpMethod.TRACE, path)); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpClientDelegate.java b/src/main/java/com/linecorp/armeria/client/http/HttpClientDelegate.java new file mode 100644 index 000000000000..ae9308f3665c --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpClientDelegate.java @@ -0,0 +1,184 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import static java.util.Objects.requireNonNull; + +import java.net.InetSocketAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.client.SessionOptions; +import com.linecorp.armeria.client.pool.DefaultKeyedChannelPool; +import com.linecorp.armeria.client.pool.KeyedChannelPool; +import com.linecorp.armeria.client.pool.KeyedChannelPoolHandler; +import com.linecorp.armeria.client.pool.KeyedChannelPoolHandlerAdapter; +import com.linecorp.armeria.client.pool.PoolKey; +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.channel.pool.ChannelHealthChecker; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; + +final class HttpClientDelegate implements Client { + + private static final KeyedChannelPoolHandlerAdapter NOOP_POOL_HANDLER = + new KeyedChannelPoolHandlerAdapter<>(); + + private static final ChannelHealthChecker POOL_HEALTH_CHECKER = + ch -> ch.eventLoop().newSucceededFuture(HttpSession.get(ch).isActive()); + + final ConcurrentMap> map = new ConcurrentHashMap<>(); + + private final Bootstrap baseBootstrap; + private final SessionOptions options; + + HttpClientDelegate(Bootstrap baseBootstrap, SessionOptions options) { + this.baseBootstrap = requireNonNull(baseBootstrap, "baseBootstrap"); + this.options = requireNonNull(options, "options"); + + assert baseBootstrap.group() == null; + } + + @Override + public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception { + final Endpoint endpoint = ctx.endpoint().resolve(); + autoFillHeaders(ctx, endpoint, req); + + final PoolKey poolKey = new PoolKey( + InetSocketAddress.createUnresolved(endpoint.host(), endpoint.port()), + ctx.sessionProtocol()); + + final EventLoop eventLoop = ctx.eventLoop(); + final Future channelFuture = pool(eventLoop).acquire(poolKey); + final DecodedHttpResponse res = new DecodedHttpResponse(eventLoop); + + if (channelFuture.isDone()) { + if (channelFuture.isSuccess()) { + Channel ch = channelFuture.getNow(); + invoke0(ch, ctx, req, res, poolKey); + } else { + res.close(channelFuture.cause()); + } + } else { + channelFuture.addListener((Future future) -> { + if (future.isSuccess()) { + Channel ch = future.getNow(); + invoke0(ch, ctx, req, res, poolKey); + } else { + res.close(channelFuture.cause()); + } + }); + } + + return res; + } + + private static void autoFillHeaders(ClientRequestContext ctx, Endpoint endpoint, HttpRequest req) { + requireNonNull(req, "req"); + final HttpHeaders headers = req.headers(); + headers.set(HttpHeaderNames.USER_AGENT, HttpHeaderUtil.USER_AGENT.toString()); + + if (headers.authority() == null) { + final String hostname = endpoint.host(); + final int port = endpoint.port(); + + final String authority; + if (port == ctx.sessionProtocol().defaultPort()) { + authority = hostname; + } else { + final StringBuilder buf = new StringBuilder(hostname.length() + 6); + buf.append(hostname); + buf.append(':'); + buf.append(port); + authority = buf.toString(); + } + + headers.authority(authority); + } + + // Add the handlers specified in ClientOptions. + if (ctx.hasAttr(ClientRequestContext.HTTP_HEADERS)) { + headers.setAll(ctx.attr(ClientRequestContext.HTTP_HEADERS).get()); + } + } + + private KeyedChannelPool pool(EventLoop eventLoop) { + KeyedChannelPool pool = map.get(eventLoop); + if (pool != null) { + return pool; + } + + return map.computeIfAbsent(eventLoop, e -> { + final Bootstrap bootstrap = baseBootstrap.clone(); + bootstrap.group(eventLoop); + + Function> factory = new HttpSessionChannelFactory(bootstrap, options); + + final KeyedChannelPoolHandler handler = + options.poolHandlerDecorator().apply(NOOP_POOL_HANDLER); + + //TODO(inch772) handle options.maxConcurrency(); + final KeyedChannelPool newPool = new DefaultKeyedChannelPool<>( + eventLoop, factory, POOL_HEALTH_CHECKER, handler, true); + + eventLoop.terminationFuture().addListener((FutureListener) f -> { + map.remove(eventLoop); + newPool.close(); + }); + + return newPool; + }); + } + + void invoke0(Channel channel, ClientRequestContext ctx, + HttpRequest req, DecodedHttpResponse res, PoolKey poolKey) { + + final HttpSession session = HttpSession.get(channel); + res.init(session.writability()); + final SessionProtocol sessionProtocol = session.protocol(); + if (sessionProtocol == null) { + res.close(ClosedSessionException.get()); + return; + } + + if (session.invoke(ctx, req, res)) { + // Return the channel to the pool. + final KeyedChannelPool pool = KeyedChannelPool.findPool(channel); + if (sessionProtocol.isMultiplex()) { + pool.release(poolKey, channel); + } else { + req.awaitClose().whenComplete((ret, cause) -> pool.release(poolKey, channel)); + } + } + } + + void close() { + map.values().forEach(KeyedChannelPool::close); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpClientFactory.java b/src/main/java/com/linecorp/armeria/client/http/HttpClientFactory.java new file mode 100644 index 000000000000..e4f8eb4993a9 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpClientFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.net.URI; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientOptions; +import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.client.NonDecoratingClientFactory; +import com.linecorp.armeria.client.SessionOptions; +import com.linecorp.armeria.common.Scheme; +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; + +public class HttpClientFactory extends NonDecoratingClientFactory { + + private static final Set SUPPORTED_SCHEMES; + + static { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + for (SessionProtocol p : SessionProtocol.ofHttp()) { + builder.add(Scheme.of(SerializationFormat.NONE, p)); + } + SUPPORTED_SCHEMES = builder.build(); + } + + private final HttpClientDelegate delegate; + + public HttpClientFactory() { + this(SessionOptions.DEFAULT); + } + + public HttpClientFactory(SessionOptions options) { + super(options); + delegate = new HttpClientDelegate(baseBootstrap(), options); + } + + @Override + public Set supportedSchemes() { + return SUPPORTED_SCHEMES; + } + + @Override + public T newClient(URI uri, Class clientType, ClientOptions options) { + final Scheme scheme = validate(uri, clientType, options); + + validateClientType(clientType); + + final Client delegate = options.decoration().decorate( + HttpRequest.class, HttpResponse.class, new HttpClientDelegate(baseBootstrap(), options())); + + if (clientType == Client.class) { + @SuppressWarnings("unchecked") + final T castClient = (T) delegate; + return castClient; + } + + final Endpoint endpoint = newEndpoint(uri); + + if (clientType == HttpClient.class) { + final HttpClient client = new DefaultHttpClient( + delegate, eventLoopSupplier(), scheme.sessionProtocol(), options, endpoint); + + + @SuppressWarnings("unchecked") + T castClient = (T) client; + return castClient; + } else { + @SuppressWarnings("deprecation") + final SimpleHttpClient client = new DefaultSimpleHttpClient(new DefaultHttpClient( + delegate, eventLoopSupplier(), scheme.sessionProtocol(), options, endpoint)); + + @SuppressWarnings("unchecked") + T castClient = (T) client; + return castClient; + } + } + + @SuppressWarnings("deprecation") + private static void validateClientType(Class clientType) { + if (clientType != HttpClient.class && clientType != SimpleHttpClient.class && + clientType != Client.class) { + throw new IllegalArgumentException( + "clientType: " + clientType + + " (expected: " + HttpClient.class.getSimpleName() + ", " + + SimpleHttpClient.class.getSimpleName() + " or " + + Client.class.getSimpleName() + ')'); + } + } + + @Override + public void close() { + delegate.close(); + super.close(); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpClientIdleTimeoutHandler.java b/src/main/java/com/linecorp/armeria/client/http/HttpClientIdleTimeoutHandler.java new file mode 100644 index 000000000000..73317059b258 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpClientIdleTimeoutHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import com.linecorp.armeria.internal.IdleTimeoutHandler; + +import io.netty.channel.ChannelHandlerContext; + +final class HttpClientIdleTimeoutHandler extends IdleTimeoutHandler { + + HttpClientIdleTimeoutHandler(long idleTimeoutMillis) { + super("client", idleTimeoutMillis); + } + + @Override + protected boolean hasRequestsInProgress(ChannelHandlerContext ctx) { + return HttpSession.get(ctx.channel()).hasUnfinishedResponses(); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/HttpConfigurator.java b/src/main/java/com/linecorp/armeria/client/http/HttpClientPipelineConfigurator.java similarity index 77% rename from src/main/java/com/linecorp/armeria/client/HttpConfigurator.java rename to src/main/java/com/linecorp/armeria/client/http/HttpClientPipelineConfigurator.java index f4634f04025e..215d3615b815 100644 --- a/src/main/java/com/linecorp/armeria/client/HttpConfigurator.java +++ b/src/main/java/com/linecorp/armeria/client/http/HttpClientPipelineConfigurator.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -14,12 +14,13 @@ * under the License. */ -package com.linecorp.armeria.client; +package com.linecorp.armeria.client.http; import static com.linecorp.armeria.common.SessionProtocol.H1; import static com.linecorp.armeria.common.SessionProtocol.H1C; import static com.linecorp.armeria.common.SessionProtocol.H2; import static com.linecorp.armeria.common.SessionProtocol.H2C; +import static io.netty.handler.codec.http.HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED; import static java.util.Objects.requireNonNull; import java.net.InetSocketAddress; @@ -28,15 +29,22 @@ import javax.net.ssl.SSLException; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.linecorp.armeria.client.SessionOptions; +import com.linecorp.armeria.client.SessionProtocolNegotiationCache; +import com.linecorp.armeria.client.SessionProtocolNegotiationException; import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.http.AbstractHttpToHttp2ConnectionHandler; -import com.linecorp.armeria.common.http.Http1ClientCodec; -import com.linecorp.armeria.common.http.Http2GoAwayListener; +import com.linecorp.armeria.common.http.HttpObject; +import com.linecorp.armeria.common.logging.ResponseLogBuilder; import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.common.util.NativeLibraries; +import com.linecorp.armeria.internal.ReadSuppressingHandler; +import com.linecorp.armeria.internal.http.Http1ClientCodec; +import com.linecorp.armeria.internal.http.Http2GoAwayListener; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; @@ -55,7 +63,6 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; @@ -69,14 +76,10 @@ import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionDecoder; import io.netty.handler.codec.http2.Http2ConnectionEncoder; -import io.netty.handler.codec.http2.Http2ConnectionHandler; import io.netty.handler.codec.http2.Http2FrameReader; import io.netty.handler.codec.http2.Http2FrameWriter; import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter; -import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.SslContext; @@ -86,10 +89,16 @@ import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.util.AsciiString; +import io.netty.util.ReferenceCountUtil; -class HttpConfigurator extends ChannelDuplexHandler { +class HttpClientPipelineConfigurator extends ChannelDuplexHandler { - private static final Logger logger = LoggerFactory.getLogger(HttpConfigurator.class); + private static final Logger logger = LoggerFactory.getLogger(HttpClientPipelineConfigurator.class); + + /** + * The maximum allowed content length of an HTTP/1 to 2 upgrade response. + */ + private static final long UPGRADE_RESPONSE_MAX_LENGTH = 16384; private enum HttpPreference { HTTP1_REQUIRED, @@ -99,10 +108,10 @@ private enum HttpPreference { private final SslContext sslCtx; private final HttpPreference httpPreference; - private final RemoteInvokerOptions options; + private final SessionOptions options; private InetSocketAddress remoteAddress; - HttpConfigurator(SessionProtocol sessionProtocol, RemoteInvokerOptions options) { + HttpClientPipelineConfigurator(SessionProtocol sessionProtocol, SessionOptions options) { switch (sessionProtocol) { case HTTP: case HTTPS: @@ -163,6 +172,10 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock // Configure the pipeline. final Channel ch = ctx.channel(); + + final ChannelPipeline p = ch.pipeline(); + p.addLast(ReadSuppressingHandler.INSTANCE); + try { if (sslCtx != null) { configureAsHttps(ch); @@ -173,22 +186,22 @@ public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, Sock promise.tryFailure(t); ctx.close(); } finally { - final ChannelPipeline pipeline = ch.pipeline(); - if (pipeline.context(this) != null) { - pipeline.remove(this); + if (p.context(this) != null) { + p.remove(this); } } ctx.connect(remoteAddress, localAddress, promise); } - - // refer https://http2.github.io/http2-spec/#discover-https + /** + * @see HTTP/2 specification + */ private void configureAsHttps(Channel ch) { - ChannelPipeline pipeline = ch.pipeline(); - SslHandler sslHandler = sslCtx.newHandler(ch.alloc()); - pipeline.addLast(sslHandler); - pipeline.addLast(new ChannelInboundHandlerAdapter() { + final ChannelPipeline p = ch.pipeline(); + final SslHandler sslHandler = sslCtx.newHandler(ch.alloc()); + p.addLast(sslHandler); + p.addLast(new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (!(evt instanceof SslHandshakeCompletionEvent)) { @@ -209,7 +222,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc return; } - addBeforeSessionHandler(pipeline, newHttp2ConnectionHandler(ch)); + addBeforeSessionHandler(p, newHttp2ConnectionHandler(ch)); protocol = H2; } else { if (httpPreference != HttpPreference.HTTP1_REQUIRED) { @@ -221,16 +234,16 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc return; } - addBeforeSessionHandler(pipeline, newHttp1Codec()); + addBeforeSessionHandler(p, newHttp1Codec()); protocol = H1; } - finishSuccessfully(pipeline, protocol); - pipeline.remove(this); + finishSuccessfully(p, protocol); + p.remove(this); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - Exceptions.logIfUnexpected(logger, ctx.channel(), null, cause); + Exceptions.logIfUnexpected(logger, ctx.channel(), cause); ctx.close(); } }); @@ -257,21 +270,23 @@ private void configureAsHttp(Channel ch) { } if (attemptUpgrade) { + final Http2ClientConnectionHandler http2Handler = newHttp2ConnectionHandler(ch); if (options.useHttp2Preface()) { pipeline.addLast(new DowngradeHandler()); - pipeline.addLast(newHttp2ConnectionHandler(ch)); + pipeline.addLast(http2Handler); } else { Http1ClientCodec http1Codec = newHttp1Codec(); Http2ClientUpgradeCodec http2ClientUpgradeCodec = - new Http2ClientUpgradeCodec(newHttp2ConnectionHandler(ch)); + new Http2ClientUpgradeCodec(http2Handler); HttpClientUpgradeHandler http2UpgradeHandler = - new HttpClientUpgradeHandler(http1Codec, http2ClientUpgradeCodec, - options.maxFrameLength()); + new HttpClientUpgradeHandler( + http1Codec, http2ClientUpgradeCodec, + (int) Math.min(Integer.MAX_VALUE, UPGRADE_RESPONSE_MAX_LENGTH)); pipeline.addLast(http1Codec); pipeline.addLast(new WorkaroundHandler()); pipeline.addLast(http2UpgradeHandler); - pipeline.addLast(new UpgradeRequestHandler()); + pipeline.addLast(new UpgradeRequestHandler(http2Handler.responseDecoder())); } } else { pipeline.addLast(newHttp1Codec()); @@ -293,33 +308,13 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { // FIXME: Ensure unnecessary handlers are all removed from the pipeline for all protocol types. void finishSuccessfully(ChannelPipeline pipeline, SessionProtocol protocol) { - switch (protocol) { - case H1: - case H1C: - addBeforeSessionHandler(pipeline, new HttpObjectAggregator(options.maxFrameLength())); - break; - case H2: - case H2C: - // HTTP/2 does not require the aggregator because - // InboundHttp2ToHttpAdapter always creates a FullHttpRequest. - break; - default: - // Should never reach here. - throw new Error(); + if (protocol == H1 || protocol == H1C) { + addBeforeSessionHandler(pipeline, new Http1ResponseDecoder(protocol)); } final long idleTimeoutMillis = options.idleTimeoutMillis(); if (idleTimeoutMillis > 0) { - final HttpClientIdleTimeoutHandler timeoutHandler; - if (protocol == H2 || protocol == H2C) { - timeoutHandler = new Http2ClientIdleTimeoutHandler(idleTimeoutMillis); - } else { - // Note: We should not use Http2ClientIdleTimeoutHandler for HTTP/1 connections, - // because we cannot tell if the headers defined in ExtensionHeaderNames such as - // 'x-http2-stream-id' have been set by us or a malicious server. - timeoutHandler = new HttpClientIdleTimeoutHandler(idleTimeoutMillis); - } - addBeforeSessionHandler(pipeline, timeoutHandler); + pipeline.addFirst(new HttpClientIdleTimeoutHandler(idleTimeoutMillis)); } pipeline.channel().eventLoop().execute(() -> pipeline.fireUserEventTriggered(protocol)); @@ -327,8 +322,10 @@ void finishSuccessfully(ChannelPipeline pipeline, SessionProtocol protocol) { void addBeforeSessionHandler(ChannelPipeline pipeline, ChannelHandler handler) { // Get the name of the HttpSessionHandler so that we can put our handlers before it. - final String sessionHandlerName = pipeline.context(HttpSessionHandler.class).name(); - pipeline.addBefore(sessionHandlerName, null, handler); + final ChannelHandlerContext lastContext = pipeline.lastContext(); + assert lastContext.handler().getClass() == HttpSessionHandler.class; + + pipeline.addBefore(lastContext.name(), null, handler); } void finishWithNegotiationFailure( @@ -350,8 +347,12 @@ boolean isHttp2Protocol(SslHandler sslHandler) { */ private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter { + private final Http2ResponseDecoder responseDecoder; private UpgradeEvent upgradeEvt; - private boolean receivedUpgradeRes; + + UpgradeRequestHandler(Http2ResponseDecoder responseDecoder) { + this.responseDecoder = responseDecoder; + } /** * Sends the initial upgrade request, which is {@code "HEAD / HTTP/1.1"} @@ -364,12 +365,50 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { // Note: There's no need to fill Connection, Upgrade, and HTTP2-Settings headers here // because they are filled by Http2ClientUpgradeCodec. - final String host = HttpHostHeaderUtil.hostHeader( + final String host = HttpHeaderUtil.hostHeader( remoteAddress.getHostString(), remoteAddress.getPort(), sslCtx != null); upgradeReq.headers().set(HttpHeaderNames.HOST, host); + upgradeReq.headers().set(HttpHeaderNames.USER_AGENT, HttpHeaderUtil.USER_AGENT); ctx.writeAndFlush(upgradeReq); + + final Http2ResponseDecoder responseDecoder = this.responseDecoder; + final DecodedHttpResponse res = new DecodedHttpResponse(ctx.channel().eventLoop()); + + res.init(responseDecoder.writability()); + res.subscribe(new Subscriber() { + + private boolean notified; + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(HttpObject o) { + if (notified) { + // Discard the first response. + return; + } + + notified = true; + assert upgradeEvt == UpgradeEvent.UPGRADE_SUCCESSFUL; + onUpgradeResponse(ctx, true, false); + } + + @Override + public void onError(Throwable t) { + ctx.fireExceptionCaught(t); + } + + @Override + public void onComplete() {} + }); + + // NB: No need to set the response timeout because we have session creation timeout. + responseDecoder.addResponse(0, res, ResponseLogBuilder.NOOP, 0, UPGRADE_RESPONSE_MAX_LENGTH); ctx.fireChannelActive(); } @@ -392,39 +431,31 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc this.upgradeEvt = upgradeEvt; } - /** - * Waits until the upgrade response is received, and performs the final configuration of the pipeline - * based on the upgrade result. - */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (!receivedUpgradeRes && msg instanceof FullHttpResponse) { - final FullHttpResponse res = (FullHttpResponse) msg; - final HttpHeaders headers = res.headers(); - final String streamId = headers.get(ExtensionHeaderNames.STREAM_ID.text()); - if (streamId == null || "1".equals(streamId)) { - // Received the response for the upgrade request sent in channelActive(). - receivedUpgradeRes = true; - onUpgradeResponse(ctx, res); - return; - } + if (msg instanceof FullHttpResponse) { + // The server rejected the upgrade request and sent its response in HTTP/1. + ReferenceCountUtil.release(msg); + assert upgradeEvt == UPGRADE_REJECTED; + onUpgradeResponse( + ctx, false, + "close".equals(((FullHttpResponse) msg).headers().get(HttpHeaderNames.CONNECTION))); + return; } ctx.fireChannelRead(msg); } - private void onUpgradeResponse(ChannelHandlerContext ctx, FullHttpResponse upgradeRes) { + private void onUpgradeResponse(ChannelHandlerContext ctx, boolean success, boolean close) { final UpgradeEvent upgradeEvt = this.upgradeEvt; assert upgradeEvt != null : "received an upgrade response before an UpgradeEvent"; - // Not interested in the content. - upgradeRes.release(); final ChannelPipeline p = ctx.pipeline(); // Done with this handler, remove it from the pipeline. p.remove(this); - if ("close".equalsIgnoreCase(upgradeRes.headers().get(HttpHeaderNames.CONNECTION))) { + if (close) { // Server wants us to close the connection, which means we cannot use this connection // to send the request that contains the actual invocation. SessionProtocolNegotiationCache.setUnsupported(ctx.channel().remoteAddress(), H2C); @@ -439,11 +470,9 @@ private void onUpgradeResponse(ChannelHandlerContext ctx, FullHttpResponse upgra return; } - switch (upgradeEvt) { - case UPGRADE_SUCCESSFUL: + if (success) { finishSuccessfully(p, H2C); - break; - case UPGRADE_REJECTED: + } else { SessionProtocolNegotiationCache.setUnsupported(ctx.channel().remoteAddress(), H2C); if (httpPreference == HttpPreference.HTTP2_REQUIRED) { @@ -452,10 +481,6 @@ private void onUpgradeResponse(ChannelHandlerContext ctx, FullHttpResponse upgra } finishSuccessfully(p, H1C); - break; - default: - // Should never reach here. - throw new Error(); } } } @@ -518,17 +543,14 @@ protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List ou } static void retryWithH1C(ChannelHandlerContext ctx) { - HttpSessionHandler.get(ctx.channel()).retryWithH1C(); + HttpSession.get(ctx.channel()).retryWithH1C(); ctx.close(); } - private Http2ConnectionHandler newHttp2ConnectionHandler(Channel ch) { + private Http2ClientConnectionHandler newHttp2ConnectionHandler(Channel ch) { final boolean validateHeaders = false; final Http2Connection conn = new DefaultHttp2Connection(false); conn.addListener(new Http2GoAwayListener(ch)); - final InboundHttp2ToHttpAdapter listener = new InboundHttp2ToHttpAdapterBuilder(conn) - .propagateSettings(true).validateHttpHeaders(validateHeaders) - .maxContentLength(options.maxFrameLength()).build(); Http2FrameReader reader = new DefaultHttp2FrameReader(validateHeaders); Http2FrameWriter writer = new DefaultHttp2FrameWriter(); @@ -536,13 +558,14 @@ private Http2ConnectionHandler newHttp2ConnectionHandler(Channel ch) { Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(conn, writer); Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(conn, encoder, reader); - final HttpToHttp2ClientConnectionHandler handler = - new HttpToHttp2ClientConnectionHandler( - decoder, encoder, new Http2Settings(), validateHeaders); + final Http2ResponseDecoder listener = new Http2ResponseDecoder( + ch.pipeline().get(SslHandler.class) != null ? H2 : H2C); + + final Http2ClientConnectionHandler handler = + new Http2ClientConnectionHandler(decoder, encoder, new Http2Settings(), listener); // Setup post build options handler.gracefulShutdownTimeoutMillis(options.idleTimeoutMillis()); - handler.decoder().frameListener(listener); return handler; } @@ -551,34 +574,12 @@ private static Http1ClientCodec newHttp1Codec() { return new Http1ClientCodec() { @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { - HttpSessionHandler.get(ctx.channel()).deactivate(); + HttpSession.get(ctx.channel()).deactivate(); super.close(ctx, promise); } }; } - private static final class HttpToHttp2ClientConnectionHandler extends AbstractHttpToHttp2ConnectionHandler { - - HttpToHttp2ClientConnectionHandler( - Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, - Http2Settings initialSettings, boolean validateHeaders) { - super(decoder, encoder, initialSettings, validateHeaders); - } - - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - super.channelActive(ctx); - - // NB: Http2ConnectionHandler does not flush the preface string automatically. - ctx.flush(); - } - - @Override - protected void onCloseRequest(ChannelHandlerContext ctx) throws Exception { - HttpSessionHandler.get(ctx.channel()).deactivate(); - } - } - /** * Workaround handler for interoperability with Jetty. * - Jetty performs case-sensitive comparison for the Connection header value. (upgrade vs Upgrade) diff --git a/src/main/java/com/linecorp/armeria/client/HttpHostHeaderUtil.java b/src/main/java/com/linecorp/armeria/client/http/HttpHeaderUtil.java similarity index 78% rename from src/main/java/com/linecorp/armeria/client/HttpHostHeaderUtil.java rename to src/main/java/com/linecorp/armeria/client/http/HttpHeaderUtil.java index 9f286c09279e..417e6d337923 100644 --- a/src/main/java/com/linecorp/armeria/client/HttpHostHeaderUtil.java +++ b/src/main/java/com/linecorp/armeria/client/http/HttpHeaderUtil.java @@ -14,9 +14,14 @@ * under the License. */ -package com.linecorp.armeria.client; +package com.linecorp.armeria.client.http; -final class HttpHostHeaderUtil { +import io.netty.util.AsciiString; + +final class HttpHeaderUtil { + + // TODO(trustin): Add version information + static final AsciiString USER_AGENT = AsciiString.of("Armeria"); static String hostHeader(String host, int port, boolean useTls) { final int defaultPort = useTls ? 443 : 80; @@ -28,5 +33,5 @@ static String hostHeader(String host, int port, boolean useTls) { return new StringBuilder(host.length() + 6).append(host).append(':').append(port).toString(); } - private HttpHostHeaderUtil() {} + private HttpHeaderUtil() {} } diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpRequestSubscriber.java b/src/main/java/com/linecorp/armeria/client/http/HttpRequestSubscriber.java new file mode 100644 index 000000000000..b4eac46e4ec0 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpRequestSubscriber.java @@ -0,0 +1,271 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.net.InetSocketAddress; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.client.WriteTimeoutException; +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpObject; +import com.linecorp.armeria.common.http.HttpResponseWriter; +import com.linecorp.armeria.common.logging.RequestLogBuilder; +import com.linecorp.armeria.common.reactivestreams.ClosedPublisherException; +import com.linecorp.armeria.common.util.Exceptions; +import com.linecorp.armeria.internal.http.HttpObjectEncoder; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.http2.Http2Error; + +final class HttpRequestSubscriber implements Subscriber, ChannelFutureListener { + + private static final Logger logger = LoggerFactory.getLogger(HttpRequestSubscriber.class); + + enum State { + NEEDS_DATA_OR_TRAILING_HEADERS, + DONE + } + + private final ChannelHandlerContext ctx; + private final HttpObjectEncoder encoder; + private final int id; + private final HttpHeaders firstHeaders; + private final HttpResponseWriter response; + private final RequestLogBuilder logBuilder; + private final long timeoutMillis; + private Subscription subscription; + private ScheduledFuture timeoutFuture; + private State state = State.NEEDS_DATA_OR_TRAILING_HEADERS; + + HttpRequestSubscriber(Channel ch, HttpObjectEncoder encoder, + int id, HttpHeaders firstHeaders, HttpResponseWriter response, + RequestLogBuilder logBuilder, long timeoutMillis) { + + ctx = ch.pipeline().lastContext(); + + this.encoder = encoder; + this.id = id; + this.firstHeaders = firstHeaders; + this.response = response; + this.logBuilder = logBuilder; + this.timeoutMillis = timeoutMillis; + } + + /** + * Invoked on each write of an {@link HttpObject}. + */ + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + if (state != State.DONE) { + subscription.request(1); + } + return; + } + + fail(future.cause()); + + final Throwable cause = future.cause(); + if (!(cause instanceof ClosedPublisherException)) { + final Channel ch = future.channel(); + Exceptions.logIfUnexpected(logger, ch, HttpSession.get(ch).protocol(), cause); + ch.close(); + } + } + + @Override + public void onSubscribe(Subscription subscription) { + assert this.subscription == null; + this.subscription = subscription; + + final EventLoop eventLoop = ctx.channel().eventLoop(); + if (timeoutMillis > 0) { + timeoutFuture = eventLoop.schedule( + () -> failAndRespond(WriteTimeoutException.get()), + timeoutMillis, TimeUnit.MILLISECONDS); + } + + // NB: This must be invoked at the end of this method because otherwise the callback methods in this + // class can be called before the member fields (subscription and timeoutFuture) are initialized. + // It is because the successful write of the first headers will trigger subscription.request(1). + eventLoop.execute(this::writeFirstHeader); + } + + private void writeFirstHeader() { + final Channel ch = ctx.channel(); + final HttpHeaders firstHeaders = this.firstHeaders; + + String host = firstHeaders.authority(); + if (host == null) { + host = ((InetSocketAddress) ch.remoteAddress()).getHostString(); + } + + logBuilder.start(ch, HttpSession.get(ch).protocol(), + host, firstHeaders.method().name(), firstHeaders.path()); + logBuilder.attach(firstHeaders); + + encoder.writeHeaders(ctx, id, streamId(), firstHeaders, false).addListener(this); + ctx.flush(); + } + + @Override + public void onNext(HttpObject o) { + if (!(o instanceof HttpData) && !(o instanceof HttpHeaders)) { + throw newIllegalStateException("published an HttpObject that's neither Http2Headers nor Http2Data: " + o); + } + + boolean endOfStream = false; + switch (state) { + case NEEDS_DATA_OR_TRAILING_HEADERS: { + if (o instanceof HttpHeaders) { + final HttpHeaders trailingHeaders = (HttpHeaders) o; + if (trailingHeaders.status() != null) { + throw newIllegalStateException("published a trailing HttpHeaders with status: " + o); + } + endOfStream = true; + } + break; + } + case DONE: + return; + } + + write(o, endOfStream, true); + } + + @Override + public void onError(Throwable cause) { + failAndRespond(cause); + } + + @Override + public void onComplete() { + if (!cancelTimeout()) { + return; + } + + if (state != State.DONE) { + write(HttpData.EMPTY_DATA, true, true); + } + } + + private void write(HttpObject o, boolean endOfStream, boolean flush) { + if (state == State.DONE) { + return; + } + + final Channel ch = ctx.channel(); + if (!ch.isActive()) { + fail(ClosedSessionException.get()); + return; + } + + if (endOfStream) { + setDone(); + } + + ch.eventLoop().execute(() -> write0(o, endOfStream, flush)); + } + + private void write0(HttpObject o, boolean endOfStream, boolean flush) { + final ChannelFuture future; + if (o instanceof HttpData) { + final HttpData data = (HttpData) o; + future = encoder.writeData(ctx, id, streamId(), data, endOfStream); + logBuilder.increaseContentLength(data.length()); + } else if (o instanceof HttpHeaders) { + future = encoder.writeHeaders(ctx, id, streamId(), (HttpHeaders) o, endOfStream); + } else { + // Should never reach here because we did validation in onNext(). + throw new Error(); + } + + if (endOfStream) { + logBuilder.end(); + } + + future.addListener(this); + if (flush) { + ctx.flush(); + } + } + + private int streamId() { + return (id << 1) + 1; + } + + private void fail(Throwable cause) { + setDone(); + logBuilder.end(cause); + } + + private void setDone() { + cancelTimeout(); + state = State.DONE; + subscription.cancel(); + } + + private void failAndRespond(Throwable cause) { + fail(cause); + + final Channel ch = ctx.channel(); + final Http2Error error; + if (response.isOpen()) { + response.close(cause); + error = Http2Error.INTERNAL_ERROR; + } else if (cause instanceof WriteTimeoutException) { + error = Http2Error.CANCEL; + } else { + Exceptions.logIfUnexpected(logger, ch, + HttpSession.get(ch).protocol(), + "a request publisher raised an exception", cause); + error = Http2Error.INTERNAL_ERROR; + } + + if (ch.isActive()) { + encoder.writeReset(ctx, id, streamId(), error); + ctx.flush(); + } + } + + private boolean cancelTimeout() { + final ScheduledFuture timeoutFuture = this.timeoutFuture; + if (timeoutFuture == null) { + return true; + } + + return timeoutFuture.cancel(false); + } + + private IllegalStateException newIllegalStateException(String msg) { + final IllegalStateException cause = new IllegalStateException(msg); + fail(cause); + return cause; + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpResponseDecoder.java b/src/main/java/com/linecorp/armeria/client/http/HttpResponseDecoder.java new file mode 100644 index 000000000000..c977b42a39a1 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpResponseDecoder.java @@ -0,0 +1,248 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.net.MediaType; + +import com.linecorp.armeria.client.ResponseTimeoutException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpObject; +import com.linecorp.armeria.common.http.HttpResponseWriter; +import com.linecorp.armeria.common.http.HttpStatus; +import com.linecorp.armeria.common.http.HttpStatusClass; +import com.linecorp.armeria.common.logging.ResponseLogBuilder; +import com.linecorp.armeria.common.util.Exceptions; +import com.linecorp.armeria.internal.Writability; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; +import io.netty.util.concurrent.ScheduledFuture; + +abstract class HttpResponseDecoder { + + private static final Logger logger = LoggerFactory.getLogger(HttpResponseDecoder.class); + + private final SessionProtocol sessionProtocol; + private final IntObjectMap responses = new IntObjectHashMap<>(); + private final Writability writability = new Writability(); + private boolean disconnectWhenFinished; + + HttpResponseDecoder(SessionProtocol sessionProtocol) { + this.sessionProtocol = sessionProtocol; + } + + final SessionProtocol sessionProtocol() { + return sessionProtocol; + } + + final Writability writability() { + return writability; + } + + final void addResponse(int id, DecodedHttpResponse res, ResponseLogBuilder logBuilder, + long responseTimeoutMillis, long maxContentLength) { + + final HttpResponseWriter oldRes = responses.put( + id, new HttpResponseWrapper(res, logBuilder, responseTimeoutMillis, maxContentLength)); + + assert oldRes == null : + "addResponse(" + id + ", " + res + ", " + responseTimeoutMillis + "): " + oldRes; + } + + final HttpResponseWrapper getResponse(int id) { + return responses.get(id); + } + + final HttpResponseWrapper getResponse(int id, boolean remove) { + return remove ? removeResponse(id) : getResponse(id); + } + + final HttpResponseWrapper removeResponse(int id) { + return responses.remove(id); + } + + final boolean hasUnfinishedResponses() { + return !responses.isEmpty(); + } + + final void failUnfinishedResponses(Throwable cause) { + try { + for (HttpResponseWrapper res : responses.values()) { + res.close(cause); + } + } finally { + responses.clear(); + } + } + + final void disconnectWhenFinished() { + disconnectWhenFinished = true; + } + + final boolean needsToDisconnect() { + return disconnectWhenFinished && !hasUnfinishedResponses(); + } + + static final class HttpResponseWrapper implements HttpResponseWriter, Runnable { + private final DecodedHttpResponse delegate; + private final ResponseLogBuilder logBuilder; + private final long responseTimeoutMillis; + private final long maxContentLength; + private ScheduledFuture responseTimeoutFuture; + + HttpResponseWrapper(DecodedHttpResponse delegate, ResponseLogBuilder logBuilder, + long responseTimeoutMillis, long maxContentLength) { + + this.delegate = delegate; + this.logBuilder = logBuilder; + this.responseTimeoutMillis = responseTimeoutMillis; + this.maxContentLength = maxContentLength; + } + + void scheduleTimeout(ChannelHandlerContext ctx) { + if (responseTimeoutMillis <= 0) { + return; + } + + responseTimeoutFuture = ctx.channel().eventLoop().schedule( + this, responseTimeoutMillis, TimeUnit.MILLISECONDS); + } + + ResponseLogBuilder logBuilder() { + return logBuilder; + } + + long maxContentLength() { + return maxContentLength; + } + + long writtenBytes() { + return delegate.writtenBytes(); + } + + @Override + public void run() { + final ResponseTimeoutException cause = ResponseTimeoutException.get(); + delegate.close(cause); + logBuilder.end(cause); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public boolean write(HttpObject o) { + if (o instanceof HttpHeaders) { + // NB: It's safe to call logBuilder.start() multiple times. + // See AbstractMessageLog.start() for more information. + logBuilder.start(); + final HttpStatus status = ((HttpHeaders) o).status(); + if (status != null && status.codeClass() != HttpStatusClass.INFORMATIONAL) { + logBuilder.statusCode(status.code()); + logBuilder.attach(o); + } + } else if (o instanceof HttpData) { + logBuilder.increaseContentLength(((HttpData) o).length()); + } + return delegate.write(o); + } + + @Override + public boolean write(Supplier o) { + return delegate.write(o); + } + + @Override + public CompletableFuture awaitDemand() { + return delegate.awaitDemand(); + } + + @Override + public void close() { + if (cancelTimeout()) { + delegate.close(); + logBuilder.end(); + } + } + + @Override + public void close(Throwable cause) { + if (cancelTimeout()) { + delegate.close(cause); + logBuilder.end(cause); + } else { + if (!Exceptions.isExpected(cause)) { + logger.warn("Unexpected exception:", cause); + } + } + } + + private boolean cancelTimeout() { + final ScheduledFuture responseTimeoutFuture = this.responseTimeoutFuture; + if (responseTimeoutFuture == null) { + return true; + } + return responseTimeoutFuture.cancel(false); + } + + @Override + public void respond(HttpStatus status) { + delegate.respond(status); + } + + @Override + public void respond(HttpStatus status, + MediaType mediaType, String content) { + delegate.respond(status, mediaType, content); + } + + @Override + public void respond(HttpStatus status, + MediaType mediaType, String format, Object... args) { + delegate.respond(status, mediaType, format, args); + } + + @Override + public void respond(HttpStatus status, + MediaType mediaType, byte[] content) { + delegate.respond(status, mediaType, content); + } + + @Override + public void respond(HttpStatus status, + MediaType mediaType, byte[] content, int offset, int length) { + delegate.respond(status, mediaType, content, offset, length); + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpSession.java b/src/main/java/com/linecorp/armeria/client/http/HttpSession.java new file mode 100644 index 000000000000..686f5434715b --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpSession.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.internal.Writability; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; + +interface HttpSession { + + HttpSession INACTIVE = new HttpSession() { + + private final Writability writability = new Writability(0, 0); + + @Override + public SessionProtocol protocol() { + return null; + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public Writability writability() { + return writability; + } + + @Override + public boolean hasUnfinishedResponses() { + return false; + } + + @Override + public boolean invoke(ClientRequestContext ctx, HttpRequest req, DecodedHttpResponse res) { + res.close(ClosedSessionException.get()); + return false; + } + + @Override + public void retryWithH1C() { + throw new IllegalStateException(); + } + + @Override + public void deactivate() {} + }; + + static HttpSession get(Channel ch) { + final ChannelHandler lastHandler = ch.pipeline().last(); + if (lastHandler instanceof HttpSession) { + return (HttpSession) lastHandler; + } + + for (ChannelHandler h : ch.pipeline().toMap().values()) { + if (h instanceof HttpSession) { + return (HttpSession) h; + } + } + + return INACTIVE; + } + + SessionProtocol protocol(); + boolean isActive(); + Writability writability(); + boolean hasUnfinishedResponses(); + boolean invoke(ClientRequestContext ctx, HttpRequest req, DecodedHttpResponse res); + void retryWithH1C(); + void deactivate(); +} diff --git a/src/main/java/com/linecorp/armeria/client/HttpSessionChannelFactory.java b/src/main/java/com/linecorp/armeria/client/http/HttpSessionChannelFactory.java similarity index 90% rename from src/main/java/com/linecorp/armeria/client/HttpSessionChannelFactory.java rename to src/main/java/com/linecorp/armeria/client/http/HttpSessionChannelFactory.java index 9fedffa26689..6e6210f62b9d 100644 --- a/src/main/java/com/linecorp/armeria/client/HttpSessionChannelFactory.java +++ b/src/main/java/com/linecorp/armeria/client/http/HttpSessionChannelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -14,7 +14,7 @@ * under the License. */ -package com.linecorp.armeria.client; +package com.linecorp.armeria.client.http; import static java.util.Objects.requireNonNull; @@ -27,6 +27,9 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; +import com.linecorp.armeria.client.SessionOptions; +import com.linecorp.armeria.client.SessionProtocolNegotiationCache; +import com.linecorp.armeria.client.SessionProtocolNegotiationException; import com.linecorp.armeria.client.pool.PoolKey; import com.linecorp.armeria.common.SessionProtocol; @@ -35,7 +38,6 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoop; -import io.netty.channel.pool.ChannelHealthChecker; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import io.netty.util.internal.OneTimeTask; @@ -45,9 +47,9 @@ class HttpSessionChannelFactory implements Function> { private final Bootstrap baseBootstrap; private final EventLoop eventLoop; private final Map bootstrapMap; - private final RemoteInvokerOptions options; + private final SessionOptions options; - HttpSessionChannelFactory(Bootstrap bootstrap, RemoteInvokerOptions options) { + HttpSessionChannelFactory(Bootstrap bootstrap,SessionOptions options) { baseBootstrap = requireNonNull(bootstrap); eventLoop = (EventLoop) bootstrap.group(); @@ -92,7 +94,7 @@ private Bootstrap bootstrap(SessionProtocol sessionProtocol) { bs.handler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { - ch.pipeline().addLast(new HttpConfigurator(sp, options)); + ch.pipeline().addLast(new HttpClientPipelineConfigurator(sp, options)); } }); return bs; @@ -142,6 +144,6 @@ public void run() { } }, options.connectTimeoutMillis(), TimeUnit.MILLISECONDS); - ch.pipeline().addLast(new HttpSessionHandler(this, sessionPromise, timeoutFuture)); + ch.pipeline().addLast(new HttpSessionHandler(this, ch, sessionPromise, timeoutFuture)); } } diff --git a/src/main/java/com/linecorp/armeria/client/http/HttpSessionHandler.java b/src/main/java/com/linecorp/armeria/client/http/HttpSessionHandler.java new file mode 100644 index 000000000000..3add3b4700c6 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/http/HttpSessionHandler.java @@ -0,0 +1,254 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.SessionProtocolNegotiationException; +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.util.Exceptions; +import com.linecorp.armeria.internal.Writability; +import com.linecorp.armeria.internal.http.Http1ObjectEncoder; +import com.linecorp.armeria.internal.http.Http2ObjectEncoder; +import com.linecorp.armeria.internal.http.HttpObjectEncoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; + +final class HttpSessionHandler extends ChannelDuplexHandler implements HttpSession { + + private static final Logger logger = LoggerFactory.getLogger(HttpSessionHandler.class); + + /** + * 2^29 - We could have used 2^30 but this should be large enough. + */ + private static final int MAX_NUM_REQUESTS_SENT = 536870912; + + private final HttpSessionChannelFactory channelFactory; + private final Channel channel; + private final Promise sessionPromise; + private final ScheduledFuture sessionTimeoutFuture; + + /** Whether the current channel is active or not **/ + private volatile boolean active; + + /** The current negotiated {@link SessionProtocol} */ + private SessionProtocol protocol; + + private HttpResponseDecoder responseDecoder; + private HttpObjectEncoder requestEncoder; + + /** The number of requests sent. Disconnects when it reaches at {@link #MAX_NUM_REQUESTS_SENT}. */ + private int numRequestsSent; + + /** + * {@code true} if the protocol upgrade to HTTP/2 has failed. + * If set to {@code true}, another connection attempt will follow. + */ + private boolean needsRetryWithH1C; + + HttpSessionHandler(HttpSessionChannelFactory channelFactory, Channel channel, + Promise sessionPromise, ScheduledFuture sessionTimeoutFuture) { + + this.channelFactory = requireNonNull(channelFactory, "channelFactory"); + this.channel = requireNonNull(channel, "channel"); + this.sessionPromise = requireNonNull(sessionPromise, "sessionPromise"); + this.sessionTimeoutFuture = requireNonNull(sessionTimeoutFuture, "sessionTimeoutFuture"); + } + + @Override + public SessionProtocol protocol() { + return protocol; + } + + @Override + public Writability writability() { + return responseDecoder.writability(); + } + + @Override + public boolean hasUnfinishedResponses() { + return responseDecoder.hasUnfinishedResponses(); + } + + @Override + public boolean isActive() { + return active; + } + + @Override + public boolean invoke(ClientRequestContext ctx, HttpRequest req, DecodedHttpResponse res) { + final SessionProtocol sessionProtocol = protocol(); + if (sessionProtocol == null) { + res.close(ClosedSessionException.get()); + return false; + } + + final long writeTimeoutMillis = ctx.writeTimeoutMillis(); + final long responseTimeoutMillis = ctx.responseTimeoutMillis(); + final long maxContentLength = ctx.maxResponseLength(); + + final int numRequestsSent = ++this.numRequestsSent; + responseDecoder.addResponse(numRequestsSent, res, ctx.responseLogBuilder(), + responseTimeoutMillis, maxContentLength); + req.subscribe( + new HttpRequestSubscriber(channel, requestEncoder, + numRequestsSent, req.headers(), res, ctx.requestLogBuilder(), + writeTimeoutMillis), + channel.eventLoop()); + + if (numRequestsSent >= MAX_NUM_REQUESTS_SENT) { + responseDecoder.disconnectWhenFinished(); + return false; + } else { + return true; + } + } + + @Override + public void retryWithH1C() { + needsRetryWithH1C = true; + } + + @Override + public void deactivate() { + active = false; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + active = ctx.channel().isActive(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + active = true; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof Http2Settings) { + // Expected + } else { + try { + final String typeInfo; + if (msg instanceof ByteBuf) { + typeInfo = msg + " HexDump: " + ByteBufUtil.hexDump((ByteBuf) msg); + } else { + typeInfo = String.valueOf(msg); + } + throw new IllegalStateException("unexpected message type: " + typeInfo); + } finally { + ReferenceCountUtil.release(msg); + } + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SessionProtocol) { + assert protocol == null; + assert responseDecoder == null; + + sessionTimeoutFuture.cancel(false); + + // Set the current protocol and its associated WaitsHolder implementation. + final SessionProtocol protocol = (SessionProtocol) evt; + this.protocol = protocol; + switch (protocol) { + case H1: case H1C: + requestEncoder = new Http1ObjectEncoder(false); + responseDecoder = ctx.pipeline().get(Http1ResponseDecoder.class); + break; + case H2: case H2C: + final Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); + requestEncoder = new Http2ObjectEncoder(handler.encoder()); + responseDecoder = ctx.pipeline().get(Http2ClientConnectionHandler.class).responseDecoder(); + break; + default: + throw new Error(); // Should never reach here. + } + + if (!sessionPromise.trySuccess(ctx.channel())) { + // Session creation has been failed already; close the connection. + ctx.close(); + } + return; + } + + if (evt instanceof SessionProtocolNegotiationException) { + sessionTimeoutFuture.cancel(false); + sessionPromise.tryFailure((SessionProtocolNegotiationException) evt); + ctx.close(); + return; + } + + logger.warn("{} Unexpected user event: {}", ctx.channel(), evt); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + active = false; + + // Protocol upgrade has failed, but needs to retry. + if (needsRetryWithH1C) { + assert responseDecoder == null || !responseDecoder.hasUnfinishedResponses(); + sessionTimeoutFuture.cancel(false); + channelFactory.connect(ctx.channel().remoteAddress(), SessionProtocol.H1C, sessionPromise); + } else { + // Fail all pending responses. + failUnfinishedResponses(ClosedSessionException.get()); + + // Cancel the timeout and reject the sessionPromise just in case the connection has been closed + // even before the session protocol negotiation is done. + sessionTimeoutFuture.cancel(false); + sessionPromise.tryFailure(ClosedSessionException.get()); + } + } + + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + Exceptions.logIfUnexpected(logger, ctx.channel(), protocol(), cause); + if (ctx.channel().isActive()) { + ctx.close(); + } + } + + private void failUnfinishedResponses(Throwable e) { + final HttpResponseDecoder responseDecoder = this.responseDecoder; + if (responseDecoder == null) { + return; + } + + responseDecoder.failUnfinishedResponses(e); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClient.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClient.java index 7f3437a4e8a3..ecb545fa11bc 100644 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClient.java +++ b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -16,15 +16,18 @@ package com.linecorp.armeria.client.http; +import com.linecorp.armeria.client.ClientOptionDerivable; + import io.netty.util.concurrent.Future; /** * A simple HTTP client that can send a {@link SimpleHttpRequest} to an HTTP/1 or 2 server. * + * @deprecated Use {@link HttpClient} instead. * @see SimpleHttpRequestBuilder */ -@FunctionalInterface -public interface SimpleHttpClient { +@Deprecated +public interface SimpleHttpClient extends ClientOptionDerivable { /** * Sends the specified {@code request} to the HTTP server asynchronously. * diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClientCodec.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClientCodec.java deleted file mode 100644 index 840c77e27f36..000000000000 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpClientCodec.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; - -import java.lang.reflect.Method; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.SerializationFormat; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.channel.Channel; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.util.concurrent.Promise; - -/** - * A HTTP {@link ClientCodec} codec for {@link SimpleHttpRequest} and {@link SimpleHttpResponse}. - */ -public class SimpleHttpClientCodec implements ClientCodec { - - private static final byte[] EMPTY = new byte[0]; - - private final String host; - - /** - * Creates a new codec with the specified {@link Scheme} and {@code host}. - */ - public SimpleHttpClientCodec(String host) { - this.host = host; - } - - @Override - public void prepareRequest(Method method, Object[] args, Promise resultPromise) { - // Nothing to do. - } - - @Override - public EncodeResult encodeRequest( - Channel channel, SessionProtocol sessionProtocol, Method method, Object[] args) { - @SuppressWarnings("unchecked") // Guaranteed by SimpleHttpClient interface. - SimpleHttpRequest request = (SimpleHttpRequest) args[0]; - FullHttpRequest fullHttpRequest = convertToFullHttpRequest(request, channel); - Scheme scheme = Scheme.of(SerializationFormat.NONE, sessionProtocol); - return new SimpleHttpInvocation(channel, scheme, host, request.uri().getPath(), - fullHttpRequest, request); - } - - @Override - public T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse) - throws Exception { - if (!(originalResponse instanceof FullHttpResponse)) { - throw new IllegalStateException("HTTP client can only be used when session protocol is HTTP: " + - ctx.scheme().uriText()); - } - FullHttpResponse httpResponse = (FullHttpResponse) originalResponse; - byte[] body = content.readableBytes() == 0 ? EMPTY : ByteBufUtil.getBytes(content); - @SuppressWarnings("unchecked") // Guaranteed by SimpleHttpClient interface. - T response = (T) new SimpleHttpResponse(httpResponse.status(), httpResponse.headers(), body); - return response; - } - - @Override - public boolean isAsyncClient() { - return true; - } - - private static FullHttpRequest convertToFullHttpRequest(SimpleHttpRequest request, Channel channel) { - FullHttpRequest fullHttpRequest; - if (request.content().length > 0) { - ByteBuf content = channel.alloc().ioBuffer().writeBytes(request.content()); - fullHttpRequest = new DefaultFullHttpRequest( - HttpVersion.HTTP_1_1, request.method(), request.uri().toASCIIString(), content); - } else { - fullHttpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, request.method(), - request.uri().toASCIIString()); - } - fullHttpRequest.headers().set(request.headers()); - return fullHttpRequest; - } -} diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpInvocation.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpInvocation.java deleted file mode 100644 index d73e3d553b34..000000000000 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpInvocation.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.http; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -import com.linecorp.armeria.client.ClientCodec.EncodeResult; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; - -import io.netty.channel.Channel; -import io.netty.handler.codec.http.FullHttpRequest; - -/** - * A container for the parameters of this client invocation. - */ -class SimpleHttpInvocation extends ServiceInvocationContext implements EncodeResult { - - private static final AtomicInteger nextInvocationId = new AtomicInteger(); - - private final int invocationId = nextInvocationId.incrementAndGet(); - private String invocationIdStr; - - - private final FullHttpRequest content; - - SimpleHttpInvocation(Channel ch, Scheme scheme, - String host, String path, - FullHttpRequest content, - SimpleHttpRequest origRequest) { - super(ch, scheme, host, path, path, SimpleHttpClient.class.getName(), origRequest); - this.content = content; - } - - @Override - public String invocationId() { - String invocationIdStr = this.invocationIdStr; - if (invocationIdStr == null) { - this.invocationIdStr = invocationIdStr = Long.toString(invocationId & 0xFFFFFFFFL, 16); - } - - return invocationIdStr; - } - - @Override - public String method() { - return path(); - } - - @Override - public List> paramTypes() { - return Collections.singletonList(SimpleHttpRequest.class); - } - - @Override - public Class returnType() { - return SimpleHttpResponse.class; - } - - @Override - public List params() { - return Collections.singletonList(originalRequest()); - } - - @Override - public boolean isSuccess() { - return true; - } - - @Override - public ServiceInvocationContext invocationContext() { - return this; - } - - @Override - public FullHttpRequest content() { - return content; - } - - @Override - public Throwable cause() { - return new IllegalStateException("A successful result does not have a cause."); - } - - @Override - public Optional encodedHost() { - return Optional.of(host()); - } - - @Override - public Optional encodedPath() { - return Optional.of(path()); - } - - @Override - public Optional encodedScheme() { - return Optional.of(scheme()); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequest.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequest.java index 41ab0f4ba83f..5d37fea40a35 100644 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequest.java +++ b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -17,8 +17,13 @@ package com.linecorp.armeria.client.http; import java.net.URI; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; -import com.linecorp.armeria.common.http.ImmutableHttpHeaders; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.HttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaders; @@ -27,7 +32,10 @@ /** * A container for information to send in an HTTP request. This is a simpler version of {@link FullHttpRequest} * which only uses a byte array to avoid callers having to worry about memory management. + * + * @deprecated Use {@link HttpRequest} or {@link AggregatedHttpMessage} instead. */ +@Deprecated public class SimpleHttpRequest { private final URI uri; @@ -35,8 +43,7 @@ public class SimpleHttpRequest { private final HttpHeaders headers; private final byte[] content; - SimpleHttpRequest(URI uri, HttpMethod method, HttpHeaders headers, - byte[] content) { + SimpleHttpRequest(URI uri, HttpMethod method, HttpHeaders headers, byte[] content) { this.uri = uri; this.method = method; this.headers = new ImmutableHttpHeaders(headers); @@ -103,4 +110,163 @@ static String toString(URI uri, HttpMethod method, HttpHeaders headers, buf.append(')'); return buf.toString(); } + + + /** + * A container for HTTP headers that cannot be mutated. Just delegates read operations to an underlying + * {@link HttpHeaders} object. + */ + private static final class ImmutableHttpHeaders extends HttpHeaders { + + private final HttpHeaders delegate; + + ImmutableHttpHeaders(HttpHeaders delegate) { + this.delegate = delegate; + } + + @Override + public String get(String name) { + return delegate.get(name); + } + + @Override + public Integer getInt(CharSequence name) { + return delegate.getInt(name); + } + + @Override + public int getInt(CharSequence name, int defaultValue) { + return delegate.getInt(name, defaultValue); + } + + @Override + public Short getShort(CharSequence name) { + return delegate.getShort(name); + } + + @Override + public short getShort(CharSequence name, short defaultValue) { + return delegate.getShort(name, defaultValue); + } + + @Override + public Long getTimeMillis(CharSequence name) { + return delegate.getTimeMillis(name); + } + + @Override + public long getTimeMillis(CharSequence name, long defaultValue) { + return delegate.getTimeMillis(name, defaultValue); + } + + @Override + @Deprecated + public List getAll(String name) { + return delegate.getAll(name); + } + + @Override + @Deprecated + public List> entries() { + return delegate.entries(); + } + + @Override + @Deprecated + public boolean contains(String name) { + return delegate.contains(name); + } + + @Override + @Deprecated + public Iterator> iterator() { + return delegate.iterator(); + } + + @Override + public Iterator> iteratorCharSequence() { + return delegate.iteratorCharSequence(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public Set names() { + return delegate.names(); + } + + @Override + public HttpHeaders add(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders add(String name, Iterable values) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders addInt(CharSequence name, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders addShort(CharSequence name, short value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders set(String name, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders set(String name, Iterable values) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders setInt(CharSequence name, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders setShort(CharSequence name, short value) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders remove(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpHeaders clear() { + throw new UnsupportedOperationException(); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object other) { + return delegate.equals(other); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } } diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequestBuilder.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequestBuilder.java index 5853118765db..3ce86be0fae2 100644 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequestBuilder.java +++ b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -22,6 +22,9 @@ import java.net.URISyntaxException; import java.nio.charset.Charset; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.HttpRequest; + import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.EmptyHttpHeaders; import io.netty.handler.codec.http.HttpHeaders; @@ -29,7 +32,10 @@ /** * Creates a new {@link SimpleHttpRequest}. + * + * @deprecated Use {@link HttpRequest} or {@link AggregatedHttpMessage} instead. */ +@Deprecated public class SimpleHttpRequestBuilder { private static final byte[] EMPTY = new byte[0]; diff --git a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpResponse.java b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpResponse.java index 49e8e02bdbb7..28acfc62bef6 100644 --- a/src/main/java/com/linecorp/armeria/client/http/SimpleHttpResponse.java +++ b/src/main/java/com/linecorp/armeria/client/http/SimpleHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -16,6 +16,9 @@ package com.linecorp.armeria.client.http; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.HttpResponse; + import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; @@ -24,7 +27,10 @@ * A container for information returned in an HTTP response. This is a simpler version of * {@link FullHttpResponse} which only uses a byte array to avoid callers having to worry about memory * management. + * + * @deprecated Use {@link HttpResponse} or {@link AggregatedHttpMessage} instead. */ +@Deprecated public class SimpleHttpResponse { private final HttpResponseStatus status; diff --git a/src/main/java/com/linecorp/armeria/client/logging/LoggingClient.java b/src/main/java/com/linecorp/armeria/client/logging/LoggingClient.java index df64e689344a..be612a3f0999 100644 --- a/src/main/java/com/linecorp/armeria/client/logging/LoggingClient.java +++ b/src/main/java/com/linecorp/armeria/client/logging/LoggingClient.java @@ -16,30 +16,54 @@ package com.linecorp.armeria.client.logging; -import java.util.function.Function; +import static java.util.Objects.requireNonNull; -import com.linecorp.armeria.client.Client; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.DecoratingClient; -import com.linecorp.armeria.common.util.Ticker; +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.common.logging.LogLevel; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.ResponseLog; /** * Decorates a {@link Client} to log invocation requests and responses. */ -public class LoggingClient extends DecoratingClient { +public class LoggingClient extends DecoratingClient { + + private static final Logger logger = LoggerFactory.getLogger(LoggingClient.class); + + private static final String REQUEST_FORMAT = "{} Request: {}"; + private static final String RESPONSE_FORMAT = "{} Response: {}"; + + private final LogLevel level; /** * Creates a new instance that decorates the specified {@link Client}. */ - public LoggingClient(Client client) { - super(client, codec -> new LoggingClientCodec(codec, Ticker.systemTicker()), Function.identity()); + public LoggingClient(Client delegate) { + this(delegate, LogLevel.INFO); } - /** - * Creates a new instance that decorates the specified {@link Client}. - * - * @param ticker an alternative {@link Ticker} - */ - public LoggingClient(Client client, Ticker ticker) { - super(client, codec -> new LoggingClientCodec(codec, ticker), Function.identity()); + public LoggingClient(Client delegate, LogLevel level) { + super(delegate); + this.level = requireNonNull(level, "level"); + } + + @Override + public O execute(ClientRequestContext ctx, I req) throws Exception { + ctx.awaitRequestLog().thenAccept(log -> log(ctx, level, log)); + ctx.awaitResponseLog().thenAccept(log -> log(ctx, level, log)); + return delegate().execute(ctx, req); + } + + protected void log(ClientRequestContext ctx, LogLevel level, RequestLog log) { + level.log(logger, REQUEST_FORMAT, ctx, log); + } + + protected void log(ClientRequestContext ctx, LogLevel level, ResponseLog log) { + level.log(logger, RESPONSE_FORMAT, ctx, log); } } diff --git a/src/main/java/com/linecorp/armeria/client/logging/LoggingClientCodec.java b/src/main/java/com/linecorp/armeria/client/logging/LoggingClientCodec.java deleted file mode 100644 index 4fe1e526054e..000000000000 --- a/src/main/java/com/linecorp/armeria/client/logging/LoggingClientCodec.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.logging; - -import static com.linecorp.armeria.common.util.UnitFormatter.elapsed; -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.util.Optional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.DecoratingClientCodec; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.util.Ticker; -import com.linecorp.armeria.common.util.UnitFormatter; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.util.AttributeKey; -import io.netty.util.concurrent.Promise; - -/** - * Decorates a {@link ClientCodec} to log invocation requests and responses. - */ -final class LoggingClientCodec extends DecoratingClientCodec { - - private static final Logger logger = LoggerFactory.getLogger(LoggingClientCodec.class); - - private static final AttributeKey START_TIME_NANOS = - AttributeKey.valueOf(LoggingClientCodec.class, "START_TIME_NANOS"); - - private final Ticker ticker; - - /** - * Creates a new instance that decorates the specified {@code codec}. - * - * @param ticker an alternative {@link Ticker} - */ - LoggingClientCodec(ClientCodec codec, Ticker ticker) { - super(codec); - this.ticker = requireNonNull(ticker); - } - - @Override - public void prepareRequest(Method method, Object[] args, Promise resultPromise) { - delegate().prepareRequest(method, args, resultPromise); - } - - @Override - public EncodeResult encodeRequest( - Channel channel, SessionProtocol sessionProtocol, Method method, Object[] args) { - - final EncodeResult result = delegate().encodeRequest(channel, sessionProtocol, method, args); - if (result.isSuccess()) { - final ServiceInvocationContext ctx = result.invocationContext(); - final Logger logger = ctx.logger(); - if (logger.isInfoEnabled()) { - logger.info("Request: {} ({})", ctx.params(), contentSize(result.content())); - ctx.attr(START_TIME_NANOS).set(ticker.read()); - } - } else { - final Optional scheme = result.encodedScheme(); - final Optional hostname = result.encodedHost(); - final Optional path = result.encodedPath(); - - logger.warn("{}[{}://{}{}#{}][] Rejected due to protocol violation:", - channel, - scheme.isPresent() ? scheme.get().uriText() : "unknown", - hostname.isPresent() ? hostname.get() : "", - path.isPresent() ? path.get() : "/", - method.getName(), - result.cause()); - } - return result; - } - - private static StringBuilder contentSize(Object content) { - StringBuilder builder = new StringBuilder(16); - if (content instanceof ByteBuf) { - ByteBuf byteBuf = (ByteBuf) content; - UnitFormatter.appendSize(builder, byteBuf); - } else { - builder.append("unknown"); - } - return builder; - } - - @Override - public T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse) - throws Exception { - final Logger logger = ctx.logger(); - if (logger.isInfoEnabled() && ctx.hasAttr(START_TIME_NANOS)) { - return logAndDecodeResponse(ctx, logger, originalResponse, content); - } else { - return delegate().decodeResponse(ctx, content, originalResponse); - } - } - - private T logAndDecodeResponse(ServiceInvocationContext ctx, Logger logger, Object originalResponse, - ByteBuf content) throws Exception { - final long endTimeNanos = ticker.read(); - final long startTimeNanos = ctx.attr(START_TIME_NANOS).get(); - - try { - T result = delegate().decodeResponse(ctx, content, originalResponse); - logger.info("Response: {} ({})", result, elapsed(startTimeNanos, endTimeNanos)); - return result; - } catch (Throwable cause) { - logger.info("Exception: {} ({})", cause, elapsed(startTimeNanos, endTimeNanos)); - throw cause; - } - } -} diff --git a/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClient.java b/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClient.java index a9b1433aeef4..0247eea0cdac 100644 --- a/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClient.java +++ b/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClient.java @@ -1,15 +1,18 @@ package com.linecorp.armeria.client.metrics; +import static java.util.Objects.requireNonNull; + import java.util.function.Function; import com.codahale.metrics.MetricRegistry; import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.DecoratingClient; import com.linecorp.armeria.common.metrics.DropwizardMetricConsumer; import com.linecorp.armeria.common.metrics.MetricConsumer; -public final class MetricCollectingClient extends DecoratingClient { +public final class MetricCollectingClient extends DecoratingClient { /** * A {@link Client} decorator that tracks request stats using the Dropwizard metrics library. @@ -30,16 +33,25 @@ public final class MetricCollectingClient extends DecoratingClient { *

It is generally recommended to define your own name for the service instead of using something like * the Java class to make sure otherwise safe changes like renames don't break metrics. */ - public static Function newDropwizardDecorator(MetricRegistry metricRegistry, - String metricNamePrefix) { + public static Function, Client> newDropwizardDecorator( + MetricRegistry metricRegistry, String metricNamePrefix) { - return client -> new MetricCollectingClient( + return client -> new MetricCollectingClient<>( client, new DropwizardMetricConsumer(metricRegistry, metricNamePrefix)); } + private final MetricConsumer consumer; + + public MetricCollectingClient(Client delegate, MetricConsumer consumer) { + super(delegate); + this.consumer = requireNonNull(consumer, "consumer"); + } - public MetricCollectingClient(Client client, MetricConsumer metricConsumer) { - super(client, codec -> new MetricCollectingClientCodec(codec, metricConsumer), Function.identity()); + @Override + public O execute(ClientRequestContext ctx, I req) throws Exception { + ctx.awaitRequestLog().thenAccept(consumer::onRequest); + ctx.awaitResponseLog().thenAccept(consumer::onResponse); + return delegate().execute(ctx, req); } } diff --git a/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClientCodec.java b/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClientCodec.java deleted file mode 100644 index 52525b9b9e7f..000000000000 --- a/src/main/java/com/linecorp/armeria/client/metrics/MetricCollectingClientCodec.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.linecorp.armeria.client.metrics; - -import java.lang.reflect.Method; -import java.util.Optional; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.DecoratingClientCodec; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.SerializationFormat; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.metrics.MetricConsumer; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufHolder; -import io.netty.channel.Channel; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.util.AttributeKey; - -/** - * Decorator to collect client metrics. - *

- * This class is expected to be used with other {@link MetricConsumer} - */ -class MetricCollectingClientCodec extends DecoratingClientCodec { - - private static final AttributeKey METRICS = - AttributeKey.valueOf(MetricCollectingClientCodec.class, "METRICS"); - - private final MetricConsumer metricConsumer; - - /** - * Creates a new instance that decorates the specified {@link ClientCodec}. - */ - MetricCollectingClientCodec(ClientCodec delegate, MetricConsumer metricConsumer) { - super(delegate); - this.metricConsumer = metricConsumer; - } - - @Override - public EncodeResult encodeRequest(Channel channel, SessionProtocol sessionProtocol, Method method, - Object[] args) { - long startTime = System.nanoTime(); - EncodeResult result = delegate().encodeRequest(channel, sessionProtocol, method, args); - if (result.isSuccess()) { - ServiceInvocationContext context = result.invocationContext(); - context.attr(METRICS).set(new MetricsData(getRequestSize(result.content()), startTime)); - metricConsumer.invocationStarted( - context.scheme(), context.host(), context.path(), Optional.of(context.method())); - } else { - metricConsumer.invocationComplete( - result.encodedScheme().orElse(Scheme.of(SerializationFormat.UNKNOWN, sessionProtocol)), - HttpResponseStatus.BAD_REQUEST.code(), - System.nanoTime() - startTime, 0, 0, - result.encodedHost().orElse("__unknown_host__"), - result.encodedPath().orElse("__unknown_path__"), - Optional.empty(), - false); - } - return result; - } - - private void invokeComplete( - ServiceInvocationContext ctx, HttpResponseStatus status, int responseSizeBytes) { - MetricsData metricsData = ctx.attr(METRICS).get(); - metricConsumer.invocationComplete( - ctx.scheme(), status.code(), System.nanoTime() - metricsData.startTimeNanos, - metricsData.requestSizeBytes, responseSizeBytes, ctx.host(), ctx.path(), - Optional.of(ctx.method()), true); - } - - @Override - public T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse) - throws Exception { - int responseSizeBytes = content.readableBytes(); - try { - T response = delegate().decodeResponse(ctx, content, originalResponse); - invokeComplete(ctx, getResponseStatus(response), responseSizeBytes); - return response; - } catch (Throwable t) { - invokeComplete(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, responseSizeBytes); - throw t; - } - } - - private static HttpResponseStatus getResponseStatus(Object response) { - if (response instanceof HttpResponse) { - return ((HttpResponse) response).status(); - } - return HttpResponseStatus.OK; - } - - private static int getRequestSize(Object content) { - if (content instanceof ByteBuf) { - return ((ByteBuf) content).readableBytes(); - } else if (content instanceof ByteBufHolder) { - return ((ByteBufHolder) content).content().readableBytes(); - } - return 0; - } - - /** - * internal container for metric data - */ - private static class MetricsData { - private final int requestSizeBytes; - private final long startTimeNanos; - - private MetricsData(int requestSizeBytes, long startTimeNanos) { - this.requestSizeBytes = requestSizeBytes; - this.startTimeNanos = startTimeNanos; - } - } -} diff --git a/src/main/java/com/linecorp/armeria/client/pool/DefaultKeyedChannelPool.java b/src/main/java/com/linecorp/armeria/client/pool/DefaultKeyedChannelPool.java index 557198abe6e0..9af9f4baee66 100644 --- a/src/main/java/com/linecorp/armeria/client/pool/DefaultKeyedChannelPool.java +++ b/src/main/java/com/linecorp/armeria/client/pool/DefaultKeyedChannelPool.java @@ -303,7 +303,7 @@ protected boolean offerChannel(K key, Channel channel) { @Override public void close() { pool.forEach((k, v) -> { - for (; ; ) { + for (;;) { Channel channel = pollChannel(k); if (channel == null) { break; diff --git a/src/main/java/com/linecorp/armeria/client/routing/DefaultWeightedEndpoint.java b/src/main/java/com/linecorp/armeria/client/routing/DefaultWeightedEndpoint.java deleted file mode 100644 index 0d76eae57b3f..000000000000 --- a/src/main/java/com/linecorp/armeria/client/routing/DefaultWeightedEndpoint.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2016 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.routing; - -import static java.util.Objects.requireNonNull; - -public class DefaultWeightedEndpoint implements WeightedEndpoint { - - private static final int DEFAULT_WEIGHT = 1; - - private final String hostname; - - private final int port; - - private final int weight; - - public DefaultWeightedEndpoint(String hostname, int port) { - this(hostname, port, DEFAULT_WEIGHT); - } - - public DefaultWeightedEndpoint(String hostname, int port, int weight) { - requireNonNull(hostname, "hostname"); - - this.hostname = hostname; - this.port = port; - this.weight = weight; - } - - @Override - public int weight() { - return weight; - } - - @Override - public String hostname() { - return hostname; - } - - @Override - public int port() { - return port; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - DefaultWeightedEndpoint node = (DefaultWeightedEndpoint) o; - - return port == node.port && hostname.equals(node.hostname); - } - - @Override - public int hashCode() { - return 31 * hostname.hashCode() + port; - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(); - buf.append("DefaultWeightedEndpoint("); - buf.append(hostname).append(':').append(port); - buf.append(',').append(weight).append(')'); - return buf.toString(); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroup.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroup.java index 9c5a27aa8eca..46eddd0ec8e4 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroup.java +++ b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroup.java @@ -17,9 +17,12 @@ import java.util.List; -public interface EndpointGroup { +import com.linecorp.armeria.client.Endpoint; + +@FunctionalInterface +public interface EndpointGroup { /** * Return the endpoints held by this {@link EndpointGroup}. */ - List endpoints(); + List endpoints(); } diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupInvoker.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupInvoker.java deleted file mode 100644 index 9d49c3547e7e..000000000000 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupInvoker.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.routing; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.client.ClientOptions; -import com.linecorp.armeria.client.DecoratingRemoteInvoker; -import com.linecorp.armeria.client.RemoteInvoker; -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; - -import java.lang.reflect.Method; -import java.net.URI; - -public final class EndpointGroupInvoker extends DecoratingRemoteInvoker { - private String groupName; - - /** - * Creates a new instance that decorates the specified {@link RemoteInvoker}, - * such that invocations are made against one of the endpoints in end - * {@link EndpointGroup} named {@code groupName}. - */ - public EndpointGroupInvoker(RemoteInvoker delegate, String groupName) { - super(delegate); - this.groupName = groupName; - } - - @Override - public Future invoke(EventLoop eventLoop, URI uri, - ClientOptions options, - ClientCodec codec, Method method, - Object[] args) throws Exception { - final Endpoint selectedEndpoint = EndpointGroupRegistry.selectNode(groupName); - final String nodeAddress = selectedEndpoint.hostname() + ':' + selectedEndpoint.port(); - uri = URI.create(EndpointGroupUtil.replaceEndpointGroup(uri.toString(), nodeAddress)); - - return delegate().invoke(eventLoop, uri, options, codec, method, args); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupRegistry.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupRegistry.java index 7ed5ad67a2af..b26d6cd3a147 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupRegistry.java +++ b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupRegistry.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.linecorp.armeria.client.Endpoint; + /** * An in-memory registry of server groups. */ @@ -84,7 +86,7 @@ public static EndpointGroup get(String groupName) { * Select a endpoint from the target endpoint group. */ public static Endpoint selectNode(String groupName) { - EndpointSelector endpointSelector = EndpointGroupRegistry.getNodeSelector(groupName); + EndpointSelector endpointSelector = getNodeSelector(groupName); if (endpointSelector == null) { throw new EndpointGroupException("non-existent EndpointGroup: " + groupName); } diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupUtil.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupUtil.java index e70b18e7b3e9..11beb0ef6e8d 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupUtil.java +++ b/src/main/java/com/linecorp/armeria/client/routing/EndpointGroupUtil.java @@ -19,15 +19,16 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public final class EndpointGroupUtil { - private final static String ENDPOINT_GROUP_MARK = "group:"; - private final static Pattern ENDPOINT_GROUP_PATTERN = Pattern.compile("://(?:[^@]*@)?(" + ENDPOINT_GROUP_MARK + "([^:/]+)(:\\d+)?)"); +final class EndpointGroupUtil { - public final static String getEndpointGroupName(URI uri) { - return EndpointGroupUtil.getEndpointGroupName(uri.toString()); + private static final String ENDPOINT_GROUP_MARK = "group:"; + private static final Pattern ENDPOINT_GROUP_PATTERN = Pattern.compile("://(?:[^@]*@)?(" + ENDPOINT_GROUP_MARK + "([^:/]+)(:\\d+)?)"); + + public static String getEndpointGroupName(URI uri) { + return getEndpointGroupName(uri.toString()); } - public final static String getEndpointGroupName(String uri) { + public static String getEndpointGroupName(String uri) { Matcher matcher = ENDPOINT_GROUP_PATTERN.matcher(uri); if (matcher.find()) { return matcher.group(2); @@ -35,11 +36,11 @@ public final static String getEndpointGroupName(String uri) { return null; } - public final static String replaceEndpointGroup(URI uri, String endpointUri) { - return EndpointGroupUtil.replaceEndpointGroup(uri.toString(), endpointUri); + public static String replaceEndpointGroup(URI uri, String endpointUri) { + return replaceEndpointGroup(uri.toString(), endpointUri); } - public final static String replaceEndpointGroup(String uri, String endpointUri) { + public static String replaceEndpointGroup(String uri, String endpointUri) { Matcher matcher = ENDPOINT_GROUP_PATTERN.matcher(uri); if (matcher.find()) { return new StringBuilder(uri). @@ -49,7 +50,9 @@ public final static String replaceEndpointGroup(String uri, String endpointUri) return uri; } - public final static String removeGroupMark(URI uri) { + public static String removeGroupMark(URI uri) { return uri.toString().replaceFirst(ENDPOINT_GROUP_MARK, ""); } + + private EndpointGroupUtil() {} } diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointSelectionStrategy.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointSelectionStrategy.java index f1949a5791d2..3754c87ef145 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointSelectionStrategy.java +++ b/src/main/java/com/linecorp/armeria/client/routing/EndpointSelectionStrategy.java @@ -18,9 +18,10 @@ /** * A factory interface of {@link EndpointSelector} */ -public interface EndpointSelectionStrategy { - EndpointSelectionStrategy WEIGHTED_ROUND_ROBIN = new WeightedRoundRobinStrategy(); +@FunctionalInterface +public interface EndpointSelectionStrategy { + EndpointSelectionStrategy WEIGHTED_ROUND_ROBIN = new WeightedRoundRobinStrategy(); - EndpointSelector newSelector(EndpointGroup endpointGroup); + EndpointSelector newSelector(EndpointGroup endpointGroup); } diff --git a/src/main/java/com/linecorp/armeria/client/routing/EndpointSelector.java b/src/main/java/com/linecorp/armeria/client/routing/EndpointSelector.java index fef213b8f926..7493f9c95574 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/EndpointSelector.java +++ b/src/main/java/com/linecorp/armeria/client/routing/EndpointSelector.java @@ -15,11 +15,13 @@ */ package com.linecorp.armeria.client.routing; -public interface EndpointSelector { +import com.linecorp.armeria.client.Endpoint; + +public interface EndpointSelector { /** * Return the {@link EndpointGroup} held by this selector. */ - EndpointGroup group(); + EndpointGroup group(); /** * Return the {@link EndpointSelectionStrategy} used by this selector to select endpoints. @@ -29,5 +31,5 @@ public interface EndpointSelector { /** * Return a selected endpoint. */ - T select(); + Endpoint select(); } diff --git a/src/main/java/com/linecorp/armeria/client/routing/StaticEndpointGroup.java b/src/main/java/com/linecorp/armeria/client/routing/StaticEndpointGroup.java index e755c28259ea..f76ed67fe657 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/StaticEndpointGroup.java +++ b/src/main/java/com/linecorp/armeria/client/routing/StaticEndpointGroup.java @@ -15,30 +15,32 @@ */ package com.linecorp.armeria.client.routing; -import com.google.common.collect.ImmutableList; - import static java.util.Objects.requireNonNull; import java.util.List; -public final class StaticEndpointGroup implements EndpointGroup { +import com.google.common.collect.ImmutableList; + +import com.linecorp.armeria.client.Endpoint; + +public final class StaticEndpointGroup implements EndpointGroup { - private final List endpoints; + private final List endpoints; - public StaticEndpointGroup(T... endpoints) { + public StaticEndpointGroup(Endpoint... endpoints) { requireNonNull(endpoints, "endpoints"); this.endpoints = ImmutableList.copyOf(endpoints); } - public StaticEndpointGroup(Iterable endpoints) { + public StaticEndpointGroup(Iterable endpoints) { requireNonNull(endpoints, "endpoints"); this.endpoints = ImmutableList.copyOf(endpoints); } @Override - public List endpoints() { + public List endpoints() { return endpoints; } @@ -47,11 +49,10 @@ public String toString() { StringBuilder buf = new StringBuilder(); buf.append("StaticEndpointGroup("); for (Endpoint endpoint : endpoints) { - buf.append(endpoint.toString()).append(","); + buf.append(endpoint).append(','); } buf.setCharAt(buf.length() - 1, ')'); return buf.toString(); } - } diff --git a/src/main/java/com/linecorp/armeria/client/routing/WeightedRoundRobinStrategy.java b/src/main/java/com/linecorp/armeria/client/routing/WeightedRoundRobinStrategy.java index 3fc37e83ae38..decb31eba6dc 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/WeightedRoundRobinStrategy.java +++ b/src/main/java/com/linecorp/armeria/client/routing/WeightedRoundRobinStrategy.java @@ -24,12 +24,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -final class WeightedRoundRobinStrategy implements EndpointSelectionStrategy { +import com.linecorp.armeria.client.Endpoint; + +final class WeightedRoundRobinStrategy implements EndpointSelectionStrategy { @Override @SuppressWarnings("unchecked") - public EndpointSelector newSelector(EndpointGroup endpointGroup) { - return new RoundRobinSelector((EndpointGroup) endpointGroup); + public EndpointSelector newSelector(EndpointGroup endpointGroup) { + return new RoundRobinSelector(endpointGroup); } @@ -41,8 +43,8 @@ public EndpointSelector newSelector(EndpointGroup * if endpoint weights are 3,5,7,then select result is abcabcabcbcbcbb abcabcabcbcbcbb ... */ - final static class RoundRobinSelector implements EndpointSelector { - private EndpointGroup endpointGroup; + static final class RoundRobinSelector implements EndpointSelector { + private final EndpointGroup endpointGroup; private final AtomicLong sequence = new AtomicLong(); @@ -50,13 +52,13 @@ final static class RoundRobinSelector implements EndpointSelector endpointGroup) { - requireNonNull(endpointGroup, "group"); + RoundRobinSelector(EndpointGroup endpointGroup) { + requireNonNull(endpointGroup, "endpointGroup"); this.endpointGroup = endpointGroup; - endpointGroup.endpoints().stream().forEach(e -> { + endpointGroup.endpoints().forEach(e -> { int weight = e.weight(); minWeight = Math.min(minWeight, weight); maxWeight = Math.max(maxWeight, weight); @@ -65,7 +67,7 @@ final static class RoundRobinSelector implements EndpointSelector group() { + public EndpointGroup group() { return endpointGroup; } @@ -75,19 +77,19 @@ public EndpointSelectionStrategy strategy() { } @Override - public WeightedEndpoint select() { - List endpoints = endpointGroup.endpoints(); + public Endpoint select() { + List endpoints = endpointGroup.endpoints(); long currentSequence = sequence.getAndIncrement(); if (minWeight < maxWeight) { - HashMap endpointWeights = new LinkedHashMap<>(); - for (WeightedEndpoint endpoint : endpoints) { + HashMap endpointWeights = new LinkedHashMap<>(); + for (Endpoint endpoint : endpoints) { endpointWeights.put(endpoint, new AtomicInteger(endpoint.weight())); } int mod = (int) (currentSequence % sumWeight); for (int i = 0; i < maxWeight; i++) { - for (Map.Entry entry : endpointWeights.entrySet()) { + for (Map.Entry entry : endpointWeights.entrySet()) { AtomicInteger weight = entry.getValue(); if (mod == 0 && weight.get() > 0) { return entry.getKey(); diff --git a/src/main/java/com/linecorp/armeria/client/thrift/DefaultThriftClient.java b/src/main/java/com/linecorp/armeria/client/thrift/DefaultThriftClient.java new file mode 100644 index 000000000000..9cfddf67a29b --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/thrift/DefaultThriftClient.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientOptions; +import com.linecorp.armeria.client.Endpoint; +import com.linecorp.armeria.client.UserClient; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.thrift.ThriftCall; +import com.linecorp.armeria.common.thrift.ThriftReply; + +import io.netty.channel.EventLoop; + +final class DefaultThriftClient + extends UserClient implements ThriftClient { + + private final AtomicInteger nextSeqId = new AtomicInteger(); + + DefaultThriftClient(Client delegate, + Supplier eventLoopSupplier, SessionProtocol sessionProtocol, + ClientOptions options, Endpoint endpoint) { + + super(delegate, eventLoopSupplier, sessionProtocol, options, endpoint); + } + + @Override + public ThriftReply execute(String path, Class serviceType, String method, Object... args) { + final ThriftCall call = new ThriftCall(nextSeqId.getAndIncrement(), serviceType, method, args); + return execute(call.method(), path, call, cause -> new ThriftReply(call.seqId(), cause)); + } + + @Override + protected ThriftClient newInstance( + Client delegate, Supplier eventLoopSupplier, + SessionProtocol sessionProtocol, ClientOptions options, Endpoint endpoint) { + + return new DefaultThriftClient(delegate, eventLoopSupplier, sessionProtocol, options, endpoint); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/TByteBufInputTransport.java b/src/main/java/com/linecorp/armeria/client/thrift/TByteBufInputTransport.java deleted file mode 100644 index 54e516c10592..000000000000 --- a/src/main/java/com/linecorp/armeria/client/thrift/TByteBufInputTransport.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; - -import java.io.IOException; - -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufInputStream; - -class TByteBufInputTransport extends TTransport { - private final ByteBufInputStream byteBufInputStream; - - TByteBufInputTransport(ByteBuf inputByteBuf) { - byteBufInputStream = new ByteBufInputStream(inputByteBuf); - } - - @Override - public boolean isOpen() { - throw new UnsupportedOperationException(); - } - - @Override - public void open() throws TTransportException { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - throw new UnsupportedOperationException(); - } - - @Override - public int read(byte[] buf, int off, int len) throws TTransportException { - try { - return byteBufInputStream.read(buf, off, len); - } catch (IOException e) { - throw new TTransportException(e); - } - } - - @Override - public void write(byte[] buf, int off, int len) throws TTransportException { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/TByteBufOutputTransport.java b/src/main/java/com/linecorp/armeria/client/thrift/TByteBufOutputTransport.java deleted file mode 100644 index 8ac558103c92..000000000000 --- a/src/main/java/com/linecorp/armeria/client/thrift/TByteBufOutputTransport.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; - -import org.apache.thrift.transport.TTransport; -import org.apache.thrift.transport.TTransportException; - -import io.netty.buffer.ByteBuf; - -class TByteBufOutputTransport extends TTransport { - private final ByteBuf outByteBuf; - - TByteBufOutputTransport(ByteBuf outByteBuf) { - this.outByteBuf = outByteBuf; - } - - @Override - public boolean isOpen() { - throw new UnsupportedOperationException(); - } - - @Override - public void open() throws TTransportException { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - throw new UnsupportedOperationException(); - } - - @Override - public int read(byte[] buf, int off, int len) throws TTransportException { - throw new UnsupportedOperationException(); - } - - @Override - public void write(byte[] buf, int off, int len) throws TTransportException { - outByteBuf.writeBytes(buf, off, len); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClient.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClient.java new file mode 100644 index 000000000000..b5d13856971a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClient.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; + +import com.linecorp.armeria.client.ClientOptionDerivable; +import com.linecorp.armeria.common.thrift.ThriftReply; + +public interface ThriftClient extends ClientOptionDerivable { + ThriftReply execute(String path, Class serviceType, String method, Object... args); +} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientCodec.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientCodec.java deleted file mode 100644 index 9a53d8633a89..000000000000 --- a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientCodec.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.thrift.TApplicationException; -import org.apache.thrift.TBase; -import org.apache.thrift.TException; -import org.apache.thrift.TFieldIdEnum; -import org.apache.thrift.async.AsyncMethodCallback; -import org.apache.thrift.protocol.TMessage; -import org.apache.thrift.protocol.TMessageType; -import org.apache.thrift.protocol.TProtocol; -import org.apache.thrift.protocol.TProtocolFactory; -import org.apache.thrift.transport.TTransportException; - -import com.linecorp.armeria.client.ClientCodec; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.util.concurrent.Promise; - -/** - * Thrift {@link ClientCodec}. - */ -public class ThriftClientCodec implements ClientCodec { - - private static final Map, Map> methodMapCache = new HashMap<>(); - - private static final String SYNC_IFACE = "Iface"; - private static final String ASYNC_IFACE = "AsyncIface"; - - private final URI uri; - private final boolean isAsyncClient; - private final Map methodMap; - private final TProtocolFactory protocolFactory; - private final String loggerName; - - private final AtomicInteger seq = new AtomicInteger(); - - /** - * Creates a new instance. - */ - public ThriftClientCodec(URI uri, Class interfaceClass, TProtocolFactory protocolFactory) { - - requireNonNull(interfaceClass, "interfaceClass"); - - this.uri = requireNonNull(uri, "uri"); - this.protocolFactory = requireNonNull(protocolFactory, "protocolFactory"); - - final String interfaceName = interfaceClass.getName(); - if (interfaceName.endsWith('$' + ASYNC_IFACE)) { - isAsyncClient = true; - } else if (interfaceName.endsWith('$' + SYNC_IFACE)) { - isAsyncClient = false; - } else { - throw new IllegalArgumentException("interfaceClass must be Iface or AsyncIface: " + interfaceName); - } - - loggerName = interfaceName.substring(0, interfaceName.lastIndexOf('$')); - methodMap = getThriftMethodMapFromInterface(interfaceClass, isAsyncClient); - } - - private static Map getThriftMethodMapFromInterface(Class interfaceClass, - boolean isAsyncInterface) { - Map methodMap = methodMapCache.get(interfaceClass); - if (methodMap != null) { - return methodMap; - } - methodMap = new HashMap<>(); - - String interfaceName = interfaceClass.getName(); - ClassLoader loader = interfaceClass.getClassLoader(); - - int interfaceNameSuffixLength = isAsyncInterface ? ASYNC_IFACE.length() : SYNC_IFACE.length(); - final String thriftServiceName = - interfaceName.substring(0, interfaceName.length() - interfaceNameSuffixLength - 1); - - final Class clientClass; - try { - clientClass = Class.forName(thriftServiceName + "$Client", false, loader); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException( - "Thrift Client Class not found. serviceName:" + thriftServiceName, e); - } - - for (Method method : interfaceClass.getMethods()) { - ThriftMethod thriftMethod = new ThriftMethod(clientClass, method, thriftServiceName); - methodMap.put(method.getName(), thriftMethod); - } - - Map resultMap = Collections.unmodifiableMap(methodMap); - methodMapCache.put(interfaceClass, resultMap); - - return methodMap; - - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public void prepareRequest(Method method, Object[] args, Promise resultPromise) { - requireNonNull(method, "method"); - requireNonNull(resultPromise, "resultPromise"); - final ThriftMethod thriftMethod = methodMap.get(method.getName()); - if (thriftMethod == null) { - throw new IllegalStateException("Thrift method not found: " + method.getName()); - } - - if (isAsyncClient) { - AsyncMethodCallback callback = ThriftMethod.asyncCallback(args); - if (callback != null) { - resultPromise.addListener(future -> { - if (future.isSuccess()) { - callback.onComplete(future.getNow()); - } else { - Exception decodedException = decodeException(future.cause(), - thriftMethod.declaredThrowableException()); - callback.onError(decodedException); - } - }); - } - } - } - - @Override - @SuppressWarnings("rawtypes") - public EncodeResult encodeRequest( - Channel channel, SessionProtocol sessionProtocol, Method method, Object[] args) { - - requireNonNull(channel, "channel"); - requireNonNull(sessionProtocol, "sessionProtocol"); - requireNonNull(method, "method"); - - final ThriftMethod thriftMethod = methodMap.get(method.getName()); - if (thriftMethod == null) { - throw new IllegalStateException("Thrift method not found: " + method.getName()); - } - - final Scheme scheme = Scheme.of(ThriftProtocolFactories.toSerializationFormat(protocolFactory), - sessionProtocol); - - try { - final ByteBuf outByteBuf = channel.alloc().buffer(); - final TByteBufOutputTransport outTransport = new TByteBufOutputTransport(outByteBuf); - final TProtocol tProtocol = protocolFactory.getProtocol(outTransport); - final TMessage tMessage = new TMessage(method.getName(), thriftMethod.methodType(), - seq.incrementAndGet()); - - tProtocol.writeMessageBegin(tMessage); - final TBase tArgs = thriftMethod.createArgs(isAsyncClient, args); - tArgs.write(tProtocol); - tProtocol.writeMessageEnd(); - - AsyncMethodCallback asyncMethodCallback = null; - if (isAsyncClient) { - asyncMethodCallback = ThriftMethod.asyncCallback(args); - } - return new ThriftInvocation( - channel, scheme, uri.getHost(), uri.getPath(), uri.getPath(), loggerName, outByteBuf, - tMessage, thriftMethod, tArgs, asyncMethodCallback); - } catch (Exception e) { - Exception decodedException = decodeException(e, thriftMethod.declaredThrowableException()); - return new ThriftEncodeFailureResult(decodedException, scheme, uri); - } - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public T decodeResponse(ServiceInvocationContext ctx, ByteBuf content, Object originalResponse) - throws Exception { - if (content == null) { - return null; - } - - if (!content.isReadable()) { - ThriftMethod thriftMethod = getThriftMethod(ctx); - if (thriftMethod != null && thriftMethod.isOneWay()) { - return null; - } - throw new TApplicationException(TApplicationException.MISSING_RESULT, ctx.toString()); - } - - TByteBufInputTransport inputTransport = new TByteBufInputTransport(content); - TProtocol inputProtocol = protocolFactory.getProtocol(inputTransport); - TMessage msg = inputProtocol.readMessageBegin(); - if (msg.type == TMessageType.EXCEPTION) { - TApplicationException ex = TApplicationException.read(inputProtocol); - inputProtocol.readMessageEnd(); - throw ex; - } - ThriftMethod method = methodMap.get(msg.name); - if (method == null) { - throw new TApplicationException(TApplicationException.WRONG_METHOD_NAME, msg.name); - } - TBase result = method.createResult(); - result.read(inputProtocol); - inputProtocol.readMessageEnd(); - - for (TFieldIdEnum fieldIdEnum : method.getExceptionFields()) { - if (result.isSet(fieldIdEnum)) { - throw (TException) result.getFieldValue(fieldIdEnum); - } - } - - TFieldIdEnum successField = method.successField(); - if (successField == null) { //void method - return null; - } - if (result.isSet(successField)) { - return (T) result.getFieldValue(successField); - } - - throw new TApplicationException(TApplicationException.MISSING_RESULT, - result.getClass().getName() + '.' + successField.getFieldName()); - } - - private ThriftMethod getThriftMethod(ServiceInvocationContext ctx) { - ThriftMethod method; - if (ctx instanceof ThriftInvocation) { - final ThriftInvocation thriftInvocation = (ThriftInvocation) ctx; - method = thriftInvocation.thriftMethod(); - } else { - method = methodMap.get(ctx.method()); - } - return method; - } - - private static Exception decodeException(Throwable cause, Class[] declaredThrowableExceptions) { - if (cause instanceof RuntimeException || cause instanceof TTransportException) { - return (Exception) cause; - } - - final boolean isDeclaredException; - if (declaredThrowableExceptions != null) { - isDeclaredException = Arrays.stream(declaredThrowableExceptions).anyMatch(v -> v.isInstance(cause)); - } else { - isDeclaredException = false; - } - if (isDeclaredException) { - return (Exception) cause; - } else if (cause instanceof Error) { - return new RuntimeException(cause); - } else { - return new TTransportException(cause); - } - } - - @Override - public boolean isAsyncClient() { - return isAsyncClient; - } - - private static final class ThriftEncodeFailureResult implements EncodeResult { - - private final Throwable cause; - private final Optional scheme; - private final Optional uri; - - ThriftEncodeFailureResult(Throwable cause, Scheme scheme, URI uri) { - this.cause = requireNonNull(cause, "cause"); - this.scheme = Optional.ofNullable(scheme); - this.uri = Optional.ofNullable(uri); - } - - @Override - public boolean isSuccess() { - return false; - } - - @Override - public ServiceInvocationContext invocationContext() { - throw new IllegalStateException("failed to encode a request; invocation context not available"); - } - - @Override - public ByteBuf content() { - throw new IllegalStateException("failed to encode a request; content not available"); - } - - @Override - public Throwable cause() { - return cause; - } - - @Override - public Optional encodedHost() { - if (uri.isPresent()) { - return Optional.ofNullable(uri.get().getHost()); - } else { - return Optional.empty(); - } - } - - @Override - public Optional encodedPath() { - if (uri.isPresent()) { - return Optional.ofNullable(uri.get().getPath()); - } else { - return Optional.empty(); - } - } - - @Override - public Optional encodedScheme() { - return scheme; - } - } -} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientDelegate.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientDelegate.java new file mode 100644 index 000000000000..9be3e323ed25 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientDelegate.java @@ -0,0 +1,220 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; + +import org.apache.thrift.TApplicationException; +import org.apache.thrift.TBase; +import org.apache.thrift.TException; +import org.apache.thrift.TFieldIdEnum; +import org.apache.thrift.protocol.TMessage; +import org.apache.thrift.protocol.TMessageType; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.protocol.TProtocolFactory; +import org.apache.thrift.transport.TMemoryBuffer; +import org.apache.thrift.transport.TMemoryInputTransport; +import org.apache.thrift.transport.TTransportException; + +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.InvalidResponseException; +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.http.AggregatedHttpMessage; +import com.linecorp.armeria.common.http.DefaultHttpRequest; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpMethod; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; +import com.linecorp.armeria.common.http.HttpStatus; +import com.linecorp.armeria.common.thrift.ThriftCall; +import com.linecorp.armeria.common.thrift.ThriftProtocolFactories; +import com.linecorp.armeria.common.thrift.ThriftReply; +import com.linecorp.armeria.internal.thrift.ThriftFunction; +import com.linecorp.armeria.internal.thrift.ThriftServiceMetadata; + +final class ThriftClientDelegate implements Client { + + private final Client httpClient; + private final String path; + private final SerializationFormat serializationFormat; + private final TProtocolFactory protocolFactory; + private final String mediaType; + private final Map, ThriftServiceMetadata> metadataMap = new ConcurrentHashMap<>(); + + ThriftClientDelegate(Client httpClient, String path, + SerializationFormat serializationFormat) { + + this.httpClient = httpClient; + this.path = path; + this.serializationFormat = serializationFormat; + protocolFactory = ThriftProtocolFactories.get(serializationFormat); + mediaType = serializationFormat.mediaType().toString(); + } + + @Override + public ThriftReply execute(ClientRequestContext ctx, ThriftCall req) throws Exception { + final int seqId = req.seqId(); + final String method = req.method(); + final List args = req.params(); + final ThriftReply reply = new ThriftReply(seqId); + + ctx.requestLogBuilder().serializationFormat(serializationFormat); + ctx.requestLogBuilder().attach(req); + ctx.responseLogBuilder().attach(reply); + + final ThriftFunction func; + try { + func = metadata(req.serviceType()).function(method); + if (func == null) { + throw new IllegalStateException("Thrift method not found: " + method); + } + } catch (Throwable cause) { + reply.completeExceptionally(cause); + return reply; + } + + try { + final TMemoryBuffer outTransport = new TMemoryBuffer(128); + final TProtocol tProtocol = protocolFactory.getProtocol(outTransport); + final TMessage tMessage = new TMessage(method, func.messageType(), seqId); + + tProtocol.writeMessageBegin(tMessage); + @SuppressWarnings("rawtypes") + final TBase tArgs = func.newArgs(args); + tArgs.write(tProtocol); + tProtocol.writeMessageEnd(); + + final DefaultHttpRequest httpReq = new DefaultHttpRequest( + HttpHeaders.of(HttpMethod.POST, path) + .set(HttpHeaderNames.CONTENT_TYPE, mediaType), true); + httpReq.write(HttpData.of(outTransport.getArray(), 0, outTransport.length())); + httpReq.close(); + + final CompletableFuture future = + httpClient.execute(ctx, httpReq).aggregate(); + + future.whenComplete((res, cause) -> { + if (cause != null) { + completeExceptionally(reply, func, + cause instanceof ExecutionException ? cause.getCause() : cause); + return; + } + + final HttpStatus status = res.headers().status(); + if (status.code() != HttpStatus.OK.code()) { + completeExceptionally(reply, func, new InvalidResponseException(status.toString())); + return; + } + + try { + reply.complete(decodeResponse(func, res.content())); + } catch (Throwable t) { + completeExceptionally(reply, func, t); + } + }); + } catch (Throwable cause) { + completeExceptionally(reply, func, cause); + } + + return reply; + } + + private ThriftServiceMetadata metadata(Class clientType) { + final ThriftServiceMetadata metadata = metadataMap.get(clientType); + if (metadata != null) { + return metadata; + } + + return metadataMap.computeIfAbsent(clientType, ThriftServiceMetadata::new); + } + + private Object decodeResponse(ThriftFunction method, HttpData content) throws TException { + if (content.isEmpty()) { + if (method.isOneway()) { + return null; + } + throw new TApplicationException(TApplicationException.MISSING_RESULT); + } + + final TMemoryInputTransport inputTransport = + new TMemoryInputTransport(content.array(), content.offset(), content.length()); + final TProtocol inputProtocol = protocolFactory.getProtocol(inputTransport); + + final TMessage msg = inputProtocol.readMessageBegin(); + if (msg.type == TMessageType.EXCEPTION) { + TApplicationException ex = TApplicationException.read(inputProtocol); + inputProtocol.readMessageEnd(); + throw ex; + } + + if (!method.name().equals(msg.name)) { + throw new TApplicationException(TApplicationException.WRONG_METHOD_NAME, msg.name); + } + TBase, TFieldIdEnum> result = method.newResult(); + result.read(inputProtocol); + inputProtocol.readMessageEnd(); + + for (TFieldIdEnum fieldIdEnum : method.exceptionFields()) { + if (result.isSet(fieldIdEnum)) { + throw (TException) result.getFieldValue(fieldIdEnum); + } + } + + TFieldIdEnum successField = method.successField(); + if (successField == null) { //void method + return null; + } + if (result.isSet(successField)) { + return result.getFieldValue(successField); + } + + throw new TApplicationException(TApplicationException.MISSING_RESULT, + result.getClass().getName() + '.' + successField.getFieldName()); + } + + private static Exception decodeException(Throwable cause, Class[] declaredThrowableExceptions) { + if (cause instanceof RuntimeException || cause instanceof TTransportException) { + return (Exception) cause; + } + + final boolean isDeclaredException; + if (declaredThrowableExceptions != null) { + isDeclaredException = Arrays.stream(declaredThrowableExceptions).anyMatch(v -> v.isInstance(cause)); + } else { + isDeclaredException = false; + } + if (isDeclaredException) { + return (Exception) cause; + } else if (cause instanceof Error) { + return new RuntimeException(cause); + } else { + return new TTransportException(cause); + } + } + + private static void completeExceptionally(ThriftReply reply, ThriftFunction thriftMethod, Throwable cause) { + reply.completeExceptionally(decodeException(cause, thriftMethod.declaredExceptions())); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientFactory.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientFactory.java new file mode 100644 index 000000000000..5793e79a5dfd --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +import com.linecorp.armeria.client.Client; +import com.linecorp.armeria.client.ClientFactory; +import com.linecorp.armeria.client.ClientOptionDerivable; +import com.linecorp.armeria.client.ClientOptions; +import com.linecorp.armeria.client.DecoratingClientFactory; +import com.linecorp.armeria.common.Scheme; +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.http.HttpRequest; +import com.linecorp.armeria.common.http.HttpResponse; +import com.linecorp.armeria.common.thrift.ThriftCall; +import com.linecorp.armeria.common.thrift.ThriftReply; + +public class ThriftClientFactory extends DecoratingClientFactory { + + private static final Set SUPPORTED_SCHEMES; + + static { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + for (SessionProtocol p : SessionProtocol.ofHttp()) { + for (SerializationFormat f : SerializationFormat.ofThrift()) { + builder.add(Scheme.of(f, p)); + } + } + SUPPORTED_SCHEMES = builder.build(); + } + + public ThriftClientFactory(ClientFactory httpClientFactory) { + super(validate(httpClientFactory)); + } + + private static ClientFactory validate(ClientFactory httpClientFactory) { + requireNonNull(httpClientFactory, "httpClientFactory"); + + for (SessionProtocol p : SessionProtocol.ofHttp()) { + if (!httpClientFactory.supportedSchemes().contains(Scheme.of(SerializationFormat.NONE, p))) { + throw new IllegalArgumentException(p.uriText() + " not supported by: " + httpClientFactory); + } + } + + return httpClientFactory; + } + + @Override + public Set supportedSchemes() { + return SUPPORTED_SCHEMES; + } + + @Override + public T newClient(URI uri, Class clientType, ClientOptions options) { + final Scheme scheme = validate(uri, clientType, options); + final SerializationFormat serializationFormat = scheme.serializationFormat(); + + final Client httpClient = newHttpClient(uri, scheme, options); + + final Client delegate = options.decoration().decorate( + ThriftCall.class, ThriftReply.class, + new ThriftClientDelegate(httpClient, uri.getPath(), serializationFormat)); + + final ThriftClient thriftClient = new DefaultThriftClient( + delegate, eventLoopSupplier(), scheme.sessionProtocol(), options, newEndpoint(uri)); + + if (clientType == ThriftClient.class) { + @SuppressWarnings("unchecked") + final T client = (T) thriftClient; + return client; + } else { + @SuppressWarnings("unchecked") + T client = (T) Proxy.newProxyInstance( + clientType.getClassLoader(), + new Class[] { clientType, ClientOptionDerivable.class }, + new ThriftClientInvocationHandler(thriftClient, uri.getPath(), clientType)); + return client; + } + } + + public Client newHttpClient(URI uri, Scheme scheme, ClientOptions options) { + try { + @SuppressWarnings("unchecked") + Client client = delegate().newClient( + new URI(Scheme.of(SerializationFormat.NONE, scheme.sessionProtocol()).uriText(), + uri.getAuthority(), null, null, null), + Client.class, options); + return client; + } catch (URISyntaxException e) { + throw new Error(e); // Should never happen. + } + } +} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientInvocationHandler.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientInvocationHandler.java new file mode 100644 index 000000000000..e9b429ff45bf --- /dev/null +++ b/src/main/java/com/linecorp/armeria/client/thrift/ThriftClientInvocationHandler.java @@ -0,0 +1,161 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; + +import org.apache.thrift.async.AsyncMethodCallback; + +import com.linecorp.armeria.client.ClientOptionDerivable; +import com.linecorp.armeria.client.ClientOptionValue; +import com.linecorp.armeria.common.thrift.ThriftReply; + +final class ThriftClientInvocationHandler implements InvocationHandler { + + private static final Object[] NO_ARGS = new Object[0]; + + private final ThriftClient thriftClient; + private final String path; + private final Class clientType; + + ThriftClientInvocationHandler(ThriftClient thriftClient, String path, Class clientType) { + this.thriftClient = thriftClient; + this.path = path; + this.clientType = clientType; + } + + private ThriftClientInvocationHandler(ThriftClientInvocationHandler handler, ThriftClient thriftClient) { + this.thriftClient = thriftClient; + path = handler.path; + clientType = handler.clientType; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + final Class declaringClass = method.getDeclaringClass(); + if (declaringClass == Object.class) { + // Handle the methods in Object + return invokeObjectMethod(proxy, method, args); + } + + if (declaringClass == ClientOptionDerivable.class) { + return invokeClientOptionDerivableMethod(method, args); + } + + assert declaringClass == clientType; + // Handle the methods in the interface. + return invokeClientMethod(method, args); + } + + private Object invokeObjectMethod(Object proxy, Method method, Object[] args) { + final String methodName = method.getName(); + + switch (methodName) { + case "toString": + return clientType.getSimpleName() + '(' + path + ')'; + case "hashCode": + return System.identityHashCode(proxy); + case "equals": + return proxy == args[0]; + default: + throw new Error("unknown method: " + methodName); + } + } + + private Object invokeClientOptionDerivableMethod(Method method, Object[] args) { + final String methodName = method.getName(); + switch (methodName) { + case "withOptions": + final Object arg = args[0]; + if (arg instanceof Iterable) { + @SuppressWarnings("unchecked") + final Iterable> options = (Iterable>) arg; + return Proxy.newProxyInstance( + clientType.getClassLoader(), + new Class[] { clientType, ClientOptionDerivable.class }, + new ThriftClientInvocationHandler(this, thriftClient.withOptions(options))); + } else if (arg instanceof ClientOptionValue[]) { + final ClientOptionValue[] options = (ClientOptionValue[]) arg; + return Proxy.newProxyInstance( + clientType.getClassLoader(), + new Class[] { clientType, ClientOptionDerivable.class }, + new ThriftClientInvocationHandler(this, thriftClient.withOptions(options))); + } else { + throw new Error("unknown argument: " + arg); + } + default: + throw new Error("unknown method: " + methodName); + } + } + + private Object invokeClientMethod(Method method, Object[] args) throws Throwable { + final AsyncMethodCallback callback; + if (args == null) { + args = NO_ARGS; + callback = null; + } else { + final int lastIdx = args.length - 1; + if (args.length > 0 && args[lastIdx] instanceof AsyncMethodCallback) { + @SuppressWarnings("unchecked") + final AsyncMethodCallback lastArg = (AsyncMethodCallback) args[lastIdx]; + callback = lastArg; + args = Arrays.copyOfRange(args, 0, lastIdx); + } else { + callback = null; + } + } + + try { + final ThriftReply reply = thriftClient.execute(path, clientType, method.getName(), args); + + if (callback != null) { + reply.whenComplete((result, cause) -> { + if (cause == null) { + callback.onComplete(result); + } else { + invokeOnError(callback, cause); + } + }); + + return null; + } else { + try { + return reply.get(); + } catch (ExecutionException e) { + throw e.getCause(); + } + } + } catch (Throwable cause) { + if (callback != null) { + invokeOnError(callback, cause); + return null; + } else { + throw cause; + } + } + } + + private static void invokeOnError(AsyncMethodCallback callback, Throwable cause) { + callback.onError(cause instanceof Exception ? (Exception) cause + : new UndeclaredThrowableException(cause)); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftInvocation.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftInvocation.java deleted file mode 100644 index aad5f4356456..000000000000 --- a/src/main/java/com/linecorp/armeria/client/thrift/ThriftInvocation.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; - -import static java.util.Objects.requireNonNull; - -import java.util.List; -import java.util.Optional; - -import org.apache.thrift.TBase; -import org.apache.thrift.async.AsyncMethodCallback; -import org.apache.thrift.protocol.TMessage; -import org.apache.thrift.protocol.TMessageType; - -import com.linecorp.armeria.client.ClientCodec.EncodeResult; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.thrift.ThriftUtil; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; - -/** - * Object that contains Thrift Method Invocation Information - */ -class ThriftInvocation extends ServiceInvocationContext implements EncodeResult { - - private final TMessage tMessage; - @SuppressWarnings("rawtypes") - private final TBase tArgs; - private final ThriftMethod method; - private final AsyncMethodCallback asyncMethodCallback; - private final ByteBuf content; - - ThriftInvocation( - Channel ch, Scheme scheme, String host, String path, String mappedPath, - String loggerName, ByteBuf content, - TMessage tMessage, ThriftMethod method, @SuppressWarnings("rawtypes") TBase tArgs, - AsyncMethodCallback asyncMethodCallback) { - - super(ch, scheme, host, path, mappedPath, loggerName, content); - - this.content = requireNonNull(content); - this.tMessage = requireNonNull(tMessage, "tMessage"); - this.tArgs = requireNonNull(tArgs, "tArgs"); - this.method = requireNonNull(method, "method"); - this.asyncMethodCallback = asyncMethodCallback; - } - - TMessage tMessage() { - return tMessage; - } - - boolean isOneway() { - return TMessageType.ONEWAY == tMessage.type; - } - - int seqId() { - return tMessage().seqid; - } - - ThriftMethod thriftMethod() { - return method; - } - - @Override - public String invocationId() { - return ThriftUtil.seqIdToString(tMessage().seqid); - } - - @Override - public String method() { - return tMessage().name; - } - - @Override - public List> paramTypes() { - return method.paramTypes(); - } - - @Override - public Class returnType() { - return method.returnType(); - } - - @Override - @SuppressWarnings("unchecked") - public List params() { - return ThriftUtil.toJavaParams(tArgs); - } - - public AsyncMethodCallback asyncMethodCallback() { - return asyncMethodCallback; - } - - @Override - public boolean isSuccess() { - return true; - } - - @Override - public ServiceInvocationContext invocationContext() { - return this; - } - - @Override - public ByteBuf content() { - return content; - } - - @Override - public Throwable cause() { - throw new IllegalStateException("A successful result does not have a cause."); - } - - @Override - public Optional encodedHost() { - return Optional.of(host()); - } - - @Override - public Optional encodedPath() { - return Optional.of(path()); - } - - @Override - public Optional encodedScheme() { - return Optional.of(scheme()); - } -} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/ThriftMethod.java b/src/main/java/com/linecorp/armeria/client/thrift/ThriftMethod.java deleted file mode 100644 index 8e056bd90475..000000000000 --- a/src/main/java/com/linecorp/armeria/client/thrift/ThriftMethod.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.thrift; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -import org.apache.thrift.TBase; -import org.apache.thrift.TFieldIdEnum; -import org.apache.thrift.async.AsyncMethodCallback; -import org.apache.thrift.meta_data.FieldMetaData; -import org.apache.thrift.meta_data.FieldValueMetaData; -import org.apache.thrift.protocol.TMessageType; - -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.thrift.ThriftUtil; - -@SuppressWarnings("rawtypes") -class ThriftMethod { - private final boolean oneWay; - private final String name; - final Class[] declaredThrowableException; - private final TBase argsObject; - private final TFieldIdEnum[] argsFieldIdEnums; - - private final TBase resultObject; - private final TFieldIdEnum successField; - private final List exceptionFields; - - private final List> paramTypes; - private final Class returnType; - - @SuppressWarnings({ "unchecked", "SuspiciousArrayCast" }) - ThriftMethod(Class clientClass, Method method, String thriftServiceName) { - requireNonNull(clientClass); - requireNonNull(method); - requireNonNull(thriftServiceName); - name = method.getName(); - declaredThrowableException = method.getExceptionTypes(); - - boolean oneWay = false; - try { - clientClass.getMethod("recv_" + name); - } catch (NoSuchMethodException ignore) { - oneWay = true; - } - this.oneWay = oneWay; - - String argClassName = thriftServiceName + '$' + name + "_args"; - final Class> argClass; - try { - argClass = (Class>) Class.forName(argClassName); - argsObject = argClass.newInstance(); - } catch (Exception e) { - throw new IllegalArgumentException("fail to create a new instance: " + argClassName, e); - } - - String argFieldEnumName = thriftServiceName + '$' + name + "_args$_Fields"; - try { - Class fieldIdEnumClass = Class.forName(argFieldEnumName); - argsFieldIdEnums = (TFieldIdEnum[]) requireNonNull(fieldIdEnumClass.getEnumConstants(), - "field enum may not be empty"); - } catch (Exception e) { - throw new IllegalArgumentException("fail to create a new instance : " + argFieldEnumName, e); - } - - FieldValueMetaData successFieldMetadata = null; - if (oneWay) { - resultObject = null; - successField = null; - exceptionFields = Collections.emptyList(); - } else { - String resultClassName = thriftServiceName + '$' + name + "_result"; - final Class resultClass; - try { - resultClass = Class.forName(resultClassName); - resultObject = (TBase) resultClass.newInstance(); - } catch (Exception e) { - throw new IllegalArgumentException("fail to create a new instance : " + resultClassName, e); - } - - try { - - @SuppressWarnings("unchecked") - final Map resultMetaDataMap = - (Map) FieldMetaData.getStructMetaDataMap(resultClass); - - TFieldIdEnum successField = null; - List exceptionFields = new ArrayList<>(resultMetaDataMap.size()); - for (Entry e : resultMetaDataMap.entrySet()) { - final TFieldIdEnum key = e.getKey(); - final String fieldName = key.getFieldName(); - if ("success".equals(fieldName)) { - successField = key; - successFieldMetadata = e.getValue().valueMetaData; - continue; - } - - Class fieldType = resultClass.getField(fieldName).getType(); - if (Throwable.class.isAssignableFrom(fieldType)) { - exceptionFields.add(key); - } - } - - this.successField = successField; - this.exceptionFields = Collections.unmodifiableList(exceptionFields); - } catch (Exception e) { - throw new IllegalArgumentException( - "failed to find the result metaDataMap: " + resultClass.getName(), - e); - } - } - - // Determine the parameter types of the function. - paramTypes = Collections.unmodifiableList( - FieldMetaData.getStructMetaDataMap(argClass).values().stream() - .map(e -> ThriftUtil.toJavaType(e.valueMetaData)).collect(Collectors.toList())); - - if (successFieldMetadata != null) { - returnType = ThriftUtil.toJavaType(successFieldMetadata); - } else { - returnType = Void.class; - } - } - - TBase createArgs() { - return argsObject.deepCopy(); - } - - @SuppressWarnings("unchecked") - TBase createArgs(boolean isAsync, Object[] args) { - final TBase newArgs = createArgs(); - if (args != null) { - final int toFillArgLength = args.length - (isAsync ? 1 : 0); - for (int i = 0; i < toFillArgLength; i++) { - newArgs.setFieldValue(argsFieldIdEnums[i], args[i]); - } - } - return newArgs; - } - - boolean isOneWay() { - return oneWay; - } - - byte methodType() { - return oneWay ? TMessageType.ONEWAY : TMessageType.CALL; - } - - List> paramTypes() { - return paramTypes; - } - - Class returnType() { - return returnType; - } - - static AsyncMethodCallback asyncCallback(Object[] args) { - if (requireNonNull(args, "args").length == 0) { - throw new IllegalArgumentException("args must contains objects"); - } - final Object lastObj = args[args.length - 1]; - if (lastObj instanceof AsyncMethodCallback) { - return (AsyncMethodCallback) lastObj; - } - if (lastObj == null) { - return null; - } - throw new IllegalArgumentException( - "the last element of args must be AsyncMethodCallback: " + lastObj.getClass().getName()); - } - - static AsyncMethodCallback asyncMethodCallbackFromContext(ServiceInvocationContext ctx) { - requireNonNull(ctx, "ctx"); - List params = ctx.params(); - if (params == null || params.isEmpty()) { - return null; - } - Object lastObj = params.get(params.size() - 1); - return lastObj instanceof AsyncMethodCallback ? (AsyncMethodCallback) lastObj : null; - } - - Class[] declaredThrowableException() { - return declaredThrowableException; - } - - TBase createResult() { - return resultObject.deepCopy(); - } - - TFieldIdEnum successField() { - return successField; - } - - List getExceptionFields() { - return exceptionFields; - } -} diff --git a/src/main/java/com/linecorp/armeria/client/thrift/package-info.java b/src/main/java/com/linecorp/armeria/client/thrift/package-info.java index 7012e03b8ac9..121a54c205d9 100644 --- a/src/main/java/com/linecorp/armeria/client/thrift/package-info.java +++ b/src/main/java/com/linecorp/armeria/client/thrift/package-info.java @@ -15,6 +15,6 @@ */ /** - * Thrift {@link com.linecorp.armeria.client.ClientCodec}. + * Thrift client. */ package com.linecorp.armeria.client.thrift; diff --git a/src/main/java/com/linecorp/armeria/client/tracing/TracingRemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/tracing/AbstractTracingClient.java similarity index 55% rename from src/main/java/com/linecorp/armeria/client/tracing/TracingRemoteInvoker.java rename to src/main/java/com/linecorp/armeria/client/tracing/AbstractTracingClient.java index 2373bbe4e66e..a76010c6884a 100644 --- a/src/main/java/com/linecorp/armeria/client/tracing/TracingRemoteInvoker.java +++ b/src/main/java/com/linecorp/armeria/client/tracing/AbstractTracingClient.java @@ -16,15 +16,17 @@ package com.linecorp.armeria.client.tracing; -import java.lang.reflect.Method; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.github.kristofa.brave.Brave; import com.github.kristofa.brave.ClientRequestAdapter; import com.github.kristofa.brave.ClientResponseAdapter; @@ -32,103 +34,102 @@ import com.github.kristofa.brave.SpanId; import com.twitter.zipkin.gen.Span; -import com.linecorp.armeria.client.ClientCodec; +import com.linecorp.armeria.client.Client; import com.linecorp.armeria.client.ClientOptions; -import com.linecorp.armeria.client.DecoratingRemoteInvoker; -import com.linecorp.armeria.client.RemoteInvoker; - -import io.netty.channel.EventLoop; -import io.netty.util.concurrent.Future; +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.client.DecoratingClient; +import com.linecorp.armeria.common.Responses; +import com.linecorp.armeria.common.RpcRequest; +import com.linecorp.armeria.common.logging.RequestLog; /** - * An abstract {@link RemoteInvoker} that traces remote service invocations. + * An abstract {@link DecoratingClient} that traces remote service invocations. *

* This class depends on Brave distributed tracing library. */ -public abstract class TracingRemoteInvoker extends DecoratingRemoteInvoker { +public abstract class AbstractTracingClient extends DecoratingClient { + + private static final Logger logger = LoggerFactory.getLogger(AbstractTracingClient.class); private final ClientTracingInterceptor clientInterceptor; - protected TracingRemoteInvoker(RemoteInvoker remoteInvoker, Brave brave) { - super(remoteInvoker); + protected AbstractTracingClient(Client delegate, Brave brave) { + super(delegate); clientInterceptor = new ClientTracingInterceptor(brave); } @Override - public final Future invoke(EventLoop eventLoop, URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args) throws Exception { - + public O execute(ClientRequestContext ctx, I req) throws Exception { // create new request adapter to catch generated spanId - final InternalClientRequestAdapter requestAdapter = new InternalClientRequestAdapter(method.getName()); + final String method = req instanceof RpcRequest ? ((RpcRequest) req).method() : ctx.method(); + final InternalClientRequestAdapter requestAdapter = new InternalClientRequestAdapter(method); final Span span = clientInterceptor.openSpan(requestAdapter); // new client options with trace data - final ClientOptions traceAwareOptions = putTraceData(options, requestAdapter.getSpanId()); + putTraceData(ctx, req, requestAdapter.getSpanId()); if (span == null) { // skip tracing - return super.invoke(eventLoop, uri, traceAwareOptions, codec, method, args); + return delegate().execute(ctx, req); } // The actual remote invocation is done asynchronously. // So we have to clear the span from current thread. clientInterceptor.clearSpan(); - Future result = null; - try { - result = super.invoke(eventLoop, uri, traceAwareOptions, codec, method, args); - result.addListener( - future -> clientInterceptor.closeSpan(span, - createResponseAdapter(uri, options, codec, - method, args, future))); - } finally { - if (result == null) { - clientInterceptor.closeSpan(span, - createResponseAdapter(uri, options, codec, method, args, null)); - } - } - return result; + final O res = delegate().execute(ctx, req); + + ctx.awaitRequestLog().thenAcceptBoth( + Responses.awaitClose(res), + (log, unused) -> clientInterceptor.closeSpan(span, createResponseAdapter(ctx, log, res))) + .exceptionally(cause -> { + logger.warn("{} Unexpected exception:", ctx, cause); + return null; + }); + + return res; } /** * Puts trace data into the specified base {@link ClientOptions}, returning new instance of * {@link ClientOptions}. */ - protected abstract ClientOptions putTraceData(ClientOptions baseOptions, @Nullable SpanId spanId); + protected abstract void putTraceData(ClientRequestContext ctx, I req, @Nullable SpanId spanId); /** * Returns client side annotations that should be added to span. */ @SuppressWarnings("UnusedParameters") - protected List annotations(URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args, - @Nullable Future result) { + protected List annotations(ClientRequestContext ctx, RequestLog log, O res) { final KeyValueAnnotation clientUriAnnotation = KeyValueAnnotation.create( - "client.uri", uri.toString() + '#' + method.getName()); + "client.uri", log.scheme().uriText() + "://" + log.host() + ctx.path() + '#' + log.method()); - if (result == null || !result.isDone()) { + final CompletableFuture f = Responses.awaitClose(res); + if (!f.isDone()) { return Collections.singletonList(clientUriAnnotation); } final List annotations = new ArrayList<>(3); annotations.add(clientUriAnnotation); - final String clientResultText = result.isSuccess() ? "success" : "failure"; - annotations.add(KeyValueAnnotation.create("client.result", clientResultText)); + // Need to use a callback because CompletableFuture does not have a getter for the cause of failure. + // The callback will be invoked immediately because the future is done already. + f.whenComplete((result, cause) -> { + final String clientResultText = cause == null ? "success" : "failure"; + annotations.add(KeyValueAnnotation.create("client.result", clientResultText)); + + if (cause != null) { + annotations.add(KeyValueAnnotation.create("client.cause", cause.toString())); + } + }); - if (result.cause() != null) { - annotations.add(KeyValueAnnotation.create("client.cause", result.cause().toString())); - } return annotations; } - protected ClientResponseAdapter createResponseAdapter(URI uri, ClientOptions options, ClientCodec codec, - Method method, Object[] args, - @Nullable Future result) { - - final List annotations = annotations(uri, options, codec, method, args, result); + protected ClientResponseAdapter createResponseAdapter(ClientRequestContext ctx, RequestLog log, O res) { + final List annotations = annotations(ctx, log, res); return () -> annotations; } diff --git a/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingClient.java b/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingClient.java index cb08700e50f0..e096c87baa1a 100644 --- a/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingClient.java +++ b/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingClient.java @@ -16,12 +16,23 @@ package com.linecorp.armeria.client.tracing; +import static com.linecorp.armeria.internal.tracing.BraveHttpHeaderNames.PARENT_SPAN_ID; +import static com.linecorp.armeria.internal.tracing.BraveHttpHeaderNames.SAMPLED; +import static com.linecorp.armeria.internal.tracing.BraveHttpHeaderNames.SPAN_ID; +import static com.linecorp.armeria.internal.tracing.BraveHttpHeaderNames.TRACE_ID; + import java.util.function.Function; +import javax.annotation.Nullable; + import com.github.kristofa.brave.Brave; +import com.github.kristofa.brave.IdConversion; +import com.github.kristofa.brave.SpanId; import com.linecorp.armeria.client.Client; -import com.linecorp.armeria.client.DecoratingClient; +import com.linecorp.armeria.client.ClientRequestContext; +import com.linecorp.armeria.common.http.DefaultHttpHeaders; +import com.linecorp.armeria.common.http.HttpHeaders; /** * A {@link Client} decorator that traces HTTP-based remote service invocations. @@ -29,17 +40,38 @@ * This decorator puts trace data into HTTP headers. The specifications of header names and its values * correspond to Zipkin. */ -public class HttpTracingClient extends DecoratingClient { +public class HttpTracingClient extends AbstractTracingClient { /** * Creates a new tracing {@link Client} decorator using the specified {@link Brave} instance. */ - public static Function newDecorator(Brave brave) { - return client -> new HttpTracingClient(client, brave); + public static Function, Client> newDecorator(Brave brave) { + return delegate -> new HttpTracingClient<>(delegate, brave); } - HttpTracingClient(Client client, Brave brave) { - super(client, Function.identity(), remoteInvoker -> new HttpTracingRemoteInvoker(remoteInvoker, brave)); + HttpTracingClient(Client delegate, Brave brave) { + super(delegate, brave); } + @Override + protected void putTraceData(ClientRequestContext ctx, I req, @Nullable SpanId spanId) { + final HttpHeaders headers; + if (ctx.hasAttr(ClientRequestContext.HTTP_HEADERS)) { + headers = ctx.attr(ClientRequestContext.HTTP_HEADERS).get(); + } else { + headers = new DefaultHttpHeaders(true); + ctx.attr(ClientRequestContext.HTTP_HEADERS).set(headers); + } + + if (spanId == null) { + headers.add(SAMPLED, "0"); + } else { + headers.add(SAMPLED, "1"); + headers.add(TRACE_ID, IdConversion.convertToString(spanId.getTraceId())); + headers.add(SPAN_ID, IdConversion.convertToString(spanId.getSpanId())); + if (spanId.getParentSpanId() != null) { + headers.add(PARENT_SPAN_ID, IdConversion.convertToString(spanId.getParentSpanId())); + } + } + } } diff --git a/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingRemoteInvoker.java b/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingRemoteInvoker.java deleted file mode 100644 index 24c5dbabe1e0..000000000000 --- a/src/main/java/com/linecorp/armeria/client/tracing/HttpTracingRemoteInvoker.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.tracing; - -import java.util.Optional; - -import javax.annotation.Nullable; - -import com.github.kristofa.brave.Brave; -import com.github.kristofa.brave.IdConversion; -import com.github.kristofa.brave.SpanId; -import com.github.kristofa.brave.http.BraveHttpHeaders; - -import com.linecorp.armeria.client.ClientOption; -import com.linecorp.armeria.client.ClientOptions; -import com.linecorp.armeria.client.RemoteInvoker; - -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.HttpHeaders; - -/** - * A {@link TracingRemoteInvoker} that uses HTTP headers as a container of trace data. - */ -class HttpTracingRemoteInvoker extends TracingRemoteInvoker { - - HttpTracingRemoteInvoker(RemoteInvoker remoteInvoker, Brave brave) { - super(remoteInvoker, brave); - } - - @Override - protected ClientOptions putTraceData(ClientOptions baseOptions, @Nullable SpanId spanId) { - final HttpHeaders headers = new DefaultHttpHeaders(); - - final Optional baseHttpHeaders = baseOptions.get(ClientOption.HTTP_HEADERS); - baseHttpHeaders.ifPresent(headers::add); - - if (spanId == null) { - headers.add(BraveHttpHeaders.Sampled.getName(), "0"); - } else { - headers.add(BraveHttpHeaders.Sampled.getName(), "1"); - headers.add(BraveHttpHeaders.TraceId.getName(), IdConversion.convertToString(spanId.getTraceId())); - headers.add(BraveHttpHeaders.SpanId.getName(), IdConversion.convertToString(spanId.getSpanId())); - if (spanId.getParentSpanId() != null) { - headers.add(BraveHttpHeaders.ParentSpanId.getName(), - IdConversion.convertToString(spanId.getParentSpanId())); - } - } - - return ClientOptions.of(baseOptions, ClientOption.HTTP_HEADERS.newValue(headers)); - } - -} diff --git a/src/main/java/com/linecorp/armeria/common/AbstractRequestContext.java b/src/main/java/com/linecorp/armeria/common/AbstractRequestContext.java new file mode 100644 index 000000000000..2dd09a179999 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/AbstractRequestContext.java @@ -0,0 +1,194 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.EventLoop; +import io.netty.util.DefaultAttributeMap; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; + +/** + * Default {@link RequestContext} implementation. + */ +public abstract class AbstractRequestContext extends DefaultAttributeMap implements RequestContext { + + private final SessionProtocol sessionProtocol; + private final String method; + private final String path; + private final Object request; + private List onEnterCallbacks; + private List onExitCallbacks; + + /** + * Creates a new instance. + * + * @param sessionProtocol the {@link SessionProtocol} of the invocation + * @param request the request associated with this context + */ + protected AbstractRequestContext( + SessionProtocol sessionProtocol, String method, String path, Object request) { + this.sessionProtocol = sessionProtocol; + this.method = method; + this.path = path; + this.request = request; + } + + @Override + public final SessionProtocol sessionProtocol() { + return sessionProtocol; + } + + @Override + public final String method() { + return method; + } + + @Override + public final String path() { + return path; + } + + @Override + @SuppressWarnings("unchecked") + public final T request() { + return (T) request; + } + + @Override + public final EventLoop contextAwareEventLoop() { + return RequestContext.super.contextAwareEventLoop(); + } + + @Override + public final Executor makeContextAware(Executor executor) { + return RequestContext.super.makeContextAware(executor); + } + + @Override + public final Callable makeContextAware(Callable callable) { + return () -> { + try (PushHandle ignored = propagateContextIfNotPresent()) { + return callable.call(); + } + }; + } + + @Override + public final Runnable makeContextAware(Runnable runnable) { + return () -> { + try (PushHandle ignored = propagateContextIfNotPresent()) { + runnable.run(); + } + }; + } + + @Override + public final FutureListener makeContextAware(FutureListener listener) { + return future -> invokeOperationComplete(listener, future); + } + + @Override + public final ChannelFutureListener makeContextAware(ChannelFutureListener listener) { + return future -> invokeOperationComplete(listener, future); + } + + @Override + public final > GenericFutureListener makeContextAware(GenericFutureListener listener) { + return future -> invokeOperationComplete(listener, future); + } + + private > void invokeOperationComplete( + GenericFutureListener listener, T future) throws Exception { + + try (PushHandle ignored = propagateContextIfNotPresent()) { + listener.operationComplete(future); + } + } + + private PushHandle propagateContextIfNotPresent() { + return RequestContext.mapCurrent(currentContext -> { + if (currentContext != this) { + throw new IllegalStateException( + "Trying to call object made with makeContextAware or object on executor made with " + + "makeContextAware with context " + this + + ", but context is currently set to " + currentContext + ". This means the " + + "callback was passed from one invocation to another which is not allowed. Make " + + "sure you are not saving callbacks into shared state."); + } + return () -> {}; + }, () -> { + final PushHandle handle = RequestContext.push(this); + final List onEnterCallbacks = this.onEnterCallbacks; + if (onEnterCallbacks != null) { + onEnterCallbacks.forEach(Runnable::run); + } + return (PushHandle) () -> { + handle.close(); + final List onExitCallbacks = this.onExitCallbacks; + if (onExitCallbacks != null) { + onExitCallbacks.forEach(Runnable::run); + } + }; + }); + } + + @Override + public final void onEnter(Runnable callback) { + if (onEnterCallbacks == null) { + onEnterCallbacks = new ArrayList<>(4); + } + onEnterCallbacks.add(callback); + } + + @Override + public final void onExit(Runnable callback) { + if (onExitCallbacks == null) { + onExitCallbacks = new ArrayList<>(4); + } + onExitCallbacks.add(callback); + } + + @Override + @Deprecated + public final void resolvePromise(Promise promise, Object result) { + RequestContext.super.resolvePromise(promise, result); + } + + @Override + @Deprecated + public final void rejectPromise(Promise promise, Throwable cause) { + RequestContext.super.rejectPromise(promise, cause); + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/AbstractRpcRequest.java b/src/main/java/com/linecorp/armeria/common/AbstractRpcRequest.java new file mode 100644 index 000000000000..065a3f79b36f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/AbstractRpcRequest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +public class AbstractRpcRequest implements RpcRequest { + + private final Class serviceType; + private final String method; + private final List args; + + protected AbstractRpcRequest(Class serviceType, String method, Iterable args) { + this(serviceType, method, ImmutableList.copyOf(args)); + } + + protected AbstractRpcRequest(Class serviceType, String method, Object... args) { + this(serviceType, method, ImmutableList.copyOf(args)); + } + + protected AbstractRpcRequest(Class serviceType, String method, List args) { + this.serviceType = requireNonNull(serviceType, "serviceType"); + this.method = requireNonNull(method, "method"); + this.args = args; + } + + @Override + public final Class serviceType() { + return serviceType; + } + + @Override + public final String method() { + return method; + } + + @Override + public final List params() { + return args; + } + + @Override + public int hashCode() { + return method().hashCode() * 31 + params().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AbstractRpcRequest)) { + return false; + } + + if (this == obj) { + return true; + } + + final AbstractRpcRequest that = (AbstractRpcRequest) obj; + return method().equals(that.method()) && + params().equals(that.params()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("serviceType", simpleServiceName()) + .add("method", method()) + .add("args", params()).toString(); + } + + protected final String simpleServiceName() { + final Class serviceType = serviceType(); + final Package pkg = serviceType.getPackage(); + final String fqcn = serviceType.getName(); + return pkg != null ? fqcn.substring(pkg.getName().length() + 1) : fqcn; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/AbstractRpcResponse.java b/src/main/java/com/linecorp/armeria/common/AbstractRpcResponse.java new file mode 100644 index 000000000000..c8dac9beac32 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/AbstractRpcResponse.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import com.google.common.base.MoreObjects; + +import com.linecorp.armeria.common.util.Exceptions; + +public class AbstractRpcResponse extends CompletableFuture implements RpcResponse { + + private static final CancellationException CANCELLED = Exceptions.clearTrace(new CancellationException()); + + private static final AtomicReferenceFieldUpdater causeUpdater = + AtomicReferenceFieldUpdater.newUpdater(AbstractRpcResponse.class, Throwable.class, "cause"); + + private volatile Throwable cause; + + protected AbstractRpcResponse() {} + + protected AbstractRpcResponse(Object result) { + complete(result); + } + + protected AbstractRpcResponse(Throwable cause) { + requireNonNull(cause, "cause"); + completeExceptionally(cause); + } + + @Override + public final Throwable getCause() { + return cause; + } + + @Override + public boolean completeExceptionally(Throwable cause) { + causeUpdater.compareAndSet(this, null, requireNonNull(cause)); + return super.completeExceptionally(cause); + } + + @Override + public void obtrudeException(Throwable cause) { + this.cause = requireNonNull(cause); + super.obtrudeException(cause); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return completeExceptionally(CANCELLED) || isCancelled(); + } + + @Override + public String toString() { + if (isDone()) { + if (isCompletedExceptionally()) { + return MoreObjects.toStringHelper(this) + .add("cause", cause).toString(); + } else { + return MoreObjects.toStringHelper(this) + .add("value", getNow(null)).toString(); + } + } + + final int count = getNumberOfDependents(); + if (count == 0) { + return MoreObjects.toStringHelper(this) + .addValue("not completed").toString(); + } else { + return MoreObjects.toStringHelper(this) + .addValue("not completed") + .add("dependents", count).toString(); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/ClosedSessionException.java b/src/main/java/com/linecorp/armeria/common/ClosedSessionException.java new file mode 100644 index 000000000000..f76fe92f9be2 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/ClosedSessionException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import com.linecorp.armeria.common.util.Exceptions; + +/** + * A {@link RuntimeException} raised when the connection to the remote peer has been closed unexpectedly. + */ +public final class ClosedSessionException extends RuntimeException { + + private static final long serialVersionUID = -78487475521731580L; + + private static final ClosedSessionException INSTANCE = Exceptions.clearTrace(new ClosedSessionException()); + + public static ClosedSessionException get() { + return Exceptions.isVerbose() ? new ClosedSessionException() : INSTANCE; + } + + /** + * Creates a new instance. + */ + private ClosedSessionException() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/ContentTooLargeException.java b/src/main/java/com/linecorp/armeria/common/ContentTooLargeException.java new file mode 100644 index 000000000000..7e8fc162f834 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/ContentTooLargeException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import com.linecorp.armeria.common.util.Exceptions; + +public final class ContentTooLargeException extends RuntimeException { + + private static final long serialVersionUID = 4901614315474105954L; + + private static final ContentTooLargeException INSTANCE = + Exceptions.clearTrace(new ContentTooLargeException()); + + public static ContentTooLargeException get() { + return Exceptions.isVerbose() ? new ContentTooLargeException() : INSTANCE; + } + + private ContentTooLargeException() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/FixedTimeoutPolicy.java b/src/main/java/com/linecorp/armeria/common/FixedTimeoutPolicy.java deleted file mode 100644 index 13397a8a7934..000000000000 --- a/src/main/java/com/linecorp/armeria/common/FixedTimeoutPolicy.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; - -import static java.util.Objects.requireNonNull; - -import java.time.Duration; - -final class FixedTimeoutPolicy implements TimeoutPolicy { - - private final Duration timeout; - private final long timeoutMillis; - private final String strVal; - - FixedTimeoutPolicy(Duration timeout) { - requireNonNull(timeout, "timeout"); - if (timeout.isNegative() || timeout.isZero()) { - throw new IllegalArgumentException("timeout: " + timeout + " (expected: > 0)"); - } - - this.timeout = timeout; - timeoutMillis = timeout.toMillis(); - - strVal = timeout.toString(); - } - - @Override - public long timeout(ServiceInvocationContext ctx) { - return timeoutMillis; - } - - @Override - public int hashCode() { - return (int) timeoutMillis; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof FixedTimeoutPolicy)) { - return false; - } - - if (this == obj) { - return true; - } - - final FixedTimeoutPolicy that = (FixedTimeoutPolicy) obj; - return timeoutMillis == that.timeoutMillis; - } - - @Override - public String toString() { - return strVal; - } -} diff --git a/src/main/java/com/linecorp/armeria/common/ProtocolViolationException.java b/src/main/java/com/linecorp/armeria/common/ProtocolViolationException.java new file mode 100644 index 000000000000..7455e06f8fc2 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/ProtocolViolationException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +public class ProtocolViolationException extends RuntimeException { + private static final long serialVersionUID = 4674394621849790490L; + + public ProtocolViolationException() {} + + public ProtocolViolationException(String message) { + super(message); + } + + public ProtocolViolationException(String message, Throwable cause) { + super(message, cause); + } + + public ProtocolViolationException(Throwable cause) { + super(cause); + } + + protected ProtocolViolationException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/RequestContext.java b/src/main/java/com/linecorp/armeria/common/RequestContext.java new file mode 100644 index 000000000000..78c2f1bfd361 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/RequestContext.java @@ -0,0 +1,290 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.RequestLogBuilder; +import com.linecorp.armeria.common.logging.ResponseLog; +import com.linecorp.armeria.common.logging.ResponseLogBuilder; +import com.linecorp.armeria.common.util.Exceptions; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.util.AttributeMap; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; + +/** + * Provides information about an invocation and related utilities. Every remote invocation, regardless of if + * it's client side or server side, has its own {@link RequestContext} instance. + */ +public interface RequestContext extends AttributeMap { + + /** + * Returns the context of the invocation that is being handled in the current thread. + * + * @throws IllegalStateException if the context is unavailable in the current thread + */ + static T current() { + final T ctx = RequestContextThreadLocal.get(); + if (ctx == null) { + throw new IllegalStateException(RequestContext.class.getSimpleName() + " unavailable"); + } + return ctx; + } + + /** + * Maps the context of the invocation that is being handled in the current thread. + * + * @param mapper the {@link Function} that maps the invocation + * @param defaultValueSupplier the {@link Supplier} that provides the value when the context is unavailable + * in the current thread. If {@code null}, the {@code null} will be returned + * when the context is unavailable in the current thread. + */ + static T mapCurrent( + Function mapper, @Nullable Supplier defaultValueSupplier) { + + final RequestContext ctx = RequestContextThreadLocal.get(); + if (ctx != null) { + return mapper.apply(ctx); + } + + if (defaultValueSupplier != null) { + return defaultValueSupplier.get(); + } + + return null; + } + + /** + * (Do not use; internal use only) Sets the invocation context of the current thread. + */ + static PushHandle push(RequestContext ctx) { + requireNonNull(ctx, "ctx"); + final RequestContext oldCtx = RequestContextThreadLocal.getAndSet(ctx); + return oldCtx != null ? () -> RequestContextThreadLocal.set(oldCtx) + : RequestContextThreadLocal::remove; + } + + /** + * Returns the {@link SessionProtocol} of this request. + */ + SessionProtocol sessionProtocol(); + + String method(); + + /** + * Returns the absolute path part of this invocation, as defined in + * the + * section 5.1.2 of RFC2616. + */ + String path(); + + /** + * Returns the request associated with this context. + */ + T request(); + + RequestLogBuilder requestLogBuilder(); + ResponseLogBuilder responseLogBuilder(); + CompletableFuture awaitRequestLog(); + CompletableFuture awaitResponseLog(); + + /** + * Returns the {@link EventLoop} that is handling this invocation. + */ + EventLoop eventLoop(); + + /** + * Returns an {@link EventLoop} that will make sure this invocation is set + * as the current invocation before executing any callback. This should + * almost always be used for executing asynchronous callbacks in service + * code to make sure features that require the invocation context work + * properly. Most asynchronous libraries like + * {@link CompletableFuture} provide methods that + * accept an {@link Executor} to run callbacks on. + */ + default EventLoop contextAwareEventLoop() { + return new RequestContextAwareEventLoop(this, eventLoop()); + } + + /** + * Returns an {@link Executor} that will execute callbacks in the given + * {@code executor}, making sure to propagate the current invocation context + * into the callback execution. It is generally preferred to use + * {@link #contextAwareEventLoop()} to ensure the callback stays on the + * same thread as well. + */ + default Executor makeContextAware(Executor executor) { + return runnable -> executor.execute(makeContextAware(runnable)); + } + + /** + * Returns a {@link Callable} that makes sure the current invocation context + * is set and then invokes the input {@code callable}. + */ + Callable makeContextAware(Callable callable); + + /** + * Returns a {@link Runnable} that makes sure the current invocation context + * is set and then invokes the input {@code runnable}. + */ + Runnable makeContextAware(Runnable runnable); + + /** + * Returns a {@link FutureListener} that makes sure the current invocation + * context is set and then invokes the input {@code listener}. + */ + @Deprecated + FutureListener makeContextAware(FutureListener listener); + + /** + * Returns a {@link ChannelFutureListener} that makes sure the current invocation + * context is set and then invokes the input {@code listener}. + */ + @Deprecated + ChannelFutureListener makeContextAware(ChannelFutureListener listener); + + /** + * Returns a {@link GenericFutureListener} that makes sure the current invocation + * context is set and then invokes the input {@code listener}. + */ + @Deprecated + > GenericFutureListener makeContextAware(GenericFutureListener listener); + + /** + * Registers {@code callback} to be run when re-entering this {@link RequestContext}, + * usually when using the {@link #makeContextAware} family of methods. Any thread-local state + * associated with this context should be restored by this callback. + */ + void onEnter(Runnable callback); + + /** + * Registers {@code callback} to be run when re-exiting this {@link RequestContext}, + * usually when using the {@link #makeContextAware} family of methods. Any thread-local state + * associated with this context should be reset by this callback. + */ + void onExit(Runnable callback); + + /** + * Resolves the specified {@code promise} with the specified {@code result} so that the {@code promise} is + * marked as 'done'. If {@code promise} is done already, this method does the following: + *
    + *
  • Log a warning about the failure, and
  • + *
  • Release {@code result} if it is {@linkplain ReferenceCounted a reference-counted object}, + * such as {@link ByteBuf} and {@link FullHttpResponse}.
  • + *
+ * Note that a {@link Promise} can be done already even if you did not call this method in the following + * cases: + *
    + *
  • Invocation timeout - The invocation associated with the {@link Promise} has been timed out.
  • + *
  • User error - A service implementation called any of the following methods more than once: + *
      + *
    • {@link #resolvePromise(Promise, Object)}
    • + *
    • {@link #rejectPromise(Promise, Throwable)}
    • + *
    • {@link Promise#setSuccess(Object)}
    • + *
    • {@link Promise#setFailure(Throwable)}
    • + *
    • {@link Promise#cancel(boolean)}
    • + *
    + *
  • + *
+ */ + @Deprecated + default void resolvePromise(Promise promise, Object result) { + @SuppressWarnings("unchecked") + final Promise castPromise = (Promise) promise; + + if (castPromise.trySuccess(result)) { + // Resolved successfully. + return; + } + + try { + if (!(promise.cause() instanceof TimeoutException)) { + // Log resolve failure unless it is due to a timeout. + LoggerFactory.getLogger(RequestContext.class).warn( + "Failed to resolve a completed promise ({}) with {}", promise, result); + } + } finally { + ReferenceCountUtil.safeRelease(result); + } + } + + /** + * Rejects the specified {@code promise} with the specified {@code cause}. If {@code promise} is done + * already, this method logs a warning about the failure. Note that a {@link Promise} can be done already + * even if you did not call this method in the following cases: + *
    + *
  • Invocation timeout - The invocation associated with the {@link Promise} has been timed out.
  • + *
  • User error - A service implementation called any of the following methods more than once: + *
      + *
    • {@link #resolvePromise(Promise, Object)}
    • + *
    • {@link #rejectPromise(Promise, Throwable)}
    • + *
    • {@link Promise#setSuccess(Object)}
    • + *
    • {@link Promise#setFailure(Throwable)}
    • + *
    • {@link Promise#cancel(boolean)}
    • + *
    + *
  • + *
+ */ + @Deprecated + default void rejectPromise(Promise promise, Throwable cause) { + if (promise.tryFailure(cause)) { + // Fulfilled successfully. + return; + } + + final Throwable firstCause = promise.cause(); + if (firstCause instanceof TimeoutException) { + // Timed out already. + return; + } + + if (Exceptions.isExpected(cause)) { + // The exception that was thrown after firstCause (often a transport-layer exception) + // was a usual expected exception, not an error. + return; + } + + LoggerFactory.getLogger(RequestContext.class).warn( + "Failed to reject a completed promise ({}) with {}", promise, cause, cause); + } + + @FunctionalInterface + interface PushHandle extends AutoCloseable { + @Override + void close(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareEventLoop.java b/src/main/java/com/linecorp/armeria/common/RequestContextAwareEventLoop.java similarity index 80% rename from src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareEventLoop.java rename to src/main/java/com/linecorp/armeria/common/RequestContextAwareEventLoop.java index b300e70e6ad5..f7f58488d693 100644 --- a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareEventLoop.java +++ b/src/main/java/com/linecorp/armeria/common/RequestContextAwareEventLoop.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; import java.util.Collection; @@ -7,6 +23,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import io.netty.channel.Channel; @@ -23,16 +40,14 @@ /** * A delegating {@link EventExecutor} that makes sure all submitted tasks are - * executed within the {@link ServiceInvocationContext}. + * executed within the {@link RequestContext}. */ -final class ServiceInvocationContextAwareEventLoop implements EventLoop { +final class RequestContextAwareEventLoop implements EventLoop { - private final ServiceInvocationContext context; + private final RequestContext context; private final EventLoop delegate; - ServiceInvocationContextAwareEventLoop(ServiceInvocationContext context, - EventLoop delegate) { - + RequestContextAwareEventLoop(RequestContext context,EventLoop delegate) { this.context = context; this.delegate = delegate; } @@ -64,22 +79,22 @@ public boolean inEventLoop(Thread thread) { @Override public Promise newPromise() { - return new ServiceInvocationContextAwarePromise<>(context, delegate.newPromise()); + return new RequestContextAwarePromise<>(context, delegate.newPromise()); } @Override public ProgressivePromise newProgressivePromise() { - return new ServiceInvocationContextProgressivePromise<>(context, delegate.newProgressivePromise()); + return new RequestContextAwareProgressivePromise<>(context, delegate.newProgressivePromise()); } @Override public Future newSucceededFuture(V result) { - return new ServiceInvocationContextAwareFuture<>(context, delegate.newSucceededFuture(result)); + return new RequestContextAwareFuture<>(context, delegate.newSucceededFuture(result)); } @Override public Future newFailedFuture(Throwable cause) { - return new ServiceInvocationContextAwareFuture<>(context, delegate.newFailedFuture(cause)); + return new RequestContextAwareFuture<>(context, delegate.newFailedFuture(cause)); } @Override @@ -199,7 +214,7 @@ public T invokeAny(Collection> tasks) @Override public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, - java.util.concurrent.TimeoutException { + TimeoutException { return delegate.invokeAny(makeContextAware(tasks), timeout, unit); } @@ -224,6 +239,11 @@ public ChannelFuture register(Channel channel, return delegate.register(channel, channelPromise); } +// @Override +// public ChannelFuture register(ChannelPromise channelPromise) { +// return delegate.register(channelPromise); +// } + @Override public ChannelHandlerInvoker asInvoker() { return delegate.asInvoker(); diff --git a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareFuture.java b/src/main/java/com/linecorp/armeria/common/RequestContextAwareFuture.java similarity index 80% rename from src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareFuture.java rename to src/main/java/com/linecorp/armeria/common/RequestContextAwareFuture.java index 82de9b60720c..f2402872c907 100644 --- a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwareFuture.java +++ b/src/main/java/com/linecorp/armeria/common/RequestContextAwareFuture.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; import java.util.concurrent.ExecutionException; @@ -7,12 +23,12 @@ import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; -final class ServiceInvocationContextAwareFuture implements Future { +final class RequestContextAwareFuture implements Future { - private final ServiceInvocationContext context; + private final RequestContext context; private final Future delegate; - ServiceInvocationContextAwareFuture(ServiceInvocationContext context, Future delegate) { + RequestContextAwareFuture(RequestContext context, Future delegate) { this.context = context; this.delegate = delegate; } diff --git a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextProgressivePromise.java b/src/main/java/com/linecorp/armeria/common/RequestContextAwareProgressivePromise.java similarity index 82% rename from src/main/java/com/linecorp/armeria/common/ServiceInvocationContextProgressivePromise.java rename to src/main/java/com/linecorp/armeria/common/RequestContextAwareProgressivePromise.java index 16f8885272d7..87dd7e9d56f3 100644 --- a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextProgressivePromise.java +++ b/src/main/java/com/linecorp/armeria/common/RequestContextAwareProgressivePromise.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; import java.util.concurrent.ExecutionException; @@ -8,13 +24,12 @@ import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.ProgressivePromise; -final class ServiceInvocationContextProgressivePromise implements ProgressivePromise { +final class RequestContextAwareProgressivePromise implements ProgressivePromise { - private final ServiceInvocationContext context; + private final RequestContext context; private final ProgressivePromise delegate; - ServiceInvocationContextProgressivePromise(ServiceInvocationContext context, - ProgressivePromise delegate) { + RequestContextAwareProgressivePromise(RequestContext context, ProgressivePromise delegate) { this.context = context; this.delegate = delegate; } @@ -61,7 +76,8 @@ public ProgressivePromise removeListener( } @Override - public ProgressivePromise removeListeners( + @SafeVarargs + public final ProgressivePromise removeListeners( GenericFutureListener>... listeners) { return delegate.removeListeners(listeners); } diff --git a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwarePromise.java b/src/main/java/com/linecorp/armeria/common/RequestContextAwarePromise.java similarity index 81% rename from src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwarePromise.java rename to src/main/java/com/linecorp/armeria/common/RequestContextAwarePromise.java index 7b5c2b7dbc87..cfdc97fae158 100644 --- a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContextAwarePromise.java +++ b/src/main/java/com/linecorp/armeria/common/RequestContextAwarePromise.java @@ -1,3 +1,19 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; import java.util.concurrent.ExecutionException; @@ -8,12 +24,12 @@ import io.netty.util.concurrent.GenericFutureListener; import io.netty.util.concurrent.Promise; -final class ServiceInvocationContextAwarePromise implements Promise { +final class RequestContextAwarePromise implements Promise { - private final ServiceInvocationContext context; + private final RequestContext context; private final Promise delegate; - ServiceInvocationContextAwarePromise(ServiceInvocationContext context, Promise delegate) { + RequestContextAwarePromise(RequestContext context, Promise delegate) { this.context = context; this.delegate = delegate; } @@ -66,7 +82,8 @@ public Promise removeListener( } @Override - public Promise removeListeners( + @SafeVarargs + public final Promise removeListeners( GenericFutureListener>... listeners) { return delegate.removeListeners(listeners); } diff --git a/src/main/java/com/linecorp/armeria/common/RequestContextThreadLocal.java b/src/main/java/com/linecorp/armeria/common/RequestContextThreadLocal.java new file mode 100644 index 000000000000..11bd71b224e2 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/RequestContextThreadLocal.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.InternalThreadLocalMap; + +final class RequestContextThreadLocal { + + private static final FastThreadLocal context = new FastThreadLocal<>(); + + @SuppressWarnings("unchecked") + static T get() { + return (T) context.get(); + } + + @SuppressWarnings("unchecked") + static T getAndSet(RequestContext ctx) { + final InternalThreadLocalMap map = InternalThreadLocalMap.get(); + final FastThreadLocal context = RequestContextThreadLocal.context; + final RequestContext oldCtx = context.get(map); + context.set(map, ctx); + return (T) oldCtx; + } + + static void set(RequestContext ctx) { + context.set(ctx); + } + + static void remove() { + context.remove(); + } + + private RequestContextThreadLocal() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/Responses.java b/src/main/java/com/linecorp/armeria/common/Responses.java new file mode 100644 index 000000000000..5af6f7d5a5a4 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/Responses.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import java.util.concurrent.CompletableFuture; + +import com.linecorp.armeria.common.reactivestreams.RichPublisher; + +public final class Responses { + + public static CompletableFuture awaitClose(Object res) { + if (res instanceof RichPublisher) { + return ((RichPublisher) res).awaitClose(); + } + + if (res instanceof CompletableFuture) { + return (CompletableFuture) res; + } + + throw new IllegalStateException( + "response must be a " + RichPublisher.class.getSimpleName() + " or a " + + CompletableFuture.class.getSimpleName() + ": " + res.getClass().getName()); + } + + private Responses() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/RpcRequest.java b/src/main/java/com/linecorp/armeria/common/RpcRequest.java new file mode 100644 index 000000000000..f9fe227932a3 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/RpcRequest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import java.util.List; + +public interface RpcRequest { + Class serviceType(); + String method(); + List params(); +} diff --git a/src/main/java/com/linecorp/armeria/common/RpcResponse.java b/src/main/java/com/linecorp/armeria/common/RpcResponse.java new file mode 100644 index 000000000000..969d78a94862 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/RpcResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; + +public interface RpcResponse extends Future, CompletionStage { + Throwable getCause(); +} diff --git a/src/main/java/com/linecorp/armeria/common/Scheme.java b/src/main/java/com/linecorp/armeria/common/Scheme.java index a98fea4f87ef..29da77e5cf0d 100644 --- a/src/main/java/com/linecorp/armeria/common/Scheme.java +++ b/src/main/java/com/linecorp/armeria/common/Scheme.java @@ -153,6 +153,6 @@ public int compareTo(Scheme o) { @Override public String toString() { - return "Scheme(" + uriText() + ')'; + return uriText(); } } diff --git a/src/main/java/com/linecorp/armeria/common/SerializationFormat.java b/src/main/java/com/linecorp/armeria/common/SerializationFormat.java index fa6eb69a018a..9d1f1295fe88 100644 --- a/src/main/java/com/linecorp/armeria/common/SerializationFormat.java +++ b/src/main/java/com/linecorp/armeria/common/SerializationFormat.java @@ -16,6 +16,8 @@ package com.linecorp.armeria.common; +import static com.google.common.net.MediaType.create; + import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; @@ -26,6 +28,8 @@ import javax.annotation.Nullable; +import com.google.common.net.MediaType; + /** * Serialization format of a remote procedure call and its reply. */ @@ -34,34 +38,34 @@ public enum SerializationFormat { /** * No serialization format. Used when no serialization/deserialization is desired. */ - NONE("none", "application/x-none"), + NONE("none", create("application", "x-none")), /** * Unknown serialization format. Used when some serialization format is desired but the server * failed to understand/recognize it. */ - UNKNOWN("unknown", "application/x-unknown"), + UNKNOWN("unknown", create("application", "x-unknown")), /** * Thrift TBinary serialization format */ - THRIFT_BINARY("tbinary", "application/x-thrift; protocol=TBINARY"), + THRIFT_BINARY("tbinary", create("application", "x-thrift").withParameter("protocol", "TBINARY")), /** * Thrift TCompact serialization format */ - THRIFT_COMPACT("tcompact", "application/x-thrift; protocol=TCOMPACT"), + THRIFT_COMPACT("tcompact", create("application", "x-thrift").withParameter("protocol", "TCOMPACT")), /** * Thrift TJSON serialization format */ - THRIFT_JSON("tjson", "application/x-thrift; protocol=TJSON"), + THRIFT_JSON("tjson", create("application", "x-thrift").withParameter("protocol", "TJSON")), /** * Thrift TText serialization format. This format is not optimized for performance or backwards * compatibility and should only be used in non-production use cases like debugging. */ - THRIFT_TEXT("ttext", "application/x-thrift; protocol=TTEXT"); + THRIFT_TEXT("ttext", create("application", "x-thrift").withParameter("protocol", "TTEXT")); private static final Set THRIFT_FORMATS = Collections.unmodifiableSet( EnumSet.of(THRIFT_BINARY, THRIFT_COMPACT, THRIFT_JSON, THRIFT_TEXT)); @@ -86,47 +90,47 @@ public static Set ofThrift() { } /** - * Returns the serialization format corresponding to the passed in {@code mimeType}, or - * {@link Optional#empty} if the mimetype is not recognized. {@code null} is treated as an unknown + * Returns the serialization format corresponding to the passed in {@code mediaType}, or + * {@link Optional#empty} if the media type is not recognized. {@code null} is treated as an unknown * mimetype. */ - public static Optional fromMimeType(@Nullable String mimeType) { - if (mimeType == null || mimeType.isEmpty()) { + public static Optional fromMediaType(@Nullable String mediaType) { + if (mediaType == null || mediaType.isEmpty()) { return Optional.empty(); } - final int semicolonIdx = mimeType.indexOf(';'); + final int semicolonIdx = mediaType.indexOf(';'); final String paramPart; if (semicolonIdx >= 0) { - paramPart = mimeType.substring(semicolonIdx).toLowerCase(Locale.US); - mimeType = mimeType.substring(0, semicolonIdx).toLowerCase(Locale.US).trim(); + paramPart = mediaType.substring(semicolonIdx).toLowerCase(Locale.US); + mediaType = mediaType.substring(0, semicolonIdx).toLowerCase(Locale.US).trim(); } else { paramPart = null; - mimeType = mimeType.toLowerCase(Locale.US).trim(); + mediaType = mediaType.toLowerCase(Locale.US).trim(); } - if ("application/x-thrift".equals(mimeType)) { - return fromThriftMimeType(paramPart); + if ("application/x-thrift".equals(mediaType)) { + return fromThriftMediaType(paramPart); } - if (NONE.mimeType().equals(mimeType)) { + if (NONE.mediaType().toString().equals(mediaType)) { return Optional.of(NONE); } return Optional.empty(); } - private static Optional fromThriftMimeType(String params) { + private static Optional fromThriftMediaType(String params) { final String protocol = MimeTypeParams.find(params, "protocol"); return PROTOCOL_TO_THRIFT_FORMATS.getOrDefault(protocol, Optional.empty()); } private final String uriText; - private final String mimeType; + private final MediaType mediaType; - SerializationFormat(String uriText, String mimeType) { + SerializationFormat(String uriText, MediaType mediaType) { this.uriText = uriText; - this.mimeType = mimeType; + this.mediaType = mediaType; } /** @@ -136,10 +140,7 @@ public String uriText() { return uriText; } - /** - * Returns the MIME type of this format. - */ - public String mimeType() { - return mimeType; + public MediaType mediaType() { + return mediaType; } } diff --git a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContext.java b/src/main/java/com/linecorp/armeria/common/ServiceInvocationContext.java deleted file mode 100644 index 67a7d77c373f..000000000000 --- a/src/main/java/com/linecorp/armeria/common/ServiceInvocationContext.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; - -import static java.util.Objects.requireNonNull; - -import java.net.SocketAddress; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; -import java.util.function.Function; -import java.util.function.Supplier; - -import javax.annotation.Nullable; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.common.util.Exceptions; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.EventLoop; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.util.DefaultAttributeMap; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.ReferenceCounted; -import io.netty.util.concurrent.FastThreadLocal; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.FutureListener; -import io.netty.util.concurrent.GenericFutureListener; -import io.netty.util.concurrent.Promise; - -/** - * Provides information about an invocation and related utilities. Every remote invocation, regardless of if - * it's client side or server side, has its own {@link ServiceInvocationContext} instance. - */ -public abstract class ServiceInvocationContext extends DefaultAttributeMap { - - private static final FastThreadLocal context = new FastThreadLocal<>(); - - /** - * Returns the context of the invocation that is being handled in the current thread. - * - * @throws IllegalStateException if the context is unavailable in the current thread - */ - public static ServiceInvocationContext current() { - final ServiceInvocationContext ctx = context.get(); - if (ctx == null) { - throw new IllegalStateException(ServiceInvocationContext.class.getSimpleName() + " unavailable"); - } - return ctx; - } - - /** - * Maps the context of the invocation that is being handled in the current thread. - * - * @param mapper the {@link Function} that maps the invocation - * @param defaultValueSupplier the {@link Supplier} that provides the value when the context is unavailable - * in the current thread. If {@code null}, the {@code null} will be returned - * when the context is unavailable in the current thread. - */ - public static T mapCurrent( - Function mapper, @Nullable Supplier defaultValueSupplier) { - - final ServiceInvocationContext ctx = context.get(); - if (ctx != null) { - return mapper.apply(ctx); - } - - if (defaultValueSupplier != null) { - return defaultValueSupplier.get(); - } - - return null; - } - - /** - * (Do not use; internal use only) Set the invocation context of the current thread. - */ - public static void setCurrent(ServiceInvocationContext ctx) { - context.set(requireNonNull(ctx, "ctx")); - } - - /** - * (Do not use; internal use only) Removes the invocation context from the current thread. - */ - public static void removeCurrent() { - context.remove(); - } - - private final Channel ch; - private final Scheme scheme; - private final String host; - private final String path; - private final String mappedPath; - private final String loggerName; - private final Object originalRequest; - private Logger logger; - private String strVal; - private List onEnterCallbacks; - private List onExitCallbacks; - - /** - * Creates a new instance. - * - * @param ch the {@link Channel} that handles the invocation - * @param scheme the {@link Scheme} of the invocation - * @param host the host part of the invocation, as defined in - * the - * section 14.23 of RFC2616 - * @param path the absolute path part of the invocation, as defined in - * the - * section 5.1.2 of RFC2616 - * @param mappedPath the path with its context path removed. Same with {@code path} if client side. - * @param loggerName the name of the {@link Logger} which is returned by {@link #logger()} - * @param originalRequest the session-level protocol message which triggered the invocation - * e.g. {@link FullHttpRequest} - */ - protected ServiceInvocationContext( - Channel ch, Scheme scheme, String host, String path, String mappedPath, - String loggerName, Object originalRequest) { - - this.ch = requireNonNull(ch, "ch"); - this.scheme = requireNonNull(scheme, "scheme"); - this.host = requireNonNull(host, "host"); - this.path = requireNonNull(path, "path"); - this.mappedPath = requireNonNull(mappedPath, "mappedPath"); - this.loggerName = requireNonNull(loggerName, "loggerName"); - this.originalRequest = originalRequest; - } - - Channel channel() { - return ch; - } - - /** - * Returns the {@link EventLoop} that is handling this invocation. - */ - public final EventLoop eventLoop() { - return channel().eventLoop(); - } - - /** - * Returns an {@link EventLoop} that will make sure this invocation is set - * as the current invocation before executing any callback. This should - * almost always be used for executing asynchronous callbacks in service - * code to make sure features that require the invocation context work - * properly. Most asynchronous libraries like - * {@link java.util.concurrent.CompletableFuture} provide methods that - * accept an {@link Executor} to run callbacks on. - */ - public final EventLoop contextAwareEventLoop() { - return new ServiceInvocationContextAwareEventLoop(this, eventLoop()); - } - - /** - * Returns an {@link Executor} that will execute callbacks in the given - * {@code executor}, making sure to propagate the current invocation context - * into the callback execution. It is generally preferred to use - * {@link #contextAwareEventLoop()} to ensure the callback stays on the - * same thread as well. - */ - public final Executor makeContextAware(Executor executor) { - return runnable -> executor.execute(makeContextAware(runnable)); - } - - /** - * Returns a {@link Callable} that makes sure the current invocation context - * is set and then invokes the input {@code callable}. - */ - public final Callable makeContextAware(Callable callable) { - ServiceInvocationContext propagatedContext = this; - return () -> { - boolean mustResetContext = propagateContextIfNotPresent(propagatedContext); - try { - return callable.call(); - } finally { - if (mustResetContext) { - resetContext(propagatedContext); - } - } - }; - } - - /** - * Returns a {@link Runnable} that makes sure the current invocation context - * is set and then invokes the input {@code runnable}. - */ - public final Runnable makeContextAware(Runnable runnable) { - ServiceInvocationContext propagatedContext = this; - return () -> { - boolean mustResetContext = propagateContextIfNotPresent(propagatedContext); - try { - runnable.run(); - } finally { - if (mustResetContext) { - resetContext(propagatedContext); - } - } - }; - } - - /** - * Returns a {@link FutureListener} that makes sure the current invocation - * context is set and then invokes the input {@code listener}. - */ - public final FutureListener makeContextAware(FutureListener listener) { - ServiceInvocationContext propagatedContext = this; - return future -> { - boolean mustResetContext = propagateContextIfNotPresent(propagatedContext); - try { - listener.operationComplete(future); - } finally { - if (mustResetContext) { - resetContext(propagatedContext); - } - } - }; - } - - /** - * Returns a {@link ChannelFutureListener} that makes sure the current invocation - * context is set and then invokes the input {@code listener}. - */ - public final ChannelFutureListener makeContextAware(ChannelFutureListener listener) { - ServiceInvocationContext propagatedContext = this; - return future -> { - boolean mustResetContext = propagateContextIfNotPresent(propagatedContext); - try { - listener.operationComplete(future); - } finally { - if (mustResetContext) { - resetContext(propagatedContext); - } - } - }; - } - - /** - * Returns a {@link ChannelFutureListener} that makes sure the current invocation - * context is set and then invokes the input {@code listener}. - */ - final > GenericFutureListener makeContextAware(GenericFutureListener listener) { - ServiceInvocationContext propagatedContext = this; - return future -> { - boolean mustResetContext = propagateContextIfNotPresent(propagatedContext); - try { - listener.operationComplete(future); - } finally { - if (mustResetContext) { - resetContext(propagatedContext); - } - } - }; - } - - private static void resetContext(ServiceInvocationContext ctx) { - removeCurrent(); - if (ctx.onExitCallbacks != null) { - ctx.onExitCallbacks.forEach(Runnable::run); - } - } - - private static boolean propagateContextIfNotPresent(ServiceInvocationContext propagatedContext) { - return mapCurrent(currentContext -> { - if (!currentContext.equals(propagatedContext)) { - throw new IllegalStateException( - "Trying to call object made with makeContextAware or object on executor made with " + - "makeContextAware with context " + propagatedContext + - ", but context is currently set to " + currentContext + ". This means the " + - "callback was passed from one invocation to another which is not allowed. Make " + - "sure you are not saving callbacks into shared state."); - } - return false; - }, () -> { - setCurrent(propagatedContext); - if (propagatedContext.onEnterCallbacks != null) { - propagatedContext.onEnterCallbacks.forEach(Runnable::run); - } - return true; - }); - } - - /** - * Registers {@code callback} to be run when re-entering this {@link ServiceInvocationContext}, - * usually when using the {@link #makeContextAware} family of methods. Any thread-local state - * associated with this context should be restored by this callback. - */ - public ServiceInvocationContext onEnter(Runnable callback) { - if (onEnterCallbacks == null) { - onEnterCallbacks = new ArrayList<>(4); - } - onEnterCallbacks.add(callback); - return this; - } - - /** - * Registers {@code callback} to be run when re-exiting this {@link ServiceInvocationContext}, - * usually when using the {@link #makeContextAware} family of methods. Any thread-local state - * associated with this context should be reset by this callback. - */ - public ServiceInvocationContext onExit(Runnable callback) { - if (onExitCallbacks == null) { - onExitCallbacks = new ArrayList<>(4); - } - onExitCallbacks.add(callback); - return this; - } - - /** - * Returns the {@link ByteBufAllocator} used by the connection that is handling this invocation. Use this - * {@link ByteBufAllocator} when allocating a new {@link ByteBuf}, so that the same pool is used for buffer - * allocation and thus the pool is utilized fully. - */ - public final ByteBufAllocator alloc() { - return channel().alloc(); - } - - /** - * Returns the {@link Logger} which logs information about this invocation as the prefix of log messages. - * e.g. If a user called {@code ctx.logger().info("Hello")}, - *
{@code
-     * [id: 0x270781f4, /127.0.0.1:63466 => /127.0.0.1:63432][tbinary+h2c://example.com/path#method][42] Hello
-     * }
- */ - public final Logger logger() { - Logger logger = this.logger; - if (logger == null) { - this.logger = logger = new ServiceInvocationAwareLogger(this, LoggerFactory.getLogger(loggerName)); - } - return logger; - } - - /** - * Returns the {@link Scheme} of this invocation. - */ - public final Scheme scheme() { - return scheme; - } - - /** - * Returns the host part of this invocation, as defined in - * the - * section 14.23 of RFC2616. e.g. {@code "example.com"} and not {@code "example.com:8080"} - * - * @see URI#getHost() - */ - public final String host() { - return host; - } - - /** - * Returns the absolute path part of this invocation, as defined in - * the - * section 5.1.2 of RFC2616. - */ - public final String path() { - return path; - } - - /** - * Returns the path with its context path removed. This method can be useful for a reusable service bound - * at various path prefixes. For client side invocations, this method always returns the same value as - * {@link #path()}. - */ - public final String mappedPath() { - return mappedPath; - } - - /** - * Returns the remote address of this invocation. - */ - public final SocketAddress remoteAddress() { - return ch.remoteAddress(); - } - - /** - * Returns the local address of this invocation. - */ - public final SocketAddress localAddress() { - return ch.localAddress(); - } - - /** - * Returns the ID of this invocation. Note that the ID returned by this method is only for debugging - * purposes and thus is never guaranteed to be unique. - */ - public abstract String invocationId(); - - /** - * Returns the method name of this invocation. - */ - public abstract String method(); - - /** - * Returns the parameter types of this invocation. Note that this is potentially an expensive operation. - */ - public abstract List> paramTypes(); - - /** - * Returns the return type of this invocation. - */ - public abstract Class returnType(); - - /** - * Returns the parameters of this invocation. - */ - public abstract List params(); - - /** - * Returns the session-level protocol message which triggered this invocation. e.g. {@link FullHttpRequest} - */ - @SuppressWarnings("unchecked") - public T originalRequest() { - return (T) originalRequest; - } - - /** - * Resolves the specified {@code promise} with the specified {@code result} so that the {@code promise} is - * marked as 'done'. If {@code promise} is done already, this method does the following: - *
    - *
  • Log a warning about the failure, and
  • - *
  • Release {@code result} if it is {@linkplain ReferenceCounted a reference-counted object}, - * such as {@link ByteBuf} and {@link FullHttpResponse}.
  • - *
- * Note that a {@link Promise} can be done already even if you did not call this method in the following - * cases: - *
    - *
  • Invocation timeout - The invocation associated with the {@link Promise} has been timed out.
  • - *
  • User error - A service implementation called any of the following methods more than once: - *
      - *
    • {@link #resolvePromise(Promise, Object)}
    • - *
    • {@link #rejectPromise(Promise, Throwable)}
    • - *
    • {@link Promise#setSuccess(Object)}
    • - *
    • {@link Promise#setFailure(Throwable)}
    • - *
    • {@link Promise#cancel(boolean)}
    • - *
    - *
  • - *
- */ - public void resolvePromise(Promise promise, Object result) { - @SuppressWarnings("unchecked") - final Promise castPromise = (Promise) promise; - - if (castPromise.trySuccess(result)) { - // Resolved successfully. - return; - } - - try { - if (!(promise.cause() instanceof TimeoutException)) { - // Log resolve failure unless it is due to a timeout. - logger().warn("Failed to resolve a completed promise ({}) with {}", promise, result); - } - } finally { - ReferenceCountUtil.safeRelease(result); - } - } - - /** - * Rejects the specified {@code promise} with the specified {@code cause}. If {@code promise} is done - * already, this method logs a warning about the failure. Note that a {@link Promise} can be done already - * even if you did not call this method in the following cases: - *
    - *
  • Invocation timeout - The invocation associated with the {@link Promise} has been timed out.
  • - *
  • User error - A service implementation called any of the following methods more than once: - *
      - *
    • {@link #resolvePromise(Promise, Object)}
    • - *
    • {@link #rejectPromise(Promise, Throwable)}
    • - *
    • {@link Promise#setSuccess(Object)}
    • - *
    • {@link Promise#setFailure(Throwable)}
    • - *
    • {@link Promise#cancel(boolean)}
    • - *
    - *
  • - *
- */ - public void rejectPromise(Promise promise, Throwable cause) { - if (promise.tryFailure(cause)) { - // Fulfilled successfully. - return; - } - - final Throwable firstCause = promise.cause(); - if (firstCause instanceof TimeoutException) { - // Timed out already. - return; - } - - if (Exceptions.isExpected(cause)) { - // The exception that was thrown after firstCause (often a transport-layer exception) - // was a usual expected exception, not an error. - return; - } - - logger().warn("Failed to reject a completed promise ({}) with {}", promise, cause, cause); - } - - @Override - public final String toString() { - String strVal = this.strVal; - if (strVal == null) { - final StringBuilder buf = new StringBuilder(64); - - buf.append(channel()); - buf.append('['); - buf.append(scheme().uriText()); - buf.append("://"); - buf.append(host()); - buf.append(path()); - buf.append('#'); - buf.append(method()); - buf.append("]["); - buf.append(invocationId()); - buf.append(']'); - - this.strVal = strVal = buf.toString(); - } - - return strVal; - } -} diff --git a/src/main/java/com/linecorp/armeria/common/SessionProtocol.java b/src/main/java/com/linecorp/armeria/common/SessionProtocol.java index 78e4cb0cb044..7d8001bea5af 100644 --- a/src/main/java/com/linecorp/armeria/common/SessionProtocol.java +++ b/src/main/java/com/linecorp/armeria/common/SessionProtocol.java @@ -27,27 +27,27 @@ public enum SessionProtocol { /** * HTTP (cleartext, HTTP/2 preferred) */ - HTTP(false, "http", false), + HTTP(false, "http", false, 80), /** * HTTP over TLS (over TLS, HTTP/2 preferred) */ - HTTPS(true, "https", false), + HTTPS(true, "https", false, 443), /** * HTTP/1 (over TLS) */ - H1(true, "h1", false), + H1(true, "h1", false, 443), /** * HTTP/1 (cleartext) */ - H1C(false, "h1c", false), + H1C(false, "h1c", false, 80), /** * HTTP/2 (over TLS) */ - H2(true, "h2", true), + H2(true, "h2", true, 443), /** * HTTP/2 (cleartext) */ - H2C(false, "h2c", true); + H2C(false, "h2c", true, 80); private static final Set HTTP_PROTOCOLS = Collections.unmodifiableSet( EnumSet.of(HTTP, HTTPS, H1, H1C, H2, H2C)); @@ -64,11 +64,13 @@ public static Set ofHttp() { private final boolean useTls; private final String uriText; private final boolean isMultiplex; + private final int defaultPort; - SessionProtocol(boolean useTls, String uriText, boolean isMultiplex) { + SessionProtocol(boolean useTls, String uriText, boolean isMultiplex, int defaultPort) { this.useTls = useTls; this.uriText = uriText; this.isMultiplex = isMultiplex; + this.defaultPort = defaultPort; } /** @@ -92,4 +94,11 @@ public String uriText() { public boolean isMultiplex() { return isMultiplex; } + + /** + * Returns the default INET port number of this protocol. + */ + public int defaultPort() { + return defaultPort; + } } diff --git a/src/main/java/com/linecorp/armeria/common/TimeoutPolicy.java b/src/main/java/com/linecorp/armeria/common/TimeoutPolicy.java deleted file mode 100644 index 8abd3f243d08..000000000000 --- a/src/main/java/com/linecorp/armeria/common/TimeoutPolicy.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common; - -import static java.util.Objects.requireNonNull; - -import java.time.Duration; -import java.util.function.Function; - -/** - * Determines when an invocation should time out. - */ -@FunctionalInterface -public interface TimeoutPolicy { - - /** - * Creates a new {@link TimeoutPolicy} that times out an invocation after a fixed amount of time. - * - * @param timeout a positive value to enable timeout. - * zero to disable timeout. - * - * @throws IllegalArgumentException if the specified {@code timeout} is negative - */ - static TimeoutPolicy ofFixed(Duration timeout) { - requireNonNull(timeout, "timeout"); - - if (timeout.isNegative()) { - throw new IllegalArgumentException("timeout: " + timeout + " (expected: >= 0)"); - } - - if (timeout.isZero()) { - return disabled(); - } - - return new FixedTimeoutPolicy(timeout); - } - - /** - * Returns a singleton instance of a {@link TimeoutPolicy} that disables timeout. - */ - static TimeoutPolicy disabled() { - return DisabledTimeoutPolicy.INSTANCE; - } - - /** - * Creates a new {@link TimeoutPolicy} decorated with the specified {@code decorator}. - */ - default TimeoutPolicy decorate(Function decorator) { - @SuppressWarnings("unchecked") - final TimeoutPolicy newPolicy = decorator.apply((T) this); - if (newPolicy == null) { - throw new NullPointerException("decorator.apply() returned null: " + decorator); - } - - return newPolicy; - } - - /** - * Determines the timeout of the invocation associated with the specified {@link ServiceInvocationContext}. - * - * @return the number of milliseconds to apply timeout to the invocation. - * {@code 0} to disable timeout for the invocation. - */ - long timeout(ServiceInvocationContext ctx); -} diff --git a/src/main/java/com/linecorp/armeria/common/http/AggregatedHttpMessage.java b/src/main/java/com/linecorp/armeria/common/http/AggregatedHttpMessage.java new file mode 100644 index 000000000000..6da9a1b3baac --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/AggregatedHttpMessage.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +public interface AggregatedHttpMessage { + + static AggregatedHttpMessage of(HttpMethod method, String path) { + return of(HttpHeaders.of(method, path)); + } + + static AggregatedHttpMessage of(HttpMethod method, String path, HttpData content) { + return of(HttpHeaders.of(method, path), content); + } + + static AggregatedHttpMessage of(int statusCode) { + return of(HttpStatus.valueOf(statusCode)); + } + + static AggregatedHttpMessage of(HttpStatus status) { + return of(HttpHeaders.of(status)); + } + + static AggregatedHttpMessage of(HttpStatus status, HttpData content) { + return of(HttpHeaders.of(status), content); + } + + static AggregatedHttpMessage of(HttpHeaders headers) { + return of(headers, HttpData.EMPTY_DATA, HttpHeaders.EMPTY_HEADERS); + } + static AggregatedHttpMessage of(HttpHeaders headers, HttpData content) { + return of(headers, content, HttpHeaders.EMPTY_HEADERS); + } + + static AggregatedHttpMessage of(HttpHeaders headers, HttpData content, HttpHeaders trailingHeaders) { + return of(Collections.emptyList(), headers, content, trailingHeaders); + } + + static AggregatedHttpMessage of(Iterable informationals, HttpHeaders headers, + HttpData content, HttpHeaders trailingHeaders) { + + requireNonNull(informationals, "informationals"); + requireNonNull(headers, "headers"); + requireNonNull(content, "content"); + requireNonNull(trailingHeaders, "trailingHeaders"); + + final List informationalList; + if (informationals instanceof List) { + informationalList = Collections.unmodifiableList((List) informationals); + } else { + informationalList = ImmutableList.builder().addAll(informationals).build(); + } + + return new DefaultAggregatedHttpMessage(informationalList, headers, content, trailingHeaders); + } + + List informationals(); + + HttpHeaders headers(); + + HttpHeaders trailingHeaders(); + + HttpData content(); + + default String scheme() { + return headers().scheme(); + } + + default HttpMethod method() { + return headers().method(); + } + + default String path() { + return headers().path(); + } + + default String authority() { + return headers().authority(); + } + + default HttpStatus status() { + return headers().status(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/DefaultAggregatedHttpMessage.java b/src/main/java/com/linecorp/armeria/common/http/DefaultAggregatedHttpMessage.java new file mode 100644 index 000000000000..985c9164596a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/DefaultAggregatedHttpMessage.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.List; + +import com.google.common.base.MoreObjects; +import com.google.common.base.MoreObjects.ToStringHelper; + +final class DefaultAggregatedHttpMessage implements AggregatedHttpMessage { + + private final List informationals; + private final HttpHeaders headers; + private final HttpData content; + private final HttpHeaders trailingHeaders; + + DefaultAggregatedHttpMessage(List informationals, HttpHeaders headers, + HttpData content, HttpHeaders trailingHeaders) { + this.informationals = informationals; + this.headers = headers; + this.content = content; + this.trailingHeaders = trailingHeaders; + } + + @Override + public List informationals() { + return informationals; + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public HttpHeaders trailingHeaders() { + return trailingHeaders; + } + + @Override + public HttpData content() { + return content; + } + + @Override + public String toString() { + final ToStringHelper helper = MoreObjects.toStringHelper(this); + + if (!informationals().isEmpty()) { + helper.add("informationals", informationals()); + } + + helper.add("headers", headers()) + .add("content", content()); + + if (!trailingHeaders().isEmpty()) { + helper.add("trailingHandlers", trailingHeaders()); + } + + return helper.toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/DefaultHttpData.java b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpData.java new file mode 100644 index 000000000000..af6785b5d1e5 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpData.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import com.google.common.base.MoreObjects; + +final class DefaultHttpData implements HttpData { + + static final HttpData EMPTY_DATA = new DefaultHttpData(new byte[0], 0, 0); + + private final byte[] data; + private final int offset; + private final int length; + + DefaultHttpData(byte[] data, int offset, int length) { + this.data = data; + this.offset = offset; + this.length = length; + } + + @Override + public byte[] array() { + return data; + } + + @Override + public int offset() { + return offset; + } + + @Override + public int length() { + return length; + } + + @Override + public int hashCode() { + final int end = offset + length; + int hash = 0; + for (int i = offset; i < end; i++) { + hash = hash *31 + data[i]; + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HttpData)) { + return false; + } + + if (this == obj) { + return true; + } + + final HttpData that = (HttpData) obj; + if (length() != that.length()) { + return false; + } + + final int endOffset = offset + length; + for (int i = offset, j = that.offset(); i < endOffset; i++, j++) { + if (data[i] != data[j]) { + return false; + } + } + + return true; + } + + @Override + @SuppressWarnings("ImplicitArrayToString") + public String toString() { + return MoreObjects.toStringHelper(this) + .add("offset", offset) + .add("length", length) + .add("array", data.toString()).toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/DefaultHttpHeaders.java b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpHeaders.java new file mode 100644 index 000000000000..21b82b083922 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpHeaders.java @@ -0,0 +1,199 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.connectionError; +import static io.netty.util.AsciiString.isUpperCase; +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import com.linecorp.armeria.common.SessionProtocol; + +import io.netty.handler.codec.DefaultHeaders; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.util.AsciiString; +import io.netty.util.HashingStrategy; +import io.netty.util.internal.PlatformDependent; + +public final class DefaultHttpHeaders + extends DefaultHeaders implements HttpHeaders { + + private static final HashingStrategy CASE_SENSITIVE_HASHER = + new HashingStrategy() { + @Override + public int hashCode(AsciiString o) { + return AsciiString.hashCode(o); + } + + @Override + public boolean equals(AsciiString a, AsciiString b) { + return AsciiString.contentEquals(a, b); + } + }; + + private static final NameValidator HTTP2_NAME_VALIDATOR = name -> { + final int index; + try { + index = name.forEachByte(value -> !isUpperCase(value)); + } catch (Http2Exception e) { + PlatformDependent.throwException(e); + return; + } catch (Throwable t) { + PlatformDependent.throwException(connectionError(PROTOCOL_ERROR, t, + "unexpected error. invalid header name [%s]", name)); + return; + } + + if (index != -1) { + PlatformDependent.throwException(connectionError(PROTOCOL_ERROR, + "invalid header name [%s]", name)); + } + }; + + private HttpMethod method; + private HttpStatus status; + + public DefaultHttpHeaders() { + this(true); + } + + public DefaultHttpHeaders(boolean validate) { + this(validate, 16); + } + + public DefaultHttpHeaders(boolean validate, int initialCapacity) { + super(CASE_SENSITIVE_HASHER, + StringValueConverter.INSTANCE, + validate ? HTTP2_NAME_VALIDATOR : NameValidator.NOT_NULL, initialCapacity); + } + + @Override + public HttpHeaders method(HttpMethod method) { + requireNonNull(method, "method"); + this.method = method; + set(HttpHeaderNames.METHOD, method.name()); + return this; + } + + @Override + public HttpHeaders scheme(String scheme) { + requireNonNull(scheme, "scheme"); + set(HttpHeaderNames.SCHEME, scheme); + return this; + } + + @Override + public HttpHeaders authority(String authority) { + requireNonNull(authority, "authority"); + set(HttpHeaderNames.AUTHORITY, authority); + return this; + } + + @Override + public HttpHeaders path(String path) { + requireNonNull(path, "path"); + set(HttpHeaderNames.PATH, path); + return this; + } + + @Override + public HttpHeaders status(int statusCode) { + final HttpStatus status = this.status = HttpStatus.valueOf(statusCode); + set(HttpHeaderNames.STATUS, status.codeAsText()); + return this; + } + + @Override + public HttpHeaders status(HttpStatus status) { + requireNonNull(status, "status"); + return status(status.code()); + } + + @Override + public HttpMethod method() { + HttpMethod method = this.method; + if (method != null) { + return method; + } + + final String methodStr = get(HttpHeaderNames.METHOD); + if (methodStr == null) { + return null; + } + + try { + return this.method = HttpMethod.valueOf(methodStr); + } catch (IllegalArgumentException ignored) { + throw new IllegalStateException("unknown method: " + methodStr); + } + } + + @Override + public String scheme() { + return get(HttpHeaderNames.SCHEME); + } + + @Override + public String authority() { + return get(HttpHeaderNames.AUTHORITY); + } + + @Override + public String path() { + return get(HttpHeaderNames.PATH); + } + + @Override + public HttpStatus status() { + HttpStatus status = this.status; + if (status != null) { + return status; + } + + final String statusStr = get(HttpHeaderNames.STATUS); + if (statusStr == null) { + return null; + } + + try { + return this.status = HttpStatus.valueOf(Integer.valueOf(statusStr)); + } catch (NumberFormatException ignored) { + throw new IllegalStateException("invalid status: " + statusStr); + } + } + + @Override + public String toString() { + final int size = size(); + if (size == 0) { + return "[]"; + } + + final StringBuilder buf = new StringBuilder(size() * 16).append('['); + String separator = ""; + for (AsciiString name : names()) { + List values = getAll(name); + for (int i = 0; i < values.size(); ++i) { + buf.append(separator); + buf.append(name).append('=').append(values.get(i)); + } + separator = ", "; + } + return buf.append(']').toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/DefaultHttpRequest.java b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpRequest.java new file mode 100644 index 000000000000..68e2dcfd634f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpRequest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.MoreObjects; + +import com.linecorp.armeria.common.reactivestreams.QueueBasedPublisher; + +public class DefaultHttpRequest + extends QueueBasedPublisher implements HttpRequest, HttpRequestWriter { + + private final HttpHeaders headers; + private final boolean keepAlive; + + public DefaultHttpRequest() { + this(new DefaultHttpHeaders()); + } + + public DefaultHttpRequest(HttpHeaders headers) { + this(headers, true); + } + + public DefaultHttpRequest(HttpMethod method, String path) { + this(HttpHeaders.of(method, path)); + } + + public DefaultHttpRequest(HttpHeaders headers, boolean keepAlive) { + this.headers = requireNonNull(headers, "headers"); + this.keepAlive = keepAlive; + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public boolean isKeepAlive() { + return keepAlive; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("keepAlive", isKeepAlive()) + .add("headers", headers()).toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/DisabledTimeoutPolicy.java b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpResponse.java similarity index 63% rename from src/main/java/com/linecorp/armeria/common/DisabledTimeoutPolicy.java rename to src/main/java/com/linecorp/armeria/common/http/DefaultHttpResponse.java index bc59fbabdc5b..f754306fde87 100644 --- a/src/main/java/com/linecorp/armeria/common/DisabledTimeoutPolicy.java +++ b/src/main/java/com/linecorp/armeria/common/http/DefaultHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -14,21 +14,17 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.http; -final class DisabledTimeoutPolicy implements TimeoutPolicy { +import com.google.common.base.MoreObjects; - static final DisabledTimeoutPolicy INSTANCE = new DisabledTimeoutPolicy(); +import com.linecorp.armeria.common.reactivestreams.QueueBasedPublisher; - private DisabledTimeoutPolicy() {} - - @Override - public long timeout(ServiceInvocationContext ctx) { - return 0; - } +public class DefaultHttpResponse + extends QueueBasedPublisher implements HttpResponse, HttpResponseWriter { @Override public String toString() { - return "disabled"; + return MoreObjects.toStringHelper(this).toString(); } } diff --git a/src/main/java/com/linecorp/armeria/common/http/HeaderDateFormat.java b/src/main/java/com/linecorp/armeria/common/http/HeaderDateFormat.java new file mode 100644 index 000000000000..b06d59316594 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HeaderDateFormat.java @@ -0,0 +1,115 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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. + */ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import io.netty.util.concurrent.FastThreadLocal; + +/** + * This {@link DateFormat} decodes 3 formats of {@link Date}. + *

+ *

    + *
  • Sun, 06 Nov 1994 08:49:37 GMT: standard specification, the only one with valid generation
  • + *
  • Sun, 06 Nov 1994 08:49:37 GMT: obsolete specification
  • + *
  • Sun Nov 6 08:49:37 1994: obsolete specification
  • + *
+ */ +final class HeaderDateFormat { + private static final FastThreadLocal dateFormatThreadLocal = + new FastThreadLocal() { + @Override + protected HeaderDateFormat initialValue() { + return new HeaderDateFormat(); + } + }; + + static HeaderDateFormat get() { + return dateFormatThreadLocal.get(); + } + + /** + * Standard date format: + *

+ *

+     * Sun, 06 Nov 1994 08:49:37 GMT -> E, d MMM yyyy HH:mm:ss z
+     * 
+ */ + private final DateFormat dateFormat1 = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale + .ENGLISH); + + /** + * First obsolete format: + *

+ *

+     * Sunday, 06-Nov-94 08:49:37 GMT -> E, d-MMM-y HH:mm:ss z
+     * 
+ */ + private final DateFormat dateFormat2 = new SimpleDateFormat("E, dd-MMM-yy HH:mm:ss z", Locale.ENGLISH); + + /** + * Second obsolete format + *

+ *

+     * Sun Nov 6 08:49:37 1994 -> EEE, MMM d HH:mm:ss yyyy
+     * 
+ */ + private final DateFormat dateFormat3 = new SimpleDateFormat("E MMM d HH:mm:ss yyyy", Locale.ENGLISH); + + private HeaderDateFormat() { + TimeZone tz = TimeZone.getTimeZone("GMT"); + dateFormat1.setTimeZone(tz); + dateFormat2.setTimeZone(tz); + dateFormat3.setTimeZone(tz); + } + + long parse(String text) throws ParseException { + Date date = dateFormat1.parse(text); + if (date == null) { + date = dateFormat2.parse(text); + } + if (date == null) { + date = dateFormat3.parse(text); + } + if (date == null) { + throw new ParseException(text, 0); + } + return date.getTime(); + } + + String format(long timeMillis) { + return dateFormat1.format(new Date(timeMillis)); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpData.java b/src/main/java/com/linecorp/armeria/common/http/HttpData.java new file mode 100644 index 000000000000..3019408eb409 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpData.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static java.util.Objects.requireNonNull; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +public interface HttpData extends HttpObject { + + HttpData EMPTY_DATA = DefaultHttpData.EMPTY_DATA; + + static HttpData of(byte[] data) { + requireNonNull(data, "data"); + if (data.length == 0) { + return EMPTY_DATA; + } + + return new DefaultHttpData(data, 0, data.length); + } + + static HttpData of(byte[] data, int offset, int length) { + requireNonNull(data); + if (offset < 0 || length < 0 || offset > data.length - length) { + throw new ArrayIndexOutOfBoundsException( + "offset: " + offset + ", length: " + length + ", data.length: " + data.length); + } + if (data.length == 0) { + return EMPTY_DATA; + } + + return new DefaultHttpData(data, offset, length); + } + + static HttpData of(Charset charset, String text) { + requireNonNull(charset, "charset"); + requireNonNull(text, "text"); + if (text.isEmpty()) { + return EMPTY_DATA; + } + + return of(text.getBytes(charset)); + } + + static HttpData of(Charset charset, String format, Object... args) { + requireNonNull(charset, "charset"); + requireNonNull(format, "format"); + requireNonNull(args, "args"); + return of(String.format(Locale.ENGLISH, format, args).getBytes(charset)); + } + + static HttpData ofUtf8(String text) { + return of(StandardCharsets.UTF_8, text); + } + + static HttpData ofUtf8(String format, Object... args) { + return of(StandardCharsets.UTF_8, format, args); + } + + static HttpData ofAscii(String text) { + return of(StandardCharsets.US_ASCII, text); + } + + static HttpData ofAscii(String format, Object... args) { + return of(StandardCharsets.US_ASCII, format, args); + } + + byte[] array(); + + int offset(); + + int length(); + + default boolean isEmpty() { + return length() == 0; + } + + default String toString(Charset charset) { + requireNonNull(charset, "charset"); + return new String(array(), offset(), length(), charset); + } + + default String toStringUtf8() { + return toString(StandardCharsets.UTF_8); + } + + default String toStringAscii() { + return toString(StandardCharsets.US_ASCII); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpHeaderNames.java b/src/main/java/com/linecorp/armeria/common/http/HttpHeaderNames.java new file mode 100644 index 000000000000..2bde12f06147 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpHeaderNames.java @@ -0,0 +1,427 @@ +/* + * Copyright 2015 LINE Corporation + * + * LINE Corporation licenses this file to you 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. + */ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.common.http; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Locale; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +import io.netty.util.AsciiString; + +/** + * Standard HTTP header names. + *

+ * These are all defined as lowercase to support HTTP/2 requirements while also not + * violating HTTP/1.x requirements. New header names should always be lowercase. + */ +public final class HttpHeaderNames { + + // Pseudo-headers + + /** + * {@code ":method"} + */ + public static final AsciiString METHOD = new AsciiString(":method"); + /** + * {@code ":scheme"} + */ + public static final AsciiString SCHEME = new AsciiString(":scheme"); + /** + * {@code ":authority"} + */ + public static final AsciiString AUTHORITY = new AsciiString(":authority"); + /** + * {@code ":path"} + */ + public static final AsciiString PATH = new AsciiString(":path"); + /** + * {@code ":status"} + */ + public static final AsciiString STATUS = new AsciiString(":status"); + + // Ordinary headers + + /** + * {@code "accept"} + */ + public static final AsciiString ACCEPT = new AsciiString("accept"); + /** + * {@code "accept-charset"} + */ + public static final AsciiString ACCEPT_CHARSET = new AsciiString("accept-charset"); + /** + * {@code "accept-encoding"} + */ + public static final AsciiString ACCEPT_ENCODING = new AsciiString("accept-encoding"); + /** + * {@code "accept-language"} + */ + public static final AsciiString ACCEPT_LANGUAGE = new AsciiString("accept-language"); + /** + * {@code "accept-ranges"} + */ + public static final AsciiString ACCEPT_RANGES = new AsciiString("accept-ranges"); + /** + * {@code "accept-patch"} + */ + public static final AsciiString ACCEPT_PATCH = new AsciiString("accept-patch"); + /** + * {@code "access-control-allow-credentials"} + */ + public static final AsciiString ACCESS_CONTROL_ALLOW_CREDENTIALS = + new AsciiString("access-control-allow-credentials"); + /** + * {@code "access-control-allow-headers"} + */ + public static final AsciiString ACCESS_CONTROL_ALLOW_HEADERS = + new AsciiString("access-control-allow-headers"); + /** + * {@code "access-control-allow-methods"} + */ + public static final AsciiString ACCESS_CONTROL_ALLOW_METHODS = + new AsciiString("access-control-allow-methods"); + /** + * {@code "access-control-allow-origin"} + */ + public static final AsciiString ACCESS_CONTROL_ALLOW_ORIGIN = + new AsciiString("access-control-allow-origin"); + /** + * {@code "access-control-expose-headers"} + */ + public static final AsciiString ACCESS_CONTROL_EXPOSE_HEADERS = + new AsciiString("access-control-expose-headers"); + /** + * {@code "access-control-max-age"} + */ + public static final AsciiString ACCESS_CONTROL_MAX_AGE = new AsciiString("access-control-max-age"); + /** + * {@code "access-control-request-headers"} + */ + public static final AsciiString ACCESS_CONTROL_REQUEST_HEADERS = + new AsciiString("access-control-request-headers"); + /** + * {@code "access-control-request-method"} + */ + public static final AsciiString ACCESS_CONTROL_REQUEST_METHOD = + new AsciiString("access-control-request-method"); + /** + * {@code "age"} + */ + public static final AsciiString AGE = new AsciiString("age"); + /** + * {@code "allow"} + */ + public static final AsciiString ALLOW = new AsciiString("allow"); + /** + * {@code "authorization"} + */ + public static final AsciiString AUTHORIZATION = new AsciiString("authorization"); + /** + * {@code "cache-control"} + */ + public static final AsciiString CACHE_CONTROL = new AsciiString("cache-control"); + /** + * {@code "connection"} + */ + public static final AsciiString CONNECTION = new AsciiString("connection"); + /** + * {@code "content-base"} + */ + public static final AsciiString CONTENT_BASE = new AsciiString("content-base"); + /** + * {@code "content-encoding"} + */ + public static final AsciiString CONTENT_ENCODING = new AsciiString("content-encoding"); + /** + * {@code "content-language"} + */ + public static final AsciiString CONTENT_LANGUAGE = new AsciiString("content-language"); + /** + * {@code "content-length"} + */ + public static final AsciiString CONTENT_LENGTH = new AsciiString("content-length"); + /** + * {@code "content-location"} + */ + public static final AsciiString CONTENT_LOCATION = new AsciiString("content-location"); + /** + * {@code "content-transfer-encoding"} + */ + public static final AsciiString CONTENT_TRANSFER_ENCODING = new AsciiString("content-transfer-encoding"); + /** + * {@code "content-disposition"} + */ + public static final AsciiString CONTENT_DISPOSITION = new AsciiString("content-disposition"); + /** + * {@code "content-md5"} + */ + public static final AsciiString CONTENT_MD5 = new AsciiString("content-md5"); + /** + * {@code "content-range"} + */ + public static final AsciiString CONTENT_RANGE = new AsciiString("content-range"); + /** + * {@code "content-type"} + */ + public static final AsciiString CONTENT_TYPE = new AsciiString("content-type"); + /** + * {@code "cookie"} + */ + public static final AsciiString COOKIE = new AsciiString("cookie"); + /** + * {@code "date"} + */ + public static final AsciiString DATE = new AsciiString("date"); + /** + * {@code "etag"} + */ + public static final AsciiString ETAG = new AsciiString("etag"); + /** + * {@code "expect"} + */ + public static final AsciiString EXPECT = new AsciiString("expect"); + /** + * {@code "expires"} + */ + public static final AsciiString EXPIRES = new AsciiString("expires"); + /** + * {@code "from"} + */ + public static final AsciiString FROM = new AsciiString("from"); + /** + * {@code "host"} + */ + public static final AsciiString HOST = new AsciiString("host"); + /** + * {@code "if-match"} + */ + public static final AsciiString IF_MATCH = new AsciiString("if-match"); + /** + * {@code "if-modified-since"} + */ + public static final AsciiString IF_MODIFIED_SINCE = new AsciiString("if-modified-since"); + /** + * {@code "if-none-match"} + */ + public static final AsciiString IF_NONE_MATCH = new AsciiString("if-none-match"); + /** + * {@code "if-range"} + */ + public static final AsciiString IF_RANGE = new AsciiString("if-range"); + /** + * {@code "if-unmodified-since"} + */ + public static final AsciiString IF_UNMODIFIED_SINCE = new AsciiString("if-unmodified-since"); + /** + * @deprecated use {@link #CONNECTION} + * + * {@code "keep-alive"} + */ + @Deprecated + public static final AsciiString KEEP_ALIVE = new AsciiString("keep-alive"); + /** + * {@code "last-modified"} + */ + public static final AsciiString LAST_MODIFIED = new AsciiString("last-modified"); + /** + * {@code "location"} + */ + public static final AsciiString LOCATION = new AsciiString("location"); + /** + * {@code "max-forwards"} + */ + public static final AsciiString MAX_FORWARDS = new AsciiString("max-forwards"); + /** + * {@code "origin"} + */ + public static final AsciiString ORIGIN = new AsciiString("origin"); + /** + * {@code "pragma"} + */ + public static final AsciiString PRAGMA = new AsciiString("pragma"); + /** + * {@code "proxy-authenticate"} + */ + public static final AsciiString PROXY_AUTHENTICATE = new AsciiString("proxy-authenticate"); + /** + * {@code "proxy-authorization"} + */ + public static final AsciiString PROXY_AUTHORIZATION = new AsciiString("proxy-authorization"); + /** + * @deprecated use {@link #CONNECTION} + * + * {@code "proxy-connection"} + */ + @Deprecated + public static final AsciiString PROXY_CONNECTION = new AsciiString("proxy-connection"); + /** + * {@code "range"} + */ + public static final AsciiString RANGE = new AsciiString("range"); + /** + * {@code "referer"} + */ + public static final AsciiString REFERER = new AsciiString("referer"); + /** + * {@code "retry-after"} + */ + public static final AsciiString RETRY_AFTER = new AsciiString("retry-after"); + /** + * {@code "sec-websocket-key1"} + */ + public static final AsciiString SEC_WEBSOCKET_KEY1 = new AsciiString("sec-websocket-key1"); + /** + * {@code "sec-websocket-key2"} + */ + public static final AsciiString SEC_WEBSOCKET_KEY2 = new AsciiString("sec-websocket-key2"); + /** + * {@code "sec-websocket-location"} + */ + public static final AsciiString SEC_WEBSOCKET_LOCATION = new AsciiString("sec-websocket-location"); + /** + * {@code "sec-websocket-origin"} + */ + public static final AsciiString SEC_WEBSOCKET_ORIGIN = new AsciiString("sec-websocket-origin"); + /** + * {@code "sec-websocket-protocol"} + */ + public static final AsciiString SEC_WEBSOCKET_PROTOCOL = new AsciiString("sec-websocket-protocol"); + /** + * {@code "sec-websocket-version"} + */ + public static final AsciiString SEC_WEBSOCKET_VERSION = new AsciiString("sec-websocket-version"); + /** + * {@code "sec-websocket-key"} + */ + public static final AsciiString SEC_WEBSOCKET_KEY = new AsciiString("sec-websocket-key"); + /** + * {@code "sec-websocket-accept"} + */ + public static final AsciiString SEC_WEBSOCKET_ACCEPT = new AsciiString("sec-websocket-accept"); + /** + * {@code "sec-websocket-protocol"} + */ + public static final AsciiString SEC_WEBSOCKET_EXTENSIONS = new AsciiString("sec-websocket-extensions"); + /** + * {@code "server"} + */ + public static final AsciiString SERVER = new AsciiString("server"); + /** + * {@code "set-cookie"} + */ + public static final AsciiString SET_COOKIE = new AsciiString("set-cookie"); + /** + * {@code "set-cookie2"} + */ + public static final AsciiString SET_COOKIE2 = new AsciiString("set-cookie2"); + /** + * {@code "te"} + */ + public static final AsciiString TE = new AsciiString("te"); + /** + * {@code "trailer"} + */ + public static final AsciiString TRAILER = new AsciiString("trailer"); + /** + * {@code "transfer-encoding"} + */ + public static final AsciiString TRANSFER_ENCODING = new AsciiString("transfer-encoding"); + /** + * {@code "upgrade"} + */ + public static final AsciiString UPGRADE = new AsciiString("upgrade"); + /** + * {@code "user-agent"} + */ + public static final AsciiString USER_AGENT = new AsciiString("user-agent"); + /** + * {@code "vary"} + */ + public static final AsciiString VARY = new AsciiString("vary"); + /** + * {@code "via"} + */ + public static final AsciiString VIA = new AsciiString("via"); + /** + * {@code "warning"} + */ + public static final AsciiString WARNING = new AsciiString("warning"); + /** + * {@code "websocket-location"} + */ + public static final AsciiString WEBSOCKET_LOCATION = new AsciiString("websocket-location"); + /** + * {@code "websocket-origin"} + */ + public static final AsciiString WEBSOCKET_ORIGIN = new AsciiString("websocket-origin"); + /** + * {@code "websocket-protocol"} + */ + public static final AsciiString WEBSOCKET_PROTOCOL = new AsciiString("websocket-protocol"); + /** + * {@code "www-authenticate"} + */ + public static final AsciiString WWW_AUTHENTICATE = new AsciiString("www-authenticate"); + + private static final Map map; + + static { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Field f : HttpHeaderNames.class.getDeclaredFields()) { + final int m = f.getModifiers(); + if (Modifier.isPublic(m) && Modifier.isStatic(m) && Modifier.isFinal(m) && + f.getType() == AsciiString.class) { + final AsciiString name; + try { + name = (AsciiString) f.get(null); + } catch (Exception e) { + throw new Error(e); + } + builder.put(name.toString(), name); + } + } + map = builder.build(); + } + + public static AsciiString of(String name) { + requireNonNull(name, "name"); + final AsciiString asciiName = map.get(name.toLowerCase(Locale.US)); + return asciiName != null ? asciiName : new AsciiString(name); + } + + private HttpHeaderNames() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpHeaders.java b/src/main/java/com/linecorp/armeria/common/http/HttpHeaders.java new file mode 100644 index 000000000000..6446b782d8a9 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpHeaders.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static java.util.Objects.requireNonNull; + +import java.util.Iterator; +import java.util.Map.Entry; + +import io.netty.handler.codec.Headers; +import io.netty.util.AsciiString; + +public interface HttpHeaders extends HttpObject, Headers { + + HttpHeaders EMPTY_HEADERS = ofImmutable(new DefaultHttpHeaders(false, 0)); + + static HttpHeaders ofImmutable(HttpHeaders headers) { + requireNonNull(headers, "headers"); + if (headers instanceof ImmutableHttpHeaders) { + return headers; + } + + return new ImmutableHttpHeaders(headers); + } + + static HttpHeaders of(HttpMethod method, String path) { + return new DefaultHttpHeaders().method(method).path(path); + } + + static HttpHeaders of(int statusCode) { + return of(HttpStatus.valueOf(statusCode)); + } + + static HttpHeaders of(HttpStatus status) { + return new DefaultHttpHeaders().status(status); + } + + static HttpHeaders of(AsciiString name, String value) { + return new DefaultHttpHeaders().add(name, value); + } + + static HttpHeaders of(AsciiString name1, String value1, AsciiString name2, String value2) { + return new DefaultHttpHeaders().add(name1, value1).add(name2, value2); + } + + static HttpHeaders of(AsciiString name1, String value1, AsciiString name2, String value2, + AsciiString name3, String value3) { + + return new DefaultHttpHeaders().add(name1, value1).add(name2, value2) + .add(name3, value3); + } + + static HttpHeaders of(AsciiString name1, String value1, AsciiString name2, String value2, + AsciiString name3, String value3, AsciiString name4, String value4) { + + return new DefaultHttpHeaders().add(name1, value1).add(name2, value2) + .add(name3, value3).add(name4, value4); + } + + /** + * Returns an iterator over all HTTP/2 headers. The iteration order is as follows: + * 1. All pseudo headers (order not specified). + * 2. All non-pseudo headers (in insertion order). + */ + @Override + Iterator> iterator(); + + /** + * Sets the {@link HttpHeaderNames#METHOD} header or {@code null} if there is no such header + */ + HttpHeaders method(HttpMethod method); + + /** + * Sets the {@link HttpHeaderNames##SCHEME} header if there is no such header + */ + HttpHeaders scheme(String scheme); + + /** + * Sets the {@link HttpHeaderNames##AUTHORITY} header or {@code null} if there is no such header + */ + HttpHeaders authority(String authority); + + /** + * Sets the {@link HttpHeaderNames#PATH} header or {@code null} if there is no such header + */ + HttpHeaders path(String path); + + /** + * Sets the {@link HttpHeaderNames#STATUS} header + */ + HttpHeaders status(int statusCode); + + /** + * Sets the {@link HttpHeaderNames#STATUS} header or {@code null} if there is no such header + */ + HttpHeaders status(HttpStatus status); + + /** + * Gets the {@link HttpHeaderNames#METHOD} header or {@code null} if there is no such header + */ + HttpMethod method(); + + /** + * Gets the {@link HttpHeaderNames#SCHEME} header or {@code null} if there is no such header + */ + String scheme(); + + /** + * Gets the {@link HttpHeaderNames#AUTHORITY} header or {@code null} if there is no such header + */ + String authority(); + + /** + * Gets the {@link HttpHeaderNames#PATH} header or {@code null} if there is no such header + */ + String path(); + + /** + * Gets the {@link HttpHeaderNames#STATUS} header or {@code null} if there is no such header + */ + HttpStatus status(); +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpMessageAggregator.java b/src/main/java/com/linecorp/armeria/common/http/HttpMessageAggregator.java new file mode 100644 index 000000000000..27d33c7bc334 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpMessageAggregator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +abstract class HttpMessageAggregator implements Subscriber { + + private final CompletableFuture future; + protected final List contentList = new ArrayList<>(); + protected int contentLength; + private Subscription subscription; + + protected HttpMessageAggregator(CompletableFuture future) { + this.future = future; + } + + protected final CompletableFuture future() { + return future; + } + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + s.request(Long.MAX_VALUE); + } + + protected final void add(HttpData data) { + final int dataLength = data.length(); + if (dataLength > 0) { + if (contentLength > Integer.MAX_VALUE - dataLength) { + clear(); + subscription.cancel(); + throw new IllegalStateException("content length greater than Integer.MAX_VALUE"); + } + + contentList.add(data); + contentLength += dataLength; + } + } + + protected final void clear() { + doClear(); + contentList.clear(); + } + + protected void doClear() {} + + protected final HttpData finish() { + final HttpData content; + if (contentLength == 0) { + content = HttpData.EMPTY_DATA; + } else { + final byte[] merged = new byte[contentLength]; + for (int i = 0, offset = 0; i < contentList.size(); i++) { + final HttpData data = contentList.set(i, null); + final int dataLength = data.length(); + System.arraycopy(data.array(), data.offset(), merged, offset, dataLength); + offset += dataLength; + } + content = HttpData.of(merged); + } + return content; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpMethod.java b/src/main/java/com/linecorp/armeria/common/http/HttpMethod.java new file mode 100644 index 000000000000..95154efe905a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpMethod.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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. + */ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.common.http; + +/** + * HTTP request method. + */ +public enum HttpMethod { + + /** + * The OPTIONS method represents a request for information about the communication options + * available on the request/response chain identified by the Request-URI. This method allows + * the client to determine the options and/or requirements associated with a resource, or the + * capabilities of a server, without implying a resource action or initiating a resource + * retrieval. + */ + OPTIONS, + + /** + * The GET method means retrieve whatever information (in the form of an entity) is identified + * by the Request-URI. If the Request-URI refers to a data-producing process, it is the + * produced data which shall be returned as the entity in the response and not the source text + * of the process, unless that text happens to be the output of the process. + */ + GET, + + /** + * The HEAD method is identical to GET except that the server MUST NOT return a message-body + * in the response. + */ + HEAD, + + /** + * The POST method is used to request that the origin server accept the entity enclosed in the + * request as a new subordinate of the resource identified by the Request-URI in the + * Request-Line. + */ + POST, + + /** + * The PUT method requests that the enclosed entity be stored under the supplied Request-URI. + */ + PUT, + + /** + * The PATCH method requests that a set of changes described in the + * request entity be applied to the resource identified by the Request-URI. + */ + PATCH, + + /** + * The DELETE method requests that the origin server delete the resource identified by the + * Request-URI. + */ + DELETE, + + /** + * The TRACE method is used to invoke a remote, application-layer loop- back of the request + * message. + */ + TRACE, + + /** + * This specification reserves the method name CONNECT for use with a proxy that can dynamically + * switch to being a tunnel + */ + CONNECT; + + public io.netty.handler.codec.http.HttpMethod toNettyMethod() { + return io.netty.handler.codec.http.HttpMethod.valueOf(name()); + } +} diff --git a/src/main/java/com/linecorp/armeria/client/routing/Endpoint.java b/src/main/java/com/linecorp/armeria/common/http/HttpObject.java similarity index 84% rename from src/main/java/com/linecorp/armeria/client/routing/Endpoint.java rename to src/main/java/com/linecorp/armeria/common/http/HttpObject.java index 1f3a91ab82fb..5f5c25df2a80 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/Endpoint.java +++ b/src/main/java/com/linecorp/armeria/common/http/HttpObject.java @@ -13,10 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package com.linecorp.armeria.client.routing; -public interface Endpoint { - String hostname(); +package com.linecorp.armeria.common.http; - int port(); -} +public interface HttpObject {} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpRequest.java b/src/main/java/com/linecorp/armeria/common/http/HttpRequest.java new file mode 100644 index 000000000000..6280e3316c5a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpRequest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.reactivestreams.Publisher; + +import com.linecorp.armeria.common.reactivestreams.RichPublisher; + +public interface HttpRequest extends RichPublisher { + + static HttpRequest of(HttpHeaders headers, Publisher publisher) { + return new PublisherBasedHttpRequest(headers, true, publisher); + } + + HttpHeaders headers(); + + boolean isKeepAlive(); + + default String scheme() { + return headers().scheme(); + } + + default HttpRequest scheme(String scheme) { + headers().scheme(scheme); + return this; + } + + default HttpMethod method() { + return headers().method(); + } + + default HttpRequest method(HttpMethod method) { + headers().method(method); + return this; + } + + default String path() { + return headers().path(); + } + + default HttpRequest path(String path) { + headers().path(path); + return this; + } + + default String authority() { + return headers().authority(); + } + + default HttpRequest authority(String authority) { + headers().authority(authority); + return this; + } + + /** + * Aggregates the request. The returned {@link CompletableFuture} will be notified when the content and + * the trailing headers of the request is received fully. + */ + default CompletableFuture aggregate() { + final CompletableFuture future = new CompletableFuture<>(); + subscribe(new HttpRequestAggregator(this, future)); + return future; + } + + /** + * Aggregates the request. The returned {@link CompletableFuture} will be notified when the content and + * the trailing headers of the request is received fully. + */ + default CompletableFuture aggregate(Executor executor) { + final CompletableFuture future = new CompletableFuture<>(); + subscribe(new HttpRequestAggregator(this, future), executor); + return future; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpRequestAggregator.java b/src/main/java/com/linecorp/armeria/common/http/HttpRequestAggregator.java new file mode 100644 index 000000000000..ad99387abfc8 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpRequestAggregator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.concurrent.CompletableFuture; + +final class HttpRequestAggregator extends HttpMessageAggregator { + + private final HttpRequest request; + private HttpHeaders trailingHeaders; + + HttpRequestAggregator(HttpRequest request, CompletableFuture future) { + super(future); + this.request = request; + trailingHeaders = HttpHeaders.EMPTY_HEADERS; + } + + @Override + public void onNext(HttpObject o) { + if (o instanceof HttpHeaders) { + trailingHeaders = (HttpHeaders) o; + } else { + add((HttpData) o); + } + } + + @Override + public void onError(Throwable t) { + clear(); + future().completeExceptionally(t); + } + + @Override + protected void doClear() { + trailingHeaders.clear(); + contentList.clear(); + } + + @Override + public void onComplete() { + final HttpData content = finish(); + future().complete(AggregatedHttpMessage.of(request.headers(), content, trailingHeaders)); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpRequestWriter.java b/src/main/java/com/linecorp/armeria/common/http/HttpRequestWriter.java new file mode 100644 index 000000000000..50f656e9f3b6 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpRequestWriter.java @@ -0,0 +1,23 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import com.linecorp.armeria.common.reactivestreams.Writer; + +public interface HttpRequestWriter extends Writer { + // TODO(trustin): Add lots of convenience methods for easier response construction. +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpResponse.java b/src/main/java/com/linecorp/armeria/common/http/HttpResponse.java new file mode 100644 index 000000000000..ef84b0be1c12 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.reactivestreams.Publisher; + +import com.linecorp.armeria.common.reactivestreams.RichPublisher; + +public interface HttpResponse extends RichPublisher { + + static HttpResponse of(Publisher publisher) { + return new PublisherBasedHttpResponse(publisher); + } + + /** + * Aggregates the response. The returned {@link CompletableFuture} will be notified when the content and + * the trailing headers of the response are received fully. + */ + default CompletableFuture aggregate() { + final CompletableFuture future = new CompletableFuture<>(); + subscribe(new HttpResponseAggregator(future)); + return future; + } + + /** + * Aggregates the response. The returned {@link CompletableFuture} will be notified when the content and + * the trailing headers of the response are received fully. + */ + default CompletableFuture aggregate(Executor executor) { + final CompletableFuture future = new CompletableFuture<>(); + subscribe(new HttpResponseAggregator(future), executor); + return future; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpResponseAggregator.java b/src/main/java/com/linecorp/armeria/common/http/HttpResponseAggregator.java new file mode 100644 index 000000000000..49ec46183006 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpResponseAggregator.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +final class HttpResponseAggregator extends HttpMessageAggregator { + + private List informationals; + private HttpHeaders headers; + private HttpHeaders trailingHeaders; + + HttpResponseAggregator(CompletableFuture future) { + super(future); + trailingHeaders = HttpHeaders.EMPTY_HEADERS; + } + + @Override + public void onNext(HttpObject o) { + if (o instanceof HttpHeaders) { + final HttpHeaders headers = (HttpHeaders) o; + if (headers.status().codeClass() == HttpStatusClass.INFORMATIONAL) { + if (informationals == null) { + informationals = new ArrayList<>(2); + } + informationals.add(headers); + } else if (this.headers == null) { + this.headers = headers; + } else { + trailingHeaders = headers; + } + } else { + add((HttpData) o); + } + } + + @Override + public void onError(Throwable t) { + clear(); + future().completeExceptionally(t); + } + + @Override + protected void doClear() { + headers = null; + trailingHeaders = null; + } + + @Override + public void onComplete() { + final HttpData content = finish(); + future().complete(AggregatedHttpMessage.of( + informationals != null ? informationals : Collections.emptyList(), + headers, content, trailingHeaders)); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpResponseWriter.java b/src/main/java/com/linecorp/armeria/common/http/HttpResponseWriter.java new file mode 100644 index 000000000000..e2d728a7ed75 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpResponseWriter.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import static com.linecorp.armeria.internal.http.ArmeriaHttpUtil.isContentAlwaysEmpty; +import static java.util.Objects.requireNonNull; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import com.google.common.net.MediaType; + +import com.linecorp.armeria.common.reactivestreams.Writer; + +public interface HttpResponseWriter extends Writer { + // TODO(trustin): Add lots of convenience methods for easier response construction. + + default void respond(int statusCode) { + respond(HttpStatus.valueOf(statusCode)); + } + + default void respond(HttpStatus status) { + requireNonNull(status, "status"); + if (status.codeClass() == HttpStatusClass.INFORMATIONAL) { + write(HttpHeaders.of(status)); + } else if (isContentAlwaysEmpty(status)) { + write(HttpHeaders.of(status)); + close(); + } else { + respond(status, MediaType.PLAIN_TEXT_UTF_8, status.toHttpData()); + } + } + + default void respond(HttpStatus status, MediaType mediaType, String content) { + requireNonNull(status, "status"); + requireNonNull(content, "content"); + requireNonNull(mediaType, "mediaType"); + respond(status, + mediaType, content.getBytes(mediaType.charset().or(StandardCharsets.UTF_8))); + } + + default void respond(HttpStatus status, MediaType mediaType, String format, Object... args) { + requireNonNull(status, "status"); + requireNonNull(mediaType, "mediaType"); + requireNonNull(format, "format"); + requireNonNull(args, "args"); + respond(status, + mediaType, + String.format(Locale.ENGLISH, format, args).getBytes( + mediaType.charset().or(StandardCharsets.UTF_8))); + } + + default void respond(HttpStatus status, MediaType mediaType, byte[] content) { + requireNonNull(content, "content"); + respond(status, mediaType, HttpData.of(content)); + } + + default void respond(HttpStatus status, MediaType mediaType, byte[] content, int offset, int length) { + requireNonNull(content, "content"); + respond(status, mediaType, HttpData.of(content, offset, length)); + } + + default void respond(HttpStatus status, MediaType mediaType, HttpData content) { + requireNonNull(status, "status"); + requireNonNull(content, "content"); + requireNonNull(mediaType, "mediaType"); + + final int length = content.length(); + final HttpHeaders headers = + HttpHeaders.of(status) + .set(HttpHeaderNames.CONTENT_TYPE, mediaType.toString()) + .setInt(HttpHeaderNames.CONTENT_LENGTH, length); + + if (isContentAlwaysEmpty(status)) { + if (length != 0) { + throw new IllegalArgumentException( + "A " + status + " response must have empty content: " + length + " byte(s)"); + } + write(headers); + } else { + write(headers); + write(content); + } + + close(); + } + + default void respond(AggregatedHttpMessage res) { + final HttpHeaders headers = res.headers(); + write(headers); + + final HttpData content = res.content(); + if (isContentAlwaysEmpty(headers.status())) { + if (!content.isEmpty()) { + throw new IllegalArgumentException( + "A " + headers.status() + " response must have empty content: " + + content.length() + " byte(s)"); + } + + if (!res.trailingHeaders().isEmpty()) { + throw new IllegalArgumentException( + "A " + headers.status() + " response must not have trailing headers: " + + res.trailingHeaders()); + } + + close(); + return; + } + + // Add content if not empty. + if (!content.isEmpty()) { + write(content); + } + + // Add trailing headers if not empty. + final HttpHeaders trailingHeaders = res.trailingHeaders(); + if (!trailingHeaders.isEmpty()) { + write(trailingHeaders); + } + + close(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpStatus.java b/src/main/java/com/linecorp/armeria/common/http/HttpStatus.java new file mode 100644 index 000000000000..dad9f94a89b3 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpStatus.java @@ -0,0 +1,454 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +/** + * HTTP response code and its description. + */ +public final class HttpStatus implements Comparable { + + private static final IntObjectMap map = new IntObjectHashMap<>(1000); + + /** + * 100 Continue + */ + public static final HttpStatus CONTINUE = newConstant(100, "Continue"); + + /** + * 101 Switching Protocols + */ + public static final HttpStatus SWITCHING_PROTOCOLS = newConstant(101, "Switching Protocols"); + + /** + * 102 Processing (WebDAV, RFC2518) + */ + public static final HttpStatus PROCESSING = newConstant(102, "Processing"); + + /** + * 200 OK + */ + public static final HttpStatus OK = newConstant(200, "OK"); + + /** + * 201 Created + */ + public static final HttpStatus CREATED = newConstant(201, "Created"); + + /** + * 202 Accepted + */ + public static final HttpStatus ACCEPTED = newConstant(202, "Accepted"); + + /** + * 203 Non-Authoritative Information (since HTTP/1.1) + */ + public static final HttpStatus NON_AUTHORITATIVE_INFORMATION = + newConstant(203, "Non-Authoritative Information"); + + /** + * 204 No Content + */ + public static final HttpStatus NO_CONTENT = newConstant(204, "No Content"); + + /** + * 205 Reset Content + */ + public static final HttpStatus RESET_CONTENT = newConstant(205, "Reset Content"); + + /** + * 206 Partial Content + */ + public static final HttpStatus PARTIAL_CONTENT = newConstant(206, "Partial Content"); + + /** + * 207 Multi-Status (WebDAV, RFC2518) + */ + public static final HttpStatus MULTI_STATUS = newConstant(207, "Multi-Status"); + + /** + * 300 Multiple Choices + */ + public static final HttpStatus MULTIPLE_CHOICES = newConstant(300, "Multiple Choices"); + + /** + * 301 Moved Permanently + */ + public static final HttpStatus MOVED_PERMANENTLY = newConstant(301, "Moved Permanently"); + + /** + * 302 Found + */ + public static final HttpStatus FOUND = newConstant(302, "Found"); + + /** + * 303 See Other (since HTTP/1.1) + */ + public static final HttpStatus SEE_OTHER = newConstant(303, "See Other"); + + /** + * 304 Not Modified + */ + public static final HttpStatus NOT_MODIFIED = newConstant(304, "Not Modified"); + + /** + * 305 Use Proxy (since HTTP/1.1) + */ + public static final HttpStatus USE_PROXY = newConstant(305, "Use Proxy"); + + /** + * 307 Temporary Redirect (since HTTP/1.1) + */ + public static final HttpStatus TEMPORARY_REDIRECT = newConstant(307, "Temporary Redirect"); + + /** + * 400 Bad Request + */ + public static final HttpStatus BAD_REQUEST = newConstant(400, "Bad Request"); + + /** + * 401 Unauthorized + */ + public static final HttpStatus UNAUTHORIZED = newConstant(401, "Unauthorized"); + + /** + * 402 Payment Required + */ + public static final HttpStatus PAYMENT_REQUIRED = newConstant(402, "Payment Required"); + + /** + * 403 Forbidden + */ + public static final HttpStatus FORBIDDEN = newConstant(403, "Forbidden"); + + /** + * 404 Not Found + */ + public static final HttpStatus NOT_FOUND = newConstant(404, "Not Found"); + + /** + * 405 Method Not Allowed + */ + public static final HttpStatus METHOD_NOT_ALLOWED = newConstant(405, "Method Not Allowed"); + + /** + * 406 Not Acceptable + */ + public static final HttpStatus NOT_ACCEPTABLE = newConstant(406, "Not Acceptable"); + + /** + * 407 Proxy Authentication Required + */ + public static final HttpStatus PROXY_AUTHENTICATION_REQUIRED = + newConstant(407, "Proxy Authentication Required"); + + /** + * 408 Request Timeout + */ + public static final HttpStatus REQUEST_TIMEOUT = newConstant(408, "Request Timeout"); + + /** + * 409 Conflict + */ + public static final HttpStatus CONFLICT = newConstant(409, "Conflict"); + + /** + * 410 Gone + */ + public static final HttpStatus GONE = newConstant(410, "Gone"); + + /** + * 411 Length Required + */ + public static final HttpStatus LENGTH_REQUIRED = newConstant(411, "Length Required"); + + /** + * 412 Precondition Failed + */ + public static final HttpStatus PRECONDITION_FAILED = newConstant(412, "Precondition Failed"); + + /** + * 413 Request Entity Too Large + */ + public static final HttpStatus REQUEST_ENTITY_TOO_LARGE = + newConstant(413, "Request Entity Too Large"); + + /** + * 414 Request-URI Too Long + */ + public static final HttpStatus REQUEST_URI_TOO_LONG = newConstant(414, "Request-URI Too Long"); + + /** + * 415 Unsupported Media Type + */ + public static final HttpStatus UNSUPPORTED_MEDIA_TYPE = newConstant(415, "Unsupported Media Type"); + + /** + * 416 Requested Range Not Satisfiable + */ + public static final HttpStatus REQUESTED_RANGE_NOT_SATISFIABLE = + newConstant(416, "Requested Range Not Satisfiable"); + + /** + * 417 Expectation Failed + */ + public static final HttpStatus EXPECTATION_FAILED = newConstant(417, "Expectation Failed"); + + /** + * 421 Misdirected Request + * + * 421 Status Code + */ + public static final HttpStatus MISDIRECTED_REQUEST = newConstant(421, "Misdirected Request"); + + /** + * 422 Unprocessable Entity (WebDAV, RFC4918) + */ + public static final HttpStatus UNPROCESSABLE_ENTITY = newConstant(422, "Unprocessable Entity"); + + /** + * 423 Locked (WebDAV, RFC4918) + */ + public static final HttpStatus LOCKED = newConstant(423, "Locked"); + + /** + * 424 Failed Dependency (WebDAV, RFC4918) + */ + public static final HttpStatus FAILED_DEPENDENCY = newConstant(424, "Failed Dependency"); + + /** + * 425 Unordered Collection (WebDAV, RFC3648) + */ + public static final HttpStatus UNORDERED_COLLECTION = newConstant(425, "Unordered Collection"); + + /** + * 426 Upgrade Required (RFC2817) + */ + public static final HttpStatus UPGRADE_REQUIRED = newConstant(426, "Upgrade Required"); + + /** + * 428 Precondition Required (RFC6585) + */ + public static final HttpStatus PRECONDITION_REQUIRED = newConstant(428, "Precondition Required"); + + /** + * 429 Too Many Requests (RFC6585) + */ + public static final HttpStatus TOO_MANY_REQUESTS = newConstant(429, "Too Many Requests"); + + /** + * 431 Request Header Fields Too Large (RFC6585) + */ + public static final HttpStatus REQUEST_HEADER_FIELDS_TOO_LARGE = + newConstant(431, "Request Header Fields Too Large"); + + /** + * 500 Internal Server Error + */ + public static final HttpStatus INTERNAL_SERVER_ERROR = newConstant(500, "Internal Server Error"); + + /** + * 501 Not Implemented + */ + public static final HttpStatus NOT_IMPLEMENTED = newConstant(501, "Not Implemented"); + + /** + * 502 Bad Gateway + */ + public static final HttpStatus BAD_GATEWAY = newConstant(502, "Bad Gateway"); + + /** + * 503 Service Unavailable + */ + public static final HttpStatus SERVICE_UNAVAILABLE = newConstant(503, "Service Unavailable"); + + /** + * 504 Gateway Timeout + */ + public static final HttpStatus GATEWAY_TIMEOUT = newConstant(504, "Gateway Timeout"); + + /** + * 505 HTTP Version Not Supported + */ + public static final HttpStatus HTTP_VERSION_NOT_SUPPORTED = + newConstant(505, "HTTP Version Not Supported"); + + /** + * 506 Variant Also Negotiates (RFC2295) + */ + public static final HttpStatus VARIANT_ALSO_NEGOTIATES = newConstant(506, "Variant Also Negotiates"); + + /** + * 507 Insufficient Storage (WebDAV, RFC4918) + */ + public static final HttpStatus INSUFFICIENT_STORAGE = newConstant(507, "Insufficient Storage"); + + /** + * 510 Not Extended (RFC2774) + */ + public static final HttpStatus NOT_EXTENDED = newConstant(510, "Not Extended"); + + /** + * 511 Network Authentication Required (RFC6585) + */ + public static final HttpStatus NETWORK_AUTHENTICATION_REQUIRED = + newConstant(511, "Network Authentication Required"); + + static { + for (int i = 0; i < 1000; i++) { + if (!map.containsKey(i)) { + map.put(i, new HttpStatus(i)); + } + } + } + + private static HttpStatus newConstant(int statusCode, String reasonPhrase) { + final HttpStatus status = new HttpStatus(statusCode, reasonPhrase); + map.put(statusCode, status); + return status; + } + + /** + * Returns the {@link HttpStatus} represented by the specified code. + */ + public static HttpStatus valueOf(int code) { + final HttpStatus status = map.get(code); + return status != null ? status : new HttpStatus(code); + } + + private final int code; + private final String codeAsText; + private final HttpStatusClass codeClass; + private final String reasonPhrase; + private final HttpData httpData; + private final String strVal; + + /** + * Creates a new instance with the specified {@code code} and the auto-generated default reason phrase. + */ + private HttpStatus(int code) { + this(code, HttpStatusClass.valueOf(code).defaultReasonPhrase() + " (" + code + ')'); + } + + /** + * Creates a new instance with the specified {@code code} and its {@code reasonPhrase}. + */ + public HttpStatus(int code, String reasonPhrase) { + if (code < 0) { + throw new IllegalArgumentException( + "code: " + code + " (expected: 0+)"); + } + + if (reasonPhrase == null) { + throw new NullPointerException("reasonPhrase"); + } + + for (int i = 0; i < reasonPhrase.length(); i ++) { + char c = reasonPhrase.charAt(i); + // Check prohibited characters. + switch (c) { + case '\n': case '\r': + throw new IllegalArgumentException( + "reasonPhrase contains one of the following prohibited characters: " + + "\\r\\n: " + reasonPhrase); + } + } + + this.code = code; + codeAsText = Integer.toString(code); + codeClass = HttpStatusClass.valueOf(code); + this.reasonPhrase = reasonPhrase; + + strVal = new StringBuilder(reasonPhrase.length() + 5).append(code) + .append(' ') + .append(reasonPhrase) + .toString(); + httpData = HttpData.ofUtf8(strVal); + } + + /** + * Returns the code of this {@link HttpStatus}. + */ + public int code() { + return code; + } + + /** + * Returns the status code as {@link String}. + */ + public String codeAsText() { + return codeAsText; + } + + /** + * Returns the reason phrase of this {@link HttpStatus}. + */ + public String reasonPhrase() { + return reasonPhrase; + } + + /** + * Returns the class of this {@link HttpStatus} + */ + public HttpStatusClass codeClass() { + return codeClass; + } + + public HttpData toHttpData() { + return httpData; + } + + public HttpResponseStatus toNettyStatus() { + return HttpResponseStatus.valueOf(code()); + } + + @Override + public int hashCode() { + return code(); + } + + /** + * Equality of {@link HttpStatus} only depends on {@link #code()}. The + * reason phrase is not considered for equality. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof HttpStatus)) { + return false; + } + + return code() == ((HttpStatus) o).code(); + } + + /** + * Equality of {@link HttpStatus} only depends on {@link #code()}. The + * reason phrase is not considered for equality. + */ + @Override + public int compareTo(HttpStatus o) { + return code() - o.code(); + } + + @Override + public String toString() { + return strVal; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/HttpStatusClass.java b/src/main/java/com/linecorp/armeria/common/http/HttpStatusClass.java new file mode 100644 index 000000000000..ca8071601f8c --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/HttpStatusClass.java @@ -0,0 +1,100 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.common.http; + +import io.netty.util.AsciiString; + +/** + * The class of HTTP status. + */ +public enum HttpStatusClass { + /** + * The informational class (1xx) + */ + INFORMATIONAL(100, 200, "Informational"), + /** + * The success class (2xx) + */ + SUCCESS(200, 300, "Success"), + /** + * The redirection class (3xx) + */ + REDIRECTION(300, 400, "Redirection"), + /** + * The client error class (4xx) + */ + CLIENT_ERROR(400, 500, "Client Error"), + /** + * The server error class (5xx) + */ + SERVER_ERROR(500, 600, "Server Error"), + /** + * The unknown class + */ + UNKNOWN(0, 0, "Unknown Status") { + @Override + public boolean contains(int code) { + return code < 100 || code >= 600; + } + }; + + /** + * Returns the class of the specified HTTP status code. + */ + public static HttpStatusClass valueOf(int code) { + if (INFORMATIONAL.contains(code)) { + return INFORMATIONAL; + } + if (SUCCESS.contains(code)) { + return SUCCESS; + } + if (REDIRECTION.contains(code)) { + return REDIRECTION; + } + if (CLIENT_ERROR.contains(code)) { + return CLIENT_ERROR; + } + if (SERVER_ERROR.contains(code)) { + return SERVER_ERROR; + } + return UNKNOWN; + } + + private final int min; + private final int max; + private final AsciiString defaultReasonPhrase; + + HttpStatusClass(int min, int max, String defaultReasonPhrase) { + this.min = min; + this.max = max; + this.defaultReasonPhrase = new AsciiString(defaultReasonPhrase); + } + + /** + * Returns {@code true} if and only if the specified HTTP status code falls into this class. + */ + public boolean contains(int code) { + return code >= min && code < max; + } + + /** + * Returns the default reason phrase of this HTTP status class. + */ + AsciiString defaultReasonPhrase() { + return defaultReasonPhrase; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/ImmutableHttpHeaders.java b/src/main/java/com/linecorp/armeria/common/http/ImmutableHttpHeaders.java index b190fb256407..f3610d06c951 100644 --- a/src/main/java/com/linecorp/armeria/common/http/ImmutableHttpHeaders.java +++ b/src/main/java/com/linecorp/armeria/common/http/ImmutableHttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -16,92 +16,352 @@ package com.linecorp.armeria.common.http; +import static java.util.Objects.requireNonNull; + import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; -import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.Headers; +import io.netty.util.AsciiString; -/** - * A container for HTTP headers that cannot be mutated. Just delegates read operations to an underlying - * {@link HttpHeaders} object. - */ -public final class ImmutableHttpHeaders extends HttpHeaders { +final class ImmutableHttpHeaders implements HttpHeaders { private final HttpHeaders delegate; - public ImmutableHttpHeaders(HttpHeaders delegate) { - this.delegate = delegate; + ImmutableHttpHeaders(HttpHeaders delegate) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + @Override + public Iterator> iterator() { + return delegate.iterator(); + } + + @Override + public HttpHeaders method(HttpMethod method) { + return unsupported(); + } + + @Override + public HttpHeaders scheme(String scheme) { + return unsupported(); + } + + @Override + public HttpHeaders authority(String authority) { + return unsupported(); + } + + @Override + public HttpHeaders path(String path) { + return unsupported(); + } + + @Override + public HttpHeaders status(int statusCode) { + return unsupported(); + } + + @Override + public HttpHeaders status(HttpStatus status) { + return unsupported(); + } + + @Override + public HttpMethod method() { + return delegate.method(); + } + + @Override + public String scheme() { + return delegate.scheme(); + } + + @Override + public String authority() { + return delegate.authority(); } @Override - public String get(String name) { + public String path() { + return delegate.path(); + } + + @Override + public HttpStatus status() { + return delegate.status(); + } + + @Override + public String get(AsciiString name) { return delegate.get(name); } @Override - public Integer getInt(CharSequence name) { - return delegate.getInt(name); + public String get(AsciiString name, String defaultValue) { + return delegate.get(name, defaultValue); } @Override - public int getInt(CharSequence name, int defaultValue) { - return delegate.getInt(name, defaultValue); + public String getAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public String getAndRemove(AsciiString name, String defaultValue) { + return unsupported(); + } + + @Override + public List getAll(AsciiString name) { + return delegate.getAll(name); + } + + @Override + public List getAllAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public Boolean getBoolean(AsciiString name) { + return delegate.getBoolean(name); + } + + @Override + public boolean getBoolean(AsciiString name, boolean defaultValue) { + return delegate.getBoolean(name, defaultValue); } @Override - public Short getShort(CharSequence name) { + public Byte getByte(AsciiString name) { + return delegate.getByte(name); + } + + @Override + public byte getByte(AsciiString name, byte defaultValue) { + return delegate.getByte(name, defaultValue); + } + + @Override + public Character getChar(AsciiString name) { + return delegate.getChar(name); + } + + @Override + public char getChar(AsciiString name, char defaultValue) { + return delegate.getChar(name, defaultValue); + } + + @Override + public Short getShort(AsciiString name) { return delegate.getShort(name); } @Override - public short getShort(CharSequence name, short defaultValue) { + public short getShort(AsciiString name, short defaultValue) { return delegate.getShort(name, defaultValue); } @Override - public Long getTimeMillis(CharSequence name) { + public Integer getInt(AsciiString name) { + return delegate.getInt(name); + } + + @Override + public int getInt(AsciiString name, int defaultValue) { + return delegate.getInt(name, defaultValue); + } + + @Override + public Long getLong(AsciiString name) { + return delegate.getLong(name); + } + + @Override + public long getLong(AsciiString name, long defaultValue) { + return delegate.getLong(name, defaultValue); + } + + @Override + public Float getFloat(AsciiString name) { + return delegate.getFloat(name); + } + + @Override + public float getFloat(AsciiString name, float defaultValue) { + return delegate.getFloat(name, defaultValue); + } + + @Override + public Double getDouble(AsciiString name) { + return delegate.getDouble(name); + } + + @Override + public double getDouble(AsciiString name, double defaultValue) { + return delegate.getDouble(name, defaultValue); + } + + @Override + public Long getTimeMillis(AsciiString name) { return delegate.getTimeMillis(name); } @Override - public long getTimeMillis(CharSequence name, long defaultValue) { + public long getTimeMillis(AsciiString name, long defaultValue) { return delegate.getTimeMillis(name, defaultValue); } @Override - @Deprecated - public List getAll(String name) { - return delegate.getAll(name); + public Boolean getBooleanAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public boolean getBooleanAndRemove(AsciiString name, boolean defaultValue) { + return unsupported(); + } + + @Override + public Byte getByteAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public byte getByteAndRemove(AsciiString name, byte defaultValue) { + return unsupported(); + } + + @Override + public Character getCharAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public char getCharAndRemove(AsciiString name, char defaultValue) { + return unsupported(); + } + + @Override + public Short getShortAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public short getShortAndRemove(AsciiString name, short defaultValue) { + return unsupported(); + } + + @Override + public Integer getIntAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public int getIntAndRemove(AsciiString name, int defaultValue) { + return unsupported(); + } + + @Override + public Long getLongAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public long getLongAndRemove(AsciiString name, long defaultValue) { + return unsupported(); } @Override - @Deprecated - public List> entries() { - return delegate.entries(); + public Float getFloatAndRemove(AsciiString name) { + return unsupported(); } @Override - @Deprecated - public boolean contains(String name) { + public float getFloatAndRemove(AsciiString name, float defaultValue) { + return unsupported(); + } + + @Override + public Double getDoubleAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public double getDoubleAndRemove(AsciiString name, double defaultValue) { + return unsupported(); + } + + @Override + public Long getTimeMillisAndRemove(AsciiString name) { + return unsupported(); + } + + @Override + public long getTimeMillisAndRemove(AsciiString name, long defaultValue) { + return unsupported(); + } + + @Override + public boolean contains(AsciiString name) { return delegate.contains(name); } @Override - @Deprecated - public Iterator> iterator() { - return delegate.iterator(); + public boolean contains(AsciiString name, String value) { + return delegate.contains(name, value); } @Override - public Iterator> iteratorCharSequence() { - return delegate.iteratorCharSequence(); + public boolean containsObject(AsciiString name, Object value) { + return delegate.containsObject(name, value); } @Override - public boolean isEmpty() { - return delegate.isEmpty(); + public boolean containsBoolean(AsciiString name, boolean value) { + return delegate.containsBoolean(name, value); + } + + @Override + public boolean containsByte(AsciiString name, byte value) { + return delegate.containsByte(name, value); + } + + @Override + public boolean containsChar(AsciiString name, char value) { + return delegate.containsChar(name, value); + } + + @Override + public boolean containsShort(AsciiString name, short value) { + return delegate.containsShort(name, value); + } + + @Override + public boolean containsInt(AsciiString name, int value) { + return delegate.containsInt(name, value); + } + + @Override + public boolean containsLong(AsciiString name, long value) { + return delegate.containsLong(name, value); + } + + @Override + public boolean containsFloat(AsciiString name, float value) { + return delegate.containsFloat(name, value); + } + + @Override + public boolean containsDouble(AsciiString name, double value) { + return delegate.containsDouble(name, value); + } + + @Override + public boolean containsTimeMillis(AsciiString name, long value) { + return delegate.containsTimeMillis(name, value); } @Override @@ -110,64 +370,191 @@ public int size() { } @Override - public Set names() { + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Set names() { return delegate.names(); } @Override - public HttpHeaders add(String name, Object value) { - throw new UnsupportedOperationException(); + public HttpHeaders add(AsciiString name, String value) { + return unsupported(); } @Override - public HttpHeaders add(String name, Iterable values) { - throw new UnsupportedOperationException(); + public HttpHeaders add(AsciiString name, Iterable values) { + return unsupported(); } @Override - public HttpHeaders addInt(CharSequence name, int value) { - throw new UnsupportedOperationException(); + public HttpHeaders add(AsciiString name, String... values) { + return unsupported(); } @Override - public HttpHeaders addShort(CharSequence name, short value) { - throw new UnsupportedOperationException(); + public HttpHeaders addObject(AsciiString name, Object value) { + return unsupported(); } @Override - public HttpHeaders set(String name, Object value) { - throw new UnsupportedOperationException(); + public HttpHeaders addObject(AsciiString name, Iterable values) { + return unsupported(); } @Override - public HttpHeaders set(String name, Iterable values) { - throw new UnsupportedOperationException(); + public HttpHeaders addObject(AsciiString name, Object... values) { + return unsupported(); } @Override - public HttpHeaders setInt(CharSequence name, int value) { - throw new UnsupportedOperationException(); + public HttpHeaders addBoolean(AsciiString name, boolean value) { + return unsupported(); } @Override - public HttpHeaders setShort(CharSequence name, short value) { - throw new UnsupportedOperationException(); + public HttpHeaders addByte(AsciiString name, byte value) { + return unsupported(); } @Override - public HttpHeaders remove(String name) { - throw new UnsupportedOperationException(); + public HttpHeaders addChar(AsciiString name, char value) { + return unsupported(); } @Override - public HttpHeaders clear() { - throw new UnsupportedOperationException(); + public HttpHeaders addShort(AsciiString name, short value) { + return unsupported(); } @Override - @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") - public boolean equals(Object other) { - return delegate.equals(other); + public HttpHeaders addInt(AsciiString name, int value) { + return unsupported(); + } + + @Override + public HttpHeaders addLong(AsciiString name, long value) { + return unsupported(); + } + + @Override + public HttpHeaders addFloat(AsciiString name, float value) { + return unsupported(); + } + + @Override + public HttpHeaders addDouble(AsciiString name, double value) { + return unsupported(); + } + + @Override + public HttpHeaders addTimeMillis(AsciiString name, long value) { + return unsupported(); + } + + @Override + public HttpHeaders add( + Headers headers) { + return unsupported(); + } + + @Override + public HttpHeaders set(AsciiString name, String value) { + return unsupported(); + } + + @Override + public HttpHeaders set(AsciiString name, Iterable values) { + return unsupported(); + } + + @Override + public HttpHeaders set(AsciiString name, String... values) { + return unsupported(); + } + + @Override + public HttpHeaders setObject(AsciiString name, Object value) { + return unsupported(); + } + + @Override + public HttpHeaders setObject(AsciiString name, Iterable values) { + return unsupported(); + } + + @Override + public HttpHeaders setObject(AsciiString name, Object... values) { + return unsupported(); + } + + @Override + public HttpHeaders setBoolean(AsciiString name, boolean value) { + return unsupported(); + } + + @Override + public HttpHeaders setByte(AsciiString name, byte value) { + return unsupported(); + } + + @Override + public HttpHeaders setChar(AsciiString name, char value) { + return unsupported(); + } + + @Override + public HttpHeaders setShort(AsciiString name, short value) { + return unsupported(); + } + + @Override + public HttpHeaders setInt(AsciiString name, int value) { + return unsupported(); + } + + @Override + public HttpHeaders setLong(AsciiString name, long value) { + return unsupported(); + } + + @Override + public HttpHeaders setFloat(AsciiString name, float value) { + return unsupported(); + } + + @Override + public HttpHeaders setDouble(AsciiString name, double value) { + return unsupported(); + } + + @Override + public HttpHeaders setTimeMillis(AsciiString name, long value) { + return unsupported(); + } + + @Override + public HttpHeaders set( + Headers headers) { + return unsupported(); + } + + @Override + public HttpHeaders setAll( + Headers headers) { + return unsupported(); + } + + @Override + public boolean remove(AsciiString name) { + return unsupported(); + } + + @Override + public HttpHeaders clear() { + return unsupported(); } @Override @@ -175,8 +562,18 @@ public int hashCode() { return delegate.hashCode(); } + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object obj) { + return delegate.equals(obj); + } + @Override public String toString() { return delegate.toString(); } + + private static T unsupported() { + throw new UnsupportedOperationException("immutable"); + } } diff --git a/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpRequest.java b/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpRequest.java new file mode 100644 index 000000000000..a35974e997ff --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import org.reactivestreams.Publisher; + +import com.linecorp.armeria.common.reactivestreams.PublisherWithCloseFuture; + +final class PublisherBasedHttpRequest extends PublisherWithCloseFuture implements HttpRequest { + + private final HttpHeaders headers; + private final boolean keepAlive; + + PublisherBasedHttpRequest(HttpHeaders headers, boolean keepAlive, + Publisher publisher) { + super(publisher); + this.headers = headers; + this.keepAlive = keepAlive; + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public boolean isKeepAlive() { + return keepAlive; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpResponse.java b/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpResponse.java new file mode 100644 index 000000000000..a61a52a63fd7 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/PublisherBasedHttpResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.http; + +import org.reactivestreams.Publisher; + +import com.linecorp.armeria.common.reactivestreams.PublisherWithCloseFuture; + +final class PublisherBasedHttpResponse extends PublisherWithCloseFuture implements HttpResponse { + PublisherBasedHttpResponse(Publisher publisher) { + super(publisher); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/StringValueConverter.java b/src/main/java/com/linecorp/armeria/common/http/StringValueConverter.java new file mode 100644 index 000000000000..5bad09973e2c --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/http/StringValueConverter.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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. + */ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.common.http; + +import java.text.ParseException; + +import io.netty.handler.codec.ValueConverter; +import io.netty.util.internal.PlatformDependent; + +/** + * Converts to/from native types, general {@link Object}, and {@link CharSequence}s. + */ +final class StringValueConverter implements ValueConverter { + + static final StringValueConverter INSTANCE = new StringValueConverter(); + + private StringValueConverter() {} + + @Override + public String convertObject(Object value) { + if (value == null) { + return null; + } + + return value.toString(); + } + + @Override + public String convertInt(int value) { + return String.valueOf(value); + } + + @Override + public String convertLong(long value) { + return String.valueOf(value); + } + + @Override + public String convertDouble(double value) { + return String.valueOf(value); + } + + @Override + public String convertChar(char value) { + return String.valueOf(value); + } + + @Override + public String convertBoolean(boolean value) { + return String.valueOf(value); + } + + @Override + public String convertFloat(float value) { + return String.valueOf(value); + } + + @Override + public boolean convertToBoolean(String value) { + return Boolean.parseBoolean(value); + } + + @Override + public String convertByte(byte value) { + return String.valueOf(value & 0xFF); + } + + @Override + public byte convertToByte(String value) { + return (byte) value.charAt(0); + } + + @Override + public char convertToChar(String value) { + return value.charAt(0); + } + + @Override + public String convertShort(short value) { + return String.valueOf(value); + } + + @Override + public short convertToShort(String value) { + return Short.valueOf(value); + } + + @Override + public int convertToInt(String value) { + return Integer.parseInt(value); + } + + @Override + public long convertToLong(String value) { + return Long.parseLong(value); + } + + @Override + public String convertTimeMillis(long value) { + return HeaderDateFormat.get().format(value); + } + + @Override + public long convertToTimeMillis(String value) { + try { + return HeaderDateFormat.get().parse(value); + } catch (ParseException e) { + PlatformDependent.throwException(e); + return 0; + } + } + + @Override + public float convertToFloat(String value) { + return Float.valueOf(value); + } + + @Override + public double convertToDouble(String value) { + return Double.valueOf(value); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/package-info.java b/src/main/java/com/linecorp/armeria/common/http/package-info.java index 65ae792c1845..89b90017d5e5 100644 --- a/src/main/java/com/linecorp/armeria/common/http/package-info.java +++ b/src/main/java/com/linecorp/armeria/common/http/package-info.java @@ -15,6 +15,6 @@ */ /** - * HTTP1/2-related classes used internally. + * HTTP-related classes. */ package com.linecorp.armeria.common.http; diff --git a/src/main/java/com/linecorp/armeria/common/logging/AbstractMessageLog.java b/src/main/java/com/linecorp/armeria/common/logging/AbstractMessageLog.java new file mode 100644 index 000000000000..3d908e7f3d52 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/AbstractMessageLog.java @@ -0,0 +1,219 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.google.common.base.MoreObjects; + +import com.linecorp.armeria.common.util.UnitFormatter; + +abstract class AbstractMessageLog + extends CompletableFuture implements MessageLog, MessageLogBuilder { + + private boolean startTimeNanosSet; + private long startTimeNanos; + private long contentLength; + private long endTimeNanos; + private List attachments; + private Throwable cause; + + boolean start0() { + if (isDone()) { + return false; + } + + if (!startTimeNanosSet) { + startTimeNanos = System.nanoTime(); + startTimeNanosSet = true; + } + + return true; + } + + @Override + public boolean isStarted() { + return startTimeNanosSet; + } + + @Override + public long startTimeNanos() { + return startTimeNanos; + } + + @Override + public void increaseContentLength(long deltaBytes) { + if (deltaBytes < 0) { + throw new IllegalArgumentException("deltaBytes: " + deltaBytes + " (expected: >= 0)"); + } + if (isDone()) { + return; + } + + contentLength += deltaBytes; + } + + @Override + public void contentLength(long contentLength) { + if (contentLength < 0) { + throw new IllegalArgumentException("contentLength: " + contentLength + " (expected: >= 0)"); + } + if (isDone()) { + return; + } + + this.contentLength = contentLength; + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public void attach(Object attachment) { + requireNonNull(attachment, "attachment"); + if (isDone()) { + return; + } + + if (attachments == null) { + attachments = new ArrayList<>(4); + } + + attachments.add(attachment); + } + + @Override + public List attachments() { + if (attachments != null) { + return attachments; + } else { + return Collections.emptyList(); + } + } + + @Override + public A attachment(Class type) { + if (attachments == null) { + return null; + } + + for (Object a : attachments) { + if (type.isInstance(a)) { + @SuppressWarnings("unchecked") + A cast = (A) a; + return cast; + } + } + + return null; + } + + @Override + public void end() { + end0(null); + } + + @Override + public void end(Throwable cause) { + requireNonNull(cause, "cause"); + end0(cause); + } + + private void end0(Throwable cause) { + if (isDone()) { + return; + } + + if (!startTimeNanosSet) { + throw new IllegalStateException("start() not called yet"); + } + + this.cause = cause; + + if (attachments != null) { + final List> dependencies = new ArrayList<>(attachments.size()); + for (Object a : attachments) { + if (a instanceof CompletableFuture) { + final CompletableFuture f = (CompletableFuture) a; + if (!f.isDone()) { + dependencies.add(f); + } + } + } + + final CompletableFuture future; + switch (dependencies.size()) { + case 0: + complete(); + return; + case 1: + future = dependencies.get(0); + break; + default: + future = CompletableFuture.allOf( + dependencies.toArray(new CompletableFuture[dependencies.size()])); + } + + future.whenComplete((unused1, unused2) -> complete()); + } else { + complete(); + } + } + + private void complete() { + endTimeNanos = System.nanoTime(); + complete(self()); + } + + @Override + public long endTimeNanos() { + return endTimeNanos; + } + + @Override + public Throwable cause() { + return cause; + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + @Override + public final String toString() { + final MoreObjects.ToStringHelper helper = + MoreObjects.toStringHelper("") + .add("timeSpan", + startTimeNanos + "+" + UnitFormatter.elapsed(startTimeNanos, endTimeNanos)) + .add("contentLength", UnitFormatter.size(contentLength)); + + append(helper); + + return helper.add("cause", cause) + .add("attachments", attachments()).toString(); + } + + protected abstract void append(MoreObjects.ToStringHelper helper); +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java b/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java new file mode 100644 index 000000000000..5734dc20ef6f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/DefaultRequestLog.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.MoreObjects.ToStringHelper; + +import com.linecorp.armeria.common.Scheme; +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; + +import io.netty.channel.Channel; + +public final class DefaultRequestLog + extends AbstractMessageLog implements RequestLog, RequestLogBuilder { + + private Channel channel; + private SessionProtocol sessionProtocol; + private SerializationFormat serializationFormat = SerializationFormat.NONE; + private String host; + private String method; + private String path; + + @Override + public void start(Channel channel, SessionProtocol sessionProtocol, String host, String method, String path) { + requireNonNull(channel, "channel"); + requireNonNull(sessionProtocol, "sessionProtocol"); + requireNonNull(host, "host"); + requireNonNull(method, "method"); + requireNonNull(path, "path"); + + if (!start0()) { + return; + } + + this.channel = channel; + this.sessionProtocol = sessionProtocol; + this.host = host; + this.method = method; + this.path = path; + } + + @Override + public Channel channel() { + return channel; + } + + @Override + public void serializationFormat(SerializationFormat serializationFormat) { + requireNonNull(serializationFormat, "serializationFormat"); + if (isDone()) { + return; + } + this.serializationFormat = serializationFormat; + } + + @Override + public Scheme scheme() { + return Scheme.of(serializationFormat, sessionProtocol); + } + + @Override + public String host() { + return host; + } + + @Override + public String method() { + return method; + } + + @Override + public String path() { + return path; + } + + @Override + protected void append(ToStringHelper helper) { + helper.add("channel", channel) + .add("scheme", (serializationFormat != null ? serializationFormat.uriText() : null) + '+' + + (sessionProtocol != null ? sessionProtocol.uriText() : null)) + .add("host", host) + .add("method", method) + .add("path", path); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/DefaultResponseLog.java b/src/main/java/com/linecorp/armeria/common/logging/DefaultResponseLog.java new file mode 100644 index 000000000000..5eb3b99d657e --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/DefaultResponseLog.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import com.google.common.base.MoreObjects.ToStringHelper; + +public final class DefaultResponseLog + extends AbstractMessageLog implements ResponseLog, ResponseLogBuilder { + + private final RequestLog request; + private int statusCode; + + public DefaultResponseLog(RequestLog request) { + this.request = request; + } + + @Override + public void start() { + start0(); + } + + @Override + public RequestLog request() { + return request; + } + + @Override + public void statusCode(int statusCode) { + if (isDone()) { + return; + } + + this.statusCode = statusCode; + } + + @Override + public int statusCode() { + return statusCode; + } + + @Override + protected void append(ToStringHelper helper) { + helper.add("statusCode", statusCode); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/LogLevel.java b/src/main/java/com/linecorp/armeria/common/logging/LogLevel.java new file mode 100644 index 000000000000..707330b4a874 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/LogLevel.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import org.slf4j.Logger; + +public enum LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR; + + @SuppressWarnings("MethodParameterNamingConvention") + public void log(Logger logger, String format, Object arg1) { + switch (this) { + case TRACE: + logger.trace(format, arg1); + break; + case DEBUG: + logger.debug(format, arg1); + break; + case INFO: + logger.info(format, arg1); + break; + case WARN: + logger.warn(format, arg1); + break; + case ERROR: + logger.error(format, arg1); + break; + default: + throw new Error(); + } + } + + @SuppressWarnings("MethodParameterNamingConvention") + public void log(Logger logger, String format, Object arg1, Object arg2) { + switch (this) { + case TRACE: + logger.trace(format, arg1, arg2); + break; + case DEBUG: + logger.debug(format, arg1, arg2); + break; + case INFO: + logger.info(format, arg1, arg2); + break; + case WARN: + logger.warn(format, arg1, arg2); + break; + case ERROR: + logger.error(format, arg1, arg2); + break; + default: + throw new Error(); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/MessageLog.java b/src/main/java/com/linecorp/armeria/common/logging/MessageLog.java new file mode 100644 index 000000000000..2d4ce253b1e0 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/MessageLog.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import java.util.List; + +public interface MessageLog { + long contentLength(); + long startTimeNanos(); + long endTimeNanos(); + Throwable cause(); // non-null if failed without sending a full message. + List attachments(); + T attachment(Class type); +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/MessageLogBuilder.java b/src/main/java/com/linecorp/armeria/common/logging/MessageLogBuilder.java new file mode 100644 index 000000000000..1bd33550eae0 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/MessageLogBuilder.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +public interface MessageLogBuilder { + boolean isStarted(); + void increaseContentLength(long deltaBytes); + void contentLength(long contentLength); + void attach(Object attachment); + void end(); + void end(Throwable cause); +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/RequestLog.java b/src/main/java/com/linecorp/armeria/common/logging/RequestLog.java new file mode 100644 index 000000000000..efd2984bd994 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/RequestLog.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import com.linecorp.armeria.common.Scheme; + +import io.netty.channel.Channel; + +public interface RequestLog extends MessageLog { + Channel channel(); + Scheme scheme(); + String host(); + String method(); + String path(); +} diff --git a/src/main/java/com/linecorp/armeria/common/logging/RequestLogBuilder.java b/src/main/java/com/linecorp/armeria/common/logging/RequestLogBuilder.java new file mode 100644 index 000000000000..2e4f1e5fd56a --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/logging/RequestLogBuilder.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.logging; + +import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.SessionProtocol; + +import io.netty.channel.Channel; + +public interface RequestLogBuilder extends MessageLogBuilder { + void start(Channel channel, SessionProtocol sessionProtocol, String host, String method, String path); + void serializationFormat(SerializationFormat serializationFormat); +} diff --git a/src/main/java/com/linecorp/armeria/client/routing/WeightedEndpoint.java b/src/main/java/com/linecorp/armeria/common/logging/ResponseLog.java similarity index 81% rename from src/main/java/com/linecorp/armeria/client/routing/WeightedEndpoint.java rename to src/main/java/com/linecorp/armeria/common/logging/ResponseLog.java index f376195a96d8..d63d5678446f 100644 --- a/src/main/java/com/linecorp/armeria/client/routing/WeightedEndpoint.java +++ b/src/main/java/com/linecorp/armeria/common/logging/ResponseLog.java @@ -13,8 +13,10 @@ * License for the specific language governing permissions and limitations * under the License. */ -package com.linecorp.armeria.client.routing; -public interface WeightedEndpoint extends Endpoint { - int weight(); +package com.linecorp.armeria.common.logging; + +public interface ResponseLog extends MessageLog { + RequestLog request(); + int statusCode(); } diff --git a/src/main/java/com/linecorp/armeria/client/HttpSession.java b/src/main/java/com/linecorp/armeria/common/logging/ResponseLogBuilder.java similarity index 53% rename from src/main/java/com/linecorp/armeria/client/HttpSession.java rename to src/main/java/com/linecorp/armeria/common/logging/ResponseLogBuilder.java index 0cbbd6729432..4277c523889c 100644 --- a/src/main/java/com/linecorp/armeria/client/HttpSession.java +++ b/src/main/java/com/linecorp/armeria/common/logging/ResponseLogBuilder.java @@ -14,40 +14,38 @@ * under the License. */ -package com.linecorp.armeria.client; +package com.linecorp.armeria.common.logging; -import com.linecorp.armeria.common.SessionProtocol; +public interface ResponseLogBuilder extends MessageLogBuilder { -interface HttpSession { - - HttpSession INACTIVE = new HttpSession() { + ResponseLogBuilder NOOP = new ResponseLogBuilder() { @Override - public SessionProtocol protocol() { - return null; + public boolean isStarted() { + return true; } @Override - public boolean isActive() { - return false; - } + public void statusCode(int statusCode) {} @Override - public boolean onRequestSent() { - throw new IllegalStateException(); - } + public void start() {} @Override - public void retryWithH1C() { - throw new IllegalStateException(); - } + public void increaseContentLength(long deltaBytes) {} + + @Override + public void contentLength(long contentLength) {} + + @Override + public void attach(Object attachment) {} + + @Override + public void end() {} @Override - public void deactivate() {} + public void end(Throwable cause) {} }; - SessionProtocol protocol(); - boolean isActive(); - boolean onRequestSent(); - void retryWithH1C(); - void deactivate(); + void start(); + void statusCode(int statusCode); } diff --git a/src/main/java/com/linecorp/armeria/common/metrics/DropwizardMetricConsumer.java b/src/main/java/com/linecorp/armeria/common/metrics/DropwizardMetricConsumer.java index dacf0c40c505..a215561276d1 100644 --- a/src/main/java/com/linecorp/armeria/common/metrics/DropwizardMetricConsumer.java +++ b/src/main/java/com/linecorp/armeria/common/metrics/DropwizardMetricConsumer.java @@ -3,14 +3,16 @@ import static java.util.Objects.requireNonNull; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import com.codahale.metrics.MetricRegistry; import com.linecorp.armeria.client.metrics.MetricCollectingClient; -import com.linecorp.armeria.common.Scheme; -import com.linecorp.armeria.server.ServiceCodec; +import com.linecorp.armeria.common.RpcRequest; +import com.linecorp.armeria.common.RpcResponse; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.ResponseLog; import com.linecorp.armeria.server.metrics.MetricCollectingService; /** @@ -24,7 +26,7 @@ public class DropwizardMetricConsumer implements MetricConsumer { private final Map methodRequestMetrics; /** - * Creates a new instance that decorates the specified {@link ServiceCodec}. + * Creates a new instance. */ public DropwizardMetricConsumer(MetricRegistry metricRegistry, String metricNamePrefix) { this.metricRegistry = requireNonNull(metricRegistry, "metricRegistry"); @@ -33,32 +35,63 @@ public DropwizardMetricConsumer(MetricRegistry metricRegistry, String metricName } @Override - public void invocationStarted(Scheme scheme, String hostname, String path, Optional method) { - final String metricName = MetricRegistry.name(metricNamePrefix, method.orElse("__unknown__")); + public void onRequest(RequestLog req) { + final String metricName = MetricRegistry.name(metricNamePrefix, method(req)); final DropwizardRequestMetrics metrics = getRequestMetrics(metricName); - metrics.markStart(); + if (req.cause() == null) { + metrics.markStart(); + } else { + metrics.markFailure(); + } } @Override - public void invocationComplete(Scheme scheme, int code, long processTimeNanos, int requestSize, - int responseSize, String hostname, String path, Optional method, - boolean started) { - - final String metricName = MetricRegistry.name(metricNamePrefix, method.orElse("__unknown__")); + public void onResponse(ResponseLog res) { + final RequestLog req = res.request(); + final String metricName = MetricRegistry.name(metricNamePrefix, method(req)); final DropwizardRequestMetrics metrics = getRequestMetrics(metricName); - metrics.updateTime(processTimeNanos); - if (code < 400) { + metrics.updateTime(res.endTimeNanos() - req.startTimeNanos()); + if (isSuccess(res)) { metrics.markSuccess(); } else { metrics.markFailure(); } - metrics.requestBytes(requestSize); - metrics.responseBytes(responseSize); - if (started) { + metrics.requestBytes(req.contentLength()); + metrics.responseBytes(res.contentLength()); + if (req.cause() == null) { metrics.markComplete(); } } + private static boolean isSuccess(ResponseLog res) { + if (res.cause() != null) { + return false; + } + + if (SessionProtocol.ofHttp().contains(res.request().scheme().sessionProtocol())) { + if (res.statusCode() >= 400) { + return false; + } + } else { + if (res.statusCode() != 0) { + return false; + } + } + + final RpcResponse rpcRes = res.attachment(RpcResponse.class); + return rpcRes == null || rpcRes.getCause() == null; + } + + private static String method(RequestLog log) { + final RpcRequest rpcReq = log.attachment(RpcRequest.class); + if (rpcReq != null) { + return rpcReq.method(); + } + + final String method = log.method(); + return method != null ? method : "__unknown__"; + } + private DropwizardRequestMetrics getRequestMetrics(String methodLoggedName) { return methodRequestMetrics.computeIfAbsent( methodLoggedName, diff --git a/src/main/java/com/linecorp/armeria/common/metrics/DropwizardRequestMetrics.java b/src/main/java/com/linecorp/armeria/common/metrics/DropwizardRequestMetrics.java index f33249174a4c..735aae71372c 100644 --- a/src/main/java/com/linecorp/armeria/common/metrics/DropwizardRequestMetrics.java +++ b/src/main/java/com/linecorp/armeria/common/metrics/DropwizardRequestMetrics.java @@ -59,11 +59,11 @@ public void markComplete() { activeRequests.dec(); } - public void requestBytes(int requestBytes) { + public void requestBytes(long requestBytes) { this.requestBytes.mark(requestBytes); } - public void responseBytes(int responseBytes) { + public void responseBytes(long responseBytes) { this.responseBytes.mark(responseBytes); } diff --git a/src/main/java/com/linecorp/armeria/common/metrics/MetricConsumer.java b/src/main/java/com/linecorp/armeria/common/metrics/MetricConsumer.java index dbf0e5a36619..ba424ef021fa 100644 --- a/src/main/java/com/linecorp/armeria/common/metrics/MetricConsumer.java +++ b/src/main/java/com/linecorp/armeria/common/metrics/MetricConsumer.java @@ -23,60 +23,45 @@ import com.linecorp.armeria.common.Scheme; import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.ResponseLog; public interface MetricConsumer { /** - * Invoked when a request is being started - * - * @param scheme the {@link Scheme} which the invocation has been performed on. + * Invoked when a request has been streamed. */ - void invocationStarted(Scheme scheme, String hostname, String path, Optional method); + void onRequest(RequestLog req); /** - * Invoked for each request that has been processed - * - * @param scheme the {@link Scheme} which the invocation has been performed on. - * @param code the {@link SessionProtocol}-specific status code that signifies the result of the - * invocation. e.g. HTTP response status code - * @param processTimeNanos elapsed nano time processing request - * @param requestSize number of bytes in request if possible, otherwise it will be 0 - * @param responseSize number of bytes in response if possible, otherwise it will be 0 - * @param started true if invocationStarted() is called before, otherwise false + * Invoked when a response has been streamed. */ - void invocationComplete(Scheme scheme, int code, long processTimeNanos, int requestSize, - int responseSize, String hostname, String path, Optional method, - boolean started); + void onResponse(ResponseLog res); default MetricConsumer andThen(MetricConsumer other) { Objects.requireNonNull(other, "other"); MetricConsumer outer = this; return new MetricConsumer() { - @Override - public void invocationStarted(Scheme scheme, String hostname, String path, Optional method) { + public void onRequest(RequestLog req) { try { - outer.invocationStarted(scheme, hostname, path, method); + outer.onRequest(req); } catch (Throwable e) { LoggerFactory.getLogger(MetricConsumer.class) .warn("invocationStarted() failed with an exception: {}", e); } - other.invocationStarted(scheme, hostname, path, method); + other.onRequest(req); } @Override - public void invocationComplete(Scheme scheme, int code, long processTimeNanos, int requestSize, - int responseSize, String hostname, String path, - Optional method, boolean started) { + public void onResponse(ResponseLog res) { try { - outer.invocationComplete(scheme, code, processTimeNanos, requestSize, responseSize, - hostname, path, method, started); + outer.onResponse(res); } catch (Throwable e) { LoggerFactory.getLogger(MetricConsumer.class) .warn("invocationComplete() failed with an exception: {}", e); } - other.invocationComplete(scheme, code, processTimeNanos, requestSize, responseSize, - hostname, path, method, started); + other.onResponse(res); } }; } diff --git a/src/main/java/com/linecorp/armeria/common/package-info.java b/src/main/java/com/linecorp/armeria/common/package-info.java index 3fdac20d6f52..9e6c4913d151 100644 --- a/src/main/java/com/linecorp/armeria/common/package-info.java +++ b/src/main/java/com/linecorp/armeria/common/package-info.java @@ -19,8 +19,7 @@ * *

Starting points

*
    - *
  • {@link com.linecorp.armeria.common.ServiceInvocationContext}
  • - *
  • {@link com.linecorp.armeria.common.TimeoutPolicy}
  • + *
  • {@link com.linecorp.armeria.common.RequestContext}
  • *
*/ package com.linecorp.armeria.common; diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/AbortingSubscriber.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/AbortingSubscriber.java new file mode 100644 index 000000000000..01f4438fc2e8 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/AbortingSubscriber.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +final class AbortingSubscriber implements Subscriber { + + static final AbortingSubscriber INSTANCE = new AbortingSubscriber(); + + private AbortingSubscriber() {} + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + s.cancel(); + } + + @Override + public void onNext(Object o) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onComplete() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/CancelledSubscriptionException.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/CancelledSubscriptionException.java new file mode 100644 index 000000000000..18b140398651 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/CancelledSubscriptionException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import com.linecorp.armeria.common.util.Exceptions; + +public final class CancelledSubscriptionException extends RuntimeException { + + private static final long serialVersionUID = -7815958463104921571L; + + private static final CancelledSubscriptionException INSTANCE = + Exceptions.clearTrace(new CancelledSubscriptionException()); + + public static CancelledSubscriptionException get() { + return Exceptions.isVerbose() ? new CancelledSubscriptionException() : INSTANCE; + } + + private CancelledSubscriptionException() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/ClosedPublisherException.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/ClosedPublisherException.java new file mode 100644 index 000000000000..d6ef0b77f1ef --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/ClosedPublisherException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import com.linecorp.armeria.common.util.Exceptions; + +public final class ClosedPublisherException extends RuntimeException { + + private static final long serialVersionUID = -7665826869012452735L; + + private static final ClosedPublisherException INSTANCE = + Exceptions.clearTrace(new ClosedPublisherException()); + + public static ClosedPublisherException get() { + return Exceptions.isVerbose() ? new ClosedPublisherException() : INSTANCE; + } + + private ClosedPublisherException() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/FunctionProcessor.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/FunctionProcessor.java new file mode 100644 index 000000000000..a7ed508d065d --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/FunctionProcessor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Function; + +import org.reactivestreams.Processor; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public final class FunctionProcessor implements Processor { + + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater subscriptionUpdater = + AtomicReferenceFieldUpdater.newUpdater(FunctionProcessor.class, Subscription.class, "subscription"); + + @SuppressWarnings({ "rawtypes", "AtomicFieldUpdaterIssues" }) + private static final AtomicReferenceFieldUpdater subscriberUpdater = + AtomicReferenceFieldUpdater.newUpdater(FunctionProcessor.class, Subscriber.class, "subscriber"); + + private final Function function; + + @SuppressWarnings("unused") + private volatile Subscription subscription; + @SuppressWarnings("unused") + private volatile Subscriber subscriber; + + public FunctionProcessor(Function function) { + this.function = requireNonNull(function, "function"); + } + + @Override + public void subscribe(Subscriber subscriber) { + requireNonNull(subscriber, "subscriber"); + if (!subscriberUpdater.compareAndSet(this, null, subscriber)) { + throw new IllegalStateException("subscribed by other subscriber already: " + this.subscriber); + } + + final Subscription subscription = this.subscription; + if (subscription != null) { + subscriber.onSubscribe(subscription); + } + } + + @Override + public void onSubscribe(Subscription subscription) { + requireNonNull(subscription, "subscription"); + if (!subscriptionUpdater.compareAndSet(this, null, subscription)) { + throw new IllegalStateException("subscribed to other publisher already: " + this.subscription); + } + + final Subscriber subscriber = this.subscriber; + if (subscriber != null) { + subscriber.onSubscribe(subscription); + } + } + + @Override + public void onNext(T obj) { + subscriber().onNext(function.apply(obj)); + } + + @Override + public void onError(Throwable cause) { + subscriber().onError(cause); + } + + @Override + public void onComplete() { + subscriber().onComplete(); + } + + private Subscriber subscriber() { + final Subscriber subscriber = this.subscriber; + if (subscriber == null) { + throw new IllegalStateException(); + } + + return subscriber; + } +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/PublisherWithCloseFuture.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/PublisherWithCloseFuture.java new file mode 100644 index 000000000000..f821415b46fe --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/PublisherWithCloseFuture.java @@ -0,0 +1,141 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class PublisherWithCloseFuture implements RichPublisher { + + private final Publisher publisher; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + + public PublisherWithCloseFuture(Publisher publisher) { + this.publisher = publisher; + } + + protected Publisher delegate() { + return publisher; + } + + @Override + public boolean isOpen() { + return !closeFuture.isDone(); + } + + @Override + public void subscribe(Subscriber subscriber) { + requireNonNull(subscriber, "subscriber"); + publisher.subscribe(new SubscriberImpl(subscriber, null)); + } + + @Override + public void subscribe(Subscriber subscriber, Executor executor) { + requireNonNull(subscriber, "subscriber"); + requireNonNull(executor, "executor"); + publisher.subscribe(new SubscriberImpl(subscriber, executor)); + } + + @Override + public void abort() { + subscribe(AbortingSubscriber.INSTANCE); + } + + @Override + public CompletableFuture awaitClose() { + return closeFuture; + } + + private final class SubscriberImpl implements Subscriber { + private final Subscriber subscriber; + private final Executor executor; + + SubscriberImpl(Subscriber subscriber, Executor executor) { + this.subscriber = subscriber; + this.executor = executor; + } + + @Override + public void onSubscribe(Subscription s) { + final Executor executor = this.executor; + if (executor == null) { + subscriber.onSubscribe(s); + } else { + executor.execute(() -> subscriber.onSubscribe(s)); + } + + } + + @Override + public void onNext(V obj) { + final Executor executor = this.executor; + if (executor == null) { + subscriber.onNext(obj); + } else { + executor.execute(() -> subscriber.onNext(obj)); + } + } + + @Override + public void onError(Throwable cause) { + final Executor executor = this.executor; + if (executor == null) { + onError0(cause); + } else { + executor.execute(() -> onError0(cause)); + } + } + + private void onError0(Throwable cause) { + try { + subscriber.onError(cause); + } finally { + CompletableFuture closeFuture = PublisherWithCloseFuture.this.closeFuture; + if (closeFuture != null) { + closeFuture.completeExceptionally(cause); + } + } + } + + @Override + public void onComplete() { + final Executor executor = this.executor; + if (executor == null) { + onComplete0(); + } else { + executor.execute(this::onComplete0); + } + } + + private void onComplete0() { + try { + subscriber.onComplete(); + } finally { + CompletableFuture closeFuture = PublisherWithCloseFuture.this.closeFuture; + if (closeFuture != null) { + closeFuture.complete(null); + } + } + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/QueueBasedPublisher.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/QueueBasedPublisher.java new file mode 100644 index 000000000000..626ead50279f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/QueueBasedPublisher.java @@ -0,0 +1,389 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import static java.util.Objects.requireNonNull; + +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Supplier; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import com.google.common.base.MoreObjects; + +import com.linecorp.armeria.common.util.Exceptions; + +public class QueueBasedPublisher implements RichPublisher, Writer { + + private static final CloseEvent SUCCESSFUL_CLOSE = new CloseEvent(null); + private static final CloseEvent CANCELLED_CLOSE = new CloseEvent( + Exceptions.clearTrace(CancelledSubscriptionException.get())); + + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater subscriptionUpdater = + AtomicReferenceFieldUpdater.newUpdater(QueueBasedPublisher.class, SubscriptionImpl.class, "subscription"); + + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater demandUpdater = + AtomicLongFieldUpdater.newUpdater(QueueBasedPublisher.class, "demand"); + + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater terminatedUpdater = + AtomicIntegerFieldUpdater.newUpdater(QueueBasedPublisher.class, "terminated"); + + private final Queue queue; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + + @SuppressWarnings("unused") + private volatile SubscriptionImpl subscription; // set only via subscriptionUpdater + + @SuppressWarnings("unused") + private volatile long demand; // set only via demandUpdater + private volatile int terminated; // 0 - not terminated, 1 - terminated + + public QueueBasedPublisher() { + this(new ConcurrentLinkedQueue<>()); + } + + public QueueBasedPublisher(Queue queue) { + this.queue = requireNonNull(queue, "queue"); + } + + @Override + public boolean isOpen() { + return terminated == 0; + } + + @Override + public void subscribe(Subscriber subscriber) { + requireNonNull(subscriber, "subscriber"); + subscribe0(new SubscriptionImpl(this, subscriber, null)); + } + + @Override + public void subscribe(Subscriber subscriber, Executor executor) { + requireNonNull(subscriber, "subscriber"); + requireNonNull(executor, "executor"); + subscribe0(new SubscriptionImpl(this, subscriber, executor)); + } + + private void subscribe0(SubscriptionImpl subscription) { + if (!subscriptionUpdater.compareAndSet(this, null, subscription)) { + throw new IllegalStateException( + "subscribed by other subscriber already: " + this.subscription.subscriber()); + } + + final Executor executor = subscription.executor(); + if (executor != null) { + executor.execute(() -> subscription.subscriber().onSubscribe(subscription)); + } else { + subscription.subscriber().onSubscribe(subscription); + } + } + + @Override + public void abort() { + final SubscriptionImpl subscription = new SubscriptionImpl(this, AbortingSubscriber.INSTANCE, null); + if (subscriptionUpdater.compareAndSet(this, null, subscription)) { + subscription.subscriber().onSubscribe(subscription); + } else { + this.subscription.cancel(); + } + } + + @Override + public boolean write(T obj) { + requireNonNull(obj, "obj"); + if (!isOpen()) { + return false; + } + + pushObject(obj); + return true; + } + + @Override + public boolean write(Supplier supplier) { + return write(supplier.get()); + } + + @Override + public CompletableFuture awaitDemand() { + final AwaitDemandFuture f = new AwaitDemandFuture(); + if (!isOpen()) { + f.completeExceptionally(ClosedPublisherException.get()); + return f; + } + + pushObject(f); + return f; + } + + private void pushObject(Object obj) { + queue.add(obj); + notifySubscriber(); + } + + protected void notifySubscriber() { + final SubscriptionImpl subscription = this.subscription; + if (subscription == null) { + return; + } + + final Queue queue = this.queue; + if (queue.isEmpty()) { + return; + } + + final Executor executor = subscription.executor(); + if (executor != null) { + executor.execute(() -> notifySubscribers0(subscription.subscriber(), queue)); + } else { + notifySubscribers0(subscription.subscriber(), queue); + } + } + + private void notifySubscribers0(Subscriber subscriber, Queue queue) { + for (;;) { + final Object o = queue.peek(); + if (o == null) { + break; + } + + if (o instanceof CloseEvent) { + notifySubscriberWithCloseEvent(subscriber, (CloseEvent) o); + break; + } + + if (o instanceof AwaitDemandFuture) { + if (notifyCompletableFuture(queue)) { + // Notified successfully. + continue; + } else { + // Not enough demand. + break; + } + } + + if (!notifySubscriber(subscriber, queue)) { + // Not enough demand. + break; + } + } + } + + private void notifySubscriberWithCloseEvent(Subscriber subscriber, CloseEvent o) { + destroy(ClosedPublisherException.get()); + + final Throwable cause = o.cause(); + if (cause == null) { + try { + subscriber.onComplete(); + } finally { + closeFuture.complete(null); + } + } else { + try { + if (!o.isCancelled()) { + subscriber.onError(cause); + } + } finally { + closeFuture.completeExceptionally(cause); + } + } + } + + private boolean notifyCompletableFuture(Queue queue) { + if (demand == 0) { + return false; + } + + @SuppressWarnings("unchecked") + final CompletableFuture f = (CompletableFuture) queue.remove(); + f.complete(null); + + return true; + } + + private boolean notifySubscriber(Subscriber subscriber, Queue queue) { + for (;;) { + final long demand = this.demand; + if (demand == 0) { + break; + } + + if (demand == Long.MAX_VALUE || demandUpdater.compareAndSet(this, demand, demand - 1)) { + @SuppressWarnings("unchecked") + final T o = (T) queue.remove(); + onRemoval(o); + subscriber.onNext(o); + return true; + } + } + + return false; + } + + protected void onRemoval(T obj) {} + + @Override + public CompletableFuture awaitClose() { + return closeFuture; + } + + @Override + public void close() { + if (setTerminated()) { + pushObject(SUCCESSFUL_CLOSE); + } + } + + @Override + public void close(Throwable cause) { + requireNonNull(cause, "cause"); + if (setTerminated()) { + pushObject(new CloseEvent(cause)); + } + } + + private boolean setTerminated() { + return terminatedUpdater.compareAndSet(this, 0, 1); + } + + private void destroy(Throwable cause) { + terminated = 1; + for (;;) { + final Object e = queue.poll(); + if (e == null) { + break; + } + + if (e instanceof CloseEvent) { + continue; + } + + if (e instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + final CompletableFuture f = (CompletableFuture) e; + f.completeExceptionally(cause); + } + + @SuppressWarnings("unchecked") + T obj = (T) e; + onRemoval(obj); + } + } + + private static final class SubscriptionImpl implements Subscription { + + private final QueueBasedPublisher publisher; + private final Subscriber subscriber; + private final Executor executor; + + @SuppressWarnings("unchecked") + SubscriptionImpl(QueueBasedPublisher publisher, Subscriber subscriber, Executor executor) { + this.publisher = publisher; + this.subscriber = (Subscriber) subscriber; + this.executor = executor; + } + + Subscriber subscriber() { + return subscriber; + } + + Executor executor() { + return executor; + } + + @Override + public void request(long n) { + if (n <= 0) { + throw new IllegalArgumentException("n: " + n + " (expected: > 0)"); + } + + for (;;) { + final long oldDemand = publisher.demand; + final long newDemand; + if (oldDemand >= Long.MAX_VALUE - n) { + newDemand = Long.MAX_VALUE; + } else { + newDemand = oldDemand + n; + } + + if (demandUpdater.compareAndSet(publisher, oldDemand, newDemand)) { + if (oldDemand == 0) { + publisher.notifySubscriber(); + } + break; + } + } + } + + @Override + public void cancel() { + if (publisher.setTerminated()) { + final CloseEvent closeEvent = + Exceptions.isVerbose() ? new CloseEvent(CancelledSubscriptionException.get()) + : CANCELLED_CLOSE; + + publisher.pushObject(closeEvent); + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(Subscription.class) + .add("publisher", publisher) + .add("demand", publisher.demand) + .add("executor", executor).toString(); + } + } + + private static final class AwaitDemandFuture extends CompletableFuture {} + + private static final class CloseEvent { + private final Throwable cause; + + CloseEvent(Throwable cause) { + this.cause = cause; + } + + boolean isCancelled() { + return cause instanceof CancelledSubscriptionException; + } + + Throwable cause() { + return cause; + } + + @Override + public String toString() { + if (cause == null) { + return "CloseEvent"; + } else { + return "CloseEvent(" + cause + ')'; + } + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/RichPublisher.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/RichPublisher.java new file mode 100644 index 000000000000..0b3615f46524 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/RichPublisher.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +public interface RichPublisher extends Publisher { + boolean isOpen(); + CompletableFuture awaitClose(); + + void subscribe(Subscriber s, Executor executor); + void abort(); +} diff --git a/src/main/java/com/linecorp/armeria/common/reactivestreams/Writer.java b/src/main/java/com/linecorp/armeria/common/reactivestreams/Writer.java new file mode 100644 index 000000000000..dfcdce5b4972 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/reactivestreams/Writer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.reactivestreams; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public interface Writer { + + boolean isOpen(); + + boolean write(T o); + boolean write(Supplier o); + + CompletableFuture awaitDemand(); + + void close(); + void close(Throwable cause); +} diff --git a/src/main/java/com/linecorp/armeria/common/thrift/ThriftCall.java b/src/main/java/com/linecorp/armeria/common/thrift/ThriftCall.java new file mode 100644 index 000000000000..0a66a9334c77 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/thrift/ThriftCall.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.thrift; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +import org.apache.thrift.TBase; +import org.apache.thrift.TFieldIdEnum; +import org.apache.thrift.meta_data.FieldMetaData; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; + +import com.linecorp.armeria.common.AbstractRpcRequest; + +public final class ThriftCall extends AbstractRpcRequest { + + private final int seqId; + + public ThriftCall(int seqId, Class serviceType, String method, Iterable args) { + this(seqId, serviceType, method, ImmutableList.copyOf(args)); + } + + public ThriftCall(int seqId, Class serviceType, String method, Object... args) { + this(seqId, serviceType, method, ImmutableList.copyOf(args)); + } + + public ThriftCall(int seqId, Class serviceType, String method, TBase thriftArgs) { + this(seqId, serviceType, method, toList(thriftArgs)); + } + + @Nonnull + private static List toList(TBase thriftArgs) { + requireNonNull(thriftArgs, "thriftArgs"); + + @SuppressWarnings("unchecked") + final TBase, TFieldIdEnum> castThriftArgs = (TBase, TFieldIdEnum>) thriftArgs; + return Collections.unmodifiableList( + FieldMetaData.getStructMetaDataMap(castThriftArgs.getClass()).keySet().stream() + .map(castThriftArgs::getFieldValue).collect(Collectors.toList())); + } + + private ThriftCall(int seqId, Class serviceType, String method, List args) { + super(serviceType, method, args); + this.seqId = seqId; + } + + public int seqId() { + return seqId; + } + + @Override + public int hashCode() { + return seqId * 31 + method().hashCode() * 31 + params().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ThriftCall)) { + return false; + } + + if (this == obj) { + return true; + } + + final ThriftCall that = (ThriftCall) obj; + return seqId() == that.seqId() && + method().equals(that.method()) && + params().equals(that.params()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("seqId", seqId()) + .add("serviceType", simpleServiceName()) + .add("method", method()) + .add("args", params()).toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java b/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java new file mode 100644 index 000000000000..583aef0c8f50 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/common/thrift/ThriftReply.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.thrift; + +import com.google.common.base.MoreObjects; + +import com.linecorp.armeria.common.AbstractRpcResponse; + +public final class ThriftReply extends AbstractRpcResponse { + + private final int seqId; + + public ThriftReply(int seqId) { + this.seqId = seqId; + } + + public ThriftReply(int seqId, Object result) { + super(result); + this.seqId = seqId; + } + + public ThriftReply(int seqId, Throwable cause) { + super(cause); + this.seqId = seqId; + } + + public int seqId() { + return seqId; + } + + @Override + public String toString() { + if (!isDone()) { + return super.toString(); + } + + if (isCompletedExceptionally()) { + return MoreObjects.toStringHelper(this) + .add("seqId", seqId()) + .add("cause", getCause()).toString(); + } else { + return MoreObjects.toStringHelper(this) + .add("seqId", seqId()) + .add("value", getNow(null)).toString(); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/thrift/ThriftUtil.java b/src/main/java/com/linecorp/armeria/common/thrift/ThriftUtil.java deleted file mode 100644 index 0558600caefa..000000000000 --- a/src/main/java/com/linecorp/armeria/common/thrift/ThriftUtil.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.common.thrift; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.thrift.TBase; -import org.apache.thrift.TFieldIdEnum; -import org.apache.thrift.meta_data.FieldMetaData; -import org.apache.thrift.meta_data.FieldValueMetaData; -import org.apache.thrift.meta_data.StructMetaData; -import org.apache.thrift.protocol.TType; - -/** - * Utility methods for use by both {@code com.linecorp.armeria.client.thrift} and - * {@code com.linecorp.armeria.server.thrift}. - */ -public final class ThriftUtil { - - /** - * Converts the specified Thrift {@code seqId} to a hexadecimal {@link String}. - */ - public static String seqIdToString(int seqId) { - return Long.toString(seqId & 0xFFFFFFFFL, 16); - } - - /** - * Converts the specified Thrift call parameters to a list of Java objects. - */ - public static List toJavaParams(TBase, TFieldIdEnum> params) { - return Collections.unmodifiableList( - FieldMetaData.getStructMetaDataMap(params.getClass()).keySet().stream() - .map(params::getFieldValue).collect(Collectors.toList())); - } - - /** - * Converts the specified {@link FieldValueMetaData} into its corresponding Java type. - */ - public static Class toJavaType(FieldValueMetaData metadata) { - switch (metadata.type) { - case TType.BOOL: - return Boolean.class; - case TType.BYTE: - return Byte.class; - case TType.DOUBLE: - return Double.class; - case TType.ENUM: - return Enum.class; - case TType.I16: - return Short.class; - case TType.I32: - return Integer.class; - case TType.I64: - return Long.class; - case TType.LIST: - return List.class; - case TType.MAP: - return Map.class; - case TType.SET: - return Set.class; - case TType.STRING: - return String.class; - case TType.STRUCT: - return ((StructMetaData) metadata).structClass; - case TType.VOID: - return Void.class; - } - - // Should never reach here. - throw new Error(); - } - - private ThriftUtil() {} -} diff --git a/src/main/java/com/linecorp/armeria/common/util/AbstractOptions.java b/src/main/java/com/linecorp/armeria/common/util/AbstractOptions.java index ec605fedc3eb..a40b5cfe1566 100644 --- a/src/main/java/com/linecorp/armeria/common/util/AbstractOptions.java +++ b/src/main/java/com/linecorp/armeria/common/util/AbstractOptions.java @@ -79,6 +79,16 @@ public abstract class AbstractOptions { putAll(valueFilter, StreamSupport.stream(values.spliterator(), false)); } + protected > AbstractOptions(AbstractOptions baseOptions, + AbstractOptions options) { + + requireNonNull(baseOptions, "baseOptions"); + requireNonNull(options, "options"); + + valueMap = new IdentityHashMap<>(baseOptions.valueMap); + valueMap.putAll(options.valueMap); + } + @SuppressWarnings("unchecked") private > void putAll(Function valueFilter, Stream values) { values.map(valueFilter) diff --git a/src/main/java/com/linecorp/armeria/common/util/Exceptions.java b/src/main/java/com/linecorp/armeria/common/util/Exceptions.java index d99a8548653f..c7fcebd8f9e4 100644 --- a/src/main/java/com/linecorp/armeria/common/util/Exceptions.java +++ b/src/main/java/com/linecorp/armeria/common/util/Exceptions.java @@ -19,14 +19,15 @@ import static java.util.Objects.requireNonNull; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.nio.channels.ClosedChannelException; import java.util.regex.Pattern; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import com.linecorp.armeria.client.ClosedSessionException; +import com.google.common.base.Throwables; + +import com.linecorp.armeria.common.ClosedSessionException; import com.linecorp.armeria.common.SessionProtocol; import io.netty.channel.Channel; @@ -38,6 +39,8 @@ */ public final class Exceptions { + private static final Logger logger = LoggerFactory.getLogger(Exceptions.class); + private static final Pattern IGNORABLE_SOCKET_ERROR_MESSAGE = Pattern.compile( "(?:connection.*(?:reset|closed|abort|broken)|broken.*pipe)", Pattern.CASE_INSENSITIVE); @@ -46,6 +49,40 @@ public final class Exceptions { private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; + private static final boolean VERBOSE = + "true".equals(System.getProperty("com.linecorp.armeria.verboseExceptions", "false")); + + static { + logger.info("com.linecorp.armeria.verboseExceptions: {}", VERBOSE); + } + + public static boolean isVerbose() { + return VERBOSE; + } + + /** + * Logs the specified exception if it is {@linkplain #isExpected(Throwable)} unexpected}. + */ + public static void logIfUnexpected(Logger logger, Channel ch, Throwable cause) { + if (!logger.isWarnEnabled() || isExpected(cause)) { + return; + } + + logger.warn("{} Unexpected exception:", ch, cause); + } + + /** + * Logs the specified exception if it is {@linkplain #isExpected(Throwable)} unexpected}. + */ + public static void logIfUnexpected(Logger logger, Channel ch, String debugData, Throwable cause) { + + if (!logger.isWarnEnabled() || isExpected(cause)) { + return; + } + + logger.warn("{} Unexpected exception: {}", ch, debugData, cause); + } + /** * Logs the specified exception if it is {@linkplain #isExpected(Throwable)} unexpected}. */ @@ -87,6 +124,10 @@ private static String protocolName(SessionProtocol protocol) { * */ public static boolean isExpected(Throwable cause) { + if (VERBOSE) { + return true; + } + // We do not need to log every exception because some exceptions are expected to occur. if (cause instanceof ClosedChannelException || cause instanceof ClosedSessionException) { @@ -121,15 +162,13 @@ public static T clearTrace(T exception) { } /** - * Returns the stack trace of the specified {@code exception} as a {@link String}. + * Returns the stack trace of the specified {@code exception} as a {@link String} instead. + * + * @deprecated Use {@link Throwables#getStackTraceAsString(Throwable)}. */ + @Deprecated public static String traceText(Throwable exception) { - requireNonNull(exception, "exception"); - final StringWriter out = new StringWriter(256); - final PrintWriter pout = new PrintWriter(out); - exception.printStackTrace(pout); - pout.flush(); - return out.toString(); + return Throwables.getStackTraceAsString(exception); } private Exceptions() {} diff --git a/src/main/java/com/linecorp/armeria/common/util/LruMap.java b/src/main/java/com/linecorp/armeria/common/util/LruMap.java index fe4a88e9c919..0e54966fa5b6 100644 --- a/src/main/java/com/linecorp/armeria/common/util/LruMap.java +++ b/src/main/java/com/linecorp/armeria/common/util/LruMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance diff --git a/src/main/java/com/linecorp/armeria/common/util/UnitFormatter.java b/src/main/java/com/linecorp/armeria/common/util/UnitFormatter.java index df9c2e5c304c..0e1ea47c6060 100644 --- a/src/main/java/com/linecorp/armeria/common/util/UnitFormatter.java +++ b/src/main/java/com/linecorp/armeria/common/util/UnitFormatter.java @@ -26,18 +26,13 @@ public final class UnitFormatter { * Appends the number of readable bytes in the specified {@link ByteBuf} to the specified * {@link StringBuilder}. */ - public static void appendSize(StringBuilder buf, ByteBuf content) { - if (content != null) { - final int size = content.readableBytes(); - if (size >= 104857600) { // >= 100 MiB - buf.append(size / 1048576).append("MiB"); - } else if (size >= 102400) { // >= 100 KiB - buf.append(size / 1024).append("KiB"); - } else { - buf.append(size).append('B'); - } + public static void appendSize(StringBuilder buf, long size) { + if (size >= 104857600) { // >= 100 MiB + buf.append(size / 1048576).append("MiB"); + } else if (size >= 102400) { // >= 100 KiB + buf.append(size / 1024).append("KiB"); } else { - buf.append("null"); + buf.append(size).append('B'); } } @@ -60,13 +55,13 @@ public static void appendElapsed(StringBuilder buf, long startTimeNanos, long en /** * A shortcut method that calls {@link #appendElapsed(StringBuilder, long, long)} and - * {@link #appendSize(StringBuilder, ByteBuf)}, concatenated by {@code ", "}. + * {@link #appendSize(StringBuilder, long)}, concatenated by {@code ", "}. */ public static void appendElapsedAndSize(StringBuilder buf, long startTimeNanos, long endTimeNanos, - ByteBuf content) { + long size) { appendElapsed(buf, startTimeNanos, endTimeNanos); buf.append(", "); - appendSize(buf, content); + appendSize(buf, size); } /** @@ -83,19 +78,19 @@ public static StringBuilder elapsed(long startTimeNanos, long endTimeNanos) { * Creates a new {@link StringBuilder} whose content is the number of readable bytes in the specified * {@code ByteBuf}. */ - public static StringBuilder size(ByteBuf content) { + public static StringBuilder size(long size) { final StringBuilder buf = new StringBuilder(16); - appendSize(buf, content); + appendSize(buf, size); return buf; } /** - * Similar to {@link #appendElapsedAndSize(StringBuilder, long, long, ByteBuf)} except that this method + * Similar to {@link #appendElapsedAndSize(StringBuilder, long, long, long)} except that this method * creates a new {@link StringBuilder}. */ - public static StringBuilder elapsedAndSize(long startTimeNanos, long endTimeNanos, ByteBuf content) { + public static StringBuilder elapsedAndSize(long startTimeNanos, long endTimeNanos, long size) { final StringBuilder buf = new StringBuilder(16); - appendElapsedAndSize(buf, startTimeNanos, endTimeNanos, content); + appendElapsedAndSize(buf, startTimeNanos, endTimeNanos, size); return buf; } diff --git a/src/main/java/com/linecorp/armeria/internal/IdleTimeoutHandler.java b/src/main/java/com/linecorp/armeria/internal/IdleTimeoutHandler.java new file mode 100644 index 000000000000..ddad03516c2f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/IdleTimeoutHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal; + +import static java.util.Objects.requireNonNull; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; + +public abstract class IdleTimeoutHandler extends IdleStateHandler { + + private static final Logger logger = LoggerFactory.getLogger(IdleTimeoutHandler.class); + + private final String name; + + protected IdleTimeoutHandler(String name, long idleTimeoutMillis) { + super(0, 0, idleTimeoutMillis, TimeUnit.MILLISECONDS); + this.name = requireNonNull(name, "name"); + } + + @Override + protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception { + if (!evt.isFirst()) { + return; + } + + if (!hasRequestsInProgress(ctx)) { + logger.debug("{} Closing an idle {} connection", ctx.channel(), name); + ctx.close(); + } + } + + protected abstract boolean hasRequestsInProgress(ChannelHandlerContext ctx); +} diff --git a/src/main/java/com/linecorp/armeria/internal/ReadSuppressingHandler.java b/src/main/java/com/linecorp/armeria/internal/ReadSuppressingHandler.java new file mode 100644 index 000000000000..ea91d1746113 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/ReadSuppressingHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal; + +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; + +@Sharable +public final class ReadSuppressingHandler extends ChannelOutboundHandlerAdapter { + + public static final ReadSuppressingHandler INSTANCE = new ReadSuppressingHandler(); + + private ReadSuppressingHandler() {} + + @Override + public void read(ChannelHandlerContext ctx) throws Exception { + if (ctx.channel().config().isAutoRead()) { + super.read(ctx); + } + } +} diff --git a/src/main/java/com/linecorp/armeria/internal/Writability.java b/src/main/java/com/linecorp/armeria/internal/Writability.java new file mode 100644 index 000000000000..3526e091c9b2 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/Writability.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal; + +import java.util.concurrent.atomic.AtomicInteger; + +import com.google.common.base.MoreObjects; + +public final class Writability extends AtomicInteger { + + private static final long serialVersionUID = 420503276551000218L; + + private final int highWatermark; + private final int lowWatermark; + private volatile boolean writable = true; + + public Writability() { + this(128 * 1024, 64*1024); + } + + public Writability(int highWatermark, int lowWatermark) { + this.highWatermark = highWatermark; + this.lowWatermark = lowWatermark; + } + + public boolean inc(int amount) { + final int newValue = addAndGet(amount); + if (newValue > highWatermark) { + return writable = false; + } else { + return writable; + } + } + + public boolean dec(int amount) { + final int newValue = addAndGet(-amount); + if (newValue < lowWatermark) { + return writable = true; + } else { + return writable; + } + } + + public boolean isWritable() { + return writable; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("level", get()) + .add("watermarks", highWatermark + "/" + lowWatermark) + .toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/AbstractHttpToHttp2ConnectionHandler.java b/src/main/java/com/linecorp/armeria/internal/http/AbstractHttp2ConnectionHandler.java similarity index 87% rename from src/main/java/com/linecorp/armeria/common/http/AbstractHttpToHttp2ConnectionHandler.java rename to src/main/java/com/linecorp/armeria/internal/http/AbstractHttp2ConnectionHandler.java index 097ebc35a47b..51d5c5351544 100644 --- a/src/main/java/com/linecorp/armeria/common/http/AbstractHttpToHttp2ConnectionHandler.java +++ b/src/main/java/com/linecorp/armeria/internal/http/AbstractHttp2ConnectionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -14,11 +14,11 @@ * under the License. */ -package com.linecorp.armeria.common.http; +package com.linecorp.armeria.internal.http; import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; -import com.linecorp.armeria.common.util.Exceptions; +import com.google.common.base.Throwables; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; @@ -30,9 +30,8 @@ import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream.State; import io.netty.handler.codec.http2.Http2StreamVisitor; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; -public abstract class AbstractHttpToHttp2ConnectionHandler extends HttpToHttp2ConnectionHandler { +public abstract class AbstractHttp2ConnectionHandler extends Http2ConnectionHandler { /** * XXX(trustin): Don't know why, but {@link Http2ConnectionHandler} does not close the last stream @@ -48,11 +47,9 @@ public abstract class AbstractHttpToHttp2ConnectionHandler extends HttpToHttp2Co private boolean closing; private boolean handlingConnectionError; - protected AbstractHttpToHttp2ConnectionHandler( - Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, - Http2Settings initialSettings, boolean validateHeaders) { - - super(decoder, encoder, initialSettings, validateHeaders); + protected AbstractHttp2ConnectionHandler( + Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) { + super(decoder, encoder, initialSettings); } public boolean isClosing() { @@ -100,7 +97,7 @@ private static String goAwayDebugData(Http2Exception http2Ex, Throwable cause) { buf.append(", message: "); buf.append(message != null ? message : "n/a"); buf.append(", cause: "); - buf.append(cause != null ? Exceptions.traceText(cause) : "n/a"); + buf.append(cause != null ? Throwables.getStackTraceAsString(cause) : "n/a"); return buf.toString(); } diff --git a/src/main/java/com/linecorp/armeria/internal/http/ArmeriaHttpUtil.java b/src/main/java/com/linecorp/armeria/internal/http/ArmeriaHttpUtil.java new file mode 100644 index 000000000000..eac4f4d40965 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/http/ArmeriaHttpUtil.java @@ -0,0 +1,469 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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. + */ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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 com.linecorp.armeria.internal.http; + +import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm; +import static io.netty.handler.codec.http.HttpUtil.isOriginForm; +import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.Http2Exception.streamError; +import static io.netty.util.AsciiString.EMPTY_STRING; +import static io.netty.util.ByteProcessor.FIND_SEMI_COLON; +import static io.netty.util.internal.StringUtil.isNullOrEmpty; +import static io.netty.util.internal.StringUtil.length; + +import java.net.URI; +import java.util.Iterator; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.linecorp.armeria.common.http.DefaultHttpHeaders; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaderNames; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpMethod; +import com.linecorp.armeria.common.http.HttpObject; +import com.linecorp.armeria.common.http.HttpStatus; +import com.linecorp.armeria.common.http.HttpStatusClass; +import com.linecorp.armeria.common.reactivestreams.Writer; +import com.linecorp.armeria.common.util.Exceptions; +import com.linecorp.armeria.internal.Writability; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.DefaultHeaders; +import io.netty.handler.codec.UnsupportedValueConverter; +import io.netty.handler.codec.ValueConverter; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; +import io.netty.util.AsciiString; +import io.netty.util.HashingStrategy; + +public final class ArmeriaHttpUtil { + + private static final Logger logger = LoggerFactory.getLogger(ArmeriaHttpUtil.class); + + /** + * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2. + */ + private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST = new CharSequenceMap(); + + static { + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE; + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION; + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING); + } + + private static int suspensionCounter; + + public static int suspensionCounter() { + return suspensionCounter; + } + + public static void writeData(ChannelHandlerContext ctx, Writer writer, ByteBuf data, + Writability writability) { + + final byte[] dataArray = new byte[data.readableBytes()]; + data.getBytes(data.readerIndex(), dataArray); + writer.write(HttpData.of(dataArray)); + + if (!writability.isWritable()) { + final Channel ch = ctx.channel(); + suspensionCounter++; + ch.config().setAutoRead(false); + writer.awaitDemand() + .whenComplete((res, cause) -> { + if (cause != null) { + Exceptions.logIfUnexpected(logger, ch, cause); + return; + } + + try { + if (writability.isWritable()) { + ch.config().setAutoRead(true); + } + } catch (Throwable t) { + Exceptions.logIfUnexpected(logger, ch, t); + } + }); + } + } + + public static boolean isContentAlwaysEmpty(HttpStatus status) { + if (status.codeClass() == HttpStatusClass.INFORMATIONAL) { + return true; + } + + switch (status.code()) { + case 204: case 205: case 304: + return true; + } + + return false; + } + + public static HttpHeaders toArmeria(Http2Headers headers) { + final HttpHeaders converted = new DefaultHttpHeaders(false, headers.size()); + for (Entry e : headers) { + converted.add(AsciiString.of(e.getKey()), e.getValue().toString()); + } + return converted; + } + + /** + * Converts the given HTTP/1.x headers into HTTP/2 headers. + * The following headers are only used if they can not be found in from the {@code HOST} header or the + * {@code Request-Line} as defined by rfc7230 + *
    + *
  • {@link ExtensionHeaderNames#SCHEME}
  • + *
+ * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}. + */ + public static HttpHeaders toArmeria(HttpMessage in) { + io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers(); + final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size()); + if (in instanceof HttpRequest) { + HttpRequest request = (HttpRequest) in; + URI requestTargetUri = URI.create(request.uri()); + out.path(toHttp2Path(requestTargetUri)); + out.method(HttpMethod.valueOf(request.method().name())); + setHttp2Scheme(inHeaders, requestTargetUri, out); + + if (!isOriginForm(requestTargetUri) && !isAsteriskForm(requestTargetUri)) { + // Attempt to take from HOST header before taking from the request-line + String host = inHeaders.getAsString(HttpHeaderNames.HOST); + setHttp2Authority(host == null || host.isEmpty() ? requestTargetUri.getAuthority() : host, out); + } + } else if (in instanceof HttpResponse) { + HttpResponse response = (HttpResponse) in; + out.status(response.status().code()); + } + + // Add the HTTP headers which have not been consumed above + toArmeria(inHeaders, out); + return out; + } + + public static HttpHeaders toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders) { + if (inHeaders.isEmpty()) { + return HttpHeaders.EMPTY_HEADERS; + } + + final HttpHeaders out = new DefaultHttpHeaders(true, inHeaders.size()); + toArmeria(inHeaders, out); + return out; + } + + public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeaders out) { + final Iterator> i = inHeaders.iteratorCharSequence(); + while (i.hasNext()) { + final Entry entry = i.next(); + final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); + if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE + if (aName.contentEqualsIgnoreCase(HttpHeaderNames.TE) && + !AsciiString.contentEqualsIgnoreCase(entry.getValue(), HttpHeaderValues.TRAILERS)) { + throw new IllegalArgumentException("Invalid value for " + HttpHeaderNames.TE + ": " + + entry.getValue()); + } + if (aName.contentEqualsIgnoreCase(HttpHeaderNames.COOKIE)) { + convertCookie(entry, out); + } else { + out.add(aName, entry.getValue().toString()); + } + } + } + } + + private static void convertCookie(Entry entry, HttpHeaders out) { + AsciiString value = AsciiString.of(entry.getValue()); + // split up cookies to allow for better compression + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + try { + int index = value.forEachByte(FIND_SEMI_COLON); + if (index != -1) { + int start = 0; + do { + out.add(HttpHeaderNames.COOKIE, value.toString(start, index)); + // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1) + start = index + 2; + } while (start < value.length() && + (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1); + if (start >= value.length()) { + throw new IllegalArgumentException("cookie value is of unexpected format: " + value); + } + out.add(HttpHeaderNames.COOKIE, value.toString(start, value.length())); + } else { + out.add(HttpHeaderNames.COOKIE, value.toString()); + } + } catch (Exception e) { + // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught + // because of the ByteProcessor interface. + throw new IllegalStateException(e); + } + } + + /** + * Generate a HTTP/2 {code :path} from a URI in accordance with + * rfc7230, 5.3. + */ + private static String toHttp2Path(URI uri) { + StringBuilder pathBuilder = new StringBuilder(length(uri.getRawPath()) + + length(uri.getRawQuery()) + length(uri.getRawFragment()) + 2); + if (!isNullOrEmpty(uri.getRawPath())) { + pathBuilder.append(uri.getRawPath()); + } + if (!isNullOrEmpty(uri.getRawQuery())) { + pathBuilder.append('?'); + pathBuilder.append(uri.getRawQuery()); + } + if (!isNullOrEmpty(uri.getRawFragment())) { + pathBuilder.append('#'); + pathBuilder.append(uri.getRawFragment()); + } + + return pathBuilder.toString(); + } + + private static void setHttp2Authority(String authority, HttpHeaders out) { + // The authority MUST NOT include the deprecated "userinfo" subcomponent + if (authority != null) { + int endOfUserInfo = authority.indexOf('@'); + if (endOfUserInfo < 0) { + out.authority(authority); + } else if (endOfUserInfo + 1 < authority.length()) { + out.authority(authority.substring(endOfUserInfo + 1)); + } else { + throw new IllegalArgumentException("authority: " + authority); + } + } + } + + private static void setHttp2Scheme(io.netty.handler.codec.http.HttpHeaders in, URI uri, HttpHeaders out) { + String value = uri.getScheme(); + if (value != null) { + out.scheme(value); + return; + } + + // Consume the Scheme extension header if present + CharSequence cValue = in.get(ExtensionHeaderNames.SCHEME.text()); + if (cValue != null) { + out.scheme(cValue.toString()); + } else { + out.scheme("unknown"); + } + } + + public static Http2Headers toNettyHttp2(HttpHeaders inputHeaders) { + final Http2Headers outputHeaders = new DefaultHttp2Headers(false, inputHeaders.size()); + outputHeaders.set(inputHeaders); + outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); + outputHeaders.remove(HttpHeaderNames.TRAILER); + return outputHeaders; + } + + public static io.netty.handler.codec.http.HttpHeaders toNettyHttp1(HttpHeaders inputHeaders) { + final io.netty.handler.codec.http.DefaultHttpHeaders outputHeaders = + new io.netty.handler.codec.http.DefaultHttpHeaders(); + for (Entry e : inputHeaders) { + final AsciiString name = e.getKey(); + if (name.isEmpty() || name.byteAt(0) == ':') { + continue; + } + outputHeaders.add(name, e.getValue()); + } + return outputHeaders; + } + + /** + * Translate and add HTTP/2 headers to HTTP/1.x headers. + * + * @param streamId The stream associated with {@code sourceHeaders}. + * @param inputHeaders The HTTP/2 headers to convert. + * @param outputHeaders The object which will contain the resulting HTTP/1.x headers.. + * @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion. + * @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers. + * {@code false} otherwise. + * @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message. + * {@code false} for response message. + * + * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x. + */ + public static void toNettyHttp2( + int streamId, HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders, + HttpVersion httpVersion, boolean isTrailer, boolean isRequest) throws Http2Exception { + + final Http2ToHttpHeaderTranslator translator = + new Http2ToHttpHeaderTranslator(streamId, outputHeaders, isRequest); + try { + for (Entry entry : inputHeaders) { + translator.translate(entry); + } + } catch (Http2Exception ex) { + throw ex; + } catch (Throwable t) { + throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error"); + } + + outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING); + outputHeaders.remove(HttpHeaderNames.TRAILER); + if (!isTrailer) { + outputHeaders.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId); + io.netty.handler.codec.http.HttpUtil.setKeepAlive(outputHeaders, httpVersion, true); + } + } + + /** + * Utility which translates HTTP/2 headers to HTTP/1 headers. + */ + private static final class Http2ToHttpHeaderTranslator { + /** + * Translations from HTTP/2 header name to the HTTP/1.x equivalent. + */ + private static final CharSequenceMap REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap(); + private static final CharSequenceMap RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap(); + static { + RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(), + HttpHeaderNames.HOST); + RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(), + ExtensionHeaderNames.SCHEME.text()); + REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS); + RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(), + ExtensionHeaderNames.PATH.text()); + } + + private final int streamId; + private final io.netty.handler.codec.http.HttpHeaders output; + private final CharSequenceMap translations; + + /** + * Create a new instance + * + * @param output The HTTP/1.x headers object to store the results of the translation + * @param request if {@code true}, translates headers using the request translation map. Otherwise uses the + * response translation map. + */ + Http2ToHttpHeaderTranslator(int streamId, io.netty.handler.codec.http.HttpHeaders output, boolean request) { + this.streamId = streamId; + this.output = output; + translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; + } + + public void translate(Entry entry) throws Http2Exception { + final AsciiString name = entry.getKey(); + final String value = entry.getValue(); + AsciiString translatedName = translations.get(name); + if (translatedName != null) { + output.add(translatedName, value); + } else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // All headers that start with ':' are only valid in HTTP/2 context + if (name.isEmpty() || name.charAt(0) == ':') { + throw streamError(streamId, PROTOCOL_ERROR, + "Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name); + } + if (HttpHeaderNames.COOKIE.equals(name)) { + // combine the cookie values into 1 header entry. + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + String existingCookie = output.get(HttpHeaderNames.COOKIE); + output.set(HttpHeaderNames.COOKIE, + existingCookie != null ? existingCookie + "; " + value : value); + } else { + output.add(name, value); + } + } + } + } + + private static final class CharSequenceMap + extends DefaultHeaders { + + private static final HashingStrategy CASE_SENSITIVE_HASHER = + new HashingStrategy() { + @Override + public int hashCode(AsciiString o) { + return AsciiString.hashCode(o); + } + + @Override + public boolean equals(AsciiString a, AsciiString b) { + return AsciiString.contentEquals(a, b); + } + }; + + private static final HashingStrategy CASE_INSENSITIVE_HASHER = + new HashingStrategy() { + @Override + public int hashCode(AsciiString o) { + return AsciiString.hashCode(o); + } + + @Override + public boolean equals(AsciiString a, AsciiString b) { + return AsciiString.contentEqualsIgnoreCase(a, b); + } + }; + + CharSequenceMap() { + this(true); + } + + CharSequenceMap(boolean caseSensitive) { + this(caseSensitive, UnsupportedValueConverter.instance()); + } + + CharSequenceMap(boolean caseSensitive, ValueConverter valueConverter) { + super(caseSensitive ? CASE_SENSITIVE_HASHER + : CASE_INSENSITIVE_HASHER, valueConverter); + } + } + + private ArmeriaHttpUtil() {} +} diff --git a/src/main/java/com/linecorp/armeria/common/http/Http1ClientCodec.java b/src/main/java/com/linecorp/armeria/internal/http/Http1ClientCodec.java similarity index 98% rename from src/main/java/com/linecorp/armeria/common/http/Http1ClientCodec.java rename to src/main/java/com/linecorp/armeria/internal/http/Http1ClientCodec.java index e39d57e40f20..de610193ef4a 100644 --- a/src/main/java/com/linecorp/armeria/common/http/Http1ClientCodec.java +++ b/src/main/java/com/linecorp/armeria/internal/http/Http1ClientCodec.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package com.linecorp.armeria.common.http; +package com.linecorp.armeria.internal.http; import java.util.ArrayDeque; import java.util.List; @@ -191,8 +191,8 @@ private void decrement(Object msg) { @Override protected boolean isContentAlwaysEmpty(HttpMessage msg) { final int statusCode = ((HttpResponse) msg).status().code(); - if (statusCode == 100) { - // 100-continue response should be excluded from paired comparison. + if (statusCode >= 100 && statusCode < 200) { + // An informational response should be excluded from paired comparison. return true; } diff --git a/src/main/java/com/linecorp/armeria/internal/http/Http1ObjectEncoder.java b/src/main/java/com/linecorp/armeria/internal/http/Http1ObjectEncoder.java new file mode 100644 index 000000000000..6f9f280b046d --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/http/Http1ObjectEncoder.java @@ -0,0 +1,359 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.http; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayDeque; +import java.util.Map.Entry; +import java.util.Queue; + +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.http.HttpMethod; +import com.linecorp.armeria.common.http.HttpStatus; +import com.linecorp.armeria.common.http.HttpStatusClass; +import com.linecorp.armeria.common.reactivestreams.ClosedPublisherException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMessage; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +public final class Http1ObjectEncoder extends HttpObjectEncoder { + + private final boolean server; + + /** + * The ID of the request which is at its turn to send a response. + */ + private int currentId = 1; + + /** + * The minimum ID of the request whose stream has been closed/reset. + */ + private int minClosedId = Integer.MAX_VALUE; + + /** + * The maximum known ID with pending writes. + */ + private int maxIdWithPendingWrites = Integer.MIN_VALUE; + + /** + * The map which maps a request ID to its related pending response. + */ + private final IntObjectMap pendingWrites = new IntObjectHashMap<>(); + + public Http1ObjectEncoder(boolean server) { + this.server = server; + } + + @Override + protected ChannelFuture doWriteHeaders( + ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, boolean endStream) { + + if (id >= minClosedId) { + return ctx.newFailedFuture(ClosedSessionException.get()); + } + + final HttpObject converted; + try { + converted = server ? convertServerHeaders(streamId, headers, endStream) + : convertClientHeaders(streamId, headers); + + return write(ctx, id, converted, endStream); + } catch (Throwable t) { + return ctx.newFailedFuture(t); + } + } + + private HttpObject convertServerHeaders( + int streamId, HttpHeaders headers, boolean endStream) throws Http2Exception { + + // Only leading headers can have :status. + final HttpStatus status = headers.status(); + if (status == null) { + return convertTrailingHeaders(streamId, headers); + } + + // Convert leading headers. + final HttpResponse res; + final boolean informational = status.codeClass() == HttpStatusClass.INFORMATIONAL; + + if (endStream || informational) { + + res = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, status.toNettyStatus(), Unpooled.EMPTY_BUFFER, false); + + headers.remove(HttpHeaderNames.TRANSFER_ENCODING); + if (informational) { + // 1xx responses does not have the 'content-length' header. + headers.remove(HttpHeaderNames.CONTENT_LENGTH); + } else if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { + // NB: Set the 'content-length' only when not set rather than always setting to 0. + // It's because a HEAD response can have empty content with non-zero 'content-length'. + // However, this also opens the possible of sending a non-zero 'content-length' header + // even when it has to be zero. + headers.setInt(HttpHeaderNames.CONTENT_LENGTH, 0); + } + + convert(streamId, headers, res.headers(), false); + } else { + res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status.toNettyStatus(), false); + // Perform conversion. + convert(streamId, headers, res.headers(), false); + setTransferEncoding(res); + } + + return res; + } + + private HttpObject convertClientHeaders(int streamId, HttpHeaders headers) throws Http2Exception { + + // Only leading headers can have :method. + final HttpMethod method = headers.method(); + if (method == null) { + return convertTrailingHeaders(streamId, headers); + } + + // Convert leading headers. + final HttpRequest req = new DefaultHttpRequest( + HttpVersion.HTTP_1_1, method.toNettyMethod(), headers.path(), false); + + convert(streamId, headers, req.headers(), false); + if (HttpUtil.getContentLength(req, -1L) >= 0) { + // Avoid the case where both 'content-length' and 'transfer-encoding' are set. + req.headers().remove(HttpHeaderNames.TRANSFER_ENCODING); + } else { + req.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + } + + return req; + } + + private void convert( + int streamId, HttpHeaders inHeaders, + io.netty.handler.codec.http.HttpHeaders outHeaders, boolean trailer) throws Http2Exception { + + ArmeriaHttpUtil.toNettyHttp2( + streamId, inHeaders, outHeaders, HttpVersion.HTTP_1_1, trailer, false); + + outHeaders.remove(ExtensionHeaderNames.STREAM_ID.text()); + if (server) { + outHeaders.remove(ExtensionHeaderNames.SCHEME.text()); + } else { + outHeaders.remove(ExtensionHeaderNames.PATH.text()); + } + } + + private LastHttpContent convertTrailingHeaders(int streamId, HttpHeaders headers) throws Http2Exception { + final LastHttpContent lastContent; + if (headers.isEmpty()) { + lastContent = LastHttpContent.EMPTY_LAST_CONTENT; + } else { + lastContent = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, false); + convert(streamId, headers, lastContent.trailingHeaders(), true); + } + return lastContent; + } + + private static void setTransferEncoding(HttpMessage out) { + final io.netty.handler.codec.http.HttpHeaders outHeaders = out.headers(); + final long contentLength = HttpUtil.getContentLength(out, -1L); + if (contentLength < 0) { + // Use chunked encoding. + outHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + outHeaders.remove(HttpHeaderNames.CONTENT_LENGTH); + } + } + + @Override + protected ChannelFuture doWriteData( + ChannelHandlerContext ctx, int id, int streamId, HttpData data, boolean endStream) { + + if (id >= minClosedId) { + return ctx.newFailedFuture(ClosedSessionException.get()); + } + + try { + final ByteBuf buf = toByteBuf(ctx, data); + final HttpContent content; + if (endStream) { + content = new DefaultLastHttpContent(buf); + } else { + content = new DefaultHttpContent(buf); + } + return write(ctx, id, content, endStream); + } catch (Throwable t) { + return ctx.newFailedFuture(t); + } + } + + private ChannelFuture write(ChannelHandlerContext ctx, int id, HttpObject obj, boolean endStream) { + if (id < currentId) { + // Attempted to write something on a finished request/response; discard. + return ctx.newFailedFuture(ClosedPublisherException.get()); + } + + final PendingWrites pendingWrites = this.pendingWrites.get(id); + if (id == currentId) { + if (pendingWrites != null) { + this.pendingWrites.remove(id); + flushPendingWrites(ctx, pendingWrites); + } + + final ChannelFuture future = ctx.write(obj); + if (endStream) { + currentId++; + + // The next PendingWrites might be complete already. + for (;;) { + final PendingWrites nextPendingWrites = this.pendingWrites.get(currentId); + if (nextPendingWrites == null) { + break; + } + + flushPendingWrites(ctx, nextPendingWrites); + if (!nextPendingWrites.isEndOfStream()) { + break; + } + + this.pendingWrites.remove(currentId); + currentId++; + } + } + + ctx.flush(); + return future; + } else { + final ChannelPromise promise = ctx.newPromise(); + final Entry entry = new SimpleImmutableEntry<>(obj, promise); + + if (pendingWrites == null) { + final PendingWrites newPendingWrites = new PendingWrites(); + maxIdWithPendingWrites = Math.max(maxIdWithPendingWrites, id); + this.pendingWrites.put(id, newPendingWrites); + newPendingWrites.add(entry); + } else { + pendingWrites.add(entry); + } + + if (endStream) { + pendingWrites.setEndOfStream(); + } + + return promise; + } + } + + private static void flushPendingWrites(ChannelHandlerContext ctx, PendingWrites pendingWrites) { + for (;;) { + final Entry e = pendingWrites.poll(); + if (e == null) { + break; + } + + ctx.write(e.getKey(), e.getValue()); + } + } + + @Override + protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + final int minClosedId = this.minClosedId = Math.min(this.minClosedId, id); + final int maxIdWithPendingWrites = this.maxIdWithPendingWrites; + + for (int i = minClosedId; i <= maxIdWithPendingWrites; i++) { + final PendingWrites pendingWrites = this.pendingWrites.remove(i); + for (;;) { + final Entry e = pendingWrites.poll(); + if (e == null) { + break; + } + e.getValue().tryFailure(ClosedSessionException.get()); + } + } + + final ChannelFuture f = ctx.write(Unpooled.EMPTY_BUFFER); + if (currentId >= minClosedId) { + f.addListener(ChannelFutureListener.CLOSE); + } + + return f; + } + + @Override + protected void doClose() { + if (pendingWrites.isEmpty()) { + return; + } + + final ClosedSessionException cause = ClosedSessionException.get(); + for (Queue> queue : pendingWrites.values()) { + for (;;) { + final Entry e = queue.poll(); + if (e == null) { + break; + } + + e.getValue().tryFailure(cause); + } + } + + pendingWrites.clear(); + } + + private static final class PendingWrites extends ArrayDeque> { + + private static final long serialVersionUID = 4241891747461017445L; + + private boolean endOfStream; + + PendingWrites() { + super(4); + } + + boolean isEndOfStream() { + return endOfStream; + } + + void setEndOfStream() { + endOfStream = true; + } + } +} diff --git a/src/main/java/com/linecorp/armeria/common/http/Http2GoAwayListener.java b/src/main/java/com/linecorp/armeria/internal/http/Http2GoAwayListener.java similarity index 94% rename from src/main/java/com/linecorp/armeria/common/http/Http2GoAwayListener.java rename to src/main/java/com/linecorp/armeria/internal/http/Http2GoAwayListener.java index e4aeb8b761ba..2f1e59b81691 100644 --- a/src/main/java/com/linecorp/armeria/common/http/Http2GoAwayListener.java +++ b/src/main/java/com/linecorp/armeria/internal/http/Http2GoAwayListener.java @@ -14,7 +14,7 @@ * under the License. */ -package com.linecorp.armeria.common.http; +package com.linecorp.armeria.internal.http; import java.nio.charset.StandardCharsets; @@ -22,7 +22,6 @@ import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; import io.netty.channel.Channel; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionAdapter; @@ -64,10 +63,9 @@ public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData private void onGoAway(String sentOrReceived, int lastStreamId, long errorCode, ByteBuf debugData) { if (errorCode != Http2Error.NO_ERROR.code()) { if (logger.isWarnEnabled()) { - logger.warn("{} {} a GOAWAY frame: lastStreamId={}, errorCode={}, debugData=\"{}\" (Hex: {})", + logger.warn("{} {} a GOAWAY frame: lastStreamId={}, errorCode={}, debugData=\"{}\"", ch, sentOrReceived, lastStreamId, errorStr(errorCode), - debugData.toString(StandardCharsets.UTF_8), - ByteBufUtil.hexDump(debugData)); + debugData.toString(StandardCharsets.UTF_8)); } } else { if (logger.isDebugEnabled()) { diff --git a/src/main/java/com/linecorp/armeria/internal/http/Http2ObjectEncoder.java b/src/main/java/com/linecorp/armeria/internal/http/Http2ObjectEncoder.java new file mode 100644 index 000000000000..f0920b06bae7 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/http/Http2ObjectEncoder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.http; + +import static java.util.Objects.requireNonNull; + +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaders; +import com.linecorp.armeria.common.reactivestreams.ClosedPublisherException; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Stream; + +public final class Http2ObjectEncoder extends HttpObjectEncoder { + + private final Http2ConnectionEncoder encoder; + + public Http2ObjectEncoder(Http2ConnectionEncoder encoder) { + this.encoder = requireNonNull(encoder, "encoder"); + } + + @Override + protected ChannelFuture doWriteHeaders( + ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, boolean endStream) { + + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + + return encoder.writeHeaders( + ctx, streamId, ArmeriaHttpUtil.toNettyHttp2(headers), 0, endStream, ctx.newPromise()); + } + + @Override + protected ChannelFuture doWriteData( + ChannelHandlerContext ctx, int id, int streamId, HttpData data, boolean endStream) { + + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + + return encoder.writeData(ctx, streamId, toByteBuf(ctx, data), 0, endStream, ctx.newPromise()); + } + + @Override + protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + + return encoder.writeRstStream(ctx, streamId, error.code(), ctx.newPromise()); + } + + private ChannelFuture validateStream(ChannelHandlerContext ctx, int streamId) { + final Http2Stream stream = encoder.connection().stream(streamId); + if (stream != null) { + switch (stream.state()) { + case RESERVED_LOCAL: + case OPEN: + case HALF_CLOSED_REMOTE: + break; + default: + // The response has been sent already. + return ctx.newFailedFuture(ClosedPublisherException.get()); + } + } else if (encoder.connection().streamMayHaveExisted(streamId)) { + // Stream has been removed because it has been closed completely. + return ctx.newFailedFuture(ClosedPublisherException.get()); + } + + return null; + } + + @Override + protected void doClose() {} +} diff --git a/src/main/java/com/linecorp/armeria/internal/http/HttpObjectEncoder.java b/src/main/java/com/linecorp/armeria/internal/http/HttpObjectEncoder.java new file mode 100644 index 000000000000..ed5568994681 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/http/HttpObjectEncoder.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.http; + +import com.linecorp.armeria.common.ClosedSessionException; +import com.linecorp.armeria.common.http.HttpData; +import com.linecorp.armeria.common.http.HttpHeaders; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.Http2Error; + +public abstract class HttpObjectEncoder { + + private volatile boolean closed; + + public final ChannelFuture writeHeaders( + ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, boolean endStream) { + + assert ctx.channel().eventLoop().inEventLoop(); + + if (closed) { + return newFailedFuture(ctx); + } + + return doWriteHeaders(ctx, id, streamId, headers, endStream); + } + + protected abstract ChannelFuture doWriteHeaders( + ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, boolean endStream); + + public final ChannelFuture writeData( + ChannelHandlerContext ctx, int id, int streamId, HttpData data, boolean endStream) { + + assert ctx.channel().eventLoop().inEventLoop(); + + if (closed) { + return newFailedFuture(ctx); + } + + return doWriteData(ctx, id, streamId, data, endStream); + } + + protected abstract ChannelFuture doWriteData( + ChannelHandlerContext ctx, int id, int streamId, HttpData data, boolean endStream); + + public final ChannelFuture writeReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + + if (closed) { + return newFailedFuture(ctx); + } + + return doWriteReset(ctx, id, streamId, error); + } + + protected abstract ChannelFuture doWriteReset( + ChannelHandlerContext ctx, int id, int streamId, Http2Error error); + + public void close() { + if (closed) { + return; + } + + closed = true; + doClose(); + } + + protected abstract void doClose(); + + private static ChannelFuture newFailedFuture(ChannelHandlerContext ctx) { + return ctx.newFailedFuture(ClosedSessionException.get()); + } + + protected static ByteBuf toByteBuf(ChannelHandlerContext ctx, HttpData data) { + final ByteBuf buf = ctx.alloc().directBuffer(data.length(), data.length()); + buf.writeBytes(data.array(), data.offset(), data.length()); + return buf; + } +} diff --git a/src/main/java/com/linecorp/armeria/internal/thrift/ThriftFunction.java b/src/main/java/com/linecorp/armeria/internal/thrift/ThriftFunction.java new file mode 100644 index 000000000000..4424e4d9eaf6 --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/thrift/ThriftFunction.java @@ -0,0 +1,331 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.thrift; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.thrift.AsyncProcessFunction; +import org.apache.thrift.ProcessFunction; +import org.apache.thrift.TApplicationException; +import org.apache.thrift.TBase; +import org.apache.thrift.TException; +import org.apache.thrift.TFieldIdEnum; +import org.apache.thrift.meta_data.FieldMetaData; +import org.apache.thrift.protocol.TMessageType; + +import com.google.common.collect.ImmutableMap; + +public final class ThriftFunction { + + private enum Type { + SYNC, + ASYNC + } + + private final Object func; + private final Type type; + private final Class serviceType; + private final String name; + private final TBase, TFieldIdEnum> result; + private final TFieldIdEnum[] argFields; + private final TFieldIdEnum successField; + private final Map, TFieldIdEnum> exceptionFields; + private final Class[] declaredExceptions; + + public ThriftFunction(Class serviceType, ProcessFunction func) throws Exception { + this(serviceType, func.getMethodName(), func, Type.SYNC, + getArgFields(func), getResult(func), getDeclaredExceptions(func)); + } + + public ThriftFunction(Class serviceType, AsyncProcessFunction func) throws Exception { + this(serviceType, func.getMethodName(), func, Type.ASYNC, + getArgFields(func), getResult(func), getDeclaredExceptions(func)); + } + + private ThriftFunction( + Class serviceType, String name, Object func, Type type, + TFieldIdEnum[] argFields, + TBase, TFieldIdEnum> result, + Class[] declaredExceptions) throws Exception { + + this.func = func; + this.type = type; + this.serviceType = serviceType; + this.name = name; + this.argFields = argFields; + this.result = result; + this.declaredExceptions = declaredExceptions; + + // Determine the success and exception fields of the function. + final ImmutableMap.Builder, TFieldIdEnum> exceptionFieldsBuilder = + ImmutableMap.builder(); + TFieldIdEnum successField = null; + + if (result != null) { + @SuppressWarnings("rawtypes") + final Class resultType = result.getClass(); + @SuppressWarnings("unchecked") + final Map metaDataMap = + (Map) FieldMetaData.getStructMetaDataMap(resultType); + + for (Entry e: metaDataMap.entrySet()) { + final TFieldIdEnum key = e.getKey(); + final String fieldName = key.getFieldName(); + if ("success".equals(fieldName)) { + successField = key; + continue; + } + + Class fieldType = resultType.getField(fieldName).getType(); + if (Throwable.class.isAssignableFrom(fieldType)) { + @SuppressWarnings("unchecked") + Class exceptionFieldType = (Class) fieldType; + exceptionFieldsBuilder.put(exceptionFieldType, key); + } + } + } + + this.successField = successField; + exceptionFields = exceptionFieldsBuilder.build(); + } + + public boolean isOneway() { + return result == null; + } + + public boolean isAsync() { + return type == Type.ASYNC; + } + + public byte messageType() { + return isOneway() ? TMessageType.ONEWAY : TMessageType.CALL; + } + + @SuppressWarnings("unchecked") + public ProcessFunction, TFieldIdEnum>> syncFunc() { + return (ProcessFunction, TFieldIdEnum>>) func; + } + + @SuppressWarnings("unchecked") + public AsyncProcessFunction, TFieldIdEnum>, Object> asyncFunc() { + return (AsyncProcessFunction, TFieldIdEnum>, Object>) func; + } + + public Class serviceType() { + return serviceType; + } + + public String name() { + return name; + } + + public TFieldIdEnum successField() { + return successField; + } + + public Collection exceptionFields() { + return exceptionFields.values(); + } + + public Class[] declaredExceptions() { + return declaredExceptions; + } + + public TBase, TFieldIdEnum> newArgs() { + if (isAsync()) { + return asyncFunc().getEmptyArgsInstance(); + } else { + return syncFunc().getEmptyArgsInstance(); + } + } + + public TBase, TFieldIdEnum> newArgs(List args) { + final TBase, TFieldIdEnum> newArgs = newArgs(); + if (args != null) { + final int size = args.size(); + for (int i = 0; i < size; i++) { + newArgs.setFieldValue(argFields[i], args.get(i)); + } + } + return newArgs; + } + + public TBase, TFieldIdEnum> newArgs(Object... args) { + final TBase, TFieldIdEnum> newArgs = newArgs(); + if (args != null) { + for (int i = 0; i < args.length; i++) { + newArgs.setFieldValue(argFields[i], args[i]); + } + } + return newArgs; + } + + public TBase, TFieldIdEnum> newResult() { + return result.deepCopy(); + } + + public void setSuccess(TBase result, Object value) { + if (successField != null) { + result.setFieldValue(successField, value); + } + } + + public Object getResult(TBase, TFieldIdEnum> result) throws TException { + for (TFieldIdEnum fieldIdEnum : exceptionFields()) { + if (result.isSet(fieldIdEnum)) { + throw (TException) result.getFieldValue(fieldIdEnum); + } + } + + final TFieldIdEnum successField = successField(); + if (successField == null) { //void method + return null; + } else if (result.isSet(successField)) { + return result.getFieldValue(successField); + } else { + throw new TApplicationException( + TApplicationException.MISSING_RESULT, + result.getClass().getName() + '.' + successField.getFieldName()); + } + } + + public boolean setException(TBase result, Throwable cause) { + Class causeType = cause.getClass(); + for (Entry, TFieldIdEnum> e: exceptionFields.entrySet()) { + if (e.getKey().isAssignableFrom(causeType)) { + result.setFieldValue(e.getValue(), cause); + return true; + } + } + return false; + } + + private static TBase, TFieldIdEnum> getArgs(ProcessFunction func) { + return getArgs0(Type.SYNC, func.getClass(), func.getMethodName()); + } + + private static TBase, TFieldIdEnum> getArgs(AsyncProcessFunction asyncFunc) { + return getArgs0(Type.ASYNC, asyncFunc.getClass(), asyncFunc.getMethodName()); + } + + private static TBase, TFieldIdEnum> getArgs0( + Type type, Class funcClass, String methodName) { + + final String argsTypeName = typeName(type, funcClass, methodName, methodName + "_args"); + try { + @SuppressWarnings("unchecked") + Class, TFieldIdEnum>> argsType = + (Class, TFieldIdEnum>>) Class.forName( + argsTypeName, false, funcClass.getClassLoader()); + return argsType.newInstance(); + } catch (Exception e) { + throw new IllegalStateException("cannot determine the args class of method: " + methodName, e); + } + } + + private static TFieldIdEnum[] getArgFields(ProcessFunction func) { + return getArgFields0(Type.SYNC, func.getClass(), func.getMethodName()); + } + + private static TFieldIdEnum[] getArgFields(AsyncProcessFunction asyncFunc) { + return getArgFields0(Type.ASYNC, asyncFunc.getClass(), asyncFunc.getMethodName()); + } + + private static TFieldIdEnum[] getArgFields0(Type type, Class funcClass, String methodName) { + final String fieldIdEnumTypeName = typeName(type, funcClass, methodName, methodName + "_args$_Fields"); + try { + Class fieldIdEnumType = Class.forName(fieldIdEnumTypeName, false, funcClass.getClassLoader()); + return (TFieldIdEnum[]) requireNonNull(fieldIdEnumType.getEnumConstants(), + "field enum may not be empty"); + } catch (Exception e) { + throw new IllegalStateException("cannot determine the arg fields of method: " + methodName, e); + } + } + + private static TBase, TFieldIdEnum> getResult(ProcessFunction func) { + return getResult0(Type.SYNC, func.getClass(), func.getMethodName()); + } + + private static TBase, TFieldIdEnum> getResult(AsyncProcessFunction asyncFunc) { + return getResult0(Type.ASYNC, asyncFunc.getClass(), asyncFunc.getMethodName()); + } + + private static TBase, TFieldIdEnum> getResult0( + Type type, Class funcClass, String methodName) { + + final String resultTypeName = typeName(type, funcClass, methodName, methodName + "_result"); + try { + @SuppressWarnings("unchecked") + Class, TFieldIdEnum>> resultType = + (Class, TFieldIdEnum>>) Class.forName( + resultTypeName, false, funcClass.getClassLoader()); + return resultType.newInstance(); + } catch (ClassNotFoundException ignored) { + // Oneway function does not have a result type. + return null; + } catch (Exception e) { + throw new IllegalStateException("cannot determine the result type of method: " + methodName, e); + } + } + + private static Class[] getDeclaredExceptions(ProcessFunction func) { + return getDeclaredExceptions0(Type.SYNC, func.getClass(), func.getMethodName()); + } + + private static Class[] getDeclaredExceptions(AsyncProcessFunction asyncFunc) { + return getDeclaredExceptions0(Type.ASYNC, asyncFunc.getClass(), asyncFunc.getMethodName()); + } + + private static Class[] getDeclaredExceptions0( + Type type, Class funcClass, String methodName) { + + final String ifaceTypeName = typeName(type, funcClass, methodName, "Iface"); + try { + Class ifaceType = Class.forName(ifaceTypeName, false, funcClass.getClassLoader()); + for (Method m : ifaceType.getDeclaredMethods()) { + if (!m.getName().equals(methodName)) { + continue; + } + + return m.getExceptionTypes(); + } + + throw new IllegalStateException("failed to find a method: " + methodName); + } catch (Exception e) { + throw new IllegalStateException( + "cannot determine the declared exceptions of method: " + methodName, e); + } + } + + private static String typeName(Type type, Class funcClass, String methodName, String toAppend) { + final String funcClassName = funcClass.getName(); + final int serviceClassEndPos = funcClassName.lastIndexOf( + (type == Type.SYNC? "$Processor$" : "$AsyncProcessor$") + methodName); + + if (serviceClassEndPos <= 0) { + throw new IllegalStateException("cannot determine the service class of method: " + methodName); + } + + return funcClassName.substring(0, serviceClassEndPos) + '$' + toAppend; + } +} diff --git a/src/main/java/com/linecorp/armeria/internal/thrift/ThriftServiceMetadata.java b/src/main/java/com/linecorp/armeria/internal/thrift/ThriftServiceMetadata.java new file mode 100644 index 000000000000..5df4ae26af1b --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/thrift/ThriftServiceMetadata.java @@ -0,0 +1,209 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.thrift; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.thrift.AsyncProcessFunction; +import org.apache.thrift.ProcessFunction; +import org.apache.thrift.TBaseAsyncProcessor; +import org.apache.thrift.TBaseProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ThriftServiceMetadata { + + private static final Logger logger = LoggerFactory.getLogger(ThriftServiceMetadata.class); + + private final Set> interfaces; + + /** + * A map whose key is a method name and whose value is {@link AsyncProcessFunction} or {@link ProcessFunction}. + */ + private final Map functions = new HashMap<>(); + + public ThriftServiceMetadata(Object implementation) { + requireNonNull(implementation, "implementation"); + interfaces = init(implementation); + } + + public ThriftServiceMetadata(Class iface) { + requireNonNull(iface, "iface"); + interfaces = init(null, Collections.singleton(iface)); + } + + private Set> init(Object implementation) { + return init(implementation, getAllInterfaces(implementation.getClass())); + } + + private Set> init(Object implementation, Iterable> candidateInterfaces) { + + final Class serviceClass = implementation != null ? implementation.getClass() : null; + + // Build the map of method names and their corresponding process functions. + final Set methodNames = new HashSet<>(); + final Set> interfaces = new HashSet<>(); + + for (Class iface : candidateInterfaces) { + final ClassLoader serviceClassLoader = + serviceClass != null ? serviceClass.getClassLoader() : iface.getClassLoader(); + + final Map> asyncProcessMap; + asyncProcessMap = getThriftAsyncProcessMap(implementation, iface, serviceClassLoader); + if (asyncProcessMap != null) { + asyncProcessMap.forEach( + (name, func) -> registerFunction(methodNames, iface, name, func)); + interfaces.add(iface); + } + + final Map> processMap; + processMap = getThriftProcessMap(implementation, iface, serviceClassLoader); + if (processMap != null) { + processMap.forEach( + (name, func) -> registerFunction(methodNames, iface, name, func)); + interfaces.add(iface); + } + } + + if (functions.isEmpty()) { + throw new IllegalArgumentException('\'' + serviceClass.getName() + + "' is not a Thrift service implementation."); + } + + return Collections.unmodifiableSet(interfaces); + } + + private static Set> getAllInterfaces(Class cls) { + final Set> interfacesFound = new HashSet<>(); + getAllInterfaces(cls, interfacesFound); + return interfacesFound; + } + + private static void getAllInterfaces(Class cls, Set> interfacesFound) { + while (cls != null) { + final Class[] interfaces = cls.getInterfaces(); + for (final Class i : interfaces) { + if (interfacesFound.add(i)) { + getAllInterfaces(i, interfacesFound); + } + } + cls = cls.getSuperclass(); + } + } + + private static Map> getThriftProcessMap( + Object service, Class iface, ClassLoader loader) { + + final String name = iface.getName(); + if (!name.endsWith("$Iface")) { + return null; + } + + final String processorName = name.substring(0, name.length() - 5) + "Processor"; + try { + final Class processorClass = Class.forName(processorName, false, loader); + if (!TBaseProcessor.class.isAssignableFrom(processorClass)) { + return null; + } + + final Constructor processorConstructor = processorClass.getConstructor(iface); + + @SuppressWarnings("rawtypes") + final TBaseProcessor processor = (TBaseProcessor) processorConstructor.newInstance(service); + + @SuppressWarnings("unchecked") + Map> processMap = + (Map>) processor.getProcessMapView(); + + return processMap; + } catch (Exception e) { + logger.debug("Failed to retrieve the process map from: {}", iface, e); + return null; + } + } + + private static Map> getThriftAsyncProcessMap( + Object service, Class iface, ClassLoader loader) { + + final String name = iface.getName(); + if (!name.endsWith("$AsyncIface")) { + return null; + } + + final String processorName = name.substring(0, name.length() - 10) + "AsyncProcessor"; + try { + Class processorClass = Class.forName(processorName, false, loader); + if (!TBaseAsyncProcessor.class.isAssignableFrom(processorClass)) { + return null; + } + + final Constructor processorConstructor = processorClass.getConstructor(iface); + + @SuppressWarnings("rawtypes") + final TBaseAsyncProcessor processor = (TBaseAsyncProcessor) processorConstructor.newInstance(service); + + @SuppressWarnings("unchecked") + Map> processMap = + (Map>) processor.getProcessMapView(); + + return processMap; + } catch (Exception e) { + logger.debug("Failed to retrieve the asynchronous process map from:: {}", iface, e); + return null; + } + } + + @SuppressWarnings("rawtypes") + private void registerFunction(Set methodNames, Class iface, String name, Object func) { + checkDuplicateMethodName(methodNames, name); + methodNames.add(name); + + try { + final ThriftFunction f; + if (func instanceof ProcessFunction) { + f = new ThriftFunction(iface, (ProcessFunction) func); + } else { + f = new ThriftFunction(iface, (AsyncProcessFunction) func); + } + functions.put(name, f); + } catch (Exception e) { + throw new IllegalArgumentException("failed to retrieve function metadata: " + + iface.getName() + '.' + name + "()", e); + } + } + + private static void checkDuplicateMethodName(Set methodNames, String name) { + if (methodNames.contains(name)) { + throw new IllegalArgumentException("duplicate Thrift method name: " + name); + } + } + + public Set> interfaces() { + return interfaces; + } + + public ThriftFunction function(String method) { + return functions.get(method); + } +} diff --git a/src/main/java/com/linecorp/armeria/internal/tracing/BraveHttpHeaderNames.java b/src/main/java/com/linecorp/armeria/internal/tracing/BraveHttpHeaderNames.java new file mode 100644 index 000000000000..c58f865d303b --- /dev/null +++ b/src/main/java/com/linecorp/armeria/internal/tracing/BraveHttpHeaderNames.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.internal.tracing; + +import java.util.Locale; + +import com.github.kristofa.brave.http.BraveHttpHeaders; + +import io.netty.util.AsciiString; + +public final class BraveHttpHeaderNames { + + public static final AsciiString SAMPLED = + AsciiString.of(BraveHttpHeaders.Sampled.getName().toLowerCase(Locale.ENGLISH)); + + public static final AsciiString TRACE_ID = + AsciiString.of(BraveHttpHeaders.TraceId.getName().toLowerCase(Locale.ENGLISH)); + + public static final AsciiString SPAN_ID = + AsciiString.of(BraveHttpHeaders.SpanId.getName().toLowerCase(Locale.ENGLISH)); + + public static final AsciiString PARENT_SPAN_ID = + AsciiString.of(BraveHttpHeaders.ParentSpanId.getName().toLowerCase(Locale.ENGLISH)); + + private BraveHttpHeaderNames() {} +} diff --git a/src/main/java/com/linecorp/armeria/server/AbstractPathMapping.java b/src/main/java/com/linecorp/armeria/server/AbstractPathMapping.java index 766d7b154e2a..1e612b956201 100644 --- a/src/main/java/com/linecorp/armeria/server/AbstractPathMapping.java +++ b/src/main/java/com/linecorp/armeria/server/AbstractPathMapping.java @@ -18,8 +18,6 @@ import static java.util.Objects.requireNonNull; -import com.linecorp.armeria.common.ServiceInvocationContext; - /** * A skeletal {@link PathMapping} implementation. Implement {@link #doApply(String)}. */ @@ -31,7 +29,7 @@ public abstract class AbstractPathMapping implements PathMapping { * and then performs sanity checks on the returned {@code mappedPath}. * * @param path an absolute path as defined in RFC3986 - * @return the translated path which is used as the value of {@link ServiceInvocationContext#mappedPath()}. + * @return the translated path which is used as the value of {@link ServiceRequestContext#mappedPath()}. * {@code null} if the specified {@code path} does not match this mapping. */ @Override @@ -61,7 +59,7 @@ public final String apply(String path) { * Invoked by {@link #apply(String)} to perform the actual path matching and translation. * * @param path an absolute path as defined in RFC3986 - * @return the translated path which is used as the value of {@link ServiceInvocationContext#mappedPath()}. + * @return the translated path which is used as the value of {@link ServiceRequestContext#mappedPath()}. * {@code null} if the specified {@code path} does not match this mapping. */ protected abstract String doApply(String path); diff --git a/src/main/java/com/linecorp/armeria/server/BiFunctionService.java b/src/main/java/com/linecorp/armeria/server/BiFunctionService.java new file mode 100644 index 000000000000..25d0476eef8c --- /dev/null +++ b/src/main/java/com/linecorp/armeria/server/BiFunctionService.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; + +import static java.util.Objects.requireNonNull; + +import java.util.function.BiFunction; + +/** + * A {@link Service} who uses a {@link BiFunction} as its implementation. + * + * @see Service#of(BiFunction) + */ +final class BiFunctionService implements Service { + + private final BiFunction function; + + /** + * Creates a new instance with the specified function. + */ + @SuppressWarnings("unchecked") + BiFunctionService(BiFunction function) { + requireNonNull(function, "function"); + this.function = (BiFunction) function; + } + + @Override + public O serve(ServiceRequestContext ctx, I req) throws Exception { + return function.apply(ctx, req); + } + + @Override + public String toString() { + return "BiFunctionService(" + function + ')'; + } +} diff --git a/src/main/java/com/linecorp/armeria/server/DecoratingService.java b/src/main/java/com/linecorp/armeria/server/DecoratingService.java index a117d414e498..c25d3f17aac4 100644 --- a/src/main/java/com/linecorp/armeria/server/DecoratingService.java +++ b/src/main/java/com/linecorp/armeria/server/DecoratingService.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 LINE Corporation + * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -23,99 +23,56 @@ /** * A {@link Service} that decorates another {@link Service}. Do not use this class unless you want to define - * a new dedicated {@link Service} type by extending this class; prefer: - *
    - *
  • {@link Service#decorate(Function)}
  • - *
  • {@link Service#decorateHandler(Function)}
  • - *
  • {@link Service#decorateCodec(Function)}
  • - *
  • {@link Service#newDecorator(Function, Function)}
  • - *
- * - * @see DecoratingServiceCodec - * @see DecoratingServiceInvocationHandler + * a new dedicated {@link Service} type by extending this class; prefer {@link Service#decorate(Function)}. */ -public class DecoratingService implements Service { +public class DecoratingService implements Service { - private final Service service; - private final ServiceCodec codec; - private final ServiceInvocationHandler handler; + private final Service delegate; /** - * Creates a new instance that decorates the specified {@link Service} and its {@link ServiceCodec} and - * {@link ServiceInvocationHandler} using the specified {@code codecDecorator} and {@code handlerDecorator}. + * Creates a new instance that decorates the specified {@link Service}. */ - protected - DecoratingService(Service service, Function codecDecorator, Function handlerDecorator) { - - this.service = requireNonNull(service, "service"); - codec = decorateCodec(service, codecDecorator); - handler = decorateHandler(service, handlerDecorator); + protected DecoratingService(Service delegate) { + this.delegate = requireNonNull(delegate, "delegate"); } - private static - ServiceCodec decorateCodec(Service service, Function codecDecorator) { - - requireNonNull(codecDecorator, "codecDecorator"); - - @SuppressWarnings("unchecked") - final T codec = (T) service.codec(); - final U decoratedCodec = codecDecorator.apply(codec); - if (decoratedCodec == null) { - throw new NullPointerException("codecDecorator.apply() returned null: " + codecDecorator); - } - - return decoratedCodec; - } - - private static - ServiceInvocationHandler decorateHandler(Service service, Function handlerDecorator) { - - requireNonNull(handlerDecorator, "handlerDecorator"); - - @SuppressWarnings("unchecked") - final T handler = (T) service.handler(); - final U decoratedHandler = handlerDecorator.apply(handler); - if (decoratedHandler == null) { - throw new NullPointerException("handlerDecorator.apply() returned null: " + handlerDecorator); - } - - return decoratedHandler; + /** + * Returns the {@link Service} being decorated. + */ + @SuppressWarnings("unchecked") + protected final > T delegate() { + return (T) delegate; } /** * Returns the {@link Service} being decorated. */ @SuppressWarnings("unchecked") - protected final T delegate() { - return (T) service; + protected final , T_I, T_O> T delegate(Class requestType, + Class responseType) { + return (T) delegate; } @Override public void serviceAdded(ServiceConfig cfg) throws Exception { - ServiceCallbackInvoker.invokeServiceAdded(cfg, delegate()); - } - - @Override - public ServiceCodec codec() { - return codec; + ServiceCallbackInvoker.invokeServiceAdded(cfg, delegate); } @Override - public ServiceInvocationHandler handler() { - return handler; + public O serve(ServiceRequestContext ctx, I req) throws Exception { + return delegate().serve(ctx, req); } @Override - public final Optional as(Class serviceType) { + public final > Optional as(Class serviceType) { final Optional result = Service.super.as(serviceType); - return result.isPresent() ? result : delegate().as(serviceType); + return result.isPresent() ? result : delegate.as(serviceType); } @Override public String toString() { final String simpleName = getClass().getSimpleName(); final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; - return name + '(' + delegate() + ')'; + return name + '(' + delegate + ')'; } } diff --git a/src/main/java/com/linecorp/armeria/server/DecoratingServiceCodec.java b/src/main/java/com/linecorp/armeria/server/DecoratingServiceCodec.java deleted file mode 100644 index faffb75f3323..000000000000 --- a/src/main/java/com/linecorp/armeria/server/DecoratingServiceCodec.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; - -import static java.util.Objects.requireNonNull; - -import java.util.Optional; -import java.util.function.Function; - -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.util.concurrent.Promise; - -/** - * A {@link ServiceCodec} that decorates another {@link ServiceCodec}. Do not use this class unless you want - * to define a new dedicated {@link ServiceCodec} type by extending this class; prefer: - *
    - *
  • {@link Service#decorate(Function)}
  • - *
  • {@link Service#decorateCodec(Function)}
  • - *
  • {@link Service#newDecorator(Function, Function)}
  • - *
- */ -public abstract class DecoratingServiceCodec implements ServiceCodec { - - private final ServiceCodec delegate; - - /** - * Creates a new instance that decorates the specified {@link ServiceCodec}. - */ - protected DecoratingServiceCodec(ServiceCodec delegate) { - this.delegate = requireNonNull(delegate, "delegate"); - } - - /** - * Returns the {@link ServiceCodec} being decorated. - */ - @SuppressWarnings("unchecked") - protected final T delegate() { - return (T) delegate; - } - - @Override - public void codecAdded(ServiceConfig cfg) throws Exception { - ServiceCallbackInvoker.invokeCodecAdded(cfg, delegate()); - } - - @Override - public DecodeResult decodeRequest(ServiceConfig cfg, Channel ch, SessionProtocol sessionProtocol, - String hostname, String path, String mappedPath, ByteBuf in, - Object originalRequest, Promise promise) throws Exception { - return delegate().decodeRequest(cfg, ch, sessionProtocol, hostname, - path, mappedPath, in, originalRequest, promise); - } - - @Override - public boolean failureResponseFailsSession(ServiceInvocationContext ctx) { - return delegate().failureResponseFailsSession(ctx); - } - - @Override - public ByteBuf encodeResponse(ServiceInvocationContext ctx, Object response) throws Exception { - return delegate().encodeResponse(ctx, response); - } - - @Override - public ByteBuf encodeFailureResponse(ServiceInvocationContext ctx, Throwable cause) throws Exception { - return delegate().encodeFailureResponse(ctx, cause); - } - - @Override - public final Optional as(Class codecType) { - final Optional result = ServiceCodec.super.as(codecType); - return result.isPresent() ? result : delegate().as(codecType); - } - - @Override - public String toString() { - final String simpleName = getClass().getSimpleName(); - final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; - return name + '(' + delegate() + ')'; - } -} diff --git a/src/main/java/com/linecorp/armeria/server/DecoratingServiceInvocationHandler.java b/src/main/java/com/linecorp/armeria/server/DecoratingServiceInvocationHandler.java deleted file mode 100644 index c294149ef3ec..000000000000 --- a/src/main/java/com/linecorp/armeria/server/DecoratingServiceInvocationHandler.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; - -import static java.util.Objects.requireNonNull; - -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.function.Function; - -import com.linecorp.armeria.common.ServiceInvocationContext; - -import io.netty.util.concurrent.Promise; - -/** - * A {@link ServiceInvocationHandler} that decorates another {@link ServiceInvocationHandler}. Do not use this - * class unless you want to define a new dedicated {@link ServiceInvocationHandler} type by extending this - * class; prefer: - *
    - *
  • {@link Service#decorate(Function)}
  • - *
  • {@link Service#decorateHandler(Function)}
  • - *
  • {@link Service#newDecorator(Function, Function)}
  • - *
- */ -public abstract class DecoratingServiceInvocationHandler implements ServiceInvocationHandler { - - private final ServiceInvocationHandler handler; - - /** - * Creates a new instance that decorates the specified {@link ServiceInvocationHandler}. - */ - protected DecoratingServiceInvocationHandler(ServiceInvocationHandler handler) { - this.handler = requireNonNull(handler, "handler"); - } - - /** - * Returns the {@link ServiceInvocationHandler} being decorated. - */ - @SuppressWarnings("unchecked") - protected final T delegate() { - return (T) handler; - } - - @Override - public void handlerAdded(ServiceConfig cfg) throws Exception { - ServiceCallbackInvoker.invokeHandlerAdded(cfg, delegate()); - } - - @Override - public void invoke(ServiceInvocationContext ctx, - Executor blockingTaskExecutor, Promise promise) throws Exception { - - delegate().invoke(ctx, blockingTaskExecutor, promise); - } - - @Override - public final Optional as(Class handlerType) { - final Optional result = ServiceInvocationHandler.super.as(handlerType); - return result.isPresent() ? result : delegate().as(handlerType); - } - - @Override - public String toString() { - final String simpleName = getClass().getSimpleName(); - final String name = simpleName.isEmpty() ? getClass().getName() : simpleName; - return name + '(' + delegate() + ')'; - } -} diff --git a/src/main/java/com/linecorp/armeria/server/DefaultServiceRequestContext.java b/src/main/java/com/linecorp/armeria/server/DefaultServiceRequestContext.java new file mode 100644 index 000000000000..e050441dad4f --- /dev/null +++ b/src/main/java/com/linecorp/armeria/server/DefaultServiceRequestContext.java @@ -0,0 +1,218 @@ +/* + * Copyright 2016 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; + +import static java.util.Objects.requireNonNull; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +import org.slf4j.Logger; + +import com.linecorp.armeria.common.AbstractRequestContext; +import com.linecorp.armeria.common.SessionProtocol; +import com.linecorp.armeria.common.logging.DefaultRequestLog; +import com.linecorp.armeria.common.logging.DefaultResponseLog; +import com.linecorp.armeria.common.logging.RequestLog; +import com.linecorp.armeria.common.logging.RequestLogBuilder; +import com.linecorp.armeria.common.logging.ResponseLog; +import com.linecorp.armeria.common.logging.ResponseLogBuilder; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; + +/** + * Default {@link ServiceRequestContext} implementation. + */ +public final class DefaultServiceRequestContext extends AbstractRequestContext implements ServiceRequestContext { + + private final Channel ch; + private final ServiceConfig cfg; + private final String mappedPath; + private final Logger logger; + private Logger wrappedLogger; + + private final DefaultRequestLog requestLog; + private final DefaultResponseLog responseLog; + + private long requestTimeoutMillis; + private long maxRequestLength; + + private String strVal; + + /** + * Creates a new instance. + * + * @param ch the {@link Channel} that handles the invocation + * @param sessionProtocol the {@link SessionProtocol} of the invocation + * @param logger the {@link Logger} for the invocation + * @param request the request associated with this context + */ + public DefaultServiceRequestContext( + ServiceConfig cfg, Channel ch, SessionProtocol sessionProtocol, + String method, String path, String mappedPath, Logger logger, Object request) { + + super(sessionProtocol, method, path, request); + + this.ch = ch; + this.cfg = cfg; + this.mappedPath = mappedPath; + this.logger = logger; + + requestLog = new DefaultRequestLog(); + requestLog.start(ch, sessionProtocol, cfg.virtualHost().defaultHostname(), method, path); + responseLog = new DefaultResponseLog(requestLog); + } + + @Override + public Server server() { + return cfg.server(); + } + + @Override + public VirtualHost virtualHost() { + return cfg.virtualHost(); + } + + @Override + public > T service() { + return cfg.service(); + } + + @Override + public ExecutorService blockingTaskExecutor() { + return server().config().blockingTaskExecutor(); + } + + @Override + @SuppressWarnings("unchecked") + public A remoteAddress() { + return (A) ch.remoteAddress(); + } + + @Override + @SuppressWarnings("unchecked") + public A localAddress() { + return (A) ch.localAddress(); + } + + @Override + public EventLoop eventLoop() { + return ch.eventLoop(); + } + + @Override + public String mappedPath() { + return mappedPath; + } + + @Override + public Logger logger() { + Logger wrappedLogger = this.wrappedLogger; + if (wrappedLogger == null) { + this.wrappedLogger = wrappedLogger = new RequestContextAwareLogger(this, logger); + } + return wrappedLogger; + } + + @Override + public long requestTimeoutMillis() { + return requestTimeoutMillis; + } + + @Override + public void setRequestTimeoutMillis(long requestTimeoutMillis) { + if (requestTimeoutMillis < 0) { + throw new IllegalArgumentException( + "requestTimeoutMillis: " + requestTimeoutMillis + " (expected: >= 0)"); + } + this.requestTimeoutMillis = requestTimeoutMillis; + } + + @Override + public void setRequestTimeout(Duration requestTimeout) { + setRequestTimeoutMillis(requireNonNull(requestTimeout, "requestTimeout").toMillis()); + } + + @Override + public long maxRequestLength() { + return maxRequestLength; + } + + @Override + public void setMaxRequestLength(long maxRequestLength) { + if (maxRequestLength < 0) { + throw new IllegalArgumentException( + "maxRequestLength: " + maxRequestLength + " (expected: >= 0)"); + } + this.maxRequestLength = maxRequestLength; + } + + @Override + public RequestLogBuilder requestLogBuilder() { + return requestLog; + } + + @Override + public ResponseLogBuilder responseLogBuilder() { + return responseLog; + } + + @Override + public CompletableFuture awaitRequestLog() { + return requestLog; + } + + @Override + public CompletableFuture awaitResponseLog() { + return responseLog; + } + + @Override + public String toString() { + String strVal = this.strVal; + if (strVal != null) { + return strVal; + } + + final StringBuilder buf = new StringBuilder(96); + + // Prepend the current channel information if available. + final Channel ch = requestLog.channel(); + final boolean hasChannel = ch != null; + if (hasChannel) { + buf.append(ch); + } + + buf.append(ch) + .append('[') + .append(sessionProtocol().uriText()) + .append("://") + .append(virtualHost().defaultHostname()) + .append(':') + .append(((InetSocketAddress) remoteAddress()).getPort()) + .append(path()) + .append('#') + .append(method()) + .append(']'); + + return this.strVal = buf.toString(); + } +} diff --git a/src/main/java/com/linecorp/armeria/server/GracefulShutdownHandler.java b/src/main/java/com/linecorp/armeria/server/GracefulShutdownHandler.java index 791c4f75aafd..9fbc6d305a0d 100644 --- a/src/main/java/com/linecorp/armeria/server/GracefulShutdownHandler.java +++ b/src/main/java/com/linecorp/armeria/server/GracefulShutdownHandler.java @@ -34,7 +34,7 @@ * after a fixed quiet period passes after the last one. */ @Sharable -class GracefulShutdownHandler extends ChannelDuplexHandler { +public final class GracefulShutdownHandler extends ChannelDuplexHandler { private final long quietPeriodNanos; private final Ticker ticker; @@ -120,7 +120,7 @@ private boolean completedBlockingTasks() { return threadPool.getQueue().isEmpty() && threadPool.getActiveCount() == 0; } - void reset() { + public void reset() { shutdownStartTimeNanos = null; } } diff --git a/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java b/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java deleted file mode 100644 index 0fdb16adaac9..000000000000 --- a/src/main/java/com/linecorp/armeria/server/HttpServerHandler.java +++ /dev/null @@ -1,659 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; - -import static java.util.Objects.requireNonNull; - -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.linecorp.armeria.common.ServiceInvocationContext; -import com.linecorp.armeria.common.SessionProtocol; -import com.linecorp.armeria.common.http.AbstractHttpToHttp2ConnectionHandler; -import com.linecorp.armeria.common.util.Exceptions; -import com.linecorp.armeria.server.ServiceCodec.DecodeResult; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeEvent; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.Http2Stream.State; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.AsciiString; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.collection.IntObjectHashMap; -import io.netty.util.collection.IntObjectMap; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.Promise; - -final class HttpServerHandler extends ChannelInboundHandlerAdapter { - - private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class); - - private static final AsciiString STREAM_ID = HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(); - private static final AsciiString ERROR_CONTENT_TYPE = new AsciiString("text/plain; charset=UTF-8"); - private static final AsciiString ALLOWED_METHODS = - new AsciiString("DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE"); - - private static final ChannelFutureListener CLOSE = future -> { - final Throwable cause = future.cause(); - final Channel ch = future.channel(); - if (cause != null) { - Exceptions.logIfUnexpected(logger, ch, protocol(ch), cause); - } - safeClose(ch); - }; - - private static final ChannelFutureListener CLOSE_ON_FAILURE = future -> { - final Throwable cause = future.cause(); - if (cause != null) { - final Channel ch = future.channel(); - Exceptions.logIfUnexpected(logger, ch, protocol(ch), cause); - safeClose(ch); - } - }; - - private static SessionProtocol protocol(Channel ch) { - final HttpServerHandler handler = ch.pipeline().get(HttpServerHandler.class); - final SessionProtocol protocol; - if (handler != null) { - protocol = handler.protocol; - } else { - protocol = null; - } - return protocol; - } - - static void safeClose(Channel ch) { - if (!ch.isActive()) { - return; - } - - // Do not call Channel.close() if AbstractHttpToHttp2ConnectionHandler.close() has been invoked - // already. Otherwise, it can trigger a bad cycle: - // - // 1. Channel.close() triggers AbstractHttpToHttp2ConnectionHandler.close(). - // 2. AbstractHttpToHttp2ConnectionHandler.close() triggers Http2Stream.close(). - // 3. Http2Stream.close() fails the promise of its pending writes. - // 4. The failed promise notifies this listener (CLOSE_ON_FAILURE). - // 5. This listener calls Channel.close(). - // 6. Repeat from the step 1. - // - - final AbstractHttpToHttp2ConnectionHandler h2handler = - ch.pipeline().get(AbstractHttpToHttp2ConnectionHandler.class); - - if (h2handler == null || !h2handler.isClosing()) { - ch.close(); - } - } - - @SuppressWarnings("ThrowableInstanceNeverThrown") - private static final Exception SERVICE_NOT_FOUND = new ServiceNotFoundException(); - - private final ServerConfig config; - private SessionProtocol protocol; - private Http2Connection http2conn; - - private boolean isReading; - - // When head-of-line blocking is enabled (i.e. HTTP/1), we assign a monotonically increasing integer - // ('request sequence') to each received request, and assign the integer of the same value - // when creating its response. - - /** - * The request sequence of the most recently received request. - * Incremented when a new request is received. - */ - private int reqSeq; - /** - * The request sequence of the request which was received least recently and has no corresponding response. - */ - private int resSeq; - - /** - * The map which maps a sequence number to its related pending response. - */ - private final IntObjectMap pendingResponses = new IntObjectHashMap<>(); - - private boolean handledLastRequest; - - HttpServerHandler(ServerConfig config, SessionProtocol protocol) { - assert protocol == SessionProtocol.H1 || - protocol == SessionProtocol.H1C || - protocol == SessionProtocol.H2; - - this.config = requireNonNull(config, "config"); - this.protocol = requireNonNull(protocol, "protocol"); - } - - private boolean isHttp2() { - return http2conn != null; - } - - private void setHttp2(ChannelHandlerContext ctx) { - switch (protocol) { - case H1: - protocol = SessionProtocol.H2; - break; - case H1C: - protocol = SessionProtocol.H2C; - break; - } - - final Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); - http2conn = handler.connection(); - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - isReading = true; // Cleared in channelReadComplete() - - if (msg instanceof Http2Settings) { - handleHttp2Settings(ctx, (Http2Settings) msg); - } else { - handleRequest(ctx, (FullHttpRequest) msg); - } - } - - private void handleHttp2Settings(ChannelHandlerContext ctx, Http2Settings h2settings) { - logger.debug("{} HTTP/2 settings: {}", ctx.channel(), h2settings); - setHttp2(ctx); - } - - private void handleRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { - // Ignore the request received after the last request, - // because we are going to close the connection after sending the last response. - if (handledLastRequest) { - return; - } - - boolean invoked = false; - try { - // If we received the message with keep-alive disabled, - // we should not accept a request anymore. - if (!HttpUtil.isKeepAlive(req)) { - handledLastRequest = true; - } - - final int reqSeq = this.reqSeq++; - - if (!req.decoderResult().isSuccess()) { - respond(ctx, reqSeq, req, HttpResponseStatus.BAD_REQUEST, req.decoderResult().cause()); - return; - } - - if (req.method() == HttpMethod.CONNECT) { - respond(ctx, reqSeq, req, HttpResponseStatus.METHOD_NOT_ALLOWED); - return; - } - - final String path = stripQuery(req.uri()); - // Reject requests without a valid path, except for the special 'OPTIONS *' request. - if (path.isEmpty() || path.charAt(0) != '/') { - if (req.method() == HttpMethod.OPTIONS && "*".equals(path)) { - handleOptions(ctx, req, reqSeq); - } else { - respond(ctx, reqSeq, req, HttpResponseStatus.BAD_REQUEST); - } - return; - } - - final String hostname = hostname(req); - final VirtualHost host = config.findVirtualHost(hostname); - - // Find the service that matches the path. - final PathMapped mapped = host.findServiceConfig(path); - if (!mapped.isPresent()) { - // No services matched the path. - handleNonExistentMapping(ctx, reqSeq, req, host, path); - return; - } - - // Decode the request and create a new invocation context from it to perform an invocation. - final String mappedPath = mapped.mappedPath(); - final ServiceConfig serviceCfg = mapped.value(); - final Service service = serviceCfg.service(); - final ServiceCodec codec = service.codec(); - final Promise promise = ctx.executor().newPromise(); - final DecodeResult decodeResult = codec.decodeRequest( - serviceCfg, ctx.channel(), protocol, - hostname, path, mappedPath, req.content(), req, promise); - - switch (decodeResult.type()) { - case SUCCESS: { - // A successful decode; perform the invocation. - final ServiceInvocationContext iCtx = decodeResult.invocationContext(); - invoke(iCtx, service.handler(), promise); - invoked = true; - - // Do the post-invocation tasks such as scheduling a timeout. - handleInvocationPromise(ctx, reqSeq, req, codec, iCtx, promise); - break; - } - case FAILURE: { - // Could not create an invocation context. - handleDecodeFailure(ctx, reqSeq, req, decodeResult, promise); - break; - } - case NOT_FOUND: - // Turned out that the request wasn't accepted by the matching service. - promise.tryFailure(SERVICE_NOT_FOUND); - respond(ctx, reqSeq, req, HttpResponseStatus.NOT_FOUND); - break; - } - } finally { - // If invocation has been started successfully, handleInvocationResult() will call - // ReferenceCountUtil.safeRelease() when the invocation is done. - if (!invoked) { - ReferenceCountUtil.safeRelease(req); - } - } - } - - private void handleOptions(ChannelHandlerContext ctx, FullHttpRequest req, int reqSeq) { - final FullHttpResponse res = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.EMPTY_BUFFER); - res.headers().set(HttpHeaderNames.ALLOW, ALLOWED_METHODS); - respond(ctx, reqSeq, req, res); - } - - private void handleNonExistentMapping(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - VirtualHost host, String path) { - - if (path.charAt(path.length() - 1) != '/') { - // Handle the case where /path doesn't exist but /path/ exists. - final String pathWithSlash = path + '/'; - if (host.findServiceConfig(pathWithSlash).isPresent()) { - final String location; - if (path.length() == req.uri().length()) { - location = pathWithSlash; - } else { - location = pathWithSlash + req.uri().substring(path.length()); - } - redirect(ctx, reqSeq, req, location); - return; - } - } - - respond(ctx, reqSeq, req, HttpResponseStatus.NOT_FOUND); - } - - private void invoke(ServiceInvocationContext iCtx, ServiceInvocationHandler handler, - Promise promise) { - - ServiceInvocationContext.setCurrent(iCtx); - try { - handler.invoke(iCtx, config.blockingTaskExecutor(), promise); - } catch (Throwable t) { - if (!promise.tryFailure(t)) { - logger.warn("{} invoke() failed with a finished promise: {}", iCtx, promise, t); - } - } finally { - ServiceInvocationContext.removeCurrent(); - } - } - - private void handleInvocationPromise(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - ServiceCodec codec, ServiceInvocationContext iCtx, - Promise promise) throws Exception { - if (promise.isDone()) { - // If the invocation has been finished immediately, - // there's no need to schedule a timeout nor to add a listener to the promise. - handleInvocationResult(ctx, reqSeq, req, iCtx, codec, promise, null); - } else { - final long timeoutMillis = config.requestTimeoutPolicy().timeout(iCtx); - final ScheduledFuture timeoutFuture; - if (timeoutMillis > 0) { - timeoutFuture = ctx.executor().schedule( - () -> promise.tryFailure(new RequestTimeoutException( - "request timed out after " + timeoutMillis + "ms: " + iCtx)), - timeoutMillis, TimeUnit.MILLISECONDS); - } else { - timeoutFuture = null; - } - - promise.addListener((Future future) -> { - try { - handleInvocationResult(ctx, reqSeq, req, iCtx, codec, future, timeoutFuture); - } catch (Exception e) { - respond(ctx, reqSeq, req, HttpResponseStatus.INTERNAL_SERVER_ERROR, e); - } - }); - } - } - - private void handleInvocationResult( - ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - ServiceInvocationContext iCtx, ServiceCodec codec, Future future, - ScheduledFuture timeoutFuture) throws Exception { - - // Release the original request which was retained before the invocation. - ReferenceCountUtil.safeRelease(req); - - // Cancel the associated timeout, if any. - if (timeoutFuture != null) { - timeoutFuture.cancel(true); - } - - // No need to build the HTTP response if the connection/stream has been closed. - if (isStreamClosed(ctx, req)) { - if (future.isSuccess()) { - ReferenceCountUtil.safeRelease(future.getNow()); - } - return; - } - - if (future.isSuccess()) { - final Object res = future.getNow(); - if (res instanceof FullHttpResponse) { - respond(ctx, reqSeq, req, (FullHttpResponse) res); - } else { - final ByteBuf encoded = codec.encodeResponse(iCtx, res); - respond(ctx, reqSeq, req, encoded); - } - } else { - final Throwable cause = future.cause(); - final ByteBuf encoded = codec.encodeFailureResponse(iCtx, cause); - if (codec.failureResponseFailsSession(iCtx)) { - respond(ctx, reqSeq, req, toHttpResponseStatus(cause), encoded); - } else { - respond(ctx, reqSeq, req, encoded); - } - } - } - - private boolean isStreamClosed(ChannelHandlerContext ctx, FullHttpRequest req) { - if (!ctx.channel().isActive()) { - // Connection has been closed. - return true; - } - - final Http2Connection http2conn = this.http2conn; - if (http2conn == null) { - // HTTP/1 connection - return false; - } - - final Integer streamId = req.headers().getInt(STREAM_ID); - if (streamId == null) { - throw new IllegalStateException("An HTTP/2 request does not have a stream ID: " + req); - } - - final Http2Stream stream = http2conn.stream(streamId); - if (stream == null) { - // The stream has been closed and removed. - return true; - } - - final State state = stream.state(); - return state == State.CLOSED || state == State.HALF_CLOSED_LOCAL; - } - - private void handleDecodeFailure(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - DecodeResult decodeResult, Promise promise) { - final Object errorResponse = decodeResult.errorResponse(); - if (errorResponse instanceof FullHttpResponse) { - FullHttpResponse httpResponse = (FullHttpResponse) errorResponse; - promise.tryFailure(new RequestDecodeException( - decodeResult.cause(), httpResponse.content().readableBytes())); - respond(ctx, reqSeq, req, (FullHttpResponse) errorResponse); - } else { - ReferenceCountUtil.safeRelease(errorResponse); - promise.tryFailure(new RequestDecodeException(decodeResult.cause(), 0)); - respond(ctx, reqSeq, req, HttpResponseStatus.BAD_REQUEST, decodeResult.cause()); - } - } - - private static String hostname(FullHttpRequest req) { - final String hostname = req.headers().getAsString(HttpHeaderNames.HOST); - if (hostname == null) { - return ""; - } - - final int hostnameColonIdx = hostname.lastIndexOf(':'); - if (hostnameColonIdx < 0) { - return hostname; - } - - return hostname.substring(0, hostnameColonIdx); - } - - private static String stripQuery(String uri) { - final int queryStart = uri.indexOf('?'); - return queryStart < 0 ? uri : uri.substring(0, queryStart); - } - - private static HttpResponseStatus toHttpResponseStatus(Throwable cause) { - if (cause instanceof RequestTimeoutException || cause instanceof ServiceUnavailableException) { - return HttpResponseStatus.SERVICE_UNAVAILABLE; - } - - return HttpResponseStatus.INTERNAL_SERVER_ERROR; - } - - private void respond(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, ByteBuf content) { - respond(ctx, reqSeq, req, HttpResponseStatus.OK, content); - } - - private void respond(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - HttpResponseStatus status, ByteBuf content) { - - if (content == null) { - content = Unpooled.EMPTY_BUFFER; - } - respond(ctx, reqSeq, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content)); - } - - private void respond(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - HttpResponseStatus status) { - - if (status.code() < 400) { - respond(ctx, reqSeq, req, status, Unpooled.EMPTY_BUFFER); - } else { - respond(ctx, reqSeq, req, status, (Throwable) null); - } - } - - private void respond(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, - HttpResponseStatus status, Throwable cause) { - - assert status.code() >= 400; - - final ByteBuf content; - if (req.method() == HttpMethod.HEAD) { - // A response to a HEAD request must have no content. - content = Unpooled.EMPTY_BUFFER; - if (cause != null) { - Exceptions.logIfUnexpected(logger, ctx.channel(), protocol, errorMessage(status), cause); - } - } else { - final String msg = errorMessage(status); - if (cause != null) { - Exceptions.logIfUnexpected(logger, ctx.channel(), protocol, msg, cause); - } - content = Unpooled.copiedBuffer(msg, StandardCharsets.UTF_8); - } - - final DefaultFullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content); - res.headers().set(HttpHeaderNames.CONTENT_TYPE, ERROR_CONTENT_TYPE); - - respond(ctx, reqSeq, req, res); - } - - private void redirect(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, String location) { - final DefaultFullHttpResponse res = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, HttpResponseStatus.TEMPORARY_REDIRECT, Unpooled.EMPTY_BUFFER); - res.headers().set(HttpHeaderNames.LOCATION, location); - - respond(ctx, reqSeq, req, res); - } - - private static String errorMessage(HttpResponseStatus status) { - String reasonPhrase = status.reasonPhrase(); - StringBuilder buf = new StringBuilder(reasonPhrase.length() + 4); - - buf.append(status.code()); - buf.append(' '); - buf.append(reasonPhrase); - return buf.toString(); - } - - private void respond(ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, FullHttpResponse res) { - if (isHttp2()) { - final String streamId = req.headers().getAsString(STREAM_ID); - res.headers().set(STREAM_ID, streamId); - } else if (!handlePendingResponses(ctx, reqSeq, req, res)) { - // HTTP/1 and the responses for the previous requests are not all ready. - return; - } - - if (!handledLastRequest) { - addKeepAliveHeaders(req, res); - ctx.write(res).addListener(CLOSE_ON_FAILURE); - } else { - // Note that it is perfectly fine not to set the 'content-length' header to the last response - // of an HTTP/1 connection. We just set it to work around overly strict HTTP clients that always - // require a 'content-length' header for non-chunked responses. - setContentLength(req, res); - ctx.write(res).addListener(CLOSE); - } - - if (!isReading) { - ctx.flush(); - } - } - - private boolean handlePendingResponses( - ChannelHandlerContext ctx, int reqSeq, FullHttpRequest req, FullHttpResponse res) { - - final IntObjectMap pendingResponses = this.pendingResponses; - while (reqSeq != resSeq) { - FullHttpResponse pendingRes = pendingResponses.remove(resSeq); - if (pendingRes == null) { - // Stuck by head-of-line blocking; try again later. - addKeepAliveHeaders(req, res); - FullHttpResponse oldPendingRes = pendingResponses.put(reqSeq, res); - if (oldPendingRes != null) { - // It is impossible to reach here as long as there are 2G+ pending responses. - logger.error("{} Orphaned pending response ({}): {}", reqSeq, oldPendingRes); - ReferenceCountUtil.safeRelease(oldPendingRes.release()); - } - return false; - } - - ctx.write(pendingRes); - resSeq++; - } - - // At this point, we have cleared all the pending responses. i.e. reqSeq = resSeq - // Increment resSeq in preparation of the next request. - resSeq++; - return true; - } - - /** - * Sets the keep alive header as per: - * - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection - */ - private static void addKeepAliveHeaders(FullHttpRequest req, FullHttpResponse res) { - res.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); - setContentLength(req, res); - } - - /** - * Sets the 'content-length' header to the response. - */ - private static void setContentLength(FullHttpRequest req, FullHttpResponse res) { - final int statusCode = res.status().code(); - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4 - // prohibits to send message body for below cases. - // and in those cases, content-length should not be sent. - if (statusCode < 200 || statusCode == 204 || statusCode == 304 || req.method() == HttpMethod.HEAD) { - return; - } - res.headers().set(HttpHeaderNames.CONTENT_LENGTH, res.content().readableBytes()); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - isReading = false; - ctx.flush(); - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof UpgradeEvent) { - assert !isReading; - - // Upgraded to HTTP/2. - setHttp2(ctx); - - // Continue handling the upgrade request after the upgrade is complete. - final FullHttpRequest req = ((UpgradeEvent) evt).upgradeRequest(); - - // Remove the headers related with the upgrade. - req.headers().remove(HttpHeaderNames.CONNECTION); - req.headers().remove(HttpHeaderNames.UPGRADE); - req.headers().remove(Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER); - - if (logger.isDebugEnabled()) { - logger.debug("{} Handling the pre-upgrade request ({}): {} {} {} ({}B)", - ctx.channel(), ((UpgradeEvent) evt).protocol(), - req.method(), req.uri(), req.protocolVersion(), req.content().readableBytes()); - } - - // Set the stream ID of the pre-upgrade request, which is always 1. - req.headers().set(STREAM_ID, "1"); - - channelRead(ctx, req); - channelReadComplete(ctx); - return; - } - - logger.warn("{} Unexpected user event: {}", ctx.channel(), evt); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - Exceptions.logIfUnexpected(logger, ctx.channel(), protocol, cause); - if (ctx.channel().isActive()) { - ctx.close(); - } - } -} diff --git a/src/main/java/com/linecorp/armeria/server/HttpServerIdleTimeoutHandler.java b/src/main/java/com/linecorp/armeria/server/HttpServerIdleTimeoutHandler.java deleted file mode 100644 index 7a6538e7900f..000000000000 --- a/src/main/java/com/linecorp/armeria/server/HttpServerIdleTimeoutHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2015 LINE Corporation - * - * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; - -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpMessage; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.handler.timeout.IdleStateHandler; - -class HttpServerIdleTimeoutHandler extends IdleStateHandler { - private static final Logger logger = LoggerFactory.getLogger(HttpServerIdleTimeoutHandler.class); - - /** - * The number of requests that are waiting for the responses - */ - protected int pendingResCount; - - HttpServerIdleTimeoutHandler(long idleTimeoutMillis) { - this(idleTimeoutMillis, TimeUnit.MILLISECONDS); - } - - HttpServerIdleTimeoutHandler(long idleTimeout, TimeUnit timeUnit) { - super(0, 0, idleTimeout, timeUnit); - } - - boolean isRequestStart(Object msg) { - return msg instanceof HttpRequest; - } - - boolean isResponseEnd(Object msg) { - if (msg instanceof FullHttpResponse) { - return !"1".equals(((HttpMessage) msg).headers().get(ExtensionHeaderNames.STREAM_ID.text())); - } - - return msg instanceof LastHttpContent; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (isRequestStart(msg)) { - pendingResCount++; - } - super.channelRead(ctx, msg); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (isResponseEnd(msg)) { - pendingResCount--; - } - super.write(ctx, msg, promise); - } - - @Override - protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception { - if (pendingResCount == 0 && evt.isFirst()) { - logger.debug("{} Closing due to idleness", ctx.channel()); - ctx.close(); - } - } -} diff --git a/src/main/java/com/linecorp/armeria/server/PathMapping.java b/src/main/java/com/linecorp/armeria/server/PathMapping.java index 82aac366b650..2d0b74d5662b 100644 --- a/src/main/java/com/linecorp/armeria/server/PathMapping.java +++ b/src/main/java/com/linecorp/armeria/server/PathMapping.java @@ -21,7 +21,6 @@ import java.util.function.Function; import java.util.regex.Pattern; -import com.linecorp.armeria.common.ServiceInvocationContext; import com.linecorp.armeria.server.PathManipulators.Prepend; import com.linecorp.armeria.server.PathManipulators.StripParents; import com.linecorp.armeria.server.PathManipulators.StripPrefixByNumPathComponents; @@ -31,13 +30,13 @@ /** * Matches the absolute path part of a URI and translates the matched path to another path string. * The translated path, returned by {@link #apply(String)}, determines the value of - * {@link ServiceInvocationContext#mappedPath()}. + * {@link ServiceRequestContext#mappedPath()}. */ @FunctionalInterface public interface PathMapping extends Function { /** - * Creates a new {@link PathMapping} that matches a {@linkplain ServiceInvocationContext#path() path} with + * Creates a new {@link PathMapping} that matches a {@linkplain ServiceRequestContext#path() path} with * the specified regular expression, as defined in {@link Pattern}. The returned {@link PathMapping} does * not perform any translation. To create a {@link PathMapping} that performs a translation, use the * decorator methods like {@link #stripPrefix(String)}. @@ -47,7 +46,7 @@ static PathMapping ofRegex(Pattern regex) { } /** - * Creates a new {@link PathMapping} that matches a {@linkplain ServiceInvocationContext#path() path} with + * Creates a new {@link PathMapping} that matches a {@linkplain ServiceRequestContext#path() path} with * the specified regular expression, as defined in {@link Pattern}. The returned {@link PathMapping} does * not perform any translation. To create a {@link PathMapping} that performs a translation, use the * decorator methods like {@link #stripPrefix(String)}. @@ -57,7 +56,7 @@ static PathMapping ofRegex(String regex) { } /** - * Creates a new {@link PathMapping} that matches a {@linkplain ServiceInvocationContext#path() path} with + * Creates a new {@link PathMapping} that matches a {@linkplain ServiceRequestContext#path() path} with * the specified glob expression, where {@code "*"} matches a path component non-recursively and * {@code "**"} matches path components recursively. The returned {@link PathMapping} does not perform any * translation. To create a {@link PathMapping} that performs a translation, use the decorator methods like @@ -74,9 +73,9 @@ static PathMapping ofGlob(String glob) { } /** - * Creates a new {@link PathMapping} that matches a {@linkplain ServiceInvocationContext#path() path} + * Creates a new {@link PathMapping} that matches a {@linkplain ServiceRequestContext#path() path} * under the specified directory prefix. It also removes the specified directory prefix from the matched - * path so that {@link ServiceInvocationContext#mappedPath()} does not have the specified directory prefix. + * path so that {@link ServiceRequestContext#mappedPath()} does not have the specified directory prefix. * For example, when {@code pathPrefix} is {@code "/foo/"}: *