stillbox/pkg/alerting/simulate.go

139 lines
3.8 KiB
Go
Raw Normal View History

2024-10-30 09:49:45 -04:00
package alerting
import (
"context"
"errors"
"fmt"
"net/http"
"sort"
"time"
2024-10-31 09:09:58 -04:00
"dynatron.me/x/stillbox/internal/forms"
2024-11-13 09:24:11 -05:00
"dynatron.me/x/stillbox/internal/jsontypes"
2024-10-30 09:49:45 -04:00
"dynatron.me/x/stillbox/internal/trending"
2024-11-03 07:19:03 -05:00
"dynatron.me/x/stillbox/pkg/config"
2024-11-03 08:09:49 -05:00
"dynatron.me/x/stillbox/pkg/talkgroups"
2024-10-30 09:49:45 -04:00
"github.com/rs/zerolog/log"
)
2024-10-31 00:10:53 -04:00
// A Simulation simulates what happens to the alerter during a specified time period using past data from the database.
2024-10-30 09:49:45 -04:00
type Simulation struct {
// normal Alerting config
config.Alerting
// ScoreStart is the time when scoring begins
2024-11-13 09:24:11 -05:00
ScoreStart jsontypes.Time `json:"scoreStart" yaml:"scoreStart" form:"scoreStart"`
2024-10-30 09:49:45 -04:00
// ScoreEnd is the time when the score simulator ends. Left blank, it defaults to time.Now()
2024-11-13 09:24:11 -05:00
ScoreEnd jsontypes.Time `json:"scoreEnd" yaml:"scoreEnd" form:"scoreEnd"`
2024-10-30 09:49:45 -04:00
// SimInterval is the interval at which the scorer will be called
2024-11-13 09:24:11 -05:00
SimInterval jsontypes.Duration `json:"simInterval" yaml:"simInterval" form:"simInterval"`
2024-10-30 09:49:45 -04:00
clock offsetClock `json:"-"`
*alerter `json:"-"`
}
func (s *Simulation) verify() error {
switch {
case !s.ScoreEnd.Time().IsZero() && s.ScoreStart.Time().After(s.ScoreEnd.Time()):
return errors.New("end is before start")
case s.LookbackDays > 14:
return errors.New("lookback days >14")
}
return nil
}
// stepClock is called by backfill during simulation operations.
2024-11-04 11:48:31 -05:00
func (s *Simulation) stepClock(t time.Time) {
2024-10-30 09:49:45 -04:00
now := s.clock.Now()
step := t.Sub(s.lastScore)
if step > time.Duration(s.SimInterval) {
s.clock += offsetClock(s.SimInterval)
2024-11-04 11:48:31 -05:00
s.scores = s.scorer.Score()
2024-10-30 09:49:45 -04:00
s.lastScore = now
}
}
// Simulate begins the simulation using the DB handle from ctx. It returns final scores.
2024-11-03 08:09:49 -05:00
func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.ID], error) {
2024-10-30 09:49:45 -04:00
now := time.Now()
2024-11-03 08:09:49 -05:00
tgc := talkgroups.NewCache()
2024-11-02 11:39:02 -04:00
2024-10-30 09:49:45 -04:00
s.Enable = true
2024-11-02 11:39:02 -04:00
s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter)
2024-10-30 09:49:45 -04:00
if time.Time(s.ScoreEnd).IsZero() {
2024-11-13 09:24:11 -05:00
s.ScoreEnd = jsontypes.Time(now)
2024-10-30 09:49:45 -04:00
}
log.Debug().Time("scoreStart", s.ScoreStart.Time()).
Time("scoreEnd", s.ScoreEnd.Time()).
Str("interval", s.SimInterval.String()).
Uint("lookbackDays", s.LookbackDays).
Msg("simulation start")
scoreEnd := time.Time(s.ScoreEnd)
// compute lookback start Time
sinceLookback := time.Time(scoreEnd).Add(-24 * time.Hour * time.Duration(s.LookbackDays))
// backfill from lookback start until score start
2024-10-31 16:50:08 -04:00
_, err := s.backfill(ctx, sinceLookback, time.Time(s.ScoreStart))
if err != nil {
2024-11-02 11:39:02 -04:00
return nil, fmt.Errorf("simulate backfill: %w", err)
2024-10-31 16:50:08 -04:00
}
2024-10-30 09:49:45 -04:00
// initial score
2024-11-04 11:48:31 -05:00
s.scores = s.scorer.Score()
2024-10-30 09:49:45 -04:00
s.lastScore = time.Time(s.ScoreStart)
ssT := time.Time(s.ScoreStart)
nowDiff := now.Sub(time.Time(ssT))
// and set the clock offset to it
s.clock -= offsetClock(nowDiff)
// turn on sim mode
s.alerter.sim = s
// compute time since score start until now
// backfill from scorestart until now. sim is enabled, so scoring will be done by stepClock()
2024-10-31 16:50:08 -04:00
_, err = s.backfill(ctx, time.Time(s.ScoreStart), scoreEnd)
if err != nil {
2024-11-02 11:39:02 -04:00
return nil, fmt.Errorf("simulate backfill final: %w", err)
2024-10-31 16:50:08 -04:00
}
2024-10-30 09:49:45 -04:00
s.lastScore = scoreEnd
sort.Sort(s.scores)
2024-11-02 11:39:02 -04:00
return s.scores, nil
2024-10-30 09:49:45 -04:00
}
// simulateHandler is the POST endpoint handler.
func (as *alerter) simulateHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
s := new(Simulation)
err := forms.Unmarshal(r, s, forms.WithAcceptBlank(), forms.WithParseLocalTime())
if err != nil {
err = fmt.Errorf("simulate unmarshal: %w", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
2024-10-30 09:49:45 -04:00
}
err = s.verify()
2024-10-30 09:49:45 -04:00
if err != nil {
err = fmt.Errorf("simulation profile verify: %w", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
2024-11-02 11:39:02 -04:00
_, err = s.Simulate(ctx)
if err != nil {
err = fmt.Errorf("simulate: %w", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
2024-10-30 09:49:45 -04:00
s.tgStatsHandler(w, r)
}