|
3 | 3 | #include <memory> |
4 | 4 | #include <sstream> |
5 | 5 | #include <sys/stat.h> |
| 6 | +#include <unordered_map> |
6 | 7 |
|
7 | 8 | #include <jwt-cpp/base.h> |
8 | 9 | #include <jwt-cpp/jwt.h> |
@@ -33,6 +34,44 @@ CurlRaii myCurl; |
33 | 34 |
|
34 | 35 | std::mutex key_refresh_mutex; |
35 | 36 |
|
| 37 | +// Per-issuer mutex map for preventing thundering herd on new issuers |
| 38 | +std::mutex issuer_mutex_map_lock; |
| 39 | +std::unordered_map<std::string, std::shared_ptr<std::mutex>> issuer_mutexes; |
| 40 | +constexpr size_t MAX_ISSUER_MUTEXES = 1000; |
| 41 | + |
| 42 | +// Get or create a mutex for a specific issuer |
| 43 | +std::shared_ptr<std::mutex> get_issuer_mutex(const std::string &issuer) { |
| 44 | + std::lock_guard<std::mutex> guard(issuer_mutex_map_lock); |
| 45 | + |
| 46 | + auto it = issuer_mutexes.find(issuer); |
| 47 | + if (it != issuer_mutexes.end()) { |
| 48 | + return it->second; |
| 49 | + } |
| 50 | + |
| 51 | + // Prevent resource exhaustion: limit the number of cached mutexes |
| 52 | + if (issuer_mutexes.size() >= MAX_ISSUER_MUTEXES) { |
| 53 | + // Remove mutexes that are no longer in use |
| 54 | + for (auto iter = issuer_mutexes.begin(); iter != issuer_mutexes.end(); ) { |
| 55 | + if (iter->second.use_count() == 1) { |
| 56 | + // Only we hold a reference, safe to remove |
| 57 | + iter = issuer_mutexes.erase(iter); |
| 58 | + } else { |
| 59 | + ++iter; |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + // If still at capacity, clear the oldest entries |
| 64 | + // Note: In a production system, we might use an LRU cache |
| 65 | + if (issuer_mutexes.size() >= MAX_ISSUER_MUTEXES) { |
| 66 | + issuer_mutexes.clear(); |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + auto mutex_ptr = std::make_shared<std::mutex>(); |
| 71 | + issuer_mutexes[issuer] = mutex_ptr; |
| 72 | + return mutex_ptr; |
| 73 | +} |
| 74 | + |
36 | 75 | } // namespace |
37 | 76 |
|
38 | 77 | namespace scitokens { |
@@ -854,16 +893,29 @@ Validator::get_public_key_pem(const std::string &issuer, const std::string &kid, |
854 | 893 | result->m_done = true; |
855 | 894 | } |
856 | 895 | } else { |
857 | | - // No keys in the DB, or they are expired |
| 896 | + // No keys in the DB, or they are expired, so get them from the web. |
858 | 897 | // Record that we had expired keys if the issuer was previously known |
859 | | - // (This is tracked by having an entry in issuer stats) |
860 | 898 | auto &issuer_stats = |
861 | 899 | internal::MonitoringStats::instance().get_issuer_stats(issuer); |
862 | 900 | issuer_stats.inc_expired_key(); |
863 | 901 |
|
864 | | - // Get keys from the web. |
865 | | - result = get_public_keys_from_web( |
866 | | - issuer, internal::SimpleCurlGet::default_timeout); |
| 902 | + // Use per-issuer lock to prevent thundering herd for new issuers |
| 903 | + auto issuer_mutex = get_issuer_mutex(issuer); |
| 904 | + std::lock_guard<std::mutex> lock(*issuer_mutex); |
| 905 | + |
| 906 | + // Check again if keys are now in DB (another thread may have fetched |
| 907 | + // them) |
| 908 | + if (get_public_keys_from_db(issuer, now, result->m_keys, |
| 909 | + result->m_next_update)) { |
| 910 | + // Keys are now available, use them |
| 911 | + result->m_continue_fetch = false; |
| 912 | + result->m_do_store = false; |
| 913 | + result->m_done = true; |
| 914 | + } else { |
| 915 | + // Still no keys, fetch them from the web |
| 916 | + result = get_public_keys_from_web( |
| 917 | + issuer, internal::SimpleCurlGet::default_timeout); |
| 918 | + } |
867 | 919 | } |
868 | 920 | result->m_issuer = issuer; |
869 | 921 | result->m_kid = kid; |
|
0 commit comments