Files

288 lines
7.5 KiB
Go

// Package supply holds bounded, per-channel telemetry independent of card volume.
package supply
import (
"sort"
"sync"
"time"
)
const SchemaVersion = 1
type Wait struct {
Count int64 `json:"count"`
TotalMS int64 `json:"totalMs"`
LongestMS int64 `json:"longestMs"`
}
type Channel struct {
Key string `json:"chanKey"`
FirstQueryMS *int64 `json:"firstQueryMs"`
FirstEnqueueMS *int64 `json:"firstEnqueueMs"`
Startup Wait `json:"startupWait"`
Confirmed Wait `json:"confirmedWait"`
Unconfirmed Wait `json:"unconfirmedIdle"`
Consumer Wait `json:"consumerStartupWait"`
UnknownMS int64 `json:"unknownMs"`
EmptyClaims int64 `json:"emptyClaims"`
ValidSamples int64 `json:"validSamples"`
MissingSamples int64 `json:"missingSamples"`
Stages map[string]int64 `json:"producerStageMs"`
Completeness string `json:"completeness"`
Termination string `json:"termination,omitempty"`
CoverageStart time.Time `json:"coverageStart"`
CoverageEnd time.Time `json:"coverageEnd"`
}
type Summary struct {
Targets int `json:"targetChannels"`
Valid int `json:"validChannels"`
Confirmed int `json:"confirmedChannels"`
Unknown int `json:"unknownChannels"`
UnknownMS int64 `json:"unknownChannelMs"`
UnconfirmedMS int64 `json:"unconfirmedChannelMs"`
ConsumerMS int64 `json:"consumerStartupChannelMs"`
TotalMS int64 `json:"confirmedChannelMs"`
LongestMS int64 `json:"longestMs"`
Worst string `json:"worstChannel,omitempty"`
}
type Snapshot struct {
SchemaVersion int `json:"schemaVersion"`
Attempt string `json:"attempt"`
ResolutionMS int `json:"resolutionMs"`
Partial bool `json:"partial"`
Truncated bool `json:"truncated"`
Summary Summary `json:"summary"`
Channels []Channel `json:"channels"`
}
type View struct {
Version uint64
Pending int
Processing int
Stage string
Ended bool
}
type Sample struct {
Ready int64
Leases int64
Future int64
Eligible bool
Known bool
}
type state struct {
Channel
View
last time.Time
kind string
continuous int64
open bool
}
type Observer struct {
mu sync.Mutex
started time.Time
attempt string
partial bool
retained Summary
truncated bool
channels map[string]*state
}
func New(attempt string, started time.Time, keys []string) *Observer {
o := &Observer{attempt: attempt, started: started, channels: make(map[string]*state, len(keys))}
for _, k := range keys {
o.channels[k] = &state{Channel: Channel{Key: k, Stages: map[string]int64{}, CoverageStart: started}, View: View{Stage: "initializing"}}
}
return o
}
func (o *Observer) Event(key, stage string, pending int, added bool, ended bool, now time.Time) {
o.mu.Lock()
defer o.mu.Unlock()
s := o.channels[key]
if s == nil {
return
}
s.Version++
s.Stage = stage
s.Pending = pending
if stage == "sql_query" && s.FirstQueryMS == nil {
v := now.Sub(o.started).Milliseconds()
s.FirstQueryMS = &v
}
if added && s.FirstEnqueueMS == nil {
v := now.Sub(o.started).Milliseconds()
s.FirstEnqueueMS = &v
s.close()
}
if ended {
s.Ended = true
s.Termination = stage
s.CoverageEnd = now
s.close()
}
}
func (o *Observer) Empty(key string) {
o.mu.Lock()
defer o.mu.Unlock()
if s := o.channels[key]; s != nil && !s.Ended {
s.EmptyClaims++
}
}
func (o *Observer) Processing(key string, delta int) {
o.mu.Lock()
defer o.mu.Unlock()
if s := o.channels[key]; s != nil {
s.Version++
s.Processing = max(0, s.Processing+delta)
if delta > 0 {
s.close()
}
}
}
func (o *Observer) Views() map[string]View {
o.mu.Lock()
defer o.mu.Unlock()
out := make(map[string]View, len(o.channels))
for k, s := range o.channels {
out[k] = s.View
}
return out
}
func (s *state) close() { s.kind = ""; s.continuous = 0; s.open = false; s.last = time.Time{} }
func (o *Observer) Partial() { o.mu.Lock(); o.partial = true; o.mu.Unlock() }
func (o *Observer) Apply(key string, before View, sample Sample, now time.Time) {
o.mu.Lock()
defer o.mu.Unlock()
s := o.channels[key]
if s == nil || s.Ended {
return
}
kind := "unknown"
if sample.Known && before.Version == s.Version {
s.ValidSamples++
switch {
case !sample.Eligible:
s.close()
s.CoverageEnd = now
return
case sample.Ready > 0:
if s.Processing == 0 && sample.Leases == 0 {
kind = "consumer"
} else {
kind = "stock"
}
case s.Processing > 0 || sample.Leases > 0:
kind = "processing"
case sample.Future > 0:
kind = "scheduled"
case s.Pending > 0:
if s.FirstEnqueueMS == nil {
kind = "startup"
} else {
kind = "confirmed"
}
default:
kind = "unconfirmed"
}
} else {
s.MissingSamples++
o.partial = true
}
elapsed := now.Sub(s.last)
if !s.last.IsZero() && elapsed > 2*time.Second {
s.MissingSamples += int64(elapsed/time.Second) - 1
o.partial = true
s.close()
}
if s.kind == kind && !s.last.IsZero() {
ms := elapsed.Milliseconds()
if ms < 0 {
ms = 0
}
s.continuous += ms
var w *Wait
switch kind {
case "startup":
w = &s.Startup
case "confirmed":
w = &s.Confirmed
case "unconfirmed":
w = &s.Unconfirmed
case "consumer":
w = &s.Consumer
case "unknown":
s.UnknownMS += ms
}
if w != nil {
if !s.open {
w.Count++
s.open = true
}
w.TotalMS += ms
if s.continuous > w.LongestMS {
w.LongestMS = s.continuous
}
if kind == "startup" || kind == "confirmed" {
s.Stages[s.Stage] += ms
}
}
} else {
s.continuous = 0
s.open = false
}
s.kind = kind
s.last = now
s.CoverageEnd = now
}
func (o *Observer) Snapshot() Snapshot {
o.mu.Lock()
defer o.mu.Unlock()
out := Snapshot{SchemaVersion: 1, Attempt: o.attempt, ResolutionMS: 1000, Partial: o.partial, Truncated: o.truncated, Channels: make([]Channel, 0, len(o.channels))}
out.Summary.Targets = len(o.channels)
for _, s := range o.channels {
c := s.Channel
c.Completeness = "observing"
if s.Ended {
c.Completeness = "complete"
}
if c.MissingSamples > 0 || c.UnknownMS > 0 || c.ValidSamples == 0 || c.Termination == "cancelled" {
c.Completeness = "partial"
}
c.Stages = make(map[string]int64, len(s.Stages))
for k, v := range s.Stages {
c.Stages[k] = v
}
out.Channels = append(out.Channels, c)
if c.ValidSamples == 0 {
out.Partial = true
}
out.Summary.UnknownMS += c.UnknownMS
out.Summary.UnconfirmedMS += c.Unconfirmed.TotalMS
out.Summary.ConsumerMS += c.Consumer.TotalMS
if c.ValidSamples > 0 {
out.Summary.Valid++
}
total := c.Startup.TotalMS + c.Confirmed.TotalMS
longest := max(c.Startup.LongestMS, c.Confirmed.LongestMS)
if total > 0 {
out.Summary.Confirmed++
}
if c.MissingSamples > 0 || c.UnknownMS > 0 || c.ValidSamples == 0 {
out.Summary.Unknown++
}
out.Summary.TotalMS += total
if longest > out.Summary.LongestMS {
out.Summary.LongestMS = longest
out.Summary.Worst = c.Key
}
}
out.Summary.Confirmed = min(out.Summary.Targets, out.Summary.Confirmed+o.retained.Confirmed)
out.Summary.TotalMS += o.retained.TotalMS
out.Summary.UnknownMS += o.retained.UnknownMS
out.Summary.UnconfirmedMS += o.retained.UnconfirmedMS
out.Summary.ConsumerMS += o.retained.ConsumerMS
if o.retained.LongestMS > out.Summary.LongestMS {
out.Summary.LongestMS = o.retained.LongestMS
out.Summary.Worst = o.retained.Worst
}
sort.Slice(out.Channels, func(i, j int) bool { return out.Channels[i].Key < out.Channels[j].Key })
return out
}