1
#include "source/extensions/http/cache/file_system_http_cache/cache_eviction_thread.h"
2

            
3
#include <limits>
4

            
5
#include "envoy/thread/thread.h"
6

            
7
#include "source/common/api/os_sys_calls_impl.h"
8
#include "source/common/filesystem/directory.h"
9
#include "source/extensions/http/cache/file_system_http_cache/file_system_http_cache.h"
10

            
11
namespace Envoy {
12
namespace Extensions {
13
namespace HttpFilters {
14
namespace Cache {
15
namespace FileSystemHttpCache {
16

            
17
namespace {
18
127
bool isCacheFile(const Filesystem::DirectoryEntry& entry) {
19
127
  return entry.type_ == Filesystem::FileType::Regular && absl::StartsWith(entry.name_, "cache-");
20
127
}
21
} // namespace
22

            
23
CacheEvictionThread::CacheEvictionThread(Thread::ThreadFactory& thread_factory)
24
62
    : thread_(thread_factory.createThread([this]() { work(); })) {}
25

            
26
62
CacheEvictionThread::~CacheEvictionThread() {
27
62
  terminate();
28
62
  thread_->join();
29
62
}
30

            
31
63
void CacheEvictionThread::addCache(std::shared_ptr<CacheShared> cache) {
32
63
  {
33
63
    absl::MutexLock lock(cache_mu_);
34
63
    bool inserted = caches_.emplace(std::move(cache)).second;
35
63
    ASSERT(inserted);
36
63
  }
37
  // Signal to unblock CacheEvictionThread to perform the initial cache measurement
38
  // (and possibly eviction if it's starting out oversized!)
39
63
  signal();
40
63
}
41

            
42
63
void CacheEvictionThread::removeCache(std::shared_ptr<CacheShared>& cache) {
43
63
  absl::MutexLock lock(cache_mu_);
44
63
  bool removed = caches_.erase(cache);
45
63
  ASSERT(removed);
46
63
}
47

            
48
66
void CacheEvictionThread::signal() {
49
66
  absl::MutexLock lock(mu_);
50
66
  signalled_ = true;
51
66
}
52

            
53
62
void CacheEvictionThread::terminate() {
54
62
  absl::MutexLock lock(mu_);
55
62
  terminating_ = true;
56
62
  signalled_ = true;
57
62
}
58

            
59
119
bool CacheEvictionThread::waitForSignal() {
60
119
  absl::MutexLock lock(mu_);
61
  // Worth noting here that if `signalled_` is already true, the lock is not released
62
  // until idle_ is false again, so waitForIdle will not return until `signalled_`
63
  // stays false for the duration of an eviction cycle.
64
119
  idle_ = true;
65
119
  mu_.Await(absl::Condition(&signalled_));
66
119
  signalled_ = false;
67
119
  idle_ = false;
68
119
  return !terminating_;
69
119
}
70

            
71
55
void CacheShared::initStats() {
72
55
  if (config_.has_max_cache_size_bytes()) {
73
2
    stats_.size_limit_bytes_.set(config_.max_cache_size_bytes().value());
74
2
  }
75
55
  if (config_.has_max_cache_entry_count()) {
76
2
    stats_.size_limit_count_.set(config_.max_cache_entry_count().value());
77
2
  }
78
  // TODO(ravenblack): Add support for directory tree structure.
79
116
  for (const Filesystem::DirectoryEntry& entry : Filesystem::Directory(std::string{cachePath()})) {
80
116
    if (!isCacheFile(entry)) {
81
110
      continue;
82
110
    }
83
6
    size_count_++;
84
6
    size_bytes_ += entry.size_bytes_.value_or(0);
85
6
  }
86
55
  stats_.size_count_.set(size_count_);
87
55
  stats_.size_bytes_.set(size_bytes_);
88
55
  needs_init_ = false;
89
55
}
90

            
91
2
void CacheShared::evict() {
92
2
  stats_.eviction_runs_.add(1);
93
2
  auto os_sys_calls = Api::OsSysCallsSingleton::get();
94
2
  uint64_t size = 0;
95
2
  uint64_t count = 0;
96
2
  struct CacheFile {
97
2
    std::string name_;
98
2
    uint64_t size_;
99
2
    Envoy::SystemTime last_touch_;
100
2
  };
101
2
  std::vector<CacheFile> cache_files;
102

            
103
  // TODO(ravenblack): Add support for directory tree structure.
104
11
  for (const Filesystem::DirectoryEntry& entry : Filesystem::Directory(std::string{cachePath()})) {
105
11
    if (!isCacheFile(entry)) {
106
4
      continue;
107
4
    }
108
7
    count++;
109
7
    size += entry.size_bytes_.value_or(0);
110
7
    struct stat s;
111
7
    if (os_sys_calls.stat(absl::StrCat(cachePath(), entry.name_).c_str(), &s).return_value_ != -1) {
112
#ifdef _DARWIN_FEATURE_64_BIT_INODE
113
      Envoy::SystemTime last_touch =
114
          std::max(timespecToChrono(s.st_atimespec), timespecToChrono(s.st_ctimespec));
115
#else
116
7
      Envoy::SystemTime last_touch =
117
7
          std::max(timespecToChrono(s.st_atim), timespecToChrono(s.st_ctim));
118
7
#endif
119

            
120
7
      cache_files.push_back(CacheFile{entry.name_, entry.size_bytes_.value_or(0), last_touch});
121
7
    }
122
7
  }
123
  // Sort the vector by last-touch timestamp, highest (i.e. youngest) first.
124
6
  std::sort(cache_files.begin(), cache_files.end(), [](CacheFile& a, CacheFile& b) {
125
6
    return std::tie(a.last_touch_, a.name_) > std::tie(b.last_touch_, b.name_);
126
6
  });
127
2
  size_bytes_ = size;
128
2
  size_count_ = count;
129
2
  stats_.size_bytes_.set(size);
130
2
  stats_.size_count_.set(count);
131
2
  uint64_t size_kept = 0;
132
2
  uint64_t count_kept = 0;
133
2
  uint64_t max_size = config_.has_max_cache_size_bytes() ? config_.max_cache_size_bytes().value()
134
2
                                                         : std::numeric_limits<uint64_t>::max();
135
2
  uint64_t max_count = config_.has_max_cache_entry_count() ? config_.max_cache_entry_count().value()
136
2
                                                           : std::numeric_limits<uint64_t>::max();
137
2
  auto it = cache_files.begin();
138
  // Keep the youngest files that won't exceed the limit.
139
5
  while (it != cache_files.end() && size_kept + it->size_ <= max_size &&
140
5
         count_kept + 1 <= max_count) {
141
3
    size_kept += it->size_;
142
3
    count_kept++;
143
3
    ++it;
144
3
  }
145
  // Evict the rest.
146
6
  while (it != cache_files.end()) {
147
4
    if (os_sys_calls.unlink(absl::StrCat(cachePath(), it->name_).c_str()).return_value_ != -1) {
148
      // May want to add logging here for cache eviction failure, but it's expected sometimes,
149
      // e.g. if another instance of Envoy is performing cleanup at the same time, or some external
150
      // operator deleted the file. If it fails we don't reduce the estimated cache size, so another
151
      // eviction run will happen sooner.
152
      // TODO(ravenblack): might be worth checking the type of the error, or whether the file is
153
      // gone - if there's a permissions issue, for example, then the cache might remain oversized
154
      // and the eviction thread will be churning, trying and failing to remove a file, which would
155
      // be worth logging a warning, versus if the file is already gone then there's no problem.
156
4
      trackFileRemoved(it->size_);
157
4
    }
158
4
    ++it;
159
4
  }
160
2
}
161

            
162
62
void CacheEvictionThread::work() {
163
62
  ENVOY_LOG(info, "Starting cache eviction thread.");
164
119
  while (waitForSignal()) {
165
57
    absl::flat_hash_set<std::shared_ptr<CacheShared>> caches;
166
57
    {
167
      // Take a local copy of the set of caches, so we don't hold the lock while
168
      // work is being performed.
169
57
      absl::MutexLock lock(cache_mu_);
170
57
      caches = caches_;
171
57
    }
172

            
173
57
    for (const std::shared_ptr<CacheShared>& cache : caches) {
174
57
      if (cache->needs_init_) {
175
55
        cache->initStats();
176
55
      }
177
57
      if (cache->needsEviction()) {
178
2
        cache->evict();
179
2
      }
180
57
    }
181
57
  }
182
62
  ENVOY_LOG(info, "Ending cache eviction thread.");
183
62
}
184

            
185
6
void CacheEvictionThread::waitForIdle() {
186
6
  absl::MutexLock lock(mu_);
187
24
  auto cond = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_) { return idle_ && !signalled_; };
188
6
  mu_.Await(absl::Condition(&cond));
189
6
}
190

            
191
} // namespace FileSystemHttpCache
192
} // namespace Cache
193
} // namespace HttpFilters
194
} // namespace Extensions
195
} // namespace Envoy