package mqe import ( "database/sql" "fmt" "time" ) // func applyCorrections(corrections []model.F64Correction, measures []Measure) []Measure { // for idx, measure := range measures { // var isCorrected bool // for _, correction := range corrections { // if measure.Time >= correction.Time { // measure.Value += correction.Value // isCorrected = true // } // } // if isCorrected { // measures[idx] = measure // } // } // return measures // } func applyCorrectionsToMeasure(corrections []_f64Correction, tm int64, value float64) float64 { for _, correction := range corrections { if tm >= correction.Time { value += correction.Value } } return value } func applyCorrectionsToMeasures(corrections []_f64Correction, measures []_measure) { for idx, m := range measures { for _, correction := range corrections { if m.Time >= correction.Time { m.Value += correction.Value } } measures[idx] = m } } func applyCorrectionsToRawMeasures(corrections []_f64Correction, measures []RawMeasure) { for idx, m := range measures { for _, correction := range corrections { if m.Time >= correction.Time { m.Value += correction.Value } } measures[idx] = m } } type _measure struct { Time int64 Value float64 } // ВАЖНО! // ВО ВСЕХ ЗАПРОСАХ ОБЯЗАТЕЛЬНА КОРРЕКТНАЯ СОРТИРОВКА! type _f64Correction struct { Time int64 Value float64 } func listF64CorrectionsTx(tx *sql.Tx, metricID int64) (list []_f64Correction, err error) { rows, err := tx.Query( "SELECT tm, value FROM metric_corrections WHERE metricID=? ORDER BY tm ASC", metricID) if err != nil { if err == sql.ErrNoRows { err = nil } return } defer rows.Close() for rows.Next() { var correction _f64Correction err = rows.Scan(&correction.Time, &correction.Value) if err != nil { return } list = append(list, correction) } return } // result = append(result, Measure{ // Time: tm, // Values: []float64{ // value, // value - prevValue, // }, // }) // listCumulativeMeasures - возвращает список CumulativeMeasure. // Метод ничего не знает про FirstHourOfDay, но извне границы Since и Until могут быть // скорректированы с учетом FirstHourOfDay. Это не поломает выборку. func (s *MeasureQueryEngine) listCumulativeMeasures(req MetricMeasuresFilter) (_ []Measure, err error) { tx, err := s.db.Driver().Begin() if err != nil { return } defer tx.Rollback() // corrections, err := listF64CorrectionsTx(tx, req.MetricID) // if err != nil { // return // } // ВАЖНО! // Оптимизация! Запрашиваем больше данных чем нужно, чтобы посчитать Total для // самого старого измерения в result. // Отнимаем от since еще 1 час (с запасом) чтобы зацепить extendedMeasure extendedSince := req.Since - 3600 rows, err := tx.Query(` SELECT tm, value FROM f64 WHERE metricID=? AND tm BETWEEN ? AND ? ORDER BY tm ASC`, req.MetricID, extendedSince, req.Until) if err != nil { if err == sql.ErrNoRows { err = nil } return } defer rows.Close() var ( result []Measure measures []_measure prev *_measure ) // Вычитываем все найденные показания for rows.Next() { var m _measure err = rows.Scan(&m.Time, &m.Value) if err != nil { return } if m.Time < req.Since { // пока не найдено первое показание внутри диапазона обновлем extendedValue prev = &_measure{ Time: m.Time, Value: m.Value, } } else { measures = append(measures, m) break } } // Остальные показания вычитываем без проверок for rows.Next() { var m _measure err = rows.Scan(&m.Time, &m.Value) if err != nil { return } measures = append(measures, m) } if err = rows.Err(); err != nil { return } if len(measures) == 0 { return } if prev == nil { // extended not found // extended показание не найдено, а значит для первого показания не сможем показать total. // Нужен дополнительный запрос. // Находим максимально свежее показание метрики до extendedSince, от которого и будем // считать total. var ( tm int64 value float64 ) err = tx.QueryRow(` SELECT tm, value FROM f64 WHERE metricID=? AND tm < ? ORDER BY tm DESC LIMIT 1`, req.MetricID, extendedSince).Scan(&tm, &value) if err != nil { if err != sql.ErrNoRows { return } err = nil } else { prev = &_measure{ Time: tm, Value: value, } } } // ВАЖНО! // Сперва применяем коррекции к показаниям, а затем рассчитываем сумму за период // if len(corrections) > 0 { // applyCorrectionsToMeasures(corrections, measures) // if prev != nil { // prev.Value = applyCorrectionsToMeasure(corrections, prev.Time, prev.Value) // } // } var prevValue float64 if prev != nil { prevValue = prev.Value } for _, m := range measures { result = append(result, Measure{ Time: m.Time, Values: []float64{ m.Value, m.Value - prevValue, }, }) prevValue = m.Value } // Корректируем самый первый тотал. Eсли prev показания не было -> total = 0 if prev == nil { first := result[0] first.Values[1] = 0 result[0] = first } return result, nil } type AggregatedMeasuresFilter struct { MetricID int64 `json:"metricID"` Since int64 `json:"since"` Until int64 `json:"until"` GroupBy GroupBy `json:"groupBy"` LastDayOfMonth int `json:"lastDayOfMonth"` // например, конец месяца 25 число FirstHourOfDay int `json:"firstHourOfDay"` // например день начинается в 6:00 } // listAggregatedCumulativeMeasures - возвращает список AggregatedCumulativeMeasure, // сгрупированный по какому-то периоду func (s *MeasureQueryEngine) listAggregatedCumulativeMeasures(req AggregatedMeasuresFilter) (_ []AggregatedMeasure, err error) { //pretty.PPrintln("listAggregatedCumulativeMeasures", req) tx, err := s.db.Driver().Begin() if err != nil { return } defer tx.Rollback() corrections, err := listF64CorrectionsTx(tx, req.MetricID) if err != nil { return } var ( // чтобы не экранировать символы в шаблоне строки groupByFormat string parsePeriodLayout string ) switch req.GroupBy { case ByHour: groupByFormat = "%Y%m%d%H" parsePeriodLayout = hourPeriodLayout case ByDay: groupByFormat = "%Y%m%d" parsePeriodLayout = dayPeriodLayout case ByMonth: groupByFormat = "%Y%m" parsePeriodLayout = monthPeriodLayout default: // Защита от дурака err = fmt.Errorf("unknown groupBy: %s", req.GroupBy) return } // ВАЖНО! // Оптимизация! Запрашиваем больше данных чем нужно, чтобы посчитать начало // первого и конец последнего периодов без дополнительных запросов. // // Отнимаем 3 минуты чтобы зацепить последнее показание предыдущего периода extendedSince := req.Since - 3*60 // Добавляем 1 час, чтобы зацепить первое показание следующего периода extendedUntil := req.Until + 60*60 //fmt.Printf("extendedSince: %s\n\n", extendedSince) // Для каждого периода выбираем первое и последнее (по времени) измерение. // Если делать выборку одним запросом без UNION, а именно сделать // INNER JOIN ... ON f64.tm=x.maxtm OR f64.tm=x.mintm - все начинает тормозить. // Два запроса работают достаточно быстро. // ВАЖНО! // Не забыть про сортировку tm DESC! dt, err := getPeriodDateTime(req.GroupBy, req.LastDayOfMonth, req.FirstHourOfDay) if err != nil { return } //fmt.Printf("DT: %s\n\n", dt) query := fmt.Sprintf(` WITH measures AS (SELECT * FROM f64 WHERE metricID=? AND tm BETWEEN ? AND ?), aggregated AS ( SELECT MIN(tm) as minTm, MAX(tm) as maxTm, DATE_FORMAT(%s, '%s') as periodStr FROM measures GROUP BY periodStr ) SELECT aggregated.periodStr, firstMeasures.tm, firstMeasures.value, lastMeasures.tm, lastMeasures.value FROM aggregated INNER JOIN measures firstMeasures ON firstMeasures.tm = aggregated.minTm INNER JOIN measures lastMeasures ON lastMeasures.tm = aggregated.maxTm ORDER BY aggregated.periodStr ASC`, dt, groupByFormat) rows, err := tx.Query(query, req.MetricID, extendedSince, extendedUntil) if err != nil { if err == sql.ErrNoRows { err = nil } return } defer rows.Close() var result []AggregatedMeasure periodCalculator := NewPeriodCalculator(PeriodCalculatorOptions{ GroupBy: req.GroupBy, LastDayOfMonth: req.LastDayOfMonth, FirstHourOfDay: req.FirstHourOfDay, Since: time.Unix(req.Since, 0), Until: time.Unix(req.Until, 0), }) periodProducer := NewPeriodProducer(PeriodProducerOptions{ MetricID: req.MetricID, ParsePeriodLayout: parsePeriodLayout, Location: s.location, Until: req.Until, Corrections: corrections, Rows: rows, Tx: tx, }) result, err = SplitByPeriods(periodProducer, periodCalculator) if err != nil { return } return result, nil } type RangeTotal struct { Since int64 `json:"since"` // реальная граница Until int64 `json:"until"` // реальная граница StartValue float64 `json:"startValue"` // на конец периода EndValue float64 `json:"endValue"` // на конец периода } type getCumulativeTotalFilter struct { MetricID int64 Since int64 Until int64 LastDayOfMonth int FirstHourOfDay int } func (s *MeasureQueryEngine) getCumulativeTotal(req getCumulativeTotalFilter) (_ *RangeTotal, err error) { measures, err := s.listAggregatedCumulativeMeasures(AggregatedMeasuresFilter{ MetricID: req.MetricID, Since: req.Since, Until: req.Until, GroupBy: ByDay, LastDayOfMonth: req.LastDayOfMonth, FirstHourOfDay: req.FirstHourOfDay, }) if err != nil { return } if len(measures) == 0 { return } //pretty.PPrintln("TOTAL", measures) first := measures[0] last := measures[len(measures)-1] return &RangeTotal{ Since: first.Since, Until: last.Until, StartValue: first.Values[0] - first.Values[1], // value on period end - period total EndValue: last.Values[0], }, nil } /* // GetCumulativeTotal - фикс, добавить опции FirstHourInDay, IsFindPeriodEndsInFuture func (s *MeasureQueryEngine) getCumulativeTotal(req getCumulativeTotalFilter) (_ *RangeTotal, err error) { tx, err := s.db.Driver().Begin() if err != nil { return } defer tx.Rollback() var ( firstTm, lastTm, futureTm int64 firstValue, lastValue, futureValue float64 foundInPast, foundInFuture, foundFirstInRange, foundLastInRange bool ) // Поиск первого показания // Заглядываем в прошлое на 3 минуты firstTm, firstValue, foundInPast, err = s.findLastInRange(tx, req.MetricID, req.Since-3*60, req.Since) if err != nil { return } if !foundInPast { // firstTm, firstValue, foundFirstInRange, err = s.findFirstInRange(tx, req.MetricID, req.Since, req.Until) if err != nil { return } if !foundFirstInRange { // Начало периода не найдено - значит нет показаний для указанного периода return } } // Поиск последнего показания lastTm, lastValue, foundLastInRange, err = s.findLastInRange(tx, req.MetricID, req.Since, req.Until) if err != nil { return } if req.CanPeriodEndsInFuture { // Если можем искать конец в периода в будущем if foundLastInRange { if lastTm > (req.Until - 3*60) { // Если lastTm в последние 3 минуты периода - период корректно завершен return &RangeTotal{ Since: firstTm, Until: lastTm, Value: lastValue, Total: lastValue - firstValue, }, nil } } // Период не завершен в последние 3 минуты периода, поэтому ищем в будущем futureTm, futureValue, foundInFuture, err = s.findFirstAfter(tx, req.MetricID, req.Until) if err != nil { return } if foundInFuture { return &RangeTotal{ Since: firstTm, Until: futureTm, Value: futureValue, Total: futureValue - firstValue, }, nil } else { // Супер важная проверка, ибо возможна ситуация, когда когда нашли начало // текущего периода в конце предыдущего (foundInPast). А это показание было // последним в БД. if foundLastInRange { // Последнее показание внутри периода нашли, неважно когда оно пришло - нам // подходит. return &RangeTotal{ Since: firstTm, Until: lastTm, Value: lastValue, Total: lastValue - firstValue, }, nil } // Последнее показание внутри периода не найдено, а значит их не было вообще. // Просто выходим return } } else { // В будущем искать нельзя // Супер важная проверка, ибо возможна ситуация, когда когда нашли начало // текущего периода в конце предыдущего (foundInPast). А это показание было // последним в БД. if foundLastInRange { // Последнее показание внутри периода нашли, неважно когда оно пришло - нам // подходит. return &RangeTotal{ Since: firstTm, Until: lastTm, Value: lastValue, Total: lastValue - firstValue, }, nil } // Последнее показание внутри периода не найдено, а значит их не было вообще. // Просто выходим return } } */ func (s *MeasureQueryEngine) findLastInRangeTx(tx *sql.Tx, metricID int64, since, until int64) (tm int64, value float64, found bool, err error) { err = tx.QueryRow(` SELECT tm, value, true FROM f64 WHERE metricID=? AND tm >= ? AND tm < ? ORDER BY tm DESC LIMIT 1`, metricID, since, until).Scan(&tm, &value, &found) if err != nil { if err != sql.ErrNoRows { return } // Нет записей err = nil } return } func (s *MeasureQueryEngine) findFirstInRangeTx(tx *sql.Tx, metricID int64, since, until int64) (tm int64, value float64, found bool, err error) { err = tx.QueryRow(` SELECT tm, value, true FROM f64 WHERE metricID=? AND tm >= ? AND tm < ? ORDER BY tm ASC LIMIT 1`, metricID, since, until).Scan(&tm, &value, &found) if err != nil { if err != sql.ErrNoRows { return } // Нет записей err = nil } return } func (s *MeasureQueryEngine) findFirstAfterTx(tx *sql.Tx, metricID int64, since int64) (tm int64, value float64, found bool, err error) { err = tx.QueryRow(` SELECT tm, value, true FROM f64 WHERE metricID=? AND tm >= ? ORDER BY tm ASC LIMIT 1`, metricID, since).Scan(&tm, &value, &found) if err != nil { if err != sql.ErrNoRows { return } // Нет записей err = nil } return }