experimaestro 2.0.0a1__tar.gz → 2.0.0a4__tar.gz
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.
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/PKG-INFO +3 -2
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/pyproject.toml +5 -2
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/__init__.py +2 -2
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/arguments.py +20 -1
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config.py +81 -25
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config_walk.py +3 -1
- experimaestro-2.0.0a4/src/experimaestro/scheduler/__init__.py +18 -0
- experimaestro-2.0.0a4/src/experimaestro/scheduler/base.py +310 -0
- experimaestro-2.0.0a4/src/experimaestro/scheduler/experiment.py +387 -0
- experimaestro-2.0.0a4/src/experimaestro/scheduler/jobs.py +475 -0
- experimaestro-2.0.0a4/src/experimaestro/scheduler/signal_handler.py +32 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/state.py +1 -1
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/__init__.py +36 -5
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_dependencies.py +1 -1
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_generators.py +41 -9
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/typingutils.py +11 -2
- experimaestro-2.0.0a1/src/experimaestro/scheduler/__init__.py +0 -1
- experimaestro-2.0.0a1/src/experimaestro/scheduler/base.py +0 -1129
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/LICENSE +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/README.md +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/__main__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/annotations.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/checkers.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/filter.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/jobs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/cli/progress.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/click.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/commandline.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/compat.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/local.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/connectors/ssh.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/callbacks.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/context.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/identifier.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config_utils.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects.pyi +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/serialization.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/serializers.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/types.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/utils.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/exceptions.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/cli.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/experiments/configuration.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/generators.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/huggingface.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/ipc.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/base.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/parser.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/registry.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launcherfinder/specs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/direct.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/oar.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/slurm/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/launchers/slurm/base.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/locking.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/annotations.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/base.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/metaloader.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mkdocs/style.css +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/mypy.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/notifications.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/progress.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/py.typed +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/rpyc.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/run.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/dependencies.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/dynamic_outputs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/services.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scheduler/workspace.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/scriptbuilder.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/favicon.ico +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.css +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.css.map +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.html +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.js +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/index.js.map +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/login.html +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/server/data/manifest.json +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/settings.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/sphinx/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/sphinx/static/experimaestro.css +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/taskglobals.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/conftest.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/bin/executable.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/test_local.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/connectors/utils.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/core/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/core/test_generics.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/definitions_types.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/sacct +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/sbatch +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/srun +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/bin/test.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/common.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/config_slurm/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/config_slurm/launchers.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/test_local.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/launchers/test_slurm.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/restart.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/restart_main.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/scripts/notifyandwait.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/scripts/waitforfile.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/task_tokens.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/all.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/tasks/foreign.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_checkers.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_experiment.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_file_progress.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_file_progress_integration.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_findlauncher.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_forward.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_identifier.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_instance.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_objects.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_outputs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_param.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_progress.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_serializers.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_snippets.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_ssh.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tags.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tasks.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_tokens.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_types.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/test_validation.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/token_reschedule.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tests/utils.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tokens.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/diff.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/documentation.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/tools/jobs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/__init__.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/asyncio.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/jobs.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/jupyter.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/multiprocessing.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/resources.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/utils/settings.py +0 -0
- {experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/xpmutils.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: experimaestro
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.0a4
|
|
4
4
|
Summary: "Experimaestro is a computer science experiment manager"
|
|
5
5
|
License: GPL-3
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: experiment manager
|
|
7
8
|
Author: Benjamin Piwowarski
|
|
8
9
|
Author-email: benjamin@piwowarski.fr
|
|
@@ -48,7 +48,7 @@ dependencies = [
|
|
|
48
48
|
"typing-extensions >=4.2; python_version < \"3.12\"",
|
|
49
49
|
"watchdog >=2"
|
|
50
50
|
]
|
|
51
|
-
version = "2.0.0-
|
|
51
|
+
version = "2.0.0-a4"
|
|
52
52
|
|
|
53
53
|
[tool.poetry-dynamic-versioning]
|
|
54
54
|
enable = false
|
|
@@ -85,6 +85,9 @@ build-backend = "poetry_dynamic_versioning.backend"
|
|
|
85
85
|
|
|
86
86
|
[dependency-groups]
|
|
87
87
|
dev = [
|
|
88
|
+
"markdown-include>=0.8.1",
|
|
89
|
+
"mkdocstrings[python]>=0.30.1",
|
|
90
|
+
"pymdown-extensions>=10.16.1",
|
|
88
91
|
"pytest>=8.4.1",
|
|
89
92
|
"pytest-timeout>=2.4.0",
|
|
90
93
|
]
|
|
@@ -146,7 +149,7 @@ warn_unused_ignores = true
|
|
|
146
149
|
|
|
147
150
|
[tool.commitizen]
|
|
148
151
|
name = "cz_conventional_commits"
|
|
149
|
-
version = "2.0.
|
|
152
|
+
version = "2.0.0a4"
|
|
150
153
|
changelog_start_rev = "v1.0.0"
|
|
151
154
|
tag_format = "v$major.$minor.$patch$prerelease"
|
|
152
155
|
# update_changelog_on_bump = true
|
|
@@ -16,7 +16,7 @@ from experimaestro.utils import logger
|
|
|
16
16
|
from experimaestro.locking import Lock
|
|
17
17
|
from experimaestro.tokens import Token
|
|
18
18
|
from experimaestro.utils.asyncio import asyncThreadcheck
|
|
19
|
-
import
|
|
19
|
+
from importlib.metadata import entry_points
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class RedirectType(enum.Enum):
|
|
@@ -101,7 +101,7 @@ class Process:
|
|
|
101
101
|
"""Get a handler"""
|
|
102
102
|
if Process.HANDLERS is None:
|
|
103
103
|
Process.HANDLERS = {}
|
|
104
|
-
for ep in
|
|
104
|
+
for ep in entry_points(group="experimaestro.process"):
|
|
105
105
|
logging.debug("Adding process handler for type %s", ep.name)
|
|
106
106
|
handler = ep.load()
|
|
107
107
|
Process.HANDLERS[ep.name] = handler
|
|
@@ -80,10 +80,13 @@ class Argument:
|
|
|
80
80
|
|
|
81
81
|
self.generator = generator
|
|
82
82
|
self.default = None
|
|
83
|
+
self.ignore_generated = False
|
|
83
84
|
|
|
84
85
|
if default is not None:
|
|
85
86
|
assert self.generator is None, "generator and default are exclusive options"
|
|
86
87
|
if isinstance(default, field):
|
|
88
|
+
self.ignore_generated = default.ignore_generated
|
|
89
|
+
|
|
87
90
|
if default.default is not None:
|
|
88
91
|
self.default = default.default
|
|
89
92
|
elif default.default_factory is not None:
|
|
@@ -184,13 +187,29 @@ DataPath = Annotated[Path, dataHint]
|
|
|
184
187
|
class field:
|
|
185
188
|
"""Extra information for a given experimaestro field (param or meta)"""
|
|
186
189
|
|
|
187
|
-
def __init__(
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
default: Any = None,
|
|
194
|
+
default_factory: Callable = None,
|
|
195
|
+
ignore_generated=False,
|
|
196
|
+
):
|
|
197
|
+
"""Gives some extra per-field information
|
|
198
|
+
|
|
199
|
+
:param default: a default value, defaults to None
|
|
200
|
+
:param default_factory: a default factory for values, defaults to None
|
|
201
|
+
:param ignore_generated: True if the value is hidden – it won't be accessible in
|
|
202
|
+
tasks, defaults to False. The interest of hidden is to add a
|
|
203
|
+
configuration field that changes the identifier, but will not be
|
|
204
|
+
used.
|
|
205
|
+
"""
|
|
188
206
|
assert not (
|
|
189
207
|
(default is not None) and (default_factory is not None)
|
|
190
208
|
), "default and default_factory are mutually exclusive options"
|
|
191
209
|
|
|
192
210
|
self.default_factory = default_factory
|
|
193
211
|
self.default = default
|
|
212
|
+
self.ignore_generated = ignore_generated
|
|
194
213
|
|
|
195
214
|
|
|
196
215
|
class help(TypeAnnotation):
|
|
@@ -169,10 +169,33 @@ class ConfigInformation:
|
|
|
169
169
|
self._sealed = False
|
|
170
170
|
self._meta = None
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
self.
|
|
172
|
+
# This contains the list of generated values (using context) in this
|
|
173
|
+
# configuration or any sub-configuration, is generated. This prevents
|
|
174
|
+
# problem when a configuration with generated values is re-used.
|
|
175
|
+
self._generated_values = []
|
|
176
|
+
|
|
177
|
+
def get_generated_paths(
|
|
178
|
+
self, path: list[str] = None, paths: list[str] = None
|
|
179
|
+
) -> list[str]:
|
|
180
|
+
"""Get the list of generated paths, useful to track down those
|
|
181
|
+
|
|
182
|
+
:param path: The current path
|
|
183
|
+
:param paths: The list of generated paths so far, defaults to None
|
|
184
|
+
:return: The full list of generated paths
|
|
185
|
+
"""
|
|
186
|
+
paths = [] if paths is None else paths
|
|
187
|
+
path = [] if path is None else path
|
|
188
|
+
|
|
189
|
+
for key in self._generated_values:
|
|
190
|
+
value = self.values[key]
|
|
191
|
+
if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
|
|
192
|
+
path.append(key)
|
|
193
|
+
value.__xpm__.get_generated_paths(path, paths)
|
|
194
|
+
path.pop()
|
|
195
|
+
else:
|
|
196
|
+
paths.append(".".join(path + [key]))
|
|
197
|
+
|
|
198
|
+
return paths
|
|
176
199
|
|
|
177
200
|
def set_meta(self, value: Optional[bool]):
|
|
178
201
|
"""Sets the meta flag"""
|
|
@@ -191,6 +214,31 @@ class ConfigInformation:
|
|
|
191
214
|
# Not an argument, bypass
|
|
192
215
|
return object.__getattribute__(self.pyobject, name)
|
|
193
216
|
|
|
217
|
+
@staticmethod
|
|
218
|
+
def is_generated_value(argument, value):
|
|
219
|
+
if argument.ignore_generated:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
if value is None:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
if isinstance(value, (int, str, float, bool, Path)):
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
if isinstance(value, ConfigMixin):
|
|
229
|
+
return value.__xpm__._generated_values and value.__xpm__.task is None
|
|
230
|
+
|
|
231
|
+
if isinstance(value, list):
|
|
232
|
+
return any(ConfigInformation.is_generated_value(argument, x) for x in value)
|
|
233
|
+
|
|
234
|
+
if isinstance(value, dict):
|
|
235
|
+
return any(
|
|
236
|
+
ConfigInformation.is_generated_value(argument, x)
|
|
237
|
+
for x in value.values()
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
assert False, f"Cannot handle values of type {type(value)}"
|
|
241
|
+
|
|
194
242
|
def set(self, k, v, bypass=False):
|
|
195
243
|
from experimaestro.generators import Generator
|
|
196
244
|
|
|
@@ -207,18 +255,16 @@ class ConfigInformation:
|
|
|
207
255
|
"Configuration (and not objects) should be used. Consider using .C(...)"
|
|
208
256
|
)
|
|
209
257
|
|
|
210
|
-
if (
|
|
211
|
-
isinstance(v, ConfigMixin)
|
|
212
|
-
and v.__xpm__._has_generated_value
|
|
213
|
-
and v.__xpm__.task is None
|
|
214
|
-
):
|
|
215
|
-
raise AttributeError(
|
|
216
|
-
f"Cannot set {k} to a configuration with generated values"
|
|
217
|
-
)
|
|
218
|
-
|
|
219
258
|
try:
|
|
220
259
|
argument = self.xpmtype.arguments.get(k, None)
|
|
221
260
|
if argument:
|
|
261
|
+
if ConfigInformation.is_generated_value(argument, v):
|
|
262
|
+
raise AttributeError(
|
|
263
|
+
f"Cannot set {k} to a configuration with generated values. "
|
|
264
|
+
"Here is the list of paths to help you: "
|
|
265
|
+
f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
|
|
266
|
+
)
|
|
267
|
+
|
|
222
268
|
if not bypass and (
|
|
223
269
|
(isinstance(argument.generator, Generator)) or argument.constant
|
|
224
270
|
):
|
|
@@ -344,14 +390,15 @@ class ConfigInformation:
|
|
|
344
390
|
Arguments:
|
|
345
391
|
- context: the generation context
|
|
346
392
|
"""
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
for v in self.values.
|
|
350
|
-
if
|
|
351
|
-
]
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
393
|
+
if generated_keys := [
|
|
394
|
+
k
|
|
395
|
+
for k, v in self.values.items()
|
|
396
|
+
if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
|
|
397
|
+
]:
|
|
398
|
+
raise AttributeError(
|
|
399
|
+
"Cannot seal a configuration with generated values:"
|
|
400
|
+
f"""{",".join(generated_keys)} in {context.currentpath}"""
|
|
401
|
+
)
|
|
355
402
|
|
|
356
403
|
class Sealer(ConfigWalk):
|
|
357
404
|
def preprocess(self, config: ConfigMixin):
|
|
@@ -375,13 +422,18 @@ class ConfigInformation:
|
|
|
375
422
|
if len(sig.parameters) == 0:
|
|
376
423
|
value = argument.generator()
|
|
377
424
|
elif len(sig.parameters) == 2:
|
|
425
|
+
# Only in that case do we need to flag this configuration
|
|
426
|
+
# as containing generated values
|
|
427
|
+
if not argument.ignore_generated:
|
|
428
|
+
config.__xpm__._generated_values.append(k)
|
|
429
|
+
else:
|
|
430
|
+
logging.warning("Ignoring %s", k)
|
|
378
431
|
value = argument.generator(self.context, config)
|
|
379
432
|
else:
|
|
380
433
|
assert (
|
|
381
434
|
False
|
|
382
435
|
), "generator has either two parameters (context and config), or none"
|
|
383
436
|
config.__xpm__.set(k, value, bypass=True)
|
|
384
|
-
config.__xpm__._has_generated_value = True
|
|
385
437
|
else:
|
|
386
438
|
value = config.__xpm__.values.get(k)
|
|
387
439
|
except Exception:
|
|
@@ -392,11 +444,14 @@ class ConfigInformation:
|
|
|
392
444
|
|
|
393
445
|
# Propagate the generated value flag
|
|
394
446
|
if (
|
|
395
|
-
|
|
447
|
+
value is not None
|
|
396
448
|
and isinstance(value, ConfigMixin)
|
|
397
|
-
and value.__xpm__.
|
|
449
|
+
and value.__xpm__._generated_values
|
|
398
450
|
):
|
|
399
|
-
|
|
451
|
+
if not argument.ignore_generated:
|
|
452
|
+
config.__xpm__._generated_values.append(k)
|
|
453
|
+
else:
|
|
454
|
+
logging.warning("Ignoring %s", k)
|
|
400
455
|
|
|
401
456
|
config.__xpm__._sealed = True
|
|
402
457
|
|
|
@@ -889,6 +944,7 @@ class ConfigInformation:
|
|
|
889
944
|
"workspace": str(context.workspace.path.absolute()),
|
|
890
945
|
"tags": {key: value for key, value in self.tags().items()},
|
|
891
946
|
"version": 2,
|
|
947
|
+
"experimaestro": experimaestro.__version__,
|
|
892
948
|
"objects": self.__get_objects__([], context),
|
|
893
949
|
},
|
|
894
950
|
out,
|
{experimaestro-2.0.0a1 → experimaestro-2.0.0a4}/src/experimaestro/core/objects/config_walk.py
RENAMED
|
@@ -71,6 +71,7 @@ class ConfigWalk:
|
|
|
71
71
|
return self.context.push(str(i))
|
|
72
72
|
|
|
73
73
|
def map(self, k: str):
|
|
74
|
+
"""Provides a path context when processing a tree"""
|
|
74
75
|
return self.context.push(k)
|
|
75
76
|
|
|
76
77
|
def stub(self, config):
|
|
@@ -123,7 +124,8 @@ class ConfigWalk:
|
|
|
123
124
|
and self.recurse_task
|
|
124
125
|
and x.__xpm__.task is not x
|
|
125
126
|
):
|
|
126
|
-
self(
|
|
127
|
+
with self.map("__task__"):
|
|
128
|
+
self(x.__xpm__.task)
|
|
127
129
|
|
|
128
130
|
processed = self.postprocess(stub, x, result)
|
|
129
131
|
self.visited[xid] = processed
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import Scheduler, Listener
|
|
2
|
+
from .workspace import Workspace, RunMode
|
|
3
|
+
from .experiment import experiment, FailedExperiment
|
|
4
|
+
from .jobs import Job, JobState, JobFailureStatus, JobDependency, JobContext
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Scheduler",
|
|
8
|
+
"Listener",
|
|
9
|
+
"Workspace",
|
|
10
|
+
"RunMode",
|
|
11
|
+
"experiment",
|
|
12
|
+
"FailedExperiment",
|
|
13
|
+
"Job",
|
|
14
|
+
"JobState",
|
|
15
|
+
"JobFailureStatus",
|
|
16
|
+
"JobDependency",
|
|
17
|
+
"JobContext",
|
|
18
|
+
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Set,
|
|
7
|
+
)
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Dict
|
|
10
|
+
|
|
11
|
+
from experimaestro.scheduler import experiment
|
|
12
|
+
from experimaestro.scheduler.jobs import Job, JobState
|
|
13
|
+
from experimaestro.scheduler.services import Service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from experimaestro.utils import logger
|
|
17
|
+
from experimaestro.utils.asyncio import asyncThreadcheck
|
|
18
|
+
import concurrent.futures
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Listener:
|
|
22
|
+
def job_submitted(self, job):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def job_state(self, job):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def service_add(self, service: Service):
|
|
29
|
+
"""Notify when a new service is added"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Scheduler(threading.Thread):
|
|
34
|
+
"""A job scheduler
|
|
35
|
+
|
|
36
|
+
The scheduler is based on asyncio for easy concurrency handling
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, xp: "experiment", name: str):
|
|
40
|
+
super().__init__(name=f"Scheduler ({name})", daemon=True)
|
|
41
|
+
self._ready = threading.Event()
|
|
42
|
+
|
|
43
|
+
# Name of the experiment
|
|
44
|
+
self.name = name
|
|
45
|
+
self.xp = xp
|
|
46
|
+
|
|
47
|
+
# Exit mode activated
|
|
48
|
+
self.exitmode = False
|
|
49
|
+
|
|
50
|
+
# List of all jobs
|
|
51
|
+
self.jobs: Dict[str, "Job"] = {}
|
|
52
|
+
|
|
53
|
+
# List of jobs
|
|
54
|
+
self.waitingjobs: Set[Job] = set()
|
|
55
|
+
|
|
56
|
+
# Listeners
|
|
57
|
+
self.listeners: Set[Listener] = set()
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def create(xp: "experiment", name: str):
|
|
61
|
+
instance = Scheduler(xp, name)
|
|
62
|
+
instance.start()
|
|
63
|
+
instance._ready.wait()
|
|
64
|
+
return instance
|
|
65
|
+
|
|
66
|
+
def run(self):
|
|
67
|
+
"""Run the event loop forever"""
|
|
68
|
+
logger.debug("Starting event loop thread")
|
|
69
|
+
# Ported from SchedulerCentral
|
|
70
|
+
self.loop = asyncio.new_event_loop()
|
|
71
|
+
asyncio.set_event_loop(self.loop)
|
|
72
|
+
# Set loop-dependent variables
|
|
73
|
+
self.exitCondition = asyncio.Condition()
|
|
74
|
+
self.dependencyLock = asyncio.Lock()
|
|
75
|
+
self._ready.set()
|
|
76
|
+
self.loop.run_forever()
|
|
77
|
+
|
|
78
|
+
def start_scheduler(self):
|
|
79
|
+
"""Start the scheduler event loop in a thread"""
|
|
80
|
+
if not self.is_alive():
|
|
81
|
+
self.start()
|
|
82
|
+
self._ready.wait()
|
|
83
|
+
else:
|
|
84
|
+
logger.warning("Scheduler already started")
|
|
85
|
+
|
|
86
|
+
def addlistener(self, listener: Listener):
|
|
87
|
+
self.listeners.add(listener)
|
|
88
|
+
|
|
89
|
+
def removelistener(self, listener: Listener):
|
|
90
|
+
self.listeners.remove(listener)
|
|
91
|
+
|
|
92
|
+
def getJobState(self, job: Job) -> "concurrent.futures.Future[JobState]":
|
|
93
|
+
# Check if the job belongs to this scheduler
|
|
94
|
+
if job.identifier not in self.jobs:
|
|
95
|
+
# If job is not in this scheduler, return its current state directly
|
|
96
|
+
future = concurrent.futures.Future()
|
|
97
|
+
future.set_result(job.state)
|
|
98
|
+
return future
|
|
99
|
+
|
|
100
|
+
return asyncio.run_coroutine_threadsafe(self.aio_getjobstate(job), self.loop)
|
|
101
|
+
|
|
102
|
+
async def aio_getjobstate(self, job: Job):
|
|
103
|
+
return job.state
|
|
104
|
+
|
|
105
|
+
def submit(self, job: Job) -> Optional[Job]:
|
|
106
|
+
# Wait for the future containing the submitted job
|
|
107
|
+
logger.debug("Registering the job %s within the scheduler", job)
|
|
108
|
+
otherFuture = asyncio.run_coroutine_threadsafe(
|
|
109
|
+
self.aio_registerJob(job), self.loop
|
|
110
|
+
)
|
|
111
|
+
other = otherFuture.result()
|
|
112
|
+
logger.debug("Job already submitted" if other else "First submission")
|
|
113
|
+
if other:
|
|
114
|
+
return other
|
|
115
|
+
|
|
116
|
+
job._future = asyncio.run_coroutine_threadsafe(self.aio_submit(job), self.loop)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def prepare(self, job: Job):
|
|
120
|
+
"""Prepares the job for running"""
|
|
121
|
+
logger.info("Preparing job %s", job.path)
|
|
122
|
+
job.prepare(overwrite=True)
|
|
123
|
+
|
|
124
|
+
async def aio_registerJob(self, job: Job):
|
|
125
|
+
"""Register a job by adding it to the list, and checks
|
|
126
|
+
whether the job has already been submitted
|
|
127
|
+
"""
|
|
128
|
+
logger.debug("Registering job %s", job)
|
|
129
|
+
|
|
130
|
+
if self.exitmode:
|
|
131
|
+
logger.warning("Exit mode: not submitting")
|
|
132
|
+
|
|
133
|
+
elif job.identifier in self.jobs:
|
|
134
|
+
other = self.jobs[job.identifier]
|
|
135
|
+
assert job.type == other.type
|
|
136
|
+
if other.state == JobState.ERROR:
|
|
137
|
+
logger.info("Re-submitting job")
|
|
138
|
+
else:
|
|
139
|
+
logger.warning("Job %s already submitted", job.identifier)
|
|
140
|
+
return other
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
# Register this job
|
|
144
|
+
self.xp.unfinishedJobs += 1
|
|
145
|
+
self.jobs[job.identifier] = job
|
|
146
|
+
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def notify_job_submitted(self, job: Job):
|
|
150
|
+
"""Notify the listeners that a job has been submitted"""
|
|
151
|
+
for listener in self.listeners:
|
|
152
|
+
try:
|
|
153
|
+
listener.job_submitted(job)
|
|
154
|
+
except Exception:
|
|
155
|
+
logger.exception("Got an error with listener %s", listener)
|
|
156
|
+
|
|
157
|
+
def notify_job_state(self, job: Job):
|
|
158
|
+
"""Notify the listeners that a job has changed state"""
|
|
159
|
+
for listener in self.listeners:
|
|
160
|
+
try:
|
|
161
|
+
listener.job_state(job)
|
|
162
|
+
except Exception:
|
|
163
|
+
logger.exception("Got an error with listener %s", listener)
|
|
164
|
+
|
|
165
|
+
async def aio_submit(self, job: Job) -> JobState: # noqa: C901
|
|
166
|
+
"""Main scheduler function: submit a job, run it (if needed), and returns
|
|
167
|
+
the status code
|
|
168
|
+
"""
|
|
169
|
+
logger.info("Submitting job %s", job)
|
|
170
|
+
job._readyEvent = asyncio.Event()
|
|
171
|
+
job.submittime = time.time()
|
|
172
|
+
job.scheduler = self
|
|
173
|
+
self.waitingjobs.add(job)
|
|
174
|
+
|
|
175
|
+
# Check that we don't have a completed job in
|
|
176
|
+
# alternate directories
|
|
177
|
+
for jobspath in experiment.current().alt_jobspaths:
|
|
178
|
+
# FIXME: check if done
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# Creates a link into the experiment folder
|
|
182
|
+
path = experiment.current().jobspath / job.relpath
|
|
183
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
if path.is_symlink():
|
|
185
|
+
path.unlink()
|
|
186
|
+
path.symlink_to(job.path)
|
|
187
|
+
|
|
188
|
+
job.state = JobState.WAITING
|
|
189
|
+
|
|
190
|
+
self.notify_job_submitted(job)
|
|
191
|
+
|
|
192
|
+
# Add dependencies, and add to blocking resources
|
|
193
|
+
if job.dependencies:
|
|
194
|
+
job.unsatisfied = len(job.dependencies)
|
|
195
|
+
|
|
196
|
+
for dependency in job.dependencies:
|
|
197
|
+
dependency.target = job
|
|
198
|
+
dependency.loop = self.loop
|
|
199
|
+
dependency.origin.dependents.add(dependency)
|
|
200
|
+
dependency.check()
|
|
201
|
+
else:
|
|
202
|
+
job._readyEvent.set()
|
|
203
|
+
job.state = JobState.READY
|
|
204
|
+
|
|
205
|
+
if job.donepath.exists():
|
|
206
|
+
job.state = JobState.DONE
|
|
207
|
+
|
|
208
|
+
# Check if we have a running process
|
|
209
|
+
process = await job.aio_process()
|
|
210
|
+
if process is not None:
|
|
211
|
+
# Yep! First we notify the listeners
|
|
212
|
+
job.state = JobState.RUNNING
|
|
213
|
+
# Notify the listeners
|
|
214
|
+
self.notify_job_state(job)
|
|
215
|
+
|
|
216
|
+
# Adds to the listeners
|
|
217
|
+
if self.xp.server is not None:
|
|
218
|
+
job.add_notification_server(self.xp.server)
|
|
219
|
+
|
|
220
|
+
# And now, we wait...
|
|
221
|
+
logger.info("Got a process for job %s - waiting to complete", job)
|
|
222
|
+
code = await process.aio_code()
|
|
223
|
+
logger.info("Job %s completed with code %s", job, code)
|
|
224
|
+
job.state = JobState.DONE if code == 0 else JobState.ERROR
|
|
225
|
+
|
|
226
|
+
# Check if done
|
|
227
|
+
if job.donepath.exists():
|
|
228
|
+
job.state = JobState.DONE
|
|
229
|
+
|
|
230
|
+
# OK, not done; let's start the job for real
|
|
231
|
+
while not job.state.finished():
|
|
232
|
+
# Wait that the job is ready
|
|
233
|
+
await job._readyEvent.wait()
|
|
234
|
+
job._readyEvent.clear()
|
|
235
|
+
|
|
236
|
+
if job.state == JobState.READY:
|
|
237
|
+
try:
|
|
238
|
+
state = await self.aio_start(job)
|
|
239
|
+
except Exception:
|
|
240
|
+
logger.exception("Got an exception while starting the job")
|
|
241
|
+
raise
|
|
242
|
+
|
|
243
|
+
if state is None:
|
|
244
|
+
# State is None if this is not the main thread
|
|
245
|
+
return JobState.ERROR
|
|
246
|
+
|
|
247
|
+
job.state = state
|
|
248
|
+
|
|
249
|
+
self.notify_job_state(job)
|
|
250
|
+
|
|
251
|
+
# Job is finished
|
|
252
|
+
if job.state != JobState.DONE:
|
|
253
|
+
self.xp.failedJobs[job.identifier] = job
|
|
254
|
+
|
|
255
|
+
# Process all remaining tasks outputs
|
|
256
|
+
await asyncThreadcheck("End of job processing", job.done_handler)
|
|
257
|
+
|
|
258
|
+
# Decrement the number of unfinished jobs and notify
|
|
259
|
+
self.xp.unfinishedJobs -= 1
|
|
260
|
+
async with self.exitCondition:
|
|
261
|
+
logging.debug("Updated number of unfinished jobs")
|
|
262
|
+
self.exitCondition.notify_all()
|
|
263
|
+
|
|
264
|
+
job.endtime = time.time()
|
|
265
|
+
if job in self.waitingjobs:
|
|
266
|
+
self.waitingjobs.remove(job)
|
|
267
|
+
|
|
268
|
+
with job.dependents as dependents:
|
|
269
|
+
logger.info("Processing %d dependent jobs", len(dependents))
|
|
270
|
+
for dependency in dependents:
|
|
271
|
+
logger.debug("Checking dependency %s", dependency)
|
|
272
|
+
self.loop.call_soon(dependency.check)
|
|
273
|
+
|
|
274
|
+
return job.state
|
|
275
|
+
|
|
276
|
+
async def aio_start(self, job: Job) -> Optional[JobState]:
|
|
277
|
+
"""Start a job (scheduler coordination layer)
|
|
278
|
+
|
|
279
|
+
This method serves as a coordination layer that delegates the actual
|
|
280
|
+
job starting logic to the job itself while handling scheduler-specific
|
|
281
|
+
concerns like state notifications and providing coordination context.
|
|
282
|
+
|
|
283
|
+
:param job: The job to start
|
|
284
|
+
:return: JobState.WAITING if dependencies could not be locked, JobState.DONE
|
|
285
|
+
if job completed successfully, JobState.ERROR if job failed during execution,
|
|
286
|
+
or None (should not occur in normal operation)
|
|
287
|
+
:raises Exception: Various exceptions during scheduler coordination
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
# Assert preconditions
|
|
291
|
+
assert job.launcher is not None
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
# Call job's start method with scheduler context
|
|
295
|
+
state = await job.aio_start(
|
|
296
|
+
sched_dependency_lock=self.dependencyLock,
|
|
297
|
+
notification_server=self.xp.server if self.xp else None,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if state is None:
|
|
301
|
+
# Dependencies couldn't be locked, return WAITING state
|
|
302
|
+
return JobState.WAITING
|
|
303
|
+
|
|
304
|
+
# Notify scheduler listeners of job state after successful start
|
|
305
|
+
self.notify_job_state(job)
|
|
306
|
+
return state
|
|
307
|
+
|
|
308
|
+
except Exception:
|
|
309
|
+
logger.warning("Error in scheduler job coordination", exc_info=True)
|
|
310
|
+
return JobState.ERROR
|