Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-88968

Optimize `APIVersionMetrics` using `RWMutex`

    • Service Arch
    • Fully Compatible
    • v8.0
    • Service Arch 2024-04-29

      The data-structure that backs APIVersionMetrics can be optimized for frequent reads:

      • Although users are not required to reuse appNames, it is expected for the set of appNames connected to a server to rarely change after warmup.
      • Currently, we only track 3 API versions: "default" (no API version is specified), "1", and "2" (test only).
      • For each appName and version combo, APIVersionMetrics records the latest timestamp.
      • To generate a report of API version usage, APIVersionMetrics first prunes the entires that are older than a day, and then report the appName and the versions used by each appName.

      Considering the above, we can change the implementation to favor readers:

      class APIVersionMetrics {
      private:
          struct VersionMetrics {
              // [0]: Default
              // [1]: Version 1
              // [2]: Version 2 (test-only).
              std::array<Atomic<Date_t>, 3> timestamps(Date_t::min());
          };
      
          mutable std::shared_mutex _mutex; // Before SERVER-86656 lands.
          StringMap<VersionMetrics> _apiVersionMetrics;
      };
      

      The above enables us to update individual timestamps with atomic operations, rather than exclusively acquire the mutex that protects _apiVersionMetrics. Furthermore, we only need to exclusively lock _mutex when updating the hash-table, which should be infrequent (hopefully very rare).

      void APIVersionMetrics::update(StringData appName, const APIParameters& apiParams) {
          auto now = getGlobalServiceContext()->getFastClockSource()->now();
          std::shared_lock sLock(_mutex);
          ...
          if (MONGO_likely(appNameIter != _apiVersionMetrics.end()) {
              appNameIter->second.timestamps[versionIndex].storeRelaxed(now);
          } else {
              sLock.unlock();
              stdx::lock_guard xLock(_mutex);
              [ appNameIter, _ ] = _apiVersionMetrics.emplace(appName, {});
              appNameIter->second.timestamps[versionIndex].storeRelaxed(now);
          }
      }
      
      void APIVersionMetrics::_appendAPIVersionData(BSONObjBuilder* bob) {
          std::shared_lock sLock(_mutex);
          for (const auto& [appName, versionTimestamps] : _apiVersionMetrics) {
              ...
          }
      }
      

      Finally, we can optimize removing stale entries by only acquiring the exclusive mutex if the hash-map needs to be changed:

      void APIVersionMetrics::_removeStaleTimestamps() {
          auto now = getGlobalServiceContext()->getFastClockSource()->now();
          // Check if we have done this less than a day ago, and return if that's the case.
          auto appNamesToRemove = [&] {
              std::vector<std::string> toRemove;
              std::shared_lock sLock(_mutex);
              for (const auto& [appName, versionTimestamps] : _apiVersionMetrics) {
                  for (const auto& ts : versionTimestamps.timestamps) {
                      if (ts.loadRelaxed() + Days(1) >= now) {
                          continue;
                      }
                      toRemove.push_back(appName);
                  }
              }
              return toRemove;
          }();
      
          if (MONGO_unlikely(!appNamesToRemove.empty())) {
              stdx::lock_guard xLock(_mutex);
              // Remove entries that are too stale and marked for removal.
          }
      }
      

            Assignee:
            blake.oler@mongodb.com Blake Oler
            Reporter:
            amirsaman.memaripour@mongodb.com Amirsaman Memaripour
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated:
              Resolved: