Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}