timeutil.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package timeutil
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "sync"
  7. "time"
  8. )
  9. const (
  10. // SecsPerMin expresses the amount of seconds in a minute
  11. SecsPerMin = 60.0
  12. // SecsPerHour expresses the amount of seconds in a minute
  13. SecsPerHour = 3600.0
  14. // SecsPerDay expressed the amount of seconds in a day
  15. SecsPerDay = 86400.0
  16. // MinsPerHour expresses the amount of minutes in an hour
  17. MinsPerHour = 60.0
  18. // MinsPerDay expresses the amount of minutes in a day
  19. MinsPerDay = 1440.0
  20. // HoursPerDay expresses the amount of hours in a day
  21. HoursPerDay = 24.0
  22. // HoursPerMonth expresses the amount of hours in a month
  23. HoursPerMonth = 730.0
  24. // DaysPerMonth expresses the amount of days in a month
  25. DaysPerMonth = 30.42
  26. )
  27. // DurationString converts a duration to a Prometheus-compatible string in
  28. // terms of days, hours, minutes, or seconds.
  29. func DurationString(duration time.Duration) string {
  30. durSecs := int64(duration.Seconds())
  31. durStr := ""
  32. if durSecs > 0 {
  33. if durSecs%SecsPerDay == 0 {
  34. // convert to days
  35. durStr = fmt.Sprintf("%dd", durSecs/SecsPerDay)
  36. } else if durSecs%SecsPerHour == 0 {
  37. // convert to hours
  38. durStr = fmt.Sprintf("%dh", durSecs/SecsPerHour)
  39. } else if durSecs%SecsPerMin == 0 {
  40. // convert to mins
  41. durStr = fmt.Sprintf("%dm", durSecs/SecsPerMin)
  42. } else if durSecs > 0 {
  43. // default to mins, as long as duration is positive
  44. durStr = fmt.Sprintf("%ds", durSecs)
  45. }
  46. }
  47. return durStr
  48. }
  49. // DurationOffsetStrings converts a (duration, offset) pair to Prometheus-
  50. // compatible strings in terms of days, hours, minutes, or seconds.
  51. func DurationOffsetStrings(duration, offset time.Duration) (string, string) {
  52. return DurationString(duration), DurationString(offset)
  53. }
  54. // FormatStoreResolution provides a clean notation for ETL store resolutions.
  55. // e.g. daily => 1d; hourly => 1h
  56. func FormatStoreResolution(dur time.Duration) string {
  57. if dur >= 24*time.Hour {
  58. return fmt.Sprintf("%dd", int(dur.Hours()/24.0))
  59. } else if dur >= time.Hour {
  60. return fmt.Sprintf("%dh", int(dur.Hours()))
  61. }
  62. return fmt.Sprint(dur)
  63. }
  64. // JobTicker is a ticker used to synchronize the next run of a repeating
  65. // process. The designated use-case is for infinitely-looping selects,
  66. // where a timeout or an exit channel might cancel the process, but otherwise
  67. // the intent is to wait at the select for some amount of time until the
  68. // next run. This differs from a standard ticker, which ticks without
  69. // waiting and drops any missed ticks; rather, this ticker must be kicked
  70. // off manually for each tick, so that after the current run of the job
  71. // completes, the timer starts again.
  72. type JobTicker struct {
  73. Ch <-chan time.Time
  74. ch chan time.Time
  75. closed bool
  76. mx sync.Mutex
  77. }
  78. // NewJobTicker instantiates a new JobTicker.
  79. func NewJobTicker() *JobTicker {
  80. c := make(chan time.Time)
  81. return &JobTicker{
  82. Ch: c,
  83. ch: c,
  84. closed: false,
  85. }
  86. }
  87. // Close closes the JobTicker channels
  88. func (jt *JobTicker) Close() {
  89. jt.mx.Lock()
  90. defer jt.mx.Unlock()
  91. if jt.closed {
  92. return
  93. }
  94. jt.closed = true
  95. close(jt.ch)
  96. }
  97. // TickAt schedules the next tick of the ticker for the given time in the
  98. // future. If the time is not in the future, the ticker will tick immediately.
  99. func (jt *JobTicker) TickAt(t time.Time) {
  100. go func(t time.Time) {
  101. n := time.Now()
  102. if t.After(n) {
  103. time.Sleep(t.Sub(n))
  104. }
  105. jt.mx.Lock()
  106. defer jt.mx.Unlock()
  107. if !jt.closed {
  108. jt.ch <- time.Now()
  109. }
  110. }(t)
  111. }
  112. // TickIn schedules the next tick of the ticker for the given duration into
  113. // the future. If the duration is less than or equal to zero, the ticker will
  114. // tick immediately.
  115. func (jt *JobTicker) TickIn(d time.Duration) {
  116. go func(d time.Duration) {
  117. if d > 0 {
  118. time.Sleep(d)
  119. }
  120. jt.mx.Lock()
  121. defer jt.mx.Unlock()
  122. if !jt.closed {
  123. jt.ch <- time.Now()
  124. }
  125. }(d)
  126. }
  127. // ParseDuration converts a Prometheus-style duration string into a Duration
  128. func ParseDuration(duration string) (*time.Duration, error) {
  129. // Trim prefix of Prometheus format duration
  130. duration = CleanDurationString(duration)
  131. if len(duration) < 2 {
  132. return nil, fmt.Errorf("error parsing duration: %s did not match expected format [0-9+](s|m|d|h)", duration)
  133. }
  134. unitStr := duration[len(duration)-1:]
  135. var unit time.Duration
  136. switch unitStr {
  137. case "s":
  138. unit = time.Second
  139. case "m":
  140. unit = time.Minute
  141. case "h":
  142. unit = time.Hour
  143. case "d":
  144. unit = 24.0 * time.Hour
  145. default:
  146. return nil, fmt.Errorf("error parsing duration: %s did not match expected format [0-9+](s|m|d|h)", duration)
  147. }
  148. amountStr := duration[:len(duration)-1]
  149. amount, err := strconv.ParseInt(amountStr, 10, 64)
  150. if err != nil {
  151. return nil, fmt.Errorf("error parsing duration: %s did not match expected format [0-9+](s|m|d|h)", duration)
  152. }
  153. dur := time.Duration(amount) * unit
  154. return &dur, nil
  155. }
  156. // CleanDurationString removes prometheus formatted prefix "offset " allong with leading a trailing whitespace
  157. // from duration string, leaving behind a string with format [0-9+](s|m|d|h)
  158. func CleanDurationString(duration string) string {
  159. duration = strings.TrimSpace(duration)
  160. duration = strings.TrimPrefix(duration, "offset ")
  161. return duration
  162. }
  163. // ParseTimeRange returns a start and end time, respectively, which are converted from
  164. // a duration and offset, defined as strings with Prometheus-style syntax.
  165. func ParseTimeRange(duration, offset string) (*time.Time, *time.Time, error) {
  166. // endTime defaults to the current time, unless an offset is explicity declared,
  167. // in which case it shifts endTime back by given duration
  168. endTime := time.Now()
  169. if offset != "" {
  170. o, err := ParseDuration(offset)
  171. if err != nil {
  172. return nil, nil, fmt.Errorf("error parsing offset (%s): %s", offset, err)
  173. }
  174. endTime = endTime.Add(-1 * *o)
  175. }
  176. // if duration is defined in terms of days, convert to hours
  177. // e.g. convert "2d" to "48h"
  178. durationNorm, err := normalizeTimeParam(duration)
  179. if err != nil {
  180. return nil, nil, fmt.Errorf("error parsing duration (%s): %s", duration, err)
  181. }
  182. // convert time duration into start and end times, formatted
  183. // as ISO datetime strings
  184. dur, err := time.ParseDuration(durationNorm)
  185. if err != nil {
  186. return nil, nil, fmt.Errorf("errorf parsing duration (%s): %s", durationNorm, err)
  187. }
  188. startTime := endTime.Add(-1 * dur)
  189. return &startTime, &endTime, nil
  190. }
  191. func normalizeTimeParam(param string) (string, error) {
  192. // convert days to hours
  193. if param[len(param)-1:] == "d" {
  194. count := param[:len(param)-1]
  195. val, err := strconv.ParseInt(count, 10, 64)
  196. if err != nil {
  197. return "", err
  198. }
  199. val = val * 24
  200. param = fmt.Sprintf("%dh", val)
  201. }
  202. return param, nil
  203. }