feat(harness): allocate canonical issue-scoped broker runtime
This commit is contained in:
@@ -24,17 +24,36 @@ def wait_ready(proc, timeout=3):
|
||||
return json.loads(line)
|
||||
|
||||
|
||||
def ensure_cache(root, owner="alice", repo="ledger"):
|
||||
repository = root / "repositories" / owner / repo
|
||||
repository.mkdir(parents=True, exist_ok=True)
|
||||
(root / "repositories" / owner).chmod(0o700)
|
||||
repository.chmod(0o700)
|
||||
return repository
|
||||
|
||||
|
||||
def prepare_fixture_runtime(root, run_id):
|
||||
path = runtime_dir(root, run_id=run_id)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
for directory in (path.parent.parent.parent, path.parent.parent, path.parent, path):
|
||||
directory.chmod(0o700)
|
||||
return path
|
||||
|
||||
|
||||
def runtime_dir(root, owner="alice", repo="ledger", issue=7, run_id="run-1"):
|
||||
return root / "repositories" / owner / repo / "worktrees" / f"issue-{issue}" / "runtime" / run_id
|
||||
|
||||
|
||||
class ManagedLockBrokerTests(unittest.TestCase):
|
||||
def start(self, root, owner="alice", repo="ledger", issue=7, ttl=5, role="gitea-impl-agent", mask=-1):
|
||||
ensure_cache(root, owner, repo)
|
||||
locks = root / "repositories" / owner / repo / "locks"
|
||||
locks.mkdir(parents=True)
|
||||
os.chmod(locks, 0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
(locks / f"issue-{issue}.lock").touch(mode=0o600)
|
||||
runtime = root / "runtime"
|
||||
runtime.mkdir()
|
||||
os.chmod(runtime, 0o700)
|
||||
handle = runtime / "handle.json"
|
||||
runtime = runtime_dir(root, owner, repo, issue, "run-1")
|
||||
handle = runtime / "broker.handle"
|
||||
socket = runtime / "broker.sock"
|
||||
cmd = [sys.executable, str(BROKER), "serve", "--root", str(root), "--owner", owner,
|
||||
"--repository", repo, "--issue", str(issue), "--role", role, "--run-id", "run-1",
|
||||
@@ -60,6 +79,97 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
with s.makefile("rb") as response_file:
|
||||
return json.loads(response_file.readline())
|
||||
|
||||
def test_constructor_rejects_lexical_root_and_handle_aliases(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d); ensure_cache(root)
|
||||
locks = root / 'repositories/alice/ledger/locks'; locks.mkdir(mode=0o700)
|
||||
values = dict(root=str(root), owner='alice', repository='ledger', issue=7,
|
||||
role='gitea-impl-agent', run_id='aliases', ttl_seconds=3,
|
||||
acquire_timeout=0, handle=None, socket=None)
|
||||
path = runtime_dir(root, run_id='aliases')
|
||||
for override in [dict(root=f'{root}/./'), dict(handle=f'{path}/./broker.handle')]:
|
||||
with self.subTest(override=override), self.assertRaises(ValueError):
|
||||
broker.Broker(Namespace(**(values | override)))
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
def test_ready_retains_only_one_pinned_runtime_directory_descriptor(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
proc, handle, socket = self.start(root)
|
||||
try:
|
||||
targets = []
|
||||
for fd in Path(f'/proc/{proc.pid}/fd').iterdir():
|
||||
try:
|
||||
if os.readlink(fd) == str(handle.parent):
|
||||
targets.append(fd.name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.assertEqual(len(targets), 1, f'leaked runtime descriptors: {targets}')
|
||||
finally:
|
||||
broker.request_handle(handle, 'release')
|
||||
proc.wait(timeout=3)
|
||||
proc.stdout.close(); proc.stderr.close()
|
||||
|
||||
def test_serve_defaults_to_canonical_names_and_rejects_path_override(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(); locks.chmod(0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
(locks / "issue-7.lock").touch(mode=0o600)
|
||||
base = [sys.executable, str(BROKER), "serve", "--root", str(root), "--owner", "alice",
|
||||
"--repository", "ledger", "--issue", "7", "--role", "gitea-impl-agent",
|
||||
"--run-id", "defaults", "--ttl-seconds", "3"]
|
||||
proc = subprocess.Popen(base, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
ready = wait_ready(proc)
|
||||
expected = runtime_dir(root, run_id="defaults")
|
||||
self.assertEqual(Path(ready["handle"]), expected / "broker.handle")
|
||||
self.assertEqual(Path(ready["socket"]), expected / "broker.sock")
|
||||
broker.request_handle(expected / "broker.handle", "release")
|
||||
self.assertEqual(proc.wait(timeout=2), 0)
|
||||
finally:
|
||||
if proc.poll() is None: proc.kill(); proc.wait()
|
||||
proc.stdout.close(); proc.stderr.close()
|
||||
runtime = runtime_dir(root, run_id="override")
|
||||
result = subprocess.run([*base[:], "--handle", str(root / "elsewhere"), "--run-id", "override"],
|
||||
capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(runtime.exists())
|
||||
|
||||
def test_issue_contention_does_not_create_runtime(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(); locks.chmod(0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
issue_lock = locks / "issue-7.lock"
|
||||
issue_lock.touch(mode=0o600)
|
||||
with ManagedLock(issue_lock):
|
||||
proc = subprocess.run([sys.executable, str(BROKER), "serve", "--root", str(root),
|
||||
"--owner", "alice", "--repository", "ledger", "--issue", "7",
|
||||
"--role", "gitea-impl-agent", "--run-id", "blocked", "--ttl-seconds", "2"],
|
||||
capture_output=True, text=True)
|
||||
self.assertNotEqual(proc.returncode, 0)
|
||||
self.assertFalse(runtime_dir(root, run_id="blocked").exists())
|
||||
|
||||
def test_long_socket_path_connects_via_held_directory_fd(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d) / ("r" * 48)
|
||||
root.mkdir()
|
||||
ensure_cache(root)
|
||||
proc, handle, sock = self.start(root)
|
||||
try:
|
||||
self.assertGreater(len(str(sock).encode()), 107)
|
||||
self.assertEqual(broker.request_handle(handle, "status")["status"], "held")
|
||||
self.assertEqual(broker.request_handle(handle, "release")["status"], "released")
|
||||
self.assertEqual(proc.wait(timeout=3), 0)
|
||||
finally:
|
||||
if proc.poll() is None: proc.kill(); proc.wait()
|
||||
proc.stdout.close(); proc.stderr.close()
|
||||
|
||||
def test_cross_process_contention_and_authenticated_release(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
@@ -133,6 +243,7 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
proc, handle, sock = self.start(Path(d), mask=mask)
|
||||
try:
|
||||
self.assertEqual(stat.S_IMODE(sock.lstat().st_mode), 0o600)
|
||||
self.assertEqual(stat.S_IMODE(handle.parent.stat().st_mode), 0o700)
|
||||
self.assertTrue(stat.S_ISSOCK(sock.lstat().st_mode))
|
||||
self.assertEqual(broker.request_handle(handle, "status")["status"], "held")
|
||||
self.assertEqual(broker.request_handle(handle, "release")["status"], "released")
|
||||
@@ -146,12 +257,13 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
|
||||
def make_bound_instance(self, root):
|
||||
import socket
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(parents=True); locks.chmod(0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
runtime = root / "runtime"; runtime.mkdir(mode=0o700)
|
||||
runtime = prepare_fixture_runtime(root, "cleanup-test")
|
||||
instance = broker.Broker(Namespace(root=str(root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="cleanup-test", handle=str(runtime / "handle.json"),
|
||||
role="gitea-impl-agent", run_id="cleanup-test", handle=str(runtime / "broker.handle"),
|
||||
socket=str(runtime / "broker.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
instance.set.issue.acquire()
|
||||
broker._write_handle(instance.handle, instance.state, exclusive=True,
|
||||
@@ -240,12 +352,16 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
instance = self.make_bound_instance(Path(d))
|
||||
instance.cleanup("released")
|
||||
args = Namespace(root=str(instance.root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="startup-permission", handle=str(instance.handle.parent / "new.handle"),
|
||||
socket=str(instance.sock_path.parent / "new.sock"), ttl_seconds=5.0, acquire_timeout=0.0)
|
||||
fresh = broker.Broker(args)
|
||||
fresh = broker.Broker(Namespace(root=str(instance.root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="startup-permission", handle=str(runtime_dir(instance.root, run_id="startup-permission") / "broker.handle"),
|
||||
socket=str(runtime_dir(instance.root, run_id="startup-permission") / "broker.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
out = io.StringIO()
|
||||
with patch.object(broker.os, "chmod", side_effect=PermissionError("injected")), contextlib.redirect_stdout(out):
|
||||
real_chmod = os.chmod
|
||||
def fail_socket_chmod(path, mode, **kwargs):
|
||||
if path == fresh.sock_path.name:
|
||||
raise PermissionError("injected")
|
||||
return real_chmod(path, mode, **kwargs)
|
||||
with patch.object(broker.os, "chmod", side_effect=fail_socket_chmod), contextlib.redirect_stdout(out):
|
||||
with self.assertRaises(PermissionError): fresh.run()
|
||||
self.assertEqual(out.getvalue(), "")
|
||||
self.assertFalse(fresh.sock_path.exists())
|
||||
@@ -272,8 +388,8 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
instance = self.make_bound_instance(Path(d)); instance.cleanup("released")
|
||||
fresh = broker.Broker(Namespace(root=str(instance.root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="handle-swap", handle=str(instance.handle.parent / "swap.handle"),
|
||||
socket=str(instance.sock_path.parent / "swap.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
role="gitea-impl-agent", run_id="handle-swap", handle=str(runtime_dir(instance.root, run_id="handle-swap") / "broker.handle"),
|
||||
socket=str(runtime_dir(instance.root, run_id="handle-swap") / "broker.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
original_write = broker._write_handle
|
||||
foreign = b'{"foreign":"preserve"}\n'
|
||||
def replace_after_create(path, state, **kwargs):
|
||||
@@ -303,13 +419,14 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
instance = self.make_bound_instance(Path(d)); instance.cleanup("released")
|
||||
fresh = broker.Broker(Namespace(root=str(instance.root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="socket-swap", handle=str(instance.handle.parent / "swap.handle"),
|
||||
socket=str(instance.sock_path.parent / "swap.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
role="gitea-impl-agent", run_id="socket-swap", handle=str(runtime_dir(instance.root, run_id="socket-swap") / "broker.handle"),
|
||||
socket=str(runtime_dir(instance.root, run_id="socket-swap") / "broker.sock"), ttl_seconds=5.0, acquire_timeout=0.0))
|
||||
real_chmod = os.chmod
|
||||
def replace_after_chmod(path, mode, **kwargs):
|
||||
real_chmod(path, mode, **kwargs)
|
||||
fresh.sock_path.unlink()
|
||||
fresh.sock_path.write_text("foreign socket replacement")
|
||||
if path == fresh.sock_path.name:
|
||||
fresh.sock_path.unlink()
|
||||
fresh.sock_path.write_text("foreign socket replacement")
|
||||
out = io.StringIO()
|
||||
with patch.object(broker.os, "chmod", side_effect=replace_after_chmod), contextlib.redirect_stdout(out):
|
||||
with self.assertRaises(ValueError): fresh.run()
|
||||
@@ -354,13 +471,14 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
for action in ("renew", "release"):
|
||||
with self.subTest(action=action), tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(parents=True); os.chmod(locks, 0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
runtime = root / "runtime"; runtime.mkdir(mode=0o700)
|
||||
runtime = prepare_fixture_runtime(root, "expiry-test")
|
||||
args = Namespace(root=str(root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="expiry-test",
|
||||
handle=str(runtime / "handle.json"), socket=str(runtime / "broker.sock"),
|
||||
handle=str(runtime / "broker.handle"), socket=str(runtime / "broker.sock"),
|
||||
ttl_seconds=5.0, acquire_timeout=0.0)
|
||||
instance = broker.Broker(args)
|
||||
instance.set.issue.acquire()
|
||||
@@ -378,11 +496,12 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
def test_startup_failure_releases_issue_and_terminalizes_partial_handle(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(parents=True); os.chmod(locks, 0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
runtime = root / "runtime"; runtime.mkdir(mode=0o700)
|
||||
handle = runtime / "handle.json"
|
||||
runtime = runtime_dir(root, run_id="startup-failure")
|
||||
handle = runtime / "broker.handle"
|
||||
args = Namespace(root=str(root), owner="alice", repository="ledger", issue=7,
|
||||
role="gitea-impl-agent", run_id="startup-failure",
|
||||
handle=str(handle), socket=str(runtime / "broker.sock"),
|
||||
@@ -405,11 +524,12 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
def test_startup_existing_handle_is_never_claimed_or_modified(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
root = Path(d)
|
||||
ensure_cache(root)
|
||||
locks = root / "repositories/alice/ledger/locks"
|
||||
locks.mkdir(parents=True); os.chmod(locks, 0o700)
|
||||
(locks / "repository.lock").touch(mode=0o600)
|
||||
runtime = root / "runtime"; runtime.mkdir(mode=0o700)
|
||||
handle = runtime / "handle.json"
|
||||
runtime = prepare_fixture_runtime(root, "existing-handle")
|
||||
handle = runtime / "broker.handle"
|
||||
original = b'{"foreign":"do-not-touch"}\n'
|
||||
handle.write_bytes(original); handle.chmod(0o600)
|
||||
args = Namespace(root=str(root), owner="alice", repository="ledger", issue=7,
|
||||
@@ -417,7 +537,7 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
handle=str(handle), socket=str(runtime / "broker.sock"),
|
||||
ttl_seconds=5.0, acquire_timeout=0.0)
|
||||
instance = broker.Broker(args)
|
||||
with self.assertRaises(FileExistsError):
|
||||
with self.assertRaises(ValueError):
|
||||
instance.run()
|
||||
self.assertEqual(handle.read_bytes(), original)
|
||||
self.assertTrue(probe_free(locks / "issue-7.lock"))
|
||||
@@ -462,4 +582,4 @@ class ManagedLockBrokerTests(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user