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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ The project is structured as a multi-module Maven build, separating the core bro
- **Persistent Storage:** Custom Write-Ahead Log (WAL) and segment-based message storage ensure messages are durably persisted to disk. Features thread-safe, atomic consumer offset management with bounds locking designed to minimize data loss during concurrent background writes and handle shutdowns gracefully.
- **Graceful Teardown Coordination:** Orchestrated, safe termination of Netty EventLoops, RPC executors, and disk storage guaranteeing state integrity without resource leaks during node shutdowns.
- **High Performance:**
- **Client-Side Batching:** Producers feature high-throughput, latency-optimized message batching via a configurable `linger.ms` window. This groups thousands of messages into a single network round-trip and Raft log flush, massively increasing throughput.
- **Configurable Disk Durability:** By default, DRMQ guarantees strict flush-before-ack durability (`fsync`). However, administrators can explicitly disable this (`--log-segment-fsync false`) for extreme throughput scenarios where hardware page-cache flushing is acceptable.
- **Follower-based Reads:** Scalable read operations allowing consumers to fetch messages from follower nodes, distributing the load across the cluster.
- **Dedicated Thread Pools:** Separated executor services for Raft tasks and client handling prevent thread starvation and ensure consistent performance. Independent scheduler threads prevent I/O blocking from stunting cluster heartbeats.
- **Robust Client Ecosystem:** Includes a producer and consumer client with automatic reconnects and randomized bootstrap load balancing for seamless failover operations.
- **Robust Client Ecosystem:** Includes Java, Python, and TypeScript SDKs featuring automatic reconnects, randomized bootstrap load balancing, typed Error Code handling, and seamless leader failovers.
- **Metrics & Observability:** Integrated with Micrometer and Prometheus to provide deep visibility into broker health, log replication lag, and throughput metrics.

## Architecture & Modules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public static BrokerConfig fromArgs(String[] args) {
String metricsPath = "/metrics";
long logSegmentBytes = 100 * 1024 * 1024L; // 100MB
long logRetentionMs = 7L * 24 * 60 * 60 * 1000; // 7 days
long raftCompactThreshold = 1000L;
long raftCompactThreshold = 50000L; // Keep 50,000 entries to buffer followers during short outages
int maxDeliveries = 5;
String dlqTopicPrefix = "dlq.";
boolean logSegmentFsync = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ private String buildTelemetryPayload() {
localNode.addProperty("y", 200);
nodes.add(localNode);

// Peer nodes — real Raft state, honest about what we don't know
if (raftNode != null) {
int[] positions = { 0, 1 };
int i = 0;
Expand All @@ -306,15 +305,37 @@ private String buildTelemetryPayload() {
Long matchIdx = raftNode.getMatchIndexMap().get(peerId);
long peerApplied = matchIdx != null ? matchIdx : 0;

// Replication lag for this peer.
// If matchIndex is unconfirmed (null/0) and commitIndex is large,
// the new leader simply hasn't heard from this peer since election.
// Report -1 to signal "unreachable" rather than a fictitious huge lag.
long replicationLag;
long replicationLag = 0;
if ((matchIdx == null || matchIdx == 0) && commitIndex > 100) {
replicationLag = -1; // unreachable / unknown
} else {
replicationLag = Math.max(0, commitIndex - peerApplied);
long lagEntries = Math.max(0, commitIndex - peerApplied);
if (lagEntries == 0) {
replicationLag = 0;
} else if (raftNode.getRaftLog() != null && lagEntries <= 2000) {
// Calculate exact message lag by inspecting uncommitted Raft entries
long msgLag = 0;
for (long idx = peerApplied + 1; idx <= commitIndex; idx++) {
com.drmq.protocol.DRMQProtocol.RaftEntry entry = raftNode.getRaftLog().getEntry(idx);
if (entry != null && entry.getCommandType() == com.drmq.protocol.DRMQProtocol.RaftCommandType.BATCH_MESSAGE) {
try {
com.drmq.protocol.DRMQProtocol.ProduceBatchRequest batch =
com.drmq.protocol.DRMQProtocol.ProduceBatchRequest.parseFrom(entry.getPayload());
msgLag += batch.getEntriesCount();
} catch (Exception e) {
msgLag += 1; // Fallback
}
} else if (entry != null && entry.getCommandType() == com.drmq.protocol.DRMQProtocol.RaftCommandType.MESSAGE) {
msgLag += 1;
}
}
replicationLag = msgLag;
} else if (lagEntries > 2000) {
// Fallback estimate if lag is extremely large to avoid blocking telemetry thread
double avgBatchSize = currentProduceRecordRate > 0 ?
Math.max(1.0, currentProduceRecordRate / 10.0) : 50.0;
replicationLag = (long) (lagEntries * avgBatchSize);
}
}

peerNode.addProperty("id", peerId);
Expand Down
59 changes: 18 additions & 41 deletions drmq-broker/src/main/java/com/drmq/broker/raft/RaftNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ public class RaftNode {
private final Map<String, Function<InstallSnapshotRequest, InstallSnapshotResponse>> installSnapshotRpcHandlers = new ConcurrentHashMap<>();

private final ReentrantLock lock = new ReentrantLock();
// Using a cached thread pool or larger scheduled pool size
// We have 5 timed tasks (election, heartbeat, quorum check, proposal cleanup, state save).
// Given they are short-lived, 4 threads minimizes scheduling collision while preserving efficiency.
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(4);
private final ExecutorService raftExecutor;
private ScheduledFuture<?> electionTimer;
Expand Down Expand Up @@ -112,7 +109,6 @@ private static class ProposalState {
private final Map<String, Long> lastContactTime = new ConcurrentHashMap<>();
private static final long LOG_RATE_LIMIT_MS = 1000;

// Snapshot receive state
private long snapshotReceiveOffset = 0;
private java.io.OutputStream snapshotReceiveStream = null;
private Path snapshotTempFile = null;
Expand Down Expand Up @@ -152,9 +148,6 @@ public RaftNode(String nodeId, int port, List<PeerAddress> peers,
loadPersistentState();
}

/**
* Backward-compatible constructor using default compaction threshold (1000).
*/
public RaftNode(String nodeId, int port, List<PeerAddress> peers,
MessageStore messageStore, OffsetManager offsetManager, Path dataDir) throws IOException {
this(nodeId, port, peers, messageStore, offsetManager, dataDir, 1000L);
Expand Down Expand Up @@ -254,6 +247,11 @@ public void stop() {
logger.info("[{}] Raft node stopped", nodeId);
}

public RaftLog getRaftLog() {
return raftLog;
}



// Peer RPC Registration

Expand Down Expand Up @@ -346,13 +344,6 @@ private void startPreVote() {
AtomicLong votesReceived = new AtomicLong(1); // self-vote
AtomicBoolean electionStarted = new AtomicBoolean(false);

// Single-node cluster: self-vote alone satisfies quorum, start election immediately
if (votesReceived.get() >= votesNeeded) {
if (electionStarted.compareAndSet(false, true)) {
logger.info("[{}] Pre-vote succeeded (single-node quorum), starting real election", nodeId);
startElection(proposedTerm);
}
}

for (PeerAddress peer : peers) {
CompletableFuture.supplyAsync(() -> {
Expand Down Expand Up @@ -389,7 +380,6 @@ private void startPreVote() {
} finally {
lock.unlock();
}
// Release lock before startElection() to avoid holding it during disk I/O
if (shouldStartElection) {
startElection(proposedTerm);
}
Expand All @@ -399,10 +389,7 @@ private void startPreVote() {
resetElectionTimer();
}

/**
* Start a real election: transition to CANDIDATE, increment term, vote for self,
* request votes from peers. Only called after a successful pre-vote.
*/

private void startElection(long proposedTerm) {
if (!running) return;

Expand Down Expand Up @@ -447,19 +434,6 @@ private void startElection(long proposedTerm) {
AtomicLong votesReceived = new AtomicLong(1); // self-vote
AtomicBoolean electionWon = new AtomicBoolean(false);

// Single-node cluster: self-vote alone satisfies quorum, become leader immediately
if (votesReceived.get() >= votesNeeded) {
if (electionWon.compareAndSet(false, true)) {
lock.lock();
try {
if (currentTerm == myTerm && state == RaftState.CANDIDATE) {
becomeLeader();
}
} finally {
lock.unlock();
}
}
}

for (PeerAddress peer : peers) {
CompletableFuture.supplyAsync(() -> {
Expand Down Expand Up @@ -647,7 +621,7 @@ private void replicateTo(PeerAddress peer) {
sendInstallSnapshotToPeer(peer);
}
}
if (needsSnapshot) return;


long prevLogTerm = raftLog.getTermAt(prevLogIndex);
List<RaftEntry> entries = raftLog.getEntriesFrom(peerNextIndex);
Expand Down Expand Up @@ -856,7 +830,6 @@ private void applyCommitted() {
nodeId, lastApplied, entry.getTopic(), batchRequest.getEntriesCount());
}
default -> {
// MESSAGE (default): apply to MessageStore
long msgOffset = messageStore.append(
entry.getTopic(),
entry.getPayload().toByteArray(),
Expand Down Expand Up @@ -896,11 +869,10 @@ private void applyCommitted() {

if (applied) {
stateSaveNeeded = true;

// Keep at least 100x the compact threshold as a retention buffer

long retentionLimit = lastApplied - (raftCompactThreshold * 100);
long safeCompactIndex;

if (isLeader()) {
long minMatchIndex = lastApplied;
for (long idx : matchIndex.values()) {
Expand All @@ -910,15 +882,20 @@ private void applyCommitted() {
} else {
safeCompactIndex = retentionLimit;
}

// Always keep at least raftCompactThreshold entries after lastApplied

long finalCompactIndex = Math.min(safeCompactIndex, lastApplied - raftCompactThreshold);

if (finalCompactIndex > 0) {

long currentLogStart = raftLog.getStartIndex();
boolean compactionDue = finalCompactIndex > 0
&& (finalCompactIndex - currentLogStart) >= raftCompactThreshold;

if (compactionDue) {
if (isCompacting.compareAndSet(false, true)) {
raftExecutor.execute(() -> {
try {
raftLog.compact(finalCompactIndex);
logger.debug("[{}] Chunked compaction complete: log now starts at {}",
nodeId, finalCompactIndex + 1);
} catch (IOException e) {
logger.error("Failed to compact Raft log", e);
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class DRMQConsumer implements AutoCloseable {
private static final int DEFAULT_MAX_MESSAGES = 100;
private static final long DEFAULT_POLL_TIMEOUT_MS = 1000;
private static final int MAX_RETRIES = 5;
private static final long RECONNECT_DELAY_MS = 500; // Brief pause between retries to allow leader election
private static final long RECONNECT_DELAY_MS = 500;

private String host;
private int port;
Expand Down
2 changes: 1 addition & 1 deletion drmq-ts-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ export class DRMQProducer extends DRMQClient {
batch.forEach((pm, i) => pm.resolve({ success: true, offset: baseOffset + i }));
return;
} else {
const errorMsg = resp.errorMessage;
if (resp.errorCode === ErrorCode.NOT_LEADER) {
const errorMsg = resp.errorMessage;
const leaderAddr = errorMsg && errorMsg.startsWith("NOT_LEADER:")
? errorMsg.substring("NOT_LEADER:".length)
: "UNKNOWN";
Expand Down
41 changes: 0 additions & 41 deletions drmq-ts-client/src/example.ts

This file was deleted.

Loading