-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30 from treid314/add-util-time
Add util/time package from cortex
- Loading branch information
Showing
2 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |