Skip to content

Commit 746bb95

Browse files
David Vitekdavidism
David Vitek
authored andcommitted
Fix race conditions in FileSystemBytecodeCache
1 parent 466a200 commit 746bb95

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
lines changed

CHANGES.rst

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Unreleased
77

88
- Add parameters to ``Environment.overlay`` to match ``__init__``.
99
:issue:`1645`
10+
- Handle race condition in ``FileSystemBytecodeCache``. :issue:`1654`
1011

1112

1213
Version 3.1.1

src/jinja2/bccache.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def load_bytecode(self, f: t.BinaryIO) -> None:
7979
self.reset()
8080
return
8181

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

265-
if os.path.exists(filename):
266-
with open(filename, "rb") as f:
267-
bucket.load_bytecode(f)
265+
# Don't test for existence before opening the file, since the
266+
# file could disappear after the test before the open.
267+
try:
268+
f = open(filename, "rb")
269+
except (FileNotFoundError, IsADirectoryError, PermissionError):
270+
# PermissionError can occur on Windows when an operation is
271+
# in progress, such as calling clear().
272+
return
273+
274+
with f:
275+
bucket.load_bytecode(f)
268276

269277
def dump_bytecode(self, bucket: Bucket) -> None:
270-
with open(self._get_cache_filename(bucket), "wb") as f:
271-
bucket.write_bytecode(f)
278+
# Write to a temporary file, then rename to the real name after
279+
# writing. This avoids another process reading the file before
280+
# it is fully written.
281+
name = self._get_cache_filename(bucket)
282+
f = tempfile.NamedTemporaryFile(
283+
mode="wb",
284+
dir=os.path.dirname(name),
285+
prefix=os.path.basename(name),
286+
suffix=".tmp",
287+
delete=False,
288+
)
289+
290+
def remove_silent() -> None:
291+
try:
292+
os.remove(f.name)
293+
except OSError:
294+
# Another process may have called clear(). On Windows,
295+
# another program may be holding the file open.
296+
pass
297+
298+
try:
299+
with f:
300+
bucket.write_bytecode(f)
301+
except BaseException:
302+
remove_silent()
303+
raise
304+
305+
try:
306+
os.replace(f.name, name)
307+
except OSError:
308+
# Another process may have called clear(). On Windows,
309+
# another program may be holding the file open.
310+
remove_silent()
311+
except BaseException:
312+
remove_silent()
313+
raise
272314

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

0 commit comments

Comments
 (0)