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,1944 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ from pandacommon.pandalogger.LogWrapper import LogWrapper
7
+ from pandacommon.pandautils.PandaUtils import get_sql_IN_bind_variables, naive_utcnow
8
+
9
+ from pandaserver.config import panda_config
10
+ from pandaserver.srvcore import CoreUtils
11
+ from pandaserver.srvcore.CoreUtils import clean_host_name
12
+ from pandaserver.taskbuffer import ErrorCode, SiteSpec
13
+ from pandaserver.taskbuffer.db_proxy_mods.base_module import BaseModule, memoize
14
+ from pandaserver.taskbuffer.db_proxy_mods.entity_module import get_entity_module
15
+ from pandaserver.taskbuffer.HarvesterMetricsSpec import HarvesterMetricsSpec
16
+ from pandaserver.taskbuffer.JobSpec import JobSpec
17
+ from pandaserver.taskbuffer.ResourceSpec import BASIC_RESOURCE_TYPE
18
+ from pandaserver.taskbuffer.WorkerSpec import WorkerSpec
19
+
20
+ DEFAULT_PRODSOURCELABEL = "managed"
21
+
22
+
23
+ # Module class to define methods related to worker and harvester
24
+ class WorkerModule(BaseModule):
25
+ # constructor
26
+ def __init__(self, log_stream: LogWrapper):
27
+ super().__init__(log_stream)
28
+
29
+ # update stat of workers with jobtype breakdown
30
+ def reportWorkerStats_jobtype(self, harvesterID, siteName, parameter_list):
31
+ comment = " /* DBProxy.reportWorkerStats_jobtype */"
32
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID} siteName={siteName}")
33
+ tmp_log.debug("start")
34
+ tmp_log.debug(f"params={str(parameter_list)}")
35
+ try:
36
+ # load new site data
37
+ parameter_list = json.loads(parameter_list)
38
+ # set autocommit on
39
+ self.conn.begin()
40
+
41
+ # lock the site data rows
42
+ var_map = {":harvesterID": harvesterID, ":siteName": siteName}
43
+ sql_lock = (
44
+ "SELECT harvester_ID, computingSite FROM ATLAS_PANDA.Harvester_Worker_Stats "
45
+ "WHERE harvester_ID=:harvesterID AND computingSite=:siteName FOR UPDATE NOWAIT "
46
+ )
47
+ try:
48
+ self.cur.execute(sql_lock + comment, var_map)
49
+ except Exception:
50
+ self._rollback()
51
+ message = "rows locked by another update"
52
+ tmp_log.debug(message)
53
+ tmp_log.debug("done")
54
+ return False, message
55
+
56
+ # delete them
57
+ sql_delete = "DELETE FROM ATLAS_PANDA.Harvester_Worker_Stats WHERE harvester_ID=:harvesterID AND computingSite=:siteName "
58
+ self.cur.execute(sql_delete + comment, var_map)
59
+
60
+ # insert new site data
61
+ sql_insert = (
62
+ "INSERT INTO ATLAS_PANDA.Harvester_Worker_Stats (harvester_ID, computingSite, jobType, resourceType, status, n_workers, lastUpdate) "
63
+ "VALUES (:harvester_ID, :siteName, :jobType, :resourceType, :status, :n_workers, CURRENT_DATE) "
64
+ )
65
+
66
+ var_map_list = []
67
+ for jobType in parameter_list:
68
+ jt_params = parameter_list[jobType]
69
+ for resourceType in jt_params:
70
+ params = jt_params[resourceType]
71
+ if resourceType == "Undefined":
72
+ continue
73
+ for status in params:
74
+ n_workers = params[status]
75
+ var_map = {
76
+ ":harvester_ID": harvesterID,
77
+ ":siteName": siteName,
78
+ ":status": status,
79
+ ":jobType": jobType,
80
+ ":resourceType": resourceType,
81
+ ":n_workers": n_workers,
82
+ }
83
+ var_map_list.append(var_map)
84
+
85
+ self.cur.executemany(sql_insert + comment, var_map_list)
86
+
87
+ if not self._commit():
88
+ raise RuntimeError("Commit error")
89
+
90
+ tmp_log.debug("done")
91
+ return True, "OK"
92
+ except Exception as e:
93
+ self._rollback()
94
+ self.dump_error_message(tmp_log)
95
+ return False, "database error"
96
+
97
+ # get stat of workers
98
+ def getWorkerStats(self):
99
+ comment = " /* DBProxy.getWorkerStats */"
100
+ tmp_log = self.create_tagged_logger(comment)
101
+ tmp_log.debug("start")
102
+ try:
103
+ # set autocommit on
104
+ self.conn.begin()
105
+
106
+ # sql to get stat of workers
107
+ sql_get_stats = (
108
+ "SELECT SUM(n_workers), computingSite, harvester_ID, jobType, resourceType, status "
109
+ "FROM ATLAS_PANDA.Harvester_Worker_Stats "
110
+ "WHERE lastUpdate>=:time_limit "
111
+ "GROUP BY computingSite,harvester_ID,jobType,resourceType,status "
112
+ )
113
+ var_map = {
114
+ ":time_limit": naive_utcnow() - datetime.timedelta(hours=4),
115
+ }
116
+ self.cur.execute(sql_get_stats + comment, var_map)
117
+ res_active = self.cur.fetchall()
118
+ result_map = {}
119
+ for cnt, computingSite, harvesterID, jobType, resourceType, status in res_active:
120
+ result_map.setdefault(computingSite, {})
121
+ result_map[computingSite].setdefault(harvesterID, {})
122
+ result_map[computingSite][harvesterID].setdefault(jobType, {})
123
+ if resourceType not in result_map[computingSite][harvesterID][jobType]:
124
+ result_map[computingSite][harvesterID][jobType][resourceType] = dict()
125
+ result_map[computingSite][harvesterID][jobType][resourceType][status] = cnt
126
+ # commit
127
+ if not self._commit():
128
+ raise RuntimeError("Commit error")
129
+ # return
130
+ tmp_log.debug(f"done with {str(result_map)}")
131
+ return result_map
132
+ except Exception:
133
+ # roll back
134
+ self._rollback()
135
+ self.dump_error_message(tmp_log)
136
+ return {}
137
+
138
+ # send command to harvester or lock command
139
+ def commandToHarvester(
140
+ self,
141
+ harvester_ID,
142
+ command,
143
+ ack_requested,
144
+ status,
145
+ lockInterval,
146
+ comInterval,
147
+ params,
148
+ useCommit=True,
149
+ ):
150
+ comment = " /* DBProxy.commandToHarvester */"
151
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvester_ID} command={command}")
152
+ tmp_log.debug("start")
153
+ tmp_log.debug(f"params={str(params)}")
154
+ try:
155
+ if useCommit:
156
+ self.conn.begin()
157
+ # check if command exists
158
+ sql_check_command = "SELECT status,status_date FROM ATLAS_PANDA.HARVESTER_COMMANDS WHERE harvester_ID=:harvester_ID AND command=:command "
159
+ var_map = {
160
+ ":harvester_ID": harvester_ID,
161
+ ":command": command,
162
+ }
163
+ self.cur.execute(sql_check_command + comment, var_map)
164
+ existing_command = self.cur.fetchone()
165
+ # check existing command
166
+ to_skip = False
167
+ if existing_command is not None:
168
+ command_status, status_date = existing_command
169
+ # not overwrite existing command
170
+ if (
171
+ command_status in ["new", "lock", "retrieved"]
172
+ and lockInterval is not None
173
+ and status_date > naive_utcnow() - datetime.timedelta(minutes=lockInterval)
174
+ ):
175
+ to_skip = True
176
+ elif (
177
+ command_status in ["retrieved", "acknowledged"]
178
+ and comInterval is not None
179
+ and status_date > naive_utcnow() - datetime.timedelta(minutes=comInterval)
180
+ ):
181
+ to_skip = True
182
+ else:
183
+ # delete existing command
184
+ sql_delete_command = "DELETE FROM ATLAS_PANDA.HARVESTER_COMMANDS WHERE harvester_ID=:harvester_ID AND command=:command "
185
+ var_map = {
186
+ ":harvester_ID": harvester_ID,
187
+ ":command": command,
188
+ }
189
+ self.cur.execute(sql_delete_command + comment, var_map)
190
+ # insert
191
+ if not to_skip:
192
+ var_map = {
193
+ ":harvester_id": harvester_ID,
194
+ ":command": command,
195
+ ":ack_requested": 1 if ack_requested else 0,
196
+ ":status": status,
197
+ }
198
+ sql_insert_command = (
199
+ "INSERT INTO ATLAS_PANDA.HARVESTER_COMMANDS (command_id, creation_date, status_date, command, harvester_id, ack_requested, status"
200
+ )
201
+ if params is not None:
202
+ var_map[":params"] = json.dumps(params)
203
+ sql_insert_command += ",params"
204
+ sql_insert_command += ") "
205
+ sql_insert_command += (
206
+ "VALUES (ATLAS_PANDA.HARVESTER_COMMAND_ID_SEQ.nextval, CURRENT_DATE, CURRENT_DATE, :command, :harvester_id, :ack_requested, :status"
207
+ )
208
+ if params is not None:
209
+ sql_insert_command += ",:params"
210
+ sql_insert_command += ") "
211
+ self.cur.execute(sql_insert_command + comment, var_map)
212
+ if useCommit:
213
+ if not self._commit():
214
+ raise RuntimeError("Commit error")
215
+ tmp_log.debug("done")
216
+ if to_skip:
217
+ return False
218
+ return True
219
+ except Exception:
220
+ # roll back
221
+ if useCommit:
222
+ self._rollback()
223
+ self.dump_error_message(tmp_log)
224
+ return False
225
+
226
+ # send command to harvester to kill all workers
227
+ def sweepPQ(self, panda_queue_des, status_list_des, ce_list_des, submission_host_list_des):
228
+ comment = " /* DBProxy.sweepPQ */"
229
+ tmp_log = self.create_tagged_logger(comment)
230
+ tmp_log.debug("start")
231
+ try:
232
+ # Figure out the harvester instance serving the queues and check the CEs match
233
+ pq_data_des = get_entity_module(self).get_config_for_pq(panda_queue_des)
234
+ if not pq_data_des:
235
+ return "Error retrieving queue configuration from DB"
236
+
237
+ harvester_id = pq_data_des["harvester"]
238
+ if not harvester_id:
239
+ return "Queue not served by any harvester ID"
240
+
241
+ # check CEs
242
+ if ce_list_des == "ALL":
243
+ ce_list_des_sanitized = "ALL"
244
+ else:
245
+ computing_elements = pq_data_des["queues"]
246
+ ce_names = [str(ce["ce_endpoint"]) for ce in computing_elements]
247
+ ce_list_des_sanitized = [ce for ce in ce_list_des if ce in ce_names]
248
+
249
+ # we can't correct submission hosts or the status list
250
+
251
+ command = "KILL_WORKERS"
252
+ ack_requested = False
253
+ status = "new"
254
+ lock_interval = None
255
+ com_interval = None
256
+ params = {
257
+ "status": status_list_des,
258
+ "computingSite": [panda_queue_des],
259
+ "computingElement": ce_list_des_sanitized,
260
+ "submissionHost": submission_host_list_des,
261
+ }
262
+
263
+ self.commandToHarvester(
264
+ harvester_id,
265
+ command,
266
+ ack_requested,
267
+ status,
268
+ lock_interval,
269
+ com_interval,
270
+ params,
271
+ )
272
+
273
+ tmp_log.debug("done")
274
+ return "OK"
275
+
276
+ except Exception:
277
+ self.dump_error_message(tmp_log)
278
+ return "Problem generating command. Check PanDA server logs"
279
+
280
+ def get_average_memory_workers(self, queue, harvester_id, target):
281
+ """
282
+ Calculates the average memory for running and queued workers at a particular panda queue
283
+
284
+ :param queue: name of the PanDA queue
285
+ :param harvester_id: string with the harvester ID serving the queue
286
+ :param target: memory target for the queue in MB. This value is only used in the logging
287
+
288
+ :return: average_memory_running_submitted, average_memory_running
289
+ """
290
+
291
+ comment = " /* DBProxy.get_average_memory_workers */"
292
+ tmp_log = self.create_tagged_logger(comment)
293
+ tmp_log.debug("start")
294
+
295
+ try:
296
+ # sql to calculate the average memory for the queue - harvester_id combination
297
+ # "* 1" in sj.data.blah * 1 is required to notify postgres the data type is an int since json element is
298
+ # treated as text otherwise. This is needed only for the first occurrence of each element in the query
299
+ per_core_attr = SiteSpec.catchall_keys["per_core_attr"]
300
+ sql_running_and_submitted = (
301
+ "SELECT /*+ RESULT_CACHE */ /* use_json_type */ sum(total_memory) / NULLIF(sum(n_workers * corecount), 0) "
302
+ "FROM ( "
303
+ " SELECT hws.computingsite, "
304
+ " hws.harvester_id, "
305
+ " hws.n_workers, "
306
+ " hws.n_workers * NVL(rt.maxcore, NVL(sj.data.corecount * 1, 1)) * NVL(rt.maxrampercore, "
307
+ f" CASE WHEN sj.data.catchall LIKE '%{per_core_attr}%' THEN TO_NUMBER(sj.data.maxrss) ELSE sj.data.maxrss * 1 / NVL(sj.data.corecount, 1) END) as total_memory, "
308
+ " NVL(rt.maxcore, NVL(sj.data.corecount, 1)) as corecount "
309
+ " FROM ATLAS_PANDA.harvester_worker_stats hws "
310
+ " JOIN ATLAS_PANDA.resource_types rt ON hws.resourcetype = rt.resource_name "
311
+ " JOIN ATLAS_PANDA.schedconfig_json sj ON hws.computingsite = sj.panda_queue "
312
+ " WHERE lastupdate > :time_limit "
313
+ " AND status IN ('running', 'submitted', 'to_submit') "
314
+ " AND computingsite=:queue AND harvester_id=:harvester_id"
315
+ ") GROUP BY computingsite, harvester_id "
316
+ )
317
+
318
+ sql_running = (
319
+ "SELECT /*+ RESULT_CACHE */ /* use_json_type */ sum(total_memory) / NULLIF(sum(n_workers * corecount), 0) "
320
+ "FROM ( "
321
+ " SELECT hws.computingsite, "
322
+ " hws.harvester_id, "
323
+ " hws.n_workers, "
324
+ " hws.n_workers * NVL(rt.maxcore, NVL(sj.data.corecount * 1, 1)) * NVL(rt.maxrampercore, "
325
+ f" CASE WHEN sj.data.catchall LIKE '%{per_core_attr}%' THEN TO_NUMBER(sj.data.maxrss) ELSE sj.data.maxrss * 1 / NVL(sj.data.corecount, 1) END) as total_memory, "
326
+ " NVL(rt.maxcore, NVL(sj.data.corecount, 1)) as corecount "
327
+ " FROM ATLAS_PANDA.harvester_worker_stats hws "
328
+ " JOIN ATLAS_PANDA.resource_types rt ON hws.resourcetype = rt.resource_name "
329
+ " JOIN ATLAS_PANDA.schedconfig_json sj ON hws.computingsite = sj.panda_queue "
330
+ " WHERE lastupdate > :time_limit "
331
+ " AND status = 'running' "
332
+ " AND computingsite=:queue AND harvester_id=:harvester_id"
333
+ ") GROUP BY computingsite, harvester_id "
334
+ )
335
+
336
+ # bind variables including truncated time_limit for result cache
337
+ var_map = {
338
+ ":queue": queue,
339
+ ":harvester_id": harvester_id,
340
+ ":time_limit": (naive_utcnow() - datetime.timedelta(hours=1)).replace(second=0, microsecond=0),
341
+ }
342
+
343
+ self.cur.execute(sql_running_and_submitted + comment, var_map)
344
+ results = self.cur.fetchone()
345
+ try:
346
+ average_memory_running_submitted = results[0] if results[0] is not None else 0
347
+ except TypeError:
348
+ average_memory_running_submitted = 0
349
+
350
+ self.cur.execute(sql_running + comment, var_map)
351
+ results = self.cur.fetchone()
352
+ try:
353
+ average_memory_running = results[0] if results[0] is not None else 0
354
+ except TypeError:
355
+ average_memory_running = 0
356
+
357
+ tmp_log.info(
358
+ f"computingsite={queue} and harvester_id={harvester_id} currently has "
359
+ f"meanrss_running_submitted={average_memory_running_submitted} "
360
+ f"meanrss_running={average_memory_running} "
361
+ f"meanrss_target={target} MB"
362
+ )
363
+ return average_memory_running_submitted, average_memory_running
364
+
365
+ except Exception:
366
+ self.dump_error_message(tmp_log)
367
+ return 0, 0
368
+
369
+ def ups_new_worker_distribution(self, queue, worker_stats):
370
+ """
371
+ Assuming we want to have n_cores_queued >= n_cores_running * .5, calculate how many pilots need to be submitted
372
+ and choose the number
373
+
374
+ :param queue: name of the PanDA queue
375
+ :param worker_stats: queue worker stats
376
+ :return:
377
+ """
378
+
379
+ comment = " /* DBProxy.ups_new_worker_distribution */"
380
+ tmp_log = self.create_tagged_logger(comment, queue)
381
+ tmp_log.debug("start")
382
+ n_cores_running = 0
383
+ workers_queued = {}
384
+ n_cores_queued = 0
385
+ harvester_ids_temp = list(worker_stats)
386
+
387
+ HIMEM = "HIMEM"
388
+ get_entity_module(self).reload_resource_spec_mapper()
389
+
390
+ # get the configuration for maximum workers of each type
391
+ pq_data_des = get_entity_module(self).get_config_for_pq(queue)
392
+ resource_type_limits = {}
393
+ cores_queue = 1
394
+ average_memory_target = None
395
+
396
+ if not pq_data_des:
397
+ tmp_log.debug("Error retrieving queue configuration from DB, limits can not be applied")
398
+ else:
399
+ try:
400
+ resource_type_limits = pq_data_des["uconfig"]["resource_type_limits"]
401
+ except KeyError:
402
+ tmp_log.debug("No resource type limits")
403
+ pass
404
+ try:
405
+ if pq_data_des["meanrss"] != 0:
406
+ average_memory_target = pq_data_des["meanrss"]
407
+ else:
408
+ tmp_log.debug("meanrss is 0, not using it as average_memory_target")
409
+ except KeyError:
410
+ tmp_log.debug("No average memory defined")
411
+ pass
412
+
413
+ try:
414
+ cores_queue = pq_data_des["corecount"]
415
+ if not cores_queue:
416
+ cores_queue = 1
417
+ except KeyError:
418
+ tmp_log.error("No corecount")
419
+
420
+ # Retrieve the assigned harvester instance and submit UPS commands only to this instance. We have had multiple
421
+ # cases of test instances submitting to large queues in classic pull mode and not following commands.
422
+ try:
423
+ assigned_harvester_id = pq_data_des["harvester"]
424
+ except KeyError:
425
+ assigned_harvester_id = None
426
+
427
+ # If there is no harvester instance assigned to the queue or there are no statistics, we exit without any action
428
+ if assigned_harvester_id and assigned_harvester_id in harvester_ids_temp:
429
+ harvester_id = assigned_harvester_id
430
+ else:
431
+ # commit for postgres to avoid idle transactions
432
+ if not self._commit():
433
+ raise RuntimeError("Commit error")
434
+ tmp_log.error("No harvester instance assigned or not in statistics")
435
+ return {}
436
+
437
+ # There is the case where the grid has no workloads and running HIMEM jobs is better than running no jobs
438
+ ignore_meanrss = self.getConfigValue("meanrss", "IGNORE_MEANRSS")
439
+ if ignore_meanrss == True:
440
+ tmp_log.debug(f"Accepting all resource types since meanrss throttling is ignored")
441
+
442
+ # If the site defined a memory target, calculate the memory requested by running and queued workers
443
+ resource_types_under_target = []
444
+ if ignore_meanrss != True and average_memory_target:
445
+ average_memory_workers_running_submitted, average_memory_workers_running = self.get_average_memory_workers(
446
+ queue, harvester_id, average_memory_target
447
+ )
448
+ # if the queue is over memory, we will only submit lower workers in the next cycle
449
+ if average_memory_target < max(average_memory_workers_running_submitted, average_memory_workers_running):
450
+ resource_types_under_target = get_entity_module(self).resource_spec_mapper.filter_out_high_memory_resourcetypes(
451
+ memory_threshold=average_memory_target
452
+ )
453
+ tmp_log.debug(f"Accepting {resource_types_under_target} resource types to respect mean memory target")
454
+ else:
455
+ tmp_log.debug(f"Accepting all resource types as under memory target")
456
+
457
+ # there is only job_type = "managed" in the current implementation, but we keep the structure due to backwards compatibility
458
+ for job_type in worker_stats[harvester_id]:
459
+ workers_queued.setdefault(job_type, {})
460
+ for resource_type in worker_stats[harvester_id][job_type]:
461
+ core_factor = get_entity_module(self).resource_spec_mapper.translate_resourcetype_to_cores(resource_type, cores_queue)
462
+ try:
463
+ n_cores_running = n_cores_running + worker_stats[harvester_id][job_type][resource_type]["running"] * core_factor
464
+
465
+ # This limit is in #JOBS or #WORKERS, not in #CORES
466
+ if resource_type in resource_type_limits:
467
+ resource_type_limits[resource_type] = (
468
+ resource_type_limits[resource_type] - worker_stats[harvester_id][job_type][resource_type]["running"]
469
+ )
470
+ tmp_log.debug(f"Limit for rt {resource_type} down to {resource_type_limits[resource_type]}")
471
+
472
+ # This limit is in #CORES, since it mixes single and multi core jobs
473
+ if get_entity_module(self).resource_spec_mapper.is_high_memory(resource_type) and HIMEM in resource_type_limits:
474
+ resource_type_limits[HIMEM] = resource_type_limits[HIMEM] - worker_stats[harvester_id][job_type][resource_type]["running"] * core_factor
475
+ tmp_log.debug(f"Limit for rt group {HIMEM} down to {resource_type_limits[HIMEM]}")
476
+
477
+ except KeyError:
478
+ pass
479
+
480
+ try: # submitted
481
+ workers_queued[job_type].setdefault(resource_type, 0)
482
+ workers_queued[job_type][resource_type] = (
483
+ workers_queued[job_type][resource_type] + worker_stats[harvester_id][job_type][resource_type]["submitted"]
484
+ )
485
+ n_cores_queued = n_cores_queued + worker_stats[harvester_id][job_type][resource_type]["submitted"] * core_factor
486
+ except KeyError:
487
+ pass
488
+
489
+ try: # ready
490
+ workers_queued[job_type].setdefault(resource_type, 0)
491
+ workers_queued[job_type][resource_type] = (
492
+ workers_queued[job_type][resource_type] + worker_stats[harvester_id][job_type][resource_type]["ready"]
493
+ )
494
+ n_cores_queued = n_cores_queued + worker_stats[harvester_id][job_type][resource_type]["ready"] * core_factor
495
+ except KeyError:
496
+ pass
497
+
498
+ tmp_log.debug(f"Queue {queue} queued worker overview: {workers_queued}")
499
+
500
+ # For queues that need more pressure towards reaching a target
501
+ n_cores_running_fake = 0
502
+ try:
503
+ if pq_data_des["status"] in [
504
+ "online",
505
+ "brokeroff",
506
+ ]: # don't flood test sites with workers
507
+ n_cores_running_fake = pq_data_des["params"]["ups_core_target"]
508
+ tmp_log.debug(f"Using ups_core_target {n_cores_running_fake} for queue {queue}")
509
+ except KeyError: # no value defined in CRIC
510
+ pass
511
+
512
+ n_cores_running = max(n_cores_running, n_cores_running_fake)
513
+
514
+ n_cores_target = max(int(n_cores_running * 0.4), 75 * cores_queue)
515
+ n_cores_to_submit = max(n_cores_target - n_cores_queued, 5 * cores_queue)
516
+ tmp_log.debug(f"IN CORES: nrunning {n_cores_running}, ntarget {n_cores_target}, nqueued {n_cores_queued}. We need to process {n_cores_to_submit} cores")
517
+
518
+ # Get the sorted global shares
519
+ sorted_shares = get_entity_module(self).get_sorted_leaves()
520
+
521
+ # Run over the activated jobs by gshare & priority, and subtract them from the queued
522
+ # A negative value for queued will mean more pilots of that resource type are missing
523
+ for share in sorted_shares:
524
+ var_map = {":queue": queue, ":gshare": share.name}
525
+ sql = (
526
+ f"SELECT gshare, prodsourcelabel, resource_type FROM {panda_config.schemaPANDA}.jobsactive4 "
527
+ "WHERE jobstatus = 'activated' "
528
+ "AND computingsite=:queue "
529
+ "AND gshare=:gshare "
530
+ )
531
+
532
+ # if we need to filter on resource types
533
+ if resource_types_under_target:
534
+ rtype_var_names_str, rtype_var_map = get_sql_IN_bind_variables(resource_types_under_target, prefix=":", value_as_suffix=True)
535
+ sql += f" AND resource_type IN ({rtype_var_names_str}) "
536
+ var_map.update(rtype_var_map)
537
+
538
+ sql += "ORDER BY currentpriority DESC"
539
+ self.cur.execute(sql + comment, var_map)
540
+ activated_jobs = self.cur.fetchall()
541
+
542
+ tmp_log.debug(f"Processing share: {share.name}. Got {len(activated_jobs)} activated jobs")
543
+ for gshare, prodsourcelabel, resource_type in activated_jobs:
544
+ core_factor = get_entity_module(self).resource_spec_mapper.translate_resourcetype_to_cores(resource_type, cores_queue)
545
+
546
+ # translate prodsourcelabel to a subset of job types, typically 'user' and 'managed'
547
+ # Harvester worker submission unified production and analysis proxies, so we don't need to separate by job_type anymore
548
+ job_type = DEFAULT_PRODSOURCELABEL
549
+ # if we reached the limit for the resource type, skip the job
550
+ if resource_type in resource_type_limits and resource_type_limits[resource_type] <= 0:
551
+ # tmp_log.debug('Reached resource type limit for {0}'.format(resource_type))
552
+ continue
553
+
554
+ # if we reached the limit for the HIMEM resource type group, skip the job
555
+ if (
556
+ get_entity_module(self).resource_spec_mapper.is_high_memory(resource_type)
557
+ and HIMEM in resource_type_limits
558
+ and resource_type_limits[HIMEM] <= 0
559
+ ):
560
+ # tmp_log.debug('Reached resource type limit for {0}'.format(resource_type))
561
+ continue
562
+
563
+ workers_queued.setdefault(job_type, {})
564
+ workers_queued[job_type].setdefault(resource_type, 0)
565
+ workers_queued[job_type][resource_type] = workers_queued[job_type][resource_type] - 1
566
+ if workers_queued[job_type][resource_type] <= 0:
567
+ # we've gone over the jobs that already have a queued worker, now we go for new workers
568
+ n_cores_to_submit = n_cores_to_submit - core_factor
569
+
570
+ # We reached the number of workers needed
571
+ if n_cores_to_submit <= 0:
572
+ tmp_log.debug("Reached cores needed (inner)")
573
+ break
574
+
575
+ # We reached the number of workers needed
576
+ if n_cores_to_submit <= 0:
577
+ tmp_log.debug("Reached cores needed (outer)")
578
+ break
579
+
580
+ tmp_log.debug(f"workers_queued: {workers_queued}")
581
+
582
+ new_workers = {}
583
+ for job_type in workers_queued:
584
+ new_workers.setdefault(job_type, {})
585
+ for resource_type in workers_queued[job_type]:
586
+ if workers_queued[job_type][resource_type] >= 0:
587
+ # we have too many workers queued already, don't submit more
588
+ new_workers[job_type][resource_type] = 0
589
+ elif workers_queued[job_type][resource_type] < 0:
590
+ # we don't have enough workers for this resource type
591
+ new_workers[job_type][resource_type] = -workers_queued[job_type][resource_type] + 1
592
+
593
+ tmp_log.debug(f"preliminary new workers: {new_workers}")
594
+
595
+ # We should still submit a basic worker, even if there are no activated jobs to avoid queue deactivation
596
+ workers = False
597
+ for job_type in new_workers:
598
+ for resource_type in new_workers[job_type]:
599
+ if new_workers[job_type][resource_type] > 0:
600
+ workers = True
601
+ break
602
+ if not workers:
603
+ new_workers[DEFAULT_PRODSOURCELABEL] = {BASIC_RESOURCE_TYPE: 1}
604
+
605
+ tmp_log.debug(f"new workers: {new_workers}")
606
+
607
+ new_workers_per_harvester = {harvester_id: new_workers}
608
+
609
+ tmp_log.debug(f"Workers to submit: {new_workers_per_harvester}")
610
+ # commit for postgres to avoid idle transactions
611
+ if not self._commit():
612
+ raise RuntimeError("Commit error")
613
+ tmp_log.debug("done")
614
+ return new_workers_per_harvester
615
+
616
+ # add command lock
617
+ def addCommandLockHarvester(self, harvester_ID, command, computingSite, resourceType, useCommit=True):
618
+ comment = " /* DBProxy.addCommandLockHarvester */"
619
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvester_ID} command={command} site={computingSite}> resource={resourceType}")
620
+ tmp_log.debug("start")
621
+ try:
622
+ # check if lock is available
623
+ sql_check_lock = (
624
+ "SELECT 1 FROM ATLAS_PANDA.Harvester_Command_Lock "
625
+ "WHERE harvester_ID=:harvester_ID AND computingSite=:siteName AND resourceType=:resourceType AND command=:command "
626
+ )
627
+
628
+ # sql to add lock
629
+ sql_add_lock = (
630
+ "INSERT INTO ATLAS_PANDA.Harvester_Command_Lock "
631
+ "(harvester_ID, computingSite, resourceType, command, lockedTime) "
632
+ "VALUES (:harvester_ID, :siteName, :resourceType, :command, CURRENT_DATE-1) "
633
+ )
634
+ if useCommit:
635
+ self.conn.begin()
636
+ # check
637
+ var_map = {
638
+ ":harvester_ID": harvester_ID,
639
+ ":siteName": computingSite,
640
+ ":resourceType": resourceType,
641
+ ":command": command,
642
+ }
643
+ self.cur.execute(sql_check_lock + comment, var_map)
644
+ existing_lock = self.cur.fetchone()
645
+ if existing_lock is None:
646
+ # add lock
647
+ self.cur.execute(sql_add_lock + comment, var_map)
648
+ # commit
649
+ if useCommit:
650
+ if not self._commit():
651
+ raise RuntimeError("Commit error")
652
+ tmp_log.debug("done")
653
+ return True
654
+ except Exception:
655
+ # roll back
656
+ if useCommit:
657
+ self._rollback()
658
+ self.dump_error_message(tmp_log)
659
+ return False
660
+
661
+ # get command locks
662
+ def getCommandLocksHarvester(self, harvester_ID, command, lockedBy, lockInterval, commandInterval):
663
+ comment = " /* DBProxy.getCommandLocksHarvester */"
664
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvester_ID} command={command}")
665
+ tmp_log.debug("start")
666
+ try:
667
+ time_now = naive_utcnow()
668
+ # sql to get commands
669
+ sql_get_locks = (
670
+ "SELECT computingSite, resourceType FROM ATLAS_PANDA.Harvester_Command_Lock "
671
+ "WHERE harvester_ID=:harvester_ID AND command=:command "
672
+ "AND ((lockedBy IS NULL AND lockedTime<:limitComm) OR (lockedBy IS NOT NULL AND lockedTime<:limitLock)) "
673
+ "FOR UPDATE "
674
+ )
675
+
676
+ # sql to lock command
677
+ sql_lock_command = (
678
+ "UPDATE ATLAS_PANDA.Harvester_Command_Lock SET lockedBy=:lockedBy, lockedTime=CURRENT_DATE "
679
+ "WHERE harvester_ID=:harvester_ID AND command=:command AND computingSite=:siteName AND resourceType=:resourceType "
680
+ "AND ((lockedBy IS NULL AND lockedTime<:limitComm) OR (lockedBy IS NOT NULL AND lockedTime<:limitLock)) "
681
+ )
682
+ # get commands
683
+ self.conn.begin()
684
+ self.cur.arraysize = 10000
685
+ var_map = {
686
+ ":harvester_ID": harvester_ID,
687
+ ":command": command,
688
+ ":limitComm": time_now - datetime.timedelta(minutes=commandInterval),
689
+ ":limitLock": time_now - datetime.timedelta(minutes=lockInterval),
690
+ }
691
+ self.cur.execute(sql_get_locks + comment, var_map)
692
+ rows = self.cur.fetchall()
693
+ # lock commands
694
+ result_map = dict()
695
+ for computing_site, resource_type in rows:
696
+ var_map = {
697
+ ":harvester_ID": harvester_ID,
698
+ ":command": command,
699
+ ":siteName": computing_site,
700
+ ":resourceType": resource_type,
701
+ ":limitComm": time_now - datetime.timedelta(minutes=commandInterval),
702
+ ":limitLock": time_now - datetime.timedelta(minutes=lockInterval),
703
+ ":lockedBy": lockedBy,
704
+ }
705
+ self.cur.execute(sql_lock_command + comment, var_map)
706
+ n_row = self.cur.rowcount
707
+ if n_row > 0:
708
+ if computing_site not in result_map:
709
+ result_map[computing_site] = []
710
+ result_map[computing_site].append(resource_type)
711
+ # commit
712
+ if not self._commit():
713
+ raise RuntimeError("Commit error")
714
+ tmp_log.debug(f"done with {str(result_map)}")
715
+ return result_map
716
+ except Exception:
717
+ # roll back
718
+ self._rollback()
719
+ self.dump_error_message(tmp_log)
720
+ return {}
721
+
722
+ # release command lock
723
+ def releaseCommandLockHarvester(self, harvester_ID, command, computingSite, resourceType, lockedBy):
724
+ comment = " /* DBProxy.releaseCommandLockHarvester */"
725
+ tmp_log = self.create_tagged_logger(
726
+ comment, f"harvesterID={harvester_ID} com={command} site={computingSite} resource={resourceType} lockedBy={lockedBy}"
727
+ )
728
+ tmp_log.debug("start")
729
+ try:
730
+ # sql to release lock
731
+ sql_release_lock = (
732
+ "UPDATE ATLAS_PANDA.Harvester_Command_Lock SET lockedBy=NULL "
733
+ "WHERE harvester_ID=:harvester_ID AND command=:command "
734
+ "AND computingSite=:computingSite AND resourceType=:resourceType AND lockedBy=:lockedBy "
735
+ )
736
+
737
+ var_map = {
738
+ ":harvester_ID": harvester_ID,
739
+ ":command": command,
740
+ ":computingSite": computingSite,
741
+ ":resourceType": resourceType,
742
+ ":lockedBy": lockedBy,
743
+ }
744
+
745
+ # release lock
746
+ self.conn.begin()
747
+ self.cur.execute(sql_release_lock + comment, var_map)
748
+ n_row = self.cur.rowcount
749
+ # commit
750
+ if not self._commit():
751
+ raise RuntimeError("Commit error")
752
+ tmp_log.debug(f"done with {n_row}")
753
+ return True
754
+ except Exception:
755
+ # roll back
756
+ self._rollback()
757
+ self.dump_error_message(tmp_log)
758
+ return False
759
+
760
+ # heartbeat for harvester
761
+ def harvesterIsAlive(self, user, host, harvesterID, data):
762
+ """
763
+ update harvester instance information
764
+ """
765
+ comment = " /* DBProxy.harvesterIsAlive */"
766
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID}")
767
+ tmp_log.debug("start")
768
+ try:
769
+
770
+ # update
771
+ owner = CoreUtils.clean_user_id(user)
772
+ var_map = {":harvesterID": harvesterID, ":owner": owner, ":hostName": host}
773
+ sql_update_instance = "UPDATE ATLAS_PANDA.Harvester_Instances SET owner=:owner,hostName=:hostName,lastUpdate=CURRENT_DATE"
774
+ for key in data:
775
+ val = data[key]
776
+ if key == "commands":
777
+ continue
778
+ sql_update_instance += ",{0}=:{0}".format(key)
779
+ if isinstance(val, str) and val.startswith("datetime/"):
780
+ val = datetime.datetime.strptime(val.split("/")[-1], "%Y-%m-%d %H:%M:%S.%f")
781
+ var_map[f":{key}"] = val
782
+ sql_update_instance += " WHERE harvester_ID=:harvesterID "
783
+ # exec
784
+ self.conn.begin()
785
+ self.cur.execute(sql_update_instance + comment, var_map)
786
+ n_row = self.cur.rowcount
787
+ if n_row == 0:
788
+ # insert instance info
789
+ var_map = {
790
+ ":harvesterID": harvesterID,
791
+ ":owner": owner,
792
+ ":hostName": host,
793
+ ":descr": "automatic",
794
+ }
795
+ sql_insert_instance = (
796
+ "INSERT INTO ATLAS_PANDA.Harvester_Instances "
797
+ "(harvester_ID, owner, hostName, lastUpdate, description) "
798
+ "VALUES(:harvesterID, :owner, :hostName, CURRENT_DATE, :descr) "
799
+ )
800
+ self.cur.execute(sql_insert_instance + comment, var_map)
801
+ # insert command locks
802
+ if "commands" in data:
803
+ for item in data["commands"]:
804
+ self.addCommandLockHarvester(
805
+ harvesterID,
806
+ item["command"],
807
+ item["computingSite"],
808
+ item["resourceType"],
809
+ False,
810
+ )
811
+ # commit
812
+ if not self._commit():
813
+ raise RuntimeError("Commit error")
814
+ tmp_log.debug("done")
815
+ if n_row == 0:
816
+ ret_str = "no instance record"
817
+ else:
818
+ ret_str = "succeeded"
819
+ return ret_str
820
+ except Exception:
821
+ # roll back
822
+ self._rollback()
823
+ self.dump_error_message(tmp_log)
824
+ return None
825
+
826
+ # update workers
827
+ def updateWorkers(self, harvesterID, data, useCommit=True):
828
+ """
829
+ Update workers
830
+ """
831
+ comment = " /* DBProxy.updateWorkers */"
832
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID} pid={os.getpid()}")
833
+ try:
834
+ tmp_log.debug(f"start {len(data)} workers")
835
+ reg_start = naive_utcnow()
836
+ sql_check_worker = f"SELECT {WorkerSpec.columnNames()} FROM ATLAS_PANDA.Harvester_Workers WHERE harvesterID=:harvesterID AND workerID=:workerID "
837
+ # loop over all workers
838
+ ret_list = []
839
+ for worker_data in data:
840
+ time_now = naive_utcnow()
841
+ if useCommit:
842
+ self.conn.begin()
843
+ worker_spec = WorkerSpec()
844
+ worker_spec.harvesterID = harvesterID
845
+ worker_spec.workerID = worker_data["workerID"]
846
+ tmp_log.debug(f"workerID={worker_spec.workerID} start")
847
+ # check if already exists
848
+ var_map = {
849
+ ":harvesterID": worker_spec.harvesterID,
850
+ ":workerID": worker_spec.workerID,
851
+ }
852
+ self.cur.execute(sql_check_worker + comment, var_map)
853
+ existing_worker = self.cur.fetchone()
854
+ if existing_worker is None:
855
+ # not exist
856
+ to_insert = True
857
+ old_last_update = None
858
+ else:
859
+ # already exists
860
+ to_insert = False
861
+ worker_spec.pack(existing_worker)
862
+ old_last_update = worker_spec.lastUpdate
863
+ # set new values
864
+ old_status = worker_spec.status
865
+ for key in worker_data:
866
+ val = worker_data[key]
867
+ if hasattr(worker_spec, key):
868
+ setattr(worker_spec, key, val)
869
+ worker_spec.lastUpdate = time_now
870
+ if old_status in ["finished", "failed", "cancelled", "missed"] and (
871
+ old_last_update is not None and old_last_update > time_now - datetime.timedelta(hours=3)
872
+ ):
873
+ tmp_log.debug(f"workerID={worker_spec.workerID} keep old status={old_status} instead of new {worker_spec.status}")
874
+ worker_spec.status = old_status
875
+ # insert or update
876
+ if to_insert:
877
+ # insert
878
+ tmp_log.debug(f"workerID={worker_spec.workerID} insert for status={worker_spec.status}")
879
+ sql_insert_worker = f"INSERT INTO ATLAS_PANDA.Harvester_Workers ({WorkerSpec.columnNames()}) "
880
+ sql_insert_worker += WorkerSpec.bindValuesExpression()
881
+ var_map = worker_spec.valuesMap()
882
+ self.cur.execute(sql_insert_worker + comment, var_map)
883
+ else:
884
+ # update
885
+ tmp_log.debug(f"workerID={worker_spec.workerID} update for status={worker_spec.status}")
886
+ sql_update_worker = (
887
+ f"UPDATE ATLAS_PANDA.Harvester_Workers SET {worker_spec.bindUpdateChangesExpression()} "
888
+ f"WHERE harvesterID=:harvesterID AND workerID=:workerID "
889
+ )
890
+ var_map = worker_spec.valuesMap(onlyChanged=True)
891
+ self.cur.execute(sql_update_worker + comment, var_map)
892
+ # job relation
893
+ if "pandaid_list" in worker_data and len(worker_data["pandaid_list"]) > 0:
894
+ tmp_log.debug(f"workerID={worker_spec.workerID} update/insert job relation")
895
+ sql_get_job_rels = "SELECT PandaID FROM ATLAS_PANDA.Harvester_Rel_Jobs_Workers WHERE harvesterID=:harvesterID AND workerID=:workerID "
896
+ sql_insert_job_rel = (
897
+ "INSERT INTO ATLAS_PANDA.Harvester_Rel_Jobs_Workers (harvesterID,workerID,PandaID,lastUpdate) "
898
+ "VALUES (:harvesterID, :workerID, :PandaID, :lastUpdate) "
899
+ )
900
+ sql_update_job_rel = (
901
+ "UPDATE ATLAS_PANDA.Harvester_Rel_Jobs_Workers SET lastUpdate=:lastUpdate "
902
+ "WHERE harvesterID=:harvesterID AND workerID=:workerID AND PandaID=:PandaID "
903
+ )
904
+ # get jobs
905
+ var_map = {
906
+ ":harvesterID": harvesterID,
907
+ ":workerID": worker_data["workerID"],
908
+ }
909
+ self.cur.execute(sql_get_job_rels + comment, var_map)
910
+ existing_job_rels = self.cur.fetchall()
911
+ existing_panda_ids = set()
912
+ for (panda_id,) in existing_job_rels:
913
+ existing_panda_ids.add(panda_id)
914
+ for panda_id in worker_data["pandaid_list"]:
915
+ # update or insert
916
+ var_map = {
917
+ ":harvesterID": harvesterID,
918
+ ":workerID": worker_data["workerID"],
919
+ ":PandaID": panda_id,
920
+ ":lastUpdate": time_now,
921
+ }
922
+ if panda_id not in existing_panda_ids:
923
+ # insert
924
+ self.cur.execute(sql_insert_job_rel + comment, var_map)
925
+ else:
926
+ # update
927
+ self.cur.execute(sql_update_job_rel + comment, var_map)
928
+ existing_panda_ids.discard(panda_id)
929
+ # delete redundant list
930
+ sql_delete_job_rel = (
931
+ "DELETE FROM ATLAS_PANDA.Harvester_Rel_Jobs_Workers WHERE harvesterID=:harvesterID AND workerID=:workerID AND PandaID=:PandaID "
932
+ )
933
+ for panda_id in existing_panda_ids:
934
+ var_map = {
935
+ ":PandaID": panda_id,
936
+ ":harvesterID": harvesterID,
937
+ ":workerID": worker_data["workerID"],
938
+ }
939
+ self.cur.execute(sql_delete_job_rel + comment, var_map)
940
+ tmp_log.debug(f"workerID={worker_spec.workerID} deleted {len(existing_panda_ids)} jobs")
941
+ # comprehensive heartbeat
942
+ tmp_log.debug(f"workerID={worker_spec.workerID} get jobs")
943
+ sql_get_active_jobs = (
944
+ "SELECT r.PandaID FROM "
945
+ "ATLAS_PANDA.Harvester_Rel_Jobs_Workers r, ATLAS_PANDA.jobsActive4 j "
946
+ "WHERE r.harvesterID=:harvesterID AND r.workerID=:workerID "
947
+ "AND j.PandaID=r.PandaID AND NOT j.jobStatus IN (:holding) "
948
+ )
949
+
950
+ sql_get_job_status = "SELECT jobStatus, prodSourceLabel, attemptNr FROM ATLAS_PANDA.jobsActive4 WHERE PandaID=:PandaID "
951
+
952
+ sql_set_job_error = (
953
+ "UPDATE ATLAS_PANDA.jobsActive4 SET taskBufferErrorCode=:code, taskBufferErrorDiag=:diag,"
954
+ "startTime=(CASE WHEN jobStatus=:starting THEN NULL ELSE startTime END) "
955
+ "WHERE PandaID=:PandaID "
956
+ )
957
+
958
+ # the table name will be inserted at execution time
959
+ sql_set_sup_error = (
960
+ "UPDATE {0} SET supErrorCode=:code, supErrorDiag=:diag, stateChangeTime=CURRENT_DATE "
961
+ "WHERE PandaID=:PandaID AND NOT jobStatus IN (:finished) AND modificationTime>CURRENT_DATE-30"
962
+ )
963
+
964
+ var_map = {
965
+ ":harvesterID": harvesterID,
966
+ ":workerID": worker_data["workerID"],
967
+ ":holding": "holding",
968
+ }
969
+ self.cur.execute(sql_get_active_jobs + comment, var_map)
970
+ active_job_rows = self.cur.fetchall()
971
+ tmp_log.debug(f"workerID={worker_spec.workerID} update {len(active_job_rows)} jobs")
972
+ for (panda_id,) in active_job_rows:
973
+ # check job status when worker is in a final state
974
+ if worker_spec.status in [
975
+ "finished",
976
+ "failed",
977
+ "cancelled",
978
+ "missed",
979
+ ]:
980
+ var_map = {
981
+ ":PandaID": panda_id,
982
+ }
983
+ self.cur.execute(sql_get_job_status + comment, var_map)
984
+ job_status_row = self.cur.fetchone()
985
+ if job_status_row is not None:
986
+ job_status, prod_source_label, attempt_nr = job_status_row
987
+ tmp_log.debug(f"workerID={worker_spec.workerID} {worker_spec.status} while PandaID={panda_id} {job_status}")
988
+ # set failed if out of sync
989
+ if "syncLevel" in worker_data and worker_data["syncLevel"] == 1 and job_status in ["running", "starting"]:
990
+ tmp_log.debug(f"workerID={worker_spec.workerID} set failed to PandaID={panda_id} due to sync error")
991
+ var_map = {
992
+ ":PandaID": panda_id,
993
+ ":code": ErrorCode.EC_WorkerDone,
994
+ ":starting": "starting",
995
+ ":diag": f"The worker was {worker_spec.status} while the job was {job_status} : {worker_spec.diagMessage}",
996
+ }
997
+ var_map[":diag"] = JobSpec.truncateStringAttr("taskBufferErrorDiag", var_map[":diag"])
998
+ self.cur.execute(sql_set_job_error + comment, var_map)
999
+ # make an empty file to trigger registration for zip files in Adder
1000
+ # tmpFileName = '{0}_{1}_{2}'.format(panda_id, 'failed',
1001
+ # uuid.uuid3(uuid.NAMESPACE_DNS,''))
1002
+ # tmpFileName = os.path.join(panda_config.logdir, tmpFileName)
1003
+ # try:
1004
+ # open(tmpFileName, 'w').close()
1005
+ # except Exception:
1006
+ # pass
1007
+ # sql to insert empty job output report for adder
1008
+ sql_insert_job_report = (
1009
+ f"INSERT INTO {panda_config.schemaPANDA}.Job_Output_Report "
1010
+ "(PandaID, prodSourceLabel, jobStatus, attemptNr, data, timeStamp) "
1011
+ "VALUES(:PandaID, :prodSourceLabel, :jobStatus, :attemptNr, :data, :timeStamp) "
1012
+ )
1013
+
1014
+ # insert
1015
+ var_map = {
1016
+ ":PandaID": panda_id,
1017
+ ":prodSourceLabel": prod_source_label,
1018
+ ":jobStatus": "failed",
1019
+ ":attemptNr": attempt_nr,
1020
+ ":data": None,
1021
+ ":timeStamp": naive_utcnow(),
1022
+ }
1023
+ try:
1024
+ self.cur.execute(sql_insert_job_report + comment, var_map)
1025
+ except Exception:
1026
+ pass
1027
+ else:
1028
+ tmp_log.debug(f"successfully inserted job output report {panda_id}.{var_map[':attemptNr']}")
1029
+ if worker_spec.errorCode not in [None, 0]:
1030
+ var_map = {
1031
+ ":PandaID": panda_id,
1032
+ ":code": worker_spec.errorCode,
1033
+ ":diag": f"Diag from worker : {worker_spec.diagMessage}",
1034
+ ":finished": "finished",
1035
+ }
1036
+ var_map[":diag"] = JobSpec.truncateStringAttr("supErrorDiag", var_map[":diag"])
1037
+ for table_name in [
1038
+ "ATLAS_PANDA.jobsActive4",
1039
+ "ATLAS_PANDA.jobsArchived4",
1040
+ "ATLAS_PANDAARCH.jobsArchived",
1041
+ ]:
1042
+ self.cur.execute(sql_set_sup_error.format(table_name) + comment, var_map)
1043
+
1044
+ tmp_log.debug(f"workerID={worker_spec.workerID} end")
1045
+ # commit
1046
+ if useCommit:
1047
+ if not self._commit():
1048
+ raise RuntimeError("Commit error")
1049
+ ret_list.append(True)
1050
+ reg_time = naive_utcnow() - reg_start
1051
+ tmp_log.debug(f"done. exec_time={reg_time.seconds}.{reg_time.microseconds // 1000:03d} sec")
1052
+ return ret_list
1053
+ except Exception:
1054
+ # roll back
1055
+ if useCommit:
1056
+ self._rollback()
1057
+ self.dump_error_message(tmp_log)
1058
+ return None
1059
+
1060
+ # update the worker status as seen by the pilot
1061
+ def updateWorkerPilotStatus(self, workerID, harvesterID, status, node_id):
1062
+ comment = " /* DBProxy.updateWorkerPilotStatus */"
1063
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID} workerID={workerID}")
1064
+
1065
+ timestamp_utc = naive_utcnow()
1066
+ var_map = {
1067
+ ":status": status,
1068
+ ":harvesterID": harvesterID,
1069
+ ":workerID": workerID,
1070
+ ":nodeID": node_id,
1071
+ }
1072
+ sql = "UPDATE ATLAS_PANDA.harvester_workers SET pilotStatus=:status,nodeID=:nodeID "
1073
+
1074
+ tmp_log.debug(f"Updating to status={status} nodeID={node_id} at {timestamp_utc}")
1075
+
1076
+ # add the start or end time
1077
+ if status == "started":
1078
+ sql += ", pilotStartTime=:now "
1079
+ var_map[":now"] = timestamp_utc
1080
+ elif status == "finished":
1081
+ sql += ", pilotEndTime=:now "
1082
+ var_map[":now"] = timestamp_utc
1083
+
1084
+ sql += "WHERE workerID=:workerID AND harvesterID=:harvesterID "
1085
+
1086
+ try:
1087
+ self.conn.begin()
1088
+ self.cur.execute(sql + comment, var_map)
1089
+ n_rows = self.cur.rowcount
1090
+ if not self._commit():
1091
+ raise RuntimeError("Commit error")
1092
+ tmp_log.debug(f"Updated successfully with {n_rows}")
1093
+ return True
1094
+
1095
+ except Exception:
1096
+ # roll back
1097
+ self._rollback(True)
1098
+ self.dump_error_message(tmp_log)
1099
+ return False
1100
+
1101
+ def update_worker_node(
1102
+ self,
1103
+ site,
1104
+ panda_queue,
1105
+ host_name,
1106
+ cpu_model,
1107
+ cpu_model_normalized,
1108
+ n_logical_cpus,
1109
+ n_sockets,
1110
+ cores_per_socket,
1111
+ threads_per_core,
1112
+ cpu_architecture,
1113
+ cpu_architecture_level,
1114
+ clock_speed,
1115
+ total_memory,
1116
+ total_local_disk,
1117
+ ):
1118
+ comment = " /* DBProxy.update_worker_node */"
1119
+ method_name = comment.split(" ")[-2].split(".")[-1]
1120
+
1121
+ tmp_logger = self.create_tagged_logger(comment, f"{method_name} < site={site} panda_queue={panda_queue} host_name={host_name} cpu_model={cpu_model} >")
1122
+ tmp_logger.debug("Start")
1123
+
1124
+ timestamp_utc = naive_utcnow()
1125
+
1126
+ # clean up host name from any prefixes
1127
+ host_name = clean_host_name(host_name)
1128
+
1129
+ locked_site = True # Track whether the worker node was locked at site level by another pilot update
1130
+ locked_queue = True # Track whether the worker node was locked at queue level by another pilot update
1131
+
1132
+ try:
1133
+ # update the worker node at site level first
1134
+ self.conn.begin()
1135
+
1136
+ # Select the worker node to see if it exists in the database
1137
+ var_map = {":site": site, ":host_name": host_name, ":cpu_model": cpu_model}
1138
+
1139
+ sql = (
1140
+ "SELECT site, host_name, cpu_model "
1141
+ "FROM ATLAS_PANDA.worker_node "
1142
+ "WHERE site=:site AND host_name=:host_name AND cpu_model=:cpu_model "
1143
+ "FOR UPDATE NOWAIT"
1144
+ )
1145
+
1146
+ self.cur.execute((sql + comment), var_map)
1147
+ res = self.cur.fetchone()
1148
+ locked_site = False # If the row was locked, the NOWAIT clause will make the query except and go to the end
1149
+
1150
+ # The worker node entry exists, we update the worker node's last_seen timestamp
1151
+ if res:
1152
+ var_map = {":site": site, ":host_name": host_name, ":cpu_model": cpu_model, ":last_seen": timestamp_utc}
1153
+
1154
+ sql = "UPDATE ATLAS_PANDA.worker_node SET last_seen=:last_seen WHERE site=:site AND host_name=:host_name AND cpu_model=:cpu_model"
1155
+
1156
+ self.cur.execute((sql + comment), var_map)
1157
+ tmp_logger.debug("Worker node was found in the database. Updated last_seen timestamp.")
1158
+ if not self._commit():
1159
+ raise RuntimeError("Commit error")
1160
+
1161
+ else:
1162
+ # The worker node entry did not exist, we insert it as a new worker node
1163
+ var_map = {
1164
+ ":site": site,
1165
+ ":host_name": host_name,
1166
+ ":cpu_model": cpu_model,
1167
+ ":cpu_model_normalized": cpu_model_normalized,
1168
+ ":n_logical_cpus": n_logical_cpus,
1169
+ ":n_sockets": n_sockets,
1170
+ ":cores_per_socket": cores_per_socket,
1171
+ ":threads_per_core": threads_per_core,
1172
+ ":cpu_architecture": cpu_architecture,
1173
+ ":cpu_architecture_level": cpu_architecture_level,
1174
+ ":clock_speed": clock_speed,
1175
+ ":total_memory": total_memory,
1176
+ ":total_local_disk": total_local_disk,
1177
+ ":last_seen": timestamp_utc,
1178
+ }
1179
+
1180
+ sql = (
1181
+ "INSERT INTO ATLAS_PANDA.worker_node "
1182
+ "(site, host_name, cpu_model, cpu_model_normalized, n_logical_cpus, n_sockets, cores_per_socket, threads_per_core, "
1183
+ "cpu_architecture, cpu_architecture_level, clock_speed, total_memory, total_local_disk, last_seen) "
1184
+ "VALUES "
1185
+ "(:site, :host_name, :cpu_model, :cpu_model_normalized, :n_logical_cpus, :n_sockets, :cores_per_socket, :threads_per_core, "
1186
+ ":cpu_architecture, :cpu_architecture_level, :clock_speed, :total_memory, :total_local_disk, :last_seen)"
1187
+ )
1188
+
1189
+ self.cur.execute(sql + comment, var_map)
1190
+ if not self._commit():
1191
+ raise RuntimeError("Commit error")
1192
+ tmp_logger.debug("Inserted as new worker node.")
1193
+
1194
+ if panda_queue:
1195
+ # now update the worker node at queue level
1196
+ self.conn.begin()
1197
+
1198
+ # Select the worker node to see if it exists in the queue table
1199
+ var_map = {":site": site, ":host_name": host_name, ":panda_queue": panda_queue}
1200
+
1201
+ sql = (
1202
+ "SELECT site, host_name, panda_queue "
1203
+ "FROM ATLAS_PANDA.worker_node_queue "
1204
+ "WHERE site=:site AND host_name=:host_name AND panda_queue=:panda_queue "
1205
+ "FOR UPDATE NOWAIT"
1206
+ )
1207
+ self.cur.execute((sql + comment), var_map)
1208
+ res = self.cur.fetchone()
1209
+ locked_queue = False # If the row was locked, the NOWAIT clause will make the query except and go to the end
1210
+
1211
+ # The worker node entry exists at queue level, we update the worker node's last_seen timestamp
1212
+ if res:
1213
+ var_map = {":site": site, ":host_name": host_name, ":panda_queue": panda_queue, ":last_seen": timestamp_utc}
1214
+
1215
+ sql = (
1216
+ "UPDATE ATLAS_PANDA.worker_node_queue SET last_seen=:last_seen "
1217
+ "WHERE site=:site AND host_name=:host_name AND panda_queue=:panda_queue"
1218
+ )
1219
+
1220
+ self.cur.execute((sql + comment), var_map)
1221
+ tmp_logger.debug("Worker node was found in the wn-queue table. Updated last_seen timestamp.")
1222
+ if not self._commit():
1223
+ raise RuntimeError("Commit error")
1224
+
1225
+ else:
1226
+ # The worker node entry did not exist at queue level, we insert it
1227
+ var_map = {
1228
+ ":site": site,
1229
+ ":host_name": host_name,
1230
+ ":panda_queue": panda_queue,
1231
+ ":last_seen": timestamp_utc,
1232
+ }
1233
+
1234
+ sql = (
1235
+ "INSERT INTO ATLAS_PANDA.worker_node_queue "
1236
+ "(site, host_name, panda_queue, last_seen) "
1237
+ "VALUES "
1238
+ "(:site, :host_name, :panda_queue, :last_seen)"
1239
+ )
1240
+
1241
+ self.cur.execute(sql + comment, var_map)
1242
+ if not self._commit():
1243
+ raise RuntimeError("Commit error")
1244
+ tmp_logger.debug("Inserted as new worker node.")
1245
+
1246
+ tmp_logger.debug("Done.")
1247
+ return True, "Inserted new worker node."
1248
+
1249
+ except Exception:
1250
+ # Always roll back the transaction
1251
+ self._rollback(True)
1252
+
1253
+ # Failed because of the NOWAIT clause
1254
+ if locked_site or locked_queue:
1255
+ return True, "Another pilot was updating the worker node at the same time."
1256
+
1257
+ # General failure
1258
+ err_type, err_value = sys.exc_info()[:2]
1259
+ error_message = f"Worker node update failed with {err_type} {err_value}"
1260
+ tmp_logger.error(error_message)
1261
+ return False, error_message
1262
+
1263
+ def update_worker_node_gpu(
1264
+ self,
1265
+ site: str,
1266
+ host_name: str,
1267
+ vendor: str,
1268
+ model: str,
1269
+ count: int,
1270
+ vram: int,
1271
+ architecture: str,
1272
+ framework: str,
1273
+ framework_version: str,
1274
+ driver_version: str,
1275
+ ):
1276
+ comment = " /* DBProxy.update_worker_node_gpu */"
1277
+ method_name = comment.split(" ")[-2].split(".")[-1]
1278
+
1279
+ tmp_logger = self.create_tagged_logger(comment, f"{method_name} < site={site} host_name={host_name} vendor={vendor} model={model} >")
1280
+ tmp_logger.debug("Start")
1281
+
1282
+ timestamp_utc = naive_utcnow()
1283
+
1284
+ # clean up host name from any prefixes
1285
+ host_name = clean_host_name(host_name)
1286
+
1287
+ locked = True # Track whether the worker node was locked by another pilot update
1288
+
1289
+ try:
1290
+ self.conn.begin()
1291
+
1292
+ # Select the GPU to see if it exists in the database
1293
+ var_map = {":site": site, ":host_name": host_name, ":vendor": vendor, ":model": model}
1294
+
1295
+ sql = (
1296
+ "SELECT site, host_name, vendor, model "
1297
+ "FROM ATLAS_PANDA.worker_node_gpus "
1298
+ "WHERE site=:site AND host_name=:host_name AND vendor=:vendor AND model=:model "
1299
+ "FOR UPDATE NOWAIT"
1300
+ )
1301
+
1302
+ self.cur.execute((sql + comment), var_map)
1303
+ res = self.cur.fetchone()
1304
+ locked = False # If the row was locked, the NOWAIT clause will make the query except and go to the end
1305
+
1306
+ # The worker node GPU entry exists, we update the worker node's last_seen timestamp, count, framework, and framework_version
1307
+ if res:
1308
+ var_map = {
1309
+ ":site": site,
1310
+ ":host_name": host_name,
1311
+ ":vendor": vendor,
1312
+ ":model": model,
1313
+ ":framework": framework,
1314
+ ":framework_version": framework_version,
1315
+ ":count": count,
1316
+ ":last_seen": timestamp_utc,
1317
+ }
1318
+
1319
+ sql = (
1320
+ "UPDATE ATLAS_PANDA.worker_node_gpus SET last_seen=:last_seen, framework=:framework, framework_version=:framework_version, count=:count "
1321
+ "WHERE site=:site AND host_name=:host_name AND vendor=:vendor AND model=:model"
1322
+ )
1323
+
1324
+ self.cur.execute((sql + comment), var_map)
1325
+ tmp_logger.debug("Worker node GPU was found in the database. Updated last_seen timestamp and count.")
1326
+ if not self._commit():
1327
+ raise RuntimeError("Commit error")
1328
+
1329
+ return True, "Updated existing worker node GPU."
1330
+
1331
+ # The worker node GPU entry did not exist, we insert it as a new worker node GPU
1332
+ var_map = {
1333
+ ":site": site,
1334
+ ":host_name": host_name,
1335
+ ":vendor": vendor,
1336
+ ":model": model,
1337
+ ":count": count,
1338
+ ":vram": vram,
1339
+ ":architecture": architecture,
1340
+ ":framework": framework,
1341
+ ":framework_version": framework_version,
1342
+ ":driver_version": driver_version,
1343
+ ":last_seen": timestamp_utc,
1344
+ }
1345
+
1346
+ sql = (
1347
+ "INSERT INTO ATLAS_PANDA.worker_node_gpus "
1348
+ "(site, host_name, vendor, model, count, vram, architecture, framework, framework_version, driver_version, last_seen) "
1349
+ "VALUES "
1350
+ "(:site, :host_name, :vendor, :model, :count, :vram, :architecture, :framework, :framework_version, :driver_version, :last_seen)"
1351
+ )
1352
+
1353
+ self.cur.execute(sql + comment, var_map)
1354
+ if not self._commit():
1355
+ raise RuntimeError("Commit error")
1356
+ tmp_logger.debug("Inserted as new worker node GPU.")
1357
+
1358
+ return True, "Inserted new worker node GPU."
1359
+
1360
+ except Exception:
1361
+ # Always roll back the transaction
1362
+ self._rollback(True)
1363
+
1364
+ # Failed because of the NOWAIT clause
1365
+ if locked:
1366
+ return False, "Another pilot was updating the worker node GPU at the same time."
1367
+
1368
+ # General failure
1369
+ err_type, err_value = sys.exc_info()[:2]
1370
+ error_message = f"Worker node GPU update failed with {err_type} {err_value}"
1371
+ tmp_logger.error(error_message)
1372
+ return False, error_message
1373
+
1374
+ @memoize
1375
+ def get_architecture_level_map(self):
1376
+ comment = " /* DBProxy.get_architecture_level_map */"
1377
+ tmp_log = self.create_tagged_logger(comment)
1378
+ tmp_log.debug("Start")
1379
+
1380
+ try:
1381
+ sql = "SELECT panda_queue, cpu_architecture_level, total_logical_cpus, pct_within_pq FROM ATLAS_PANDA.MV_WORKER_NODE_SUMMARY "
1382
+ self.cur.execute(sql + comment, {})
1383
+ results = self.cur.fetchall()
1384
+
1385
+ tmp_log.debug(f"Got {len(results)} entries from MV_WORKER_NODE_SUMMARY")
1386
+
1387
+ # Build a queue dictionary with the results
1388
+ architecture_map = {}
1389
+ for result in results:
1390
+ panda_queue, cpu_architecture_level, total_logical_cpus, pct_within_queue = result
1391
+ architecture_map.setdefault(panda_queue, {})
1392
+ architecture_map[panda_queue][cpu_architecture_level] = {
1393
+ "total_logical_cpus": total_logical_cpus,
1394
+ "pct_within_queue": pct_within_queue,
1395
+ }
1396
+
1397
+ tmp_log.debug(f"Done")
1398
+ return architecture_map
1399
+
1400
+ except Exception:
1401
+ self.dump_error_message(tmp_log)
1402
+ return {}
1403
+
1404
+ # get workers for a job
1405
+ def getWorkersForJob(self, PandaID):
1406
+ comment = " /* DBProxy.getWorkersForJob */"
1407
+ tmp_log = self.create_tagged_logger(comment, f"PandaID={PandaID}")
1408
+ tmp_log.debug("start")
1409
+ try:
1410
+ # sql to get workers
1411
+ sql_get_workers = (
1412
+ f"SELECT {WorkerSpec.columnNames(prefix='w')} FROM ATLAS_PANDA.Harvester_Workers w, ATLAS_PANDA.Harvester_Rel_Jobs_Workers r "
1413
+ "WHERE w.harvesterID=r.harvesterID AND w.workerID=r.workerID AND r.PandaID=:PandaID "
1414
+ )
1415
+ var_map = {":PandaID": PandaID}
1416
+
1417
+ # start transaction
1418
+ self.conn.begin()
1419
+ self.cur.execute(sql_get_workers + comment, var_map)
1420
+ worker_rows = self.cur.fetchall()
1421
+ ret_list = []
1422
+ for worker_row in worker_rows:
1423
+ worker_spec = WorkerSpec()
1424
+ worker_spec.pack(worker_row)
1425
+ ret_list.append(worker_spec)
1426
+ # commit
1427
+ if not self._commit():
1428
+ raise RuntimeError("Commit error")
1429
+ tmp_log.debug(f"got {len(ret_list)} workers")
1430
+ return ret_list
1431
+ except Exception:
1432
+ # roll back
1433
+ self._rollback()
1434
+ # error
1435
+ self.dump_error_message(tmp_log)
1436
+ return []
1437
+
1438
+ # get workers with stale harvester states and newer pilot state
1439
+ def get_workers_to_synchronize(self):
1440
+ comment = " /* DBProxy.get_workers_to_synchronize */"
1441
+ tmp_log = self.create_tagged_logger(comment)
1442
+ try:
1443
+ tmp_log.debug("Starting")
1444
+
1445
+ # give harvester a chance to discover the status change itself
1446
+ discovery_period = naive_utcnow() - datetime.timedelta(minutes=60)
1447
+ # don't repeat the same workers in each cycle
1448
+ retry_period = naive_utcnow() - datetime.timedelta(minutes=30)
1449
+
1450
+ # Select workers where the status is more advanced according to the pilot than to harvester
1451
+ sql_select = (
1452
+ "SELECT /*+ INDEX_RS_ASC(harvester_workers HARVESTER_WORKERS_STATUS_IDX) */ harvesterID, workerID, pilotStatus "
1453
+ "FROM ATLAS_PANDA.harvester_workers "
1454
+ "WHERE (status in ('submitted', 'ready') AND pilotStatus='running' AND pilotStartTime < :discovery_period) "
1455
+ "OR (status in ('submitted', 'ready', 'running', 'idle') AND pilotStatus='finished' AND pilotEndTime < :discovery_period) "
1456
+ "AND lastupdate > sysdate - interval '7' day "
1457
+ "AND submittime > sysdate - interval '14' day "
1458
+ "AND (pilotStatusSyncTime > :retry_period OR pilotStatusSyncTime IS NULL) "
1459
+ "FOR UPDATE"
1460
+ )
1461
+
1462
+ var_map = {
1463
+ ":discovery_period": discovery_period,
1464
+ ":retry_period": retry_period,
1465
+ }
1466
+
1467
+ now_ts = naive_utcnow()
1468
+ sql_update = "UPDATE ATLAS_PANDA.harvester_workers SET pilotStatusSyncTime = :lastSync WHERE harvesterID= :harvesterID AND workerID= :workerID "
1469
+
1470
+ # run query to select workers
1471
+ self.conn.begin()
1472
+ self.cur.arraysize = 10000
1473
+ self.cur.execute(sql_select + comment, var_map)
1474
+ db_workers = self.cur.fetchall()
1475
+
1476
+ # prepare workers and separate by harvester instance and site
1477
+ workers_to_sync = {}
1478
+ var_maps = []
1479
+ for harvester_id, worker_id, pilot_status in db_workers:
1480
+ workers_to_sync.setdefault(harvester_id, {})
1481
+ workers_to_sync[harvester_id].setdefault(pilot_status, [])
1482
+
1483
+ # organization for harvester commands
1484
+ workers_to_sync[harvester_id][pilot_status].append(worker_id)
1485
+ # organization to set lastSync
1486
+ var_maps.append(
1487
+ {
1488
+ ":workerID": worker_id,
1489
+ ":harvesterID": harvester_id,
1490
+ ":lastSync": now_ts,
1491
+ }
1492
+ )
1493
+
1494
+ self.cur.executemany(sql_update + comment, var_maps)
1495
+
1496
+ # commit
1497
+ if not self._commit():
1498
+ raise RuntimeError("Commit error")
1499
+
1500
+ tmp_log.debug("Done")
1501
+ return workers_to_sync
1502
+ except Exception:
1503
+ # roll back
1504
+ self._rollback()
1505
+ # error
1506
+ self.dump_error_message(tmp_log)
1507
+ return []
1508
+
1509
+ # get the max workerID
1510
+ def get_max_worker_id(self, harvester_id):
1511
+ comment = " /* DBProxy.get_max_worker_id */"
1512
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvester_id}")
1513
+ tmp_log.debug("start")
1514
+ try:
1515
+ # sql to get workers
1516
+ sql_get_max = "SELECT MAX(workerID) FROM ATLAS_PANDA.Harvester_Workers WHERE harvesterID=:harvesterID "
1517
+ var_map = {":harvesterID": harvester_id}
1518
+
1519
+ # start transaction
1520
+ self.conn.begin()
1521
+ self.cur.execute(sql_get_max + comment, var_map)
1522
+ row = self.cur.fetchone()
1523
+ if row:
1524
+ (max_id,) = row
1525
+ else:
1526
+ max_id = None
1527
+ if not self._commit():
1528
+ raise RuntimeError("Commit error")
1529
+ tmp_log.debug(f"got max workerID={max_id}")
1530
+ return max_id
1531
+ except Exception:
1532
+ # roll back
1533
+ self._rollback()
1534
+ # error
1535
+ self.dump_error_message(tmp_log)
1536
+ return None
1537
+
1538
+ # add harvester dialog messages
1539
+ def addHarvesterDialogs(self, harvesterID, dialogs):
1540
+ comment = " /* DBProxy.addHarvesterDialogs */"
1541
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID}")
1542
+ tmp_log.debug("start")
1543
+ try:
1544
+ # sql to delete message
1545
+ sql_delete_dialog = f"DELETE FROM {panda_config.schemaPANDA}.Harvester_Dialogs WHERE harvester_id=:harvester_id AND diagID=:diagID "
1546
+
1547
+ # sql to insert message
1548
+ sql_insert_dialog = (
1549
+ f"INSERT INTO {panda_config.schemaPANDA}.Harvester_Dialogs "
1550
+ "(harvester_id, diagID, moduleName, identifier, creationTime, messageLevel, diagMessage) "
1551
+ "VALUES(:harvester_id, :diagID, :moduleName, :identifier, :creationTime, :messageLevel, :diagMessage) "
1552
+ )
1553
+
1554
+ for dialog_dict in dialogs:
1555
+ # start transaction
1556
+ self.conn.begin()
1557
+ # delete
1558
+ var_map = {
1559
+ ":diagID": dialog_dict["diagID"],
1560
+ ":harvester_id": harvesterID,
1561
+ }
1562
+ self.cur.execute(sql_delete_dialog + comment, var_map)
1563
+ # insert
1564
+ var_map = {
1565
+ ":diagID": dialog_dict["diagID"],
1566
+ ":identifier": dialog_dict["identifier"],
1567
+ ":moduleName": dialog_dict["moduleName"],
1568
+ ":creationTime": datetime.datetime.strptime(dialog_dict["creationTime"], "%Y-%m-%d %H:%M:%S.%f"),
1569
+ ":messageLevel": dialog_dict["messageLevel"],
1570
+ ":diagMessage": dialog_dict["diagMessage"],
1571
+ ":harvester_id": harvesterID,
1572
+ }
1573
+ self.cur.execute(sql_insert_dialog + comment, var_map)
1574
+ # commit
1575
+ if not self._commit():
1576
+ raise RuntimeError("Commit error")
1577
+ tmp_log.debug(f"added {len(dialogs)} messages")
1578
+ return True
1579
+ except Exception:
1580
+ # roll back
1581
+ self._rollback()
1582
+ # error
1583
+ self.dump_error_message(tmp_log)
1584
+ return False
1585
+
1586
+ # set num slots for workload provisioning
1587
+ def setNumSlotsForWP(self, pandaQueueName, numSlots, gshare, resourceType, validPeriod):
1588
+ comment = " /* DBProxy.setNumSlotsForWP */"
1589
+ tmp_log = self.create_tagged_logger(comment, f"pq={pandaQueueName}")
1590
+ tmp_log.debug("start")
1591
+ # sql to check
1592
+ sql_check = "SELECT 1 FROM ATLAS_PANDA.Harvester_Slots "
1593
+ sql_check += "WHERE pandaQueueName=:pandaQueueName "
1594
+ if gshare is None:
1595
+ sql_check += "AND gshare IS NULL "
1596
+ else:
1597
+ sql_check += "AND gshare=:gshare "
1598
+ if resourceType is None:
1599
+ sql_check += "AND resourceType IS NULL "
1600
+ else:
1601
+ sql_check += "AND resourceType=:resourceType "
1602
+ # sql to insert
1603
+ sql_insert = (
1604
+ "INSERT INTO ATLAS_PANDA.Harvester_Slots (pandaQueueName, gshare, resourceType, numSlots, modificationTime, expirationTime) "
1605
+ "VALUES (:pandaQueueName, :gshare, :resourceType, :numSlots, :modificationTime, :expirationTime) "
1606
+ )
1607
+ # sql to update
1608
+ if numSlots == -1:
1609
+ sql_update = "DELETE FROM ATLAS_PANDA.Harvester_Slots "
1610
+ else:
1611
+ sql_update = "UPDATE ATLAS_PANDA.Harvester_Slots SET numSlots=:numSlots, modificationTime=:modificationTime, expirationTime=:expirationTime "
1612
+ sql_update += "WHERE pandaQueueName=:pandaQueueName "
1613
+ if gshare is None:
1614
+ sql_update += "AND gshare IS NULL "
1615
+ else:
1616
+ sql_update += "AND gshare=:gshare "
1617
+ if resourceType is None:
1618
+ sql_update += "AND resourceType IS NULL "
1619
+ else:
1620
+ sql_update += "AND resourceType=:resourceType "
1621
+ try:
1622
+ time_now = naive_utcnow()
1623
+ # start transaction
1624
+ self.conn.begin()
1625
+ # check
1626
+ var_map = {
1627
+ ":pandaQueueName": pandaQueueName,
1628
+ }
1629
+ if gshare is not None:
1630
+ var_map[":gshare"] = gshare
1631
+ if resourceType is not None:
1632
+ var_map[":resourceType"] = resourceType
1633
+ self.cur.execute(sql_check, var_map)
1634
+ existing_slot = self.cur.fetchone()
1635
+ # insert or update
1636
+ var_map = {
1637
+ ":pandaQueueName": pandaQueueName,
1638
+ }
1639
+ if existing_slot is None or gshare is not None:
1640
+ var_map[":gshare"] = gshare
1641
+ if existing_slot is None or resourceType is not None:
1642
+ var_map[":resourceType"] = resourceType
1643
+ if numSlots != -1:
1644
+ var_map[":numSlots"] = numSlots
1645
+ var_map[":modificationTime"] = time_now
1646
+ if validPeriod is None:
1647
+ var_map[":expirationTime"] = None
1648
+ else:
1649
+ var_map[":expirationTime"] = time_now + datetime.timedelta(days=int(validPeriod))
1650
+ if existing_slot is None:
1651
+ # insert
1652
+ self.cur.execute(sql_insert, var_map)
1653
+ else:
1654
+ # update
1655
+ self.cur.execute(sql_update, var_map)
1656
+ else:
1657
+ # delete
1658
+ if existing_slot is not None:
1659
+ self.cur.execute(sql_update, var_map)
1660
+ # commit
1661
+ if not self._commit():
1662
+ raise RuntimeError("Commit error")
1663
+ # return
1664
+ tmp_log.debug(f"set nSlots={numSlots}")
1665
+ return (
1666
+ 0,
1667
+ f"set numSlots={numSlots} for PQ={pandaQueueName} gshare={gshare} resource={resourceType}",
1668
+ )
1669
+ except Exception:
1670
+ # roll back
1671
+ self._rollback()
1672
+ # error
1673
+ self.dump_error_message(tmp_log)
1674
+ return (1, "database error in the panda server")
1675
+
1676
+ def getCommands(self, harvester_id, n_commands):
1677
+ """
1678
+ Gets n commands in status 'new' for a particular harvester instance and updates their status to 'retrieved'
1679
+ """
1680
+
1681
+ comment = " /* DBProxy.getCommands */"
1682
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvester_id}")
1683
+ tmp_log.debug("start")
1684
+
1685
+ try:
1686
+ self.conn.begin()
1687
+ # Prepare the bindings and var map to get the oldest n commands in 'new' status
1688
+ var_map = {
1689
+ ":harvester_id": harvester_id,
1690
+ ":n_commands": n_commands,
1691
+ ":status": "new",
1692
+ }
1693
+
1694
+ sql = None
1695
+ if self.backend in ["oracle", "postgres"]:
1696
+ sql = """
1697
+ SELECT command_id, command, params, ack_requested, creation_date FROM
1698
+ (SELECT command_id, command, params, ack_requested, creation_date FROM ATLAS_PANDA.HARVESTER_COMMANDS
1699
+ WHERE harvester_id=:harvester_id AND status=:status
1700
+ ORDER BY creation_date) a
1701
+ WHERE ROWNUM <= :n_commands
1702
+ """
1703
+ else:
1704
+ sql = """
1705
+ SELECT command_id, command, params, ack_requested, creation_date FROM (SELECT (@rownum:=@rownum+1) AS ROWNUM, command_id, command, params, ack_requested, creation_date FROM
1706
+ (SELECT command_id, command, params, ack_requested, creation_date FROM ATLAS_PANDA.HARVESTER_COMMANDS
1707
+ WHERE harvester_id=:harvester_id AND status=:status
1708
+ ORDER BY creation_date) a, (SELECT @rownum:=0) r ) comm
1709
+ WHERE ROWNUM <= :n_commands
1710
+ """
1711
+ self.cur.execute(sql + comment, var_map)
1712
+ entries = self.cur.fetchall()
1713
+ tmp_log.debug(f"entries {entries}")
1714
+
1715
+ # pack the commands into a dictionary for transmission to harvester
1716
+ commands = []
1717
+ command_ids = []
1718
+ for entry in entries:
1719
+ command_dict = {}
1720
+ command_dict["command_id"] = entry[0]
1721
+ command_ids.append(entry[0]) # we need to update the commands as dispatched
1722
+ command_dict["command"] = entry[1]
1723
+ command_dict["params"] = entry[2]
1724
+ if command_dict["params"] is not None:
1725
+ try:
1726
+ parameters = command_dict["params"].read()
1727
+ except AttributeError:
1728
+ parameters = command_dict["params"]
1729
+ command_dict["params"] = json.loads(parameters)
1730
+ command_dict["ack_requested"] = entry[3]
1731
+ command_dict["creation_date"] = entry[4].isoformat()
1732
+ commands.append(command_dict)
1733
+
1734
+ tmp_log.debug(f"commands {commands}")
1735
+ tmp_log.debug(f"command_ids {command_ids}")
1736
+
1737
+ if command_ids:
1738
+ # update the commands and set them as retrieved
1739
+ # Prepare the bindings and var map
1740
+ var_map = {":retrieved": "retrieved"}
1741
+ for i, command_id in enumerate(command_ids):
1742
+ var_map[f":command_id{i}"] = command_id
1743
+ command_id_bindings = ",".join(f":command_id{i}" for i in range(len(command_ids)))
1744
+
1745
+ sql = f"UPDATE ATLAS_PANDA.HARVESTER_COMMANDS SET status=:retrieved, status_date=CURRENT_DATE WHERE command_id IN({command_id_bindings})"
1746
+
1747
+ # run the update
1748
+ self.cur.execute(sql + comment, var_map)
1749
+
1750
+ if not self._commit():
1751
+ raise RuntimeError("Commit error")
1752
+
1753
+ tmp_log.debug("done")
1754
+ return 0, commands
1755
+
1756
+ except Exception:
1757
+ self._rollback()
1758
+ self.dump_error_message(tmp_log)
1759
+ return -1, []
1760
+
1761
+ def ackCommands(self, command_ids):
1762
+ """
1763
+ Sets the commands to acknowledged
1764
+ """
1765
+ comment = " /* DBProxy.ackCommands */"
1766
+ tmp_log = self.create_tagged_logger(comment)
1767
+ tmp_log.debug("start")
1768
+
1769
+ try:
1770
+ # Prepare the bindings and var map
1771
+ var_map = {":acknowledged": "acknowledged"}
1772
+ for i, command_id in enumerate(command_ids):
1773
+ var_map[f":command_id{i}"] = command_id
1774
+ command_id_bindings = ",".join(f":command_id{i}" for i in range(len(command_ids)))
1775
+
1776
+ sql = f"UPDATE ATLAS_PANDA.HARVESTER_COMMANDS SET status=:acknowledged, status_date=CURRENT_DATE WHERE command_id IN({command_id_bindings})"
1777
+
1778
+ # run the update
1779
+ self.conn.begin()
1780
+ self.cur.execute(sql + comment, var_map)
1781
+ if not self._commit():
1782
+ raise RuntimeError("Commit error")
1783
+
1784
+ return 0
1785
+
1786
+ except Exception:
1787
+ # roll back
1788
+ self._rollback()
1789
+ self.dump_error_message(tmp_log)
1790
+ return -1
1791
+
1792
+ # update workers
1793
+ def updateServiceMetrics(self, harvesterID, data):
1794
+ """
1795
+ Update service metrics
1796
+ """
1797
+ comment = " /* DBProxy.updateServiceMetrics */"
1798
+ tmp_log = self.create_tagged_logger(comment, f"harvesterID={harvesterID}")
1799
+ try:
1800
+ # generate the SQL to insert metrics into the DB
1801
+ sql = f"INSERT INTO ATLAS_PANDA.harvester_Metrics ({HarvesterMetricsSpec.columnNames()}) "
1802
+ sql += HarvesterMetricsSpec.bindValuesExpression()
1803
+
1804
+ # generate the entries for the DB
1805
+ var_maps = []
1806
+ for entry in data:
1807
+ tmp_log.debug(f"entry {entry}")
1808
+ metrics_spec = HarvesterMetricsSpec()
1809
+ metrics_spec.harvester_ID = harvesterID
1810
+ metrics_spec.creation_time = datetime.datetime.strptime(entry[0], "%Y-%m-%d %H:%M:%S.%f")
1811
+ metrics_spec.harvester_host = entry[1]
1812
+ metrics_spec.metrics = entry[2]
1813
+
1814
+ var_maps.append(metrics_spec.valuesMap())
1815
+
1816
+ # run the SQL
1817
+ self.cur.executemany(sql + comment, var_maps)
1818
+ if not self._commit():
1819
+ raise RuntimeError("Commit error")
1820
+ tmp_log.debug("done")
1821
+ return [True]
1822
+ except Exception:
1823
+ # roll back
1824
+ self._rollback()
1825
+ self.dump_error_message(tmp_log)
1826
+ return None
1827
+
1828
+ def storePilotLog(self, panda_id, pilot_log):
1829
+ """
1830
+ Stores the pilotlog in the pandalog table
1831
+ """
1832
+ comment = " /* DBProxy.storePilotLog */"
1833
+ tmp_log = self.create_tagged_logger(comment, f"PandaID={panda_id}")
1834
+ tmp_log.debug(f"start")
1835
+
1836
+ try:
1837
+ # Prepare the bindings and var map
1838
+ var_map = {
1839
+ ":panda_id": panda_id,
1840
+ ":message": pilot_log[:4000], # clip if longer than 4k characters
1841
+ ":now": naive_utcnow(),
1842
+ ":name": "panda.mon.prod",
1843
+ ":module": "JobDispatcher",
1844
+ ":type": "pilotLog",
1845
+ ":file_name": "JobDispatcher.py",
1846
+ ":log_level": 20,
1847
+ ":level_name": "INFO",
1848
+ }
1849
+
1850
+ sql = (
1851
+ "INSERT INTO ATLAS_PANDA.PANDALOG (BINTIME, NAME, MODULE, TYPE, PID, LOGLEVEL, LEVELNAME, TIME, FILENAME, MESSAGE) "
1852
+ "VALUES (:now, :name, :module, :type, :panda_id, :log_level, :level_name, :now, :file_name, :message)"
1853
+ )
1854
+
1855
+ # run the insert
1856
+ self.conn.begin()
1857
+ self.cur.execute(sql + comment, var_map)
1858
+ if not self._commit():
1859
+ raise RuntimeError("Commit error")
1860
+
1861
+ return 0
1862
+
1863
+ except Exception:
1864
+ # roll back
1865
+ self._rollback()
1866
+ self.dump_error_message(tmp_log)
1867
+ return -1
1868
+
1869
+ def ups_load_worker_stats(self):
1870
+ """
1871
+ Load the harvester worker stats. Historically this would separate between prodsource labels due to different proxies for analysis and production,
1872
+ but all workers get submitted with production proxy and the pilot changes to analysis proxy if required. So we are not separating by prodsource label,
1873
+ in the query anymore, but we are keeping the prodsource label dimension in the dictionary for compatibility reasons.
1874
+ :return: dictionary with worker statistics
1875
+ """
1876
+ comment = " /* DBProxy.ups_load_worker_stats */"
1877
+ tmp_log = self.create_tagged_logger(comment)
1878
+ tmp_log.debug("start")
1879
+
1880
+ # start transaction for postgres to avoid idle transactions
1881
+ self.conn.begin()
1882
+
1883
+ # get current pilot distribution in harvester for the queue
1884
+ sql = (
1885
+ "SELECT computingsite, harvester_id, resourceType, status, sum(n_workers) "
1886
+ f"FROM {panda_config.schemaPANDA}.harvester_worker_stats WHERE lastupdate > :time_limit "
1887
+ "GROUP BY computingsite, harvester_id, resourceType, status"
1888
+ )
1889
+ var_map = {":time_limit": naive_utcnow() - datetime.timedelta(minutes=60)}
1890
+
1891
+ self.cur.execute(sql + comment, var_map)
1892
+ worker_stats_rows = self.cur.fetchall()
1893
+ worker_stats_dict = {}
1894
+ for (
1895
+ computing_site,
1896
+ harvester_id,
1897
+ resource_type,
1898
+ status,
1899
+ n_workers,
1900
+ ) in worker_stats_rows:
1901
+ worker_stats_dict.setdefault(computing_site, {})
1902
+ worker_stats_dict[computing_site].setdefault(harvester_id, {})
1903
+ worker_stats_dict[computing_site][harvester_id].setdefault(DEFAULT_PRODSOURCELABEL, {})
1904
+ worker_stats_dict[computing_site][harvester_id][DEFAULT_PRODSOURCELABEL].setdefault(resource_type, {})
1905
+ worker_stats_dict[computing_site][harvester_id][DEFAULT_PRODSOURCELABEL][resource_type][status] = n_workers
1906
+
1907
+ if not self._commit():
1908
+ raise RuntimeError("Commit error")
1909
+ tmp_log.debug("done")
1910
+ return worker_stats_dict
1911
+
1912
+ def get_cpu_benchmarks_by_host(self, site: str, host_name: str) -> list[tuple[str, float]]:
1913
+ comment = " /* DBProxy.get_cpu_benchmarks_by_host */"
1914
+ tmp_log = self.create_tagged_logger(comment, f"host_name={host_name}")
1915
+ tmp_log.debug("Start")
1916
+
1917
+ host_name_clean = clean_host_name(host_name)
1918
+
1919
+ try:
1920
+ sql = (
1921
+ "SELECT cb.site, score_per_core FROM atlas_panda.worker_node wn, atlas_panda.cpu_benchmarks cb "
1922
+ "WHERE wn.site = :site "
1923
+ "AND wn.host_name = :host_name "
1924
+ "AND cb.cpu_type_normalized = wn.cpu_model_normalized "
1925
+ "AND wn.threads_per_core = cb.smt_enabled + 1 "
1926
+ "AND wn.n_logical_cpus = cb.ncores " # protects against site misreporting SMT, e.g. due to VMs
1927
+ )
1928
+
1929
+ var_map = {":site": site, ":host_name": host_name_clean}
1930
+
1931
+ self.cur.execute(sql + comment, var_map)
1932
+ results = self.cur.fetchall()
1933
+
1934
+ tmp_log.debug(f"Got {len(results)} benchmarks")
1935
+ return results
1936
+
1937
+ except Exception:
1938
+ self.dump_error_message(tmp_log)
1939
+ return []
1940
+
1941
+
1942
+ # get worker module
1943
+ def get_worker_module(base_mod) -> WorkerModule:
1944
+ return base_mod.get_composite_module("worker")