Skip to content

Commit

Permalink
Be resilient against parallel directory creations. (#75)
Browse files Browse the repository at this point in the history
* Be resilient against parallel directory creations.

When using multiprocessing, simultaneous writes can lead to the
situation where both code paths first check if the directory exists and
create it in the negative case. As check and creation aren't a single
atomic operation, one of them might fail.

* Add test case to check concurrent failure scenario

* Assert correct Error in Python 2

* Amend changelog
  • Loading branch information
xhochy authored and fmarczin committed Jul 9, 2018
1 parent 599788b commit 9ac9f3a
Show file tree
Hide file tree
Showing 4 changed files with 30 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
0.11.9
======
* Add option to set the checksum for Azure blobs.
* Make the FilesystemStore resilient to parallel directory creations.

0.11.8
======
Expand Down
6 changes: 5 additions & 1 deletion simplekv/fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ def _copy(self, source, dest):

def _ensure_dir_exists(self, path):
if not os.path.isdir(path):
os.makedirs(path)
try:
os.makedirs(path)
except OSError as e:
if not os.path.isdir(path):
raise e

def _put_file(self, key, file):
bufsize = self.bufsize
Expand Down
24 changes: 23 additions & 1 deletion tests/test_filesystem_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os
import stat
from simplekv._compat import BytesIO, url_quote, url_unquote
from simplekv._compat import BytesIO, url_quote, url_unquote, PY2
import tempfile
from simplekv._compat import urlparse

Expand Down Expand Up @@ -31,6 +31,28 @@ def store(self, tmpdir):
return FilesystemStore(tmpdir)


class TestFilesystemStoreMkdir(TestBaseFilesystemStore):

def test_concurrent_mkdir(self, tmpdir, mocker):
# Concurrent instantiation of the store in two threads could lead to
# the situation where both threads see that the directory does not
# exists. For one, the call to mkdir succeeds, for the other it fails.
# This is ok for us as long as the directory exists afterwards.
makedirs = mocker.patch('os.makedirs')
makedirs.side_effect = OSError("Failure")
mocker.patch('os.path.isdir')

store = FilesystemStore(os.path.join(tmpdir, 'test'))
# We have mocked os.makedirs, so this won't work. But it should
# pass beyond the OS error and simply fail on writing the file itself.
if PY2:
with pytest.raises(IOError):
store.put('test', b'test')
else:
with pytest.raises(FileNotFoundError):
store.put('test', b'test')


class TestFilesystemStoreFileURI(TestBaseFilesystemStore):
@pytest.mark.skipif(os.name != 'posix',
reason='Not supported outside posix.')
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ deps=
pytest
pytest-pep8
pytest-cov
pytest-mock
pytest-xdist
mock
tempdir
Expand Down

0 comments on commit 9ac9f3a

Please sign in to comment.