personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add supervisor singleton guard via fcntl.flock

Prevents a second `sol supervisor` / `sol start` from silently destroying
the first instance's Callosum socket. On lock contention, prints the
running supervisor's PID and status (via health_check if socket exists),
then exits 1.

+122
+89
tests/test_supervisor.py
··· 4 4 import importlib 5 5 import io 6 6 import logging 7 + import os 7 8 import subprocess 9 + import sys 8 10 import time 11 + from unittest.mock import MagicMock 9 12 10 13 import pytest 11 14 ··· 822 825 assert len(spawned) == 1 823 826 assert spawned[0][0] == ["ref-A", "ref-B", "ref-C"] # all refs passed 824 827 assert spawned[0][1] == ["sol", "indexer", "--rescan"] 828 + 829 + 830 + def test_supervisor_singleton_lock_acquired(tmp_path, monkeypatch): 831 + mod = importlib.reload(importlib.import_module("think.supervisor")) 832 + 833 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 834 + (tmp_path / "health").mkdir(parents=True, exist_ok=True) 835 + monkeypatch.setattr(sys, "argv", ["supervisor"]) 836 + 837 + def stop_after_lock(): 838 + raise SystemExit(0) 839 + 840 + monkeypatch.setattr(mod, "start_callosum_in_process", stop_after_lock) 841 + 842 + with pytest.raises(SystemExit) as exc: 843 + mod.main() 844 + 845 + assert exc.value.code == 0 846 + assert (tmp_path / "health" / "supervisor.lock").exists() 847 + assert (tmp_path / "health" / "supervisor.pid").read_text().strip() == str( 848 + os.getpid() 849 + ) 850 + 851 + 852 + def test_supervisor_singleton_lock_blocked(tmp_path, monkeypatch, capsys): 853 + import fcntl 854 + 855 + mod = importlib.reload(importlib.import_module("think.supervisor")) 856 + 857 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 858 + health_dir = tmp_path / "health" 859 + health_dir.mkdir(parents=True, exist_ok=True) 860 + lock_file = open(health_dir / "supervisor.lock", "w") 861 + fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) 862 + (health_dir / "supervisor.pid").write_text("12345") 863 + monkeypatch.setattr(sys, "argv", ["supervisor"]) 864 + 865 + start_mock = MagicMock() 866 + monkeypatch.setattr(mod, "start_callosum_in_process", start_mock) 867 + 868 + try: 869 + with pytest.raises(SystemExit) as exc: 870 + mod.main() 871 + finally: 872 + lock_file.close() 873 + 874 + assert exc.value.code == 1 875 + output = capsys.readouterr().out 876 + assert "Supervisor already running" in output 877 + assert "PID 12345" in output 878 + start_mock.assert_not_called() 879 + 880 + 881 + def test_supervisor_singleton_lock_blocked_with_health( 882 + tmp_path, monkeypatch, capsys 883 + ): 884 + import fcntl 885 + 886 + mod = importlib.reload(importlib.import_module("think.supervisor")) 887 + 888 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 889 + health_dir = tmp_path / "health" 890 + health_dir.mkdir(parents=True, exist_ok=True) 891 + lock_file = open(health_dir / "supervisor.lock", "w") 892 + fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) 893 + (health_dir / "supervisor.pid").write_text("12345") 894 + (health_dir / "callosum.sock").touch() 895 + monkeypatch.setattr(sys, "argv", ["supervisor"]) 896 + 897 + start_mock = MagicMock() 898 + health_mock = MagicMock(return_value=0) 899 + monkeypatch.setattr(mod, "start_callosum_in_process", start_mock) 900 + monkeypatch.setattr("think.health_cli.health_check", health_mock) 901 + 902 + try: 903 + with pytest.raises(SystemExit) as exc: 904 + mod.main() 905 + finally: 906 + lock_file.close() 907 + 908 + assert exc.value.code == 1 909 + output = capsys.readouterr().out 910 + assert "Supervisor already running" in output 911 + assert "PID 12345" in output 912 + health_mock.assert_called_once_with() 913 + start_mock.assert_not_called()
+33
think/supervisor.py
··· 10 10 import os 11 11 import signal 12 12 import subprocess 13 + import sys 13 14 import threading 14 15 import time 15 16 from dataclasses import dataclass, field ··· 1545 1546 logging.Formatter("%(asctime)s %(levelname)s %(message)s") 1546 1547 ) 1547 1548 logging.getLogger().addHandler(console_handler) 1549 + 1550 + # Singleton guard: only one supervisor per journal 1551 + health_dir = journal_path / "health" 1552 + lock_path = health_dir / "supervisor.lock" 1553 + pid_path = health_dir / "supervisor.pid" 1554 + import fcntl 1555 + 1556 + lock_fd = open(lock_path, "w") 1557 + try: 1558 + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 1559 + except OSError: 1560 + lock_fd.close() 1561 + pid_str = "" 1562 + try: 1563 + pid_str = pid_path.read_text().strip() 1564 + except OSError: 1565 + pass 1566 + pid_msg = f" (PID {pid_str})" if pid_str else "" 1567 + sock_path = health_dir / "callosum.sock" 1568 + if sock_path.exists(): 1569 + try: 1570 + from think.health_cli import health_check 1571 + 1572 + print(f"Supervisor already running{pid_msg}\n") 1573 + health_check() 1574 + except Exception: 1575 + print(f"Supervisor already running{pid_msg}") 1576 + else: 1577 + print(f"Supervisor already running{pid_msg}") 1578 + sys.exit(1) 1579 + pid_path.write_text(str(os.getpid())) 1580 + logging.info("Singleton lock acquired (PID %d)", os.getpid()) 1548 1581 1549 1582 # Set up signal handlers 1550 1583 signal.signal(signal.SIGINT, handle_shutdown)