simstack 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simstack/__init__.py +0 -0
- simstack/_version.py +24 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +406 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +283 -0
- simstack/core/definitions.py +22 -0
- simstack/core/engine.py +115 -0
- simstack/core/find_simstack_modules.py +41 -0
- simstack/core/hash.py +179 -0
- simstack/core/node.py +1137 -0
- simstack/core/node_claim.py +31 -0
- simstack/core/node_runner.py +362 -0
- simstack/core/process_results.py +102 -0
- simstack/core/recompute_artifacts.py +117 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resource_assignment.py +222 -0
- simstack/core/resources.py +90 -0
- simstack/core/route_table.py +24 -0
- simstack/core/run_docker.py +109 -0
- simstack/core/run_node.py +71 -0
- simstack/core/runner.py +169 -0
- simstack/core/services/__init__.py +11 -0
- simstack/core/services/base_service.py +191 -0
- simstack/core/services/file_transfer_service.py +117 -0
- simstack/core/services/git_restart_service.py +12 -0
- simstack/core/services/git_uv_update_service.py +105 -0
- simstack/core/services/node_execution_service.py +182 -0
- simstack/core/services/resource_branch_monitor_service.py +71 -0
- simstack/core/services/runner_cleanup_service.py +149 -0
- simstack/core/services/runner_manager.py +163 -0
- simstack/core/services/runner_status_service.py +10 -0
- simstack/core/services/slurm_status_service.py +67 -0
- simstack/core/services/stop_check_service.py +27 -0
- simstack/core/services/timeout_restart_service.py +26 -0
- simstack/core/simstack_result.py +39 -0
- simstack/core/submit_node.py +217 -0
- simstack/core/task_id.py +22 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/archive_files.py +292 -0
- simstack/methods/fire_and_forget_runner.py +113 -0
- simstack/methods/generate_test.py +125 -0
- simstack/methods/get_file.py +26 -0
- simstack/methods/mass_runner.py +154 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/methods/switch_git.py +16 -0
- simstack/methods/upload_helpers.py +81 -0
- simstack/models/__init__.py +74 -0
- simstack/models/array_list.py +21 -0
- simstack/models/array_storage.py +83 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_lists.py +51 -0
- simstack/models/base_types.py +209 -0
- simstack/models/charts_artifact.py +690 -0
- simstack/models/dataset.py +451 -0
- simstack/models/dataset_metadata.py +390 -0
- simstack/models/datasettuple.py +587 -0
- simstack/models/file_instance.py +150 -0
- simstack/models/file_list.py +45 -0
- simstack/models/files.py +424 -0
- simstack/models/fire_and_forget_result.py +19 -0
- simstack/models/images2d.py +113 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +52 -0
- simstack/models/named_data_reference.py +36 -0
- simstack/models/node_registry.py +96 -0
- simstack/models/pandas_model.py +239 -0
- simstack/models/parameters.py +402 -0
- simstack/models/pickle_models.py +300 -0
- simstack/models/project.py +13 -0
- simstack/models/resource_assignment.py +170 -0
- simstack/models/resource_definition.py +125 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +79 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/models/tag.py +12 -0
- simstack/models/unused_test_file.py +5 -0
- simstack/simstack_marker.com +1 -0
- simstack/static/docs/.buildinfo +4 -0
- simstack/static/docs/_images/GUI Concept.webp +0 -0
- simstack/static/docs/_modules/index.html +434 -0
- simstack/static/docs/_modules/simstack/core/artifacts.html +743 -0
- simstack/static/docs/_modules/simstack/core/asnyc_helper.html +330 -0
- simstack/static/docs/_modules/simstack/core/config_file.html +379 -0
- simstack/static/docs/_modules/simstack/core/definitions.html +341 -0
- simstack/static/docs/_modules/simstack/core/engine.html +437 -0
- simstack/static/docs/_modules/simstack/core/find_simstack_modules.html +360 -0
- simstack/static/docs/_modules/simstack/core/hash.html +528 -0
- simstack/static/docs/_modules/simstack/core/node.html +1392 -0
- simstack/static/docs/_modules/simstack/core/node_claim.html +347 -0
- simstack/static/docs/_modules/simstack/core/node_runner.html +711 -0
- simstack/static/docs/_modules/simstack/core/process_results.html +418 -0
- simstack/static/docs/_modules/simstack/core/recompute_artifacts.html +433 -0
- simstack/static/docs/_modules/simstack/core/reset_db.html +328 -0
- simstack/static/docs/_modules/simstack/core/resource_assignment.html +553 -0
- simstack/static/docs/_modules/simstack/core/resources.html +424 -0
- simstack/static/docs/_modules/simstack/core/route_table.html +349 -0
- simstack/static/docs/_modules/simstack/core/run_docker.html +425 -0
- simstack/static/docs/_modules/simstack/core/run_node.html +390 -0
- simstack/static/docs/_modules/simstack/core/runner.html +491 -0
- simstack/static/docs/_modules/simstack/core/services/base_service.html +522 -0
- simstack/static/docs/_modules/simstack/core/services/file_transfer_service.html +436 -0
- simstack/static/docs/_modules/simstack/core/services/git_restart_service.html +331 -0
- simstack/static/docs/_modules/simstack/core/services/git_uv_update_service.html +427 -0
- simstack/static/docs/_modules/simstack/core/services/node_execution_service.html +507 -0
- simstack/static/docs/_modules/simstack/core/services/resource_branch_monitor_service.html +390 -0
- simstack/static/docs/_modules/simstack/core/services/runner_cleanup_service.html +468 -0
- simstack/static/docs/_modules/simstack/core/services/runner_manager.html +485 -0
- simstack/static/docs/_modules/simstack/core/services/runner_status_service.html +329 -0
- simstack/static/docs/_modules/simstack/core/services/slurm_status_service.html +386 -0
- simstack/static/docs/_modules/simstack/core/services/stop_check_service.html +346 -0
- simstack/static/docs/_modules/simstack/core/services/timeout_restart_service.html +345 -0
- simstack/static/docs/_modules/simstack/core/simstack_result.html +355 -0
- simstack/static/docs/_modules/simstack/core/submit_node.html +536 -0
- simstack/static/docs/_modules/simstack/core/task_id.html +344 -0
- simstack/static/docs/_modules/simstack/methods/archive_files.html +598 -0
- simstack/static/docs/_modules/simstack/methods/fire_and_forget_runner.html +432 -0
- simstack/static/docs/_modules/simstack/methods/generate_test.html +443 -0
- simstack/static/docs/_modules/simstack/methods/get_file.html +342 -0
- simstack/static/docs/_modules/simstack/methods/mass_runner.html +476 -0
- simstack/static/docs/_modules/simstack/methods/multiple_executor.html +674 -0
- simstack/static/docs/_modules/simstack/methods/switch_git.html +332 -0
- simstack/static/docs/_modules/simstack/methods/upload_helpers.html +403 -0
- simstack/static/docs/_modules/simstack/models/array_list.html +337 -0
- simstack/static/docs/_modules/simstack/models/array_storage.html +417 -0
- simstack/static/docs/_modules/simstack/models/base_lists.html +374 -0
- simstack/static/docs/_modules/simstack/models/base_types.html +588 -0
- simstack/static/docs/_modules/simstack/models/charts_artifact.html +1035 -0
- simstack/static/docs/_modules/simstack/models/dataset.html +866 -0
- simstack/static/docs/_modules/simstack/models/dataset_metadata.html +711 -0
- simstack/static/docs/_modules/simstack/models/datasettuple.html +1005 -0
- simstack/static/docs/_modules/simstack/models/file_instance.html +472 -0
- simstack/static/docs/_modules/simstack/models/file_list.html +367 -0
- simstack/static/docs/_modules/simstack/models/files.html +773 -0
- simstack/static/docs/_modules/simstack/models/fire_and_forget_result.html +335 -0
- simstack/static/docs/_modules/simstack/models/images2d.html +447 -0
- simstack/static/docs/_modules/simstack/models/log_entry_model.html +356 -0
- simstack/static/docs/_modules/simstack/models/models.html +374 -0
- simstack/static/docs/_modules/simstack/models/named_data_reference.html +355 -0
- simstack/static/docs/_modules/simstack/models/node_registry.html +415 -0
- simstack/static/docs/_modules/simstack/models/pandas_model.html +570 -0
- simstack/static/docs/_modules/simstack/models/parameters.html +739 -0
- simstack/static/docs/_modules/simstack/models/pickle_models.html +631 -0
- simstack/static/docs/_modules/simstack/models/project.html +329 -0
- simstack/static/docs/_modules/simstack/models/resource_assignment.html +501 -0
- simstack/static/docs/_modules/simstack/models/resource_definition.html +477 -0
- simstack/static/docs/_modules/simstack/models/runner_model.html +361 -0
- simstack/static/docs/_modules/simstack/models/simple_table.html +413 -0
- simstack/static/docs/_modules/simstack/models/simstack_model.html +416 -0
- simstack/static/docs/_modules/simstack/models/slurm_info.html +333 -0
- simstack/static/docs/_modules/simstack/models/table_artifact.html +406 -0
- simstack/static/docs/_modules/simstack/models/tag.html +328 -0
- simstack/static/docs/_modules/simstack/tables/model_table.html +519 -0
- simstack/static/docs/_modules/simstack/tables/node_children.html +538 -0
- simstack/static/docs/_modules/simstack/tables/node_table.html +854 -0
- simstack/static/docs/_modules/simstack/tables/table_builder.html +642 -0
- simstack/static/docs/_modules/simstack/util/async_zip_utils.html +337 -0
- simstack/static/docs/_modules/simstack/util/b64mixin.html +339 -0
- simstack/static/docs/_modules/simstack/util/cleaned_json_schema.html +336 -0
- simstack/static/docs/_modules/simstack/util/config_reader.html +498 -0
- simstack/static/docs/_modules/simstack/util/custom_model_dump.html +391 -0
- simstack/static/docs/_modules/simstack/util/database_information.html +488 -0
- simstack/static/docs/_modules/simstack/util/db.html +1392 -0
- simstack/static/docs/_modules/simstack/util/db_logger.html +448 -0
- simstack/static/docs/_modules/simstack/util/default_from_dict.html +376 -0
- simstack/static/docs/_modules/simstack/util/directory_iterator.html +553 -0
- simstack/static/docs/_modules/simstack/util/docstring_parser.html +526 -0
- simstack/static/docs/_modules/simstack/util/file_hashing.html +394 -0
- simstack/static/docs/_modules/simstack/util/file_transfer_client.html +791 -0
- simstack/static/docs/_modules/simstack/util/files.html +733 -0
- simstack/static/docs/_modules/simstack/util/find_python_files.html +349 -0
- simstack/static/docs/_modules/simstack/util/generate_ui_schema.html +396 -0
- simstack/static/docs/_modules/simstack/util/generic_list_mixin.html +487 -0
- simstack/static/docs/_modules/simstack/util/get_module_path.html +336 -0
- simstack/static/docs/_modules/simstack/util/git_pull.html +381 -0
- simstack/static/docs/_modules/simstack/util/git_repository_status.html +411 -0
- simstack/static/docs/_modules/simstack/util/import_module.html +377 -0
- simstack/static/docs/_modules/simstack/util/importer.html +637 -0
- simstack/static/docs/_modules/simstack/util/init_data_source.html +357 -0
- simstack/static/docs/_modules/simstack/util/make_graph.html +423 -0
- simstack/static/docs/_modules/simstack/util/make_table.html +1171 -0
- simstack/static/docs/_modules/simstack/util/mappings.html +378 -0
- simstack/static/docs/_modules/simstack/util/minimal_route_finder.html +388 -0
- simstack/static/docs/_modules/simstack/util/mock_db.html +472 -0
- simstack/static/docs/_modules/simstack/util/module_path_checker.html +354 -0
- simstack/static/docs/_modules/simstack/util/object_list_mixin.html +649 -0
- simstack/static/docs/_modules/simstack/util/path_manager.html +566 -0
- simstack/static/docs/_modules/simstack/util/project_root_finder.html +357 -0
- simstack/static/docs/_modules/simstack/util/queue_watcher.html +593 -0
- simstack/static/docs/_modules/simstack/util/resource_config.html +532 -0
- simstack/static/docs/_modules/simstack/util/route_finder.html +390 -0
- simstack/static/docs/_modules/simstack/util/runner_util.html +487 -0
- simstack/static/docs/_modules/simstack/util/runner_utils.html +468 -0
- simstack/static/docs/_modules/simstack/util/safe_code_executor.html +510 -0
- simstack/static/docs/_modules/simstack/util/sample_config.html +414 -0
- simstack/static/docs/_modules/simstack/util/sensitive_content_filter.html +364 -0
- simstack/static/docs/_modules/simstack/util/setup_logging.html +365 -0
- simstack/static/docs/_modules/simstack/util/submit_to_watchdog.html +425 -0
- simstack/static/docs/_modules/simstack/util/toml_reader.html +482 -0
- simstack/static/docs/_modules/simstack/util/transform_file_name.html +357 -0
- simstack/static/docs/_modules/simstack/util/ui_tools.html +541 -0
- simstack/static/docs/_sources/architecture.rst.txt +25 -0
- simstack/static/docs/_sources/array_lists.rst.txt +60 -0
- simstack/static/docs/_sources/ci_pipeline.rst.txt +34 -0
- simstack/static/docs/_sources/data.rst.txt +175 -0
- simstack/static/docs/_sources/database_migrations.rst.txt +310 -0
- simstack/static/docs/_sources/datasets.rst.txt +93 -0
- simstack/static/docs/_sources/developer_guide.rst.txt +25 -0
- simstack/static/docs/_sources/docker_nodes.rst.txt +149 -0
- simstack/static/docs/_sources/files.rst.txt +159 -0
- simstack/static/docs/_sources/fire_and_forget_runner.rst.txt +67 -0
- simstack/static/docs/_sources/index.rst.txt +25 -0
- simstack/static/docs/_sources/installation.rst.txt +213 -0
- simstack/static/docs/_sources/lists.rst.txt +111 -0
- simstack/static/docs/_sources/mass_runner.rst.txt +91 -0
- simstack/static/docs/_sources/misc.rst.txt +106 -0
- simstack/static/docs/_sources/overview.rst.txt +91 -0
- simstack/static/docs/_sources/recovery.rst.txt +38 -0
- simstack/static/docs/_sources/resource_config.rst.txt +141 -0
- simstack/static/docs/_sources/simstack/modules/modules.rst.txt +7 -0
- simstack/static/docs/_sources/simstack/modules/simstack.core.rst.txt +205 -0
- simstack/static/docs/_sources/simstack/modules/simstack.core.services.rst.txt +109 -0
- simstack/static/docs/_sources/simstack/modules/simstack.methods.rst.txt +77 -0
- simstack/static/docs/_sources/simstack/modules/simstack.models.rst.txt +261 -0
- simstack/static/docs/_sources/simstack/modules/simstack.rst.txt +22 -0
- simstack/static/docs/_sources/simstack/modules/simstack.tables.rst.txt +45 -0
- simstack/static/docs/_sources/simstack/modules/simstack.util.rst.txt +381 -0
- simstack/static/docs/_sources/submit.rst.txt +141 -0
- simstack/static/docs/_sources/tables.rst.txt +50 -0
- simstack/static/docs/_sources/testing_complex_nodes.rst.txt +92 -0
- simstack/static/docs/_sources/ui_documentation.rst.txt +272 -0
- simstack/static/docs/_sources/workflows.rst.txt +446 -0
- simstack/static/docs/_static/base-stemmer.js +476 -0
- simstack/static/docs/_static/basic.css +906 -0
- simstack/static/docs/_static/cobalt2-theme.css +1 -0
- simstack/static/docs/_static/debug.css +69 -0
- simstack/static/docs/_static/docs-build-badge.css +51 -0
- simstack/static/docs/_static/docs-build-badge.js +51 -0
- simstack/static/docs/_static/doctools.js +150 -0
- simstack/static/docs/_static/documentation_options.js +13 -0
- simstack/static/docs/_static/english-stemmer.js +1066 -0
- simstack/static/docs/_static/file.png +0 -0
- simstack/static/docs/_static/language_data.js +13 -0
- simstack/static/docs/_static/minus.png +0 -0
- simstack/static/docs/_static/plus.png +0 -0
- simstack/static/docs/_static/pygments.css +250 -0
- simstack/static/docs/_static/scripts/furo-extensions.js +0 -0
- simstack/static/docs/_static/scripts/furo.js +3 -0
- simstack/static/docs/_static/scripts/furo.js.LICENSE.txt +7 -0
- simstack/static/docs/_static/scripts/furo.js.map +1 -0
- simstack/static/docs/_static/searchtools.js +693 -0
- simstack/static/docs/_static/skeleton.css +296 -0
- simstack/static/docs/_static/sphinx_highlight.js +159 -0
- simstack/static/docs/_static/styles/furo-extensions.css +2 -0
- simstack/static/docs/_static/styles/furo-extensions.css.map +1 -0
- simstack/static/docs/_static/styles/furo.css +2 -0
- simstack/static/docs/_static/styles/furo.css.map +1 -0
- simstack/static/docs/architecture.html +340 -0
- simstack/static/docs/array_lists.html +678 -0
- simstack/static/docs/ci_pipeline.html +385 -0
- simstack/static/docs/data.html +545 -0
- simstack/static/docs/database_migrations.html +652 -0
- simstack/static/docs/datasets.html +1268 -0
- simstack/static/docs/developer_guide.html +359 -0
- simstack/static/docs/docker_nodes.html +503 -0
- simstack/static/docs/files.html +718 -0
- simstack/static/docs/fire_and_forget_runner.html +641 -0
- simstack/static/docs/genindex.html +6390 -0
- simstack/static/docs/index.html +516 -0
- simstack/static/docs/installation.html +553 -0
- simstack/static/docs/lists.html +478 -0
- simstack/static/docs/mass_runner.html +481 -0
- simstack/static/docs/misc.html +454 -0
- simstack/static/docs/objects.inv +0 -0
- simstack/static/docs/overview.html +469 -0
- simstack/static/docs/py-modindex.html +1111 -0
- simstack/static/docs/recovery.html +371 -0
- simstack/static/docs/resource_config.html +590 -0
- simstack/static/docs/search.html +327 -0
- simstack/static/docs/searchindex.js +1 -0
- simstack/static/docs/simstack/modules/modules.html +489 -0
- simstack/static/docs/simstack/modules/simstack.core.html +1883 -0
- simstack/static/docs/simstack/modules/simstack.core.services.html +698 -0
- simstack/static/docs/simstack/modules/simstack.html +3060 -0
- simstack/static/docs/simstack/modules/simstack.methods.html +1150 -0
- simstack/static/docs/simstack/modules/simstack.models.html +13113 -0
- simstack/static/docs/simstack/modules/simstack.tables.html +635 -0
- simstack/static/docs/simstack/modules/simstack.util.html +3320 -0
- simstack/static/docs/submit.html +487 -0
- simstack/static/docs/tables.html +539 -0
- simstack/static/docs/testing_complex_nodes.html +458 -0
- simstack/static/docs/ui_documentation.html +788 -0
- simstack/static/docs/workflows.html +766 -0
- simstack/tables/__init__.py +0 -0
- simstack/tables/model_table.py +191 -0
- simstack/tables/node_children.py +219 -0
- simstack/tables/node_table.py +517 -0
- simstack/tables/table_builder.py +314 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/b64mixin.py +23 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +179 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/database_information.py +160 -0
- simstack/util/db.py +1013 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/docstring_parser.py +186 -0
- simstack/util/file_hashing.py +72 -0
- simstack/util/file_transfer_client.py +427 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/generic_list_mixin.py +120 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +20 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +95 -0
- simstack/util/import_module.py +63 -0
- simstack/util/importer.py +312 -0
- simstack/util/init_data_source.py +41 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +837 -0
- simstack/util/mappings.py +41 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/object_list_mixin.py +282 -0
- simstack/util/path_manager.py +223 -0
- simstack/util/project_root_finder.py +41 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/resource_config.py +198 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +166 -0
- simstack/util/runner_utils.py +143 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/toml_reader.py +142 -0
- simstack/util/transform_file_name.py +41 -0
- simstack/util/ui_tools.py +213 -0
- simstack-2.0.1.dist-info/METADATA +454 -0
- simstack-2.0.1.dist-info/RECORD +354 -0
- simstack-2.0.1.dist-info/WHEEL +5 -0
- simstack-2.0.1.dist-info/entry_points.txt +12 -0
- simstack-2.0.1.dist-info/licenses/LICENSE +21 -0
- simstack-2.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
import sys
|
|
6
|
+
import platform
|
|
7
|
+
import subprocess
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from odmantic import ObjectId
|
|
12
|
+
|
|
13
|
+
from simstack.core.context import context
|
|
14
|
+
from simstack.models.parameters import Resource
|
|
15
|
+
from simstack.models.runner_model import RunnerEvent, RunnerType, RunnerEventEnum
|
|
16
|
+
from simstack.util.runner_utils import make_git_status_list
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("NodeRunner")
|
|
19
|
+
|
|
20
|
+
class BaseService(ABC):
|
|
21
|
+
"""A managed periodic service that can be stopped gracefully"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str, resource: Resource, interval: int, shutdown_event: asyncio.Event = None):
|
|
24
|
+
self._name = name
|
|
25
|
+
self._resource = resource
|
|
26
|
+
self._interval: int = interval
|
|
27
|
+
self._stop_event = asyncio.Event()
|
|
28
|
+
self._shutdown_event = shutdown_event
|
|
29
|
+
self._task = None
|
|
30
|
+
self._consecutive_failures = 0
|
|
31
|
+
self._log_traceback_on_failure = True
|
|
32
|
+
|
|
33
|
+
# Common identity attributes
|
|
34
|
+
self._pid = os.getpid()
|
|
35
|
+
self._hostname = socket.gethostname()
|
|
36
|
+
self._username = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
|
|
37
|
+
self._time_started = datetime.now()
|
|
38
|
+
|
|
39
|
+
def _get_uptime_string(self) -> str:
|
|
40
|
+
time_diff = datetime.now() - self._time_started
|
|
41
|
+
total_seconds = int(time_diff.total_seconds())
|
|
42
|
+
days, remainder = divmod(total_seconds, 86400)
|
|
43
|
+
hours, remainder = divmod(remainder, 3600)
|
|
44
|
+
minutes, seconds = divmod(remainder, 60)
|
|
45
|
+
return f"{days}d {hours}h {minutes}m {seconds}s"
|
|
46
|
+
|
|
47
|
+
async def write_node_event(self, event: RunnerEventEnum, node_id: ObjectId, message: str = None):
|
|
48
|
+
runner_event = RunnerEvent(
|
|
49
|
+
runner_type=RunnerType.NODE_RUNNER,
|
|
50
|
+
event=event,
|
|
51
|
+
pid=self._pid,
|
|
52
|
+
hostname=self._hostname,
|
|
53
|
+
user=self._username,
|
|
54
|
+
resource=self._resource,
|
|
55
|
+
node_id=node_id,
|
|
56
|
+
message=message,
|
|
57
|
+
)
|
|
58
|
+
await context.db.save(runner_event)
|
|
59
|
+
|
|
60
|
+
async def write_resource_event(self, event: RunnerEventEnum, message: str = None):
|
|
61
|
+
git_list = make_git_status_list()
|
|
62
|
+
if event == RunnerEventEnum.ALIVE:
|
|
63
|
+
uptime = self._get_uptime_string()
|
|
64
|
+
message = f"Uptime: {uptime}"
|
|
65
|
+
|
|
66
|
+
runner_event = await context.db.find_one(
|
|
67
|
+
RunnerEvent,
|
|
68
|
+
(RunnerEvent.runner_type == RunnerType.RESOURCE_RUNNER)
|
|
69
|
+
& (RunnerEvent.resource == self._resource)
|
|
70
|
+
& (RunnerEvent.event == RunnerEventEnum.ALIVE)
|
|
71
|
+
& (RunnerEvent.pid == self._pid),
|
|
72
|
+
)
|
|
73
|
+
if runner_event:
|
|
74
|
+
runner_event.message = message
|
|
75
|
+
runner_event.timestamp = datetime.now()
|
|
76
|
+
runner_event.git_status = git_list
|
|
77
|
+
await context.db.save(runner_event)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
runner_event = RunnerEvent(
|
|
81
|
+
runner_type=RunnerType.RESOURCE_RUNNER,
|
|
82
|
+
pid=self._pid,
|
|
83
|
+
hostname=self._hostname,
|
|
84
|
+
user=self._username,
|
|
85
|
+
timestamp=datetime.now(),
|
|
86
|
+
git_status=git_list,
|
|
87
|
+
event=event,
|
|
88
|
+
resource=self._resource,
|
|
89
|
+
message=message,
|
|
90
|
+
)
|
|
91
|
+
await context.db.save(runner_event)
|
|
92
|
+
|
|
93
|
+
async def _run_loop(self):
|
|
94
|
+
"""Internal loop that respects the stop event"""
|
|
95
|
+
logger.info(f"Service {self._name} started.")
|
|
96
|
+
while not self._stop_event.is_set():
|
|
97
|
+
start_time = asyncio.get_event_loop().time()
|
|
98
|
+
try:
|
|
99
|
+
await self.execute()
|
|
100
|
+
self._consecutive_failures = 0
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self._consecutive_failures += 1
|
|
103
|
+
msg = f"Error in service {self._name} (failure {self._consecutive_failures}/3): {e}"
|
|
104
|
+
if self._log_traceback_on_failure:
|
|
105
|
+
logger.exception(msg)
|
|
106
|
+
else:
|
|
107
|
+
logger.error(msg)
|
|
108
|
+
|
|
109
|
+
if self._consecutive_failures >= 3:
|
|
110
|
+
logger.error(f"Service {self._name} failed 3 times in a row. Shutting down.")
|
|
111
|
+
self._stop_event.set()
|
|
112
|
+
if self._shutdown_event:
|
|
113
|
+
self._shutdown_event.set()
|
|
114
|
+
|
|
115
|
+
# Calculate wait time to maintain interval regardless of execution duration
|
|
116
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
117
|
+
wait_time = max(0, int(self._interval - elapsed))
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Wait for interval OR until stop event is set
|
|
121
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=wait_time)
|
|
122
|
+
except asyncio.TimeoutError:
|
|
123
|
+
# Normal path: the interval passed, loop again
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
logger.info(f"Service {self._name} stopped.")
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def execute(self):
|
|
130
|
+
raise NotImplementedError("Services must implement execute()")
|
|
131
|
+
|
|
132
|
+
def start(self):
|
|
133
|
+
if self._task is None or self._task.done():
|
|
134
|
+
self._stop_event.clear()
|
|
135
|
+
self._task = asyncio.create_task(self._run_loop())
|
|
136
|
+
return self._task
|
|
137
|
+
|
|
138
|
+
async def stop(self):
|
|
139
|
+
self._stop_event.set()
|
|
140
|
+
if self._task:
|
|
141
|
+
await self._task
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RestartService(BaseService, ABC):
|
|
145
|
+
"""
|
|
146
|
+
Intermediate abstract class for services that can trigger a runner restart.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, name: str, resource: Resource, interval: int):
|
|
150
|
+
super().__init__(name, resource, interval)
|
|
151
|
+
self._pid_file = context.config.workdir / "runner.pid"
|
|
152
|
+
|
|
153
|
+
async def trigger_restart(self):
|
|
154
|
+
"""Spawns an independent process to kill current PID and start new one"""
|
|
155
|
+
current_pid = os.getpid()
|
|
156
|
+
|
|
157
|
+
# Write current PID to file so the restarter knows who to kill
|
|
158
|
+
self._pid_file.write_text(str(current_pid))
|
|
159
|
+
|
|
160
|
+
# Command to restart depends on OS
|
|
161
|
+
# We use sys.executable to ensure we use the same Python interpreter
|
|
162
|
+
# We'll call the runner module again
|
|
163
|
+
# IMPORTANT: We need the path to the original runner.py script.
|
|
164
|
+
# Since we are now in services/base_service.py, we need to go up two levels.
|
|
165
|
+
# But actually Path(__file__).resolve() in the original runner.py was used.
|
|
166
|
+
# We should probably pass the runner script path or rely on how it was invoked.
|
|
167
|
+
# In runner.py it was: script_path = Path(__file__).resolve()
|
|
168
|
+
|
|
169
|
+
# Let's try to find runner.py relative to this file.
|
|
170
|
+
script_path = (Path(__file__).parent.parent / "runner.py").resolve()
|
|
171
|
+
|
|
172
|
+
log_file = context.config.workdir / "runner.out"
|
|
173
|
+
|
|
174
|
+
args = [sys.executable, str(script_path)] + sys.argv[1:]
|
|
175
|
+
|
|
176
|
+
# We need a small helper script or a one-liner that:
|
|
177
|
+
# 1. Waits for this process to die (to avoid port/resource conflicts)
|
|
178
|
+
# 2. Starts the new one
|
|
179
|
+
if platform.system() == "Windows":
|
|
180
|
+
# Windows 'start' command handles detachment well
|
|
181
|
+
# Use ping for a 2-second delay as 'timeout' fails in non-interactive shells
|
|
182
|
+
cmd = f"taskkill /F /PID {current_pid} && ping 127.0.0.1 -n 3 > nul && {' '.join(args)} >> \"{log_file}\" 2>&1"
|
|
183
|
+
subprocess.Popen(cmd, shell=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
|
|
184
|
+
else:
|
|
185
|
+
# Linux: use a subshell that nohup/disowns
|
|
186
|
+
# Kill, wait, and start
|
|
187
|
+
cmd = f"kill -9 {current_pid} && sleep 2 && {' '.join(args)} >> \"{log_file}\" 2>&1"
|
|
188
|
+
subprocess.Popen(["/bin/bash", "-c", cmd], start_new_session=True)
|
|
189
|
+
|
|
190
|
+
# The above commands kill us, so this line might not even log
|
|
191
|
+
logger.info("Restart signal sent. Goodbye!")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from simstack.core.context import context
|
|
7
|
+
from simstack.core.services.base_service import BaseService
|
|
8
|
+
from simstack.models.parameters import Resource
|
|
9
|
+
from simstack.util.file_transfer_client import (
|
|
10
|
+
FileTransferClient,
|
|
11
|
+
FileTransferError,
|
|
12
|
+
resolve_instance_path,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("NodeRunner")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileTransferService(BaseService):
|
|
19
|
+
"""Poll the SimStack server for hidden file transfer uploads assigned to this runner."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
resource: Resource,
|
|
24
|
+
interval: int = 10,
|
|
25
|
+
max_concurrent: int = 2,
|
|
26
|
+
shutdown_event: asyncio.Event | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(
|
|
29
|
+
"FileTransfer", resource, interval, shutdown_event=shutdown_event
|
|
30
|
+
)
|
|
31
|
+
self._log_traceback_on_failure = False
|
|
32
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
33
|
+
self._running_tasks: set[asyncio.Task[bool]] = set()
|
|
34
|
+
self._client: FileTransferClient | None = None
|
|
35
|
+
self._configuration_checked = False
|
|
36
|
+
|
|
37
|
+
def _get_client(self) -> FileTransferClient | None:
|
|
38
|
+
if self._client is not None:
|
|
39
|
+
return self._client
|
|
40
|
+
if self._configuration_checked:
|
|
41
|
+
return None
|
|
42
|
+
self._configuration_checked = True
|
|
43
|
+
self._client = FileTransferClient.from_context(required=False)
|
|
44
|
+
if self._client is None:
|
|
45
|
+
logger.info(
|
|
46
|
+
"File transfer service disabled; SIMSTACK_SERVER_URL or SIMSTACK_RUNNER_TOKEN is not configured."
|
|
47
|
+
)
|
|
48
|
+
return self._client
|
|
49
|
+
|
|
50
|
+
async def execute(self) -> None:
|
|
51
|
+
completed_tasks = {task for task in self._running_tasks if task.done()}
|
|
52
|
+
for task in completed_tasks:
|
|
53
|
+
try:
|
|
54
|
+
await task
|
|
55
|
+
except Exception as exc:
|
|
56
|
+
logger.exception("File transfer task completed with error: %s", exc)
|
|
57
|
+
self._running_tasks.remove(task)
|
|
58
|
+
|
|
59
|
+
client = self._get_client()
|
|
60
|
+
if client is None:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
transfers = client.list_transfers(role="source", status="created", limit=10)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise exc
|
|
67
|
+
|
|
68
|
+
for transfer in transfers:
|
|
69
|
+
transfer_id = str(transfer.get("transfer_id") or "")
|
|
70
|
+
if not transfer_id:
|
|
71
|
+
continue
|
|
72
|
+
if any(
|
|
73
|
+
getattr(task, "transfer_id", None) == transfer_id
|
|
74
|
+
for task in self._running_tasks
|
|
75
|
+
):
|
|
76
|
+
continue
|
|
77
|
+
task = asyncio.create_task(self._run_with_semaphore(client, transfer))
|
|
78
|
+
setattr(task, "transfer_id", transfer_id)
|
|
79
|
+
self._running_tasks.add(task)
|
|
80
|
+
|
|
81
|
+
async def _run_with_semaphore(
|
|
82
|
+
self, client: FileTransferClient, transfer: dict[str, Any]
|
|
83
|
+
) -> bool:
|
|
84
|
+
async with self._semaphore:
|
|
85
|
+
return await asyncio.to_thread(self._upload_transfer, client, transfer)
|
|
86
|
+
|
|
87
|
+
def _upload_transfer(
|
|
88
|
+
self, client: FileTransferClient, transfer: dict[str, Any]
|
|
89
|
+
) -> bool:
|
|
90
|
+
transfer_id = str(transfer.get("transfer_id"))
|
|
91
|
+
source_path = transfer.get("source_path")
|
|
92
|
+
if not source_path:
|
|
93
|
+
client.fail_transfer(
|
|
94
|
+
transfer_id,
|
|
95
|
+
error_message="Transfer is missing source_path metadata.",
|
|
96
|
+
error_code="SOURCE_PATH_MISSING",
|
|
97
|
+
)
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
path = resolve_instance_path(str(source_path), Path(context.config.workdir))
|
|
101
|
+
try:
|
|
102
|
+
client.upload_file(transfer_id, path)
|
|
103
|
+
logger.info("Uploaded FileStack transfer %s from %s", transfer_id, path)
|
|
104
|
+
return True
|
|
105
|
+
except FileTransferError as exc:
|
|
106
|
+
logger.warning("File transfer upload failed for %s: %s", transfer_id, exc)
|
|
107
|
+
try:
|
|
108
|
+
client.fail_transfer(
|
|
109
|
+
transfer_id,
|
|
110
|
+
error_message=str(exc),
|
|
111
|
+
error_code="SOURCE_UPLOAD_FAILED",
|
|
112
|
+
)
|
|
113
|
+
except Exception:
|
|
114
|
+
logger.exception(
|
|
115
|
+
"Failed to report failed file transfer %s", transfer_id
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from simstack.models.parameters import Resource
|
|
2
|
+
from simstack.core.services.base_service import BaseService
|
|
3
|
+
|
|
4
|
+
class GitRestartService(BaseService):
|
|
5
|
+
"""Service that runs an external script to check git/restart the runner"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, resource: Resource, interval):
|
|
8
|
+
super().__init__("GitRestart", resource, interval)
|
|
9
|
+
self._resource_name = str(resource)
|
|
10
|
+
|
|
11
|
+
def execute(self):
|
|
12
|
+
raise NotImplementedError("GitRestartService must implement execute()")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import hashlib
|
|
5
|
+
|
|
6
|
+
from simstack.core.context import context
|
|
7
|
+
from simstack.models.parameters import Resource
|
|
8
|
+
from simstack.models.runner_model import RunnerEventEnum
|
|
9
|
+
from simstack.core.services.base_service import RestartService
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("NodeRunner")
|
|
12
|
+
|
|
13
|
+
def get_file_checksum(filepath: Path) -> str:
|
|
14
|
+
"""Calculate SHA256 checksum of a file"""
|
|
15
|
+
if not filepath.exists():
|
|
16
|
+
return ""
|
|
17
|
+
hash_sha256 = hashlib.sha256()
|
|
18
|
+
with open(filepath, "rb") as f:
|
|
19
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
20
|
+
hash_sha256.update(chunk)
|
|
21
|
+
return hash_sha256.hexdigest()
|
|
22
|
+
|
|
23
|
+
class GitUvUpdateService(RestartService):
|
|
24
|
+
"""
|
|
25
|
+
Advanced service that performs git pull and uv lock upgrades.
|
|
26
|
+
If changes are detected, it restarts the runner.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, resource: Resource, interval):
|
|
30
|
+
super().__init__("GitUvUpdate", resource, interval)
|
|
31
|
+
# Resolve project root (assuming we are in src/simstack/core/runner.py)
|
|
32
|
+
self._project_dir = context.config.project_root.resolve(strict=True)
|
|
33
|
+
self._uv_lock_path = context.config.project_root / "uv.lock"
|
|
34
|
+
self._uv_extra_depencency_path = context.config.project_root / "user_extra_config.toml"
|
|
35
|
+
self.extras=False
|
|
36
|
+
self.desired_extras=[]
|
|
37
|
+
#check if the file _uv_extra_depencency_path exists if yes - read toml and then extras=True
|
|
38
|
+
try :
|
|
39
|
+
import tomllib
|
|
40
|
+
logger.info(f" trying to read user-specified (extras) optional dependencies from {self._uv_extra_depencency_path}")
|
|
41
|
+
with open (self._uv_extra_depencency_path,"rb") as f:
|
|
42
|
+
desired_extras_all=tomllib.load(f)
|
|
43
|
+
# Access the nested section - this was the issue with the keys
|
|
44
|
+
extras_section = desired_extras_all.get("optional_dependencies_desired_by_ressource", {})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Access raw value without triggering validation against allowed_resources
|
|
48
|
+
resource_name = object.__getattribute__(resource, "__dict__").get("value") or str(resource)
|
|
49
|
+
#logger.info(f" name {resource_name} , type {type(resource_name)}, compared against {extras_section.keys()}")
|
|
50
|
+
if resource_name in extras_section.keys():
|
|
51
|
+
self.desired_extras = extras_section[resource_name]
|
|
52
|
+
self.extras = True
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.warning(f"resource {resource} spec with user file {self._uv_extra_depencency_path} failed with the exception {e}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _run_command(self, cmd: list, ignore_error=False) -> str:
|
|
61
|
+
"""Run a shell command and return output"""
|
|
62
|
+
process = await asyncio.create_subprocess_exec(
|
|
63
|
+
*cmd,
|
|
64
|
+
cwd=str(self._project_dir),
|
|
65
|
+
stdout=asyncio.subprocess.PIPE,
|
|
66
|
+
stderr=asyncio.subprocess.PIPE
|
|
67
|
+
)
|
|
68
|
+
stdout, stderr = await process.communicate()
|
|
69
|
+
if process.returncode != 0:
|
|
70
|
+
if ignore_error:
|
|
71
|
+
logger.warning(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
|
|
74
|
+
return stdout.decode().strip()
|
|
75
|
+
|
|
76
|
+
async def execute(self):
|
|
77
|
+
# 1. Git Pull
|
|
78
|
+
# Check checksum before and after pull to see if Git brought a new lockfile
|
|
79
|
+
# old_uv_checksum = get_file_checksum(self._uv_lock_path)
|
|
80
|
+
|
|
81
|
+
# Ensure we don't have local lockfile changes that block the pull
|
|
82
|
+
|
|
83
|
+
await self._run_command(["git", "stash"], ignore_error=True)
|
|
84
|
+
|
|
85
|
+
git_output = await self._run_command(["git", "pull", "--recurse-submodules" ])
|
|
86
|
+
git_changed = "Already up to date." not in git_output
|
|
87
|
+
|
|
88
|
+
# Clear the stash now that we've pulled
|
|
89
|
+
# await self._run_command(["git", "stash", "drop"], ignore_error=True)
|
|
90
|
+
|
|
91
|
+
if git_changed: # or uv_locally_upgraded:
|
|
92
|
+
command_list=["uv", "sync", "--locked"]
|
|
93
|
+
if self.extras and len(self.desired_extras) > 0:
|
|
94
|
+
|
|
95
|
+
for desired_extra in self.desired_extras:
|
|
96
|
+
command_list.extend(["--extra", str(desired_extra)])
|
|
97
|
+
logger.info(f"user specified extra dependency {desired_extra}")
|
|
98
|
+
|
|
99
|
+
await self._run_command(command_list) # Update local .venv but with extras as specified by the user for the ressource
|
|
100
|
+
else:
|
|
101
|
+
await self._run_command(command_list) # Update local .venv
|
|
102
|
+
reason = "Git pull" if git_changed else "Local UV upgrade"
|
|
103
|
+
await self.write_resource_event(RunnerEventEnum.SHUTDOWN, message=reason)
|
|
104
|
+
logger.info(f"Update detected ({reason}). Triggering restart...")
|
|
105
|
+
await self.trigger_restart()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from simstack.core.context import context
|
|
7
|
+
from simstack.core.definitions import TaskStatus
|
|
8
|
+
from simstack.core.node import node_from_database
|
|
9
|
+
from simstack.core.node_claim import claim_submitted_node
|
|
10
|
+
from simstack.core.run_docker import run_docker
|
|
11
|
+
from simstack.core.services.base_service import BaseService
|
|
12
|
+
from simstack.core.submit_node import submit_node
|
|
13
|
+
from simstack.models import NodeRegistry
|
|
14
|
+
from simstack.models.parameters import Resource
|
|
15
|
+
from simstack.models.runner_model import RunnerEventEnum
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("NodeRunner")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def run_node_from_registry(registry_entry: NodeRegistry) -> bool:
|
|
21
|
+
# Create the node from the registry entry
|
|
22
|
+
node = await node_from_database(registry_entry)
|
|
23
|
+
if not node:
|
|
24
|
+
logger.error(
|
|
25
|
+
f"Failed to create node from registry entry task_id: {registry_entry.id} on resource {context.config.resource}"
|
|
26
|
+
)
|
|
27
|
+
registry_entry.status = TaskStatus.FAILED
|
|
28
|
+
await context.db.save(registry_entry)
|
|
29
|
+
return False
|
|
30
|
+
registry_entry = node.registry_entry # it may have changed
|
|
31
|
+
assert registry_entry is not None
|
|
32
|
+
if (
|
|
33
|
+
node.status == TaskStatus.RETRIEVED
|
|
34
|
+
or node.status == TaskStatus.SUBMITTED
|
|
35
|
+
or node.status == TaskStatus.SLURM_QUEUED
|
|
36
|
+
) or (
|
|
37
|
+
node.status == TaskStatus.COMPLETED and registry_entry.parameters.force_rerun
|
|
38
|
+
):
|
|
39
|
+
await node.execute_node_locally()
|
|
40
|
+
else:
|
|
41
|
+
logger.info(
|
|
42
|
+
f"task_id: {registry_entry.id} skipping task: {registry_entry.name} with status {registry_entry.status}"
|
|
43
|
+
)
|
|
44
|
+
return bool(node.status == TaskStatus.COMPLETED)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NodeExecutionService(BaseService):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
resource: Resource,
|
|
51
|
+
interval: int,
|
|
52
|
+
max_concurrent: int,
|
|
53
|
+
shutdown_event: asyncio.Event | None,
|
|
54
|
+
detach: bool = True,
|
|
55
|
+
is_default: bool = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__(
|
|
58
|
+
"JobPolling", resource, interval, shutdown_event=shutdown_event
|
|
59
|
+
)
|
|
60
|
+
self._resource_name = str(resource)
|
|
61
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
62
|
+
self._running_tasks: set[asyncio.Task[bool]] = set()
|
|
63
|
+
self._started = False
|
|
64
|
+
self._detach = detach
|
|
65
|
+
self._is_default = is_default
|
|
66
|
+
|
|
67
|
+
async def run_node(self, registry_entry: NodeRegistry) -> bool:
|
|
68
|
+
"""Run a single node by its ID from the database"""
|
|
69
|
+
|
|
70
|
+
await self.write_node_event(RunnerEventEnum.NODE_STARTED, registry_entry.id)
|
|
71
|
+
try:
|
|
72
|
+
logger.info(
|
|
73
|
+
f"Running node task_id: {registry_entry.id} on resource {context.config.resource} with status {registry_entry.status}"
|
|
74
|
+
)
|
|
75
|
+
queue = (
|
|
76
|
+
registry_entry.parameters.queue
|
|
77
|
+
if hasattr(registry_entry.parameters, "queue")
|
|
78
|
+
else "default"
|
|
79
|
+
)
|
|
80
|
+
if queue is None:
|
|
81
|
+
logger.error(
|
|
82
|
+
f"Queue parameter not found for task_id: {registry_entry.id}"
|
|
83
|
+
)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
if queue == "slurm-queue":
|
|
87
|
+
return await submit_node(registry_entry)
|
|
88
|
+
elif queue == "docker":
|
|
89
|
+
return await run_docker(registry_entry)
|
|
90
|
+
|
|
91
|
+
elif queue == "default":
|
|
92
|
+
if self._detach:
|
|
93
|
+
# Spawn independent process that survives when the runner dies
|
|
94
|
+
cmd = [
|
|
95
|
+
"uv",
|
|
96
|
+
"run",
|
|
97
|
+
"--directory",
|
|
98
|
+
str(context.config.project_root),
|
|
99
|
+
"run_node",
|
|
100
|
+
"--node-id",
|
|
101
|
+
str(registry_entry.id),
|
|
102
|
+
"--resource",
|
|
103
|
+
str(self._resource_name),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Use platform specific flags to ensure the process survives if runner is killed
|
|
107
|
+
creationflags = 0
|
|
108
|
+
if platform.system() == "Windows":
|
|
109
|
+
create_new_process_group = getattr(
|
|
110
|
+
subprocess, "CREATE_NEW_PROCESS_GROUP", 0
|
|
111
|
+
)
|
|
112
|
+
detached_process = getattr(subprocess, "DETACHED_PROCESS", 0)
|
|
113
|
+
creationflags = create_new_process_group | detached_process
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
process = await asyncio.create_subprocess_exec(
|
|
117
|
+
*cmd,
|
|
118
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
119
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
120
|
+
creationflags=creationflags,
|
|
121
|
+
start_new_session=True
|
|
122
|
+
if platform.system() != "Windows"
|
|
123
|
+
else False,
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(
|
|
127
|
+
f"Failed to spawn detached process for task id: {registry_entry.id}. task_id: {registry_entry.id}. "
|
|
128
|
+
f"Command: {' '.join(cmd)}. Error: {str(e)}"
|
|
129
|
+
)
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
logger.info(
|
|
133
|
+
f"Spawned detached process for task_id: {registry_entry.id} with PID: {process.pid}"
|
|
134
|
+
)
|
|
135
|
+
return True
|
|
136
|
+
else:
|
|
137
|
+
return await run_node_from_registry(registry_entry)
|
|
138
|
+
else:
|
|
139
|
+
logger.error(
|
|
140
|
+
f"Queue {queue} not supported for task_id: {registry_entry.id}"
|
|
141
|
+
)
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.exception(
|
|
146
|
+
f"Error running node task_id: {registry_entry.id} on resource {context.config.resource} : {str(e)}"
|
|
147
|
+
)
|
|
148
|
+
if registry_entry:
|
|
149
|
+
registry_entry.status = TaskStatus.FAILED
|
|
150
|
+
await context.db.save(registry_entry)
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
async def execute(self) -> None:
|
|
154
|
+
if not self._started:
|
|
155
|
+
await self.write_resource_event(RunnerEventEnum.RUNNER_STARTED)
|
|
156
|
+
self._started = True
|
|
157
|
+
|
|
158
|
+
# Clean up the completed tasks
|
|
159
|
+
completed_tasks = {task for task in self._running_tasks if task.done()}
|
|
160
|
+
for task in completed_tasks:
|
|
161
|
+
try:
|
|
162
|
+
await task
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.exception(f"Task completed with error: {e}")
|
|
165
|
+
self._running_tasks.remove(task)
|
|
166
|
+
|
|
167
|
+
# Load tasks
|
|
168
|
+
registry_entry_list = await context.db.load_waiting_tasks_for_resource(self._resource_name)
|
|
169
|
+
|
|
170
|
+
if registry_entry_list:
|
|
171
|
+
logger.info(
|
|
172
|
+
f"Retrieved {len(registry_entry_list)} tasks for {self._resource_name}"
|
|
173
|
+
)
|
|
174
|
+
for entry in registry_entry_list:
|
|
175
|
+
if not await claim_submitted_node(entry):
|
|
176
|
+
continue
|
|
177
|
+
task = asyncio.create_task(self._run_with_semaphore(entry))
|
|
178
|
+
self._running_tasks.add(task)
|
|
179
|
+
|
|
180
|
+
async def _run_with_semaphore(self, entry: NodeRegistry) -> bool:
|
|
181
|
+
async with self._semaphore:
|
|
182
|
+
return await self.run_node(entry)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from simstack.core.context import context
|
|
5
|
+
from simstack.models.parameters import Resource
|
|
6
|
+
from simstack.models.runner_model import RunnerEventEnum
|
|
7
|
+
from simstack.models.resource_definition import ResourceDefinition
|
|
8
|
+
from simstack.core.services.base_service import RestartService
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("NodeRunner")
|
|
11
|
+
|
|
12
|
+
class ResourceBranchMonitorService(RestartService):
|
|
13
|
+
"""
|
|
14
|
+
Monitors the ResourceDefinition for the current resource.
|
|
15
|
+
If the git_branch field changes, it stashes, switches branch, syncs, and restarts.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, resource: Resource, interval: int):
|
|
19
|
+
super().__init__("ResourceBranchMonitor", resource, interval)
|
|
20
|
+
self._project_dir = context.config.project_root.resolve(strict=True)
|
|
21
|
+
|
|
22
|
+
async def _run_command(self, cmd: list) -> str:
|
|
23
|
+
process = await asyncio.create_subprocess_exec(
|
|
24
|
+
*cmd,
|
|
25
|
+
cwd=str(self._project_dir),
|
|
26
|
+
stdout=asyncio.subprocess.PIPE,
|
|
27
|
+
stderr=asyncio.subprocess.PIPE
|
|
28
|
+
)
|
|
29
|
+
stdout, stderr = await process.communicate()
|
|
30
|
+
if process.returncode != 0:
|
|
31
|
+
logger.error(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
|
|
32
|
+
return ""
|
|
33
|
+
return stdout.decode().strip()
|
|
34
|
+
|
|
35
|
+
async def _get_current_branch(self) -> str:
|
|
36
|
+
return await self._run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
|
37
|
+
|
|
38
|
+
async def execute(self):
|
|
39
|
+
resource_def = await context.db.find_one(
|
|
40
|
+
ResourceDefinition,
|
|
41
|
+
ResourceDefinition.resource_str == str(self._resource)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not resource_def:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
target_branch = resource_def.git_branch
|
|
48
|
+
current_branch = await self._get_current_branch()
|
|
49
|
+
|
|
50
|
+
if target_branch and current_branch and target_branch != current_branch:
|
|
51
|
+
logger.info(f"Branch change detected: {current_branch} -> {target_branch}. Updating...")
|
|
52
|
+
|
|
53
|
+
# 1. Stash existing changes
|
|
54
|
+
await self._run_command(["git", "stash"])
|
|
55
|
+
|
|
56
|
+
# 2. Checkout new branch
|
|
57
|
+
checkout_res = await self._run_command(["git", "checkout", target_branch])
|
|
58
|
+
if not checkout_res:
|
|
59
|
+
logger.error(f"Failed to checkout branch {target_branch}")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# 3. UV Sync
|
|
63
|
+
logger.info("Running uv sync --locked...")
|
|
64
|
+
await self._run_command(["uv", "sync", "--locked"])
|
|
65
|
+
|
|
66
|
+
# 4. Trigger Restart
|
|
67
|
+
await self.write_resource_event(
|
|
68
|
+
RunnerEventEnum.SHUTDOWN,
|
|
69
|
+
message=f"Branch switched to {target_branch}"
|
|
70
|
+
)
|
|
71
|
+
await self.trigger_restart()
|