]>
Commit | Line | Data |
---|---|---|
213d9883 OL |
1 | // Copyright (C) 2020 Joel Rosdahl and other contributors |
2 | // | |
3 | // See doc/AUTHORS.adoc for a complete list of contributors. | |
4 | // | |
5 | // This program is free software; you can redistribute it and/or modify it | |
6 | // under the terms of the GNU General Public License as published by the Free | |
7 | // Software Foundation; either version 3 of the License, or (at your option) | |
8 | // any later version. | |
9 | // | |
10 | // This program is distributed in the hope that it will be useful, but WITHOUT | |
11 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
12 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | |
13 | // more details. | |
14 | // | |
15 | // You should have received a copy of the GNU General Public License along with | |
16 | // this program; if not, write to the Free Software Foundation, Inc., 51 | |
17 | // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
18 | ||
19 | #include "InodeCache.hpp" | |
20 | ||
2354adc2 JR |
21 | #include "Config.hpp" |
22 | #include "Fd.hpp" | |
23 | #include "Finalizer.hpp" | |
a5021452 | 24 | #include "Hash.hpp" |
2354adc2 | 25 | #include "Stat.hpp" |
53b35a42 | 26 | #include "TemporaryFile.hpp" |
2354adc2 JR |
27 | #include "Util.hpp" |
28 | #include "ccache.hpp" | |
2354adc2 JR |
29 | #include "logging.hpp" |
30 | ||
31 | #include <atomic> | |
32 | #include <libgen.h> | |
33 | #include <sys/mman.h> | |
34 | #include <type_traits> | |
213d9883 | 35 | |
213d9883 | 36 | // The inode cache resides on a file that is mapped into shared memory by |
88699012 JR |
37 | // running processes. It is implemented as a two level structure, where the top |
38 | // level is a hash table consisting of buckets. Each bucket contains entries | |
213d9883 OL |
39 | // that are sorted in LRU order. Entries map from keys representing files to |
40 | // cached hash results. | |
41 | // | |
42 | // Concurrent access is guarded by a mutex in each bucket. | |
43 | // | |
44 | // Current cache size is fixed and the given constants are considered large | |
45 | // enough for most projects. The size could be made configurable if there is a | |
46 | // demand for it. | |
213d9883 | 47 | |
88699012 JR |
48 | namespace { |
49 | ||
50 | // The version number corresponds to the format of the cache entries and to | |
51 | // semantics of the key fields. | |
52 | // | |
53 | // Note: The key is hashed using the main hash algorithm, so the version number | |
54 | // does not need to be incremented if said algorithm is changed (except if the | |
55 | // digest size changes since that affects the entry format). | |
56 | const uint32_t k_version = 1; | |
57 | ||
58 | // Note: Increment the version number if constants affecting storage size are | |
59 | // changed. | |
213d9883 OL |
60 | const uint32_t k_num_buckets = 32 * 1024; |
61 | const uint32_t k_num_entries = 4; | |
62 | ||
0a189c2c | 63 | static_assert(Digest::size() == 20, |
213d9883 | 64 | "Increment version number if size of digest is changed."); |
0a189c2c JR |
65 | static_assert(std::is_trivially_copyable<Digest>::value, |
66 | "Digest is expected to be trivially copyable."); | |
213d9883 OL |
67 | |
68 | static_assert( | |
69 | static_cast<int>(InodeCache::ContentType::binary) == 0, | |
70 | "Numeric value is part of key, increment version number if changed."); | |
71 | static_assert( | |
72 | static_cast<int>(InodeCache::ContentType::code) == 1, | |
73 | "Numeric value is part of key, increment version number if changed."); | |
74 | static_assert( | |
75 | static_cast<int>(InodeCache::ContentType::code_with_sloppy_time_macros) == 2, | |
76 | "Numeric value is part of key, increment version number if changed."); | |
77 | static_assert( | |
78 | static_cast<int>(InodeCache::ContentType::precompiled_header) == 3, | |
79 | "Numeric value is part of key, increment version number if changed."); | |
80 | ||
81 | } // namespace | |
82 | ||
83 | struct InodeCache::Key | |
84 | { | |
85 | ContentType type; | |
86 | dev_t st_dev; | |
87 | ino_t st_ino; | |
88 | mode_t st_mode; | |
2354adc2 | 89 | #ifdef HAVE_STRUCT_STAT_ST_MTIM |
213d9883 | 90 | timespec st_mtim; |
2354adc2 | 91 | #else |
213d9883 | 92 | time_t st_mtim; |
2354adc2 JR |
93 | #endif |
94 | #ifdef HAVE_STRUCT_STAT_ST_CTIM | |
213d9883 | 95 | timespec st_ctim; // Included for sanity checking. |
2354adc2 | 96 | #else |
213d9883 | 97 | time_t st_ctim; // Included for sanity checking. |
2354adc2 | 98 | #endif |
213d9883 OL |
99 | off_t st_size; // Included for sanity checking. |
100 | bool sloppy_time_macros; | |
101 | }; | |
102 | ||
103 | struct InodeCache::Entry | |
104 | { | |
0a189c2c JR |
105 | Digest key_digest; // Hashed key |
106 | Digest file_digest; // Cached file hash | |
213d9883 OL |
107 | int return_value; // Cached return value |
108 | }; | |
109 | ||
110 | struct InodeCache::Bucket | |
111 | { | |
112 | pthread_mutex_t mt; | |
113 | Entry entries[k_num_entries]; | |
114 | }; | |
115 | ||
116 | struct InodeCache::SharedRegion | |
117 | { | |
118 | uint32_t version; | |
119 | std::atomic<int64_t> hits; | |
120 | std::atomic<int64_t> misses; | |
121 | std::atomic<int64_t> errors; | |
122 | Bucket buckets[k_num_buckets]; | |
123 | }; | |
124 | ||
125 | bool | |
126 | InodeCache::mmap_file(const std::string& inode_cache_file) | |
127 | { | |
128 | if (m_sr) { | |
129 | munmap(m_sr, sizeof(SharedRegion)); | |
130 | m_sr = nullptr; | |
131 | } | |
e071bcfd JR |
132 | Fd fd(open(inode_cache_file.c_str(), O_RDWR)); |
133 | if (!fd) { | |
213d9883 OL |
134 | cc_log("Failed to open inode cache %s: %s", |
135 | inode_cache_file.c_str(), | |
136 | strerror(errno)); | |
137 | return false; | |
138 | } | |
139 | bool is_nfs; | |
e071bcfd | 140 | if (Util::is_nfs_fd(*fd, &is_nfs) == 0 && is_nfs) { |
213d9883 OL |
141 | cc_log( |
142 | "Inode cache not supported because the cache file is located on nfs: %s", | |
143 | inode_cache_file.c_str()); | |
213d9883 OL |
144 | return false; |
145 | } | |
146 | SharedRegion* sr = reinterpret_cast<SharedRegion*>(mmap( | |
e071bcfd JR |
147 | nullptr, sizeof(SharedRegion), PROT_READ | PROT_WRITE, MAP_SHARED, *fd, 0)); |
148 | fd.close(); | |
213d9883 OL |
149 | if (sr == reinterpret_cast<void*>(-1)) { |
150 | cc_log("Failed to mmap %s: %s", inode_cache_file.c_str(), strerror(errno)); | |
151 | return false; | |
152 | } | |
153 | // Drop the file from disk if the found version is not matching. This will | |
154 | // allow a new file to be generated. | |
155 | if (sr->version != k_version) { | |
156 | cc_log( | |
77bf7bc3 JR |
157 | "Dropping inode cache because found version %u does not match expected" |
158 | " version %u", | |
213d9883 OL |
159 | sr->version, |
160 | k_version); | |
161 | munmap(sr, sizeof(SharedRegion)); | |
162 | unlink(inode_cache_file.c_str()); | |
163 | return false; | |
164 | } | |
165 | m_sr = sr; | |
166 | if (m_config.debug()) { | |
167 | cc_log("inode cache file loaded: %s", inode_cache_file.c_str()); | |
168 | } | |
169 | return true; | |
170 | } | |
171 | ||
172 | bool | |
0a189c2c | 173 | InodeCache::hash_inode(const char* path, ContentType type, Digest& digest) |
213d9883 OL |
174 | { |
175 | Stat stat = Stat::stat(path); | |
176 | if (!stat) { | |
177 | cc_log("Could not stat %s: %s", path, strerror(stat.error_number())); | |
178 | return false; | |
179 | } | |
180 | ||
181 | Key key; | |
182 | memset(&key, 0, sizeof(Key)); | |
183 | key.type = type; | |
184 | key.st_dev = stat.device(); | |
185 | key.st_ino = stat.inode(); | |
186 | key.st_mode = stat.mode(); | |
2354adc2 | 187 | #ifdef HAVE_STRUCT_STAT_ST_MTIM |
213d9883 | 188 | key.st_mtim = stat.mtim(); |
2354adc2 | 189 | #else |
213d9883 | 190 | key.st_mtim = stat.mtime(); |
2354adc2 JR |
191 | #endif |
192 | #ifdef HAVE_STRUCT_STAT_ST_CTIM | |
213d9883 | 193 | key.st_ctim = stat.ctim(); |
2354adc2 | 194 | #else |
213d9883 | 195 | key.st_ctim = stat.ctime(); |
2354adc2 | 196 | #endif |
213d9883 OL |
197 | key.st_size = stat.size(); |
198 | ||
a5021452 JR |
199 | Hash hash; |
200 | hash.hash(&key, sizeof(Key)); | |
201 | digest = hash.digest(); | |
213d9883 OL |
202 | return true; |
203 | } | |
204 | ||
205 | InodeCache::Bucket* | |
206 | InodeCache::acquire_bucket(uint32_t index) | |
207 | { | |
208 | Bucket* bucket = &m_sr->buckets[index]; | |
209 | int err = pthread_mutex_lock(&bucket->mt); | |
2354adc2 | 210 | #ifdef PTHREAD_MUTEX_ROBUST |
213d9883 | 211 | if (err == EOWNERDEAD) { |
77bf7bc3 | 212 | if (m_config.debug()) { |
213d9883 | 213 | ++m_sr->errors; |
77bf7bc3 | 214 | } |
213d9883 OL |
215 | err = pthread_mutex_consistent(&bucket->mt); |
216 | if (err) { | |
217 | cc_log( | |
218 | "Can't consolidate stale mutex at index %u: %s", index, strerror(err)); | |
219 | cc_log("Consider removing the inode cache file if the problem persists"); | |
220 | return nullptr; | |
221 | } | |
222 | cc_log("Wiping bucket at index %u because of stale mutex", index); | |
223 | memset(bucket->entries, 0, sizeof(Bucket::entries)); | |
224 | } else { | |
2354adc2 | 225 | #endif |
213d9883 OL |
226 | if (err) { |
227 | cc_log("Failed to lock mutex at index %u: %s", index, strerror(err)); | |
228 | cc_log("Consider removing the inode cache file if problem persists"); | |
229 | ++m_sr->errors; | |
230 | return nullptr; | |
231 | } | |
2354adc2 | 232 | #ifdef PTHREAD_MUTEX_ROBUST |
213d9883 | 233 | } |
2354adc2 | 234 | #endif |
213d9883 OL |
235 | return bucket; |
236 | } | |
237 | ||
238 | InodeCache::Bucket* | |
0a189c2c | 239 | InodeCache::acquire_bucket(const Digest& key_digest) |
213d9883 OL |
240 | { |
241 | uint32_t hash; | |
0a189c2c | 242 | Util::big_endian_to_int(key_digest.bytes(), hash); |
213d9883 OL |
243 | return acquire_bucket(hash % k_num_buckets); |
244 | } | |
245 | ||
246 | void | |
247 | InodeCache::release_bucket(Bucket* bucket) | |
248 | { | |
249 | pthread_mutex_unlock(&bucket->mt); | |
250 | } | |
251 | ||
252 | bool | |
253 | InodeCache::create_new_file(const std::string& filename) | |
254 | { | |
255 | cc_log("Creating a new inode cache"); | |
256 | ||
257 | // Create the new file to a temporary name to prevent other processes from | |
258 | // mapping it before it is fully initialized. | |
53b35a42 | 259 | TemporaryFile tmp_file(filename); |
e071bcfd | 260 | |
53b35a42 | 261 | Finalizer temp_file_remover([&] { unlink(tmp_file.path.c_str()); }); |
e071bcfd | 262 | |
213d9883 | 263 | bool is_nfs; |
53b35a42 | 264 | if (Util::is_nfs_fd(*tmp_file.fd, &is_nfs) == 0 && is_nfs) { |
213d9883 | 265 | cc_log( |
77bf7bc3 JR |
266 | "Inode cache not supported because the cache file would be located on" |
267 | " nfs: %s", | |
213d9883 | 268 | filename.c_str()); |
213d9883 OL |
269 | return false; |
270 | } | |
53b35a42 | 271 | int err = Util::fallocate(*tmp_file.fd, sizeof(SharedRegion)); |
213d9883 OL |
272 | if (err) { |
273 | cc_log("Failed to allocate file space for inode cache: %s", strerror(err)); | |
213d9883 OL |
274 | return false; |
275 | } | |
276 | SharedRegion* sr = | |
277 | reinterpret_cast<SharedRegion*>(mmap(nullptr, | |
278 | sizeof(SharedRegion), | |
279 | PROT_READ | PROT_WRITE, | |
280 | MAP_SHARED, | |
53b35a42 | 281 | *tmp_file.fd, |
213d9883 OL |
282 | 0)); |
283 | if (sr == reinterpret_cast<void*>(-1)) { | |
284 | cc_log("Failed to mmap new inode cache: %s", strerror(errno)); | |
213d9883 OL |
285 | return false; |
286 | } | |
287 | ||
288 | // Initialize new shared region. | |
289 | sr->version = k_version; | |
290 | pthread_mutexattr_t mattr; | |
291 | pthread_mutexattr_init(&mattr); | |
292 | pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); | |
2354adc2 | 293 | #ifdef PTHREAD_MUTEX_ROBUST |
213d9883 | 294 | pthread_mutexattr_setrobust(&mattr, PTHREAD_MUTEX_ROBUST); |
2354adc2 | 295 | #endif |
62a4755c JR |
296 | for (auto& bucket : sr->buckets) { |
297 | pthread_mutex_init(&bucket.mt, &mattr); | |
213d9883 OL |
298 | } |
299 | ||
300 | munmap(sr, sizeof(SharedRegion)); | |
53b35a42 | 301 | tmp_file.fd.close(); |
213d9883 OL |
302 | |
303 | // link() will fail silently if a file with the same name already exists. | |
304 | // This will be the case if two processes try to create a new file | |
305 | // simultaneously. Thus close the current file handle and reopen a new one, | |
306 | // which will make us use the first created file even if we didn't win the | |
307 | // race. | |
53b35a42 | 308 | if (link(tmp_file.path.c_str(), filename.c_str()) != 0) { |
213d9883 | 309 | cc_log("Failed to link new inode cache: %s", strerror(errno)); |
213d9883 OL |
310 | return false; |
311 | } | |
312 | ||
213d9883 OL |
313 | return true; |
314 | } | |
315 | ||
316 | bool | |
317 | InodeCache::initialize() | |
318 | { | |
319 | if (m_failed || !m_config.inode_cache()) { | |
320 | return false; | |
321 | } | |
322 | ||
323 | if (m_sr) { | |
324 | return true; | |
325 | } | |
326 | ||
327 | std::string filename = get_file(); | |
328 | if (m_sr || mmap_file(filename)) { | |
329 | return true; | |
330 | } | |
331 | ||
332 | // Try to create a new cache if we failed to map an existing file. | |
333 | create_new_file(filename); | |
334 | ||
335 | // Concurrent processes could try to create new files simultaneously and the | |
336 | // file that actually landed on disk will be from the process that won the | |
337 | // race. Thus we try to open the file from disk instead of reusing the file | |
338 | // handle to the file we just created. | |
339 | if (mmap_file(filename)) { | |
340 | return true; | |
341 | } | |
342 | ||
343 | m_failed = true; | |
344 | return false; | |
345 | } | |
346 | ||
77bf7bc3 | 347 | InodeCache::InodeCache(const Config& config) : m_config(config) |
213d9883 OL |
348 | { |
349 | } | |
350 | ||
351 | InodeCache::~InodeCache() | |
352 | { | |
353 | if (m_sr) { | |
354 | munmap(m_sr, sizeof(SharedRegion)); | |
355 | } | |
356 | } | |
357 | ||
358 | bool | |
359 | InodeCache::get(const char* path, | |
360 | ContentType type, | |
0a189c2c | 361 | Digest& file_digest, |
213d9883 OL |
362 | int* return_value) |
363 | { | |
364 | if (!initialize()) { | |
365 | return false; | |
366 | } | |
367 | ||
0a189c2c JR |
368 | Digest key_digest; |
369 | if (!hash_inode(path, type, key_digest)) { | |
213d9883 OL |
370 | return false; |
371 | } | |
372 | ||
373 | Bucket* bucket = acquire_bucket(key_digest); | |
374 | ||
375 | if (!bucket) { | |
376 | return false; | |
377 | } | |
378 | ||
379 | bool found = false; | |
380 | ||
381 | for (uint32_t i = 0; i < k_num_entries; ++i) { | |
0a189c2c | 382 | if (bucket->entries[i].key_digest == key_digest) { |
213d9883 OL |
383 | if (i > 0) { |
384 | Entry tmp = bucket->entries[i]; | |
385 | memmove(&bucket->entries[1], &bucket->entries[0], sizeof(Entry) * i); | |
386 | bucket->entries[0] = tmp; | |
387 | } | |
388 | ||
0a189c2c | 389 | file_digest = bucket->entries[0].file_digest; |
213d9883 OL |
390 | if (return_value) { |
391 | *return_value = bucket->entries[0].return_value; | |
392 | } | |
393 | found = true; | |
394 | break; | |
395 | } | |
396 | } | |
397 | release_bucket(bucket); | |
398 | ||
399 | cc_log("inode cache %s: %s", found ? "hit" : "miss", path); | |
400 | ||
401 | if (m_config.debug()) { | |
402 | if (found) { | |
403 | ++m_sr->hits; | |
404 | } else { | |
405 | ++m_sr->misses; | |
406 | } | |
407 | cc_log( | |
408 | "accumulated stats for inode cache: hits=%ld, misses=%ld, errors=%ld", | |
409 | static_cast<long>(m_sr->hits.load()), | |
410 | static_cast<long>(m_sr->misses.load()), | |
411 | static_cast<long>(m_sr->errors.load())); | |
412 | } | |
413 | return found; | |
414 | } | |
415 | ||
416 | bool | |
417 | InodeCache::put(const char* path, | |
418 | ContentType type, | |
0a189c2c | 419 | const Digest& file_digest, |
213d9883 OL |
420 | int return_value) |
421 | { | |
422 | if (!initialize()) { | |
423 | return false; | |
424 | } | |
425 | ||
0a189c2c JR |
426 | Digest key_digest; |
427 | if (!hash_inode(path, type, key_digest)) { | |
213d9883 OL |
428 | return false; |
429 | } | |
430 | ||
431 | Bucket* bucket = acquire_bucket(key_digest); | |
432 | ||
433 | if (!bucket) { | |
434 | return false; | |
435 | } | |
436 | ||
437 | memmove(&bucket->entries[1], | |
438 | &bucket->entries[0], | |
439 | sizeof(Entry) * (k_num_entries - 1)); | |
440 | ||
441 | bucket->entries[0].key_digest = key_digest; | |
442 | bucket->entries[0].file_digest = file_digest; | |
443 | bucket->entries[0].return_value = return_value; | |
444 | ||
445 | release_bucket(bucket); | |
446 | ||
447 | cc_log("inode cache insert: %s", path); | |
448 | ||
449 | return true; | |
450 | } | |
451 | ||
452 | bool | |
453 | InodeCache::drop() | |
454 | { | |
455 | std::string file = get_file(); | |
456 | if (unlink(file.c_str()) != 0) { | |
457 | return false; | |
458 | } | |
459 | if (m_sr) { | |
460 | munmap(m_sr, sizeof(SharedRegion)); | |
461 | m_sr = nullptr; | |
462 | } | |
463 | return true; | |
464 | } | |
465 | ||
466 | std::string | |
467 | InodeCache::get_file() | |
468 | { | |
469 | return fmt::format("{}/inode-cache.v{}", m_config.temporary_dir(), k_version); | |
470 | } | |
471 | ||
472 | int64_t | |
473 | InodeCache::get_hits() | |
474 | { | |
475 | return initialize() ? m_sr->hits.load() : -1; | |
476 | } | |
477 | ||
478 | int64_t | |
479 | InodeCache::get_misses() | |
480 | { | |
481 | return initialize() ? m_sr->misses.load() : -1; | |
482 | } | |
483 | ||
484 | int64_t | |
485 | InodeCache::get_errors() | |
486 | { | |
487 | return initialize() ? m_sr->errors.load() : -1; | |
488 | } |