58#include <userver/compiler/impl/lifetime.hpp>
59#include <userver/engine/awaitable.hpp>
60#include <userver/engine/task/cancel.hpp>
61#include <userver/engine/wait_any.hpp>
62#include <userver/utils/assert.hpp>
63#include <userver/utils/async.hpp>
64#include <userver/utils/datetime.hpp>
66USERVER_NAMESPACE_BEGIN
68namespace utils::hedging {
80template <
typename RequestStrategy>
83 typename std::invoke_result_t<
decltype(&RequestStrategy::Create), RequestStrategy,
int>::value_type;
85 typename std::invoke_result_t<
decltype(&RequestStrategy::ExtractReply), RequestStrategy>::value_type;
91using TimePoint = Clock::time_point;
93enum class Action { kStartTry, kStop };
96 PlanEntry(TimePoint timepoint, std::size_t request_index, std::size_t attempt_id, Action action)
97 : timepoint(timepoint),
98 request_index(request_index),
99 attempt_id(attempt_id),
103 auto operator<=>(
const PlanEntry& other)
const noexcept =
default;
106 std::size_t request_index{0};
107 std::size_t attempt_id{0};
113template <
typename RequestStrategy>
114struct SubrequestWrapper {
115 using RequestType =
typename RequestTraits<RequestStrategy>::RequestType;
117 SubrequestWrapper() =
default;
118 SubrequestWrapper(SubrequestWrapper&&)
noexcept =
default;
119 explicit SubrequestWrapper(std::optional<RequestType>&& request)
120 : request(std::move(request))
124 engine::AwaitableToken GetAwaitableToken() USERVER_IMPL_LIFETIME_BOUND {
126 return engine
::AwaitableToken
{};
128 return request->GetAwaitableToken();
131 std::optional<RequestType> request;
135 std::vector<std::size_t> subrequest_indices;
136 std::size_t attempts_made = 0;
137 bool finished =
false;
140template <
typename RequestStrategy>
142 using RequestType =
typename RequestTraits<RequestStrategy>::RequestType;
143 using ReplyType =
typename RequestTraits<RequestStrategy>::ReplyType;
146 : inputs_(std::move(inputs)),
147 settings_(std::move(settings))
149 const std::size_t size =
this->inputs_.size();
150 request_states_.resize(size);
152 Context(Context&&)
noexcept =
default;
154 void Prepare(TimePoint start_time) {
155 const auto request_count = GetRequestsCount();
156 for (std::size_t request_id = 0; request_id < request_count; ++request_id) {
157 plan_.emplace(start_time, request_id, 0, Action::kStartTry);
159 plan_.emplace(start_time + settings_
.timeout_all, 0, 0, Action::kStop);
160 subrequests_.reserve(settings_
.max_attempts * request_count);
163 std::optional<TimePoint> NextEventTime()
const {
167 return plan_.top().timepoint;
169 std::optional<PlanEntry> PopPlan() {
173 auto ret = plan_.top();
177 bool IsStop()
const {
return stop_; }
179 void FinishAllSubrequests(std::size_t request_index) {
180 auto& request_state = request_states_[request_index];
181 request_state.finished =
true;
182 const auto& subrequest_indices = request_state.subrequest_indices;
183 auto& strategy = inputs_[request_index];
184 for (
auto i : subrequest_indices) {
185 auto& request = subrequests_[i].request;
187 strategy.Finish(std::move(*request));
195 size_t GetRequestsCount()
const {
return inputs_.size(); }
197 size_t GetRequestIdxBySubrequestIdx(size_t subrequest_idx)
const {
198 return input_by_subrequests_.at(subrequest_idx);
201 RequestStrategy& GetStrategy(size_t index) {
return inputs_[index]; }
203 auto& GetSubRequests() {
return subrequests_; }
205 std::vector<std::optional<ReplyType>> ExtractAllReplies() {
206 std::vector<std::optional<ReplyType>> ret;
207 ret.reserve(GetRequestsCount());
208 for (
auto&& strategy : inputs_) {
209 ret.emplace_back(strategy.ExtractReply());
218 void OnActionStop() {
219 for (std::size_t i = 0; i < inputs_.size(); ++i) {
220 FinishAllSubrequests(i);
227 void OnActionStartTry(std::size_t request_index, std::size_t attempt_id, TimePoint now) {
228 auto& request_state = request_states_[request_index];
229 if (request_state.finished) {
232 auto& attempts_made = request_state.attempts_made;
238 if (attempt_id < attempts_made) {
245 auto& strategy = inputs_[request_index];
246 auto request_opt = strategy.Create(attempts_made);
248 request_state.finished =
true;
253 const auto idx = subrequests_.size();
254 subrequests_.emplace_back(std::move(request_opt));
255 request_state.subrequest_indices.push_back(idx);
256 input_by_subrequests_[idx] = request_index;
258 plan_.emplace(now + settings_
.hedging_delay, request_index, attempts_made, Action::kStartTry);
262 void OnRetriableReply(std::size_t request_idx, std::chrono::milliseconds retry_delay, TimePoint now) {
263 const auto& request_state = request_states_[request_idx];
264 if (request_state.finished) {
271 plan_.emplace(now + retry_delay, request_idx, request_state.attempts_made, Action::kStartTry);
274 void OnNonRetriableReply(std::size_t request_idx) { FinishAllSubrequests(request_idx); }
279 std::vector<RequestStrategy> inputs_;
283 std::priority_queue<PlanEntry, std::vector<PlanEntry>, std::greater<>> plan_{};
284 std::vector<SubrequestWrapper<RequestStrategy>> subrequests_{};
286 std::unordered_map<std::size_t, std::size_t> input_by_subrequests_{};
287 std::vector<RequestState> request_states_{};
294template <
typename RequestStrategy>
296 using RequestType =
typename RequestTraits<RequestStrategy>::RequestType;
297 using ReplyType =
typename RequestTraits<RequestStrategy>::ReplyType;
300 ~HedgedRequestBulkFuture() { task_.SyncCancel(); }
306 std::vector<std::optional<ReplyType>>
Get() {
return task_.Get(); }
309 engine::AwaitableToken
GetAwaitableToken() USERVER_IMPL_LIFETIME_BOUND {
return task_.GetAwaitableToken(); }
312 template <
typename TRequestStrategy>
313 friend auto HedgeRequestsBulkAsync(std::vector<TRequestStrategy> inputs,
HedgingSettings settings);
314 using Task = engine::
TaskWithResult<std::vector<std::optional<ReplyType>>>;
315 HedgedRequestBulkFuture(Task&& task)
316 : task_(std::move(task))
322template <
typename RequestStrategy>
324 using RequestType =
typename RequestTraits<RequestStrategy>::RequestType;
325 using ReplyType =
typename RequestTraits<RequestStrategy>::ReplyType;
328 ~HedgedRequestFuture() { task_.SyncCancel(); }
334 std::optional<ReplyType>
Get() {
return task_.Get(); }
336 void IgnoreResult() {}
339 engine::AwaitableToken
GetAwaitableToken() USERVER_IMPL_LIFETIME_BOUND {
return task_.GetAwaitableToken(); }
342 template <
typename TRequestStrategy>
343 friend auto HedgeRequestAsync(TRequestStrategy input,
HedgingSettings settings);
345 HedgedRequestFuture(Task&& task)
346 : task_(std::move(task))
358template <
typename RequestStrategy>
361 using Action = impl::Action;
362 using Clock = impl::Clock;
363 auto context = impl::Context(std::move(inputs), std::move(hedging_settings));
365 auto& sub_requests = context.GetSubRequests();
367 auto wakeup_time = Clock::now();
368 context.Prepare(wakeup_time);
370 while (!context.IsStop()) {
371 auto wait_result = engine::WaitAnyUntil(wakeup_time, sub_requests);
372 if (!wait_result.has_value()) {
374 context.OnActionStop();
378 auto plan_entry = context.PopPlan();
379 if (!plan_entry.has_value()) {
383 const auto [timestamp, request_index, attempt_id, action] = *plan_entry;
385 case Action::kStartTry:
386 context.OnActionStartTry(request_index, attempt_id, timestamp);
389 context.OnActionStop();
392 auto next_wakeup_time = context.NextEventTime();
393 if (!next_wakeup_time.has_value()) {
396 wakeup_time = *next_wakeup_time;
399 const auto result_idx = *wait_result;
400 UASSERT(result_idx < sub_requests.size());
401 const auto request_idx = context.GetRequestIdxBySubrequestIdx(result_idx);
402 auto& strategy = context.GetStrategy(request_idx);
404 auto& request = sub_requests[result_idx].request;
405 UASSERT_MSG(request,
"Finished requests must not be empty");
406 auto reply = strategy.ProcessReply(std::move(*request));
407 if (reply.has_value()) {
410 context.OnRetriableReply(request_idx, *reply, Clock::now());
412 wakeup_time = *context.NextEventTime();
414 context.OnNonRetriableReply(request_idx);
417 return context.ExtractAllReplies();
428template <
typename RequestStrategy>
431 "hedged-bulk-request",
432 [inputs{std::move(inputs)}, settings{std::move(settings)}]()
mutable {
433 return HedgeRequestsBulk(std::move(inputs), std::move(settings));
442template <
typename RequestStrategy>
444 RequestStrategy input,
447 std::vector<RequestStrategy> inputs;
448 inputs.emplace_back(std::move(input));
449 auto bulk_ret = HedgeRequestsBulk(std::move(inputs), std::move(settings));
450 if (bulk_ret.size() != 1) {
460template <
typename RequestStrategy>
464 [input{std::move(input)}, settings{std::move(settings)}]()
mutable {
465 return HedgeRequest(std::move(input), std::move(settings));