SIENTIAPDE-2226: Stop installing runtime in worker; unpin requirements

- The sientia-module entrypoint now merges requirements.txt with the
  Plugin Store runtime and installs both before boot (SIENTIAPDE-2223),
  so the worker no longer calls install_runtime or runs the loaded
  package version guard.
- Leave runtime libs unpinned in requirements files so the runtime
  version wins; cap sqlalchemy<2.1 and evidently<0.7 for compatibility.
- Update README, CLAUDE.md and worker tests to match.
This commit is contained in:
2026-10-27 10:43:41 -03:00
parent c71e426b18
commit 34eaa01ca7
6 changed files with 63 additions and 222 deletions

View File

@@ -3,7 +3,7 @@
import asyncio
import os
import sys
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from unittest.mock import AsyncMock, Mock, patch
import pytest
@@ -132,7 +132,7 @@ def test_start_prometheus_server_success(
# Verify HTTP server started
mock_start_http_server.assert_called_once_with(9090)
# app_up goes to 1 in main(), after the runtime version guard.
# app_up goes to 1 in main(), after initialization.
mock_app_up.set.assert_not_called()
mock_logger.custom_info.assert_called_once()
@@ -232,9 +232,6 @@ async def test_main_successful_startup(
mock_activities_class.return_value = mock_activities
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'model-manager-worker', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -283,8 +280,10 @@ async def test_main_successful_startup(
# Verify cleanup was performed
mock_notification_handler.shutdown.assert_called_once()
mock_activities.shutdown.assert_called_once()
# Ready after the guard, down again on shutdown.
# Ready after initialization, down again on shutdown.
assert [c.args[0] for c in mock_app_up.set.call_args_list] == [1, 0]
# The runtime libraries come from the sientia-module entrypoint, not from the worker.
mock_plugin_store_instance.install_runtime.assert_not_called()
@pytest.mark.asyncio
@@ -360,9 +359,6 @@ async def test_main_handles_exception(
mock_metrics.APP_UP.labels.return_value = mock_app_up
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'model-manager-worker', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -459,9 +455,6 @@ async def test_main_temporal_client_configuration(
mock_build_minio.return_value = {}
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'model-manager-worker', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -594,9 +587,6 @@ async def test_main_worker_configuration(
mock_metrics.APP_UP.labels.return_value = mock_app_up
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'model-manager-worker', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -716,9 +706,6 @@ async def test_main_schedule_creation_failure_does_not_stop_worker(
mock_metrics.APP_UP.labels.return_value = mock_app_up
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'model-manager-worker', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -816,9 +803,6 @@ async def test_main_missing_runtime_uses_single_fallback(
mock_prepare_worker.return_value = mock_worker_instance
mock_plugin_store_instance = AsyncMock()
mock_plugin_store_instance.install_runtime = AsyncMock(
return_value={'runtime': 'single', 'installed': []},
)
mock_plugin_store_class.return_value = mock_plugin_store_instance
mock_build_plugin_store_config.return_value = {
'base_url': 'http://sientia-plugin-store.svc.cluster.local',
@@ -903,105 +887,3 @@ def test_start_prometheus_server_prints_failure(
# Agora o erro é logado via logger crítico
mock_logger.custom_critical.assert_called_once()
mock_exit.assert_called_once_with(1)
def test_loaded_distribution_versions_reads_imported_packages():
"""An imported module maps to its distribution's installed version."""
import importlib.metadata
from model_manager.worker.worker import _loaded_distribution_versions
assert _loaded_distribution_versions()['pytest'] == importlib.metadata.version('pytest')
def test_runtime_changed_loaded_packages_reports_only_changes():
"""Only distributions whose version on disk differs after the install are reported."""
import importlib.metadata
from model_manager.worker.worker import _runtime_changed_loaded_packages
on_disk = {'scikit-learn': '1.8.0', 'numpy': '1.26.4'}
def fake_version(dist):
if dist not in on_disk:
raise importlib.metadata.PackageNotFoundError(dist)
return on_disk[dist]
before = {'scikit-learn': '1.9.1', 'numpy': '1.26.4', 'gone': '1.0'}
with patch('model_manager.worker.worker.importlib.metadata.version', side_effect=fake_version):
changed = _runtime_changed_loaded_packages(before)
assert changed == {'scikit-learn': '1.9.1 -> 1.8.0', 'gone': '1.0 -> removed'}
@pytest.mark.asyncio
async def test_main_runtime_changed_loaded_package_exits(mock_env_vars, mock_logger):
"""Boot stops before Activities when install_runtime changed an imported package."""
import model_manager.worker.worker as worker_module
plugin_store = Mock()
plugin_store.install_runtime = AsyncMock(return_value=None)
before = {'scikit-learn': '1.9.1'}
def snapshot():
# Taken after install_runtime, it would already see 1.8.0 and the guard would pass.
assert plugin_store.install_runtime.await_count == 0
return before
mongo = {'connection_string': 'mongodb://test', 'database_name': 'test_db', 'uri': 'x'}
with (
patch.object(worker_module, 'RUNTIME', 'basic'),
patch.object(worker_module, 'ensure_runtime_directories'),
patch.object(worker_module, 'metrics') as mock_metrics,
patch.object(worker_module, 'start_prometheus_server'),
patch.object(worker_module, 'get_logger', return_value=mock_logger),
patch.object(worker_module, 'build_mongodb_config', return_value=mongo),
patch.object(worker_module, 'NotificationHandler'),
patch.object(worker_module, 'build_plugin_store_config', return_value=MagicMock()),
patch.object(worker_module, 'PluginStore', return_value=plugin_store),
patch.object(worker_module, '_loaded_distribution_versions', side_effect=snapshot),
patch.object(
worker_module,
'_runtime_changed_loaded_packages',
return_value={'scikit-learn': '1.9.1 -> 1.8.0'},
) as mock_changed,
patch.object(worker_module, 'Activities') as mock_activities_class,
patch.object(worker_module.sys, 'exit', side_effect=SystemExit(1)) as mock_exit,
):
with pytest.raises(SystemExit):
await worker_module.main()
mock_exit.assert_called_once_with(1)
mock_activities_class.assert_not_called()
plugin_store.install_runtime.assert_awaited_once()
mock_changed.assert_called_once_with(before)
mock_metrics.APP_UP.labels.return_value.set.assert_called_once_with(0)
message = mock_logger.custom_critical.call_args.args[0]
assert "'scikit-learn': '1.9.1 -> 1.8.0'" in message
def test_guard_detects_version_change_on_disk(tmp_path, monkeypatch):
"""The real importlib.metadata path: a package swapped on disk after import is reported."""
import types
from model_manager.worker.worker import (
_loaded_distribution_versions,
_runtime_changed_loaded_packages,
)
info = tmp_path / 'fakepkg_2214-1.0.dist-info'
info.mkdir()
(info / 'METADATA').write_text('Metadata-Version: 2.1\nName: fakepkg-2214\nVersion: 1.0\n')
(info / 'top_level.txt').write_text('fakepkg_2214\n')
monkeypatch.syspath_prepend(str(tmp_path))
# The guard only reads sys.modules keys; setitem removes the entry again on teardown.
monkeypatch.setitem(sys.modules, 'fakepkg_2214', types.ModuleType('fakepkg_2214'))
before = _loaded_distribution_versions()
assert before['fakepkg-2214'] == '1.0'
# What pip does on an upgrade: the old dist-info goes away and a new one takes its place.
new_info = info.rename(tmp_path / 'fakepkg_2214-2.0.dist-info')
(new_info / 'METADATA').write_text('Metadata-Version: 2.1\nName: fakepkg-2214\nVersion: 2.0\n')
assert _runtime_changed_loaded_packages(before) == {'fakepkg-2214': '1.0 -> 2.0'}