package upstreamimpl import ( "context" "sync" "time" ) const tibiotSimDataMarginInterval = 650 * time.Millisecond // tibiotSimDataMarginLimiter spaces requests globally because Tibiot applies // the simDataMargin quota to the gateway's public IP, not to a channel. var tibiotSimDataMarginLimiter = newTibiotRateLimiter(tibiotSimDataMarginInterval) type tibiotRateLimiter struct { mu sync.Mutex interval time.Duration next time.Time } func newTibiotRateLimiter(interval time.Duration) *tibiotRateLimiter { if interval <= 0 { interval = tibiotSimDataMarginInterval } return &tibiotRateLimiter{interval: interval} } // wait blocks until a slot is available and only consumes the slot when the // caller's context is still active. Waiting callers do not reserve future // slots, so canceled requests cannot delay later retries. func (l *tibiotRateLimiter) wait(ctx context.Context) error { if l == nil { return nil } if ctx == nil { ctx = context.Background() } for { if err := ctx.Err(); err != nil { return err } l.mu.Lock() now := time.Now() if !l.next.After(now) { // Check again while holding the mutex so a cancellation racing with // the slot boundary cannot consume a slot unnecessarily. if err := ctx.Err(); err != nil { l.mu.Unlock() return err } l.next = now.Add(l.interval) l.mu.Unlock() return nil } wakeAt := l.next l.mu.Unlock() timer := time.NewTimer(time.Until(wakeAt)) select { case <-timer.C: case <-ctx.Done(): timer.Stop() return ctx.Err() } } }