Skip to content

feat: adds EventBus and introduces Mpsc as alternative of disruptor - #1238

Draft
killme2008 wants to merge 4 commits into
masterfrom
feature/minor-change-read-state
Draft

feat: adds EventBus and introduces Mpsc as alternative of disruptor#1238
killme2008 wants to merge 4 commits into
masterfrom
feature/minor-change-read-state

Conversation

@killme2008

@killme2008 killme2008 commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

Motivation:

Explain the context, and why you're making that change.
To make others understand what is the problem you're trying to solve.

Modification:

Describe the idea and modifications you've done.

Result:

Fixes #1231

If there is no issue then describe the changes introduced by this PR.

Summary by CodeRabbit

  • New Features

    • Added configurable EventBus support with two modes (DISRUPTOR and MPSC) and public options in RaftOptions and FSMCallerOptions to choose behavior.
    • New default EventBus factory and selectable implementations to improve event/task delivery and shutdown handling.
  • Tests

    • Added comprehensive unit and integration tests validating both EventBus modes, options, batching, shutdown and cluster behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

Copilot AI left a comment

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.

Pull request overview

This PR introduces an EventBus abstraction layer to provide an alternative to the existing Disruptor-based event processing, addressing cross-generation reference issues in generational garbage collectors (Issue #1231). The implementation adds a new MPSC (Multi-Producer Single-Consumer) queue-based EventBus alongside the existing Disruptor implementation, wrapped behind a common interface.

Key Changes:

  • Introduces EventBus abstraction with two implementations: DisruptorEventBus (object reuse) and MpscEventBus (no object reuse)
  • Adds EventBusMode configuration option (DISRUPTOR/MPSC) to RaftOptions for selecting the implementation
  • Refactors LogManagerImpl, NodeImpl, FSMCallerImpl, and ReadOnlyServiceImpl to use the new EventBus abstraction
  • Provides comprehensive test coverage including parameterized tests across both implementations

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
EventBus.java Core interface defining the event bus contract with publish, shutdown, and capacity management methods
EventBusHandler.java Functional interface for event processing with endOfBatch signaling
EventBusOptions.java Configuration class for EventBus instances with fluent API and validation
EventBusMode.java Enum defining DISRUPTOR and MPSC modes with documentation on GC compatibility
EventBusFactory.java Factory for creating EventBus instances based on configured mode
WaitStrategyType.java Enum for consumer thread wait strategies (BLOCKING, TIMEOUT_BLOCKING)
DisruptorEventBus.java Disruptor-based implementation wrapping existing RingBuffer functionality
MpscEventBus.java New MPSC queue-based implementation using JCTools without object reuse
LogManagerImpl.java Refactored to use EventBus instead of direct Disruptor usage for disk operations
NodeImpl.java Refactored to use EventBus for log entry application queue
FSMCallerImpl.java Refactored to use EventBus for FSM task queue
ReadOnlyServiceImpl.java Refactored to use EventBus for read-index request queue
RaftOptions.java Added eventBusMode configuration field with DISRUPTOR as default
FSMCallerOptions.java Added eventBusMode field for FSMCaller configuration
LogManager.java Minor whitespace fix
EventBusTest.java Parameterized tests covering both implementations with concurrent scenarios
MpscEventBusTest.java MPSC-specific tests for maxBatchSize behavior and shutdown draining
EventBusOptionsTest.java Comprehensive tests for configuration options and validation
EventBusFactoryTest.java Tests for factory creation logic and mode selection
NodeEventBusModeTest.java Integration tests exercising Node operations with both EventBus modes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ThreadPoolsFactory.runInThread(this.node.getGroupId(), () -> {
final ReadIndexEvent event = new ReadIndexEvent();
event.shutdownLatch = this.shutdownLatch;
this.readIndexEventBus.publish(event);

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

Similar to LogManagerImpl, the shutdown event publish call lacks error handling. If readIndexEventBus.publish() fails, the shutdownLatch will never be counted down, causing join() to hang indefinitely. Consider checking the return value or implementing a timeout mechanism.

Copilot uses AI. Check for mistakes.
() -> this.applyQueue.publishEvent((event, sequence) -> event.shutdownLatch = latch));
final LogEntryAndClosure shutdownEvent = new LogEntryAndClosure();
shutdownEvent.shutdownLatch = latch;
ThreadPoolsFactory.runInThread(this.groupId, () -> this.applyEventBus.publish(shutdownEvent));

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The shutdown event publish call lacks error handling. If applyEventBus.publish() fails, the shutdownLatch will never be counted down, causing join() to hang indefinitely. Consider checking the return value or implementing a timeout mechanism.

Suggested change
ThreadPoolsFactory.runInThread(this.groupId, () -> this.applyEventBus.publish(shutdownEvent));
ThreadPoolsFactory.runInThread(this.groupId, () -> {
try {
this.applyEventBus.publish(shutdownEvent);
} catch (final Throwable t) {
// Ensure that shutdown does not hang if the shutdown event cannot be published.
LOG.error("Failed to publish shutdown event on applyEventBus for node {}.", this.nodeId, t);
latch.countDown();
final int num = GLOBAL_NUM_NODES.decrementAndGet();
LOG.info("The number of active nodes decrement to {}.", num);
}
});

Copilot uses AI. Check for mistakes.
task.type = TaskType.SHUTDOWN;
task.shutdownLatch = latch;
}));
this.taskEventBus.publish(task);

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The shutdown event publish call lacks error handling. If taskEventBus.publish() fails, the shutdownLatch will never be counted down, causing join() to hang indefinitely. Consider checking the return value or implementing a timeout mechanism.

Suggested change
this.taskEventBus.publish(task);
try {
final boolean published = this.taskEventBus.publish(task);
if (!published) {
LOG.error("Failed to publish SHUTDOWN task to taskEventBus, counting down latch directly.");
latch.countDown();
}
} catch (final Exception e) {
LOG.error("Exception while publishing SHUTDOWN task to taskEventBus, counting down latch directly.", e);
latch.countDown();
}

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
this.consumerThread = opts.getThreadFactory().newThread(() -> consumeLoop(handler));
this.consumerThread.start();

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The MpscEventBus constructor starts the consumer thread before the constructor completes (line 86), which could lead to the consumer thread accessing fields before they are fully initialized. While the current implementation may work due to the specific field initialization order, this pattern is generally unsafe. Consider deferring thread start until after the constructor completes, or ensure all fields are initialized before starting the thread.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +83
final int initialCapacity = Math.min(INITIAL_CAPACITY, this.bufferSize / 2);
this.queue = new MpscGrowableAtomicArrayQueue<>(Math.max(2, initialCapacity), this.bufferSize);

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The queue initialization at line 83 uses integer division for initialCapacity calculation. When bufferSize is small (e.g., 2 or 3), bufferSize / 2 will be 1 or 1 respectively, but the Math.max(2, initialCapacity) ensures at least 2. However, for bufferSize=1, this would result in initialCapacity=2 which is greater than maxCapacity=1, violating the MpscGrowableAtomicArrayQueue requirement that initialCapacity must be less than maxCapacity. Consider adding validation to ensure bufferSize is at least 2, or adjust the calculation to handle edge cases.

Suggested change
final int initialCapacity = Math.min(INITIAL_CAPACITY, this.bufferSize / 2);
this.queue = new MpscGrowableAtomicArrayQueue<>(Math.max(2, initialCapacity), this.bufferSize);
final int maxCapacity = this.bufferSize;
Requires.requireTrue(maxCapacity > 1, "bufferSize must be greater than 1");
int initialCapacity = Math.min(INITIAL_CAPACITY, maxCapacity / 2);
// Ensure initialCapacity is strictly less than maxCapacity
if (initialCapacity >= maxCapacity) {
initialCapacity = maxCapacity - 1;
}
// Prefer a minimum initial capacity of 2 when possible (and still < maxCapacity)
if (initialCapacity < 2 && maxCapacity >= 3) {
initialCapacity = 2;
}
this.queue = new MpscGrowableAtomicArrayQueue<>(initialCapacity, maxCapacity);

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +166
// Queue is empty
if (this.shutdown) {
// Shutdown requested and queue is empty, exit
return;
}
// Reset batch counter and wait for new events

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The shutdown logic has a potential race condition. The consumer thread checks this.shutdown at line 162 to exit when the queue is empty, but this check happens before the ShutdownEvent is processed. If the queue is empty when shutdown is called, the consumer might exit before seeing the ShutdownEvent, causing the shutdown latch to never be counted down. Consider checking for shutdown only after processing ShutdownEvent, or ensure the ShutdownEvent is added before setting the shutdown flag.

Suggested change
// Queue is empty
if (this.shutdown) {
// Shutdown requested and queue is empty, exit
return;
}
// Reset batch counter and wait for new events
// Queue is empty, reset batch counter and wait for new events

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +128
public void shutdown(final CountDownLatch latch) {
this.shutdown = true;
this.ringBuffer.publishEvent((wrapper, seq) -> wrapper.shutdownLatch = latch);
}

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The DisruptorEventBus shutdown method may block indefinitely if the RingBuffer is full. After setting this.shutdown = true, the call to publishEvent on line 127 will block if there's no capacity, but since shutdown is already set to true, any new publish attempts will fail (line 93-96), preventing the queue from draining. Consider using tryPublishEvent with a timeout or loop, or ensure there's always capacity for the shutdown event.

Copilot uses AI. Check for mistakes.
}));
final StableClosureEvent shutdownEvent = new StableClosureEvent();
shutdownEvent.type = EventType.SHUTDOWN;
ThreadPoolsFactory.runInThread(this.groupId, () -> this.diskEventBus.publish(shutdownEvent));

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

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

The shutdown event publish call lacks proper error handling. If diskEventBus.publish() fails (e.g., if the event bus is already shut down or the queue is full), the shutDownLatch will never be counted down, causing join() to hang indefinitely. Consider adding a check for the return value or using a timeout mechanism.

Suggested change
ThreadPoolsFactory.runInThread(this.groupId, () -> this.diskEventBus.publish(shutdownEvent));
ThreadPoolsFactory.runInThread(this.groupId, () -> {
final EventBus eventBus = this.diskEventBus;
if (eventBus == null) {
// Event bus already shut down or not available; avoid hanging join().
if (this.shutDownLatch != null) {
this.shutDownLatch.countDown();
}
return;
}
try {
final boolean published = eventBus.publish(shutdownEvent);
if (!published) {
// Failed to enqueue shutdown event; ensure join() does not block indefinitely.
LOG.warn("Failed to publish shutdown event for group {} because the event bus rejected the event.",
this.groupId);
if (this.shutDownLatch != null) {
this.shutDownLatch.countDown();
}
}
} catch (final Throwable t) {
// Any unexpected error while publishing the shutdown event should not prevent shutdown completion.
LOG.warn("Exception occurred while publishing shutdown event for group {}.", this.groupId, t);
if (this.shutDownLatch != null) {
this.shutDownLatch.countDown();
}
}
});

Copilot uses AI. Check for mistakes.
@coderabbitai

coderabbitai Bot commented Dec 18, 2025

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Replaces Disruptor-only task queues with a configurable EventBus abstraction (DISRUPTOR or MPSC). Adds EventBus core APIs, two implementations, options and factory, and migrates FSMCaller, Node, ReadOnlyService, and LogManager to publish/consume via EventBus instead of Disruptor.

Changes

Cohort / File(s) Summary
EventBus Core & Config
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBus.java, EventBusHandler.java, EventBusMode.java, EventBusOptions.java, EventBusFactory.java, WaitStrategyType.java
New generic EventBus abstraction, handler contract, modes (DISRUPTOR/MPSC), options (buffer/maxBatch/wait strategy), SPI factory interface, and wait-strategy enum.
EventBus Implementations & Factory
DisruptorEventBus.java, MpscEventBus.java, DefaultEventBusFactory.java,META-INF/services/...EventBusFactory
New Disruptor-backed and MPSC-backed concrete EventBus implementations; DefaultEventBusFactory selects implementation by mode; service loader entry added.
Raft & FSM Options
jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java, FSMCallerOptions.java
Add eventBusMode and eventBusFactory fields with getters/setters; copy/toString updated to propagate options.
FSM Caller & Node apply path
jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java, NodeImpl.java
Replace disruptor task queues with EventBus: construct EventBus via options/factory, publish ApplyTask/LogEntryAndClosure instances directly (no pooled reset), update handler interfaces to EventBusHandler with onEvent(event, endOfBatch). Shutdown/join and capacity checks migrated to EventBus APIs.
ReadOnlyService & LogManager disk queue
jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java, jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java
Replace disruptor-based readIndex/disk queues with EventBus equivalents; switch to non-reuse event creation, update handlers to EventBusHandler, adjust publishing, capacity checks, shutdown/drain logic, and related logging/metrics removal.
Storage small change
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/LogManager.java
Trailing newline added (formatting-only).
Tests - integration & unit
jraft-core/src/test/java/.../NodeEventBusModeTest.java, EventBusFactoryTest.java, EventBusOptionsTest.java, EventBusTest.java, MpscEventBusTest.java
New parameterized integration test exercising DISRUPTOR and MPSC modes; unit tests for EventBus implementations, options, factory behavior, publish/tryPublish/shutdown/capacity, batching and drain semantics.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Producer as Producer (FSM/Node/LogManager)
participant Bus as EventBus (Disruptor/MPSC)
participant Handler as EventBusHandler
participant Target as Component (FSM / Storage / ReadOnly)
Note right of Bus: mode selected via EventBusOptions/Factory
Producer->>Bus: publish(event)
alt publish accepted
Bus-->>Handler: deliver(event, endOfBatch)
Handler->>Target: process(event)
Target-->>Handler: ack / closure
else publish rejected (capacity/shutdown)
Bus-->>Producer: false / throw
end
Note over Bus,Handler: shutdown: publish SHUTDOWN sentinel -> Bus drains -> handler finishes

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding an EventBus abstraction and introducing MPSC as an alternative to Disruptor.
Linked Issues check ✅ Passed The PR addresses issue #1231 by replacing Disruptor object reuse with EventBus abstraction supporting both Disruptor and MPSC modes, eliminating per-operation allocations.
Out of Scope Changes check ✅ Passed All changes are scoped to EventBus infrastructure, configuration options, and affected core classes. No unrelated modifications found outside the stated objectives.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

♻️ Duplicate comments (7)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBus.java (2)

82-83: Critical: Queue initialization violates MpscGrowableAtomicArrayQueue requirements for bufferSize < 3.

The calculation can produce initialCapacity >= maxCapacity when bufferSize is 1 or 2. For example, with bufferSize=1: initialCapacity = Math.min(1024, 0) = 0, then Math.max(2, 0) = 2, resulting in initialCapacity=2 > maxCapacity=1. This violates the MpscGrowableAtomicArrayQueue requirement that initial capacity must be strictly less than max capacity.

🔎 Apply this diff to add validation and fix the edge case:
+        Requires.requireTrue(this.bufferSize >= 2, "bufferSize must be at least 2");
         // Use bounded MPSC queue
         // initialCapacity must be less than maxCapacity for MpscGrowableAtomicArrayQueue
         final int initialCapacity = Math.min(INITIAL_CAPACITY, this.bufferSize / 2);
-        this.queue = new MpscGrowableAtomicArrayQueue<>(Math.max(2, initialCapacity), this.bufferSize);
+        final int actualInitialCapacity = Math.min(Math.max(2, initialCapacity), this.bufferSize - 1);
+        this.queue = new MpscGrowableAtomicArrayQueue<>(actualInitialCapacity, this.bufferSize);

138-177: Critical: Shutdown race condition can prevent graceful termination.

There's a race between setting this.shutdown = true (line 140) and adding the ShutdownEvent to the queue (lines 142-144). If the consumer thread polls and sees an empty queue after shutdown is set but before the ShutdownEvent is enqueued, it will exit at line 162-164 without processing the ShutdownEvent, causing the shutdown latch to never be counted down.

🔎 Apply this diff to fix the race condition:
     @Override
     public void shutdown(final CountDownLatch latch) {
-        this.shutdown = true;
         // Spin until ShutdownEvent is successfully added
         while (!this.queue.offer(new ShutdownEvent(latch))) {
             LockSupport.parkNanos(SPIN_PARK_NANOS);
         }
+        this.shutdown = true;
         LockSupport.unpark(this.consumerThread);
     }

Or alternatively, modify the consumer logic:

             if (item == null) {
                 // Queue is empty
+                // Reset batch counter and wait for new events
+                processedInBatch = 0;
+                parkWait();
+                continue;
+            }
+
+            if (item instanceof ShutdownEvent) {
+                LOG.info("EventBus '{}' received shutdown signal, draining remaining events", this.name);
+                drainAndProcess(handler);
+                ((ShutdownEvent) item).latch.countDown();
+                return;
+            }
+
+            // Check shutdown after processing ShutdownEvent
+            if (this.shutdown) {
                 if (this.shutdown) {
                     // Shutdown requested and queue is empty, exit
                     return;
                 }
-                // Reset batch counter and wait for new events
-                processedInBatch = 0;
-                parkWait();
-                continue;
             }
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (1)

232-237: Major: Shutdown can hang indefinitely if event publish fails.

If diskEventBus.publish() returns false (e.g., bus already shut down or queue full) or throws an exception, the shutDownLatch will never be counted down, causing join() to hang indefinitely. The async execution via ThreadPoolsFactory.runInThread() makes this harder to detect.

🔎 Apply this diff to add error handling:
     private void stopDiskThread() {
         this.shutDownLatch = new CountDownLatch(1);
         final StableClosureEvent shutdownEvent = new StableClosureEvent();
         shutdownEvent.type = EventType.SHUTDOWN;
-        ThreadPoolsFactory.runInThread(this.groupId, () -> this.diskEventBus.publish(shutdownEvent));
+        ThreadPoolsFactory.runInThread(this.groupId, () -> {
+            try {
+                final boolean published = this.diskEventBus.publish(shutdownEvent);
+                if (!published) {
+                    LOG.warn("Failed to publish shutdown event for group {}, counting down latch anyway", this.groupId);
+                    this.shutDownLatch.countDown();
+                }
+            } catch (final Throwable t) {
+                LOG.error("Exception while publishing shutdown event for group {}, counting down latch", this.groupId, t);
+                this.shutDownLatch.countDown();
+            }
+        });
     }
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java (1)

124-128: Shutdown may block indefinitely if RingBuffer is full.

After setting this.shutdown = true, the publishEvent call on line 127 will block if there's no capacity. However, since publish() now returns false when shutdown is true (lines 93-96), no new events will drain the queue, creating a potential deadlock.

Consider using tryPublishEvent with retry/timeout logic, or ensure capacity exists before the blocking publish.

🔎 Suggested fix using tryPublishEvent with spin
     @Override
     public void shutdown(final CountDownLatch latch) {
         this.shutdown = true;
-        this.ringBuffer.publishEvent((wrapper, seq) -> wrapper.shutdownLatch = latch);
+        // Use tryPublishEvent to avoid blocking indefinitely if buffer is full
+        while (!this.ringBuffer.tryPublishEvent((wrapper, seq) -> wrapper.shutdownLatch = latch)) {
+            Thread.yield();
+        }
     }
jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java (1)

284-288: Shutdown publish lacks error handling — shutdownLatch may never count down.

If readIndexEventBus.publish(event) fails (returns false or throws), the shutdownLatch will never be counted down, causing join() to hang indefinitely.

🔎 Apply this diff to handle publish failures:
         ThreadPoolsFactory.runInThread(this.node.getGroupId(), () -> {
             final ReadIndexEvent event = new ReadIndexEvent();
             event.shutdownLatch = this.shutdownLatch;
-            this.readIndexEventBus.publish(event);
+            if (!this.readIndexEventBus.publish(event)) {
+                LOG.warn("Failed to publish shutdown event, counting down latch directly.");
+                this.shutdownLatch.countDown();
+            }
         });
jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (1)

197-202: Shutdown publish lacks error handling — shutdownLatch may never count down.

If taskEventBus.publish(task) fails, the shutdownLatch will never be counted down, causing join() to hang indefinitely.

🔎 Apply this diff to handle publish failures:
             ThreadPoolsFactory.runInThread(getNode().getGroupId(), () -> {
                 final ApplyTask task = new ApplyTask();
                 task.type = TaskType.SHUTDOWN;
                 task.shutdownLatch = latch;
-                this.taskEventBus.publish(task);
+                if (!this.taskEventBus.publish(task)) {
+                    LOG.error("Failed to publish SHUTDOWN task, counting down latch directly.");
+                    latch.countDown();
+                }
             });
jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (1)

2813-2818: Critical: Unaddressed past review - shutdown event publish lacks error handling.

This issue was previously flagged but remains unresolved. If applyEventBus.publish(shutdownEvent) fails or throws an exception, the shutdownLatch will never be counted down, causing join() at line 2905 to hang indefinitely.

🔎 Apply this fix to handle publish failures:
                 if (this.applyEventBus != null) {
                     final CountDownLatch latch = new CountDownLatch(1);
                     this.shutdownLatch = latch;
                     final LogEntryAndClosure shutdownEvent = new LogEntryAndClosure();
                     shutdownEvent.shutdownLatch = latch;
-                    ThreadPoolsFactory.runInThread(this.groupId, () -> this.applyEventBus.publish(shutdownEvent));
+                    ThreadPoolsFactory.runInThread(this.groupId, () -> {
+                        try {
+                            this.applyEventBus.publish(shutdownEvent);
+                        } catch (final Throwable t) {
+                            LOG.error("Failed to publish shutdown event for node {}.", getNodeId(), t);
+                            latch.countDown();
+                            final int num = GLOBAL_NUM_NODES.decrementAndGet();
+                            LOG.info("The number of active nodes decrement to {}.", num);
+                        }
+                    });
                 } else {
🧹 Nitpick comments (9)
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptionsTest.java (1)

192-194: Optional: Simplify assertion helper.

The private assertTrue() wrapper is unnecessary since you can use a static import or call org.junit.Assert.assertTrue() directly. This adds a minor indirection without clear benefit.

🔎 Consider this simpler approach:
+import static org.junit.Assert.assertTrue;
+
     @Test
     public void testToString() {
         final EventBusOptions opts = new EventBusOptions().setMode(EventBusMode.MPSC).setName("test-bus");

         final String str = opts.toString();
         assertNotNull(str);
         assertTrue(str.contains("MPSC"));
         assertTrue(str.contains("test-bus"));
     }
-
-    private void assertTrue(final boolean condition) {
-        org.junit.Assert.assertTrue(condition);
-    }
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java (1)

79-96: Consider asserting the await() result in testFactoryRespectsMode.

Lines 87 and 95 call latch.await() but don't assert the result, unlike lines 48 and 65 which do assert. This could mask test failures if shutdown doesn't complete in time.

🔎 Suggested fix
         CountDownLatch latch = new CountDownLatch(1);
         bus.shutdown(latch);
-        latch.await(5, TimeUnit.SECONDS);
+        assertTrue(latch.await(5, TimeUnit.SECONDS));

         // Test MPSC mode
         opts = new EventBusOptions().setMode(EventBusMode.MPSC);
         bus = EventBusFactory.create(opts, (e, b) -> {});
         assertTrue("Expected MpscEventBus for MPSC mode", bus instanceof MpscEventBus);
         latch = new CountDownLatch(1);
         bus.shutdown(latch);
-        latch.await(5, TimeUnit.SECONDS);
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBusTest.java (1)

159-169: Spin-wait without timeout could hang indefinitely.

The while loop on lines 166-168 spins until eventCount.get() >= 3 without any timeout. If the test fails for some reason, this will hang indefinitely rather than failing with a clear error.

🔎 Suggested fix with timeout
         // Wait for all events to be processed
-        while (eventCount.get() < 3) {
-            Thread.sleep(10);
+        long deadline = System.currentTimeMillis() + 5000;
+        while (eventCount.get() < 3) {
+            if (System.currentTimeMillis() > deadline) {
+                throw new AssertionError("Timeout waiting for events to be processed");
+            }
+            Thread.sleep(10);
         }
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (2)

165-166: Thread.sleep for synchronization can cause flaky tests.

Using Thread.sleep(100) to wait for "event processing to complete" is timing-dependent and may fail on slow CI environments. Consider using a more deterministic synchronization mechanism like a CountDownLatch that the handler counts down after setting the flag.


251-274: Consider ensuring blockingBus is properly shut down in all test paths.

If an assertion fails before the blockingBus.shutdown() call (e.g., line 265), the bus and its consumer thread may leak. The finally block only unblocks the consumer but doesn't shut down the bus if shutdown wasn't reached.

🔎 Suggested improvement:
         try {
             // ... test logic ...
         } finally {
             blockLatch.countDown(); // Ensure unblocked even if test fails
+            if (!blockingBus.isShutdown()) {
+                final CountDownLatch cleanupLatch = new CountDownLatch(1);
+                blockingBus.shutdown(cleanupLatch);
+                cleanupLatch.await(5, TimeUnit.SECONDS);
+            }
         }
jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java (1)

203-242: Consider using lambda or named Runnable instead of anonymous Thread subclass.

The anonymous Thread subclass pattern (new Thread() { @Override public void run() {...} }) is verbose. Using new Thread(() -> {...}) or extracting the logic would improve readability.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (2)

43-45: Default ThreadFactory uses initial name, won't reflect subsequent setName() calls.

The constructor creates a NamedThreadFactory using this.name + "-". If a user calls setName() after construction but before setThreadFactory(), the thread factory prefix won't match the updated name. This is a minor inconsistency.

🔎 Option 1: Lazy-initialize threadFactory in getter:
 public EventBusOptions() {
-    this.threadFactory = new NamedThreadFactory(this.name + "-", true);
+    // threadFactory will be lazily initialized in getThreadFactory() if not set
 }

 public ThreadFactory getThreadFactory() {
+    if (this.threadFactory == null) {
+        this.threadFactory = new NamedThreadFactory(this.name + "-", true);
+    }
     return this.threadFactory;
 }
🔎 Option 2: Document that setThreadFactory should be called after setName:

Add Javadoc noting the order dependency.


81-85: Consider enforcing power-of-2 validation for Disruptor mode's bufferSize.

The Javadoc notes that for Disruptor mode, bufferSize "must be power of 2", but the setter only validates bufferSize > 0. While the Disruptor implementation likely enforces this, validating here would provide earlier, clearer error messages.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (1)

1629-1630: Consider reformatting for readability.

The closure invocation is split across two lines in an inconsistent manner. Consider reformatting for better readability.

🔎 Suggested formatting improvement:
-            ThreadPoolsFactory.runClosureInThread(this.groupId, task.getDone(), new Status(RaftError.ENODESHUTDOWN,
-                "Node is shutting down."));
+            ThreadPoolsFactory.runClosureInThread(this.groupId, task.getDone(),
+                new Status(RaftError.ENODESHUTDOWN, "Node is shutting down."));
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f56081 and bb4715e.

📒 Files selected for processing (20)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (7 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (9 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java (10 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/option/FSMCallerOptions.java (3 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java (5 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/storage/LogManager.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (10 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBus.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusHandler.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusMode.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBus.java (1 hunks)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/WaitStrategyType.java (1 hunks)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java (1 hunks)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java (1 hunks)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptionsTest.java (1 hunks)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (1 hunks)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBusTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java (1)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)
  • EventBusFactory (26-52)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java (1)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/DisruptorBuilder.java (1)
  • DisruptorBuilder (32-98)
jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (2)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)
  • EventBusFactory (26-52)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)
  • EventBusOptions (29-192)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/NamedThreadFactory.java (1)
  • NamedThreadFactory (32-70)
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (2)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)
  • EventBusFactory (26-52)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)
  • EventBusOptions (29-192)
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (2)
jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java (1)
  • RunWith (67-369)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)
  • EventBusFactory (26-52)
🔇 Additional comments (39)
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/LogManager.java (1)

249-249: Formatting change approved.

This is a cosmetic addition of a trailing newline at the end of the file. No functional or API changes are present.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/WaitStrategyType.java (1)

24-41: LGTM! Clear and well-documented enum.

The enum constants are well-defined with comprehensive Javadoc explaining their behavior in both Disruptor and MPSC contexts. This provides clear guidance for users selecting a wait strategy.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBus.java (2)

92-120: LGTM! Publish methods correctly handle blocking and non-blocking semantics.

The publish() method properly spins with backpressure when the queue is full, while tryPublish() provides a non-blocking alternative. Both correctly unpark the consumer thread to ensure timely processing.


153-223: LGTM! Event consumption logic correctly implements batching and error handling.

The consumer loop properly tracks batch boundaries using queue emptiness and max batch size, and handles exceptions without crashing the consumer thread. The drain logic during shutdown correctly ignores additional ShutdownEvent instances.

jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (2)

197-206: LGTM! EventBus initialization properly configured.

The EventBus is correctly initialized with options from RaftOptions, including the configurable mode, buffer size, wait strategy, and timeout. The factory pattern cleanly abstracts the implementation selection.


498-571: LGTM! EventBusHandler implementation correctly handles batching and shutdown.

The handler properly implements the EventBusHandler<StableClosureEvent> interface, respects batch boundaries for efficiency, and correctly handles shutdown by flushing pending work before counting down the latch.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusMode.java (1)

25-52: LGTM! Well-documented enum with clear GC guidance.

The enum provides excellent documentation explaining when to use each mode based on GC characteristics. The guidance clearly addresses the cross-generation reference issue mentioned in #1231, helping users make informed configuration choices.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)

26-52: LGTM! Clean factory implementation with proper validation.

The factory correctly validates inputs and uses a switch statement to instantiate the appropriate EventBus implementation based on the configured mode. The default case properly handles unknown modes with a clear exception.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusHandler.java (1)

25-41: LGTM! Well-designed functional interface with clear batch semantics.

The interface provides a clean contract for event processing with explicit batch boundary signaling. The Javadoc clearly explains when endOfBatch is true, enabling handlers to optimize batch operations.

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptionsTest.java (1)

35-190: LGTM! Comprehensive test coverage for EventBusOptions.

The tests thoroughly validate all configuration options including default values, setters, boundary conditions, null checks, and fluent API chaining. The use of expected exceptions for negative test cases is appropriate.

jraft-core/src/main/java/com/alipay/sofa/jraft/option/FSMCallerOptions.java (1)

45-64: LGTM! EventBusMode property properly integrated.

The new eventBusMode property with a default value of DISRUPTOR maintains backward compatibility while enabling users to opt into the MPSC mode for generational GC environments. The getter/setter follow the existing pattern in the class.

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java (3)

34-49: LGTM!

Good test coverage for DisruptorEventBus creation with proper type assertion and graceful shutdown handling.


51-66: LGTM!

Good test coverage for MpscEventBus creation with proper type assertion and graceful shutdown handling.


68-77: LGTM!

Null parameter validation tests are correctly implemented using JUnit's expected attribute.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBus.java (1)

31-89: LGTM!

Well-designed interface with clear API documentation. The separation between blocking (publish) and non-blocking (tryPublish) operations is appropriate, and the CountDownLatch-based shutdown coordination provides a clean synchronization mechanism.

jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java (3)

116-128: LGTM!

Excellent documentation explaining the trade-offs between DISRUPTOR and MPSC modes with respect to GC behavior. The default value of DISRUPTOR maintains backward compatibility.


137-143: LGTM!

Standard getter/setter pattern consistent with other fields in the class.


331-331: LGTM!

The copy() method correctly propagates the new eventBusMode field.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java (2)

57-65: LGTM!

The EventWrapper class with reset() method properly supports object reuse and reference clearing for GC-friendliness.


156-172: LGTM!

The InternalHandler correctly adapts the Disruptor event model to EventBusHandler, handles shutdown gracefully by counting down the latch and stopping the disruptor, and ensures wrapper cleanup in the finally block.

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBusTest.java (3)

40-47: LGTM!

Proper cleanup in @After with null check and shutdown handling.


52-98: LGTM!

Good test for batch size triggering behavior. The assertion on line 95-96 correctly accounts for potential additional endOfBatch=true occurrences when the queue empties.


183-216: LGTM!

Excellent test for drain-on-shutdown behavior. The test correctly verifies that all 50 events are processed even when shutdown is called while events are still queued, with appropriate timeout on the shutdown latch.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java (2)

195-196: Good addition: Clearing state references helps reduce cross-generation GC pressure.

Explicitly clearing the states list after processing ensures that completed ReadIndexState objects can be collected sooner, addressing the GC pressure issue described in Issue #1231.


255-260: LGTM: EventBus initialization is properly configured.

The initialization correctly sources the mode from raftOptions.getEventBusMode(), sets an appropriate buffer size, and uses a descriptive thread factory name.

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (1)

176-212: Good concurrency test coverage for multi-threaded publish scenarios.

This test properly validates thread safety by using multiple producer threads with proper synchronization via CountDownLatch. The assertions verify that all published events are received.

jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java (2)

122-174: Good test coverage for basic cluster operations with both EventBus modes.

This test validates that the fundamental apply and TaskClosure semantics work identically under both DISRUPTOR and MPSC modes, which is essential for ensuring the new abstraction maintains compatibility.


301-352: LGTM: Good stress test for concurrent apply scenarios.

This test validates that both EventBus implementations can handle high-concurrency task submissions (10 threads × 100 tasks) without failures, which is critical for production workloads.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (3)

118-135: LGTM: Handler correctly implements EventBusHandler with preserved batching semantics.

The ApplyTaskHandler properly implements the new EventBusHandler interface while maintaining the maxCommittedIndex batching optimization for committed tasks. The fsmThread assignment is safe since there's only one consumer thread.


176-179: LGTM: EventBus initialization properly configured from FSMCallerOptions.


229-234: Consider the GC implications of allocating new ApplyTask per operation.

Each call to onCommitted, onSnapshotLoad, etc., allocates a new ApplyTask(). While this simplifies the code (no translator/factory pattern), it may still create cross-generation references if these tasks reference older objects.

For MPSC mode, this is expected since there's no object reuse. For DISRUPTOR mode, the original Disruptor pattern pre-allocated events in the RingBuffer and used translators to populate them, avoiding allocations. This change trades that optimization for simpler code.

Is this allocation pattern intentional? The PR objective mentions eliminating "new" allocations to reduce GC pressure.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)

29-191: Well-designed configuration class with fluent API and sensible defaults.

The EventBusOptions class provides a clean configuration interface with:

  • Sensible defaults (16K buffer, 32 batch size, blocking wait strategy)
  • Fluent setters for chaining
  • Validation on all setters
  • Informative toString() for debugging
jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (7)

123-128: LGTM - EventBus imports added.

The new imports for EventBus infrastructure are correctly added and all are used in the refactored code.


209-210: LGTM - Core abstraction change from Disruptor to EventBus.

The field type and documentation have been correctly updated to reflect the new EventBus-based architecture.


274-297: LGTM - Handler correctly adapted to EventBus interface.

The handler now implements EventBusHandler<LogEntryAndClosure> with the updated signature. The use of tasks.clear() is appropriate for the EventBus abstraction, which may use different object lifecycle strategies depending on the mode (Disruptor vs MPSC).


750-750: LGTM - EventBus mode propagated to FSMCaller.

The eventBusMode configuration is correctly propagated from RaftOptions to FSMCallerOptions, ensuring consistent EventBus behavior across components.


970-976: LGTM - EventBus initialization correctly configured.

The EventBus is properly initialized with appropriate parameters:

  • Mode from RaftOptions for flexibility
  • Buffer size and batch size from existing Raft options for consistency
  • Blocking wait strategy suitable for the apply path
  • Daemon threads for proper lifecycle management

1638-1661: LGTM - Apply method correctly refactored for EventBus.

The apply() method now:

  • Creates LogEntryAndClosure events directly (appropriate for both MPSC and Disruptor modes)
  • Handles blocking and non-blocking publish modes
  • Properly handles overload conditions in non-blocking mode

The object allocation pattern is intentional: MPSC mode avoids object reuse to prevent cross-generation references (the main goal of this PR), while Disruptor mode can handle reuse internally if needed.


2905-2907: LGTM - EventBus cleanup in join().

The EventBus reference is properly nulled out after the shutdown latch completes, ensuring clean resource release.

Comment on lines +314 to +318
final ReadIndexEvent event = new ReadIndexEvent();
event.readOnlyOptions = readOnlyOptions;
event.done = closure;
event.requestContext = new Bytes(reqCtx);
event.startTime = Utils.monotonicMs();

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "ReadOnlyServiceImpl.java" | head -5

Repository: sofastack/sofa-jraft

Length of output: 142


🏁 Script executed:

head -100 ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java

Repository: sofastack/sofa-jraft

Length of output: 4426


🏁 Script executed:

sed -n '300,330p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java

Repository: sofastack/sofa-jraft

Length of output: 1611


🏁 Script executed:

sed -n '50,120p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java

Repository: sofastack/sofa-jraft

Length of output: 3077


NewReadIndexEvent allocations per request defeat the GC optimization goal when using EventBus instead of Disruptor.

Lines 314 and 317 allocate new objects on every addRequest call:

  • new ReadIndexEvent() creates a fresh event object
  • new Bytes(reqCtx) creates a fresh wrapper object

Disruptor pre-allocates event objects in the ring buffer during construction and reuses them by allowing callers to modify fields on the stored objects. EventBus, by contrast, uses a thread pool to reuse threads but does not provide pre-allocated event object pooling.

If the PR migrated ReadOnlyService from Disruptor to EventBus to address Issue #1231's GC pressure concerns, this implementation does not achieve that goal without explicit event object pooling. The handler at lines 98-117 only batches event processing; it does not reuse event objects across requests.

Consider implementing an object pool for ReadIndexEvent or reverting to Disruptor's event reuse pattern for the ReadOnlyService queue.

🤖 Prompt for AI Agents
In jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java
around lines 314 to 318, new ReadIndexEvent() and new Bytes(reqCtx) are
allocated per addRequest which defeats the GC optimization when using EventBus;
change addRequest to borrow a reusable ReadIndexEvent (and reusable Bytes
wrapper) from a thread-safe object pool preallocated at service construction,
set its fields (readOnlyOptions, done, requestContext, startTime), and change
the event handler (lines ~98-117) to reset/clear the event fields and return the
event (and wrapper) to the pool after processing; ensure the pool is
thread-safe, prefilled to a reasonable size, defines a clear policy on
exhaustion (block, grow, or drop with fallback allocation), and that returned
events are safely reset to avoid leaking state between requests.

Comment on lines +350 to +353
final StableClosureEvent event = new StableClosureEvent();
event.type = EventType.OTHER;
event.done = done;
this.diskEventBus.publish(event);

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the event bus implementations
find . -type f -name "*.java" | xargs grep -l "class DisruptorEventBus\|class MpscEventBus" | head -20

Repository: sofastack/sofa-jraft

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Read DisruptorEventBus to check publish() method
echo "=== DisruptorEventBus ==="
cat ./jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java

echo -e "\n\n=== MpscEventBus ==="
cat ./jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/MpscEventBus.java

Repository: sofastack/sofa-jraft

Length of output: 14325


🏁 Script executed:

#!/bin/bash
# Read LogManagerImpl around lines 340-380 to see context and error handling
sed -n '340,380p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java

Repository: sofastack/sofa-jraft

Length of output: 1302


Check publish() return value to prevent silent closure failures.

Both DisruptorEventBus.publish() and MpscEventBus.publish() can return false if the event bus is shut down. At lines 350-353 in the appendEntries method and lines 374-377 in the offerEvent method, the return values are ignored. If publish() fails and returns false, the closure callback will never be invoked, causing a silent failure where the caller never receives a response. Even in offerEvent where this.stopped is checked beforehand, a race condition exists if shutdown occurs between the check and the publish call. Add return value checks or error handling to ensure closures are always invoked on failure.

🤖 Prompt for AI Agents
In
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java
around lines 350-353 and 374-377, the calls to this.diskEventBus.publish(...)
ignore the boolean return value and can silently drop events during shutdown;
change both sites to capture the publish result and, if publish returns false,
immediately invoke the provided closure (done) with a failure/aborted status and
log the condition so the caller is notified; do the same even when this.stopped
is true (to cover the race) — i.e., check the boolean return, call the closure
with an appropriate error Status and return, and add a small debug/error log
explaining that publish failed due to event bus shutdown.

Comment on lines +91 to +99
@Override
public boolean publish(final T event) {
if (this.shutdown) {
LOG.warn("EventBus '{}' is shutdown, cannot publish event", this.name);
return false;
}
this.ringBuffer.publishEvent((wrapper, seq) -> wrapper.event = event);
return true;
}

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.

⚠️ Potential issue | 🟡 Minor

Minor race between shutdown check and publish.

There's a small window where shutdown could be set to true after the check on line 93 but before publishEvent on line 97. This could result in an event being published after the shutdown signal. In practice, this is likely benign since the event will still be processed before the shutdown event, but it's worth noting for correctness.

🤖 Prompt for AI Agents
In
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DisruptorEventBus.java
around lines 91 to 99, there's a race where shutdown can flip true after the
initial check but before publishEvent, allowing publishes after shutdown; wrap
the shutdown-check and publishEvent call in a single atomic guarded section (for
example use a private final lock object and synchronize the check + publish, or
use an AtomicBoolean and compare-and-act) so you check shutdown inside the same
guard that performs ringBuffer.publishEvent; return false if shutdown is
observed inside the guard.

@dreamlike-ocean

Copy link
Copy Markdown

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java
我阅读代码发现这里的实现写死了两个实现类
请问这里可以支持java spi使得用户可以使用自定的EventBus实现类吗?

I was reading the code and noticed that the implementation here hardcodes two concrete classes.
Could this be extended to support Java SPI so that users can plug in their own custom EventBus implementation?

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
@killme2008
killme2008 marked this pull request as draft December 23, 2025 09:24

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (7)
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (3)

232-237: Shutdown publish lacks error handling.

The publish() return value is ignored. If the EventBus is already shut down or the queue is full, the shutdown event won't be processed, and shutDownLatch will never count down, causing join() to hang indefinitely.

🔎 Proposed fix
     private void stopDiskThread() {
         this.shutDownLatch = new CountDownLatch(1);
         final StableClosureEvent shutdownEvent = new StableClosureEvent();
         shutdownEvent.type = EventType.SHUTDOWN;
-        ThreadPoolsFactory.runInThread(this.groupId, () -> this.diskEventBus.publish(shutdownEvent));
+        ThreadPoolsFactory.runInThread(this.groupId, () -> {
+            if (!this.diskEventBus.publish(shutdownEvent)) {
+                LOG.warn("Failed to publish shutdown event, counting down latch directly.");
+                this.shutDownLatch.countDown();
+            }
+        });
     }

350-353: Check publish() return value to prevent silent failures.

Both appendEntries and offerEvent ignore the boolean return from diskEventBus.publish(). If publish fails, the closure callback will never be invoked, causing silent failures where the caller never receives a response.

🔎 Proposed fix
             // publish event out of lock
             final StableClosureEvent event = new StableClosureEvent();
             event.type = EventType.OTHER;
             event.done = done;
-            this.diskEventBus.publish(event);
+            if (!this.diskEventBus.publish(event)) {
+                ThreadPoolsFactory.runClosureInThread(this.groupId, done,
+                    new Status(RaftError.EBUSY, "Failed to publish event to disk EventBus"));
+            }

374-377: Same issue: publish() return value ignored in offerEvent.

If publish fails after the stopped check passes (race condition), the closure won't be invoked.

🔎 Proposed fix
         final StableClosureEvent event = new StableClosureEvent();
         event.type = type;
         event.done = done;
-        this.diskEventBus.publish(event);
+        if (!this.diskEventBus.publish(event)) {
+            ThreadPoolsFactory.runClosureInThread(this.groupId, done,
+                new Status(RaftError.EBUSY, "Failed to publish event to disk EventBus"));
+        }
     }
jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java (2)

283-287: Shutdown publish lacks error handling.

Similar to other components, if readIndexEventBus.publish() fails, the shutdownLatch will never count down, causing join() to hang.


313-318: New object allocations per request may not fully address GC optimization goal.

Each addRequest creates a new ReadIndexEvent and new Bytes(reqCtx). The PR objective (Issue #1231) was to reduce GC pressure from cross-generation references in Disruptor by avoiding new allocations. Without object pooling, this EventBus migration doesn't achieve the GC optimization goal—it just moves allocations from Disruptor's translator to the caller side.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (1)

196-201: Shutdown publish lacks error handling.

If taskEventBus.publish(task) fails (returns false), the shutdown latch will never be counted down, causing join() to hang indefinitely. This is a common pattern issue across all EventBus usages in this PR.

🔎 Proposed fix
             ThreadPoolsFactory.runInThread(getNode().getGroupId(), () -> {
                 final ApplyTask task = new ApplyTask();
                 task.type = TaskType.SHUTDOWN;
                 task.shutdownLatch = latch;
-                this.taskEventBus.publish(task);
+                if (!this.taskEventBus.publish(task)) {
+                    LOG.warn("Failed to publish SHUTDOWN task, counting down latch directly.");
+                    latch.countDown();
+                }
             });
jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (1)

2814-2819: Add error handling for shutdown event publication to prevent hang.

If applyEventBus.publish(shutdownEvent) fails or throws an exception, the shutdownLatch will never be counted down, causing join() at line 2906 to hang indefinitely. This is critical for graceful shutdown.

🔎 Recommended fix
 if (this.applyEventBus != null) {
     final CountDownLatch latch = new CountDownLatch(1);
     this.shutdownLatch = latch;
     final LogEntryAndClosure shutdownEvent = new LogEntryAndClosure();
     shutdownEvent.shutdownLatch = latch;
-    ThreadPoolsFactory.runInThread(this.groupId, () -> this.applyEventBus.publish(shutdownEvent));
+    ThreadPoolsFactory.runInThread(this.groupId, () -> {
+        try {
+            this.applyEventBus.publish(shutdownEvent);
+        } catch (final Throwable t) {
+            LOG.error("Failed to publish shutdown event on applyEventBus for node {}.", this.nodeId, t);
+            latch.countDown();
+            final int num = GLOBAL_NUM_NODES.decrementAndGet();
+            LOG.info("The number of active nodes decrement to {}.", num);
+        }
+    });
 }
🧹 Nitpick comments (3)
jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (1)

166-174: Consider using awaitility instead of Thread.sleep for timing-sensitive assertions.

Thread.sleep(100) in tests can be flaky on slow CI machines. Consider using a polling mechanism or the Awaitility library for more robust timing verification.

jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java (2)

159-161: Consider adding null validation in setters.

The setters for eventBusMode and eventBusFactory accept null values without validation. If null is passed, it could lead to NullPointerException at runtime when these values are used.

🔎 Proposed validation
 public void setEventBusMode(final EventBusMode eventBusMode) {
+    Requires.requireNonNull(eventBusMode, "eventBusMode");
     this.eventBusMode = eventBusMode;
 }
 public void setEventBusFactory(final EventBusFactory eventBusFactory) {
+    Requires.requireNonNull(eventBusFactory, "eventBusFactory");
     this.eventBusFactory = eventBusFactory;
 }

373-373: Consider including eventBusFactory in toString() for better debugging.

The toString() method includes eventBusMode but omits eventBusFactory. Including the factory (even just its class name) could help with debugging configuration issues.

🔎 Proposed enhancement
-               + ", startUpOldStorage=" + startupOldStorage + ", eventBusMode=" + eventBusMode + '}';
+               + ", startUpOldStorage=" + startupOldStorage + ", eventBusMode=" + eventBusMode 
+               + ", eventBusFactory=" + (eventBusFactory != null ? eventBusFactory.getClass().getSimpleName() : "null") + '}';
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb4715e and fc95cd1.

📒 Files selected for processing (12)
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/option/FSMCallerOptions.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DefaultEventBusFactory.java
  • jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java
  • jraft-core/src/main/resources/META-INF/services/com.alipay.sofa.jraft.util.concurrent.EventBusFactory
  • jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java
  • jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java
✅ Files skipped from review due to trivial changes (1)
  • jraft-core/src/main/resources/META-INF/services/com.alipay.sofa.jraft.util.concurrent.EventBusFactory
🚧 Files skipped from review as they are similar to previous changes (1)
  • jraft-core/src/test/java/com/alipay/sofa/jraft/core/NodeEventBusModeTest.java
🧰 Additional context used
🧬 Code graph analysis (4)
jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java (2)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/JRaftServiceLoader.java (1)
  • JRaftServiceLoader (42-357)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/RpcFactoryHelper.java (1)
  • RpcFactoryHelper (25-37)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DefaultEventBusFactory.java (1)
  • SPI (30-47)
jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (3)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)
  • EventBusOptions (29-192)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/LongHeldDetectingReadWriteLock.java (1)
  • LongHeldDetectingReadWriteLock (31-153)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/ThreadPoolsFactory.java (1)
  • ThreadPoolsFactory (38-114)
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (2)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusOptions.java (1)
  • EventBusOptions (29-192)
jraft-core/src/main/java/com/alipay/sofa/jraft/util/ThreadPoolsFactory.java (1)
  • ThreadPoolsFactory (38-114)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: test_rpc_grpc_impl
  • GitHub Check: test_rheakv_core
  • GitHub Check: test_rheakv_pd
  • GitHub Check: test_jraft_core
  • GitHub Check: sca
🔇 Additional comments (21)
jraft-core/src/main/java/com/alipay/sofa/jraft/storage/impl/LogManagerImpl.java (2)

195-206: EventBus initialization looks correct.

The initialization properly configures the EventBus with mode from RaftOptions, buffer size, thread factory, and timeout-based wait strategy. The factory pattern allows for SPI-based customization as intended.


498-505: Handler correctly implements EventBusHandler interface.

The signature change from onEvent(event, sequence, endOfBatch) to onEvent(event, endOfBatch) aligns with the new EventBusHandler contract. The logic is preserved correctly.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactory.java (1)

34-46: Clean SPI interface design.

The interface is well-documented, supports both SPI-based discovery and direct injection via RaftOptions.setEventBusFactory(), and addresses the extensibility concern raised in the PR comments about supporting custom implementations.

jraft-core/src/main/java/com/alipay/sofa/jraft/util/concurrent/DefaultEventBusFactory.java (1)

30-47: Factory implementation is correct and follows best practices.

Proper null validation with Requires.requireNonNull, exhaustive switch handling with a defensive default case, and @SPI(priority = 0) ensures this is the default implementation while allowing higher-priority custom factories to override it.

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusFactoryTest.java (1)

39-208: Comprehensive test coverage for EventBusFactory.

The test suite covers:

  • Both bus modes (DISRUPTOR, MPSC)
  • Null argument validation
  • SPI loading mechanism
  • Custom factory injection
  • RaftOptions integration and copy semantics

All tests properly clean up by shutting down EventBus instances, preventing resource leaks.

jraft-core/src/main/java/com/alipay/sofa/jraft/option/FSMCallerOptions.java (1)

46-77: Configuration options correctly extended for EventBus support.

The new eventBusMode with DISRUPTOR default ensures backward compatibility. The eventBusFactory field allows callers to inject custom factories. Standard getter/setter pattern is consistent with existing options.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/ReadOnlyServiceImpl.java (2)

253-259: EventBus initialization is correct.

Configuration properly uses RaftOptions for mode, buffer size, and creates a dedicated thread factory. The factory pattern correctly delegates to the configured EventBusFactory.


194-196: Good addition of explicit collection clearing for GC.

The states.clear() calls after processing help break reference chains and allow earlier garbage collection, directly addressing part of Issue #1231's concerns about long-lived objects holding references to newly allocated objects.

Also applies to: 426-428, 443-445

jraft-core/src/test/java/com/alipay/sofa/jraft/util/concurrent/EventBusTest.java (1)

42-277: Comprehensive parameterized test suite for EventBus implementations.

Excellent coverage testing both DisruptorEventBus and MpscEventBus implementations for:

  • Basic publish/consume semantics
  • Non-blocking tryPublish
  • Shutdown lifecycle and post-shutdown behavior
  • Capacity and buffer size reporting
  • Concurrent publishing from multiple threads
  • Pending event counting

The parameterized approach ensures both implementations conform to the same contract.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java (2)

174-178: EventBus initialization is properly configured.

Uses mode and buffer size from FSMCallerOptions, with a dedicated named thread factory for the consumer thread.


117-126: Handler implementation correctly adapted for EventBusHandler.

The ApplyTaskHandler properly implements EventBusHandler<ApplyTask> with the updated signature. The FSM thread initialization and batch processing logic are preserved correctly.

jraft-core/src/main/java/com/alipay/sofa/jraft/option/RaftOptions.java (2)

357-358: LGTM! EventBus configuration properly propagated in copy().

The new fields are correctly copied. The shallow copy of eventBusFactory is appropriate since factories are typically stateless and designed to be shared.


144-145: EventBusFactory SPI implementation is properly registered and null-safe.

A default DefaultEventBusFactory implementation is registered in META-INF/services/com.alipay.sofa.jraft.util.concurrent.EventBusFactory. Additionally, JRaftServiceLoader.first() throws a ServiceConfigurationError when no provider is found rather than returning null, making the code safe from NullPointerException.

Likely an incorrect or invalid review comment.

jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java (8)

123-127: LGTM! EventBus imports properly added.

The new imports support the migration from Disruptor to EventBus and are correctly organized.


208-209: LGTM! Field declaration correctly updated for EventBus.

The field type change from Disruptor to EventBus<LogEntryAndClosure> is appropriate and the comment accurately describes its purpose.


273-278: LGTM! Handler correctly migrated to EventBusHandler interface.

The signature change from onEvent(event, sequence, endOfBatch) to onEvent(event, endOfBatch) aligns with the EventBus API. The removal of the sequence parameter is correct as EventBus doesn't expose sequence numbers.


280-295: LGTM! Event batch processing correctly updated.

The change from per-event reset() calls to batch clear() aligns with the PR objective of reducing cross-generational references. This approach is cleaner and better suited for generational GC modes like ZGC-gen and G1.


749-750: LGTM! EventBus configuration properly propagated to FSMCaller.

The EventBus mode and factory are correctly passed from RaftOptions to FSMCallerOptions, ensuring consistent EventBus configuration across components.


970-976: LGTM! EventBus initialization is comprehensive and well-configured.

The EventBus is properly initialized with:

  • Mode from RaftOptions (supporting DISRUPTOR/MPSC choice)
  • Appropriate buffer size and batch size from existing configurations
  • Named thread factory for debuggability
  • BLOCKING wait strategy matching previous Disruptor behavior

1629-1662: LGTM! Task application properly migrated to EventBus with good error handling.

The changes correctly implement:

  • Direct event creation instead of translator-based publishing
  • Proper handling of both Blocking and NonBlocking apply modes
  • Appropriate error handling when the EventBus is full
  • Metrics recording for overload situations

The error message accurately references applyEventBus.getBufferSize() for debugging.


2906-2907: LGTM! EventBus properly cleaned up in join().

Setting applyEventBus to null after the shutdown latch completes is appropriate and prevents accidental use after shutdown.

Comment on lines +210 to 217
private boolean enqueueTask(final ApplyTask task) {
if (this.shutdownLatch != null) {
// Shutting down
LOG.warn("FSMCaller is stopped, can not apply new task.");
return false;
}
this.taskQueue.publishEvent(tpl);
return true;
return this.taskEventBus.publish(task);
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine enqueueTask usage in FSMCallerImpl.java
find . -name "FSMCallerImpl.java" -type f | head -5

Repository: sofastack/sofa-jraft

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Read the FSMCallerImpl.java file and search for enqueueTask calls
wc -l ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java

Repository: sofastack/sofa-jraft

Length of output: 140


🏁 Script executed:

#!/bin/bash
# Search for all enqueueTask calls in the file with context
rg -n "enqueueTask" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java -A 5 -B 2

Repository: sofastack/sofa-jraft

Length of output: 2779


🏁 Script executed:

#!/bin/bash
# Search for callers of onCommitted, onSnapshotLoad, onSnapshotSave, onLeaderStart, etc.
# First, let's see the full context around the flush method
sed -n '235,250p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java

Repository: sofastack/sofa-jraft

Length of output: 548


🏁 Script executed:

#!/bin/bash
# Check how external code calls these FSMCaller methods
# Search for callers of onCommitted, onSnapshotLoad, onSnapshotSave, etc.
rg -n "\.onCommitted\(|\.onSnapshotLoad\(|\.onSnapshotSave\(|\.onLeaderStart\(" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ -A 2 | head -60

Repository: sofastack/sofa-jraft

Length of output: 2696


🏁 Script executed:

#!/bin/bash
# Check callers in NodeImpl and BallotBox
# First check BallotBox.commitAt method
sed -n '135,145p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/BallotBox.java

Repository: sofastack/sofa-jraft

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Search for actual calls to fsmCaller or waiter methods from FSMCaller interface
# in other files to see if return values are checked
rg -n "fsmCaller\.(onCommitted|onSnapshotLoad|onSnapshotSave|onLeaderStart|onLeaderStop|onStartFollowing|onStopFollowing)" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ -B 2 -A 2

Repository: sofastack/sofa-jraft

Length of output: 2549


🏁 Script executed:

#!/bin/bash
# Check if the callers in NodeImpl check return values from these methods
# Line 1199 - onStopFollowing
sed -n '1195,1210p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java

Repository: sofastack/sofa-jraft

Length of output: 753


🏁 Script executed:

#!/bin/bash
# Check line 2935 - onLeaderStart call in NodeImpl
sed -n '2930,2945p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java

Repository: sofastack/sofa-jraft

Length of output: 606


🏁 Script executed:

#!/bin/bash
# Check line 3270 - onLeaderStop call in NodeImpl
sed -n '3265,3275p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java

Repository: sofastack/sofa-jraft

Length of output: 414


🏁 Script executed:

#!/bin/bash
# Now let's check how BallotBox calls onCommitted
# to see if it checks the return value
sed -n '125,150p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/BallotBox.java

Repository: sofastack/sofa-jraft

Length of output: 1384


🏁 Script executed:

#!/bin/bash
# Let's look at how the flush method (line 244) handles the return value
# and check what taskEventBus.publish does
sed -n '240,250p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java

Repository: sofastack/sofa-jraft

Length of output: 430


🏁 Script executed:

#!/bin/bash
# Let's check the publish method and see what it does
# Also check if there's any exception handling
sed -n '210,220p' ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java

Repository: sofastack/sofa-jraft

Length of output: 430


🏁 Script executed:

#!/bin/bash
# Let's verify the actual implementation by checking where taskEventBus is initialized
# and what publish() returns
rg -n "taskEventBus" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/FSMCallerImpl.java | head -20

Repository: sofastack/sofa-jraft

Length of output: 480


🏁 Script executed:

#!/bin/bash
# Let's check if there are any other callers that DO check return values
# to compare patterns
rg -n "if.*onCommitted|if.*onSnapshotLoad|if.*onSnapshotSave|if.*onLeaderStart|if.*onLeaderStop|if.*onStartFollowing|if.*onStopFollowing" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/ -B 1 -A 1

Repository: sofastack/sofa-jraft

Length of output: 428


🏁 Script executed:

#!/bin/bash
# Let's check the exact lines in NodeImpl where return values are not checked
grep -n "fsmCaller\.\(onStopFollowing\|onStartFollowing\|onLeaderStart\|onLeaderStop\)" ./jraft-core/src/main/java/com/alipay/sofa/jraft/core/NodeImpl.java

Repository: sofastack/sofa-jraft

Length of output: 412


Verify return value handling in FSMCaller method callers to prevent silent task drops.

The concern is valid. Several callers in NodeImpl and BallotBox invoke FSMCaller methods without checking their boolean return values:

  • NodeImpl.resetLeaderId() (lines 1199, 1204): ignores returns from onStopFollowing() and onStartFollowing()
  • NodeImpl (line 2935): ignores return from onLeaderStart()
  • NodeImpl.onLeaderStop() (line 3270): ignores return from onLeaderStop()
  • BallotBox.commitAt() (line 141): ignores return from waiter.onCommitted()
  • FSMCallerImpl.flush() (line 244): ignores return from enqueueTask(), could deadlock if publish fails

When taskEventBus.publish() returns false (e.g., due to overload), tasks are silently dropped. The flush method is particularly problematic—if publish fails, the latch never signals, potentially causing indefinite blocking. All these callers should handle false returns to avoid silent task drops and state machine inconsistencies.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disruptor中有new对象,导致分代zgc跨代引用压力

3 participants