Реализуйте ограничитель «дырявое ведро». Запросы приходят как угодно, пачками и
вразнобой, а проходят дальше строго по одному раз в interval. Лишние ждут.
От token bucket это отличается тем, что Acquire() здесь блокирующий: он не
отказывает, а ставит горутину в очередь. Любой всплеск превращается в ровный поток.
type LeakyBucket struct {
// ...
}
// NewLeakyBucket создаёт ведро, пропускающее 1 запрос раз в interval.
func NewLeakyBucket(interval time.Duration) *LeakyBucket
// Acquire блокирует вызывающую горутину до её очереди.
func (lb *LeakyBucket) Acquire()
// Stop останавливает внутренний таймер.
func (lb *LeakyBucket) Stop()
lb := NewLeakyBucket(10 * time.Millisecond)
defer lb.Stop()
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
lb.Acquire()
}()
}
wg.Wait()
fmt.Println(time.Since(start)) // порядка 100ms, а не мгновенно
Десять горутин стартуют одновременно, но пропускаются по одной: первая примерно
через 10 мс, последняя примерно через 100 мс.
- 10 одновременных
Acquire при interval = 10ms все возвращаются (таймаут
теста 10 секунд) и вместе занимают не меньше 70 мс.
- 5
Acquire подряд в одной горутине занимают не меньше 30 мс.
- После простоя ведро не копит разрешения. Если оно простояло три интервала,
два следующих
Acquire всё равно разделены почти целым интервалом
(не меньше 3/4 interval).
- Освобождения идут ровно раз в
interval: между соседними не меньше 0,6 interval, а десять Acquire при 20 мс занимают не больше 600 мс.
Stop() после Acquire не паникует и не блокируется, а после него внутренняя горутина завершается.
- Всё идёт под детектором гонок (
-race).
interval положителен. Сначала дождитесь завершения всех Acquire, затем
вызовите Stop один раз. После Stop новых Acquire быть не должно, а уже
заблокированные эта задача отменять не требует.
На что смотрит интервьюер: понимаете ли вы разницу между leaky и token
bucket, откуда берётся темп (кто «открывает ворота»), и не остаётся ли после
Stop() живая горутина.