Skip to content
Open
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
23 changes: 23 additions & 0 deletions ratis-docs/src/site/markdown/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,29 @@ if it fails to receive any RPC responses from this peer within this specified ti
| **Default** | false |


| **Property** | `raft.server.read.read-index.heartbeat.skip.enabled` |
Copy link
Contributor

@OneSizeFitsQuorum OneSizeFitsQuorum Jan 16, 2026

Choose a reason for hiding this comment

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

LGTM for the idea and code.

Just want to discuss whether heartbeat.skip.enabled is a good name, I think maybe raft.server.read.read-index.leadership-check.skip.enabled

Copy link
Contributor Author

@ivandika3 ivandika3 Jan 16, 2026

Choose a reason for hiding this comment

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

@OneSizeFitsQuorum Thanks for the review. I was also grappling with whether "leadership check" is a better name. However, my reasoning of choosing "heartbeat" is because we already have a LeaderStateImpl#checkLeadership to decide whether to leader need to step down due to lost majority heartbeats. LeaderStateImpl#checkLeadership does not send heartbeat, it simply checks the last RPC response time of the majority. Therefore, I picked "heartbeat" to explicitly mention the RPC that is going to be skipped.

That said, I'm ok if the community decide "leadership-check" is a more fitting name.

|:----------------|:--------------------------------------------------------------------|
| **Description** | whether to skip the leadership check heartbeat for read index. |
| **Type** | boolean |
| **Default** | false |

Note that although enabling `heartbeat.skip.enabled` reduce the RTT due to the leadership check heartbeat,
it causes reads to not be linearizable in some cases.
This is because without the leadership check heartbeats, the leader might not be the latest leader
and might serve stale reads.
There might be a small period of time when there is a split brain and two Raft peers that believe that
they are leaders (old leader and new leader where old leader's term < new leader's term).
The old leader might not have detected that it has lost majority heartbeats and stepped down or the old leader
has not received any RPC with higher term which forces the old leader to steps down.
Without the leadership check heartbeat which should detect that the old leader is no longer the latest leader and
force the old leader to step down, the old leader ReadIndex might return an index that is lower than
the new leader's applied index.
This means that the old leader might return stale data which is not linearizable by definition.
However, this situation should happen only abnormal situations when the Raft group encounters a leader election
due to network partition between the old leader and the other majority quorum (which will elect a new leader).
Therefore, this might be an acceptable tradeoff for applications that seek to improve the linearizable read
performance.

### Write - Configurations related to write requests.

* Limits on pending write requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,17 @@ static boolean appliedIndexEnabled(RaftProperties properties) {
static void setAppliedIndexEnabled(RaftProperties properties, boolean enabled) {
setBoolean(properties::setBoolean, APPLIED_INDEX_ENABLED_KEY, enabled);
}

String HEARTBEAT_SKIP_ENABLED_KEY = PREFIX + ".heartbeat.skip.enabled";
boolean HEARTBEAT_SKIP_ENABLED_DEFAULT = false;
static boolean heartbeatSkipEnabled(RaftProperties properties) {
return getBoolean(properties::getBoolean, HEARTBEAT_SKIP_ENABLED_KEY,
HEARTBEAT_SKIP_ENABLED_DEFAULT, getDefaultLog());
}

static void setHeartbeatSkipEnabled(RaftProperties properties, boolean enabled) {
setBoolean(properties::setBoolean, HEARTBEAT_SKIP_ENABLED_KEY, enabled);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ boolean isApplied() {

private final ReadIndexHeartbeats readIndexHeartbeats;
private final boolean readIndexAppliedIndexEnabled;
private final boolean readIndexHeartbeatSkipEnabled;
private final LeaderLease lease;

LeaderStateImpl(RaftServerImpl server) {
Expand Down Expand Up @@ -392,6 +393,8 @@ boolean isApplied() {
}
this.readIndexAppliedIndexEnabled = RaftServerConfigKeys.Read.ReadIndex
.appliedIndexEnabled(properties);
this.readIndexHeartbeatSkipEnabled = RaftServerConfigKeys.Read.ReadIndex
.heartbeatSkipEnabled(properties);

final RaftConfigurationImpl conf = state.getRaftConf();
Collection<RaftPeer> others = conf.getOtherPeers(server.getId());
Expand Down Expand Up @@ -1166,7 +1169,8 @@ CompletableFuture<Long> getReadIndex(Long readAfterWriteConsistentIndex) {
}

// if lease is enabled, check lease first
if (hasLease()) {
// if we allow leader to skip the leadership check heartbeat, we can return immediately
if (readIndexHeartbeatSkipEnabled || hasLease()) {
return CompletableFuture.completedFuture(readIndex);
}

Expand Down
Loading