mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
fix: restart wipes stats history — LoadAudit replayed only the last 3000 lines (92% of which are 3s access events) and fed access rows into aggregates; now replays every real request into totals/quota windows, skips event rows, tolerates >64KB lines; +TestLoadAuditFullReplay
This commit is contained in:
@ -135,21 +135,30 @@ func incStatus(a *Stat, name string, r Req) {
|
||||
func (s *Stats) LoadAudit(path string) {
|
||||
f, err := os.Open(path)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var recs []Req
|
||||
// tolerate long error summaries / oversized junk lines
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 64*1024), 16*1024*1024)
|
||||
for sc.Scan() {
|
||||
var r Req
|
||||
if json.Unmarshal(sc.Bytes(), &r) == nil {
|
||||
recs = append(recs, r)
|
||||
if json.Unmarshal(sc.Bytes(), &r) != nil || r.Type == "" {
|
||||
continue // access/event rows (obj) and malformed lines are no requests
|
||||
}
|
||||
// replay EVERY request row into the aggregates so totals and
|
||||
// quota windows survive restarts; only the ring buffer view stays
|
||||
// capped at maxRecs.
|
||||
s.aggregateLocked(r)
|
||||
recs = append(recs, r)
|
||||
}
|
||||
_ = f.Close()
|
||||
if len(recs) > s.maxRecs {
|
||||
recs = recs[len(recs)-s.maxRecs:]
|
||||
}
|
||||
for _, r := range recs {
|
||||
s.Record(r)
|
||||
if n := len(recs); n > s.maxRecs {
|
||||
recs = append([]Req(nil), recs[n-s.maxRecs:]...)
|
||||
}
|
||||
s.recs = recs
|
||||
s.auditPath = path
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.auditPath = path
|
||||
@ -160,6 +169,20 @@ func (s *Stats) LoadAudit(path string) {
|
||||
func (s *Stats) Record(r Req) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.aggregateLocked(r)
|
||||
s.recs = append(s.recs, r)
|
||||
if len(s.recs) > s.maxRecs {
|
||||
s.recs = s.recs[len(s.recs)-s.maxRecs:]
|
||||
}
|
||||
if s.auditPath != "" {
|
||||
s.rotateAuditLocked()
|
||||
appendAuditLine(s.auditPath, r)
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateLocked folds r into every aggregate row and the quota window
|
||||
// bucket. Caller must hold s.mu.
|
||||
func (s *Stats) aggregateLocked(r Req) {
|
||||
inc(s.byKey, r.Key, r)
|
||||
if r.Model != "" {
|
||||
inc(s.byModel, r.Model, r)
|
||||
@ -180,13 +203,12 @@ func (s *Stats) Record(r Req) {
|
||||
}
|
||||
inc(ks, r.Source, r)
|
||||
if r.Status > 0 {
|
||||
name := strconv.Itoa(r.Status)
|
||||
a := s.byStatus[r.Status]
|
||||
if a == nil {
|
||||
a = &Stat{}
|
||||
s.byStatus[r.Status] = a
|
||||
}
|
||||
incStatus(a, name, r)
|
||||
incStatus(a, strconv.Itoa(r.Status), r)
|
||||
}
|
||||
// window bucket for quota enforcement (per source-model pair, per unix hour)
|
||||
tok := r.Prompt + r.Compl
|
||||
@ -210,14 +232,6 @@ func (s *Stats) Record(r Req) {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.recs = append(s.recs, r)
|
||||
if len(s.recs) > s.maxRecs {
|
||||
s.recs = s.recs[len(s.recs)-s.maxRecs:]
|
||||
}
|
||||
if s.auditPath != "" {
|
||||
s.rotateAuditLocked()
|
||||
appendAuditLine(s.auditPath, r)
|
||||
}
|
||||
}
|
||||
|
||||
// rotateAuditLocked renames the audit file to <path>.<unix>.old once it
|
||||
|
||||
Reference in New Issue
Block a user