XRootD
XrdThrottleManager.cc
Go to the documentation of this file.
1 
2 #include "XrdThrottleManager.hh"
3 
4 #include "XrdOuc/XrdOucEnv.hh"
5 #include "XrdSec/XrdSecEntity.hh"
8 #include "XrdSys/XrdSysTimer.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>
27 unsigned 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 
37 unsigned XrdThrottleManager::GetTimerListHash() {
38  return 0;
39 }
40 
41 #endif
42 
43 const char *
44 XrdThrottleManager::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 
63 void
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(),
76  config.GetThrottleConcurrency(),
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 
102 void
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 
131 std::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  */
157 inline void
158 XrdThrottleManager::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  */
172 void
173 XrdThrottleManager::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  */
194 bool
195 XrdThrottleManager::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  */
278 bool
279 XrdThrottleManager::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  */
344 void
345 XrdThrottleManager::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 
385 void
386 XrdThrottleManager::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 
407 void
408 XrdThrottleManager::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 
530 void *
531 XrdThrottleManager::RecomputeBootstrap(void *instance)
532 {
533  XrdThrottleManager * manager = static_cast<XrdThrottleManager*>(instance);
534  manager->Recompute();
535  return NULL;
536 }
537 
538 void
539 XrdThrottleManager::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  */
609 void
610 XrdThrottleManager::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  */
692 uint16_t
693 XrdThrottleManager::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  */
705 void
706 XrdThrottleManager::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  */
731 XrdThrottleManager::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  */
763 void
764 XrdThrottleManager::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  */
796 bool
797 XrdThrottleManager::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 
818 void
819 XrdThrottleManager::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 
842 void
843 XrdThrottleManager::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 
851 bool
852 XrdThrottleManager::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  */
883 int
884 XrdThrottleManager::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  */
941 int
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  */
959 unsigned long
960 XrdThrottleManager::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)
Definition: XrdBwmTrace.hh:54
#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)
Definition: XrdSecEntity.hh:71
XrdSecEntityAttr * eaAPI
non-const API to attributes
Definition: XrdSecEntity.hh:92
char * name
Entity's name.
Definition: XrdSecEntity.hh:69
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
Definition: XrdSysError.cc:116
void Say(const char *text1, const char *text2=0, const char *txt3=0, const char *text4=0, const char *text5=0, const char *txt6=0)
Definition: XrdSysError.cc:162
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)
Definition: XrdSysTimer.cc:227
T exchange(T v, std::memory_order mo=std::memory_order_relaxed) noexcept
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
long long GetThrottleConcurrency() const
const std::string & GetLoadshedHost() const
long long GetLoadshedFreq() const
long long GetThrottleIOPSRate() const
long long GetThrottleRecomputeIntervalMS() const
const std::string & GetUserConfigFile() const
bool Insert(const char *data, int dlen)