Skip to content

Commit

Permalink
Add context methods to Clock interface
Browse files Browse the repository at this point in the history
This allows for using timeout/deadline functionality built in to
context.Context with a custom clock implementation.

Module Go version bumped to 1.20 due to use of
context.WithDeadlineCause and context.WithTimeoutCause.
  • Loading branch information
justinruggles committed Oct 26, 2023
1 parent ed66e78 commit e7d80d7
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 1 deletion.
29 changes: 29 additions & 0 deletions clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ func (c defaultClock) AfterFunc(d time.Duration, f func()) StopTimer {
return time.AfterFunc(d, f)
}

func (c defaultClock) ContextWithDeadline(ctx context.Context, t time.Time) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, t)
}

func (c defaultClock) ContextWithDeadlineCause(ctx context.Context, t time.Time, cause error) (context.Context, context.CancelFunc) {
return context.WithDeadlineCause(ctx, t, cause)

Check failure on line 62 in clock.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 1.18)

undefined: context.WithDeadlineCause

Check failure on line 62 in clock.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 1.19)

undefined: context.WithDeadlineCause
}

func (c defaultClock) ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, d)
}

func (c defaultClock) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return context.WithTimeoutCause(ctx, d, cause)

Check failure on line 70 in clock.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 1.18)

undefined: context.WithTimeoutCause

Check failure on line 70 in clock.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 1.19)

undefined: context.WithTimeoutCause
}

// DefaultClock returns a clock that minimally wraps the `time` package
func DefaultClock() Clock {
return defaultClock{}
Expand All @@ -80,4 +96,17 @@ type Clock interface {
// The callback function f will be executed after the interval d has
// elapsed, unless the returned timer's Stop() method is called first.
AfterFunc(d time.Duration, f func()) StopTimer

// ContextWithDeadline behaves like context.WithDeadline, but it uses the
// clock to determine the when the deadline has expired.
ContextWithDeadline(ctx context.Context, t time.Time) (context.Context, context.CancelFunc)
// ContextWithDeadlineCause behaves like context.WithDeadlineCause, but it
// uses the clock to determine the when the deadline has expired.
ContextWithDeadlineCause(ctx context.Context, t time.Time, cause error) (context.Context, context.CancelFunc)
// ContextWithTimeout behaves like context.WithTimeout, but it uses the
// clock to determine the when the timeout has elapsed.
ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc)
// ContextWithTimeoutCause behaves like context.WithTimeoutCause, but it
// uses the clock to determine the when the timeout has elapsed.
ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc)
}
57 changes: 57 additions & 0 deletions fake/fake_clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fake
import (
"context"
"sync"
"sync/atomic"
"time"

clocks "github.com/vimeo/go-clocks"
Expand Down Expand Up @@ -410,3 +411,59 @@ func (f *Clock) AwaitTimerAborts(n int) {
func (f *Clock) WaitAfterFuncs() {
f.cbsWG.Wait()
}

type deadlineContext struct {
context.Context
timedOut atomic.Bool
deadline time.Time
}

func (d *deadlineContext) Deadline() (time.Time, bool) {
return d.deadline, true
}

func (d *deadlineContext) Err() error {
if d.timedOut.Load() {
return context.DeadlineExceeded
}
return d.Context.Err()
}

func (f *Clock) ContextWithDeadlineCause(ctx context.Context, t time.Time, cause error) (context.Context, context.CancelFunc) {
cctx, cancelCause := context.WithCancelCause(ctx)
dctx := &deadlineContext{
Context: cctx,
deadline: t,
}
dur := f.Until(t)
if dur <= 0 {
dctx.timedOut.CompareAndSwap(false, true)
cancelCause(cause)
return dctx, func() {
cancelCause(context.Canceled)
}
}
stop := f.AfterFunc(dur, func() {
if cctx.Err() == nil {
dctx.timedOut.CompareAndSwap(false, true)
}
cancelCause(cause)
})
cancel := func() {
cancelCause(context.Canceled)
stop.Stop()
}
return dctx, cancel
}

func (c *Clock) ContextWithDeadline(ctx context.Context, t time.Time) (context.Context, context.CancelFunc) {
return c.ContextWithDeadlineCause(ctx, t, nil)
}

func (c *Clock) ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return c.ContextWithDeadlineCause(ctx, c.Now().Add(d), nil)
}

func (c *Clock) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return c.ContextWithDeadlineCause(ctx, c.Now().Add(d), cause)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/vimeo/go-clocks

go 1.14
go 1.20
16 changes: 16 additions & 0 deletions offset/offset_clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ func (o *Clock) AfterFunc(d time.Duration, f func()) clocks.StopTimer {
return o.inner.AfterFunc(d, f)
}

func (o *Clock) ContextWithDeadline(ctx context.Context, t time.Time) (context.Context, context.CancelFunc) {
return o.inner.ContextWithDeadline(ctx, t.Add(o.offset))
}

func (o *Clock) ContextWithDeadlineCause(ctx context.Context, t time.Time, cause error) (context.Context, context.CancelFunc) {
return o.inner.ContextWithDeadlineCause(ctx, t.Add(o.offset), cause)
}

func (o *Clock) ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return o.inner.ContextWithTimeout(ctx, d+o.offset)
}

func (o *Clock) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return o.inner.ContextWithTimeoutCause(ctx, d+o.offset, cause)
}

// NewOffsetClock creates an OffsetClock. offset is added to all absolute times.
func NewOffsetClock(inner clocks.Clock, offset time.Duration) *Clock {
return &Clock{
Expand Down

0 comments on commit e7d80d7

Please sign in to comment.