-
Notifications
You must be signed in to change notification settings - Fork 53
isolate validator worker installs and raise default concurrency #719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c20f4e0
f603b2c
f63c809
05e874b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -386,12 +386,12 @@ def test_load_plugins_index_rejects_non_dict_values(self): | |
|
|
||
|
|
||
| class ValidationProgressTests(unittest.TestCase): | ||
| def test_build_parser_defaults_max_workers_to_eight(self): | ||
| def test_build_parser_defaults_max_workers_to_sixteen(self): | ||
| module = load_validator_module() | ||
|
|
||
| args = module.build_parser().parse_args(["--astrbot-path", "/tmp/AstrBot"]) | ||
|
|
||
| self.assertEqual(args.max_workers, 8) | ||
| self.assertEqual(args.max_workers, 16) | ||
|
|
||
| def test_build_parser_rejects_non_positive_worker_and_timeout_values(self): | ||
| module = load_validator_module() | ||
|
|
@@ -748,6 +748,131 @@ async def load(self, specified_dir_name: str): | |
| self.assertEqual(result["message"], "{'reason': 'boom'}") | ||
|
|
||
|
|
||
| class RunWorkerIsolationTests(unittest.TestCase): | ||
| def _assert_worker_isolated(self, *, temp_root: Path, args, observed: dict) -> None: | ||
| self.assertIn("astrbot_root", observed) | ||
| astrbot_root = Path(observed["astrbot_root"]).resolve() | ||
| shared_astrbot_path = Path(args.astrbot_path).resolve() | ||
|
|
||
| self.assertNotEqual(astrbot_root, shared_astrbot_path) | ||
| self.assertTrue( | ||
| astrbot_root.is_relative_to(temp_root.resolve()), | ||
| f"worker astrbot_root {astrbot_root} should be under {temp_root}", | ||
| ) | ||
|
|
||
| current = astrbot_root | ||
| found_worker_prefix = False | ||
| while True: | ||
| if current.name.startswith("astrbot-plugin-worker-"): | ||
| found_worker_prefix = True | ||
| break | ||
| if current.parent == current: | ||
| break | ||
| current = current.parent | ||
|
|
||
| self.assertTrue(found_worker_prefix) | ||
| self.assertTrue((astrbot_root / "data" / "plugins").is_dir()) | ||
| self.assertTrue((astrbot_root / "data" / "config").is_dir()) | ||
|
|
||
| def test_configure_worker_install_target_deduplicates_process_paths(self): | ||
| module = load_validator_module() | ||
| original_sys_path = list(sys.path) | ||
|
|
||
| with tempfile.TemporaryDirectory() as tmp_dir: | ||
| temp_root = Path(tmp_dir) | ||
| site_packages = (temp_root / "site-packages").resolve() | ||
| site_packages_alias = os.path.join(str(site_packages.parent), ".", site_packages.name) | ||
| extra_path = (temp_root / "extra-path").resolve() | ||
| extra_path.mkdir() | ||
| observed = {} | ||
|
|
||
| sys.path[:0] = [str(site_packages), site_packages_alias] | ||
| with mock.patch.dict( | ||
| os.environ, | ||
| {"PYTHONPATH": os.pathsep.join([site_packages_alias, str(extra_path)])}, | ||
| clear=True, | ||
| ): | ||
| module.configure_worker_install_target(temp_root=temp_root) | ||
| module.configure_worker_install_target(temp_root=temp_root) | ||
|
|
||
| observed["pip_target"] = os.environ["PIP_TARGET"] | ||
| observed["pythonpath_entries"] = os.environ["PYTHONPATH"].split(os.pathsep) | ||
| observed["resolved_pythonpath_count"] = sum( | ||
| 1 | ||
| for entry in observed["pythonpath_entries"] | ||
| if Path(entry).resolve() == site_packages | ||
| ) | ||
| observed["resolved_sys_path_count"] = sum( | ||
| 1 for entry in sys.path if Path(entry).resolve() == site_packages | ||
| ) | ||
|
|
||
| self.assertEqual(observed["pip_target"], str(site_packages)) | ||
| self.assertEqual(observed["resolved_pythonpath_count"], 1) | ||
| self.assertEqual(observed["resolved_sys_path_count"], 1) | ||
| self.assertIn(str(extra_path), observed["pythonpath_entries"]) | ||
|
|
||
| sys.path[:] = original_sys_path | ||
|
|
||
| def test_run_worker_isolates_plugin_installs_under_temp_root(self): | ||
| module = load_validator_module() | ||
| observed = {} | ||
| original_sys_path = list(sys.path) | ||
|
|
||
| async def fake_run_worker_load_check(plugin_dir_name: str, normalized_repo_url: str): | ||
| observed["plugin_dir_name"] = plugin_dir_name | ||
| observed["normalized_repo_url"] = normalized_repo_url | ||
| observed["astrbot_root"] = os.environ.get("ASTRBOT_ROOT") | ||
| observed["pip_target"] = os.environ.get("PIP_TARGET") | ||
| observed["sys_path"] = list(sys.path) | ||
| return module.build_result( | ||
| plugin=plugin_dir_name, | ||
| repo=normalized_repo_url, | ||
| normalized_repo_url=normalized_repo_url, | ||
| ok=True, | ||
| stage="load", | ||
| message="plugin loaded successfully", | ||
| plugin_dir_name=plugin_dir_name, | ||
| ) | ||
|
|
||
| with tempfile.TemporaryDirectory() as tmp_dir: | ||
| worker_temp_root = Path(tmp_dir) / "astrbot-plugin-worker-test-root" | ||
| worker_temp_root.mkdir() | ||
| plugin_source_dir = Path(tmp_dir) / "plugin-src" | ||
| plugin_source_dir.mkdir() | ||
| (plugin_source_dir / "main.py").write_text("print('hello')\n", encoding="utf-8") | ||
| args = module.argparse.Namespace( | ||
| astrbot_path="/tmp/AstrBot", | ||
| plugin_source_dir=str(plugin_source_dir), | ||
| plugin_dir_name="demo_plugin", | ||
| normalized_repo_url="https://github.com/example/demo-plugin", | ||
| ) | ||
|
|
||
| with mock.patch.dict(os.environ, {}, clear=True): | ||
| with mock.patch.object(module, "run_worker_load_check", side_effect=fake_run_worker_load_check): | ||
| with mock.patch.object(module.tempfile, "mkdtemp", return_value=str(worker_temp_root)): | ||
| with mock.patch.object(module.shutil, "rmtree") as rmtree_mock: | ||
| exit_code = module.run_worker(args) | ||
|
|
||
| self._assert_worker_isolated(temp_root=worker_temp_root, args=args, observed=observed) | ||
| rmtree_mock.assert_called_once_with(worker_temp_root, ignore_errors=True) | ||
|
|
||
| sys.path[:] = original_sys_path | ||
|
Comment on lines
+819
to
+859
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The restoration of original_sys_path = list(sys.path)
def restore_path():
sys.path[:] = original_sys_path
self.addCleanup(restore_path)
async def fake_run_worker_load_check(plugin_dir_name: str, normalized_repo_url: str):
observed["plugin_dir_name"] = plugin_dir_name
observed["normalized_repo_url"] = normalized_repo_url
observed["astrbot_root"] = os.environ.get("ASTRBOT_ROOT")
observed["pip_target"] = os.environ.get("PIP_TARGET")
observed["sys_path"] = list(sys.path)
return module.build_result(
plugin=plugin_dir_name,
repo=normalized_repo_url,
normalized_repo_url=normalized_repo_url,
ok=True,
stage="load",
message="plugin loaded successfully",
plugin_dir_name=plugin_dir_name,
)
with tempfile.TemporaryDirectory() as tmp_dir:
plugin_source_dir = Path(tmp_dir) / "plugin-src"
plugin_source_dir.mkdir()
(plugin_source_dir / "main.py").write_text("print('hello')\n", encoding="utf-8")
args = module.argparse.Namespace(
astrbot_path="/tmp/AstrBot",
plugin_source_dir=str(plugin_source_dir),
plugin_dir_name="demo_plugin",
normalized_repo_url="https://github.com/example/demo-plugin",
)
with mock.patch.dict(os.environ, {}, clear=True):
with mock.patch.object(module, "run_worker_load_check", side_effect=fake_run_worker_load_check):
exit_code = module.run_worker(args) |
||
|
|
||
| self.assertEqual(exit_code, 0) | ||
| self.assertEqual(observed["plugin_dir_name"], "demo_plugin") | ||
| self.assertEqual( | ||
| observed["normalized_repo_url"], | ||
| "https://github.com/example/demo-plugin", | ||
| ) | ||
| self.assertIsNotNone(observed["astrbot_root"]) | ||
| self.assertIsNotNone(observed["pip_target"]) | ||
| self.assertEqual( | ||
| Path(observed["pip_target"]).resolve(), | ||
| (Path(observed["astrbot_root"]).parent / "site-packages").resolve(), | ||
| ) | ||
| self.assertIn(observed["pip_target"], observed["sys_path"]) | ||
|
|
||
|
|
||
| class ReportBuilderTests(unittest.TestCase): | ||
| def test_build_report_counts_passed_warned_and_failed_results(self): | ||
| module = load_validator_module() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Strengthen
run_workerisolation test by asserting properties of the temporary root, not just that it existsThe current test only checks that
ASTRBOT_ROOTandPIP_TARGETare set and related, but not that the worker is actually isolated from the real AstrBot path. Consider also asserting that:observed["astrbot_root"]differs fromargs.astrbot_path.observed["astrbot_root"]is under a temporary worker directory (e.g., theastrbot-plugin-worker-prefix or a parent dir starting with it).These checks would more directly validate that each worker uses its own temp root instead of the shared AstrBot path.
Suggested implementation:
To fully implement the suggested stronger isolation checks, you should:
Ensure that
test_configure_worker_install_target_deduplicates_process_paths(or another test inRunWorkerIsolationTests) captures:args = module.build_parser().parse_args([...])),temp_root, already present),observed["astrbot_root"](this appears to already exist per your comment).After the worker has been configured/run and
observed["astrbot_root"]is populated, call the helper:If part of the worker isolation contract is that specific plugin store/config directories are created under
astrbot_root, extend_assert_worker_isolatedwith additional checks, for example:and adjust the directory names to match your actual layout.
You’ll need to wire in step (2) at the appropriate point in the existing test body where
observedis fully populated, since that portion of the test is not visible in the provided snippet.