diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf8b574..3d5f820f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Custom autoscaler trigger packs**: `autoscaler` now + constructs and dispatches each configured trigger correctly instead of + failing template instantiation because the trigger argument was omitted. + Unit coverage now exercises all three documented custom trigger forms + together (#1020). - **Non-blocking epoll connect precondition**: the epoll backend now rejects `async_connect()` on a blocking socket with `-EINVAL` before `connect(2)` can block a scheduler worker. Direct callers must set `O_NONBLOCK` first (#997). diff --git a/include/elio/runtime/autoscaler.hpp b/include/elio/runtime/autoscaler.hpp index 92353b00..d70bd83e 100644 --- a/include/elio/runtime/autoscaler.hpp +++ b/include/elio/runtime/autoscaler.hpp @@ -70,7 +70,8 @@ class autoscaler_impl { // Handle each trigger if constexpr (sizeof...(Triggers) > 0) { // Custom triggers provided - ((handle_trigger(pending, num_workers, now, last_idle_time, cfg)), ...); + (handle_trigger(Triggers{}, pending, num_workers, now, + last_idle_time, cfg), ...); } else { // Default behavior: scale up on overload, scale down on idle handle_default_overload(pending, num_workers, cfg); diff --git a/tests/unit/test_autoscaler.cpp b/tests/unit/test_autoscaler.cpp index 4aacffbf..06dc59ed 100644 --- a/tests/unit/test_autoscaler.cpp +++ b/tests/unit/test_autoscaler.cpp @@ -1,11 +1,84 @@ #include #include +#include #include #include using namespace elio; using namespace elio::runtime; +namespace { + +template +bool wait_for_condition(Predicate&& predicate, + std::chrono::milliseconds timeout) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + if (predicate()) return true; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return predicate(); +} + +struct trigger_test_worker { + [[nodiscard]] bool is_idle() const noexcept { return false; } + + [[nodiscard]] std::chrono::steady_clock::time_point + last_task_time() const noexcept { + return {}; + } +}; + +struct trigger_test_scheduler { + [[nodiscard]] size_t pending_tasks() const noexcept { + return pending.load(std::memory_order_relaxed); + } + + [[nodiscard]] size_t num_threads() const noexcept { + return workers.load(std::memory_order_relaxed); + } + + void set_thread_count(size_t count) noexcept { + workers.store(count, std::memory_order_relaxed); + } + + [[nodiscard]] trigger_test_worker* get_worker(size_t) noexcept { + return &worker; + } + + std::atomic pending{2}; + std::atomic workers{2}; + trigger_test_worker worker; +}; + +struct overload_capture_action { + inline static std::atomic calls{0}; + + void operator()(trigger_test_scheduler*, size_t) const noexcept { + calls.fetch_add(1, std::memory_order_release); + } +}; + +struct idle_capture_action { + inline static std::atomic calls{0}; + + void operator()(trigger_test_scheduler*, size_t, + std::chrono::seconds) const noexcept { + calls.fetch_add(1, std::memory_order_release); + } +}; + +struct block_capture_action { + inline static std::atomic calls{0}; + + void operator()(trigger_test_scheduler*, size_t, + std::chrono::milliseconds) const noexcept { + calls.fetch_add(1, std::memory_order_release); + } +}; + +} // namespace + TEST_CASE("autoscaler config defaults", "[autoscaler]") { autoscaler_config config; @@ -30,6 +103,53 @@ TEST_CASE("autoscaler start/stop", "[autoscaler]") { sched.shutdown(); } +TEST_CASE("autoscaler custom trigger pack", "[autoscaler]") { + overload_capture_action::calls.store(0, std::memory_order_relaxed); + idle_capture_action::calls.store(0, std::memory_order_relaxed); + block_capture_action::calls.store(0, std::memory_order_relaxed); + + autoscaler_config config; + config.tick_interval = std::chrono::milliseconds(1); + config.overload_threshold = 1; + config.idle_threshold = 1; + config.idle_delay = std::chrono::seconds(0); + config.min_workers = 1; + config.max_workers = 3; + config.block_threshold = std::chrono::milliseconds(0); + + trigger_test_scheduler sched; + autoscaler, + on_idle, + on_block> scaler{config}; + scaler.start(&sched); + + const bool overload_and_block_observed = wait_for_condition( + [] { + return overload_capture_action::calls.load( + std::memory_order_acquire) > 0 && + block_capture_action::calls.load( + std::memory_order_acquire) > 0; + }, + std::chrono::seconds(2)); + + sched.pending.store(0, std::memory_order_relaxed); + const bool idle_observed = wait_for_condition( + [] { + return idle_capture_action::calls.load( + std::memory_order_acquire) > 0; + }, + std::chrono::seconds(2)); + + scaler.stop(); + + CHECK(overload_and_block_observed); + CHECK(idle_observed); + CHECK(overload_capture_action::calls.load(std::memory_order_relaxed) > 0); + CHECK(idle_capture_action::calls.load(std::memory_order_relaxed) > 0); + CHECK(block_capture_action::calls.load(std::memory_order_relaxed) > 0); +} + TEST_CASE("worker_thread last_task_time", "[autoscaler]") { scheduler sched{1}; sched.start();