Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 19 additions & 16 deletions ratis-grpc/src/main/java/org/apache/ratis/grpc/GrpcConfigKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -332,18 +332,6 @@ static void setCredentials(Parameters parameters, ServerCredentials credentials)
parameters.put(CREDENTIALS_PARAMETER, credentials, CREDENTIALS_CLASS);
}

String LOG_APPENDER_LISTENER_FACTORY_PARAMETER = PREFIX + ".log.appender.listener.factory";
Class<GrpcLogAppenderListener.Factory> LOG_APPENDER_LISTENER_FACTORY_CLASS = GrpcLogAppenderListener.Factory.class;
static GrpcLogAppenderListener.Factory logAppenderListenerFactory(Parameters parameters) {
return parameters == null ? null : parameters.get(
LOG_APPENDER_LISTENER_FACTORY_PARAMETER, LOG_APPENDER_LISTENER_FACTORY_CLASS);
}

/** Sets an optional factory for observing the lifecycle of each peer log appender. */
static void setLogAppenderListenerFactory(Parameters parameters, GrpcLogAppenderListener.Factory factory) {
parameters.put(LOG_APPENDER_LISTENER_FACTORY_PARAMETER, factory, LOG_APPENDER_LISTENER_FACTORY_CLASS);
}

String TLS_CONF_PARAMETER = PREFIX + ".tls.conf";
Class<GrpcTlsConfig> TLS_CONF_CLASS = TLS.CONF_CLASS;
static GrpcTlsConfig tlsConf(Parameters parameters) {
Expand All @@ -362,12 +350,27 @@ static void setStubPoolSize(RaftProperties properties, int size) {
setInt(properties::setInt, STUB_POOL_SIZE_KEY, size);
}

String LOG_APPENDER_LISTENER_FACTORY_PARAMETER = PREFIX + ".log.appender.listener.factory";
Class<GrpcLogAppenderListener.Factory> LOG_APPENDER_LISTENER_FACTORY_CLASS = GrpcLogAppenderListener.Factory.class;
static GrpcLogAppenderListener.Factory logAppenderListenerFactory(Parameters parameters) {
return parameters == null ? null : parameters.get(
LOG_APPENDER_LISTENER_FACTORY_PARAMETER, LOG_APPENDER_LISTENER_FACTORY_CLASS);
}
/** Sets an optional factory for observing the lifecycle of each peer log appender. */
static void setLogAppenderListenerFactory(Parameters parameters, GrpcLogAppenderListener.Factory factory) {
parameters.put(LOG_APPENDER_LISTENER_FACTORY_PARAMETER, factory, LOG_APPENDER_LISTENER_FACTORY_CLASS);
}

/**
* The number of connections to each peer used for appendEntries. The groups shared with a peer are spread over
* them, each group always using the same one, so that the replication between two servers is not limited to one
* connection and so one event-loop thread at each end. Heartbeats keep their own channel.
* The number of connections to each peer for appendEntries.
* When it is 1 (the default), appendEntries uses the main connection to the peer,
* which is shared with the other RPCs such as requestVote and installSnapshot.
* Otherwise, appendEntries uses this many extra connections dedicated to it, but not the main connection.
* The connections are shared by the appendEntries streams of all the groups with the peer;
* each stream uses one connection and the new streams take the connections in turn.
* When there is a separate heartbeat channel (see {@link #HEARTBEAT_CHANNEL_KEY}), heartbeats do not use them.
*/
String LOG_APPENDER_CONNECTIONS_KEY = PREFIX + ".log-appender.connections";
String LOG_APPENDER_CONNECTIONS_KEY = PREFIX + ".log.appender.connections";
int LOG_APPENDER_CONNECTIONS_DEFAULT = 1;
static int logAppenderConnections(RaftProperties properties) {
return getInt(properties::getInt, LOG_APPENDER_CONNECTIONS_KEY,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.grpc.server;

import org.apache.ratis.proto.RaftProtos.AppendEntriesReplyProto;
import org.apache.ratis.proto.RaftProtos.AppendEntriesRequestProto;
import org.apache.ratis.proto.grpc.RaftServerProtocolServiceGrpc;
import org.apache.ratis.proto.grpc.RaftServerProtocolServiceGrpc.RaftServerProtocolServiceStub;
import org.apache.ratis.thirdparty.io.grpc.ManagedChannel;
import org.apache.ratis.thirdparty.io.grpc.stub.CallStreamObserver;
import org.apache.ratis.thirdparty.io.grpc.stub.StreamObserver;
import org.apache.ratis.util.Preconditions;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
* Channels for creating appendEntries streams to a peer.
* <p>
* When there are no other channels, use only the main channel to create streams,
* which is shared with the other RPCs (requestVote, installSnapshot, etc.).
* Otherwise, use only the other channels, which are dedicated to appendEntries;
* each stream uses one channel and the new streams take the channels in turn.
* <p>
* All the channels, including the other channels, are owned by the caller,
* which is also responsible for shutting them down.
*/
class GrpcAppendEntriesChannels {
// The main channel; null if the other channels are used for appendEntries.
private final ManagedChannel mainChannel;
private final RaftServerProtocolServiceStub mainStub;
// The channels dedicated to appendEntries; null if the main channel is used for appendEntries.
private final List<ManagedChannel> otherChannels;
private final List<RaftServerProtocolServiceStub> otherStubs;
// The index of the channel in otherChannels for the next stream
private final AtomicInteger channelIndex = new AtomicInteger();

/**
* @param mainChannel the main channel, used for appendEntries only if there are no other channels;
* otherwise, it is ignored.
* @param otherChannels the channels dedicated to appendEntries;
* null means that there are no other channels, otherwise it must be non-empty.
*/
GrpcAppendEntriesChannels(ManagedChannel mainChannel, List<ManagedChannel> otherChannels) {
if (otherChannels == null) {
this.mainChannel = mainChannel;
this.mainStub = RaftServerProtocolServiceGrpc.newStub(mainChannel);
this.otherChannels = null;
this.otherStubs = null;
} else {
this.mainChannel = null;
this.mainStub = null;
Preconditions.assertTrue(!otherChannels.isEmpty(), "otherChannels is empty");
this.otherChannels = Collections.unmodifiableList(otherChannels);
this.otherStubs = Collections.unmodifiableList(otherChannels.stream()
.map(RaftServerProtocolServiceGrpc::newStub)
.collect(Collectors.toList()));
}
}

/** Create a new {@link CallStreamObserver} for appendEntries. */
CallStreamObserver<AppendEntriesRequestProto> newStream(StreamObserver<AppendEntriesReplyProto> responseHandler) {
final RaftServerProtocolServiceStub stub = otherStubs != null ? otherStubs.get(nextChannelIndex()) : mainStub;
return (CallStreamObserver<AppendEntriesRequestProto>) stub.appendEntries(responseHandler);
}

/**
* This method must be called only when there are other channels.

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.

This is an odd contract that the caller has to be aware of otherStubs is not null otherwise this call will hit an NPE.

The best way to avoid it is to make this nextChannelIndex as private thus only this class will use it and we know what we are doing. However I see that we need it in testing as well so have to keep this as package private. But then package private means other Ratis developer might still call it and cause unseen issue when testing is not enough or reviewers not having enough context.

I am wondering if we can return -1 when otherStubs is null? Though still not perfect, it avoids surprising NPE. Meanwhile negative values are often used to indicate something wrong in other languages like C++.

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.

Since the method is not expected to be called when otherStubs is null, throwing an exception is better. Here is some AI suggestion:

image image

@amaliujia amaliujia Nov 13, 2026 •

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.

Yep throw an exception other than NPE also works.

*
* @return the index in the other channels for a new stream.
*/
int nextChannelIndex() {
Preconditions.assertNotNull(otherStubs, "The method should not be called when otherStubs == null");
return channelIndex.getAndUpdate(n -> (n + 1) % otherStubs.size());
}

List<ManagedChannel> getAppendEntriesChannelsForTesting() {
return otherChannels != null ? otherChannels : Collections.singletonList(mainChannel);
}

List<ManagedChannel> getOtherChannelsForTesting() {
return otherChannels;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import org.apache.ratis.server.protocol.TermIndex;
import org.apache.ratis.server.raftlog.RaftLog;
import org.apache.ratis.server.util.ServerStringUtils;
import org.apache.ratis.thirdparty.com.google.common.annotations.VisibleForTesting;
import org.apache.ratis.thirdparty.io.grpc.StatusRuntimeException;
import org.apache.ratis.thirdparty.io.grpc.stub.CallStreamObserver;
import org.apache.ratis.thirdparty.io.grpc.stub.StreamObserver;
Expand Down Expand Up @@ -164,8 +163,6 @@ synchronized int process(Event event) {
private final TimeoutExecutor scheduler = TimeoutExecutor.getInstance();
@SuppressWarnings({"squid:S3077"}) // Suppress volatile for generic type
private volatile StreamObservers appendLogRequestObserver;
// the appendEntries channel of this appender, taken when it creates its first stream; guarded by the write lock
private volatile int appendEntriesIndex = -1;
private final boolean useSeparateHBChannel;

private final GrpcServerMetrics grpcServerMetrics;
Expand Down Expand Up @@ -392,10 +389,10 @@ static class StreamObservers {
private final TimeDuration waitForReady;
private volatile boolean running = true;

StreamObservers(GrpcServerProtocolClient client, int appendEntriesIndex, AppendLogResponseHandler handler,
StreamObservers(GrpcServerProtocolClient client, AppendLogResponseHandler handler,
boolean separateHeartbeat, TimeDuration waitTimeMin) {
this.appendLog = client.appendEntries(appendEntriesIndex, handler, false);
this.heartbeat = separateHeartbeat? client.appendEntries(appendEntriesIndex, handler, true): null;
this.appendLog = client.appendEntries(handler, false);
this.heartbeat = separateHeartbeat? client.appendEntries(handler, true): null;
this.waitForReady = waitTimeMin.isPositive()? waitTimeMin: TimeDuration.ONE_MILLISECOND;
}

Expand Down Expand Up @@ -425,11 +422,6 @@ void onCompleted() {
}
}

@VisibleForTesting
int getAppendEntriesIndex() {
return appendEntriesIndex;
}

@Override
public long getCallId() {
return callId.get();
Expand All @@ -456,11 +448,7 @@ void appendLog(boolean heartbeat) throws IOException {
notifyAppendEntriesListener(l -> l.onRequest(pending));
increaseNextIndex(pending);
if (appendLogRequestObserver == null) {
final GrpcServerProtocolClient client = getClient();
if (appendEntriesIndex < 0) {
appendEntriesIndex = client.nextAppendEntriesIndex();
}
appendLogRequestObserver = new StreamObservers(client, appendEntriesIndex,
appendLogRequestObserver = new StreamObservers(getClient(),
new AppendLogResponseHandler(), useSeparateHBChannel, getWaitTimeMin());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.apache.ratis.grpc.util.StreamObserverWithTimeout;
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.server.util.ServerStringUtils;
import org.apache.ratis.thirdparty.com.google.common.annotations.VisibleForTesting;
import org.apache.ratis.thirdparty.io.grpc.ManagedChannel;
import org.apache.ratis.thirdparty.io.grpc.netty.NegotiationType;
import org.apache.ratis.thirdparty.io.grpc.netty.NettyChannelBuilder;
Expand All @@ -43,7 +42,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* This is a RaftClient implementation that supports streaming data to the raft
Expand All @@ -52,18 +50,14 @@
class GrpcServerProtocolClient implements Closeable {
// Common channel
private final ManagedChannel channel;
private final GrpcStubPool<RaftServerProtocolServiceStub> pool;
// Channel and stub for heartbeat
private ManagedChannel hbChannel;
private RaftServerProtocolServiceStub hbAsyncStub;
private final RaftServerProtocolServiceStub asyncStub;
// Channels and stubs for appendEntries, each group always using the same one; the first is the main channel
private final List<ManagedChannel> appendEntriesChannels;
private final List<RaftServerProtocolServiceStub> appendEntriesStubs;
// The appendEntries channel for the next new log appender, so that the appenders take the channels in turn
private final AtomicInteger nextAppendEntriesIndex = new AtomicInteger();
private final RaftServerProtocolServiceStub hbAsyncStub;
// All the channels: the main channel, the other appendEntries channels and the heartbeat channel
private final List<ManagedChannel> allChannels;
// The appendEntries streams, using either the main channel or the other channels dedicated to appendEntries
private final GrpcAppendEntriesChannels appendEntriesStreams;
private final GrpcStubPool<RaftServerProtocolServiceStub> pool;
private final RaftServerProtocolServiceBlockingStub blockingStub;
private final boolean useSeparateHBChannel;

private final TimeDuration requestTimeoutDuration;
private static final Logger LOG = LoggerFactory.getLogger(GrpcServerProtocolClient.class);
Expand All @@ -77,28 +71,34 @@ class GrpcServerProtocolClient implements Closeable {
EventLoopGroup eventLoopGroup) {
raftPeerId = target.getId();
LOG.info("Build channel for {}", target);
useSeparateHBChannel = separateHBChannel;
this.eventLoopGroup = eventLoopGroup;
channel = buildChannel(target, flowControlWindow, sslContext);
blockingStub = RaftServerProtocolServiceGrpc.newBlockingStub(channel);
asyncStub = RaftServerProtocolServiceGrpc.newStub(channel);
final List<ManagedChannel> channels = new ArrayList<>(appendEntriesConnections);
final List<RaftServerProtocolServiceStub> stubs = new ArrayList<>(appendEntriesConnections);
channels.add(channel);
stubs.add(asyncStub);
for (int i = 1; i < appendEntriesConnections; i++) {
final ManagedChannel c = buildChannel(target, flowControlWindow, sslContext);
channels.add(c);
stubs.add(RaftServerProtocolServiceGrpc.newStub(c));

final List<ManagedChannel> all = new ArrayList<>();
all.add(channel);
List<ManagedChannel> otherChannels = null;
if (appendEntriesConnections > 1) {
otherChannels = new ArrayList<>();
for (int i = 0; i < appendEntriesConnections; i++) {
otherChannels.add(buildChannel(target, flowControlWindow, sslContext));
}
all.addAll(otherChannels);
}
appendEntriesChannels = Collections.unmodifiableList(channels);
appendEntriesStubs = Collections.unmodifiableList(stubs);
if (useSeparateHBChannel) {
hbChannel = buildChannel(target, flowControlWindow, sslContext);
this.appendEntriesStreams = new GrpcAppendEntriesChannels(channel, otherChannels);

// Channel and stub for heartbeat
if (separateHBChannel) {
final ManagedChannel hbChannel = buildChannel(target, flowControlWindow, sslContext);
all.add(hbChannel);
hbAsyncStub = RaftServerProtocolServiceGrpc.newStub(hbChannel);
} else {
hbAsyncStub = null;
}
requestTimeoutDuration = requestTimeout;
this.pool = connections == 1? null : newGrpcStubPool(target.getAddress(), sslContext, connections);
this.allChannels = Collections.unmodifiableList(all);
}

GrpcStubPool<RaftServerProtocolServiceStub> newGrpcStubPool(String address, SslContext sslContext, int connections) {
Expand Down Expand Up @@ -128,12 +128,7 @@ private ManagedChannel buildChannel(RaftPeer target, int flowControlWindow, SslC
@Override
public void close() {
LOG.info("{} Close channels", raftPeerId);
// the first one is the main channel
final List<ManagedChannel> channels = new ArrayList<>(appendEntriesChannels);
if (useSeparateHBChannel) {
channels.add(hbChannel);
}
GrpcUtil.shutdownManagedChannels(channels);
GrpcUtil.shutdownManagedChannels(allChannels);
if (pool != null) {
pool.close();
}
Expand Down Expand Up @@ -195,32 +190,25 @@ public void onCompleted() {
}
}

/** @param appendEntriesIndex the appendEntries channel, from {@link #nextAppendEntriesIndex()} */
CallStreamObserver<AppendEntriesRequestProto> appendEntries(int appendEntriesIndex,
/**
* Create a new appendEntries stream.
* A heartbeat stream uses the heartbeat channel if there is a separate one.
* Otherwise, the stream is created by {@link GrpcAppendEntriesChannels#newStream(StreamObserver)}.
*/
CallStreamObserver<AppendEntriesRequestProto> appendEntries(
StreamObserver<AppendEntriesReplyProto> responseHandler, boolean isHeartbeat) {
if (isHeartbeat && useSeparateHBChannel) {
if (isHeartbeat && hbAsyncStub != null) {
return (CallStreamObserver<AppendEntriesRequestProto>) hbAsyncStub.appendEntries(responseHandler);
} else {
final RaftServerProtocolServiceStub stub = appendEntriesStubs.get(appendEntriesIndex);
return (CallStreamObserver<AppendEntriesRequestProto>) stub.appendEntries(responseHandler);
return appendEntriesStreams.newStream(responseHandler);
}
}

/**
* @return the index of the appendEntries channel for a new log appender. The appenders take the channels in turn;
* an appender keeps its channel, so the appendEntries of a group to this peer use one channel.
*/
int nextAppendEntriesIndex() {
return Math.floorMod(nextAppendEntriesIndex.getAndIncrement(), appendEntriesStubs.size());
GrpcAppendEntriesChannels getAppendEntriesStreamsForTesting() {
return appendEntriesStreams;
}

@VisibleForTesting
List<ManagedChannel> getAppendEntriesChannels() {
return appendEntriesChannels;
}

@VisibleForTesting
ManagedChannel getMainChannel() {
ManagedChannel getMainChannelForTesting() {
return channel;
}

Expand All @@ -232,10 +220,6 @@ StreamObserver<InstallSnapshotRequestProto> installSnapshot(

// short-circuit the backoff timer and make them reconnect immediately.
public void resetConnectBackoff() {
if (useSeparateHBChannel) {
hbChannel.resetConnectBackoff();
}
// the first one is the main channel
appendEntriesChannels.forEach(ManagedChannel::resetConnectBackoff);
allChannels.forEach(ManagedChannel::resetConnectBackoff);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
import java.util.concurrent.TimeUnit;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
Expand Down Expand Up @@ -189,7 +188,7 @@ public void testStreamCreationFailure(boolean failProxy) throws Exception {
when(serverRpc.getProxies().getProxy(destination)).thenThrow(error);
} else {
error = new IllegalStateException("Failed to create append stream");
when(client.appendEntries(anyInt(), any(), eq(false))).thenThrow(error);
when(client.appendEntries(any(), eq(false))).thenThrow(error);
}
Assertions.assertSame(error, Assertions.assertThrows(Exception.class, () -> appender.appendLog(false)));
final InOrder order = inOrder(appendEntries);
Expand Down
Loading
Loading