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)
+ }
+ }
+}