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,954 @@
1
+ """
2
+ General Adder plugin. Add data to dataset
3
+
4
+ """
5
+
6
+ import datetime
7
+ import json
8
+ import re
9
+ import sys
10
+ import time
11
+ import traceback
12
+
13
+ from pandacommon.pandalogger.LogWrapper import LogWrapper
14
+ from pandacommon.pandalogger.PandaLogger import PandaLogger
15
+ from pandacommon.pandautils.PandaUtils import naive_utcnow
16
+
17
+ import pandaserver.dataservice.ErrorCode
18
+ import pandaserver.taskbuffer.ErrorCode
19
+ from pandaserver.config import panda_config
20
+ from pandaserver.dataservice import closer
21
+ from pandaserver.srvcore.CoreUtils import normalize_cpu_model
22
+ from pandaserver.taskbuffer import EventServiceUtils, JobUtils, retryModule
23
+
24
+ _logger = PandaLogger().getLogger("adder")
25
+
26
+ panda_config.setupPlugin()
27
+
28
+
29
+ class AdderGen:
30
+ """
31
+ General Adder plugin.
32
+ """
33
+
34
+ # constructor
35
+ def __init__(
36
+ self,
37
+ taskBuffer,
38
+ job_id,
39
+ job_status,
40
+ attempt_nr,
41
+ ignore_tmp_error=True,
42
+ siteMapper=None,
43
+ pid=None,
44
+ prelock_pid=None,
45
+ lock_offset=10,
46
+ lock_pool=None,
47
+ ) -> None:
48
+ """
49
+ Initialize the AdderGen.
50
+
51
+ :param job: The job object.
52
+ :param params: Additional parameters.
53
+ """
54
+ self.job = None
55
+ self.job_id = job_id
56
+ self.job_status = job_status
57
+ self.taskBuffer = taskBuffer
58
+ self.ignore_tmp_error = ignore_tmp_error
59
+ self.lock_offset = lock_offset
60
+ self.siteMapper = siteMapper
61
+ self.dataset_map = {}
62
+ self.extra_info = {
63
+ "surl": {},
64
+ "nevents": {},
65
+ "lbnr": {},
66
+ "endpoint": {},
67
+ "guid": {},
68
+ }
69
+ self.attempt_nr = attempt_nr
70
+ self.pid = pid
71
+ self.prelock_pid = prelock_pid
72
+ self.data = None
73
+ self.lock_pool = lock_pool
74
+ self.report_dict = None
75
+ self.adder_plugin = None
76
+ self.add_result = None
77
+ self.adder_plugin_class = None
78
+ # logger
79
+ self.logger = LogWrapper(_logger, str(self.job_id))
80
+
81
+ # main
82
+ def run(self):
83
+ """
84
+ Run the AdderGen plugin.
85
+ """
86
+ try:
87
+ start_time = naive_utcnow()
88
+ self.logger.debug(f"new start: {self.job_status} attemptNr={self.attempt_nr}")
89
+
90
+ # got lock, get the report
91
+ self.get_report()
92
+
93
+ # query job
94
+ self.job = self.taskBuffer.peekJobs([self.job_id], fromDefined=False, fromWaiting=False, fromArchived=False, forAnal=True)[0]
95
+
96
+ # execute plugin to process job report and update the job
97
+ processed = self.process_job_report()
98
+ self.logger.debug(f"job report was successfully processed and can be deleted now: {processed}")
99
+
100
+ duration = naive_utcnow() - start_time
101
+ self.logger.debug("end: took %s.%03d sec in total" % (duration.seconds, duration.microseconds / 1000))
102
+
103
+ # remove Catalog
104
+ if processed:
105
+ self.taskBuffer.deleteJobOutputReport(panda_id=self.job_id, attempt_nr=self.attempt_nr)
106
+
107
+ del self.data
108
+ del self.report_dict
109
+ except Exception as e:
110
+ err_str = f"{str(e)} {traceback.format_exc()}"
111
+ self.logger.error(err_str)
112
+ duration = naive_utcnow() - start_time
113
+ self.logger.error("except: took %s.%03d sec in total" % (duration.seconds, duration.microseconds / 1000))
114
+ # unlock job output report
115
+ self.taskBuffer.unlockJobOutputReport(
116
+ panda_id=self.job_id,
117
+ attempt_nr=self.attempt_nr,
118
+ pid=self.pid,
119
+ lock_offset=self.lock_offset,
120
+ )
121
+
122
+ # dump file report
123
+ def dump_file_report(self, file_catalog, attempt_nr):
124
+ """
125
+ Dump the file report.
126
+
127
+ :param file_catalog: The file catalog.
128
+ :param attempt_nr: The attempt number.
129
+ """
130
+ self.logger.debug("dump file report")
131
+
132
+ attempt_nr = 0 if attempt_nr is None else attempt_nr
133
+ if self.job is None:
134
+ self.job = self.taskBuffer.peekJobs([self.job_id], fromDefined=False, fromWaiting=False, forAnal=True)[0]
135
+ if self.job:
136
+ self.taskBuffer.insertJobOutputReport(
137
+ panda_id=self.job_id,
138
+ prod_source_label=self.job.prodSourceLabel,
139
+ job_status=self.job_status,
140
+ attempt_nr=attempt_nr,
141
+ data=file_catalog,
142
+ )
143
+
144
+ # get plugin class
145
+ def get_plugin_class(self, tmp_vo, tmp_group):
146
+ """
147
+ Get the plugin class for the given VO and group.
148
+
149
+ This method attempts to retrieve a specific plugin class based on the provided
150
+ VO (Virtual Organization) and group. If no specific plugin is found, it defaults
151
+ to using the ATLAS plugin.
152
+
153
+ :param tmp_vo: The Virtual Organization identifier.
154
+ :param tmp_group: The group identifier.
155
+ :return: The plugin class.
156
+ """
157
+ # instantiate concrete plugin
158
+ self.adder_plugin_class = panda_config.getPlugin("adder_plugins", tmp_vo, tmp_group)
159
+ if self.adder_plugin_class is None:
160
+ # use ATLAS plugin by default
161
+ from pandaserver.dataservice.adder_atlas_plugin import AdderAtlasPlugin
162
+
163
+ self.adder_plugin_class = AdderAtlasPlugin
164
+ self.logger.debug(f"plugin name {self.adder_plugin_class.__name__}")
165
+ return self.adder_plugin_class
166
+
167
+ def get_report(self) -> None:
168
+ """
169
+ Get the job output report.
170
+ """
171
+ self.report_dict = self.taskBuffer.getJobOutputReport(panda_id=self.job_id, attempt_nr=self.attempt_nr)
172
+ self.data = self.report_dict.get("data")
173
+
174
+ def register_event_service_files(self) -> None:
175
+ """
176
+ Register Event Service (ES) files.
177
+ """
178
+ # instantiate concrete plugin
179
+ self.adder_plugin_class = self.get_plugin_class(self.job.VO, self.job.cloud)
180
+ self.adder_plugin = self.adder_plugin_class(
181
+ self.job,
182
+ taskBuffer=self.taskBuffer,
183
+ siteMapper=self.siteMapper,
184
+ logger=self.logger,
185
+ )
186
+ self.logger.debug("plugin is ready for ES file registration")
187
+ self.adder_plugin.register_event_service_files()
188
+
189
+ def check_file_status_in_jedi(self) -> None:
190
+ """
191
+ Check the file status in JEDI.
192
+ """
193
+ if not self.job.isCancelled() and self.job.taskBufferErrorCode not in [pandaserver.taskbuffer.ErrorCode.EC_PilotRetried]:
194
+ file_check_in_jedi = self.taskBuffer.checkInputFileStatusInJEDI(self.job)
195
+ self.logger.debug(f"check file status in JEDI : {file_check_in_jedi}")
196
+ if file_check_in_jedi is None:
197
+ raise RuntimeError("failed to check file status in JEDI")
198
+ if file_check_in_jedi is False:
199
+ # set job status to failed since some file status is wrong in JEDI
200
+ self.job_status = "failed"
201
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
202
+ err_str = "inconsistent file status between Panda and JEDI. "
203
+ err_str += "failed to avoid duplicated processing caused by synchronization failure"
204
+ self.job.ddmErrorDiag = err_str
205
+ self.logger.debug(f"set jobStatus={self.job_status} since input is inconsistent between Panda and JEDI")
206
+ elif self.job.jobSubStatus in ["pilot_closed"]:
207
+ # terminated by the pilot
208
+ self.logger.debug("going to closed since terminated by the pilot")
209
+ ret_closed = self.taskBuffer.killJobs([self.job_id], "pilot", "60", True)
210
+ if ret_closed[0] is True:
211
+ self.logger.debug("end")
212
+ # remove Catalog
213
+ self.taskBuffer.deleteJobOutputReport(panda_id=self.job_id, attempt_nr=self.attempt_nr)
214
+ return
215
+ # check for cloned jobs
216
+ if EventServiceUtils.isJobCloningJob(self.job) and self.job_status == "finished":
217
+ # get semaphore for storeonce
218
+ if EventServiceUtils.getJobCloningType(self.job) == "storeonce":
219
+ self.taskBuffer.getEventRanges(self.job.PandaID, self.job.jobsetID, self.job.jediTaskID, 1, False, False, None)
220
+ # check semaphore
221
+ check_jc = self.taskBuffer.checkClonedJob(self.job)
222
+ if check_jc is None:
223
+ raise RuntimeError("failed to check the cloned job")
224
+ # failed to lock semaphore
225
+ if check_jc["lock"] is False:
226
+ self.job_status = "failed"
227
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
228
+ self.job.ddmErrorDiag = "failed to lock semaphore for job cloning"
229
+ self.logger.debug(f"set jobStatus={self.job_status} since did not get semaphore for job cloning")
230
+
231
+ def execute_plugin(self) -> None:
232
+ """
233
+ Execute the Adder plugin.
234
+ """
235
+ # interaction with DDM
236
+ try:
237
+ # instantiate concrete plugin
238
+ self.adder_plugin_class = self.get_plugin_class(self.job.VO, self.job.cloud)
239
+ self.adder_plugin = self.adder_plugin_class(
240
+ self.job,
241
+ taskBuffer=self.taskBuffer,
242
+ siteMapper=self.siteMapper,
243
+ extra_info=self.extra_info,
244
+ logger=self.logger,
245
+ )
246
+ self.logger.debug("plugin is ready")
247
+ self.adder_plugin.execute()
248
+ self.add_result = self.adder_plugin.result
249
+ self.logger.debug(f"plugin done with {self.add_result.status_code}")
250
+ except Exception:
251
+ err_type, err_value = sys.exc_info()[:2]
252
+ self.logger.error(f"failed to execute AdderPlugin for VO={self.job.VO} with {err_type}:{err_value}")
253
+ self.logger.error(f"failed to execute AdderPlugin for VO={self.job.VO} with {traceback.format_exc()}")
254
+ self.add_result = None
255
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
256
+ self.job.ddmErrorDiag = "AdderPlugin failure"
257
+
258
+ def finalize_job_status(self) -> None:
259
+ """
260
+ Finalize the job status.
261
+ """
262
+ if self.job.jobStatus == "failed" or self.job_status == "failed":
263
+ self.handle_failed_job()
264
+ else:
265
+ # reset errors
266
+ self.job.jobDispatcherErrorCode = 0
267
+ self.job.jobDispatcherErrorDiag = "NULL"
268
+ # set status
269
+ if self.add_result is not None and self.add_result.merging_files != []:
270
+ # set status for merging:
271
+ for file in self.job.Files:
272
+ if file.lfn in self.add_result.merging_files:
273
+ file.status = "merging"
274
+ self.job.jobStatus = "merging"
275
+ # propagate transition to prodDB
276
+ self.job.stateChangeTime = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
277
+ elif self.add_result is not None and self.add_result.transferring_files != []:
278
+ # set status for transferring
279
+ for file in self.job.Files:
280
+ if file.lfn in self.add_result.transferring_files:
281
+ file.status = "transferring"
282
+ self.job.jobStatus = "transferring"
283
+ self.job.jobSubStatus = None
284
+ # propagate transition to prodDB
285
+ self.job.stateChangeTime = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
286
+ else:
287
+ self.job.jobStatus = "finished"
288
+
289
+ def handle_failed_job(self) -> None:
290
+ """
291
+ Handle failed job status.
292
+ """
293
+ # First of all: check if job failed and in this case take first actions according to error table
294
+ source, error_code, error_diag = None, None, None
295
+ errors = []
296
+ if self.job.pilotErrorCode:
297
+ source = "pilotErrorCode"
298
+ error_code = self.job.pilotErrorCode
299
+ error_diag = self.job.pilotErrorDiag
300
+ errors.append(
301
+ {
302
+ "source": source,
303
+ "error_code": error_code,
304
+ "error_diag": error_diag,
305
+ }
306
+ )
307
+ if self.job.exeErrorCode:
308
+ source = "exeErrorCode"
309
+ error_code = self.job.exeErrorCode
310
+ error_diag = self.job.exeErrorDiag
311
+ errors.append(
312
+ {
313
+ "source": source,
314
+ "error_code": error_code,
315
+ "error_diag": error_diag,
316
+ }
317
+ )
318
+ if self.job.ddmErrorCode:
319
+ source = "ddmErrorCode"
320
+ error_code = self.job.ddmErrorCode
321
+ error_diag = self.job.ddmErrorDiag
322
+ errors.append(
323
+ {
324
+ "source": source,
325
+ "error_code": error_code,
326
+ "error_diag": error_diag,
327
+ }
328
+ )
329
+ if self.job.transExitCode:
330
+ source = "transExitCode"
331
+ error_code = self.job.transExitCode
332
+ error_diag = ""
333
+ errors.append(
334
+ {
335
+ "source": source,
336
+ "error_code": error_code,
337
+ "error_diag": error_diag,
338
+ }
339
+ )
340
+
341
+ if source and error_code:
342
+ try:
343
+ self.logger.debug("AdderGen.run will call job_failure_postprocessing")
344
+ retryModule.job_failure_postprocessing(
345
+ self.taskBuffer,
346
+ self.job.PandaID,
347
+ errors,
348
+ self.job.attemptNr,
349
+ )
350
+ self.logger.debug("job_failure_postprocessing is back")
351
+ except Exception as e:
352
+ self.logger.error(f"job_failure_postprocessing excepted and needs to be investigated ({e}): {traceback.format_exc()}")
353
+
354
+ self.job.jobStatus = "failed"
355
+ for file in self.job.Files:
356
+ if file.type in ["output", "log"]:
357
+ if self.add_result is not None and file.lfn in self.add_result.merging_files:
358
+ file.status = "merging"
359
+ else:
360
+ file.status = "failed"
361
+
362
+ def process_job_report(self) -> bool:
363
+ """
364
+ Check the job status, execute the plugin if job status is applicable, and update job in the database.
365
+ return: True if successful or unnecessary, False otherwise to retry.
366
+ """
367
+ if self.job is None:
368
+ self.logger.debug("job not found in DB")
369
+ return True
370
+ if self.job.jobStatus in ["finished", "failed", "unknown", "merging"]:
371
+ self.logger.error(f"invalid state -> {self.job.jobStatus}")
372
+ return True
373
+ if self.attempt_nr is not None and self.job.attemptNr != self.attempt_nr:
374
+ self.logger.error(f"wrong attemptNr -> job={self.job.attemptNr} <> {self.attempt_nr}")
375
+ return True
376
+ if self.job_status == EventServiceUtils.esRegStatus:
377
+ self.register_event_service_files()
378
+ return True
379
+
380
+ # check file status in JEDI
381
+ self.check_file_status_in_jedi()
382
+
383
+ # use failed for cancelled/closed jobs
384
+ if self.job.isCancelled():
385
+ self.job_status = "failed"
386
+ # reset error codes to skip retrial module
387
+ self.job.pilotErrorCode = 0
388
+ self.job.exeErrorCode = 0
389
+ self.job.ddmErrorCode = 0
390
+
391
+ # keep old status
392
+ old_job_status = self.job.jobStatus
393
+
394
+ # set job status
395
+ if self.job.jobStatus not in ["transferring"]:
396
+ self.job.jobStatus = self.job_status
397
+ self.add_result = None
398
+ self.adder_plugin = None
399
+
400
+ # parse Job Output Report JSON
401
+ parse_result = self.parse_job_output_report()
402
+
403
+ # If the parse_result is less than 2, it means that the parsing either succeeded or encountered a harmless error
404
+ if parse_result < 2:
405
+ self.execute_plugin()
406
+
407
+ # ignore temporary errors
408
+ if self.ignore_tmp_error and self.add_result is not None and self.add_result.is_temporary():
409
+ self.logger.debug(f"ignore {self.job.ddmErrorDiag} ")
410
+ self.logger.debug("escape")
411
+ # unlock job output report
412
+ self.taskBuffer.unlockJobOutputReport(
413
+ panda_id=self.job_id,
414
+ attempt_nr=self.attempt_nr,
415
+ pid=self.pid,
416
+ lock_offset=self.lock_offset,
417
+ )
418
+ return False
419
+ # failed
420
+ if self.add_result is None or not self.add_result.is_succeeded():
421
+ self.job.jobStatus = "failed"
422
+
423
+ # set file status for failed jobs or failed transferring jobs
424
+ self.logger.debug(f"status after plugin call :job.jobStatus={self.job.jobStatus} jobStatus={self.job_status}")
425
+
426
+ self.finalize_job_status()
427
+
428
+ # endtime
429
+ if self.job.endTime == "NULL":
430
+ self.job.endTime = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
431
+ # output size and # of outputs
432
+ self.job.nOutputDataFiles = 0
433
+ self.job.outputFileBytes = 0
434
+ for tmp_file in self.job.Files:
435
+ if tmp_file.type == "output":
436
+ self.job.nOutputDataFiles += 1
437
+ try:
438
+ self.job.outputFileBytes += tmp_file.fsize
439
+ except Exception:
440
+ pass
441
+ # protection
442
+ max_output_file_bytes = 99999999999
443
+ self.job.outputFileBytes = min(self.job.outputFileBytes, max_output_file_bytes)
444
+ # set cancelled state
445
+ if self.job.commandToPilot == "tobekilled" and self.job.jobStatus == "failed":
446
+ self.job.jobStatus = "cancelled"
447
+ # update job
448
+ if old_job_status in ["cancelled", "closed"]:
449
+ pass
450
+ else:
451
+ self.logger.debug("updating DB")
452
+ update_result = self.taskBuffer.updateJobs(
453
+ [self.job],
454
+ False,
455
+ oldJobStatusList=[old_job_status],
456
+ extraInfo=self.extra_info,
457
+ async_dataset_update=True,
458
+ )
459
+ self.logger.debug(f"retU: {update_result}")
460
+
461
+ # failed
462
+ if not update_result[0]:
463
+ self.logger.error(f"failed to update DB for pandaid={self.job.PandaID}")
464
+ # unlock job output report
465
+ self.taskBuffer.unlockJobOutputReport(
466
+ panda_id=self.job_id,
467
+ attempt_nr=self.attempt_nr,
468
+ pid=self.pid,
469
+ lock_offset=self.lock_offset,
470
+ )
471
+ return False
472
+
473
+ try:
474
+ # updateJobs was successful and it failed a job with taskBufferErrorCode
475
+ self.logger.debug("AdderGen.run will peek the job")
476
+ job_tmp = self.taskBuffer.peekJobs(
477
+ [self.job.PandaID],
478
+ fromDefined=False,
479
+ fromArchived=True,
480
+ fromWaiting=False,
481
+ )[0]
482
+ self.logger.debug(
483
+ f"status {job_tmp.jobStatus}, taskBufferErrorCode {job_tmp.taskBufferErrorCode}, taskBufferErrorDiag {job_tmp.taskBufferErrorDiag}"
484
+ )
485
+ if job_tmp.jobStatus == "failed" and job_tmp.taskBufferErrorCode:
486
+ source = "taskBufferErrorCode"
487
+ error_code = job_tmp.taskBufferErrorCode
488
+ error_diag = job_tmp.taskBufferErrorDiag
489
+ errors = [
490
+ {
491
+ "source": source,
492
+ "error_code": error_code,
493
+ "error_diag": error_diag,
494
+ }
495
+ ]
496
+ self.logger.debug("AdderGen.run 2 will call job_failure_postprocessing")
497
+ retryModule.job_failure_postprocessing(
498
+ self.taskBuffer,
499
+ job_tmp.PandaID,
500
+ errors,
501
+ job_tmp.attemptNr,
502
+ )
503
+ self.logger.debug("job_failure_postprocessing 2 is back")
504
+ except IndexError:
505
+ pass
506
+ except Exception as e:
507
+ self.logger.error(f"job_failure_postprocessing 2 excepted and needs to be investigated ({e}): {traceback.format_exc()}")
508
+
509
+ self.setup_closer()
510
+ return True
511
+
512
+ def setup_closer(self) -> None:
513
+ """
514
+ Setup closer for the job.
515
+ """
516
+ # setup for closer
517
+ if not (EventServiceUtils.isEventServiceJob(self.job) and self.job.isCancelled()):
518
+ destination_dispatch_block_list = []
519
+ guid_list = []
520
+ for file in self.job.Files:
521
+ # ignore inputs
522
+ if file.type == "input":
523
+ continue
524
+ # skip pseudo datasets
525
+ if file.destinationDBlock in ["", None, "NULL"]:
526
+ continue
527
+ # start closer for output/log datasets
528
+ if file.destinationDBlock not in destination_dispatch_block_list:
529
+ destination_dispatch_block_list.append(file.destinationDBlock)
530
+ # collect GUIDs
531
+ if (
532
+ self.job.prodSourceLabel == "panda"
533
+ or (
534
+ self.job.prodSourceLabel in ["rucio_test"] + JobUtils.list_ptest_prod_sources
535
+ and self.job.processingType
536
+ in [
537
+ "pathena",
538
+ "prun",
539
+ "gangarobot-rctest",
540
+ "hammercloud",
541
+ ]
542
+ )
543
+ ) and file.type == "output":
544
+ # extract base LFN since LFN was changed to full LFN for CMS
545
+ base_lfn = file.lfn.split("/")[-1]
546
+ guid_list.append(
547
+ {
548
+ "lfn": base_lfn,
549
+ "guid": file.GUID,
550
+ "type": file.type,
551
+ "checksum": file.checksum,
552
+ "md5sum": file.md5sum,
553
+ "fsize": file.fsize,
554
+ "scope": file.scope,
555
+ }
556
+ )
557
+ if guid_list:
558
+ self.taskBuffer.setGUIDs(guid_list)
559
+ if destination_dispatch_block_list:
560
+ # start Closer
561
+ if self.adder_plugin is not None and hasattr(self.adder_plugin, "dataset_map") and self.adder_plugin.dataset_map != {}:
562
+ closer_thread = closer.Closer(
563
+ self.taskBuffer,
564
+ destination_dispatch_block_list,
565
+ self.job,
566
+ dataset_map=self.adder_plugin.dataset_map,
567
+ )
568
+ else:
569
+ closer_thread = closer.Closer(self.taskBuffer, destination_dispatch_block_list, self.job)
570
+ self.logger.debug("start Closer")
571
+ closer_thread.run()
572
+ del closer_thread
573
+ self.logger.debug("end Closer")
574
+ # run closer for associate parallel jobs
575
+ if EventServiceUtils.isJobCloningJob(self.job):
576
+ associate_dispatch_block_map = self.taskBuffer.getDestDBlocksWithSingleConsumer(
577
+ self.job.jediTaskID, self.job.PandaID, destination_dispatch_block_list
578
+ )
579
+ for associate_job_id in associate_dispatch_block_map:
580
+ associate_dispatch_blocks = associate_dispatch_block_map[associate_job_id]
581
+ associate_job = self.taskBuffer.peekJobs(
582
+ [associate_job_id],
583
+ fromDefined=False,
584
+ fromArchived=False,
585
+ fromWaiting=False,
586
+ forAnal=True,
587
+ )[0]
588
+ if self.job is None:
589
+ self.logger.debug(f"associated job PandaID={associate_job_id} not found in DB")
590
+ else:
591
+ closer_thread = closer.Closer(self.taskBuffer, associate_dispatch_blocks, associate_job)
592
+ self.logger.debug(f"start Closer for PandaID={associate_job_id}")
593
+ closer_thread.run()
594
+ del closer_thread
595
+ self.logger.debug(f"end Closer for PandaID={associate_job_id}")
596
+
597
+ def update_worker_node(self, json_dict):
598
+ try:
599
+ self.logger.debug(f"update_worker_node: start")
600
+ wn_specs = json_dict.get("worker_node", {})
601
+ if not wn_specs:
602
+ self.logger.debug(f"update_worker_node: done. No worker node specs found")
603
+ return
604
+
605
+ site = wn_specs.get("site")
606
+ host_name = wn_specs.get("host_name")
607
+ cpu_model = wn_specs.get("cpu_model")
608
+ if not site or not host_name or not cpu_model:
609
+ self.logger.debug(f"update_worker_node: done. Incomplete worker node specs found: site={site}, host_name={host_name}, cpu_model={cpu_model}")
610
+ return
611
+
612
+ cpu_model_normalized = normalize_cpu_model(cpu_model)
613
+ panda_queue = wn_specs.get("panda_queue")
614
+ n_logical_cpus = wn_specs.get("n_logical_cpus")
615
+ n_sockets = wn_specs.get("n_sockets")
616
+ cores_per_socket = wn_specs.get("cores_per_socket")
617
+ threads_per_core = wn_specs.get("threads_per_core")
618
+ cpu_architecture = wn_specs.get("cpu_architecture")
619
+ cpu_architecture_level = wn_specs.get("cpu_architecture_level")
620
+ clock_speed = wn_specs.get("clock_speed")
621
+ total_memory = wn_specs.get("total_memory")
622
+ total_local_disk = wn_specs.get("total_local_disk")
623
+
624
+ self.taskBuffer.update_worker_node(
625
+ site,
626
+ panda_queue,
627
+ host_name,
628
+ cpu_model,
629
+ cpu_model_normalized,
630
+ n_logical_cpus,
631
+ n_sockets,
632
+ cores_per_socket,
633
+ threads_per_core,
634
+ cpu_architecture,
635
+ cpu_architecture_level,
636
+ clock_speed,
637
+ total_memory,
638
+ total_local_disk,
639
+ )
640
+ self.logger.debug(f"update_worker_node: done for site={site}, host_name={host_name}, cpu_model={cpu_model}")
641
+ return
642
+ except Exception:
643
+ self.logger.error(f"update_worker_node: issue with updating worker node specs: {traceback.format_exc()}")
644
+
645
+ def update_worker_node_gpu(self, json_dict):
646
+ try:
647
+ self.logger.debug(f"update_worker_node_gpu: start")
648
+ wn_gpu_specs = json_dict.get("worker_node_gpus", {})
649
+ if not wn_gpu_specs:
650
+ self.logger.debug(f"update_worker_node_gpu: done. No worker node GPU specs found")
651
+ return
652
+
653
+ site = wn_gpu_specs.get("site")
654
+ host_name = wn_gpu_specs.get("host_name")
655
+ if not site or not host_name:
656
+ self.logger.debug(f"update_worker_node_gpu: done. Incomplete worker node GPU specs found: site={site}, host_name={host_name}")
657
+ return
658
+
659
+ vendor = wn_gpu_specs.get("vendor")
660
+ model = wn_gpu_specs.get("model")
661
+ count = wn_gpu_specs.get("count")
662
+ vram = wn_gpu_specs.get("vram")
663
+ architecture = wn_gpu_specs.get("architecture")
664
+ framework = wn_gpu_specs.get("framework")
665
+ framework_version = wn_gpu_specs.get("framework_version")
666
+ driver_version = wn_gpu_specs.get("driver_version")
667
+
668
+ self.taskBuffer.update_worker_node_gpu(site, host_name, vendor, model, count, vram, architecture, framework, framework_version, driver_version)
669
+ self.logger.debug(f"update_worker_node_gpu: done for site={site}, host_name={host_name}")
670
+ return
671
+ except Exception:
672
+ self.logger.error(f"update_worker_node_gpu: issue with updating worker node GPU specs: {traceback.format_exc()}")
673
+
674
+ # parse JSON
675
+ # 0: succeeded, 1: harmless error to exit, 2: fatal error, 3: event service
676
+ def parse_job_output_report(self):
677
+ """
678
+ Parse the JSON data associated with the job to extract file information.
679
+
680
+ This method processes the JSON data to retrieve Logical File Names (LFNs),
681
+ Globally Unique Identifiers (GUIDs), file sizes, checksums, and other metadata.
682
+ It updates the job's file information and ensures consistency between the JSON
683
+ data and the job's file records.
684
+
685
+ :return: An integer indicating the result of the parsing process.
686
+ 0 - succeeded
687
+ 1 - harmless error to exit
688
+ 2 - fatal error
689
+ 3 - event service
690
+ """
691
+ # get LFN and GUID
692
+ log_out = [f for f in self.job.Files if f.type in ["log", "output"]]
693
+
694
+ # no outputs
695
+ if not log_out:
696
+ self.logger.debug("has no outputs")
697
+ self.logger.debug("parse_job_output_report end")
698
+ return 0
699
+
700
+ # get input files
701
+ input_lfns = [file.lfn for file in self.job.Files if file.type == "input"]
702
+
703
+ # parse JSON
704
+ lfns = []
705
+ guids = []
706
+ fsizes = []
707
+ md5sums = []
708
+ chksums = []
709
+ surls = []
710
+ full_lfn_map = {}
711
+ n_events_map = {}
712
+ guid_map = {}
713
+
714
+ try:
715
+ json_dict = json.loads(self.data)
716
+ for lfn in json_dict:
717
+ file_data = json_dict[lfn]
718
+ lfn = str(lfn)
719
+ fsize = None
720
+ md5sum = None
721
+ adler32 = None
722
+ surl = None
723
+ full_lfn = None
724
+ guid = str(file_data["guid"])
725
+ if "fsize" in file_data:
726
+ fsize = int(file_data["fsize"])
727
+ if "md5sum" in file_data:
728
+ md5sum = str(file_data["md5sum"])
729
+ # check the md5sum is a 32-character hexadecimal number
730
+ if re.search("^[a-fA-F0-9]{32}$", md5sum) is None:
731
+ md5sum = None
732
+ if "adler32" in file_data:
733
+ adler32 = str(file_data["adler32"])
734
+ if "surl" in file_data:
735
+ surl = str(file_data["surl"])
736
+ if "full_lfn" in file_data:
737
+ full_lfn = str(file_data["full_lfn"])
738
+ # endpoints
739
+ self.extra_info["endpoint"][lfn] = []
740
+ if "endpoint" in file_data:
741
+ self.extra_info["endpoint"][lfn] = [file_data["endpoint"]]
742
+ # error check
743
+ if (lfn not in input_lfns) and (fsize is None or (md5sum is None and adler32 is None)):
744
+ if not EventServiceUtils.isEventServiceMerge(self.job):
745
+ raise RuntimeError("fsize/md5sum/adler32/surl=None")
746
+ # append
747
+ lfns.append(lfn)
748
+ guids.append(guid)
749
+ fsizes.append(fsize)
750
+ md5sums.append(md5sum)
751
+ surls.append(surl)
752
+ if adler32 is not None:
753
+ # use adler32 if available
754
+ chksums.append(f"ad:{adler32}")
755
+ else:
756
+ chksums.append(f"md5:{md5sum}")
757
+ if full_lfn is not None:
758
+ full_lfn_map[lfn] = full_lfn
759
+ except Exception:
760
+ exc_type, value, _ = sys.exc_info()
761
+ self.logger.warning("Issue with parsing JSON")
762
+ self.logger.error(f"{exc_type} {value}")
763
+ # set failed anyway
764
+ self.job.jobStatus = "failed"
765
+ # JSON error happens when pilot got killed due to wall-time limit or failures in wrapper
766
+ if (
767
+ (self.job.pilotErrorCode in [0, "0", "NULL"])
768
+ and (self.job.taskBufferErrorCode not in [pandaserver.taskbuffer.ErrorCode.EC_WorkerDone])
769
+ and (self.job.transExitCode in [0, "0", "NULL"])
770
+ ):
771
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
772
+ self.job.ddmErrorDiag = "Could not get GUID/LFN/MD5/FSIZE/SURL from pilot JSON"
773
+ return 2
774
+
775
+ # parse metadata to get nEvents
776
+ n_events_from = ""
777
+ json_dict = {}
778
+ try:
779
+ json_dict = json.loads(self.job.metadata)
780
+ for json_file_item in json_dict["files"]["output"]:
781
+ for json_sub_file_item in json_file_item["subFiles"]:
782
+ lfn = str(json_sub_file_item["name"])
783
+ try:
784
+ n_events = int(json_sub_file_item["nentries"])
785
+ n_events_map[lfn] = n_events
786
+ except Exception:
787
+ pass
788
+ try:
789
+ guid = str(json_sub_file_item["file_guid"])
790
+ guid_map[lfn] = guid
791
+ except Exception:
792
+ pass
793
+ n_events_from = "json"
794
+ except Exception:
795
+ self.logger.warning("Issue with parsing JSON for nEvents")
796
+ pass
797
+
798
+ # parse metadata to get worker node specs and GPU specs
799
+ if isinstance(json_dict, dict):
800
+ self.update_worker_node(json_dict)
801
+ self.update_worker_node_gpu(json_dict)
802
+
803
+ # use nEvents and GUIDs reported by the pilot if no job report
804
+ if self.job.metadata == "NULL" and self.job_status == "finished" and self.job.nEvents > 0 and self.job.prodSourceLabel in ["managed"]:
805
+ for file in self.job.Files:
806
+ if file.type == "output":
807
+ n_events_map[file.lfn] = self.job.nEvents
808
+ for lfn, guid in zip(lfns, guids):
809
+ guid_map[lfn] = guid
810
+ n_events_from = "pilot"
811
+
812
+ self.logger.debug(f"nEventsMap={str(n_events_map)}")
813
+ self.logger.debug(f"nEventsFrom={str(n_events_from)}")
814
+ self.logger.debug(f"guidMap={str(guid_map)}")
815
+ self.logger.debug(f"self.job.jobStatus={self.job.jobStatus} in parse_job_output_report")
816
+ self.logger.debug(f"isES={EventServiceUtils.isEventServiceJob(self.job)} isJumbo={EventServiceUtils.isJumboJob(self.job)}")
817
+
818
+ # get lumi block number
819
+ lumi_block_nr = self.job.getLumiBlockNr()
820
+
821
+ # copy files for variable number of outputs
822
+ tmp_stat = self.copy_files_for_variable_num_outputs(lfns)
823
+ if not tmp_stat:
824
+ err_msg = "failed to copy files for variable number of outputs"
825
+ self.logger.error(err_msg)
826
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
827
+ self.job.ddmErrorDiag = err_msg
828
+ self.job.jobStatus = "failed"
829
+ return 2
830
+
831
+ # check files
832
+ lfns_set = set(lfns)
833
+ file_list = []
834
+ for file in self.job.Files:
835
+ file_list.append(file.lfn)
836
+ if file.type == "input":
837
+ if file.lfn in lfns_set:
838
+ if self.job.prodSourceLabel in ["user", "panda"] or self.job.is_on_site_merging():
839
+ # skipped file
840
+ file.status = "skipped"
841
+ self.logger.debug(f"skipped input : {file.lfn}")
842
+ elif self.job.prodSourceLabel in ["managed", "test"] + JobUtils.list_ptest_prod_sources:
843
+ # failed by pilot
844
+ file.status = "failed"
845
+ self.logger.debug(f"failed input : {file.lfn}")
846
+ elif file.type in {"output", "log"}:
847
+ # add only log file for failed jobs
848
+ if self.job_status == "failed" and file.type != "log":
849
+ file.status = "failed"
850
+ continue
851
+ # set failed if it is missing in JSON
852
+ if file.lfn not in lfns_set:
853
+ if (self.job.jobStatus == "finished" and EventServiceUtils.isEventServiceJob(self.job)) or EventServiceUtils.isJumboJob(self.job):
854
+ # unset file status for ES jobs
855
+ pass
856
+ elif file.isAllowedNoOutput():
857
+ # allowed not to be produced
858
+ file.status = "nooutput"
859
+ self.logger.debug(f"set {file.lfn} to status={file.status}")
860
+ else:
861
+ file.status = "failed"
862
+ self.job.jobStatus = "failed"
863
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
864
+ self.job.ddmErrorDiag = f"expected output {file.lfn} is missing in pilot JSON"
865
+ self.logger.error(self.job.ddmErrorDiag)
866
+ continue
867
+
868
+ # look for GUID with LFN
869
+ try:
870
+ i = lfns.index(file.lfn)
871
+ file.GUID = guids[i]
872
+ file.fsize = fsizes[i]
873
+ file.md5sum = md5sums[i]
874
+ file.checksum = chksums[i]
875
+ surl = surls[i]
876
+ # status
877
+ file.status = "ready"
878
+ # change to full LFN
879
+ if file.lfn in full_lfn_map:
880
+ file.lfn = full_lfn_map[file.lfn]
881
+ # add SURL to extraInfo
882
+ self.extra_info["surl"][file.lfn] = surl
883
+ # add nevents
884
+ if file.lfn in n_events_map:
885
+ self.extra_info["nevents"][file.lfn] = n_events_map[file.lfn]
886
+ except Exception:
887
+ # status
888
+ file.status = "failed"
889
+ exc_type, value, _ = sys.exc_info()
890
+ self.logger.error(f"{exc_type} {value}")
891
+
892
+ # set lumi block number
893
+ if lumi_block_nr is not None and file.status != "failed":
894
+ self.extra_info["lbnr"][file.lfn] = lumi_block_nr
895
+
896
+ self.extra_info["guid"] = guid_map
897
+
898
+ # check consistency between JSON and filesTable
899
+ for lfn in lfns:
900
+ if lfn not in file_list:
901
+ self.logger.error(f"{lfn} is not found in filesTable")
902
+ self.job.jobStatus = "failed"
903
+ for tmp_file in self.job.Files:
904
+ tmp_file.status = "failed"
905
+ self.job.ddmErrorCode = pandaserver.dataservice.ErrorCode.EC_Adder
906
+ self.job.ddmErrorDiag = f"pilot produced {lfn} inconsistently with jobdef"
907
+ return 2
908
+ # return
909
+ self.logger.debug("parse_job_output_report end")
910
+ return 0
911
+
912
+ # copy files for variable number of outputs
913
+ def copy_files_for_variable_num_outputs(self, lfns):
914
+ """
915
+ Copy files for variable number of outputs.
916
+
917
+ This method handles the copying of file records when there is a variable number of output files.
918
+ It identifies new Logical File Names (LFNs) that correspond to the original output files and
919
+ updates the job information accordingly.
920
+
921
+ :param lfns: List of new Logical File Names (LFNs).
922
+ :return: True if the operation is successful, False otherwise.
923
+ """
924
+ # get original output files
925
+ original_output_files = {}
926
+ for tmp_file in self.job.Files:
927
+ if tmp_file.type in ["output", "log"]:
928
+ original_output_files[tmp_file.lfn] = tmp_file
929
+ # look for unknown files
930
+ orig_to_new_map = {}
931
+ for new_lfn in lfns:
932
+ if new_lfn not in original_output_files:
933
+ # look for corresponding original output
934
+ for original_lfn in original_output_files:
935
+ # match LFNs that have a similar base name but may have additional suffixes
936
+ tmp_patt = r"^{0}\.*_\d+$".format(original_lfn)
937
+ # removing any prefix up to and including the first pipe character (|) from the original LFN
938
+ reg_patt = re.sub(r"^[^|]+\|", "", original_lfn)
939
+ if re.search(tmp_patt, new_lfn) or (original_lfn.startswith("regex|") and re.search(reg_patt, new_lfn)):
940
+ self.logger.debug(f"use new LFN {new_lfn} for {original_lfn}")
941
+ # collect new filenames
942
+ orig_to_new_map.setdefault(original_lfn, [])
943
+ orig_to_new_map[original_lfn].append(new_lfn)
944
+ break
945
+ # copy file records
946
+ for original_lfn, new_lfn in orig_to_new_map.items():
947
+ tmp_stat = self.taskBuffer.copy_file_records(new_lfn, original_output_files[original_lfn])
948
+ if not tmp_stat:
949
+ return False
950
+ # refresh job info
951
+ if orig_to_new_map:
952
+ self.job = self.taskBuffer.peekJobs([self.job_id], fromDefined=False, fromWaiting=False, forAnal=True)[0]
953
+ # return
954
+ return True