This commit is contained in:
Christian Schwarz
2018-07-15 17:36:53 +02:00
parent 1a8d2c5ebe
commit 1826535e6f
6 changed files with 406 additions and 104 deletions
+83
View File
@@ -0,0 +1,83 @@
package util
import (
"context"
"time"
"sync"
)
type contextWithOptionalDeadline struct {
context.Context
m sync.Mutex
deadline time.Time
done chan struct{}
err error
}
func (c *contextWithOptionalDeadline) Deadline() (deadline time.Time, ok bool) {
c.m.Lock()
defer c.m.Unlock()
return c.deadline, !c.deadline.IsZero()
}
func (c *contextWithOptionalDeadline) Err() error {
c.m.Lock()
defer c.m.Unlock()
return c.err
}
func (c *contextWithOptionalDeadline) Done() (<-chan struct{}) {
return c.done
}
func ContextWithOptionalDeadline(pctx context.Context) (ctx context.Context, enforceDeadline func(deadline time.Time)) {
// mctx can only be cancelled by cancelMctx, not by a potential cancel of pctx
rctx := &contextWithOptionalDeadline{
Context: pctx,
done: make(chan struct{}),
err: nil,
}
enforceDeadline = func(deadline time.Time) {
// Set deadline and prohibit multiple calls
rctx.m.Lock()
alreadyCalled := !rctx.deadline.IsZero()
if !alreadyCalled {
rctx.deadline = deadline
}
rctx.m.Unlock()
if alreadyCalled {
return
}
// Deadline in past?
sleepTime := deadline.Sub(time.Now())
if sleepTime <= 0 {
rctx.m.Lock()
rctx.err = context.DeadlineExceeded
rctx.m.Unlock()
close(rctx.done)
return
}
go func() {
// Set a timer and wait for timer or parent context to be cancelled
timer := time.NewTimer(sleepTime)
var setErr error
select {
case <-pctx.Done():
timer.Stop()
setErr = pctx.Err()
case <-timer.C:
setErr = context.DeadlineExceeded
}
rctx.m.Lock()
rctx.err = setErr
rctx.m.Unlock()
close(rctx.done)
}()
}
return rctx, enforceDeadline
}
+84
View File
@@ -0,0 +1,84 @@
package util
import (
"testing"
"context"
"time"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
func TestContextWithOptionalDeadline(t *testing.T) {
ctx := context.Background()
cctx, enforceDeadline := ContextWithOptionalDeadline(ctx)
begin := time.Now()
var receivedCancellation time.Time
var cancellationError error
go func() {
select {
case <- cctx.Done():
receivedCancellation = time.Now()
cancellationError = cctx.Err()
case <- time.After(600*time.Millisecond):
t.Fatalf("should have been cancelled by deadline")
}
}()
time.Sleep(100*time.Millisecond)
if !receivedCancellation.IsZero() {
t.Fatalf("no enforcement means no cancellation")
}
require.Nil(t, cctx.Err(), "no error while not cancelled")
dl, ok := cctx.Deadline()
require.False(t, ok)
require.Zero(t, dl)
enforceDeadline(begin.Add(200*time.Millisecond))
// second call must be ignored, i.e. we expect the deadline to be at begin+200ms, not begin+400ms
enforceDeadline(begin.Add(400*time.Millisecond))
time.Sleep(300*time.Millisecond) // 100ms margin for scheduler
if receivedCancellation.Sub(begin) > 250*time.Millisecond {
t.Fatalf("cancellation is beyond acceptable scheduler latency")
}
require.Equal(t, context.DeadlineExceeded, cancellationError)
}
func TestContextWithOptionalDeadlineNegativeDeadline(t *testing.T) {
ctx := context.Background()
cctx, enforceDeadline := ContextWithOptionalDeadline(ctx)
enforceDeadline(time.Now().Add(-10*time.Second))
select {
case <-cctx.Done():
default:
t.FailNow()
}
}
func TestContextWithOptionalDeadlineParentCancellation(t *testing.T) {
pctx, cancel := context.WithCancel(context.Background())
cctx, enforceDeadline := ContextWithOptionalDeadline(pctx)
// 0 ms
start := time.Now()
enforceDeadline(start.Add(400*time.Millisecond))
time.Sleep(100*time.Millisecond)
cancel() // cancel @ ~100ms
time.Sleep(100*time.Millisecond) // give 100ms time to propagate cancel
// @ ~200ms
select {
case <-cctx.Done():
assert.True(t, time.Now().Before(start.Add(300*time.Millisecond)))
assert.Equal(t, context.Canceled, cctx.Err())
default:
t.FailNow()
}
}
func TestContextWithOptionalDeadlineValue(t *testing.T) {
pctx := context.WithValue(context.Background(), "key", "value")
cctx, _ := ContextWithOptionalDeadline(pctx)
assert.Equal(t, "value", cctx.Value("key"))
}