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