99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""Exclusive ownership with real competing processes (T11)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from wf_scheduling.ownership import (
|
|
SchedulerOwnership,
|
|
SecondOwnerError,
|
|
)
|
|
|
|
|
|
def test_second_owner_rejected_while_held(tmp_path: Path) -> None:
|
|
first = SchedulerOwnership(tmp_path, owner="proc-A").acquire()
|
|
try:
|
|
with pytest.raises(SecondOwnerError):
|
|
SchedulerOwnership(tmp_path, owner="proc-B").acquire()
|
|
finally:
|
|
first.release()
|
|
# After release, a new owner acquires.
|
|
second = SchedulerOwnership(tmp_path, owner="proc-B").acquire()
|
|
second.release()
|
|
|
|
|
|
def test_competing_process_cannot_acquire_while_held(tmp_path: Path) -> None:
|
|
holder = SchedulerOwnership(tmp_path, owner="parent").acquire()
|
|
try:
|
|
script_path = tmp_path / "compete.py"
|
|
script_path.write_text(
|
|
"import sys\n"
|
|
"sys.path.insert(0, '.')\n"
|
|
"from pathlib import Path\n"
|
|
"from wf_scheduling.ownership import SchedulerOwnership, SecondOwnerError\n"
|
|
f"root = Path({str(tmp_path)!r})\n"
|
|
"try:\n"
|
|
" SchedulerOwnership(root, owner='child').acquire()\n"
|
|
"except SecondOwnerError:\n"
|
|
" print('second-owner-rejected')\n"
|
|
" raise SystemExit(0)\n"
|
|
"print('unexpectedly-acquired')\n"
|
|
"raise SystemExit(1)\n",
|
|
encoding="utf-8",
|
|
)
|
|
proc = subprocess.run(
|
|
[sys.executable, str(script_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
assert "second-owner-rejected" in proc.stdout
|
|
finally:
|
|
holder.release()
|
|
|
|
|
|
def test_lock_released_on_process_death(tmp_path: Path) -> None:
|
|
script_path = tmp_path / "hold.py"
|
|
script_path.write_text(
|
|
"import sys, time\n"
|
|
"sys.path.insert(0, '.')\n"
|
|
"from pathlib import Path\n"
|
|
"from wf_scheduling.ownership import SchedulerOwnership\n"
|
|
f"root = Path({str(tmp_path)!r})\n"
|
|
"own = SchedulerOwnership(root, owner='dying').acquire()\n"
|
|
"print('acquired', flush=True)\n"
|
|
"time.sleep(30)\n",
|
|
encoding="utf-8",
|
|
)
|
|
proc = subprocess.Popen(
|
|
[sys.executable, str(script_path)],
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
try:
|
|
assert proc.stdout is not None
|
|
line = proc.stdout.readline()
|
|
assert "acquired" in line
|
|
# Competing acquisition fails while the child lives.
|
|
with pytest.raises(SecondOwnerError):
|
|
SchedulerOwnership(tmp_path, owner="parent").acquire()
|
|
finally:
|
|
proc.kill()
|
|
proc.wait(timeout=30)
|
|
# After process death the OS releases the held lock; allow a beat.
|
|
acquired = None
|
|
for _ in range(50):
|
|
try:
|
|
acquired = SchedulerOwnership(tmp_path, owner="parent").acquire()
|
|
break
|
|
except SecondOwnerError:
|
|
time.sleep(0.1)
|
|
assert acquired is not None
|
|
acquired.release()
|