| 148 | } |
| 149 | |
| 150 | func (l *attemptLimiter) allow(key string) bool { |
| 151 | if strings.TrimSpace(key) == "" { |
| 152 | key = "unknown" |
| 153 | } |
| 154 | now := time.Now() |
| 155 | cutoff := now.Add(-l.window) |
| 156 | |
| 157 | l.mu.Lock() |
| 158 | defer l.mu.Unlock() |
| 159 | |
| 160 | history := l.hits[key][:0] |
| 161 | for _, ts := range l.hits[key] { |
| 162 | if ts.After(cutoff) { |
| 163 | history = append(history, ts) |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | // Exponential backoff between consecutive attempts. The window-based |
| 168 | // cap below is the absolute ceiling; the per-attempt backoff makes |
| 169 | // even the first few failures cost real time. |
| 170 | if n := len(history); n > 0 { |
| 171 | if wait := backoffDelay(n); now.Sub(history[n-1]) < wait { |
| 172 | l.hits[key] = history |
| 173 | return false |
| 174 | } |
| 175 | } |
| 176 | if len(history) >= l.maxHits { |
| 177 | l.hits[key] = history |
| 178 | return false |
| 179 | } |
| 180 | history = append(history, now) |
| 181 | l.hits[key] = history |
| 182 | return true |
| 183 | } |
| 184 | |
| 185 | // backoffDelay returns the minimum time the caller must wait before the |
| 186 | // (consecutiveFailures+1)-th attempt is allowed: 0, 1, 2, 4, 8, 16, 32, |