Files
new-api-custom/model/detection_schedule.go
T

65 lines
2.0 KiB
Go

package model
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// DetectionSchedule survives task-history cleanup and is never projected directly
// to public APIs. Only automatic results advance this channel/kind clock.
type DetectionSchedule struct {
ChannelID int `gorm:"primaryKey;autoIncrement:false"`
Kind string `gorm:"primaryKey;type:varchar(24)"`
LastValid string `gorm:"type:varchar(16)"`
IntervalMinutes int
NextDueAt int64
LastFinishedAt int64
LastResultID string `gorm:"type:varchar(64)"`
}
func NextDetectionInterval(lastValid, outcome string, current int) int {
if outcome != "normal" && outcome != "degraded" && outcome != "incomplete" {
return 15
}
if lastValid == "" {
return 30
}
if lastValid != outcome {
return 15
}
if current == 15 {
return 30
}
return 60
}
func EnsureDetectionSchedule(ctx context.Context, seed DetectionSchedule) error {
return DB.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&seed).Error
}
func ApplyDetectionScheduleResult(ctx context.Context, channelID int, kind, resultID string, finishedAt int64, outcome string) error {
return DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var state DetectionSchedule
if err := lockForUpdate(tx).Where("channel_id = ? AND kind = ?", channelID, kind).First(&state).Error; err != nil {
return err
}
// A result is committed before its clock advances. Reconciliation can
// safely replay it after a crash, without advancing the clock twice.
if finishedAt <= state.LastFinishedAt || resultID == state.LastResultID {
return nil
}
state.IntervalMinutes = NextDetectionInterval(state.LastValid, outcome, state.IntervalMinutes)
if kind == "ciallo" {
state.IntervalMinutes = 45
}
if outcome == "normal" || outcome == "degraded" || outcome == "incomplete" {
state.LastValid = outcome
}
state.LastFinishedAt, state.LastResultID = finishedAt, resultID
state.NextDueAt = finishedAt + int64(state.IntervalMinutes)*60
return tx.Save(&state).Error
})
}