Skip to content

Commit 327e64e

Browse files
committed
feat(runtime): add autoscaler for automatic worker thread scaling
Add autoscaler component that automatically adjusts worker thread count based on load. Includes: - autoscaler_config: Configurable thresholds for scaling decisions - autoscaler_triggers: Template-based trigger types (on_overload, on_idle, on_block) - autoscaler_actions: Action types and combinators (scale_up, scale_down, log, null) - autoscaler_impl: Main autoscaler with default trigger behavior Features: - Automatic scale-up when pending tasks exceed overload threshold - Automatic scale-down when queue is idle for configured delay - Block detection for workers stuck in user code - Template-based trigger-action system for extensibility - Default behavior works without custom triggers Add worker metrics tracking (last_task_time, is_idle) to support autoscaler block detection. Update examples, documentation, and tests.
1 parent db6e716 commit 327e64e

15 files changed

Lines changed: 712 additions & 3 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Build directories
22
build/
3+
.worktrees/
34
cmake-build-*/
45
*.build/
56

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- **Virtual Stack Tracking** for natural exception propagation
1414
- **Work-Stealing Scheduler** with lock-free Chase-Lev deques
1515
- **Dynamic Thread Pool** with runtime adjustment
16+
- **Autoscaler** for automatic worker thread scaling under load
1617
- **Synchronization Primitives**: mutex, semaphore, event, channel
1718
- **Timers**: sleep_for, sleep_until, yield
1819
- **I/O Backends**: io_uring (preferred) and epoll fallback

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ add_executable(dynamic_threads dynamic_threads.cpp)
2323
target_link_libraries(dynamic_threads PRIVATE elio)
2424
target_link_options(dynamic_threads PRIVATE ${STATIC_LINK_FLAGS})
2525

26+
add_executable(autoscaler_example autoscaler_example.cpp)
27+
target_link_libraries(autoscaler_example PRIVATE elio)
28+
target_link_options(autoscaler_example PRIVATE ${STATIC_LINK_FLAGS})
29+
2630
add_executable(thread_affinity thread_affinity.cpp)
2731
target_link_libraries(thread_affinity PRIVATE elio)
2832
target_link_options(thread_affinity PRIVATE ${STATIC_LINK_FLAGS})

examples/autoscaler_example.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#include <elio/elio.hpp>
2+
#include <elio/runtime/autoscaler.hpp>
3+
#include <iostream>
4+
#include <atomic>
5+
#include <chrono>
6+
#include <random>
7+
8+
using namespace elio;
9+
10+
// Task that simulates work with random duration
11+
coro::task<void> workload_task(std::atomic<int>& counter) {
12+
// Simulate variable work duration (10-100ms)
13+
static thread_local std::mt19937 rng(std::hash<std::thread::id>{}(std::this_thread::get_id()));
14+
std::uniform_int_distribution<int> dist(10, 100);
15+
std::this_thread::sleep_for(std::chrono::milliseconds(dist(rng)));
16+
17+
counter.fetch_add(1, std::memory_order_relaxed);
18+
co_return;
19+
}
20+
21+
int main() {
22+
log::logger::instance().set_level(log::level::warning);
23+
24+
std::cout << "=== Elio Autoscaler Example ===" << std::endl;
25+
std::cout << "Demonstrating automatic worker thread scaling" << std::endl;
26+
std::cout << std::endl;
27+
28+
// Configure autoscaler
29+
elio::runtime::autoscaler_config config;
30+
config.tick_interval = std::chrono::milliseconds(200);
31+
config.overload_threshold = 20;
32+
config.idle_threshold = 5;
33+
config.idle_delay = std::chrono::seconds(3);
34+
config.min_workers = 2;
35+
config.max_workers = 8;
36+
37+
// Create scheduler with minimum workers
38+
runtime::scheduler sched(config.min_workers);
39+
sched.start();
40+
41+
// Create and start autoscaler with default triggers
42+
elio::runtime::autoscaler<runtime::scheduler> autoscaler(config);
43+
autoscaler.start(&sched);
44+
45+
std::cout << "Initial workers: " << sched.num_threads() << std::endl;
46+
std::cout << std::endl;
47+
48+
// Phase 1: High load - demonstrate scale-up
49+
{
50+
std::atomic<int> completed{0};
51+
52+
// Submit heavy workload
53+
for (int i = 0; i < 2000; ++i) {
54+
sched.spawn(workload_task(completed).release());
55+
}
56+
57+
std::cout << "Phase 1: High load - expecting scale-up..." << std::endl;
58+
std::cout << "----------------------------------------" << std::endl;
59+
60+
// Monitor autoscaler for 5 seconds
61+
for (int i = 0; i < 25; ++i) {
62+
std::this_thread::sleep_for(config.tick_interval);
63+
64+
size_t workers = sched.num_threads();
65+
size_t pending = sched.pending_tasks();
66+
67+
if (i % 2 == 0) {
68+
std::cout << " Workers: " << workers
69+
<< ", Pending: " << pending
70+
<< ", Completed: " << completed.load() << std::endl;
71+
}
72+
}
73+
}
74+
75+
std::cout << std::endl;
76+
77+
// Phase 2: Even higher load
78+
{
79+
std::atomic<int> completed2{0};
80+
81+
// Submit even heavier workload
82+
for (int i = 0; i < 3000; ++i) {
83+
sched.spawn(workload_task(completed2).release());
84+
}
85+
86+
std::cout << "Phase 2: Higher load - expecting more scale-up..." << std::endl;
87+
std::cout << "-------------------------------------------" << std::endl;
88+
89+
for (int i = 0; i < 25; ++i) {
90+
std::this_thread::sleep_for(config.tick_interval);
91+
92+
size_t pending = sched.pending_tasks();
93+
94+
if (i % 2 == 0) {
95+
std::cout << " Workers: " << sched.num_threads()
96+
<< ", Pending: " << pending << std::endl;
97+
}
98+
}
99+
}
100+
101+
std::cout << std::endl;
102+
103+
// Phase 3: Low load - wait for scale-down
104+
{
105+
std::cout << "Phase 3: Low load - waiting for scale-down..." << std::endl;
106+
std::cout << "------------------------------------------" << std::endl;
107+
108+
// Wait longer for idle_delay to trigger scale-down
109+
for (int i = 0; i < 30; ++i) {
110+
std::this_thread::sleep_for(config.tick_interval);
111+
112+
size_t workers = sched.num_threads();
113+
size_t pending = sched.pending_tasks();
114+
115+
if (i % 2 == 0) {
116+
std::cout << " Workers: " << workers
117+
<< ", Pending: " << pending << std::endl;
118+
}
119+
}
120+
}
121+
122+
std::cout << std::endl;
123+
124+
// Stop autoscaler
125+
autoscaler.stop();
126+
127+
std::cout << "Final workers: " << sched.num_threads() << std::endl;
128+
129+
// Shutdown
130+
sched.shutdown();
131+
132+
std::cout << std::endl;
133+
std::cout << "=== Example completed ===" << std::endl;
134+
std::cout << "Autoscaler automatically adjusted worker count based on load!" << std::endl;
135+
136+
return 0;
137+
}

include/elio/elio.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
#include "runtime/async_main.hpp"
2727
#include "runtime/affinity.hpp"
2828
#include "runtime/serve.hpp"
29+
#include "runtime/autoscaler_config.hpp"
30+
#include "runtime/autoscaler_triggers.hpp"
31+
#include "runtime/autoscaler_actions.hpp"
32+
#include "runtime/autoscaler.hpp"
2933

3034
// I/O backend
3135
#include "io/io_backend.hpp"

0 commit comments

Comments
 (0)