Skip to content

Commit

Permalink
Replace benbjohnson/clock with custom MockClock (#1349)
Browse files Browse the repository at this point in the history
This drops the dependency on benbjohnson/clock as it's no longer
maintained.
It replaces it with a hand-rolled mock clock implementation.

The core of the functionality of MockClock is provided by the following:

- waiter: a function waiting to be executed
- runAt: schedules a function to be executed when time progresses
- Add: moves time forward, running all functions in range

The rest of the necessary functionality is built on top of these.

This is a pretty simple implementation.
We schedule work, but nothing happens until Add is called.
There are no goroutines running additional work in the background.

Resolves #1331
  • Loading branch information
abhinav committed Sep 9, 2023
1 parent b7aed24 commit 82c728b
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 20 deletions.
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
123 changes: 113 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,133 @@
package ztest

import (
"sort"
"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

// 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 []waiter
}

// NewMockClock builds a new mock clock that provides control of time.
// 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.
//
// Calling Stop on the returned ticker is a no-op.
// The ticker only runs when the clock is advanced.
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}
}

// 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 = append(c.waiters, waiter{until: t, fn: fn})
}

type waiter struct {
until time.Time
fn func()
}

// Add progresses time by the given duration.
// Other operations waiting for the time to advance
// will be resolved if they are within range.
//
// Side effects of operations waiting for the time to advance
// will take effect on a best-effort basis.
// Avoid racing with operations that have side effects.
//
// 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()

sort.Slice(c.waiters, func(i, j int) bool {
return c.waiters[i].until.Before(c.waiters[j].until)
})

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 len(c.waiters) > 0 {
w := c.waiters[0]
if w.until.After(newTime) {
break
}
c.waiters[0] = waiter{} // avoid memory leak
c.waiters = c.waiters[1:]

// 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
}
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

0 comments on commit 82c728b

Please sign in to comment.