mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-11-04 08:04:10 +00:00
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.
108 lines
2.5 KiB
Go
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
|
|
}
|