XRootD
XrdClHttpOptionsCache.hh
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 #ifndef _XRDCLHTTP__OPTIONSCACHE_HH__
22 #define _XRDCLHTTP__OPTIONSCACHE_HH__
23 
24 #include <array>
25 #include <atomic>
26 #include <chrono>
27 #include <condition_variable>
28 #include <mutex>
29 #include <shared_mutex>
30 #include <string>
31 #include <thread>
32 #include <unordered_map>
33 
34  namespace XrdClHttp {
35 
36  // A cache holding the known HTTP verbs for a given endpoint.
37  class VerbsCache {
38  public:
39 
40  // Enumeration bitmask of the HTTP verbs that we can test for
41  enum class HttpVerb {
42  kUnset = 0, // Indicates that we haven't yet probed for the HTTP verb support.
43  kUnknown = 1, // Indicates we probed for support but the result was indeterminate (not provided by the server, network error)
44  kPROPFIND = 2, // Server claims to support PROPFIND
45  };
46  class HttpVerbs {
47  public:
48  HttpVerbs() = default;
49  HttpVerbs(HttpVerb verb) : m_verbs(static_cast<unsigned>(verb)) {}
50  HttpVerbs &operator|=(HttpVerb verb) {m_verbs |= static_cast<unsigned>(verb); return *this;}
51  bool IsSet(HttpVerb verb) const
52  {
53  if (verb == HttpVerb::kUnset) {return !m_verbs;}
54  return static_cast<unsigned>(m_verbs) & static_cast<unsigned>(verb);
55  }
56  unsigned GetValue() const {return m_verbs;}
57  private:
58  unsigned m_verbs{0};
59  };
60 
61  static const std::string GetVerbString(HttpVerb ctype) {
62  switch (ctype) {
63  case HttpVerb::kUnset:
64  return "(unset)";
65  case HttpVerb::kUnknown:
66  return "(unknown)";
68  return "PROPFIND";
69  }
70  }
71 
72  void Put(const std::string &url, const HttpVerbs &verbs, const std::chrono::steady_clock::time_point &now=std::chrono::steady_clock::now()) const {
73  std::string modified_url;
74  auto key = GetUrlKey(url, modified_url);
75 
76  const std::unique_lock sentry(m_mutex);
77 
78  auto isKnown = !verbs.IsSet(HttpVerb::kUnknown);
79  auto lifetime = isKnown ? g_expiry_duration : g_negative_expiry_duration;
80 
81 // C++20 can elide the allocation for the string_view
82 #if __cplusplus >= 202002L
83  auto iter = m_verbs_map.find(key);
84 #else
85  auto iter = m_verbs_map.find(std::string(key));
86 #endif
87  if (iter == m_verbs_map.end()) {
88  m_verbs_map.emplace(key, VerbEntry{now + lifetime, verbs});
89  } else if (isKnown || iter->second.m_verbs.IsSet(HttpVerb::kUnknown)) {
90  // Previous entry didn't know the verbs, but now we do
91  iter->second = {now + lifetime, verbs};
92  }
93  }
94 
95  HttpVerbs Get(const std::string &url, const std::chrono::steady_clock::time_point &now=std::chrono::steady_clock::now()) const {
96  std::string modified_url;
97  auto key = GetUrlKey(url, modified_url);
98 
99  const std::shared_lock sentry(m_mutex);
100 #if __cplusplus >= 202002L
101  auto iter = m_verbs_map.find(key);
102 #else
103  auto iter = m_verbs_map.find(std::string(key));
104 #endif
105  if (iter == m_verbs_map.end()) {
106  m_cache_miss++;
107  return HttpVerbs{};
108  }
109  if (iter->second.m_expiry < now) {
110  m_cache_miss++;
111  return HttpVerbs{};
112  }
113  m_cache_hit++;
114  return iter->second.m_verbs;
115  }
116 
117  // Get the cache key for a given URL
118  //
119  // Cache key should consist of the schema, host, and port portion of the URL.
120  static std::string_view GetUrlKey(const std::string &url, std::string &modified_url) {
121  auto authority_loc = url.find("://");
122  if (authority_loc == std::string::npos) {
123  return std::string_view();
124  }
125  auto path_loc = url.find('/', authority_loc + 3);
126  if (path_loc == std::string::npos) {
127  path_loc = url.length();
128  }
129 
130  std::string_view url_view{url};
131  auto host_loc = url_view.substr(authority_loc + 3, path_loc - authority_loc - 3).find('@');
132  if (host_loc == std::string::npos) {
133  return url_view.substr(0, path_loc);
134  }
135  host_loc += authority_loc + 3;
136  modified_url = url.substr(0, authority_loc + 3) + std::string(url_view.substr(host_loc + 1, path_loc - host_loc - 1));
137  return modified_url;
138  }
139 
140  uint64_t GetCacheHits() const {return m_cache_hit;}
141  uint64_t GetCacheMisses() const {return m_cache_miss;}
142 
143  // Expire all entries in the cache whose expiration is older than `now`.
144  void Expire(std::chrono::steady_clock::time_point now);
145 
146  // Return the global instance of the verbs cache.
147  static VerbsCache &Instance();
148 
149 private:
150  VerbsCache() = default;
151  VerbsCache(const VerbsCache &) = delete;
152  VerbsCache(VerbsCache &&) = delete;
153 
154  // Background thread periodically invoking `Expire` on the cache.
155  static void ExpireThread();
156 
157  // Invoked by the destructor of a static member. Triggered when the library
158  // is shutting down or is unloaded from the process.
159  static void Shutdown();
160 
161  mutable std::atomic<uint64_t> m_cache_hit{0};
162  mutable std::atomic<uint64_t> m_cache_miss{0};
163 
164  template<typename ... Bases>
165  struct overload : Bases ...
166  {
167  using is_transparent = void;
168  using Bases::operator() ... ;
169  };
170  using transparent_string_hash = overload<
171  std::hash<std::string>,
172  std::hash<std::string_view>
173  >;
174 
175  struct VerbEntry {
176  std::chrono::steady_clock::time_point m_expiry;
177  HttpVerbs m_verbs;
178  };
179 
180  mutable std::shared_mutex m_mutex;
181  mutable std::unordered_map<std::string, VerbEntry, VerbsCache::transparent_string_hash, std::equal_to<>> m_verbs_map;
182 
183  static std::once_flag m_expiry_launch;
184  static VerbsCache g_cache;
185  static constexpr std::chrono::steady_clock::duration g_expiry_duration = std::chrono::hours(6);
186  static constexpr std::chrono::steady_clock::duration g_negative_expiry_duration = std::chrono::minutes(15);
187 
188  // Mutex for managing the shutdown of the background thread
189  static std::mutex m_shutdown_lock;
190  // Condition variable managing the requested shutdown of the background thread.
191  static std::condition_variable m_shutdown_requested_cv;
192  // Flag indicating that a shutdown was requested.
193  static bool m_shutdown_requested;
194  // The cache expire thread
195  static std::thread m_expire_tid;
196  // shutdown trigger
197  static struct shutdown_s {
198  ~shutdown_s() { Shutdown(); }
199  } m_shutdowns;
200 };
201 
202  } // namespace XrdClHttp
203 
204 #endif // _XRDCLHTTP__OPTIONSCACHE_HH__
HttpVerbs & operator|=(HttpVerb verb)
void Expire(std::chrono::steady_clock::time_point now)
HttpVerbs Get(const std::string &url, const std::chrono::steady_clock::time_point &now=std::chrono::steady_clock::now()) const
static std::string_view GetUrlKey(const std::string &url, std::string &modified_url)
static const std::string GetVerbString(HttpVerb ctype)
void Put(const std::string &url, const HttpVerbs &verbs, const std::chrono::steady_clock::time_point &now=std::chrono::steady_clock::now()) const
static VerbsCache & Instance()