panda-server-source 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- panda_server_source-0.8.0.dist-info/METADATA +55 -0
- panda_server_source-0.8.0.dist-info/RECORD +375 -0
- panda_server_source-0.8.0.dist-info/WHEEL +4 -0
- panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
- pandajedi/__init__.py +0 -0
- pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
- pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
- pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
- pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
- pandajedi/jedibrokerage/GenJobBroker.py +381 -0
- pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
- pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
- pandajedi/jedibrokerage/__init__.py +0 -0
- pandajedi/jediconfig/__init__.py +0 -0
- pandajedi/jediconfig/jedi_config.py +40 -0
- pandajedi/jedicore/FactoryBase.py +182 -0
- pandajedi/jedicore/Interaction.py +433 -0
- pandajedi/jedicore/JediDBProxy.py +56 -0
- pandajedi/jedicore/JediDBProxyPool.py +37 -0
- pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
- pandajedi/jedicore/JediException.py +13 -0
- pandajedi/jedicore/JediTaskBuffer.py +984 -0
- pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
- pandajedi/jedicore/MsgWrapper.py +124 -0
- pandajedi/jedicore/ProcessUtils.py +34 -0
- pandajedi/jedicore/SiteCandidate.py +90 -0
- pandajedi/jedicore/ThreadUtils.py +205 -0
- pandajedi/jedicore/__init__.py +0 -0
- pandajedi/jediddm/AtlasDDMClient.py +1708 -0
- pandajedi/jediddm/DDMClientBase.py +16 -0
- pandajedi/jediddm/DDMInterface.py +83 -0
- pandajedi/jediddm/GenDDMClient.py +17 -0
- pandajedi/jediddm/__init__.py +0 -0
- pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
- pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
- pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
- pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
- pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
- pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
- pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
- pandajedi/jedidog/GenWatchDog.py +21 -0
- pandajedi/jedidog/JumboWatchDog.py +147 -0
- pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
- pandajedi/jedidog/WatchDogBase.py +53 -0
- pandajedi/jedidog/__init__.py +0 -0
- pandajedi/jediexec/reniceJEDI.py +25 -0
- pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
- pandajedi/jedigen/TaskGeneratorBase.py +15 -0
- pandajedi/jedigen/__init__.py +0 -0
- pandajedi/jedimsgprocessor/__init__.py +0 -0
- pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
- pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
- pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
- pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
- pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
- pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
- pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
- pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
- pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
- pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
- pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
- pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
- pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
- pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
- pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
- pandajedi/jediorder/ContentsFeeder.py +785 -0
- pandajedi/jediorder/JediDaemon.py +27 -0
- pandajedi/jediorder/JediKnight.py +55 -0
- pandajedi/jediorder/JediMaster.py +238 -0
- pandajedi/jediorder/JediMsgProcessor.py +32 -0
- pandajedi/jediorder/JobBroker.py +33 -0
- pandajedi/jediorder/JobGenerator.py +2763 -0
- pandajedi/jediorder/JobSplitter.py +308 -0
- pandajedi/jediorder/JobThrottler.py +28 -0
- pandajedi/jediorder/PostProcessor.py +166 -0
- pandajedi/jediorder/TaskBroker.py +268 -0
- pandajedi/jediorder/TaskCommando.py +334 -0
- pandajedi/jediorder/TaskGenerator.py +17 -0
- pandajedi/jediorder/TaskRefiner.py +611 -0
- pandajedi/jediorder/TaskSetupper.py +17 -0
- pandajedi/jediorder/WatchDog.py +79 -0
- pandajedi/jediorder/__init__.py +0 -0
- pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
- pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
- pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
- pandajedi/jedipprocess/GenPostProcessor.py +35 -0
- pandajedi/jedipprocess/MailTemplates.py +185 -0
- pandajedi/jedipprocess/PostProcessorBase.py +269 -0
- pandajedi/jedipprocess/__init__.py +0 -0
- pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
- pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
- pandajedi/jedirefine/GenTaskRefiner.py +61 -0
- pandajedi/jedirefine/RefinerUtils.py +154 -0
- pandajedi/jedirefine/TaskRefinerBase.py +996 -0
- pandajedi/jedirefine/__init__.py +0 -0
- pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
- pandajedi/jedisetup/GenTaskSetupper.py +16 -0
- pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
- pandajedi/jedisetup/TaskSetupperBase.py +15 -0
- pandajedi/jedisetup/__init__.py +0 -0
- pandajedi/jeditest/SchemaChecker.py +41 -0
- pandajedi/jeditest/addContWoSoTask.py +54 -0
- pandajedi/jeditest/addDataCarouselRequest.py +47 -0
- pandajedi/jeditest/addEventServiceTask.py +101 -0
- pandajedi/jeditest/addHpoTask.py +81 -0
- pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
- pandajedi/jeditest/addNonAtlasTask.py +71 -0
- pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
- pandajedi/jeditest/addTestTask.py +124 -0
- pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
- pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
- pandajedi/jeditest/brokerTest.py +78 -0
- pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
- pandajedi/jeditest/contentsFeederTest.py +28 -0
- pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
- pandajedi/jeditest/ddmTest.py +4 -0
- pandajedi/jeditest/jobGenForOneTask.py +38 -0
- pandajedi/jeditest/jobGeneratorTest.py +43 -0
- pandajedi/jeditest/jobSplitterTest.py +104 -0
- pandajedi/jeditest/jobThrottlerTest.py +22 -0
- pandajedi/jeditest/msgProcessorTest.py +9 -0
- pandajedi/jeditest/postProcessorTest.py +18 -0
- pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
- pandajedi/jeditest/resubmitTaskParam.py +20 -0
- pandajedi/jeditest/taskBrokerTest.py +22 -0
- pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
- pandajedi/jeditest/taskBufferTest.py +5 -0
- pandajedi/jeditest/taskCommandoTest.py +28 -0
- pandajedi/jeditest/taskRefinerTest.py +30 -0
- pandajedi/jeditest/testOneTaskToRefine.py +49 -0
- pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
- pandajedi/jeditest/testPandaDDMMap.py +62 -0
- pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
- pandajedi/jeditest/watchDogTest.py +25 -0
- pandajedi/jeditest/workQueueTest.py +22 -0
- pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
- pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
- pandajedi/jedithrottle/GenJobThrottler.py +26 -0
- pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
- pandajedi/jedithrottle/__init__.py +0 -0
- pandaserver/__init__.py +0 -0
- pandaserver/api/__init__.py +0 -0
- pandaserver/api/v1/__init__.py +0 -0
- pandaserver/api/v1/common.py +339 -0
- pandaserver/api/v1/credential_management_api.py +372 -0
- pandaserver/api/v1/data_carousel_api.py +264 -0
- pandaserver/api/v1/event_api.py +269 -0
- pandaserver/api/v1/file_server_api.py +942 -0
- pandaserver/api/v1/harvester_api.py +445 -0
- pandaserver/api/v1/http_client.py +166 -0
- pandaserver/api/v1/idds_api.py +180 -0
- pandaserver/api/v1/job_api.py +463 -0
- pandaserver/api/v1/metaconfig_api.py +122 -0
- pandaserver/api/v1/pilot_api.py +895 -0
- pandaserver/api/v1/statistics_api.py +207 -0
- pandaserver/api/v1/system_api.py +186 -0
- pandaserver/api/v1/task_api.py +1520 -0
- pandaserver/api/v1/tests/__init__.py +0 -0
- pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
- pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
- pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
- pandaserver/api/v1/tests/idds_api_tests.py +45 -0
- pandaserver/api/v1/tests/job_api_tests.py +112 -0
- pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
- pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
- pandaserver/api/v1/tests/system_api_tests.py +58 -0
- pandaserver/api/v1/tests/task_api_tests.py +590 -0
- pandaserver/api/v1/version.py +2 -0
- pandaserver/api/v1/workflow_api.py +136 -0
- pandaserver/brokerage/SiteMapper.py +406 -0
- pandaserver/brokerage/__init__.py +0 -0
- pandaserver/brokerage/broker.py +272 -0
- pandaserver/config/__init__.py +0 -0
- pandaserver/config/config_utils.py +15 -0
- pandaserver/config/daemon_config.py +29 -0
- pandaserver/config/mcp_config.py +38 -0
- pandaserver/config/panda_config.py +233 -0
- pandaserver/configurator/Carbon.py +65 -0
- pandaserver/configurator/Configurator.py +908 -0
- pandaserver/configurator/__init__.py +0 -0
- pandaserver/configurator/aux.py +73 -0
- pandaserver/daemons/__init__.py +0 -0
- pandaserver/daemons/master.py +110 -0
- pandaserver/daemons/master_systemd.py +79 -0
- pandaserver/daemons/scripts/__init__.py +0 -0
- pandaserver/daemons/scripts/add_main.py +264 -0
- pandaserver/daemons/scripts/add_sub.py +221 -0
- pandaserver/daemons/scripts/cache_pilots.py +47 -0
- pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
- pandaserver/daemons/scripts/carbon.py +50 -0
- pandaserver/daemons/scripts/configurator.py +94 -0
- pandaserver/daemons/scripts/copyArchive.py +1387 -0
- pandaserver/daemons/scripts/datasetManager.py +1154 -0
- pandaserver/daemons/scripts/dummy_test.py +25 -0
- pandaserver/daemons/scripts/evpPD2P.py +144 -0
- pandaserver/daemons/scripts/hs_scrapers.py +264 -0
- pandaserver/daemons/scripts/metric_collector.py +927 -0
- pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
- pandaserver/daemons/scripts/pilotStreaming.py +109 -0
- pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
- pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
- pandaserver/daemons/scripts/task_evaluator.py +405 -0
- pandaserver/daemons/scripts/tmpwatch.py +70 -0
- pandaserver/daemons/scripts/worker_synchronization.py +97 -0
- pandaserver/daemons/utils.py +753 -0
- pandaserver/dataservice/DataServiceUtils.py +323 -0
- pandaserver/dataservice/ErrorCode.py +25 -0
- pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
- pandaserver/dataservice/__init__.py +0 -0
- pandaserver/dataservice/activator.py +79 -0
- pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
- pandaserver/dataservice/adder_dummy_plugin.py +31 -0
- pandaserver/dataservice/adder_gen.py +954 -0
- pandaserver/dataservice/adder_plugin_base.py +27 -0
- pandaserver/dataservice/adder_result.py +75 -0
- pandaserver/dataservice/adder_simple_plugin.py +160 -0
- pandaserver/dataservice/closer.py +247 -0
- pandaserver/dataservice/closer_atlas_plugin.py +90 -0
- pandaserver/dataservice/ddm.py +1381 -0
- pandaserver/dataservice/ddm_handler.py +95 -0
- pandaserver/dataservice/dyn_data_distributer.py +824 -0
- pandaserver/dataservice/event_lookup_client_ei.py +77 -0
- pandaserver/dataservice/event_picker.py +268 -0
- pandaserver/dataservice/finisher.py +194 -0
- pandaserver/dataservice/setupper.py +225 -0
- pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
- pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
- pandaserver/dataservice/setupper_plugin_base.py +67 -0
- pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
- pandaserver/jobdispatcher/ErrorCode.py +19 -0
- pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
- pandaserver/jobdispatcher/Protocol.py +443 -0
- pandaserver/jobdispatcher/Watcher.py +186 -0
- pandaserver/jobdispatcher/__init__.py +0 -0
- pandaserver/pandamcp/__init__.py +0 -0
- pandaserver/pandamcp/mcp_main.py +56 -0
- pandaserver/pandamcp/mcp_test_client.py +95 -0
- pandaserver/pandamcp/mcp_utils.py +74 -0
- pandaserver/proxycache/__init__.py +0 -0
- pandaserver/proxycache/panda_proxy_cache.py +240 -0
- pandaserver/proxycache/token_cache.py +141 -0
- pandaserver/server/.gacl +6 -0
- pandaserver/server/panda.py +539 -0
- pandaserver/srvcore/CoreUtils.py +572 -0
- pandaserver/srvcore/MailUtils.py +106 -0
- pandaserver/srvcore/__init__.py +0 -0
- pandaserver/srvcore/allowed_methods.py +100 -0
- pandaserver/srvcore/oidc_utils.py +152 -0
- pandaserver/srvcore/panda_request.py +196 -0
- pandaserver/srvcore/srv_msg_utils.py +42 -0
- pandaserver/taskbuffer/ConBridge.py +513 -0
- pandaserver/taskbuffer/DBProxyPool.py +95 -0
- pandaserver/taskbuffer/DataCarousel.py +3053 -0
- pandaserver/taskbuffer/DatasetSpec.py +137 -0
- pandaserver/taskbuffer/DdmSpec.py +201 -0
- pandaserver/taskbuffer/ErrorCode.py +87 -0
- pandaserver/taskbuffer/EventServiceUtils.py +396 -0
- pandaserver/taskbuffer/FileSpec.py +290 -0
- pandaserver/taskbuffer/GlobalShares.py +244 -0
- pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
- pandaserver/taskbuffer/Initializer.py +82 -0
- pandaserver/taskbuffer/InputChunk.py +1080 -0
- pandaserver/taskbuffer/JediCacheSpec.py +57 -0
- pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
- pandaserver/taskbuffer/JediFileSpec.py +242 -0
- pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
- pandaserver/taskbuffer/JobSpec.py +1006 -0
- pandaserver/taskbuffer/JobUtils.py +232 -0
- pandaserver/taskbuffer/NucleusSpec.py +116 -0
- pandaserver/taskbuffer/OraDBProxy.py +174 -0
- pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
- pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
- pandaserver/taskbuffer/ParseJobXML.py +350 -0
- pandaserver/taskbuffer/PickleFileSpec.py +18 -0
- pandaserver/taskbuffer/PickleJobSpec.py +25 -0
- pandaserver/taskbuffer/PrioUtil.py +40 -0
- pandaserver/taskbuffer/ProcessGroups.py +111 -0
- pandaserver/taskbuffer/ResourceSpec.py +175 -0
- pandaserver/taskbuffer/SQLDumper.py +37 -0
- pandaserver/taskbuffer/SQLManager.py +60 -0
- pandaserver/taskbuffer/SiteSpec.py +304 -0
- pandaserver/taskbuffer/SupErrors.py +10 -0
- pandaserver/taskbuffer/TaskBuffer.py +2882 -0
- pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
- pandaserver/taskbuffer/Utils.py +848 -0
- pandaserver/taskbuffer/WorkQueue.py +227 -0
- pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
- pandaserver/taskbuffer/WorkerSpec.py +143 -0
- pandaserver/taskbuffer/WrappedCursor.py +487 -0
- pandaserver/taskbuffer/WrappedPickle.py +88 -0
- pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
- pandaserver/taskbuffer/__init__.py +0 -0
- pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
- pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
- pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
- pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
- pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
- pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
- pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
- pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
- pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
- pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
- pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
- pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
- pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
- pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
- pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
- pandaserver/taskbuffer/retryModule.py +552 -0
- pandaserver/taskbuffer/task_split_rules.py +308 -0
- pandaserver/taskbuffer/workflow_processor.py +249 -0
- pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
- pandaserver/test/SchemaChecker.py +40 -0
- pandaserver/test/__init__.py +0 -0
- pandaserver/test/banUser.py +43 -0
- pandaserver/test/boostPrio.py +19 -0
- pandaserver/test/boostUser.py +35 -0
- pandaserver/test/callbackDDM.py +11 -0
- pandaserver/test/daod_on_demand.py +95 -0
- pandaserver/test/finishJob.py +184 -0
- pandaserver/test/finishTaskJEDI.py +11 -0
- pandaserver/test/getJobs.py +53 -0
- pandaserver/test/killJob.py +79 -0
- pandaserver/test/killJobLowPrio.py +99 -0
- pandaserver/test/killJobsInTask.py +56 -0
- pandaserver/test/killTask.py +80 -0
- pandaserver/test/killTaskJEDI.py +11 -0
- pandaserver/test/killUser.py +83 -0
- pandaserver/test/lsst/README.txt +7 -0
- pandaserver/test/lsst/lsstSubmit.py +139 -0
- pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
- pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
- pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
- pandaserver/test/reassignJobs.py +17 -0
- pandaserver/test/reassignSite.py +82 -0
- pandaserver/test/reassignTask.py +117 -0
- pandaserver/test/reassignWaiting.py +39 -0
- pandaserver/test/recoverLostFiles.py +7 -0
- pandaserver/test/reloadInputDS.py +51 -0
- pandaserver/test/sendCommandToJob.py +26 -0
- pandaserver/test/setDebugMode.py +38 -0
- pandaserver/test/setPriority.py +28 -0
- pandaserver/test/testEvgen.py +60 -0
- pandaserver/test/testEvgen17.py +58 -0
- pandaserver/test/testG4sim.py +81 -0
- pandaserver/test/testG4sim17.py +90 -0
- pandaserver/test/testGlobalShares.py +131 -0
- pandaserver/test/testReco.py +103 -0
- pandaserver/test/testSimulReco14.py +98 -0
- pandaserver/test/testSiteMap.py +26 -0
- pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
- pandaserver/userinterface/Client.py +1686 -0
- pandaserver/userinterface/UserIF.py +1799 -0
- pandaserver/userinterface/__init__.py +0 -0
- pandaserver/workflow/__init__.py +0 -0
- pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
- pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
- pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
- pandaserver/workflow/pcwl_test.py +122 -0
- pandaserver/workflow/pcwl_utils.py +384 -0
- pandaserver/workflow/psnakemake_container.json +68 -0
- pandaserver/workflow/psnakemake_task.json +62 -0
- pandaserver/workflow/psnakemake_test.py +60 -0
- pandaserver/workflow/snakeparser/__init__.py +3 -0
- pandaserver/workflow/snakeparser/extensions.py +74 -0
- pandaserver/workflow/snakeparser/log.py +53 -0
- pandaserver/workflow/snakeparser/names.py +3 -0
- pandaserver/workflow/snakeparser/parser.py +332 -0
- pandaserver/workflow/snakeparser/utils.py +29 -0
- pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
- pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
- pandaserver/workflow/workflow_base.py +531 -0
- pandaserver/workflow/workflow_core.py +1974 -0
- pandaserver/workflow/workflow_core_smoketest.py +388 -0
- pandaserver/workflow/workflow_parser.py +222 -0
- pandaserver/workflow/workflow_utils.py +1047 -0
|
@@ -0,0 +1,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")
|