Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to close a connection when exceeding the maximum age on server-side #2747

Merged
merged 9 commits into from
Jun 5, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ final class Http1ClientKeepAliveHandler extends KeepAliveHandler {

Http1ClientKeepAliveHandler(Channel channel, ClientHttp1ObjectEncoder encoder, Http1ResponseDecoder decoder,
long idleTimeoutMillis, long pingIntervalMillis) {
super(channel, "client", idleTimeoutMillis, pingIntervalMillis);
// TODO(ikhoon): Should set maxConnectionAgeMillis by https://github.com/line/armeria/pull/2741
super(channel, "client", idleTimeoutMillis, pingIntervalMillis, /* maxConnectionAgeMillis */ 0);
httpSession = HttpSession.get(requireNonNull(channel, "channel"));
this.encoder = requireNonNull(encoder, "encoder");
this.decoder = requireNonNull(decoder, "decoder");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception {
protected boolean needsImmediateDisconnection() {
return clientFactory.isClosing() ||
responseDecoder.goAwayHandler().receivedErrorGoAway() ||
keepAliveHandler.isClosing();
(keepAliveHandler != null && keepAliveHandler.isClosing());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
final class Http2ClientKeepAliveHandler extends Http2KeepAliveHandler {
Http2ClientKeepAliveHandler(Channel channel, Http2FrameWriter frameWriter,
long idleTimeoutMillis, long pingIntervalMillis) {
super(channel, frameWriter, "client", idleTimeoutMillis, pingIntervalMillis);

// TODO(ikhoon): Should set maxConnectionAgeMillis by https://github.com/line/armeria/pull/2741
super(channel, frameWriter, "client", idleTimeoutMillis,
pingIntervalMillis, /* maxConnectionAgeMillis */ 0);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ public void onNext(HttpObject o) {
if (o instanceof HttpHeaders) {
final HttpHeaders trailers = (HttpHeaders) o;
if (trailers.contains(HttpHeaderNames.STATUS)) {
failAndReset(
new IllegalArgumentException("published a trailers with status: " + o));
failAndReset(new IllegalArgumentException("published a trailers with status: " + o));
return;
}
// Trailers always end the stream even if not explicitly set.
Expand Down
22 changes: 22 additions & 0 deletions core/src/main/java/com/linecorp/armeria/common/Flags.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ public final class Flags {
DEFAULT_DEFAULT_PING_INTERVAL_MILLIS,
value -> value >= 0);

private static final long DEFAULT_DEFAULT_MAX_SERVER_CONNECTION_AGE_MILLIS = 0; // Disabled
private static final long DEFAULT_MAX_SERVER_CONNECTION_AGE_MILLIS =
getLong("defaultMaxServerConnectionAgeMillis",
DEFAULT_DEFAULT_MAX_SERVER_CONNECTION_AGE_MILLIS,
value -> value >= 0);

private static final int DEFAULT_DEFAULT_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE = 1024 * 1024; // 1MiB
private static final int DEFAULT_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE =
getInt("defaultHttp2InitialConnectionWindowSize",
Expand Down Expand Up @@ -756,6 +762,22 @@ public static long defaultPingIntervalMillis() {
return DEFAULT_PING_INTERVAL_MILLIS;
}

/**
* Returns the default server-side max age of a connection for keep-alive in milliseconds.
* If the value of this flag is greater than {@code 0}, a connection is disconnected after the specified
* amount of the time since the connection was established.
*
* <p>The default value of this flag is {@value #DEFAULT_DEFAULT_MAX_SERVER_CONNECTION_AGE_MILLIS}.
* Specify the {@code -Dcom.linecorp.armeria.defaultMaxServerConnectionAgeMillis=<integer>} JVM option
* to override the default value. If the specified value was smaller than 1 second,
* bumps the max connection age to 1 second.
*
* @see ServerBuilder#maxConnectionAgeMillis(long)
*/
public static long defaultMaxServerConnectionAgeMillis() {
return DEFAULT_MAX_SERVER_CONNECTION_AGE_MILLIS;
}

/**
* Returns the default value of the {@link ServerBuilder#http2InitialConnectionWindowSize(int)} and
* {@link ClientFactoryBuilder#http2InitialConnectionWindowSize(int)} option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ public abstract class Http2KeepAliveHandler extends KeepAliveHandler {

private long lastPingPayload;

protected Http2KeepAliveHandler(Channel channel, Http2FrameWriter frameWriter,
String name, long idleTimeoutMillis, long pingIntervalMillis) {
super(channel, name, idleTimeoutMillis, pingIntervalMillis);
protected Http2KeepAliveHandler(Channel channel, Http2FrameWriter frameWriter, String name,
long idleTimeoutMillis, long pingIntervalMillis,
long maxConnectionAgeMillis) {
super(channel, name, idleTimeoutMillis, pingIntervalMillis, maxConnectionAgeMillis);
this.channel = requireNonNull(channel, "channel");
this.frameWriter = requireNonNull(frameWriter, "frameWriter");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public abstract class KeepAliveHandler {
private long lastPingIdleTime;
private boolean firstPingIdleEvent = true;

@Nullable
private ScheduledFuture<?> maxConnectionAgeFuture;
private final long maxConnectionAgeNanos;
private boolean isMaxConnectionAgeExceeded;

private boolean isInitialized;
private PingState pingState = PingState.IDLE;

Expand All @@ -91,7 +96,8 @@ public abstract class KeepAliveHandler {
@Nullable
private Future<?> shutdownFuture;

protected KeepAliveHandler(Channel channel, String name, long idleTimeoutMillis, long pingIntervalMillis) {
protected KeepAliveHandler(Channel channel, String name,
long idleTimeoutMillis, long pingIntervalMillis, long maxConnectionAgeMillis) {
this.channel = channel;
this.name = name;

Expand All @@ -100,11 +106,18 @@ protected KeepAliveHandler(Channel channel, String name, long idleTimeoutMillis,
} else {
connectionIdleTimeNanos = TimeUnit.MILLISECONDS.toNanos(idleTimeoutMillis);
}

if (pingIntervalMillis <= 0) {
pingIdleTimeNanos = 0;
} else {
pingIdleTimeNanos = TimeUnit.MILLISECONDS.toNanos(pingIntervalMillis);
}

if (maxConnectionAgeMillis <= 0) {
maxConnectionAgeNanos = 0;
} else {
maxConnectionAgeNanos = TimeUnit.MILLISECONDS.toNanos(maxConnectionAgeMillis);
}
}

public final void initialize(ChannelHandlerContext ctx) {
Expand All @@ -124,6 +137,13 @@ public final void initialize(ChannelHandlerContext ctx) {
pingIdleTimeout = executor().schedule(new PingIdleTimeoutTask(ctx),
pingIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (maxConnectionAgeNanos > 0) {
maxConnectionAgeFuture = executor().schedule(() -> {
logger.info("make isMaxConnectionAgeExceeded true");
trustin marked this conversation as resolved.
Show resolved Hide resolved
isMaxConnectionAgeExceeded = true;
},
maxConnectionAgeNanos, TimeUnit.NANOSECONDS);
}
}

public final void destroy() {
Expand All @@ -136,7 +156,12 @@ public final void destroy() {
pingIdleTimeout.cancel(false);
pingIdleTimeout = null;
}
if (maxConnectionAgeFuture != null) {
maxConnectionAgeFuture.cancel(false);
maxConnectionAgeFuture = null;
}
pingState = PingState.SHUTDOWN;
isMaxConnectionAgeExceeded = true;
cancelFutures();
}

Expand Down Expand Up @@ -176,6 +201,10 @@ public final boolean isClosing() {
return pingState == PingState.SHUTDOWN;
}

public final boolean isMaxConnectionAgeExceeded() {
return isMaxConnectionAgeExceeded;
}

protected abstract ChannelFuture writePing(ChannelHandlerContext ctx);

protected abstract boolean pingResetsPreviousPing();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
import io.netty.channel.ChannelHandlerContext;

class Http1ServerKeepAliveHandler extends KeepAliveHandler {
Http1ServerKeepAliveHandler(Channel channel, long idleTimeoutMillis) {
super(channel, "server", idleTimeoutMillis, 0);
Http1ServerKeepAliveHandler(Channel channel, long idleTimeoutMillis, long maxConnectionAgeMillis) {
super(channel, "server", idleTimeoutMillis, 0, maxConnectionAgeMillis);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ final class Http2ServerConnectionHandler extends AbstractHttp2ConnectionHandler
if (config.idleTimeoutMillis() > 0 || config.pingIntervalMillis() > 0) {
keepAliveHandler = new Http2ServerKeepAliveHandler(channel, encoder().frameWriter(),
config.idleTimeoutMillis(),
config.pingIntervalMillis());
config.pingIntervalMillis(),
config.maxConnectionAgeMillis());
} else {
keepAliveHandler = null;
}
Expand All @@ -69,7 +70,7 @@ final class Http2ServerConnectionHandler extends AbstractHttp2ConnectionHandler
protected boolean needsImmediateDisconnection() {
return gracefulShutdownSupport.isShuttingDown() ||
requestDecoder.goAwayHandler().receivedErrorGoAway() ||
keepAliveHandler.isClosing();
(keepAliveHandler != null && keepAliveHandler.isClosing());
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@

final class Http2ServerKeepAliveHandler extends Http2KeepAliveHandler {
Http2ServerKeepAliveHandler(Channel channel, Http2FrameWriter frameWriter,
long idleTimeoutMillis, long pingIntervalMillis) {
super(channel, frameWriter, "server", idleTimeoutMillis, pingIntervalMillis);
long idleTimeoutMillis, long pingIntervalMillis, long maxConnectionAgeMillis) {
super(channel, frameWriter, "server", idleTimeoutMillis, pingIntervalMillis, maxConnectionAgeMillis);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,10 @@ private void handleHttp2Settings(ChannelHandlerContext ctx, Http2Settings h2sett
final ChannelPipeline pipeline = ctx.pipeline();
final Http2ServerConnectionHandler handler = pipeline.get(Http2ServerConnectionHandler.class);
if (responseEncoder == null) {
responseEncoder = new ServerHttp2ObjectEncoder(ctx, handler.encoder(), handler.keepAliveHandler(),
config.isDateHeaderEnabled(),
config.isServerHeaderEnabled()
);
responseEncoder = newServerHttp2ObjectEncoder(ctx, handler);
} else if (responseEncoder instanceof Http1ObjectEncoder) {
responseEncoder.close();
responseEncoder = new ServerHttp2ObjectEncoder(ctx, handler.encoder(), handler.keepAliveHandler(),
config.isDateHeaderEnabled(),
config.isServerHeaderEnabled()
);
responseEncoder = newServerHttp2ObjectEncoder(ctx, handler);
}

// Update the connection-level flow-control window size.
Expand All @@ -285,6 +279,14 @@ private void handleHttp2Settings(ChannelHandlerContext ctx, Http2Settings h2sett
}
}

private ServerHttp2ObjectEncoder newServerHttp2ObjectEncoder(ChannelHandlerContext ctx,
Http2ServerConnectionHandler handler) {
return new ServerHttp2ObjectEncoder(ctx, handler.encoder(), handler.keepAliveHandler(),
config.isDateHeaderEnabled(),
config.isServerHeaderEnabled()
);
}

private static void incrementLocalWindowSize(ChannelPipeline pipeline, int delta) {
try {
final Http2Connection connection = pipeline.get(Http2ServerConnectionHandler.class).connection();
Expand Down Expand Up @@ -314,6 +316,13 @@ private void handleRequest(ChannelHandlerContext ctx, DecodedHttpRequest req) th
final ProxiedAddresses proxiedAddresses = determineProxiedAddresses(channel, headers);
final InetAddress clientAddress = config.clientAddressMapper().apply(proxiedAddresses).getAddress();

// Handle max connection age for HTTP/1.
if (!protocol.isMultiplex() &&
((ServerHttp1ObjectEncoder) responseEncoder).isSentConnectionCloseHeader()) {
channel.close();
return;
}

// Handle 'OPTIONS * HTTP/1.1'.
final String originalPath = headers.path();
if (originalPath.isEmpty() || originalPath.charAt(0) != '/') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,16 @@ private void configurePipeline(ChannelPipeline p, Set<SessionProtocol> protocols

private void configureHttp(ChannelPipeline p, @Nullable ProxiedAddresses proxiedAddresses) {
final long idleTimeoutMillis = config.idleTimeoutMillis();
final KeepAliveHandler keepAliveHandler =
idleTimeoutMillis > 0 ? new Http1ServerKeepAliveHandler(p.channel(), idleTimeoutMillis) : null;
final KeepAliveHandler keepAliveHandler;
if (idleTimeoutMillis > 0) {
keepAliveHandler = new Http1ServerKeepAliveHandler(p.channel(), idleTimeoutMillis,
config.maxConnectionAgeMillis());
} else {
keepAliveHandler = null;
}
final ServerHttp1ObjectEncoder responseEncoder = new ServerHttp1ObjectEncoder(
p.channel(), SessionProtocol.H1C, keepAliveHandler, config.isDateHeaderEnabled(),
config.isServerHeaderEnabled()
p.channel(), SessionProtocol.H1C, keepAliveHandler,
config.isDateHeaderEnabled(), config.isServerHeaderEnabled()
);
p.addLast(TrafficLoggingHandler.SERVER);
p.addLast(new Http2PrefaceOrHttpHandler(responseEncoder));
Expand Down Expand Up @@ -399,11 +404,17 @@ private void addHttpHandlers(ChannelHandlerContext ctx) {
final Channel ch = ctx.channel();
final ChannelPipeline p = ctx.pipeline();
final long idleTimeoutMillis = config.idleTimeoutMillis();
final KeepAliveHandler keepAliveHandler =
idleTimeoutMillis > 0 ? new Http1ServerKeepAliveHandler(ch, idleTimeoutMillis) : null;
final KeepAliveHandler keepAliveHandler;
if (idleTimeoutMillis > 0) {
keepAliveHandler = new Http1ServerKeepAliveHandler(ch, idleTimeoutMillis,
config.maxConnectionAgeMillis());
} else {
keepAliveHandler = null;
}

final ServerHttp1ObjectEncoder writer = new ServerHttp1ObjectEncoder(
ch, SessionProtocol.H1, keepAliveHandler, config.isDateHeaderEnabled(),
config.isServerHeaderEnabled());
ch, SessionProtocol.H1, keepAliveHandler,
config.isDateHeaderEnabled(), config.isServerHeaderEnabled());
p.addLast(new HttpServerCodec(
config.http1MaxInitialLineLength(),
config.http1MaxHeaderSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public final class ServerBuilder {

@VisibleForTesting
static final long MIN_PING_INTERVAL_MILLIS = 10_000L;
private static final long MIN_MAX_CONNECTION_AGE_MILLIS = 1_000L;

static {
RequestContextUtil.init();
Expand All @@ -172,6 +173,7 @@ public final class ServerBuilder {
private int maxNumConnections = Flags.maxNumConnections();
private long idleTimeoutMillis = Flags.defaultServerIdleTimeoutMillis();
private long pingIntervalMillis = Flags.defaultPingIntervalMillis();
private long maxConnectionAgeMillis = Flags.defaultMaxServerConnectionAgeMillis();
private int http2InitialConnectionWindowSize = Flags.defaultHttp2InitialConnectionWindowSize();
private int http2InitialStreamWindowSize = Flags.defaultHttp2InitialStreamWindowSize();
private long http2MaxStreamsPerConnection = Flags.defaultHttp2MaxStreamsPerConnection();
Expand Down Expand Up @@ -499,6 +501,34 @@ public ServerBuilder pingInterval(Duration pingInterval) {
return this;
}

/**
* Sets the maximum allowed age of a connection in millis for keep-alive. A connection is disconnected
* after the specified {@code maxConnectionAgeMillis} since the connection was established.
*
* @param maxConnectionAgeMillis the maximum connection age in millis. {@code 0} disables the limit.
* @throws IllegalArgumentException if the specified {@code maxConnectionAge} is smaller than
* {@value #MIN_MAX_CONNECTION_AGE_MILLIS} second.
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
*/
public ServerBuilder maxConnectionAgeMillis(long maxConnectionAgeMillis) {
checkArgument(maxConnectionAgeMillis >= MIN_MAX_CONNECTION_AGE_MILLIS || maxConnectionAgeMillis == 0,
"maxConnectionAgeMillis: %s (expected: >= %s or == 0)",
maxConnectionAgeMillis, MIN_MAX_CONNECTION_AGE_MILLIS);
this.maxConnectionAgeMillis = maxConnectionAgeMillis;
return this;
}

/**
* Sets the maximum allowed age of a connection for keep-alive. A connection is disconnected
* after the specified {@code maxConnectionAge} since the connection was established.
*
* @param maxConnectionAge the maximum connection age. {@code 0} disables the limit.
* @throws IllegalArgumentException if the specified {@code maxConnectionAge} is smaller than
* {@code 1} second.
*/
public ServerBuilder maxConnectionAge(Duration maxConnectionAge) {
return maxConnectionAgeMillis(requireNonNull(maxConnectionAge, "maxConnectionAge").toMillis());
}

/**
* Sets the initial connection-level HTTP/2 flow control window size. Larger values can lower stream
* warmup time at the expense of being easier to overload the server. Defaults to
Expand Down Expand Up @@ -1483,10 +1513,18 @@ public Server build() {
}
}

if (maxConnectionAgeMillis > 0) {
maxConnectionAgeMillis = Math.max(maxConnectionAgeMillis, MIN_MAX_CONNECTION_AGE_MILLIS);
if (idleTimeoutMillis == 0 || idleTimeoutMillis > maxConnectionAgeMillis) {
idleTimeoutMillis = maxConnectionAgeMillis;
}
}

final Server server = new Server(new ServerConfig(
ports, setSslContextIfAbsent(defaultVirtualHost, defaultSslContext), virtualHosts,
workerGroup, shutdownWorkerGroupOnStop, startStopExecutor, maxNumConnections,
idleTimeoutMillis, pingIntervalMillis, http2InitialConnectionWindowSize,
idleTimeoutMillis, pingIntervalMillis, maxConnectionAgeMillis,
http2InitialConnectionWindowSize,
http2InitialStreamWindowSize, http2MaxStreamsPerConnection,
http2MaxFrameSize, http2MaxHeaderListSize, http1MaxInitialLineLength, http1MaxHeaderSize,
http1MaxChunkSize, gracefulShutdownQuietPeriod, gracefulShutdownTimeout,
Expand Down
Loading