Track locks by (dev, ino); close file handlers between tests (#34122)
This commit is contained in:
parent
7a5e527cab
commit
2167cbf72c
3 changed files with 37 additions and 21 deletions
|
@ -9,7 +9,6 @@
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Tuple # novm
|
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.lang import pretty_seconds
|
from llnl.util.lang import pretty_seconds
|
||||||
|
@ -81,7 +80,7 @@ class OpenFileTracker(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Create a new ``OpenFileTracker``."""
|
"""Create a new ``OpenFileTracker``."""
|
||||||
self._descriptors = {} # type: Dict[Tuple[int, int], OpenFile]
|
self._descriptors = {}
|
||||||
|
|
||||||
def get_fh(self, path):
|
def get_fh(self, path):
|
||||||
"""Get a filehandle for a lockfile.
|
"""Get a filehandle for a lockfile.
|
||||||
|
@ -103,7 +102,7 @@ def get_fh(self, path):
|
||||||
try:
|
try:
|
||||||
# see whether we've seen this inode/pid before
|
# see whether we've seen this inode/pid before
|
||||||
stat = os.stat(path)
|
stat = os.stat(path)
|
||||||
key = (stat.st_ino, pid)
|
key = (stat.st_dev, stat.st_ino, pid)
|
||||||
open_file = self._descriptors.get(key)
|
open_file = self._descriptors.get(key)
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@ -129,32 +128,32 @@ def get_fh(self, path):
|
||||||
|
|
||||||
# if we just created the file, we'll need to get its inode here
|
# if we just created the file, we'll need to get its inode here
|
||||||
if not stat:
|
if not stat:
|
||||||
inode = os.fstat(fd).st_ino
|
stat = os.fstat(fd)
|
||||||
key = (inode, pid)
|
key = (stat.st_dev, stat.st_ino, pid)
|
||||||
|
|
||||||
self._descriptors[key] = open_file
|
self._descriptors[key] = open_file
|
||||||
|
|
||||||
open_file.refs += 1
|
open_file.refs += 1
|
||||||
return open_file.fh
|
return open_file.fh
|
||||||
|
|
||||||
def release_fh(self, path):
|
def release_by_stat(self, stat):
|
||||||
"""Release a filehandle, only closing it if there are no more references."""
|
key = (stat.st_dev, stat.st_ino, os.getpid())
|
||||||
try:
|
|
||||||
inode = os.stat(path).st_ino
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT: # only handle file not found
|
|
||||||
raise
|
|
||||||
inode = None # this will not be in self._descriptors
|
|
||||||
|
|
||||||
key = (inode, os.getpid())
|
|
||||||
open_file = self._descriptors.get(key)
|
open_file = self._descriptors.get(key)
|
||||||
assert open_file, "Attempted to close non-existing lock path: %s" % path
|
assert open_file, "Attempted to close non-existing inode: %s" % stat.st_inode
|
||||||
|
|
||||||
open_file.refs -= 1
|
open_file.refs -= 1
|
||||||
if not open_file.refs:
|
if not open_file.refs:
|
||||||
del self._descriptors[key]
|
del self._descriptors[key]
|
||||||
open_file.fh.close()
|
open_file.fh.close()
|
||||||
|
|
||||||
|
def release_by_fh(self, fh):
|
||||||
|
self.release_by_stat(os.fstat(fh.fileno()))
|
||||||
|
|
||||||
|
def purge(self):
|
||||||
|
for key in list(self._descriptors.keys()):
|
||||||
|
self._descriptors[key].fh.close()
|
||||||
|
del self._descriptors[key]
|
||||||
|
|
||||||
|
|
||||||
#: Open file descriptors for locks in this process. Used to prevent one process
|
#: Open file descriptors for locks in this process. Used to prevent one process
|
||||||
#: from opening the sam file many times for different byte range locks
|
#: from opening the sam file many times for different byte range locks
|
||||||
|
@ -432,8 +431,7 @@ def _unlock(self):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
fcntl.lockf(self._file, fcntl.LOCK_UN, self._length, self._start, os.SEEK_SET)
|
fcntl.lockf(self._file, fcntl.LOCK_UN, self._length, self._start, os.SEEK_SET)
|
||||||
|
file_tracker.release_by_fh(self._file)
|
||||||
file_tracker.release_fh(self.path)
|
|
||||||
self._file = None
|
self._file = None
|
||||||
self._reads = 0
|
self._reads = 0
|
||||||
self._writes = 0
|
self._writes = 0
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
from typing import Dict # novm
|
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -26,6 +25,7 @@
|
||||||
import archspec.cpu.schema
|
import archspec.cpu.schema
|
||||||
|
|
||||||
import llnl.util.lang
|
import llnl.util.lang
|
||||||
|
import llnl.util.lock
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import copy_tree, mkdirp, remove_linked_tree, working_dir
|
from llnl.util.filesystem import copy_tree, mkdirp, remove_linked_tree, working_dir
|
||||||
|
|
||||||
|
@ -1640,7 +1640,6 @@ def mock_clone_repo(tmpdir_factory):
|
||||||
class MockBundle(object):
|
class MockBundle(object):
|
||||||
has_code = False
|
has_code = False
|
||||||
name = "mock-bundle"
|
name = "mock-bundle"
|
||||||
versions = {} # type: Dict
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -1692,6 +1691,19 @@ def mock_test_stage(mutable_config, tmpdir):
|
||||||
yield tmp_stage
|
yield tmp_stage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def inode_cache():
|
||||||
|
llnl.util.lock.file_tracker.purge()
|
||||||
|
yield
|
||||||
|
# TODO: it is a bug when the file tracker is non-empty after a test,
|
||||||
|
# since it means a lock was not released, or the inode was not purged
|
||||||
|
# when acquiring the lock failed. So, we could assert that here, but
|
||||||
|
# currently there are too many issues to fix, so look for the more
|
||||||
|
# serious issue of having a closed file descriptor in the cache.
|
||||||
|
assert not any(f.fh.closed for f in llnl.util.lock.file_tracker._descriptors.values())
|
||||||
|
llnl.util.lock.file_tracker.purge()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def brand_new_binary_cache():
|
def brand_new_binary_cache():
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -687,7 +687,9 @@ def test_upgrade_read_to_write_fails_with_readonly_file(private_lock_path):
|
||||||
# upgrade to write here
|
# upgrade to write here
|
||||||
with pytest.raises(lk.LockROFileError):
|
with pytest.raises(lk.LockROFileError):
|
||||||
lock.acquire_write()
|
lock.acquire_write()
|
||||||
lk.file_tracker.release_fh(lock.path)
|
|
||||||
|
# TODO: lk.file_tracker does not release private_lock_path
|
||||||
|
lk.file_tracker.release_by_stat(os.stat(private_lock_path))
|
||||||
|
|
||||||
|
|
||||||
class ComplexAcquireAndRelease(object):
|
class ComplexAcquireAndRelease(object):
|
||||||
|
@ -1313,6 +1315,7 @@ def test_downgrade_write_okay(tmpdir):
|
||||||
lock.downgrade_write_to_read()
|
lock.downgrade_write_to_read()
|
||||||
assert lock._reads == 1
|
assert lock._reads == 1
|
||||||
assert lock._writes == 0
|
assert lock._writes == 0
|
||||||
|
lock.release_read()
|
||||||
|
|
||||||
|
|
||||||
def test_downgrade_write_fails(tmpdir):
|
def test_downgrade_write_fails(tmpdir):
|
||||||
|
@ -1323,6 +1326,7 @@ def test_downgrade_write_fails(tmpdir):
|
||||||
msg = "Cannot downgrade lock from write to read on file: lockfile"
|
msg = "Cannot downgrade lock from write to read on file: lockfile"
|
||||||
with pytest.raises(lk.LockDowngradeError, match=msg):
|
with pytest.raises(lk.LockDowngradeError, match=msg):
|
||||||
lock.downgrade_write_to_read()
|
lock.downgrade_write_to_read()
|
||||||
|
lock.release_read()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -1362,6 +1366,7 @@ def test_upgrade_read_okay(tmpdir):
|
||||||
lock.upgrade_read_to_write()
|
lock.upgrade_read_to_write()
|
||||||
assert lock._reads == 0
|
assert lock._reads == 0
|
||||||
assert lock._writes == 1
|
assert lock._writes == 1
|
||||||
|
lock.release_write()
|
||||||
|
|
||||||
|
|
||||||
def test_upgrade_read_fails(tmpdir):
|
def test_upgrade_read_fails(tmpdir):
|
||||||
|
@ -1372,3 +1377,4 @@ def test_upgrade_read_fails(tmpdir):
|
||||||
msg = "Cannot upgrade lock from read to write on file: lockfile"
|
msg = "Cannot upgrade lock from read to write on file: lockfile"
|
||||||
with pytest.raises(lk.LockUpgradeError, match=msg):
|
with pytest.raises(lk.LockUpgradeError, match=msg):
|
||||||
lock.upgrade_read_to_write()
|
lock.upgrade_read_to_write()
|
||||||
|
lock.release_write()
|
||||||
|
|
Loading…
Reference in a new issue