Skip to content

Commit

Permalink
Merge pull request #30 from treid314/add-util-time
Browse files Browse the repository at this point in the history
Add util/time package from cortex
  • Loading branch information
pstibrany authored Sep 7, 2021
2 parents e2165e2 + 4a74065 commit 29a5cc8
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 0 deletions.
86 changes: 86 additions & 0 deletions time/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package time

import (
"math"
"math/rand"
"net/http"
"strconv"
"time"

"github.com/prometheus/common/model"
"github.com/weaveworks/common/httpgrpc"
)

const (
nanosecondsInMillisecond = int64(time.Millisecond / time.Nanosecond)
)

func ToMillis(t time.Time) int64 {
return t.UnixNano() / nanosecondsInMillisecond
}

// FromMillis is a helper to turn milliseconds -> time.Time
func FromMillis(ms int64) time.Time {
return time.Unix(0, ms*nanosecondsInMillisecond)
}

// FormatTimeMillis returns a human readable version of the input time (in milliseconds).
func FormatTimeMillis(ms int64) string {
return FromMillis(ms).String()
}

// FormatTimeModel returns a human readable version of the input time.
func FormatTimeModel(t model.Time) string {
return FromMillis(int64(t)).String()
}

// ParseTime parses the string into an int64, milliseconds since epoch.
func ParseTime(s string) (int64, error) {
if t, err := strconv.ParseFloat(s, 64); err == nil {
s, ns := math.Modf(t)
ns = math.Round(ns*1000) / 1000
tm := time.Unix(int64(s), int64(ns*float64(time.Second)))
return ToMillis(tm), nil
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return ToMillis(t), nil
}
return 0, httpgrpc.Errorf(http.StatusBadRequest, "cannot parse %q to a valid timestamp", s)
}

// DurationWithJitter returns random duration from "input - input*variance" to "input + input*variance" interval.
func DurationWithJitter(input time.Duration, variancePerc float64) time.Duration {
// No duration? No jitter.
if input == 0 {
return 0
}

variance := int64(float64(input) * variancePerc)
jitter := rand.Int63n(variance*2) - variance

return input + time.Duration(jitter)
}

// DurationWithPositiveJitter returns random duration from "input" to "input + input*variance" interval.
func DurationWithPositiveJitter(input time.Duration, variancePerc float64) time.Duration {
// No duration? No jitter.
if input == 0 {
return 0
}

variance := int64(float64(input) * variancePerc)
jitter := rand.Int63n(variance)

return input + time.Duration(jitter)
}

// NewDisableableTicker essentially wraps NewTicker but allows the ticker to be disabled by passing
// zero duration as the interval. Returns a function for stopping the ticker, and the ticker channel.
func NewDisableableTicker(interval time.Duration) (func(), <-chan time.Time) {
if interval == 0 {
return func() {}, nil
}

tick := time.NewTicker(interval)
return func() { tick.Stop() }, tick.C
}
133 changes: 133 additions & 0 deletions time/time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package time

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTimeFromMillis(t *testing.T) {
var testExpr = []struct {
input int64
expected time.Time
}{
{input: 1000, expected: time.Unix(1, 0)},
{input: 1500, expected: time.Unix(1, 500*nanosecondsInMillisecond)},
}

for i, c := range testExpr {
t.Run(fmt.Sprint(i), func(t *testing.T) {
res := FromMillis(c.input)
require.Equal(t, c.expected, res)
})
}
}

func TestDurationWithJitter(t *testing.T) {
const numRuns = 1000

for i := 0; i < numRuns; i++ {
actual := DurationWithJitter(time.Minute, 0.5)
assert.GreaterOrEqual(t, int64(actual), int64(30*time.Second))
assert.LessOrEqual(t, int64(actual), int64(90*time.Second))
}
}

func TestDurationWithJitter_ZeroInputDuration(t *testing.T) {
assert.Equal(t, time.Duration(0), DurationWithJitter(time.Duration(0), 0.5))
}

func TestDurationWithPositiveJitter(t *testing.T) {
const numRuns = 1000

for i := 0; i < numRuns; i++ {
actual := DurationWithPositiveJitter(time.Minute, 0.5)
assert.GreaterOrEqual(t, int64(actual), int64(60*time.Second))
assert.LessOrEqual(t, int64(actual), int64(90*time.Second))
}
}

func TestDurationWithPositiveJitter_ZeroInputDuration(t *testing.T) {
assert.Equal(t, time.Duration(0), DurationWithPositiveJitter(time.Duration(0), 0.5))
}

func TestParseTime(t *testing.T) {
var tests = []struct {
input string
fail bool
result time.Time
}{
{
input: "",
fail: true,
}, {
input: "abc",
fail: true,
}, {
input: "30s",
fail: true,
}, {
input: "123",
result: time.Unix(123, 0),
}, {
input: "123.123",
result: time.Unix(123, 123000000),
}, {
input: "2015-06-03T13:21:58.555Z",
result: time.Unix(1433337718, 555*time.Millisecond.Nanoseconds()),
}, {
input: "2015-06-03T14:21:58.555+01:00",
result: time.Unix(1433337718, 555*time.Millisecond.Nanoseconds()),
}, {
// Test nanosecond rounding.
input: "2015-06-03T13:21:58.56789Z",
result: time.Unix(1433337718, 567*1e6),
}, {
// Test float rounding.
input: "1543578564.705",
result: time.Unix(1543578564, 705*1e6),
},
}

for _, test := range tests {
ts, err := ParseTime(test.input)
if test.fail {
require.Error(t, err)
continue
}

require.NoError(t, err)
assert.Equal(t, ToMillis(test.result), ts)
}
}

func TestNewDisableableTicker_Enabled(t *testing.T) {
stop, ch := NewDisableableTicker(10 * time.Millisecond)
defer stop()

time.Sleep(100 * time.Millisecond)

select {
case <-ch:
break
default:
t.Error("ticker should have ticked when enabled")
}
}

func TestNewDisableableTicker_Disabled(t *testing.T) {
stop, ch := NewDisableableTicker(0)
defer stop()

time.Sleep(100 * time.Millisecond)

select {
case <-ch:
t.Error("ticker should not have ticked when disabled")
default:
break
}
}

0 comments on commit 29a5cc8

Please sign in to comment.