XRootD
XrdClHttpFactory.cc
Go to the documentation of this file.
1 /******************************************************************************/
2 /* Copyright (C) 2025, Pelican Project, Morgridge Institute for Research */
3 /* */
4 /* This file is part of the XrdClHttp client plugin for XRootD. */
5 /* */
6 /* XRootD is free software: you can redistribute it and/or modify it under */
7 /* the terms of the GNU Lesser General Public License as published by the */
8 /* Free Software Foundation, either version 3 of the License, or (at your */
9 /* option) any later version. */
10 /* */
11 /* XRootD is distributed in the hope that it will be useful, but WITHOUT */
12 /* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or */
13 /* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public */
14 /* License for more details. */
15 /* */
16 /* The copyright holder's institutional names and contributor's names may not */
17 /* be used to endorse or promote products derived from this software without */
18 /* specific prior written permission of the institution or contributor. */
19 /******************************************************************************/
20 
21 #include "XrdClHttpFactory.hh"
22 #include "XrdClHttpFile.hh"
23 #include "XrdClHttpFilesystem.hh"
24 #include "XrdClHttpUtil.hh"
25 #include "XrdClHttpOps.hh"
26 #include "XrdClHttpParseTimeout.hh"
27 #include "XrdClHttpWorker.hh"
28 
29 #include "XrdCl/XrdClConstants.hh"
30 #include "XrdCl/XrdClDefaultEnv.hh"
31 #include "XrdCl/XrdClLog.hh"
33 #include "XrdVersion.hh"
34 
35 #include <stdio.h>
36 #include <unistd.h>
37 
39 
40 using namespace XrdClHttp;
41 
42 struct timespec
43 Factory::GetHeaderTimeoutWithDefault(time_t oper_timeout)
44 {
45  if (oper_timeout == 0) {
47  XrdCl::DefaultEnv::GetEnv()->GetInt( "RequestTimeout", val );
48  oper_timeout = val;
49  }
50  if (oper_timeout <= 0) {
51  return {0, 0};
52  }
53  return {oper_timeout, 0};
54 }
55 
56 bool Factory::m_initialized = false;
57 std::shared_ptr<XrdClHttp::HandlerQueue> Factory::m_queue;
58 XrdCl::Log *Factory::m_log = nullptr;
59 std::once_flag Factory::m_init_once;
60 std::string Factory::m_stats_location;
61 std::chrono::system_clock::time_point Factory::m_start{};
62 
63 std::mutex Factory::m_shutdown_lock;
64 std::thread Factory::m_monitor_tid;
65 std::condition_variable Factory::m_shutdown_requested_cv;
66 bool Factory::m_shutdown_requested = false;
67 
68 // shutdown trigger, must be last of the static members
69 Factory::shutdown_s Factory::m_shutdowns;
70 
71 void
72 Factory::Initialize()
73 {
74  std::unique_lock lock(m_shutdown_lock);
75  if (m_shutdown_requested) {
76  return;
77  }
78  std::call_once(m_init_once, [&] {
79  m_log = XrdCl::DefaultEnv::GetLog();
80  if (!m_log) {
81  return;
82  }
83  m_log->SetTopicName(kLogXrdClHttp, "XrdClHttp");
84 
85  auto env = XrdCl::DefaultEnv::GetEnv();
86  if (!env) {
87  return;
88  }
89 
90  SetupX509();
91 
92  // The location for the client to write the statistics file; this will be dropped
93  // atomically every ~5 seconds and is meant to be complementary to the future g-stream.
94  env->PutString("HttpStatisticsLocation", "");
95  env->ImportString("HttpStatisticsLocation", "XRD_HTTPSTATISTICSLOCATION");
96  if (env->GetString("HttpStatisticsLocation", m_stats_location)) {
97  m_log->Debug(kLogXrdClHttp, "Will write client statistics to %s", m_stats_location.c_str());
98  } else {
99  m_log->Debug(kLogXrdClHttp, "Not writing client statistics to disk");
100  }
101  m_start = std::chrono::system_clock::now();
102 
103  // The minimum value we will accept from the request for a header timeout.
104  // (i.e., the amount of time the plugin will wait to receive headers from the remote server)
105  env->PutString("HttpMinimumHeaderTimeout", "");
106  env->ImportString("HttpMinimumHeaderTimeout", "XRD_HTTPMINIMUMHEADERTIMEOUT");
107 
108  // The default value of the header timeout (the amount of time the plugin will wait)
109  // to receive headers from the remote server.
110  env->PutString("HttpDefaultHeaderTimeout", "");
111  env->ImportString("HttpDefaultHeaderTimeout", "XRD_HTTPDEFAULTHEADERTIMEOUT");
112 
113  // The number of pending operations allowed in the global work queue.
114  env->PutInt("HttpMaxPendingOps", XrdClHttp::HandlerQueue::GetDefaultMaxPendingOps());
115  env->ImportInt("HttpMaxPendingOps", "XRD_HTTPMAXPENDINGOPS");
117  if (env->GetInt("HttpMaxPendingOps", max_pending)) {
118  if (max_pending <= 0 || max_pending > 10'000'000) {
119  m_log->Error(kLogXrdClHttp,
120  "Invalid value for the maximum number of pending operations in the global work queue (%d); using default value of %d",
121  max_pending,
124  env->PutInt("HttpMaxPendingOps", max_pending);
125  }
126  m_log->Debug(kLogXrdClHttp, "Using %d pending operations in the global work queue", max_pending);
127  }
128  m_queue.reset(new XrdClHttp::HandlerQueue(max_pending));
129 
130  // The number of threads to use for curl operations.
131  env->PutInt("HttpNumThreads", m_poll_threads);
132  env->ImportInt("HttpNumThreads", "XRD_HTTPNUMTHREADS");
133  int num_threads = m_poll_threads;
134  if (env->GetInt("HttpNumThreads", num_threads)) {
135  if (num_threads <= 0 || num_threads > 1'000) {
136  m_log->Error(kLogXrdClHttp, "Invalid value for the number of threads to use for curl operations (%d); using default value of %d", num_threads, m_poll_threads);
137  num_threads = m_poll_threads;
138  env->PutInt("HttpNumThreads", num_threads);
139  }
140  m_log->Debug(kLogXrdClHttp, "Using %d threads for curl operations", num_threads);
141  }
142 
143  // The stall timeout to use for transfer operations.
144  env->PutInt("HttpStallTimeout", XrdClHttp::CurlOperation::GetDefaultStallTimeout());
145  env->ImportInt("HttpStallTimeout", "XRD_HTTPSTALLTIMEOUT");
147  if (env->GetInt("HttpStallTimeout", stall_timeout)) {
148  if (stall_timeout < 0 || stall_timeout > 86'400) {
149  m_log->Error(kLogXrdClHttp, "Invalid value for the stall timeout (%d); using default value of %d", stall_timeout, XrdClHttp::CurlOperation::GetDefaultStallTimeout());
151  env->PutInt("HttpStallTimeout", stall_timeout);
152  }
153  m_log->Debug(kLogXrdClHttp, "Using %d seconds for the stall timeout", stall_timeout);
154  }
156 
157  // The slow transfer rate, in bytes per second, for timing out slow uploads/downloads.
158  env->PutInt("HttpSlowRateBytesSec", XrdClHttp::CurlOperation::GetDefaultSlowRateBytesSec());
159  env->ImportInt("HttpSlowRateBytesSec", "XRD_HTTPSLOWRATEBYTESSEC");
161  if (env->GetInt("HttpSlowRateBytesSec", slow_xfer_rate)) {
162  if (slow_xfer_rate < 0 || slow_xfer_rate > (1024 * 1024 * 1024)) {
163  m_log->Error(kLogXrdClHttp, "Invalid value for the slow transfer rate threshold (%d); using default value of %d", stall_timeout, XrdClHttp::CurlOperation::GetDefaultSlowRateBytesSec());
165  env->PutInt("HttpSlowRateBytesSec", slow_xfer_rate);
166  }
167  m_log->Debug(kLogXrdClHttp, "Using %d bytes/sec for the slow transfer rate threshold", slow_xfer_rate);
168  }
170 
171  // Determine the minimum header timeout. It's somewhat arbitrarily defaulted to 2s; below
172  // that and timeouts could be caused by OS scheduling noise. If the client has unreasonable
173  // expectations of the origin, we don't want to cause it to generate lots of origin-side load.
174  std::string val;
175  struct timespec mct{2, 0};
176  if (env->GetString("HttpMinimumHeaderTimeout", val) && !val.empty()) {
177  std::string errmsg;
178  if (!ParseTimeout(val, mct, errmsg)) {
179  m_log->Error(kLogXrdClHttp, "Failed to parse the minimum client timeout (%s): %s", val.c_str(), errmsg.c_str());
180  }
181  }
183 
184  struct timespec dht{9, 500'000'000};
185  if (env->GetString("HttpDefaultHeaderTimeout", val) && !val.empty()) {
186  std::string errmsg;
187  if (!ParseTimeout(val, dht, errmsg)) {
188  m_log->Error(kLogXrdClHttp, "Failed to parse the default header timeout (%s): %s", val.c_str(), errmsg.c_str());
189  }
190  }
192 
193  // Headers to inject into outgoing requests, given as a newline separated
194  // list of "<name>: <value>" entries. Set programmatically by xrdcp for
195  // its --header option, so only fall back to the environment variable when
196  // no value was provided. HeaderBuilder::Build is what interprets it, once
197  // per request.
198  //
199  // Note this deliberately does not use SetIfEmpty: header values routinely
200  // carry credentials and must not be written to the log.
201  if (!env->GetString("HttpHeaders", val) || val.empty()) {
202  env->PutString("HttpHeaders", "");
203  env->ImportString("HttpHeaders", "XRD_HTTPHEADERS");
204  }
205 
206  // Start up the cache for the OPTIONS response
207  auto &cache = XrdClHttp::VerbsCache::Instance();
208 
209  // Startup curl workers after we've set the configs to avoid race conditions
210  for (int idx=0; idx<num_threads; idx++) {
211  auto wk = std::make_unique<XrdClHttp::CurlWorker>(m_queue, cache, m_log);
212  auto wkp = wk.get();
213  std::thread t(XrdClHttp::CurlWorker::RunStatic, wkp);
214  wkp->Start(std::move(wk), std::move(t));
215  }
216 
217  std::thread t([this]{Monitor();});
218  m_monitor_tid = std::move(t);
219 
220  m_initialized = true;
221  });
222 }
223 
224 void
225 Factory::Monitor()
226 {
227  // This function is run in a separate thread to monitor the XrdClHttp statistics.
228  // It periodically saves the statistics to the stats file.
229  // Note: this previously had support for sending the statistics through the gstream.
230  // However, this was removed because gstream currently requires linking against XrdServer
231  // which is not available in the client; some further rearranging of headers and linkages
232  // is necessary.
233 
234  while (true) {
235  {
236  std::unique_lock lock(m_shutdown_lock);
237  m_shutdown_requested_cv.wait_for(
238  lock,
239  std::chrono::seconds(5),
240  []{return m_shutdown_requested;}
241  );
242  if (m_shutdown_requested) {
243  break;
244  }
245  }
246 
247  auto now = std::chrono::system_clock::now();
248 
249  std::string monitoring = "{\"event\": \"xrdclhttp\", "
250  "\"start\": " + std::to_string(std::chrono::duration<double>(m_start.time_since_epoch()).count()) + ","
251  "\"now\": " + std::to_string(std::chrono::duration<double>(now.time_since_epoch()).count()) + ","
252  "\"file\": " + File::GetMonitoringJson() + ","
253  "\"workers\": " + CurlWorker::GetMonitoringJson() + ","
254  "\"queues\": " + HandlerQueue::GetMonitoringJson() +
255  " }";
256  m_log->Info(kLogXrdClHttp, "Client monitoring statistics: %s", monitoring.c_str());
257  if (!m_stats_location.empty())
258  {
259  auto stats_tmp = m_stats_location + ".XXXXXX";
260  std::vector<char> stats_vector(stats_tmp.size() + 1, '\0');
261  memcpy(&stats_vector[0], stats_tmp.data(), stats_tmp.size() + 1);
262  auto fd = mkstemp(&stats_vector[0]);
263  if (fd == -1) {
264  m_log->Warning(kLogXrdClHttp, "Failed to create temporary stats file %s: %s", m_stats_location.c_str(), strerror(errno));
265  continue;
266  }
267  auto nb = write(fd, monitoring.data(), monitoring.size());
268  if (nb != static_cast<ssize_t>(monitoring.size())) {
269  if (nb == -1) m_log->Warning(kLogXrdClHttp, "Failed to write statistics into temporary file %s: %s", &stats_vector[0], strerror(errno));
270  else m_log->Warning(kLogXrdClHttp, "Failed to write statistics into temporary file %s: short write", &stats_vector[0]);
271  close(fd);
272  continue;
273  }
274  close(fd);
275  auto rv = rename(&stats_vector[0], m_stats_location.c_str());
276  if (rv) {
277  m_log->Warning(kLogXrdClHttp, "Failed to atomically rename stats file to final destination %s: %s", m_stats_location.c_str(), strerror(errno));
278  }
279  }
280  }
281 }
282 
283 namespace {
284 
285 void SetIfEmpty(XrdCl::Env *env, XrdCl::Log &log, const std::string &optName, const std::string &envName) {
286  if (!env) return;
287 
288  std::string val;
289  if (!env->GetString(optName, val) || val.empty()) {
290  env->PutString(optName, "");
291  env->ImportString(optName, envName);
292  }
293  if (env->GetString(optName, val) && !val.empty()) {
294  log.Info(kLogXrdClHttp, "Setting %s to value '%s'", optName.c_str(), val.c_str());
295  }
296 }
297 
298 } // namespace
299 
300 void
301 Factory::SetupX509() {
302 
303  auto env = XrdCl::DefaultEnv::GetEnv();
304  SetIfEmpty(env, *m_log, "HttpCertFile", "XRD_HTTPCERTFILE");
305  SetIfEmpty(env, *m_log, "HttpCertDir", "XRD_HTTPCERTDIR");
306  SetIfEmpty(env, *m_log, "HttpClientCertFile", "XRD_HTTPCLIENTCERTFILE");
307  SetIfEmpty(env, *m_log, "HttpClientKeyFile", "XRD_HTTPCLIENTKEYFILE");
308 
309  int disable_proxy = 0;
310  env->PutInt("HttpDisableX509", 0);
311  env->ImportInt("HttpDisableX509", "XRD_HTTPDISABLEX509");
312 
313  std::string filename;
314  char *filename_char;
315  if (!disable_proxy && (!env->GetString("HttpClientCertFile", filename) || filename.empty())) {
316  if ((filename_char = getenv("X509_USER_PROXY"))) {
317  filename = filename_char;
318  }
319  if (filename.empty()) {
320  filename = "/tmp/x509up_u" + std::to_string(geteuid());
321  }
322  if (access(filename.c_str(), R_OK) == 0) {
323  m_log->Debug(kLogXrdClHttp, "Using X509 proxy file found at %s for TLS client credential", filename.c_str());
324  env->PutString("HttpClientCertFile", filename);
325  env->PutString("HttpClientKeyFile", filename);
326  }
327  }
328  if ((!env->GetString("HttpCertDir", filename) || filename.empty()) && (filename_char = getenv("X509_CERT_DIR"))) {
329  env->PutString("HttpCertDir", filename_char);
330  }
331 }
332 
333 void
334 Factory::Shutdown()
335 {
336  {
337  std::unique_lock lock(m_shutdown_lock);
338  m_shutdown_requested = true;
339  m_shutdown_requested_cv.notify_one();
340  }
341  if (m_monitor_tid.joinable()) {
342  m_monitor_tid.join();
343  }
344 }
345 
346 void
347 Factory::Produce(std::unique_ptr<XrdClHttp::CurlOperation> operation)
348 {
349  m_queue->Produce(std::move(operation));
350 }
351 
353 Factory::CreateFile(const std::string & /*url*/) {
354  Initialize();
355  if (!m_initialized) {return nullptr;}
356  return new File(m_queue, m_log);
357 }
358 
360 Factory::CreateFileSystem(const std::string & url) {
361  Initialize();
362  if (!m_initialized) {return nullptr;}
363  return new Filesystem(url, m_queue, m_log);
364 }
365 
366 extern "C"
367 {
368  XrdEXPORT void *XrdClGetPlugIn(const void*)
369  {
370  return static_cast<void*>(new Factory());
371  }
372 }
XrdVERSIONINFO(XrdClGetPlugIn, XrdClGetPlugIn) using namespace XrdClHttp
XrdEXPORT void * XrdClGetPlugIn(const void *)
int rename(const char *oldpath, const char *newpath)
int access(const char *path, int amode)
#define close(a)
Definition: XrdPosix.hh:48
#define write(a, b, c)
Definition: XrdPosix.hh:121
XrdOucString File
static void SetSlowRateBytesSec(int rate)
static void SetStallTimeout(int stall_interval)
static int GetDefaultSlowRateBytesSec()
static int GetDefaultStallTimeout()
static void RunStatic(CurlWorker *myself)
static std::string GetMonitoringJson()
void Produce(std::unique_ptr< XrdClHttp::CurlOperation > operation)
virtual XrdCl::FileSystemPlugIn * CreateFileSystem(const std::string &url) override
Create a file system plug-in for the given URL.
virtual XrdCl::FilePlugIn * CreateFile(const std::string &url) override
Create a file plug-in for the given URL.
static void SetDefaultHeaderTimeout(struct timespec &ts)
static std::string GetMonitoringJson()
static void SetMinimumHeaderTimeout(struct timespec &ts)
static std::string GetMonitoringJson()
static unsigned GetDefaultMaxPendingOps()
static VerbsCache & Instance()
static Log * GetLog()
Get default log.
static Env * GetEnv()
Get default client environment.
bool PutInt(const std::string &key, int value)
Definition: XrdClEnv.cc:136
bool PutString(const std::string &key, const std::string &value)
Definition: XrdClEnv.cc:52
bool ImportString(const std::string &key, const std::string &shellKey)
Definition: XrdClEnv.cc:266
bool ImportInt(const std::string &key, const std::string &shellKey)
Definition: XrdClEnv.cc:237
bool GetString(const std::string &key, std::string &value)
Definition: XrdClEnv.cc:31
bool GetInt(const std::string &key, int &value)
Definition: XrdClEnv.cc:115
An interface for file plug-ins.
An interface for file plug-ins.
Handle diagnostics.
Definition: XrdClLog.hh:101
void SetTopicName(uint64_t topic, std::string name)
Map a topic number to a string.
Definition: XrdClLog.cc:163
void Error(uint64_t topic, const char *format,...)
Report an error.
Definition: XrdClLog.cc:231
void Warning(uint64_t topic, const char *format,...)
Report a warning.
Definition: XrdClLog.cc:248
void Info(uint64_t topic, const char *format,...)
Print an info.
Definition: XrdClLog.cc:265
void Debug(uint64_t topic, const char *format,...)
Print a debug message.
Definition: XrdClLog.cc:282
bool ParseTimeout(const std::string &duration, struct timespec &, std::string &errmsg)
const int DefaultRequestTimeout
const uint64_t kLogXrdClHttp