time.go 5.9 KB

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