Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(921)

Unified Diff: common/errors/tags.go

Issue 2951393002: [errors] de-specialize Transient in favor of Tags. (Closed)
Patch Set: copyright Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: common/errors/tags.go
diff --git a/common/errors/tags.go b/common/errors/tags.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc584bf80d950c71566808c2a947e6648266e57b
--- /dev/null
+++ b/common/errors/tags.go
@@ -0,0 +1,235 @@
+// Copyright 2017 The LUCI Authors. All rights reserved.
iannucci 2017/06/24 00:01:11 This is where the bulk of the new logic lives.
+// Use of this source code is governed under the Apache License, Version 2.0
+// that can be found in the LICENSE file.
+
+package errors
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+)
+
+type (
+ tagKey int
+
+ // TagKey objects are used for doing map lookups in GetTags, as well as
+ // checking for the presence and/or value of a tag on a specific error.
+ //
+ // The return values of NewBoolTag and NewTagger should be used directly as
+ // TagKeys.
+ TagKey interface {
+ isTagKey()
+
+ // returns true if the error contains a tag with this key.
+ In(err error) bool
+
+ // returns the value associated with this tag, if this error is tagged with
+ // this key.
+ ValueIn(err error) (val interface{}, ok bool)
+ }
+
+ // TagValue represents a (tag, value) to be used with Annotate.Tag, or may be
+ // applied to an error directly with the Apply method.
+ TagValue interface {
+ getKey() tagKey
+ getValue() interface{}
+
+ // Applies this TagValue to the provided error. This is equivalent to
+ // Annotate(err).Tag(tagValue).Err()
+ Apply(err error) error
+ }
+
+ tagValue struct {
+ key tagKey
+ value interface{}
+ }
+
+ // BoolTag is both a TagKey and a TagValue (whose data value is nil). This is
+ // obtained from NewBoolTag.
+ BoolTag interface {
dnj 2017/06/24 14:53:55 I'd: 1) Make the NewBoolTag method BoolTag. 2) Hav
iannucci 2017/06/24 20:16:10 Done.
+ TagKey
+ TagValue
+ }
+
+ // Tagger has a With method to mint TagValues with associated with its
+ // underlying tag. This is obtained from NewTagger.
+ Tagger interface {
+ TagKey
+ With(value interface{}) TagValue
+ }
+
+ tagDetail struct {
+ description string
+ requiresValue bool
+ validate func(v interface{})
+ }
+)
+
+var (
+ tagsLock sync.RWMutex
+ curTag tagKey
+ tags = map[tagKey]tagDetail{}
+)
+
+func getTagDetail(key tagKey) tagDetail {
+ tagsLock.RLock()
+ detail, ok := tags[key]
+ tagsLock.RUnlock()
+ if !ok {
+ panic("unknown tag")
+ }
+ return detail
+}
+
+// tagKey is actually secretly both a TagKey, a TagValue, and a Tagger. This is
+// done so that the tags map can be a very simple, direct, int lookup.
+//
+// Technically a user could cast a Tagger to a BoolTag, which would be strange,
+// however preventing this cast makes the whole thing much more complex.
dnj 2017/06/24 14:53:55 (Unless you delete the BoolTag interface)
iannucci 2017/06/24 20:16:10 They could still cast it to TagValue
+func (t tagKey) isTagKey() {}
+func (t tagKey) getKey() tagKey { return t }
+func (t tagKey) getValue() interface{} { return nil }
+func (t tagKey) String() string {
+ return fmt.Sprintf("errors.TagKey{%q}", getTagDetail(t).description)
+}
+
+func (t tagKey) Apply(err error) error {
+ detail := getTagDetail(t)
+ if detail.requiresValue {
+ panic(fmt.Sprintf("tag %q requires a value", detail.description))
+ }
+ if err == nil {
+ return nil
+ }
+ a := &Annotator{err, stackContext{FrameInfo: stackFrameInfoForError(1, err)}}
dnj 2017/06/24 14:53:54 We're dong a full captureStack every time we apply
iannucci 2017/06/24 20:16:09 If you apply it singly, then yes, if you apply man
+ return a.Tag(t).Err()
+}
+
+func (t tagKey) With(val interface{}) TagValue {
dnj 2017/06/24 14:53:55 I don't like the binary "Apply" XOR "With" as an i
iannucci 2017/06/24 20:16:10 I actually don't understand this comment at all...
+ detail := getTagDetail(t)
+ if !detail.requiresValue {
+ panic(fmt.Sprintf("tag %q doesn't allow a value", detail.description))
+ }
+
+ switch k := reflect.ValueOf(val).Kind(); {
+ case k < reflect.Array || k == reflect.String:
dnj 2017/06/24 14:53:54 I think it would be better to just enumerate the "
iannucci 2017/06/24 20:16:09 ALL OF THIS CODE IS GONE NOW!
+ default:
+ panic("Tagger.With(value) only takes simple types for `value`, not " + k.String())
dnj 2017/06/24 14:53:55 Why? I get that you're trying to not have this be
iannucci 2017/06/24 20:16:10 I was trying to prevent people from putting mutabl
+ }
+ if detail.validate != nil {
dnj 2017/06/24 14:53:55 Similar concerns for validation, as either validat
iannucci 2017/06/24 20:16:10 Done.
+ detail.validate(val)
+ }
+ return tagValue{t, val}
+}
+
+func (t tagKey) In(err error) bool {
+ _, ok := t.ValueIn(err)
+ return ok
+}
+
+// ValueIn implements TagKey.
dnj 2017/06/24 14:53:55 Comment not needed, or comment other methods unifo
iannucci 2017/06/24 20:16:10 Acknowledged.
+func (t tagKey) ValueIn(err error) (value interface{}, ok bool) {
dnj 2017/06/24 14:53:55 Is it OK that this traverses MultiError? Probably,
iannucci 2017/06/24 20:16:09 yes, I have a test for it too
+ Walk(err, func(err error) bool {
+ if sc, isSC := err.(stackContexter); isSC {
+ if value, ok = sc.stackContext().Tags[t]; ok {
+ return false
+ }
+ }
+ return true
+ })
+ return
+}
+
+func (t tagValue) getKey() tagKey { return t.key }
+func (t tagValue) getValue() interface{} { return t.value }
+func (t tagValue) Apply(err error) error {
+ if err == nil {
+ return nil
+ }
+ a := &Annotator{err, stackContext{FrameInfo: stackFrameInfoForError(1, err)}}
dnj 2017/06/24 14:53:55 (Same, full stack trace seems like a not great thi
iannucci 2017/06/24 20:16:10 If you need to apply more than one tag, use the An
+ return a.Tag(t).Err()
+}
+
+// NewBoolTag creates a new tag with a fixed "nil" value.
+//
+// You should use this in the top level context of your package like:
+// var myTag = errors.NewBoolTag("this error is a user error")
+//
+// You could then use it like:
+// err = myTag.Apply(err)
+// myTag.In(err) // == true
+// myTag.ValueIn(err) // == (nil, true)
+func NewBoolTag(description string) BoolTag {
dnj 2017/06/24 14:53:55 Note that there's nothing stopping someone from na
iannucci 2017/06/24 20:16:10 That's why I had "New" here. It would also mean th
+ tagsLock.Lock()
+ defer tagsLock.Unlock()
+
+ t := curTag + 1
+ curTag++
+ tags[t] = tagDetail{description: description}
+
+ return t
+}
+
+// NewTagger creates a new Tagger which can be added to errors with
+// a corresponding value.
+//
+// Values used with this tag are must be a simple type (i.e. has a reflect.Kind
+// which is a base data type like bool, string, or int).
+//
+// The `validate` function is an optional argument which allows you to further
+// validate values assocaited with this tag. The function should panic if the
+// validation fails.
+//
+// You should use this in the top level context of your package like:
+// type CodeType int
+// var CodeTag = errors.NewTagger(
+// "has a response code", func(v interface) {
+// v.(CodeType) // panics if v is not CodeType
+// })
+//
+// You could then use it like:
+// err = CodeTag.With(CodeType(100)).Apply(err)
+// CodeTag.In(err) // == true
+// CodeTag.ValueIn(err) // == (CodeType(100), true)
+func NewTagger(description string, validate ...func(v interface{})) Tagger {
+ var valFn func(interface{})
+ switch len(validate) {
+ case 0:
+ case 1:
+ valFn = validate[0]
+ default:
+ panic("NewTagger takes exactly 0 or 1 validate function")
+ }
+
+ tagsLock.Lock()
+ defer tagsLock.Unlock()
+
+ t := curTag + 1
+ curTag++
+ tags[t] = tagDetail{description, true, valFn}
+
+ return t
+}
+
+// GetTags returns a map of all TagKeys set in this error to their value.
+//
+// A nil value means that the tag is present, but has a nil associated value.
+//
+// This is done in a depth-first traversal of the error stack, with the
+// most-recently-set value of the tag taking precendence.
+func GetTags(err error) map[TagKey]interface{} {
+ ret := map[TagKey]interface{}{}
+ Walk(err, func(err error) bool {
+ if sc, ok := err.(stackContexter); ok {
+ ctx := sc.stackContext()
+ for k, v := range ctx.Tags {
+ if _, ok := ret[k]; !ok {
+ ret[k] = v
+ }
+ }
+ }
+ return true
+ })
+ return ret
+}

Powered by Google App Engine
This is Rietveld 408576698