107class ResumableWorkQueue {
108 static_assert(std::is_default_constructible_v<T>,
109 "ResumableWorkQueue<T> requires T to be default "
115 ResumableWorkQueue() =
default;
116 ResumableWorkQueue(
const ResumableWorkQueue&) =
delete;
117 auto operator=(
const ResumableWorkQueue&) -> ResumableWorkQueue& =
delete;
118 ResumableWorkQueue(ResumableWorkQueue&&) =
delete;
119 auto operator=(ResumableWorkQueue&&) -> ResumableWorkQueue& =
delete;
120 ~ResumableWorkQueue() =
default;
129 if (!slots_.empty()) {
131 "ResumableWorkQueue::set_flow_accounting requires an empty queue");
133 accounting_ = accounting;
144 if (reject_reentrant_mutation()) {
147 if (accounting_ ==
nullptr) {
150 accounting_->observe_tick(tick);
151 const auto now = accounting_->last_observed_tick;
153 auto any_pending =
false;
154 for (
const auto& slot : slots_) {
155 if (slot.state == AsyncResultState::Pending) {
157 oldest = slot.submitted_tick < oldest ? slot.submitted_tick : oldest;
160 accounting_->counters.oldest_outstanding_age_ticks =
161 any_pending ? now - oldest : 0;
164 void reserve_tickets(std::size_t count) {
165 if (reject_reentrant_mutation()) {
168 slots_.reserve(count);
171 template <
typename Work>
172 [[nodiscard]]
auto submit(
173 Work& work, AsyncVersion required_version = {},
174 std::source_location source = std::source_location::current())
177 std::is_invocable_r_v<AsyncWorkStep, Work&, AsyncWorkBudget, T&>,
178 "ResumableWorkQueue::submit requires Work(AsyncWorkBudget, T&) to "
179 "return AsyncWorkStep");
181 static_cast<void*
>(&work),
182 [](
void* context, AsyncWorkBudget budget, T& value) -> AsyncWorkStep {
183 return (*
static_cast<Work*
>(context))(budget, value);
185 required_version, source);
188 [[nodiscard]]
auto submit(
189 void* context, WorkFn work, AsyncVersion required_version = {},
190 std::source_location source = std::source_location::current())
192 if (reject_reentrant_mutation()) {
193 account_rejected_offer();
196 if (work ==
nullptr) {
197 detail::fail_fast(
"ResumableWorkQueue::submit received a null callback");
199 if (slots_.size() > std::numeric_limits<std::uint32_t>::max()) {
201 "ResumableWorkQueue::submit exhausted the AsyncTicket index space");
203 const auto index =
static_cast<std::uint32_t
>(slots_.size());
204 slots_.push_back(Slot{});
205 auto& slot = slots_.back();
206 slot.context = context;
208 slot.required_version = required_version;
209 slot.source = source;
210 slot.state = AsyncResultState::Pending;
211 if (accounting_ !=
nullptr) {
212 ++accounting_->counters.offered;
213 accounting_->record_admitted();
214 slot.submitted_tick = accounting_->last_observed_tick;
216 return AsyncTicket{index, generation_};
219 [[nodiscard]]
auto submit_immediate(
220 T value, AsyncVersion result_version = {},
221 std::source_location source = std::source_location::current())
223 if (reject_reentrant_mutation()) {
224 account_rejected_offer();
227 if (slots_.size() > std::numeric_limits<std::uint32_t>::max()) {
229 "ResumableWorkQueue::submit_immediate exhausted the AsyncTicket "
232 const auto index =
static_cast<std::uint32_t
>(slots_.size());
236 slot.value = std::move(value);
237 slot.result_version = result_version;
238 slot.source = source;
239 slot.state = AsyncResultState::Immediate;
240 slots_.push_back(std::move(slot));
241 if (accounting_ !=
nullptr) {
242 auto& counters = accounting_->counters;
245 ++counters.completed;
247 return AsyncTicket{index, generation_};
250 [[nodiscard]]
auto advance(AsyncWorkBudget budget) -> AsyncAdvanceStats {
253 "ResumableWorkQueue::advance rejected mutation during advance");
255 struct AdvanceGuard {
260 ~AdvanceGuard() { active =
false; }
263 const auto guard = AdvanceGuard{in_advance_};
264 auto stats = AsyncAdvanceStats{};
265 if (accounting_ !=
nullptr) {
266 accounting_->counters.offered_work_units += budget.max_items;
268 auto remaining = budget.max_items;
269 auto invocations_remaining = budget.max_items;
270 for (
auto& slot : slots_) {
271 if (remaining == 0 || invocations_remaining == 0) {
274 if (slot.state != AsyncResultState::Pending) {
277 TESS_ASSERT(slot.work !=
nullptr);
279 slot.work(slot.context, AsyncWorkBudget{remaining}, slot.value);
281 --invocations_remaining;
282 if (step.items_done > remaining) {
283 slot.state = AsyncResultState::Failed;
284 account_terminal(slot);
287 remaining -= step.items_done;
288 if (accounting_ !=
nullptr) {
291 accounting_->counters.consumed_work_units += step.items_done;
293 slot.result_version = step.result_version;
294 switch (step.state) {
295 case AsyncStepState::Pending:
297 case AsyncStepState::Ready:
298 slot.state = AsyncResultState::Ready;
299 account_terminal(slot);
301 case AsyncStepState::Failed:
302 slot.state = AsyncResultState::Failed;
303 account_terminal(slot);
305 case AsyncStepState::Stale:
306 slot.state = AsyncResultState::Stale;
307 account_terminal(slot);
310 stats.items_done += step.items_done;
312 summarize_states(stats);
316 [[nodiscard]]
auto state(AsyncTicket ticket)
const noexcept
317 -> AsyncResultState {
318 const auto* slot = find(ticket);
319 return slot ==
nullptr ? AsyncResultState::Unbound : slot->state;
330 const auto* slot = find(ticket);
331 if (slot ==
nullptr || (slot->state != AsyncResultState::Immediate &&
332 slot->state != AsyncResultState::Ready)) {
338 [[nodiscard]]
auto required_version(
AsyncTicket ticket)
const noexcept
340 const auto* slot = find(ticket);
341 return slot ==
nullptr ?
AsyncVersion{} : slot->required_version;
344 [[nodiscard]]
auto result_version(AsyncTicket ticket)
const noexcept
346 const auto* slot = find(ticket);
347 return slot ==
nullptr ? AsyncVersion{} : slot->result_version;
350 [[nodiscard]]
auto source(AsyncTicket ticket)
const noexcept
351 -> std::source_location {
352 const auto* slot = find(ticket);
353 return slot ==
nullptr ? std::source_location{} : slot->source;
374 return set_terminal(ticket, AsyncResultState::Cancelled);
379 return set_terminal(ticket, AsyncResultState::Superseded);
384 return set_terminal(ticket, AsyncResultState::Failed);
389 return set_terminal(ticket, AsyncResultState::Stale);
409 if (reject_reentrant_mutation()) {
412 auto* slot = find(ticket);
413 if (slot ==
nullptr ||
414 (slot->state != AsyncResultState::Immediate &&
415 slot->state != AsyncResultState::Ready) ||
416 slot->result_version == current) {
419 slot->state = AsyncResultState::Stale;
420 if (accounting_ !=
nullptr) {
425 auto& counters = accounting_->counters;
426 if (counters.completed > 0) {
427 --counters.completed;
434 [[nodiscard]]
auto size() const noexcept -> std::
size_t {
435 return slots_.size();
438 [[nodiscard]]
auto generation() const noexcept -> std::uint64_t {
442 void clear() noexcept {
443 if (reject_reentrant_mutation()) {
446 if (accounting_ !=
nullptr) {
447 for (
auto& slot : slots_) {
448 if (slot.state == AsyncResultState::Pending) {
449 ++accounting_->counters.dropped_after_admission;
450 close_outstanding(slot);
456 if (generation_ == 0) {
464 void* context =
nullptr;
465 WorkFn work =
nullptr;
466 AsyncVersion required_version{};
467 AsyncVersion result_version{};
468 std::source_location source = std::source_location::current();
469 AsyncResultState state = AsyncResultState::Unbound;
470 std::uint64_t submitted_tick = 0;
473 [[nodiscard]]
auto reject_reentrant_mutation() const noexcept ->
bool {
477 detail::fail_fast(
"ResumableWorkQueue rejected mutation during advance");
480 void account_rejected_offer() noexcept {
481 if (accounting_ !=
nullptr) {
482 ++accounting_->counters.offered;
483 ++accounting_->counters.rejected;
487 [[nodiscard]]
auto find(AsyncTicket ticket)
noexcept -> Slot* {
488 if (ticket.generation != generation_ || ticket.index >= slots_.size()) {
491 return &slots_[ticket.index];
494 [[nodiscard]]
auto find(AsyncTicket ticket)
const noexcept ->
const Slot* {
495 if (ticket.generation != generation_ || ticket.index >= slots_.size()) {
498 return &slots_[ticket.index];
501 [[nodiscard]]
bool set_terminal(AsyncTicket ticket,
502 AsyncResultState state)
noexcept {
503 if (reject_reentrant_mutation()) {
506 auto* slot = find(ticket);
507 if (slot ==
nullptr || slot->state != AsyncResultState::Pending) {
511 account_terminal(*slot);
517 void account_terminal(Slot& slot)
noexcept {
518 if (accounting_ ==
nullptr) {
521 auto& counters = accounting_->counters;
522 switch (slot.state) {
523 case AsyncResultState::Ready:
524 ++counters.completed;
526 case AsyncResultState::Failed:
529 case AsyncResultState::Cancelled:
530 ++counters.cancelled;
532 case AsyncResultState::Superseded:
533 ++counters.superseded;
535 case AsyncResultState::Stale:
538 case AsyncResultState::Unbound:
539 case AsyncResultState::Immediate:
540 case AsyncResultState::Pending:
543 close_outstanding(slot);
546 void close_outstanding(
const Slot& slot)
noexcept {
547 accounting_->record_left_outstanding();
548 accounting_->counters.residence_ticks_accumulated +=
549 accounting_->last_observed_tick - slot.submitted_tick;
552 void summarize_states(AsyncAdvanceStats& stats)
const noexcept {
553 for (
const auto& slot : slots_) {
554 switch (slot.state) {
555 case AsyncResultState::Pending:
558 case AsyncResultState::Immediate:
559 case AsyncResultState::Ready:
562 case AsyncResultState::Failed:
563 case AsyncResultState::Cancelled:
564 case AsyncResultState::Superseded:
567 case AsyncResultState::Stale:
570 case AsyncResultState::Unbound:
576 std::vector<Slot> slots_;
577 std::uint64_t generation_ = 1;
578 bool in_advance_ =
false;
579 diagnostics::FlowAccounting* accounting_ =
nullptr;