gddo-server/poller: add package poller Copied from https://go.googlesource.com/pkgsite/+/cb9361e611fe25baabab637bc260f48b345bd1da/internal/poller Change-Id: I02fe11b8ef1be38af705038c8ff5ef1c945c4f6c Reviewed-on: https://go-review.googlesource.com/c/gddo/+/285832 Trust: Julie Qiu <julie@golang.org> Run-TryBot: Julie Qiu <julie@golang.org> Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/gddo-server/poller/poller.go b/gddo-server/poller/poller.go new file mode 100644 index 0000000..f99b112 --- /dev/null +++ b/gddo-server/poller/poller.go
@@ -0,0 +1,75 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package poller supports periodic polling to load a value. +package poller + +import ( + "context" + "sync" + "time" +) + +// A Getter returns a value. +type Getter func(context.Context) (interface{}, error) + +// A Poller maintains a current value, and refreshes it by periodically +// polling for a new value. +type Poller struct { + getter Getter + onError func(error) + mu sync.Mutex + current interface{} +} + +// New creates a new poller with an initial value. The getter is invoked +// to obtain updated values. Errors returned from the getter are passed +// to onError. +func New(initial interface{}, getter Getter, onError func(error)) *Poller { + return &Poller{ + getter: getter, + onError: onError, + current: initial, + } +} + +// Start begins polling in a separate goroutine, at the given period. To stop +// the goroutine, cancel the context passed to Start. +func (p *Poller) Start(ctx context.Context, period time.Duration) { + ticker := time.NewTicker(period) + go func() { + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + ctx2, cancel := context.WithTimeout(ctx, period) + p.Poll(ctx2) + cancel() + } + } + }() +} + +// Poll calls p's getter immediately and synchronously. +func (p *Poller) Poll(ctx context.Context) { + next, err := p.getter(ctx) + if err != nil { + p.onError(err) + } else { + p.mu.Lock() + p.current = next + p.mu.Unlock() + } +} + +// Current returns the current value. Initially, this is the value passed to New. +// After each successful poll, the value is updated. +// If a poll fails, the value remains unchanged. +func (p *Poller) Current() interface{} { + p.mu.Lock() + defer p.mu.Unlock() + return p.current +}
diff --git a/gddo-server/poller/poller_test.go b/gddo-server/poller/poller_test.go new file mode 100644 index 0000000..2ed7c5c --- /dev/null +++ b/gddo-server/poller/poller_test.go
@@ -0,0 +1,56 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package poller + +import ( + "context" + "strconv" + "testing" + "time" +) + +type numError struct { + num int +} + +func (e numError) Error() string { return strconv.Itoa(e.num) } +func Test(t *testing.T) { + var goods, bads []int + cur := -1 + getter := func(context.Context) (interface{}, error) { + // Even: success; odd: failure. + cur++ + if cur%2 == 0 { + return cur, nil + } + return nil, numError{cur} + } + onError := func(err error) { + bads = append(bads, err.(numError).num) + } + p := New(cur, getter, onError) + if got, want := p.Current(), cur; got != want { + t.Fatalf("got %v, want %v", got, want) + } + ctx, cancel := context.WithCancel(context.Background()) + p.Start(ctx, 50*time.Millisecond) + time.Sleep(100 * time.Millisecond) // wait for first poll + for i := 0; i < 10; i++ { + goods = append(goods, p.Current().(int)) + time.Sleep(60 * time.Millisecond) + } + cancel() + // Expect goods to be all even and non-decreasing. + for i, g := range goods { + if g%2 != 0 || (i > 0 && goods[i-1] > g) { + t.Errorf("incorrect 'good' value %d", g) + } + } + // Expect bads to be consecutive odd numbers. + for i, b := range bads { + if b%2 == 0 || (i > 0 && bads[i-1]+2 != b) { + t.Errorf("incorrect 'bad' value %d", b) + } + } +}