wp
This commit is contained in:
585
mqe/cumulative.go
Normal file
585
mqe/cumulative.go
Normal file
@@ -0,0 +1,585 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user