Refuse to run a second eris instance on the same directory
flock on .eris/lock prevents two eris processes racing the cache, which previously produced a mixed-format summary (int-list pages) and crashed sync_with_filesystem. The lock is acquired before manage_cache, the lock file survives checksum-mismatch cleanup, and the holder's PID is written to the lock file so a refused instance can report it.
This commit is contained in:
parent
980ee7071c
commit
329f500c8e
2 changed files with 115 additions and 13 deletions
|
|
@ -14,6 +14,7 @@ directory.
|
|||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import fcntl
|
||||
import functools
|
||||
import gzip
|
||||
import hashlib
|
||||
|
|
@ -1106,23 +1107,58 @@ def source_checksum():
|
|||
return sha256.hexdigest()
|
||||
|
||||
|
||||
class LockError(Exception):
|
||||
|
||||
def __init__(self, holder_pid):
|
||||
super().__init__(holder_pid)
|
||||
self.holder_pid = holder_pid
|
||||
|
||||
|
||||
def acquire_lock(cache_path):
|
||||
"""Take an exclusive flock on the cache lock file, refusing if another
|
||||
eris instance is running on the same directory."""
|
||||
os.makedirs(cache_path, exist_ok=True)
|
||||
lock_path = os.path.join(cache_path, "lock")
|
||||
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR)
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
os.close(lock_fd)
|
||||
holder_pid = None
|
||||
try:
|
||||
with open(lock_path, "r") as lock_file:
|
||||
holder_pid = lock_file.read().strip() or None
|
||||
except OSError:
|
||||
pass
|
||||
raise LockError(holder_pid) from None
|
||||
os.ftruncate(lock_fd, 0)
|
||||
os.write(lock_fd, str(os.getpid()).encode())
|
||||
return lock_fd
|
||||
|
||||
|
||||
def manage_cache(root_path):
|
||||
cache_path = os.path.join(root_path, tools.CACHE_PATH)
|
||||
checksum_path = os.path.join(cache_path, "source_checksum")
|
||||
if os.path.exists(cache_path):
|
||||
try:
|
||||
with open(checksum_path, "r") as checksum_file:
|
||||
cache_checksum = checksum_file.read()
|
||||
except FileNotFoundError:
|
||||
cache_checksum = None
|
||||
if source_checksum() != cache_checksum:
|
||||
cache_checksum = None
|
||||
if os.path.exists(checksum_path):
|
||||
with open(checksum_path, "r") as checksum_file:
|
||||
cache_checksum = checksum_file.read()
|
||||
if source_checksum() != cache_checksum:
|
||||
contents = ([name for name in os.listdir(cache_path) if name != "lock"]
|
||||
if os.path.isdir(cache_path) else [])
|
||||
if contents:
|
||||
print("Eris has changed, recalculating all results…")
|
||||
shutil.rmtree(cache_path)
|
||||
if not os.path.exists(cache_path):
|
||||
os.mkdir(cache_path)
|
||||
with open(checksum_path, "w") as checksum_file:
|
||||
checksum_file.write(source_checksum())
|
||||
gitignore_path = os.path.join(cache_path, ".gitignore")
|
||||
for name in contents:
|
||||
path = os.path.join(cache_path, name)
|
||||
if os.path.isdir(path) and not os.path.islink(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
os.unlink(path)
|
||||
os.makedirs(cache_path, exist_ok=True)
|
||||
with open(checksum_path, "w") as checksum_file:
|
||||
checksum_file.write(source_checksum())
|
||||
gitignore_path = os.path.join(cache_path, ".gitignore")
|
||||
if not os.path.exists(gitignore_path):
|
||||
with open(gitignore_path, "w") as gitignore_file:
|
||||
gitignore_file.write("*")
|
||||
|
||||
|
|
@ -1268,6 +1304,17 @@ def inotify_watches_exceeded():
|
|||
def entry_point():
|
||||
(root_path, worker_count, editor_command, theme, compression,
|
||||
use_window) = check_arguments()
|
||||
try:
|
||||
lock_fd = acquire_lock(os.path.join(root_path, tools.CACHE_PATH))
|
||||
except LockError as lock_error:
|
||||
if lock_error.holder_pid:
|
||||
print(f"Error: another eris is already running on this directory "
|
||||
f"(pid {lock_error.holder_pid}).")
|
||||
else:
|
||||
print("Error: another eris is already running on this directory.")
|
||||
print("Eris only supports one instance per project directory.")
|
||||
print("If this is a stale process, kill it first and try again.")
|
||||
sys.exit(1)
|
||||
manage_cache(root_path)
|
||||
title = "eris: " + os.path.basename(root_path)
|
||||
with chdir(root_path): # FIX: Don't change directory if possible.
|
||||
|
|
|
|||
|
|
@ -201,5 +201,60 @@ class SummarySyncWithFilesystemTestCase(unittest.TestCase):
|
|||
id(self.summary._entries[3])) # zoo
|
||||
self.assertTrue(self.jobs_added_event.is_set())
|
||||
|
||||
class CacheLockTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.cache_path = os.path.join(self.temp_dir, tools.CACHE_PATH)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_acquire_lock_conflict_and_release(self):
|
||||
lock_fd = __main__.acquire_lock(self.cache_path)
|
||||
lock_path = os.path.join(self.cache_path, "lock")
|
||||
self.assertTrue(os.path.exists(lock_path))
|
||||
with open(lock_path) as lock_file:
|
||||
self.assertEqual(lock_file.read(), str(os.getpid()))
|
||||
with self.assertRaises(__main__.LockError) as error:
|
||||
__main__.acquire_lock(self.cache_path)
|
||||
self.assertEqual(error.exception.holder_pid, str(os.getpid()))
|
||||
os.close(lock_fd)
|
||||
lock_fd = __main__.acquire_lock(self.cache_path)
|
||||
os.close(lock_fd)
|
||||
|
||||
def test_manage_cache_preserves_lock_on_checksum_mismatch(self):
|
||||
lock_fd = __main__.acquire_lock(self.cache_path)
|
||||
lock_path = os.path.join(self.cache_path, "lock")
|
||||
with open(os.path.join(self.cache_path, "source_checksum"), "w") as checksum_file:
|
||||
checksum_file.write("stale-checksum")
|
||||
stale_pickle = os.path.join(self.cache_path, "summary.pickle")
|
||||
stale_dir = os.path.join(self.cache_path, "summary_dir")
|
||||
_touch(stale_pickle)
|
||||
os.mkdir(stale_dir)
|
||||
original_checksum = __main__.source_checksum
|
||||
__main__.source_checksum = lambda: "new-checksum"
|
||||
try:
|
||||
__main__.manage_cache(self.temp_dir)
|
||||
finally:
|
||||
__main__.source_checksum = original_checksum
|
||||
os.close(lock_fd)
|
||||
self.assertTrue(os.path.exists(lock_path))
|
||||
self.assertFalse(os.path.exists(stale_pickle))
|
||||
self.assertFalse(os.path.exists(stale_dir))
|
||||
with open(os.path.join(self.cache_path, "source_checksum")) as checksum_file:
|
||||
self.assertEqual(checksum_file.read(), "new-checksum")
|
||||
self.assertTrue(os.path.exists(os.path.join(self.cache_path, ".gitignore")))
|
||||
|
||||
def test_manage_cache_first_run_keeps_lock(self):
|
||||
lock_fd = __main__.acquire_lock(self.cache_path)
|
||||
os.close(lock_fd)
|
||||
__main__.manage_cache(self.temp_dir)
|
||||
self.assertTrue(os.path.exists(os.path.join(self.cache_path, "lock")))
|
||||
with open(os.path.join(self.cache_path, "source_checksum")) as checksum_file:
|
||||
self.assertEqual(checksum_file.read(), __main__.source_checksum())
|
||||
self.assertTrue(os.path.exists(os.path.join(self.cache_path, ".gitignore")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
golden.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue