diff options
Diffstat (limited to 'vendor/github.com/getlantern/ops')
-rw-r--r-- | vendor/github.com/getlantern/ops/ops.go | 154 | ||||
-rw-r--r-- | vendor/github.com/getlantern/ops/ops_test.go | 76 |
2 files changed, 230 insertions, 0 deletions
diff --git a/vendor/github.com/getlantern/ops/ops.go b/vendor/github.com/getlantern/ops/ops.go new file mode 100644 index 0000000..136302d --- /dev/null +++ b/vendor/github.com/getlantern/ops/ops.go @@ -0,0 +1,154 @@ +// Package ops provides a facility for tracking the processing of operations, +// including contextual metadata about the operation and their final success or +// failure. An op is assumed to have succeeded if by the time of calling Exit() +// no errors have been reported. The final status can be reported to a metrics +// facility. +package ops + +import ( + "sync" + "sync/atomic" + + "github.com/getlantern/context" +) + +var ( + cm = context.NewManager() + reporters []Reporter + reportersMutex sync.RWMutex +) + +// Reporter is a function that reports the success or failure of an Op. If +// failure is nil, the Op can be considered successful. +type Reporter func(failure error, ctx map[string]interface{}) + +// Op represents an operation that's being performed. It mimics the API of +// context.Context. +type Op interface { + // Begin marks the beginning of an Op under this Op. + Begin(name string) Op + + // Go starts the given function on a new goroutine. + Go(fn func()) + + // End marks the end of this op, at which point the Op will report its success + // or failure to all registered Reporters. + End() + + // Cancel cancels this op so that even if End() is called later, it will not + // report its success or failure. + Cancel() + + // Set puts a key->value pair into the current Op's context. + Set(key string, value interface{}) Op + + // SetDynamic puts a key->value pair into the current Op's context, where the + // value is generated by a function that gets evaluated at every Read. + SetDynamic(key string, valueFN func() interface{}) Op + + // FailIf marks this Op as failed if the given err is not nil. If FailIf is + // called multiple times, the latest error will be reported as the failure. + // Returns the original error for convenient chaining. + FailIf(err error) error +} + +type op struct { + ctx context.Context + canceled bool + failure atomic.Value +} + +// RegisterReporter registers the given reporter. +func RegisterReporter(reporter Reporter) { + reportersMutex.Lock() + reporters = append(reporters, reporter) + reportersMutex.Unlock() +} + +// Begin marks the beginning of a new Op. +func Begin(name string) Op { + return &op{ctx: cm.Enter().Put("op", name).PutIfAbsent("root_op", name)} +} + +func (o *op) Begin(name string) Op { + return &op{ctx: o.ctx.Enter().Put("op", name).PutIfAbsent("root_op", name)} +} + +func (o *op) Go(fn func()) { + o.ctx.Go(fn) +} + +// Go mimics the method from context.Manager. +func Go(fn func()) { + cm.Go(fn) +} + +func (o *op) Cancel() { + o.canceled = true +} + +func (o *op) End() { + if o.canceled { + return + } + + var reportersCopy []Reporter + reportersMutex.RLock() + if len(reporters) > 0 { + reportersCopy = make([]Reporter, len(reporters)) + copy(reportersCopy, reporters) + } + reportersMutex.RUnlock() + + if len(reportersCopy) > 0 { + var failure error + _failure := o.failure.Load() + ctx := o.ctx.AsMap(_failure, true) + if _failure != nil { + failure = _failure.(error) + _, errorSet := ctx["error"] + if !errorSet { + ctx["error"] = failure.Error() + } + } + for _, reporter := range reportersCopy { + reporter(failure, ctx) + } + } + + o.ctx.Exit() +} + +func (o *op) Set(key string, value interface{}) Op { + o.ctx.Put(key, value) + return o +} + +// SetGlobal puts a key->value pair into the global context, which is inherited +// by all Ops. +func SetGlobal(key string, value interface{}) { + cm.PutGlobal(key, value) +} + +func (o *op) SetDynamic(key string, valueFN func() interface{}) Op { + o.ctx.PutDynamic(key, valueFN) + return o +} + +// SetGlobalDynamic is like SetGlobal but uses a function to derive the value +// at read time. +func SetGlobalDynamic(key string, valueFN func() interface{}) { + cm.PutGlobalDynamic(key, valueFN) +} + +// AsMap mimics the method from context.Manager. +func AsMap(obj interface{}, includeGlobals bool) context.Map { + return cm.AsMap(obj, includeGlobals) +} + +func (o *op) FailIf(err error) error { + if err != nil { + o.failure.Store(err) + } + return err +} diff --git a/vendor/github.com/getlantern/ops/ops_test.go b/vendor/github.com/getlantern/ops/ops_test.go new file mode 100644 index 0000000..1c16e73 --- /dev/null +++ b/vendor/github.com/getlantern/ops/ops_test.go @@ -0,0 +1,76 @@ +package ops_test + +import ( + "sync" + "testing" + + "github.com/getlantern/errors" + "github.com/getlantern/ops" + "github.com/stretchr/testify/assert" +) + +func TestSuccess(t *testing.T) { + var reportedFailure error + var reportedCtx map[string]interface{} + report := func(failure error, ctx map[string]interface{}) { + reportedFailure = failure + reportedCtx = ctx + } + + ops.RegisterReporter(report) + ops.SetGlobal("g", "g1") + op := ops.Begin("test_success").Set("a", 1).SetDynamic("b", func() interface{} { return 2 }) + defer op.End() + innerOp := op.Begin("inside") + innerOp.FailIf(nil) + innerOp.End() + + assert.Nil(t, reportedFailure) + expectedCtx := map[string]interface{}{ + "op": "inside", + "root_op": "test_success", + "g": "g1", + "a": 1, + "b": 2, + } + assert.Equal(t, expectedCtx, reportedCtx) +} + +func TestFailure(t *testing.T) { + doTestFailure(t, false) +} + +func TestCancel(t *testing.T) { + doTestFailure(t, true) +} + +func doTestFailure(t *testing.T, cancel bool) { + var reportedFailure error + var reportedCtx map[string]interface{} + report := func(failure error, ctx map[string]interface{}) { + reportedFailure = failure + reportedCtx = ctx + } + + ops.RegisterReporter(report) + op := ops.Begin("test_failure") + var wg sync.WaitGroup + wg.Add(1) + op.Go(func() { + op.FailIf(errors.New("I failed").With("errorcontext", 5)) + wg.Done() + }) + wg.Wait() + if cancel { + op.Cancel() + } + op.End() + + if cancel { + assert.Nil(t, reportedFailure) + assert.Nil(t, reportedCtx) + } else { + assert.Contains(t, reportedFailure.Error(), "I failed") + assert.Equal(t, 5, reportedCtx["errorcontext"]) + } +} |