You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2025/09/04 12:17:52 UTC
(skywalking-banyandb) 07/11: Enhance error handling in batch publisher and circuit breaker logic
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch pub/perf
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit bab4865349c525f3a9c3f85a3688b1d47d05eb9a
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Thu Sep 4 17:39:38 2025 +0800
Enhance error handling in batch publisher and circuit breaker logic
- Added failure recording in the batch publisher for circuit breaker events, improving diagnostics during failover scenarios.
- Updated the `isTransientError` and `isInternalError` functions to handle `common.Error` types, ensuring consistent error classification.
---
banyand/queue/pub/batch.go | 6 ++++
banyand/queue/pub/circuitbreaker_test.go | 51 ++++++++++++++++++++++++++++++++
banyand/queue/pub/client.go | 21 +++++++++++++
3 files changed, 78 insertions(+)
diff --git a/banyand/queue/pub/batch.go b/banyand/queue/pub/batch.go
index f1cacc94..127bec11 100644
--- a/banyand/queue/pub/batch.go
+++ b/banyand/queue/pub/batch.go
@@ -172,6 +172,8 @@ func (bp *batchPublisher) Publish(ctx context.Context, topic bus.Topic, messages
resp, errRecv := s.Recv()
if errRecv != nil {
if isFailoverError(errRecv) {
+ // Record circuit breaker failure before creating failover event
+ bp.pub.recordFailure(curNode, errRecv)
bc <- batchEvent{n: curNode, e: common.NewErrorWithStatus(modelv1.Status_STATUS_INTERNAL_ERROR, errRecv.Error())}
}
return
@@ -184,6 +186,8 @@ func (bp *batchPublisher) Publish(ctx context.Context, topic bus.Topic, messages
}
if isFailoverStatus(resp.Status) {
ce := common.NewErrorWithStatus(resp.Status, resp.Error)
+ // Record circuit breaker failure before creating failover event
+ bp.pub.recordFailure(curNode, ce)
bc <- batchEvent{n: curNode, e: ce}
}
}(stream, deferFn, bp.f.events[len(bp.f.events)-1], nodeName)
@@ -206,6 +210,8 @@ func (bp *batchPublisher) Close() (cee map[string]*common.Error, err error) {
go func() {
defer bp.pub.closer.Done()
for n, e := range batchEvents {
+ // Record circuit breaker failure before failover
+ bp.pub.recordFailure(n, e.e)
if bp.topic == nil {
bp.pub.failover(n, e.e, data.TopicCommon)
continue
diff --git a/banyand/queue/pub/circuitbreaker_test.go b/banyand/queue/pub/circuitbreaker_test.go
index b22fc06e..8bc90223 100644
--- a/banyand/queue/pub/circuitbreaker_test.go
+++ b/banyand/queue/pub/circuitbreaker_test.go
@@ -27,6 +27,8 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
+ "github.com/apache/skywalking-banyandb/api/common"
+ modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
"github.com/apache/skywalking-banyandb/pkg/logger"
)
@@ -630,3 +632,52 @@ func TestCircuitBreakerErrorFiltering(t *testing.T) {
assert.Equal(t, StateOpen, cb.state)
assert.Equal(t, defaultCBThreshold, cb.consecutiveFailures)
}
+
+func TestCircuitBreakerFailoverErrors(t *testing.T) {
+ p := &pub{
+ cbStates: make(map[string]*circuitState),
+ cbMu: sync.RWMutex{},
+ log: logger.GetLogger("test"),
+ }
+
+ node := testNodeName
+
+ // Test failover errors from Recv() - using codes.Unavailable which is a failover error
+ failoverErr := status.Error(codes.Unavailable, "service unavailable")
+
+ // Verify initial state is closed
+ assert.True(t, p.isRequestAllowed(node))
+
+ // Simulate failover errors that should increment circuit breaker
+ for i := 0; i < defaultCBThreshold; i++ {
+ p.recordFailure(node, failoverErr)
+ }
+
+ // Circuit should be open after failover errors
+ assert.False(t, p.isRequestAllowed(node))
+ p.cbMu.Lock()
+ cb, exists := p.cbStates[node]
+ p.cbMu.Unlock()
+ assert.True(t, exists)
+ assert.Equal(t, StateOpen, cb.state)
+ assert.Equal(t, defaultCBThreshold, cb.consecutiveFailures)
+
+ // Reset and test with common.Error (simulating Close() method errors)
+ p.recordSuccess(node)
+ assert.True(t, p.isRequestAllowed(node))
+
+ // Test with common.Error types that would come from Close() method
+ internalErr := common.NewErrorWithStatus(modelv1.Status_STATUS_INTERNAL_ERROR, "internal error")
+ for i := 0; i < defaultCBThreshold; i++ {
+ p.recordFailure(node, internalErr)
+ }
+
+ // Circuit should be open again
+ assert.False(t, p.isRequestAllowed(node))
+ p.cbMu.Lock()
+ cb, exists = p.cbStates[node]
+ p.cbMu.Unlock()
+ assert.True(t, exists)
+ assert.Equal(t, StateOpen, cb.state)
+ assert.Equal(t, defaultCBThreshold, cb.consecutiveFailures)
+}
diff --git a/banyand/queue/pub/client.go b/banyand/queue/pub/client.go
index 2b090f0f..482df7a0 100644
--- a/banyand/queue/pub/client.go
+++ b/banyand/queue/pub/client.go
@@ -20,6 +20,7 @@ package pub
import (
"context"
"crypto/rand"
+ "errors"
"fmt"
"math/big"
"time"
@@ -400,10 +401,23 @@ func isTransientError(err error) bool {
return false
}
+ // Handle gRPC status errors
if s, ok := status.FromError(err); ok {
return retryableCodes[s.Code()]
}
+ // Handle common.Error types
+ var ce *common.Error
+ if errors.As(err, &ce) {
+ // Map common status to gRPC codes for consistency
+ switch ce.Status() {
+ case modelv1.Status_STATUS_INTERNAL_ERROR:
+ return retryableCodes[codes.Internal]
+ default:
+ return false
+ }
+ }
+
return false
}
@@ -413,10 +427,17 @@ func isInternalError(err error) bool {
return false
}
+ // Handle gRPC status errors
if s, ok := status.FromError(err); ok {
return s.Code() == codes.Internal
}
+ // Handle common.Error types
+ var ce *common.Error
+ if errors.As(err, &ce) {
+ return ce.Status() == modelv1.Status_STATUS_INTERNAL_ERROR
+ }
+
return false
}