Skip to content

Commit a9fdad9

Browse files
committed
block concurrent writes on ensemble CSV file
Signed-off-by: Lance-Drane <Lance-Drane@users.noreply.github.com>
1 parent eec683b commit a9fdad9

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

ipsportal/ensemble.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Any
66

77
from . import environment
8+
from .utils.file_locker import FileLocker
89

910
logger = logging.getLogger(__name__)
1011

@@ -37,7 +38,7 @@ def update_ensemble_information(
3738
) -> None:
3839
# we will want to make sure that each call to this function has an exclusive read/write to the file descriptor at one time.
3940
# do not read from one FD and write to a separate FD, as this can cause update issues.
40-
with open(csv_path, 'r+') as fd:
41+
with FileLocker(open(csv_path, 'r+')) as fd:
4142
# we will need to read the entire file into memory while still holding the file descriptor
4243
reader = csv.reader(fd.readlines())
4344
fd.seek(0)

ipsportal/utils/__init__.py

Whitespace-only changes.

ipsportal/utils/file_locker.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Manage locks on files so they cannot be accessed concurrently"""
2+
3+
import os
4+
import sys
5+
import time
6+
from typing import IO, Any
7+
8+
9+
def _portable_lock_fsize(fd: IO[Any]) -> int:
10+
return os.path.getsize(os.path.realpath(fd.name))
11+
12+
13+
if sys.platform == 'win32':
14+
import msvcrt
15+
16+
# lock/unlock the entire file
17+
18+
def _portable_lock(fd: IO[Any]) -> None:
19+
fd.seek(0)
20+
msvcrt.locking(fd.fileno(), msvcrt.LK_LOCK, _portable_lock_fsize(fd))
21+
22+
def _portable_unlock(fd: IO[Any]) -> None:
23+
fd.seek(0)
24+
msvcrt.locking(fd.fileno(), msvcrt.LK_UNLCK, _portable_lock_fsize(fd))
25+
else:
26+
import fcntl
27+
28+
def _portable_lock(fd: IO[Any]) -> None:
29+
fcntl.flock(fd.fileno(), fcntl.LOCK_EX)
30+
31+
def _portable_unlock(fd: IO[Any]) -> None:
32+
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
33+
34+
35+
class FileLocker:
36+
"""
37+
Cross-platform way to acquire a file lock for concurrent calls. Note that calling the constructor is a blocking call
38+
39+
use "timeout" to determine how long the function should block. Raises OSError if timeout exceeded.
40+
"""
41+
42+
def __init__(self, fd: IO[Any], timeout: float = 30.0) -> None:
43+
self.fd = fd
44+
start_time = time.time()
45+
while time.time() < start_time + timeout:
46+
try:
47+
_portable_lock(self.fd)
48+
except OSError:
49+
time.sleep(0.05)
50+
else:
51+
return
52+
53+
msg = f'Unable to secure file lock for {fd.name}'
54+
raise OSError(msg)
55+
56+
def __enter__(self) -> IO[Any]:
57+
return self.fd
58+
59+
def __exit__(self, _type: object, value: object, tb: object) -> None:
60+
try:
61+
_portable_unlock(self.fd)
62+
finally:
63+
self.fd.close()

0 commit comments

Comments
 (0)