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,1106 @@
1
+ """
2
+ Plugin class for adding data to datasets in ATLAS.
3
+ Inherits from AdderPluginBase.
4
+
5
+ """
6
+
7
+ import datetime
8
+ import gc
9
+ import re
10
+ import sys
11
+ import time
12
+ import traceback
13
+ from typing import Dict, List
14
+
15
+ from pandacommon.pandautils.PandaUtils import naive_utcnow
16
+ from rucio.common.exception import (
17
+ DataIdentifierNotFound,
18
+ FileConsistencyMismatch,
19
+ InsufficientAccountLimit,
20
+ InvalidObject,
21
+ InvalidPath,
22
+ InvalidRSEExpression,
23
+ RSENotFound,
24
+ RSEProtocolNotSupported,
25
+ UnsupportedOperation,
26
+ )
27
+
28
+ from pandaserver.config import panda_config
29
+ from pandaserver.dataservice import DataServiceUtils, ErrorCode
30
+ from pandaserver.dataservice.adder_plugin_base import AdderPluginBase
31
+ from pandaserver.dataservice.DataServiceUtils import select_scope
32
+ from pandaserver.dataservice.ddm import rucioAPI
33
+ from pandaserver.srvcore.MailUtils import MailUtils
34
+ from pandaserver.taskbuffer import EventServiceUtils, JobUtils
35
+
36
+
37
+ class AdderAtlasPlugin(AdderPluginBase):
38
+ """
39
+ Plugin class for adding data to datasets in ATLAS.
40
+ Inherits from AdderPluginBase.
41
+ """
42
+
43
+ # constructor
44
+ def __init__(self, job, **params):
45
+ """
46
+ Constructor for AdderAtlasPlugin.
47
+
48
+ Args:
49
+ job: The job object containing job details.
50
+ **params: Additional parameters.
51
+ """
52
+ AdderPluginBase.__init__(self, job, params)
53
+ self.job_id = self.job.PandaID
54
+ self.job_status = self.job.jobStatus
55
+ self.dataset_map = {}
56
+ self.add_to_top_only = False
57
+ self.go_to_transferring = False
58
+ self.log_transferring = False
59
+ self.subscription_map = {}
60
+ self.go_to_merging = False
61
+
62
+ # main
63
+ def execute(self) -> None:
64
+ """
65
+ Main execution method for the plugin.
66
+ Handles the logic for adding files to datasets based on job status and other conditions.
67
+ """
68
+ try:
69
+ self.logger.debug(f"start plugin : {self.job_status}")
70
+ # add files only to top-level datasets for transferring jobs
71
+ if self.job.jobStatus == "transferring":
72
+ self.add_to_top_only = True
73
+ self.logger.debug("adder for transferring")
74
+ self.check_output_file_metadata()
75
+ # check if the job goes to merging
76
+ if self.job.produceUnMerge():
77
+ self.go_to_merging = True
78
+ # check if the job should go to transferring
79
+ src_site_spec = self.siteMapper.getSite(self.job.computingSite)
80
+ _, scope_src_site_spec_output = select_scope(src_site_spec, self.job.prodSourceLabel, self.job.job_label)
81
+ tmp_src_ddm = src_site_spec.ddm_output[scope_src_site_spec_output]
82
+ if self.job.prodSourceLabel == "user" and self.job.destinationSE not in self.siteMapper.siteSpecList:
83
+ # DQ2 ID was set by using --destSE for analysis job to transfer output
84
+ tmp_dst_ddm = self.job.destinationSE
85
+ else:
86
+ dst_site_spec = self.siteMapper.getSite(self.job.destinationSE)
87
+ _, scope_dst_site_spec_output = select_scope(dst_site_spec, self.job.prodSourceLabel, self.job.job_label)
88
+ tmp_dst_ddm = dst_site_spec.ddm_output[scope_dst_site_spec_output]
89
+ # protection against disappearance of dest from schedconfig
90
+ if self.job.destinationSE not in ["NULL", None] and not self.siteMapper.checkSite(self.job.destinationSE) and self.job.destinationSE != "local":
91
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
92
+ self.job.ddmErrorDiag = f"destinationSE {self.job.destinationSE} is unknown in schedconfig"
93
+ self.logger.error(f"{self.job.ddmErrorDiag}")
94
+ # set fatal error code and return
95
+ self.result.set_fatal()
96
+ return
97
+
98
+ # protection against disappearance of src from schedconfig
99
+ if not self.siteMapper.checkSite(self.job.computingSite):
100
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
101
+ self.job.ddmErrorDiag = f"computingSite {self.job.computingSite} is unknown in schedconfig"
102
+ self.logger.error(f"{self.job.ddmErrorDiag}")
103
+ # set fatal error code and return
104
+ self.result.set_fatal()
105
+ return
106
+ # check if the job has something to transfer
107
+ self.logger.debug(f"alt stage-out:{str(self.job.altStgOutFileList())}")
108
+ something_to_transfer = False
109
+ for file in self.job.Files:
110
+ if file.type in {"output", "log"}:
111
+ if file.status == "nooutput":
112
+ continue
113
+ if DataServiceUtils.getDistributedDestination(file.destinationDBlockToken) is None and file.lfn not in self.job.altStgOutFileList():
114
+ something_to_transfer = True
115
+ break
116
+ self.logger.debug(f"DDM src:{tmp_src_ddm} dst:{tmp_dst_ddm}")
117
+ job_type = JobUtils.translate_prodsourcelabel_to_jobtype(src_site_spec.type, self.job.prodSourceLabel)
118
+ if re.search("^ANALY_", self.job.computingSite) is not None:
119
+ # analysis site. Should be obsoleted by the next check
120
+ pass
121
+ elif job_type == JobUtils.ANALY_PS:
122
+ # analysis job
123
+ pass
124
+ elif self.job.computingSite == self.job.destinationSE:
125
+ # same site ID for computingSite and destinationSE
126
+ pass
127
+ elif tmp_src_ddm == tmp_dst_ddm:
128
+ # same DDMID for src/dest
129
+ pass
130
+ elif self.job.destinationSE in ["NULL", None]:
131
+ # jobs stayed without destinationSE, e.g. there is no job output
132
+ pass
133
+ elif self.add_to_top_only:
134
+ # already in transferring
135
+ pass
136
+ elif self.go_to_merging:
137
+ # no transferring for merging
138
+ pass
139
+ elif self.job.jobStatus == "failed":
140
+ # failed jobs
141
+ if self.job.prodSourceLabel in ["managed", "test"]:
142
+ self.log_transferring = True
143
+ elif (
144
+ self.job.jobStatus == "finished"
145
+ and (EventServiceUtils.isEventServiceJob(self.job) or EventServiceUtils.isJumboJob(self.job))
146
+ and not EventServiceUtils.isJobCloningJob(self.job)
147
+ ):
148
+ # transfer only log file for normal ES jobs
149
+ self.log_transferring = True
150
+ elif not something_to_transfer:
151
+ # nothing to transfer
152
+ pass
153
+ else:
154
+ self.go_to_transferring = True
155
+ self.logger.debug(f"somethingToTransfer={something_to_transfer}")
156
+ self.logger.debug(f"goToTransferring={self.go_to_transferring}")
157
+ self.logger.debug(f"logTransferring={self.log_transferring}")
158
+ self.logger.debug(f"goToMerging={self.go_to_merging}")
159
+ self.logger.debug(f"addToTopOnly={self.add_to_top_only}")
160
+ return_output = self.update_outputs()
161
+ self.logger.debug(f"added outputs with {return_output}")
162
+ if return_output != 0:
163
+ self.logger.debug("terminated when adding")
164
+ return
165
+ # succeeded
166
+ self.result.set_succeeded()
167
+ self.logger.debug("end plugin")
168
+ except Exception:
169
+ error_type, error_value = sys.exc_info()[:2]
170
+ err_str = f"execute() : {error_type} {error_value}"
171
+ err_str += traceback.format_exc()
172
+ self.logger.debug(err_str)
173
+ # set fatal error code
174
+ self.result.set_fatal()
175
+ # return
176
+ return
177
+
178
+ # check output file metadata
179
+ def check_output_file_metadata(self):
180
+ """
181
+ Check the metadata of output files for consistency and change the job status if inconsistencies are found.
182
+ """
183
+ # check only successful jobs
184
+ if self.job_status in ["failed", "cancelled", "closed"]:
185
+ return
186
+ # get the zip file map
187
+ zip_file_map = self.job.getZipFileMap()
188
+ # check only output files
189
+ for file in self.job.Files:
190
+ is_zip_file = file.lfn in zip_file_map
191
+ # check num events and GUID
192
+ if file.type == "output" and not is_zip_file and not self.add_to_top_only and self.job.prodSourceLabel == "managed":
193
+ if file.lfn not in self.extra_info["nevents"]:
194
+ to_skip = False
195
+ # exclude some formats
196
+ for patt in ["TXT", "NTUP", "HIST"]:
197
+ if file.lfn.startswith(patt):
198
+ to_skip = True
199
+ if not to_skip:
200
+ err_msg = f"nEvents is missing in jobReport for {file.lfn}"
201
+ self.logger.error(err_msg)
202
+ self.job.ddmErrorCode = ErrorCode.EC_MissingNumEvents
203
+ self.job.ddmErrorDiag = err_msg
204
+ self.job.jobStatus = "failed"
205
+ self.job_status = "failed"
206
+ return
207
+ if file.lfn not in self.extra_info["guid"] or file.GUID != self.extra_info["guid"][file.lfn]:
208
+ self.logger.debug(f"extra_info = {str(self.extra_info)}")
209
+ err_msg = f"GUID is inconsistent between jobReport and pilot report for {file.lfn}"
210
+ self.logger.error(err_msg)
211
+ self.job.ddmErrorCode = ErrorCode.EC_InconsistentGUID
212
+ self.job.ddmErrorDiag = err_msg
213
+ self.job.jobStatus = "failed"
214
+ self.job_status = "failed"
215
+ return
216
+
217
+ # update output files
218
+ def update_outputs(self):
219
+ """
220
+ Update output files for the job.
221
+ Handles the logic for adding files to datasets and registering them with Rucio.
222
+
223
+ Returns:
224
+ int: 0 if successful, 1 if there was an error.
225
+ """
226
+ # return if non-DQ2
227
+ if self.job.destinationSE == "local":
228
+ return 0
229
+
230
+ # Get the computingsite spec and scope
231
+ src_site_spec = self.siteMapper.getSite(self.job.computingSite)
232
+ _, scope_src_site_spec_output = select_scope(src_site_spec, self.job.prodSourceLabel, self.job.job_label)
233
+
234
+ # Get the zip file map
235
+ zip_file_map = self.job.getZipFileMap()
236
+
237
+ # Get campaign and nEvents input
238
+ campaign = None
239
+ n_events_input = {}
240
+
241
+ if self.job.jediTaskID not in [0, None, "NULL"]:
242
+ tmp_ret = self.taskBuffer.getTaskAttributesPanda(self.job.jediTaskID, ["campaign"])
243
+ campaign = tmp_ret.get("campaign")
244
+ for file_spec in self.job.Files:
245
+ if file_spec.type == "input":
246
+ tmp_dict = self.taskBuffer.getJediFileAttributes(
247
+ file_spec.PandaID, file_spec.jediTaskID, file_spec.datasetID, file_spec.fileID, ["nEvents"]
248
+ )
249
+ if "nEvents" in tmp_dict:
250
+ n_events_input[file_spec.lfn] = tmp_dict["nEvents"]
251
+
252
+ # Add nEvents info for zip files
253
+ for tmp_zip_file_name, tmp_zip_contents in zip_file_map.items():
254
+ if tmp_zip_file_name not in self.extra_info["nevents"]:
255
+ for tmp_zip_content in tmp_zip_contents:
256
+ for tmp_lfn in list(n_events_input):
257
+ # checking if a logical file name (LFN) matches a pattern corresponding to a zip file content and if the LFN exists in the n_events_input dictionary with a non-None value.
258
+ # If these conditions are met, the code updates the extra_info["nevents"] dictionary by setting a default value of 0 for the zip file name.
259
+ if re.search("^" + tmp_zip_content + "$", tmp_lfn) is not None and tmp_lfn in n_events_input and n_events_input[tmp_lfn] is not None:
260
+ self.extra_info["nevents"].setdefault(tmp_zip_file_name, 0)
261
+ self.extra_info["nevents"][tmp_zip_file_name] += n_events_input[tmp_lfn]
262
+
263
+ # check files
264
+ id_map = {}
265
+ # fileList = []
266
+ sub_map = {}
267
+ dataset_destination_map = {}
268
+ dist_datasets = set()
269
+ map_for_alt_stage_out = {}
270
+ alt_staged_files = set()
271
+ zip_files = {}
272
+ cont_zip_map = {}
273
+ sub_to_ds_map = {}
274
+ ds_id_to_ds_map = self.taskBuffer.getOutputDatasetsJEDI(self.job.PandaID)
275
+ self.logger.debug(f"dsInJEDI={str(ds_id_to_ds_map)}")
276
+
277
+ for file in self.job.Files:
278
+ # gc
279
+ gc.collect()
280
+ if file.type not in {"output", "log"}:
281
+ continue
282
+
283
+ # prepare the site spec and scope for the destinationSE site
284
+ destination_se_site_spec = self.siteMapper.getSite(file.destinationSE)
285
+ _, scope_dst_se_site_spec_output = select_scope(destination_se_site_spec, self.job.prodSourceLabel, self.job.job_label)
286
+
287
+ # added to map
288
+ sub_to_ds_map[file.destinationDBlock] = ds_id_to_ds_map.get(file.datasetID, file.dataset)
289
+
290
+ # add only log file for failed jobs
291
+ if self.job_status == "failed" and file.type != "log":
292
+ continue
293
+ # add only log file for successful ES jobs
294
+ if (
295
+ self.job.jobStatus == "finished"
296
+ and EventServiceUtils.isEventServiceJob(self.job)
297
+ and not EventServiceUtils.isJobCloningJob(self.job)
298
+ and file.type != "log"
299
+ ):
300
+ continue
301
+ # add only log for jumbo jobs
302
+ if EventServiceUtils.isJumboJob(self.job) and file.type != "log":
303
+ continue
304
+ # skip no output or failed
305
+ if file.status in ["nooutput", "failed"]:
306
+ continue
307
+
308
+ # check if zip file
309
+ if file.lfn in zip_file_map:
310
+ is_zip_file = True
311
+ if file.lfn not in zip_files and not self.add_to_top_only:
312
+ zip_files[file.lfn] = {}
313
+ else:
314
+ is_zip_file = False
315
+
316
+ # check if zip content
317
+ zip_file_name = None
318
+ if not is_zip_file and not self.add_to_top_only:
319
+ for tmp_zip_file_name in zip_file_map:
320
+ tmp_zip_contents = zip_file_map[tmp_zip_file_name]
321
+ for tmp_zip_content in tmp_zip_contents:
322
+ if re.search("^" + tmp_zip_content + "$", file.lfn) is not None:
323
+ zip_file_name = tmp_zip_file_name
324
+ break
325
+ if zip_file_name is not None:
326
+ break
327
+ if zip_file_name is not None:
328
+ if zip_file_name not in zip_files:
329
+ zip_files[zip_file_name] = {}
330
+ cont_zip_map[file.lfn] = zip_file_name
331
+
332
+ try:
333
+ # check nevents
334
+ if file.type == "output" and not is_zip_file and not self.add_to_top_only and self.job.prodSourceLabel == "managed":
335
+ if file.lfn not in self.extra_info["nevents"]:
336
+ to_skip = False
337
+ # exclude some formats
338
+ for patt in ["TXT", "NTUP", "HIST"]:
339
+ if file.lfn.startswith(patt):
340
+ to_skip = True
341
+ if not to_skip:
342
+ err_msg = f"nevents is missing in jobReport for {file.lfn}"
343
+ self.logger.warning(err_msg)
344
+ self.job.ddmErrorCode = ErrorCode.EC_MissingNumEvents
345
+ raise ValueError(err_msg)
346
+ if file.lfn not in self.extra_info["guid"] or file.GUID != self.extra_info["guid"][file.lfn]:
347
+ self.logger.debug(f"extra_info = {str(self.extra_info)}")
348
+ err_msg = f"GUID is inconsistent between jobReport and pilot report for {file.lfn}"
349
+ self.logger.warning(err_msg)
350
+ self.job.ddmErrorCode = ErrorCode.EC_InconsistentGUID
351
+ raise ValueError(err_msg)
352
+
353
+ # fsize
354
+ fsize = None
355
+ if file.fsize not in ["NULL", "", 0]:
356
+ try:
357
+ fsize = int(file.fsize)
358
+ except Exception:
359
+ error_type, error_value, _ = sys.exc_info()
360
+ self.logger.error(f"{self.job_id} : {error_type} {error_value}")
361
+ # use top-level dataset name for alternative stage-out
362
+ if file.lfn not in self.job.altStgOutFileList():
363
+ file_destination_dispatch_block = file.destinationDBlock
364
+ else:
365
+ file_destination_dispatch_block = file.dataset
366
+ # append to map
367
+ if file_destination_dispatch_block not in id_map:
368
+ id_map[file_destination_dispatch_block] = []
369
+ file_attrs = {
370
+ "guid": file.GUID,
371
+ "lfn": file.lfn,
372
+ "size": fsize,
373
+ "checksum": file.checksum,
374
+ "ds": file_destination_dispatch_block,
375
+ }
376
+ # add SURLs if LFC registration is required
377
+ if not self.add_to_top_only:
378
+ file_attrs["surl"] = self.extra_info["surl"][file.lfn]
379
+ if file_attrs["surl"] is None:
380
+ del file_attrs["surl"]
381
+
382
+ # get destination
383
+ if file_destination_dispatch_block not in dataset_destination_map:
384
+ if file.lfn in self.job.altStgOutFileList():
385
+ # alternative stage-out
386
+ tmp_dest_list = (
387
+ [DataServiceUtils.getDestinationSE(file.destinationDBlockToken)]
388
+ if DataServiceUtils.getDestinationSE(file.destinationDBlockToken)
389
+ else [
390
+ destination_se_site_spec.setokens_output[scope_dst_se_site_spec_output].get(
391
+ file.destinationDBlockToken, destination_se_site_spec.ddm_output[scope_dst_se_site_spec_output]
392
+ )
393
+ ]
394
+ )
395
+ elif file.destinationDBlockToken in ["", None, "NULL"]:
396
+ # use default endpoint
397
+ tmp_dest_list = [src_site_spec.ddm_output[scope_src_site_spec_output]]
398
+ elif (
399
+ DataServiceUtils.getDestinationSE(file.destinationDBlockToken)
400
+ and src_site_spec.ddm_output[scope_src_site_spec_output] == destination_se_site_spec.ddm_output[scope_dst_se_site_spec_output]
401
+ ):
402
+ tmp_dest_list = [DataServiceUtils.getDestinationSE(file.destinationDBlockToken)]
403
+ # RSE is specified
404
+ elif DataServiceUtils.getDistributedDestination(file.destinationDBlockToken):
405
+ tmp_dest_list = [DataServiceUtils.getDistributedDestination(file.destinationDBlockToken)]
406
+ dist_datasets.add(file_destination_dispatch_block)
407
+ # RSE is specified for distributed datasets
408
+ elif src_site_spec.cloud != self.job.cloud and (self.job.prodSourceLabel not in ["user", "panda"]):
409
+ # T1 used as T2
410
+ tmp_dest_list = [src_site_spec.ddm_output[scope_src_site_spec_output]]
411
+ else:
412
+ tmp_dest_list = []
413
+ tmp_se_tokens = src_site_spec.setokens_output[scope_src_site_spec_output]
414
+ for tmp_dest_token in file.destinationDBlockToken.split(","):
415
+ if tmp_dest_token in tmp_se_tokens:
416
+ tmp_dest = tmp_se_tokens[tmp_dest_token]
417
+ else:
418
+ tmp_dest = src_site_spec.ddm_output[scope_src_site_spec_output]
419
+ if tmp_dest not in tmp_dest_list:
420
+ tmp_dest_list.append(tmp_dest)
421
+ # add
422
+ dataset_destination_map[file_destination_dispatch_block] = tmp_dest_list
423
+
424
+ # extra meta data
425
+ if file.lfn in self.extra_info["lbnr"]:
426
+ file_attrs["lumiblocknr"] = self.extra_info["lbnr"][file.lfn]
427
+ if file.lfn in self.extra_info["nevents"]:
428
+ file_attrs["events"] = self.extra_info["nevents"][file.lfn]
429
+ elif self.extra_info["nevents"] != {}:
430
+ file_attrs["events"] = None
431
+ # if file.jediTaskID not in [0,None,'NULL']:
432
+ # fileAttrs['task_id'] = file.jediTaskID
433
+ file_attrs["panda_id"] = file.PandaID
434
+ if campaign not in ["", None]:
435
+ file_attrs["campaign"] = campaign
436
+
437
+ # For files uploaded to alternative RSEs
438
+ has_normal_url = True
439
+ if file.lfn in self.extra_info["endpoint"] and self.extra_info["endpoint"][file.lfn] != []:
440
+ for pilot_end_point in self.extra_info["endpoint"][file.lfn]:
441
+ # pilot uploaded to endpoint consistently with original job definition
442
+ if pilot_end_point in dataset_destination_map[file_destination_dispatch_block]:
443
+ has_normal_url = True
444
+ else:
445
+ # alternative stage-out
446
+ alt_staged_files.add(file.lfn)
447
+ map_for_alt_stage_out.setdefault(pilot_end_point, {})
448
+ if file_destination_dispatch_block in dist_datasets:
449
+ # add files to top-level distributed datasets without triggering aggregation
450
+ tmp_destination_dispatch_block = sub_to_ds_map[file_destination_dispatch_block]
451
+ else:
452
+ # add files to dispatch blocks for aggregation to the destination
453
+ tmp_destination_dispatch_block = file_destination_dispatch_block
454
+ map_for_alt_stage_out[pilot_end_point].setdefault(tmp_destination_dispatch_block, [])
455
+ map_for_alt_stage_out[pilot_end_point][tmp_destination_dispatch_block].append(file_attrs)
456
+
457
+ if has_normal_url:
458
+ # add file to be added to dataset
459
+ id_map[file_destination_dispatch_block].append(file_attrs)
460
+ # add file to be added to zip
461
+ if not is_zip_file and zip_file_name:
462
+ if "files" not in zip_files[zip_file_name]:
463
+ zip_files[zip_file_name]["files"] = []
464
+ zip_files[zip_file_name]["files"].append(file_attrs)
465
+ if is_zip_file and not self.add_to_top_only:
466
+ # copy file attribute for zip file registration
467
+ for tmp_file_attr_name, tmp_file_attr_val in file_attrs.items():
468
+ zip_files[file.lfn][tmp_file_attr_name] = tmp_file_attr_val
469
+ zip_files[file.lfn]["scope"] = file.scope
470
+ zip_files[file.lfn]["rse"] = dataset_destination_map[file_destination_dispatch_block]
471
+
472
+ # for subscription
473
+ if re.search("_sub\d+$", file_destination_dispatch_block) and (not self.add_to_top_only) and self.job.destinationSE != "local":
474
+ if not self.siteMapper:
475
+ self.logger.error("SiteMapper is None")
476
+ else:
477
+ # get dataset spec
478
+ if file_destination_dispatch_block not in self.dataset_map:
479
+ tmp_dataset = self.taskBuffer.queryDatasetWithMap({"name": file_destination_dispatch_block})
480
+ self.dataset_map[file_destination_dispatch_block] = tmp_dataset
481
+ # check if valid dataset
482
+ if self.dataset_map[file_destination_dispatch_block] is None:
483
+ self.logger.error(f": cannot find {file_destination_dispatch_block} in DB")
484
+ else:
485
+ if self.dataset_map[file_destination_dispatch_block].status not in ["defined"]:
486
+ # not a fresh dataset
487
+ self.logger.debug(
488
+ f": subscription was already made for {self.dataset_map[file_destination_dispatch_block].status}:{file_destination_dispatch_block}"
489
+ )
490
+ else:
491
+ # get DDM IDs
492
+ tmp_src_ddm = src_site_spec.ddm_output[scope_src_site_spec_output]
493
+ if self.job.prodSourceLabel == "user" and file.destinationSE not in self.siteMapper.siteSpecList:
494
+ # DDM ID was set by using --destSE for analysis job to transfer output
495
+ tmp_dst_ddm = file.destinationSE
496
+ else:
497
+ if DataServiceUtils.getDestinationSE(file.destinationDBlockToken):
498
+ tmp_dst_ddm = DataServiceUtils.getDestinationSE(file.destinationDBlockToken)
499
+ else:
500
+ tmp_dst_ddm = destination_se_site_spec.ddm_output[scope_dst_se_site_spec_output]
501
+ # if src != dest or multi-token
502
+ if (tmp_src_ddm != tmp_dst_ddm) or (tmp_src_ddm == tmp_dst_ddm and file.destinationDBlockToken.count(",") != 0):
503
+ opt_sub = {
504
+ "DATASET_COMPLETE_EVENT": [
505
+ f"http://{panda_config.pserverhosthttp}:{panda_config.pserverporthttp}/server/panda/datasetCompleted"
506
+ ]
507
+ }
508
+ # append
509
+ if file_destination_dispatch_block not in sub_map:
510
+ sub_map[file_destination_dispatch_block] = []
511
+ # sources
512
+ opt_source = {}
513
+ # set sources
514
+ if file.destinationDBlockToken in [
515
+ "NULL",
516
+ "",
517
+ ]:
518
+ # use default DDM ID as source
519
+ opt_source[tmp_src_ddm] = {"policy": 0}
520
+ else:
521
+ # convert token to DDM ID
522
+ ddm_id = tmp_src_ddm
523
+ # use the first token's location as source for T1D1
524
+ tmp_src_token = file.destinationDBlockToken.split(",")[0]
525
+ if tmp_src_token in src_site_spec.setokens_output[scope_src_site_spec_output]:
526
+ ddm_id = src_site_spec.setokens_output[scope_src_site_spec_output][tmp_src_token]
527
+ opt_source[ddm_id] = {"policy": 0}
528
+ # T1 used as T2
529
+ if src_site_spec.cloud != self.job.cloud and (self.job.prodSourceLabel not in ["user", "panda"]):
530
+ if tmp_src_ddm not in opt_source:
531
+ opt_source[tmp_src_ddm] = {"policy": 0}
532
+ # use another location when token is set
533
+ if file.destinationDBlockToken not in [
534
+ "NULL",
535
+ "",
536
+ ]:
537
+ tmp_ddm_id_list = []
538
+ tmp_dst_tokens = file.destinationDBlockToken.split(",")
539
+ # remove the first one because it is already used as a location
540
+ if tmp_src_ddm == tmp_dst_ddm:
541
+ tmp_dst_tokens = tmp_dst_tokens[1:]
542
+ # loop over all tokens
543
+ for idx_token, tmp_dst_token in enumerate(tmp_dst_tokens):
544
+ ddm_id = tmp_dst_ddm
545
+ if tmp_dst_token in destination_se_site_spec.setokens_output[scope_dst_se_site_spec_output]:
546
+ ddm_id = destination_se_site_spec.setokens_output[scope_dst_se_site_spec_output][tmp_dst_token]
547
+ # keep the fist destination for multi-hop
548
+ if idx_token == 0:
549
+ first_destination_ddm = ddm_id
550
+ else:
551
+ # use the fist destination as source for T1D1
552
+ opt_source = {}
553
+ opt_source[first_destination_ddm] = {"policy": 0}
554
+ # remove looping subscription
555
+ if ddm_id == tmp_src_ddm:
556
+ continue
557
+ # avoid duplication
558
+ if ddm_id not in tmp_ddm_id_list:
559
+ sub_map[file_destination_dispatch_block].append((ddm_id, opt_sub, opt_source))
560
+ else:
561
+ # use default DDM
562
+ for ddm_id in tmp_dst_ddm.split(","):
563
+ sub_map[file_destination_dispatch_block].append((ddm_id, opt_sub, opt_source))
564
+ except Exception as e:
565
+ err_str = str(e)
566
+ self.logger.error(err_str + " " + traceback.format_exc())
567
+ self.result.set_fatal()
568
+ self.job.ddmErrorDiag = "failed before adding files : " + err_str
569
+ return 1
570
+
571
+ # release some memory
572
+ del ds_id_to_ds_map
573
+ gc.collect()
574
+
575
+ # zipping input files
576
+ if len(zip_file_map) > 0 and not self.add_to_top_only:
577
+ for file_spec in self.job.Files:
578
+ if file_spec.type != "input":
579
+ continue
580
+
581
+ zip_file_name = None
582
+ for tmp_zip_file_name in zip_file_map:
583
+ tmp_zip_contents = zip_file_map[tmp_zip_file_name]
584
+ for tmp_zip_content in tmp_zip_contents:
585
+ if re.search("^" + tmp_zip_content + "$", file_spec.lfn) is not None:
586
+ zip_file_name = tmp_zip_file_name
587
+ break
588
+ if zip_file_name:
589
+ break
590
+ if zip_file_name:
591
+ if zip_file_name not in zip_files:
592
+ continue
593
+ if "files" not in zip_files[zip_file_name]:
594
+ zip_files[zip_file_name]["files"] = []
595
+ file_attrs = {
596
+ "guid": file_spec.GUID,
597
+ "lfn": file_spec.lfn,
598
+ "size": file_spec.fsize,
599
+ "checksum": file_spec.checksum,
600
+ "ds": file_spec.dataset,
601
+ }
602
+ zip_files[zip_file_name]["files"].append(file_attrs)
603
+
604
+ # cleanup submap
605
+ sub_map = {k: v for k, v in sub_map.items() if v}
606
+
607
+ # add data to original dataset
608
+ for destination_dispatch_block in list(id_map):
609
+ orig_dispatch_block = None
610
+ match = re.search("^(.+)_sub\d+$", destination_dispatch_block)
611
+ if match:
612
+ # add files to top-level datasets
613
+ orig_dispatch_block = sub_to_ds_map[destination_dispatch_block]
614
+ if (not self.go_to_transferring) or (not self.add_to_top_only and destination_dispatch_block in dist_datasets):
615
+ id_map[orig_dispatch_block] = id_map[destination_dispatch_block]
616
+ # add files to top-level datasets only
617
+ if self.add_to_top_only or self.go_to_merging or (destination_dispatch_block in dist_datasets and orig_dispatch_block):
618
+ del id_map[destination_dispatch_block]
619
+ # skip sub unless getting transferred
620
+ if orig_dispatch_block:
621
+ if not self.go_to_transferring and not self.log_transferring and destination_dispatch_block in id_map:
622
+ del id_map[destination_dispatch_block]
623
+
624
+ # print idMap
625
+ self.logger.debug(f"dataset to files map = {id_map}")
626
+ self.logger.debug(f"transfer rules = {sub_map}")
627
+ self.logger.debug(f"dataset to destination map = {dataset_destination_map}")
628
+ self.logger.debug(f"extra info = {str(self.extra_info)}")
629
+ self.logger.debug(f"""alternatively staged files = {",".join(list(alt_staged_files)) if alt_staged_files else "NA"}""")
630
+
631
+ # check consistency of destinationDBlock
632
+ has_sub = False
633
+ for destination_dispatch_block in id_map:
634
+ match = re.search("^(.+)_sub\d+$", destination_dispatch_block)
635
+ if match is not None:
636
+ has_sub = True
637
+ break
638
+ if id_map and self.go_to_transferring and not has_sub:
639
+ err_str = "no sub datasets for transferring. destinationDBlock may be wrong"
640
+ self.logger.error(err_str)
641
+ self.result.set_fatal()
642
+ self.job.ddmErrorDiag = "failed before adding files : " + err_str
643
+ return 1
644
+
645
+ # add data
646
+ self.logger.debug("addFiles start")
647
+ # count the number of files
648
+ reg_file_list = {tmp_reg_item["lfn"] for tmp_reg_list in id_map.values() for tmp_reg_item in tmp_reg_list}
649
+ reg_num_files = len(reg_file_list)
650
+
651
+ # decompose idMap
652
+ if self.add_to_top_only:
653
+ dest_id_map = {None: id_map}
654
+ else:
655
+ dest_id_map = self.decompose_id_map(id_map, dataset_destination_map, map_for_alt_stage_out, sub_to_ds_map, alt_staged_files)
656
+
657
+ # add files
658
+ result = self.register_files(reg_num_files, zip_files, dest_id_map, cont_zip_map)
659
+ if result == 1:
660
+ return 1
661
+
662
+ # release some memory
663
+ del dest_id_map
664
+ del dataset_destination_map
665
+ del map_for_alt_stage_out
666
+ del zip_files
667
+ del cont_zip_map
668
+ gc.collect()
669
+
670
+ if self.job.processingType == "urgent" or self.job.currentPriority > 1000:
671
+ sub_activity = "Express"
672
+ else:
673
+ sub_activity = "Production Output"
674
+
675
+ # register dataset subscription
676
+ self.process_subscriptions(sub_map, sub_to_ds_map, dist_datasets, sub_activity)
677
+
678
+ # collect list of merging files
679
+ if self.go_to_merging and self.job_status not in ["failed", "cancelled", "closed"]:
680
+ for tmp_file_list in id_map.values():
681
+ for tmp_file in tmp_file_list:
682
+ if tmp_file["lfn"] not in self.result.merging_files:
683
+ self.result.merging_files.append(tmp_file["lfn"])
684
+
685
+ # register ES files
686
+ if (EventServiceUtils.isEventServiceJob(self.job) or EventServiceUtils.isJumboJob(self.job)) and not EventServiceUtils.isJobCloningJob(self.job):
687
+ if self.job.registerEsFiles():
688
+ try:
689
+ self.register_event_service_files()
690
+ except Exception:
691
+ err_type, err_value = sys.exc_info()[:2]
692
+ self.logger.error(f"failed to register ES files with {err_type}:{err_value}")
693
+ self.result.set_temporary()
694
+ return 1
695
+
696
+ # properly finished
697
+ self.logger.debug("addFiles end")
698
+ return 0
699
+
700
+ def register_files(self, reg_num_files: int, zip_files: list, dest_id_map: dict, cont_zip_map: dict) -> int | None:
701
+ """
702
+ Register files with Rucio.
703
+
704
+ :param reg_num_files: Number of files to register.
705
+ :param zip_files: List of zip files to register.
706
+ :param dest_id_map: Destination ID map.
707
+ :param cont_zip_map: Container zip map.
708
+ :return: 1 if registration fails, None otherwise.
709
+ """
710
+ max_attempt = 3
711
+ for attempt_number in range(max_attempt):
712
+ is_fatal = False
713
+ is_failed = False
714
+ reg_start = naive_utcnow()
715
+ try:
716
+ if self.add_to_top_only:
717
+ reg_msg_str = f"File registration for {reg_num_files} files "
718
+ else:
719
+ reg_msg_str = f"File registration with rucio for {reg_num_files} files "
720
+ if len(zip_files) > 0:
721
+ self.logger.debug(f"registerZipFiles {str(zip_files)}")
722
+ rucioAPI.register_zip_files(zip_files)
723
+ self.logger.debug(f"registerFilesInDatasets {str(dest_id_map)} zip={str(cont_zip_map)}")
724
+ out = rucioAPI.register_files_in_dataset(dest_id_map, cont_zip_map)
725
+ except (
726
+ DataIdentifierNotFound,
727
+ FileConsistencyMismatch,
728
+ UnsupportedOperation,
729
+ InvalidPath,
730
+ InvalidObject,
731
+ RSENotFound,
732
+ RSEProtocolNotSupported,
733
+ InvalidRSEExpression,
734
+ KeyError,
735
+ ):
736
+ # fatal errors
737
+ err_type, err_value = sys.exc_info()[:2]
738
+ out = f"{err_type} : {err_value}"
739
+ out += traceback.format_exc()
740
+ is_fatal = True
741
+ is_failed = True
742
+ except Exception:
743
+ # unknown errors
744
+ err_type, err_value = sys.exc_info()[:2]
745
+ out = f"{err_type} : {err_value}"
746
+ out += traceback.format_exc()
747
+ is_fatal = (
748
+ "value too large for column" in out
749
+ or "unique constraint (ATLAS_RUCIO.DIDS_GUID_IDX) violate" in out
750
+ or "unique constraint (ATLAS_RUCIO.DIDS_PK) violated" in out
751
+ or "unique constraint (ATLAS_RUCIO.ARCH_CONTENTS_PK) violated" in out
752
+ )
753
+ is_failed = True
754
+ reg_time = naive_utcnow() - reg_start
755
+ self.logger.debug(f"{reg_msg_str} took {reg_time.seconds}.{reg_time.microseconds // 1000:03d} sec")
756
+ # failed
757
+ if is_failed or is_fatal:
758
+ self.logger.error(f"{out}")
759
+ if (attempt_number + 1) == max_attempt or is_fatal:
760
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
761
+ # extract important error string
762
+ extracted_err_str = DataServiceUtils.extractImportantError(out)
763
+ err_msg = "Could not add files to DDM: "
764
+ if extracted_err_str == "":
765
+ self.job.ddmErrorDiag = err_msg + out.split("\n")[-1]
766
+ else:
767
+ self.job.ddmErrorDiag = err_msg + extracted_err_str
768
+ if is_fatal:
769
+ self.result.set_fatal()
770
+ else:
771
+ self.result.set_temporary()
772
+ return 1
773
+ self.logger.error(f"Try:{attempt_number}")
774
+ # sleep
775
+ time.sleep(10)
776
+ else:
777
+ self.logger.debug(f"{str(out)}")
778
+ break
779
+
780
+ def process_subscriptions(self, sub_map: Dict[str, str], sub_to_ds_map: Dict[str, List[str]], dist_datasets: List[str], sub_activity: str):
781
+ """
782
+ Process the subscriptions for the job.
783
+
784
+ This method handles the processing of subscriptions, including keeping subscriptions,
785
+ collecting transferring jobs, and sending requests to DaTRI.
786
+
787
+ :param sub_map: A dictionary mapping subscription names to their values.
788
+ :param sub_to_ds_map: A dictionary mapping subscriptions to datasets.
789
+ :param dist_datasets: A list of distributed datasets.
790
+ :param sub_activity: The subscription activity type.
791
+ """
792
+ if self.job.prodSourceLabel not in ["user"]:
793
+ for tmp_name, tmp_val in sub_map.items():
794
+ for ddm_id, opt_sub, opt_source in tmp_val:
795
+ if not self.go_to_merging:
796
+ # make subscription for prod jobs
797
+ rep_life_time = 14
798
+ self.logger.debug(
799
+ f"registerDatasetSubscription {tmp_name} {ddm_id} {{'activity': {sub_activity}, 'replica_lifetime': {rep_life_time}}}"
800
+ )
801
+ for _ in range(3):
802
+ is_failed = False
803
+ try:
804
+ status = rucioAPI.register_dataset_subscription(
805
+ tmp_name,
806
+ [ddm_id],
807
+ owner="panda",
808
+ activity=sub_activity,
809
+ lifetime=rep_life_time,
810
+ )
811
+ out = "OK"
812
+ break
813
+ except InvalidRSEExpression:
814
+ status = False
815
+ err_type, err_value = sys.exc_info()[:2]
816
+ out = f"{err_type} {err_value}"
817
+ is_failed = True
818
+ self.job.ddmErrorCode = ErrorCode.EC_Subscription
819
+ break
820
+ except Exception:
821
+ status = False
822
+ err_type, err_value = sys.exc_info()[:2]
823
+ out = f"{err_type} {err_value}"
824
+ is_failed = True
825
+ # retry for temporary errors
826
+ time.sleep(10)
827
+ if is_failed:
828
+ self.logger.error(f"{out}")
829
+ if self.job.ddmErrorCode == ErrorCode.EC_Subscription:
830
+ # fatal error
831
+ self.job.ddmErrorDiag = f"subscription failure with {out}"
832
+ self.result.set_fatal()
833
+ else:
834
+ # temporary errors
835
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
836
+ self.job.ddmErrorDiag = f"could not register subscription : {tmp_name}"
837
+ self.result.set_temporary()
838
+ return 1
839
+ self.logger.debug(f"{str(out)}")
840
+ else:
841
+ # register location
842
+ tmp_dataset_name_location = sub_to_ds_map[tmp_name]
843
+ rep_life_time = 14
844
+ for tmp_location_name in opt_source:
845
+ self.logger.debug(f"registerDatasetLocation {tmp_dataset_name_location} {tmp_location_name} {{'lifetime': '14 days'}}")
846
+
847
+ for _ in range(3):
848
+ is_failed = False
849
+ try:
850
+ rucioAPI.register_dataset_location(
851
+ tmp_dataset_name_location,
852
+ [tmp_location_name],
853
+ owner="panda",
854
+ activity=sub_activity,
855
+ lifetime=rep_life_time,
856
+ )
857
+ out = "OK"
858
+ break
859
+ except Exception:
860
+ status = False
861
+ err_type, err_value = sys.exc_info()[:2]
862
+ out = f"{err_type} {err_value}"
863
+ is_failed = True
864
+ # retry for temporary errors
865
+ time.sleep(10)
866
+ if is_failed:
867
+ self.logger.error(f"{out}")
868
+ if self.job.ddmErrorCode == ErrorCode.EC_Location:
869
+ # fatal error
870
+ self.job.ddmErrorDiag = f"location registration failure with {out}"
871
+ self.result.set_fatal()
872
+ else:
873
+ # temporary errors
874
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
875
+ self.job.ddmErrorDiag = f"could not register location : {tmp_dataset_name_location}"
876
+ self.result.set_temporary()
877
+ return 1
878
+ self.logger.debug(f"{str(out)}")
879
+
880
+ # set dataset status
881
+ self.dataset_map[tmp_name].status = "running"
882
+
883
+ # keep subscriptions
884
+ self.subscription_map = sub_map
885
+
886
+ # collect list of transferring jobs
887
+ for tmp_file in self.job.Files:
888
+ if tmp_file.type in ["log", "output"]:
889
+ if self.go_to_transferring or (self.log_transferring and tmp_file.type == "log"):
890
+ # don't go to transferring for successful ES jobs
891
+ if (
892
+ self.job.jobStatus == "finished"
893
+ and (EventServiceUtils.isEventServiceJob(self.job) and EventServiceUtils.isJumboJob(self.job))
894
+ and not EventServiceUtils.isJobCloningJob(self.job)
895
+ ):
896
+ continue
897
+ # skip distributed datasets
898
+ if tmp_file.destinationDBlock in dist_datasets:
899
+ continue
900
+ # skip no output
901
+ if tmp_file.status == "nooutput":
902
+ continue
903
+ # skip alternative stage-out
904
+ if tmp_file.lfn in self.job.altStgOutFileList():
905
+ continue
906
+ self.result.transferring_files.append(tmp_file.lfn)
907
+
908
+ elif "--mergeOutput" not in self.job.jobParameters:
909
+ # send request to DaTRI unless files will be merged
910
+ tmp_top_datasets = {}
911
+ # collect top-level datasets
912
+ for tmp_name, tmp_val in sub_map.items():
913
+ for ddm_id, opt_sub, opt_source in tmp_val:
914
+ tmp_top_name = sub_to_ds_map[tmp_name]
915
+ # append
916
+ if tmp_top_name not in tmp_top_datasets:
917
+ tmp_top_datasets[tmp_top_name] = []
918
+ if ddm_id not in tmp_top_datasets[tmp_top_name]:
919
+ tmp_top_datasets[tmp_top_name].append(ddm_id)
920
+ # remove redundant CN from DN
921
+ tmp_dn = self.job.prodUserID
922
+ # send request
923
+ if tmp_top_datasets and self.job_status == "finished":
924
+ try:
925
+ status, user_info = rucioAPI.finger(tmp_dn)
926
+ if not status:
927
+ raise RuntimeError(f"user info not found for {tmp_dn} with {user_info}")
928
+ user_endpoints = []
929
+ # loop over all output datasets
930
+ for tmp_dataset_name, ddm_id_list in tmp_top_datasets.items():
931
+ for tmp_ddm_id in ddm_id_list:
932
+ if tmp_ddm_id == "NULL":
933
+ continue
934
+ if tmp_ddm_id not in user_endpoints:
935
+ user_endpoints.append(tmp_ddm_id)
936
+ # use group account for group.*
937
+ if tmp_dataset_name.startswith("group") and self.job.workingGroup not in ["", "NULL", None]:
938
+ tmp_dn = self.job.workingGroup
939
+ else:
940
+ tmp_dn = user_info["nickname"]
941
+ tmp_msg = f"registerDatasetLocation for Rucio ds={tmp_dataset_name} site={tmp_ddm_id} id={tmp_dn}"
942
+ self.logger.debug(tmp_msg)
943
+ rucioAPI.register_dataset_location(
944
+ tmp_dataset_name,
945
+ [tmp_ddm_id],
946
+ owner=tmp_dn,
947
+ activity="Analysis Output",
948
+ )
949
+ # set dataset status
950
+ for tmp_name in sub_map:
951
+ self.dataset_map[tmp_name].status = "running"
952
+ except (InsufficientAccountLimit, InvalidRSEExpression) as err_type:
953
+ tmp_msg = f"Rucio rejected to transfer files to {','.join(user_endpoints)} since {err_type}"
954
+ self.logger.error(tmp_msg)
955
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
956
+ self.job.ddmErrorDiag = f"Rucio failed with {err_type}"
957
+ # set dataset status
958
+ for tmp_name in sub_map:
959
+ self.dataset_map[tmp_name].status = "running"
960
+ # send warning
961
+ tmp_st = self.taskBuffer.update_problematic_resource_info(self.job.prodUserName, self.job.jediTaskID, user_endpoints[0], "dest")
962
+ if not tmp_st:
963
+ self.logger.debug("skip to send warning since already done")
964
+ else:
965
+ to_adder = self.taskBuffer.getEmailAddr(self.job.prodUserName)
966
+ if to_adder is None or to_adder.startswith("notsend"):
967
+ self.logger.debug("skip to send warning since suppressed")
968
+ else:
969
+ tmp_sm = self.send_email(to_adder, tmp_msg, self.job.jediTaskID)
970
+ self.logger.debug(f"sent warning with {tmp_sm}")
971
+ except Exception:
972
+ err_type, err_value = sys.exc_info()[:2]
973
+ tmp_msg = f"registerDatasetLocation failed with {err_type} {err_value}"
974
+ self.logger.error(tmp_msg)
975
+ self.job.ddmErrorCode = ErrorCode.EC_Adder
976
+ self.job.ddmErrorDiag = f"Rucio failed with {err_type} {err_value}"
977
+
978
+ # decompose idMap
979
+ def decompose_id_map(self, id_map, dataset_destination_map, map_for_alt_stage_out, sub_to_dataset_map, alt_staged_files):
980
+ """
981
+ Decompose the idMap into a structure suitable for file registration.
982
+
983
+ Args:
984
+ id_map (dict): A dictionary mapping dataset names to file attributes.
985
+ dataset_destination_map (dict): A dictionary mapping dataset names to destination sites.
986
+ map_for_alt_stage_out (dict): A dictionary mapping alternative destination sites to file attributes.
987
+ sub_to_dataset_map (dict): A dictionary mapping sub-dataset names to top-level dataset names.
988
+ alt_staged_files (set): A set of files uploaded with alternative stage-out.
989
+
990
+ Returns:
991
+ dict: A decomposed idMap suitable for file registration.
992
+ """
993
+ # add item for top datasets
994
+ for tmp_dataset in list(dataset_destination_map):
995
+ tmp_top_dataset = sub_to_dataset_map[tmp_dataset]
996
+ if tmp_top_dataset != tmp_dataset:
997
+ dataset_destination_map[tmp_top_dataset] = dataset_destination_map[tmp_dataset]
998
+
999
+ destination_id_map = {}
1000
+ for tmp_dataset in id_map:
1001
+ # exclude files uploaded with alternative stage-out
1002
+ tmp_files = [f for f in id_map[tmp_dataset] if f["lfn"] not in alt_staged_files]
1003
+ if not tmp_files:
1004
+ continue
1005
+ for tmp_dest in dataset_destination_map[tmp_dataset]:
1006
+ if tmp_dest not in destination_id_map:
1007
+ destination_id_map[tmp_dest] = {}
1008
+ destination_id_map[tmp_dest][tmp_dataset] = tmp_files
1009
+
1010
+ # add alternative stage-out info
1011
+ for tmp_dest in map_for_alt_stage_out:
1012
+ tmp_id_map = map_for_alt_stage_out[tmp_dest]
1013
+ for tmp_dataset in tmp_id_map:
1014
+ tmp_files = tmp_id_map[tmp_dataset]
1015
+ if tmp_dest not in destination_id_map:
1016
+ destination_id_map[tmp_dest] = {}
1017
+ destination_id_map[tmp_dest][tmp_dataset] = tmp_files
1018
+ return destination_id_map
1019
+
1020
+ # send email notification
1021
+ def send_email(self, to_adder, message, jedi_task_id):
1022
+ """
1023
+ Send an email notification.
1024
+
1025
+ Args:
1026
+ to_adder (str): The email address to send the notification to.
1027
+ message (str): The message body of the email.
1028
+ jedi_task_id (int): The JEDI task ID associated with the email.
1029
+
1030
+ Returns:
1031
+ bool: True if the email was sent successfully, False otherwise.
1032
+ """
1033
+ # subject
1034
+ mail_subject = f"PANDA WARNING for TaskID:{jedi_task_id} with --destSE"
1035
+ # message
1036
+ mail_body = f"Hello,\n\nTaskID:{jedi_task_id} cannot process the --destSE request\n\n"
1037
+ mail_body += f"Reason : {message}\n"
1038
+ # send
1039
+ ret_val = MailUtils().send(to_adder, mail_subject, mail_body)
1040
+ # return
1041
+ return ret_val
1042
+
1043
+ # register ES files
1044
+ def register_event_service_files(self) -> None:
1045
+ """
1046
+ Register Event Service (ES) files with Rucio.
1047
+
1048
+ Raises:
1049
+ Exception: If there is an error during the registration process.
1050
+ """
1051
+ self.logger.debug("registering ES files")
1052
+ try:
1053
+ # get ES dataset name
1054
+ event_service_dataset = EventServiceUtils.getEsDatasetName(self.job.jediTaskID)
1055
+ # collect files
1056
+ id_map = {}
1057
+ file_set = set()
1058
+ for file_spec in self.job.Files:
1059
+ if file_spec.type != "zipoutput":
1060
+ continue
1061
+ if file_spec.lfn in file_set:
1062
+ continue
1063
+ file_set.add(file_spec.lfn)
1064
+ # make file data
1065
+ file_data = {
1066
+ "scope": EventServiceUtils.esScopeDDM,
1067
+ "name": file_spec.lfn,
1068
+ "bytes": file_spec.fsize,
1069
+ "panda_id": file_spec.PandaID,
1070
+ "task_id": file_spec.jediTaskID,
1071
+ }
1072
+ if file_spec.GUID not in [None, "NULL", ""]:
1073
+ file_data["guid"] = file_spec.GUID
1074
+ if file_spec.dispatchDBlockToken not in [None, "NULL", ""]:
1075
+ try:
1076
+ file_data["events"] = int(file_spec.dispatchDBlockToken)
1077
+ except Exception:
1078
+ pass
1079
+ if file_spec.checksum not in [None, "NULL", ""]:
1080
+ file_data["checksum"] = file_spec.checksum
1081
+ # get endpoint ID
1082
+ endpoint_id = int(file_spec.destinationSE.split("/")[0])
1083
+ # convert to DDM endpoint
1084
+ rse = self.taskBuffer.convertObjIDtoEndPoint(panda_config.endpoint_mapfile, endpoint_id)
1085
+ if rse is not None and rse["is_deterministic"]:
1086
+ endpoint_name = rse["name"]
1087
+ if endpoint_name not in id_map:
1088
+ id_map[endpoint_name] = {}
1089
+ if event_service_dataset not in id_map[endpoint_name]:
1090
+ id_map[endpoint_name][event_service_dataset] = []
1091
+ id_map[endpoint_name][event_service_dataset].append(file_data)
1092
+
1093
+ # add files to dataset
1094
+ if id_map:
1095
+ self.logger.debug(f"adding ES files {str(id_map)}")
1096
+ try:
1097
+ rucioAPI.register_files_in_dataset(id_map)
1098
+ except DataIdentifierNotFound:
1099
+ self.logger.debug("ignored DataIdentifierNotFound")
1100
+ except Exception:
1101
+ err_type, err_value = sys.exc_info()[:2]
1102
+ err_str = f" : {err_type} {err_value}"
1103
+ err_str += traceback.format_exc()
1104
+ self.logger.error(err_str)
1105
+ raise
1106
+ self.logger.debug("done")