Skip to content

Commit f9b19d7

Browse files
joyeecheungtargos
authored andcommitted
module: write compile cache to temporary file and then rename it
This works better in terms of avoiding race conditions. PR-URL: #54971 Fixes: #54770 Fixes: #54465 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0876f78 commit f9b19d7

File tree

4 files changed

+107
-20
lines changed

4 files changed

+107
-20
lines changed

src/compile_cache.cc

+96-19
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(
219219
return loaded->second.get();
220220
}
221221

222+
// If the code hash mismatches, the code has changed, discard the stale entry
223+
// and create a new one.
222224
auto emplaced =
223225
compiler_cache_store_.emplace(key, std::make_unique<CompileCacheEntry>());
224226
auto* result = emplaced.first->second.get();
@@ -287,23 +289,26 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry,
287289
MaybeSaveImpl(entry, func, rejected);
288290
}
289291

290-
// Layout of a cache file:
291-
// [uint32_t] magic number
292-
// [uint32_t] code size
293-
// [uint32_t] code hash
294-
// [uint32_t] cache size
295-
// [uint32_t] cache hash
296-
// .... compile cache content ....
292+
/**
293+
* Persist the compile cache accumulated in memory to disk.
294+
*
295+
* To avoid race conditions, the cache file includes hashes of the original
296+
* source code and the cache content. It's first written to a temporary file
297+
* before being renamed to the target name.
298+
*
299+
* Layout of a cache file:
300+
* [uint32_t] magic number
301+
* [uint32_t] code size
302+
* [uint32_t] code hash
303+
* [uint32_t] cache size
304+
* [uint32_t] cache hash
305+
* .... compile cache content ....
306+
*/
297307
void CompileCacheHandler::Persist() {
298308
DCHECK(!compile_cache_dir_.empty());
299309

300-
// NOTE(joyeecheung): in most circumstances the code caching reading
301-
// writing logic is lenient enough that it's fine even if someone
302-
// overwrites the cache (that leads to either size or hash mismatch
303-
// in subsequent loads and the overwritten cache will be ignored).
304-
// Also in most use cases users should not change the files on disk
305-
// too rapidly. Therefore locking is not currently implemented to
306-
// avoid the cost.
310+
// TODO(joyeecheung): do this using a separate event loop to utilize the
311+
// libuv thread pool and do the file system operations concurrently.
307312
for (auto& pair : compiler_cache_store_) {
308313
auto* entry = pair.second.get();
309314
if (entry->cache == nullptr) {
@@ -316,6 +321,11 @@ void CompileCacheHandler::Persist() {
316321
entry->source_filename);
317322
continue;
318323
}
324+
if (entry->persisted == true) {
325+
Debug("[compile cache] skip %s because cache was already persisted\n",
326+
entry->source_filename);
327+
continue;
328+
}
319329

320330
DCHECK_EQ(entry->cache->buffer_policy,
321331
v8::ScriptCompiler::CachedData::BufferOwned);
@@ -332,27 +342,94 @@ void CompileCacheHandler::Persist() {
332342
headers[kCodeHashOffset] = entry->code_hash;
333343
headers[kCacheHashOffset] = cache_hash;
334344

335-
Debug("[compile cache] writing cache for %s in %s [%d %d %d %d %d]...",
345+
// Generate the temporary filename.
346+
// The temporary file should be placed in a location like:
347+
//
348+
// $NODE_COMPILE_CACHE_DIR/v23.0.0-pre-arm64-5fad6d45-501/e7f8ef7f.cache.tcqrsK
349+
//
350+
// 1. $NODE_COMPILE_CACHE_DIR either comes from the $NODE_COMPILE_CACHE
351+
// environment
352+
// variable or `module.enableCompileCache()`.
353+
// 2. v23.0.0-pre-arm64-5fad6d45-501 is the sub cache directory and
354+
// e7f8ef7f is the hash for the cache (see
355+
// CompileCacheHandler::Enable()),
356+
// 3. tcqrsK is generated by uv_fs_mkstemp() as a temporary indentifier.
357+
uv_fs_t mkstemp_req;
358+
auto cleanup_mkstemp =
359+
OnScopeLeave([&mkstemp_req]() { uv_fs_req_cleanup(&mkstemp_req); });
360+
std::string cache_filename_tmp = entry->cache_filename + ".XXXXXX";
361+
Debug("[compile cache] Creating temporary file for cache of %s...",
362+
entry->source_filename);
363+
int err = uv_fs_mkstemp(
364+
nullptr, &mkstemp_req, cache_filename_tmp.c_str(), nullptr);
365+
if (err < 0) {
366+
Debug("failed. %s\n", uv_strerror(err));
367+
continue;
368+
}
369+
Debug(" -> %s\n", mkstemp_req.path);
370+
Debug("[compile cache] writing cache for %s to temporary file %s [%d %d %d "
371+
"%d %d]...",
336372
entry->source_filename,
337-
entry->cache_filename,
373+
mkstemp_req.path,
338374
headers[kMagicNumberOffset],
339375
headers[kCodeSizeOffset],
340376
headers[kCacheSizeOffset],
341377
headers[kCodeHashOffset],
342378
headers[kCacheHashOffset]);
343379

380+
// Write to the temporary file.
344381
uv_buf_t headers_buf = uv_buf_init(reinterpret_cast<char*>(headers.data()),
345382
headers.size() * sizeof(uint32_t));
346383
uv_buf_t data_buf = uv_buf_init(cache_ptr, entry->cache->length);
347384
uv_buf_t bufs[] = {headers_buf, data_buf};
348385

349-
int err = WriteFileSync(entry->cache_filename.c_str(), bufs, 2);
386+
uv_fs_t write_req;
387+
auto cleanup_write =
388+
OnScopeLeave([&write_req]() { uv_fs_req_cleanup(&write_req); });
389+
err = uv_fs_write(
390+
nullptr, &write_req, mkstemp_req.result, bufs, 2, 0, nullptr);
391+
if (err < 0) {
392+
Debug("failed: %s\n", uv_strerror(err));
393+
continue;
394+
}
395+
396+
uv_fs_t close_req;
397+
auto cleanup_close =
398+
OnScopeLeave([&close_req]() { uv_fs_req_cleanup(&close_req); });
399+
err = uv_fs_close(nullptr, &close_req, mkstemp_req.result, nullptr);
400+
401+
if (err < 0) {
402+
Debug("failed: %s\n", uv_strerror(err));
403+
continue;
404+
}
405+
406+
Debug("success\n");
407+
408+
// Rename the temporary file to the actual cache file.
409+
uv_fs_t rename_req;
410+
auto cleanup_rename =
411+
OnScopeLeave([&rename_req]() { uv_fs_req_cleanup(&rename_req); });
412+
std::string cache_filename_final = entry->cache_filename;
413+
Debug("[compile cache] Renaming %s to %s...",
414+
mkstemp_req.path,
415+
cache_filename_final);
416+
err = uv_fs_rename(nullptr,
417+
&rename_req,
418+
mkstemp_req.path,
419+
cache_filename_final.c_str(),
420+
nullptr);
350421
if (err < 0) {
351422
Debug("failed: %s\n", uv_strerror(err));
352-
} else {
353-
Debug("success\n");
423+
continue;
354424
}
425+
Debug("success\n");
426+
entry->persisted = true;
355427
}
428+
429+
// Clear the map at the end in one go instead of during the iteration to
430+
// avoid rehashing costs.
431+
Debug("[compile cache] Clear deserialized cache.\n");
432+
compiler_cache_store_.clear();
356433
}
357434

358435
CompileCacheHandler::CompileCacheHandler(Environment* env)

src/compile_cache.h

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ struct CompileCacheEntry {
3030
std::string source_filename;
3131
CachedCodeType type;
3232
bool refreshed = false;
33+
bool persisted = false;
34+
3335
// Copy the cache into a new store for V8 to consume. Caller takes
3436
// ownership.
3537
v8::ScriptCompiler::CachedData* CopyCache() const;

src/env.cc

+8-1
Original file line numberDiff line numberDiff line change
@@ -1154,7 +1154,7 @@ CompileCacheEnableResult Environment::EnableCompileCache(
11541154
compile_cache_handler_ = std::move(handler);
11551155
AtExit(
11561156
[](void* env) {
1157-
static_cast<Environment*>(env)->compile_cache_handler()->Persist();
1157+
static_cast<Environment*>(env)->FlushCompileCache();
11581158
},
11591159
this);
11601160
}
@@ -1171,6 +1171,13 @@ CompileCacheEnableResult Environment::EnableCompileCache(
11711171
return result;
11721172
}
11731173

1174+
void Environment::FlushCompileCache() {
1175+
if (!compile_cache_handler_ || compile_cache_handler_->cache_dir().empty()) {
1176+
return;
1177+
}
1178+
compile_cache_handler_->Persist();
1179+
}
1180+
11741181
void Environment::ExitEnv(StopFlags::Flags flags) {
11751182
// Should not access non-thread-safe methods here.
11761183
set_stopping(true);

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,7 @@ class Environment final : public MemoryRetainer {
10451045
// Enable built-in compile cache if it has not yet been enabled.
10461046
// The cache will be persisted to disk on exit.
10471047
CompileCacheEnableResult EnableCompileCache(const std::string& cache_dir);
1048+
void FlushCompileCache();
10481049

10491050
void RunAndClearNativeImmediates(bool only_refed = false);
10501051
void RunAndClearInterrupts();

0 commit comments

Comments
 (0)