zen: update error handling

This commit is contained in:
Tigor Hutasuhut 2024-08-26 16:24:41 +07:00
parent f20b4a6359
commit 34e7ed06cb
5 changed files with 113 additions and 104 deletions

View file

@ -10,6 +10,8 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
_ "gitlab.bareksa.com/backend/zen/core/zerr"
) )
func Serve() { func Serve() {

View file

@ -3,12 +3,14 @@ package zerr
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"connectrpc.com/connect" "connectrpc.com/connect"
"github.com/pborman/indent"
"gitlab.bareksa.com/backend/zen/core/zoptions" "gitlab.bareksa.com/backend/zen/core/zoptions"
) )
@ -28,73 +30,81 @@ type Err struct {
sequence *Sequence sequence *Sequence
} }
// Join implements Error.
func (mu *Err) Join(errs ...error) Error {
for _, err := range errs {
if err != nil {
mu.errs = append(mu.errs, err)
}
}
return mu
}
// Resolve implements Error.
func (mu *Err) Resolve() error {
if len(mu.errs) == 0 {
return nil
}
return mu
}
func (mu *Err) Error() string { func (mu *Err) Error() string {
if len(mu.errs) == 0 { if len(mu.errs) == 0 {
return "[nil]" return "[nil]"
} }
s := strings.Builder{} s := &strings.Builder{}
if len(mu.errs) == 1 { if mu.message != "" {
str := getNestedErrorString(mu.errs[0])
str, found := strings.CutPrefix(str, mu.message)
if found {
str, _ = strings.CutPrefix(str, ": ")
}
s.WriteString(mu.message) s.WriteString(mu.message)
}
if len(mu.errs) == 1 {
err := mu.errs[0]
if wb, ok := err.(interface{ WriteBuilder(io.Writer) }); ok {
wb.WriteBuilder(s)
return s.String()
}
next := strings.TrimPrefix(err.Error(), mu.message)
if next == "" {
return s.String()
}
s.WriteString(": ") s.WriteString(": ")
s.WriteString(str) s.WriteString(next)
return s.String() return s.String()
} }
s.WriteString(mu.message) if mu.message != "" {
s.WriteString(":\n")
}
w := indent.New(s, " ")
for i, e := range mu.errs { for i, e := range mu.errs {
if i > 0 { if i > 0 {
s.WriteString("\n") _, _ = io.WriteString(w, "\n")
} }
s.WriteString(strconv.Itoa(i + 1)) _, _ = io.WriteString(w, strconv.Itoa(i+1))
s.WriteString(". ") _, _ = io.WriteString(w, ". ")
s.WriteString(getNestedErrorString(e)) if wb, ok := e.(interface{ WriteBuilder(io.Writer) }); ok {
} wb.WriteBuilder(w)
return s.String() } else {
} _, _ = io.WriteString(w, e.Error())
func getNestedErrorString(err error) string {
if err == nil {
return "[nil]"
}
s := strings.Builder{}
if e, ok := err.(interface{ Unwrap() error }); ok {
current := err.Error()
if err := e.Unwrap(); err != nil {
next := getNestedErrorString(err)
hasPrefix := strings.HasPrefix(next, current)
if !hasPrefix {
s.WriteString(current)
s.WriteString(": ")
}
s.WriteString(next)
} }
} }
if errs, ok := err.(interface{ Unwrap() []error }); ok {
for i, e := range errs.Unwrap() {
if i > 0 {
s.WriteString("\n")
}
s.WriteString(strconv.Itoa(i + 1))
s.WriteString(". ")
s.WriteString(getNestedErrorString(e))
}
}
if s.Len() == 0 {
s.WriteString(err.Error())
}
return s.String() return s.String()
} }
// LogValue returns log fields to be consumed by logger. // LogValue returns log fields to be consumed by logger.
func (mu *Err) LogValue() slog.Value { func (mu *Err) LogValue() slog.Value {
attrs := make([]slog.Attr, 0, len(mu.errs)) attrs := make([]slog.Attr, 0, 8)
if len(mu.message) > 0 {
attrs = append(attrs, slog.String("message", mu.message))
}
if len(mu.publicMessage) > 0 {
attrs = append(attrs, slog.String("public", mu.message))
}
if mu.code != connect.CodeUnknown && mu.code != connect.CodeInternal {
attrs = append(attrs, slog.String("code", mu.code.String()))
}
if mu.sequence.IsRoot() {
}
if len(mu.errs) == 1 {
}
i := 0 i := 0
for _, err := range mu.errs { for _, err := range mu.errs {
if err == nil { if err == nil {

View file

@ -11,7 +11,7 @@ import (
// WriteBuilder writes the error to the builder. // WriteBuilder writes the error to the builder.
// //
// w is guaranteed to never return error because // w is guaranteed to never return error because
// the underlying most writer is a strings.Builder. // the bottom most writer is a strings.Builder.
func (er *Err) WriteBuilder(w io.Writer) { func (er *Err) WriteBuilder(w io.Writer) {
if len(er.errs) == 0 { if len(er.errs) == 0 {
_, _ = io.WriteString(w, "[nil]") _, _ = io.WriteString(w, "[nil]")
@ -21,7 +21,7 @@ func (er *Err) WriteBuilder(w io.Writer) {
if err := er.sequence.Outer(); err != nil && !err.IsMulti() { if err := er.sequence.Outer(); err != nil && !err.IsMulti() {
previous = err.GetMessage() previous = err.GetMessage()
} }
current, _ := strings.CutPrefix(er.message, previous) current := strings.TrimPrefix(er.message, previous)
if current != "" { if current != "" {
if previous != "" { if previous != "" {
_, _ = io.WriteString(w, ": ") _, _ = io.WriteString(w, ": ")

View file

@ -52,10 +52,24 @@ type Error interface {
// //
// ID is used to identify the error. Used by Zen to consider if an error is the same as another. // ID is used to identify the error. Used by Zen to consider if an error is the same as another.
// //
// Current implementation // If unset, the ID will be automatically generated depending
ID(msg string, args ...any) Error // on the backend logging and notification.
ID(s string, args ...any) Error
GetID() string GetID() string
// Join appends given errors to this zerr.Error.
//
// Join discards nil errors.
//
// When searching for Code, Message, PublicMessage, etc,
// only the first error containing valid values will be used.
//
// The searching method is depth-first.
//
// It's undefined behavior to call Join when another thread calls
// .Sequence().Iter() on this error.
Join(errs ...error) Error
Log(ctx context.Context) Error Log(ctx context.Context) Error
Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error Notify(ctx context.Context, opts ...zoptions.NotifyOption) Error
@ -66,6 +80,7 @@ type Error interface {
IsMulti() bool IsMulti() bool
// Resolve returns error (self) if all the errors // Resolve returns error (self) if all the errors
// are valid. // are valid (not nil) and Error contains at least one error
// wrapped.
Resolve() error Resolve() error
} }

View file

@ -2,97 +2,79 @@ package zerr
import "iter" import "iter"
// Sequence is the implementation of zerr.Sequence. // Sequence keeps track of the sequence of errors.
// //
// The implementation is reversed linked list. // The implementation is reversed linked list.
// //
// New Sequence value will be added to the start of the list. // When wrapping errors, the wrapping error
// will be the parent of the wrapped Error
// if the wrapped Error is a zerr.Error.
type Sequence struct { type Sequence struct {
prev *Sequence parent *Sequence
next *Sequence children []*Sequence
err Error err Error
index int
} }
// Set prepends the next Sequence to the current Sequence. // WrapBy prepends the next Sequence to the current Sequence.
func (se *Sequence) Set(next *Sequence) { func (se *Sequence) WrapBy(parent *Sequence) {
next.next = se parent.children = append(parent.children, se)
se.prev = next se.parent = parent
se.index = next.index + 1
if next.next != nil {
next.next.Set(se)
}
} }
// Outer returns the error that wraps this Error. // Outer returns the error that wraps this Error.
// Return nil if this Error is not wrapped // Return nil if this Error is not wrapped
// or wrapping outer is not a zerr.Error. // or wrapping outer is not a zerr.Error.
func (se *Sequence) Outer() Error { func (se *Sequence) Outer() Error {
if se.prev != nil { if se.parent != nil {
return se.prev.err return se.parent.err
} }
return nil return nil
} }
// Inner returns the inner zerr.Error in the sequence.
//
// if there is no inner zerr.Error, it returns nil.
//
// if the inner error is not a zerr.Error, this returns nil.
func (se *Sequence) Inner() Error {
if se.next != nil {
return se.next.err
}
return nil
}
// Index returns the index of this Error in the sequence.
func (se *Sequence) Index() int {
return se.index
}
// Root returns the outermost Error in the sequence. // Root returns the outermost Error in the sequence.
// //
// Returns self if there is no outermost. // Returns self if there is no outermost.
func (se *Sequence) Root() *Sequence { func (se *Sequence) Root() *Sequence {
root := se root := se
for root.prev != nil { for root.parent != nil {
root = root.prev root = root.parent
} }
return root return root
} }
// IsRoot checks if the Error is the outermost Error in the sequence.
func (se *Sequence) IsRoot() bool { func (se *Sequence) IsRoot() bool {
return se.prev == nil return se.parent == nil
}
// Error returns the Error in the sequence.
func (se *Sequence) Error() Error {
return se.err
} }
// Iter returns an iterator to traverse the sequence. // Iter returns an iterator to traverse the sequence.
// //
// The iterator will start from outermost zerr.Error (root) to the last zerr.Error
// in depth-first order.
//
// Iterator will skip any nil values in the sequence. // Iterator will skip any nil values in the sequence.
// //
// Iterator does not guarantee idempotent behavior. // Iteration is traversed depth first.
// Next call to iterator may start from different root
// because the parent in the tree may be wrapped
// by another zerr.Error.
// //
// Example: // Example:
// //
// for _, e := range err.Sequence().Iter() { // for e := range err.Sequence().Iter() {
// // e is zerr.Error // // e is zerr.Error
// // do something with e // // do something with e
// } // }
func (se *Sequence) Iter() iter.Seq[Error] { func (se *Sequence) Iter() iter.Seq[Error] {
root := se.Root() return func(yield func(Error) bool) {
return iter.Seq[Error](func(yield func(V Error) bool) { if se.err != nil && !yield(se.err) {
for next := root; next != nil; next = next.next { return
if next.err != nil { }
if !yield(next.err) { for _, child := range se.children {
for e := range child.Iter() {
if !yield(e) {
return return
} }
} }
} }
}) }
} }