Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ratis-netty/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ static boolean useEpoll(RaftProperties properties) {
static void setUseEpoll(RaftProperties properties, boolean enable) {
setBoolean(properties::setBoolean, USE_EPOLL_KEY, enable);
}

String ASYNC_REQUEST_THREAD_POOL_CACHED_KEY = PREFIX + ".async.request.thread.pool.cached";
boolean ASYNC_REQUEST_THREAD_POOL_CACHED_DEFAULT = true;
static boolean asyncRequestThreadPoolCached(RaftProperties properties) {
return getBoolean(properties::getBoolean, ASYNC_REQUEST_THREAD_POOL_CACHED_KEY,
ASYNC_REQUEST_THREAD_POOL_CACHED_DEFAULT, getDefaultLog());
}
static void setAsyncRequestThreadPoolCached(RaftProperties properties, boolean useCached) {
setBoolean(properties::setBoolean, ASYNC_REQUEST_THREAD_POOL_CACHED_KEY, useCached);
}

String ASYNC_REQUEST_THREAD_POOL_SIZE_KEY = PREFIX + ".async.request.thread.pool.size";
int ASYNC_REQUEST_THREAD_POOL_SIZE_DEFAULT = 32;
static int asyncRequestThreadPoolSize(RaftProperties properties) {
return getInt(properties::getInt, ASYNC_REQUEST_THREAD_POOL_SIZE_KEY,
ASYNC_REQUEST_THREAD_POOL_SIZE_DEFAULT, getDefaultLog(),
requireMin(0), requireMax(65536));
}
static void setAsyncRequestThreadPoolSize(RaftProperties properties, int size) {
setInt(properties::setInt, ASYNC_REQUEST_THREAD_POOL_SIZE_KEY, size);
}
}

interface Client {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.apache.ratis.proto.netty.NettyProtos.RaftNettyServerReplyProto;
import org.apache.ratis.proto.netty.NettyProtos.RaftNettyServerRequestProto;
import org.apache.ratis.util.CodeInjectionForTesting;
import org.apache.ratis.util.ConcurrentUtils;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.MemoizedSupplier;
import org.apache.ratis.util.ProtoUtils;
Expand All @@ -50,8 +51,8 @@

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.Objects;

/**
* A netty server endpoint that acts as the communication layer.
Expand Down Expand Up @@ -87,12 +88,31 @@ public static Builder newBuilder() {
private final MemoizedSupplier<ChannelFuture> channel;
private final InetSocketAddress socketAddress;

private final ExecutorService requestExecutor;

@ChannelHandler.Sharable
class InboundHandler extends SimpleChannelInboundHandler<RaftNettyServerRequestProto> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RaftNettyServerRequestProto proto) {
final RaftNettyServerReplyProto reply = handle(proto);
ctx.writeAndFlush(reply);
requestExecutor.execute(() -> {
final RaftNettyServerReplyProto reply;
try {
// handle() already builds an error reply whenever it has a request context
reply = handle(proto);
} catch (Exception e) {
// Close the channel so the client fails fast instead of blocking until timeout.
LOG.warn("{}: Failed to handle request; closing the channel.", getId(), e);
ctx.close();
return;
}
ctx.writeAndFlush(reply);
});
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
LOG.warn("{}: exceptionCaught on channel {}; closing it.", getId(), ctx.channel(), cause);
ctx.close();
}
}

Expand All @@ -116,6 +136,11 @@ protected void initChannel(SocketChannel ch) {
}
};

this.requestExecutor = ConcurrentUtils.newThreadPoolWithMax(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, it's still a single worker pool (corePoolSize is 0). But instead of using TCP backpressure, we are pushing everything to the unlimited queue and creating pressure on the memory. Moreover, previously, Netty’s worker EventLoops handled connection shards independently, so a blocked request delayed only that shard’s RPCs. The new default single request worker queues all inbound RPCs, including heartbeats, so one slow request can delay heartbeats and trigger leader election.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was one issue which I missed and faced before (hence the test failure in flaky test suite).
corePoolSize=0 + an unbounded queue causes requestExecutor to be single-threaded, and since handle() blocks until commit, this causes the timeouts.
I have addressed this by switching to a fixed pool for now as the related change would increase LoC.

Filed https://issues.apache.org/jira/browse/RATIS-2637 for the improvement as a follow up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backpressure control should be a part of these changes. You have removed the one that was going through TCP flow control and don't provide any replacement. Another thing: we have ThreadPoolExecutor with corePoolSize=0 and unbounded queue. Let me quote "Java Concurrency in Practice":
[3] Developers are sometimes tempted to set the core size to zero so that the worker threads will eventually be torn down and therefore won't prevent the JVM from exiting, but this can cause some strange‐seeming behavior in thread pools that don't use a SynchronousQueue for their work queue (as newCachedThreadPool does). If the pool is already at the core size, ThreadPoolExecutor creates a new thread only if the work queue is full. So tasks submitted to a thread pool with a work queue that has any capacity and a core size of zero will not execute until the queue fills up, which is usually not what is desired.
So, it's still a single thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing: we have ThreadPoolExecutor with corePoolSize=0 and unbounded queue.
the corePoolSize=0 + unbounded-queue cached pool is effectively single-threaded, however this PR now defaults to a fixed pool (corePoolSize=32), so inbound RPCs including heartbeats are no longer serialized behind one slow request.

So I have effectively switched back to the earlier behaviour for the time being.
Let's avoid making this patch bigger and address separately as right now this is effective the previous behaviour.

NettyConfigKeys.Server.asyncRequestThreadPoolCached(server.getProperties()),
NettyConfigKeys.Server.asyncRequestThreadPoolSize(server.getProperties()),
server.getId() + "-request-");

final boolean useEpoll = NettyConfigKeys.Server.useEpoll(server.getProperties());
this.bossGroup = NettyUtils.newEventLoopGroup(CLASS_NAME + "-bossGroup", 0, useEpoll);
this.workerGroup = NettyUtils.newEventLoopGroup(CLASS_NAME + "-workerGroup",0, useEpoll);
Expand Down Expand Up @@ -155,6 +180,7 @@ public void startImpl() throws IOException {

@Override
public void closeImpl() throws IOException {
ConcurrentUtils.shutdownAndWait(requestExecutor);
Comment thread
spacemonkd marked this conversation as resolved.
Outdated
final ChannelFuture f = getChannel().close();
f.syncUninterruptibly();
bossGroup.shutdownGracefully(0, 100, TimeUnit.MILLISECONDS);
Expand Down Expand Up @@ -296,9 +322,15 @@ RaftNettyServerReplyProto handle(RaftNettyServerRequestProto proto) {
throw new UnsupportedOperationException("Request case not supported: "
+ proto.getRaftNettyServerRequestCase());
}
} catch (IOException ioe) {
return toRaftNettyServerReplyProto(
Objects.requireNonNull(rpcRequest, "rpcRequest = null"), ioe);
} catch (Exception e) {
if (rpcRequest == null) {
// let InboundHandler close the channel so the client fails fast.
throw new IllegalStateException(getId() + ": Failed to handle request " + proto, e);
}
// The client deserializes the reply and casts it to IOException, so always send an
// IOException regardless of the actual failure type.
final IOException ioe = e instanceof IOException ? (IOException) e : new IOException(e);
return toRaftNettyServerReplyProto(rpcRequest, ioe);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.ratis.netty.server;

import org.apache.ratis.conf.RaftProperties;
import org.apache.ratis.proto.RaftProtos.RaftRpcRequestProto;
import org.apache.ratis.proto.RaftProtos.RequestVoteRequestProto;
import org.apache.ratis.proto.netty.NettyProtos.RaftNettyServerReplyProto;
import org.apache.ratis.proto.netty.NettyProtos.RaftNettyServerReplyProto.RaftNettyServerReplyCase;
import org.apache.ratis.proto.netty.NettyProtos.RaftNettyServerRequestProto;
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.server.RaftServer;
import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/** Tests for {@link NettyRpcService} request handling. */
public class TestNettyRpcService {
private static final RaftPeerId ID = RaftPeerId.valueOf("s0");

private static RaftServer newMockServer() {
final RaftServer server = Mockito.mock(RaftServer.class);
Mockito.when(server.getId()).thenReturn(ID);
Mockito.when(server.getProperties()).thenReturn(new RaftProperties());
return server;
}

private static RaftNettyServerRequestProto newRequestVoteProto() {
final RaftRpcRequestProto rpc = RaftRpcRequestProto.newBuilder()
.setRequestorId(ID.toByteString())
.setReplyId(ID.toByteString())
.setCallId(1)
.build();
final RequestVoteRequestProto request = RequestVoteRequestProto.newBuilder()
.setServerRequest(rpc)
.build();
return RaftNettyServerRequestProto.newBuilder()
.setRequestVoteRequest(request)
.build();
}

/**
* A non-{@link java.io.IOException} thrown by the server must be turned into an error reply
* instead of escaping the handler and leaving the client to block until its request timeout.
*/
@Test
public void testHandleReturnsErrorReplyOnRuntimeException() throws Exception {
final RaftServer server = newMockServer();
Mockito.when(server.requestVote(Mockito.any())).thenThrow(new RuntimeException("injected"));

final NettyRpcService service = NettyRpcService.newBuilder().setServer(server).build();
service.start();
try {
final RaftNettyServerReplyProto reply = service.handle(newRequestVoteProto());
Assertions.assertEquals(RaftNettyServerReplyCase.EXCEPTIONREPLY, reply.getRaftNettyServerReplyCase());
} finally {
service.close();
}
}

/** Requests must be handled off the Netty I/O event loop, on the request executor thread. */
@Test
public void testRequestHandledOffEventLoop() throws Exception {
final RaftServer server = newMockServer();
final CompletableFuture<String> handlingThreadName = new CompletableFuture<>();
Mockito.when(server.requestVote(Mockito.any())).thenAnswer(invocation -> {
handlingThreadName.complete(Thread.currentThread().getName());
throw new RuntimeException("injected");
});

final NettyRpcService service = NettyRpcService.newBuilder().setServer(server).build();
service.start();
try {
final ChannelHandlerContext ctx = Mockito.mock(ChannelHandlerContext.class);
service.new InboundHandler().channelRead0(ctx, newRequestVoteProto());

final String threadName = handlingThreadName.get(5, TimeUnit.SECONDS);
Assertions.assertTrue(threadName.startsWith(ID + "-request-"),
"Request was handled on an unexpected thread: " + threadName);
Assertions.assertNotEquals(Thread.currentThread().getName(), threadName,
"Request was handled on the calling thread, not offloaded");
} finally {
service.close();
}
}
}
Loading