wp
This commit is contained in:
340
mqe/aggregated.go
Normal file
340
mqe/aggregated.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gordenko.dev/dima/qb/timeutil"
|
||||
)
|
||||
|
||||
type AggregatedMeasure struct {
|
||||
Period int64 `json:"p"`
|
||||
Since int64 `json:"s"`
|
||||
Until int64 `json:"u"`
|
||||
// для cumulative метрики 2 значения - value и total
|
||||
// для instant метрики от 1 до 3 значений - min, max, avg
|
||||
Values []float64 `json:"v"`
|
||||
}
|
||||
|
||||
type AggregatedFilter struct {
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
GroupBy GroupBy
|
||||
LastDayOfMonth int // например, конец месяца 25 число
|
||||
FirstHourOfDay int // например день начинается в 6:00
|
||||
InstantMetrics []InstantMetricAndFuncs
|
||||
CumulativeMetrics []int64
|
||||
}
|
||||
|
||||
type InstantMetricAndFuncs struct {
|
||||
MetricID int64
|
||||
AggregateFuncs byte
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) SelectAggregatedData(req AggregatedFilter) (resultMap map[int64][]AggregatedMeasure, err error) {
|
||||
since := req.Since
|
||||
until := req.Until
|
||||
// Валидация
|
||||
switch req.GroupBy {
|
||||
case ByHour, ByDay, ByMonth:
|
||||
// pass
|
||||
default:
|
||||
err = fix.Field(WrongValue, "groupBy")
|
||||
return
|
||||
}
|
||||
|
||||
if req.FirstHourOfDay < 0 || req.FirstHourOfDay > 23 {
|
||||
err = fix.Field(InvalidFirstHourOfDay, "firstHourOfDay")
|
||||
return
|
||||
}
|
||||
|
||||
if req.LastDayOfMonth < 0 || req.LastDayOfMonth > 28 {
|
||||
err = fix.Field(InvalidLastDayOfMonth, "lastDayOfMonth")
|
||||
return
|
||||
}
|
||||
|
||||
// Корректируем, ВСЕГДА на последнюю секунду суток
|
||||
//until = timeutil.LastSecondInPeriod(until, "d")
|
||||
|
||||
// fmt.Printf(`SelectAggregatedData: {
|
||||
// Since: %s
|
||||
// Until: %s
|
||||
// GroupBy: %s
|
||||
// FirstHourOfDay: %d
|
||||
// LastDayOfMonth: %d
|
||||
// }
|
||||
// `, since, until, req.GroupBy, req.FirstHourOfDay, req.LastDayOfMonth)
|
||||
// FIX проверить как добавляются часы в DST часовых поясах
|
||||
// Для req.FirstHourOfDay=4 since должен быть 4:00:00, until должен быть
|
||||
// 3:59:59 следующего дня
|
||||
if req.FirstHourOfDay > 0 {
|
||||
since = since.Add(time.Duration(req.FirstHourOfDay) * time.Hour)
|
||||
until = until.Add(time.Duration(req.FirstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
sinceUnixtime := since.Unix()
|
||||
untilUnixtime := until.Unix()
|
||||
|
||||
if sinceUnixtime >= untilUnixtime {
|
||||
err = fix.Error(WrongDateRange)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
cumulativeTasks []*AggregatedCumulativeTask
|
||||
instantTasks []*AggregatedInstantTask
|
||||
wg = new(sync.WaitGroup)
|
||||
)
|
||||
|
||||
// Важно вирутальная метрика зависит от реальных, которые к объекту могут быть
|
||||
// привязаны или нет. Если нет - не возвращать.
|
||||
resultMap = make(map[int64][]AggregatedMeasure)
|
||||
|
||||
if len(req.CumulativeMetrics) > 0 {
|
||||
for _, metricID := range req.CumulativeMetrics {
|
||||
task := &AggregatedCumulativeTask{
|
||||
In: AggregatedMeasuresFilter{
|
||||
MetricID: metricID,
|
||||
Since: sinceUnixtime,
|
||||
Until: untilUnixtime,
|
||||
GroupBy: req.GroupBy,
|
||||
LastDayOfMonth: req.LastDayOfMonth,
|
||||
FirstHourOfDay: req.FirstHourOfDay,
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.aggregatedCumulativeCh <- task
|
||||
cumulativeTasks = append(cumulativeTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.InstantMetrics) > 0 {
|
||||
for _, metric := range req.InstantMetrics {
|
||||
task := &AggregatedInstantTask{
|
||||
In: AggregatedInstantMeasuresIn{
|
||||
MetricID: metric.MetricID,
|
||||
Since: sinceUnixtime,
|
||||
Until: untilUnixtime,
|
||||
GroupBy: req.GroupBy,
|
||||
LastDayOfMonth: req.LastDayOfMonth,
|
||||
FirstHourOfDay: req.FirstHourOfDay,
|
||||
Flags: metric.AggregateFuncs, // ФЛАГИ !!!
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.aggregatedInstantCh <- task
|
||||
instantTasks = append(instantTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, task := range cumulativeTasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
resultMap[task.In.MetricID] = task.Result
|
||||
}
|
||||
|
||||
for _, task := range instantTasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
resultMap[task.In.MetricID] = task.Result
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type IndividualAggregatedFilter struct {
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
GroupBy GroupBy
|
||||
InstantMetrics []IndividualInstantMetric
|
||||
CumulativeMetrics []IndividualCumulativeMetric
|
||||
}
|
||||
|
||||
type IndividualInstantMetric struct {
|
||||
MetricID int64
|
||||
AggregateFuncs byte
|
||||
LastDayOfMonth int // например, конец месяца 25 число
|
||||
FirstHourOfDay int // например день начинается в 6:00
|
||||
}
|
||||
|
||||
type IndividualCumulativeMetric struct {
|
||||
MetricID int64
|
||||
LastDayOfMonth int // например, конец месяца 25 число
|
||||
FirstHourOfDay int // например день начинается в 6:00
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) SelectIndividualAggregatedData(req IndividualAggregatedFilter) (resultMap map[int64][]AggregatedMeasure, err error) {
|
||||
if req.Since.IsZero() {
|
||||
err = fmt.Errorf("zero Since")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Until.IsZero() {
|
||||
err = fmt.Errorf("zero Until")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Since.After(req.Until) {
|
||||
err = fmt.Errorf("wrong time range: since %s after until %s", req.Since, req.Until)
|
||||
return
|
||||
}
|
||||
|
||||
// Корректируем, ВСЕГДА на первую и последнюю секунду суток
|
||||
req.Since = timeutil.FirstSecondInPeriod(req.Since, "d")
|
||||
req.Until = timeutil.LastSecondInPeriod(req.Until, "d")
|
||||
|
||||
// Валидация
|
||||
switch req.GroupBy {
|
||||
case ByHour, ByDay, ByMonth:
|
||||
// pass
|
||||
default:
|
||||
err = fix.Field(WrongValue, "groupBy")
|
||||
return
|
||||
}
|
||||
|
||||
// fmt.Printf(`SelectIndividualAggregatedData: {
|
||||
// Since: %s
|
||||
// Until: %s
|
||||
// GroupBy: %s
|
||||
// }
|
||||
// `, req.Since, req.Until, req.GroupBy)
|
||||
// Важно вирутальная метрика зависит от реальных, которые к объекту могут быть
|
||||
// привязаны или нет. Если нет - не возвращать.
|
||||
|
||||
resultMap = make(map[int64][]AggregatedMeasure)
|
||||
|
||||
var (
|
||||
unexpectedErrors []error
|
||||
errorMutex sync.Mutex
|
||||
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
wg.Add(len(req.CumulativeMetrics) + len(req.InstantMetrics))
|
||||
|
||||
if len(req.CumulativeMetrics) > 0 {
|
||||
for _, m := range req.CumulativeMetrics {
|
||||
go func(metric IndividualCumulativeMetric) {
|
||||
if metric.FirstHourOfDay < 0 || metric.FirstHourOfDay > 23 {
|
||||
err = fix.Field(InvalidFirstHourOfDay, "firstHourOfDay")
|
||||
return
|
||||
}
|
||||
|
||||
if metric.LastDayOfMonth < 0 || metric.LastDayOfMonth > 28 {
|
||||
err = fix.Field(InvalidLastDayOfMonth, "lastDayOfMonth")
|
||||
return
|
||||
}
|
||||
|
||||
since := req.Since
|
||||
until := req.Until
|
||||
// FIX проверить как добавляются часы в DST часовых поясах
|
||||
// Для req.FirstHourOfDay=4 since должен быть 4:00:00, until должен быть
|
||||
// 3:59:59 следующего дня
|
||||
if metric.FirstHourOfDay > 0 {
|
||||
since = req.Since.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
until = req.Until.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
result, err := s.listAggregatedCumulativeMeasures(AggregatedMeasuresFilter{
|
||||
MetricID: metric.MetricID,
|
||||
Since: since.Unix(),
|
||||
Until: until.Unix(),
|
||||
GroupBy: req.GroupBy,
|
||||
LastDayOfMonth: metric.LastDayOfMonth,
|
||||
FirstHourOfDay: metric.FirstHourOfDay,
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ListAggregatedCumulativeMeasures: %s", err)
|
||||
|
||||
errorMutex.Lock()
|
||||
unexpectedErrors = append(unexpectedErrors, err)
|
||||
errorMutex.Unlock()
|
||||
} else {
|
||||
errorMutex.Lock()
|
||||
resultMap[metric.MetricID] = result
|
||||
errorMutex.Unlock()
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}(m)
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.InstantMetrics) > 0 {
|
||||
for _, m := range req.InstantMetrics {
|
||||
go func(metric IndividualInstantMetric) {
|
||||
if metric.FirstHourOfDay < 0 || metric.FirstHourOfDay > 23 {
|
||||
err = fix.Field(InvalidFirstHourOfDay, "firstHourOfDay")
|
||||
return
|
||||
}
|
||||
|
||||
if metric.LastDayOfMonth < 0 || metric.LastDayOfMonth > 28 {
|
||||
err = fix.Field(InvalidLastDayOfMonth, "lastDayOfMonth")
|
||||
return
|
||||
}
|
||||
|
||||
since := req.Since
|
||||
until := req.Until
|
||||
// FIX проверить как добавляются часы в DST часовых поясах
|
||||
// Для req.FirstHourOfDay=4 since должен быть 4:00:00, until должен быть
|
||||
// 3:59:59 следующего дня
|
||||
if metric.FirstHourOfDay > 0 {
|
||||
since = req.Since.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
until = req.Until.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
result, err := s.listAggregatedInstantMeasures(AggregatedInstantMeasuresIn{
|
||||
MetricID: metric.MetricID,
|
||||
Since: since.Unix(),
|
||||
Until: until.Unix(),
|
||||
GroupBy: req.GroupBy,
|
||||
LastDayOfMonth: metric.LastDayOfMonth,
|
||||
FirstHourOfDay: metric.FirstHourOfDay,
|
||||
Flags: metric.AggregateFuncs, // ФЛАГИ !!!
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("listAggregatedInstantMeasures: %s", err)
|
||||
|
||||
errorMutex.Lock()
|
||||
unexpectedErrors = append(unexpectedErrors, err)
|
||||
errorMutex.Unlock()
|
||||
} else {
|
||||
errorMutex.Lock()
|
||||
resultMap[metric.MetricID] = result
|
||||
errorMutex.Unlock()
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}(m)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Проверяем что все запросы завершились удачно
|
||||
if len(unexpectedErrors) > 0 {
|
||||
// Нумеруем сообщения об ошибках и склеиваем в одно большое сообщение
|
||||
var errorStrings []string
|
||||
|
||||
for idx, err := range unexpectedErrors {
|
||||
errorStrings = append(errorStrings, fmt.Sprintf("\n#%d %s", idx+1, err))
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%d errors occured:%s", len(unexpectedErrors),
|
||||
strings.Join(errorStrings, ""))
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
29
mqe/fix.go
Normal file
29
mqe/fix.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"gordenko.dev/dima/fixme"
|
||||
)
|
||||
|
||||
const (
|
||||
EmptyValue = "EMPTY_VALUE"
|
||||
BusyName = "BUSY_NAME"
|
||||
WrongValue = "WRONG_VALUE"
|
||||
WrongDate = "WRONG_DATE"
|
||||
WrongDateRange = "WRONG_DATE_RANGE"
|
||||
InvalidFirstHourOfDay = "WRONG_FIRST_HOUR_OF_DAY"
|
||||
InvalidLastDayOfMonth = "WRONG_LAST_DAY_OF_MONTH"
|
||||
)
|
||||
|
||||
var (
|
||||
errorMessages = map[fixme.Code]string{
|
||||
EmptyValue: "порожнє значення неприпустимо",
|
||||
BusyName: "имя уже используется",
|
||||
WrongValue: "неправильне значення",
|
||||
WrongDate: "некоректна дата",
|
||||
WrongDateRange: "некоректний діапазон",
|
||||
InvalidFirstHourOfDay: "корректное значение от 0 до 23",
|
||||
InvalidLastDayOfMonth: "корректное значение от 0 до 28",
|
||||
}
|
||||
|
||||
fix = fixme.New(errorMessages)
|
||||
)
|
||||
158
mqe/helpers.go
Normal file
158
mqe/helpers.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func getPeriodDateTime(groupBy GroupBy, lastDayOfMonth int, firstHourOfDay int) (dt string, err error) {
|
||||
//fmt.Printf("getPeriodDateTime: %s, lastDay: %d, firstHour: %d\n", groupBy, lastDayOfMonth, firstHourOfDay)
|
||||
// Если FirstHourOfDay и LastDayOfMonth равны 0 - модифицировать дату не нужно
|
||||
dt = `FROM_UNIXTIME(tm)`
|
||||
|
||||
switch groupBy {
|
||||
case ByDay:
|
||||
if firstHourOfDay > 0 {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)
|
||||
)`,
|
||||
firstHourOfDay)
|
||||
}
|
||||
|
||||
case ByMonth:
|
||||
if lastDayOfMonth > 0 {
|
||||
if lastDayOfMonth > 28 {
|
||||
// На всякий случай добавляем защиту от дурака, ибо модификация даты
|
||||
// в запросе (+1 месяц) для 29, 30 и 31 чисел сделает запрос неверным.
|
||||
err = fmt.Errorf("LastDayOfMonth: %d, max allowed value is 28", lastDayOfMonth)
|
||||
return
|
||||
}
|
||||
|
||||
if firstHourOfDay > 0 {
|
||||
// В псевдокоде модификация времени показания по корректный период
|
||||
// выглядит так:
|
||||
//
|
||||
// if hour >= FirstHourOfDay {
|
||||
// if day <= LastDayOfMonth {
|
||||
// return tm
|
||||
// } else {
|
||||
// return tm + 1 month
|
||||
// }
|
||||
// } else {
|
||||
// if (day - 1) <= LastDayOfMonth {
|
||||
// return tm - 1 day
|
||||
// } else {
|
||||
// return tm - 1 day + 1 month
|
||||
// }
|
||||
// }
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
IF (DAYOFMONTH(FROM_UNIXTIME(tm)) <= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_ADD(FROM_UNIXTIME(tm), INTERVAL 1 MONTH)
|
||||
),
|
||||
IF (DAYOFMONTH(DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)) <= %d,
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY),
|
||||
DATE_ADD(DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY), INTERVAL 1 MONTH)
|
||||
)
|
||||
)`,
|
||||
firstHourOfDay, lastDayOfMonth, lastDayOfMonth)
|
||||
} else {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (DAYOFMONTH(FROM_UNIXTIME(tm)) <= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_ADD(FROM_UNIXTIME(tm), INTERVAL 1 MONTH)
|
||||
)`,
|
||||
lastDayOfMonth)
|
||||
}
|
||||
} else {
|
||||
if firstHourOfDay > 0 {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)
|
||||
)`,
|
||||
firstHourOfDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getDateTimeFacingFHD(firstHourOfDay int) (dt string, err error) {
|
||||
if firstHourOfDay > 0 {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)
|
||||
)`,
|
||||
firstHourOfDay)
|
||||
} else {
|
||||
// Если FirstHourOfDay = 0 - модифицировать дату не нужно
|
||||
dt = "FROM_UNIXTIME(tm)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getDateTimeFacingFHDAndLDM(firstHourOfDay int, lastDayOfMonth int) (dt string, err error) {
|
||||
// Если FirstHourOfDay и LastDayOfMonth равны 0 - модифицировать дату не нужно
|
||||
dt = "FROM_UNIXTIME(tm)"
|
||||
|
||||
if lastDayOfMonth > 0 {
|
||||
if lastDayOfMonth > 28 {
|
||||
// На всякий случай добавляем защиту от дурака, ибо модификация даты
|
||||
// в запросе (+1 месяц) для 29, 30 и 31 чисел сделает запрос неверным.
|
||||
err = fmt.Errorf("LastDayOfMonth: %d, max allowed value is 28", lastDayOfMonth)
|
||||
return
|
||||
}
|
||||
|
||||
if firstHourOfDay > 0 {
|
||||
// В псевдокоде модификация времени показания по корректный период
|
||||
// выглядит так:
|
||||
//
|
||||
// if hour >= FirstHourOfDay {
|
||||
// if day <= LastDayOfMonth {
|
||||
// return tm
|
||||
// } else {
|
||||
// return tm + 1 month
|
||||
// }
|
||||
// } else {
|
||||
// if (day - 1) <= LastDayOfMonth {
|
||||
// return tm - 1 day
|
||||
// } else {
|
||||
// return tm - 1 day + 1 month
|
||||
// }
|
||||
// }
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
IF (DAYOFMONTH(FROM_UNIXTIME(tm)) <= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_ADD(FROM_UNIXTIME(tm), INTERVAL 1 MONTH)
|
||||
),
|
||||
IF (DAYOFMONTH(DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)) <= %d,
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY),
|
||||
DATE_ADD(DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY), INTERVAL 1 MONTH)
|
||||
)
|
||||
)`,
|
||||
firstHourOfDay, lastDayOfMonth, lastDayOfMonth)
|
||||
} else {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (DAYOFMONTH(FROM_UNIXTIME(tm)) <= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_ADD(FROM_UNIXTIME(tm), INTERVAL 1 MONTH)
|
||||
)`,
|
||||
lastDayOfMonth)
|
||||
}
|
||||
} else {
|
||||
if firstHourOfDay > 0 {
|
||||
dt = fmt.Sprintf(`
|
||||
IF (HOUR(FROM_UNIXTIME(tm)) >= %d,
|
||||
FROM_UNIXTIME(tm),
|
||||
DATE_SUB(FROM_UNIXTIME(tm), INTERVAL 1 DAY)
|
||||
)`,
|
||||
firstHourOfDay)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
320
mqe/index.go
Normal file
320
mqe/index.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type GetYearIndexReq struct {
|
||||
Since int64 `json:"since"` // из фильтр-формы
|
||||
Until int64 `json:"until"` // из фильтр-формы
|
||||
LastDayOfMonth int `json:"lastDayOfMonth"` // например, конец месяца 25 число
|
||||
FirstHourOfDay int `json:"firstHourOfDay"` // например день начинается в 6:00
|
||||
MetricIDs []int64 `json:"metricIDs"`
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) GetYearIndex(req GetYearIndexReq) (years []int, err error) {
|
||||
if len(req.MetricIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
lte, gte string
|
||||
|
||||
args = []any{
|
||||
req.MetricIDs,
|
||||
}
|
||||
)
|
||||
|
||||
if req.Since > 0 {
|
||||
lte = " AND tm >= ?"
|
||||
args = append(args, req.Since)
|
||||
}
|
||||
|
||||
if req.Until > 0 {
|
||||
gte = " AND tm <= ?"
|
||||
args = append(args, req.Until)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`
|
||||
SELECT YEAR(FROM_UNIXTIME(tm)) as y
|
||||
FROM f64
|
||||
WHERE metricID IN (?)%s%s
|
||||
GROUP BY y`,
|
||||
lte, gte)
|
||||
|
||||
// fmt.Println(q)
|
||||
// fmt.Printf("%v\n", args)
|
||||
|
||||
err = s.db.ListQuery(&years, q, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Ints(years)
|
||||
return
|
||||
}
|
||||
|
||||
type GetMonthIndexReq struct {
|
||||
Since int64 `json:"since"` // из фильтр-формы
|
||||
Until int64 `json:"until"` // из фильтр-формы
|
||||
LastDayOfMonth int `json:"lastDayOfMonth"` // например, конец месяца 25 число
|
||||
FirstHourOfDay int `json:"firstHourOfDay"` // например день начинается в 6:00
|
||||
MetricIDs []int64 `json:"metricIDs"`
|
||||
}
|
||||
|
||||
type YearMonth struct {
|
||||
Year int `json:"year"`
|
||||
Months []int `json:"months"`
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) GetMonthIndex(req GetMonthIndexReq) (years []YearMonth, err error) {
|
||||
if len(req.MetricIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
dt, err := getDateTimeFacingFHDAndLDM(req.FirstHourOfDay, req.LastDayOfMonth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
lte, gte string
|
||||
|
||||
args = []any{
|
||||
req.MetricIDs,
|
||||
}
|
||||
)
|
||||
|
||||
if req.Since > 0 {
|
||||
lte = " AND tm >= ?"
|
||||
args = append(args, req.Since)
|
||||
}
|
||||
|
||||
if req.Until > 0 {
|
||||
gte = " AND tm <= ?"
|
||||
args = append(args, req.Until)
|
||||
}
|
||||
|
||||
// ВАЖНО!
|
||||
// Не сортируем средствами MySQL чтобы не создавать временный файл
|
||||
query := fmt.Sprintf(`
|
||||
SELECT DATE_FORMAT(%s, '%s') as p
|
||||
FROM f64
|
||||
WHERE metricID IN (?)%s%s
|
||||
GROUP BY p`,
|
||||
dt, "%Y%m", lte, gte)
|
||||
|
||||
var (
|
||||
temp []string
|
||||
yms []int
|
||||
)
|
||||
|
||||
err = s.db.ListQuery(&temp, query, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, str := range temp {
|
||||
var ym int
|
||||
ym, err = strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
yms = append(yms, ym)
|
||||
}
|
||||
return IndexYms(yms), nil
|
||||
}
|
||||
|
||||
type GetDayIndexReq struct {
|
||||
Since int64 `json:"since"` // из фильтр-формы
|
||||
Until int64 `json:"until"` // из фильтр-формы
|
||||
FirstHourOfDay int `json:"firstHourOfDay"` // например день начинается в 6:00
|
||||
MetricIDs []int64 `json:"metricIDs"`
|
||||
}
|
||||
|
||||
type YearMonthDay struct {
|
||||
Year int `json:"year"`
|
||||
Months []MonthDay `json:"months"`
|
||||
}
|
||||
|
||||
type MonthDay struct {
|
||||
Month int `json:"month"`
|
||||
Days []int `json:"days"`
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) GetDayIndex(req GetDayIndexReq) (years []YearMonthDay, err error) {
|
||||
if len(req.MetricIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
dt, err := getDateTimeFacingFHD(req.FirstHourOfDay)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
lte, gte string
|
||||
|
||||
args = []any{
|
||||
req.MetricIDs,
|
||||
}
|
||||
)
|
||||
|
||||
if req.Since > 0 {
|
||||
lte = " AND tm >= ?"
|
||||
args = append(args, req.Since)
|
||||
}
|
||||
|
||||
if req.Until > 0 {
|
||||
gte = " AND tm <= ?"
|
||||
args = append(args, req.Until)
|
||||
}
|
||||
// ВАЖНО!
|
||||
// Не сортируем средствами MySQL чтобы не создавать временный файл
|
||||
query := fmt.Sprintf(`
|
||||
SELECT DATE_FORMAT(%s, '%s') as p
|
||||
FROM f64
|
||||
WHERE metricID IN(?)%s%s
|
||||
GROUP BY p`,
|
||||
dt, "%Y%m%d", lte, gte)
|
||||
|
||||
//fmt.Printf("%s\n", query)
|
||||
|
||||
var (
|
||||
temp []string
|
||||
ymds []int
|
||||
)
|
||||
|
||||
err = s.db.ListQuery(&temp, query, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//pretty.PPrintln("temp", temp)
|
||||
|
||||
for _, str := range temp {
|
||||
var ymd int
|
||||
ymd, err = strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ymds = append(ymds, ymd)
|
||||
}
|
||||
|
||||
return IndexYmds(ymds), nil
|
||||
}
|
||||
|
||||
func IndexYmds(ymds []int) (years []YearMonthDay) {
|
||||
if len(ymds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Ints(ymds)
|
||||
|
||||
var (
|
||||
months []MonthDay
|
||||
days []int
|
||||
year, month, day int
|
||||
)
|
||||
|
||||
for _, ymd := range ymds {
|
||||
y := ymd / 10000
|
||||
m := (ymd % 10000) / 100
|
||||
d := ymd % 100
|
||||
|
||||
if year == 0 {
|
||||
// инициализация
|
||||
year = y
|
||||
month = m
|
||||
day = d
|
||||
} else {
|
||||
if y != year {
|
||||
days = append(days, day)
|
||||
months = append(months, MonthDay{
|
||||
Month: month,
|
||||
Days: days,
|
||||
})
|
||||
years = append(years, YearMonthDay{
|
||||
Year: year,
|
||||
Months: months,
|
||||
})
|
||||
year = y
|
||||
month = m
|
||||
day = d
|
||||
months = nil
|
||||
days = nil
|
||||
} else if m != month {
|
||||
days = append(days, day)
|
||||
months = append(months, MonthDay{
|
||||
Month: month,
|
||||
Days: days,
|
||||
})
|
||||
month = m
|
||||
day = d
|
||||
days = nil
|
||||
} else if d != day {
|
||||
days = append(days, day)
|
||||
day = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
days = append(days, day)
|
||||
months = append(months, MonthDay{
|
||||
Month: month,
|
||||
Days: days,
|
||||
})
|
||||
years = append(years, YearMonthDay{
|
||||
Year: year,
|
||||
Months: months,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func IndexYms(yms []int) (years []YearMonth) {
|
||||
if len(yms) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Ints(yms)
|
||||
|
||||
var (
|
||||
months []int
|
||||
year, month int
|
||||
)
|
||||
|
||||
for _, ym := range yms {
|
||||
y := ym / 100
|
||||
m := ym % 100
|
||||
|
||||
if year == 0 {
|
||||
// инициализация
|
||||
year = y
|
||||
month = m
|
||||
} else {
|
||||
if y != year {
|
||||
months = append(months, month)
|
||||
years = append(years, YearMonth{
|
||||
Year: year,
|
||||
Months: months,
|
||||
})
|
||||
year = y
|
||||
month = m
|
||||
months = nil
|
||||
} else if m != month {
|
||||
months = append(months, month)
|
||||
month = m
|
||||
}
|
||||
}
|
||||
}
|
||||
months = append(months, month)
|
||||
years = append(years, YearMonth{
|
||||
Year: year,
|
||||
Months: months,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
//SELECT DATE_FORMAT(FROM_UNIXTIME(tm), '%Y%m%d') as p FROM f64 WHERE metricID IN (50) AND tm >= 1700863200 AND tm <= 1700949599 GROUP BY p;
|
||||
39
mqe/index_test.go
Normal file
39
mqe/index_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gordenko.dev/dima/pretty"
|
||||
)
|
||||
|
||||
func TestDayIndex(t *testing.T) {
|
||||
index := IndexYmds([]int{
|
||||
20221201,
|
||||
20221202,
|
||||
20230110,
|
||||
20230210,
|
||||
20230330,
|
||||
20230331,
|
||||
20240101,
|
||||
20240102,
|
||||
20240103,
|
||||
})
|
||||
|
||||
pretty.Println(index)
|
||||
}
|
||||
|
||||
func TestMonthIndex(t *testing.T) {
|
||||
index := IndexYms([]int{
|
||||
202211,
|
||||
202212,
|
||||
202301,
|
||||
202302,
|
||||
202303,
|
||||
202304,
|
||||
202404,
|
||||
202405,
|
||||
202406,
|
||||
})
|
||||
|
||||
pretty.Println(index)
|
||||
}
|
||||
226
mqe/instant.go
Normal file
226
mqe/instant.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CurrentValue struct {
|
||||
MetricID uint32
|
||||
Time int64 `json:"t"`
|
||||
Value float64 `json:"v"`
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) ListCurrentValues(metricIDs []uint32) (result []CurrentValue, err error) {
|
||||
driver := s.db.Driver()
|
||||
|
||||
for _, metricID := range metricIDs {
|
||||
var rows *sql.Rows
|
||||
rows, err = driver.Query(`
|
||||
SELECT tm, value
|
||||
FROM f64
|
||||
WHERE metricID=?
|
||||
ORDER BY tm DESC
|
||||
LIMIT 1`,
|
||||
metricID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
tm int64
|
||||
value float64
|
||||
)
|
||||
err = rows.Scan(&tm, &value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = append(result, CurrentValue{
|
||||
MetricID: metricID,
|
||||
Time: tm,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListInstantMeasures - cписок показаний мгновенных метрик (Температура, Давление, Расход)
|
||||
// за за интервал без группировки
|
||||
func (s *MeasureQueryEngine) listInstantMeasures(req MetricMeasuresFilter) (_ []Measure, err error) {
|
||||
rows, err := s.db.Driver().Query(`
|
||||
SELECT tm, value
|
||||
FROM f64
|
||||
WHERE metricID=? AND tm BETWEEN ? AND ?
|
||||
ORDER BY tm ASC`,
|
||||
req.MetricID, req.Since, req.Until)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []Measure
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
tm int64
|
||||
value float64
|
||||
)
|
||||
err = rows.Scan(&tm, &value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = append(result, Measure{
|
||||
Time: tm,
|
||||
Values: []float64{
|
||||
value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type AggregatedInstantMeasuresIn struct {
|
||||
MetricID int64 `json:"metricID"`
|
||||
Since int64 `json:"since"`
|
||||
Until int64 `json:"until"`
|
||||
GroupBy GroupBy `json:"groupBy"`
|
||||
Flags byte `json:"flags"` // из настроек по умолчанию либо из запроса
|
||||
LastDayOfMonth int `json:"lastDayOfMonth"` // например, конец месяца 25 число
|
||||
FirstHourOfDay int `json:"firstHourOfDay"` // например день начинается в 6:00
|
||||
}
|
||||
|
||||
// Заполняет поля Period и Values (от 1 до 3 значений) в структуре AggregatedMeasure
|
||||
func (s *MeasureQueryEngine) listAggregatedInstantMeasures(req AggregatedInstantMeasuresIn) (_ []AggregatedMeasure, err error) {
|
||||
var (
|
||||
groupByFormat string
|
||||
parsePeriodLayout string
|
||||
|
||||
result []AggregatedMeasure
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
valuesQty := 0
|
||||
values := ""
|
||||
|
||||
if (req.Flags & AggregateMin) == AggregateMin {
|
||||
values += " MIN(value),"
|
||||
valuesQty++
|
||||
}
|
||||
|
||||
if (req.Flags & AggregateMax) == AggregateMax {
|
||||
values += " MAX(value),"
|
||||
valuesQty++
|
||||
}
|
||||
|
||||
if (req.Flags & AggregateAvg) == AggregateAvg {
|
||||
values += " AVG(value),"
|
||||
valuesQty++
|
||||
}
|
||||
|
||||
dt, err := getPeriodDateTime(req.GroupBy, req.LastDayOfMonth, req.FirstHourOfDay)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT%s DATE_FORMAT(%s, '%s') as periodStr
|
||||
FROM f64
|
||||
WHERE metricID=? AND tm BETWEEN ? AND ?
|
||||
GROUP BY periodStr
|
||||
ORDER BY periodStr ASC`,
|
||||
values, dt, groupByFormat)
|
||||
|
||||
rows, err := s.db.Driver().Query(query, req.MetricID, req.Since, req.Until)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
m = AggregatedMeasure{
|
||||
Values: make([]float64, valuesQty),
|
||||
}
|
||||
|
||||
periodStr string
|
||||
period time.Time
|
||||
|
||||
valueIdx int
|
||||
dest []interface{}
|
||||
)
|
||||
|
||||
if (req.Flags & AggregateMin) == AggregateMin {
|
||||
dest = append(dest, &m.Values[valueIdx])
|
||||
valueIdx++
|
||||
}
|
||||
|
||||
if (req.Flags & AggregateMax) == AggregateMax {
|
||||
dest = append(dest, &m.Values[valueIdx])
|
||||
valueIdx++
|
||||
}
|
||||
|
||||
if (req.Flags & AggregateAvg) == AggregateAvg {
|
||||
dest = append(dest, &m.Values[valueIdx])
|
||||
}
|
||||
|
||||
dest = append(dest, &periodStr)
|
||||
|
||||
err = rows.Scan(dest...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
period, err = time.ParseInLocation(parsePeriodLayout, periodStr, s.location)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("time.ParseInLocation: %s; layout=%q; str=%q",
|
||||
err, parsePeriodLayout, periodStr)
|
||||
return
|
||||
}
|
||||
m.Period = period.Unix()
|
||||
|
||||
result = append(result, m)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
161
mqe/mqe.go
Normal file
161
mqe/mqe.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gordenko.dev/dima/qx"
|
||||
)
|
||||
|
||||
type GroupBy string
|
||||
|
||||
// Для парсинга since и until
|
||||
const (
|
||||
NoAggregateFunc byte = 1
|
||||
AggregateMin byte = 2
|
||||
AggregateMax byte = 4
|
||||
AggregateAvg byte = 8
|
||||
|
||||
ByHour GroupBy = "h"
|
||||
ByDay GroupBy = "d"
|
||||
ByMonth GroupBy = "m"
|
||||
|
||||
hourPeriodLayout = "2006010215"
|
||||
dayPeriodLayout = "20060102"
|
||||
monthPeriodLayout = "200601"
|
||||
|
||||
dateLayout = "2006-01-02"
|
||||
)
|
||||
|
||||
type MetricMeasuresFilter struct {
|
||||
MetricID int64 `json:"metricID"`
|
||||
Since int64 `json:"since"` // уже учтен firstHourOfDay
|
||||
Until int64 `json:"until"` // уже учтен firstHourOfDay
|
||||
}
|
||||
|
||||
type MeasureQueryEngine struct {
|
||||
db *qx.Db
|
||||
location *time.Location
|
||||
cumulativeCh chan *CumulativeTask
|
||||
instantCh chan *InstantTask
|
||||
aggregatedCumulativeCh chan *AggregatedCumulativeTask
|
||||
aggregatedInstantCh chan *AggregatedInstantTask
|
||||
cumulativeTotalCh chan *CumulativeTotalTask
|
||||
metricReadingsCh chan *MetricReadingsTask
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Db *qx.Db
|
||||
Location *time.Location
|
||||
MinWorkers int
|
||||
QueueSize int
|
||||
}
|
||||
|
||||
func New(opt Options) *MeasureQueryEngine {
|
||||
if opt.Db == nil {
|
||||
panic("Db option is required")
|
||||
}
|
||||
|
||||
if opt.Location == nil {
|
||||
panic("Location option is required")
|
||||
}
|
||||
|
||||
if opt.MinWorkers <= 0 {
|
||||
panic("MinWorkers option is required")
|
||||
}
|
||||
|
||||
if opt.QueueSize <= 0 {
|
||||
panic("QueueSize option is required")
|
||||
}
|
||||
|
||||
s := new(MeasureQueryEngine)
|
||||
s.db = opt.Db
|
||||
s.location = opt.Location
|
||||
//
|
||||
s.cumulativeCh = make(chan *CumulativeTask, opt.QueueSize)
|
||||
s.instantCh = make(chan *InstantTask, opt.QueueSize)
|
||||
s.aggregatedCumulativeCh = make(chan *AggregatedCumulativeTask, opt.QueueSize)
|
||||
s.aggregatedInstantCh = make(chan *AggregatedInstantTask, opt.QueueSize)
|
||||
s.cumulativeTotalCh = make(chan *CumulativeTotalTask, opt.QueueSize)
|
||||
s.metricReadingsCh = make(chan *MetricReadingsTask, opt.QueueSize)
|
||||
|
||||
//
|
||||
for i := 0; i < opt.MinWorkers; i++ {
|
||||
go s.worker()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *MeasureQueryEngine) worker() {
|
||||
for {
|
||||
select {
|
||||
case task := <-s.cumulativeCh:
|
||||
task.Result, task.Err = s.listCumulativeMeasures(task.In)
|
||||
task.WaitGroup.Done()
|
||||
|
||||
case task := <-s.instantCh:
|
||||
task.Result, task.Err = s.listInstantMeasures(task.In)
|
||||
task.WaitGroup.Done()
|
||||
|
||||
case task := <-s.aggregatedCumulativeCh:
|
||||
task.Result, task.Err = s.listAggregatedCumulativeMeasures(task.In)
|
||||
task.WaitGroup.Done()
|
||||
|
||||
case task := <-s.aggregatedInstantCh:
|
||||
task.Result, task.Err = s.listAggregatedInstantMeasures(task.In)
|
||||
task.WaitGroup.Done()
|
||||
|
||||
case task := <-s.cumulativeTotalCh:
|
||||
task.Result, task.Err = s.getCumulativeTotal(task.In)
|
||||
task.WaitGroup.Done()
|
||||
|
||||
case task := <-s.metricReadingsCh:
|
||||
task.Result, task.Err = s.listMetricReadings(task.In)
|
||||
task.WaitGroup.Done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
type CumulativeTask struct {
|
||||
In MetricMeasuresFilter
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result []Measure
|
||||
Err error
|
||||
}
|
||||
|
||||
type InstantTask struct {
|
||||
In MetricMeasuresFilter
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result []Measure
|
||||
Err error
|
||||
}
|
||||
|
||||
type AggregatedCumulativeTask struct {
|
||||
In AggregatedMeasuresFilter
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result []AggregatedMeasure
|
||||
Err error
|
||||
}
|
||||
|
||||
type AggregatedInstantTask struct {
|
||||
In AggregatedInstantMeasuresIn
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result []AggregatedMeasure
|
||||
Err error
|
||||
}
|
||||
|
||||
type MetricReadingsTask struct {
|
||||
In MetricMeasuresFilter
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result MetricReadings
|
||||
Err error
|
||||
}
|
||||
|
||||
type CumulativeTotalTask struct {
|
||||
In getCumulativeTotalFilter
|
||||
WaitGroup *sync.WaitGroup
|
||||
Result *RangeTotal
|
||||
Err error
|
||||
}
|
||||
283
mqe/newreq.go
Normal file
283
mqe/newreq.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package mqe
|
||||
|
||||
// НОВАЯ ВЕРСИЯ
|
||||
|
||||
// type MetricsDataFilter2 struct {
|
||||
// Since time.Time `json:"since"`
|
||||
// Until time.Time `json:"until"`
|
||||
// GroupBy string `json:"groupBy"`
|
||||
|
||||
// InstantMetrics []InstantMetricFilter // virtualMetricID + aggregateFuncs
|
||||
// CumulativeMetrics []CumulativeMetricFilter
|
||||
// }
|
||||
|
||||
// type InstantMetricFilter struct {
|
||||
// MetricID int64
|
||||
// AggregateFuncs byte
|
||||
// LastDayOfMonth int // например, конец месяца 25 число
|
||||
// FirstHourOfDay int // например день начинается в 6:00
|
||||
// }
|
||||
|
||||
// type CumulativeMetricFilter struct {
|
||||
// MetricID int64
|
||||
// LastDayOfMonth int // например, конец месяца 25 число
|
||||
// FirstHourOfDay int // например день начинается в 6:00
|
||||
// }
|
||||
|
||||
// func (s *MeasureQueryEngine) SelectMetricsDataInParallel2(req MetricsDataFilter2) (_ MetricsDataResultMaps, err error) {
|
||||
// pretty.PPrintln("SelectMetricsDataInParallel2", req)
|
||||
// // Строгая валидация
|
||||
|
||||
// if req.Since.IsZero() {
|
||||
// err = fmt.Errorf("Since option is required")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if req.Until.IsZero() {
|
||||
// err = fmt.Errorf("Until option is required")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if req.Since.After(req.Until) {
|
||||
// err = fmt.Errorf("Wrong time range: Since %d after Until %d", req.Since.Unix(), req.Until.Unix())
|
||||
// return
|
||||
// }
|
||||
|
||||
// switch req.GroupBy {
|
||||
// case "h", "d", "m", "":
|
||||
// // pass
|
||||
// default:
|
||||
// err = fmt.Errorf("Wrong groupBy option value: %s", req.GroupBy)
|
||||
// return
|
||||
// }
|
||||
|
||||
// for _, mf := range req.InstantMetrics {
|
||||
// if mf.MetricID == 0 {
|
||||
// err = fmt.Errorf("Empty instant metric's MetricID: %d", mf.MetricID)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if mf.FirstHourOfDay < 0 || mf.FirstHourOfDay > 23 {
|
||||
// err = fmt.Errorf("Wrong instant metric's FirstHourOfDay option value: %d", mf.FirstHourOfDay)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if mf.LastDayOfMonth < 0 || mf.LastDayOfMonth > 28 {
|
||||
// err = fmt.Errorf("Wrong instant metric's LastDayOfMonth option value: %d", mf.LastDayOfMonth)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// for _, mf := range req.CumulativeMetrics {
|
||||
// if mf.MetricID == 0 {
|
||||
// err = fmt.Errorf("Empty cumulative metric's MetricID: %d", mf.MetricID)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if mf.FirstHourOfDay < 0 || mf.FirstHourOfDay > 23 {
|
||||
// err = fmt.Errorf("Wrong cumulative metric's FirstHourOfDay option value: %d", mf.FirstHourOfDay)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if mf.LastDayOfMonth < 0 || mf.LastDayOfMonth > 28 {
|
||||
// err = fmt.Errorf("Wrong cumulative metric's LastDayOfMonth option value: %d", mf.LastDayOfMonth)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Корректируем, ВСЕГДА на первую и последнюю секунду суток
|
||||
// since := timeutil.FirstSecondInPeriod(req.Since, "d")
|
||||
// until := timeutil.LastSecondInPeriod(req.Until, "d")
|
||||
|
||||
// fmt.Printf(`SelectMetricsDataInParallel2: {
|
||||
// Since: %s
|
||||
// Until: %s
|
||||
// GroupBy: %s
|
||||
// }
|
||||
// `, since, until, req.GroupBy)
|
||||
|
||||
// // Важно вирутальная метрика зависит от реальных, которые к объекту могут быть
|
||||
// // привязаны или нет. Если нет - не возвращать.
|
||||
|
||||
// var (
|
||||
// unexpectedErrors []error
|
||||
// errorMutex sync.Mutex
|
||||
|
||||
// wg sync.WaitGroup
|
||||
|
||||
// resultMaps = MetricsDataResultMaps{
|
||||
// AggregatedCumulative: make(map[int64][]CalculatedAggregatedMeasure),
|
||||
// AggregatedByFuncInstant: make(map[int64][]AggregatedByFuncInstantMeasure),
|
||||
// Cumulative: make(map[int64][]CalculatedMeasure),
|
||||
// Instant: make(map[int64][]CalculatedMeasure),
|
||||
// }
|
||||
// )
|
||||
|
||||
// wg.Add(len(req.CumulativeMetrics) + len(req.InstantMetrics))
|
||||
|
||||
// if req.GroupBy != "" {
|
||||
// // AGGREGATED
|
||||
|
||||
// // CUMULATIVE
|
||||
// for _, metricFilter := range req.CumulativeMetrics {
|
||||
// go func(mf CumulativeMetricFilter) {
|
||||
// // У каждой метрики индивидуальные настройки,
|
||||
// // ибо метрики принадлежат разным объектам!!!
|
||||
// metricSince := since
|
||||
// metricUntil := until
|
||||
|
||||
// if mf.FirstHourOfDay > 0 {
|
||||
// metricSince = since.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// metricUntil = until.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// }
|
||||
|
||||
// result, err := s.listAggregatedCumulativeMeasures(AggregatedMeasuresFilter{
|
||||
// MetricID: mf.MetricID,
|
||||
// Since: metricSince.Unix(),
|
||||
// Until: metricUntil.Unix(),
|
||||
// GroupBy: req.GroupBy,
|
||||
// LastDayOfMonth: mf.LastDayOfMonth,
|
||||
// FirstHourOfDay: mf.FirstHourOfDay,
|
||||
// })
|
||||
// if err != nil {
|
||||
// err = fmt.Errorf("ListAggregatedCumulativeMeasures: %s", err)
|
||||
|
||||
// errorMutex.Lock()
|
||||
// unexpectedErrors = append(unexpectedErrors, err)
|
||||
// errorMutex.Unlock()
|
||||
// } else {
|
||||
// errorMutex.Lock()
|
||||
// resultMaps.AggregatedCumulative[mf.MetricID] = result
|
||||
// errorMutex.Unlock()
|
||||
// }
|
||||
|
||||
// wg.Done()
|
||||
// }(metricFilter)
|
||||
// }
|
||||
|
||||
// // INSTANT
|
||||
// for _, metricFilter := range req.InstantMetrics {
|
||||
// go func(mf InstantMetricFilter) {
|
||||
// // У каждой метрики индивидуальные настройки,
|
||||
// // ибо метрики принадлежат разным объектам!!!
|
||||
// metricSince := since
|
||||
// metricUntil := until
|
||||
|
||||
// if mf.FirstHourOfDay > 0 {
|
||||
// metricSince = since.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// metricUntil = until.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// }
|
||||
|
||||
// result, err := s.listAggregatedByFuncsInstantMeasures(AggregatedByFuncInstantMeasuresFilter{
|
||||
// MetricID: mf.MetricID,
|
||||
// Since: metricSince.Unix(),
|
||||
// Until: metricUntil.Unix(),
|
||||
// GroupBy: req.GroupBy,
|
||||
// LastDayOfMonth: mf.LastDayOfMonth,
|
||||
// FirstHourOfDay: mf.FirstHourOfDay,
|
||||
// Flags: mf.AggregateFuncs, // ФЛАГИ !!!
|
||||
// })
|
||||
// if err != nil {
|
||||
// err = fmt.Errorf("ListAggregatedByFuncInstantMeasures: %s", err)
|
||||
|
||||
// errorMutex.Lock()
|
||||
// unexpectedErrors = append(unexpectedErrors, err)
|
||||
// errorMutex.Unlock()
|
||||
// } else {
|
||||
// errorMutex.Lock()
|
||||
// resultMaps.AggregatedByFuncInstant[mf.MetricID] = result
|
||||
// errorMutex.Unlock()
|
||||
// }
|
||||
|
||||
// wg.Done()
|
||||
// }(metricFilter)
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// // NON AGGREGATED
|
||||
|
||||
// // CUMULATIVE
|
||||
// for _, metricFilter := range req.CumulativeMetrics {
|
||||
// go func(mf CumulativeMetricFilter) {
|
||||
// // У каждой метрики индивидуальные настройки,
|
||||
// // ибо метрики принадлежат разным объектам!!!
|
||||
// metricSince := since
|
||||
// metricUntil := until
|
||||
|
||||
// if mf.FirstHourOfDay > 0 {
|
||||
// metricSince = since.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// metricUntil = until.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// }
|
||||
|
||||
// result, err := s.listCumulativeMeasures(MeasuresFilter{
|
||||
// MetricID: mf.MetricID,
|
||||
// Since: metricSince.Unix(),
|
||||
// Until: metricUntil.Unix(),
|
||||
// })
|
||||
// if err != nil {
|
||||
// err = fmt.Errorf("ListCumulativeMeasures: %s", err)
|
||||
|
||||
// errorMutex.Lock()
|
||||
// unexpectedErrors = append(unexpectedErrors, err)
|
||||
// errorMutex.Unlock()
|
||||
// } else {
|
||||
// errorMutex.Lock()
|
||||
// resultMaps.Cumulative[mf.MetricID] = result
|
||||
// errorMutex.Unlock()
|
||||
// }
|
||||
// wg.Done()
|
||||
// }(metricFilter)
|
||||
// }
|
||||
|
||||
// // INSTANT
|
||||
// for _, metricFilter := range req.InstantMetrics {
|
||||
// go func(mf InstantMetricFilter) {
|
||||
// // У каждой метрики индивидуальные настройки,
|
||||
// // ибо метрики принадлежат разным объектам!!!
|
||||
// metricSince := since
|
||||
// metricUntil := until
|
||||
|
||||
// if mf.FirstHourOfDay > 0 {
|
||||
// metricSince = since.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// metricUntil = until.Add(time.Duration(mf.FirstHourOfDay) * time.Hour)
|
||||
// }
|
||||
|
||||
// result, err := s.listInstantMeasures(MeasuresFilter{
|
||||
// MetricID: mf.MetricID,
|
||||
// Since: metricSince.Unix(),
|
||||
// Until: metricUntil.Unix(),
|
||||
// })
|
||||
// if err != nil {
|
||||
// err = fmt.Errorf("ListInstantMeasures: %s", err)
|
||||
|
||||
// errorMutex.Lock()
|
||||
// unexpectedErrors = append(unexpectedErrors, err)
|
||||
// errorMutex.Unlock()
|
||||
// } else {
|
||||
// errorMutex.Lock()
|
||||
// resultMaps.Instant[mf.MetricID] = result
|
||||
// errorMutex.Unlock()
|
||||
// }
|
||||
// wg.Done()
|
||||
// }(metricFilter)
|
||||
// }
|
||||
// }
|
||||
|
||||
// wg.Wait()
|
||||
|
||||
// // Проверяем что все запросы завершились удачно
|
||||
// if len(unexpectedErrors) > 0 {
|
||||
// // Нумеруем сообщения об ошибках и склеиваем в одно большое сообщение
|
||||
// var errorStrings []string
|
||||
|
||||
// for idx, err := range unexpectedErrors {
|
||||
// errorStrings = append(errorStrings, fmt.Sprintf("\n#%d %s", idx+1, err))
|
||||
// }
|
||||
|
||||
// err = fmt.Errorf("%d errors occured:%s", len(unexpectedErrors),
|
||||
// strings.Join(errorStrings, ""))
|
||||
// return
|
||||
// }
|
||||
|
||||
// return resultMaps, nil
|
||||
// }
|
||||
172
mqe/period_calculator.go
Normal file
172
mqe/period_calculator.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gordenko.dev/dima/qb/timeutil"
|
||||
)
|
||||
|
||||
type PeriodCalculatorOptions struct {
|
||||
GroupBy GroupBy
|
||||
LastDayOfMonth int
|
||||
FirstHourOfDay int
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
}
|
||||
|
||||
func NewPeriodCalculator(opt PeriodCalculatorOptions) PeriodCalculator {
|
||||
switch opt.GroupBy {
|
||||
case ByHour, ByDay, ByMonth:
|
||||
// pass
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("bug: unknown groupBy %q", opt.GroupBy))
|
||||
}
|
||||
|
||||
s := PeriodCalculator{
|
||||
groupBy: opt.GroupBy,
|
||||
lastDayOfMonth: opt.LastDayOfMonth,
|
||||
firstHourOfDay: opt.FirstHourOfDay,
|
||||
}
|
||||
s.sincePeriod = s.TimeToPeriod(opt.Since)
|
||||
s.untilPeriod = s.TimeToPeriod(opt.Until)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type PeriodCalculator struct {
|
||||
groupBy GroupBy
|
||||
lastDayOfMonth int
|
||||
firstHourOfDay int
|
||||
sincePeriod time.Time
|
||||
untilPeriod time.Time
|
||||
}
|
||||
|
||||
func (s PeriodCalculator) IsExtendedSincePeriod(period time.Time) bool {
|
||||
return period.Before(s.sincePeriod)
|
||||
}
|
||||
|
||||
func (s PeriodCalculator) IsExtendedUntilPeriod(period time.Time) bool {
|
||||
return period.After(s.untilPeriod)
|
||||
}
|
||||
|
||||
func (s PeriodCalculator) NextPeriod(period time.Time) time.Time {
|
||||
switch s.groupBy {
|
||||
case ByHour:
|
||||
return period.Add(time.Hour)
|
||||
|
||||
case ByDay:
|
||||
return period.AddDate(0, 0, 1)
|
||||
|
||||
case ByMonth:
|
||||
return period.AddDate(0, 1, 0)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("bug: unknown groupBy %q", s.groupBy))
|
||||
}
|
||||
}
|
||||
|
||||
func (s PeriodCalculator) IsPeriodCorrectEnds(period time.Time, lastMeasureTime int64) bool {
|
||||
// по умолчанию
|
||||
tm := timeutil.LastSecondInPeriod(period, string(s.groupBy))
|
||||
|
||||
switch s.groupBy {
|
||||
case ByDay:
|
||||
if s.firstHourOfDay > 0 {
|
||||
// Было 23:59, стало - 01:59
|
||||
tm = tm.Add(time.Duration(s.firstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
case ByMonth:
|
||||
if s.lastDayOfMonth == 0 {
|
||||
if s.firstHourOfDay > 0 {
|
||||
// Было 23:59, стало - 01:59
|
||||
tm = tm.Add(time.Duration(s.firstHourOfDay) * time.Hour)
|
||||
}
|
||||
} else {
|
||||
if s.firstHourOfDay > 0 {
|
||||
// last day 28, first hour 2 превращается в day 29 01:59:59
|
||||
tm = time.Date(
|
||||
tm.Year(),
|
||||
tm.Month(),
|
||||
s.lastDayOfMonth+1,
|
||||
s.firstHourOfDay-1,
|
||||
59,
|
||||
59,
|
||||
0,
|
||||
tm.Location(),
|
||||
)
|
||||
} else {
|
||||
// last day 23:59:59
|
||||
tm = time.Date(
|
||||
tm.Year(),
|
||||
tm.Month(),
|
||||
s.lastDayOfMonth,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
0,
|
||||
tm.Location(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timestamp := tm.Add(-179 * time.Second).Unix()
|
||||
|
||||
if lastMeasureTime >= timestamp && lastMeasureTime <= tm.Unix() {
|
||||
return true
|
||||
}
|
||||
//fmt.Printf("Ends: lastMeasureTime=%s, timetsamp=%s\n", time.Unix(lastMeasureTime, 0), time.Unix(timestamp, 0))
|
||||
return false
|
||||
}
|
||||
|
||||
func (s PeriodCalculator) TimeToPeriod(tm time.Time) (period time.Time) {
|
||||
switch s.groupBy {
|
||||
case ByDay:
|
||||
if s.firstHourOfDay > 0 {
|
||||
if tm.Hour() < s.firstHourOfDay {
|
||||
tm = tm.AddDate(0, 0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
case ByMonth:
|
||||
if s.lastDayOfMonth > 0 {
|
||||
if s.lastDayOfMonth > 28 {
|
||||
// На всякий случай добавляем защиту от дурака, ибо модификация даты
|
||||
// в запросе (+1 месяц) для 29, 30 и 31 чисел сделает запрос неверным.
|
||||
panic(fmt.Sprintf("LastDayOfMonth: %d, max allowed value is 28", s.lastDayOfMonth))
|
||||
}
|
||||
|
||||
if s.firstHourOfDay > 0 {
|
||||
if tm.Hour() >= s.firstHourOfDay {
|
||||
if tm.Day() > s.lastDayOfMonth {
|
||||
// add 1 month
|
||||
tm = tm.AddDate(0, 1, 0)
|
||||
}
|
||||
} else {
|
||||
// firstHourOfDay показывает что текущий день еще не наступил,
|
||||
// поэтому day - 1
|
||||
if (tm.Day() - 1) <= s.lastDayOfMonth {
|
||||
tm = tm.AddDate(0, 0, -1)
|
||||
} else {
|
||||
// - 1 day, + 1 month
|
||||
tm = tm.AddDate(0, 0, -1).AddDate(0, 1, 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if tm.Day() > s.lastDayOfMonth {
|
||||
tm = tm.AddDate(0, 1, 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if s.firstHourOfDay > 0 {
|
||||
if tm.Hour() < s.firstHourOfDay {
|
||||
tm = tm.AddDate(0, 0, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return timeutil.FirstSecondInPeriod(tm, string(s.groupBy))
|
||||
}
|
||||
168
mqe/period_producers.go
Normal file
168
mqe/period_producers.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type periodProducer interface {
|
||||
Next() (time.Time, PeriodBound, PeriodBound, bool, error)
|
||||
Last() (PeriodBound, bool, error)
|
||||
}
|
||||
|
||||
type PeriodBound struct {
|
||||
Time int64
|
||||
Value float64
|
||||
}
|
||||
|
||||
type PeriodProducer struct {
|
||||
metricID int64
|
||||
parsePeriodLayout string
|
||||
location *time.Location
|
||||
until int64
|
||||
corrections []_f64Correction
|
||||
rows *sql.Rows
|
||||
tx *sql.Tx
|
||||
}
|
||||
|
||||
type PeriodProducerOptions struct {
|
||||
MetricID int64
|
||||
ParsePeriodLayout string
|
||||
Location *time.Location
|
||||
Until int64
|
||||
Corrections []_f64Correction
|
||||
Rows *sql.Rows
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
func NewPeriodProducer(opt PeriodProducerOptions) *PeriodProducer {
|
||||
if opt.Location == nil {
|
||||
panic("Location option is required")
|
||||
}
|
||||
|
||||
if opt.MetricID == 0 {
|
||||
panic("MetricID option is required")
|
||||
}
|
||||
|
||||
if opt.ParsePeriodLayout == "" {
|
||||
panic("ParsePeriodLayout option is required")
|
||||
}
|
||||
|
||||
if opt.Until == 0 {
|
||||
panic("Until option is required")
|
||||
}
|
||||
|
||||
if opt.Rows == nil {
|
||||
panic("Rows option is required")
|
||||
}
|
||||
|
||||
if opt.Tx == nil {
|
||||
panic("Tx option is required")
|
||||
}
|
||||
|
||||
s := new(PeriodProducer)
|
||||
s.metricID = opt.MetricID
|
||||
s.parsePeriodLayout = opt.ParsePeriodLayout
|
||||
s.location = opt.Location
|
||||
s.until = opt.Until
|
||||
s.corrections = opt.Corrections
|
||||
s.rows = opt.Rows
|
||||
s.tx = opt.Tx
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *PeriodProducer) Next() (period time.Time, first PeriodBound, last PeriodBound, found bool, err error) {
|
||||
var frameStr string
|
||||
|
||||
hasNext := s.rows.Next()
|
||||
if hasNext {
|
||||
err = s.rows.Scan(&frameStr, &first.Time, &first.Value, &last.Time, &last.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
first.Value = applyCorrectionsToMeasure(s.corrections, first.Time, first.Value)
|
||||
last.Value = applyCorrectionsToMeasure(s.corrections, last.Time, last.Value)
|
||||
|
||||
period, err = time.ParseInLocation(s.parsePeriodLayout, frameStr, s.location)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("time.ParseInLocation: %s; layout=%q; str=%q",
|
||||
err, s.parsePeriodLayout, frameStr)
|
||||
return
|
||||
}
|
||||
|
||||
found = true
|
||||
} else {
|
||||
err = s.rows.Err()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *PeriodProducer) Last() (next PeriodBound, found bool, err error) {
|
||||
// Находим следующее показание метрики.
|
||||
err = s.tx.QueryRow(`
|
||||
SELECT tm, value, true
|
||||
FROM f64
|
||||
WHERE metricID=? AND tm > ?
|
||||
ORDER BY tm ASC
|
||||
LIMIT 1`,
|
||||
s.metricID, s.until).Scan(&next.Time, &next.Value, &found)
|
||||
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return
|
||||
}
|
||||
// Нет записей
|
||||
err = nil
|
||||
}
|
||||
|
||||
next.Value = applyCorrectionsToMeasure(s.corrections, next.Time, next.Value)
|
||||
return
|
||||
}
|
||||
|
||||
// ДЛЯ ТЕСТИРОВАНИЯ
|
||||
|
||||
type stubPeriod struct {
|
||||
Period time.Time
|
||||
First PeriodBound
|
||||
Last PeriodBound
|
||||
}
|
||||
|
||||
type StubPeriodProducerOptions struct {
|
||||
Last *PeriodBound
|
||||
Periods []stubPeriod
|
||||
}
|
||||
|
||||
func NewStubPeriodProducer(opt StubPeriodProducerOptions) *StubPeriodProducer {
|
||||
s := new(StubPeriodProducer)
|
||||
s.last = opt.Last
|
||||
s.periods = opt.Periods
|
||||
return s
|
||||
}
|
||||
|
||||
type StubPeriodProducer struct {
|
||||
last *PeriodBound // для отдельного запроса
|
||||
periods []stubPeriod
|
||||
idx int
|
||||
}
|
||||
|
||||
func (s *StubPeriodProducer) Next() (period time.Time, first PeriodBound, last PeriodBound, found bool, err error) {
|
||||
if s.idx < len(s.periods) {
|
||||
p := s.periods[s.idx]
|
||||
s.idx++
|
||||
period = p.Period
|
||||
first = p.First
|
||||
last = p.Last
|
||||
found = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StubPeriodProducer) Last() (next PeriodBound, found bool, err error) {
|
||||
if s.last != nil {
|
||||
next = *s.last
|
||||
found = true
|
||||
}
|
||||
return
|
||||
}
|
||||
81
mqe/raw.go
Normal file
81
mqe/raw.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"gordenko.dev/dima/qb"
|
||||
)
|
||||
|
||||
type RawMeasure struct {
|
||||
Time int64 `json:"t"`
|
||||
Value float64 `json:"v"`
|
||||
}
|
||||
|
||||
type RawMeasuresFilter struct {
|
||||
MetricID int64 `json:"metricID"`
|
||||
MetricType qb.MetricType `json:"metricType"`
|
||||
Since int64 `json:"since"` // уже учтен firstHourOfDay
|
||||
Until int64 `json:"until"` // уже учтен firstHourOfDay
|
||||
}
|
||||
|
||||
// ListRawMeasures - cписок показаний мгновенных метрик (Температура, Давление, Расход)
|
||||
// за за интервал без группировки
|
||||
func (s *MeasureQueryEngine) ListRawMeasures(req RawMeasuresFilter) (_ []RawMeasure, err error) {
|
||||
tx, err := s.db.Driver().Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rows, err := tx.Query(`
|
||||
SELECT tm, value
|
||||
FROM f64
|
||||
WHERE metricID=? AND tm BETWEEN ? AND ?
|
||||
ORDER BY tm ASC`,
|
||||
req.MetricID, req.Since, req.Until)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []RawMeasure
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
tm int64
|
||||
value float64
|
||||
)
|
||||
err = rows.Scan(&tm, &value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = append(result, RawMeasure{
|
||||
Time: tm,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if req.MetricType == qb.Cumulative {
|
||||
var corrections []_f64Correction
|
||||
corrections, err = listF64CorrectionsTx(tx, req.MetricID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(corrections) > 0 {
|
||||
applyCorrectionsToRawMeasures(corrections, result)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
179
mqe/readings.go
Normal file
179
mqe/readings.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gordenko.dev/dima/pretty"
|
||||
"gordenko.dev/dima/qb/timeutil"
|
||||
)
|
||||
|
||||
////////////////////////////
|
||||
|
||||
type MetricMeasure struct {
|
||||
Time int64 `json:"t"`
|
||||
Value float64 `json:"v"`
|
||||
}
|
||||
|
||||
type MetricError struct {
|
||||
Time int64 `json:"time"`
|
||||
Code int64 `json:"code"`
|
||||
}
|
||||
|
||||
type ReadingsFilter struct {
|
||||
Since string `json:"since"`
|
||||
Until string `json:"until"`
|
||||
MetricIDs []int64
|
||||
}
|
||||
|
||||
type MetricReadings struct {
|
||||
MetricID int64
|
||||
Measures []MetricMeasure
|
||||
Errors []MetricError
|
||||
}
|
||||
|
||||
// SelectReadingsInParallel - метод достает из БД данные всех метрик, которые связаны
|
||||
// с объектом. Учитываются настройки объекта (LastDayOfMonth, FirstHourOfDay), а также
|
||||
// фильтры выбранные пользователем на странице объекта (Since, Until, GroupBy).
|
||||
// ВАЖНО!
|
||||
// Запросы по каждой метрике отправляются в БД в отдельной горутине, что позволяет
|
||||
// в разы ускорить получение данных по объекту.
|
||||
func (s *MeasureQueryEngine) SelectReadingsInParallel(req ReadingsFilter) (result []MetricReadings, err error) {
|
||||
pretty.PPrintln("SelectReadingsInParallel", req)
|
||||
|
||||
if len(req.MetricIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Since == "" {
|
||||
err = fix.Field(EmptyValue, "since")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Until == "" {
|
||||
err = fix.Field(EmptyValue, "until")
|
||||
return
|
||||
}
|
||||
|
||||
// ВАЖНО!
|
||||
// Если в строке нет таймзоны - Parse функция возвращает время в UTC.
|
||||
// Поэтому нужно использовать ParseInLocation!
|
||||
|
||||
since, err := time.ParseInLocation("2006-01-02", req.Since, s.location)
|
||||
if err != nil {
|
||||
err = fix.Field(WrongValue, "since")
|
||||
return
|
||||
}
|
||||
|
||||
until, err := time.ParseInLocation("2006-01-02", req.Until, s.location)
|
||||
if err != nil {
|
||||
err = fix.Field(WrongValue, "until")
|
||||
return
|
||||
}
|
||||
|
||||
// Корректируем, ВСЕГДА на последнюю секунду суток
|
||||
until = timeutil.LastSecondInPeriod(until, string(ByDay))
|
||||
|
||||
fmt.Printf(`SelectReadingsInParallel: {
|
||||
Since: %s
|
||||
Until: %s
|
||||
MetricIDs: %v
|
||||
}
|
||||
`, since, until, req.MetricIDs)
|
||||
// FIX проверить как добавляются часы в DST часовых поясах
|
||||
// Для req.FirstHourOfDay=4 since должен быть 4:00:00, until должен быть
|
||||
// 3:59:59 следующего дня
|
||||
|
||||
sinceUnixtime := since.Unix()
|
||||
untilUnixtime := until.Unix()
|
||||
|
||||
if sinceUnixtime >= untilUnixtime {
|
||||
err = fix.Error(WrongDateRange)
|
||||
return
|
||||
}
|
||||
|
||||
// Важно вирутальная метрика зависит от реальных, которые к объекту могут быть
|
||||
// привязаны или нет. Если нет - не возвращать.
|
||||
|
||||
var (
|
||||
tasks []*MetricReadingsTask
|
||||
wg = new(sync.WaitGroup)
|
||||
)
|
||||
|
||||
for _, metricID := range req.MetricIDs {
|
||||
task := &MetricReadingsTask{
|
||||
In: MetricMeasuresFilter{
|
||||
MetricID: metricID,
|
||||
Since: sinceUnixtime,
|
||||
Until: untilUnixtime,
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.metricReadingsCh <- task
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, task := range tasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
result = append(result, task.Result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListInstantMeasures - cписок показаний для юнитов без накопительного итога
|
||||
// (например, Давление)
|
||||
func (s *MeasureQueryEngine) listMetricReadings(req MetricMeasuresFilter) (result MetricReadings, err error) {
|
||||
result.MetricID = req.MetricID
|
||||
// buf, _ := json.MarshalIndent(req, "", " ")
|
||||
// fmt.Printf("listInstantMeasures: %s\n\n", buf)
|
||||
|
||||
tx, err := s.db.Driver().Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rows, err := tx.Query(`
|
||||
SELECT tm, value
|
||||
FROM f64
|
||||
WHERE metricID=? AND tm BETWEEN ? AND ?
|
||||
ORDER BY tm ASC`,
|
||||
req.MetricID, req.Since, req.Until)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var measure MetricMeasure
|
||||
err = rows.Scan(&measure.Time, &measure.Value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// fix - для cumulative метрик учет поправок
|
||||
result.Measures = append(result.Measures, measure)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//for _, m := range result {
|
||||
// fmt.Printf("raw: %d\t%f\n", m.Time, m.Value)
|
||||
//}
|
||||
//fmt.Printf("listInstantMeasures (qty): %d\n", len(result))
|
||||
|
||||
return
|
||||
}
|
||||
570
mqe/split.go
Normal file
570
mqe/split.go
Normal file
@@ -0,0 +1,570 @@
|
||||
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
|
||||
}
|
||||
*/
|
||||
141
mqe/total.go
Normal file
141
mqe/total.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gordenko.dev/dima/qb/timeutil"
|
||||
)
|
||||
|
||||
// TOTALS
|
||||
|
||||
type TotalMetricSpec struct {
|
||||
MetricID int64
|
||||
LastDayOfMonth int
|
||||
FirstHourOfDay int
|
||||
}
|
||||
|
||||
// LastDayOfMonth и FirstHourOfDay нет, ибо у каждой метрики свои настройки
|
||||
type RangeTotalsFilter struct {
|
||||
Since string
|
||||
Until string
|
||||
// Если учитывать не нужно - программа уровнем выше может передать нули.
|
||||
// FIX у каждой метрики свои настройки
|
||||
// искать ли конец периода в следующих периодах, если в последние 3 минуты периода
|
||||
// не было показаний
|
||||
CumulativeMetrics []TotalMetricSpec // metricID
|
||||
}
|
||||
|
||||
// SelectRangeTotalsInParallel - метод рассчитывает накопленный объем от Since до Until
|
||||
// по каждой метрике из массива CumulativeMetricIDs. Учитываются настройки объекта
|
||||
// (LastDayOfMonth, FirstHourOfDay).
|
||||
// ВАЖНО!
|
||||
// Запросы по каждой метрике отправляются в БД в отдельной горутине, что позволяет
|
||||
// в разы ускорить получение данных по объекту.
|
||||
func (s *MeasureQueryEngine) GetRangeTotalsInParallel(req RangeTotalsFilter) (resultMap map[int64]RangeTotal, err error) {
|
||||
if req.Since == "" {
|
||||
err = fix.Field(EmptyValue, "since")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Until == "" {
|
||||
err = fix.Field(EmptyValue, "until")
|
||||
return
|
||||
}
|
||||
|
||||
// ВАЖНО!
|
||||
// Если в строке нет таймзоны - Parse функция возвращает время в UTC.
|
||||
// Поэтому нужно использовать ParseInLocation!
|
||||
|
||||
since, err := time.ParseInLocation("2006-01-02", req.Since, s.location)
|
||||
if err != nil {
|
||||
err = fix.Field(WrongValue, "since")
|
||||
return
|
||||
}
|
||||
|
||||
until, err := time.ParseInLocation("2006-01-02", req.Until, s.location)
|
||||
if err != nil {
|
||||
err = fix.Field(WrongValue, "until")
|
||||
return
|
||||
}
|
||||
|
||||
// Корректируем, чтобы указывало на последнюю секунду суток
|
||||
until = timeutil.LastSecondInPeriod(until, string(ByDay))
|
||||
|
||||
var metrics []string
|
||||
|
||||
for _, metric := range req.CumulativeMetrics {
|
||||
metrics = append(metrics, fmt.Sprintf(` {
|
||||
MetricID: %d
|
||||
LastDayOfMonth: %d
|
||||
FirstHourOfDay: %d
|
||||
}`, metric.MetricID, metric.LastDayOfMonth, metric.FirstHourOfDay))
|
||||
}
|
||||
|
||||
fmt.Printf(`GetRangeTotalsInParallel: {
|
||||
Since: %s
|
||||
Until: %s
|
||||
CumulativeMetrics [
|
||||
%s
|
||||
]
|
||||
}
|
||||
`, since, until, strings.Join(metrics, "\n"))
|
||||
if since.Unix() >= until.Unix() {
|
||||
err = fix.Error(WrongDateRange)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
tasks []*CumulativeTotalTask
|
||||
wg = new(sync.WaitGroup)
|
||||
)
|
||||
|
||||
resultMap = make(map[int64]RangeTotal)
|
||||
|
||||
// CUMULATIVE METRIC TOTALS
|
||||
if len(req.CumulativeMetrics) > 0 {
|
||||
for _, metric := range req.CumulativeMetrics {
|
||||
// У каждой метрики индивидуальные настройки,
|
||||
// ибо метрики принадлежат разным объектам!!!
|
||||
metricSince := since
|
||||
metricUntil := until
|
||||
|
||||
if metric.FirstHourOfDay > 0 {
|
||||
metricSince = since.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
metricUntil = until.Add(time.Duration(metric.FirstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
task := &CumulativeTotalTask{
|
||||
In: getCumulativeTotalFilter{
|
||||
MetricID: metric.MetricID,
|
||||
Since: metricSince.Unix(),
|
||||
Until: metricUntil.Unix(),
|
||||
LastDayOfMonth: metric.LastDayOfMonth,
|
||||
FirstHourOfDay: metric.FirstHourOfDay,
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.cumulativeTotalCh <- task
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, task := range tasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
if task.Result != nil {
|
||||
resultMap[task.In.MetricID] = *task.Result
|
||||
} else {
|
||||
resultMap[task.In.MetricID] = RangeTotal{}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
138
mqe/unaggregated.go
Normal file
138
mqe/unaggregated.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package mqe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Measure - рассчитанное показание реальной метрики. Рассчитанное означает
|
||||
// что учтен Factor. То есть значение уже можно показывать юзеру
|
||||
// Для instant - одно значение value, для cumulative - 2 значения (value, total)
|
||||
type Measure struct {
|
||||
Time int64 `json:"t"`
|
||||
Values []float64 `json:"v"`
|
||||
}
|
||||
|
||||
type UnaggregatedFilter struct {
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
LastDayOfMonth int // например, конец месяца 25 число
|
||||
FirstHourOfDay int // например день начинается в 6:00
|
||||
InstantMetrics []int64
|
||||
CumulativeMetrics []int64
|
||||
}
|
||||
|
||||
// type MetricsDataResultMaps struct {
|
||||
// AggregatedCumulative map[int64][]CalculatedAggregatedMeasure
|
||||
// AggregatedByFuncInstant map[int64][]AggregatedByFuncInstantMeasure
|
||||
// Cumulative map[int64][]CalculatedMeasure
|
||||
// Instant map[int64][]CalculatedMeasure
|
||||
// }
|
||||
|
||||
// SelectObjectDataInParallel - метод достает из БД данные всех метрик, которые связаны
|
||||
// с объектом. Учитываются настройки объекта (LastDayOfMonth, FirstHourOfDay), а также
|
||||
// фильтры выбранные пользователем на странице объекта (Since, Until, GroupBy).
|
||||
// ВАЖНО!
|
||||
// Запросы по каждой метрике отправляются в БД в отдельной горутине, что позволяет
|
||||
// в разы ускорить получение данных по объекту.
|
||||
func (s *MeasureQueryEngine) SelectUnaggregatedData(req UnaggregatedFilter) (resultMap map[int64][]Measure, err error) {
|
||||
since := req.Since
|
||||
until := req.Until
|
||||
|
||||
if req.FirstHourOfDay < 0 || req.FirstHourOfDay > 23 {
|
||||
err = fix.Field(InvalidFirstHourOfDay, "firstHourOfDay")
|
||||
return
|
||||
}
|
||||
|
||||
if req.LastDayOfMonth < 0 || req.LastDayOfMonth > 28 {
|
||||
err = fix.Field(InvalidLastDayOfMonth, "lastDayOfMonth")
|
||||
return
|
||||
}
|
||||
|
||||
// fmt.Printf(`SelectUnaggregatedData: {
|
||||
// Since: %s
|
||||
// Until: %s
|
||||
// FirstHourOfDay: %d
|
||||
// LastDayOfMonth: %d
|
||||
// }
|
||||
// `, since, until, req.FirstHourOfDay, req.LastDayOfMonth)
|
||||
// FIX проверить как добавляются часы в DST часовых поясах
|
||||
// Для req.FirstHourOfDay=4 since должен быть 4:00:00, until должен быть
|
||||
// 3:59:59 следующего дня
|
||||
if req.FirstHourOfDay > 0 {
|
||||
since = since.Add(time.Duration(req.FirstHourOfDay) * time.Hour)
|
||||
until = until.Add(time.Duration(req.FirstHourOfDay) * time.Hour)
|
||||
}
|
||||
|
||||
sinceUnixtime := since.Unix()
|
||||
untilUnixtime := until.Unix()
|
||||
|
||||
if sinceUnixtime >= untilUnixtime {
|
||||
err = fix.Error(WrongDateRange)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
cumulativeTasks []*CumulativeTask
|
||||
instantTasks []*InstantTask
|
||||
wg = new(sync.WaitGroup)
|
||||
)
|
||||
|
||||
// Важно вирутальная метрика зависит от реальных, которые к объекту могут быть
|
||||
// привязаны или нет. Если нет - не возвращать.
|
||||
resultMap = make(map[int64][]Measure)
|
||||
|
||||
if len(req.CumulativeMetrics) > 0 {
|
||||
for _, metricID := range req.CumulativeMetrics {
|
||||
task := &CumulativeTask{
|
||||
In: MetricMeasuresFilter{
|
||||
MetricID: metricID,
|
||||
Since: sinceUnixtime,
|
||||
Until: untilUnixtime,
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.cumulativeCh <- task
|
||||
cumulativeTasks = append(cumulativeTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.InstantMetrics) > 0 {
|
||||
for _, metricID := range req.InstantMetrics {
|
||||
task := &InstantTask{
|
||||
In: MetricMeasuresFilter{
|
||||
MetricID: metricID,
|
||||
Since: sinceUnixtime,
|
||||
Until: untilUnixtime,
|
||||
},
|
||||
WaitGroup: wg,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
s.instantCh <- task
|
||||
instantTasks = append(instantTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, task := range cumulativeTasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
resultMap[task.In.MetricID] = task.Result
|
||||
}
|
||||
|
||||
for _, task := range instantTasks {
|
||||
if task.Err != nil {
|
||||
err = fmt.Errorf("get data for the metric %d: %s", task.In.MetricID, task.Err)
|
||||
return
|
||||
}
|
||||
resultMap[task.In.MetricID] = task.Result
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user