Skip to content

Commit

Permalink
Initial version of the code.
Browse files Browse the repository at this point in the history
  • Loading branch information
karagog committed Sep 7, 2021
1 parent a7e57b2 commit 54eb166
Show file tree
Hide file tree
Showing 11 changed files with 510 additions and 0 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
# clock-go
Provides a "Clock" interface for facilitating dependency injection in time-related Go code.

## Why?

Golang's "time" package does not facilitate dependency injection by itself, because it relies on package-level functions (e.g. time.Now()). It can be very tricky to properly unit test code like that, because it may require carefully choreographed sleep statements and may end up non-deterministic/flaky. Using a dependency-injected clock allows you to use a real clock in production and a deterministic simulated clock in unit tests.

## Example

```go
package main

import (
"github.com/karagog/clock-go"
"github.com/karagog/clock-go/real"
"github.com/karagog/clock-go/simulated"
)

// Given a function that needs the current time, inject a clock
// object instead of calling "time.Now()".
func DoIt(c clock.Clock) {
fmt.Printf("The time is %v", c.Now())
}

func main() {
// You can inject a real clock, which delegates to time.Now().
DoIt(&real.Clock{})

// ...or you can use a simulated clock, whose output is
// deterministic and controlled by you.
//
// For example, this simulated clock is one hour in the future:
s := simulated.NewClock(time.Now().Add(time.Hour))
DoIt(s)

// You can advance the time in any increments you want.
s.Advance(time.Second)
DoIt(s)
}
```
30 changes: 30 additions & 0 deletions clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Package clock implements a clock interface.
//
// Your application code should use the clock interface to interact with time wherever possible,
// and then it can be easily unit-tested by swapping out the "real" clock with a "simulated"
// one during unit tests via dependency injection techniques.
package clock

import (
"time"
)

// An abstract clock interface to facilitate dependency injection of time.
//
// Implementations must provide thread-safe access to interface methods, to allow
// clocks to be used in background goroutines.
type Clock interface {
// Returns the current time.
Now() time.Time

// Returns a timer that will fire after the given duration from Now().
NewTimer(time.Duration) Timer
}

// This has essentially the same interface as a time.Timer, except C() is a function
// in order to make it a pure interface that we can mock.
type Timer interface {
Reset(time.Duration) bool
Stop() bool
C() <-chan time.Time
}
3 changes: 3 additions & 0 deletions clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package clock

// There are no tests for an abstract interface.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/karagog/clock-go

go 1.17

require github.com/golang/glog v1.0.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
36 changes: 36 additions & 0 deletions real/real.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Package real implements a real clock, which delegates to the "time" package
// methods to access the time.
package real

import (
"time"

"github.com/karagog/clock-go"
)

// A real clock which calls `time.Now()` to get the time.
type Clock struct{}

func (*Clock) Now() time.Time { return time.Now() }
func (*Clock) NewTimer(d time.Duration) clock.Timer {
return &realTimer{
timer: time.NewTimer(d),
}
}

// This implementation of the timer interface is backed by a real timer.
type realTimer struct {
timer *time.Timer
}

func (t *realTimer) Reset(d time.Duration) bool {
return t.timer.Reset(d)
}

func (t *realTimer) Stop() bool {
return t.timer.Stop()
}

func (t *realTimer) C() <-chan time.Time {
return t.timer.C
}
35 changes: 35 additions & 0 deletions real/real_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package real

import (
"testing"
"time"

"github.com/karagog/clock-go/testutil"
)

func TestRealClock(t *testing.T) {
c := Clock{}
n := c.Now()
now := time.Now()
diff := now.Sub(n)
tolerance := time.Millisecond
if diff > tolerance {
t.Fatalf("Got time %v, want %v to within %v tolerance. Diff was %v.",
n, now, tolerance, diff)
}
}

// No need to test this thoroughly, since it's backed by Golang's real timer.
// We'll just test it enough to show that it doesn't crash.
func TestRealClockTimer(t *testing.T) {
c := Clock{}
d := time.Millisecond
tmr := c.NewTimer(d)
if _, fired := testutil.TryRead(tmr.C(), time.Second); !fired {
t.Fatalf("Timer did not fire after %v, wanted it to fire", time.Second)
}
if tmr.Stop() {
<-tmr.C()
}
tmr.Reset(time.Second)
}
114 changes: 114 additions & 0 deletions simulated/simulated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Package simulated implements a simulated clock for unit tests.
package simulated

import (
"sync"
"time"

"github.com/golang/glog"
"github.com/karagog/clock-go"
)

// Clock is a fake clock that you can manipulate through the accessor methods.
//
// Create with NewClock(). Advance time with Advance().
type Clock struct {
mu sync.RWMutex // guards all members below
now time.Time
activeTimers map[*simulatedTimer]bool
}

func NewClock(now time.Time) *Clock {
return &Clock{
now: now,
activeTimers: make(map[*simulatedTimer]bool),
}
}

func (s *Clock) Advance(d time.Duration) {
if d < 0 {
panic("Time can only advance in the positive direction")
}
s.mu.Lock()
defer s.mu.Unlock()
newTime := s.now.Add(d)
glog.V(1).Infof("Advancing the time %v from %v to %v", newTime.Sub(s.now), s.now, newTime)
s.now = newTime
for t := range s.activeTimers {
if t.shouldFire(s.now) {
t.fire() // timer fired!
}
}
}

func (s *Clock) Now() time.Time {
s.mu.RLock()
defer s.mu.RUnlock()
return s.now
}

func (s *Clock) NewTimer(d time.Duration) clock.Timer {
t := &simulatedTimer{
c: make(chan time.Time, 1), // small buffer to avoid blocking
clock: s,
}
t.Reset(d)
return t
}

// The timer object may run on a separate goroutine from the clock object, but
// it should never be used from multiple goroutines simultaneously.
type simulatedTimer struct {
clock *Clock
c chan time.Time
deadline time.Time // when the timer should fire
active bool // true if the timer has not yet fired
}

func (t *simulatedTimer) Reset(d time.Duration) bool {
t.clock.mu.Lock()
defer t.clock.mu.Unlock()
wasActive := t.active
t.active = true
now := t.clock.now
newDeadline := now.Add(d)
glog.V(1).Infof("Resetting timer from deadline %v to %v", t.deadline, newDeadline)
t.deadline = newDeadline
if t.shouldFire(now) {
t.fire()
return wasActive
}
if !wasActive {
glog.V(1).Infof("Timer became active")
t.clock.activeTimers[t] = true
}
return wasActive
}

func (t *simulatedTimer) shouldFire(now time.Time) bool {
return !now.Before(t.deadline)
}

func (t *simulatedTimer) fire() {
glog.V(1).Infof("Timer fired at %v", t.deadline)
t.c <- t.deadline
t.stopNoLock()
}

func (t *simulatedTimer) Stop() bool {
t.clock.mu.Lock()
defer t.clock.mu.Unlock()
return t.stopNoLock()
}

// LOCK_REQUIRED t.clock.mu
func (t *simulatedTimer) stopNoLock() bool {
wasActive := t.active
t.active = false
delete(t.clock.activeTimers, t)
return wasActive
}

func (t *simulatedTimer) C() <-chan time.Time {
return t.c
}
Loading

0 comments on commit 54eb166

Please sign in to comment.