Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace benbjohnson/clock with custom MockClock #1349

Merged
merged 6 commits into from Sep 9, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion benchmarks/go.mod
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions benchmarks/go.sum
Expand Up @@ -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=
Expand Down
1 change: 0 additions & 1 deletion 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=
Expand Down
1 change: 0 additions & 1 deletion go.mod
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions 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=
Expand Down
157 changes: 147 additions & 10 deletions 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
Expand All @@ -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.
abhinav marked this conversation as resolved.
Show resolved Hide resolved
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) {
abhinav marked this conversation as resolved.
Show resolved Hide resolved
// 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)
abhinav marked this conversation as resolved.
Show resolved Hide resolved
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 }
abhinav marked this conversation as resolved.
Show resolved Hide resolved

// 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]
abhinav marked this conversation as resolved.
Show resolved Hide resolved
*h = old[:n-1]
return x
}
25 changes: 24 additions & 1 deletion 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
Expand Down Expand Up @@ -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) })
}
2 changes: 0 additions & 2 deletions zapgrpc/internal/test/go.sum
Expand Up @@ -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=
Expand Down