merr: refactor Wrap/WrapSkip/New to take multiple contexts rather than annotations

This commit is contained in:
Brian Picciano 2019-02-27 13:05:51 -05:00
parent 4303da03cc
commit 4536db8968
17 changed files with 112 additions and 83 deletions

5
m/m.go
View File

@ -6,7 +6,6 @@ package m
import (
"context"
"log"
"os"
"os/signal"
@ -43,9 +42,9 @@ func ServiceContext() context.Context {
ctx, logLevelStr := mcfg.WithString(ctx, "log-level", "info", "Maximum log level which will be printed.")
ctx = mrun.WithStartHook(ctx, func(context.Context) error {
logLevel := mlog.LevelFromString(*logLevelStr)
log.Printf("setting log level to %v", logLevel)
if logLevel == nil {
return merr.New(ctx, "invalid log level", "log-level", *logLevelStr)
ctx := mctx.Annotate(ctx, "log-level", *logLevelStr)
return merr.New("invalid log level", ctx)
}
logger.SetMaxLevel(logLevel)
return nil

View File

@ -90,7 +90,8 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
break
}
if !pvOk {
return nil, merr.New(context.Background(), "unexpected config parameter", "param", arg)
ctx := mctx.Annotate(context.Background(), "param", arg)
return nil, merr.New("unexpected config parameter", ctx)
}
}
@ -121,7 +122,8 @@ func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
pvStrValOk = false
}
if pvOk && !pvStrValOk {
return nil, merr.New(p.Context, "param expected a value", "param", key)
ctx := mctx.Annotate(p.Context, "param", key)
return nil, merr.New("param expected a value", ctx)
}
return pvs, nil
}

View File

@ -59,7 +59,8 @@ func (env SourceEnv) Parse(params []Param) ([]ParamValue, error) {
for _, kv := range kvs {
split := strings.SplitN(kv, "=", 2)
if len(split) != 2 {
return nil, merr.New(context.Background(), "malformed environment key/value pair", "kv", kv)
ctx := mctx.Annotate(context.Background(), "kv", kv)
return nil, merr.New("malformed environment key/value pair", ctx)
}
k, v := split[0], split[1]
if p, ok := pM[k]; ok {

View File

@ -110,8 +110,9 @@ func populate(params []Param, src Source) error {
if !p.Required {
continue
} else if _, ok := pvM[hash]; !ok {
return merr.New(p.Context, "required parameter is not set",
ctx := mctx.Annotate(p.Context,
"param", paramFullName(mctx.Path(p.Context), p.Name))
return merr.New("required parameter is not set", ctx)
}
}
@ -133,7 +134,7 @@ func populate(params []Param, src Source) error {
// of the Params which were provided by the respective Source.
//
// Source may be nil to indicate that no configuration is provided. Only default
// values will be used, and if any paramaters are required this will error.
// values will be used, and if any parameters are required this will error.
func Populate(ctx context.Context, src Source) error {
return populate(collectParams(ctx), src)
}

View File

@ -14,6 +14,7 @@ import (
"math/big"
"time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr"
)
@ -59,7 +60,8 @@ func (pk PublicKey) verify(s Signature, r io.Reader) error {
return err
}
if err := rsa.VerifyPSS(&pk.PublicKey, crypto.SHA256, h.Sum(nil), s.sig, nil); err != nil {
return merr.Wrap(context.Background(), ErrInvalidSig, "sig", s)
ctx := mctx.Annotate(context.Background(), "sig", s)
return merr.Wrap(ErrInvalidSig, ctx)
}
return nil
}
@ -89,12 +91,14 @@ func (pk *PublicKey) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, pubKeyV0)
if !ok || len(strEnc) <= hex.EncodedLen(8) {
return merr.Wrap(context.Background(), errMalformedPublicKey, "pubKeyStr", str)
ctx := mctx.Annotate(context.Background(), "pubKeyStr", str)
return merr.Wrap(errMalformedPublicKey, ctx)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.Wrap(context.Background(), err, "pubKeyStr", str)
ctx := mctx.Annotate(context.Background(), "pubKeyStr", str)
return merr.Wrap(err, ctx)
}
pk.E = int(binary.BigEndian.Uint64(b))
@ -185,17 +189,17 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, privKeyV0)
if !ok {
return merr.Wrap(context.Background(), errMalformedPrivateKey)
return merr.Wrap(errMalformedPrivateKey)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.Wrap(context.Background(), errMalformedPrivateKey)
return merr.Wrap(errMalformedPrivateKey)
}
e, n := binary.Uvarint(b)
if n <= 0 {
return merr.Wrap(context.Background(), errMalformedPrivateKey)
return merr.Wrap(errMalformedPrivateKey)
}
pk.PublicKey.E = int(e)
b = b[n:]
@ -206,7 +210,7 @@ func (pk *PrivateKey) UnmarshalText(b []byte) error {
}
l, n := binary.Uvarint(b)
if n <= 0 {
err = merr.Wrap(context.Background(), errMalformedPrivateKey)
err = merr.Wrap(errMalformedPrivateKey)
}
b = b[n:]
i := new(big.Int)

View File

@ -1,7 +1,6 @@
package mcrypto
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
@ -77,9 +76,9 @@ func (s Secret) sign(r io.Reader) (Signature, error) {
func (s Secret) verify(sig Signature, r io.Reader) error {
sigB, err := s.signRaw(r, uint8(len(sig.sig)), sig.salt, sig.t)
if err != nil {
return merr.Wrap(context.Background(), err, "sig", sig)
return merr.Wrap(err)
} else if !hmac.Equal(sigB, sig.sig) {
return merr.Wrap(context.Background(), ErrInvalidSig, "sig", sig)
return merr.Wrap(ErrInvalidSig)
}
return nil
}

View File

@ -2,7 +2,6 @@ package mcrypto
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
@ -67,12 +66,12 @@ func (s *Signature) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, sigV0)
if !ok || len(strEnc) < hex.EncodedLen(10) {
return merr.Wrap(context.Background(), errMalformedSig, "sigStr", str)
return merr.Wrap(errMalformedSig)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.Wrap(context.Background(), err, "sigStr", str)
return merr.Wrap(err)
}
unixNano, b := int64(binary.BigEndian.Uint64(b[:8])), b[8:]
@ -82,7 +81,7 @@ func (s *Signature) UnmarshalText(b []byte) error {
if err != nil {
return nil
} else if len(b) < 1+int(b[0]) {
err = merr.Wrap(context.Background(), errMalformedSig, "sigStr", str)
err = merr.Wrap(errMalformedSig)
return nil
}
out := b[1 : 1+b[0]]

View File

@ -10,6 +10,7 @@ import (
"errors"
"time"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr"
)
@ -69,11 +70,13 @@ func (u *UUID) UnmarshalText(b []byte) error {
str := string(b)
strEnc, ok := stripPrefix(str, uuidV0)
if !ok || len(strEnc) != hex.EncodedLen(16) {
return merr.Wrap(context.Background(), errMalformedUUID, "uuidStr", str)
ctx := mctx.Annotate(context.Background(), "uuidStr", str)
return merr.Wrap(errMalformedUUID, ctx)
}
b, err := hex.DecodeString(strEnc)
if err != nil {
return merr.Wrap(context.Background(), err, "uuidStr", str)
ctx := mctx.Annotate(context.Background(), "uuidStr", str)
return merr.Wrap(errMalformedUUID, ctx)
}
u.b = b
return nil

View File

@ -278,9 +278,14 @@ func mergeAnnotations(ctxA, ctxB context.Context) context.Context {
//
// NOTE this will panic if no Contexts are passed in.
func MergeAnnotations(ctxs ...context.Context) context.Context {
ctxA := ctxs[0]
for _, ctxB := range ctxs[1:] {
ctxA = mergeAnnotations(ctxA, ctxB)
}
return ctxA
return MergeAnnotationsInto(ctxs[0], ctxs[1:]...)
}
// MergeAnnotationsInto is a convenience function which works like
// MergeAnnotations.
func MergeAnnotationsInto(ctx context.Context, ctxs ...context.Context) context.Context {
for _, ctxB := range ctxs {
ctx = mergeAnnotations(ctx, ctxB)
}
return ctx
}

View File

@ -66,7 +66,7 @@ func WithBigQuery(parent context.Context, gce *mdb.GCE) (context.Context, *BigQu
mlog.Info("connecting to bigquery", bq.ctx)
var err error
bq.Client, err = bigquery.NewClient(innerCtx, bq.gce.Project, bq.gce.ClientOptions()...)
return merr.Wrap(bq.ctx, err)
return merr.Wrap(err, bq.ctx)
})
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return bq.Client.Close()
@ -100,12 +100,12 @@ func (bq *BigQuery) Table(
mlog.Debug("creating/grabbing table", bq.ctx)
schema, err := bigquery.InferSchema(schemaObj)
if err != nil {
return nil, nil, merr.Wrap(ctx, err)
return nil, nil, merr.Wrap(err, ctx)
}
ds := bq.Dataset(dataset)
if err := ds.Create(ctx, nil); err != nil && !isErrAlreadyExists(err) {
return nil, nil, merr.Wrap(ctx, err)
return nil, nil, merr.Wrap(err, ctx)
}
table := ds.Table(tableName)
@ -114,7 +114,7 @@ func (bq *BigQuery) Table(
Schema: schema,
}
if err := table.Create(ctx, meta); err != nil && !isErrAlreadyExists(err) {
return nil, nil, merr.Wrap(ctx, err)
return nil, nil, merr.Wrap(err, ctx)
}
uploader := table.Uploader()

View File

@ -74,7 +74,7 @@ func WithBigTable(parent context.Context, gce *mdb.GCE, defaultInstance string)
bt.gce.Project, bt.Instance,
bt.gce.ClientOptions()...,
)
return merr.Wrap(bt.ctx, err)
return merr.Wrap(err, bt.ctx)
})
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return bt.Client.Close()
@ -95,14 +95,14 @@ func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...str
mlog.Debug("creating admin client", ctx)
adminClient, err := bigtable.NewAdminClient(ctx, bt.gce.Project, bt.Instance)
if err != nil {
return merr.Wrap(ctx, err)
return merr.Wrap(err, ctx)
}
defer adminClient.Close()
mlog.Debug("creating bigtable table (if needed)", ctx)
err = adminClient.CreateTable(ctx, name)
if err != nil && !isErrAlreadyExists(err) {
return merr.Wrap(ctx, err)
return merr.Wrap(err, ctx)
}
for _, colFam := range colFams {
@ -110,7 +110,7 @@ func (bt *Bigtable) EnsureTable(ctx context.Context, name string, colFams ...str
mlog.Debug("creating bigtable column family (if needed)", ctx)
err := adminClient.CreateColumnFamily(ctx, name, colFam)
if err != nil && !isErrAlreadyExists(err) {
return merr.Wrap(ctx, err)
return merr.Wrap(err, ctx)
}
}

View File

@ -44,7 +44,7 @@ func WithDatastore(parent context.Context, gce *mdb.GCE) (context.Context, *Data
mlog.Info("connecting to datastore", ds.ctx)
var err error
ds.Client, err = datastore.NewClient(innerCtx, ds.gce.Project, ds.gce.ClientOptions()...)
return merr.Wrap(ds.ctx, err)
return merr.Wrap(err, ds.ctx)
})
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return ds.Client.Close()

View File

@ -62,7 +62,7 @@ func WithPubSub(parent context.Context, gce *mdb.GCE) (context.Context, *PubSub)
mlog.Info("connecting to pubsub", ps.ctx)
var err error
ps.Client, err = pubsub.NewClient(innerCtx, ps.gce.Project, ps.gce.ClientOptions()...)
return merr.Wrap(ps.ctx, err)
return merr.Wrap(err, ps.ctx)
})
ctx = mrun.WithStopHook(ctx, func(context.Context) error {
return ps.Client.Close()
@ -93,14 +93,14 @@ func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic,
if isErrAlreadyExists(err) {
t.topic = ps.Client.Topic(name)
} else if err != nil {
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
return nil, merr.Wrap(err, t.ctx, ctx)
}
} else {
t.topic = ps.Client.Topic(name)
if exists, err := t.topic.Exists(t.ctx); err != nil {
return nil, merr.Wrap(ctx, merr.Wrap(t.ctx, err))
return nil, merr.Wrap(err, t.ctx, ctx)
} else if !exists {
return nil, merr.Wrap(ctx, merr.New(t.ctx, "topic dne"))
return nil, merr.New("topic dne", t.ctx, ctx)
}
}
return t, nil
@ -110,7 +110,7 @@ func (ps *PubSub) Topic(ctx context.Context, name string, create bool) (*Topic,
func (t *Topic) Publish(ctx context.Context, data []byte) error {
_, err := t.topic.Publish(ctx, &Message{Data: data}).Get(ctx)
if err != nil {
return merr.Wrap(ctx, merr.Wrap(t.ctx, err))
return merr.Wrap(err, t.ctx, ctx)
}
return nil
}
@ -144,14 +144,14 @@ func (t *Topic) Subscription(ctx context.Context, name string, create bool) (*Su
if isErrAlreadyExists(err) {
s.sub = t.ps.Subscription(name)
} else if err != nil {
return nil, merr.Wrap(ctx, merr.Wrap(s.ctx, err))
return nil, merr.Wrap(err, s.ctx, ctx)
}
} else {
s.sub = t.ps.Subscription(name)
if exists, err := s.sub.Exists(ctx); err != nil {
return nil, merr.Wrap(ctx, merr.Wrap(s.ctx, err))
return nil, merr.Wrap(err, s.ctx, ctx)
} else if !exists {
return nil, merr.Wrap(ctx, merr.New(s.ctx, "sub dne"))
return nil, merr.New("sub dne", s.ctx, ctx)
}
}
return s, nil
@ -350,7 +350,7 @@ func (s *Subscription) BatchConsume(
case ret := <-retCh:
return ret, nil
case <-ctx.Done():
return false, merr.Wrap(ctx, merr.New(s.ctx, "reading from batch grouping process timed out"))
return false, merr.New("reading from batch grouping process timed out", s.ctx, ctx)
}
}, opts)

View File

@ -46,7 +46,7 @@ func WithMySQL(parent context.Context, defaultDB string) (context.Context, *SQL)
mlog.Info("connecting to mysql server", sql.ctx)
var err error
sql.DB, err = sqlx.ConnectContext(innerCtx, "mysql", dsn)
return merr.Wrap(sql.ctx, err)
return merr.Wrap(err, sql.ctx)
})
ctx = mrun.WithStopHook(ctx, func(innerCtx context.Context) error {
mlog.Info("closing connection to sql server", sql.ctx)

View File

@ -57,7 +57,7 @@ func wrap(e error, cp bool) *err {
er2 := &err{
err: er.err,
attr: make(map[interface{}]interface{}, len(er.attr)),
attr: make(map[interface{}]interface{}, len(er.attr)+1),
}
for k, v := range er.attr {
er2.attr[k] = v
@ -102,47 +102,47 @@ func Value(e error, k interface{}) interface{} {
// WrapSkip is like Wrap but also allows for skipping extra stack frames when
// embedding the stack into the error.
func WrapSkip(ctx context.Context, e error, skip int, kvs ...interface{}) error {
func WrapSkip(e error, skip int, ctxs ...context.Context) error {
if e == nil {
return nil
}
er := wrap(e, true)
if _, ok := getStack(er); !ok {
setStack(er, skip+1)
}
if prevCtx, _ := er.attr[attrKeyCtx].(context.Context); prevCtx != nil {
ctx = mctx.MergeAnnotations(prevCtx, ctx)
}
if len(kvs) > 0 {
ctx = mctx.Annotate(ctx, kvs...)
ctx, _ := er.attr[attrKeyCtx].(context.Context)
if ctx != nil {
ctx = mctx.MergeAnnotationsInto(ctx, ctxs...)
} else if len(ctxs) > 0 {
ctx = mctx.MergeAnnotations(ctxs...)
}
er.attr[attrKeyCtx] = ctx
return er
}
// Wrap takes in an error and returns one which wraps it in merr's inner type,
// embedding the given Context (which can later be retrieved by Ctx) at the same
// time.
//
// For convenience, extra annotation information can be passed in here as well
// via the kvs argument. See mctx.Annotate for more information.
// Wrap returns a copy of the given error wrapped in merr's inner type. It will
// perform an mctx.MergeAnnotations on the given Contexts to create a new
// Context, and embed that in the returned error. If the given error already has
// an embedded Context then ctxs will be merged into that.
//
// This function automatically embeds stack information into the error as it's
// being stored, using WithStack, unless the error already has stack information
// in it.
func Wrap(ctx context.Context, e error, kvs ...interface{}) error {
return WrapSkip(ctx, e, 1, kvs...)
//
// Wrapping nil returns nil.
func Wrap(e error, ctx ...context.Context) error {
return WrapSkip(e, 1, ctx...)
}
// New is a shortcut for:
// merr.Wrap(ctx, errors.New(str), kvs...)
func New(ctx context.Context, str string, kvs ...interface{}) error {
return WrapSkip(ctx, errors.New(str), 1, kvs...)
// merr.Wrap(errors.New(str), ctxs...)
func New(str string, ctxs ...context.Context) error {
return WrapSkip(errors.New(str), 1, ctxs...)
}
// TODO it would be more convenient in a lot of cases if New and Wrap took in a
// list of Contexts.
type annotateKey string
func ctx(e error) context.Context {

View File

@ -10,24 +10,41 @@ import (
)
func TestError(t *T) {
e := New(context.Background(), "foo",
massert.Fatal(t, massert.Nil(Wrap(nil)))
ctx := mctx.Annotate(context.Background(),
"a", "aaa aaa\n",
"c", "ccc\nccc\n",
"d\t", "weird key but ok",
)
"d\t", "weird key but ok")
{
e := New("foo", ctx)
exp := `foo
* a: aaa aaa
* c:
ccc
ccc
* d: weird key but ok
* errLoc: merr/merr_test.go:13`
* errLoc: merr/merr_test.go:19`
massert.Fatal(t, massert.Equal(exp, e.Error()))
}
{
e := Wrap(errors.New("foo"), ctx)
exp := `foo
* a: aaa aaa
* c:
ccc
ccc
* d: weird key but ok
* errLoc: merr/merr_test.go:31`
massert.Fatal(t, massert.Equal(exp, e.Error()))
}
}
func TestBase(t *T) {
errFoo, errBar := errors.New("foo"), errors.New("bar")
erFoo := Wrap(context.Background(), errFoo)
erFoo := Wrap(errFoo)
massert.Fatal(t, massert.All(
massert.Nil(Base(nil)),
massert.Equal(errFoo, Base(erFoo)),
@ -45,7 +62,7 @@ func TestValue(t *T) {
massert.Nil(Value(nil, "foo")),
))
e1 := New(context.Background(), "foo")
e1 := New("foo")
e1 = WithValue(e1, "a", "A")
e2 := WithValue(errors.New("bar"), "a", "A")
massert.Fatal(t, massert.All(
@ -62,7 +79,7 @@ func TestValue(t *T) {
}
func mkErr(ctx context.Context, err error) error {
return Wrap(ctx, err) // it's important that this is line 65
return Wrap(err, ctx)
}
func TestCtx(t *T) {
@ -72,14 +89,14 @@ func TestCtx(t *T) {
// use mkErr so that it's easy to test that the stack info isn't overwritten
// when Wrap is called with ctxB.
e := mkErr(ctxA, errors.New("hello"))
e = Wrap(ctxB, e)
e = Wrap(e, ctxB)
err := massert.Equal(map[string]string{
"0": "ZERO",
"1": "ONE",
"2": "TWO",
"err": "hello",
"errLoc": "merr/merr_test.go:65",
"errLoc": "merr/merr_test.go:80",
}, mctx.Annotations(Context(e)).StringMap()).Assert()
if err != nil {
t.Fatal(err)

View File

@ -1,7 +1,6 @@
package merr
import (
"context"
"strings"
. "testing"
@ -9,7 +8,7 @@ import (
)
func TestStack(t *T) {
foo := New(context.Background(), "test")
foo := New("test")
fooStack, ok := Stack(foo)
massert.Fatal(t, massert.Equal(true, ok))