Skip to content

Commit

Permalink
feat!: add support for errors.Is() on QuorumCallError
Browse files Browse the repository at this point in the history
This refactors the QuorumCallError to support the errors.Is() func.
This is a breaking change since it unexports the Reason field;
users of the QuorumCallError must update their code to use the
errors.Is() function.

This also adds the gorums.Incomplete sentinal error that can be
used to check for incomplete quorum calls.
  • Loading branch information
meling committed Mar 23, 2024
1 parent 1fbfdbe commit a438538
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 12 deletions.
4 changes: 2 additions & 2 deletions async.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ func (c RawConfiguration) handleAsyncCall(ctx context.Context, fut *Async, state
return
}
case <-ctx.Done():
fut.reply, fut.err = resp, QuorumCallError{Reason: ctx.Err().Error(), errors: errs, replies: len(replies)}
fut.reply, fut.err = resp, QuorumCallError{cause: ctx.Err(), errors: errs, replies: len(replies)}
return
}
if len(errs)+len(replies) == state.expectedReplies {
fut.reply, fut.err = resp, QuorumCallError{Reason: "incomplete call", errors: errs, replies: len(replies)}
fut.reply, fut.err = resp, QuorumCallError{cause: Incomplete, errors: errs, replies: len(replies)}
return
}
}
Expand Down
4 changes: 2 additions & 2 deletions correctable.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,12 @@ func (c RawConfiguration) handleCorrectableCall(ctx context.Context, corr *Corre
}
}
case <-ctx.Done():
corr.set(resp, clevel, QuorumCallError{Reason: ctx.Err().Error(), errors: errs, replies: len(replies)}, true)
corr.set(resp, clevel, QuorumCallError{cause: ctx.Err(), errors: errs, replies: len(replies)}, true)
return
}
if (state.data.ServerStream && len(errs) == state.expectedReplies) ||
(!state.data.ServerStream && len(errs)+len(replies) == state.expectedReplies) {
corr.set(resp, clevel, QuorumCallError{Reason: "incomplete call", errors: errs, replies: len(replies)}, true)
corr.set(resp, clevel, QuorumCallError{cause: Incomplete, errors: errs, replies: len(replies)}, true)
return
}
}
Expand Down
17 changes: 15 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package gorums

import (
"errors"
"fmt"
"strings"
)

// Incomplete is the error returned by a quorum call when the call cannot completed
// due insufficient non-error replies to form a quorum according to the quorum function.
var Incomplete = errors.New("incomplete call")

// QuorumCallError reports on a failed quorum call.
type QuorumCallError struct {
Reason string
cause error
errors []nodeError
replies int
}

// Is reports whether the target error is the same as the cause of the QuorumCallError.
func (e QuorumCallError) Is(target error) bool {
if t, ok := target.(QuorumCallError); ok {
return e.cause == t.cause
}
return e.cause == target
}

func (e QuorumCallError) Error() string {
s := fmt.Sprintf("quorum call error: %s (errors: %d, replies: %d)", e.Reason, len(e.errors), e.replies)
s := fmt.Sprintf("quorum call error: %s (errors: %d, replies: %d)", e.cause, len(e.errors), e.replies)
var b strings.Builder
b.WriteString(s)
if len(e.errors) == 0 {
Expand Down
60 changes: 60 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package gorums

import (
"context"
"errors"
"testing"
)

func TestQuorumCallErrorIs(t *testing.T) {
tests := []struct {
name string
err error
target error
want bool
}{
{
name: "SameCauseError",
err: QuorumCallError{cause: Incomplete},
target: Incomplete,
want: true,
},
{
name: "SameCauseQCError",
err: QuorumCallError{cause: Incomplete},
target: QuorumCallError{cause: Incomplete},
want: true,
},
{
name: "DifferentError",
err: QuorumCallError{cause: Incomplete},
target: errors.New("incomplete call"),
want: false,
},
{
name: "DifferentQCError",
err: QuorumCallError{cause: Incomplete},
target: QuorumCallError{cause: errors.New("incomplete call")},
want: false,
},
{
name: "ContextCanceled",
err: QuorumCallError{cause: context.Canceled},
target: context.Canceled,
want: true,
},
{
name: "ContextCanceledQC",
err: QuorumCallError{cause: context.Canceled},
target: QuorumCallError{cause: context.Canceled},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := errors.Is(tt.err, tt.target); got != tt.want {
t.Errorf("QuorumCallError.Is(%v, %v) = %v, want %v", tt.err, tt.target, got, tt.want)
}
})
}
}
4 changes: 2 additions & 2 deletions quorumcall.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ func (c RawConfiguration) QuorumCall(ctx context.Context, d QuorumCallData) (res
return resp, nil
}
case <-ctx.Done():
return resp, QuorumCallError{Reason: ctx.Err().Error(), errors: errs, replies: len(replies)}
return resp, QuorumCallError{cause: ctx.Err(), errors: errs, replies: len(replies)}
}
if len(errs)+len(replies) == expectedReplies {
return resp, QuorumCallError{Reason: "incomplete call", errors: errs, replies: len(replies)}
return resp, QuorumCallError{cause: Incomplete, errors: errs, replies: len(replies)}
}
}
}
7 changes: 3 additions & 4 deletions tests/ordering/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ordering

import (
"context"
"errors"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -165,10 +166,8 @@ func TestQCAsyncOrdering(t *testing.T) {
defer wg.Done()
resp, err := promise.Get()
if err != nil {
if qcError, ok := err.(gorums.QuorumCallError); ok {
if qcError.Reason == context.Canceled.Error() {
return
}
if errors.Is(err, context.Canceled) {
return
}
t.Errorf("QC error: %v", err)
}
Expand Down

0 comments on commit a438538

Please sign in to comment.