timeutil.go 6.2 KB

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