Внешний API разрешает не больше rps запросов в секунду. Нужно опросить его для списка пользователей, параллельно, но так, чтобы суммарный темп вызовов на весь пул не превышал лимит.
Конвейер из двух стадий. Первая отдаёт userIDs в канал. Вторая, workers воркеров, берёт id из канала и вызывает callAPI(id). Перед каждым вызовом воркер получает разрешение от одного общего на весь пул лимитера, который выдаёт его раз в time.Second/rps.
// RunRateLimited вызывает callAPI для каждого id из userIDs ровно один раз
// и возвращает результаты в любом порядке.
func RunRateLimited(userIDs []int, workers, rps int, callAPI func(id int) string) []string
Пять id, два воркера, лимит 10 запросов в секунду, то есть одно разрешение раз в 100 мс:
start := time.Now()
res := RunRateLimited([]int{1, 2, 3, 4, 5}, 2, 10, func(id int) string {
return fmt.Sprintf("user-%d", id)
})
sort.Strings(res)
fmt.Println(res)
fmt.Println(time.Since(start) >= 450*time.Millisecond)
// [user-1 user-2 user-3 user-4 user-5]
// true
Результатов пять, порядок в срезе не важен. Пять разрешений по 100 мс не получить быстрее, чем примерно за полсекунды.
- Каждый id из
userIDs обрабатывается ровно один раз, в ответе ровно len(userIDs) строк.
- Лимитер один на весь пул. Лимитер на каждого воркера нарушит условие: суммарный темп вырастет в
workers раз.
- До
workers вызовов callAPI могут идти одновременно. Лимитер ограничивает темп стартов, а не число параллельных вызовов.
- Нижняя граница по времени:
N вызовов при rps не заканчиваются быстрее, чем примерно за N/rps секунд. Тест с 40 вызовами и 20 rps проверяет, что прошло не меньше секунды, и что в любом окне в одну секунду стартов не больше rps + 5.
- Лимитер пишите сами, на
time.Ticker, без сторонних пакетов.
workers < 1 и rps < 1 считайте единицей. Код проходит go test -race.
Остальные тесты проверяют, что каждый id уходит в callAPI ровно один раз, что медленные вызовы перекрываются (лимитер не сериализует callAPI, но одновременных вызовов не больше workers) и что пустой вход, workers = 0 и rps = 0 не подвешивают конвейер.
Это учебная модель: тикер выдаёт разрешения с заданным темпом, и этого достаточно для тестов. Настоящий лимит с гарантией на каждое скользящее окно строят на token bucket, о нём рассказано в разборе.
callAPI рано или поздно возвращается. Результаты собирайте без гонок и дождитесь всех воркеров перед return.