Skip to content

Commit 0c4ad1c

Browse files
Merge pull request #91 from vbharadwaj-bk/main
2 parents 0849d17 + 5788402 commit 0c4ad1c

File tree

8 files changed

+102
-35
lines changed

8 files changed

+102
-35
lines changed

.github/workflows/test.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
os: ["ubuntu-latest", "macos-latest"]
19-
python-version: [ "3.7", "3.8", "3.9", "3.10"]
19+
python-version: [ "3.8", "3.9", "3.10"]
2020
name: Test (${{ matrix.python-version }}, ${{ matrix.os }})
2121
runs-on: ${{ matrix.os }}
2222
defaults:
2323
run:
2424
shell: bash -l {0}
2525
steps:
2626
- uses: actions/checkout@v2
27-
- uses: conda-incubator/setup-miniconda@v2
27+
- uses: conda-incubator/setup-miniconda@v3
2828
with:
2929
mamba-version: "*"
3030
channels: conda-forge
@@ -58,7 +58,7 @@ jobs:
5858
name: Build and publish Python distributions to PyPI and TestPyPI
5959
runs-on: ubuntu-latest
6060
steps:
61-
- uses: actions/checkout@v2
61+
- uses: actions/checkout@v3
6262
- uses: conda-incubator/setup-miniconda@v2
6363
with:
6464
mamba-version: "*"

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ htmlcov
1515
**/.DS_Store
1616
.eggs
1717
cppimport/_version.py
18+
*.lock

README.md

+40-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ Note that `force_rebuild` does not work when importing the module concurrently.
161161

162162
### Can I import my model concurrently?
163163

164-
It's safe to use `cppimport` to import a module concurrently using multiple threads, processes or even machines!
164+
It's (mostly) safe to use `cppimport` to import a module concurrently using multiple threads, processes or even machines!
165+
There's an exception if your filesystem does not support file locking - see the next section.
165166

166167
Before building a module, `cppimport` obtains a lockfile preventing other processors from building it at the same time - this prevents clashes that can lead to failure.
167168
Other processes will wait maximum 10 mins until the first process has built the module and load it. If your module does not build within 10 mins then it will timeout.
@@ -173,6 +174,44 @@ cppimport.settings['lock_timeout'] = 10*60 # 10 mins
173174

174175
You should not use `force_rebuild` when importing concurrently.
175176

177+
### Acquiring the lock hangs or times out unexpectedly - what's going on?
178+
Certain platforms (e.g. those running
179+
a Data Virtualization Service, DVS) do not support file locking. If you're on Linux with access to `flock`, you can test whether
180+
locking is supported (credit to [this page](https://help.univention.com/t/howto-verify-the-mounted-filesystem-supports-file-locking/10149)):
181+
182+
```bash
183+
touch testfile
184+
flock ./testfile true && echo ok || echo nok
185+
```
186+
187+
If locking is not supported, you can disable the file lock in
188+
the cppimport global settings:
189+
190+
```python
191+
cppimport.settings['use_filelock'] = False
192+
```
193+
194+
This setting must be changed before you import any
195+
code. By setting `use_filelock=False`, you become responsible
196+
for ensuring that only a single process
197+
(re)builds the package at a time. For example: if you're
198+
using [mpi4py](https://mpi4py.readthedocs.io/en/stable/)
199+
to run independent, communicating processes, here's how
200+
to protect the build:
201+
202+
```python
203+
from mpi4py import MPI
204+
import cppimport, cppimport.import_hook
205+
cppimport.settings["use_filelock"] = False
206+
207+
pid = MPI.COMM_WORLD.Get_rank()
208+
209+
if pid == 0:
210+
import somecode # Process 0 compiles extension if needed
211+
MPI.COMM_WORLD.Barrier() # Remaining processes wait
212+
import somecode # All processes use compiled extension
213+
```
214+
176215
### How can I get information about filepaths in the configuration block?
177216
The module name is available as the `fullname` variable and the C++ module file is available as `filepath`.
178217
For example,

cppimport/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
See CONTRIBUTING.md for a description of the project structure and the internal logic.
33
"""
4+
45
import ctypes
56
import logging
67
import os
@@ -18,6 +19,7 @@
1819
force_rebuild=False, # `force_rebuild` with multiple processes is not supported
1920
file_exts=[".cpp", ".c"],
2021
rtld_flags=ctypes.RTLD_LOCAL,
22+
use_filelock=True, # Filelock if multiple processes try to build simultaneously
2123
lock_suffix=".lock",
2224
lock_timeout=10 * 60,
2325
remove_strict_prototypes=True,

cppimport/build_module.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def _handle_strict_prototypes():
9898

9999
cfg_vars = distutils.sysconfig.get_config_vars()
100100
for key, value in cfg_vars.items():
101-
if type(value) == str:
101+
if value is str:
102102
cfg_vars[key] = value.replace("-Wstrict-prototypes", "")
103103

104104

@@ -144,7 +144,6 @@ def _parallel_compile(
144144
extra_postargs=None,
145145
depends=None,
146146
):
147-
148147
# these lines are copied directly from distutils.ccompiler.CCompiler
149148
macros, objects, extra_postargs, pp_opts, build = self._setup_compile(
150149
output_dir, macros, include_dirs, sources, depends, extra_postargs

cppimport/importer.py

+30-27
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,36 @@ def build_completed():
4343

4444
t = time()
4545

46-
# Race to obtain the lock and build. Other processes can wait
47-
while not build_completed() and time() - t < cppimport.settings["lock_timeout"]:
48-
try:
49-
with filelock.FileLock(lock_path, timeout=1):
50-
if build_completed():
51-
break
52-
template_and_build(filepath, module_data)
53-
except filelock.Timeout:
54-
logging.debug(f"Could not obtain lock (pid {os.getpid()})")
55-
if cppimport.settings["force_rebuild"]:
56-
raise ValueError(
57-
"force_build must be False to build concurrently."
58-
"This process failed to claim a filelock indicating that"
59-
" a concurrent build is in progress"
60-
)
61-
sleep(1)
62-
63-
if os.path.exists(lock_path):
64-
with suppress(OSError):
65-
os.remove(lock_path)
66-
67-
if not build_completed():
68-
raise Exception(
69-
f"Could not compile binary as lock already taken and timed out."
70-
f" Try increasing the timeout setting if "
71-
f"the build time is longer (pid {os.getpid()})."
72-
)
46+
if cppimport.settings["use_filelock"]:
47+
# Race to obtain the lock and build. Other processes can wait
48+
while not build_completed() and time() - t < cppimport.settings["lock_timeout"]:
49+
try:
50+
with filelock.FileLock(lock_path, timeout=1):
51+
if build_completed():
52+
break
53+
template_and_build(filepath, module_data)
54+
except filelock.Timeout:
55+
logging.debug(f"Could not obtain lock (pid {os.getpid()})")
56+
if cppimport.settings["force_rebuild"]:
57+
raise ValueError(
58+
"force_build must be False to build concurrently."
59+
"This process failed to claim a filelock indicating that"
60+
" a concurrent build is in progress"
61+
)
62+
sleep(1)
63+
64+
if os.path.exists(lock_path):
65+
with suppress(OSError):
66+
os.remove(lock_path)
67+
68+
if not build_completed():
69+
raise Exception(
70+
f"Could not compile binary as lock already taken and timed out."
71+
f" Try increasing the timeout setting if "
72+
f"the build time is longer (pid {os.getpid()})."
73+
)
74+
else:
75+
template_and_build(filepath, module_data)
7376

7477

7578
def template_and_build(filepath, module_data):

tests/conftest.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def pytest_addoption(parser):
2+
parser.addoption(
3+
"--multiprocessing",
4+
action="store_true",
5+
dest="multiprocessing",
6+
default=False,
7+
help="enable multiprocessing tests with filelock",
8+
)

tests/test_cppimport.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@
88
from multiprocessing import Process
99
from tempfile import TemporaryDirectory
1010

11+
import pytest
12+
1113
import cppimport
1214
import cppimport.build_module
1315
import cppimport.templating
1416
from cppimport.find import find_module_cpppath
1517

18+
cppimport.settings["use_filelock"] = False # Filelock only enabled for multiprocessing
19+
multiprocessing_enable = pytest.mark.skipif("not config.getoption('multiprocessing')")
20+
1621
root_logger = logging.getLogger()
1722
root_logger.setLevel(logging.DEBUG)
1823

@@ -127,8 +132,11 @@ def test_with_file_in_syspath():
127132
def test_rebuild_after_failed_compile():
128133
cppimport.imp("mymodule")
129134
test_code = """
130-
import cppimport; mymodule = cppimport.imp("mymodule");assert(mymodule.add(1,2) == 3)
131-
"""
135+
import cppimport;
136+
cppimport.settings["use_filelock"] = False;
137+
mymodule = cppimport.imp("mymodule");
138+
assert(mymodule.add(1,2) == 3)
139+
"""
132140
with appended("tests/mymodule.cpp", ";asdf;"):
133141
subprocess_check(test_code, 1)
134142
subprocess_check(test_code, 0)
@@ -149,6 +157,7 @@ def test_no_rebuild_if_no_deps_change():
149157
cppimport.imp("mymodule")
150158
test_code = """
151159
import cppimport;
160+
cppimport.settings["use_filelock"] = False;
152161
mymodule = cppimport.imp("mymodule");
153162
assert(not hasattr(mymodule, 'Thing'))
154163
"""
@@ -160,6 +169,7 @@ def test_rebuild_header_after_change():
160169
cppimport.imp("mymodule")
161170
test_code = """
162171
import cppimport;
172+
cppimport.settings["use_filelock"] = False;
163173
mymodule = cppimport.imp("mymodule");
164174
mymodule.Thing().cheer()
165175
"""
@@ -215,7 +225,12 @@ def test_relative_import():
215225
assert f() == 3
216226

217227

228+
@multiprocessing_enable
218229
def test_multiple_processes():
230+
"""
231+
Only runs if the flag --multiprocessing is passed to
232+
pytest. This function requires file locking enabled.
233+
"""
219234
with tmp_dir(["tests/hook_test.cpp"]) as tmp_path:
220235
test_code = f"""
221236
import os;

0 commit comments

Comments
 (0)