timeutil.go 6.5 KB

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