diff --git a/benchmarks/go.mod b/benchmarks/go.mod index 8024e66b2..acd36a71d 100644 --- a/benchmarks/go.mod +++ b/benchmarks/go.mod @@ -16,7 +16,6 @@ require ( ) require ( - github.com/benbjohnson/clock v1.3.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/benchmarks/go.sum b/benchmarks/go.sum index 4ae6da835..c5ab9b30e 100644 --- a/benchmarks/go.sum +++ b/benchmarks/go.sum @@ -5,8 +5,6 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/exp/go.sum b/exp/go.sum index 96489348a..89b07e5ed 100644 --- a/exp/go.sum +++ b/exp/go.sum @@ -1,4 +1,3 @@ -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/go.mod b/go.mod index 455dae496..9a091d941 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module go.uber.org/zap go 1.19 require ( - github.com/benbjohnson/clock v1.3.0 github.com/stretchr/testify v1.8.1 go.uber.org/goleak v1.2.0 go.uber.org/multierr v1.10.0 diff --git a/go.sum b/go.sum index ffa795531..6f3b5b06c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/internal/ztest/clock.go b/internal/ztest/clock.go index fe8026d94..cfdd83d69 100644 --- a/internal/ztest/clock.go +++ b/internal/ztest/clock.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Uber Technologies, Inc. +// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,30 +21,167 @@ package ztest import ( + "container/heap" + "sync" "time" - - "github.com/benbjohnson/clock" ) -// MockClock provides control over the time. -type MockClock struct{ m *clock.Mock } +// MockClock is a fake source of time. +// It implements standard time operations, +// but allows the user to control the passage of time. +// +// Use the [Add] method to progress time. +type MockClock struct { + mu sync.RWMutex + now time.Time -// NewMockClock builds a new mock clock that provides control of time. + // The MockClock works by maintaining a list of waiters. + // Each waiter knows the time at which it should be resolved. + // When the clock advances, all waiters that are in range are resolved + // in chronological order. + waiters waiters +} + +// NewMockClock builds a new mock clock +// using the current actual time as the initial time. func NewMockClock() *MockClock { - return &MockClock{clock.NewMock()} + return &MockClock{ + now: time.Now(), + } } // Now reports the current time. func (c *MockClock) Now() time.Time { - return c.m.Now() + c.mu.RLock() + defer c.mu.RUnlock() + return c.now } // NewTicker returns a time.Ticker that ticks at the specified frequency. +// +// As with [time.NewTicker], +// the ticker will drop ticks if the receiver is slow, +// and the channel is never closed. func (c *MockClock) NewTicker(d time.Duration) *time.Ticker { - return &time.Ticker{C: c.m.Ticker(d).C} + ch := make(chan time.Time, 1) + + var tick func(time.Time) + tick = func(now time.Time) { + next := now.Add(d) + c.runAt(next, func() { + defer tick(next) + + select { + case ch <- next: + // ok + default: + // The receiver is slow. + // Drop the tick and continue. + } + }) + } + tick(c.Now()) + + return &time.Ticker{C: ch} } // Add progresses time by the given duration. +// +// Other operations waiting for the time to advance +// will be resolved if they are within range. +// +// Panics if the duration is negative. func (c *MockClock) Add(d time.Duration) { - c.m.Add(d) + if d < 0 { + panic("cannot add negative duration") + } + + c.mu.Lock() + defer c.mu.Unlock() + + newTime := c.now.Add(d) + // newTime won't be recorded until the end of this method. + // This ensures that any waiters that are resolved + // are resolved at the time they were expecting. + + for w, ok := c.waiters.PopLTE(newTime); ok; w, ok = c.waiters.PopLTE(newTime) { + // The waiter is within range. + // Travel to the time of the waiter and resolve it. + c.now = w.until + + // The waiter may schedule more work + // so we must release the lock. + c.mu.Unlock() + w.fn() + // Sleeping here is necessary to let the side effects of waiters + // take effect before we continue. + time.Sleep(1 * time.Millisecond) + c.mu.Lock() + } + + c.now = newTime +} + +// runAt schedules the given function to be run at the given time. +// The function runs without a lock held, so it may schedule more work. +func (c *MockClock) runAt(t time.Time, fn func()) { + c.mu.Lock() + defer c.mu.Unlock() + c.waiters.Push(waiter{until: t, fn: fn}) +} + +type waiter struct { + until time.Time + fn func() +} + +// waiters is a thread-safe collection of waiters +// with the next waiter to be resolved at the front. +// +// Use the methods on this type to manipulate the collection. +// Do not modify the slice directly. +type waiters struct{ heap waiterHeap } + +// Push adds a new waiter to the collection. +func (w *waiters) Push(v waiter) { + heap.Push(&w.heap, v) +} + +// PopLTE removes and returns the next waiter to be resolved +// if it is scheduled to be resolved at or before the given time. +// +// Returns false if there are no waiters in range. +func (w *waiters) PopLTE(t time.Time) (_ waiter, ok bool) { + if len(w.heap) == 0 || w.heap[0].until.After(t) { + return waiter{}, false + } + + return heap.Pop(&w.heap).(waiter), true +} + +// waiterHeap implements a min-heap of waiters based on their 'until' time. +// +// This is separate from the waiters type so that we can implement heap.Interface +// while still exposing a type-safe API on waiters. +type waiterHeap []waiter + +var _ heap.Interface = (*waiterHeap)(nil) + +func (h waiterHeap) Len() int { return len(h) } +func (h waiterHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h waiterHeap) Less(i, j int) bool { + return h[i].until.Before(h[j].until) +} + +func (h *waiterHeap) Push(x interface{}) { + *h = append(*h, x.(waiter)) +} + +func (h *waiterHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[:n-1] + return x } diff --git a/internal/ztest/clock_test.go b/internal/ztest/clock_test.go index 3808ed782..6db724bd9 100644 --- a/internal/ztest/clock_test.go +++ b/internal/ztest/clock_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Uber Technologies, Inc. +// Copyright (c) 2023 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -55,3 +55,26 @@ func TestMockClock_NewTicker(t *testing.T) { assert.Equal(t, int32(2), n.Load()) close(quit) } + +func TestMockClock_NewTicker_slowConsumer(t *testing.T) { + clock := NewMockClock() + + ticker := clock.NewTicker(time.Microsecond) + defer ticker.Stop() + + // Two ticks, only one consumed. + clock.Add(2 * time.Microsecond) + <-ticker.C + + select { + case <-ticker.C: + t.Fatal("unexpected tick") + default: + // ok + } +} + +func TestMockClock_Add_negative(t *testing.T) { + clock := NewMockClock() + assert.Panics(t, func() { clock.Add(-1) }) +} diff --git a/zapgrpc/internal/test/go.sum b/zapgrpc/internal/test/go.sum index 51be79372..c2611afc8 100644 --- a/zapgrpc/internal/test/go.sum +++ b/zapgrpc/internal/test/go.sum @@ -2,8 +2,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=