Реализуйте QueryReplicas. Она отправляет один и тот же запрос во все реплики параллельно. Как только первая реплика ответила успешно, остальные отменяются через ctx, а функция возвращает ответ победителя.
func QueryReplicas(ctx context.Context, replicas []func(ctx context.Context) (string, error)) (string, error)
Пример. Первая реплика отвечает за 3 секунды, вторая за 50 мс, третья падает с ошибкой сразу:
got, err := QueryReplicas(ctx, replicas)
// got == "fast", err == nil, время ~50 мс
// первая реплика получила отмену через свой ctx и вернулась досрочно
Требования:
- Из
ctx создаётся производный отменяемый контекст, и он передаётся всем репликам.
- Реплики запускаются параллельно.
- Первый успех (
err == nil) выигрывает: его значение возвращается сразу, остальные отменяются.
- Ошибка одной реплики не повод сдаваться. Если же ошибкой закончились все реплики, возвращается ошибка (в эталоне последняя полученная).
- Пустой список реплик это ошибка, а не пустой успех.
- Не должно оставаться горутин, застрявших на отправке результата.
Реплики сами отвечают за реакцию на ctx.Done(): отмена не останавливает функцию принудительно, она лишь сообщает, что результат больше не нужен.
Тесты: победитель по скорости среди двух медленных реплик, которые должны увидеть отмену; быстрый отказ одной реплики не мешает успеху другой; пустой список даёт ошибку; когда все реплики падают, возвращается ошибка одной из них; после возврата нет застрявших на отправке горутин, даже если проигравшие реплики заканчиваются успехом уже после отмены.