feat(channel-monitor): aggregate history to daily rollups + soft delete
明细只保留 1 天,超过 1 天聚合到新表 channel_monitor_daily_rollups(按 monitor_id/model/bucket_date 维度),聚合保留 30 天。两张表都用 SoftDeleteMixin 软删除(DELETE 自动改为 UPDATE deleted_at = NOW())。 聚合 + 清理任务由 OpsCleanupService 的 cron 统一调度,与运维监控的清理共享 schedule(默认 0 2 * * *)和 leader lock。ChannelMonitorRunner 的 cleanupLoop 被移除,只保留 dueCheckLoop。 读取路径 ComputeAvailability* 改为 UNION 明细(今天 deleted_at IS NULL)+ 聚合(过去 windowDays 天 deleted_at IS NULL),SUM(ok)/SUM(total) 自然加权 计算可用率,AVG latency 用 SUM(sum_latency_ms)/SUM(count_latency)。 watermark 表 channel_monitor_aggregation_watermark 单行(id=1),记录 last_aggregated_date,重启后从该日期 +1 继续聚合,首次为 nil 则从 today - 30d 开始回填,单次最多 35 天上限避免长事务。 raw SQL 的 ListLatestPerModel / ListLatestForMonitorIDs / ListRecentHistoryForMonitors 都补上 deleted_at IS NULL 过滤(SoftDeleteMixin interceptor 只对 ent query 生效)。 bump version to 0.1.114.28 GroupBadge 在 MonitorKeyPickerDialog 中复用平台主题色 + 倍率/专属倍率 (顺手优化)。
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/schema/field"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitor"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitordailyrollup"
|
||||
"github.com/Wei-Shaw/sub2api/ent/channelmonitorhistory"
|
||||
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||
)
|
||||
@@ -21,12 +22,13 @@ import (
|
||||
// ChannelMonitorQuery is the builder for querying ChannelMonitor entities.
|
||||
type ChannelMonitorQuery struct {
|
||||
config
|
||||
ctx *QueryContext
|
||||
order []channelmonitor.OrderOption
|
||||
inters []Interceptor
|
||||
predicates []predicate.ChannelMonitor
|
||||
withHistory *ChannelMonitorHistoryQuery
|
||||
modifiers []func(*sql.Selector)
|
||||
ctx *QueryContext
|
||||
order []channelmonitor.OrderOption
|
||||
inters []Interceptor
|
||||
predicates []predicate.ChannelMonitor
|
||||
withHistory *ChannelMonitorHistoryQuery
|
||||
withDailyRollups *ChannelMonitorDailyRollupQuery
|
||||
modifiers []func(*sql.Selector)
|
||||
// intermediate query (i.e. traversal path).
|
||||
sql *sql.Selector
|
||||
path func(context.Context) (*sql.Selector, error)
|
||||
@@ -85,6 +87,28 @@ func (_q *ChannelMonitorQuery) QueryHistory() *ChannelMonitorHistoryQuery {
|
||||
return query
|
||||
}
|
||||
|
||||
// QueryDailyRollups chains the current query on the "daily_rollups" edge.
|
||||
func (_q *ChannelMonitorQuery) QueryDailyRollups() *ChannelMonitorDailyRollupQuery {
|
||||
query := (&ChannelMonitorDailyRollupClient{config: _q.config}).Query()
|
||||
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
|
||||
if err := _q.prepareQuery(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selector := _q.sqlQuery(ctx)
|
||||
if err := selector.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step := sqlgraph.NewStep(
|
||||
sqlgraph.From(channelmonitor.Table, channelmonitor.FieldID, selector),
|
||||
sqlgraph.To(channelmonitordailyrollup.Table, channelmonitordailyrollup.FieldID),
|
||||
sqlgraph.Edge(sqlgraph.O2M, false, channelmonitor.DailyRollupsTable, channelmonitor.DailyRollupsColumn),
|
||||
)
|
||||
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
|
||||
return fromU, nil
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// First returns the first ChannelMonitor entity from the query.
|
||||
// Returns a *NotFoundError when no ChannelMonitor was found.
|
||||
func (_q *ChannelMonitorQuery) First(ctx context.Context) (*ChannelMonitor, error) {
|
||||
@@ -272,12 +296,13 @@ func (_q *ChannelMonitorQuery) Clone() *ChannelMonitorQuery {
|
||||
return nil
|
||||
}
|
||||
return &ChannelMonitorQuery{
|
||||
config: _q.config,
|
||||
ctx: _q.ctx.Clone(),
|
||||
order: append([]channelmonitor.OrderOption{}, _q.order...),
|
||||
inters: append([]Interceptor{}, _q.inters...),
|
||||
predicates: append([]predicate.ChannelMonitor{}, _q.predicates...),
|
||||
withHistory: _q.withHistory.Clone(),
|
||||
config: _q.config,
|
||||
ctx: _q.ctx.Clone(),
|
||||
order: append([]channelmonitor.OrderOption{}, _q.order...),
|
||||
inters: append([]Interceptor{}, _q.inters...),
|
||||
predicates: append([]predicate.ChannelMonitor{}, _q.predicates...),
|
||||
withHistory: _q.withHistory.Clone(),
|
||||
withDailyRollups: _q.withDailyRollups.Clone(),
|
||||
// clone intermediate query.
|
||||
sql: _q.sql.Clone(),
|
||||
path: _q.path,
|
||||
@@ -295,6 +320,17 @@ func (_q *ChannelMonitorQuery) WithHistory(opts ...func(*ChannelMonitorHistoryQu
|
||||
return _q
|
||||
}
|
||||
|
||||
// WithDailyRollups tells the query-builder to eager-load the nodes that are connected to
|
||||
// the "daily_rollups" edge. The optional arguments are used to configure the query builder of the edge.
|
||||
func (_q *ChannelMonitorQuery) WithDailyRollups(opts ...func(*ChannelMonitorDailyRollupQuery)) *ChannelMonitorQuery {
|
||||
query := (&ChannelMonitorDailyRollupClient{config: _q.config}).Query()
|
||||
for _, opt := range opts {
|
||||
opt(query)
|
||||
}
|
||||
_q.withDailyRollups = query
|
||||
return _q
|
||||
}
|
||||
|
||||
// GroupBy is used to group vertices by one or more fields/columns.
|
||||
// It is often used with aggregate functions, like: count, max, mean, min, sum.
|
||||
//
|
||||
@@ -373,8 +409,9 @@ func (_q *ChannelMonitorQuery) sqlAll(ctx context.Context, hooks ...queryHook) (
|
||||
var (
|
||||
nodes = []*ChannelMonitor{}
|
||||
_spec = _q.querySpec()
|
||||
loadedTypes = [1]bool{
|
||||
loadedTypes = [2]bool{
|
||||
_q.withHistory != nil,
|
||||
_q.withDailyRollups != nil,
|
||||
}
|
||||
)
|
||||
_spec.ScanValues = func(columns []string) ([]any, error) {
|
||||
@@ -405,6 +442,15 @@ func (_q *ChannelMonitorQuery) sqlAll(ctx context.Context, hooks ...queryHook) (
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if query := _q.withDailyRollups; query != nil {
|
||||
if err := _q.loadDailyRollups(ctx, query, nodes,
|
||||
func(n *ChannelMonitor) { n.Edges.DailyRollups = []*ChannelMonitorDailyRollup{} },
|
||||
func(n *ChannelMonitor, e *ChannelMonitorDailyRollup) {
|
||||
n.Edges.DailyRollups = append(n.Edges.DailyRollups, e)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
@@ -438,6 +484,36 @@ func (_q *ChannelMonitorQuery) loadHistory(ctx context.Context, query *ChannelMo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (_q *ChannelMonitorQuery) loadDailyRollups(ctx context.Context, query *ChannelMonitorDailyRollupQuery, nodes []*ChannelMonitor, init func(*ChannelMonitor), assign func(*ChannelMonitor, *ChannelMonitorDailyRollup)) error {
|
||||
fks := make([]driver.Value, 0, len(nodes))
|
||||
nodeids := make(map[int64]*ChannelMonitor)
|
||||
for i := range nodes {
|
||||
fks = append(fks, nodes[i].ID)
|
||||
nodeids[nodes[i].ID] = nodes[i]
|
||||
if init != nil {
|
||||
init(nodes[i])
|
||||
}
|
||||
}
|
||||
if len(query.ctx.Fields) > 0 {
|
||||
query.ctx.AppendFieldOnce(channelmonitordailyrollup.FieldMonitorID)
|
||||
}
|
||||
query.Where(predicate.ChannelMonitorDailyRollup(func(s *sql.Selector) {
|
||||
s.Where(sql.InValues(s.C(channelmonitor.DailyRollupsColumn), fks...))
|
||||
}))
|
||||
neighbors, err := query.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range neighbors {
|
||||
fk := n.MonitorID
|
||||
node, ok := nodeids[fk]
|
||||
if !ok {
|
||||
return fmt.Errorf(`unexpected referenced foreign-key "monitor_id" returned %v for node %v`, fk, n.ID)
|
||||
}
|
||||
assign(node, n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_q *ChannelMonitorQuery) sqlCount(ctx context.Context) (int, error) {
|
||||
_spec := _q.querySpec()
|
||||
|
||||
Reference in New Issue
Block a user