XRootD
Loading...
Searching...
No Matches
XrdThrottleManager.cc
Go to the documentation of this file.
1
3
4#include "XrdOuc/XrdOucEnv.hh"
12
13#define XRD_TRACE m_trace->
15
16#include <algorithm>
17#include <array>
18#include <cmath>
19#include <random>
20#include <sstream>
21
22#if defined(__linux__)
23
24#include <sched.h>
25unsigned XrdThrottleManager::GetTimerListHash() {
26 int cpu = sched_getcpu();
27 if (cpu < 0) {
28 return 0;
29 }
30 return cpu % m_timer_list_size;
31}
32
33#else
34
35unsigned XrdThrottleManager::GetTimerListHash() {
36 return 0;
37}
38
39#endif
40
41const char *
42XrdThrottleManager::TraceID = "ThrottleManager";
43
45 m_trace(tP),
46 m_log(lP),
47 m_interval_length_seconds(1.0),
48 m_bytes_per_second(-1),
49 m_ops_per_second(-1),
50 m_concurrency_limit(-1),
51 m_last_round_allocation(100*1024),
52 m_loadshed_host(""),
53 m_loadshed_port(0),
54 m_loadshed_frequency(0)
55{
56 // Until the first recompute fills them in, no user is in the wake order.
57 std::fill(m_wake_order_0.begin(), m_wake_order_0.end(), -1);
58 std::fill(m_wake_order_1.begin(), m_wake_order_1.end(), -1);
59}
60
61void
63{
64
65 auto max_open = config.GetMaxOpen();
66 if (max_open != -1) SetMaxOpen(max_open);
67 auto max_conn = config.GetMaxConn();
68 if (max_conn != -1) SetMaxConns(max_conn);
69 auto max_wait = config.GetMaxWait();
70 if (max_wait != -1) SetMaxWait(max_wait);
71
73 config.GetThrottleIOPSRate(),
75 static_cast<float>(config.GetThrottleRecomputeIntervalMS())/1000.0);
76
77 m_trace->What = config.GetTraceLevels();
78
79 auto loadshed_host = config.GetLoadshedHost();
80 auto loadshed_port = config.GetLoadshedPort();
81 auto loadshed_freq = config.GetLoadshedFreq();
82 if (!loadshed_host.empty() && loadshed_port > 0 && loadshed_freq > 0)
83 {
84 // Loadshed specified, so set it.
85 SetLoadShed(loadshed_host, loadshed_port, loadshed_freq);
86 }
87}
88
89void
91{
92 TRACE(DEBUG, "Initializing the throttle manager.");
93 // Initialize all our shares to zero.
94 m_primary_bytes_shares.resize(m_max_users);
95 m_secondary_bytes_shares.resize(m_max_users);
96 m_primary_ops_shares.resize(m_max_users);
97 m_secondary_ops_shares.resize(m_max_users);
98 for (auto & waiter : m_waiter_info) {
99 waiter.m_manager = this;
100 }
101
102 // Allocate each user 100KB and 10 ops to bootstrap;
103 for (int i=0; i<m_max_users; i++)
104 {
105 m_primary_bytes_shares[i] = m_last_round_allocation;
106 m_secondary_bytes_shares[i] = 0;
107 m_primary_ops_shares[i] = 10;
108 m_secondary_ops_shares[i] = 0;
109 }
110
111 int rc;
112 pthread_t tid;
113 if ((rc = XrdSysThread::Run(&tid, XrdThrottleManager::RecomputeBootstrap, static_cast<void *>(this), 0, "Buffer Manager throttle")))
114 m_log->Emsg("ThrottleManager", rc, "create throttle thread");
115
116}
117
118std::tuple<std::string, uint16_t>
120 // client can be null, if so, return nobody
121 if (!client) {
122 return std::make_tuple("nobody", GetUid("nobody"));
123 }
124
125 // Try various potential "names" associated with the request, from the most
126 // specific to most generic.
127 std::string user;
128
129 if (client->eaAPI && client->eaAPI->Get("token.subject", user)) {
130 if (client->vorg) user = std::string(client->vorg) + ":" + user;
131 } else if (client->eaAPI) {
132 std::string request_name;
133 if (client->eaAPI->Get("request.name", request_name) && !request_name.empty()) user = request_name;
134 }
135 if (user.empty()) {user = client->name ? client->name : "nobody";}
136 uint16_t uid = GetUid(user.c_str());
137 return std::make_tuple(user, uid);
138}
139
140/*
141 * Take as many shares as possible to fulfill the request; update
142 * request with current remaining value, or zero if satisfied.
143 */
144inline void
145XrdThrottleManager::GetShares(int &shares, int &request)
146{
147 int remaining;
148 AtomicFSub(remaining, shares, request);
149 if (remaining > 0)
150 {
151 request -= (remaining < request) ? remaining : request;
152 }
153}
154
155/*
156 * Iterate through all of the secondary shares, attempting
157 * to steal enough to fulfill the request.
158 */
159void
160XrdThrottleManager::StealShares(int uid, int &reqsize, int &reqops)
161{
162 if (!reqsize && !reqops) return;
163 TRACE(BANDWIDTH, "Stealing shares to fill request of " << reqsize << " bytes");
164 TRACE(IOPS, "Stealing shares to fill request of " << reqops << " ops.");
165
166 for (int i=uid+1; i % m_max_users == uid; i++)
167 {
168 if (reqsize) GetShares(m_secondary_bytes_shares[i % m_max_users], reqsize);
169 if (reqops) GetShares(m_secondary_ops_shares[ i % m_max_users], reqops);
170 }
171
172 TRACE(BANDWIDTH, "After stealing shares, " << reqsize << " of request bytes remain.");
173 TRACE(IOPS, "After stealing shares, " << reqops << " of request ops remain.");
174}
175
176/*
177 * Increment the number of files held open by a given entity. Returns false
178 * if the user is at the maximum; in this case, the internal counter is not
179 * incremented.
180 */
181bool
182XrdThrottleManager::OpenFile(const std::string &entity, std::string &error_message)
183{
184 if (m_max_open == 0 && m_max_conns == 0) return true;
185
186 const std::lock_guard<std::mutex> lock(m_file_mutex);
187 auto iter = m_file_counters.find(entity);
188 unsigned long cur_open_files = 0, cur_open_conns;
189 if (m_max_open) {
190 if (iter == m_file_counters.end()) {
191 m_file_counters[entity] = 1;
192 TRACE(FILES, "User " << entity << " has opened their first file");
193 cur_open_files = 1;
194 } else if (iter->second < m_max_open) {
195 iter->second++;
196 cur_open_files = iter->second;
197 } else {
198 std::stringstream ss;
199 ss << "User " << entity << " has hit the limit of " << m_max_open << " open files";
200 TRACE(FILES, ss.str());
201 error_message = ss.str();
202 return false;
203 }
204 }
205
206 if (m_max_conns) {
207 auto pid = XrdSysThread::Num();
208 auto conn_iter = m_active_conns.find(entity);
209 auto conn_count_iter = m_conn_counters.find(entity);
210 if ((conn_count_iter != m_conn_counters.end()) && (conn_count_iter->second == m_max_conns) &&
211 (conn_iter == m_active_conns.end() || ((*(conn_iter->second))[pid] == 0)))
212 {
213 // note: we are rolling back the increment in open files
214 if (m_max_open) iter->second--;
215 std::stringstream ss;
216 ss << "User " << entity << " has hit the limit of " << m_max_conns <<
217 " open connections";
218 TRACE(CONNS, ss.str());
219 error_message = ss.str();
220 return false;
221 }
222 if (conn_iter == m_active_conns.end()) {
223 std::unique_ptr<std::unordered_map<pid_t, unsigned long>> conn_map(
224 new std::unordered_map<pid_t, unsigned long>());
225 (*conn_map)[pid] = 1;
226 m_active_conns[entity] = std::move(conn_map);
227 if (conn_count_iter == m_conn_counters.end()) {
228 m_conn_counters[entity] = 1;
229 cur_open_conns = 1;
230 } else {
231 m_conn_counters[entity] ++;
232 cur_open_conns = m_conn_counters[entity];
233 }
234 } else {
235 auto pid_iter = conn_iter->second->find(pid);
236 if (pid_iter == conn_iter->second->end() || pid_iter->second == 0) {
237 (*(conn_iter->second))[pid] = 1;
238 conn_count_iter->second++;
239 cur_open_conns = conn_count_iter->second;
240 } else {
241 (*(conn_iter->second))[pid] ++;
242 cur_open_conns = conn_count_iter->second;
243 }
244 }
245 TRACE(CONNS, "User " << entity << " has " << cur_open_conns << " open connections");
246 }
247 if (m_max_open) TRACE(FILES, "User " << entity << " has " << cur_open_files << " open files");
248 return true;
249}
250
251
252/*
253 * Decrement the number of files held open by a given entity.
254 *
255 * Returns false if the value would have fallen below zero or
256 * if the entity isn't tracked.
257 */
258bool
259XrdThrottleManager::CloseFile(const std::string &entity)
260{
261 if (m_max_open == 0 && m_max_conns == 0) return true;
262
263 bool result = true;
264 const std::lock_guard<std::mutex> lock(m_file_mutex);
265 if (m_max_open) {
266 auto iter = m_file_counters.find(entity);
267 if (iter == m_file_counters.end()) {
268 TRACE(FILES, "WARNING: User " << entity << " closed a file but throttle plugin never saw an open file");
269 result = false;
270 } else if (iter->second == 0) {
271 TRACE(FILES, "WARNING: User " << entity << " closed a file but throttle plugin thinks all files were already closed");
272 result = false;
273 } else {
274 iter->second--;
275 }
276 if (result) TRACE(FILES, "User " << entity << " closed a file; " << iter->second <<
277 " remain open");
278 }
279
280 if (m_max_conns) {
281 auto pid = XrdSysThread::Num();
282 auto conn_iter = m_active_conns.find(entity);
283 auto conn_count_iter = m_conn_counters.find(entity);
284 if (conn_iter == m_active_conns.end() || !(conn_iter->second)) {
285 TRACE(CONNS, "WARNING: User " << entity << " closed a file on a connection we are not"
286 " tracking");
287 return false;
288 }
289 auto pid_iter = conn_iter->second->find(pid);
290 if (pid_iter == conn_iter->second->end()) {
291 TRACE(CONNS, "WARNING: User " << entity << " closed a file on a connection we are not"
292 " tracking");
293 return false;
294 }
295 if (pid_iter->second == 0) {
296 TRACE(CONNS, "WARNING: User " << entity << " closed a file on connection the throttle"
297 " plugin thinks was idle");
298 } else {
299 pid_iter->second--;
300 }
301 if (conn_count_iter == m_conn_counters.end()) {
302 TRACE(CONNS, "WARNING: User " << entity << " closed a file but the throttle plugin never"
303 " observed an open file");
304 } else if (pid_iter->second == 0) {
305 if (conn_count_iter->second == 0) {
306 TRACE(CONNS, "WARNING: User " << entity << " had a connection go idle but the "
307 " throttle plugin already thought all connections were idle");
308 } else {
309 conn_count_iter->second--;
310 TRACE(CONNS, "User " << entity << " had connection on thread " << pid << " go idle; "
311 << conn_count_iter->second << " active connections remain");
312 }
313 }
314 }
315
316 return result;
317}
318
319
320/*
321 * Apply the throttle. If there are no limits set, returns immediately. Otherwise,
322 * this applies the limits as best possible, stalling the thread if necessary.
323 */
324void
325XrdThrottleManager::Apply(int reqsize, int reqops, int uid)
326{
327 if (m_bytes_per_second < 0)
328 reqsize = 0;
329 if (m_ops_per_second < 0)
330 reqops = 0;
331 while (reqsize || reqops)
332 {
333 // Subtract the requested out of the shares
334 AtomicBeg(m_compute_var);
335 GetShares(m_primary_bytes_shares[uid], reqsize);
336 if (reqsize)
337 {
338 TRACE(BANDWIDTH, "Using secondary shares; request has " << reqsize << " bytes left.");
339 GetShares(m_secondary_bytes_shares[uid], reqsize);
340 TRACE(BANDWIDTH, "Finished with secondary shares; request has " << reqsize << " bytes left.");
341 }
342 else
343 {
344 TRACE(BANDWIDTH, "Filled byte shares out of primary; " << m_primary_bytes_shares[uid] << " left.");
345 }
346 GetShares(m_primary_ops_shares[uid], reqops);
347 if (reqops)
348 {
349 GetShares(m_secondary_ops_shares[uid], reqops);
350 }
351 StealShares(uid, reqsize, reqops);
352 AtomicEnd(m_compute_var);
353
354 if (reqsize || reqops)
355 {
356 if (reqsize) TRACE(BANDWIDTH, "Sleeping to wait for throttle fairshare.");
357 if (reqops) TRACE(IOPS, "Sleeping to wait for throttle fairshare.");
358 m_compute_var.Wait();
359 m_loadshed_limit_hit++;
360 }
361 }
362
363}
364
365void
366XrdThrottleManager::UserIOAccounting()
367{
368 std::chrono::steady_clock::duration::rep total_active_time = 0;
369 for (size_t idx = 0; idx < m_timer_list.size(); idx++) {
370 auto &timerList = m_timer_list[idx];
371 std::unique_lock<std::mutex> lock(timerList.m_mutex);
372 auto timer = timerList.m_first;
373 while (timer) {
374 auto next = timer->m_next;
375 auto uid = timer->m_owner;
376 auto &waiter = m_waiter_info[uid];
377 auto recent_duration = timer->Reset();
378 waiter.m_io_time += recent_duration.count();
379
380 total_active_time += recent_duration.count();
381 timer = next;
382 }
383 }
384 m_io_active_time += total_active_time;
385}
386
387void
388XrdThrottleManager::ComputeWaiterOrder()
389{
390 // Update the IO time for long-running I/O operations. This prevents,
391 // for example, a 2-minute I/O operation from causing a spike in
392 // concurrency because it's otherwise only reported at the end.
393 UserIOAccounting();
394
395 auto now = std::chrono::steady_clock::now();
396 auto elapsed = now - m_last_waiter_recompute_time;
397 m_last_waiter_recompute_time = now;
398 std::chrono::duration<double> elapsed_secs = elapsed;
399 // Alpha is the decay factor for the exponential moving average. One window is 10 seconds,
400 // so every 10 seconds we decay the prior average by 1/e (that is, the weight is 64% of the
401 // total). This means the contribution of I/O load from a minute ago is 0.2% of the total.
402
403 // The moving average will be used to determine how close the user is to their "fair share"
404 // of the concurrency limit among the users that are waiting.
405 auto alpha = 1 - std::exp(-1 * elapsed_secs.count() / 10.0);
406
407 std::vector<double> share;
408 share.resize(m_max_users);
409 size_t users_with_waiters = 0;
410 // For each user, compute their current concurrency and determine how many waiting users
411 // total there are.
412 for (int i = 0; i < m_max_users; i++)
413 {
414 auto &waiter = m_waiter_info[i];
415 auto io_duration_rep = waiter.m_io_time.exchange(std::chrono::steady_clock::duration(0).count());
416 std::chrono::steady_clock::duration io_duration = std::chrono::steady_clock::duration(io_duration_rep);
417 std::chrono::duration<double> io_duration_secs = io_duration;
418 auto prev_concurrency = io_duration_secs.count() / elapsed_secs.count();
419 float new_concurrency = waiter.m_concurrency;
420
421 new_concurrency = (1 - alpha) * new_concurrency + alpha * prev_concurrency;
422 waiter.m_concurrency = new_concurrency;
423 if (new_concurrency > 0) {
424 TRACE(DEBUG, "User " << i << " has concurrency of " << new_concurrency);
425 }
426 unsigned waiting;
427 {
428 std::lock_guard<std::mutex> lock(waiter.m_mutex);
429 waiting = waiter.m_waiting;
430 }
431 if (waiting > 0)
432 {
433 share[i] = new_concurrency;
434 TRACE(DEBUG, "User " << i << " has concurrency of " << share[i] << " and is waiting for " << waiting);
435 // Handle the division-by-zero case; if we have no history of usage whatsoever, we should pretend we
436 // have at least some minimal load
437 if (share[i] == 0) {
438 share[i] = 0.1;
439 }
440 users_with_waiters++;
441 }
442 else
443 {
444 share[i] = 0;
445 }
446 }
447 auto fair_share = static_cast<double>(m_concurrency_limit) / static_cast<double>(users_with_waiters);
448 std::vector<uint16_t> waiter_order;
449 waiter_order.resize(m_max_users);
450
451 // Calculate the share for each user. We assume the user should get a share proportional to how
452 // far above or below the fair share they are. So, a user with concurrency of 20 when the fairshare
453 // is 10 will get 0.5 shares; a user with concurrency of 5 when the fairshare is 10 will get 2.0 shares.
454 double shares_sum = 0;
455 for (int idx = 0; idx < m_max_users; idx++)
456 {
457 if (share[idx]) {
458 shares_sum += fair_share / share[idx];
459 }
460 }
461
462 // We must quantize the overall shares into an array of 1024 elements. We do this by
463 // scaling up (or down) based on the total number of shares computed above. Note this
464 // quantization can lead to an over-provisioned user being assigned zero shares; thus,
465 // we scale based on (1024-#users) so we can give one extra share to each user.
466 auto scale_factor = (static_cast<double>(m_max_users) - static_cast<double>(users_with_waiters)) / shares_sum;
467 size_t offset = 0;
468 for (int uid = 0; uid < m_max_users; uid++) {
469 if (share[uid] > 0) {
470 auto shares = static_cast<unsigned>(scale_factor * fair_share / share[uid]) + 1;
471 TRACE(DEBUG, "User " << uid << " has " << shares << " shares");
472 for (unsigned idx = 0; idx < shares; idx++)
473 {
474 waiter_order[offset % m_max_users] = uid;
475 offset++;
476 }
477 }
478 }
479 if (offset < m_max_users) {
480 for (size_t idx = offset; idx < m_max_users; idx++) {
481 waiter_order[idx] = -1;
482 }
483 }
484 // Shuffle the order to randomize the wakeup order.
485 std::shuffle(waiter_order.begin(), waiter_order.end(), std::default_random_engine());
486
487 // Copy the order to the inactive array. We do not shuffle in-place because RAtomics are
488 // not move constructible, which is a requirement for std::shuffle.
489 auto &waiter_order_to_modify = (m_wake_order_active == 0) ? m_wake_order_1 : m_wake_order_0;
490 std::copy(waiter_order.begin(), waiter_order.end(), waiter_order_to_modify.begin());
491
492 // Set the array we just modified to be the active one. Since this is a relaxed write, it could take
493 // some time for other CPUs to see the change; that's OK as this is all stochastic anyway.
494 m_wake_order_active = (m_wake_order_active + 1) % 2;
495
496 m_waiter_offset = 0;
497
498 // If we find ourselves below the concurrency limit because we woke up too few operations in the last
499 // interval, try waking up enough operations to fill the gap. If we race with new incoming operations,
500 // the threads will just go back to sleep.
501 if (users_with_waiters) {
502 m_waiting_users = users_with_waiters;
503 auto io_active = m_io_active.load(std::memory_order_acquire);
504 for (size_t idx = io_active; idx < static_cast<size_t>(m_concurrency_limit); idx++) {
505 NotifyOne();
506 }
507 }
508}
509
510void *
511XrdThrottleManager::RecomputeBootstrap(void *instance)
512{
513 XrdThrottleManager * manager = static_cast<XrdThrottleManager*>(instance);
514 manager->Recompute();
515 return NULL;
516}
517
518void
519XrdThrottleManager::Recompute()
520{
521 while (1)
522 {
523 // The connection counter can accumulate a number of known-idle connections.
524 // We only need to keep long-term memory of idle ones. Take this chance to garbage
525 // collect old connection counters.
526 if (m_max_open || m_max_conns) {
527 const std::lock_guard<std::mutex> lock(m_file_mutex);
528 for (auto iter = m_active_conns.begin(); iter != m_active_conns.end();)
529 {
530 auto & conn_count = *iter;
531 if (!conn_count.second) {
532 iter = m_active_conns.erase(iter);
533 continue;
534 }
535 for (auto iter2 = conn_count.second->begin(); iter2 != conn_count.second->end();) {
536 if (iter2->second == 0) {
537 iter2 = conn_count.second->erase(iter2);
538 } else {
539 iter2++;
540 }
541 }
542 if (!conn_count.second->size()) {
543 iter = m_active_conns.erase(iter);
544 } else {
545 iter++;
546 }
547 }
548 for (auto iter = m_conn_counters.begin(); iter != m_conn_counters.end();) {
549 if (!iter->second) {
550 iter = m_conn_counters.erase(iter);
551 } else {
552 iter++;
553 }
554 }
555 for (auto iter = m_file_counters.begin(); iter != m_file_counters.end();) {
556 if (!iter->second) {
557 iter = m_file_counters.erase(iter);
558 } else {
559 iter++;
560 }
561 }
562 }
563
564 TRACE(DEBUG, "Recomputing fairshares for throttle.");
565 RecomputeInternal();
566 ComputeWaiterOrder();
567 TRACE(DEBUG, "Finished recomputing fairshares for throttle; sleeping for " << m_interval_length_seconds << " seconds.");
568 XrdSysTimer::Wait(static_cast<int>(1000*m_interval_length_seconds));
569 }
570}
571
572/*
573 * The heart of the manager approach.
574 *
575 * This routine periodically recomputes the shares of each current user.
576 * Each user has a "primary" and a "secondary" share. At the end of the
577 * each time interval, the remaining primary share is moved to secondary.
578 * A user can utilize both shares; if both are gone, they must block until
579 * the next recompute interval.
580 *
581 * The secondary share can be "stolen" by any other user; so, if a user
582 * is idle or under-utilizing, their share can be used by someone else.
583 * However, they can never be completely starved, as no one can steal
584 * primary share.
585 *
586 * In this way, we violate the throttle for an interval, but never starve.
587 *
588 */
589void
590XrdThrottleManager::RecomputeInternal()
591{
592 // Compute total shares for this interval;
593 float intervals_per_second = 1.0/m_interval_length_seconds;
594 float total_bytes_shares = m_bytes_per_second / intervals_per_second;
595 float total_ops_shares = m_ops_per_second / intervals_per_second;
596
597 // Compute the number of active users; a user is active if they used
598 // any primary share during the last interval;
599 AtomicBeg(m_compute_var);
600 float active_users = 0;
601 long bytes_used = 0;
602 for (int i=0; i<m_max_users; i++)
603 {
604 int primary = AtomicFAZ(m_primary_bytes_shares[i]);
605 if (primary != m_last_round_allocation)
606 {
607 active_users++;
608 if (primary >= 0)
609 m_secondary_bytes_shares[i] = primary;
610 primary = AtomicFAZ(m_primary_ops_shares[i]);
611 if (primary >= 0)
612 m_secondary_ops_shares[i] = primary;
613 bytes_used += (primary < 0) ? m_last_round_allocation : (m_last_round_allocation-primary);
614 }
615 }
616
617 if (active_users == 0)
618 {
619 active_users++;
620 }
621
622 // Note we allocate the same number of shares to *all* users, not
623 // just the active ones. If a new user becomes active in the next
624 // interval, we'll go over our bandwidth budget just a bit.
625 m_last_round_allocation = static_cast<int>(total_bytes_shares / active_users);
626 int ops_shares = static_cast<int>(total_ops_shares / active_users);
627 TRACE(BANDWIDTH, "Round byte allocation " << m_last_round_allocation << " ; last round used " << bytes_used << ".");
628 TRACE(IOPS, "Round ops allocation " << ops_shares);
629 for (int i=0; i<m_max_users; i++)
630 {
631 m_primary_bytes_shares[i] = m_last_round_allocation;
632 m_primary_ops_shares[i] = ops_shares;
633 }
634
635 AtomicEnd(m_compute_var);
636
637 // Reset the loadshed limit counter.
638 int limit_hit = m_loadshed_limit_hit.exchange(0);
639 TRACE(DEBUG, "Throttle limit hit " << limit_hit << " times during last interval.");
640
641 // Update the IO counters
642 m_compute_var.Lock();
643 m_stable_io_active = m_io_active.load(std::memory_order_acquire);
644 auto io_active = m_stable_io_active;
645 m_stable_io_total = m_io_total;
646 auto io_total = m_stable_io_total;
647 auto io_wait_rep = m_io_active_time.exchange(std::chrono::steady_clock::duration(0).count());
648 m_stable_io_wait += std::chrono::steady_clock::duration(io_wait_rep);
649
650 m_compute_var.UnLock();
651
652 auto io_wait_ms = std::chrono::duration_cast<std::chrono::milliseconds>(m_stable_io_wait).count();
653 TRACE(IOLOAD, "Current IO counter is " << io_active << "; total IO active time is " << io_wait_ms << "ms.");
654 if (m_gstream)
655 {
656 char buf[128];
657 auto len = snprintf(buf, 128,
658 R"({"event":"throttle_update","io_wait":%.4f,"io_active":%d,"io_total":%llu})",
659 static_cast<double>(io_wait_ms) / 1000.0, io_active, static_cast<long long unsigned>(io_total));
660 auto suc = (len < 128) ? m_gstream->Insert(buf, len + 1) : false;
661 if (!suc)
662 {
663 TRACE(IOLOAD, "Failed g-stream insertion of throttle_update record (len=" << len << "): " << buf);
664 }
665 }
666 m_compute_var.Broadcast();
667}
668
669/*
670 * Do a simple hash across the username.
671 */
672uint16_t
673XrdThrottleManager::GetUid(const std::string &username)
674{
675 std::hash<std::string> hash_fn;
676 auto hash = hash_fn(username);
677 auto uid = static_cast<uint16_t>(hash % m_max_users);
678 TRACE(DEBUG, "Mapping user " << username << " to UID " << uid);
679 return uid;
680}
681
682/*
683 * Notify a single waiter thread that it can proceed.
684 */
685void
686XrdThrottleManager::NotifyOne()
687{
688 auto &wake_order = (m_wake_order_active == 0) ? m_wake_order_0 : m_wake_order_1;
689
690 for (size_t idx = 0; idx < m_max_users; ++idx)
691 {
692 auto offset = m_waiter_offset.fetch_add(1, std::memory_order_acq_rel);
693 int16_t uid = wake_order[offset % m_max_users];
694 if (uid < 0)
695 {
696 continue;
697 }
698 auto &waiter_info = m_waiter_info[uid];
699 std::unique_lock<std::mutex> lock(waiter_info.m_mutex);
700 if (waiter_info.m_waiting) {
701 waiter_info.NotifyOne(std::move(lock));
702 return;
703 }
704 }
705}
706
707/*
708 * Create an IO timer object; increment the number of outstanding IOs.
709 */
711XrdThrottleManager::StartIOTimer(uint16_t uid, bool &ok)
712{
713 int cur_counter = m_io_active.fetch_add(1, std::memory_order_acq_rel);
714 m_io_total++;
715
716 while (m_concurrency_limit >= 0 && cur_counter >= m_concurrency_limit)
717 {
718 // If the user has essentially no concurrency, then we let them
719 // temporarily exceed the limit. This prevents potential waits for
720 // every single read for an infrequent user.
721 if (m_waiter_info[uid].m_concurrency < 1)
722 {
723 break;
724 }
725 m_loadshed_limit_hit++;
726 m_io_active.fetch_sub(1, std::memory_order_acq_rel);
727 TRACE(DEBUG, "ThrottleManager (user=" << uid << "): IO concurrency limit hit; waiting for other IOs to finish.");
728 ok = m_waiter_info[uid].Wait();
729 if (!ok) {
730 TRACE(DEBUG, "ThrottleManager (user=" << uid << "): timed out waiting for other IOs to finish.");
731 return XrdThrottleTimer();
732 }
733 cur_counter = m_io_active.fetch_add(1, std::memory_order_acq_rel);
734 }
735
736 ok = true;
737 return XrdThrottleTimer(this, uid);
738}
739
740/*
741 * Finish recording an IO timer.
742 */
743void
744XrdThrottleManager::StopIOTimer(std::chrono::steady_clock::duration & event_duration, uint16_t uid)
745{
746 m_io_active_time += event_duration.count();
747 auto old_active = m_io_active.fetch_sub(1, std::memory_order_acq_rel);
748 m_waiter_info[uid].m_io_time += event_duration.count();
749 if (old_active == static_cast<unsigned>(m_concurrency_limit))
750 {
751 // If we are below the concurrency limit threshold and have another waiter
752 // for our user, then execute it immediately. Otherwise, we will give
753 // someone else a chance to run (as we have gotten more than our share recently).
754 unsigned waiting_users = m_waiting_users;
755 if (waiting_users == 0) waiting_users = 1;
756 if (m_waiter_info[uid].m_concurrency < m_concurrency_limit / waiting_users)
757 {
758 std::unique_lock<std::mutex> lock(m_waiter_info[uid].m_mutex);
759 if (m_waiter_info[uid].m_waiting > 0)
760 {
761 m_waiter_info[uid].NotifyOne(std::move(lock));
762 return;
763 }
764 }
765 NotifyOne();
766 }
767}
768
769/*
770 * Check the counters to see if we have hit any throttle limits in the
771 * current time period. If so, shed the client randomly.
772 *
773 * If the client has already been load-shedded once and reconnected to this
774 * server, then do not load-shed it again.
775 */
776bool
777XrdThrottleManager::CheckLoadShed(const std::string &opaque)
778{
779 if (m_loadshed_port == 0)
780 {
781 return false;
782 }
783 if (m_loadshed_limit_hit == 0)
784 {
785 return false;
786 }
787 if (static_cast<unsigned>(rand()) % 100 > m_loadshed_frequency)
788 {
789 return false;
790 }
791 if (opaque.empty())
792 {
793 return false;
794 }
795 return true;
796}
797
798void
799XrdThrottleManager::PrepLoadShed(const char * opaque, std::string &lsOpaque)
800{
801 if (m_loadshed_port == 0)
802 {
803 return;
804 }
805 if (opaque && opaque[0])
806 {
807 XrdOucEnv env(opaque);
808 // Do not load shed client if it has already been done once.
809 if (env.Get("throttle.shed") != 0)
810 {
811 return;
812 }
813 lsOpaque = opaque;
814 lsOpaque += "&throttle.shed=1";
815 }
816 else
817 {
818 lsOpaque = "throttle.shed=1";
819 }
820}
821
822void
823XrdThrottleManager::PerformLoadShed(const std::string &opaque, std::string &host, unsigned &port)
824{
825 host = m_loadshed_host;
826 host += "?";
827 host += opaque;
828 port = m_loadshed_port;
829}
830
831bool
832XrdThrottleManager::Waiter::Wait()
833{
834 auto timeout = std::chrono::steady_clock::now() + m_manager->m_max_wait_time;
835 {
836 std::unique_lock<std::mutex> lock(m_mutex);
837 m_waiting++;
838 m_cv.wait_until(lock, timeout,
839 [&] { return m_manager->m_io_active.load(std::memory_order_acquire) < static_cast<unsigned>(m_manager->m_concurrency_limit) || std::chrono::steady_clock::now() >= timeout; });
840 m_waiting--;
841 }
842 if (std::chrono::steady_clock::now() > timeout) {
843 return false;
844 }
845 return true;
846}
#define DEBUG(x)
#define AtomicFSub(w, x, y)
#define AtomicFAZ(x)
#define AtomicBeg(Mtx)
#define AtomicEnd(Mtx)
#define TRACE(act, x)
Definition XrdTrace.hh:63
char * Get(const char *varname)
Definition XrdOucEnv.hh:69
XrdSecAttr * Get(const void *sigkey)
char * vorg
Entity's virtual organization(s)
XrdSecEntityAttr * eaAPI
non-const API to attributes
char * name
Entity's name.
static int Run(pthread_t *, void *(*proc)(void *), void *arg, int opts=0, const char *desc=0)
static unsigned long Num(void)
static void Wait(int milliseconds)
void StopIOTimer(std::chrono::steady_clock::duration &event_duration, uint16_t uid)
void SetThrottles(float reqbyterate, float reqoprate, int concurrency, float interval_length)
void SetMaxOpen(unsigned long max_open)
void FromConfig(XrdThrottle::Configuration &config)
void Apply(int reqsize, int reqops, int uid)
std::tuple< std::string, uint16_t > GetUserInfo(const XrdSecEntity *client)
XrdThrottleTimer StartIOTimer(uint16_t uid, bool &ok)
void SetLoadShed(std::string &hostname, unsigned port, unsigned frequency)
friend class XrdThrottleTimer
void PrepLoadShed(const char *opaque, std::string &lsOpaque)
bool CheckLoadShed(const std::string &opaque)
void SetMaxWait(unsigned long max_wait)
void SetMaxConns(unsigned long max_conns)
XrdThrottleManager(XrdSysError *lP, XrdOucTrace *tP)
void PerformLoadShed(const std::string &opaque, std::string &host, unsigned &port)
bool CloseFile(const std::string &entity)
bool OpenFile(const std::string &entity, std::string &open_error_message)
long long GetLoadshedPort() const
long long GetThrottleDataRate() const
long long GetThrottleConcurrency() const
const std::string & GetLoadshedHost() const
long long GetLoadshedFreq() const
long long GetThrottleIOPSRate() const
long long GetThrottleRecomputeIntervalMS() const