package mqe import ( "fmt" "time" ) func SplitByPeriods(producer periodProducer, calc PeriodCalculator) (result []AggregatedMeasure, err error) { var ( isOpened bool openedPeriod time.Time since int64 sinceValue float64 until int64 untilValue float64 // period time.Time first, last PeriodBound found bool ) for { period, first, last, found, err = producer.Next() if err != nil { return } if !found { break } if isOpened { if calc.IsExtendedSincePeriod(period) { err = fmt.Errorf("extended period while already opened") return } else if calc.IsExtendedUntilPeriod(period) { result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: first.Time, Values: []float64{ first.Value, first.Value - sinceValue, }, }) // Последний период успешно закрыли данными из Extended Until, поэтому выходим return } else { if period.Equal(openedPeriod) { // + // Данная ситуация будет когда предыдущий период был корректно // закрыт за 3 минуты до конца. После этого сразу открылся // новый период. А запись для нового периода получена только // сейчас. if calc.IsPeriodCorrectEnds(period, last.Time) { // CLOSE result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: last.Time, Values: []float64{ last.Value, last.Value - sinceValue, }, }) // OPEN PERIOD (extended since, last 3 min closed) openedPeriod = calc.NextPeriod(period) if calc.IsExtendedUntilPeriod(openedPeriod) { return } since = last.Time sinceValue = last.Value until = 0 untilValue = 0 } else { // Период закроем где-то в будущем, на всякий случай запомнили последнее // показание внутри периода until = last.Time untilValue = last.Value } } else { // + // Периоды не совпадают - смело закрываем // Возможные ситуации: // - был открыт мартовский период, а текущий период - апрель // - result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: first.Time, Values: []float64{ first.Value, first.Value - sinceValue, }, }) if first.Time != last.Time { // + openedPeriod = period since = first.Time sinceValue = first.Value until = 0 untilValue = 0 if calc.IsPeriodCorrectEnds(period, last.Time) { // Закрываем только что открытый период result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: last.Time, Values: []float64{ last.Value, last.Value - sinceValue, }, }) openedPeriod = calc.NextPeriod(period) if calc.IsExtendedUntilPeriod(openedPeriod) { return } since = last.Time sinceValue = last.Value until = 0 untilValue = 0 } else { // Период закроем где-то в будущем, на всякий случай запомнили последнее // показание внутри периода until = last.Time untilValue = last.Value } } else { // + // first == last if calc.IsPeriodCorrectEnds(period, last.Time) { // Допустим закрыли предыдущий период 30 апреля в 23:58, новый стартуем за май. openedPeriod = calc.NextPeriod(period) if calc.IsExtendedUntilPeriod(openedPeriod) { return } } else { // Допустим закрыли предыдущий период 15 апреля, новый стартуем за апрель. openedPeriod = period } since = last.Time sinceValue = last.Value until = 0 untilValue = 0 } } } } else { if calc.IsExtendedSincePeriod(period) { // Если последние 3 минуты if calc.IsPeriodCorrectEnds(period, last.Time) { // OPEN PERIOD (extended since, last 3 min closed) isOpened = true openedPeriod = calc.NextPeriod(period) since = last.Time sinceValue = last.Value until = 0 untilValue = 0 } } else if calc.IsExtendedUntilPeriod(period) { // Ничего не нашли, пустой результат return } else { // OPEN PERIOD (regular first) isOpened = true openedPeriod = period since = first.Time sinceValue = first.Value until = last.Time untilValue = last.Value if calc.IsPeriodCorrectEnds(openedPeriod, last.Time) { // Закрываем только что открытый период result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: last.Time, Values: []float64{ last.Value, last.Value - sinceValue, }, }) openedPeriod = calc.NextPeriod(period) if calc.IsExtendedUntilPeriod(openedPeriod) { return } since = last.Time sinceValue = last.Value until = 0 untilValue = 0 } } } } // Доп запрос. if openedPeriod.IsZero() { // Открытого периода не будет если вообще не нашли записей return } next, found, err := producer.Last() if err != nil { return } if found { result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: next.Time, Values: []float64{ next.Value, next.Value - sinceValue, }, }) } else { if until > 0 { result = append(result, AggregatedMeasure{ Period: openedPeriod.Unix(), Since: since, Until: until, Values: []float64{ untilValue, untilValue - sinceValue, }, }) } } return } // Period - это начало периода, например для дней - это 0ч 0м 0с // lastTm предыдущего периода всегда будет меньше current period // since - это всегда сдвинутая метка времени (+ FirstHourOfDay * 3600) // При FirstHourOfDay = 3 и группировке по дням, будет такая ситуация. // Period будет указывать на 1 марта 00:00:00, а since на 1 марта 02:00:00, // а lastTm предыдущего периода на 1 марта 01:59:00. // Поэтому нельзя сравнивать текущий period и lastTm предыдущего периода. // Преобразовать lastTm к period непонятно как если нужно учитывать LastDayOfMonth. // Пример: 28 марта 00ч стало 1 апреля 00ч, а 27 марта 23:59 так и осталось. // Можно вычислить endPeriodTime для previousPeriodEndTime, но текущий период может // быть не строго следующим после предыдущего (например, июнь после марта). // Правильное решение - берем firstTm текущего периода и вычисляем его реальное // календарное начало. Например, FirstHourOfDay = 3, LastDayOfMonth = 28, // firstTm = 1 мая. Календарное начало - 28 апреля 03:00:00. И уже с этим значением // сравниваем previousPeriodEndTime. // Но можно сравнивать since c lastTm и until c firstTm для определения // extended периодов. /* func periodToCalendarTime(period time.Time, groupBy string, lastDayOfMonth, firstHourOfDay int) time.Time { switch groupBy { case "d": if firstHourOfDay == 0 { return period } // Было 0ч, стало - 2ч return period.Add(time.Duration(firstHourOfDay) * time.Hour) case "m": if lastDayOfMonth == 0 { if firstHourOfDay == 0 { return period } // Было 0ч, стало - 2ч return period.Add(time.Duration(firstHourOfDay) * time.Hour) } else { // текущий период начинается в прошлом месяце tm := period.AddDate(0, -1, 0) return time.Date( tm.Year(), tm.Month(), lastDayOfMonth+1, firstHourOfDay, 0, 0, 0, tm.Location(), ) } default: return period } } */ //var threeMinutes int64 = 3 * 60 /* type readOptions struct { MetricID int64 // только для readAggregatedMeasuresCanEndsInFuture LastDayOfMonth int FirstHourOfDay int GroupBy string Since int64 Until int64 ParsePeriodLayout string } */ // Читает из sql.Rows показания и строит периоды /* func (s *MeasureQueryEngine) readAggregatedMeasuresCanEndsInFuture(tx *sql.Tx, rows *sql.Rows, opt readOptions) (result []CalculatedAggregatedMeasure, err error) { var ( openPeriod time.Time since int64 sinceValue float64 until int64 untilValue float64 // frameStr string firstTm int64 firstValue float64 lastTm int64 lastValue float64 period time.Time ) for rows.Next() { err = rows.Scan(&frameStr, &firstTm, &firstValue, &lastTm, &lastValue) if err != nil { return } period, err = time.ParseInLocation(opt.ParsePeriodLayout, frameStr, s.location) if err != nil { err = fmt.Errorf("time.ParseInLocation: %s; layout=%q; str=%q", err, opt.ParsePeriodLayout, frameStr) return } //fmt.Printf("%s s: %s, %v; u: %s, %v\n", frameStr, time.Unix(firstTm, 0), firstValue, time.Unix(lastTm, 0), lastValue) // Читаем записи из БД. Одна запись - один период. Возможны 3 вида периодов: // 1. extended since - период длиной до 3 минут перед since. Оптимизация, чтобы // не отправлять дополнительный запрос. // 2. обычный период. // 3. extended until - период длиной до 1 часа после until. Оптимизация, чтобы // не отправлять дополнительный запрос (в большинстве случаев). // // ВАЖНО! // Любой период (1, 2 или 3) может встретится первым. Определить что это // первая запись модно простой проверкой openPeriod.IsZero() if lastTm < opt.Since { // 1. extended since period if isPeriodCorrectEnds(period, opt.GroupBy, opt.LastDayOfMonth, opt.FirstHourOfDay, lastTm) { openPeriod = nextPeriod(period, opt.GroupBy) since = lastTm sinceValue = lastValue } continue } else if firstTm > opt.Until { // 3. extended until period if openPeriod.IsZero() { // Нет показаний кроме extended until, поэтому выходим return } // закрываем открытый период result = append(result, CalculatedAggregatedMeasure{ Period: openPeriod.Unix(), Since: since, Until: firstTm, Value: firstValue, Total: firstValue - sinceValue, }) return } // 2. обычный период if openPeriod.IsZero() { // это первая запись (extended since period не найден) openPeriod = period since = firstTm sinceValue = firstValue } else { if !period.Equal(openPeriod) { //fmt.Printf("\nperiod %s != openPeriod %s\n", period, openPeriod) // Пример: открытый период - март. В period - апрель. // закрываем открытый период result = append(result, CalculatedAggregatedMeasure{ Period: openPeriod.Unix(), Since: since, Until: firstTm, Value: firstValue, Total: firstValue - sinceValue, }) // И сразу открываем новый период openPeriod = period since = firstTm sinceValue = firstValue } // Периоды совпадают. Когда такая ситуация возможна? // Например, на предыдущей итерации был корректно закрыт (57-59 минуты) // прошлый период и сразу открыт новый (текущий). } if isPeriodCorrectEnds(openPeriod, opt.GroupBy, opt.LastDayOfMonth, opt.FirstHourOfDay, lastTm) { // fmt.Printf("\nCORRECT END period %s\n", openPeriod) // закрываем открытый период result = append(result, CalculatedAggregatedMeasure{ Period: openPeriod.Unix(), Since: since, Until: lastTm, Value: lastValue, Total: lastValue - sinceValue, }) // Открываем новый период openPeriod = nextPeriod(openPeriod, opt.GroupBy) since = lastTm sinceValue = lastValue } else { // На последних минутах периода (57-59) не было показания, поэтому // запоминаем конец периода какой есть. Если конец периода в будущем // не будет найден - закроем период по текущему последнему показанию. until = lastTm untilValue = lastValue } // Валидируем новый период. // Зачем? // Пример: юзер задал opt.Until - 31 марта 23:59:59. Только что корректно // закрылся март показанием от 31 марта 23:57:00. И сразу открылся новый // период за апрель. // Понять что закрыли последний период из запрошенных юзером просто - // вычисляем календарное начало нового периода и сравниваем с opt.Until. mustLessThanUntil := periodToCalendarTime(openPeriod, opt.GroupBy, opt.LastDayOfMonth, opt.FirstHourOfDay) if mustLessThanUntil.Unix() > opt.Until { // Новый период вышел за opt.Until, а это означает что мы корректно // закрыли последний период из запрошенных юзером. Bыходим. return } } // Цикл завершен, а из метода мы не вышли, а это означает одно - есть незакрытый // период. Extended until не найден. Значит нужно отправить дополнительный запрос // к БД. var ( isNextValueFound bool nextTm int64 nextValue float64 ) // Находим следующее показание метрики. err = tx.QueryRow(` SELECT tm, value, true FROM f64 WHERE metricID=? AND tm > ? ORDER BY tm ASC LIMIT 1`, opt.MetricID, opt.Until).Scan(&nextTm, &nextValue, &isNextValueFound) if err != nil { if err != sql.ErrNoRows { return } // Нет записей err = nil } if isNextValueFound { // Найдено какое-то показание в будущем. result = append(result, CalculatedAggregatedMeasure{ Period: openPeriod.Unix(), Since: since, Until: nextTm, Value: nextValue, Total: nextValue - sinceValue, }) } else { // Закрываем период самым последним показанием внутри периода // (НЕ 57-59 минуты). result = append(result, CalculatedAggregatedMeasure{ Period: openPeriod.Unix(), Since: since, Until: until, Value: untilValue, Total: untilValue - sinceValue, }) } return } */ /* // Читает из sql.Rows показания и строит периоды func (s *MeasureQueryEngine) readAggregatedMeasuresEndsInsidePeriod(tx *sql.Tx, rows *sql.Rows, opt readOptions) (result []CalculatedAggregatedMeasure, err error) { var ( previousPeriodEndTime int64 previousPeriodEndValue float64 // frameStr string firstTm int64 firstValue float64 lastTm int64 lastValue float64 period time.Time ) for rows.Next() { err = rows.Scan(&frameStr, &firstTm, &firstValue, &lastTm, &lastValue) if err != nil { return } period, err = time.ParseInLocation(opt.ParsePeriodLayout, frameStr, s.location) if err != nil { err = fmt.Errorf("time.ParseInLocation: %s; layout=%q; str=%q", err, opt.ParsePeriodLayout, frameStr) return } //fmt.Printf("%s s: %s, %v; u: %s, %v\n", frameStr, time.Unix(firstTm, 0), firstValue, time.Unix(lastTm, 0), lastValue) if lastTm < opt.Since { // extended since period previousPeriodEndTime = lastTm previousPeriodEndValue = lastValue continue } else if firstTm > opt.Until { // extended until period return } // целевой период. В будущем конец периода не ищем, поэтому закрываем сразу. // Вычисляем реальное календарное начало периода. Например, period - 1 мая 00:00:00, // FirstHourOfDay = 3, LastDayOfMonth = 28. Календарное начало - 29 апреля 03:00:00. periodCalendarTime := periodToCalendarTime(period, opt.GroupBy, opt.LastDayOfMonth, opt.FirstHourOfDay) minAge := periodCalendarTime.Add(-3 * time.Minute).Unix() if previousPeriodEndTime > 0 && previousPeriodEndTime >= minAge { result = append(result, CalculatedAggregatedMeasure{ Period: period.Unix(), Since: previousPeriodEndTime, Until: lastTm, Value: lastValue, Total: lastValue - previousPeriodEndValue, }) } else { result = append(result, CalculatedAggregatedMeasure{ Period: period.Unix(), Since: firstTm, Until: lastTm, Value: lastValue, Total: lastValue - firstValue, }) } previousPeriodEndTime = lastTm previousPeriodEndValue = lastValue } return } */