Skip to content

Commit

Permalink
Add ServiceOptions and @ServiceOption (#5574)
Browse files Browse the repository at this point in the history
Motivation:

Allow users to set default options for a specific service 

Modifications:

- Add `ServiceOptions` and `@ServiceOption` annotation to allow users
specify default options

Result:

Users are able to set service specific options like below 
```java
final HttpService httpService = new HttpService() {
    ...

    @OverRide
    public ServiceOptions options() {
        return defaultServiceOptions;
    }
};
```

or use annotation 

```java
  final class MyService {

      @ServiceOption(requestTimeoutMillis = 11111, maxRequestLength = 1111,
              requestAutoAbortDelayMillis = 111)
      @get("/test1")
      public HttpResponse test() {
          return HttpResponse.of("OK");
      }
   ...
}
```

- Closes <#5071>. (If this
resolves the issue.)

<!--
Visit this URL to learn more about how to write a pull request
description:

https://armeria.dev/community/developer-guide#how-to-write-pull-request-description
-->

---------

Co-authored-by: minux <[email protected]>
Co-authored-by: jrhee17 <[email protected]>
Co-authored-by: Ikhun Um <[email protected]>
Co-authored-by: Ikhun Um <[email protected]>
Co-authored-by: minux <[email protected]>
  • Loading branch information
6 people authored Jun 10, 2024
1 parent b6c4d73 commit 395fc27
Show file tree
Hide file tree
Showing 18 changed files with 772 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.Route;
import com.linecorp.armeria.server.RoutingContext;
import com.linecorp.armeria.server.ServiceOption;
import com.linecorp.armeria.server.ServiceOptions;
import com.linecorp.armeria.server.ServiceOptionsBuilder;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.SimpleDecoratingHttpService;
import com.linecorp.armeria.server.annotation.AnnotatedService;
Expand Down Expand Up @@ -111,6 +114,8 @@ final class DefaultAnnotatedService implements AnnotatedService {
@Nullable
private final String name;

private final ServiceOptions options;

DefaultAnnotatedService(Object object, Method method,
int overloadId, List<AnnotatedValueResolver> resolvers,
List<ExceptionHandlerFunction> exceptionHandlers,
Expand Down Expand Up @@ -176,6 +181,16 @@ final class DefaultAnnotatedService implements AnnotatedService {
this.method.setAccessible(true);
// following must be called only after method.setAccessible(true)
methodHandle = asMethodHandle(method, object);

ServiceOption serviceOption = AnnotationUtil.findFirst(method, ServiceOption.class);
if (serviceOption == null) {
serviceOption = AnnotationUtil.findFirst(object.getClass(), ServiceOption.class);
}
if (serviceOption != null) {
options = buildServiceOptions(serviceOption);
} else {
options = ServiceOptions.of();
}
}

private static Type getActualReturnType(Method method) {
Expand Down Expand Up @@ -221,6 +236,20 @@ private static void warnIfHttpResponseArgumentExists(Type returnType,
}
}

private static ServiceOptions buildServiceOptions(ServiceOption serviceOption) {
final ServiceOptionsBuilder builder = ServiceOptions.builder();
if (serviceOption.requestTimeoutMillis() >= 0) {
builder.requestTimeoutMillis(serviceOption.requestTimeoutMillis());
}
if (serviceOption.maxRequestLength() >= 0) {
builder.maxRequestLength(serviceOption.maxRequestLength());
}
if (serviceOption.requestAutoAbortDelayMillis() >= 0) {
builder.requestAutoAbortDelayMillis(serviceOption.requestAutoAbortDelayMillis());
}
return builder.build();
}

@Override
public String name() {
return name;
Expand Down Expand Up @@ -486,6 +515,11 @@ public ExchangeType exchangeType(RoutingContext routingContext) {
}
}

@Override
public ServiceOptions options() {
return options;
}

/**
* An {@link ExceptionHandlerFunction} which wraps a list of {@link ExceptionHandlerFunction}s.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.stream.ClosedStreamException;
import com.linecorp.armeria.common.stream.StreamMessage;
import com.linecorp.armeria.common.util.TimeoutMode;
import com.linecorp.armeria.common.websocket.WebSocket;
import com.linecorp.armeria.internal.common.websocket.WebSocketFrameEncoder;
import com.linecorp.armeria.internal.common.websocket.WebSocketWrapper;
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.ServiceConfig;
import com.linecorp.armeria.server.ServiceOptions;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.websocket.WebSocketProtocolHandler;
import com.linecorp.armeria.server.websocket.WebSocketService;
Expand Down Expand Up @@ -97,12 +99,13 @@ public final class DefaultWebSocketService implements WebSocketService, WebSocke
@Nullable
private final Predicate<? super String> originPredicate;
private final boolean aggregateContinuation;
private final ServiceOptions serviceOptions;

public DefaultWebSocketService(WebSocketServiceHandler handler, @Nullable HttpService fallbackService,
int maxFramePayloadLength, boolean allowMaskMismatch,
Set<String> subprotocols, boolean allowAnyOrigin,
@Nullable Predicate<? super String> originPredicate,
boolean aggregateContinuation) {
boolean aggregateContinuation, ServiceOptions serviceOptions) {
this.handler = handler;
this.fallbackService = fallbackService;
this.maxFramePayloadLength = maxFramePayloadLength;
Expand All @@ -111,6 +114,7 @@ public DefaultWebSocketService(WebSocketServiceHandler handler, @Nullable HttpSe
this.allowAnyOrigin = allowAnyOrigin;
this.originPredicate = originPredicate;
this.aggregateContinuation = aggregateContinuation;
this.serviceOptions = serviceOptions;
}

@Override
Expand Down Expand Up @@ -205,6 +209,27 @@ private WebSocketUpgradeResult upgradeHttp1(ServiceRequestContext ctx, HttpReque
private HttpResponse failOrFallback(ServiceRequestContext ctx, HttpRequest req,
Supplier<HttpResponse> invalidResponse) throws Exception {
if (fallbackService != null) {
// Try to apply ServiceOptions from fallbackService first. If not set, use the settings of the
// virtual host.
final ServiceOptions options = fallbackService.options();
long requestTimeoutMillis = options.requestTimeoutMillis();
if (requestTimeoutMillis < 0) {
requestTimeoutMillis = ctx.config().virtualHost().requestTimeoutMillis();
}
ctx.setRequestTimeoutMillis(TimeoutMode.SET_FROM_START, requestTimeoutMillis);

long maxRequestLength = options.maxRequestLength();
if (maxRequestLength < 0) {
maxRequestLength = ctx.config().virtualHost().maxRequestLength();
}
ctx.setMaxRequestLength(maxRequestLength);

long requestAutoAbortDelayMillis = options.requestAutoAbortDelayMillis();
if (requestAutoAbortDelayMillis < 0) {
requestAutoAbortDelayMillis = ctx.config().virtualHost().requestAutoAbortDelayMillis();
}
ctx.setRequestAutoAbortDelayMillis(requestAutoAbortDelayMillis);

return fallbackService.serve(ctx, req);
} else {
return invalidResponse.get();
Expand Down Expand Up @@ -397,4 +422,9 @@ public HttpResponse encode(ServiceRequestContext ctx, WebSocket out) {
public WebSocketProtocolHandler protocolHandler() {
return this;
}

@Override
public ServiceOptions options() {
return serviceOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,12 @@ default HttpService decorate(DecoratingHttpServiceFunction function) {
default ExchangeType exchangeType(RoutingContext routingContext) {
return ExchangeType.BIDI_STREAMING;
}

/**
* Returns the {@link ServiceOptions} of this {@link HttpService}.
*/
@UnstableApi
default ServiceOptions options() {
return ServiceOptions.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,13 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;

import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpHeadersBuilder;
import com.linecorp.armeria.common.RequestId;
import com.linecorp.armeria.common.SuccessFunction;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.BlockingTaskExecutor;
import com.linecorp.armeria.common.util.EventLoopGroups;
import com.linecorp.armeria.internal.common.websocket.WebSocketUtil;
import com.linecorp.armeria.internal.server.websocket.DefaultWebSocketService;
import com.linecorp.armeria.server.logging.AccessLogWriter;

import io.netty.channel.EventLoopGroup;
Expand Down Expand Up @@ -93,6 +90,17 @@ final class ServiceConfigBuilder implements ServiceConfigSetters<ServiceConfigBu
ServiceConfigBuilder(Route route, String contextPath, HttpService service) {
this.route = requireNonNull(route, "route").withPrefix(contextPath);
this.service = requireNonNull(service, "service");

final ServiceOptions options = service.options();
if (options.requestTimeoutMillis() != -1) {
requestTimeoutMillis = options.requestTimeoutMillis();
}
if (options.maxRequestLength() != -1) {
maxRequestLength = options.maxRequestLength();
}
if (options.requestAutoAbortDelayMillis() != -1) {
requestAutoAbortDelayMillis = options.requestAutoAbortDelayMillis();
}
}

void addMappedRoute(Route mappedRoute) {
Expand Down Expand Up @@ -352,34 +360,13 @@ ServiceConfig build(ServiceNaming defaultServiceNaming,
unloggedExceptionsReporter);
}

final boolean webSocket = service.as(DefaultWebSocketService.class) != null;
final long requestTimeoutMillis;
if (this.requestTimeoutMillis != null) {
requestTimeoutMillis = this.requestTimeoutMillis;
} else if (!webSocket || defaultRequestTimeoutMillis != Flags.defaultRequestTimeoutMillis()) {
requestTimeoutMillis = defaultRequestTimeoutMillis;
} else {
requestTimeoutMillis = WebSocketUtil.DEFAULT_REQUEST_RESPONSE_TIMEOUT_MILLIS;
}

final long maxRequestLength;
if (this.maxRequestLength != null) {
maxRequestLength = this.maxRequestLength;
} else if (!webSocket || defaultMaxRequestLength != Flags.defaultMaxRequestLength()) {
maxRequestLength = defaultMaxRequestLength;
} else {
maxRequestLength = WebSocketUtil.DEFAULT_MAX_REQUEST_RESPONSE_LENGTH;
}

final long requestAutoAbortDelayMillis;
if (this.requestAutoAbortDelayMillis != null) {
requestAutoAbortDelayMillis = this.requestAutoAbortDelayMillis;
} else if (!webSocket ||
defaultRequestAutoAbortDelayMillis != Flags.defaultRequestAutoAbortDelayMillis()) {
requestAutoAbortDelayMillis = defaultRequestAutoAbortDelayMillis;
} else {
requestAutoAbortDelayMillis = WebSocketUtil.DEFAULT_REQUEST_AUTO_ABORT_DELAY_MILLIS;
}
final long requestTimeoutMillis = this.requestTimeoutMillis != null ? this.requestTimeoutMillis
: defaultRequestTimeoutMillis;
final long maxRequestLength = this.maxRequestLength != null ? this.maxRequestLength
: defaultMaxRequestLength;
final long requestAutoAbortDelayMillis =
this.requestAutoAbortDelayMillis != null ? this.requestAutoAbortDelayMillis
: defaultRequestAutoAbortDelayMillis;

final Supplier<AutoCloseable> mergedContextHook = mergeHooks(contextHook, this.contextHook);

Expand Down
49 changes: 49 additions & 0 deletions core/src/main/java/com/linecorp/armeria/server/ServiceOption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024 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:
*
* https://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.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;

/**
* An annotation used to configure {@link ServiceOptions} of an {@link HttpService}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface ServiceOption {

/**
* Server-side timeout of a request in milliseconds.
*/
long requestTimeoutMillis() default -1;

/**
* Server-side maximum length of a request.
*/
long maxRequestLength() default -1;

/**
* The amount of time to wait before aborting an {@link HttpRequest} when its corresponding
* {@link HttpResponse} is complete.
*/
long requestAutoAbortDelayMillis() default -1;
}
111 changes: 111 additions & 0 deletions core/src/main/java/com/linecorp/armeria/server/ServiceOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2024 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:
*
* https://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.Objects;

import com.google.common.base.MoreObjects;

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.annotation.UnstableApi;

/**
* Options for configuring an {@link HttpService}.
* You can override the default options by implementing {@link HttpService#options()}.
*/
@UnstableApi
public final class ServiceOptions {
private static final ServiceOptions DEFAULT_OPTIONS = builder().build();

/**
* Returns the default {@link ServiceOptions}.
*/
public static ServiceOptions of() {
return DEFAULT_OPTIONS;
}

/**
* Returns a new {@link ServiceOptionsBuilder}.
*/
public static ServiceOptionsBuilder builder() {
return new ServiceOptionsBuilder();
}

private final long requestTimeoutMillis;
private final long maxRequestLength;
private final long requestAutoAbortDelayMillis;

ServiceOptions(long requestTimeoutMillis, long maxRequestLength, long requestAutoAbortDelayMillis) {
this.requestTimeoutMillis = requestTimeoutMillis;
this.maxRequestLength = maxRequestLength;
this.requestAutoAbortDelayMillis = requestAutoAbortDelayMillis;
}

/**
* Returns the server-side timeout of a request in milliseconds. {@code -1} if not set.
*/
public long requestTimeoutMillis() {
return requestTimeoutMillis;
}

/**
* Returns the server-side maximum length of a request. {@code -1} if not set.
*/
public long maxRequestLength() {
return maxRequestLength;
}

/**
* Returns the amount of time to wait before aborting an {@link HttpRequest} when its corresponding
* {@link HttpResponse} is complete. {@code -1} if not set.
*/
public long requestAutoAbortDelayMillis() {
return requestAutoAbortDelayMillis;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

final ServiceOptions that = (ServiceOptions) o;

return requestTimeoutMillis == that.requestTimeoutMillis &&
maxRequestLength == that.maxRequestLength &&
requestAutoAbortDelayMillis == that.requestAutoAbortDelayMillis;
}

@Override
public int hashCode() {
return Objects.hash(requestTimeoutMillis, maxRequestLength, requestAutoAbortDelayMillis);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("requestTimeoutMillis", requestTimeoutMillis)
.add("maxRequestLength", maxRequestLength)
.add("requestAutoAbortDelayMillis", requestAutoAbortDelayMillis)
.toString();
}
}

Loading

0 comments on commit 395fc27

Please sign in to comment.