Skip to content

Commit 87a25dc

Browse files
authored
[ports] Unpack ports atomically to improve concurrency robustness (#27161)
When building ports in parallel, different processes can race on checking the port's up-to-date status. Currently, we unpack directly into the target directory and then write the marker file. This creates a window of time where the directory exists but is incomplete and lacks the marker. If another process checks the port status during this window, it will conclude the port is invalid/incomplete, and aggressively delete the directory and its cached builds to re-unpack it. This can disrupt other processes currently compiling from that directory or lead to EM_CACHE_IS_LOCKED errors when child processes try to rebuild deleted libraries. By unpacking to a temporary directory and atomically renaming it to the final destination, we ensure that the target directory either does not exist at all or is 100% complete with the marker. This eliminates the partially unpacked state and leverages the atomicity of directory rename (which is highly coherent in OS VFS layers) to reduce the race window to zero. This should help resolve intermittent bot failures on macOS and Windows, where VFS metadata latency can cause processes to see inconsistent directory states. Hopefully this will address #27160, although time will tell.
1 parent 8f23d7b commit 87a25dc

1 file changed

Lines changed: 16 additions & 3 deletions

File tree

tools/ports/__init__.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,22 @@ def retrieve():
373373

374374
def unpack():
375375
logger.info(f'unpacking port: {name}')
376-
utils.safe_ensure_dirs(fullname)
377-
shutil.unpack_archive(filename=fullpath, extract_dir=fullname)
378-
utils.write_file(marker, url + '\n')
376+
unpack_dir = fullname + '.tmp'
377+
# We unpack to a temporary directory and then atomically rename it to the
378+
# final destination. This ensures that the destination directory either
379+
# does not exist or is 100% complete, avoiding races where other processes
380+
# might see a partially unpacked directory (lacking the marker) and
381+
# incorrectly assume it is invalid or needs to be cleared.
382+
utils.delete_dir(unpack_dir)
383+
utils.safe_ensure_dirs(unpack_dir)
384+
385+
shutil.unpack_archive(filename=fullpath, extract_dir=unpack_dir)
386+
tmp_marker = os.path.join(unpack_dir, '.emscripten_url')
387+
utils.write_file(tmp_marker, url + '\n')
388+
389+
# Atomically replace the target directory
390+
utils.delete_dir(fullname)
391+
os.replace(unpack_dir, fullname)
379392

380393
def up_to_date():
381394
return os.path.exists(marker) and utils.read_file(marker).strip() == url

0 commit comments

Comments
 (0)