/
time.go
163 lines (138 loc) · 4.77 KB
/
time.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// SPDX-License-Identifier: AGPL-3.0-only
// Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/util/time.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Cortex Authors.
package util
import (
"math"
"math/rand"
"net/http"
"sort"
"strconv"
"time"
"github.com/grafana/dskit/httpgrpc"
"github.com/prometheus/common/model"
)
func TimeToMillis(t time.Time) int64 {
return t.Unix()*1000 + int64(t.Nanosecond())/int64(time.Millisecond)
}
// TimeFromMillis is a helper to turn milliseconds -> time.Time
func TimeFromMillis(ms int64) time.Time {
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC()
}
// FormatTimeMillis returns a human readable version of the input time (in milliseconds).
func FormatTimeMillis(ms int64) string {
return TimeFromMillis(ms).String()
}
// FormatTimeModel returns a human readable version of the input time.
func FormatTimeModel(t model.Time) string {
return TimeFromMillis(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 TimeToMillis(tm), nil
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return TimeToMillis(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)
}
// DurationWithNegativeJitter returns random duration from "input - input*variance" to "input" interval.
func DurationWithNegativeJitter(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
}
type TimeRange struct {
Start time.Time
End time.Time
Resolution time.Duration
}
// SplitTimeRangeByResolution splits the given time range into the
// minimal number of non-overlapping sub-ranges aligned with resolutions.
// All ranges have inclusive start and end; one millisecond step.
func SplitTimeRangeByResolution(start, end time.Time, resolutions []time.Duration, fn func(TimeRange)) {
if len(resolutions) == 0 {
fn(TimeRange{Start: start, End: end})
return
}
// Sorting resolutions in ascending order.
sort.Slice(resolutions, func(i, j int) bool {
return resolutions[i] > resolutions[j]
})
// Time ranges are inclusive on both ends. In order to simplify calculation
// of resolution alignment, we add a millisecond to the end time.
// Added millisecond is subtracted from the final ranges.
end = end.Add(time.Millisecond)
var (
c = start // Current range start position.
r time.Duration // Current resolution.
)
for c.Before(end) {
var d time.Duration = -1
// Find the lowest resolution aligned with the current position.
for _, res := range resolutions {
if c.UnixNano()%res.Nanoseconds() == 0 && !c.Add(res).After(end) {
d = res
break
}
}
res := d
if d < 0 {
// No suitable resolution found: add distance
// to the next closest aligned boundary.
l := resolutions[len(resolutions)-1]
d = l - time.Duration(c.UnixNano()%l.Nanoseconds())
}
if end.Before(c.Add(d)) {
d = end.Sub(c)
}
// If the resolution has changed, emit a new range.
if r != res && c.After(start) {
fn(TimeRange{Start: start, End: c.Add(-time.Millisecond), Resolution: r})
start = c
}
c = c.Add(d)
r = res
}
if start != c {
fn(TimeRange{Start: start, End: c.Add(-time.Millisecond), Resolution: r})
}
}