maxseen.go 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. // maxseen.go: a tiny thread-safe per-key max-value tracker used
  2. // by M6 to expose `ba_ingestd_dedupe_count_max_observed`.
  3. //
  4. // Why not a Prometheus Counter? Counters are monotonic and
  5. // cannot decrease. A Gauge of "max ever observed" needs to
  6. // only tick upward, but Prometheus Gauges can be Set() to
  7. // any value, so we must guard against races where a smaller
  8. // observation arrives after a larger one (e.g. process restart
  9. // reads from a fresh in-memory state). The tracker stores the
  10. // last-known max in a sync.Map[source]uint32, and only calls
  11. // the export callback when the new value is strictly higher.
  12. //
  13. // The tracker is intentionally allocation-free on the hot path:
  14. // a sync.Map.Load is one atomic, the comparison is one int,
  15. // and on equality we do nothing (no callback fires, no metric
  16. // write).
  17. package observability
  18. import "sync"
  19. // MaxSeen is a per-key monotonic max tracker. Safe for
  20. // concurrent use from any number of goroutines.
  21. type MaxSeen struct {
  22. m sync.Map // map[string]uint32
  23. }
  24. // NewMaxSeen returns an empty tracker.
  25. func NewMaxSeen() *MaxSeen { return &MaxSeen{} }
  26. // RecordAndExport updates the max for `key` to `val` and
  27. // invokes export(key, newVal) if and only if val strictly
  28. // exceeds the previously observed max. If no previous value
  29. // exists, val is the new max and export fires with val.
  30. //
  31. // export may be nil; in that case RecordAndExport is a
  32. // write-only op useful in tests that don't care about
  33. // the Prometheus side-effect.
  34. func (m *MaxSeen) RecordAndExport(key string, val uint32, export func(key string, val float64)) {
  35. cur, loaded := m.m.Load(key)
  36. for {
  37. if !loaded {
  38. // First observation for this key. CAS the slot.
  39. if _, loaded := m.m.LoadOrStore(key, val); !loaded {
  40. if export != nil {
  41. export(key, float64(val))
  42. }
  43. return
  44. }
  45. // Someone else just installed the slot; reload
  46. // and re-enter the loop.
  47. cur, loaded = m.m.Load(key)
  48. continue
  49. }
  50. prev := cur.(uint32)
  51. if val <= prev {
  52. return
  53. }
  54. // Try to swap. Use CompareAndSwap to handle the
  55. // race where two goroutines both see the same prev
  56. // and try to install a new max. Loser re-reads and
  57. // loops; that's the path to convergence.
  58. if m.m.CompareAndSwap(key, prev, val) {
  59. if export != nil {
  60. export(key, float64(val))
  61. }
  62. return
  63. }
  64. // CAS lost; reload and retry.
  65. cur, loaded = m.m.Load(key)
  66. }
  67. }
  68. // Get returns the current max for `key`, or 0 if unseen.
  69. // Useful in tests.
  70. func (m *MaxSeen) Get(key string) uint32 {
  71. if v, ok := m.m.Load(key); ok {
  72. return v.(uint32)
  73. }
  74. return 0
  75. }