panda-server-source 0.8.0__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.
- panda_server_source-0.8.0.dist-info/METADATA +55 -0
- panda_server_source-0.8.0.dist-info/RECORD +375 -0
- panda_server_source-0.8.0.dist-info/WHEEL +4 -0
- panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
- pandajedi/__init__.py +0 -0
- pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
- pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
- pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
- pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
- pandajedi/jedibrokerage/GenJobBroker.py +381 -0
- pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
- pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
- pandajedi/jedibrokerage/__init__.py +0 -0
- pandajedi/jediconfig/__init__.py +0 -0
- pandajedi/jediconfig/jedi_config.py +40 -0
- pandajedi/jedicore/FactoryBase.py +182 -0
- pandajedi/jedicore/Interaction.py +433 -0
- pandajedi/jedicore/JediDBProxy.py +56 -0
- pandajedi/jedicore/JediDBProxyPool.py +37 -0
- pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
- pandajedi/jedicore/JediException.py +13 -0
- pandajedi/jedicore/JediTaskBuffer.py +984 -0
- pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
- pandajedi/jedicore/MsgWrapper.py +124 -0
- pandajedi/jedicore/ProcessUtils.py +34 -0
- pandajedi/jedicore/SiteCandidate.py +90 -0
- pandajedi/jedicore/ThreadUtils.py +205 -0
- pandajedi/jedicore/__init__.py +0 -0
- pandajedi/jediddm/AtlasDDMClient.py +1708 -0
- pandajedi/jediddm/DDMClientBase.py +16 -0
- pandajedi/jediddm/DDMInterface.py +83 -0
- pandajedi/jediddm/GenDDMClient.py +17 -0
- pandajedi/jediddm/__init__.py +0 -0
- pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
- pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
- pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
- pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
- pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
- pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
- pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
- pandajedi/jedidog/GenWatchDog.py +21 -0
- pandajedi/jedidog/JumboWatchDog.py +147 -0
- pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
- pandajedi/jedidog/WatchDogBase.py +53 -0
- pandajedi/jedidog/__init__.py +0 -0
- pandajedi/jediexec/reniceJEDI.py +25 -0
- pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
- pandajedi/jedigen/TaskGeneratorBase.py +15 -0
- pandajedi/jedigen/__init__.py +0 -0
- pandajedi/jedimsgprocessor/__init__.py +0 -0
- pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
- pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
- pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
- pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
- pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
- pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
- pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
- pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
- pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
- pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
- pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
- pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
- pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
- pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
- pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
- pandajedi/jediorder/ContentsFeeder.py +785 -0
- pandajedi/jediorder/JediDaemon.py +27 -0
- pandajedi/jediorder/JediKnight.py +55 -0
- pandajedi/jediorder/JediMaster.py +238 -0
- pandajedi/jediorder/JediMsgProcessor.py +32 -0
- pandajedi/jediorder/JobBroker.py +33 -0
- pandajedi/jediorder/JobGenerator.py +2763 -0
- pandajedi/jediorder/JobSplitter.py +308 -0
- pandajedi/jediorder/JobThrottler.py +28 -0
- pandajedi/jediorder/PostProcessor.py +166 -0
- pandajedi/jediorder/TaskBroker.py +268 -0
- pandajedi/jediorder/TaskCommando.py +334 -0
- pandajedi/jediorder/TaskGenerator.py +17 -0
- pandajedi/jediorder/TaskRefiner.py +611 -0
- pandajedi/jediorder/TaskSetupper.py +17 -0
- pandajedi/jediorder/WatchDog.py +79 -0
- pandajedi/jediorder/__init__.py +0 -0
- pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
- pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
- pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
- pandajedi/jedipprocess/GenPostProcessor.py +35 -0
- pandajedi/jedipprocess/MailTemplates.py +185 -0
- pandajedi/jedipprocess/PostProcessorBase.py +269 -0
- pandajedi/jedipprocess/__init__.py +0 -0
- pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
- pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
- pandajedi/jedirefine/GenTaskRefiner.py +61 -0
- pandajedi/jedirefine/RefinerUtils.py +154 -0
- pandajedi/jedirefine/TaskRefinerBase.py +996 -0
- pandajedi/jedirefine/__init__.py +0 -0
- pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
- pandajedi/jedisetup/GenTaskSetupper.py +16 -0
- pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
- pandajedi/jedisetup/TaskSetupperBase.py +15 -0
- pandajedi/jedisetup/__init__.py +0 -0
- pandajedi/jeditest/SchemaChecker.py +41 -0
- pandajedi/jeditest/addContWoSoTask.py +54 -0
- pandajedi/jeditest/addDataCarouselRequest.py +47 -0
- pandajedi/jeditest/addEventServiceTask.py +101 -0
- pandajedi/jeditest/addHpoTask.py +81 -0
- pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
- pandajedi/jeditest/addNonAtlasTask.py +71 -0
- pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
- pandajedi/jeditest/addTestTask.py +124 -0
- pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
- pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
- pandajedi/jeditest/brokerTest.py +78 -0
- pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
- pandajedi/jeditest/contentsFeederTest.py +28 -0
- pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
- pandajedi/jeditest/ddmTest.py +4 -0
- pandajedi/jeditest/jobGenForOneTask.py +38 -0
- pandajedi/jeditest/jobGeneratorTest.py +43 -0
- pandajedi/jeditest/jobSplitterTest.py +104 -0
- pandajedi/jeditest/jobThrottlerTest.py +22 -0
- pandajedi/jeditest/msgProcessorTest.py +9 -0
- pandajedi/jeditest/postProcessorTest.py +18 -0
- pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
- pandajedi/jeditest/resubmitTaskParam.py +20 -0
- pandajedi/jeditest/taskBrokerTest.py +22 -0
- pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
- pandajedi/jeditest/taskBufferTest.py +5 -0
- pandajedi/jeditest/taskCommandoTest.py +28 -0
- pandajedi/jeditest/taskRefinerTest.py +30 -0
- pandajedi/jeditest/testOneTaskToRefine.py +49 -0
- pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
- pandajedi/jeditest/testPandaDDMMap.py +62 -0
- pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
- pandajedi/jeditest/watchDogTest.py +25 -0
- pandajedi/jeditest/workQueueTest.py +22 -0
- pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
- pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
- pandajedi/jedithrottle/GenJobThrottler.py +26 -0
- pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
- pandajedi/jedithrottle/__init__.py +0 -0
- pandaserver/__init__.py +0 -0
- pandaserver/api/__init__.py +0 -0
- pandaserver/api/v1/__init__.py +0 -0
- pandaserver/api/v1/common.py +339 -0
- pandaserver/api/v1/credential_management_api.py +372 -0
- pandaserver/api/v1/data_carousel_api.py +264 -0
- pandaserver/api/v1/event_api.py +269 -0
- pandaserver/api/v1/file_server_api.py +942 -0
- pandaserver/api/v1/harvester_api.py +445 -0
- pandaserver/api/v1/http_client.py +166 -0
- pandaserver/api/v1/idds_api.py +180 -0
- pandaserver/api/v1/job_api.py +463 -0
- pandaserver/api/v1/metaconfig_api.py +122 -0
- pandaserver/api/v1/pilot_api.py +895 -0
- pandaserver/api/v1/statistics_api.py +207 -0
- pandaserver/api/v1/system_api.py +186 -0
- pandaserver/api/v1/task_api.py +1520 -0
- pandaserver/api/v1/tests/__init__.py +0 -0
- pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
- pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
- pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
- pandaserver/api/v1/tests/idds_api_tests.py +45 -0
- pandaserver/api/v1/tests/job_api_tests.py +112 -0
- pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
- pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
- pandaserver/api/v1/tests/system_api_tests.py +58 -0
- pandaserver/api/v1/tests/task_api_tests.py +590 -0
- pandaserver/api/v1/version.py +2 -0
- pandaserver/api/v1/workflow_api.py +136 -0
- pandaserver/brokerage/SiteMapper.py +406 -0
- pandaserver/brokerage/__init__.py +0 -0
- pandaserver/brokerage/broker.py +272 -0
- pandaserver/config/__init__.py +0 -0
- pandaserver/config/config_utils.py +15 -0
- pandaserver/config/daemon_config.py +29 -0
- pandaserver/config/mcp_config.py +38 -0
- pandaserver/config/panda_config.py +233 -0
- pandaserver/configurator/Carbon.py +65 -0
- pandaserver/configurator/Configurator.py +908 -0
- pandaserver/configurator/__init__.py +0 -0
- pandaserver/configurator/aux.py +73 -0
- pandaserver/daemons/__init__.py +0 -0
- pandaserver/daemons/master.py +110 -0
- pandaserver/daemons/master_systemd.py +79 -0
- pandaserver/daemons/scripts/__init__.py +0 -0
- pandaserver/daemons/scripts/add_main.py +264 -0
- pandaserver/daemons/scripts/add_sub.py +221 -0
- pandaserver/daemons/scripts/cache_pilots.py +47 -0
- pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
- pandaserver/daemons/scripts/carbon.py +50 -0
- pandaserver/daemons/scripts/configurator.py +94 -0
- pandaserver/daemons/scripts/copyArchive.py +1387 -0
- pandaserver/daemons/scripts/datasetManager.py +1154 -0
- pandaserver/daemons/scripts/dummy_test.py +25 -0
- pandaserver/daemons/scripts/evpPD2P.py +144 -0
- pandaserver/daemons/scripts/hs_scrapers.py +264 -0
- pandaserver/daemons/scripts/metric_collector.py +927 -0
- pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
- pandaserver/daemons/scripts/pilotStreaming.py +109 -0
- pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
- pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
- pandaserver/daemons/scripts/task_evaluator.py +405 -0
- pandaserver/daemons/scripts/tmpwatch.py +70 -0
- pandaserver/daemons/scripts/worker_synchronization.py +97 -0
- pandaserver/daemons/utils.py +753 -0
- pandaserver/dataservice/DataServiceUtils.py +323 -0
- pandaserver/dataservice/ErrorCode.py +25 -0
- pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
- pandaserver/dataservice/__init__.py +0 -0
- pandaserver/dataservice/activator.py +79 -0
- pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
- pandaserver/dataservice/adder_dummy_plugin.py +31 -0
- pandaserver/dataservice/adder_gen.py +954 -0
- pandaserver/dataservice/adder_plugin_base.py +27 -0
- pandaserver/dataservice/adder_result.py +75 -0
- pandaserver/dataservice/adder_simple_plugin.py +160 -0
- pandaserver/dataservice/closer.py +247 -0
- pandaserver/dataservice/closer_atlas_plugin.py +90 -0
- pandaserver/dataservice/ddm.py +1381 -0
- pandaserver/dataservice/ddm_handler.py +95 -0
- pandaserver/dataservice/dyn_data_distributer.py +824 -0
- pandaserver/dataservice/event_lookup_client_ei.py +77 -0
- pandaserver/dataservice/event_picker.py +268 -0
- pandaserver/dataservice/finisher.py +194 -0
- pandaserver/dataservice/setupper.py +225 -0
- pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
- pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
- pandaserver/dataservice/setupper_plugin_base.py +67 -0
- pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
- pandaserver/jobdispatcher/ErrorCode.py +19 -0
- pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
- pandaserver/jobdispatcher/Protocol.py +443 -0
- pandaserver/jobdispatcher/Watcher.py +186 -0
- pandaserver/jobdispatcher/__init__.py +0 -0
- pandaserver/pandamcp/__init__.py +0 -0
- pandaserver/pandamcp/mcp_main.py +56 -0
- pandaserver/pandamcp/mcp_test_client.py +95 -0
- pandaserver/pandamcp/mcp_utils.py +74 -0
- pandaserver/proxycache/__init__.py +0 -0
- pandaserver/proxycache/panda_proxy_cache.py +240 -0
- pandaserver/proxycache/token_cache.py +141 -0
- pandaserver/server/.gacl +6 -0
- pandaserver/server/panda.py +539 -0
- pandaserver/srvcore/CoreUtils.py +572 -0
- pandaserver/srvcore/MailUtils.py +106 -0
- pandaserver/srvcore/__init__.py +0 -0
- pandaserver/srvcore/allowed_methods.py +100 -0
- pandaserver/srvcore/oidc_utils.py +152 -0
- pandaserver/srvcore/panda_request.py +196 -0
- pandaserver/srvcore/srv_msg_utils.py +42 -0
- pandaserver/taskbuffer/ConBridge.py +513 -0
- pandaserver/taskbuffer/DBProxyPool.py +95 -0
- pandaserver/taskbuffer/DataCarousel.py +3053 -0
- pandaserver/taskbuffer/DatasetSpec.py +137 -0
- pandaserver/taskbuffer/DdmSpec.py +201 -0
- pandaserver/taskbuffer/ErrorCode.py +87 -0
- pandaserver/taskbuffer/EventServiceUtils.py +396 -0
- pandaserver/taskbuffer/FileSpec.py +290 -0
- pandaserver/taskbuffer/GlobalShares.py +244 -0
- pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
- pandaserver/taskbuffer/Initializer.py +82 -0
- pandaserver/taskbuffer/InputChunk.py +1080 -0
- pandaserver/taskbuffer/JediCacheSpec.py +57 -0
- pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
- pandaserver/taskbuffer/JediFileSpec.py +242 -0
- pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
- pandaserver/taskbuffer/JobSpec.py +1006 -0
- pandaserver/taskbuffer/JobUtils.py +232 -0
- pandaserver/taskbuffer/NucleusSpec.py +116 -0
- pandaserver/taskbuffer/OraDBProxy.py +174 -0
- pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
- pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
- pandaserver/taskbuffer/ParseJobXML.py +350 -0
- pandaserver/taskbuffer/PickleFileSpec.py +18 -0
- pandaserver/taskbuffer/PickleJobSpec.py +25 -0
- pandaserver/taskbuffer/PrioUtil.py +40 -0
- pandaserver/taskbuffer/ProcessGroups.py +111 -0
- pandaserver/taskbuffer/ResourceSpec.py +175 -0
- pandaserver/taskbuffer/SQLDumper.py +37 -0
- pandaserver/taskbuffer/SQLManager.py +60 -0
- pandaserver/taskbuffer/SiteSpec.py +304 -0
- pandaserver/taskbuffer/SupErrors.py +10 -0
- pandaserver/taskbuffer/TaskBuffer.py +2882 -0
- pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
- pandaserver/taskbuffer/Utils.py +848 -0
- pandaserver/taskbuffer/WorkQueue.py +227 -0
- pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
- pandaserver/taskbuffer/WorkerSpec.py +143 -0
- pandaserver/taskbuffer/WrappedCursor.py +487 -0
- pandaserver/taskbuffer/WrappedPickle.py +88 -0
- pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
- pandaserver/taskbuffer/__init__.py +0 -0
- pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
- pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
- pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
- pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
- pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
- pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
- pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
- pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
- pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
- pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
- pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
- pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
- pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
- pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
- pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
- pandaserver/taskbuffer/retryModule.py +552 -0
- pandaserver/taskbuffer/task_split_rules.py +308 -0
- pandaserver/taskbuffer/workflow_processor.py +249 -0
- pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
- pandaserver/test/SchemaChecker.py +40 -0
- pandaserver/test/__init__.py +0 -0
- pandaserver/test/banUser.py +43 -0
- pandaserver/test/boostPrio.py +19 -0
- pandaserver/test/boostUser.py +35 -0
- pandaserver/test/callbackDDM.py +11 -0
- pandaserver/test/daod_on_demand.py +95 -0
- pandaserver/test/finishJob.py +184 -0
- pandaserver/test/finishTaskJEDI.py +11 -0
- pandaserver/test/getJobs.py +53 -0
- pandaserver/test/killJob.py +79 -0
- pandaserver/test/killJobLowPrio.py +99 -0
- pandaserver/test/killJobsInTask.py +56 -0
- pandaserver/test/killTask.py +80 -0
- pandaserver/test/killTaskJEDI.py +11 -0
- pandaserver/test/killUser.py +83 -0
- pandaserver/test/lsst/README.txt +7 -0
- pandaserver/test/lsst/lsstSubmit.py +139 -0
- pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
- pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
- pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
- pandaserver/test/reassignJobs.py +17 -0
- pandaserver/test/reassignSite.py +82 -0
- pandaserver/test/reassignTask.py +117 -0
- pandaserver/test/reassignWaiting.py +39 -0
- pandaserver/test/recoverLostFiles.py +7 -0
- pandaserver/test/reloadInputDS.py +51 -0
- pandaserver/test/sendCommandToJob.py +26 -0
- pandaserver/test/setDebugMode.py +38 -0
- pandaserver/test/setPriority.py +28 -0
- pandaserver/test/testEvgen.py +60 -0
- pandaserver/test/testEvgen17.py +58 -0
- pandaserver/test/testG4sim.py +81 -0
- pandaserver/test/testG4sim17.py +90 -0
- pandaserver/test/testGlobalShares.py +131 -0
- pandaserver/test/testReco.py +103 -0
- pandaserver/test/testSimulReco14.py +98 -0
- pandaserver/test/testSiteMap.py +26 -0
- pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
- pandaserver/userinterface/Client.py +1686 -0
- pandaserver/userinterface/UserIF.py +1799 -0
- pandaserver/userinterface/__init__.py +0 -0
- pandaserver/workflow/__init__.py +0 -0
- pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
- pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
- pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
- pandaserver/workflow/pcwl_test.py +122 -0
- pandaserver/workflow/pcwl_utils.py +384 -0
- pandaserver/workflow/psnakemake_container.json +68 -0
- pandaserver/workflow/psnakemake_task.json +62 -0
- pandaserver/workflow/psnakemake_test.py +60 -0
- pandaserver/workflow/snakeparser/__init__.py +3 -0
- pandaserver/workflow/snakeparser/extensions.py +74 -0
- pandaserver/workflow/snakeparser/log.py +53 -0
- pandaserver/workflow/snakeparser/names.py +3 -0
- pandaserver/workflow/snakeparser/parser.py +332 -0
- pandaserver/workflow/snakeparser/utils.py +29 -0
- pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
- pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
- pandaserver/workflow/workflow_base.py +531 -0
- pandaserver/workflow/workflow_core.py +1974 -0
- pandaserver/workflow/workflow_core_smoketest.py +388 -0
- pandaserver/workflow/workflow_parser.py +222 -0
- pandaserver/workflow/workflow_utils.py +1047 -0
|
@@ -0,0 +1,1974 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import copy
|
|
3
|
+
import functools
|
|
4
|
+
import importlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
import socket
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from collections import namedtuple
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
from typing import Any, Dict, List
|
|
16
|
+
|
|
17
|
+
from pandacommon.pandalogger.LogWrapper import LogWrapper
|
|
18
|
+
from pandacommon.pandalogger.PandaLogger import PandaLogger
|
|
19
|
+
from pandacommon.pandautils.PandaUtils import get_sql_IN_bind_variables, naive_utcnow
|
|
20
|
+
|
|
21
|
+
from pandaserver.config import panda_config
|
|
22
|
+
from pandaserver.dataservice.ddm import rucioAPI
|
|
23
|
+
from pandaserver.srvcore.CoreUtils import clean_user_id
|
|
24
|
+
from pandaserver.workflow.workflow_base import (
|
|
25
|
+
WFDataProcessResult,
|
|
26
|
+
WFDataSpec,
|
|
27
|
+
WFDataStatus,
|
|
28
|
+
WFDataTargetCheckStatus,
|
|
29
|
+
WFDataType,
|
|
30
|
+
WFStepProcessResult,
|
|
31
|
+
WFStepSpec,
|
|
32
|
+
WFStepStatus,
|
|
33
|
+
WFStepTargetCheckResult,
|
|
34
|
+
WFStepTargetSubmitResult,
|
|
35
|
+
WFStepType,
|
|
36
|
+
WorkflowProcessResult,
|
|
37
|
+
WorkflowSpec,
|
|
38
|
+
WorkflowStatus,
|
|
39
|
+
)
|
|
40
|
+
from pandaserver.workflow.workflow_parser import (
|
|
41
|
+
json_serialize_default,
|
|
42
|
+
parse_raw_request,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# import polars as pl # isort:skip
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# main logger
|
|
49
|
+
logger = PandaLogger().getLogger(__name__.split(".")[-1])
|
|
50
|
+
|
|
51
|
+
# named tuple for attribute with type
|
|
52
|
+
AttributeWithType = namedtuple("AttributeWithType", ["attribute", "type"])
|
|
53
|
+
|
|
54
|
+
# ==== Global Parameters =======================================
|
|
55
|
+
|
|
56
|
+
WORKFLOW_CHECK_INTERVAL_SEC = 60
|
|
57
|
+
MESSAGE_QUEUE_NAME = "jedi_workflow_manager"
|
|
58
|
+
|
|
59
|
+
# ==== Plugin Map ==============================================
|
|
60
|
+
|
|
61
|
+
PLUGIN_RAW_MAP = {
|
|
62
|
+
"step_handler": {
|
|
63
|
+
"panda_task": ("panda_task_step_handler", "PandaTaskStepHandler"),
|
|
64
|
+
# Add more step handler plugins here
|
|
65
|
+
},
|
|
66
|
+
"data_handler": {
|
|
67
|
+
"ddm_collection": ("ddm_collection_data_handler", "DDMCollectionDataHandler"),
|
|
68
|
+
"panda_task": ("panda_task_data_handler", "PandaTaskDataHandler"),
|
|
69
|
+
# Add more data handler plugins here
|
|
70
|
+
},
|
|
71
|
+
# Add more plugin types here
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Global variable to cache the flavor to plugin class map, initialized lazily in _get_flavor_plugin_class_map
|
|
76
|
+
_flavor_plugin_class_map_cache = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_flavor_plugin_class_map() -> Dict[str, Dict[str, Any]]:
|
|
80
|
+
"""Get the map of plugin type and flavor to plugin class, with lazy initialization and caching"""
|
|
81
|
+
global _flavor_plugin_class_map_cache
|
|
82
|
+
if _flavor_plugin_class_map_cache is not None:
|
|
83
|
+
return _flavor_plugin_class_map_cache
|
|
84
|
+
logger.debug("Initializing workflow plugin class map")
|
|
85
|
+
flavor_plugin_class_map = {}
|
|
86
|
+
for plugin_type, plugins in PLUGIN_RAW_MAP.items():
|
|
87
|
+
flavor_plugin_class_map[plugin_type] = {}
|
|
88
|
+
for flavor, (module_name, class_name) in plugins.items():
|
|
89
|
+
try:
|
|
90
|
+
full_module_name = f"pandaserver.workflow.{plugin_type}_plugins.{module_name}"
|
|
91
|
+
module = importlib.import_module(full_module_name)
|
|
92
|
+
cls = getattr(module, class_name)
|
|
93
|
+
flavor_plugin_class_map[plugin_type][flavor] = cls
|
|
94
|
+
logger.debug(f"Imported {plugin_type} plugin {flavor} from {module_name}.{class_name}")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Failed to import {plugin_type} plugin {flavor} from {module_name}.{class_name}: {e}")
|
|
97
|
+
_flavor_plugin_class_map_cache = flavor_plugin_class_map
|
|
98
|
+
return _flavor_plugin_class_map_cache
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ==== Functions ===============================================
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_plugin_class(plugin_type: str, flavor: str):
|
|
105
|
+
"""
|
|
106
|
+
Get the plugin class for the given type and flavor
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
plugin_type (str): Type of the plugin (e.g., "step_handler", "data_handler")
|
|
110
|
+
flavor (str): Flavor of the plugin (e.g., "panda_task")
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
class: The plugin class if found, otherwise None
|
|
114
|
+
"""
|
|
115
|
+
flavor_plugin_class_map = _get_flavor_plugin_class_map()
|
|
116
|
+
return flavor_plugin_class_map.get(plugin_type, {}).get(flavor)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ==== Workflow Interface ======================================
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class WorkflowInterface(object):
|
|
123
|
+
"""
|
|
124
|
+
Interface for workflow management methods
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, task_buffer, *args, **kwargs):
|
|
128
|
+
"""
|
|
129
|
+
Constructor
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
task_buffer (TaskBufferInterface): Interface to the task buffer
|
|
133
|
+
*args: Additional arguments
|
|
134
|
+
**kwargs: Additional keyword arguments
|
|
135
|
+
"""
|
|
136
|
+
self.tbif = task_buffer
|
|
137
|
+
self.ddm_if = rucioAPI
|
|
138
|
+
self.full_pid = f"{socket.getfqdn().split('.')[0]}-{os.getpgrp()}-{os.getpid()}"
|
|
139
|
+
self.plugin_map = {}
|
|
140
|
+
self.mb_proxy = None
|
|
141
|
+
self.set_mb_proxy()
|
|
142
|
+
|
|
143
|
+
def get_plugin(self, plugin_type: str, flavor: str):
|
|
144
|
+
"""
|
|
145
|
+
Get the plugin instance for the given type and flavor
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
plugin_type (str): Type of the plugin (e.g., "step_handler", "data_handler")
|
|
149
|
+
flavor (str): Flavor of the plugin (e.g., "panda_task")
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Any: The plugin instance if found, otherwise None
|
|
153
|
+
"""
|
|
154
|
+
plugin = self.plugin_map.get(plugin_type, {}).get(flavor)
|
|
155
|
+
if plugin is not None:
|
|
156
|
+
return plugin
|
|
157
|
+
else:
|
|
158
|
+
# not yet loaded, try to load
|
|
159
|
+
cls = get_plugin_class(plugin_type, flavor)
|
|
160
|
+
if cls is not None:
|
|
161
|
+
self.plugin_map.setdefault(plugin_type, {})[flavor] = cls(task_buffer=self.tbif, ddm_if=self.ddm_if)
|
|
162
|
+
plugin = self.plugin_map[plugin_type][flavor]
|
|
163
|
+
return plugin
|
|
164
|
+
|
|
165
|
+
def set_mb_proxy(self):
|
|
166
|
+
"""
|
|
167
|
+
Set the message broker proxy for workflow manager messaging
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
jedi_config = None
|
|
171
|
+
try:
|
|
172
|
+
jedi_config = importlib.import_module("pandajedi.jediconfig.jedi_config")
|
|
173
|
+
except Exception:
|
|
174
|
+
jedi_config = importlib.import_module("pandajedi.jediconfig").jedi_config
|
|
175
|
+
if hasattr(jedi_config, "mq") and hasattr(jedi_config.mq, "configFile") and jedi_config.mq.configFile:
|
|
176
|
+
MsgProcAgent = importlib.import_module(f"pandajedi.jediorder.JediMsgProcessor").MsgProcAgent
|
|
177
|
+
else:
|
|
178
|
+
logger.warning("Message queue config not found in jedi_config; skipped workflow manager messaging")
|
|
179
|
+
return None
|
|
180
|
+
out_q_list = [MESSAGE_QUEUE_NAME]
|
|
181
|
+
mq_agent = MsgProcAgent(config_file=jedi_config.mq.configFile)
|
|
182
|
+
mb_proxy_dict = mq_agent.start_passive_mode(in_q_list=[], out_q_list=out_q_list)
|
|
183
|
+
# stop with atexit
|
|
184
|
+
atexit.register(mq_agent.stop_passive_mode)
|
|
185
|
+
# set mb_proxy
|
|
186
|
+
self.mb_proxy = mb_proxy_dict["out"].get(MESSAGE_QUEUE_NAME)
|
|
187
|
+
if self.mb_proxy is None:
|
|
188
|
+
logger.warning(f"Message queue {MESSAGE_QUEUE_NAME} not found in mb_proxy_dict; skipped workflow manager messaging")
|
|
189
|
+
return None
|
|
190
|
+
# logger.debug(f"Set mb_proxy about queue {MESSAGE_QUEUE_NAME} for workflow manager messaging")
|
|
191
|
+
except Exception:
|
|
192
|
+
logger.warning(f"Failed to set mb_proxy about queue {MESSAGE_QUEUE_NAME}; skipped workflow manager messaging: {traceback.format_exc()}")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def _send_message(self, tmp_log, msg_type: str, data_dict: Dict[str, Any] = None):
|
|
196
|
+
"""
|
|
197
|
+
Send a message to the workflow manager message queue
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
tmp_log (logging.Logger): Logger for logging messages
|
|
201
|
+
msg_type (str): Type of the message (e.g., "workflow", "wfstep", "wfdata")
|
|
202
|
+
data_dict (Dict[str, Any], optional): Additional data to include in the message
|
|
203
|
+
"""
|
|
204
|
+
if self.mb_proxy is None:
|
|
205
|
+
return None
|
|
206
|
+
try:
|
|
207
|
+
now_time = naive_utcnow()
|
|
208
|
+
now_ts = int(now_time.timestamp())
|
|
209
|
+
# get mbproxy
|
|
210
|
+
msg_dict = {}
|
|
211
|
+
if data_dict:
|
|
212
|
+
msg_dict.update(data_dict)
|
|
213
|
+
msg_dict.update(
|
|
214
|
+
{
|
|
215
|
+
"msg_type": msg_type,
|
|
216
|
+
"timestamp": now_ts,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
msg = json.dumps(msg_dict)
|
|
220
|
+
self.mb_proxy.send(msg)
|
|
221
|
+
tmp_log.debug(f"Sent message")
|
|
222
|
+
except Exception:
|
|
223
|
+
tmp_log.error(f"Failed to send message to workflow manager queue {MESSAGE_QUEUE_NAME}: {traceback.format_exc()}")
|
|
224
|
+
|
|
225
|
+
def send_workflow_message(self, workflow_id: int):
|
|
226
|
+
"""
|
|
227
|
+
Send a message about the workflow to the workflow manager message queue
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
workflow_id (int): ID of the workflow
|
|
231
|
+
"""
|
|
232
|
+
tmp_log = LogWrapper(logger, f"send_workflow_message <workflow_id={workflow_id}>")
|
|
233
|
+
self._send_message(tmp_log, "workflow", {"workflow_id": workflow_id})
|
|
234
|
+
|
|
235
|
+
def send_step_message(self, step_id: int):
|
|
236
|
+
"""
|
|
237
|
+
Send a message about the workflow step to the workflow manager message queue
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
step_id (int): ID of the workflow step
|
|
241
|
+
"""
|
|
242
|
+
tmp_log = LogWrapper(logger, f"send_step_message <step_id={step_id}>")
|
|
243
|
+
self._send_message(tmp_log, "wfstep", {"step_id": step_id})
|
|
244
|
+
|
|
245
|
+
def send_data_message(self, data_id: int):
|
|
246
|
+
"""
|
|
247
|
+
Send a message about the workflow data to the workflow manager message queue
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
data_id (int): ID of the workflow data
|
|
251
|
+
"""
|
|
252
|
+
tmp_log = LogWrapper(logger, f"send_data_message <data_id={data_id}>")
|
|
253
|
+
self._send_message(tmp_log, "wfdata", {"data_id": data_id})
|
|
254
|
+
|
|
255
|
+
# --- Context managers for locking -------------------------
|
|
256
|
+
|
|
257
|
+
@contextmanager
|
|
258
|
+
def workflow_lock(self, workflow_id: int, lock_expiration_sec: int = 120):
|
|
259
|
+
"""
|
|
260
|
+
Context manager to lock a workflow
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
workflow_id (int): ID of the workflow to lock
|
|
264
|
+
lock_expiration_sec (int): Time in seconds after which the lock expires
|
|
265
|
+
|
|
266
|
+
Yields:
|
|
267
|
+
WorkflowSpec | None: The locked workflow specification if the lock was acquired, otherwise None
|
|
268
|
+
"""
|
|
269
|
+
if self.tbif.lock_workflow(workflow_id, self.full_pid, lock_expiration_sec):
|
|
270
|
+
try:
|
|
271
|
+
# get the workflow spec locked
|
|
272
|
+
locked_spec = self.tbif.get_workflow(workflow_id)
|
|
273
|
+
# yield and run wrapped function
|
|
274
|
+
yield locked_spec
|
|
275
|
+
finally:
|
|
276
|
+
self.tbif.unlock_workflow(workflow_id, self.full_pid)
|
|
277
|
+
else:
|
|
278
|
+
# lock not acquired
|
|
279
|
+
yield None
|
|
280
|
+
|
|
281
|
+
@contextmanager
|
|
282
|
+
def workflow_step_lock(self, step_id: int, lock_expiration_sec: int = 120):
|
|
283
|
+
"""
|
|
284
|
+
Context manager to lock a workflow step
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
step_id (int): ID of the workflow step to lock
|
|
288
|
+
lock_expiration_sec (int): Time in seconds after which the lock expires
|
|
289
|
+
|
|
290
|
+
Yields:
|
|
291
|
+
WFStepSpec | None: The locked workflow step specification if the lock was acquired, otherwise None
|
|
292
|
+
"""
|
|
293
|
+
if self.tbif.lock_workflow_step(step_id, self.full_pid, lock_expiration_sec):
|
|
294
|
+
try:
|
|
295
|
+
# get the workflow step spec locked
|
|
296
|
+
locked_spec = self.tbif.get_workflow_step(step_id)
|
|
297
|
+
# yield and run wrapped function
|
|
298
|
+
yield locked_spec
|
|
299
|
+
finally:
|
|
300
|
+
self.tbif.unlock_workflow_step(step_id, self.full_pid)
|
|
301
|
+
else:
|
|
302
|
+
# lock not acquired
|
|
303
|
+
yield None
|
|
304
|
+
|
|
305
|
+
@contextmanager
|
|
306
|
+
def workflow_data_lock(self, data_id: int, lock_expiration_sec: int = 120):
|
|
307
|
+
"""
|
|
308
|
+
Context manager to lock workflow data
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
data_id (int): ID of the workflow data to lock
|
|
312
|
+
lock_expiration_sec (int): Time in seconds after which the lock expires
|
|
313
|
+
|
|
314
|
+
Yields:
|
|
315
|
+
WFDataSpec | None: The locked workflow data specification if the lock was acquired, otherwise None
|
|
316
|
+
"""
|
|
317
|
+
if self.tbif.lock_workflow_data(data_id, self.full_pid, lock_expiration_sec):
|
|
318
|
+
try:
|
|
319
|
+
# get the workflow data spec locked
|
|
320
|
+
locked_spec = self.tbif.get_workflow_data(data_id)
|
|
321
|
+
# yield and run wrapped function
|
|
322
|
+
yield locked_spec
|
|
323
|
+
finally:
|
|
324
|
+
self.tbif.unlock_workflow_data(data_id, self.full_pid)
|
|
325
|
+
else:
|
|
326
|
+
# lock not acquired
|
|
327
|
+
yield None
|
|
328
|
+
|
|
329
|
+
# --- Workflow operation -----------------------------------
|
|
330
|
+
|
|
331
|
+
def register_workflow(
|
|
332
|
+
self,
|
|
333
|
+
prodsourcelabel: str,
|
|
334
|
+
user_dn: str,
|
|
335
|
+
workflow_name: str | None = None,
|
|
336
|
+
workflow_definition: dict | None = None,
|
|
337
|
+
raw_request_params: dict | None = None,
|
|
338
|
+
*args,
|
|
339
|
+
**kwargs,
|
|
340
|
+
) -> int | None:
|
|
341
|
+
"""
|
|
342
|
+
Register a new workflow
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
prodsourcelabel (str): Production source label for the workflow
|
|
346
|
+
user_dn (str): Distinguished name of the user submitting the workflow
|
|
347
|
+
workflow_name (str | None): Name of the workflow
|
|
348
|
+
workflow_definition (dict | None): Dictionary of workflow definition
|
|
349
|
+
raw_request_params (dict | None): Dictionary of parameters of the raw request
|
|
350
|
+
*args: Additional arguments
|
|
351
|
+
**kwargs: Additional keyword arguments
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
int | None: The ID of the registered workflow if successful, otherwise None
|
|
355
|
+
"""
|
|
356
|
+
username = clean_user_id(user_dn)
|
|
357
|
+
tmp_log = LogWrapper(logger, f"register_workflow prodsourcelabel={prodsourcelabel} username={username} name={workflow_name}")
|
|
358
|
+
tmp_log.debug(f'Start, user_dn is "{user_dn}"')
|
|
359
|
+
# Implementation of workflow registration logic
|
|
360
|
+
...
|
|
361
|
+
workflow_spec = WorkflowSpec()
|
|
362
|
+
workflow_spec.prodsourcelabel = prodsourcelabel
|
|
363
|
+
workflow_spec.username = username
|
|
364
|
+
if workflow_name is not None:
|
|
365
|
+
workflow_spec.name = workflow_name
|
|
366
|
+
if workflow_definition is not None:
|
|
367
|
+
workflow_definition["user_dn"] = user_dn
|
|
368
|
+
workflow_spec.definition_json = json.dumps(workflow_definition, default=json_serialize_default)
|
|
369
|
+
elif raw_request_params is not None:
|
|
370
|
+
raw_request_params["user_dn"] = user_dn
|
|
371
|
+
workflow_spec.raw_request_json = json.dumps(raw_request_params, default=json_serialize_default)
|
|
372
|
+
else:
|
|
373
|
+
tmp_log.error(f"Either workflow_definition or raw_request_params must be provided")
|
|
374
|
+
return None
|
|
375
|
+
workflow_spec.creation_time = naive_utcnow()
|
|
376
|
+
workflow_spec.status = WorkflowStatus.registered
|
|
377
|
+
# Insert to DB
|
|
378
|
+
ret_workflow_id = self.tbif.insert_workflow(workflow_spec)
|
|
379
|
+
if ret_workflow_id is None:
|
|
380
|
+
tmp_log.error(f"Failed to register workflow")
|
|
381
|
+
return None
|
|
382
|
+
tmp_log.info(f"Registered workflow <workflow_id={ret_workflow_id}>")
|
|
383
|
+
return ret_workflow_id
|
|
384
|
+
|
|
385
|
+
def cancel_workflow(self, workflow_id: int, force: bool = False) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Cancel the workflow
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
workflow_id (int): ID of the workflow to cancel
|
|
391
|
+
force (bool): Whether to force into cancelled status
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
bool: True if the workflow was successfully cancelled, otherwise False
|
|
395
|
+
"""
|
|
396
|
+
tmp_log = LogWrapper(logger, f"cancel_workflow <workflow_id={workflow_id}>")
|
|
397
|
+
# tmp_log.debug("Start")
|
|
398
|
+
try:
|
|
399
|
+
with self.workflow_lock(workflow_id) as workflow_spec:
|
|
400
|
+
if workflow_spec is None:
|
|
401
|
+
tmp_log.warning(f"Failed to acquire lock; skipped")
|
|
402
|
+
return False
|
|
403
|
+
if workflow_spec.status in WorkflowStatus.final_statuses:
|
|
404
|
+
tmp_log.debug(f"Workflow already in final status {workflow_spec.status}; skipped")
|
|
405
|
+
return True
|
|
406
|
+
# Cancel all steps and data of the workflow
|
|
407
|
+
all_cancelled = True
|
|
408
|
+
step_specs = self.tbif.get_steps_of_workflow(workflow_id=workflow_spec.workflow_id)
|
|
409
|
+
if step_specs is None:
|
|
410
|
+
tmp_log.warning(f"Failed to get steps of the workflow; skipped cancelling steps")
|
|
411
|
+
all_cancelled = False
|
|
412
|
+
else:
|
|
413
|
+
for step_spec in step_specs:
|
|
414
|
+
if not self.cancel_step(step_spec.step_id, force):
|
|
415
|
+
all_cancelled = False
|
|
416
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=workflow_spec.workflow_id)
|
|
417
|
+
if data_specs is None:
|
|
418
|
+
tmp_log.warning(f"Failed to get data of the workflow; skipped cancelling data")
|
|
419
|
+
all_cancelled = False
|
|
420
|
+
else:
|
|
421
|
+
for data_spec in data_specs:
|
|
422
|
+
if not self.cancel_data(data_spec.data_id, force):
|
|
423
|
+
all_cancelled = False
|
|
424
|
+
# Update workflow status to cancelled if all steps and data are cancelled
|
|
425
|
+
if not all_cancelled and not force:
|
|
426
|
+
tmp_log.warning(f"Not all steps and data could be cancelled; skipped updating workflow status")
|
|
427
|
+
return False
|
|
428
|
+
else:
|
|
429
|
+
workflow_spec.status = WorkflowStatus.cancelled
|
|
430
|
+
workflow_spec.end_time = naive_utcnow()
|
|
431
|
+
self.tbif.update_workflow(workflow_spec)
|
|
432
|
+
if force and not all_cancelled:
|
|
433
|
+
tmp_log.warning(f"Force cancelled workflow without cancelling all steps and data")
|
|
434
|
+
else:
|
|
435
|
+
tmp_log.info(f"Cancelled workflow, updated status to {workflow_spec.status}")
|
|
436
|
+
return True
|
|
437
|
+
except Exception as e:
|
|
438
|
+
tmp_log.error(f"Got error {str(e)}")
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
# --- Step operation ---------------------------------------
|
|
442
|
+
|
|
443
|
+
def cancel_step(self, step_id: int, force: bool = False) -> bool:
|
|
444
|
+
"""
|
|
445
|
+
Cancel the workflow step
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
step_id (int): ID of the workflow step to cancel
|
|
449
|
+
force (bool): Whether to force into cancelled status; if False, the step will only be cancelled if the target cancellation is successful, while if True, the step will be marked as cancelled regardless of the target cancellation result
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
bool: True if the step was successfully cancelled, otherwise False
|
|
453
|
+
"""
|
|
454
|
+
log_prefix = f"cancel_step <step_id={step_id}>"
|
|
455
|
+
tmp_log = LogWrapper(logger, log_prefix)
|
|
456
|
+
# tmp_log.debug("Start")
|
|
457
|
+
try:
|
|
458
|
+
with self.workflow_step_lock(step_id) as step_spec:
|
|
459
|
+
if step_spec is None:
|
|
460
|
+
tmp_log.warning(f"Failed to acquire lock; skipped")
|
|
461
|
+
return False
|
|
462
|
+
log_prefix += f" workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}"
|
|
463
|
+
tmp_log = LogWrapper(logger, log_prefix)
|
|
464
|
+
if step_spec.status in WFStepStatus.final_statuses:
|
|
465
|
+
tmp_log.debug(f"Step already in final status {step_spec.status}; skipped")
|
|
466
|
+
return True
|
|
467
|
+
# Call plugin to cancel the target of the step
|
|
468
|
+
target_is_cancelled = False
|
|
469
|
+
step_handler = self.get_plugin("step_handler", step_spec.flavor)
|
|
470
|
+
if step_handler is None:
|
|
471
|
+
tmp_log.warning(f"Step handler plugin not found for flavor {step_spec.flavor}; skipped target cancellation")
|
|
472
|
+
else:
|
|
473
|
+
cancel_result = step_handler.cancel_target(step_spec)
|
|
474
|
+
if not cancel_result.success:
|
|
475
|
+
tmp_log.warning(f"Failed to cancel target with plugin {step_spec.flavor}; got message: {cancel_result.message}")
|
|
476
|
+
else:
|
|
477
|
+
tmp_log.debug(f"Cancelled target with flavor {step_spec.flavor}")
|
|
478
|
+
target_is_cancelled = True
|
|
479
|
+
# Update step status to cancelled
|
|
480
|
+
if not target_is_cancelled and not force:
|
|
481
|
+
tmp_log.warning(f"Target not cancelled; skipped updating step status")
|
|
482
|
+
return False
|
|
483
|
+
else:
|
|
484
|
+
step_spec.status = WFStepStatus.cancelled
|
|
485
|
+
step_spec.end_time = naive_utcnow()
|
|
486
|
+
self.tbif.update_workflow_step(step_spec)
|
|
487
|
+
if force and not target_is_cancelled:
|
|
488
|
+
tmp_log.warning(f"Force cancelled step without cancelling target")
|
|
489
|
+
else:
|
|
490
|
+
tmp_log.info(f"Cancelled step, updated status to {step_spec.status}")
|
|
491
|
+
return True
|
|
492
|
+
except Exception as e:
|
|
493
|
+
tmp_log.error(f"Got error {str(e)}")
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
# --- Data operation ---------------------------------------
|
|
497
|
+
|
|
498
|
+
def cancel_data(self, data_id: int, force: bool = False) -> bool:
|
|
499
|
+
"""
|
|
500
|
+
Cancel the workflow data
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
data_id (int): ID of the workflow data to cancel
|
|
504
|
+
force (bool): Whether to force into cancelled status; currently has no effect since data cancellation is not implemented in plugins, but reserved for future use
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
bool: True if the data was successfully cancelled, otherwise False
|
|
508
|
+
"""
|
|
509
|
+
log_prefix = f"cancel_data <data_id={data_id}>"
|
|
510
|
+
tmp_log = LogWrapper(logger, log_prefix)
|
|
511
|
+
# tmp_log.debug("Start")
|
|
512
|
+
try:
|
|
513
|
+
with self.workflow_data_lock(data_id) as data_spec:
|
|
514
|
+
if data_spec is None:
|
|
515
|
+
tmp_log.warning(f"Failed to acquire lock; skipped")
|
|
516
|
+
return False
|
|
517
|
+
log_prefix += f" workflow_id={data_spec.workflow_id}"
|
|
518
|
+
tmp_log = LogWrapper(logger, log_prefix)
|
|
519
|
+
if data_spec.status in WFDataStatus.terminated_statuses:
|
|
520
|
+
tmp_log.debug(f"Data already terminated with status {data_spec.status}; skipped")
|
|
521
|
+
return True
|
|
522
|
+
data_spec.status = WFDataStatus.cancelled
|
|
523
|
+
data_spec.end_time = naive_utcnow()
|
|
524
|
+
self.tbif.update_workflow_data(data_spec)
|
|
525
|
+
tmp_log.info(f"Cancelled data, updated status to {data_spec.status}")
|
|
526
|
+
return True
|
|
527
|
+
except Exception as e:
|
|
528
|
+
tmp_log.error(f"Got error {str(e)}")
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
# ---- Data status transitions -----------------------------
|
|
532
|
+
|
|
533
|
+
def process_data_registered(self, data_spec: WFDataSpec) -> WFDataProcessResult:
|
|
534
|
+
"""
|
|
535
|
+
Process data in registered status
|
|
536
|
+
To prepare for checking the data
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
WFDataProcessResult: The result of processing the data
|
|
543
|
+
"""
|
|
544
|
+
tmp_log = LogWrapper(logger, f"process_data_registered <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
545
|
+
# tmp_log.debug("Start")
|
|
546
|
+
# Initialize
|
|
547
|
+
process_result = WFDataProcessResult()
|
|
548
|
+
# Check status
|
|
549
|
+
if data_spec.status != WFDataStatus.registered:
|
|
550
|
+
process_result.message = f"Data status changed unexpectedly from {WFDataStatus.registered} to {data_spec.status}; skipped"
|
|
551
|
+
tmp_log.warning(f"{process_result.message}")
|
|
552
|
+
return process_result
|
|
553
|
+
# Process
|
|
554
|
+
try:
|
|
555
|
+
# For now, just update status to checking
|
|
556
|
+
data_spec.status = WFDataStatus.checking
|
|
557
|
+
self.tbif.update_workflow_data(data_spec)
|
|
558
|
+
process_result.success = True
|
|
559
|
+
process_result.new_status = data_spec.status
|
|
560
|
+
tmp_log.info(f"Done, status={data_spec.status}")
|
|
561
|
+
except Exception as e:
|
|
562
|
+
process_result.message = f"Got error {str(e)}"
|
|
563
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
564
|
+
return process_result
|
|
565
|
+
|
|
566
|
+
def process_data_checking(self, data_spec: WFDataSpec) -> WFDataProcessResult:
|
|
567
|
+
"""
|
|
568
|
+
Process data in checking status
|
|
569
|
+
To check the conditions about whether the data is available
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
WFDataProcessResult: The result of processing the data
|
|
576
|
+
"""
|
|
577
|
+
tmp_log = LogWrapper(logger, f"process_data_checking <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
578
|
+
# tmp_log.debug("Start")
|
|
579
|
+
# Initialize
|
|
580
|
+
process_result = WFDataProcessResult()
|
|
581
|
+
# Check status
|
|
582
|
+
if data_spec.status != WFDataStatus.checking:
|
|
583
|
+
process_result.message = f"Data status changed unexpectedly from {WFDataStatus.checking} to {data_spec.status}; skipped"
|
|
584
|
+
tmp_log.warning(f"{process_result.message}")
|
|
585
|
+
return process_result
|
|
586
|
+
# Process
|
|
587
|
+
try:
|
|
588
|
+
# Check data availability
|
|
589
|
+
original_status = data_spec.status
|
|
590
|
+
# Get the data handler plugin
|
|
591
|
+
data_handler = self.get_plugin("data_handler", data_spec.flavor)
|
|
592
|
+
# Check the data status
|
|
593
|
+
check_result = data_handler.check_target(data_spec)
|
|
594
|
+
if check_result.success and check_result.check_status is None:
|
|
595
|
+
# No status change
|
|
596
|
+
process_result.message = f"Skipped; {check_result.message}"
|
|
597
|
+
tmp_log.debug(f"{process_result.message}")
|
|
598
|
+
process_result.success = True
|
|
599
|
+
return process_result
|
|
600
|
+
elif not check_result.success or check_result.check_status is None:
|
|
601
|
+
process_result.message = f"Failed to check data; {check_result.message}"
|
|
602
|
+
tmp_log.error(f"{process_result.message}")
|
|
603
|
+
return process_result
|
|
604
|
+
# Update data status
|
|
605
|
+
now_time = naive_utcnow()
|
|
606
|
+
match check_result.check_status:
|
|
607
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
608
|
+
data_spec.status = WFDataStatus.checked_nonexist
|
|
609
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
610
|
+
data_spec.status = WFDataStatus.checked_insuffi
|
|
611
|
+
case WFDataTargetCheckStatus.suffice:
|
|
612
|
+
data_spec.status = WFDataStatus.checked_suffice
|
|
613
|
+
case WFDataTargetCheckStatus.complete:
|
|
614
|
+
data_spec.status = WFDataStatus.checked_complete
|
|
615
|
+
data_spec.check_time = now_time
|
|
616
|
+
self.tbif.update_workflow_data(data_spec)
|
|
617
|
+
process_result.new_status = data_spec.status
|
|
618
|
+
process_result.success = True
|
|
619
|
+
tmp_log.info(f"Done, status={data_spec.status}")
|
|
620
|
+
except Exception as e:
|
|
621
|
+
process_result.message = f"Got error {str(e)}"
|
|
622
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
623
|
+
return process_result
|
|
624
|
+
|
|
625
|
+
def process_data_checked(self, data_spec: WFDataSpec) -> WFDataProcessResult:
|
|
626
|
+
"""
|
|
627
|
+
Process data in checked status
|
|
628
|
+
To advance to next status based on check result
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
WFDataProcessResult: The result of processing the data
|
|
635
|
+
"""
|
|
636
|
+
tmp_log = LogWrapper(logger, f"process_data_checked <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
637
|
+
# tmp_log.debug("Start")
|
|
638
|
+
# Initialize
|
|
639
|
+
process_result = WFDataProcessResult()
|
|
640
|
+
# Check status
|
|
641
|
+
if data_spec.status not in WFDataStatus.checked_statuses:
|
|
642
|
+
process_result.message = f"Data status changed unexpectedly from checked_* to {data_spec.status}; skipped"
|
|
643
|
+
tmp_log.warning(f"{process_result.message}")
|
|
644
|
+
return process_result
|
|
645
|
+
# Process
|
|
646
|
+
try:
|
|
647
|
+
original_status = data_spec.status
|
|
648
|
+
# Update data status based on check result
|
|
649
|
+
now_time = naive_utcnow()
|
|
650
|
+
match data_spec.status:
|
|
651
|
+
case WFDataStatus.checked_nonexist:
|
|
652
|
+
# Data does not exist, advance to binding
|
|
653
|
+
data_spec.status = WFDataStatus.binding
|
|
654
|
+
data_spec.start_time = now_time
|
|
655
|
+
self.tbif.update_workflow_data(data_spec)
|
|
656
|
+
case WFDataStatus.checked_insuffi:
|
|
657
|
+
# Data insufficient, advance to waiting_insuffi
|
|
658
|
+
data_spec.status = WFDataStatus.waiting_insuffi
|
|
659
|
+
self.tbif.update_workflow_data(data_spec)
|
|
660
|
+
case WFDataStatus.checked_suffice:
|
|
661
|
+
# Data partially exist, advance to waiting_suffice
|
|
662
|
+
data_spec.status = WFDataStatus.waiting_suffice
|
|
663
|
+
self.tbif.update_workflow_data(data_spec)
|
|
664
|
+
case WFDataStatus.checked_complete:
|
|
665
|
+
# Data already fully exist, advance to done_skipped
|
|
666
|
+
data_spec.status = WFDataStatus.done_skipped
|
|
667
|
+
data_spec.end_time = now_time
|
|
668
|
+
self.tbif.update_workflow_data(data_spec)
|
|
669
|
+
process_result.success = True
|
|
670
|
+
process_result.new_status = data_spec.status
|
|
671
|
+
tmp_log.info(f"Done, from {original_status} to status={data_spec.status}")
|
|
672
|
+
except Exception as e:
|
|
673
|
+
process_result.message = f"Got error {str(e)}"
|
|
674
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
675
|
+
return process_result
|
|
676
|
+
|
|
677
|
+
def process_data_binding(self, data_spec: WFDataSpec, step_spec: WFStepSpec) -> WFDataProcessResult:
|
|
678
|
+
"""
|
|
679
|
+
Process data in binding status
|
|
680
|
+
To bind the data to the step that will generate it
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
684
|
+
step_spec (WFStepSpec): The workflow step specification to bind the data to
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
WFDataProcessResult: The result of processing the data
|
|
688
|
+
"""
|
|
689
|
+
tmp_log = LogWrapper(logger, f"process_data_binding <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
690
|
+
# tmp_log.debug("Start")
|
|
691
|
+
# Initialize
|
|
692
|
+
process_result = WFDataProcessResult()
|
|
693
|
+
# Check status
|
|
694
|
+
if data_spec.status != WFDataStatus.binding:
|
|
695
|
+
process_result.message = f"Data status changed unexpectedly from {WFDataStatus.binding} to {data_spec.status}; skipped"
|
|
696
|
+
tmp_log.warning(f"{process_result.message}")
|
|
697
|
+
return process_result
|
|
698
|
+
# Process
|
|
699
|
+
try:
|
|
700
|
+
original_status = data_spec.status
|
|
701
|
+
data_spec.source_step_id = step_spec.step_id
|
|
702
|
+
data_spec.status = WFDataStatus.generating_bound
|
|
703
|
+
self.tbif.update_workflow_data(data_spec)
|
|
704
|
+
process_result.success = True
|
|
705
|
+
process_result.new_status = data_spec.status
|
|
706
|
+
tmp_log.info(f"Done, bound to step_id={step_spec.step_id}, from {original_status} to status={data_spec.status}")
|
|
707
|
+
except Exception as e:
|
|
708
|
+
process_result.message = f"Got error {str(e)}"
|
|
709
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
710
|
+
return process_result
|
|
711
|
+
|
|
712
|
+
def process_data_generating(self, data_spec: WFDataSpec) -> WFDataProcessResult:
|
|
713
|
+
"""
|
|
714
|
+
Process data in generating status
|
|
715
|
+
To check the status of the data being generated
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
719
|
+
|
|
720
|
+
Returns:
|
|
721
|
+
WFDataProcessResult: The result of processing the data
|
|
722
|
+
"""
|
|
723
|
+
tmp_log = LogWrapper(logger, f"process_data_generating <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
724
|
+
# tmp_log.debug("Start")
|
|
725
|
+
# Initialize
|
|
726
|
+
process_result = WFDataProcessResult()
|
|
727
|
+
# Check status
|
|
728
|
+
if data_spec.status not in WFDataStatus.generating_statuses:
|
|
729
|
+
process_result.message = f"Data status changed unexpectedly from generating_* to {data_spec.status}; skipped"
|
|
730
|
+
tmp_log.warning(f"{process_result.message}")
|
|
731
|
+
return process_result
|
|
732
|
+
# Process
|
|
733
|
+
try:
|
|
734
|
+
original_status = data_spec.status
|
|
735
|
+
# Get the data handler plugin
|
|
736
|
+
data_handler = self.get_plugin("data_handler", data_spec.flavor)
|
|
737
|
+
# Check the data status
|
|
738
|
+
check_result = data_handler.check_target(data_spec)
|
|
739
|
+
if check_result.success and check_result.check_status is None:
|
|
740
|
+
# No status change
|
|
741
|
+
process_result.message = f"Skipped; {check_result.message}"
|
|
742
|
+
tmp_log.debug(f"{process_result.message}")
|
|
743
|
+
process_result.success = True
|
|
744
|
+
return process_result
|
|
745
|
+
elif not check_result.success or check_result.check_status is None:
|
|
746
|
+
process_result.message = f"Failed to check data; {check_result.message}"
|
|
747
|
+
tmp_log.error(f"{process_result.message}")
|
|
748
|
+
return process_result
|
|
749
|
+
# Update data status
|
|
750
|
+
now_time = naive_utcnow()
|
|
751
|
+
if original_status == WFDataStatus.generating_bound:
|
|
752
|
+
match check_result.check_status:
|
|
753
|
+
case WFDataTargetCheckStatus.suffice | WFDataTargetCheckStatus.complete:
|
|
754
|
+
# Data exist, advance to generating_suffice
|
|
755
|
+
data_spec.status = WFDataStatus.generating_suffice
|
|
756
|
+
process_result.new_status = data_spec.status
|
|
757
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
758
|
+
# Data insufficient, move to generating_insuffi
|
|
759
|
+
data_spec.status = WFDataStatus.generating_insuffi
|
|
760
|
+
process_result.new_status = data_spec.status
|
|
761
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
762
|
+
# Data not yet exist, stay in generating_bound
|
|
763
|
+
pass
|
|
764
|
+
case _:
|
|
765
|
+
# Unexpected status, log and skip
|
|
766
|
+
tmp_log.warning(f"Invalid check_status {check_result.check_status} from target check result; skipped")
|
|
767
|
+
elif original_status == WFDataStatus.generating_insuffi:
|
|
768
|
+
match check_result.check_status:
|
|
769
|
+
case WFDataTargetCheckStatus.suffice | WFDataTargetCheckStatus.complete:
|
|
770
|
+
# Data now exist, advance to generating_suffice
|
|
771
|
+
data_spec.status = WFDataStatus.generating_suffice
|
|
772
|
+
process_result.new_status = data_spec.status
|
|
773
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
774
|
+
# Data still insufficient, stay in generating_insuffi
|
|
775
|
+
pass
|
|
776
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
777
|
+
# Data not exist anymore, unexpected, log and skip
|
|
778
|
+
tmp_log.warning(f"Data do not exist anymore, unexpected; skipped")
|
|
779
|
+
case _:
|
|
780
|
+
# Unexpected status, log and skip
|
|
781
|
+
tmp_log.warning(f"Invalid check_status {check_result.check_status} from target check result; skipped")
|
|
782
|
+
elif original_status == WFDataStatus.generating_suffice:
|
|
783
|
+
match check_result.check_status:
|
|
784
|
+
case WFDataTargetCheckStatus.complete:
|
|
785
|
+
# Data fully exist, advance to final status done_generated
|
|
786
|
+
data_spec.status = WFDataStatus.done_generated
|
|
787
|
+
process_result.new_status = data_spec.status
|
|
788
|
+
data_spec.end_time = now_time
|
|
789
|
+
case WFDataTargetCheckStatus.suffice:
|
|
790
|
+
# Data still partially exist, stay in generating_suffice
|
|
791
|
+
pass
|
|
792
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
793
|
+
# Data not sufficient anymore, unexpected, log and skip
|
|
794
|
+
tmp_log.warning(f"Data are not sufficient anymore, unexpected; skipped")
|
|
795
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
796
|
+
# Data not exist anymore, unexpected, log and skip
|
|
797
|
+
tmp_log.warning(f"Data do not exist anymore, unexpected; skipped")
|
|
798
|
+
case _:
|
|
799
|
+
# Unexpected status, log and skip
|
|
800
|
+
tmp_log.warning(f"Invalid check_status {check_result.check_status} from target check result; skipped")
|
|
801
|
+
data_spec.check_time = now_time
|
|
802
|
+
self.tbif.update_workflow_data(data_spec)
|
|
803
|
+
process_result.success = True
|
|
804
|
+
if data_spec.status == original_status:
|
|
805
|
+
tmp_log.info(f"Done, status stays in {data_spec.status}")
|
|
806
|
+
else:
|
|
807
|
+
tmp_log.info(f"Done, from {original_status} to status={data_spec.status}")
|
|
808
|
+
except Exception as e:
|
|
809
|
+
process_result.message = f"Got error {str(e)}"
|
|
810
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
811
|
+
return process_result
|
|
812
|
+
|
|
813
|
+
def process_data_waiting(self, data_spec: WFDataSpec) -> WFDataProcessResult:
|
|
814
|
+
"""
|
|
815
|
+
Process data in waiting status
|
|
816
|
+
To check the status of the data being waited for, probably generating by other workflow steps or external sources
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
WFDataProcessResult: The result of processing the data
|
|
823
|
+
"""
|
|
824
|
+
tmp_log = LogWrapper(logger, f"process_data_waiting <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id}")
|
|
825
|
+
# tmp_log.debug("Start")
|
|
826
|
+
# Initialize
|
|
827
|
+
process_result = WFDataProcessResult()
|
|
828
|
+
# Check status
|
|
829
|
+
if data_spec.status not in WFDataStatus.waiting_statuses:
|
|
830
|
+
process_result.message = f"Data status changed unexpectedly from waiting_* to {data_spec.status}; skipped"
|
|
831
|
+
tmp_log.warning(f"{process_result.message}")
|
|
832
|
+
return process_result
|
|
833
|
+
# Process
|
|
834
|
+
try:
|
|
835
|
+
original_status = data_spec.status
|
|
836
|
+
# Get the data handler plugin
|
|
837
|
+
data_handler = self.get_plugin("data_handler", data_spec.flavor)
|
|
838
|
+
# Check the data status
|
|
839
|
+
check_result = data_handler.check_target(data_spec)
|
|
840
|
+
if check_result.success and check_result.check_status is None:
|
|
841
|
+
# No status change
|
|
842
|
+
process_result.message = f"Skipped; {check_result.message}"
|
|
843
|
+
tmp_log.debug(f"{process_result.message}")
|
|
844
|
+
process_result.success = True
|
|
845
|
+
return process_result
|
|
846
|
+
elif not check_result.success or check_result.check_status is None:
|
|
847
|
+
process_result.message = f"Failed to check data; {check_result.message}"
|
|
848
|
+
tmp_log.error(f"{process_result.message}")
|
|
849
|
+
return process_result
|
|
850
|
+
# Update data status
|
|
851
|
+
now_time = naive_utcnow()
|
|
852
|
+
if original_status == WFDataStatus.waiting_suffice:
|
|
853
|
+
match check_result.check_status:
|
|
854
|
+
case WFDataTargetCheckStatus.complete:
|
|
855
|
+
# Data fully exist, advance to final status done_waited
|
|
856
|
+
data_spec.status = WFDataStatus.done_waited
|
|
857
|
+
process_result.new_status = data_spec.status
|
|
858
|
+
data_spec.end_time = now_time
|
|
859
|
+
case WFDataTargetCheckStatus.suffice:
|
|
860
|
+
# Data still partially exist, stay in waiting_suffice
|
|
861
|
+
pass
|
|
862
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
863
|
+
# Data not sufficient anymore, unexpected, log and skip
|
|
864
|
+
tmp_log.warning(f"Data are not sufficient anymore, unexpected; skipped")
|
|
865
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
866
|
+
# Data not exist anymore, unexpected, log and skip
|
|
867
|
+
tmp_log.warning(f"Data do not exist anymore, unexpected; skipped")
|
|
868
|
+
case _:
|
|
869
|
+
tmp_log.warning(f"Invalid check_status {check_result.check_status} from target check result; skipped")
|
|
870
|
+
elif original_status == WFDataStatus.waiting_insuffi:
|
|
871
|
+
match check_result.check_status:
|
|
872
|
+
case WFDataTargetCheckStatus.suffice:
|
|
873
|
+
# Data partially exist, advance to waiting_suffice
|
|
874
|
+
data_spec.status = WFDataStatus.waiting_suffice
|
|
875
|
+
process_result.new_status = data_spec.status
|
|
876
|
+
case WFDataTargetCheckStatus.complete:
|
|
877
|
+
# Data fully exist, advance to final status done_waited
|
|
878
|
+
data_spec.status = WFDataStatus.done_waited
|
|
879
|
+
process_result.new_status = data_spec.status
|
|
880
|
+
data_spec.end_time = now_time
|
|
881
|
+
case WFDataTargetCheckStatus.insuffi:
|
|
882
|
+
# Data still insufficient, stay in waiting_insuffi
|
|
883
|
+
pass
|
|
884
|
+
case WFDataTargetCheckStatus.nonexist:
|
|
885
|
+
# Data not exist anymore, unexpected, log and skip
|
|
886
|
+
tmp_log.warning(f"Data do not exist anymore, unexpected; skipped")
|
|
887
|
+
case _:
|
|
888
|
+
tmp_log.warning(f"Invalid check_status {check_result.check_status} from target check result; skipped")
|
|
889
|
+
data_spec.check_time = now_time
|
|
890
|
+
self.tbif.update_workflow_data(data_spec)
|
|
891
|
+
process_result.success = True
|
|
892
|
+
if data_spec.status == original_status:
|
|
893
|
+
tmp_log.info(f"Done, status stays in {data_spec.status}")
|
|
894
|
+
else:
|
|
895
|
+
tmp_log.info(f"Done, from {original_status} to status={data_spec.status}")
|
|
896
|
+
except Exception as e:
|
|
897
|
+
process_result.message = f"Got error {str(e)}"
|
|
898
|
+
tmp_log.error(f"{traceback.format_exc()}")
|
|
899
|
+
return process_result
|
|
900
|
+
|
|
901
|
+
def process_data(self, data_spec: WFDataSpec, by: str = "dog") -> tuple[WFDataProcessResult | None, WFDataSpec]:
|
|
902
|
+
"""
|
|
903
|
+
Process a single workflow data specification
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
data_spec (WFDataSpec): The workflow data specification to process
|
|
907
|
+
by (str): Identifier of the entity processing the data specification
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
WFDataProcessResult | None: The result of processing the data specification, or None if skipped
|
|
911
|
+
WFDataSpec: The updated workflow data specification
|
|
912
|
+
"""
|
|
913
|
+
tmp_log = LogWrapper(logger, f"process_data <data_id={data_spec.data_id}> workflow_id={data_spec.workflow_id} by={by}")
|
|
914
|
+
# tmp_log.debug("Start")
|
|
915
|
+
tmp_res = None
|
|
916
|
+
with self.workflow_data_lock(data_spec.data_id) as locked_data_spec:
|
|
917
|
+
if locked_data_spec is None:
|
|
918
|
+
tmp_log.warning(f"Failed to acquire lock for data_id={data_spec.data_id}; skipped")
|
|
919
|
+
return None, data_spec
|
|
920
|
+
data_spec = locked_data_spec
|
|
921
|
+
orig_status = data_spec.status
|
|
922
|
+
# Process the data
|
|
923
|
+
if data_spec.status == WFDataStatus.registered:
|
|
924
|
+
tmp_res = self.process_data_registered(data_spec)
|
|
925
|
+
elif data_spec.status == WFDataStatus.checking:
|
|
926
|
+
tmp_res = self.process_data_checking(data_spec)
|
|
927
|
+
elif data_spec.status in WFDataStatus.checked_statuses:
|
|
928
|
+
tmp_res = self.process_data_checked(data_spec)
|
|
929
|
+
elif data_spec.status == WFDataStatus.binding:
|
|
930
|
+
# dummy result since binding data are handled in step processing
|
|
931
|
+
dummy_process_result = WFDataProcessResult()
|
|
932
|
+
dummy_process_result.success = True
|
|
933
|
+
tmp_res = dummy_process_result
|
|
934
|
+
tmp_log.debug(f"Data status {data_spec.status} ; wait for step processing")
|
|
935
|
+
elif data_spec.status in WFDataStatus.generating_statuses:
|
|
936
|
+
tmp_res = self.process_data_generating(data_spec)
|
|
937
|
+
elif data_spec.status in WFDataStatus.waiting_statuses:
|
|
938
|
+
tmp_res = self.process_data_waiting(data_spec)
|
|
939
|
+
else:
|
|
940
|
+
tmp_log.debug(f"Data status {data_spec.status} is not handled in this context; skipped")
|
|
941
|
+
# For changes into transient status, send message to trigger processing immediately
|
|
942
|
+
if data_spec.status != orig_status and data_spec.status in WFDataStatus.transient_statuses:
|
|
943
|
+
self.send_data_message(data_spec.data_id)
|
|
944
|
+
return tmp_res, data_spec
|
|
945
|
+
|
|
946
|
+
def process_datas(self, data_specs: List[WFDataSpec], by: str = "dog") -> Dict:
|
|
947
|
+
"""
|
|
948
|
+
Process a list of workflow data specifications
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
data_specs (List[WFDataSpec]): List of workflow data specifications to process
|
|
952
|
+
by (str): Identifier of the entity processing the data specifications
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Dict: Statistics of the processing results
|
|
956
|
+
"""
|
|
957
|
+
tmp_log = LogWrapper(logger, f"process_datas <workflow_id={data_specs[0].workflow_id}> by={by}")
|
|
958
|
+
n_data = len(data_specs)
|
|
959
|
+
tmp_log.debug(f"Start, processing {n_data} data specs")
|
|
960
|
+
data_status_stats = {"n_data": n_data, "changed": {}, "unchanged": {}, "processed": {}, "n_processed": 0}
|
|
961
|
+
for data_spec in data_specs:
|
|
962
|
+
orig_status = data_spec.status
|
|
963
|
+
tmp_res, data_spec = self.process_data(data_spec, by=by)
|
|
964
|
+
if tmp_res and tmp_res.success:
|
|
965
|
+
# update stats
|
|
966
|
+
if tmp_res.new_status and data_spec.status != orig_status:
|
|
967
|
+
data_status_stats["changed"].setdefault(data_spec.status, 0)
|
|
968
|
+
data_status_stats["changed"][data_spec.status] += 1
|
|
969
|
+
else:
|
|
970
|
+
data_status_stats["unchanged"].setdefault(data_spec.status, 0)
|
|
971
|
+
data_status_stats["unchanged"][data_spec.status] += 1
|
|
972
|
+
data_status_stats["processed"].setdefault(data_spec.status, 0)
|
|
973
|
+
data_status_stats["processed"][data_spec.status] += 1
|
|
974
|
+
data_status_stats["n_processed"] += 1
|
|
975
|
+
tmp_log.info(
|
|
976
|
+
f"Done, processed {data_status_stats['n_processed']}/{n_data} data specs, unchanged: {data_status_stats['unchanged']}, changed: {data_status_stats['changed']}"
|
|
977
|
+
)
|
|
978
|
+
return data_status_stats
|
|
979
|
+
|
|
980
|
+
# ---- Step status transitions -----------------------------
|
|
981
|
+
|
|
982
|
+
def _check_all_inputs_of_step(self, tmp_log: LogWrapper, input_data_list: List[str], data_spec_map: Dict[str, WFDataSpec]) -> Dict[str, bool]:
|
|
983
|
+
"""
|
|
984
|
+
Check whether all input data of a step are sufficient or complete
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
tmp_log (LogWrapper): Logger for logging messages
|
|
988
|
+
input_data_list (List[str]): List of input data names for the step
|
|
989
|
+
data_spec_map (Dict[str, WFDataSpec]): Mapping of data names to their specifications
|
|
990
|
+
|
|
991
|
+
Returns:
|
|
992
|
+
Dict[str, bool]: Dictionary indicating whether all inputs sufficient and complete
|
|
993
|
+
"""
|
|
994
|
+
# Check if all input data sufficient or complete
|
|
995
|
+
ret_dict = {"all_inputs_sufficient": True, "all_inputs_complete": True}
|
|
996
|
+
for input_data_name in input_data_list:
|
|
997
|
+
data_spec = data_spec_map.get(input_data_name)
|
|
998
|
+
if data_spec is None:
|
|
999
|
+
tmp_log.warning(f"Input data {input_data_name} not found in workflow data")
|
|
1000
|
+
ret_dict["all_inputs_sufficient"] = False
|
|
1001
|
+
ret_dict["all_inputs_complete"] = False
|
|
1002
|
+
break
|
|
1003
|
+
elif data_spec.status not in WFDataStatus.good_input_statuses:
|
|
1004
|
+
tmp_log.debug(f"Input data {input_data_name} status {data_spec.status} is not sufficient as input")
|
|
1005
|
+
ret_dict["all_inputs_sufficient"] = False
|
|
1006
|
+
ret_dict["all_inputs_complete"] = False
|
|
1007
|
+
break
|
|
1008
|
+
elif data_spec.status not in WFDataStatus.done_statuses:
|
|
1009
|
+
ret_dict["all_inputs_complete"] = False
|
|
1010
|
+
if ret_dict["all_inputs_complete"]:
|
|
1011
|
+
tmp_log.debug("All input data are complete")
|
|
1012
|
+
elif ret_dict["all_inputs_sufficient"]:
|
|
1013
|
+
tmp_log.debug("All input data are sufficient as input")
|
|
1014
|
+
return ret_dict
|
|
1015
|
+
|
|
1016
|
+
def process_step_registered(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1017
|
+
"""
|
|
1018
|
+
Process a step in registered status
|
|
1019
|
+
To prepare for checking the step
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1023
|
+
|
|
1024
|
+
Returns:
|
|
1025
|
+
WFStepProcessResult: The result of processing the step
|
|
1026
|
+
"""
|
|
1027
|
+
tmp_log = LogWrapper(
|
|
1028
|
+
logger, f"process_step_registered <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}"
|
|
1029
|
+
)
|
|
1030
|
+
# tmp_log.debug("Start")
|
|
1031
|
+
# Initialize
|
|
1032
|
+
process_result = WFStepProcessResult()
|
|
1033
|
+
# Check status
|
|
1034
|
+
if step_spec.status != WFStepStatus.registered:
|
|
1035
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.registered} to {step_spec.status}; skipped"
|
|
1036
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1037
|
+
return process_result
|
|
1038
|
+
# Process
|
|
1039
|
+
try:
|
|
1040
|
+
step_spec.status = WFStepStatus.checking
|
|
1041
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1042
|
+
process_result.success = True
|
|
1043
|
+
process_result.new_status = step_spec.status
|
|
1044
|
+
tmp_log.info(f"Done, status={step_spec.status}")
|
|
1045
|
+
except Exception as e:
|
|
1046
|
+
process_result.message = f"Got error {str(e)}"
|
|
1047
|
+
tmp_log.error(f"{process_result.message}")
|
|
1048
|
+
return process_result
|
|
1049
|
+
|
|
1050
|
+
def process_step_checking(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1051
|
+
"""
|
|
1052
|
+
Process a step in checking status
|
|
1053
|
+
To check the conditions about whether to process the step
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1057
|
+
|
|
1058
|
+
Returns:
|
|
1059
|
+
WFStepProcessResult: The result of processing the step
|
|
1060
|
+
"""
|
|
1061
|
+
tmp_log = LogWrapper(logger, f"process_step_checking <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1062
|
+
# tmp_log.debug("Start")
|
|
1063
|
+
# Initialize
|
|
1064
|
+
process_result = WFStepProcessResult()
|
|
1065
|
+
# Check status
|
|
1066
|
+
if step_spec.status != WFStepStatus.checking:
|
|
1067
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.checking} to {step_spec.status}; skipped"
|
|
1068
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1069
|
+
return process_result
|
|
1070
|
+
# Process
|
|
1071
|
+
try:
|
|
1072
|
+
# Decide whether to run the step: True = must run, False = can skip, None = undecided yet and must check later
|
|
1073
|
+
to_run_step = False
|
|
1074
|
+
# FIXME: For now, always check outputs, not customizable
|
|
1075
|
+
check_outputs = True
|
|
1076
|
+
if check_outputs and to_run_step is False:
|
|
1077
|
+
to_generate_output = False
|
|
1078
|
+
output_data_names = step_spec.definition_json_map.get("output_data_list", [])
|
|
1079
|
+
for output_data_name in output_data_names:
|
|
1080
|
+
data_spec = self.tbif.get_workflow_data_by_name(output_data_name, step_spec.workflow_id)
|
|
1081
|
+
if data_spec is None:
|
|
1082
|
+
tmp_log.warning(f"Output {output_data_name} not found in workflow data; skipped")
|
|
1083
|
+
to_run_step = None
|
|
1084
|
+
break
|
|
1085
|
+
if data_spec.status == WFDataStatus.binding:
|
|
1086
|
+
tmp_log.debug(f"Output data {output_data_name} status {data_spec.status} requires step to generate")
|
|
1087
|
+
to_generate_output = True
|
|
1088
|
+
break
|
|
1089
|
+
elif data_spec.status in (WFDataStatus.registered, WFDataStatus.checking) or data_spec.status in WFDataStatus.checked_statuses:
|
|
1090
|
+
tmp_log.debug(f"Output data {output_data_name} status {data_spec.status} is not after checked; skipped")
|
|
1091
|
+
to_run_step = None
|
|
1092
|
+
break
|
|
1093
|
+
else:
|
|
1094
|
+
tmp_log.debug(f"Output data {output_data_name} status {data_spec.status} does not require step to generate")
|
|
1095
|
+
continue
|
|
1096
|
+
if to_run_step is not None and to_generate_output:
|
|
1097
|
+
# Outputs are not all good; need to run the step
|
|
1098
|
+
to_run_step = True
|
|
1099
|
+
# Update step status
|
|
1100
|
+
now_time = naive_utcnow()
|
|
1101
|
+
if to_run_step is None:
|
|
1102
|
+
step_spec.check_time = now_time
|
|
1103
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1104
|
+
process_result.success = True
|
|
1105
|
+
tmp_log.info(f"Done, status stays in {step_spec.status}")
|
|
1106
|
+
else:
|
|
1107
|
+
if to_run_step is True:
|
|
1108
|
+
step_spec.status = WFStepStatus.checked_true
|
|
1109
|
+
elif to_run_step is False:
|
|
1110
|
+
step_spec.status = WFStepStatus.checked_false
|
|
1111
|
+
step_spec.check_time = now_time
|
|
1112
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1113
|
+
process_result.success = True
|
|
1114
|
+
process_result.new_status = step_spec.status
|
|
1115
|
+
tmp_log.info(f"Done, status={step_spec.status}")
|
|
1116
|
+
except Exception as e:
|
|
1117
|
+
process_result.message = f"Got error {str(e)}"
|
|
1118
|
+
tmp_log.error(f"{process_result.message}")
|
|
1119
|
+
return process_result
|
|
1120
|
+
|
|
1121
|
+
def process_step_checked(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1122
|
+
"""
|
|
1123
|
+
Process a step in checked status
|
|
1124
|
+
To advance to pending or closed based on check result
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
WFStepProcessResult: The result of processing the step
|
|
1131
|
+
"""
|
|
1132
|
+
tmp_log = LogWrapper(logger, f"process_step_checked <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1133
|
+
# tmp_log.debug("Start")
|
|
1134
|
+
# Initialize
|
|
1135
|
+
process_result = WFStepProcessResult()
|
|
1136
|
+
# Check status
|
|
1137
|
+
if step_spec.status not in WFStepStatus.checked_statuses:
|
|
1138
|
+
process_result.message = f"Step status changed unexpectedly from checked_* to {step_spec.status}; skipped"
|
|
1139
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1140
|
+
return process_result
|
|
1141
|
+
# Process
|
|
1142
|
+
original_status = step_spec.status
|
|
1143
|
+
try:
|
|
1144
|
+
now_time = naive_utcnow()
|
|
1145
|
+
match step_spec.status:
|
|
1146
|
+
case WFStepStatus.checked_true:
|
|
1147
|
+
# Conditions met, advance to pending
|
|
1148
|
+
step_spec.status = WFStepStatus.pending
|
|
1149
|
+
step_spec.check_time = now_time
|
|
1150
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1151
|
+
case WFStepStatus.checked_false:
|
|
1152
|
+
# Conditions not met, advanced to closed
|
|
1153
|
+
step_spec.status = WFStepStatus.closed
|
|
1154
|
+
step_spec.check_time = now_time
|
|
1155
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1156
|
+
process_result.success = True
|
|
1157
|
+
process_result.new_status = step_spec.status
|
|
1158
|
+
tmp_log.info(f"Done, from {original_status} to status={step_spec.status}")
|
|
1159
|
+
except Exception as e:
|
|
1160
|
+
process_result.message = f"Got error {str(e)}"
|
|
1161
|
+
tmp_log.error(f"{process_result.message}")
|
|
1162
|
+
return process_result
|
|
1163
|
+
|
|
1164
|
+
def process_step_pending(self, step_spec: WFStepSpec, data_spec_map: Dict[str, WFDataSpec] | None = None) -> WFStepProcessResult:
|
|
1165
|
+
"""
|
|
1166
|
+
Process a step in pending status
|
|
1167
|
+
To check the inputs of the step
|
|
1168
|
+
|
|
1169
|
+
Args:
|
|
1170
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1171
|
+
data_spec_map (Dict[str, WFDataSpec] | None): Optional map of data name to WFDataSpec for the workflow
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
WFStepProcessResult: The result of processing the step
|
|
1175
|
+
"""
|
|
1176
|
+
tmp_log = LogWrapper(logger, f"process_step_pending <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1177
|
+
# tmp_log.debug("Start")
|
|
1178
|
+
# Initialize
|
|
1179
|
+
process_result = WFStepProcessResult()
|
|
1180
|
+
# Check status
|
|
1181
|
+
if step_spec.status != WFStepStatus.pending:
|
|
1182
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.pending} to {step_spec.status}; skipped"
|
|
1183
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1184
|
+
return process_result
|
|
1185
|
+
# Process
|
|
1186
|
+
try:
|
|
1187
|
+
# Input data list of the step
|
|
1188
|
+
step_spec_definition = step_spec.definition_json_map
|
|
1189
|
+
input_data_list = step_spec_definition.get("input_data_list")
|
|
1190
|
+
if input_data_list is None:
|
|
1191
|
+
process_result.message = f"Step definition does not have input_data_list; skipped"
|
|
1192
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1193
|
+
return process_result
|
|
1194
|
+
# Get data spec map of the workflow
|
|
1195
|
+
if data_spec_map is None:
|
|
1196
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=step_spec.workflow_id)
|
|
1197
|
+
data_spec_map = {data_spec.name: data_spec for data_spec in data_specs}
|
|
1198
|
+
# Check if all input data are good
|
|
1199
|
+
all_inputs_stats = self._check_all_inputs_of_step(tmp_log, input_data_list, data_spec_map)
|
|
1200
|
+
# If not all inputs are sufficient as input, just return and wait for next round
|
|
1201
|
+
if not all_inputs_stats["all_inputs_sufficient"]:
|
|
1202
|
+
tmp_log.debug(f"Some input data are not sufficient as input; skipped")
|
|
1203
|
+
process_result.success = True
|
|
1204
|
+
return process_result
|
|
1205
|
+
# All inputs are good, register outputs of the step and update step status to ready
|
|
1206
|
+
output_data_list = step_spec_definition.get("output_data_list", [])
|
|
1207
|
+
# outputs_raw_dict = step_spec_definition.get("outputs", {})
|
|
1208
|
+
# output_types = step_spec_definition.get("output_types", [])
|
|
1209
|
+
# now_time = naive_utcnow()
|
|
1210
|
+
# New code: for all output data, set source_step_id to this step
|
|
1211
|
+
for output_data_name in output_data_list:
|
|
1212
|
+
data_spec = self.tbif.get_workflow_data_by_name(output_data_name, step_spec.workflow_id)
|
|
1213
|
+
if data_spec is not None:
|
|
1214
|
+
if data_spec.status == WFDataStatus.binding:
|
|
1215
|
+
self.process_data_binding(data_spec, step_spec)
|
|
1216
|
+
tmp_log.debug(f"Bound output data_id={data_spec.data_id} name={output_data_name} to the step")
|
|
1217
|
+
else:
|
|
1218
|
+
tmp_log.debug(f"Output data_id={data_spec.data_id} name={output_data_name} status={data_spec.status} not in binding; skipped")
|
|
1219
|
+
else:
|
|
1220
|
+
tmp_log.warning(f"Output data {output_data_name} not found in workflow data; skipped")
|
|
1221
|
+
if all_inputs_stats["all_inputs_complete"]:
|
|
1222
|
+
# All inputs are complete, mark in step_spec
|
|
1223
|
+
step_spec.set_parameter("all_inputs_complete", True)
|
|
1224
|
+
# Old code for reference
|
|
1225
|
+
# if step_spec_definition.get("is_tail"):
|
|
1226
|
+
# # Tail step, set root output source_step_id
|
|
1227
|
+
# for output_data_name in output_data_list:
|
|
1228
|
+
# data_spec = self.tbif.get_workflow_data_by_name(output_data_name, step_spec.workflow_id)
|
|
1229
|
+
# if data_spec is not None:
|
|
1230
|
+
# data_spec.source_step_id = step_spec.step_id
|
|
1231
|
+
# self.tbif.update_workflow_data(data_spec)
|
|
1232
|
+
# tmp_log.debug(f"Updated output data_id={data_spec.data_id} name={output_data_name} about source_step_id")
|
|
1233
|
+
# else:
|
|
1234
|
+
# tmp_log.warning(f"Output data {output_data_name} not found in workflow data; skipped")
|
|
1235
|
+
# else:
|
|
1236
|
+
# # Intermediate step, update mid output data specs source_step_id
|
|
1237
|
+
# for output_data_name in output_data_list:
|
|
1238
|
+
# data_spec = self.tbif.get_workflow_data_by_name(output_data_name, step_spec.workflow_id)
|
|
1239
|
+
# if data_spec is None:
|
|
1240
|
+
# tmp_log.warning(f"Output data {output_data_name} not found in workflow data; skipped")
|
|
1241
|
+
# continue
|
|
1242
|
+
# elif data_spec.status == WFDataStatus.binding:
|
|
1243
|
+
# # mid data in binding, bind it to the step
|
|
1244
|
+
# data_spec.source_step_id = step_spec.step_id
|
|
1245
|
+
# data_spec.name = output_data_name
|
|
1246
|
+
# data_spec.target_id = outputs_raw_dict.get(output_data_name, {}).get("value") # caution: may be None
|
|
1247
|
+
# data_spec.set_parameter("output_types", output_types)
|
|
1248
|
+
# data_spec.status = WFDataStatus.registered
|
|
1249
|
+
# data_spec.type = WFDataType.mid
|
|
1250
|
+
# data_spec.flavor = "panda_task" # FIXME: hardcoded flavor, should be configurable
|
|
1251
|
+
# data_spec.creation_time = now_time
|
|
1252
|
+
# self.tbif.update_workflow_data(data_spec)
|
|
1253
|
+
# tmp_log.debug(f"Updated mid data {output_data_name} about source step")
|
|
1254
|
+
# # update data_spec_map
|
|
1255
|
+
# data_spec_map[output_data_name] = data_spec
|
|
1256
|
+
step_spec.status = WFStepStatus.ready
|
|
1257
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1258
|
+
process_result.success = True
|
|
1259
|
+
process_result.new_status = step_spec.status
|
|
1260
|
+
tmp_log.info(f"Done, status={step_spec.status}")
|
|
1261
|
+
except Exception as e:
|
|
1262
|
+
process_result.message = f"Got error {str(e)}"
|
|
1263
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1264
|
+
return process_result
|
|
1265
|
+
|
|
1266
|
+
def process_step_ready(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1267
|
+
"""
|
|
1268
|
+
Process a step in ready status
|
|
1269
|
+
To start the step by submitting its target
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
WFStepProcessResult: The result of processing the step
|
|
1276
|
+
"""
|
|
1277
|
+
tmp_log = LogWrapper(logger, f"process_step_ready <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1278
|
+
# tmp_log.debug("Start")
|
|
1279
|
+
# Initialize
|
|
1280
|
+
process_result = WFStepProcessResult()
|
|
1281
|
+
# Check status
|
|
1282
|
+
if step_spec.status != WFStepStatus.ready:
|
|
1283
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.ready} to {step_spec.status}; skipped"
|
|
1284
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1285
|
+
return process_result
|
|
1286
|
+
# Process
|
|
1287
|
+
try:
|
|
1288
|
+
# Get the step handler plugin
|
|
1289
|
+
step_handler = self.get_plugin("step_handler", step_spec.flavor)
|
|
1290
|
+
# Submit the step target
|
|
1291
|
+
submit_result = step_handler.submit_target(step_spec)
|
|
1292
|
+
if not submit_result.success or submit_result.target_id is None:
|
|
1293
|
+
process_result.message = f"Failed to submit step target; {submit_result.message}"
|
|
1294
|
+
tmp_log.error(f"{process_result.message}")
|
|
1295
|
+
return process_result
|
|
1296
|
+
# Update step status to starting
|
|
1297
|
+
step_spec.target_id = submit_result.target_id
|
|
1298
|
+
step_spec.status = WFStepStatus.starting
|
|
1299
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1300
|
+
process_result.success = True
|
|
1301
|
+
process_result.new_status = step_spec.status
|
|
1302
|
+
tmp_log.info(f"Done, submitted target flavor={step_spec.flavor} target_id={step_spec.target_id}, status={step_spec.status}")
|
|
1303
|
+
except Exception as e:
|
|
1304
|
+
process_result.message = f"Got error {str(e)}"
|
|
1305
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1306
|
+
return process_result
|
|
1307
|
+
|
|
1308
|
+
def process_step_starting(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1309
|
+
"""
|
|
1310
|
+
Process a step in starting status
|
|
1311
|
+
To check the status of the starting step
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1315
|
+
|
|
1316
|
+
Returns:
|
|
1317
|
+
WFStepProcessResult: The result of processing the step
|
|
1318
|
+
"""
|
|
1319
|
+
tmp_log = LogWrapper(logger, f"process_step_starting <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1320
|
+
# tmp_log.debug("Start")
|
|
1321
|
+
# Initialize
|
|
1322
|
+
process_result = WFStepProcessResult()
|
|
1323
|
+
# Check status
|
|
1324
|
+
if step_spec.status != WFStepStatus.starting:
|
|
1325
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.starting} to {step_spec.status}; skipped"
|
|
1326
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1327
|
+
return process_result
|
|
1328
|
+
# Process
|
|
1329
|
+
try:
|
|
1330
|
+
# Input data list of the step
|
|
1331
|
+
step_spec_definition = step_spec.definition_json_map
|
|
1332
|
+
input_data_list = step_spec_definition.get("input_data_list")
|
|
1333
|
+
if input_data_list is None:
|
|
1334
|
+
process_result.message = f"Step definition does not have input_data_list; skipped"
|
|
1335
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1336
|
+
return process_result
|
|
1337
|
+
# Get data spec map of the workflow
|
|
1338
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=step_spec.workflow_id)
|
|
1339
|
+
data_spec_map = {data_spec.name: data_spec for data_spec in data_specs}
|
|
1340
|
+
# Check if all input data are good
|
|
1341
|
+
all_inputs_stats = self._check_all_inputs_of_step(tmp_log, input_data_list, data_spec_map)
|
|
1342
|
+
# Get the step handler plugin
|
|
1343
|
+
step_handler = self.get_plugin("step_handler", step_spec.flavor)
|
|
1344
|
+
# Check the step status
|
|
1345
|
+
check_result = step_handler.check_target(step_spec)
|
|
1346
|
+
if not check_result.success or check_result.step_status is None:
|
|
1347
|
+
process_result.message = f"Failed to check step; {check_result.message}"
|
|
1348
|
+
tmp_log.error(f"{process_result.message}")
|
|
1349
|
+
return process_result
|
|
1350
|
+
# If all inputs are complete, mark in step_spec and call the hook of step_handler
|
|
1351
|
+
if all_inputs_stats["all_inputs_complete"]:
|
|
1352
|
+
step_spec.set_parameter("all_inputs_complete", True)
|
|
1353
|
+
step_handler.on_all_inputs_done(step_spec)
|
|
1354
|
+
# Update step status
|
|
1355
|
+
if check_result.step_status in WFStepStatus.after_starting_statuses:
|
|
1356
|
+
# Step status advanced
|
|
1357
|
+
step_spec.status = check_result.step_status
|
|
1358
|
+
process_result.new_status = step_spec.status
|
|
1359
|
+
elif check_result.step_status == WFStepStatus.starting:
|
|
1360
|
+
# Still in starting, do nothing
|
|
1361
|
+
pass
|
|
1362
|
+
else:
|
|
1363
|
+
tmp_log.warning(f"Invalid step_status {check_result.step_status} from target check result; skipped")
|
|
1364
|
+
now_time = naive_utcnow()
|
|
1365
|
+
step_spec.check_time = now_time
|
|
1366
|
+
if step_spec.status in WFStepStatus.after_starting_uninterrupted_statuses and step_spec.start_time is None:
|
|
1367
|
+
# step has run, set start_time if not yet set
|
|
1368
|
+
step_spec.start_time = now_time
|
|
1369
|
+
if step_spec.status in WFStepStatus.final_statuses and step_spec.start_time is not None and step_spec.end_time is None:
|
|
1370
|
+
# step has ended, set end_time if not yet set
|
|
1371
|
+
step_spec.end_time = now_time
|
|
1372
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1373
|
+
process_result.success = True
|
|
1374
|
+
tmp_log.info(f"Checked step, flavor={step_spec.flavor}, target_id={step_spec.target_id}, status={step_spec.status}")
|
|
1375
|
+
except Exception as e:
|
|
1376
|
+
process_result.message = f"Got error {str(e)}"
|
|
1377
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1378
|
+
return process_result
|
|
1379
|
+
|
|
1380
|
+
def process_step_running(self, step_spec: WFStepSpec) -> WFStepProcessResult:
|
|
1381
|
+
"""
|
|
1382
|
+
Process a step in running status
|
|
1383
|
+
To check the status of the running step
|
|
1384
|
+
|
|
1385
|
+
Args:
|
|
1386
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1387
|
+
|
|
1388
|
+
Returns:
|
|
1389
|
+
WFStepProcessResult: The result of processing the step
|
|
1390
|
+
"""
|
|
1391
|
+
tmp_log = LogWrapper(logger, f"process_step_running <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id}")
|
|
1392
|
+
# tmp_log.debug("Start")
|
|
1393
|
+
# Initialize
|
|
1394
|
+
process_result = WFStepProcessResult()
|
|
1395
|
+
# Check status
|
|
1396
|
+
if step_spec.status != WFStepStatus.running:
|
|
1397
|
+
process_result.message = f"Step status changed unexpectedly from {WFStepStatus.running} to {step_spec.status}; skipped"
|
|
1398
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1399
|
+
return process_result
|
|
1400
|
+
# Process
|
|
1401
|
+
try:
|
|
1402
|
+
# Input data list of the step
|
|
1403
|
+
step_spec_definition = step_spec.definition_json_map
|
|
1404
|
+
input_data_list = step_spec_definition.get("input_data_list")
|
|
1405
|
+
if input_data_list is None:
|
|
1406
|
+
process_result.message = f"Step definition does not have input_data_list; skipped"
|
|
1407
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1408
|
+
return process_result
|
|
1409
|
+
# Get data spec map of the workflow
|
|
1410
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=step_spec.workflow_id)
|
|
1411
|
+
data_spec_map = {data_spec.name: data_spec for data_spec in data_specs}
|
|
1412
|
+
# Check if all input data are good
|
|
1413
|
+
all_inputs_stats = self._check_all_inputs_of_step(tmp_log, input_data_list, data_spec_map)
|
|
1414
|
+
# Get the step handler plugin
|
|
1415
|
+
step_handler = self.get_plugin("step_handler", step_spec.flavor)
|
|
1416
|
+
# If all inputs are complete, mark in step_spec and call the hook of step_handler
|
|
1417
|
+
if all_inputs_stats["all_inputs_complete"]:
|
|
1418
|
+
step_spec.set_parameter("all_inputs_complete", True)
|
|
1419
|
+
step_handler.on_all_inputs_done(step_spec)
|
|
1420
|
+
# Check the step status
|
|
1421
|
+
check_result = step_handler.check_target(step_spec)
|
|
1422
|
+
if not check_result.success or check_result.step_status is None:
|
|
1423
|
+
process_result.message = f"Failed to check step; {check_result.message}"
|
|
1424
|
+
tmp_log.error(f"{process_result.message}")
|
|
1425
|
+
return process_result
|
|
1426
|
+
# Update step status
|
|
1427
|
+
if check_result.step_status in WFStepStatus.after_running_statuses:
|
|
1428
|
+
# Step status advanced
|
|
1429
|
+
step_spec.status = check_result.step_status
|
|
1430
|
+
process_result.new_status = step_spec.status
|
|
1431
|
+
elif check_result.step_status == WFStepStatus.running:
|
|
1432
|
+
# Still in running, do nothing
|
|
1433
|
+
pass
|
|
1434
|
+
else:
|
|
1435
|
+
tmp_log.warning(f"Invalid step_status {check_result.step_status} from target check result; skipped")
|
|
1436
|
+
now_time = naive_utcnow()
|
|
1437
|
+
step_spec.check_time = now_time
|
|
1438
|
+
if step_spec.status in WFStepStatus.after_starting_uninterrupted_statuses and step_spec.start_time is None:
|
|
1439
|
+
# step has run, set start_time if not yet set
|
|
1440
|
+
step_spec.start_time = now_time
|
|
1441
|
+
if step_spec.status in WFStepStatus.final_statuses and step_spec.start_time is not None and step_spec.end_time is None:
|
|
1442
|
+
# step has ended, set end_time if not yet set
|
|
1443
|
+
step_spec.end_time = now_time
|
|
1444
|
+
self.tbif.update_workflow_step(step_spec)
|
|
1445
|
+
process_result.success = True
|
|
1446
|
+
tmp_log.info(f"Checked step, flavor={step_spec.flavor}, target_id={step_spec.target_id}, status={step_spec.status}")
|
|
1447
|
+
except Exception as e:
|
|
1448
|
+
process_result.message = f"Got error {str(e)}"
|
|
1449
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1450
|
+
return process_result
|
|
1451
|
+
|
|
1452
|
+
def process_step(
|
|
1453
|
+
self, step_spec: WFStepSpec, data_spec_map: Dict[str, WFDataSpec] | None = None, by: str = "dog"
|
|
1454
|
+
) -> tuple[WFStepProcessResult | None, WFStepSpec]:
|
|
1455
|
+
"""
|
|
1456
|
+
Process a single workflow step
|
|
1457
|
+
|
|
1458
|
+
Args:
|
|
1459
|
+
step_spec (WFStepSpec): The workflow step specification to process
|
|
1460
|
+
data_spec_map (Dict[str, WFDataSpec] | None): Optional map of data name to WFDataSpec for the workflow
|
|
1461
|
+
by (str): The entity processing the step, e.g., "dog" or "user"
|
|
1462
|
+
|
|
1463
|
+
Returns:
|
|
1464
|
+
WFStepProcessResult | None: The result of processing the step, or None if the step was skipped
|
|
1465
|
+
WFStepSpec: The updated workflow step specification
|
|
1466
|
+
"""
|
|
1467
|
+
tmp_log = LogWrapper(logger, f"process_step <step_id={step_spec.step_id}> workflow_id={step_spec.workflow_id} member_id={step_spec.member_id} by={by}")
|
|
1468
|
+
# tmp_log.debug("Start")
|
|
1469
|
+
tmp_res = None
|
|
1470
|
+
with self.workflow_step_lock(step_spec.step_id) as locked_step_spec:
|
|
1471
|
+
if locked_step_spec is None:
|
|
1472
|
+
tmp_log.warning(f"Failed to acquire lock for step_id={step_spec.step_id}; skipped")
|
|
1473
|
+
return None, step_spec
|
|
1474
|
+
step_spec = locked_step_spec
|
|
1475
|
+
orig_status = step_spec.status
|
|
1476
|
+
# Process the step
|
|
1477
|
+
if step_spec.status == WFStepStatus.registered:
|
|
1478
|
+
tmp_res = self.process_step_registered(step_spec)
|
|
1479
|
+
elif step_spec.status == WFStepStatus.checking:
|
|
1480
|
+
tmp_res = self.process_step_checking(step_spec)
|
|
1481
|
+
elif step_spec.status in WFStepStatus.checked_statuses:
|
|
1482
|
+
tmp_res = self.process_step_checked(step_spec)
|
|
1483
|
+
elif step_spec.status == WFStepStatus.pending:
|
|
1484
|
+
tmp_res = self.process_step_pending(step_spec, data_spec_map=data_spec_map)
|
|
1485
|
+
elif step_spec.status == WFStepStatus.ready:
|
|
1486
|
+
tmp_res = self.process_step_ready(step_spec)
|
|
1487
|
+
elif step_spec.status == WFStepStatus.starting:
|
|
1488
|
+
tmp_res = self.process_step_starting(step_spec)
|
|
1489
|
+
elif step_spec.status == WFStepStatus.running:
|
|
1490
|
+
tmp_res = self.process_step_running(step_spec)
|
|
1491
|
+
elif step_spec.status in WFStepStatus.final_statuses:
|
|
1492
|
+
# dummy result since final steps need no processing
|
|
1493
|
+
dummy_process_result = WFStepProcessResult()
|
|
1494
|
+
dummy_process_result.success = True
|
|
1495
|
+
tmp_res = dummy_process_result
|
|
1496
|
+
tmp_log.debug(f"Step in final status {step_spec.status} ; skipped")
|
|
1497
|
+
else:
|
|
1498
|
+
tmp_log.debug(f"Step status {step_spec.status} is not handled in this context; skipped")
|
|
1499
|
+
# For changes into transient status, send message to trigger processing immediately
|
|
1500
|
+
if step_spec.status != orig_status and step_spec.status in WFStepStatus.transient_statuses:
|
|
1501
|
+
self.send_step_message(step_spec.step_id)
|
|
1502
|
+
return tmp_res, step_spec
|
|
1503
|
+
|
|
1504
|
+
def process_steps(self, step_specs: List[WFStepSpec], data_spec_map: Dict[str, WFDataSpec] | None = None, by: str = "dog") -> Dict:
|
|
1505
|
+
"""
|
|
1506
|
+
Process a list of workflow steps
|
|
1507
|
+
|
|
1508
|
+
Args:
|
|
1509
|
+
step_specs (List[WFStepSpec]): List of workflow step specifications to process
|
|
1510
|
+
data_spec_map (Dict[str, WFDataSpec] | None): Optional map of data name to WFDataSpec for the workflow
|
|
1511
|
+
by (str): The entity processing the steps, e.g., "dog" or "user"
|
|
1512
|
+
|
|
1513
|
+
Returns:
|
|
1514
|
+
Dict: Statistics of the processing results
|
|
1515
|
+
"""
|
|
1516
|
+
tmp_log = LogWrapper(logger, f"process_steps <workflow_id={step_specs[0].workflow_id}> by={by}")
|
|
1517
|
+
n_steps = len(step_specs)
|
|
1518
|
+
tmp_log.debug(f"Start, processing {n_steps} steps")
|
|
1519
|
+
steps_status_stats = {"n_steps": n_steps, "changed": {}, "unchanged": {}, "processed": {}, "n_processed": 0}
|
|
1520
|
+
for step_spec in step_specs:
|
|
1521
|
+
orig_status = step_spec.status
|
|
1522
|
+
tmp_res, step_spec = self.process_step(step_spec, data_spec_map=data_spec_map, by=by)
|
|
1523
|
+
if tmp_res and tmp_res.success:
|
|
1524
|
+
# update stats
|
|
1525
|
+
if tmp_res.new_status and step_spec.status != orig_status:
|
|
1526
|
+
steps_status_stats["changed"].setdefault(step_spec.status, 0)
|
|
1527
|
+
steps_status_stats["changed"][step_spec.status] += 1
|
|
1528
|
+
else:
|
|
1529
|
+
steps_status_stats["unchanged"].setdefault(step_spec.status, 0)
|
|
1530
|
+
steps_status_stats["unchanged"][step_spec.status] += 1
|
|
1531
|
+
steps_status_stats["processed"].setdefault(step_spec.status, 0)
|
|
1532
|
+
steps_status_stats["processed"][step_spec.status] += 1
|
|
1533
|
+
steps_status_stats["n_processed"] += 1
|
|
1534
|
+
tmp_log.info(
|
|
1535
|
+
f"Done, processed {steps_status_stats['n_processed']}/{n_steps} steps, unchanged: {steps_status_stats['unchanged']}, changed: {steps_status_stats['changed']}"
|
|
1536
|
+
)
|
|
1537
|
+
return steps_status_stats
|
|
1538
|
+
|
|
1539
|
+
# ---- Workflow status transitions -------------------------
|
|
1540
|
+
|
|
1541
|
+
def process_workflow_registered(self, workflow_spec: WorkflowSpec) -> WorkflowProcessResult:
|
|
1542
|
+
"""
|
|
1543
|
+
Process a workflow in registered status
|
|
1544
|
+
To parse to get workflow definition from raw request
|
|
1545
|
+
|
|
1546
|
+
Args:
|
|
1547
|
+
workflow_spec (WorkflowSpec): The workflow specification to process
|
|
1548
|
+
|
|
1549
|
+
Returns:
|
|
1550
|
+
WorkflowProcessResult: The result of processing the workflow
|
|
1551
|
+
"""
|
|
1552
|
+
tmp_log = LogWrapper(logger, f"process_workflow_registered <workflow_id={workflow_spec.workflow_id}>")
|
|
1553
|
+
# tmp_log.debug("Start")
|
|
1554
|
+
# Initialize
|
|
1555
|
+
process_result = WorkflowProcessResult()
|
|
1556
|
+
# Check status
|
|
1557
|
+
if workflow_spec.status != WorkflowStatus.registered:
|
|
1558
|
+
process_result.message = f"Workflow status changed unexpectedly from {WorkflowStatus.registered} to {workflow_spec.status}; skipped"
|
|
1559
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1560
|
+
return process_result
|
|
1561
|
+
# Process
|
|
1562
|
+
try:
|
|
1563
|
+
if workflow_spec.definition_json is not None:
|
|
1564
|
+
# Already has definition, skip parsing
|
|
1565
|
+
tmp_log.debug(f"Workflow already has definition; skipped parsing")
|
|
1566
|
+
else:
|
|
1567
|
+
# Parse the workflow definition from raw request
|
|
1568
|
+
raw_request_dict = workflow_spec.raw_request_json_map
|
|
1569
|
+
sandbox_url = os.path.join(raw_request_dict["sourceURL"], "cache", raw_request_dict["sandbox"])
|
|
1570
|
+
log_token = f'< user="{workflow_spec.username}" outDS={raw_request_dict["outDS"]}>'
|
|
1571
|
+
is_ok, is_fatal, workflow_definition = parse_raw_request(
|
|
1572
|
+
sandbox_url=sandbox_url,
|
|
1573
|
+
log_token=log_token,
|
|
1574
|
+
user_name=workflow_spec.username,
|
|
1575
|
+
raw_request_dict=raw_request_dict,
|
|
1576
|
+
)
|
|
1577
|
+
# Failure handling
|
|
1578
|
+
# if is_fatal:
|
|
1579
|
+
if False: # disable fatal for now
|
|
1580
|
+
process_result.message = f"Fatal error in parsing raw request; cancelled the workflow"
|
|
1581
|
+
tmp_log.error(f"{process_result.message}")
|
|
1582
|
+
workflow_spec.status = WorkflowStatus.cancelled
|
|
1583
|
+
workflow_spec.set_parameter("cancel_reason", "Fatal error in parsing raw request")
|
|
1584
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1585
|
+
return process_result
|
|
1586
|
+
if not is_ok:
|
|
1587
|
+
process_result.message = f"Failed to parse raw request; skipped"
|
|
1588
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1589
|
+
return process_result
|
|
1590
|
+
# extra info from raw request
|
|
1591
|
+
workflow_definition["user_dn"] = raw_request_dict.get("user_dn")
|
|
1592
|
+
# Parsed successfully, update definition
|
|
1593
|
+
workflow_spec.definition_json = json.dumps(workflow_definition, default=json_serialize_default)
|
|
1594
|
+
tmp_log.debug(f"Parsed raw request into definition")
|
|
1595
|
+
# Update status to parsed
|
|
1596
|
+
# workflow_spec.status = WorkflowStatus.parsed
|
|
1597
|
+
workflow_spec.status = WorkflowStatus.checked # skip parsed for now
|
|
1598
|
+
# Update DB
|
|
1599
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1600
|
+
process_result.success = True
|
|
1601
|
+
process_result.new_status = workflow_spec.status
|
|
1602
|
+
tmp_log.info(f"Done, status={workflow_spec.status}")
|
|
1603
|
+
except Exception as e:
|
|
1604
|
+
process_result.message = f"Got error {str(e)}"
|
|
1605
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1606
|
+
return process_result
|
|
1607
|
+
|
|
1608
|
+
def process_workflow_checked(self, workflow_spec: WorkflowSpec) -> WorkflowProcessResult:
|
|
1609
|
+
"""
|
|
1610
|
+
Process a workflow in checked status
|
|
1611
|
+
Register steps, and update its status
|
|
1612
|
+
Parse raw request into workflow definition, register steps, and update its status
|
|
1613
|
+
|
|
1614
|
+
Args:
|
|
1615
|
+
workflow_spec (WorkflowSpec): The workflow specification to process
|
|
1616
|
+
|
|
1617
|
+
Returns:
|
|
1618
|
+
WorkflowProcessResult: The result of processing the workflow
|
|
1619
|
+
"""
|
|
1620
|
+
tmp_log = LogWrapper(logger, f"process_workflow_checked <workflow_id={workflow_spec.workflow_id}>")
|
|
1621
|
+
# tmp_log.debug("Start")
|
|
1622
|
+
# Initialize
|
|
1623
|
+
process_result = WorkflowProcessResult()
|
|
1624
|
+
# Check status
|
|
1625
|
+
if workflow_spec.status != WorkflowStatus.checked:
|
|
1626
|
+
process_result.message = f"Workflow status changed unexpectedly from {WorkflowStatus.checked} to {workflow_spec.status}; skipped"
|
|
1627
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1628
|
+
return process_result
|
|
1629
|
+
# Process
|
|
1630
|
+
try:
|
|
1631
|
+
# Parse the workflow definition
|
|
1632
|
+
workflow_definition = workflow_spec.definition_json_map
|
|
1633
|
+
if workflow_definition is None:
|
|
1634
|
+
process_result.message = f"Workflow definition is None; cancelled the workflow"
|
|
1635
|
+
tmp_log.error(f"{process_result.message}")
|
|
1636
|
+
workflow_spec.status = WorkflowStatus.cancelled
|
|
1637
|
+
workflow_spec.set_parameter("cancel_reason", "Workflow definition is None")
|
|
1638
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1639
|
+
return process_result
|
|
1640
|
+
# initialize
|
|
1641
|
+
data_specs = []
|
|
1642
|
+
step_specs = []
|
|
1643
|
+
now_time = naive_utcnow()
|
|
1644
|
+
# Register root outputs
|
|
1645
|
+
for output_name, output_dict in workflow_definition["root_outputs"].items():
|
|
1646
|
+
data_spec = WFDataSpec()
|
|
1647
|
+
data_spec.workflow_id = workflow_spec.workflow_id
|
|
1648
|
+
data_spec.source_step_id = None # to be set when the step producing it starts
|
|
1649
|
+
data_spec.name = output_name
|
|
1650
|
+
data_spec.target_id = output_dict.get("value")
|
|
1651
|
+
data_spec.set_parameter("output_types", output_dict.get("output_types"))
|
|
1652
|
+
data_spec.status = WFDataStatus.registered
|
|
1653
|
+
data_spec.type = WFDataType.output
|
|
1654
|
+
data_spec.flavor = "panda_task" # FIXME: hardcoded flavor, should be configurable
|
|
1655
|
+
data_spec.creation_time = now_time
|
|
1656
|
+
data_specs.append(data_spec)
|
|
1657
|
+
# Register root inputs
|
|
1658
|
+
for input_name, input_target in workflow_definition["root_inputs"].items():
|
|
1659
|
+
data_spec = WFDataSpec()
|
|
1660
|
+
data_spec.workflow_id = workflow_spec.workflow_id
|
|
1661
|
+
data_spec.name = input_name
|
|
1662
|
+
data_spec.target_id = input_target
|
|
1663
|
+
data_spec.status = WFDataStatus.registered
|
|
1664
|
+
data_spec.type = WFDataType.input
|
|
1665
|
+
data_spec.flavor = "ddm_collection" # FIXME: hardcoded flavor, should be configurable
|
|
1666
|
+
data_spec.creation_time = now_time
|
|
1667
|
+
data_specs.append(data_spec)
|
|
1668
|
+
# Register steps and their intermediate outputs based on nodes in the definition
|
|
1669
|
+
for node in workflow_definition["nodes"]:
|
|
1670
|
+
# FIXME: not yet consider scatter, condition, loop, etc.
|
|
1671
|
+
if not (node.get("condition") or node.get("scatter") or node.get("loop")):
|
|
1672
|
+
step_spec = WFStepSpec()
|
|
1673
|
+
step_spec.workflow_id = workflow_spec.workflow_id
|
|
1674
|
+
step_spec.member_id = node["id"]
|
|
1675
|
+
step_spec.name = node["name"]
|
|
1676
|
+
step_spec.status = WFStepStatus.registered
|
|
1677
|
+
step_spec.type = WFStepType.ordinary
|
|
1678
|
+
step_spec.flavor = "panda_task" # FIXME: hardcoded flavor, should be configurable
|
|
1679
|
+
# step definition
|
|
1680
|
+
step_definition = copy.deepcopy(node)
|
|
1681
|
+
# propagate user name and DN from workflow to step
|
|
1682
|
+
step_definition["user_name"] = workflow_spec.username
|
|
1683
|
+
step_definition["user_dn"] = workflow_definition.get("user_dn")
|
|
1684
|
+
# resolve inputs and outputs
|
|
1685
|
+
input_data_set = set()
|
|
1686
|
+
output_data_dict = dict()
|
|
1687
|
+
for input_target in step_definition.get("inputs", {}).values():
|
|
1688
|
+
if not input_target.get("source"):
|
|
1689
|
+
continue
|
|
1690
|
+
sources = []
|
|
1691
|
+
if isinstance(input_target["source"], list):
|
|
1692
|
+
sources = copy.deepcopy(input_target["source"])
|
|
1693
|
+
else:
|
|
1694
|
+
sources = [input_target["source"]]
|
|
1695
|
+
input_data_set.update(sources)
|
|
1696
|
+
for output_name, output_value in step_definition.get("outputs", {}).items():
|
|
1697
|
+
output_data_dict[output_name] = output_value.get("value")
|
|
1698
|
+
step_definition["input_data_list"] = list(input_data_set)
|
|
1699
|
+
step_definition["output_data_list"] = list(output_data_dict.keys())
|
|
1700
|
+
step_spec.definition_json_map = step_definition
|
|
1701
|
+
step_spec.creation_time = now_time
|
|
1702
|
+
step_specs.append(step_spec)
|
|
1703
|
+
# intermediate outputs of the step
|
|
1704
|
+
for output_data_name in output_data_dict.keys():
|
|
1705
|
+
if output_data_name not in workflow_definition["root_outputs"]:
|
|
1706
|
+
data_spec = WFDataSpec()
|
|
1707
|
+
data_spec.workflow_id = workflow_spec.workflow_id
|
|
1708
|
+
data_spec.source_step_id = None # to be set when step starts
|
|
1709
|
+
data_spec.name = output_data_name
|
|
1710
|
+
data_spec.target_id = output_data_dict[output_data_name]
|
|
1711
|
+
data_spec.set_parameter("output_types", step_definition.get("output_types", []))
|
|
1712
|
+
data_spec.status = WFDataStatus.registered
|
|
1713
|
+
data_spec.type = WFDataType.mid
|
|
1714
|
+
data_spec.flavor = "panda_task" # FIXME: hardcoded flavor, should be configurable
|
|
1715
|
+
data_spec.creation_time = now_time
|
|
1716
|
+
data_specs.append(data_spec)
|
|
1717
|
+
# Update status to starting
|
|
1718
|
+
workflow_spec.status = WorkflowStatus.starting
|
|
1719
|
+
# Upsert DB
|
|
1720
|
+
self.tbif.upsert_workflow_entities(
|
|
1721
|
+
workflow_spec.workflow_id,
|
|
1722
|
+
actions_dict={"workflow": "update", "steps": "insert", "data": "insert"},
|
|
1723
|
+
workflow_spec=workflow_spec,
|
|
1724
|
+
step_specs=step_specs,
|
|
1725
|
+
data_specs=data_specs,
|
|
1726
|
+
)
|
|
1727
|
+
process_result.success = True
|
|
1728
|
+
process_result.new_status = workflow_spec.status
|
|
1729
|
+
tmp_log.info(f"Done, inserted {len(step_specs)} steps and {len(data_specs)} data, status={workflow_spec.status}")
|
|
1730
|
+
except Exception as e:
|
|
1731
|
+
process_result.message = f"Got error {str(e)}"
|
|
1732
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1733
|
+
return process_result
|
|
1734
|
+
|
|
1735
|
+
def process_workflow_starting(self, workflow_spec: WorkflowSpec) -> WorkflowProcessResult:
|
|
1736
|
+
"""
|
|
1737
|
+
Process a workflow in starting status
|
|
1738
|
+
To start the steps in the workflow
|
|
1739
|
+
|
|
1740
|
+
Args:
|
|
1741
|
+
workflow_spec (WorkflowSpec): The workflow specification to process
|
|
1742
|
+
|
|
1743
|
+
Returns:
|
|
1744
|
+
WorkflowProcessResult: The result of processing the workflow
|
|
1745
|
+
"""
|
|
1746
|
+
tmp_log = LogWrapper(logger, f"process_workflow_starting <workflow_id={workflow_spec.workflow_id}>")
|
|
1747
|
+
# tmp_log.debug("Start")
|
|
1748
|
+
# Initialize
|
|
1749
|
+
process_result = WorkflowProcessResult()
|
|
1750
|
+
# Check status
|
|
1751
|
+
if workflow_spec.status != WorkflowStatus.starting:
|
|
1752
|
+
process_result.message = f"Workflow status changed unexpectedly from {WorkflowStatus.starting} to {workflow_spec.status}; skipped"
|
|
1753
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1754
|
+
return process_result
|
|
1755
|
+
# Process
|
|
1756
|
+
try:
|
|
1757
|
+
# Process data specs first
|
|
1758
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=workflow_spec.workflow_id, status_exclusion_list=list(WFDataStatus.terminated_statuses))
|
|
1759
|
+
if data_specs:
|
|
1760
|
+
data_status_stats = self.process_datas(data_specs)
|
|
1761
|
+
# Get steps in registered status
|
|
1762
|
+
required_step_statuses = list(WFStepStatus.to_advance_step_statuses)
|
|
1763
|
+
over_advanced_step_statuses = list(WFStepStatus.after_starting_uninterrupted_statuses)
|
|
1764
|
+
step_specs = self.tbif.get_steps_of_workflow(workflow_id=workflow_spec.workflow_id, status_filter_list=required_step_statuses)
|
|
1765
|
+
over_advanced_step_specs = self.tbif.get_steps_of_workflow(workflow_id=workflow_spec.workflow_id, status_filter_list=over_advanced_step_statuses)
|
|
1766
|
+
if not step_specs:
|
|
1767
|
+
process_result.message = f"No step in required status; skipped"
|
|
1768
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1769
|
+
return process_result
|
|
1770
|
+
if over_advanced_step_specs:
|
|
1771
|
+
process_result.message = f"Some steps are not in required status; force to advance the workflow"
|
|
1772
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1773
|
+
# Advance the workflow to running directly
|
|
1774
|
+
workflow_spec.status = WorkflowStatus.running
|
|
1775
|
+
workflow_spec.start_time = naive_utcnow()
|
|
1776
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1777
|
+
process_result.success = True
|
|
1778
|
+
process_result.new_status = workflow_spec.status
|
|
1779
|
+
tmp_log.info(f"Done, forced advanced to status={workflow_spec.status}")
|
|
1780
|
+
return process_result
|
|
1781
|
+
# Get data spec map of the workflow
|
|
1782
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=workflow_spec.workflow_id)
|
|
1783
|
+
data_spec_map = {data_spec.name: data_spec for data_spec in data_specs}
|
|
1784
|
+
# Process steps
|
|
1785
|
+
steps_status_stats = self.process_steps(step_specs, data_spec_map=data_spec_map)
|
|
1786
|
+
# Update workflow status to running if any of step is starting
|
|
1787
|
+
now_time = naive_utcnow()
|
|
1788
|
+
if steps_status_stats["processed"].get(WFStepStatus.starting):
|
|
1789
|
+
workflow_spec.status = WorkflowStatus.running
|
|
1790
|
+
workflow_spec.start_time = now_time
|
|
1791
|
+
workflow_spec.check_time = now_time
|
|
1792
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1793
|
+
process_result.success = True
|
|
1794
|
+
process_result.new_status = workflow_spec.status
|
|
1795
|
+
tmp_log.info(f"Done, advanced to status={workflow_spec.status}")
|
|
1796
|
+
else:
|
|
1797
|
+
workflow_spec.check_time = now_time
|
|
1798
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1799
|
+
process_result.success = True
|
|
1800
|
+
tmp_log.info(f"Done, status remains {workflow_spec.status}")
|
|
1801
|
+
except Exception as e:
|
|
1802
|
+
process_result.message = f"Got error {str(e)}"
|
|
1803
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1804
|
+
return process_result
|
|
1805
|
+
|
|
1806
|
+
def process_workflow_running(self, workflow_spec: WorkflowSpec) -> WorkflowProcessResult:
|
|
1807
|
+
"""
|
|
1808
|
+
Process a workflow in running status
|
|
1809
|
+
To monitor the steps in the workflow
|
|
1810
|
+
|
|
1811
|
+
Args:
|
|
1812
|
+
workflow_spec (WorkflowSpec): The workflow specification to process
|
|
1813
|
+
|
|
1814
|
+
Returns:
|
|
1815
|
+
WorkflowProcessResult: The result of processing the workflow
|
|
1816
|
+
"""
|
|
1817
|
+
tmp_log = LogWrapper(logger, f"process_workflow_running <workflow_id={workflow_spec.workflow_id}>")
|
|
1818
|
+
# tmp_log.debug("Start")
|
|
1819
|
+
# Initialize
|
|
1820
|
+
process_result = WorkflowProcessResult()
|
|
1821
|
+
# Check status
|
|
1822
|
+
if workflow_spec.status != WorkflowStatus.running:
|
|
1823
|
+
process_result.message = f"Workflow status changed unexpectedly from {WorkflowStatus.running} to {workflow_spec.status}; skipped"
|
|
1824
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1825
|
+
return process_result
|
|
1826
|
+
# Process
|
|
1827
|
+
try:
|
|
1828
|
+
# Process data specs first
|
|
1829
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=workflow_spec.workflow_id, status_exclusion_list=list(WFDataStatus.terminated_statuses))
|
|
1830
|
+
if data_specs:
|
|
1831
|
+
data_status_stats = self.process_datas(data_specs)
|
|
1832
|
+
# Get steps
|
|
1833
|
+
step_specs = self.tbif.get_steps_of_workflow(workflow_id=workflow_spec.workflow_id)
|
|
1834
|
+
if not step_specs:
|
|
1835
|
+
process_result.message = f"No step in required status; skipped"
|
|
1836
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1837
|
+
return process_result
|
|
1838
|
+
# Get data spec map of the workflow
|
|
1839
|
+
data_specs = self.tbif.get_data_of_workflow(workflow_id=workflow_spec.workflow_id)
|
|
1840
|
+
data_spec_map = {data_spec.name: data_spec for data_spec in data_specs}
|
|
1841
|
+
output_data_spec_map = {data_spec.name: data_spec for data_spec in data_specs if data_spec.type == WFDataType.output}
|
|
1842
|
+
# Check if all output data are good
|
|
1843
|
+
all_outputs_good = None
|
|
1844
|
+
for output_data_name, output_data_spec in output_data_spec_map.items():
|
|
1845
|
+
if output_data_spec.status in WFDataStatus.good_output_statuses:
|
|
1846
|
+
if all_outputs_good is None:
|
|
1847
|
+
all_outputs_good = True
|
|
1848
|
+
else:
|
|
1849
|
+
all_outputs_good = False
|
|
1850
|
+
break
|
|
1851
|
+
if all_outputs_good is True:
|
|
1852
|
+
# All outputs are good, mark the workflow as done
|
|
1853
|
+
workflow_spec.status = WorkflowStatus.done
|
|
1854
|
+
workflow_spec.end_time = naive_utcnow()
|
|
1855
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1856
|
+
process_result.success = True
|
|
1857
|
+
process_result.new_status = workflow_spec.status
|
|
1858
|
+
tmp_log.info(f"Done, all output data are good; advanced to status={workflow_spec.status}")
|
|
1859
|
+
return process_result
|
|
1860
|
+
# Process each step
|
|
1861
|
+
steps_status_stats = self.process_steps(step_specs, data_spec_map=data_spec_map)
|
|
1862
|
+
# Update workflow status by steps
|
|
1863
|
+
now_time = naive_utcnow()
|
|
1864
|
+
if (processed_steps_stats := steps_status_stats["processed"]) and (
|
|
1865
|
+
processed_steps_stats.get(WFStepStatus.failed) or processed_steps_stats.get(WFStepStatus.cancelled)
|
|
1866
|
+
):
|
|
1867
|
+
# TODO: cancel all unfinished steps
|
|
1868
|
+
# self.cancel_step(...)
|
|
1869
|
+
# mark workflow as failed
|
|
1870
|
+
tmp_log.warning(f"workflow failed due to some steps failed or cancelled")
|
|
1871
|
+
workflow_spec.status = WorkflowStatus.failed
|
|
1872
|
+
workflow_spec.end_time = now_time
|
|
1873
|
+
workflow_spec.check_time = now_time
|
|
1874
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1875
|
+
process_result.success = True
|
|
1876
|
+
process_result.new_status = workflow_spec.status
|
|
1877
|
+
tmp_log.info(f"Done, advanced to status={workflow_spec.status}")
|
|
1878
|
+
else:
|
|
1879
|
+
workflow_spec.check_time = now_time
|
|
1880
|
+
self.tbif.update_workflow(workflow_spec)
|
|
1881
|
+
process_result.success = True
|
|
1882
|
+
tmp_log.info(f"Done, status remains {workflow_spec.status}")
|
|
1883
|
+
if processed_steps_stats.get(WFStepStatus.done) == len(step_specs):
|
|
1884
|
+
# all steps are done, trigger re-check to update workflow status
|
|
1885
|
+
self.send_workflow_message(workflow_spec.workflow_id)
|
|
1886
|
+
except Exception as e:
|
|
1887
|
+
process_result.message = f"Got error {str(e)}"
|
|
1888
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1889
|
+
return process_result
|
|
1890
|
+
|
|
1891
|
+
def process_workflow(self, workflow_spec: WorkflowSpec, by: str = "dog") -> tuple[WorkflowProcessResult, WorkflowSpec]:
|
|
1892
|
+
"""
|
|
1893
|
+
Process a workflow based on its current status
|
|
1894
|
+
|
|
1895
|
+
Args:
|
|
1896
|
+
workflow_spec (WorkflowSpec): The workflow specification to process
|
|
1897
|
+
by (str): The entity processing the workflow
|
|
1898
|
+
|
|
1899
|
+
Returns:
|
|
1900
|
+
WorkflowProcessResult: The result of processing the workflow
|
|
1901
|
+
WorkflowSpec: The updated workflow specification
|
|
1902
|
+
"""
|
|
1903
|
+
tmp_log = LogWrapper(logger, f"process_workflow <workflow_id={workflow_spec.workflow_id}> by={by}")
|
|
1904
|
+
tmp_log.debug(f"Start, current status={workflow_spec.status}")
|
|
1905
|
+
# Initialize
|
|
1906
|
+
process_result = WorkflowProcessResult()
|
|
1907
|
+
orig_status = workflow_spec.status
|
|
1908
|
+
# Process based on status
|
|
1909
|
+
match workflow_spec.status:
|
|
1910
|
+
case WorkflowStatus.registered:
|
|
1911
|
+
process_result = self.process_workflow_registered(workflow_spec)
|
|
1912
|
+
case WorkflowStatus.checked:
|
|
1913
|
+
process_result = self.process_workflow_checked(workflow_spec)
|
|
1914
|
+
case WorkflowStatus.starting:
|
|
1915
|
+
process_result = self.process_workflow_starting(workflow_spec)
|
|
1916
|
+
case WorkflowStatus.running:
|
|
1917
|
+
process_result = self.process_workflow_running(workflow_spec)
|
|
1918
|
+
case _:
|
|
1919
|
+
process_result.message = f"Workflow status {workflow_spec.status} is not handled in this context; skipped"
|
|
1920
|
+
tmp_log.warning(f"{process_result.message}")
|
|
1921
|
+
# For changes into transient status, send message to trigger processing immediately
|
|
1922
|
+
if workflow_spec.status != orig_status and workflow_spec.status in WorkflowStatus.transient_statuses:
|
|
1923
|
+
self.send_workflow_message(workflow_spec.workflow_id)
|
|
1924
|
+
return process_result, workflow_spec
|
|
1925
|
+
|
|
1926
|
+
# ---- Process all workflows -------------------------------------
|
|
1927
|
+
|
|
1928
|
+
def process_active_workflows(self) -> Dict:
|
|
1929
|
+
"""
|
|
1930
|
+
Process all active workflows in the system
|
|
1931
|
+
|
|
1932
|
+
Returns:
|
|
1933
|
+
Dict: Statistics of the processing results
|
|
1934
|
+
"""
|
|
1935
|
+
tmp_log = LogWrapper(logger, "process_active_workflows")
|
|
1936
|
+
# tmp_log.debug("Start")
|
|
1937
|
+
# Initialize
|
|
1938
|
+
workflows_status_stats = {"n_workflows": 0, "changed": {}, "unchanged": {}, "processed": {}, "n_processed": 0}
|
|
1939
|
+
try:
|
|
1940
|
+
# Query active workflows to process
|
|
1941
|
+
workflow_specs = self.tbif.query_workflows(status_filter_list=WorkflowStatus.active_statuses, check_interval_sec=WORKFLOW_CHECK_INTERVAL_SEC)
|
|
1942
|
+
n_workflows = len(workflow_specs)
|
|
1943
|
+
tmp_log.debug(f"Got {n_workflows} workflows to process")
|
|
1944
|
+
if n_workflows == 0:
|
|
1945
|
+
tmp_log.info("Done, no workflow to process")
|
|
1946
|
+
return workflows_status_stats
|
|
1947
|
+
# Process each workflow
|
|
1948
|
+
for workflow_spec in workflow_specs:
|
|
1949
|
+
with self.workflow_lock(workflow_spec.workflow_id) as locked_workflow_spec:
|
|
1950
|
+
if locked_workflow_spec is None:
|
|
1951
|
+
tmp_log.warning(f"Failed to acquire lock for workflow_id={workflow_spec.workflow_id}; skipped")
|
|
1952
|
+
continue
|
|
1953
|
+
workflow_spec = locked_workflow_spec
|
|
1954
|
+
orig_status = workflow_spec.status
|
|
1955
|
+
# Process the workflow
|
|
1956
|
+
tmp_res, workflow_spec = self.process_workflow(workflow_spec)
|
|
1957
|
+
if tmp_res and tmp_res.success:
|
|
1958
|
+
# update stats
|
|
1959
|
+
if tmp_res.new_status and workflow_spec.status != orig_status:
|
|
1960
|
+
workflows_status_stats["changed"].setdefault(workflow_spec.status, 0)
|
|
1961
|
+
workflows_status_stats["changed"][workflow_spec.status] += 1
|
|
1962
|
+
else:
|
|
1963
|
+
workflows_status_stats["unchanged"].setdefault(workflow_spec.status, 0)
|
|
1964
|
+
workflows_status_stats["unchanged"][workflow_spec.status] += 1
|
|
1965
|
+
workflows_status_stats["processed"].setdefault(workflow_spec.status, 0)
|
|
1966
|
+
workflows_status_stats["processed"][workflow_spec.status] += 1
|
|
1967
|
+
workflows_status_stats["n_processed"] += 1
|
|
1968
|
+
workflows_status_stats["n_workflows"] = n_workflows
|
|
1969
|
+
tmp_log.info(
|
|
1970
|
+
f"Done, processed {workflows_status_stats['n_processed']}/{n_workflows} workflows, unchanged: {workflows_status_stats['unchanged']}, changed: {workflows_status_stats['changed']}"
|
|
1971
|
+
)
|
|
1972
|
+
except Exception as e:
|
|
1973
|
+
tmp_log.error(f"Got error ; {traceback.format_exc()}")
|
|
1974
|
+
return workflows_status_stats
|