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.
Files changed (375) hide show
  1. panda_server_source-0.8.0.dist-info/METADATA +55 -0
  2. panda_server_source-0.8.0.dist-info/RECORD +375 -0
  3. panda_server_source-0.8.0.dist-info/WHEEL +4 -0
  4. panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
  5. pandajedi/__init__.py +0 -0
  6. pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
  7. pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
  8. pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
  9. pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
  10. pandajedi/jedibrokerage/GenJobBroker.py +381 -0
  11. pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
  12. pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
  13. pandajedi/jedibrokerage/__init__.py +0 -0
  14. pandajedi/jediconfig/__init__.py +0 -0
  15. pandajedi/jediconfig/jedi_config.py +40 -0
  16. pandajedi/jedicore/FactoryBase.py +182 -0
  17. pandajedi/jedicore/Interaction.py +433 -0
  18. pandajedi/jedicore/JediDBProxy.py +56 -0
  19. pandajedi/jedicore/JediDBProxyPool.py +37 -0
  20. pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
  21. pandajedi/jedicore/JediException.py +13 -0
  22. pandajedi/jedicore/JediTaskBuffer.py +984 -0
  23. pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
  24. pandajedi/jedicore/MsgWrapper.py +124 -0
  25. pandajedi/jedicore/ProcessUtils.py +34 -0
  26. pandajedi/jedicore/SiteCandidate.py +90 -0
  27. pandajedi/jedicore/ThreadUtils.py +205 -0
  28. pandajedi/jedicore/__init__.py +0 -0
  29. pandajedi/jediddm/AtlasDDMClient.py +1708 -0
  30. pandajedi/jediddm/DDMClientBase.py +16 -0
  31. pandajedi/jediddm/DDMInterface.py +83 -0
  32. pandajedi/jediddm/GenDDMClient.py +17 -0
  33. pandajedi/jediddm/__init__.py +0 -0
  34. pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
  35. pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
  36. pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
  37. pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
  38. pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
  39. pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
  40. pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
  41. pandajedi/jedidog/GenWatchDog.py +21 -0
  42. pandajedi/jedidog/JumboWatchDog.py +147 -0
  43. pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
  44. pandajedi/jedidog/WatchDogBase.py +53 -0
  45. pandajedi/jedidog/__init__.py +0 -0
  46. pandajedi/jediexec/reniceJEDI.py +25 -0
  47. pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
  48. pandajedi/jedigen/TaskGeneratorBase.py +15 -0
  49. pandajedi/jedigen/__init__.py +0 -0
  50. pandajedi/jedimsgprocessor/__init__.py +0 -0
  51. pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
  52. pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
  53. pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
  54. pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
  55. pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
  56. pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
  57. pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
  58. pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
  59. pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
  60. pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
  61. pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
  62. pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
  63. pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
  64. pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
  65. pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
  66. pandajedi/jediorder/ContentsFeeder.py +785 -0
  67. pandajedi/jediorder/JediDaemon.py +27 -0
  68. pandajedi/jediorder/JediKnight.py +55 -0
  69. pandajedi/jediorder/JediMaster.py +238 -0
  70. pandajedi/jediorder/JediMsgProcessor.py +32 -0
  71. pandajedi/jediorder/JobBroker.py +33 -0
  72. pandajedi/jediorder/JobGenerator.py +2763 -0
  73. pandajedi/jediorder/JobSplitter.py +308 -0
  74. pandajedi/jediorder/JobThrottler.py +28 -0
  75. pandajedi/jediorder/PostProcessor.py +166 -0
  76. pandajedi/jediorder/TaskBroker.py +268 -0
  77. pandajedi/jediorder/TaskCommando.py +334 -0
  78. pandajedi/jediorder/TaskGenerator.py +17 -0
  79. pandajedi/jediorder/TaskRefiner.py +611 -0
  80. pandajedi/jediorder/TaskSetupper.py +17 -0
  81. pandajedi/jediorder/WatchDog.py +79 -0
  82. pandajedi/jediorder/__init__.py +0 -0
  83. pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
  84. pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
  85. pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
  86. pandajedi/jedipprocess/GenPostProcessor.py +35 -0
  87. pandajedi/jedipprocess/MailTemplates.py +185 -0
  88. pandajedi/jedipprocess/PostProcessorBase.py +269 -0
  89. pandajedi/jedipprocess/__init__.py +0 -0
  90. pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
  91. pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
  92. pandajedi/jedirefine/GenTaskRefiner.py +61 -0
  93. pandajedi/jedirefine/RefinerUtils.py +154 -0
  94. pandajedi/jedirefine/TaskRefinerBase.py +996 -0
  95. pandajedi/jedirefine/__init__.py +0 -0
  96. pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
  97. pandajedi/jedisetup/GenTaskSetupper.py +16 -0
  98. pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
  99. pandajedi/jedisetup/TaskSetupperBase.py +15 -0
  100. pandajedi/jedisetup/__init__.py +0 -0
  101. pandajedi/jeditest/SchemaChecker.py +41 -0
  102. pandajedi/jeditest/addContWoSoTask.py +54 -0
  103. pandajedi/jeditest/addDataCarouselRequest.py +47 -0
  104. pandajedi/jeditest/addEventServiceTask.py +101 -0
  105. pandajedi/jeditest/addHpoTask.py +81 -0
  106. pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
  107. pandajedi/jeditest/addNonAtlasTask.py +71 -0
  108. pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
  109. pandajedi/jeditest/addTestTask.py +124 -0
  110. pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
  111. pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
  112. pandajedi/jeditest/brokerTest.py +78 -0
  113. pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
  114. pandajedi/jeditest/contentsFeederTest.py +28 -0
  115. pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
  116. pandajedi/jeditest/ddmTest.py +4 -0
  117. pandajedi/jeditest/jobGenForOneTask.py +38 -0
  118. pandajedi/jeditest/jobGeneratorTest.py +43 -0
  119. pandajedi/jeditest/jobSplitterTest.py +104 -0
  120. pandajedi/jeditest/jobThrottlerTest.py +22 -0
  121. pandajedi/jeditest/msgProcessorTest.py +9 -0
  122. pandajedi/jeditest/postProcessorTest.py +18 -0
  123. pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
  124. pandajedi/jeditest/resubmitTaskParam.py +20 -0
  125. pandajedi/jeditest/taskBrokerTest.py +22 -0
  126. pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
  127. pandajedi/jeditest/taskBufferTest.py +5 -0
  128. pandajedi/jeditest/taskCommandoTest.py +28 -0
  129. pandajedi/jeditest/taskRefinerTest.py +30 -0
  130. pandajedi/jeditest/testOneTaskToRefine.py +49 -0
  131. pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
  132. pandajedi/jeditest/testPandaDDMMap.py +62 -0
  133. pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
  134. pandajedi/jeditest/watchDogTest.py +25 -0
  135. pandajedi/jeditest/workQueueTest.py +22 -0
  136. pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
  137. pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
  138. pandajedi/jedithrottle/GenJobThrottler.py +26 -0
  139. pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
  140. pandajedi/jedithrottle/__init__.py +0 -0
  141. pandaserver/__init__.py +0 -0
  142. pandaserver/api/__init__.py +0 -0
  143. pandaserver/api/v1/__init__.py +0 -0
  144. pandaserver/api/v1/common.py +339 -0
  145. pandaserver/api/v1/credential_management_api.py +372 -0
  146. pandaserver/api/v1/data_carousel_api.py +264 -0
  147. pandaserver/api/v1/event_api.py +269 -0
  148. pandaserver/api/v1/file_server_api.py +942 -0
  149. pandaserver/api/v1/harvester_api.py +445 -0
  150. pandaserver/api/v1/http_client.py +166 -0
  151. pandaserver/api/v1/idds_api.py +180 -0
  152. pandaserver/api/v1/job_api.py +463 -0
  153. pandaserver/api/v1/metaconfig_api.py +122 -0
  154. pandaserver/api/v1/pilot_api.py +895 -0
  155. pandaserver/api/v1/statistics_api.py +207 -0
  156. pandaserver/api/v1/system_api.py +186 -0
  157. pandaserver/api/v1/task_api.py +1520 -0
  158. pandaserver/api/v1/tests/__init__.py +0 -0
  159. pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
  160. pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
  161. pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
  162. pandaserver/api/v1/tests/idds_api_tests.py +45 -0
  163. pandaserver/api/v1/tests/job_api_tests.py +112 -0
  164. pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
  165. pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
  166. pandaserver/api/v1/tests/system_api_tests.py +58 -0
  167. pandaserver/api/v1/tests/task_api_tests.py +590 -0
  168. pandaserver/api/v1/version.py +2 -0
  169. pandaserver/api/v1/workflow_api.py +136 -0
  170. pandaserver/brokerage/SiteMapper.py +406 -0
  171. pandaserver/brokerage/__init__.py +0 -0
  172. pandaserver/brokerage/broker.py +272 -0
  173. pandaserver/config/__init__.py +0 -0
  174. pandaserver/config/config_utils.py +15 -0
  175. pandaserver/config/daemon_config.py +29 -0
  176. pandaserver/config/mcp_config.py +38 -0
  177. pandaserver/config/panda_config.py +233 -0
  178. pandaserver/configurator/Carbon.py +65 -0
  179. pandaserver/configurator/Configurator.py +908 -0
  180. pandaserver/configurator/__init__.py +0 -0
  181. pandaserver/configurator/aux.py +73 -0
  182. pandaserver/daemons/__init__.py +0 -0
  183. pandaserver/daemons/master.py +110 -0
  184. pandaserver/daemons/master_systemd.py +79 -0
  185. pandaserver/daemons/scripts/__init__.py +0 -0
  186. pandaserver/daemons/scripts/add_main.py +264 -0
  187. pandaserver/daemons/scripts/add_sub.py +221 -0
  188. pandaserver/daemons/scripts/cache_pilots.py +47 -0
  189. pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
  190. pandaserver/daemons/scripts/carbon.py +50 -0
  191. pandaserver/daemons/scripts/configurator.py +94 -0
  192. pandaserver/daemons/scripts/copyArchive.py +1387 -0
  193. pandaserver/daemons/scripts/datasetManager.py +1154 -0
  194. pandaserver/daemons/scripts/dummy_test.py +25 -0
  195. pandaserver/daemons/scripts/evpPD2P.py +144 -0
  196. pandaserver/daemons/scripts/hs_scrapers.py +264 -0
  197. pandaserver/daemons/scripts/metric_collector.py +927 -0
  198. pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
  199. pandaserver/daemons/scripts/pilotStreaming.py +109 -0
  200. pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
  201. pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
  202. pandaserver/daemons/scripts/task_evaluator.py +405 -0
  203. pandaserver/daemons/scripts/tmpwatch.py +70 -0
  204. pandaserver/daemons/scripts/worker_synchronization.py +97 -0
  205. pandaserver/daemons/utils.py +753 -0
  206. pandaserver/dataservice/DataServiceUtils.py +323 -0
  207. pandaserver/dataservice/ErrorCode.py +25 -0
  208. pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
  209. pandaserver/dataservice/__init__.py +0 -0
  210. pandaserver/dataservice/activator.py +79 -0
  211. pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
  212. pandaserver/dataservice/adder_dummy_plugin.py +31 -0
  213. pandaserver/dataservice/adder_gen.py +954 -0
  214. pandaserver/dataservice/adder_plugin_base.py +27 -0
  215. pandaserver/dataservice/adder_result.py +75 -0
  216. pandaserver/dataservice/adder_simple_plugin.py +160 -0
  217. pandaserver/dataservice/closer.py +247 -0
  218. pandaserver/dataservice/closer_atlas_plugin.py +90 -0
  219. pandaserver/dataservice/ddm.py +1381 -0
  220. pandaserver/dataservice/ddm_handler.py +95 -0
  221. pandaserver/dataservice/dyn_data_distributer.py +824 -0
  222. pandaserver/dataservice/event_lookup_client_ei.py +77 -0
  223. pandaserver/dataservice/event_picker.py +268 -0
  224. pandaserver/dataservice/finisher.py +194 -0
  225. pandaserver/dataservice/setupper.py +225 -0
  226. pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
  227. pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
  228. pandaserver/dataservice/setupper_plugin_base.py +67 -0
  229. pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
  230. pandaserver/jobdispatcher/ErrorCode.py +19 -0
  231. pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
  232. pandaserver/jobdispatcher/Protocol.py +443 -0
  233. pandaserver/jobdispatcher/Watcher.py +186 -0
  234. pandaserver/jobdispatcher/__init__.py +0 -0
  235. pandaserver/pandamcp/__init__.py +0 -0
  236. pandaserver/pandamcp/mcp_main.py +56 -0
  237. pandaserver/pandamcp/mcp_test_client.py +95 -0
  238. pandaserver/pandamcp/mcp_utils.py +74 -0
  239. pandaserver/proxycache/__init__.py +0 -0
  240. pandaserver/proxycache/panda_proxy_cache.py +240 -0
  241. pandaserver/proxycache/token_cache.py +141 -0
  242. pandaserver/server/.gacl +6 -0
  243. pandaserver/server/panda.py +539 -0
  244. pandaserver/srvcore/CoreUtils.py +572 -0
  245. pandaserver/srvcore/MailUtils.py +106 -0
  246. pandaserver/srvcore/__init__.py +0 -0
  247. pandaserver/srvcore/allowed_methods.py +100 -0
  248. pandaserver/srvcore/oidc_utils.py +152 -0
  249. pandaserver/srvcore/panda_request.py +196 -0
  250. pandaserver/srvcore/srv_msg_utils.py +42 -0
  251. pandaserver/taskbuffer/ConBridge.py +513 -0
  252. pandaserver/taskbuffer/DBProxyPool.py +95 -0
  253. pandaserver/taskbuffer/DataCarousel.py +3053 -0
  254. pandaserver/taskbuffer/DatasetSpec.py +137 -0
  255. pandaserver/taskbuffer/DdmSpec.py +201 -0
  256. pandaserver/taskbuffer/ErrorCode.py +87 -0
  257. pandaserver/taskbuffer/EventServiceUtils.py +396 -0
  258. pandaserver/taskbuffer/FileSpec.py +290 -0
  259. pandaserver/taskbuffer/GlobalShares.py +244 -0
  260. pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
  261. pandaserver/taskbuffer/Initializer.py +82 -0
  262. pandaserver/taskbuffer/InputChunk.py +1080 -0
  263. pandaserver/taskbuffer/JediCacheSpec.py +57 -0
  264. pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
  265. pandaserver/taskbuffer/JediFileSpec.py +242 -0
  266. pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
  267. pandaserver/taskbuffer/JobSpec.py +1006 -0
  268. pandaserver/taskbuffer/JobUtils.py +232 -0
  269. pandaserver/taskbuffer/NucleusSpec.py +116 -0
  270. pandaserver/taskbuffer/OraDBProxy.py +174 -0
  271. pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
  272. pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
  273. pandaserver/taskbuffer/ParseJobXML.py +350 -0
  274. pandaserver/taskbuffer/PickleFileSpec.py +18 -0
  275. pandaserver/taskbuffer/PickleJobSpec.py +25 -0
  276. pandaserver/taskbuffer/PrioUtil.py +40 -0
  277. pandaserver/taskbuffer/ProcessGroups.py +111 -0
  278. pandaserver/taskbuffer/ResourceSpec.py +175 -0
  279. pandaserver/taskbuffer/SQLDumper.py +37 -0
  280. pandaserver/taskbuffer/SQLManager.py +60 -0
  281. pandaserver/taskbuffer/SiteSpec.py +304 -0
  282. pandaserver/taskbuffer/SupErrors.py +10 -0
  283. pandaserver/taskbuffer/TaskBuffer.py +2882 -0
  284. pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
  285. pandaserver/taskbuffer/Utils.py +848 -0
  286. pandaserver/taskbuffer/WorkQueue.py +227 -0
  287. pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
  288. pandaserver/taskbuffer/WorkerSpec.py +143 -0
  289. pandaserver/taskbuffer/WrappedCursor.py +487 -0
  290. pandaserver/taskbuffer/WrappedPickle.py +88 -0
  291. pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
  292. pandaserver/taskbuffer/__init__.py +0 -0
  293. pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
  294. pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
  295. pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
  296. pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
  297. pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
  298. pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
  299. pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
  300. pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
  301. pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
  302. pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
  303. pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
  304. pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
  305. pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
  306. pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
  307. pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
  308. pandaserver/taskbuffer/retryModule.py +552 -0
  309. pandaserver/taskbuffer/task_split_rules.py +308 -0
  310. pandaserver/taskbuffer/workflow_processor.py +249 -0
  311. pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
  312. pandaserver/test/SchemaChecker.py +40 -0
  313. pandaserver/test/__init__.py +0 -0
  314. pandaserver/test/banUser.py +43 -0
  315. pandaserver/test/boostPrio.py +19 -0
  316. pandaserver/test/boostUser.py +35 -0
  317. pandaserver/test/callbackDDM.py +11 -0
  318. pandaserver/test/daod_on_demand.py +95 -0
  319. pandaserver/test/finishJob.py +184 -0
  320. pandaserver/test/finishTaskJEDI.py +11 -0
  321. pandaserver/test/getJobs.py +53 -0
  322. pandaserver/test/killJob.py +79 -0
  323. pandaserver/test/killJobLowPrio.py +99 -0
  324. pandaserver/test/killJobsInTask.py +56 -0
  325. pandaserver/test/killTask.py +80 -0
  326. pandaserver/test/killTaskJEDI.py +11 -0
  327. pandaserver/test/killUser.py +83 -0
  328. pandaserver/test/lsst/README.txt +7 -0
  329. pandaserver/test/lsst/lsstSubmit.py +139 -0
  330. pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
  331. pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
  332. pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
  333. pandaserver/test/reassignJobs.py +17 -0
  334. pandaserver/test/reassignSite.py +82 -0
  335. pandaserver/test/reassignTask.py +117 -0
  336. pandaserver/test/reassignWaiting.py +39 -0
  337. pandaserver/test/recoverLostFiles.py +7 -0
  338. pandaserver/test/reloadInputDS.py +51 -0
  339. pandaserver/test/sendCommandToJob.py +26 -0
  340. pandaserver/test/setDebugMode.py +38 -0
  341. pandaserver/test/setPriority.py +28 -0
  342. pandaserver/test/testEvgen.py +60 -0
  343. pandaserver/test/testEvgen17.py +58 -0
  344. pandaserver/test/testG4sim.py +81 -0
  345. pandaserver/test/testG4sim17.py +90 -0
  346. pandaserver/test/testGlobalShares.py +131 -0
  347. pandaserver/test/testReco.py +103 -0
  348. pandaserver/test/testSimulReco14.py +98 -0
  349. pandaserver/test/testSiteMap.py +26 -0
  350. pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
  351. pandaserver/userinterface/Client.py +1686 -0
  352. pandaserver/userinterface/UserIF.py +1799 -0
  353. pandaserver/userinterface/__init__.py +0 -0
  354. pandaserver/workflow/__init__.py +0 -0
  355. pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
  356. pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
  357. pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
  358. pandaserver/workflow/pcwl_test.py +122 -0
  359. pandaserver/workflow/pcwl_utils.py +384 -0
  360. pandaserver/workflow/psnakemake_container.json +68 -0
  361. pandaserver/workflow/psnakemake_task.json +62 -0
  362. pandaserver/workflow/psnakemake_test.py +60 -0
  363. pandaserver/workflow/snakeparser/__init__.py +3 -0
  364. pandaserver/workflow/snakeparser/extensions.py +74 -0
  365. pandaserver/workflow/snakeparser/log.py +53 -0
  366. pandaserver/workflow/snakeparser/names.py +3 -0
  367. pandaserver/workflow/snakeparser/parser.py +332 -0
  368. pandaserver/workflow/snakeparser/utils.py +29 -0
  369. pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
  370. pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
  371. pandaserver/workflow/workflow_base.py +531 -0
  372. pandaserver/workflow/workflow_core.py +1974 -0
  373. pandaserver/workflow/workflow_core_smoketest.py +388 -0
  374. pandaserver/workflow/workflow_parser.py +222 -0
  375. 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