1 //  Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
2 //  This source code is licensed under both the GPLv2 (found in the
3 //  COPYING file in the root directory) and Apache 2.0 License
4 //  (found in the LICENSE.Apache file in the root directory).
5 
6 #pragma once
7 
8 #include <array>
9 #include <cstdint>
10 #include <memory>
11 #include <mutex>
12 
13 #include "cache/cache_helpers.h"
14 #include "port/lang.h"
15 #include "rocksdb/cache.h"
16 #include "rocksdb/status.h"
17 #include "rocksdb/system_clock.h"
18 #include "test_util/sync_point.h"
19 #include "util/coding_lean.h"
20 
21 namespace ROCKSDB_NAMESPACE {
22 
23 // A generic helper object for gathering stats about cache entries by
24 // iterating over them with ApplyToAllEntries. This class essentially
25 // solves the problem of slowing down a Cache with too many stats
26 // collectors that could be sharing stat results, such as from multiple
27 // column families or multiple DBs sharing a Cache. We employ a few
28 // mitigations:
29 // * Only one collector for a particular kind of Stats is alive
30 // for each Cache. This is guaranteed using the Cache itself to hold
31 // the collector.
32 // * A mutex ensures only one thread is gathering stats for this
33 // collector.
34 // * The most recent gathered stats are saved and simply copied to
35 // satisfy requests within a time window (default: 3 minutes) of
36 // completion of the most recent stat gathering.
37 //
38 // Template parameter Stats must be copyable and trivially constructable,
39 // as well as...
40 // concept Stats {
41 //   // Notification before applying callback to all entries
42 //   void BeginCollection(Cache*, SystemClock*, uint64_t start_time_micros);
43 //   // Get the callback to apply to all entries. `callback`
44 //   // type must be compatible with Cache::ApplyToAllEntries
45 //   callback GetEntryCallback();
46 //   // Notification after applying callback to all entries
47 //   void EndCollection(Cache*, SystemClock*, uint64_t end_time_micros);
48 //   // Notification that a collection was skipped because of
49 //   // sufficiently recent saved results.
50 //   void SkippedCollection();
51 // }
52 template <class Stats>
53 class CacheEntryStatsCollector {
54  public:
55   // Gather and save stats if saved stats are too old. (Use GetStats() to
56   // read saved stats.)
57   //
58   // Maximum allowed age for a "hit" on saved results is determined by the
59   // two interval parameters. Both set to 0 forces a re-scan. For example
60   // with min_interval_seconds=300 and min_interval_factor=100, if the last
61   // scan took 10s, we would only rescan ("miss") if the age in seconds of
62   // the saved results is > max(300, 100*10).
63   // Justification: scans can vary wildly in duration, e.g. from 0.02 sec
64   // to as much as 20 seconds, so we want to be able to cap the absolute
65   // and relative frequency of scans.
CollectStats(int min_interval_seconds,int min_interval_factor)66   void CollectStats(int min_interval_seconds, int min_interval_factor) {
67     // Waits for any pending reader or writer (collector)
68     std::lock_guard<std::mutex> lock(working_mutex_);
69 
70     uint64_t max_age_micros =
71         static_cast<uint64_t>(std::max(min_interval_seconds, 0)) * 1000000U;
72 
73     if (last_end_time_micros_ > last_start_time_micros_ &&
74         min_interval_factor > 0) {
75       max_age_micros = std::max(
76           max_age_micros, min_interval_factor * (last_end_time_micros_ -
77                                                  last_start_time_micros_));
78     }
79 
80     uint64_t start_time_micros = clock_->NowMicros();
81     if ((start_time_micros - last_end_time_micros_) > max_age_micros) {
82       last_start_time_micros_ = start_time_micros;
83       working_stats_.BeginCollection(cache_, clock_, start_time_micros);
84 
85       cache_->ApplyToAllEntries(working_stats_.GetEntryCallback(), {});
86       TEST_SYNC_POINT_CALLBACK(
87           "CacheEntryStatsCollector::GetStats:AfterApplyToAllEntries", nullptr);
88 
89       uint64_t end_time_micros = clock_->NowMicros();
90       last_end_time_micros_ = end_time_micros;
91       working_stats_.EndCollection(cache_, clock_, end_time_micros);
92     } else {
93       working_stats_.SkippedCollection();
94     }
95 
96     // Save so that we don't need to wait for an outstanding collection in
97     // order to make of copy of the last saved stats
98     std::lock_guard<std::mutex> lock2(saved_mutex_);
99     saved_stats_ = working_stats_;
100   }
101 
102   // Gets saved stats, regardless of age
GetStats(Stats * stats)103   void GetStats(Stats *stats) {
104     std::lock_guard<std::mutex> lock(saved_mutex_);
105     *stats = saved_stats_;
106   }
107 
GetCache()108   Cache *GetCache() const { return cache_; }
109 
110   // Gets or creates a shared instance of CacheEntryStatsCollector in the
111   // cache itself, and saves into `ptr`. This shared_ptr will hold the
112   // entry in cache until all refs are destroyed.
GetShared(Cache * cache,SystemClock * clock,std::shared_ptr<CacheEntryStatsCollector> * ptr)113   static Status GetShared(Cache *cache, SystemClock *clock,
114                           std::shared_ptr<CacheEntryStatsCollector> *ptr) {
115     std::array<uint64_t, 3> cache_key_data{
116         {// First 16 bytes == md5 of class name
117          0x7eba5a8fb5437c90U, 0x8ca68c9b11655855U,
118          // Last 8 bytes based on a function pointer to make unique for each
119          // template instantiation
120          reinterpret_cast<uint64_t>(&CacheEntryStatsCollector::GetShared)}};
121     Slice cache_key = GetSlice(&cache_key_data);
122 
123     Cache::Handle *h = cache->Lookup(cache_key);
124     if (h == nullptr) {
125       // Not yet in cache, but Cache doesn't provide a built-in way to
126       // avoid racing insert. So we double-check under a shared mutex,
127       // inspired by TableCache.
128       STATIC_AVOID_DESTRUCTION(std::mutex, static_mutex);
129       std::lock_guard<std::mutex> lock(static_mutex);
130 
131       h = cache->Lookup(cache_key);
132       if (h == nullptr) {
133         auto new_ptr = new CacheEntryStatsCollector(cache, clock);
134         // TODO: non-zero charge causes some tests that count block cache
135         // usage to go flaky. Fix the problem somehow so we can use an
136         // accurate charge.
137         size_t charge = 0;
138         Status s = cache->Insert(cache_key, new_ptr, charge, Deleter, &h,
139                                  Cache::Priority::HIGH);
140         if (!s.ok()) {
141           assert(h == nullptr);
142           delete new_ptr;
143           return s;
144         }
145       }
146     }
147     // If we reach here, shared entry is in cache with handle `h`.
148     assert(cache->GetDeleter(h) == Deleter);
149 
150     // Build an aliasing shared_ptr that keeps `ptr` in cache while there
151     // are references.
152     *ptr = MakeSharedCacheHandleGuard<CacheEntryStatsCollector>(cache, h);
153     return Status::OK();
154   }
155 
156  private:
CacheEntryStatsCollector(Cache * cache,SystemClock * clock)157   explicit CacheEntryStatsCollector(Cache *cache, SystemClock *clock)
158       : saved_stats_(),
159         working_stats_(),
160         last_start_time_micros_(0),
161         last_end_time_micros_(/*pessimistic*/ 10000000),
162         cache_(cache),
163         clock_(clock) {}
164 
Deleter(const Slice &,void * value)165   static void Deleter(const Slice &, void *value) {
166     delete static_cast<CacheEntryStatsCollector *>(value);
167   }
168 
169   std::mutex saved_mutex_;
170   Stats saved_stats_;
171 
172   std::mutex working_mutex_;
173   Stats working_stats_;
174   uint64_t last_start_time_micros_;
175   uint64_t last_end_time_micros_;
176 
177   Cache *const cache_;
178   SystemClock *const clock_;
179 };
180 
181 }  // namespace ROCKSDB_NAMESPACE
182