Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Race conditions in FileSystemBytecodeCache #1655

Merged
merged 1 commit into from Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -7,6 +7,7 @@ Unreleased

- Add parameters to ``Environment.overlay`` to match ``__init__``.
:issue:`1645`
- Handle race condition in ``FileSystemBytecodeCache``. :issue:`1654`


Version 3.1.1
Expand Down
54 changes: 48 additions & 6 deletions src/jinja2/bccache.py
Expand Up @@ -79,7 +79,7 @@ def load_bytecode(self, f: t.BinaryIO) -> None:
self.reset()
return

def write_bytecode(self, f: t.BinaryIO) -> None:
def write_bytecode(self, f: t.IO[bytes]) -> None:
"""Dump the bytecode into the file or file like object passed."""
if self.code is None:
raise TypeError("can't write empty bucket")
Expand Down Expand Up @@ -262,13 +262,55 @@ def _get_cache_filename(self, bucket: Bucket) -> str:
def load_bytecode(self, bucket: Bucket) -> None:
filename = self._get_cache_filename(bucket)

if os.path.exists(filename):
with open(filename, "rb") as f:
bucket.load_bytecode(f)
# Don't test for existence before opening the file, since the
# file could disappear after the test before the open.
try:
f = open(filename, "rb")
except (FileNotFoundError, IsADirectoryError, PermissionError):
# PermissionError can occur on Windows when an operation is
# in progress, such as calling clear().
return

with f:
bucket.load_bytecode(f)

def dump_bytecode(self, bucket: Bucket) -> None:
with open(self._get_cache_filename(bucket), "wb") as f:
bucket.write_bytecode(f)
# Write to a temporary file, then rename to the real name after
# writing. This avoids another process reading the file before
# it is fully written.
name = self._get_cache_filename(bucket)
f = tempfile.NamedTemporaryFile(
mode="wb",
dir=os.path.dirname(name),
prefix=os.path.basename(name),
suffix=".tmp",
delete=False,
)

def remove_silent() -> None:
try:
os.remove(f.name)
except OSError:
# Another process may have called clear(). On Windows,
# another program may be holding the file open.
pass

try:
with f:
bucket.write_bytecode(f)
except BaseException:
remove_silent()
raise

try:
os.replace(f.name, name)
except OSError:
# Another process may have called clear(). On Windows,
# another program may be holding the file open.
remove_silent()
except BaseException:
remove_silent()
raise

def clear(self) -> None:
# imported lazily here because google app-engine doesn't support
Expand Down