[automatic failover] Refactor Circuit Breaker Command Tracking#3670
[automatic failover] Refactor Circuit Breaker Command Tracking#3670atakavci wants to merge 3 commits intoredis:mainfrom
Conversation
- early capture circuit breaker metrics with CircuitBreakerAwareCommand
There was a problem hiding this comment.
Pull request overview
This PR refactors the circuit breaker command tracking mechanism from a callback-based approach to a wrapper-based approach. The refactoring introduces a new CircuitBreakerAwareCommand wrapper class that uses lifecycle hooks (doBeforeComplete and doBeforeError) to track command results. This change aims to address a sporadic test failure in CircuitBreakerMetricsIntegrationTests where circuit breaker failures were being tracked in command future completion handlers instead of on actual command completion.
Changes:
- Introduced lifecycle hooks in
CommandWrapperfor tracking before command completion and error handling - Created
CircuitBreakerAwareCommandwrapper that records all command results through these hooks - Simplified
MultiDbOutboundHandlerto only attach the wrapper as a listener to write promises
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/main/java/io/lettuce/core/protocol/CommandWrapper.java | Added doBeforeComplete() and doBeforeError() lifecycle hooks to enable tracking before command completion |
| src/main/java/io/lettuce/core/failover/DatabaseCommandTracker.java | Introduced CircuitBreakerAwareCommand wrapper and modified write methods to wrap commands before delegation |
| src/main/java/io/lettuce/core/failover/MultiDbOutboundHandler.java | Simplified to attach CircuitBreakerAwareCommand as listener; removed old callback-based tracking logic |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Override | ||
| protected void doBeforeComplete() { | ||
| generation.recordResult(null); | ||
| } | ||
|
|
||
| @Override | ||
| protected void doBeforeError(Throwable throwable) { | ||
| generation.recordResult(throwable); | ||
| } |
There was a problem hiding this comment.
Critical issue: The new implementation changes the tracking behavior in a way that conflicts with existing tests. The CircuitBreakerAwareCommand records ALL successes (line 156) and ALL errors (line 161), but the existing tests expect that only timeout exceptions are recorded by DatabaseCommandTracker while successes and non-timeout exceptions are recorded by MultiDbOutboundHandler. This change appears to cause double-recording when combined with MultiDbOutboundHandler, or the tests need to be updated to reflect the new behavior. The PR description does not mention this behavioral change, and the tests are not included in the diff, suggesting they may not have been updated or verified to pass with these changes.
| } catch (Exception e) { | ||
| // TODO: here not sure we should record exception for each command or just once for the batch | ||
| circuitBreaker.getGeneration().recordResult(e); | ||
| commands.forEach(c -> circuitBreaker.getGeneration().recordResult(e)); | ||
| throw e; |
There was a problem hiding this comment.
Critical bug: Potential triple recording of failures for batch write exceptions. When writeMany throws an exception, line 137 records the exception once for each command. Then, each command will also be completed exceptionally (presumably), which will trigger doBeforeError (line 160-161) recording the exception again. Additionally, if the write promise fails, operationComplete (line 165-168) will record it a third time. This results in each batch write failure being recorded 2-3 times per command.
| } catch (Exception e) { | ||
| circuitBreaker.getGeneration().recordResult(e); | ||
| throw e; |
There was a problem hiding this comment.
Critical bug: Potential triple recording of write exceptions. When writeOne throws an exception, line 103 records it. Then, the command will also be completed exceptionally (presumably), which will trigger doBeforeError (line 160-161) recording the exception again. Additionally, if the write promise fails, operationComplete (line 165-168) will record it a third time. This results in each write failure being recorded 2-3 times.
| } | ||
|
|
||
| /** | ||
| * Callback method called after successful completion and before notifying downstream consumers. |
There was a problem hiding this comment.
The JavaDoc comment is incorrect. It says "after successful completion and before notifying downstream consumers", but the method is called BEFORE the wrapped command is completed (line 69), not after. The comment should say "before successful completion of the wrapped command and before notifying downstream consumers".
| * Callback method called after successful completion and before notifying downstream consumers. | |
| * Callback method called before successful completion of the wrapped command and before notifying downstream | |
| * consumers. |
| } | ||
|
|
||
| /** | ||
| * Callback method called after error completion and before notifying downstream consumers. |
There was a problem hiding this comment.
The JavaDoc comment is incorrect. It says "after error completion and before notifying downstream consumers", but the method is called BEFORE the wrapped command is completed exceptionally (line 145 for completeExceptionally, line 129 for cancel), not after. The comment should say "before error completion of the wrapped command and before notifying downstream consumers".
| * Callback method called after error completion and before notifying downstream consumers. | |
| * Callback method called before error completion of the wrapped command and before notifying downstream consumers. |
| @Override | ||
| public void operationComplete(Future<Void> future) throws Exception { | ||
| if (!future.isSuccess()) { | ||
| generation.recordResult(future.cause()); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Critical bug: This implementation can lead to double recording of failures. When a write operation fails, both operationComplete() (line 165-168) and doBeforeError() (line 160-161) will record the same failure to the circuit breaker generation. The operationComplete() method should be removed, or the doBeforeError() should check if the error is being recorded due to a write failure to avoid duplicate recording.
| @Override | |
| public void operationComplete(Future<Void> future) throws Exception { | |
| if (!future.isSuccess()) { | |
| generation.recordResult(future.cause()); | |
| } | |
| } |
| for (Object cmd : collection) { | ||
| if (cmd instanceof CircuitBreakerAwareCommand<?, ?, ?>) { | ||
| // we want to use the generation that is active at the time of write. | ||
| // Using a lamda which acces directly to circuitBreaker.getGeneration() here would delay capturing the |
There was a problem hiding this comment.
Spelling error: "acces" should be "access", and "lamda" should be "lambda".
| // Using a lamda which acces directly to circuitBreaker.getGeneration() here would delay capturing the | |
| // Using a lambda which access directly to circuitBreaker.getGeneration() here would delay capturing the |
There was a problem hiding this comment.
@atakavci
I see it as a simplified and more predictable way of tracking command completion.
From my point of view, this approach is better, and guaranitee command result is tracked in CB before notifying of command completion
@ggivo there is a downside of this approach that is contradicting with fundamentals of async API; the wrapper command is not the one API returns, it remains completely hidden/internal. In case the async command itself is somehow directly managed, CircuitBreakerAware is left out of equation. And there is no elegant way to improve that,, at least i couldn't see one. |
Closes #3567
Summary
Refactored circuit breaker command tracking to use a wrapper-based approach instead of callback-based tracking, improving consistency and reducing code duplication.
Changes
Core Improvements
CircuitBreakerAwareCommand: NewCommandWrappersubclass that tracks circuit breaker results through lifecycle hooksdoBeforeComplete()anddoBeforeError()hooksModified Components
DatabaseCommandTracker:CircuitBreakerAwareCommandbefore writingattachRecorder()method and callback-based trackingMultiDbOutboundHandler:CircuitBreakerAwareCommandinstancesrecorder()andrecordOnCommandComplete()methodsListtoCollectionfor better type flexibilityCommandWrapper:doBeforeComplete()anddoBeforeError()lifecycle hooksBenefits