diff --git a/.golangci.yml b/.golangci.yml index 39eb9771..df4792bc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,7 @@ run: deadline: 5m skip-files: [ ] - skip-dirs: [ ] + skip-dirs: ["internal/holsterv4"] linters-settings: govet: diff --git a/Makefile b/Makefile index eb157866..331bb8ce 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ export GO111MODULE=on default: clean checks test test: clean - go test -race -cover ./... + go test -race -cover -count 1 ./... test-verbose: clean go test -v -race -cover ./... diff --git a/cbreaker/cbreaker.go b/cbreaker/cbreaker.go index 3fec6ba9..a2176613 100644 --- a/cbreaker/cbreaker.go +++ b/cbreaker/cbreaker.go @@ -31,8 +31,8 @@ import ( "sync" "time" - "github.com/mailgun/timetools" log "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/memmetrics" "github.com/vulcand/oxy/utils" ) @@ -51,18 +51,16 @@ type CircuitBreaker struct { onStandby SideEffect state cbState - until time.Time + until clock.Time rc *ratioController checkPeriod time.Duration - lastCheck time.Time + lastCheck clock.Time fallback http.Handler next http.Handler - clock timetools.TimeProvider - log *log.Logger } @@ -72,7 +70,6 @@ func New(next http.Handler, expression string, options ...CircuitBreakerOption) m: &sync.RWMutex{}, next: next, // Default values. Might be overwritten by options below. - clock: &timetools.RealTime{}, checkPeriod: defaultCheckPeriod, fallbackDuration: defaultFallbackDuration, recoveryDuration: defaultRecoveryDuration, @@ -151,7 +148,7 @@ func (c *CircuitBreaker) activateFallback(_ http.ResponseWriter, _ *http.Request // someone else has set it to standby just now return false case stateTripped: - if c.clock.UtcNow().Before(c.until) { + if clock.Now().UTC().Before(c.until) { return true } // We have been in active state enough, enter recovering state @@ -159,8 +156,8 @@ func (c *CircuitBreaker) activateFallback(_ http.ResponseWriter, _ *http.Request fallthrough case stateRecovering: // We have been in recovering state enough, enter standby and allow request - if c.clock.UtcNow().After(c.until) { - c.setState(stateStandby, c.clock.UtcNow()) + if clock.Now().UTC().After(c.until) { + c.setState(stateStandby, clock.Now().UTC()) return false } // ratio controller allows this request @@ -173,12 +170,12 @@ func (c *CircuitBreaker) activateFallback(_ http.ResponseWriter, _ *http.Request } func (c *CircuitBreaker) serve(w http.ResponseWriter, req *http.Request) { - start := c.clock.UtcNow() + start := clock.Now().UTC() p := utils.NewProxyWriterWithLogger(w, c.log) c.next.ServeHTTP(p, req) - latency := c.clock.UtcNow().Sub(start) + latency := clock.Now().UTC().Sub(start) c.metrics.Record(p.StatusCode(), latency) // Note that this call is less expensive than it looks -- checkCondition only performs the real check @@ -229,7 +226,7 @@ func (c *CircuitBreaker) setState(state cbState, until time.Time) { func (c *CircuitBreaker) timeToCheck() bool { c.m.RLock() defer c.m.RUnlock() - return c.clock.UtcNow().After(c.lastCheck) + return clock.Now().UTC().After(c.lastCheck) } // Checks if tripping condition matches and sets circuit breaker to the tripped state. @@ -242,10 +239,10 @@ func (c *CircuitBreaker) checkAndSet() { defer c.m.Unlock() // Other goroutine could have updated the lastCheck variable before we grabbed mutex - if !c.clock.UtcNow().After(c.lastCheck) { + if !clock.Now().UTC().After(c.lastCheck) { return } - c.lastCheck = c.clock.UtcNow().Add(c.checkPeriod) + c.lastCheck = clock.Now().UTC().Add(c.checkPeriod) if c.state == stateTripped { c.log.Debugf("%v skip set tripped", c) @@ -256,28 +253,19 @@ func (c *CircuitBreaker) checkAndSet() { return } - c.setState(stateTripped, c.clock.UtcNow().Add(c.fallbackDuration)) + c.setState(stateTripped, clock.Now().UTC().Add(c.fallbackDuration)) c.metrics.Reset() } func (c *CircuitBreaker) setRecovering() { - c.setState(stateRecovering, c.clock.UtcNow().Add(c.recoveryDuration)) - c.rc = newRatioController(c.clock, c.recoveryDuration, c.log) + c.setState(stateRecovering, clock.Now().UTC().Add(c.recoveryDuration)) + c.rc = newRatioController(c.recoveryDuration, c.log) } // CircuitBreakerOption represents an option you can pass to New. // See the documentation for the individual options below. type CircuitBreakerOption func(*CircuitBreaker) error -// Clock allows you to fake che CircuitBreaker's view of the current time. -// Intended for unit tests. -func Clock(clock timetools.TimeProvider) CircuitBreakerOption { - return func(c *CircuitBreaker) error { - c.clock = clock - return nil - } -} - // FallbackDuration is how long the CircuitBreaker will remain in the Tripped // state before trying to recover. func FallbackDuration(d time.Duration) CircuitBreakerOption { @@ -357,9 +345,9 @@ const ( ) const ( - defaultFallbackDuration = 10 * time.Second - defaultRecoveryDuration = 10 * time.Second - defaultCheckPeriod = 100 * time.Millisecond + defaultFallbackDuration = 10 * clock.Second + defaultRecoveryDuration = 10 * clock.Second + defaultCheckPeriod = 100 * clock.Millisecond ) var defaultFallback = &fallback{} diff --git a/cbreaker/cbreaker_test.go b/cbreaker/cbreaker_test.go index e78fec87..a500de0f 100644 --- a/cbreaker/cbreaker_test.go +++ b/cbreaker/cbreaker_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/memmetrics" "github.com/vulcand/oxy/testutils" ) @@ -39,9 +40,10 @@ func TestFullCycle(t *testing.T) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - cb, err := New(handler, triggerNetRatio, Clock(clock)) + cb, err := New(handler, triggerNetRatio) require.NoError(t, err) srv := httptest.NewServer(cb) @@ -52,27 +54,27 @@ func TestFullCycle(t *testing.T) { assert.Equal(t, http.StatusOK, re.StatusCode) cb.metrics = statsNetErrors(0.6) - clock.CurrentTime = clock.CurrentTime.Add(defaultCheckPeriod + time.Millisecond) + clock.Advance(defaultCheckPeriod + clock.Millisecond) _, _, err = testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, cbState(stateTripped), cb.state) // Some time has passed, but we are still in trapped state. - clock.CurrentTime = clock.CurrentTime.Add(9 * time.Second) + clock.Advance(9 * clock.Second) re, _, err = testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode) assert.Equal(t, cbState(stateTripped), cb.state) // We should be in recovering state by now - clock.CurrentTime = clock.CurrentTime.Add(time.Second*1 + time.Millisecond) + clock.Advance(clock.Second*1 + clock.Millisecond) re, _, err = testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode) assert.Equal(t, cbState(stateRecovering), cb.state) // 5 seconds after we should be allowing some requests to pass - clock.CurrentTime = clock.CurrentTime.Add(5 * time.Second) + clock.Advance(5 * clock.Second) allowed := 0 for i := 0; i < 100; i++ { re, _, err = testutils.Get(srv.URL) @@ -83,7 +85,7 @@ func TestFullCycle(t *testing.T) { assert.NotEqual(t, 0, allowed) // After some time, all is good and we should be in stand by mode again - clock.CurrentTime = clock.CurrentTime.Add(5*time.Second + time.Millisecond) + clock.Advance(5*clock.Second + clock.Millisecond) re, _, err = testutils.Get(srv.URL) assert.Equal(t, cbState(stateStandby), cb.state) require.NoError(t, err) @@ -101,7 +103,7 @@ func TestRedirectWithPath(t *testing.T) { }) require.NoError(t, err) - cb, err := New(handler, triggerNetRatio, Clock(testutils.GetClock()), Fallback(fallbackRedirectPath)) + cb, err := New(handler, triggerNetRatio, Fallback(fallbackRedirectPath)) require.NoError(t, err) srv := httptest.NewServer(cb) @@ -131,7 +133,7 @@ func TestRedirect(t *testing.T) { fallbackRedirect, err := NewRedirectFallback(Redirect{URL: "http://localhost:5000"}) require.NoError(t, err) - cb, err := New(handler, triggerNetRatio, Clock(testutils.GetClock()), Fallback(fallbackRedirect)) + cb, err := New(handler, triggerNetRatio, Fallback(fallbackRedirect)) require.NoError(t, err) srv := httptest.NewServer(cb) @@ -158,9 +160,10 @@ func TestTriggerDuringRecovery(t *testing.T) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - cb, err := New(handler, triggerNetRatio, Clock(clock), CheckPeriod(time.Microsecond)) + cb, err := New(handler, triggerNetRatio, CheckPeriod(clock.Microsecond)) require.NoError(t, err) srv := httptest.NewServer(cb) @@ -172,14 +175,14 @@ func TestTriggerDuringRecovery(t *testing.T) { assert.Equal(t, cbState(stateTripped), cb.state) // We should be in recovering state by now - clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond) + clock.Advance(10*clock.Second + clock.Millisecond) re, _, err := testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, http.StatusServiceUnavailable, re.StatusCode) assert.Equal(t, cbState(stateRecovering), cb.state) // We have matched error condition during recovery state and are going back to tripped state - clock.CurrentTime = clock.CurrentTime.Add(5 * time.Second) + clock.Advance(5 * clock.Second) cb.metrics = statsNetErrors(0.6) allowed := 0 for i := 0; i < 100; i++ { @@ -234,9 +237,10 @@ func TestSideEffects(t *testing.T) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - cb, err := New(handler, triggerNetRatio, Clock(clock), CheckPeriod(time.Microsecond), OnTripped(onTripped), OnStandby(onStandby)) + cb, err := New(handler, triggerNetRatio, CheckPeriod(clock.Microsecond), OnTripped(onTripped), OnStandby(onStandby)) require.NoError(t, err) srv := httptest.NewServer(cb) @@ -254,19 +258,19 @@ func TestSideEffects(t *testing.T) { assert.Equal(t, "/post.json", req.URL.Path) assert.Equal(t, `{"Key": ["val1", "val2"]}`, string(srv1Body)) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) - case <-time.After(time.Second): + case <-clock.After(clock.Second): t.Error("timeout waiting for side effect to kick off") } // Transition to recovering state - clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond) + clock.Advance(10*clock.Second + clock.Millisecond) cb.metrics = statsOK() _, _, err = testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, cbState(stateRecovering), cb.state) // Going back to standby - clock.CurrentTime = clock.CurrentTime.Add(10*time.Second + time.Millisecond) + clock.Advance(10*clock.Second + clock.Millisecond) _, _, err = testutils.Get(srv.URL) require.NoError(t, err) assert.Equal(t, cbState(stateStandby), cb.state) @@ -276,7 +280,7 @@ func TestSideEffects(t *testing.T) { assert.Equal(t, http.MethodPost, req.Method) assert.Equal(t, "/post", req.URL.Path) assert.Equal(t, url.Values{"key": []string{"val1", "val2"}}, req.Form) - case <-time.After(time.Second): + case <-clock.After(clock.Second): t.Error("timeout waiting for side effect to kick off") } } diff --git a/cbreaker/predicates.go b/cbreaker/predicates.go index 3253a906..34a66ab8 100644 --- a/cbreaker/predicates.go +++ b/cbreaker/predicates.go @@ -2,8 +2,8 @@ package cbreaker import ( "fmt" - "time" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/predicate" ) @@ -53,7 +53,7 @@ func latencyAtQuantile(quantile float64) toInt { c.log.Errorf("Failed to get latency histogram, for %v error: %v", c, err) return 0 } - return int(h.LatencyAtQuantile(quantile) / time.Millisecond) + return int(h.LatencyAtQuantile(quantile) / clock.Millisecond) } } diff --git a/cbreaker/predicates_test.go b/cbreaker/predicates_test.go index 524fb77e..15e3827a 100644 --- a/cbreaker/predicates_test.go +++ b/cbreaker/predicates_test.go @@ -2,10 +2,10 @@ package cbreaker import ( "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/memmetrics" ) @@ -27,12 +27,12 @@ func TestTripped(t *testing.T) { }, { expression: "LatencyAtQuantileMS(50.0) > 50", - metrics: statsLatencyAtQuantile(50, time.Millisecond*51), + metrics: statsLatencyAtQuantile(50, clock.Millisecond*51), expected: true, }, { expression: "LatencyAtQuantileMS(50.0) < 50", - metrics: statsLatencyAtQuantile(50, time.Millisecond*51), + metrics: statsLatencyAtQuantile(50, clock.Millisecond*51), expected: false, }, { diff --git a/cbreaker/ratio.go b/cbreaker/ratio.go index 3cf9b580..7a12d64f 100644 --- a/cbreaker/ratio.go +++ b/cbreaker/ratio.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/mailgun/timetools" "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) // ratioController allows passing portions traffic back to the endpoints, @@ -15,21 +15,18 @@ import ( // type ratioController struct { duration time.Duration - start time.Time - tm timetools.TimeProvider + start clock.Time allowed int denied int log *logrus.Logger } -func newRatioController(tm timetools.TimeProvider, rampUp time.Duration, log *logrus.Logger) *ratioController { +func newRatioController(rampUp time.Duration, log *logrus.Logger) *ratioController { return &ratioController{ duration: rampUp, - tm: tm, - start: tm.UtcNow(), - - log: log, + start: clock.Now().UTC(), + log: log, } } @@ -70,5 +67,5 @@ func (r *ratioController) targetRatio() float64 { // after this point to achieve ratio of 1 (that can never be reached unless d is 0) // so we stop from there multiplier := 0.5 / float64(r.duration) - return multiplier * float64(r.tm.UtcNow().Sub(r.start)) + return multiplier * float64(clock.Now().UTC().Sub(r.start)) } diff --git a/cbreaker/ratio_test.go b/cbreaker/ratio_test.go index c780f41a..8a57e46a 100644 --- a/cbreaker/ratio_test.go +++ b/cbreaker/ratio_test.go @@ -3,25 +3,29 @@ package cbreaker import ( "math" "testing" - "time" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) func TestRampUp(t *testing.T) { - clock := testutils.GetClock() - duration := 10 * time.Second - rc := newRatioController(clock, duration, log.StandardLogger()) + done := testutils.FreezeTime() + defer done() + duration := 10 * clock.Second + rc := newRatioController(duration, log.StandardLogger()) allowed, denied := 0, 0 - for i := 0; i < int(duration/time.Millisecond); i++ { + for i := 0; i < int(duration/clock.Millisecond); i++ { ratio := sendRequest(&allowed, &denied, rc) expected := rc.targetRatio() diff := math.Abs(expected - ratio) + t.Log("Ratio", ratio) + t.Log("Expected", expected) + t.Log("Diff", diff) assert.EqualValues(t, 0, round(diff, 0.5, 1)) - clock.CurrentTime = clock.CurrentTime.Add(time.Millisecond) + clock.Advance(clock.Millisecond) } } diff --git a/forward/fwd.go b/forward/fwd.go index e25eb399..02ec52bc 100644 --- a/forward/fwd.go +++ b/forward/fwd.go @@ -19,6 +19,7 @@ import ( "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/utils" ) @@ -182,7 +183,7 @@ type httpForwarder struct { websocketConnectionClosedHook func(req *http.Request, conn net.Conn) } -const defaultFlushInterval = time.Duration(100) * time.Millisecond +const defaultFlushInterval = 100 * clock.Millisecond // Connection states. const ( @@ -492,7 +493,7 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ct defer logEntry.Debug("vulcand/oxy/forward/http: completed ServeHttp on request") } - start := time.Now().UTC() + start := clock.Now().UTC() outReq := new(http.Request) *outReq = *inReq // includes shallow copies of maps, but we handle this in Director @@ -514,14 +515,14 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, inReq *http.Request, ct if inReq.TLS != nil { f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v", - inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start), + inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start), inReq.TLS.Version, inReq.TLS.DidResume, inReq.TLS.CipherSuite, inReq.TLS.ServerName) } else { f.log.Debugf("vulcand/oxy/forward/http: Round trip: %v, code: %v, Length: %v, duration: %v", - inReq.URL, pw.StatusCode(), pw.GetLength(), time.Now().UTC().Sub(start)) + inReq.URL, pw.StatusCode(), pw.GetLength(), clock.Now().UTC().Sub(start)) } } else { revproxy.ServeHTTP(w, outReq) diff --git a/forward/fwd_test.go b/forward/fwd_test.go index 8f0a13b6..8c447b12 100644 --- a/forward/fwd_test.go +++ b/forward/fwd_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "net/url" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" "github.com/vulcand/oxy/utils" ) @@ -218,14 +218,14 @@ func TestCustomRewriter(t *testing.T) { func TestCustomTransportTimeout(t *testing.T) { srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) { - time.Sleep(20 * time.Millisecond) + clock.Sleep(20 * clock.Millisecond) _, _ = w.Write([]byte("hello")) }) defer srv.Close() f, err := New(RoundTripper( &http.Transport{ - ResponseHeaderTimeout: 5 * time.Millisecond, + ResponseHeaderTimeout: 5 * clock.Millisecond, })) require.NoError(t, err) diff --git a/forward/fwd_websocket_test.go b/forward/fwd_websocket_test.go index 0f5e4a5e..8df6aadf 100644 --- a/forward/fwd_websocket_test.go +++ b/forward/fwd_websocket_test.go @@ -9,11 +9,11 @@ import ( "net/http/httptest" "runtime" "testing" - "time" gorillawebsocket "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" "golang.org/x/net/websocket" ) @@ -98,7 +98,7 @@ func TestWebsocketConnectionClosedHook(t *testing.T) { _ = conn.Close() select { - case <-time.After(time.Second): + case <-clock.After(clock.Second): t.Errorf("Websocket Hook not called") case <-closed: } @@ -109,7 +109,7 @@ func TestWebSocketPingPong(t *testing.T) { require.NoError(t, err) upgrader := gorillawebsocket.Upgrader{ - HandshakeTimeout: 10 * time.Second, + HandshakeTimeout: 10 * clock.Second, CheckOrigin: func(*http.Request) bool { return true }, @@ -158,7 +158,7 @@ func TestWebSocketPingPong(t *testing.T) { return badErr }) - _ = conn.WriteControl(gorillawebsocket.PingMessage, []byte("Ping"), time.Now().Add(time.Second)) + _ = conn.WriteControl(gorillawebsocket.PingMessage, []byte("Ping"), clock.Now().Add(clock.Second)) _, _, err = conn.ReadMessage() if err != goodErr { @@ -316,7 +316,7 @@ func TestWebSocketNumGoRoutine(t *testing.T) { _ = conn.Close() - time.Sleep(time.Second) + clock.Sleep(clock.Second) assert.Equal(t, num, runtime.NumGoroutine()) } @@ -716,7 +716,7 @@ func TestWebSocketTransferTLSConfig(t *testing.T) { assert.Equal(t, "ok", resp) } -const dialTimeout = time.Second +const dialTimeout = clock.Second type websocketRequestOpt func(w *websocketRequest) diff --git a/go.mod b/go.mod index 6e8dd97f..1e8c7ca1 100644 --- a/go.mod +++ b/go.mod @@ -6,24 +6,21 @@ require ( github.com/HdrHistogram/hdrhistogram-go v1.1.0 github.com/gorilla/websocket v1.4.2 github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb - github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51 - github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f github.com/segmentio/fasthash v1.0.3 - github.com/sirupsen/logrus v1.4.2 + github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 github.com/vulcand/predicate v1.1.0 - golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f // indirect github.com/jonboulle/clockwork v0.1.0 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect - github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 // indirect - golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect - launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect + golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 3da2a231..b1bde0e5 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f h1:68WxnfBzJRYktZ30fmIjGQ74RsXYLoeH2/NITPktTMY= @@ -19,38 +20,30 @@ github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdO github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f h1:aOqSQstfwSx9+tcM/xiKTio3IVjs7ZL2vU8kI9bI6bM= -github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f/go.mod h1:V3EvCedtJTvUYzJF2GZMRB0JMlai+6cBu3VCTQz33GQ= github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb h1:m2FGM8K2LC9Zyt/7zbQNn5Uvf/YV7vFWKtoMcC7hHU8= github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb/go.mod h1:E0vRBBIQUHcRtmL/oR6w/jehh4FJqJFxe86gBnw9gXc= -github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51 h1:Kg/NPZLLC3aAFr1YToMs98dbCdhootQ1hZIvZU28hAQ= -github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4= -github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f h1:ZZYhg16XocqSKPGNQAe0aeweNtFxuedbwwb4fSlg7h4= -github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f/go.mod h1:8heskWJ5c0v5J9WH89ADhyal1DOZcayll8fSbhB+/9A= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vulcand/predicate v1.1.0 h1:Gq/uWopa4rx/tnZu2opOSBqHK63Yqlou/SzrbwdJiNg= github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -64,17 +57,24 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE= +golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -88,8 +88,7 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= -launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/holsterv4/LICENSE b/internal/holsterv4/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/internal/holsterv4/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/holsterv4/README.md b/internal/holsterv4/README.md new file mode 100644 index 00000000..2a87deb4 --- /dev/null +++ b/internal/holsterv4/README.md @@ -0,0 +1,24 @@ +# What is this? + +This is a vendored copy of 2 packages (`clock` and `collections`) from the +github.com/mailgun/holster@v4.2.5 module. + +The `clock` package was completely copied over and the following modifications +were made: + +* pkg/errors was replaced with the stdlib errors package / fmt.Errorf's %w; +* import names changed in blackbox test packages; +* a small race condition in the testing logic was fixed using the provided + mutex. + +The `collections` package only contains the priority_queue and ttlmap and +corresponding test files. The only changes made to those files were to adjust +the package names to use the vendored packages. + +## Why + +TL;DR: holster is a utility repo with many dependencies and even with graph +pruning using it in oxy can transitively impact oxy users in negative ways by +forcing version bumps (at the least). + +Full details can be found here: https://github.com/vulcand/oxy/pull/223 diff --git a/internal/holsterv4/clock/README.md b/internal/holsterv4/clock/README.md new file mode 100644 index 00000000..5caff759 --- /dev/null +++ b/internal/holsterv4/clock/README.md @@ -0,0 +1,47 @@ +# Clock + +A drop in (almost) replacement for the system `time` package. It provides a way +to make scheduled calls, timers and tickers deterministic in tests. By default +it forwards all calls to the system `time` package. In test, however, it is +possible to enable the frozen clock mode, and advance time manually to make +scheduled even trigger at certain moments. + +# Usage + +```go +package foo + +import ( + "testing" + + "github.com/vulcand/oxy/internal/holsterv4/clock" + "github.com/stretchr/testify/assert" +) + +func TestSleep(t *testing.T) { + // Freeze switches the clock package to the frozen clock mode. You need to + // advance time manually from now on. Note that all scheduled events, timers + // and ticker created before this call keep operating in real time. + // + // The initial time is set to now here, but you can set any datetime. + clock.Freeze(clock.Now()) + // Do not forget to revert the effect of Freeze at the end of the test. + defer clock.Unfreeze() + + var fired bool + + clock.AfterFunc(100*clock.Millisecond, func() { + fired = true + }) + clock.Advance(93*clock.Millisecond) + + // Advance will make all fire all events, timers, tickers that are + // scheduled for the passed period of time. Note that scheduled functions + // are called from within Advanced unlike system time package that calls + // them in their own goroutine. + assert.Equal(t, 97*clock.Millisecond, clock.Advance(6*clock.Millisecond)) + assert.True(t, fired) + assert.Equal(t, 100*clock.Millisecond, clock.Advance(1*clock.Millisecond)) + assert.True(t, fired) +} +``` diff --git a/internal/holsterv4/clock/clock.go b/internal/holsterv4/clock/clock.go new file mode 100644 index 00000000..ca329adc --- /dev/null +++ b/internal/holsterv4/clock/clock.go @@ -0,0 +1,105 @@ +//go:build !holster_test_mode + +// Package clock provides the same functions as the system package time. In +// production it forwards all calls to the system time package, but in tests +// the time can be frozen by calling Freeze function and from that point it has +// to be advanced manually with Advance function making all scheduled calls +// deterministic. +// +// The functions provided by the package have the same parameters and return +// values as their system counterparts with a few exceptions. Where either +// *time.Timer or *time.Ticker is returned by a system function, the clock +// package counterpart returns clock.Timer or clock.Ticker interface +// respectively. The interfaces provide API as respective structs except C is +// not a channel, but a function that returns <-chan time.Time. +package clock + +import "time" + +var ( + frozenAt time.Time + realtime = &systemTime{} + provider Clock = realtime +) + +// Freeze after this function is called all time related functions start +// generate deterministic timers that are triggered by Advance function. It is +// supposed to be used in tests only. Returns an Unfreezer so it can be a +// one-liner in tests: defer clock.Freeze(clock.Now()).Unfreeze() +func Freeze(now time.Time) Unfreezer { + frozenAt = now.UTC() + provider = &frozenTime{now: now} + return Unfreezer{} +} + +type Unfreezer struct{} + +func (u Unfreezer) Unfreeze() { + Unfreeze() +} + +// Unfreeze reverses effect of Freeze. +func Unfreeze() { + provider = realtime +} + +// Realtime returns a clock provider wrapping the SDK's time package. It is +// supposed to be used in tests when time is frozen to schedule test timeouts. +func Realtime() Clock { + return realtime +} + +// Makes the deterministic time move forward by the specified duration, firing +// timers along the way in the natural order. It returns how much time has +// passed since it was frozen. So you can assert on the return value in tests +// to make it explicit where you stand on the deterministic time scale. +func Advance(d time.Duration) time.Duration { + ft, ok := provider.(*frozenTime) + if !ok { + panic("Freeze time first!") + } + ft.advance(d) + return Now().UTC().Sub(frozenAt) +} + +// Wait4Scheduled blocks until either there are n or more scheduled events, or +// the timeout elapses. It returns true if the wait condition has been met +// before the timeout expired, false otherwise. +func Wait4Scheduled(count int, timeout time.Duration) bool { + return provider.Wait4Scheduled(count, timeout) +} + +// Now see time.Now. +func Now() time.Time { + return provider.Now() +} + +// Sleep see time.Sleep. +func Sleep(d time.Duration) { + provider.Sleep(d) +} + +// After see time.After. +func After(d time.Duration) <-chan time.Time { + return provider.After(d) +} + +// NewTimer see time.NewTimer. +func NewTimer(d time.Duration) Timer { + return provider.NewTimer(d) +} + +// AfterFunc see time.AfterFunc. +func AfterFunc(d time.Duration, f func()) Timer { + return provider.AfterFunc(d, f) +} + +// NewTicker see time.Ticker. +func NewTicker(d time.Duration) Ticker { + return provider.NewTicker(d) +} + +// Tick see time.Tick. +func Tick(d time.Duration) <-chan time.Time { + return provider.Tick(d) +} diff --git a/internal/holsterv4/clock/clock_mutex.go b/internal/holsterv4/clock/clock_mutex.go new file mode 100644 index 00000000..f7f87080 --- /dev/null +++ b/internal/holsterv4/clock/clock_mutex.go @@ -0,0 +1,131 @@ +//go:build holster_test_mode + +// Package clock provides the same functions as the system package time. In +// production it forwards all calls to the system time package, but in tests +// the time can be frozen by calling Freeze function and from that point it has +// to be advanced manually with Advance function making all scheduled calls +// deterministic. +// +// The functions provided by the package have the same parameters and return +// values as their system counterparts with a few exceptions. Where either +// *time.Timer or *time.Ticker is returned by a system function, the clock +// package counterpart returns clock.Timer or clock.Ticker interface +// respectively. The interfaces provide API as respective structs except C is +// not a channel, but a function that returns <-chan time.Time. +package clock + +import ( + "sync" + "time" +) + +var ( + frozenAt time.Time + realtime = &systemTime{} + provider Clock = realtime + rwMutex = sync.RWMutex{} +) + +// Freeze after this function is called all time related functions start +// generate deterministic timers that are triggered by Advance function. It is +// supposed to be used in tests only. Returns an Unfreezer so it can be a +// one-liner in tests: defer clock.Freeze(clock.Now()).Unfreeze() +func Freeze(now time.Time) Unfreezer { + frozenAt = now.UTC() + rwMutex.Lock() + defer rwMutex.Unlock() + provider = &frozenTime{now: now} + return Unfreezer{} +} + +type Unfreezer struct{} + +func (u Unfreezer) Unfreeze() { + Unfreeze() +} + +// Unfreeze reverses effect of Freeze. +func Unfreeze() { + rwMutex.Lock() + defer rwMutex.Unlock() + provider = realtime +} + +// Realtime returns a clock provider wrapping the SDK's time package. It is +// supposed to be used in tests when time is frozen to schedule test timeouts. +func Realtime() Clock { + return realtime +} + +// Makes the deterministic time move forward by the specified duration, firing +// timers along the way in the natural order. It returns how much time has +// passed since it was frozen. So you can assert on the return value in tests +// to make it explicit where you stand on the deterministic time scale. +func Advance(d time.Duration) time.Duration { + rwMutex.RLock() + ft, ok := provider.(*frozenTime) + rwMutex.RUnlock() + if !ok { + panic("Freeze time first!") + } + ft.advance(d) + return Now().UTC().Sub(frozenAt) +} + +// Wait4Scheduled blocks until either there are n or more scheduled events, or +// the timeout elapses. It returns true if the wait condition has been met +// before the timeout expired, false otherwise. +func Wait4Scheduled(count int, timeout time.Duration) bool { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.Wait4Scheduled(count, timeout) +} + +// Now see time.Now. +func Now() time.Time { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.Now() +} + +// Sleep see time.Sleep. +func Sleep(d time.Duration) { + rwMutex.RLock() + defer rwMutex.RUnlock() + provider.Sleep(d) +} + +// After see time.After. +func After(d time.Duration) <-chan time.Time { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.After(d) +} + +// NewTimer see time.NewTimer. +func NewTimer(d time.Duration) Timer { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.NewTimer(d) +} + +// AfterFunc see time.AfterFunc. +func AfterFunc(d time.Duration, f func()) Timer { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.AfterFunc(d, f) +} + +// NewTicker see time.Ticker. +func NewTicker(d time.Duration) Ticker { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.NewTicker(d) +} + +// Tick see time.Tick. +func Tick(d time.Duration) <-chan time.Time { + rwMutex.RLock() + defer rwMutex.RUnlock() + return provider.Tick(d) +} diff --git a/internal/holsterv4/clock/duration.go b/internal/holsterv4/clock/duration.go new file mode 100644 index 00000000..f15801fc --- /dev/null +++ b/internal/holsterv4/clock/duration.go @@ -0,0 +1,65 @@ +package clock + +import ( + "encoding/json" + "fmt" +) + +type DurationJSON struct { + Duration Duration +} + +func NewDurationJSON(v interface{}) (DurationJSON, error) { + switch v := v.(type) { + case Duration: + return DurationJSON{Duration: v}, nil + case float64: + return DurationJSON{Duration: Duration(v)}, nil + case int64: + return DurationJSON{Duration: Duration(v)}, nil + case int: + return DurationJSON{Duration: Duration(v)}, nil + case []byte: + duration, err := ParseDuration(string(v)) + if err != nil { + return DurationJSON{}, fmt.Errorf("while parsing []byte: %w", err) + } + return DurationJSON{Duration: duration}, nil + case string: + duration, err := ParseDuration(v) + if err != nil { + return DurationJSON{}, fmt.Errorf("while parsing string: %w", err) + } + return DurationJSON{Duration: duration}, nil + default: + return DurationJSON{}, fmt.Errorf("bad type %T", v) + } +} + +func NewDurationJSONOrPanic(v interface{}) DurationJSON { + d, err := NewDurationJSON(v) + if err != nil { + panic(err) + } + return d +} + +func (d DurationJSON) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Duration.String()) +} + +func (d *DurationJSON) UnmarshalJSON(b []byte) error { + var v interface{} + var err error + + if err = json.Unmarshal(b, &v); err != nil { + return err + } + + *d, err = NewDurationJSON(v) + return err +} + +func (d DurationJSON) String() string { + return d.Duration.String() +} diff --git a/internal/holsterv4/clock/duration_test.go b/internal/holsterv4/clock/duration_test.go new file mode 100644 index 00000000..eb465eda --- /dev/null +++ b/internal/holsterv4/clock/duration_test.go @@ -0,0 +1,79 @@ +package clock_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/vulcand/oxy/internal/holsterv4/clock" +) + +type DurationSuite struct { + suite.Suite +} + +func TestDurationSuite(t *testing.T) { + suite.Run(t, new(DurationSuite)) +} + +func (s *DurationSuite) TestNewOk() { + for _, v := range []interface{}{ + 42 * clock.Second, + int(42000000000), + int64(42000000000), + 42000000000., + "42s", + []byte("42s"), + } { + d, err := clock.NewDurationJSON(v) + s.Nil(err) + s.Equal(42*clock.Second, d.Duration) + } +} + +func (s *DurationSuite) TestNewError() { + for _, tc := range []struct { + v interface{} + errMsg string + }{{ + v: "foo", + errMsg: "while parsing string: time: invalid duration \"foo\"", + }, { + v: []byte("foo"), + errMsg: "while parsing []byte: time: invalid duration \"foo\"", + }, { + v: true, + errMsg: "bad type bool", + }} { + d, err := clock.NewDurationJSON(tc.v) + s.Equal(tc.errMsg, err.Error()) + s.Equal(clock.DurationJSON{}, d) + } +} + +func (s *DurationSuite) TestUnmarshal() { + for _, v := range []string{ + `{"foo": 42000000000}`, + `{"foo": 0.42e11}`, + `{"foo": "42s"}`, + } { + var withDuration struct { + Foo clock.DurationJSON `json:"foo"` + } + err := json.Unmarshal([]byte(v), &withDuration) + s.Nil(err) + s.Equal(42*clock.Second, withDuration.Foo.Duration) + } +} + +func (s *DurationSuite) TestMarshalling() { + d, err := clock.NewDurationJSON(42 * clock.Second) + s.Nil(err) + encoded, err := d.MarshalJSON() + s.Nil(err) + var decoded clock.DurationJSON + err = decoded.UnmarshalJSON(encoded) + s.Nil(err) + s.Equal(d, decoded) + s.Equal("42s", decoded.String()) +} diff --git a/internal/holsterv4/clock/frozen.go b/internal/holsterv4/clock/frozen.go new file mode 100644 index 00000000..df85d005 --- /dev/null +++ b/internal/holsterv4/clock/frozen.go @@ -0,0 +1,231 @@ +package clock + +import ( + "errors" + "sync" + "time" +) + +type frozenTime struct { + mu sync.Mutex + now time.Time + timers []*frozenTimer + waiter *waiter +} + +type waiter struct { + count int + signalCh chan struct{} +} + +func (ft *frozenTime) Now() time.Time { + ft.mu.Lock() + defer ft.mu.Unlock() + return ft.now +} + +func (ft *frozenTime) Sleep(d time.Duration) { + <-ft.NewTimer(d).C() +} + +func (ft *frozenTime) After(d time.Duration) <-chan time.Time { + return ft.NewTimer(d).C() +} + +func (ft *frozenTime) NewTimer(d time.Duration) Timer { + return ft.AfterFunc(d, nil) +} + +func (ft *frozenTime) AfterFunc(d time.Duration, f func()) Timer { + t := &frozenTimer{ + ft: ft, + when: ft.Now().Add(d), + f: f, + } + if f == nil { + t.c = make(chan time.Time, 1) + } + ft.startTimer(t) + return t +} + +func (ft *frozenTime) advance(d time.Duration) { + ft.mu.Lock() + defer ft.mu.Unlock() + + ft.now = ft.now.Add(d) + for t := ft.nextExpired(); t != nil; t = ft.nextExpired() { + // Send the timer expiration time to the timer channel if it is + // defined. But make sure not to block on the send if the channel is + // full. This behavior will make a ticker skip beats if it readers are + // not fast enough. + if t.c != nil { + select { + case t.c <- t.when: + default: + } + } + // If it is a ticking timer then schedule next tick, otherwise mark it + // as stopped. + if t.interval != 0 { + t.when = t.when.Add(t.interval) + t.stopped = false + ft.unlockedStartTimer(t) + } + // If a function is associated with the timer then call it, but make + // sure to release the lock for the time of call it is necessary + // because the lock is not re-entrant but the function may need to + // start another timer or ticker. + if t.f != nil { + func() { + ft.mu.Unlock() + defer ft.mu.Lock() + t.f() + }() + } + } +} + +func (ft *frozenTime) stopTimer(t *frozenTimer) bool { + ft.mu.Lock() + defer ft.mu.Unlock() + + if t.stopped { + return false + } + for i, curr := range ft.timers { + if curr == t { + t.stopped = true + copy(ft.timers[i:], ft.timers[i+1:]) + lastIdx := len(ft.timers) - 1 + ft.timers[lastIdx] = nil + ft.timers = ft.timers[:lastIdx] + return true + } + } + return false +} + +func (ft *frozenTime) nextExpired() *frozenTimer { + if len(ft.timers) == 0 { + return nil + } + t := ft.timers[0] + if ft.now.Before(t.when) { + return nil + } + copy(ft.timers, ft.timers[1:]) + lastIdx := len(ft.timers) - 1 + ft.timers[lastIdx] = nil + ft.timers = ft.timers[:lastIdx] + t.stopped = true + return t +} + +func (ft *frozenTime) startTimer(t *frozenTimer) { + ft.mu.Lock() + defer ft.mu.Unlock() + + ft.unlockedStartTimer(t) + + if ft.waiter == nil { + return + } + if len(ft.timers) >= ft.waiter.count { + close(ft.waiter.signalCh) + } +} + +func (ft *frozenTime) unlockedStartTimer(t *frozenTimer) { + pos := 0 + for _, curr := range ft.timers { + if t.when.Before(curr.when) { + break + } + pos++ + } + ft.timers = append(ft.timers, nil) + copy(ft.timers[pos+1:], ft.timers[pos:]) + ft.timers[pos] = t +} + +type frozenTimer struct { + ft *frozenTime + when time.Time + interval time.Duration + stopped bool + c chan time.Time + f func() +} + +func (t *frozenTimer) C() <-chan time.Time { + return t.c +} + +func (t *frozenTimer) Stop() bool { + return t.ft.stopTimer(t) +} + +func (t *frozenTimer) Reset(d time.Duration) bool { + active := t.ft.stopTimer(t) + t.when = t.ft.Now().Add(d) + t.ft.startTimer(t) + return active +} + +type frozenTicker struct { + t *frozenTimer +} + +func (t *frozenTicker) C() <-chan time.Time { + return t.t.C() +} + +func (t *frozenTicker) Stop() { + t.t.Stop() +} + +func (ft *frozenTime) NewTicker(d time.Duration) Ticker { + if d <= 0 { + panic(errors.New("non-positive interval for NewTicker")) + } + t := &frozenTimer{ + ft: ft, + when: ft.Now().Add(d), + interval: d, + c: make(chan time.Time, 1), + } + ft.startTimer(t) + return &frozenTicker{t} +} + +func (ft *frozenTime) Tick(d time.Duration) <-chan time.Time { + if d <= 0 { + return nil + } + return ft.NewTicker(d).C() +} + +func (ft *frozenTime) Wait4Scheduled(count int, timeout time.Duration) bool { + ft.mu.Lock() + if len(ft.timers) >= count { + ft.mu.Unlock() + return true + } + if ft.waiter != nil { + panic("Concurrent call") + } + ft.waiter = &waiter{count, make(chan struct{})} + ft.mu.Unlock() + + success := false + select { + case <-ft.waiter.signalCh: + success = true + case <-time.After(timeout): + } + ft.mu.Lock() + ft.waiter = nil + ft.mu.Unlock() + return success +} diff --git a/internal/holsterv4/clock/frozen_test.go b/internal/holsterv4/clock/frozen_test.go new file mode 100644 index 00000000..0e07368e --- /dev/null +++ b/internal/holsterv4/clock/frozen_test.go @@ -0,0 +1,340 @@ +package clock + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +func TestFreezeUnfreeze(t *testing.T) { + defer Freeze(Now()).Unfreeze() +} + +type FrozenSuite struct { + suite.Suite + epoch time.Time +} + +func TestFrozenSuite(t *testing.T) { + suite.Run(t, new(FrozenSuite)) +} + +func (s *FrozenSuite) SetupSuite() { + var err error + s.epoch, err = time.Parse(time.RFC3339, "2009-02-19T00:00:00Z") + s.Require().NoError(err) +} + +func (s *FrozenSuite) SetupTest() { + Freeze(s.epoch) +} + +func (s *FrozenSuite) TearDownTest() { + Unfreeze() +} + +func (s *FrozenSuite) TestAdvanceNow() { + s.Require().Equal(s.epoch, Now()) + s.Require().Equal(42*time.Millisecond, Advance(42*time.Millisecond)) + s.Require().Equal(s.epoch.Add(42*time.Millisecond), Now()) + s.Require().Equal(55*time.Millisecond, Advance(13*time.Millisecond)) + s.Require().Equal(74*time.Millisecond, Advance(19*time.Millisecond)) + s.Require().Equal(s.epoch.Add(74*time.Millisecond), Now()) +} + +func (s *FrozenSuite) TestSleep() { + hits := make(chan int, 100) + + delays := []int{60, 100, 90, 131, 999, 5} + for i, tc := range []struct { + desc string + fn func(delayMs int) + }{{ + desc: "Sleep", + fn: func(delay int) { + Sleep(time.Duration(delay) * time.Millisecond) + hits <- delay + }, + }, { + desc: "After", + fn: func(delay int) { + <-After(time.Duration(delay) * time.Millisecond) + hits <- delay + }, + }, { + desc: "AfterFunc", + fn: func(delay int) { + AfterFunc(time.Duration(delay)*time.Millisecond, + func() { + hits <- delay + }) + }, + }, { + desc: "NewTimer", + fn: func(delay int) { + t := NewTimer(time.Duration(delay) * time.Millisecond) + <-t.C() + hits <- delay + }, + }} { + fmt.Printf("Test case #%d: %s", i, tc.desc) + for _, delay := range delays { + go tc.fn(delay) + } + // Spin-wait for all goroutines to fall asleep. + ft := provider.(*frozenTime) + for { + var brk bool + ft.mu.Lock() + if len(ft.timers) == len(delays) { + brk = true + } + ft.mu.Unlock() + if brk { + break + } + time.Sleep(10 * time.Millisecond) + } + + runningMs := 0 + for i, delayMs := range []int{5, 60, 90, 100, 131, 999} { + fmt.Printf("Checking timer #%d, delay=%d\n", i, delayMs) + delta := delayMs - runningMs - 1 + Advance(time.Duration(delta) * time.Millisecond) + // Check before each timer deadline that it is not triggered yet. + s.assertHits(hits, []int{}) + + // When + Advance(1 * time.Millisecond) + + // Then + s.assertHits(hits, []int{delayMs}) + + runningMs += delta + 1 + } + + Advance(1000 * time.Millisecond) + s.assertHits(hits, []int{}) + } +} + +// Timers scheduled to trigger at the same time do that in the order they were +// created. +func (s *FrozenSuite) TestSameTime() { + var hits []int + + AfterFunc(100, func() { hits = append(hits, 3) }) + AfterFunc(100, func() { hits = append(hits, 1) }) + AfterFunc(99, func() { hits = append(hits, 2) }) + AfterFunc(100, func() { hits = append(hits, 5) }) + AfterFunc(101, func() { hits = append(hits, 4) }) + AfterFunc(101, func() { hits = append(hits, 6) }) + + // When + Advance(100) + + // Then + s.Require().Equal([]int{2, 3, 1, 5}, hits) +} + +func (s *FrozenSuite) TestTimerStop() { + hits := []int{} + + AfterFunc(100, func() { hits = append(hits, 1) }) + t := AfterFunc(100, func() { hits = append(hits, 2) }) + AfterFunc(100, func() { hits = append(hits, 3) }) + Advance(99) + s.Require().Equal([]int{}, hits) + + // When + active1 := t.Stop() + active2 := t.Stop() + + // Then + s.Require().Equal(true, active1) + s.Require().Equal(false, active2) + Advance(1) + s.Require().Equal([]int{1, 3}, hits) +} + +func (s *FrozenSuite) TestReset() { + hits := []int{} + + t1 := AfterFunc(100, func() { hits = append(hits, 1) }) + t2 := AfterFunc(100, func() { hits = append(hits, 2) }) + AfterFunc(100, func() { hits = append(hits, 3) }) + Advance(99) + s.Require().Equal([]int{}, hits) + + // When + active1 := t1.Reset(1) // Reset to the same time + active2 := t2.Reset(7) + + // Then + s.Require().Equal(true, active1) + s.Require().Equal(true, active2) + + Advance(1) + s.Require().Equal([]int{3, 1}, hits) + Advance(5) + s.Require().Equal([]int{3, 1}, hits) + Advance(1) + s.Require().Equal([]int{3, 1, 2}, hits) +} + +// Reset to the same time just puts the timer at the end of the trigger list +// for the date. +func (s *FrozenSuite) TestResetSame() { + hits := []int{} + + t := AfterFunc(100, func() { hits = append(hits, 1) }) + AfterFunc(100, func() { hits = append(hits, 2) }) + AfterFunc(100, func() { hits = append(hits, 3) }) + AfterFunc(101, func() { hits = append(hits, 4) }) + Advance(9) + + // When + active := t.Reset(91) + + // Then + s.Require().Equal(true, active) + + Advance(90) + s.Require().Equal([]int{}, hits) + Advance(1) + s.Require().Equal([]int{2, 3, 1}, hits) +} + +func (s *FrozenSuite) TestTicker() { + t := NewTicker(100) + + Advance(99) + s.assertNotFired(t.C()) + Advance(1) + s.Require().Equal(<-t.C(), s.epoch.Add(100)) + Advance(750) + s.Require().Equal(<-t.C(), s.epoch.Add(200)) + Advance(49) + s.assertNotFired(t.C()) + Advance(1) + s.Require().Equal(<-t.C(), s.epoch.Add(900)) + + t.Stop() + Advance(300) + s.assertNotFired(t.C()) +} + +func (s *FrozenSuite) TestTickerZero() { + defer func() { + recover() + }() + + NewTicker(0) + s.Fail("Should panic") +} + +func (s *FrozenSuite) TestTick() { + ch := Tick(100) + + Advance(99) + s.assertNotFired(ch) + Advance(1) + s.Require().Equal(<-ch, s.epoch.Add(100)) + Advance(750) + s.Require().Equal(<-ch, s.epoch.Add(200)) + Advance(49) + s.assertNotFired(ch) + Advance(1) + s.Require().Equal(<-ch, s.epoch.Add(900)) +} + +func (s *FrozenSuite) TestTickZero() { + ch := Tick(0) + s.Require().Nil(ch) +} + +func (s *FrozenSuite) TestNewStoppedTimer() { + t := NewStoppedTimer() + + // When/Then + select { + case <-t.C(): + s.Fail("Timer should not have fired") + default: + } + s.Require().Equal(false, t.Stop()) +} + +func (s *FrozenSuite) TestWait4Scheduled() { + After(100 * Millisecond) + After(100 * Millisecond) + s.Require().Equal(false, Wait4Scheduled(3, 0)) + + startedCh := make(chan struct{}) + resultCh := make(chan bool) + go func() { + close(startedCh) + resultCh <- Wait4Scheduled(3, 5*Second) + }() + // Allow some time for waiter to be set and start waiting for a signal. + <-startedCh + time.Sleep(50 * Millisecond) + + // When + After(100 * Millisecond) + + // Then + s.Require().Equal(true, <-resultCh) +} + +// If there is enough timers scheduled already, then a shortcut execution path +// is taken and Wait4Scheduled returns immediately. +func (s *FrozenSuite) TestWait4ScheduledImmediate() { + After(100 * Millisecond) + After(100 * Millisecond) + // When/Then + s.Require().Equal(true, Wait4Scheduled(2, 0)) +} + +func (s *FrozenSuite) TestSince() { + s.Require().Equal(Duration(0), Since(Now())) + s.Require().Equal(-Millisecond, Since(Now().Add(Millisecond))) + s.Require().Equal(Millisecond, Since(Now().Add(-Millisecond))) +} + +func (s *FrozenSuite) TestUntil() { + s.Require().Equal(Duration(0), Until(Now())) + s.Require().Equal(Millisecond, Until(Now().Add(Millisecond))) + s.Require().Equal(-Millisecond, Until(Now().Add(-Millisecond))) +} + +func (s *FrozenSuite) assertHits(got <-chan int, want []int) { + for i, w := range want { + var g int + select { + case g = <-got: + case <-time.After(100 * time.Millisecond): + s.Failf("Missing hit", "want=%v", w) + return + } + s.Require().Equal(w, g, "Hit #%d", i) + } + for { + select { + case g := <-got: + s.Failf("Unexpected hit", "got=%v", g) + default: + return + } + } +} + +func (s *FrozenSuite) assertNotFired(ch <-chan time.Time) { + select { + case <-ch: + s.Fail("Premature fire") + default: + } +} diff --git a/internal/holsterv4/clock/go19.go b/internal/holsterv4/clock/go19.go new file mode 100644 index 00000000..f5e169e9 --- /dev/null +++ b/internal/holsterv4/clock/go19.go @@ -0,0 +1,106 @@ +// +build go1.9 + +// This file introduces aliases to allow using of the clock package as a +// drop-in replacement of the standard time package. + +package clock + +import "time" + +type ( + Time = time.Time + Duration = time.Duration + Location = time.Location + + Weekday = time.Weekday + Month = time.Month + + ParseError = time.ParseError +) + +const ( + Nanosecond = time.Nanosecond + Microsecond = time.Microsecond + Millisecond = time.Millisecond + Second = time.Second + Minute = time.Minute + Hour = time.Hour + + Sunday = time.Sunday + Monday = time.Monday + Tuesday = time.Tuesday + Wednesday = time.Wednesday + Thursday = time.Thursday + Friday = time.Friday + Saturday = time.Saturday + + January = time.January + February = time.February + March = time.March + April = time.April + May = time.May + June = time.June + July = time.July + August = time.August + September = time.September + October = time.October + November = time.November + December = time.December + + ANSIC = time.ANSIC + UnixDate = time.UnixDate + RubyDate = time.RubyDate + RFC822 = time.RFC822 + RFC822Z = time.RFC822Z + RFC850 = time.RFC850 + RFC1123 = time.RFC1123 + RFC1123Z = time.RFC1123Z + RFC3339 = time.RFC3339 + RFC3339Nano = time.RFC3339Nano + Kitchen = time.Kitchen + Stamp = time.Stamp + StampMilli = time.StampMilli + StampMicro = time.StampMicro + StampNano = time.StampNano +) + +var ( + UTC = time.UTC + Local = time.Local +) + +func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time { + return time.Date(year, month, day, hour, min, sec, nsec, loc) +} + +func FixedZone(name string, offset int) *Location { + return time.FixedZone(name, offset) +} + +func LoadLocation(name string) (*Location, error) { + return time.LoadLocation(name) +} + +func Parse(layout, value string) (Time, error) { + return time.Parse(layout, value) +} + +func ParseDuration(s string) (Duration, error) { + return time.ParseDuration(s) +} + +func ParseInLocation(layout, value string, loc *Location) (Time, error) { + return time.ParseInLocation(layout, value, loc) +} + +func Unix(sec int64, nsec int64) Time { + return time.Unix(sec, nsec) +} + +func Since(t Time) Duration { + return provider.Now().Sub(t) +} + +func Until(t Time) Duration { + return t.Sub(provider.Now()) +} diff --git a/internal/holsterv4/clock/interface.go b/internal/holsterv4/clock/interface.go new file mode 100644 index 00000000..15f5ca1b --- /dev/null +++ b/internal/holsterv4/clock/interface.go @@ -0,0 +1,35 @@ +package clock + +import "time" + +// Timer see time.Timer. +type Timer interface { + C() <-chan time.Time + Stop() bool + Reset(d time.Duration) bool +} + +// Ticker see time.Ticker. +type Ticker interface { + C() <-chan time.Time + Stop() +} + +// NewStoppedTimer returns a stopped timer. Call Reset to get it ticking. +func NewStoppedTimer() Timer { + t := NewTimer(42 * time.Hour) + t.Stop() + return t +} + +// Clock is an interface that mimics the one of the SDK time package. +type Clock interface { + Now() time.Time + Sleep(d time.Duration) + After(d time.Duration) <-chan time.Time + NewTimer(d time.Duration) Timer + AfterFunc(d time.Duration, f func()) Timer + NewTicker(d time.Duration) Ticker + Tick(d time.Duration) <-chan time.Time + Wait4Scheduled(n int, timeout time.Duration) bool +} diff --git a/internal/holsterv4/clock/rfc822.go b/internal/holsterv4/clock/rfc822.go new file mode 100644 index 00000000..664941d5 --- /dev/null +++ b/internal/holsterv4/clock/rfc822.go @@ -0,0 +1,119 @@ +package clock + +import ( + "strconv" + "time" +) + +var datetimeLayouts = [48]string{ + // Day first month 2nd abbreviated. + "Mon, 2 Jan 2006 15:04:05 MST", + "Mon, 2 Jan 2006 15:04:05 -0700", + "Mon, 2 Jan 2006 15:04:05 -0700 (MST)", + "2 Jan 2006 15:04:05 MST", + "2 Jan 2006 15:04:05 -0700", + "2 Jan 2006 15:04:05 -0700 (MST)", + "Mon, 2 Jan 2006 15:04 MST", + "Mon, 2 Jan 2006 15:04 -0700", + "Mon, 2 Jan 2006 15:04 -0700 (MST)", + "2 Jan 2006 15:04 MST", + "2 Jan 2006 15:04 -0700", + "2 Jan 2006 15:04 -0700 (MST)", + + // Month first day 2nd abbreviated. + "Mon, Jan 2 2006 15:04:05 MST", + "Mon, Jan 2 2006 15:04:05 -0700", + "Mon, Jan 2 2006 15:04:05 -0700 (MST)", + "Jan 2 2006 15:04:05 MST", + "Jan 2 2006 15:04:05 -0700", + "Jan 2 2006 15:04:05 -0700 (MST)", + "Mon, Jan 2 2006 15:04 MST", + "Mon, Jan 2 2006 15:04 -0700", + "Mon, Jan 2 2006 15:04 -0700 (MST)", + "Jan 2 2006 15:04 MST", + "Jan 2 2006 15:04 -0700", + "Jan 2 2006 15:04 -0700 (MST)", + + // Day first month 2nd not abbreviated. + "Mon, 2 January 2006 15:04:05 MST", + "Mon, 2 January 2006 15:04:05 -0700", + "Mon, 2 January 2006 15:04:05 -0700 (MST)", + "2 January 2006 15:04:05 MST", + "2 January 2006 15:04:05 -0700", + "2 January 2006 15:04:05 -0700 (MST)", + "Mon, 2 January 2006 15:04 MST", + "Mon, 2 January 2006 15:04 -0700", + "Mon, 2 January 2006 15:04 -0700 (MST)", + "2 January 2006 15:04 MST", + "2 January 2006 15:04 -0700", + "2 January 2006 15:04 -0700 (MST)", + + // Month first day 2nd not abbreviated. + "Mon, January 2 2006 15:04:05 MST", + "Mon, January 2 2006 15:04:05 -0700", + "Mon, January 2 2006 15:04:05 -0700 (MST)", + "January 2 2006 15:04:05 MST", + "January 2 2006 15:04:05 -0700", + "January 2 2006 15:04:05 -0700 (MST)", + "Mon, January 2 2006 15:04 MST", + "Mon, January 2 2006 15:04 -0700", + "Mon, January 2 2006 15:04 -0700 (MST)", + "January 2 2006 15:04 MST", + "January 2 2006 15:04 -0700", + "January 2 2006 15:04 -0700 (MST)", +} + +// Allows seamless JSON encoding/decoding of rfc822 formatted timestamps. +// https://www.ietf.org/rfc/rfc822.txt section 5. +type RFC822Time struct { + Time +} + +// NewRFC822Time creates RFC822Time from a standard Time. The created value is +// truncated down to second precision because RFC822 does not allow for better. +func NewRFC822Time(t Time) RFC822Time { + return RFC822Time{Time: t.Truncate(Second)} +} + +// ParseRFC822Time parses an RFC822 time string. +func ParseRFC822Time(s string) (Time, error) { + var t time.Time + var err error + for _, layout := range datetimeLayouts { + t, err = Parse(layout, s) + if err == nil { + return t, err + } + } + return t, err +} + +// NewRFC822Time creates RFC822Time from a Unix timestamp (seconds from Epoch). +func NewRFC822TimeFromUnix(timestamp int64) RFC822Time { + return RFC822Time{Time: Unix(timestamp, 0).UTC()} +} + +func (t RFC822Time) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(t.Format(RFC1123))), nil +} + +func (t *RFC822Time) UnmarshalJSON(s []byte) error { + q, err := strconv.Unquote(string(s)) + if err != nil { + return err + } + parsed, err := ParseRFC822Time(q) + if err != nil { + return err + } + t.Time = parsed + return nil +} + +func (t RFC822Time) String() string { + return t.Format(RFC1123) +} + +func (t RFC822Time) StringWithOffset() string { + return t.Format(RFC1123Z) +} diff --git a/internal/holsterv4/clock/rfc822_test.go b/internal/holsterv4/clock/rfc822_test.go new file mode 100644 index 00000000..d83d6bfa --- /dev/null +++ b/internal/holsterv4/clock/rfc822_test.go @@ -0,0 +1,205 @@ +package clock + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type testStruct struct { + Time RFC822Time `json:"ts"` +} + +func TestRFC822New(t *testing.T) { + stdTime, err := Parse(RFC3339, "2019-08-29T11:20:07.123456+03:00") + assert.NoError(t, err) + + rfc822TimeFromTime := NewRFC822Time(stdTime) + rfc822TimeFromUnix := NewRFC822TimeFromUnix(stdTime.Unix()) + assert.True(t, rfc822TimeFromTime.Equal(rfc822TimeFromUnix.Time), + "want=%s, got=%s", rfc822TimeFromTime.Time, rfc822TimeFromUnix.Time) + + // Parsing from numerical offset to abbreviated offset is not always reliable. In this + // context Go will fallback to the known numerical offset. + assert.Equal(t, "Thu, 29 Aug 2019 11:20:07 +0300", rfc822TimeFromTime.String()) + assert.Equal(t, "Thu, 29 Aug 2019 08:20:07 UTC", rfc822TimeFromUnix.String()) +} + +// NewRFC822Time truncates to second precision. +func TestRFC822SecondPrecision(t *testing.T) { + stdTime1, err := Parse(RFC3339, "2019-08-29T11:20:07.111111+03:00") + assert.NoError(t, err) + stdTime2, err := Parse(RFC3339, "2019-08-29T11:20:07.999999+03:00") + assert.NoError(t, err) + assert.False(t, stdTime1.Equal(stdTime2)) + + rfc822Time1 := NewRFC822Time(stdTime1) + rfc822Time2 := NewRFC822Time(stdTime2) + assert.True(t, rfc822Time1.Equal(rfc822Time2.Time), + "want=%s, got=%s", rfc822Time1.Time, rfc822Time2.Time) +} + +// Marshaled representation is truncated down to second precision. +func TestRFC822Marshaling(t *testing.T) { + stdTime, err := Parse(RFC3339Nano, "2019-08-29T11:20:07.123456789+03:30") + assert.NoError(t, err) + + ts := testStruct{Time: NewRFC822Time(stdTime)} + encoded, err := json.Marshal(&ts) + assert.NoError(t, err) + assert.Equal(t, `{"ts":"Thu, 29 Aug 2019 11:20:07 +0330"}`, string(encoded)) +} + +func TestRFC822Unmarshaling(t *testing.T) { + for i, tc := range []struct { + inRFC822 string + outRFC3339 string + outRFC822 string + }{{ + inRFC822: "Thu, 29 Aug 2019 11:20:07 GMT", + outRFC3339: "2019-08-29T11:20:07Z", + outRFC822: "Thu, 29 Aug 2019 11:20:07 GMT", + }, { + inRFC822: "Thu, 29 Aug 2019 11:20:07 MSK", + // Extrapolating the numerical offset from an abbreviated offset is unreliable. In + // this test case the RFC3339 will have the incorrect result due to limitation in + // Go's time parser. + outRFC3339: "2019-08-29T11:20:07Z", + outRFC822: "Thu, 29 Aug 2019 11:20:07 MSK", + }, { + inRFC822: "Thu, 29 Aug 2019 11:20:07 -0000", + outRFC3339: "2019-08-29T11:20:07Z", + outRFC822: "Thu, 29 Aug 2019 11:20:07 -0000", + }, { + inRFC822: "Thu, 29 Aug 2019 11:20:07 +0000", + outRFC3339: "2019-08-29T11:20:07Z", + outRFC822: "Thu, 29 Aug 2019 11:20:07 +0000", + }, { + inRFC822: "Thu, 29 Aug 2019 11:20:07 +0300", + outRFC3339: "2019-08-29T11:20:07+03:00", + outRFC822: "Thu, 29 Aug 2019 11:20:07 +0300", + }, { + inRFC822: "Thu, 29 Aug 2019 11:20:07 +0330", + outRFC3339: "2019-08-29T11:20:07+03:30", + outRFC822: "Thu, 29 Aug 2019 11:20:07 +0330", + }, { + inRFC822: "Sun, 01 Sep 2019 11:20:07 +0300", + outRFC3339: "2019-09-01T11:20:07+03:00", + outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300", + }, { + inRFC822: "Sun, 1 Sep 2019 11:20:07 +0300", + outRFC3339: "2019-09-01T11:20:07+03:00", + outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300", + }, { + inRFC822: "Sun, 1 Sep 2019 11:20:07 +0300", + outRFC3339: "2019-09-01T11:20:07+03:00", + outRFC822: "Sun, 01 Sep 2019 11:20:07 +0300", + }, { + inRFC822: "Sun, 1 Sep 2019 11:20:07 UTC", + outRFC3339: "2019-09-01T11:20:07Z", + outRFC822: "Sun, 01 Sep 2019 11:20:07 UTC", + }, { + inRFC822: "Sun, 1 Sep 2019 11:20:07 UTC", + outRFC3339: "2019-09-01T11:20:07Z", + outRFC822: "Sun, 01 Sep 2019 11:20:07 UTC", + }, { + inRFC822: "Sun, 1 Sep 2019 11:20:07 GMT", + outRFC3339: "2019-09-01T11:20:07Z", + outRFC822: "Sun, 01 Sep 2019 11:20:07 GMT", + }, { + inRFC822: "Fri, 21 Nov 1997 09:55:06 -0600 (MDT)", + outRFC3339: "1997-11-21T09:55:06-06:00", + outRFC822: "Fri, 21 Nov 1997 09:55:06 MDT", + }} { + t.Run(tc.inRFC822, func(t *testing.T) { + tcDesc := fmt.Sprintf("Test case #%d: %v", i, tc) + var ts testStruct + + inEncoded := []byte(fmt.Sprintf(`{"ts":"%s"}`, tc.inRFC822)) + err := json.Unmarshal(inEncoded, &ts) + assert.NoError(t, err, tcDesc) + assert.Equal(t, tc.outRFC3339, ts.Time.Format(RFC3339), tcDesc) + + actualEncoded, err := json.Marshal(&ts) + assert.NoError(t, err, tcDesc) + outEncoded := fmt.Sprintf(`{"ts":"%s"}`, tc.outRFC822) + assert.Equal(t, outEncoded, string(actualEncoded), tcDesc) + }) + } +} + +func TestRFC822UnmarshalingError(t *testing.T) { + for _, tc := range []struct { + inEncoded string + outError string + }{{ + inEncoded: `{"ts": "Thu, 29 Aug 2019 11:20:07"}`, + outError: `parsing time "Thu, 29 Aug 2019 11:20:07" as "January 2 2006 15:04 -0700 (MST)": cannot parse "Thu, 29 Aug 2019 11:20:07" as "January"`, + }, { + inEncoded: `{"ts": "foo"}`, + outError: `parsing time "foo" as "January 2 2006 15:04 -0700 (MST)": cannot parse "foo" as "January"`, + }, { + inEncoded: `{"ts": 42}`, + outError: "invalid syntax", + }} { + t.Run(tc.inEncoded, func(t *testing.T) { + var ts testStruct + err := json.Unmarshal([]byte(tc.inEncoded), &ts) + assert.EqualError(t, err, tc.outError) + }) + } +} + +func TestParseRFC822Time(t *testing.T) { + for _, tt := range []struct { + rfc822Time string + }{ + {"Thu, 3 Jun 2021 12:01:05 MST"}, + {"Thu, 3 Jun 2021 12:01:05 -0700"}, + {"Thu, 3 Jun 2021 12:01:05 -0700 (MST)"}, + {"2 Jun 2021 17:06:41 GMT"}, + {"2 Jun 2021 17:06:41 -0700"}, + {"2 Jun 2021 17:06:41 -0700 (MST)"}, + {"Mon, 30 August 2021 11:05:00 -0400"}, + {"Thu, 3 June 2021 12:01:05 MST"}, + {"Thu, 3 June 2021 12:01:05 -0700"}, + {"Thu, 3 June 2021 12:01:05 -0700 (MST)"}, + {"2 June 2021 17:06:41 GMT"}, + {"2 June 2021 17:06:41 -0700"}, + {"2 June 2021 17:06:41 -0700 (MST)"}, + {"Wed, Nov 03 2021 17:48:06 CST"}, + {"Wed, November 03 2021 17:48:06 CST"}, + + // Timestamps without seconds. + {"Sun, 31 Oct 2021 12:10 -5000"}, + {"Thu, 3 Jun 2021 12:01 MST"}, + {"Thu, 3 Jun 2021 12:01 -0700"}, + {"Thu, 3 Jun 2021 12:01 -0700 (MST)"}, + {"2 Jun 2021 17:06 GMT"}, + {"2 Jun 2021 17:06 -0700"}, + {"2 Jun 2021 17:06 -0700 (MST)"}, + {"Mon, 30 August 2021 11:05 -0400"}, + {"Thu, 3 June 2021 12:01 MST"}, + {"Thu, 3 June 2021 12:01 -0700"}, + {"Thu, 3 June 2021 12:01 -0700 (MST)"}, + {"2 June 2021 17:06 GMT"}, + {"2 June 2021 17:06 -0700"}, + {"2 June 2021 17:06 -0700 (MST)"}, + {"Wed, Nov 03 2021 17:48 CST"}, + {"Wed, November 03 2021 17:48 CST"}, + } { + t.Run(tt.rfc822Time, func(t *testing.T) { + _, err := ParseRFC822Time(tt.rfc822Time) + assert.NoError(t, err) + }) + } +} + +func TestStringWithOffset(t *testing.T) { + now := time.Now().UTC() + r := NewRFC822Time(now) + assert.Equal(t, now.Format(time.RFC1123Z), r.StringWithOffset()) +} diff --git a/internal/holsterv4/clock/system.go b/internal/holsterv4/clock/system.go new file mode 100644 index 00000000..04d6673e --- /dev/null +++ b/internal/holsterv4/clock/system.go @@ -0,0 +1,68 @@ +package clock + +import "time" + +type systemTime struct{} + +func (st *systemTime) Now() time.Time { + return time.Now() +} + +func (st *systemTime) Sleep(d time.Duration) { + time.Sleep(d) +} + +func (st *systemTime) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +type systemTimer struct { + t *time.Timer +} + +func (st *systemTime) NewTimer(d time.Duration) Timer { + t := time.NewTimer(d) + return &systemTimer{t} +} + +func (st *systemTime) AfterFunc(d time.Duration, f func()) Timer { + t := time.AfterFunc(d, f) + return &systemTimer{t} +} + +func (t *systemTimer) C() <-chan time.Time { + return t.t.C +} + +func (t *systemTimer) Stop() bool { + return t.t.Stop() +} + +func (t *systemTimer) Reset(d time.Duration) bool { + return t.t.Reset(d) +} + +type systemTicker struct { + t *time.Ticker +} + +func (t *systemTicker) C() <-chan time.Time { + return t.t.C +} + +func (t *systemTicker) Stop() { + t.t.Stop() +} + +func (st *systemTime) NewTicker(d time.Duration) Ticker { + t := time.NewTicker(d) + return &systemTicker{t} +} + +func (st *systemTime) Tick(d time.Duration) <-chan time.Time { + return time.Tick(d) +} + +func (st *systemTime) Wait4Scheduled(count int, timeout time.Duration) bool { + panic("Not supported") +} diff --git a/internal/holsterv4/clock/system_test.go b/internal/holsterv4/clock/system_test.go new file mode 100644 index 00000000..a3af2604 --- /dev/null +++ b/internal/holsterv4/clock/system_test.go @@ -0,0 +1,143 @@ +package clock + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSleep(t *testing.T) { + start := Now() + + // When + Sleep(100 * time.Millisecond) + + // Then + if Now().Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } +} + +func TestAfter(t *testing.T) { + start := Now() + + // When + end := <-After(100 * time.Millisecond) + + // Then + if end.Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } +} + +func TestAfterFunc(t *testing.T) { + start := Now() + endCh := make(chan time.Time, 1) + + // When + AfterFunc(100*time.Millisecond, func() { endCh <- time.Now() }) + + // Then + end := <-endCh + if end.Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } +} + +func TestNewTimer(t *testing.T) { + start := Now() + + // When + timer := NewTimer(100 * time.Millisecond) + + // Then + end := <-timer.C() + if end.Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } +} + +func TestTimerStop(t *testing.T) { + timer := NewTimer(50 * time.Millisecond) + + // When + active := timer.Stop() + + // Then + assert.Equal(t, true, active) + time.Sleep(100) + select { + case <-timer.C(): + assert.Fail(t, "Timer should not have fired") + default: + } +} + +func TestTimerReset(t *testing.T) { + start := time.Now() + timer := NewTimer(300 * time.Millisecond) + + // When + timer.Reset(100 * time.Millisecond) + + // Then + end := <-timer.C() + if end.Sub(start) > 150*time.Millisecond { + assert.Fail(t, "Waited too long") + } +} + +func TestNewTicker(t *testing.T) { + start := Now() + + // When + timer := NewTicker(100 * time.Millisecond) + + // Then + end := <-timer.C() + if end.Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } + end = <-timer.C() + if end.Sub(start) < 200*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } + + timer.Stop() + time.Sleep(150) + select { + case <-timer.C(): + assert.Fail(t, "Ticker should not have fired") + default: + } +} + +func TestTick(t *testing.T) { + start := Now() + + // When + ch := Tick(100 * time.Millisecond) + + // Then + end := <-ch + if end.Sub(start) < 100*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } + end = <-ch + if end.Sub(start) < 200*time.Millisecond { + assert.Fail(t, "Sleep did not last long enough") + } +} + +func TestNewStoppedTimer(t *testing.T) { + timer := NewStoppedTimer() + + // When/Then + select { + case <-timer.C(): + assert.Fail(t, "Timer should not have fired") + default: + } + assert.Equal(t, false, timer.Stop()) +} diff --git a/internal/holsterv4/collections/README.md b/internal/holsterv4/collections/README.md new file mode 100644 index 00000000..fc56c81a --- /dev/null +++ b/internal/holsterv4/collections/README.md @@ -0,0 +1,28 @@ +## Priority Queue +Provides a Priority Queue implementation as described [here](https://en.wikipedia.org/wiki/Priority_queue) + +```go +queue := collections.NewPriorityQueue() + +queue.Push(&collections.PQItem{ + Value: "thing3", + Priority: 3, +}) + +queue.Push(&collections.PQItem{ + Value: "thing1", + Priority: 1, +}) + +queue.Push(&collections.PQItem{ + Value: "thing2", + Priority: 2, +}) + +// Pops item off the queue according to the priority instead of the Push() order +item := queue.Pop() + +fmt.Printf("Item: %s", item.Value.(string)) + +// Output: Item: thing1 +``` diff --git a/internal/holsterv4/collections/priority_queue.go b/internal/holsterv4/collections/priority_queue.go new file mode 100644 index 00000000..5992ff06 --- /dev/null +++ b/internal/holsterv4/collections/priority_queue.go @@ -0,0 +1,96 @@ +/* +Copyright 2017 Mailgun Technologies Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package collections + +import ( + "container/heap" +) + +// An PQItem is something we manage in a priority queue. +type PQItem struct { + Value interface{} + Priority int // The priority of the item in the queue. + // The index is needed by update and is maintained by the heap.Interface methods. + index int // The index of the item in the heap. +} + +// Implements a PriorityQueue +type PriorityQueue struct { + impl *pqImpl +} + +func NewPriorityQueue() *PriorityQueue { + mh := &pqImpl{} + heap.Init(mh) + return &PriorityQueue{impl: mh} +} + +func (p PriorityQueue) Len() int { return p.impl.Len() } + +func (p *PriorityQueue) Push(el *PQItem) { + heap.Push(p.impl, el) +} + +func (p *PriorityQueue) Pop() *PQItem { + el := heap.Pop(p.impl) + return el.(*PQItem) +} + +func (p *PriorityQueue) Peek() *PQItem { + return (*p.impl)[0] +} + +// Modifies the priority and value of an Item in the queue. +func (p *PriorityQueue) Update(el *PQItem, priority int) { + heap.Remove(p.impl, el.index) + el.Priority = priority + heap.Push(p.impl, el) +} + +func (p *PriorityQueue) Remove(el *PQItem) { + heap.Remove(p.impl, el.index) +} + +// Actual Implementation using heap.Interface +type pqImpl []*PQItem + +func (mh pqImpl) Len() int { return len(mh) } + +func (mh pqImpl) Less(i, j int) bool { + return mh[i].Priority < mh[j].Priority +} + +func (mh pqImpl) Swap(i, j int) { + mh[i], mh[j] = mh[j], mh[i] + mh[i].index = i + mh[j].index = j +} + +func (mh *pqImpl) Push(x interface{}) { + n := len(*mh) + item := x.(*PQItem) + item.index = n + *mh = append(*mh, item) +} + +func (mh *pqImpl) Pop() interface{} { + old := *mh + n := len(old) + item := old[n-1] + item.index = -1 // for safety + *mh = old[0 : n-1] + return item +} diff --git a/internal/holsterv4/collections/priority_queue_test.go b/internal/holsterv4/collections/priority_queue_test.go new file mode 100644 index 00000000..2a626065 --- /dev/null +++ b/internal/holsterv4/collections/priority_queue_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2017 Mailgun Technologies Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package collections_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vulcand/oxy/internal/holsterv4/collections" +) + +func toPtr(i int) interface{} { + return &i +} + +func toInt(i interface{}) int { + return *(i.(*int)) +} + +func TestPeek(t *testing.T) { + mh := collections.NewPriorityQueue() + + el := &collections.PQItem{ + Value: toPtr(1), + Priority: 5, + } + + mh.Push(el) + assert.Equal(t, 1, toInt(mh.Peek().Value)) + assert.Equal(t, 1, mh.Len()) + + el = &collections.PQItem{ + Value: toPtr(2), + Priority: 1, + } + mh.Push(el) + assert.Equal(t, 2, mh.Len()) + assert.Equal(t, 2, toInt(mh.Peek().Value)) + assert.Equal(t, 2, toInt(mh.Peek().Value)) + assert.Equal(t, 2, mh.Len()) + + el = mh.Pop() + + assert.Equal(t, 2, toInt(el.Value)) + assert.Equal(t, 1, mh.Len()) + assert.Equal(t, 1, toInt(mh.Peek().Value)) + + mh.Pop() + assert.Equal(t, 0, mh.Len()) +} + +func TestUpdate(t *testing.T) { + mh := collections.NewPriorityQueue() + x := &collections.PQItem{ + Value: toPtr(1), + Priority: 4, + } + y := &collections.PQItem{ + Value: toPtr(2), + Priority: 3, + } + z := &collections.PQItem{ + Value: toPtr(3), + Priority: 8, + } + mh.Push(x) + mh.Push(y) + mh.Push(z) + assert.Equal(t, 2, toInt(mh.Peek().Value)) + + mh.Update(z, 1) + assert.Equal(t, 3, toInt(mh.Peek().Value)) + + mh.Update(x, 0) + assert.Equal(t, 1, toInt(mh.Peek().Value)) +} + +func ExampleNewPriorityQueue() { + queue := collections.NewPriorityQueue() + + queue.Push(&collections.PQItem{ + Value: "thing3", + Priority: 3, + }) + + queue.Push(&collections.PQItem{ + Value: "thing1", + Priority: 1, + }) + + queue.Push(&collections.PQItem{ + Value: "thing2", + Priority: 2, + }) + + // Pops item off the queue according to the priority instead of the Push() order + item := queue.Pop() + + fmt.Printf("Item: %s", item.Value.(string)) + + // Output: Item: thing1 +} diff --git a/internal/holsterv4/collections/ttlmap.go b/internal/holsterv4/collections/ttlmap.go new file mode 100644 index 00000000..44442d86 --- /dev/null +++ b/internal/holsterv4/collections/ttlmap.go @@ -0,0 +1,233 @@ +/* +Copyright 2017 Mailgun Technologies Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package collections + +import ( + "fmt" + "sync" + "time" + + "github.com/vulcand/oxy/internal/holsterv4/clock" +) + +type TTLMap struct { + // Optionally specifies a callback function to be + // executed when an entry has expired + OnExpire func(key string, i interface{}) + + capacity int + elements map[string]*mapElement + expiryTimes *PriorityQueue + mutex *sync.RWMutex +} + +type mapElement struct { + key string + value interface{} + heapEl *PQItem +} + +func NewTTLMap(capacity int) *TTLMap { + if capacity <= 0 { + capacity = 0 + } + + return &TTLMap{ + capacity: capacity, + elements: make(map[string]*mapElement), + expiryTimes: NewPriorityQueue(), + mutex: &sync.RWMutex{}, + } +} + +func (m *TTLMap) Set(key string, value interface{}, ttlSeconds int) error { + expiryTime, err := m.toEpochSeconds(ttlSeconds) + if err != nil { + return err + } + m.mutex.Lock() + defer m.mutex.Unlock() + return m.set(key, value, expiryTime) +} + +func (m *TTLMap) Len() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + return len(m.elements) +} + +func (m *TTLMap) Get(key string) (interface{}, bool) { + value, mapEl, expired := m.lockNGet(key) + if mapEl == nil { + return nil, false + } + if expired { + m.lockNDel(mapEl) + return nil, false + } + return value, true +} + +func (m *TTLMap) Increment(key string, value int, ttlSeconds int) (int, error) { + expiryTime, err := m.toEpochSeconds(ttlSeconds) + if err != nil { + return 0, err + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + mapEl, expired := m.get(key) + if mapEl == nil || expired { + m.set(key, value, expiryTime) + return value, nil + } + + currentValue, ok := mapEl.value.(int) + if !ok { + return 0, fmt.Errorf("Expected existing value to be integer, got %T", mapEl.value) + } + + currentValue += value + m.set(key, currentValue, expiryTime) + return currentValue, nil +} + +func (m *TTLMap) GetInt(key string) (int, bool, error) { + valueI, exists := m.Get(key) + if !exists { + return 0, false, nil + } + value, ok := valueI.(int) + if !ok { + return 0, false, fmt.Errorf("Expected existing value to be integer, got %T", valueI) + } + return value, true, nil +} + +func (m *TTLMap) set(key string, value interface{}, expiryTime int) error { + if mapEl, ok := m.elements[key]; ok { + mapEl.value = value + m.expiryTimes.Update(mapEl.heapEl, expiryTime) + return nil + } + + if len(m.elements) >= m.capacity { + m.freeSpace(1) + } + heapEl := &PQItem{ + Priority: expiryTime, + } + mapEl := &mapElement{ + key: key, + value: value, + heapEl: heapEl, + } + heapEl.Value = mapEl + m.elements[key] = mapEl + m.expiryTimes.Push(heapEl) + return nil +} + +func (m *TTLMap) lockNGet(key string) (value interface{}, mapEl *mapElement, expired bool) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + mapEl, expired = m.get(key) + value = nil + if mapEl != nil { + value = mapEl.value + } + return value, mapEl, expired +} + +func (m *TTLMap) get(key string) (*mapElement, bool) { + mapEl, ok := m.elements[key] + if !ok { + return nil, false + } + now := int(clock.Now().Unix()) + expired := mapEl.heapEl.Priority <= now + return mapEl, expired +} + +func (m *TTLMap) lockNDel(mapEl *mapElement) { + m.mutex.Lock() + defer m.mutex.Unlock() + + // Map element could have been updated. Now that we have a lock + // retrieve it again and check if it is still expired. + var ok bool + if mapEl, ok = m.elements[mapEl.key]; !ok { + return + } + now := int(clock.Now().Unix()) + if mapEl.heapEl.Priority > now { + return + } + + if m.OnExpire != nil { + m.OnExpire(mapEl.key, mapEl.value) + } + + delete(m.elements, mapEl.key) + m.expiryTimes.Remove(mapEl.heapEl) +} + +func (m *TTLMap) freeSpace(count int) { + removed := m.RemoveExpired(count) + if removed >= count { + return + } + m.RemoveLastUsed(count - removed) +} + +func (m *TTLMap) RemoveExpired(iterations int) int { + removed := 0 + now := int(clock.Now().Unix()) + for i := 0; i < iterations; i += 1 { + if len(m.elements) == 0 { + break + } + heapEl := m.expiryTimes.Peek() + if heapEl.Priority > now { + break + } + m.expiryTimes.Pop() + mapEl := heapEl.Value.(*mapElement) + delete(m.elements, mapEl.key) + removed += 1 + } + return removed +} + +func (m *TTLMap) RemoveLastUsed(iterations int) { + for i := 0; i < iterations; i += 1 { + if len(m.elements) == 0 { + return + } + heapEl := m.expiryTimes.Pop() + mapEl := heapEl.Value.(*mapElement) + delete(m.elements, mapEl.key) + } +} + +func (m *TTLMap) toEpochSeconds(ttlSeconds int) (int, error) { + if ttlSeconds <= 0 { + return 0, fmt.Errorf("ttlSeconds should be >= 0, got %d", ttlSeconds) + } + return int(clock.Now().Add(time.Second * time.Duration(ttlSeconds)).Unix()), nil +} diff --git a/internal/holsterv4/collections/ttlmap_test.go b/internal/holsterv4/collections/ttlmap_test.go new file mode 100644 index 00000000..5c1bac12 --- /dev/null +++ b/internal/holsterv4/collections/ttlmap_test.go @@ -0,0 +1,337 @@ +/* +Copyright 2017 Mailgun Technologies Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/vulcand/oxy/internal/holsterv4/clock" +) + +type TTLMapSuite struct { + suite.Suite +} + +func TestTTLMapSuite(t *testing.T) { + suite.Run(t, new(TTLMapSuite)) +} + +func (s *TTLMapSuite) SetupTest() { + clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC)) +} + +func (s *TTLMapSuite) TearDownSuite() { + clock.Unfreeze() +} + +func (s *TTLMapSuite) TestSetWrong() { + m := NewTTLMap(1) + + err := m.Set("a", 1, -1) + s.Require().EqualError(err, "ttlSeconds should be >= 0, got -1") + + err = m.Set("a", 1, 0) + s.Require().EqualError(err, "ttlSeconds should be >= 0, got 0") + + _, err = m.Increment("a", 1, 0) + s.Require().EqualError(err, "ttlSeconds should be >= 0, got 0") + + _, err = m.Increment("a", 1, -1) + s.Require().EqualError(err, "ttlSeconds should be >= 0, got -1") +} + +func (s *TTLMapSuite) TestRemoveExpiredEmpty() { + m := NewTTLMap(1) + m.RemoveExpired(100) +} + +func (s *TTLMapSuite) TestRemoveLastUsedEmpty() { + m := NewTTLMap(1) + m.RemoveLastUsed(100) +} + +func (s *TTLMapSuite) TestGetSetExpire() { + m := NewTTLMap(1) + + err := m.Set("a", 1, 1) + s.Require().Equal(nil, err) + + valI, exists := m.Get("a") + s.Require().Equal(true, exists) + s.Require().Equal(1, valI) + + clock.Advance(1 * clock.Second) + + _, exists = m.Get("a") + s.Require().Equal(false, exists) +} + +func (s *TTLMapSuite) TestSetOverwrite() { + m := NewTTLMap(1) + + err := m.Set("o", 1, 1) + s.Require().Equal(nil, err) + + valI, exists := m.Get("o") + s.Require().Equal(true, exists) + s.Require().Equal(1, valI) + + err = m.Set("o", 2, 1) + s.Require().Equal(nil, err) + + valI, exists = m.Get("o") + s.Require().Equal(true, exists) + s.Require().Equal(2, valI) +} + +func (s *TTLMapSuite) TestRemoveExpiredEdgeCase() { + m := NewTTLMap(1) + + err := m.Set("a", 1, 1) + s.Require().Equal(nil, err) + + clock.Advance(1 * clock.Second) + + err = m.Set("b", 2, 1) + s.Require().Equal(nil, err) + + valI, exists := m.Get("a") + s.Require().Equal(false, exists) + + valI, exists = m.Get("b") + s.Require().Equal(true, exists) + s.Require().Equal(2, valI) + + s.Require().Equal(1, m.Len()) +} + +func (s *TTLMapSuite) TestRemoveOutOfCapacity() { + m := NewTTLMap(2) + + err := m.Set("a", 1, 5) + s.Require().Equal(nil, err) + + clock.Advance(1 * clock.Second) + + err = m.Set("b", 2, 6) + s.Require().Equal(nil, err) + + err = m.Set("c", 3, 10) + s.Require().Equal(nil, err) + + valI, exists := m.Get("a") + s.Require().Equal(false, exists) + + valI, exists = m.Get("b") + s.Require().Equal(true, exists) + s.Require().Equal(2, valI) + + valI, exists = m.Get("c") + s.Require().Equal(true, exists) + s.Require().Equal(3, valI) + + s.Require().Equal(2, m.Len()) +} + +func (s *TTLMapSuite) TestGetNotExists() { + m := NewTTLMap(1) + _, exists := m.Get("a") + s.Require().Equal(false, exists) +} + +func (s *TTLMapSuite) TestGetIntNotExists() { + m := NewTTLMap(1) + _, exists, err := m.GetInt("a") + s.Require().Equal(nil, err) + s.Require().Equal(false, exists) +} + +func (s *TTLMapSuite) TestGetInvalidType() { + m := NewTTLMap(1) + m.Set("a", "banana", 5) + + _, _, err := m.GetInt("a") + s.Require().EqualError(err, "Expected existing value to be integer, got string") + + _, err = m.Increment("a", 4, 1) + s.Require().EqualError(err, "Expected existing value to be integer, got string") +} + +func (s *TTLMapSuite) TestIncrementGetExpire() { + m := NewTTLMap(1) + + m.Increment("a", 5, 1) + val, exists, err := m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(5, val) + + clock.Advance(1 * clock.Second) + + m.Increment("a", 4, 1) + val, exists, err = m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(4, val) +} + +func (s *TTLMapSuite) TestIncrementOverwrite() { + m := NewTTLMap(1) + + m.Increment("a", 5, 1) + val, exists, err := m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(5, val) + + m.Increment("a", 4, 1) + val, exists, err = m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(9, val) +} + +func (s *TTLMapSuite) TestIncrementOutOfCapacity() { + m := NewTTLMap(1) + + m.Increment("a", 5, 1) + val, exists, err := m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(5, val) + + m.Increment("b", 4, 1) + val, exists, err = m.GetInt("b") + + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(4, val) + + val, exists, err = m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(false, exists) +} + +func (s *TTLMapSuite) TestIncrementRemovesExpired() { + m := NewTTLMap(2) + + m.Increment("a", 1, 1) + m.Increment("b", 2, 2) + + clock.Advance(1 * clock.Second) + m.Increment("c", 3, 3) + + val, exists, err := m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(false, exists) + + val, exists, err = m.GetInt("b") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(2, val) + + val, exists, err = m.GetInt("c") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(3, val) +} + +func (s *TTLMapSuite) TestIncrementRemovesLastUsed() { + m := NewTTLMap(2) + + m.Increment("a", 1, 10) + m.Increment("b", 2, 11) + m.Increment("c", 3, 12) + + val, exists, err := m.GetInt("a") + + s.Require().Equal(nil, err) + s.Require().Equal(false, exists) + + val, exists, err = m.GetInt("b") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + + s.Require().Equal(2, val) + + val, exists, err = m.GetInt("c") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(3, val) +} + +func (s *TTLMapSuite) TestIncrementUpdatesTtl() { + m := NewTTLMap(1) + + m.Increment("a", 1, 1) + m.Increment("a", 1, 10) + + clock.Advance(1 * clock.Second) + + val, exists, err := m.GetInt("a") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(2, val) +} + +func (s *TTLMapSuite) TestUpdate() { + m := NewTTLMap(1) + + m.Increment("a", 1, 1) + m.Increment("a", 1, 10) + + clock.Advance(1 * clock.Second) + + val, exists, err := m.GetInt("a") + s.Require().Equal(nil, err) + s.Require().Equal(true, exists) + s.Require().Equal(2, val) +} + +func (s *TTLMapSuite) TestCallOnExpire() { + var called bool + var key string + var val interface{} + m := NewTTLMap(1) + m.OnExpire = func(k string, el interface{}) { + called = true + key = k + val = el + } + + err := m.Set("a", 1, 1) + s.Require().Equal(nil, err) + + valI, exists := m.Get("a") + s.Require().Equal(true, exists) + s.Require().Equal(1, valI) + + clock.Advance(1 * clock.Second) + + _, exists = m.Get("a") + s.Require().Equal(false, exists) + s.Require().Equal(true, called) + s.Require().Equal("a", key) + s.Require().Equal(1, val) +} diff --git a/memmetrics/anomaly_test.go b/memmetrics/anomaly_test.go index 87b5d7c1..6f89f4dc 100644 --- a/memmetrics/anomaly_test.go +++ b/memmetrics/anomaly_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) func TestMedian(t *testing.T) { @@ -175,20 +176,20 @@ func TestSplitLatencies(t *testing.T) { values := make([]time.Duration, len(test.values)) for i, d := range test.values { - values[i] = time.Millisecond * time.Duration(d) + values[i] = clock.Millisecond * time.Duration(d) } - good, bad := SplitLatencies(values, time.Millisecond) + good, bad := SplitLatencies(values, clock.Millisecond) vgood := make(map[time.Duration]bool, len(test.good)) for _, v := range test.good { - vgood[time.Duration(v)*time.Millisecond] = true + vgood[time.Duration(v)*clock.Millisecond] = true } assert.Equal(t, vgood, good) vbad := make(map[time.Duration]bool, len(test.bad)) for _, v := range test.bad { - vbad[time.Duration(v)*time.Millisecond] = true + vbad[time.Duration(v)*clock.Millisecond] = true } assert.Equal(t, vbad, bad) }) diff --git a/memmetrics/counter.go b/memmetrics/counter.go index 0ceaa3a4..853acb77 100644 --- a/memmetrics/counter.go +++ b/memmetrics/counter.go @@ -4,27 +4,18 @@ import ( "fmt" "time" - "github.com/mailgun/timetools" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) type rcOptSetter func(*RollingCounter) error -// CounterClock defines a counter clock. -func CounterClock(c timetools.TimeProvider) rcOptSetter { - return func(r *RollingCounter) error { - r.clock = c - return nil - } -} - // RollingCounter Calculates in memory failure rate of an endpoint using rolling window of a predefined size. type RollingCounter struct { - clock timetools.TimeProvider resolution time.Duration values []int countedBuckets int // how many samples in different buckets have we collected so far lastBucket int // last recorded bucket - lastUpdated time.Time + lastUpdated clock.Time } // NewCounter creates a counter with fixed amount of buckets that are rotated every resolution period. @@ -34,7 +25,7 @@ func NewCounter(buckets int, resolution time.Duration, options ...rcOptSetter) ( if buckets <= 0 { return nil, fmt.Errorf("Buckets should be >= 0") } - if resolution < time.Second { + if resolution < clock.Second { return nil, fmt.Errorf("Resolution should be larger than a second") } @@ -51,10 +42,6 @@ func NewCounter(buckets int, resolution time.Duration, options ...rcOptSetter) ( } } - if rc.clock == nil { - rc.clock = &timetools.RealTime{} - } - return rc, nil } @@ -70,7 +57,6 @@ func (c *RollingCounter) Clone() *RollingCounter { other := &RollingCounter{ resolution: c.resolution, values: make([]int, len(c.values)), - clock: c.clock, lastBucket: c.lastBucket, lastUpdated: c.lastUpdated, } @@ -82,7 +68,7 @@ func (c *RollingCounter) Clone() *RollingCounter { func (c *RollingCounter) Reset() { c.lastBucket = -1 c.countedBuckets = 0 - c.lastUpdated = time.Time{} + c.lastUpdated = clock.Time{} for i := range c.values { c.values[i] = 0 } @@ -121,7 +107,7 @@ func (c *RollingCounter) Inc(v int) { } func (c *RollingCounter) incBucketValue(v int) { - now := c.clock.UtcNow() + now := clock.Now().UTC() bucket := c.getBucket(now) c.values[bucket] += v c.lastUpdated = now @@ -143,7 +129,7 @@ func (c *RollingCounter) getBucket(t time.Time) int { // Reset buckets that were not updated. func (c *RollingCounter) cleanup() { - now := c.clock.UtcNow() + now := clock.Now().UTC() for i := 0; i < len(c.values); i++ { now = now.Add(time.Duration(-1*i) * c.resolution) if now.Truncate(c.resolution).After(c.lastUpdated.Truncate(c.resolution)) { diff --git a/memmetrics/counter_test.go b/memmetrics/counter_test.go index eb07a5cc..099b236c 100644 --- a/memmetrics/counter_test.go +++ b/memmetrics/counter_test.go @@ -2,30 +2,27 @@ package memmetrics import ( "testing" - "time" - "github.com/mailgun/timetools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) func TestCloneExpired(t *testing.T) { - clockTest := &timetools.FreezedTime{ - CurrentTime: time.Date(2012, 3, 4, 5, 6, 7, 0, time.UTC), - } + clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC)) - cnt, err := NewCounter(3, time.Second, CounterClock(clockTest)) + cnt, err := NewCounter(3, clock.Second) require.NoError(t, err) cnt.Inc(1) - clockTest.Sleep(time.Second) + clock.Advance(clock.Second) cnt.Inc(1) - clockTest.Sleep(time.Second) + clock.Advance(clock.Second) cnt.Inc(1) - clockTest.Sleep(time.Second) + clock.Advance(clock.Second) out := cnt.Clone() assert.EqualValues(t, 2, out.Count()) diff --git a/memmetrics/histogram.go b/memmetrics/histogram.go index dfd8374d..9cfde955 100644 --- a/memmetrics/histogram.go +++ b/memmetrics/histogram.go @@ -5,7 +5,7 @@ import ( "time" "github.com/HdrHistogram/hdrhistogram-go" - "github.com/mailgun/timetools" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) // HDRHistogram is a tiny wrapper around github.com/HdrHistogram/hdrhistogram-go that provides convenience functions for measuring http latencies. @@ -47,12 +47,12 @@ func (h *HDRHistogram) Export() *HDRHistogram { // LatencyAtQuantile sets latency at quantile with microsecond precision. func (h *HDRHistogram) LatencyAtQuantile(q float64) time.Duration { - return time.Duration(h.ValueAtQuantile(q)) * time.Microsecond + return time.Duration(h.ValueAtQuantile(q)) * clock.Microsecond } // RecordLatencies Records latencies with microsecond precision. func (h *HDRHistogram) RecordLatencies(d time.Duration, n int64) error { - return h.RecordValues(int64(d/time.Microsecond), n) + return h.RecordValues(int64(d/clock.Microsecond), n) } // Reset reset a HDRHistogram. @@ -81,26 +81,17 @@ func (h *HDRHistogram) Merge(other *HDRHistogram) error { type rhOptSetter func(r *RollingHDRHistogram) error -// RollingClock sets a clock. -func RollingClock(clock timetools.TimeProvider) rhOptSetter { - return func(r *RollingHDRHistogram) error { - r.clock = clock - return nil - } -} - // RollingHDRHistogram holds multiple histograms and rotates every period. // It provides resulting histogram as a result of a call of 'Merged' function. type RollingHDRHistogram struct { idx int - lastRoll time.Time + lastRoll clock.Time period time.Duration bucketCount int low int64 high int64 sigfigs int buckets []*HDRHistogram - clock timetools.TimeProvider } // NewRollingHDRHistogram created a new RollingHDRHistogram. @@ -119,10 +110,6 @@ func NewRollingHDRHistogram(low, high int64, sigfigs int, period time.Duration, } } - if rh.clock == nil { - rh.clock = &timetools.RealTime{} - } - buckets := make([]*HDRHistogram, rh.bucketCount) for i := range buckets { h, err := NewHDRHistogram(low, high, sigfigs) @@ -145,7 +132,6 @@ func (r *RollingHDRHistogram) Export() *RollingHDRHistogram { export.low = r.low export.high = r.high export.sigfigs = r.sigfigs - export.clock = r.clock exportBuckets := make([]*HDRHistogram, len(r.buckets)) for i, hist := range r.buckets { @@ -173,7 +159,7 @@ func (r *RollingHDRHistogram) Append(o *RollingHDRHistogram) error { // Reset reset a RollingHDRHistogram. func (r *RollingHDRHistogram) Reset() { r.idx = 0 - r.lastRoll = r.clock.UtcNow() + r.lastRoll = clock.Now().UTC() for _, b := range r.buckets { b.Reset() } @@ -199,9 +185,9 @@ func (r *RollingHDRHistogram) Merged() (*HDRHistogram, error) { } func (r *RollingHDRHistogram) getHist() *HDRHistogram { - if r.clock.UtcNow().Sub(r.lastRoll) >= r.period { + if clock.Now().UTC().Sub(r.lastRoll) >= r.period { r.rotate() - r.lastRoll = r.clock.UtcNow() + r.lastRoll = clock.Now().UTC() } return r.buckets[r.idx] } diff --git a/memmetrics/histogram_test.go b/memmetrics/histogram_test.go index 1ecca8e4..3c27698d 100644 --- a/memmetrics/histogram_test.go +++ b/memmetrics/histogram_test.go @@ -2,11 +2,11 @@ package memmetrics import ( "testing" - "time" "github.com/HdrHistogram/hdrhistogram-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) @@ -35,15 +35,16 @@ func TestMergeNil(t *testing.T) { } func TestRotation(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() h, err := NewRollingHDRHistogram( - 1, // min value - 3600000, // max value - 3, // significant figures - time.Second, // 1 second is a rolling period - 2, // 2 histograms in a window - RollingClock(clock)) + 1, // min value + 3600000, // max value + 3, // significant figures + clock.Second, + 2, // 2 histograms in a window + ) require.NoError(t, err) require.NotNil(t, h) @@ -55,7 +56,7 @@ func TestRotation(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, 5, m.ValueAtQuantile(100)) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) require.NoError(t, h.RecordValues(2, 1)) require.NoError(t, h.RecordValues(1, 1)) @@ -64,7 +65,7 @@ func TestRotation(t *testing.T) { assert.EqualValues(t, 5, m.ValueAtQuantile(100)) // rotate, this means that the old value would evaporate - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) require.NoError(t, h.RecordValues(1, 1)) @@ -74,15 +75,16 @@ func TestRotation(t *testing.T) { } func TestReset(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() h, err := NewRollingHDRHistogram( - 1, // min value - 3600000, // max value - 3, // significant figures - time.Second, // 1 second is a rolling period - 2, // 2 histograms in a window - RollingClock(clock)) + 1, // min value + 3600000, // max value + 3, // significant figures + clock.Second, + 2, // 2 histograms in a window + ) require.NoError(t, err) require.NotNil(t, h) @@ -93,7 +95,7 @@ func TestReset(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, 5, m.ValueAtQuantile(100)) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) require.NoError(t, h.RecordValues(2, 1)) require.NoError(t, h.RecordValues(1, 1)) @@ -109,7 +111,7 @@ func TestReset(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, 5, m.ValueAtQuantile(100)) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) require.NoError(t, h.RecordValues(2, 1)) require.NoError(t, h.RecordValues(1, 1)) @@ -142,37 +144,37 @@ func TestHDRHistogramExportReturnsNewCopy(t *testing.T) { } func TestRollingHDRHistogramExportReturnsNewCopy(t *testing.T) { - origTime := time.Now() + origTime := clock.Now() + + done := testutils.FreezeTime() + defer done() a := RollingHDRHistogram{ idx: 1, lastRoll: origTime, - period: 2 * time.Second, + period: 2 * clock.Second, bucketCount: 3, low: 4, high: 5, sigfigs: 1, buckets: []*HDRHistogram{}, - clock: testutils.GetClock(), } b := a.Export() a.idx = 11 - a.lastRoll = time.Now().Add(1 * time.Minute) - a.period = 12 * time.Second + a.lastRoll = clock.Now().Add(1 * clock.Minute) + a.period = 12 * clock.Second a.bucketCount = 13 a.low = 14 a.high = 15 a.sigfigs = 1 a.buckets = nil - a.clock = nil assert.Equal(t, 1, b.idx) assert.Equal(t, origTime, b.lastRoll) - assert.Equal(t, 2*time.Second, b.period) + assert.Equal(t, 2*clock.Second, b.period) assert.Equal(t, 3, b.bucketCount) assert.Equal(t, int64(4), b.low) assert.EqualValues(t, 5, b.high) assert.NotNil(t, b.buckets) - assert.NotNil(t, b.clock) } diff --git a/memmetrics/ratio.go b/memmetrics/ratio.go index db95ef97..4c220217 100644 --- a/memmetrics/ratio.go +++ b/memmetrics/ratio.go @@ -1,26 +1,13 @@ package memmetrics -import ( - "time" - - "github.com/mailgun/timetools" -) +import "time" type ratioOptSetter func(r *RatioCounter) error -// RatioClock sets a clock. -func RatioClock(clock timetools.TimeProvider) ratioOptSetter { - return func(r *RatioCounter) error { - r.clock = clock - return nil - } -} - // RatioCounter calculates a ratio of a/a+b over a rolling window of predefined buckets. type RatioCounter struct { - clock timetools.TimeProvider - a *RollingCounter - b *RollingCounter + a *RollingCounter + b *RollingCounter } // NewRatioCounter creates a new RatioCounter. @@ -33,16 +20,12 @@ func NewRatioCounter(buckets int, resolution time.Duration, options ...ratioOptS } } - if rc.clock == nil { - rc.clock = &timetools.RealTime{} - } - - a, err := NewCounter(buckets, resolution, CounterClock(rc.clock)) + a, err := NewCounter(buckets, resolution) if err != nil { return nil, err } - b, err := NewCounter(buckets, resolution, CounterClock(rc.clock)) + b, err := NewCounter(buckets, resolution) if err != nil { return nil, err } diff --git a/memmetrics/ratio_test.go b/memmetrics/ratio_test.go index 66320b4c..9c664a62 100644 --- a/memmetrics/ratio_test.go +++ b/memmetrics/ratio_test.go @@ -2,43 +2,47 @@ package memmetrics import ( "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) func TestNewRatioCounterInvalidParams(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Bad buckets count - _, err := NewRatioCounter(0, time.Second, RatioClock(clock)) + _, err := NewRatioCounter(0, clock.Second) require.Error(t, err) // Too precise resolution - _, err = NewRatioCounter(10, time.Millisecond, RatioClock(clock)) + _, err = NewRatioCounter(10, clock.Millisecond) require.Error(t, err) } func TestNotReady(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // No data - fr, err := NewRatioCounter(10, time.Second, RatioClock(clock)) + fr, err := NewRatioCounter(10, clock.Second) require.NoError(t, err) assert.Equal(t, false, fr.IsReady()) assert.Equal(t, 0.0, fr.Ratio()) // Not enough data - fr, err = NewRatioCounter(10, time.Second, RatioClock(clock)) + fr, err = NewRatioCounter(10, clock.Second) require.NoError(t, err) fr.CountA() assert.Equal(t, false, fr.IsReady()) } func TestNoB(t *testing.T) { - fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock())) + done := testutils.FreezeTime() + defer done() + fr, err := NewRatioCounter(1, clock.Second) require.NoError(t, err) fr.IncA(1) assert.Equal(t, true, fr.IsReady()) @@ -46,7 +50,10 @@ func TestNoB(t *testing.T) { } func TestNoA(t *testing.T) { - fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock())) + done := testutils.FreezeTime() + defer done() + + fr, err := NewRatioCounter(1, clock.Second) require.NoError(t, err) fr.IncB(1) assert.Equal(t, true, fr.IsReady()) @@ -55,16 +62,17 @@ func TestNoA(t *testing.T) { // Make sure that data is properly calculated over several buckets. func TestMultipleBuckets(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - fr, err := NewRatioCounter(3, time.Second, RatioClock(clock)) + fr, err := NewRatioCounter(3, clock.Second) require.NoError(t, err) fr.IncB(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) assert.Equal(t, true, fr.IsReady()) @@ -74,21 +82,22 @@ func TestMultipleBuckets(t *testing.T) { // Make sure that data is properly calculated over several buckets // When we overwrite old data when the window is rolling. func TestOverwriteBuckets(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - fr, err := NewRatioCounter(3, time.Second, RatioClock(clock)) + fr, err := NewRatioCounter(3, clock.Second) require.NoError(t, err) fr.IncB(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) // This time we should overwrite the old data points - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) fr.IncB(2) @@ -99,26 +108,27 @@ func TestOverwriteBuckets(t *testing.T) { // Make sure we cleanup the data after periods of inactivity // So it does not mess up the stats. func TestInactiveBuckets(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - fr, err := NewRatioCounter(3, time.Second, RatioClock(clock)) + fr, err := NewRatioCounter(3, clock.Second) require.NoError(t, err) fr.IncB(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) // This time we should overwrite the old data points with new data - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) fr.IncB(2) // Jump to the last bucket and change the data - clock.CurrentTime = clock.CurrentTime.Add(time.Second * 2) + clock.Advance(clock.Second * 2) fr.IncB(1) assert.Equal(t, true, fr.IsReady()) @@ -126,27 +136,31 @@ func TestInactiveBuckets(t *testing.T) { } func TestLongPeriodsOfInactivity(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - fr, err := NewRatioCounter(2, time.Second, RatioClock(clock)) + fr, err := NewRatioCounter(2, clock.Second) require.NoError(t, err) fr.IncB(1) - clock.CurrentTime = clock.CurrentTime.Add(time.Second) + clock.Advance(clock.Second) fr.IncA(1) assert.Equal(t, true, fr.IsReady()) assert.Equal(t, 0.5, fr.Ratio()) // This time we should overwrite all data points - clock.CurrentTime = clock.CurrentTime.Add(100 * time.Second) + clock.Advance(100 * clock.Second) fr.IncA(1) assert.Equal(t, 1.0, fr.Ratio()) } func TestNewRatioCounterReset(t *testing.T) { - fr, err := NewRatioCounter(1, time.Second, RatioClock(testutils.GetClock())) + done := testutils.FreezeTime() + defer done() + + fr, err := NewRatioCounter(1, clock.Second) require.NoError(t, err) fr.IncB(1) diff --git a/memmetrics/roundtrip.go b/memmetrics/roundtrip.go index ceb3fb17..b2416342 100644 --- a/memmetrics/roundtrip.go +++ b/memmetrics/roundtrip.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/mailgun/timetools" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) // RTMetrics provides aggregated performance metrics for HTTP requests processing @@ -24,7 +24,6 @@ type RTMetrics struct { newCounter NewCounterFn newHist NewRollingHistogramFn - clock timetools.TimeProvider } type rrOptSetter func(r *RTMetrics) error @@ -54,14 +53,6 @@ func RTHistogram(fn NewRollingHistogramFn) rrOptSetter { } } -// RTClock sets a clock. -func RTClock(clock timetools.TimeProvider) rrOptSetter { - return func(r *RTMetrics) error { - r.clock = clock - return nil - } -} - // NewRTMetrics returns new instance of metrics collector. func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) { m := &RTMetrics{ @@ -74,19 +65,15 @@ func NewRTMetrics(settings ...rrOptSetter) (*RTMetrics, error) { } } - if m.clock == nil { - m.clock = &timetools.RealTime{} - } - if m.newCounter == nil { m.newCounter = func() (*RollingCounter, error) { - return NewCounter(counterBuckets, counterResolution, CounterClock(m.clock)) + return NewCounter(counterBuckets, counterResolution) } } if m.newHist == nil { m.newHist = func() (*RollingHDRHistogram, error) { - return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets, RollingClock(m.clock)) + return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets) } } @@ -133,7 +120,6 @@ func (m *RTMetrics) Export() *RTMetrics { } export.newCounter = m.newCounter export.newHist = m.newHist - export.clock = m.clock return export } @@ -293,10 +279,10 @@ func (m *RTMetrics) recordStatusCode(statusCode int) error { const ( counterBuckets = 10 - counterResolution = time.Second + counterResolution = clock.Second histMin = 1 - histMax = 3600000000 // 1 hour in microseconds - histSignificantFigures = 2 // significant figures (1% precision) - histBuckets = 6 // number of sub-histograms in a rolling histogram - histPeriod = 10 * time.Second // roll time + histMax = 3600000000 // 1 hour in microseconds + histSignificantFigures = 2 // significant figures (1% precision) + histBuckets = 6 // number of sub-histograms in a rolling histogram + histPeriod = 10 * clock.Second // roll time ) diff --git a/memmetrics/roundtrip_test.go b/memmetrics/roundtrip_test.go index 2de6be23..6009a750 100644 --- a/memmetrics/roundtrip_test.go +++ b/memmetrics/roundtrip_test.go @@ -6,21 +6,24 @@ import ( "testing" "time" - "github.com/mailgun/timetools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) func TestDefaults(t *testing.T) { - rr, err := NewRTMetrics(RTClock(testutils.GetClock())) + done := testutils.FreezeTime() + defer done() + + rr, err := NewRTMetrics() require.NoError(t, err) require.NotNil(t, rr) - rr.Record(200, time.Second) - rr.Record(502, 2*time.Second) - rr.Record(200, time.Second) - rr.Record(200, time.Second) + rr.Record(200, clock.Second) + rr.Record(502, 2*clock.Second) + rr.Record(200, clock.Second) + rr.Record(200, clock.Second) assert.EqualValues(t, 1, rr.NetworkErrorCount()) assert.EqualValues(t, 4, rr.TotalCount()) @@ -30,7 +33,7 @@ func TestDefaults(t *testing.T) { h, err := rr.LatencyHistogram() require.NoError(t, err) - assert.Equal(t, 2, int(h.LatencyAtQuantile(100)/time.Second)) + assert.Equal(t, 2, int(h.LatencyAtQuantile(100)/clock.Second)) rr.Reset() assert.EqualValues(t, 0, rr.NetworkErrorCount()) @@ -45,25 +48,26 @@ func TestDefaults(t *testing.T) { } func TestAppend(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rr, err := NewRTMetrics(RTClock(clock)) + rr, err := NewRTMetrics() require.NoError(t, err) require.NotNil(t, rr) - rr.Record(200, time.Second) - rr.Record(502, 2*time.Second) - rr.Record(200, time.Second) - rr.Record(200, time.Second) + rr.Record(200, clock.Second) + rr.Record(502, 2*clock.Second) + rr.Record(200, clock.Second) + rr.Record(200, clock.Second) - rr2, err := NewRTMetrics(RTClock(clock)) + rr2, err := NewRTMetrics() require.NoError(t, err) require.NotNil(t, rr2) - rr2.Record(200, 3*time.Second) - rr2.Record(501, 3*time.Second) - rr2.Record(200, 3*time.Second) - rr2.Record(200, 3*time.Second) + rr2.Record(200, 3*clock.Second) + rr2.Record(501, 3*clock.Second) + rr2.Record(200, 3*clock.Second) + rr2.Record(200, 3*clock.Second) require.NoError(t, rr2.Append(rr)) assert.Equal(t, map[int]int64{501: 1, 502: 1, 200: 6}, rr2.StatusCodesCounts()) @@ -71,14 +75,14 @@ func TestAppend(t *testing.T) { h, err := rr2.LatencyHistogram() require.NoError(t, err) - assert.EqualValues(t, 3, h.LatencyAtQuantile(100)/time.Second) + assert.EqualValues(t, 3, h.LatencyAtQuantile(100)/clock.Second) } func TestConcurrentRecords(t *testing.T) { // This test asserts a race condition which requires parallelism runtime.GOMAXPROCS(100) - rr, err := NewRTMetrics(RTClock(testutils.GetClock())) + rr, err := NewRTMetrics() require.NoError(t, err) for code := 0; code < 100; code++ { @@ -92,7 +96,6 @@ func TestConcurrentRecords(t *testing.T) { func TestRTMetricExportReturnsNewCopy(t *testing.T) { a := RTMetrics{ - clock: &timetools.RealTime{}, statusCodes: map[int]*RollingCounter{}, statusCodesLock: sync.RWMutex{}, histogram: &RollingHDRHistogram{}, @@ -100,17 +103,17 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) { } var err error - a.total, err = NewCounter(1, time.Second, CounterClock(a.clock)) + a.total, err = NewCounter(1, clock.Second) require.NoError(t, err) - a.netErrors, err = NewCounter(1, time.Second, CounterClock(a.clock)) + a.netErrors, err = NewCounter(1, clock.Second) require.NoError(t, err) a.newCounter = func() (*RollingCounter, error) { - return NewCounter(counterBuckets, counterResolution, CounterClock(a.clock)) + return NewCounter(counterBuckets, counterResolution) } a.newHist = func() (*RollingHDRHistogram, error) { - return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets, RollingClock(a.clock)) + return NewRollingHDRHistogram(histMin, histMax, histSignificantFigures, histPeriod, histBuckets) } b := a.Export() @@ -120,7 +123,6 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) { a.histogram = nil a.newCounter = nil a.newHist = nil - a.clock = nil assert.NotNil(t, b.total) assert.NotNil(t, b.netErrors) @@ -128,7 +130,6 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) { assert.NotNil(t, b.histogram) assert.NotNil(t, b.newCounter) assert.NotNil(t, b.newHist) - assert.NotNil(t, b.clock) // a and b should have different locks locksSucceed := make(chan bool) @@ -144,7 +145,7 @@ func TestRTMetricExportReturnsNewCopy(t *testing.T) { select { case <-locksSucceed: return - case <-time.After(10 * time.Second): + case <-clock.After(10 * clock.Second): t.FailNow() } } diff --git a/ratelimit/bucket.go b/ratelimit/bucket.go index e0a1602b..9d81c0a1 100644 --- a/ratelimit/bucket.go +++ b/ratelimit/bucket.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/mailgun/timetools" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) // UndefinedDelay default delay. @@ -34,27 +34,24 @@ type tokenBucket struct { // The number of tokens available for consumption at the moment. It can // nether be larger then capacity. availableTokens int64 - // Interface that gives current time (so tests can override) - clock timetools.TimeProvider // Tells when tokensAvailable was updated the last time. - lastRefresh time.Time + lastRefresh clock.Time // The number of tokens consumed the last time. lastConsumed int64 } // newTokenBucket crates a `tokenBucket` instance for the specified `Rate`. -func newTokenBucket(rate *rate, clock timetools.TimeProvider) *tokenBucket { +func newTokenBucket(rate *rate) *tokenBucket { period := rate.period if period == 0 { - period = time.Nanosecond + period = clock.Nanosecond } return &tokenBucket{ period: period, timePerToken: time.Duration(int64(period) / rate.average), burst: rate.burst, - clock: clock, - lastRefresh: clock.UtcNow(), + lastRefresh: clock.Now().UTC(), availableTokens: rate.burst, } } @@ -114,7 +111,7 @@ func (tb *tokenBucket) timeTillAvailable(tokens int64) time.Duration { // It is calculated based on the refill rate, the time passed since last refresh, // and is limited by the bucket capacity. func (tb *tokenBucket) updateAvailableTokens() { - now := tb.clock.UtcNow() + now := clock.Now().UTC() timePassed := now.Sub(tb.lastRefresh) if tb.timePerToken == 0 { diff --git a/ratelimit/bucket_test.go b/ratelimit/bucket_test.go index dca551c8..1d2d76ce 100644 --- a/ratelimit/bucket_test.go +++ b/ratelimit/bucket_test.go @@ -4,16 +4,17 @@ import ( "testing" "time" - "github.com/mailgun/timetools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) func TestConsumeSingleToken(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 1}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 1}) // First request passes delay, err := tb.consume(1) @@ -23,10 +24,10 @@ func TestConsumeSingleToken(t *testing.T) { // Next request does not pass the same second delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, time.Second, delay) + assert.Equal(t, clock.Second, delay) // Second later, the request passes - clock.Sleep(time.Second) + clock.Advance(clock.Second) delay, err = tb.consume(1) require.NoError(t, err) @@ -34,7 +35,7 @@ func TestConsumeSingleToken(t *testing.T) { // Five seconds later, still only one request is allowed // because maxBurst is 1 - clock.Sleep(5 * time.Second) + clock.Advance(5 * clock.Second) delay, err = tb.consume(1) require.NoError(t, err) @@ -43,13 +44,14 @@ func TestConsumeSingleToken(t *testing.T) { // The next one is forbidden delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, time.Second, delay) + assert.Equal(t, clock.Second, delay) } func TestFastConsumption(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 1}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 1}) // First request passes delay, err := tb.consume(1) @@ -57,21 +59,21 @@ func TestFastConsumption(t *testing.T) { assert.Equal(t, time.Duration(0), delay) // Try 200 ms later - clock.Sleep(time.Millisecond * 200) + clock.Advance(clock.Millisecond * 200) delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, time.Second, delay) + assert.Equal(t, clock.Second, delay) // Try 700 ms later - clock.Sleep(time.Millisecond * 700) + clock.Advance(clock.Millisecond * 700) delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, time.Second, delay) + assert.Equal(t, clock.Second, delay) // Try 100 ms later, success! - clock.Sleep(time.Millisecond * 100) + clock.Advance(clock.Millisecond * 100) delay, err = tb.consume(1) require.NoError(t, err) @@ -79,7 +81,10 @@ func TestFastConsumption(t *testing.T) { } func TestConsumeMultipleTokens(t *testing.T) { - tb := newTokenBucket(&rate{period: time.Second, average: 3, burst: 5}, testutils.GetClock()) + done := testutils.FreezeTime() + defer done() + + tb := newTokenBucket(&rate{period: clock.Second, average: 3, burst: 5}) delay, err := tb.consume(3) require.NoError(t, err) @@ -95,9 +100,10 @@ func TestConsumeMultipleTokens(t *testing.T) { } func TestDelayIsCorrect(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tb := newTokenBucket(&rate{period: time.Second, average: 3, burst: 5}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 3, burst: 5}) // Exhaust initial capacity delay, err := tb.consume(5) @@ -109,7 +115,7 @@ func TestDelayIsCorrect(t *testing.T) { assert.NotEqual(t, time.Duration(0), delay) // Now wait provided delay and make sure we can consume now - clock.Sleep(delay) + clock.Advance(delay) delay, err = tb.consume(3) require.NoError(t, err) @@ -118,17 +124,23 @@ func TestDelayIsCorrect(t *testing.T) { // Make sure requests that exceed burst size are not allowed. func TestExceedsBurst(t *testing.T) { - tb := newTokenBucket(&rate{period: time.Second, average: 1, burst: 10}, testutils.GetClock()) + done := testutils.FreezeTime() + defer done() + + tb := newTokenBucket(&rate{period: clock.Second, average: 1, burst: 10}) _, err := tb.consume(11) require.Error(t, err) } func TestConsumeBurst(t *testing.T) { - tb := newTokenBucket(&rate{period: time.Second, average: 2, burst: 5}, testutils.GetClock()) + done := testutils.FreezeTime() + defer done() + + tb := newTokenBucket(&rate{period: clock.Second, average: 2, burst: 5}) // In two seconds we would have 5 tokens - testutils.GetClock().Sleep(2 * time.Second) + clock.Advance(2 * clock.Second) // Lets consume 5 at once delay, err := tb.consume(5) @@ -137,7 +149,10 @@ func TestConsumeBurst(t *testing.T) { } func TestConsumeEstimate(t *testing.T) { - tb := newTokenBucket(&rate{period: time.Second, average: 2, burst: 4}, testutils.GetClock()) + done := testutils.FreezeTime() + defer done() + + tb := newTokenBucket(&rate{period: clock.Second, average: 2, burst: 4}) // Consume all burst at once delay, err := tb.consume(4) @@ -147,31 +162,32 @@ func TestConsumeEstimate(t *testing.T) { // Now try to consume it and face delay delay, err = tb.consume(4) require.NoError(t, err) - assert.Equal(t, time.Duration(2)*time.Second, delay) + assert.Equal(t, time.Duration(2)*clock.Second, delay) } // If a rate with different period is passed to the `update` method, then an // error is returned but the state of the bucket remains valid and unchanged. func TestUpdateInvalidPeriod(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(15) // 5 tokens available require.NoError(t, err) // When - err = tb.update(&rate{period: time.Second + 1, average: 30, burst: 40}) // still 5 tokens available + err = tb.update(&rate{period: clock.Second + 1, average: 30, burst: 40}) // still 5 tokens available require.Error(t, err) // Then // ...check that rate did not change - clock.Sleep(500 * time.Millisecond) + clock.Advance(500 * clock.Millisecond) delay, err := tb.consume(11) require.NoError(t, err) - assert.Equal(t, 100*time.Millisecond, delay) + assert.Equal(t, 100*clock.Millisecond, delay) delay, err = tb.consume(10) require.NoError(t, err) @@ -179,7 +195,7 @@ func TestUpdateInvalidPeriod(t *testing.T) { assert.Equal(t, time.Duration(0), delay) // ...check that burst did not change - clock.Sleep(40 * time.Second) + clock.Advance(40 * clock.Second) _, err = tb.consume(21) require.Error(t, err) @@ -192,35 +208,37 @@ func TestUpdateInvalidPeriod(t *testing.T) { // If the capacity of the bucket is increased by the update then it takes some // time to fill the bucket with tokens up to the new capacity. func TestUpdateBurstIncreased(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(15) // 5 tokens available require.NoError(t, err) // When - err = tb.update(&rate{period: time.Second, average: 10, burst: 50}) // still 5 tokens available + err = tb.update(&rate{period: clock.Second, average: 10, burst: 50}) // still 5 tokens available require.NoError(t, err) // Then delay, err := tb.consume(50) require.NoError(t, err) - assert.Equal(t, time.Second/10*45, delay) + assert.Equal(t, clock.Second/10*45, delay) } // If the capacity of the bucket is increased by the update then it takes some // time to fill the bucket with tokens up to the new capacity. func TestUpdateBurstDecreased(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 50}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 50}) _, err := tb.consume(15) // 35 tokens available require.NoError(t, err) // When - err = tb.update(&rate{period: time.Second, average: 10, burst: 20}) // the number of available tokens reduced to 20. + err = tb.update(&rate{period: clock.Second, average: 10, burst: 20}) // the number of available tokens reduced to 20. require.NoError(t, err) // Then @@ -231,29 +249,31 @@ func TestUpdateBurstDecreased(t *testing.T) { // If rate is updated then it affects the bucket refill speed. func TestUpdateRateChanged(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(15) // 5 tokens available require.NoError(t, err) // When - err = tb.update(&rate{period: time.Second, average: 20, burst: 20}) // still 5 tokens available + err = tb.update(&rate{period: clock.Second, average: 20, burst: 20}) // still 5 tokens available require.NoError(t, err) // Then delay, err := tb.consume(20) require.NoError(t, err) - assert.Equal(t, time.Second/20*15, delay) + assert.Equal(t, clock.Second/20*15, delay) } // Only the most recent consumption is reverted by `Rollback`. func TestRollback(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(8) // 12 tokens available require.NoError(t, err) _, err = tb.consume(7) // 5 tokens available @@ -269,14 +289,17 @@ func TestRollback(t *testing.T) { delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, 100*time.Millisecond, delay) + assert.Equal(t, 100*clock.Millisecond, delay) } // It is safe to call `Rollback` several times. The second and all subsequent // calls just do nothing. func TestRollbackSeveralTimes(t *testing.T) { + done := testutils.FreezeTime() + defer done() + // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, testutils.GetClock()) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(8) // 12 tokens available require.NoError(t, err) tb.rollback() // 20 tokens available @@ -293,19 +316,22 @@ func TestRollbackSeveralTimes(t *testing.T) { delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, 100*time.Millisecond, delay) + assert.Equal(t, 100*clock.Millisecond, delay) } // If previous consumption returned a delay due to an attempt to consume more // tokens then there are available, then `Rollback` has no effect. func TestRollbackAfterAvailableExceeded(t *testing.T) { + done := testutils.FreezeTime() + defer done() + // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, testutils.GetClock()) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(8) // 12 tokens available require.NoError(t, err) delay, err := tb.consume(15) // still 12 tokens available require.NoError(t, err) - assert.Equal(t, 300*time.Millisecond, delay) + assert.Equal(t, 300*clock.Millisecond, delay) // When tb.rollback() // Previous operation consumed 0 tokens, so rollback has no effect. @@ -317,16 +343,17 @@ func TestRollbackAfterAvailableExceeded(t *testing.T) { delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, 100*time.Millisecond, delay) + assert.Equal(t, 100*clock.Millisecond, delay) } // If previous consumption returned a error due to an attempt to consume more // tokens then the bucket's burst size, then `Rollback` has no effect. func TestRollbackAfterError(t *testing.T) { - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // Given - tb := newTokenBucket(&rate{period: time.Second, average: 10, burst: 20}, clock) + tb := newTokenBucket(&rate{period: clock.Second, average: 10, burst: 20}) _, err := tb.consume(8) // 12 tokens available require.NoError(t, err) delay, err := tb.consume(21) // still 12 tokens available @@ -343,18 +370,16 @@ func TestRollbackAfterError(t *testing.T) { delay, err = tb.consume(1) require.NoError(t, err) - assert.Equal(t, 100*time.Millisecond, delay) + assert.Equal(t, 100*clock.Millisecond, delay) } func TestDivisionByZeroOnPeriod(t *testing.T) { - clock := &timetools.RealTime{} - var emptyPeriod int64 - tb := newTokenBucket(&rate{period: time.Duration(emptyPeriod), average: 2, burst: 2}, clock) + tb := newTokenBucket(&rate{period: time.Duration(emptyPeriod), average: 2, burst: 2}) _, err := tb.consume(1) assert.NoError(t, err) - err = tb.update(&rate{period: time.Nanosecond, average: 1, burst: 1}) + err = tb.update(&rate{period: clock.Nanosecond, average: 1, burst: 1}) assert.NoError(t, err) } diff --git a/ratelimit/bucketset.go b/ratelimit/bucketset.go index 71572fea..b2f5b204 100644 --- a/ratelimit/bucketset.go +++ b/ratelimit/bucketset.go @@ -5,25 +5,21 @@ import ( "sort" "strings" "time" - - "github.com/mailgun/timetools" ) // TokenBucketSet represents a set of TokenBucket covering different time periods. type TokenBucketSet struct { buckets map[time.Duration]*tokenBucket maxPeriod time.Duration - clock timetools.TimeProvider } // NewTokenBucketSet creates a `TokenBucketSet` from the specified `rates`. -func NewTokenBucketSet(rates *RateSet, clock timetools.TimeProvider) *TokenBucketSet { +func NewTokenBucketSet(rates *RateSet) *TokenBucketSet { tbs := new(TokenBucketSet) - tbs.clock = clock // In the majority of cases we will have only one bucket. tbs.buckets = make(map[time.Duration]*tokenBucket, len(rates.m)) for _, rate := range rates.m { - newBucket := newTokenBucket(rate, clock) + newBucket := newTokenBucket(rate) tbs.buckets[rate.period] = newBucket tbs.maxPeriod = maxDuration(tbs.maxPeriod, rate.period) } @@ -43,7 +39,7 @@ func (tbs *TokenBucketSet) Update(rates *RateSet) { // Add missing buckets. for _, rate := range rates.m { if _, ok := tbs.buckets[rate.period]; !ok { - newBucket := newTokenBucket(rate, tbs.clock) + newBucket := newTokenBucket(rate) tbs.buckets[rate.period] = newBucket } } @@ -102,7 +98,7 @@ func (tbs *TokenBucketSet) debugState() string { return strings.Join(bucketRepr, ", ") } -func maxDuration(x time.Duration, y time.Duration) time.Duration { +func maxDuration(x, y time.Duration) time.Duration { if x > y { return x } diff --git a/ratelimit/bucketset_test.go b/ratelimit/bucketset_test.go index fd76319c..613b637a 100644 --- a/ratelimit/bucketset_test.go +++ b/ratelimit/bucketset_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) @@ -13,29 +14,31 @@ import ( func TestLongestPeriod(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 20)) - require.NoError(t, rates.Add(7*time.Second, 10, 20)) - require.NoError(t, rates.Add(5*time.Second, 11, 21)) + require.NoError(t, rates.Add(1*clock.Second, 10, 20)) + require.NoError(t, rates.Add(7*clock.Second, 10, 20)) + require.NoError(t, rates.Add(5*clock.Second, 11, 21)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() // When - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) // Then - assert.Equal(t, 7*time.Second, tbs.maxPeriod) + assert.Equal(t, 7*clock.Second, tbs.maxPeriod) } // Successful token consumption updates state of all buckets in the set. func TestConsume(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 20)) - require.NoError(t, rates.Add(10*time.Second, 20, 50)) + require.NoError(t, rates.Add(1*clock.Second, 10, 20)) + require.NoError(t, rates.Add(10*clock.Second, 20, 50)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) // When delay, err := tbs.Consume(15) @@ -50,19 +53,20 @@ func TestConsume(t *testing.T) { func TestConsumeRefill(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 10, 20)) - require.NoError(t, rates.Add(100*time.Second, 20, 50)) + require.NoError(t, rates.Add(10*clock.Second, 10, 20)) + require.NoError(t, rates.Add(100*clock.Second, 20, 50)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(15) require.NoError(t, err) assert.Equal(t, "{10s: 5}, {1m40s: 35}", tbs.debugState()) // When - clock.Sleep(10 * time.Second) + clock.Advance(10 * clock.Second) delay, err := tbs.Consume(0) // Consumes nothing but forces an internal state update. require.NoError(t, err) @@ -77,12 +81,13 @@ func TestConsumeRefill(t *testing.T) { func TestConsumeLimitedBy1st(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 10, 10)) - require.NoError(t, rates.Add(100*time.Second, 20, 20)) + require.NoError(t, rates.Add(10*clock.Second, 10, 10)) + require.NoError(t, rates.Add(100*clock.Second, 20, 20)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(5) require.NoError(t, err) @@ -93,7 +98,7 @@ func TestConsumeLimitedBy1st(t *testing.T) { require.NoError(t, err) // Then - assert.Equal(t, 5*time.Second, delay) + assert.Equal(t, 5*clock.Second, delay) assert.Equal(t, "{10s: 5}, {1m40s: 15}", tbs.debugState()) } @@ -102,22 +107,23 @@ func TestConsumeLimitedBy1st(t *testing.T) { func TestConsumeLimitedBy2st(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 10, 10)) - require.NoError(t, rates.Add(100*time.Second, 20, 20)) + require.NoError(t, rates.Add(10*clock.Second, 10, 10)) + require.NoError(t, rates.Add(100*clock.Second, 20, 20)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(10) require.NoError(t, err) - clock.Sleep(10 * time.Second) + clock.Advance(10 * clock.Second) _, err = tbs.Consume(10) require.NoError(t, err) - clock.Sleep(5 * time.Second) + clock.Advance(5 * clock.Second) _, err = tbs.Consume(0) require.NoError(t, err) @@ -128,7 +134,7 @@ func TestConsumeLimitedBy2st(t *testing.T) { require.NoError(t, err) // Then - assert.Equal(t, 7*(5*time.Second), delay) + assert.Equal(t, 7*(5*clock.Second), delay) assert.Equal(t, "{10s: 5}, {1m40s: 3}", tbs.debugState()) } @@ -137,12 +143,13 @@ func TestConsumeLimitedBy2st(t *testing.T) { func TestConsumeMoreThenBurst(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 20)) - require.NoError(t, rates.Add(10*time.Second, 50, 100)) + require.NoError(t, rates.Add(1*clock.Second, 10, 20)) + require.NoError(t, rates.Add(10*clock.Second, 50, 100)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(5) require.NoError(t, err) @@ -160,84 +167,87 @@ func TestConsumeMoreThenBurst(t *testing.T) { func TestUpdateMore(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 20)) - require.NoError(t, rates.Add(10*time.Second, 20, 50)) - require.NoError(t, rates.Add(20*time.Second, 45, 90)) + require.NoError(t, rates.Add(1*clock.Second, 10, 20)) + require.NoError(t, rates.Add(10*clock.Second, 20, 50)) + require.NoError(t, rates.Add(20*clock.Second, 45, 90)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(5) require.NoError(t, err) assert.Equal(t, "{1s: 15}, {10s: 45}, {20s: 85}", tbs.debugState()) rates = NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 30, 40)) - require.NoError(t, rates.Add(11*time.Second, 30, 40)) - require.NoError(t, rates.Add(12*time.Second, 30, 40)) - require.NoError(t, rates.Add(13*time.Second, 30, 40)) + require.NoError(t, rates.Add(10*clock.Second, 30, 40)) + require.NoError(t, rates.Add(11*clock.Second, 30, 40)) + require.NoError(t, rates.Add(12*clock.Second, 30, 40)) + require.NoError(t, rates.Add(13*clock.Second, 30, 40)) // When tbs.Update(rates) // Then assert.Equal(t, "{10s: 40}, {11s: 40}, {12s: 40}, {13s: 40}", tbs.debugState()) - assert.Equal(t, 13*time.Second, tbs.maxPeriod) + assert.Equal(t, 13*clock.Second, tbs.maxPeriod) } // Update operation can remove buckets. func TestUpdateLess(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 20)) - require.NoError(t, rates.Add(10*time.Second, 20, 50)) - require.NoError(t, rates.Add(20*time.Second, 45, 90)) - require.NoError(t, rates.Add(30*time.Second, 50, 100)) + require.NoError(t, rates.Add(1*clock.Second, 10, 20)) + require.NoError(t, rates.Add(10*clock.Second, 20, 50)) + require.NoError(t, rates.Add(20*clock.Second, 45, 90)) + require.NoError(t, rates.Add(30*clock.Second, 50, 100)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(5) require.NoError(t, err) assert.Equal(t, "{1s: 15}, {10s: 45}, {20s: 85}, {30s: 95}", tbs.debugState()) rates = NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 25, 20)) - require.NoError(t, rates.Add(20*time.Second, 30, 21)) + require.NoError(t, rates.Add(10*clock.Second, 25, 20)) + require.NoError(t, rates.Add(20*clock.Second, 30, 21)) // When tbs.Update(rates) // Then assert.Equal(t, "{10s: 20}, {20s: 21}", tbs.debugState()) - assert.Equal(t, 20*time.Second, tbs.maxPeriod) + assert.Equal(t, 20*clock.Second, tbs.maxPeriod) } // Update operation can remove buckets. func TestUpdateAllDifferent(t *testing.T) { // Given rates := NewRateSet() - require.NoError(t, rates.Add(10*time.Second, 20, 50)) - require.NoError(t, rates.Add(30*time.Second, 50, 100)) + require.NoError(t, rates.Add(10*clock.Second, 20, 50)) + require.NoError(t, rates.Add(30*clock.Second, 50, 100)) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tbs := NewTokenBucketSet(rates, clock) + tbs := NewTokenBucketSet(rates) _, err := tbs.Consume(5) require.NoError(t, err) assert.Equal(t, "{10s: 45}, {30s: 95}", tbs.debugState()) rates = NewRateSet() - require.NoError(t, rates.Add(1*time.Second, 10, 40)) - require.NoError(t, rates.Add(60*time.Second, 100, 150)) + require.NoError(t, rates.Add(1*clock.Second, 10, 40)) + require.NoError(t, rates.Add(60*clock.Second, 100, 150)) // When tbs.Update(rates) // Then assert.Equal(t, "{1s: 40}, {1m0s: 150}", tbs.debugState()) - assert.Equal(t, 60*time.Second, tbs.maxPeriod) + assert.Equal(t, 60*clock.Second, tbs.maxPeriod) } diff --git a/ratelimit/tokenlimiter.go b/ratelimit/tokenlimiter.go index 2251d377..023cff1d 100644 --- a/ratelimit/tokenlimiter.go +++ b/ratelimit/tokenlimiter.go @@ -7,9 +7,9 @@ import ( "sync" "time" - "github.com/mailgun/timetools" - "github.com/mailgun/ttlmap" log "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" + "github.com/vulcand/oxy/internal/holsterv4/collections" "github.com/vulcand/oxy/utils" ) @@ -66,9 +66,8 @@ type TokenLimiter struct { defaultRates *RateSet extract utils.SourceExtractor extractRates RateExtractor - clock timetools.TimeProvider mutex sync.Mutex - bucketSets *ttlmap.TtlMap + bucketSets *collections.TTLMap errHandler utils.ErrorHandler capacity int next http.Handler @@ -98,11 +97,7 @@ func New(next http.Handler, extract utils.SourceExtractor, defaultRates *RateSet } } setDefaults(tl) - bucketSets, err := ttlmap.NewMapWithProvider(tl.capacity, tl.clock) - if err != nil { - return nil, err - } - tl.bucketSets = bucketSets + tl.bucketSets = collections.NewTTLMap(tl.capacity) return tl, nil } @@ -149,10 +144,10 @@ func (tl *TokenLimiter) consumeRates(req *http.Request, source string, amount in bucketSet = bucketSetI.(*TokenBucketSet) bucketSet.Update(effectiveRates) } else { - bucketSet = NewTokenBucketSet(effectiveRates, tl.clock) + bucketSet = NewTokenBucketSet(effectiveRates) // We set ttl as 10 times rate period. E.g. if rate is 100 requests/second per client ip // the counters for this ip will expire after 10 seconds of inactivity - err := tl.bucketSets.Set(source, bucketSet, int(bucketSet.maxPeriod/time.Second)*10+1) + err := tl.bucketSets.Set(source, bucketSet, int(bucketSet.maxPeriod/clock.Second)*10+1) if err != nil { return err } @@ -231,14 +226,6 @@ func ExtractRates(e RateExtractor) TokenLimiterOption { } } -// Clock sets the clock. -func Clock(clock timetools.TimeProvider) TokenLimiterOption { - return func(cl *TokenLimiter) error { - cl.clock = clock - return nil - } -} - // Capacity sets the capacity. func Capacity(capacity int) TokenLimiterOption { return func(cl *TokenLimiter) error { @@ -256,9 +243,6 @@ func setDefaults(tl *TokenLimiter) { if tl.capacity <= 0 { tl.capacity = DefaultCapacity } - if tl.clock == nil { - tl.clock = &timetools.RealTime{} - } if tl.errHandler == nil { tl.errHandler = defaultErrHandler } diff --git a/ratelimit/tokenlimiter_test.go b/ratelimit/tokenlimiter_test.go index 89f19e49..c6f866e3 100644 --- a/ratelimit/tokenlimiter_test.go +++ b/ratelimit/tokenlimiter_test.go @@ -5,10 +5,10 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" "github.com/vulcand/oxy/utils" ) @@ -21,14 +21,14 @@ func TestRateSetAdd(t *testing.T) { require.Error(t, err) // Invalid Average - err = rs.Add(time.Second, 0, 1) + err = rs.Add(clock.Second, 0, 1) require.Error(t, err) // Invalid Burst - err = rs.Add(time.Second, 1, 0) + err = rs.Add(clock.Second, 1, 0) require.Error(t, err) - err = rs.Add(time.Second, 1, 1) + err = rs.Add(clock.Second, 1, 1) require.NoError(t, err) assert.Equal(t, rs.String(), "map[1s:rate(1/1s, burst=1)]") } @@ -40,12 +40,13 @@ func TestHitLimit(t *testing.T) { }) rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, Clock(clock)) + l, err := New(handler, headerLimit, rates) require.NoError(t, err) srv := httptest.NewServer(l) @@ -61,7 +62,7 @@ func TestHitLimit(t *testing.T) { assert.Equal(t, 429, re.StatusCode) // Second later, the request from this ip will succeed - clock.Sleep(time.Second) + clock.Advance(clock.Second) re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a")) require.NoError(t, err) assert.Equal(t, http.StatusOK, re.StatusCode) @@ -74,12 +75,13 @@ func TestFailure(t *testing.T) { }) rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, faultyExtract, rates, Clock(clock)) + l, err := New(handler, faultyExtract, rates) require.NoError(t, err) srv := httptest.NewServer(l) @@ -97,12 +99,13 @@ func TestIsolation(t *testing.T) { }) rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, Clock(clock)) + l, err := New(handler, headerLimit, rates) require.NoError(t, err) srv := httptest.NewServer(l) @@ -130,12 +133,13 @@ func TestExpiration(t *testing.T) { }) rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, Clock(clock)) + l, err := New(handler, headerLimit, rates) require.NoError(t, err) srv := httptest.NewServer(l) @@ -151,7 +155,7 @@ func TestExpiration(t *testing.T) { assert.Equal(t, 429, re.StatusCode) // 24 hours later, the request from this ip will succeed - clock.Sleep(24 * time.Hour) + clock.Advance(24 * clock.Hour) re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a")) require.NoError(t, err) @@ -163,11 +167,11 @@ func TestExtractRates(t *testing.T) { // Given extractRates := func(*http.Request) (*RateSet, error) { rates := NewRateSet() - err := rates.Add(time.Second, 2, 2) + err := rates.Add(clock.Second, 2, 2) if err != nil { return nil, err } - err = rates.Add(60*time.Second, 10, 10) + err = rates.Add(60*clock.Second, 10, 10) if err != nil { return nil, err } @@ -175,16 +179,17 @@ func TestExtractRates(t *testing.T) { } rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - tl, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractRates))) + tl, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractRates))) require.NoError(t, err) srv := httptest.NewServer(tl) @@ -203,7 +208,7 @@ func TestExtractRates(t *testing.T) { require.NoError(t, err) assert.Equal(t, 429, re.StatusCode) - clock.Sleep(time.Second) + clock.Advance(clock.Second) re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a")) require.NoError(t, err) assert.Equal(t, http.StatusOK, re.StatusCode) @@ -217,16 +222,17 @@ func TestBadRateExtractor(t *testing.T) { } rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractor))) + l, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractor))) require.NoError(t, err) srv := httptest.NewServer(l) @@ -241,7 +247,7 @@ func TestBadRateExtractor(t *testing.T) { require.NoError(t, err) assert.Equal(t, 429, re.StatusCode) - clock.Sleep(time.Second) + clock.Advance(clock.Second) re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a")) require.NoError(t, err) assert.Equal(t, http.StatusOK, re.StatusCode) @@ -255,16 +261,17 @@ func TestExtractorEmpty(t *testing.T) { } rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { _, _ = w.Write([]byte("hello")) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, Clock(clock), ExtractRates(RateExtractorFunc(extractor))) + l, err := New(handler, headerLimit, rates, ExtractRates(RateExtractorFunc(extractor))) require.NoError(t, err) srv := httptest.NewServer(l) @@ -279,7 +286,7 @@ func TestExtractorEmpty(t *testing.T) { require.NoError(t, err) assert.Equal(t, 429, re.StatusCode) - clock.Sleep(time.Second) + clock.Advance(clock.Second) re, _, err = testutils.Get(srv.URL, testutils.Header("Source", "a")) require.NoError(t, err) @@ -289,7 +296,7 @@ func TestExtractorEmpty(t *testing.T) { func TestInvalidParams(t *testing.T) { // Rates are missing rs := NewRateSet() - err := rs.Add(time.Second, 1, 1) + err := rs.Add(clock.Second, 1, 1) require.NoError(t, err) // Empty @@ -312,7 +319,7 @@ func TestOptions(t *testing.T) { }) rates := NewRateSet() - err := rates.Add(time.Second, 1, 1) + err := rates.Add(clock.Second, 1, 1) require.NoError(t, err) errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, req *http.Request, err error) { @@ -320,9 +327,10 @@ func TestOptions(t *testing.T) { _, _ = w.Write([]byte(http.StatusText(http.StatusTeapot))) }) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - l, err := New(handler, headerLimit, rates, ErrorHandler(errHandler), Clock(clock)) + l, err := New(handler, headerLimit, rates, ErrorHandler(errHandler)) require.NoError(t, err) srv := httptest.NewServer(l) diff --git a/roundrobin/rebalancer.go b/roundrobin/rebalancer.go index b63c7986..bdcdcf3e 100644 --- a/roundrobin/rebalancer.go +++ b/roundrobin/rebalancer.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/mailgun/timetools" log "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/memmetrics" "github.com/vulcand/oxy/utils" ) @@ -31,12 +31,10 @@ type NewMeterFn func() (Meter, error) type Rebalancer struct { // mutex mtx *sync.Mutex - // As usual, control time in tests - clock timetools.TimeProvider // Time that freezes state machine to accumulate stats after updating the weights backoffDuration time.Duration // Timer is set to give probing some time to take place - timer time.Time + timer clock.Time // server records that remember original weights servers []*rbServer // next is internal load balancer next in chain @@ -57,14 +55,6 @@ type Rebalancer struct { log *log.Logger } -// RebalancerClock sets a clock. -func RebalancerClock(clock timetools.TimeProvider) RebalancerOption { - return func(r *Rebalancer) error { - r.clock = clock - return nil - } -} - // RebalancerBackoff sets a beck off duration. func RebalancerBackoff(d time.Duration) RebalancerOption { return func(r *Rebalancer) error { @@ -119,15 +109,12 @@ func NewRebalancer(handler balancerHandler, opts ...RebalancerOption) (*Rebalanc return nil, err } } - if rb.clock == nil { - rb.clock = &timetools.RealTime{} - } if rb.backoffDuration == 0 { - rb.backoffDuration = 10 * time.Second + rb.backoffDuration = 10 * clock.Second } if rb.newMeter == nil { rb.newMeter = func() (Meter, error) { - rc, err := memmetrics.NewRatioCounter(10, time.Second, memmetrics.RatioClock(rb.clock)) + rc, err := memmetrics.NewRatioCounter(10, clock.Second) if err != nil { return nil, err } @@ -170,7 +157,7 @@ func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { } pw := utils.NewProxyWriter(w) - start := rb.clock.UtcNow() + start := clock.Now().UTC() // make shallow copy of request before changing anything to avoid side effects newReq := *req @@ -214,7 +201,7 @@ func (rb *Rebalancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { rb.next.Next().ServeHTTP(pw, &newReq) - rb.recordMetrics(newReq.URL, pw.StatusCode(), rb.clock.UtcNow().Sub(start)) + rb.recordMetrics(newReq.URL, pw.StatusCode(), clock.Now().UTC().Sub(start)) rb.adjustWeights() } @@ -231,7 +218,7 @@ func (rb *Rebalancer) reset() { s.curWeight = s.origWeight _ = rb.next.UpsertServer(s.url, Weight(s.origWeight)) } - rb.timer = rb.clock.UtcNow().Add(-1 * time.Second) + rb.timer = clock.Now().UTC().Add(-1 * clock.Second) rb.ratings = make([]float64, len(rb.servers)) } @@ -369,11 +356,11 @@ func (rb *Rebalancer) setMarkedWeights() bool { } func (rb *Rebalancer) setTimer() { - rb.timer = rb.clock.UtcNow().Add(rb.backoffDuration) + rb.timer = clock.Now().UTC().Add(rb.backoffDuration) } func (rb *Rebalancer) timerExpired() bool { - return rb.timer.Before(rb.clock.UtcNow()) + return rb.timer.Before(clock.Now().UTC()) } func (rb *Rebalancer) metricsReady() bool { diff --git a/roundrobin/rebalancer_test.go b/roundrobin/rebalancer_test.go index 84bfcc43..5a43f9b4 100644 --- a/roundrobin/rebalancer_test.go +++ b/roundrobin/rebalancer_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vulcand/oxy/forward" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) @@ -99,9 +100,10 @@ func TestRebalancerRecovery(t *testing.T) { return &testMeter{}, nil } - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock)) + rb, err := NewRebalancer(lb, RebalancerMeter(newMeter)) require.NoError(t, err) err = rb.UpsertServer(testutils.ParseURI(a.URL)) @@ -119,7 +121,7 @@ func TestRebalancerRecovery(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } assert.Equal(t, 1, rb.servers[0].curWeight) @@ -136,7 +138,7 @@ func TestRebalancerRecovery(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } assert.Equal(t, 1, rb.servers[0].curWeight) @@ -164,9 +166,10 @@ func TestRebalancerCascading(t *testing.T) { return &testMeter{}, nil } - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock)) + rb, err := NewRebalancer(lb, RebalancerMeter(newMeter)) require.NoError(t, err) err = rb.UpsertServer(testutils.ParseURI(a.URL)) @@ -186,7 +189,7 @@ func TestRebalancerCascading(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } // We have increased the load, and the situation became worse as the other servers started failing @@ -204,7 +207,7 @@ func TestRebalancerCascading(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } // the algo reverted it back @@ -230,9 +233,10 @@ func TestRebalancerAllBad(t *testing.T) { return &testMeter{}, nil } - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock)) + rb, err := NewRebalancer(lb, RebalancerMeter(newMeter)) require.NoError(t, err) err = rb.UpsertServer(testutils.ParseURI(a.URL)) @@ -254,7 +258,7 @@ func TestRebalancerAllBad(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } // load balancer does nothing @@ -280,9 +284,10 @@ func TestRebalancerReset(t *testing.T) { return &testMeter{}, nil } - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rb, err := NewRebalancer(lb, RebalancerMeter(newMeter), RebalancerClock(clock)) + rb, err := NewRebalancer(lb, RebalancerMeter(newMeter)) require.NoError(t, err) err = rb.UpsertServer(testutils.ParseURI(a.URL)) @@ -304,7 +309,7 @@ func TestRebalancerReset(t *testing.T) { require.NoError(t, err) _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } // load balancer changed weights @@ -331,9 +336,10 @@ func TestRebalancerRequestRewriteListenerLive(t *testing.T) { lb, err := New(fwd) require.NoError(t, err) - clock := testutils.GetClock() + done := testutils.FreezeTime() + defer done() - rb, err := NewRebalancer(lb, RebalancerBackoff(time.Millisecond), RebalancerClock(clock)) + rb, err := NewRebalancer(lb, RebalancerBackoff(clock.Millisecond)) require.NoError(t, err) err = rb.UpsertServer(testutils.ParseURI(a.URL)) @@ -350,7 +356,7 @@ func TestRebalancerRequestRewriteListenerLive(t *testing.T) { _, _, err = testutils.Get(proxy.URL) require.NoError(t, err) if i%10 == 0 { - clock.CurrentTime = clock.CurrentTime.Add(rb.backoffDuration + time.Second) + clock.Advance(rb.backoffDuration + clock.Second) } } diff --git a/roundrobin/stickycookie/aes_value.go b/roundrobin/stickycookie/aes_value.go index ea47681a..3ac71132 100644 --- a/roundrobin/stickycookie/aes_value.go +++ b/roundrobin/stickycookie/aes_value.go @@ -13,6 +13,8 @@ import ( "strconv" "strings" "time" + + "github.com/vulcand/oxy/internal/holsterv4/clock" ) // AESValue manages hashed sticky value. @@ -41,14 +43,14 @@ func NewAESValue(key []byte, ttl time.Duration) (*AESValue, error) { func (v *AESValue) Get(raw *url.URL) string { base := raw.String() if v.ttl > 0 { - base = fmt.Sprintf("%s|%d", base, time.Now().UTC().Add(v.ttl).Unix()) + base = fmt.Sprintf("%s|%d", base, clock.Now().UTC().Add(v.ttl).Unix()) } // Nonce is the 64bit nanosecond-resolution time, plus 32bits of crypto/rand, for 96bits (12Bytes). // Theoretically, if 2^32 calls were made in 1 nanoseconds, there might be a repeat. // Adds ~765ns, and 4B heap in 1 alloc nonce := make([]byte, 12) - binary.PutVarint(nonce, time.Now().UnixNano()) + binary.PutVarint(nonce, clock.Now().UnixNano()) rpend := make([]byte, 4) if _, err := io.ReadFull(rand.Reader, rpend); err != nil { @@ -126,8 +128,8 @@ func (v *AESValue) fromValue(obfuscatedStr string) (string, error) { return "", err } - if time.Now().UTC().After(time.Unix(i, 0).UTC()) { - strTime := time.Unix(i, 0).UTC().String() + if clock.Now().UTC().After(clock.Unix(i, 0).UTC()) { + strTime := clock.Unix(i, 0).UTC().String() return "", fmt.Errorf("TTL expired: '%s' (%s)\n", raw, strTime) } diff --git a/roundrobin/stickycookie/fallback_value_test.go b/roundrobin/stickycookie/fallback_value_test.go index 08ed1d54..cfda4dfb 100644 --- a/roundrobin/stickycookie/fallback_value_test.go +++ b/roundrobin/stickycookie/fallback_value_test.go @@ -5,10 +5,10 @@ import ( "net/url" "path" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vulcand/oxy/internal/holsterv4/clock" ) func TestFallbackValue_FindURL(t *testing.T) { @@ -19,7 +19,7 @@ func TestFallbackValue_FindURL(t *testing.T) { {Scheme: "http", Host: "10.10.10.10", Path: "/"}, } - aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second) + aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second) require.NoError(t, err) values := []struct { @@ -81,7 +81,7 @@ func TestFallbackValue_FindURL_error(t *testing.T) { hashValue := &HashValue{Salt: "foo"} rawValue := &RawValue{} - aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second) + aesValue, err := NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second) require.NoError(t, err) tests := []struct { diff --git a/roundrobin/stickysessions_test.go b/roundrobin/stickysessions_test.go index 5f32408a..9a3e1615 100644 --- a/roundrobin/stickysessions_test.go +++ b/roundrobin/stickysessions_test.go @@ -7,11 +7,11 @@ import ( "net/http/httptest" "net/url" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vulcand/oxy/forward" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/roundrobin/stickycookie" "github.com/vulcand/oxy/testutils" ) @@ -125,7 +125,7 @@ func TestBasicWithAESValue(t *testing.T) { sticky := NewStickySession("test") require.NotNil(t, sticky) - aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second) + aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second) require.NoError(t, err) sticky.SetCookieValue(aesValue) @@ -284,13 +284,13 @@ func TestStickyCookieWithOptions(t *testing.T) { desc: "Expires", name: "test", options: CookieOptions{ - Expires: time.Date(1955, 11, 12, 1, 22, 0, 0, time.UTC), + Expires: clock.Date(1955, 11, 12, 1, 22, 0, 0, clock.UTC), }, expected: &http.Cookie{ Name: "test", Value: a.URL, Path: "/", - Expires: time.Date(1955, 11, 12, 1, 22, 0, 0, time.UTC), + Expires: clock.Date(1955, 11, 12, 1, 22, 0, 0, clock.UTC), RawExpires: "Sat, 12 Nov 1955 01:22:00 GMT", Raw: fmt.Sprintf("test=%s; Path=/; Expires=Sat, 12 Nov 1955 01:22:00 GMT", a.URL), }, @@ -536,11 +536,11 @@ func TestStickySession_GetBackend(t *testing.T) { rawValue := &stickycookie.RawValue{} hashValue := &stickycookie.HashValue{} saltyHashValue := &stickycookie.HashValue{Salt: "test salt"} - aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*time.Second) + aesValue, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 5*clock.Second) require.NoError(t, err) aesValueInfinite, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 0) require.NoError(t, err) - aesValueExpired, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 1*time.Nanosecond) + aesValueExpired, err := stickycookie.NewAESValue([]byte("95Bx9JkKX3xbd7z3"), 1*clock.Nanosecond) require.NoError(t, err) tests := []struct { diff --git a/stream/stream_test.go b/stream/stream_test.go index 26240589..803e8ab8 100644 --- a/stream/stream_test.go +++ b/stream/stream_test.go @@ -9,12 +9,12 @@ import ( "net/http" "net/http/httptest" "testing" - "time" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vulcand/oxy/forward" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/testutils" ) @@ -73,10 +73,10 @@ func TestChunkedEncodingSuccess(t *testing.T) { } _, _ = fmt.Fprint(w, "Response") flusher.Flush() - time.Sleep(time.Duration(500) * time.Millisecond) + clock.Sleep(500 * clock.Millisecond) _, _ = fmt.Fprint(w, "in") flusher.Flush() - time.Sleep(time.Duration(500) * time.Millisecond) + clock.Sleep(500 * clock.Millisecond) _, _ = fmt.Fprint(w, "Chunks") flusher.Flush() }) diff --git a/testutils/utils.go b/testutils/utils.go index b73f37b5..3d21df80 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -8,9 +8,8 @@ import ( "net/http/httptest" "net/url" "strings" - "time" - "github.com/mailgun/timetools" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/utils" ) @@ -175,9 +174,9 @@ func Post(uri string, opts ...ReqOption) (*http.Response, []byte, error) { return MakeRequest(uri, opts...) } -// GetClock gets a FreezedTime. -func GetClock() *timetools.FreezedTime { - return &timetools.FreezedTime{ - CurrentTime: time.Date(2012, 3, 4, 5, 6, 7, 0, time.UTC), - } +// FreezeTime to the predetermined time. Returns a function that should be +// deferred to unfreeze time. Meant for testing. +func FreezeTime() func() { + clock.Freeze(clock.Date(2012, 3, 4, 5, 6, 7, 0, clock.UTC)) + return clock.Unfreeze } diff --git a/trace/trace.go b/trace/trace.go index 96892404..ce5e51d9 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -11,6 +11,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "github.com/vulcand/oxy/internal/holsterv4/clock" "github.com/vulcand/oxy/utils" ) @@ -84,11 +85,11 @@ func Logger(l *log.Logger) Option { } func (t *Tracer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - start := time.Now() + start := clock.Now() pw := utils.NewProxyWriterWithLogger(w, t.log) t.next.ServeHTTP(pw, req) - l := t.newRecord(req, pw, time.Since(start)) + l := t.newRecord(req, pw, clock.Since(start)) if err := json.NewEncoder(t.writer).Encode(l); err != nil { t.log.Errorf("Failed to marshal request: %v", err) } @@ -106,7 +107,7 @@ func (t *Tracer) newRecord(req *http.Request, pw *utils.ProxyWriter, diff time.D Response: Response{ Code: pw.StatusCode(), BodyBytes: bodyBytes(pw.Header()), - Roundtrip: float64(diff) / float64(time.Millisecond), + Roundtrip: float64(diff) / float64(clock.Millisecond), Headers: captureHeaders(pw.Header(), t.respHeaders), }, }