tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
async_work.h
1#pragma once
2
3#include <tess/core/assert.h>
4#include <tess/core/fail_fast.h>
5#include <tess/diagnostics/diagnostics.h>
6
7#include <cstddef>
8#include <cstdint>
9#include <limits>
10#include <source_location>
11#include <type_traits>
12#include <utility>
13#include <vector>
14
15namespace tess {
16
19 std::uint32_t index = 0;
20 std::uint64_t generation = 0;
21
22 friend constexpr bool operator==(AsyncTicket lhs,
23 AsyncTicket rhs) noexcept = default;
24};
25
27struct AsyncVersion {
28 std::uint64_t value = 0;
29
30 constexpr AsyncVersion() noexcept = default;
31 constexpr AsyncVersion(std::uint64_t version) noexcept : value(version) {}
32
33 friend constexpr bool operator==(AsyncVersion lhs,
34 AsyncVersion rhs) noexcept = default;
35};
36
38enum class AsyncResultState : std::uint8_t {
39 Unbound,
40 Immediate,
41 Pending,
42 Ready,
43 Failed,
44 Cancelled,
45 Superseded,
46 Stale,
47};
48
51 std::uint32_t max_items = 0;
52};
53
55enum class AsyncStepState : std::uint8_t {
56 Pending,
57 Ready,
58 Failed,
59 Stale,
60};
61
64 AsyncStepState state = AsyncStepState::Pending;
65 std::uint32_t items_done = 0;
66 AsyncVersion result_version{};
67};
68
79 std::uint32_t invoked = 0;
80 std::uint32_t items_done = 0;
81 std::uint32_t pending = 0;
82 std::uint32_t ready = 0;
83 std::uint32_t failed = 0;
84 std::uint32_t stale = 0;
85};
86
106template <typename T>
107class ResumableWorkQueue {
108 static_assert(std::is_default_constructible_v<T>,
109 "ResumableWorkQueue<T> requires T to be default "
110 "constructible");
111
112 public:
113 using WorkFn = AsyncWorkStep (*)(void*, AsyncWorkBudget, T&);
114
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;
121
129 if (!slots_.empty()) {
130 detail::fail_fast(
131 "ResumableWorkQueue::set_flow_accounting requires an empty queue");
132 }
133 accounting_ = accounting;
134 }
135
143 void observe_flow_tick(std::uint64_t tick) noexcept {
144 if (reject_reentrant_mutation()) {
145 return;
146 }
147 if (accounting_ == nullptr) {
148 return;
149 }
150 accounting_->observe_tick(tick);
151 const auto now = accounting_->last_observed_tick;
152 auto oldest = now;
153 auto any_pending = false;
154 for (const auto& slot : slots_) {
155 if (slot.state == AsyncResultState::Pending) {
156 any_pending = true;
157 oldest = slot.submitted_tick < oldest ? slot.submitted_tick : oldest;
158 }
159 }
160 accounting_->counters.oldest_outstanding_age_ticks =
161 any_pending ? now - oldest : 0;
162 }
163
164 void reserve_tickets(std::size_t count) {
165 if (reject_reentrant_mutation()) {
166 return;
167 }
168 slots_.reserve(count);
169 }
170
171 template <typename Work>
172 [[nodiscard]] auto submit(
173 Work& work, AsyncVersion required_version = {},
174 std::source_location source = std::source_location::current())
175 -> AsyncTicket {
176 static_assert(
177 std::is_invocable_r_v<AsyncWorkStep, Work&, AsyncWorkBudget, T&>,
178 "ResumableWorkQueue::submit requires Work(AsyncWorkBudget, T&) to "
179 "return AsyncWorkStep");
180 return submit(
181 static_cast<void*>(&work),
182 [](void* context, AsyncWorkBudget budget, T& value) -> AsyncWorkStep {
183 return (*static_cast<Work*>(context))(budget, value);
184 },
185 required_version, source);
186 }
187
188 [[nodiscard]] auto submit(
189 void* context, WorkFn work, AsyncVersion required_version = {},
190 std::source_location source = std::source_location::current())
191 -> AsyncTicket {
192 if (reject_reentrant_mutation()) {
193 account_rejected_offer();
194 return {};
195 }
196 if (work == nullptr) {
197 detail::fail_fast("ResumableWorkQueue::submit received a null callback");
198 }
199 if (slots_.size() > std::numeric_limits<std::uint32_t>::max()) {
200 detail::fail_fast(
201 "ResumableWorkQueue::submit exhausted the AsyncTicket index space");
202 }
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;
207 slot.work = work;
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;
215 }
216 return AsyncTicket{index, generation_};
217 }
218
219 [[nodiscard]] auto submit_immediate(
220 T value, AsyncVersion result_version = {},
221 std::source_location source = std::source_location::current())
222 -> AsyncTicket {
223 if (reject_reentrant_mutation()) {
224 account_rejected_offer();
225 return {};
226 }
227 if (slots_.size() > std::numeric_limits<std::uint32_t>::max()) {
228 detail::fail_fast(
229 "ResumableWorkQueue::submit_immediate exhausted the AsyncTicket "
230 "index space");
231 }
232 const auto index = static_cast<std::uint32_t>(slots_.size());
233 // Build the slot before storing it so a throwing value move admits
234 // nothing and leaves no Unbound slot behind.
235 auto slot = Slot{};
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;
243 ++counters.offered;
244 ++counters.admitted;
245 ++counters.completed;
246 }
247 return AsyncTicket{index, generation_};
248 }
249
250 [[nodiscard]] auto advance(AsyncWorkBudget budget) -> AsyncAdvanceStats {
251 if (in_advance_) {
252 detail::fail_fast(
253 "ResumableWorkQueue::advance rejected mutation during advance");
254 }
255 struct AdvanceGuard {
256 bool& active;
257 // Exception propagation is intentional. This guard restores the
258 // reentrancy sentinel while leaving the throwing slot Pending so the
259 // caller may inspect state and choose whether to retry or fail it.
260 ~AdvanceGuard() { active = false; }
261 };
262 in_advance_ = true;
263 const auto guard = AdvanceGuard{in_advance_};
264 auto stats = AsyncAdvanceStats{};
265 if (accounting_ != nullptr) {
266 accounting_->counters.offered_work_units += budget.max_items;
267 }
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) {
272 break;
273 }
274 if (slot.state != AsyncResultState::Pending) {
275 continue;
276 }
277 TESS_ASSERT(slot.work != nullptr);
278 const auto step =
279 slot.work(slot.context, AsyncWorkBudget{remaining}, slot.value);
280 ++stats.invoked;
281 --invocations_remaining;
282 if (step.items_done > remaining) {
283 slot.state = AsyncResultState::Failed;
284 account_terminal(slot);
285 continue;
286 }
287 remaining -= step.items_done;
288 if (accounting_ != nullptr) {
289 // Committed per step: a later callback's exception must not
290 // discard work already consumed in this advance.
291 accounting_->counters.consumed_work_units += step.items_done;
292 }
293 slot.result_version = step.result_version;
294 switch (step.state) {
295 case AsyncStepState::Pending:
296 break;
297 case AsyncStepState::Ready:
298 slot.state = AsyncResultState::Ready;
299 account_terminal(slot);
300 break;
301 case AsyncStepState::Failed:
302 slot.state = AsyncResultState::Failed;
303 account_terminal(slot);
304 break;
305 case AsyncStepState::Stale:
306 slot.state = AsyncResultState::Stale;
307 account_terminal(slot);
308 break;
309 }
310 stats.items_done += step.items_done;
311 }
312 summarize_states(stats);
313 return stats;
314 }
315
316 [[nodiscard]] auto state(AsyncTicket ticket) const noexcept
317 -> AsyncResultState {
318 const auto* slot = find(ticket);
319 return slot == nullptr ? AsyncResultState::Unbound : slot->state;
320 }
321
329 [[nodiscard]] auto result(AsyncTicket ticket) const noexcept -> const T* {
330 const auto* slot = find(ticket);
331 if (slot == nullptr || (slot->state != AsyncResultState::Immediate &&
332 slot->state != AsyncResultState::Ready)) {
333 return nullptr;
334 }
335 return &slot->value;
336 }
337
338 [[nodiscard]] auto required_version(AsyncTicket ticket) const noexcept
339 -> AsyncVersion {
340 const auto* slot = find(ticket);
341 return slot == nullptr ? AsyncVersion{} : slot->required_version;
342 }
343
344 [[nodiscard]] auto result_version(AsyncTicket ticket) const noexcept
345 -> AsyncVersion {
346 const auto* slot = find(ticket);
347 return slot == nullptr ? AsyncVersion{} : slot->result_version;
348 }
349
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;
354 }
355
373 [[nodiscard]] bool cancel(AsyncTicket ticket) noexcept {
374 return set_terminal(ticket, AsyncResultState::Cancelled);
375 }
376
378 [[nodiscard]] bool supersede(AsyncTicket ticket) noexcept {
379 return set_terminal(ticket, AsyncResultState::Superseded);
380 }
381
383 [[nodiscard]] bool fail(AsyncTicket ticket) noexcept {
384 return set_terminal(ticket, AsyncResultState::Failed);
385 }
386
388 [[nodiscard]] bool mark_stale(AsyncTicket ticket) noexcept {
389 return set_terminal(ticket, AsyncResultState::Stale);
390 }
391
407 [[nodiscard]] bool mark_stale_if_version(AsyncTicket ticket,
408 AsyncVersion current) noexcept {
409 if (reject_reentrant_mutation()) {
410 return false;
411 }
412 auto* slot = find(ticket);
413 if (slot == nullptr ||
414 (slot->state != AsyncResultState::Immediate &&
415 slot->state != AsyncResultState::Ready) ||
416 slot->result_version == current) {
417 return false;
418 }
419 slot->state = AsyncResultState::Stale;
420 if (accounting_ != nullptr) {
421 // Reclassification, not a second terminal outcome: the result
422 // completed earlier and its residence was already recorded, so
423 // only the buckets swap. `completed` is non-monotonic here by
424 // documented design.
425 auto& counters = accounting_->counters;
426 if (counters.completed > 0) {
427 --counters.completed;
428 }
429 ++counters.stale;
430 }
431 return true;
432 }
433
434 [[nodiscard]] auto size() const noexcept -> std::size_t {
435 return slots_.size();
436 }
437
438 [[nodiscard]] auto generation() const noexcept -> std::uint64_t {
439 return generation_;
440 }
441
442 void clear() noexcept {
443 if (reject_reentrant_mutation()) {
444 return;
445 }
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);
451 }
452 }
453 }
454 slots_.clear();
455 ++generation_;
456 if (generation_ == 0) {
457 ++generation_;
458 }
459 }
460
461 private:
462 struct Slot {
463 T value{};
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;
471 };
472
473 [[nodiscard]] auto reject_reentrant_mutation() const noexcept -> bool {
474 if (!in_advance_) {
475 return false;
476 }
477 detail::fail_fast("ResumableWorkQueue rejected mutation during advance");
478 }
479
480 void account_rejected_offer() noexcept {
481 if (accounting_ != nullptr) {
482 ++accounting_->counters.offered;
483 ++accounting_->counters.rejected;
484 }
485 }
486
487 [[nodiscard]] auto find(AsyncTicket ticket) noexcept -> Slot* {
488 if (ticket.generation != generation_ || ticket.index >= slots_.size()) {
489 return nullptr;
490 }
491 return &slots_[ticket.index];
492 }
493
494 [[nodiscard]] auto find(AsyncTicket ticket) const noexcept -> const Slot* {
495 if (ticket.generation != generation_ || ticket.index >= slots_.size()) {
496 return nullptr;
497 }
498 return &slots_[ticket.index];
499 }
500
501 [[nodiscard]] bool set_terminal(AsyncTicket ticket,
502 AsyncResultState state) noexcept {
503 if (reject_reentrant_mutation()) {
504 return false;
505 }
506 auto* slot = find(ticket);
507 if (slot == nullptr || slot->state != AsyncResultState::Pending) {
508 return false;
509 }
510 slot->state = state;
511 account_terminal(*slot);
512 return true;
513 }
514
517 void account_terminal(Slot& slot) noexcept {
518 if (accounting_ == nullptr) {
519 return;
520 }
521 auto& counters = accounting_->counters;
522 switch (slot.state) {
523 case AsyncResultState::Ready:
524 ++counters.completed;
525 break;
526 case AsyncResultState::Failed:
527 ++counters.failed;
528 break;
529 case AsyncResultState::Cancelled:
530 ++counters.cancelled;
531 break;
532 case AsyncResultState::Superseded:
533 ++counters.superseded;
534 break;
535 case AsyncResultState::Stale:
536 ++counters.stale;
537 break;
538 case AsyncResultState::Unbound:
539 case AsyncResultState::Immediate:
540 case AsyncResultState::Pending:
541 return;
542 }
543 close_outstanding(slot);
544 }
545
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;
550 }
551
552 void summarize_states(AsyncAdvanceStats& stats) const noexcept {
553 for (const auto& slot : slots_) {
554 switch (slot.state) {
555 case AsyncResultState::Pending:
556 ++stats.pending;
557 break;
558 case AsyncResultState::Immediate:
559 case AsyncResultState::Ready:
560 ++stats.ready;
561 break;
562 case AsyncResultState::Failed:
563 case AsyncResultState::Cancelled:
564 case AsyncResultState::Superseded:
565 ++stats.failed;
566 break;
567 case AsyncResultState::Stale:
568 ++stats.stale;
569 break;
570 case AsyncResultState::Unbound:
571 break;
572 }
573 }
574 }
575
576 std::vector<Slot> slots_;
577 std::uint64_t generation_ = 1;
578 bool in_advance_ = false;
579 diagnostics::FlowAccounting* accounting_ = nullptr;
580};
581
582} // namespace tess
auto result(AsyncTicket ticket) const noexcept -> const T *
Definition async_work.h:329
bool cancel(AsyncTicket ticket) noexcept
Definition async_work.h:373
bool fail(AsyncTicket ticket) noexcept
Moves a Pending ticket to Failed; see cancel for false.
Definition async_work.h:383
bool mark_stale_if_version(AsyncTicket ticket, AsyncVersion current) noexcept
Definition async_work.h:407
void observe_flow_tick(std::uint64_t tick) noexcept
Definition async_work.h:143
bool mark_stale(AsyncTicket ticket) noexcept
Moves a Pending ticket to Stale; see cancel for false.
Definition async_work.h:388
void set_flow_accounting(diagnostics::FlowAccounting *accounting) noexcept
Definition async_work.h:128
bool supersede(AsyncTicket ticket) noexcept
Moves a Pending ticket to Superseded; see cancel for false.
Definition async_work.h:378
Definition async_work.h:78
Generation-stamped handle for one cooperative asynchronous result.
Definition async_work.h:18
Caller-defined version stamp attached to requirements and results.
Definition async_work.h:27
Deterministic item allowance shared by one queue advance.
Definition async_work.h:50
Reports progress and the version produced by one continuation step.
Definition async_work.h:63
Definition diagnostics.h:505
Definition version.h:21