Генератор отдаёт ссылки, и их нужно обойти HTTP-запросами. Одновременно работают
не больше 3 воркеров. Исходный main такой, и менять его нежелательно:
// пример ссылок от генератора:
// http://svc.example/?delay=100&tag=a
// http://svc.example/?delay=1500&tag=b
// http://svc.example/?delay=500&tag=c
func main() {
pool := newPool(3)
for link := range genLinks(5) {
log.Printf("dispatch %s", link)
pool.Submit(link)
}
}
Подвох в том, что main ни на что не ждёт: программа завершится раньше, чем
воркеры что-то скачают. Поэтому без метода ожидания не обойтись. В отличие от
пула с очередью и ErrQueueFull, здесь нужен backpressure: если все воркеры
заняты, Submit ждёт.
type Result struct {
Link string
Status int // HTTP-код ответа; 0, если запрос не удался
Err error // ошибка транспорта или контекста; nil, если ответ получен (даже 500)
}
var ErrClosed = errors.New("pool is closed")
// NewPool запускает n воркеров (n < 1 считается как 1), которые делают GET
// по ссылкам через client с контекстом ctx.
func NewPool(ctx context.Context, n int, client *http.Client) *Pool
// Submit отдаёт ссылку воркеру. Блокируется, пока свободный воркер её не возьмёт.
// Если ctx отменён — возвращает ctx.Err(); после Wait — ErrClosed.
func (p *Pool) Submit(link string) error
// Wait прекращает приём ссылок, дожидается всех принятых и возвращает результаты
// (по одному на каждую принятую ссылку, порядок любой).
func (p *Pool) Wait() []Result
Submit и Wait вызываются из одной горутины, как в main.
Требования
- Одновременно выполняется не больше
n запросов, и пул действительно
параллелен: если ссылок хватает, работают все n.
- Очереди нет: пока все
n воркеров заняты, Submit блокируется.
- На каждую принятую ссылку ровно один
Result. Ответ с любым кодом — это
Status и Err == nil. Ошибка запроса — Status == 0 и Err != nil.
- Тело каждого ответа вычитывается и закрывается.
- При отмене
ctx текущие запросы прерываются (запрос создаётся с этим ctx),
заблокированный Submit возвращает ctx.Err(), а Wait быстро возвращается.
- После
Wait горутин пула не остаётся. Код проходит go test -race.
Пример
pool := NewPool(ctx, 3, http.DefaultClient)
for link := range genLinks(5) {
if err := pool.Submit(link); err != nil {
break
}
}
for _, r := range pool.Wait() { // ≈1.5 с: дольше всего ждём ссылку b
log.Println(r.Link, r.Status, r.Err)
}
Устно: зачем закрывать и вычитывать resp.Body? Что будет с соединениями,
если этого не делать? Почему http.DefaultClient опасен в проде (нет
таймаута)? Чем пул с блокирующим Submit отличается от пула с ограниченной
очередью и отказом?