@@ -11,6 +11,7 @@ use crossbeam_channel::{bounded, Receiver, Sender};
11
11
use lazy_static:: lazy_static;
12
12
13
13
use crate :: future:: Future ;
14
+ use crate :: io:: ErrorKind ;
14
15
use crate :: task:: { Context , Poll } ;
15
16
use crate :: utils:: abort_on_panic;
16
17
use std:: sync:: { Arc , Mutex } ;
@@ -31,13 +32,13 @@ const FREQUENCY_QUEUE_SIZE: usize = 10;
31
32
// Smoothing factor is estimated with: 2 / (N + 1) where N is sample size.
32
33
const EMA_COEFFICIENT : f64 = 2_f64 / ( FREQUENCY_QUEUE_SIZE as f64 + 1_f64 ) ;
33
34
34
- // Possible max threads (without OS contract)
35
- const MAX_THREADS : u64 = 10_000 ;
36
-
37
35
// Pool task frequency variable
38
36
// Holds scheduled tasks onto the thread pool for the calculation window
39
37
static FREQUENCY : AtomicU64 = AtomicU64 :: new ( 0 ) ;
40
38
39
+ // Possible max threads (without OS contract)
40
+ static MAX_THREADS : AtomicU64 = AtomicU64 :: new ( 10_000 ) ;
41
+
41
42
struct Pool {
42
43
sender : Sender < async_task:: Task < ( ) > > ,
43
44
receiver : Receiver < async_task:: Task < ( ) > > ,
@@ -89,14 +90,19 @@ lazy_static! {
89
90
static ref POOL_SIZE : Arc <Mutex <u64 >> = Arc :: new( Mutex :: new( LOW_WATERMARK ) ) ;
90
91
}
91
92
92
- // Gets the current pool size
93
- // Used for pool size boundary checking in pool manager
94
- fn get_current_pool_size ( ) -> u64 {
95
- let current_arc = POOL_SIZE . clone ( ) ;
96
- let current_pool_size = * current_arc. lock ( ) . unwrap ( ) ;
97
- LOW_WATERMARK . max ( current_pool_size)
98
- }
99
-
93
+ // Exponentially Weighted Moving Average calculation
94
+ //
95
+ // This allows us to find the EMA value.
96
+ // This value represents the trend of tasks mapped onto the thread pool.
97
+ // Calculation is following:
98
+ //
99
+ // α :: EMA_COEFFICIENT :: smoothing factor between 0 and 1
100
+ // Yt :: freq :: frequency sample at time t
101
+ // St :: acc :: EMA at time t
102
+ //
103
+ // Under these definitions formula is following:
104
+ // EMA = α * [ Yt + (1 - α)*Yt-1 + ((1 - α)^2)*Yt-2 + ((1 - α)^3)*Yt-3 ... ] + St
105
+ #[ inline]
100
106
fn calculate_ema ( freq_queue : & VecDeque < u64 > ) -> f64 {
101
107
freq_queue. iter ( ) . enumerate ( ) . fold ( 0_f64 , |acc, ( i, freq) | {
102
108
acc + ( ( * freq as f64 ) * ( ( 1_f64 - EMA_COEFFICIENT ) . powf ( i as f64 ) as f64 ) )
@@ -124,11 +130,7 @@ fn scale_pool() {
124
130
// Calculate message rate for the given time window
125
131
let frequency = ( current_frequency as f64 / MANAGER_POLL_INTERVAL as f64 ) as u64 ;
126
132
127
- // Adapts the thread count of pool
128
- //
129
- // Sliding window of frequencies visited by the pool manager.
130
- // Select the maximum from the window and check against the current task dispatch frequency.
131
- // If current frequency is bigger, we will scale up.
133
+ // Calculates current time window's EMA value (including last sample)
132
134
let prev_ema_frequency = calculate_ema ( & freq_queue) ;
133
135
134
136
// Add seen frequency data to the frequency histogram.
@@ -137,22 +139,35 @@ fn scale_pool() {
137
139
freq_queue. pop_front ( ) ;
138
140
}
139
141
142
+ // Calculates current time window's EMA value (including last sample)
140
143
let curr_ema_frequency = calculate_ema ( & freq_queue) ;
141
144
145
+ // Adapts the thread count of pool
146
+ //
147
+ // Sliding window of frequencies visited by the pool manager.
148
+ // Pool manager creates EMA value for previous window and current window.
149
+ // Compare them to determine scaling amount based on the trends.
150
+ // If current EMA value is bigger, we will scale up.
142
151
if curr_ema_frequency > prev_ema_frequency {
152
+ // "Scale by" amount can be seen as "how much load is coming".
153
+ // "Scale" amount is "how many threads we should spawn".
143
154
let scale_by: f64 = curr_ema_frequency - prev_ema_frequency;
144
155
let scale = ( ( LOW_WATERMARK as f64 * scale_by) + LOW_WATERMARK as f64 ) as u64 ;
145
156
146
- // Pool size shouldn't reach to max_threads anyway.
147
- // Pool manager backpressures itself while visiting message rate frequencies.
148
- // You will get an error before hitting to limits by OS.
157
+ // It is time to scale the pool!
149
158
( 0 ..scale) . for_each ( |_| {
150
159
create_blocking_thread ( ) ;
151
160
} ) ;
152
- } else if curr_ema_frequency == prev_ema_frequency && current_frequency != 0 {
161
+ } else if ( curr_ema_frequency - prev_ema_frequency) . abs ( ) < std:: f64:: EPSILON
162
+ && current_frequency != 0
163
+ {
153
164
// Throughput is low. Allocate more threads to unblock flow.
165
+ // If we fall to this case, scheduler is congested by longhauling tasks.
166
+ // For unblock the flow we should add up some threads to the pool, but not that much to
167
+ // stagger the program's operation.
154
168
let scale = LOW_WATERMARK * current_frequency + 1 ;
155
169
170
+ // Scale it up!
156
171
( 0 ..scale) . for_each ( |_| {
157
172
create_blocking_thread ( ) ;
158
173
} ) ;
@@ -165,6 +180,16 @@ fn scale_pool() {
165
180
// Dynamic threads will terminate themselves if they don't
166
181
// receive any work after between one and ten seconds.
167
182
fn create_blocking_thread ( ) {
183
+ // Check that thread is spawnable.
184
+ // If it hits to the OS limits don't spawn it.
185
+ {
186
+ let current_arc = POOL_SIZE . clone ( ) ;
187
+ let pool_size = * current_arc. lock ( ) . unwrap ( ) ;
188
+ if pool_size >= MAX_THREADS . load ( Ordering :: SeqCst ) {
189
+ MAX_THREADS . store ( 10_000 , Ordering :: SeqCst ) ;
190
+ return ;
191
+ }
192
+ }
168
193
// We want to avoid having all threads terminate at
169
194
// exactly the same time, causing thundering herd
170
195
// effects. We want to stagger their destruction over
@@ -174,7 +199,7 @@ fn create_blocking_thread() {
174
199
// Generate a simple random number of milliseconds
175
200
let rand_sleep_ms = u64:: from ( random ( 10_000 ) ) ;
176
201
177
- thread:: Builder :: new ( )
202
+ let _ = thread:: Builder :: new ( )
178
203
. name ( "async-blocking-driver-dynamic" . to_string ( ) )
179
204
. spawn ( move || {
180
205
let wait_limit = Duration :: from_millis ( 1000 + rand_sleep_ms) ;
@@ -192,15 +217,30 @@ fn create_blocking_thread() {
192
217
* current_arc. lock ( ) . unwrap ( ) -= 1 ;
193
218
}
194
219
} )
195
- . expect ( "cannot start a dynamic thread driving blocking tasks" ) ;
220
+ . map_err ( |err| {
221
+ match err. kind ( ) {
222
+ ErrorKind :: WouldBlock => {
223
+ // Maximum allowed threads per process is varying from system to system.
224
+ // Some systems has it(like MacOS), some doesn't(Linux)
225
+ // This case expected to not happen.
226
+ // But when happened this shouldn't throw a panic.
227
+ let current_arc = POOL_SIZE . clone ( ) ;
228
+ MAX_THREADS . store ( * current_arc. lock ( ) . unwrap ( ) - 1 , Ordering :: SeqCst ) ;
229
+ }
230
+ _ => eprintln ! (
231
+ "cannot start a dynamic thread driving blocking tasks: {}" ,
232
+ err
233
+ ) ,
234
+ }
235
+ } ) ;
196
236
}
197
237
198
238
// Enqueues work, attempting to send to the threadpool in a
199
239
// nonblocking way and spinning up needed amount of threads
200
240
// based on the previous statistics without relying on
201
241
// if there is not a thread ready to accept the work or not.
202
242
fn schedule ( t : async_task:: Task < ( ) > ) {
203
- // Add up for every incoming task schedule
243
+ // Add up for every incoming scheduled task
204
244
FREQUENCY . fetch_add ( 1 , Ordering :: Acquire ) ;
205
245
206
246
if let Err ( err) = POOL . sender . try_send ( t) {
0 commit comments