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