tess 1.0.0
Performance-first tile and path simulation substrate
Loading...
Searching...
No Matches
registered_maintenance.h
1#pragma once
2
3#include <tess/core/fail_fast.h>
4#include <tess/experimental/maintenance.h>
5
6#include <atomic>
7#include <concepts>
8#include <cstddef>
9#include <cstdint>
10#include <limits>
11#include <memory>
12#include <mutex>
13#include <optional>
14#include <shared_mutex>
15#include <type_traits>
16
17namespace tess::experimental::maintenance {
18
30enum class ScheduleResult : std::uint8_t {
31 Accepted,
32 CapacityExhausted,
33 Stalled,
34};
35
43enum class BackendDrainResult : std::uint8_t {
44 Completed,
45 Stalled,
46};
47
49enum class DrainResult : std::uint8_t {
50 Idle,
51 Drained,
52 BudgetExhausted,
53 Stalled,
54};
55
57enum class ReleaseResult : std::uint8_t {
58 Released,
59 InvalidHandle,
60 NotIdle,
61};
62
86template <typename Backend>
88 std::constructible_from<Backend, std::size_t> &&
89 requires(Backend& backend, const Backend& const_backend,
90 MaintenanceTask& task, MaintenanceBudget budget) {
91 { backend.schedule(task) } -> std::same_as<ScheduleResult>;
92 { backend.run_some(budget) } -> std::same_as<BackendDrainResult>;
93 { backend.flush() } -> std::same_as<BackendDrainResult>;
94 { const_backend.metrics() } noexcept -> std::same_as<MaintenanceMetrics>;
95 { const_backend.has_pending() } noexcept -> std::same_as<bool>;
96 };
97
108template <typename Backend>
111 requires(Backend& backend, MaintenanceTask& task) {
112 { backend.register_task(task) } noexcept -> std::same_as<bool>;
113 { backend.seal() } noexcept -> std::same_as<void>;
114 };
115
117class MaintenanceHandle {
118 public:
119 constexpr MaintenanceHandle() noexcept = default;
120
121 friend constexpr auto operator==(MaintenanceHandle,
122 MaintenanceHandle) noexcept
123 -> bool = default;
124
125 private:
126 template <typename Backend>
128
129 constexpr MaintenanceHandle(std::uint64_t owner_epoch, std::size_t slot,
130 std::uint64_t slot_generation) noexcept
131 : owner_epoch_(owner_epoch),
132 slot_(slot),
133 slot_generation_(slot_generation) {}
134
135 std::uint64_t owner_epoch_ = 0;
136 std::size_t slot_ = std::numeric_limits<std::size_t>::max();
137 std::uint64_t slot_generation_ = 0;
138};
139
140namespace detail {
141
142inline std::atomic<std::uint64_t> next_owner_epoch = 1;
143// Reuse the handle owner identity for reentrancy checks. Keeping only the
144// nonzero epoch avoids retaining an object address beyond the lexical guard.
145inline thread_local std::uint64_t active_registered_scheduler_epoch = 0;
146
147[[nodiscard]] inline auto claim_owner_epoch() noexcept -> std::uint64_t {
148 const auto epoch = next_owner_epoch.fetch_add(1, std::memory_order_relaxed);
149 if (epoch == 0 || epoch == std::numeric_limits<std::uint64_t>::max()) {
150 ::tess::detail::fail_fast(
151 "RegisteredScheduler exhausted maintenance owner epochs");
152 }
153 return epoch;
154}
155
156template <typename Backend>
157inline constexpr bool is_immediate_v =
158 std::is_same_v<Backend, ImmediateScheduler>;
159
160template <typename Backend>
161inline constexpr bool is_dirty_bit_v =
162 std::is_same_v<Backend, DirtyBitScheduler>;
163
164template <typename Backend>
165inline constexpr bool is_raw_backend_v =
166 is_immediate_v<Backend> || is_dirty_bit_v<Backend> ||
167 std::is_same_v<Backend, FifoScheduler> ||
168 std::is_same_v<Backend, CoalescingScheduler>;
169
170} // namespace detail
171
198template <typename Backend>
199class RegisteredScheduler final {
200 static_assert(MaintenanceBackend<Backend> ||
201 detail::is_raw_backend_v<Backend>,
202 "RegisteredScheduler requires a MaintenanceBackend or a "
203 "built-in experimental raw backend");
204
205 static constexpr bool has_backend_register = requires(
206 Backend& backend, MaintenanceTask& task) { backend.register_task(task); };
207 static constexpr bool has_backend_seal =
208 requires(Backend& backend) { backend.seal(); };
209 static_assert(
210 detail::is_dirty_bit_v<Backend> ||
211 ((!has_backend_register && !has_backend_seal) ||
213 "a custom maintenance backend must provide both no-throw "
214 "register_task(MaintenanceTask&) -> bool and seal() -> void hooks, or "
215 "neither");
216
217 struct SlotTask final : MaintenanceTask {
218 MaintenanceTask* target = nullptr;
219
220 void run(MaintenanceBudget& budget) override {
221 if (target == nullptr) {
222 ::tess::detail::fail_fast(
223 "RegisteredScheduler invoked a released maintenance slot");
224 }
225 target->run(budget);
226 }
227 };
228
229 struct Slot {
230 SlotTask task{};
231 std::uint64_t generation = 1;
232 };
233
234 class OperationGuard {
235 public:
236 explicit OperationGuard(RegisteredScheduler& scheduler)
237 : previous_(detail::active_registered_scheduler_epoch) {
238 if (previous_ == scheduler.owner_epoch_) {
239 return;
240 }
241 if (previous_ != 0) {
242 ::tess::detail::fail_fast(
243 "RegisteredScheduler nested cross-scheduler operation called "
244 "from a running task");
245 }
246 lock_ = std::shared_lock<std::shared_mutex>{scheduler.lifecycle_mutex_};
247 detail::active_registered_scheduler_epoch = scheduler.owner_epoch_;
248 outer_ = true;
249 }
250
251 OperationGuard(const OperationGuard&) = delete;
252 auto operator=(const OperationGuard&) -> OperationGuard& = delete;
253
254 ~OperationGuard() {
255 if (outer_) {
256 detail::active_registered_scheduler_epoch = previous_;
257 }
258 }
259
260 private:
261 std::uint64_t previous_ = 0;
262 std::shared_lock<std::shared_mutex> lock_{};
263 bool outer_ = false;
264 };
265
266 class ScheduleActivityGuard {
267 public:
268 explicit ScheduleActivityGuard(std::atomic<std::uint64_t>& in_flight,
269 std::mutex& observation_mutex)
270 : in_flight_(in_flight), observation_mutex_(observation_mutex) {
271 const auto lock = std::scoped_lock{observation_mutex_};
272 const auto previous = in_flight_.fetch_add(1, std::memory_order_acq_rel);
273 if (previous == std::numeric_limits<std::uint64_t>::max()) {
274 ::tess::detail::fail_fast(
275 "RegisteredScheduler exhausted in-flight schedule count");
276 }
277 }
278
279 ScheduleActivityGuard(const ScheduleActivityGuard&) = delete;
280 auto operator=(const ScheduleActivityGuard&)
281 -> ScheduleActivityGuard& = delete;
282
283 ~ScheduleActivityGuard() {
284 const auto lock = std::scoped_lock{observation_mutex_};
285 const auto previous = in_flight_.fetch_sub(1, std::memory_order_acq_rel);
286 if (previous == 0) {
287 ::tess::detail::fail_fast(
288 "RegisteredScheduler in-flight schedule count underflowed");
289 }
290 }
291
292 private:
293 std::atomic<std::uint64_t>& in_flight_;
294 std::mutex& observation_mutex_;
295 };
296
297 public:
298 explicit RegisteredScheduler(std::size_t capacity)
299 : RegisteredScheduler(capacity, capacity) {}
300
301 RegisteredScheduler(std::size_t registry_capacity,
302 std::size_t backend_capacity)
303 : owner_epoch_(detail::claim_owner_epoch()),
304 capacity_(registry_capacity),
305 slots_(std::make_unique<Slot[]>(registry_capacity)),
306 backend_(backend_capacity) {}
307
308 RegisteredScheduler(const RegisteredScheduler&) = delete;
309 auto operator=(const RegisteredScheduler&) -> RegisteredScheduler& = delete;
310 RegisteredScheduler(RegisteredScheduler&&) = delete;
311 auto operator=(RegisteredScheduler&&) -> RegisteredScheduler& = delete;
312
313 ~RegisteredScheduler() {
314 for (std::size_t index = 0; index < capacity_; ++index) {
315 auto* task = slots_[index].task.target;
316 if (task == nullptr) {
317 continue;
318 }
319 auto expected = owner_epoch_;
320 if (!task->registration_epoch_.compare_exchange_strong(
321 expected, 0, std::memory_order_relaxed)) {
322 ::tess::detail::fail_fast(
323 "RegisteredScheduler task ownership changed unexpectedly");
324 }
325 slots_[index].task.target = nullptr;
326 }
327 }
328
330 [[nodiscard]] auto register_task(MaintenanceTask& task)
331 -> std::optional<MaintenanceHandle> {
332 reject_reentrant_lifecycle("register_task");
333 const auto lock = std::unique_lock<std::shared_mutex>{lifecycle_mutex_};
334 if (sealed_) {
335 ::tess::detail::fail_fast(
336 "RegisteredScheduler::register_task called after seal");
337 }
338 for (std::size_t index = 0; index < capacity_; ++index) {
339 if (slots_[index].task.target == &task) {
340 return make_handle(index);
341 }
342 }
343 auto index = capacity_;
344 for (std::size_t candidate = 0; candidate < capacity_; ++candidate) {
345 if (slots_[candidate].task.target == nullptr) {
346 index = candidate;
347 break;
348 }
349 }
350 if (index == capacity_) {
351 return std::nullopt;
352 }
353 auto expected = std::uint64_t{0};
354 if (!task.registration_epoch_.compare_exchange_strong(
355 expected, owner_epoch_, std::memory_order_relaxed)) {
356 ::tess::detail::fail_fast(
357 "RegisteredScheduler::register_task task belongs to another "
358 "scheduler or registration epoch");
359 }
360 slots_[index].task.target = &task;
361 return make_handle(index);
362 }
363
365 void seal() {
366 reject_reentrant_lifecycle("seal");
367 const auto lock = std::unique_lock<std::shared_mutex>{lifecycle_mutex_};
368 if (sealed_) {
369 ::tess::detail::fail_fast("RegisteredScheduler::seal called twice");
370 }
371 if constexpr (FixedRegistrationBackend<Backend> ||
372 detail::is_dirty_bit_v<Backend>) {
373 for (std::size_t index = 0; index < capacity_; ++index) {
374 if (slots_[index].task.target != nullptr &&
375 !backend_.register_task(slots_[index].task)) {
376 ::tess::detail::fail_fast(
377 "RegisteredScheduler fixed backend registry capacity mismatch");
378 }
379 }
380 backend_.seal();
381 }
382 sealed_ = true;
383 }
384
386 [[nodiscard]] auto valid(MaintenanceHandle handle) const -> bool {
387 auto& self = const_cast<RegisteredScheduler&>(*this);
388 const auto operation = OperationGuard{self};
389 return self.resolve(handle) != nullptr;
390 }
391
393 [[nodiscard]] auto try_schedule(MaintenanceHandle handle)
394 -> std::optional<ScheduleResult> {
395 const auto operation = OperationGuard{*this};
396 auto* slot = resolve(handle);
397 if (slot == nullptr) {
398 return std::nullopt;
399 }
400 require_sealed("try_schedule");
401 const auto schedule_activity =
402 ScheduleActivityGuard{in_flight_schedules_, observation_mutex_};
403#if TESS_HAS_EXCEPTIONS
404 try {
405 const auto result = schedule_slot(*slot);
406 if (result != ScheduleResult::CapacityExhausted) {
407 invalidate_idle_observation();
408 }
409 return result;
410 } catch (...) {
411 invalidate_idle_observation();
412 throw;
413 }
414#else
415 const auto result = schedule_slot(*slot);
416 if (result != ScheduleResult::CapacityExhausted) {
417 invalidate_idle_observation();
418 }
419 return result;
420#endif
421 }
422
424 [[nodiscard]] auto schedule(MaintenanceHandle handle) -> ScheduleResult {
425 const auto result = try_schedule(handle);
426 if (!result.has_value()) {
427 ::tess::detail::fail_fast(
428 "RegisteredScheduler::schedule received a stale handle from the "
429 "wrong scheduler or registration epoch; use try_schedule for "
430 "expected uncertainty");
431 }
432 return *result;
433 }
434
436 [[nodiscard]] auto run_some(MaintenanceBudget budget) -> DrainResult {
437 reject_reentrant_drain("run_some");
438 const auto operation = OperationGuard{*this};
439 require_sealed("run_some");
440 const auto drain_lock = std::scoped_lock{drain_mutex_};
441 return observe_drain([&] { return run_backend_some(budget); });
442 }
443
445 [[nodiscard]] auto flush() -> DrainResult {
446 reject_reentrant_drain("flush");
447 const auto operation = OperationGuard{*this};
448 require_sealed("flush");
449 const auto drain_lock = std::scoped_lock{drain_mutex_};
450 return observe_drain([&] { return flush_backend(); });
451 }
452
457 [[nodiscard]] auto try_release(MaintenanceHandle handle) -> ReleaseResult {
458 reject_reentrant_lifecycle("try_release");
459 const auto lock = std::unique_lock<std::shared_mutex>{lifecycle_mutex_};
460 auto* slot = resolve(handle);
461 if (slot == nullptr) {
462 return ReleaseResult::InvalidHandle;
463 }
464 if (sealed_ && (idle_epoch_.load(std::memory_order_acquire) !=
465 activity_epoch_.load(std::memory_order_acquire) ||
466 backend_.has_pending())) {
467 return ReleaseResult::NotIdle;
468 }
469 auto* task = slot->task.target;
470 auto expected = owner_epoch_;
471 if (!task->registration_epoch_.compare_exchange_strong(
472 expected, 0, std::memory_order_relaxed)) {
473 ::tess::detail::fail_fast(
474 "RegisteredScheduler::try_release task ownership changed");
475 }
476 slot->task.target = nullptr;
477 ++slot->generation;
478 if (slot->generation == 0) {
479 ++slot->generation;
480 }
481 return ReleaseResult::Released;
482 }
483
486 switch (try_release(handle)) {
487 case ReleaseResult::Released:
488 return;
489 case ReleaseResult::InvalidHandle:
490 ::tess::detail::fail_fast(
491 "RegisteredScheduler::release received a stale maintenance "
492 "handle; use try_release for expected uncertainty");
493 case ReleaseResult::NotIdle:
494 ::tess::detail::fail_fast(
495 "RegisteredScheduler::release requires a positive Idle result "
496 "after the last successful schedule; release never cancels");
497 }
498 ::tess::detail::fail_fast("RegisteredScheduler::release invalid result");
499 }
500
502 [[nodiscard]] auto metrics() const noexcept -> MaintenanceMetrics {
503 return backend_.metrics();
504 }
505
506 private:
507 [[nodiscard]] auto make_handle(std::size_t index) const noexcept
509 return MaintenanceHandle{owner_epoch_, index, slots_[index].generation};
510 }
511
512 [[nodiscard]] auto resolve(MaintenanceHandle handle) noexcept -> Slot* {
513 if (handle.owner_epoch_ != owner_epoch_ || handle.slot_ >= capacity_) {
514 return nullptr;
515 }
516 auto& slot = slots_[handle.slot_];
517 if (slot.task.target == nullptr ||
518 slot.generation != handle.slot_generation_) {
519 return nullptr;
520 }
521 return &slot;
522 }
523
524 void require_sealed(const char* operation) const {
525 if (sealed_) {
526 return;
527 }
528 if (operation == nullptr) {
529 ::tess::detail::fail_fast("RegisteredScheduler used before seal");
530 }
531 ::tess::detail::fail_fast(
532 "RegisteredScheduler operation called before seal");
533 }
534
535 void reject_reentrant_lifecycle(const char* operation) const {
536 if (detail::active_registered_scheduler_epoch == 0) {
537 return;
538 }
539 static_cast<void>(operation);
540 ::tess::detail::fail_fast(
541 "RegisteredScheduler lifecycle mutation called from a running task");
542 }
543
544 void reject_reentrant_drain(const char* operation) const {
545 if (detail::active_registered_scheduler_epoch != owner_epoch_) {
546 return;
547 }
548 static_cast<void>(operation);
549 ::tess::detail::fail_fast(
550 "RegisteredScheduler drain called from a running task");
551 }
552
553 void invalidate_idle_observation() noexcept {
554 const auto previous =
555 activity_epoch_.fetch_add(1, std::memory_order_acq_rel);
556 if (previous == std::numeric_limits<std::uint64_t>::max()) {
557 ::tess::detail::fail_fast(
558 "RegisteredScheduler exhausted activity epochs");
559 }
560 idle_epoch_.store(std::numeric_limits<std::uint64_t>::max(),
561 std::memory_order_release);
562 }
563
564 [[nodiscard]] auto schedule_slot(Slot& slot) -> ScheduleResult {
565 if constexpr (MaintenanceBackend<Backend>) {
566 return backend_.schedule(slot.task);
567 } else {
568 const auto accepted = backend_.schedule(slot.task);
569 if (accepted) {
570 return ScheduleResult::Accepted;
571 }
572 if constexpr (detail::is_immediate_v<Backend>) {
573 return ScheduleResult::Stalled;
574 }
575 if constexpr (detail::is_dirty_bit_v<Backend>) {
576 ::tess::detail::fail_fast(
577 "RegisteredScheduler dirty-bit backend rejected a live sealed "
578 "registration");
579 }
580 return ScheduleResult::CapacityExhausted;
581 }
582 }
583
584 [[nodiscard]] auto run_backend_some(MaintenanceBudget budget)
585 -> BackendDrainResult {
586 if constexpr (MaintenanceBackend<Backend>) {
587 return backend_.run_some(budget);
588 } else {
589 return backend_.run_some(budget) ? BackendDrainResult::Completed
590 : BackendDrainResult::Stalled;
591 }
592 }
593
594 [[nodiscard]] auto flush_backend() -> BackendDrainResult {
595 if constexpr (MaintenanceBackend<Backend>) {
596 return backend_.flush();
597 } else {
598 return backend_.flush() ? BackendDrainResult::Completed
599 : BackendDrainResult::Stalled;
600 }
601 }
602
603 template <typename Drain>
604 [[nodiscard]] auto observe_drain(Drain&& drain) -> DrainResult {
605 idle_epoch_.store(std::numeric_limits<std::uint64_t>::max(),
606 std::memory_order_release);
607 const auto activity_before =
608 activity_epoch_.load(std::memory_order_acquire);
609 const auto in_flight_before =
610 in_flight_schedules_.load(std::memory_order_acquire);
611 const auto pending_before = backend_.has_pending();
612 const auto completed = drain();
613 const auto pending_after = backend_.has_pending();
614 const auto observation_lock = std::scoped_lock{observation_mutex_};
615 const auto in_flight_after =
616 in_flight_schedules_.load(std::memory_order_acquire);
617 const auto activity_after = activity_epoch_.load(std::memory_order_acquire);
618 if (completed == BackendDrainResult::Stalled) {
619 return DrainResult::Stalled;
620 }
621 if (pending_after) {
622 return DrainResult::BudgetExhausted;
623 }
624 if (!pending_before && in_flight_before == 0 && in_flight_after == 0 &&
625 activity_before == activity_after) {
626 idle_epoch_.store(activity_after, std::memory_order_release);
627 return DrainResult::Idle;
628 }
629 return DrainResult::Drained;
630 }
631
632 const std::uint64_t owner_epoch_;
633 const std::size_t capacity_;
634 std::unique_ptr<Slot[]> slots_;
635 Backend backend_;
636 mutable std::shared_mutex lifecycle_mutex_;
637 std::mutex observation_mutex_;
638 std::mutex drain_mutex_;
639 std::atomic<std::uint64_t> activity_epoch_ = 0;
640 std::atomic<std::uint64_t> in_flight_schedules_ = 0;
641 std::atomic<std::uint64_t> idle_epoch_ =
642 std::numeric_limits<std::uint64_t>::max();
643 bool sealed_ = false;
644};
645
646} // namespace tess::experimental::maintenance
Shared unit budget passed through one maintenance drain.
Definition maintenance.h:20
Definition registered_maintenance.h:117
friend class RegisteredScheduler
Definition registered_maintenance.h:127
Long-lived derived-state maintenance operation.
Definition maintenance.h:44
auto flush() -> DrainResult
Definition registered_maintenance.h:445
auto try_schedule(MaintenanceHandle handle) -> std::optional< ScheduleResult >
Definition registered_maintenance.h:393
auto register_task(MaintenanceTask &task) -> std::optional< MaintenanceHandle >
Definition registered_maintenance.h:330
void release(MaintenanceHandle handle)
Definition registered_maintenance.h:485
auto valid(MaintenanceHandle handle) const -> bool
Definition registered_maintenance.h:386
auto metrics() const noexcept -> MaintenanceMetrics
Definition registered_maintenance.h:502
auto try_release(MaintenanceHandle handle) -> ReleaseResult
Definition registered_maintenance.h:457
auto schedule(MaintenanceHandle handle) -> ScheduleResult
Definition registered_maintenance.h:424
void seal()
Definition registered_maintenance.h:365
auto run_some(MaintenanceBudget budget) -> DrainResult
Definition registered_maintenance.h:436
Definition registered_maintenance.h:109
Definition registered_maintenance.h:87
Scheduler observations used by experiments and diagnostics.
Definition maintenance.h:69