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 131057c
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 0 deletions.
21 changes: 21 additions & 0 deletions clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ 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) ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, d)
}

// DefaultClock returns a clock that minimally wraps the `time` package
func DefaultClock() Clock {
return defaultClock{}
Expand All @@ -80,4 +88,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)
}
16 changes: 16 additions & 0 deletions clock_120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build go1.20

package clocks

import (
"context"
"time"
)

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

func (c defaultClock) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return context.WithTimeoutCause(ctx, d, cause)
}
16 changes: 16 additions & 0 deletions clock_pre120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !go1.20

package clocks

import (
"context"
"time"
)

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

func (c defaultClock) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, d)
}
30 changes: 30 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,32 @@ func (f *Clock) AwaitTimerAborts(n int) {
func (f *Clock) WaitAfterFuncs() {
f.cbsWG.Wait()
}

type deadlineContext struct {
context.Context
timedOut atomic.Bool

Check failure on line 417 in fake/fake_clock.go

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest, 1.18)

undefined: 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 (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)
}
35 changes: 35 additions & 0 deletions fake/fake_clock_120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build go1.20

package fake

import (
"context"
"time"
)

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
}
35 changes: 35 additions & 0 deletions fake/fake_clock_pre120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build !go1.20

package fake

import (
"context"
"time"
)

func (f *Clock) ContextWithDeadlineCause(ctx context.Context, t time.Time, cause error) (context.Context, context.CancelFunc) {
cctx, cancel := context.WithCancel(ctx)
dctx := &deadlineContext{
Context: cctx,
deadline: t,
}
dur := f.Until(t)
if dur <= 0 {
dctx.timedOut.CompareAndSwap(false, true)
cancel()
return dctx, func() {
cancel()
}
}
stop := f.AfterFunc(dur, func() {
if cctx.Err() == nil {
dctx.timedOut.CompareAndSwap(false, true)
}
cancel()
})
cancelStop := func() {
cancel()
stop.Stop()
}
return dctx, cancelStop
}
8 changes: 8 additions & 0 deletions offset/offset_clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ 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) ContextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
return o.inner.ContextWithTimeout(ctx, d+o.offset)
}

// NewOffsetClock creates an OffsetClock. offset is added to all absolute times.
func NewOffsetClock(inner clocks.Clock, offset time.Duration) *Clock {
return &Clock{
Expand Down
16 changes: 16 additions & 0 deletions offset/offset_clock_120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build go1.20

package offset

import (
"context"
"time"
)

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) ContextWithTimeoutCause(ctx context.Context, d time.Duration, cause error) (context.Context, context.CancelFunc) {
return o.inner.ContextWithTimeoutCause(ctx, d+o.offset, cause)
}
16 changes: 16 additions & 0 deletions offset/offset_clock_pre120.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build !go1.20

package offset

import (
"context"
"time"
)

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

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

0 comments on commit 131057c

Please sign in to comment.