Files
graphql-monitoring-proxy/rps_tracker.go
T
lukaszraczylo 10d3946f1e fix(rps): stop dropping requests recorded during a sample
The sampler read the counter and then reset it in two steps. Requests recorded between the steps were lost. The sampler now swaps the counter to zero in one atomic step.
2026-11-03 00:00:24 +00:00

108 lines
2.5 KiB
Go

package main
import (
"context"
"sync/atomic"
"time"
)
// RPSTracker tracks requests per second using periodic sampling
type RPSTracker struct {
lastCount atomic.Int64
lastSampleTime atomic.Int64 // Unix nano
currentRPS atomic.Uint64 // centirps (RPS * 100)
ctx context.Context
cancel context.CancelFunc
}
// NewRPSTracker creates a new RPS tracker with context for graceful shutdown
func NewRPSTracker(ctx context.Context) *RPSTracker {
trackerCtx, cancel := context.WithCancel(ctx)
tracker := &RPSTracker{
ctx: trackerCtx,
cancel: cancel,
}
tracker.lastSampleTime.Store(time.Now().UnixNano())
go tracker.updateLoop()
return tracker
}
// RecordRequest increments the request counter
func (r *RPSTracker) RecordRequest() {
// Just increment the counter, sampling happens in background
r.lastCount.Add(1)
}
// updateLoop periodically calculates current RPS
func (r *RPSTracker) updateLoop() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-r.ctx.Done():
return
case <-ticker.C:
r.sample()
}
}
}
// Shutdown stops the RPS tracker
func (r *RPSTracker) Shutdown() {
if r.cancel != nil {
r.cancel()
}
}
// sample calculates RPS since last sample
func (r *RPSTracker) sample() {
r.sampleCount()
}
// sampleCount performs one sample and returns the requests it consumed.
func (r *RPSTracker) sampleCount() int64 {
nowNano := time.Now().UnixNano()
lastSampleNano := r.lastSampleTime.Load()
if lastSampleNano == 0 {
r.lastSampleTime.Store(nowNano)
return 0
}
// Swap, not Load+Store(0): requests recorded between the two would be lost.
currentCount := r.lastCount.Swap(0)
elapsed := float64(nowNano-lastSampleNano) / float64(time.Second)
if elapsed > 0 {
rps := float64(currentCount) / elapsed
// Store RPS as centirps for precision (multiply by 100)
r.currentRPS.Store(uint64(rps * 100))
}
r.lastSampleTime.Store(nowNano)
return currentCount
}
// GetCurrentRPS returns the current requests per second
func (r *RPSTracker) GetCurrentRPS() float64 {
centirps := r.currentRPS.Load()
return float64(centirps) / 100.0
}
var globalRPSTracker *RPSTracker
// InitializeRPSTracker initializes the global RPS tracker with context for graceful shutdown
func InitializeRPSTracker(ctx context.Context) *RPSTracker {
if globalRPSTracker == nil {
globalRPSTracker = NewRPSTracker(ctx)
}
return globalRPSTracker
}
// GetRPSTracker returns the global RPS tracker
func GetRPSTracker() *RPSTracker {
return globalRPSTracker
}