Add shared managed lock broker

This commit is contained in:
2026-10-07 02:23:39 +00:00
parent 18676070b2
commit 470c7fb09b
16 changed files with 1583 additions and 120 deletions
+263
View File
@@ -0,0 +1,263 @@
import fcntl
import os
import stat
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import patch
from harness.lib.managed_lock import (
LockOrderError,
LockTimeoutError,
LockValidationError,
ManagedLock,
ManagedLockSet,
probe_free,
validate_held_flock,
)
class ManagedLockTests(unittest.TestCase):
def test_lock_is_created_securely_and_held_for_context(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "repository.lock"
with ManagedLock(path) as lock:
self.assertIsNotNone(lock.fd)
st = path.stat()
self.assertEqual(stat.S_IMODE(st.st_mode), 0o600)
self.assertEqual(st.st_nlink, 1)
self.assertEqual(st.st_uid, os.getuid())
self.assertTrue(validate_held_flock(lock.fd, path))
self.assertTrue(probe_free(path))
def test_lock_timeout_is_bounded_and_does_not_steal_lock(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "issue.lock"
with ManagedLock(path):
start = time.monotonic()
with self.assertRaises(LockTimeoutError):
with ManagedLock(path, timeout=0.05, poll_interval=0.005):
pass
self.assertLess(time.monotonic() - start, 1)
self.assertTrue(probe_free(path))
def test_existing_unsafe_lock_file_is_rejected_without_repair(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "bad.lock"
path.write_text("x")
os.chmod(path, 0o644)
with self.assertRaises(LockValidationError):
ManagedLock(path).acquire()
self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o644)
self.assertEqual(path.read_text(), "x")
def test_literal_dotdot_component_is_rejected_before_normalization(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "a" / ".." / "b.lock"
with self.assertRaises(LockValidationError):
ManagedLock(path)
def test_symlink_lock_is_rejected(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
target = root / "target"
target.write_text("do not touch")
path = root / "link.lock"
path.symlink_to(target)
with self.assertRaises(LockValidationError):
ManagedLock(path).acquire()
self.assertEqual(target.read_text(), "do not touch")
def test_symlink_to_safe_lock_is_rejected(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
target = root / "safe-target.lock"
target.touch(mode=0o600)
path = root / "safe-link.lock"
path.symlink_to(target)
with self.assertRaises(LockValidationError):
ManagedLock(path)
def test_lock_path_through_symlinked_parent_is_rejected(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
target = root / "real"
target.mkdir()
parent = root / "linked"
parent.symlink_to(target, target_is_directory=True)
with self.assertRaises(LockValidationError):
ManagedLock(parent / "issue.lock").acquire()
self.assertFalse((target / "issue.lock").exists())
def test_ancestor_swap_after_parent_open_cannot_redirect_lock(self):
"""The final open stays relative to the already pinned parent inode."""
with tempfile.TemporaryDirectory() as d:
root = Path(d)
parent = root / "locks"
parent.mkdir()
diverted = root / "diverted"
diverted.mkdir()
module = __import__("harness.lib.managed_lock", fromlist=["_open_parent"])
original_open_parent = module._open_parent
def swap_after_open(path, *, create_parent):
fd, name = original_open_parent(path, create_parent=create_parent)
parent.rename(root / "original-locks")
parent.symlink_to(diverted, target_is_directory=True)
return fd, name
lock = ManagedLock(parent / "issue.lock")
with patch("harness.lib.managed_lock._open_parent", side_effect=swap_after_open):
with self.assertRaises(LockValidationError):
lock.acquire()
self.assertTrue((root / "original-locks" / "issue.lock").exists())
self.assertFalse((diverted / "issue.lock").exists())
self.assertFalse(lock.held)
def test_hardlink_lock_is_rejected(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
path = root / "lock"
alias = root / "alias"
path.touch(mode=0o600)
os.link(path, alias)
with self.assertRaises(LockValidationError):
ManagedLock(path).acquire()
def test_for_issue_builds_canonical_paths(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d).absolute()
issue, repository = ManagedLockSet.paths_for_issue(root, "alice", "ledger", 7)
self.assertEqual(issue, root / "repositories/alice/ledger/locks/issue-7.lock")
self.assertEqual(repository, root / "repositories/alice/ledger/locks/repository.lock")
locks = ManagedLockSet.for_issue(root, "alice", "ledger", 7)
self.assertEqual(locks.issue.path, issue)
self.assertEqual(locks.repository.path, repository)
def test_for_issue_rejects_unsafe_identity_issue_and_root(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d).absolute()
for owner, repo in [("../alice", "ledger"), ("alice/x", "ledger"), ("alice", ".."), ("alice", "")]:
with self.subTest(owner=owner, repo=repo), self.assertRaises(ValueError):
ManagedLockSet.for_issue(root, owner, repo, 1)
for issue in (0, -1, True, "1"):
with self.subTest(issue=issue), self.assertRaises(ValueError):
ManagedLockSet.for_issue(root, "alice", "ledger", issue)
with self.assertRaises(ValueError):
ManagedLockSet.for_issue(Path("relative"), "alice", "ledger", 1)
with self.assertRaises(ValueError):
ManagedLockSet.for_issue(root / ".." / root.name, "alice", "ledger", 1)
def test_managed_lock_set_holds_issue_then_repository_scope_only(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
issue, repo = root / "issue", root / "repo"
events = []
locks = ManagedLockSet(issue, repo, event_sink=events.append)
with locks:
self.assertTrue(validate_held_flock(locks.issue.fd, issue))
self.assertFalse(locks.repository.held)
with locks.repository_scope():
self.assertTrue(validate_held_flock(locks.repository.fd, repo))
self.assertFalse(locks.repository.held)
self.assertEqual(events, ["acquire:issue", "acquire:repository", "release:repository", "release:issue"])
def test_release_issue_rejects_while_repository_held(self):
with tempfile.TemporaryDirectory() as d:
locks = ManagedLockSet(Path(d) / "issue", Path(d) / "repo")
with locks:
with locks.repository_scope():
with self.assertRaises(LockOrderError):
locks.release_issue()
locks.release_issue()
self.assertFalse(locks.issue.held)
def test_context_exception_cleans_up_issue_and_repository(self):
with tempfile.TemporaryDirectory() as d:
locks = ManagedLockSet(Path(d) / "issue", Path(d) / "repo")
with self.assertRaises(RuntimeError):
with locks:
with locks.repository_scope():
raise RuntimeError("boom")
self.assertFalse(locks.issue.held)
self.assertFalse(locks.repository.held)
self.assertTrue(probe_free(locks.issue.path))
self.assertTrue(probe_free(locks.repository.path))
def test_repository_scope_requires_issue_and_cleans_up_acquire_exception(self):
with tempfile.TemporaryDirectory() as d:
locks = ManagedLockSet(Path(d) / "issue", Path(d) / "repo")
with self.assertRaises(LockOrderError):
with locks.repository_scope():
pass
with ManagedLock(locks.repository.path):
with locks:
with self.assertRaises(LockTimeoutError):
with locks.repository_scope():
pass
self.assertTrue(locks.issue.held)
locks.release_issue()
def test_repository_scope_event_exception_releases_repository_and_issue_context(self):
with tempfile.TemporaryDirectory() as d:
def fail_on_repository(event):
if event == "acquire:repository":
raise RuntimeError("event failure")
locks = ManagedLockSet(Path(d) / "issue", Path(d) / "repo", event_sink=fail_on_repository)
locks.issue.acquire()
with self.assertRaises(RuntimeError):
with locks.repository_scope():
pass
self.assertTrue(locks.issue.held)
self.assertFalse(locks.repository.held)
locks.release_issue()
self.assertTrue(probe_free(locks.issue.path))
self.assertTrue(probe_free(locks.repository.path))
def test_write_metadata_replaces_bytes_exactly_on_repeated_writes(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "metadata.lock"
with ManagedLock(path) as lock:
lock.write_metadata({"long": "x" * 100})
lock.write_metadata({"x": 1})
self.assertEqual(path.read_bytes(), b'{"x":1}\n')
def test_metadata_requires_held_lock(self):
with tempfile.TemporaryDirectory() as d:
lock = ManagedLock(Path(d) / "metadata.lock")
with self.assertRaises(RuntimeError):
lock.write_metadata({"state": "held"})
def test_probe_free_does_not_delete_or_modify_lock(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "probe.lock"
with ManagedLock(path):
pass
before = path.stat()
self.assertTrue(probe_free(path))
after = path.stat()
self.assertEqual((before.st_ino, before.st_size, before.st_mode), (after.st_ino, after.st_size, after.st_mode))
def test_other_process_cannot_acquire_while_context_alive(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "process.lock"
script = """
import sys
sys.path.insert(0, sys.argv[2])
from harness.lib.managed_lock import ManagedLock, LockTimeoutError
try:
with ManagedLock(sys.argv[1], timeout=0.03, poll_interval=0.005):
print('acquired')
except LockTimeoutError:
print('blocked')
"""
with ManagedLock(path):
result = subprocess.run([sys.executable, "-c", script, str(path), str(Path.cwd())], text=True, capture_output=True, check=True)
self.assertEqual(result.stdout.strip(), "blocked")
if __name__ == "__main__":
unittest.main()