diff --git a/context.go b/context.go new file mode 100644 index 0000000..5fa6d08 --- /dev/null +++ b/context.go @@ -0,0 +1,50 @@ +package linger + +import ( + "context" + "time" +) + +// FromContextDeadline returns the duration until the deadline of ctx is +// reached. +// +// ok is false if ctx does not have a deadline. +func FromContextDeadline(ctx context.Context) (d time.Duration, ok bool) { + if dl, ok := ctx.Deadline(); ok { + return time.Until(dl), true + } + + return 0, false +} + +// ContextWithTimeout returns a context with a deadline some duration after the +// current time. +// +// The timeout duration is computed by finding the first of the supplied +// durations that is positive. It uses a zero duration if none of the supplied +// durations are positive. +func ContextWithTimeout( + ctx context.Context, + durations ...time.Duration, +) (context.Context, func()) { + return ContextWithTimeoutX(ctx, Identity, durations...) +} + +// ContextWithTimeoutX returns a context with a deadline some duration after the +// current time. +// +// The timeout duration is computed by finding the first of the supplied +// durations that is positive, then applying the transform x. It uses a zero +// duration if none of the supplied durations are positive. +// +// The transform can be used to apply jitter to the timeout duration, for +// example, by using one of the built-in jitter transforms such as FullJitter() +// or ProportionalJitter(). +func ContextWithTimeoutX( + ctx context.Context, + x DurationTransform, + durations ...time.Duration, +) (context.Context, func()) { + d, _ := Coalesce(durations...) + return context.WithTimeout(ctx, x(d)) +} diff --git a/context_test.go b/context_test.go new file mode 100644 index 0000000..24ab499 --- /dev/null +++ b/context_test.go @@ -0,0 +1,62 @@ +package linger_test + +import ( + "context" + "time" + + . "github.com/dogmatiq/linger" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("func FromContextDeadline()", func() { + It("returns the time until the deadline of the context", func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + d, ok := FromContextDeadline(ctx) + Expect(ok).To(BeTrue()) + Expect(d).To(BeNumerically("~", 10*time.Second, 1*time.Second)) + }) + + It("returns false if the context does not have a deadline", func() { + _, ok := FromContextDeadline(context.Background()) + Expect(ok).To(BeFalse()) + }) +}) + +var _ = Describe("func ContextWithTimeout()", func() { + It("sets a timeout for the first positive duration", func() { + expect := time.Now().Add(10 * time.Second) + ctx, cancel := ContextWithTimeout(context.Background(), 0*time.Second, -1*time.Second, 10*time.Second) + defer cancel() + + dl, ok := ctx.Deadline() + Expect(ok).To(BeTrue()) + Expect(dl).To(BeTemporally("~", expect)) + }) + + It("times out 'immediately' if none of the durations are positive", func() { + ctx, cancel := ContextWithTimeout(context.Background(), 0*time.Second, -1*time.Second) + defer cancel() + + err := ctx.Err() + Expect(err).To(Equal(context.DeadlineExceeded)) + }) +}) + +var _ = Describe("func ContextWithTimeoutX()", func() { + It("applies the transform", func() { + x := func(t time.Duration) time.Duration { + return t / 2 + } + + expect := time.Now().Add(10 * time.Second) + ctx, cancel := ContextWithTimeoutX(context.Background(), x, 20*time.Second) + defer cancel() + + dl, ok := ctx.Deadline() + Expect(ok).To(BeTrue()) + Expect(dl).To(BeTemporally("~", expect)) + }) +}) diff --git a/convert.go b/seconds.go similarity index 100% rename from convert.go rename to seconds.go diff --git a/convert_test.go b/seconds_test.go similarity index 100% rename from convert_test.go rename to seconds_test.go