DIRAC 9.0.13__py3-none-any.whl → 9.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. DIRAC/ConfigurationSystem/Client/CSAPI.py +11 -0
  2. DIRAC/Core/Utilities/CGroups2.py +1 -0
  3. DIRAC/Core/Utilities/ElasticSearchDB.py +1 -1
  4. DIRAC/Core/Utilities/MySQL.py +51 -25
  5. DIRAC/DataManagementSystem/Client/DataManager.py +7 -10
  6. DIRAC/DataManagementSystem/Client/FTS3Job.py +12 -3
  7. DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py +41 -11
  8. DIRAC/Interfaces/API/Dirac.py +12 -4
  9. DIRAC/Interfaces/API/Job.py +62 -17
  10. DIRAC/RequestManagementSystem/private/RequestTask.py +2 -1
  11. DIRAC/Resources/Catalog/FileCatalogClient.py +18 -7
  12. DIRAC/Resources/Catalog/Utilities.py +3 -3
  13. DIRAC/Resources/Computing/BatchSystems/SLURM.py +1 -1
  14. DIRAC/Resources/Computing/BatchSystems/TimeLeft/TimeLeft.py +3 -1
  15. DIRAC/Resources/Computing/ComputingElement.py +39 -34
  16. DIRAC/Resources/Computing/InProcessComputingElement.py +20 -7
  17. DIRAC/Resources/Computing/PoolComputingElement.py +76 -37
  18. DIRAC/Resources/Computing/SingularityComputingElement.py +19 -9
  19. DIRAC/Resources/Computing/test/Test_InProcessComputingElement.py +69 -8
  20. DIRAC/Resources/Computing/test/Test_PoolComputingElement.py +102 -35
  21. DIRAC/Resources/Storage/GFAL2_StorageBase.py +9 -0
  22. DIRAC/TransformationSystem/Agent/TransformationAgent.py +12 -13
  23. DIRAC/WorkloadManagementSystem/Agent/JobCleaningAgent.py +1 -1
  24. DIRAC/WorkloadManagementSystem/Agent/PilotSyncAgent.py +4 -3
  25. DIRAC/WorkloadManagementSystem/Agent/StalledJobAgent.py +1 -1
  26. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_JobAgent.py +4 -3
  27. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_PilotLoggingAgent.py +3 -3
  28. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_PilotStatusAgent.py +4 -2
  29. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_PushJobAgent.py +5 -4
  30. DIRAC/WorkloadManagementSystem/Agent/test/Test_Agent_StalledJobAgent.py +4 -2
  31. DIRAC/WorkloadManagementSystem/Client/JobReport.py +10 -6
  32. DIRAC/WorkloadManagementSystem/Client/JobState/JobState.py +12 -3
  33. DIRAC/WorkloadManagementSystem/Client/Matcher.py +18 -24
  34. DIRAC/WorkloadManagementSystem/DB/TaskQueueDB.py +137 -7
  35. DIRAC/WorkloadManagementSystem/Executor/JobScheduling.py +8 -14
  36. DIRAC/WorkloadManagementSystem/Executor/test/Test_Executor.py +3 -5
  37. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +4 -5
  38. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapperOfflineTemplate.py +1 -1
  39. DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapperTemplate.py +1 -2
  40. DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +1 -1
  41. DIRAC/WorkloadManagementSystem/Utilities/JobParameters.py +81 -2
  42. DIRAC/WorkloadManagementSystem/Utilities/QueueUtilities.py +5 -5
  43. DIRAC/WorkloadManagementSystem/Utilities/RemoteRunner.py +2 -1
  44. DIRAC/WorkloadManagementSystem/Utilities/test/Test_RemoteRunner.py +7 -3
  45. DIRAC/WorkloadManagementSystem/scripts/dirac_wms_get_wn_parameters.py +3 -3
  46. DIRAC/__init__.py +1 -1
  47. DIRAC/tests/Utilities/testJobDefinitions.py +57 -20
  48. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/METADATA +2 -2
  49. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/RECORD +53 -53
  50. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/WHEEL +0 -0
  51. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/entry_points.txt +0 -0
  52. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/licenses/LICENSE +0 -0
  53. {dirac-9.0.13.dist-info → dirac-9.0.15.dist-info}/top_level.txt +0 -0
@@ -299,8 +299,19 @@ class CSAPI:
299
299
  if "Users" in groupsDict[group] and entity in groupsDict[group]["Users"]:
300
300
  entitiesDict[entity]["Groups"].append(group)
301
301
  entitiesDict[entity]["Groups"].sort()
302
+ entitiesDict[entity]["AffiliationEnds"] = self.getUserAffiliationEnds(entity)
302
303
  return S_OK(entitiesDict)
303
304
 
305
+ def getUserAffiliationEnds(self, nick):
306
+ affiliation_ends_current = {}
307
+ csSection = f"{self.__baseSecurity}/Users/{nick}/"
308
+ user_sections = self.__csMod.getSections(csSection)
309
+ if "AffiliationEnds" in user_sections:
310
+ affiliation_ends_opts = self.__csMod.getOptions(f"{csSection}/AffiliationEnds")
311
+ for vo_ in affiliation_ends_opts:
312
+ affiliation_ends_current[vo_] = self.__csMod.getValue(f"{csSection}/AffiliationEnds/{vo_}")
313
+ return affiliation_ends_current
314
+
304
315
  def listGroups(self):
305
316
  """
306
317
  List all groups
@@ -300,6 +300,7 @@ class CG2Manager(metaclass=DIRACSingleton):
300
300
  if "ceParameters" in kwargs:
301
301
  if cpuLimit := kwargs["ceParameters"].get("CPULimit", None):
302
302
  cores = float(cpuLimit)
303
+ # MemoryLimitMB should be the job upper limit
303
304
  if memoryMB := int(kwargs["ceParameters"].get("MemoryLimitMB", 0)):
304
305
  memory = memoryMB * 1024 * 1024
305
306
  if kwargs["ceParameters"].get("MemoryNoSwap", "no").lower() in ("yes", "true"):
@@ -621,7 +621,7 @@ class ElasticSearchDB:
621
621
  if period.lower() == "day":
622
622
  suffix = todayUTC.strftime("%Y-%m-%d")
623
623
  elif period.lower() == "week":
624
- suffix = todayUTC.isocalendar()[1]
624
+ suffix = f"{todayUTC.strftime('%Y')}-{todayUTC.isocalendar()[1]}"
625
625
  elif period.lower() == "month":
626
626
  suffix = todayUTC.strftime("%Y-%m")
627
627
  elif period.lower() == "year":
@@ -903,6 +903,21 @@ class MySQL:
903
903
  return createView
904
904
  return S_OK()
905
905
 
906
+ def _parseForeignKeyReference(self, auxTable, defaultKey):
907
+ """
908
+ Parse foreign key reference in format 'Table' or 'Table.key'
909
+
910
+ :param str auxTable: Foreign key reference (e.g., 'MyTable' or 'MyTable.id')
911
+ :param str defaultKey: Default key name if not specified in auxTable
912
+ :return: tuple (table_name, key_name)
913
+ """
914
+ if "." in auxTable:
915
+ parts = auxTable.split(".", 1)
916
+ if len(parts) != 2:
917
+ raise ValueError(f"Invalid foreign key reference format: {auxTable}")
918
+ return parts[0], parts[1]
919
+ return auxTable, defaultKey
920
+
906
921
  def _createTables(self, tableDict, force=False):
907
922
  """
908
923
  tableDict:
@@ -957,30 +972,37 @@ class MySQL:
957
972
  if "Fields" not in thisTable:
958
973
  return S_ERROR(DErrno.EMYSQL, f"Missing `Fields` key in `{table}` table dictionary")
959
974
 
960
- tableCreationList = [[]]
961
-
975
+ # Build dependency-ordered list of tables to create
976
+ # Tables with foreign keys must be created after their referenced tables
977
+ tableCreationList = []
962
978
  auxiliaryTableList = []
963
979
 
964
- i = 0
980
+ # Get list of existing tables in the database to handle migrations
981
+ existingTablesResult = self._query("SHOW TABLES")
982
+ if not existingTablesResult["OK"]:
983
+ return existingTablesResult
984
+ existingTables = [t[0] for t in existingTablesResult["Value"]]
985
+
965
986
  extracted = True
966
987
  while tableList and extracted:
967
988
  # iterate extracting tables from list if they only depend on
968
989
  # already extracted tables.
969
990
  extracted = False
970
- auxiliaryTableList += tableCreationList[i]
971
- i += 1
972
- tableCreationList.append([])
991
+ currentLevelTables = []
992
+
973
993
  for table in list(tableList):
974
994
  toBeExtracted = True
975
995
  thisTable = tableDict[table]
976
996
  if "ForeignKeys" in thisTable:
977
997
  thisKeys = thisTable["ForeignKeys"]
978
998
  for key, auxTable in thisKeys.items():
979
- forTable = auxTable.split(".")[0]
980
- forKey = key
981
- if forTable != auxTable:
982
- forKey = auxTable.split(".")[1]
983
- if forTable not in auxiliaryTableList:
999
+ try:
1000
+ forTable, forKey = self._parseForeignKeyReference(auxTable, key)
1001
+ except ValueError as e:
1002
+ return S_ERROR(DErrno.EMYSQL, str(e))
1003
+
1004
+ # Check if the referenced table is either being created or already exists
1005
+ if forTable not in auxiliaryTableList and forTable not in existingTables:
984
1006
  toBeExtracted = False
985
1007
  break
986
1008
  if key not in thisTable["Fields"]:
@@ -988,24 +1010,29 @@ class MySQL:
988
1010
  DErrno.EMYSQL,
989
1011
  f"ForeignKey `{key}` -> `{forKey}` not defined in Primary table `{table}`.",
990
1012
  )
991
- if forKey not in tableDict[forTable]["Fields"]:
1013
+ # Only validate field existence if the referenced table is in tableDict
1014
+ if forTable in tableDict and forKey not in tableDict[forTable]["Fields"]:
992
1015
  return S_ERROR(
993
1016
  DErrno.EMYSQL,
994
- "ForeignKey `%s` -> `%s` not defined in Auxiliary table `%s`."
995
- % (key, forKey, forTable),
1017
+ f"ForeignKey `{key}` -> `{forKey}` not defined in Auxiliary table `{forTable}`.",
996
1018
  )
997
1019
 
998
1020
  if toBeExtracted:
999
1021
  # self.log.debug('Table %s ready to be created' % table)
1000
1022
  extracted = True
1001
1023
  tableList.remove(table)
1002
- tableCreationList[i].append(table)
1024
+ currentLevelTables.append(table)
1025
+
1026
+ if currentLevelTables:
1027
+ tableCreationList.append(currentLevelTables)
1028
+ auxiliaryTableList.extend(currentLevelTables)
1003
1029
 
1004
1030
  if tableList:
1005
1031
  return S_ERROR(DErrno.EMYSQL, f"Recursive Foreign Keys in {', '.join(tableList)}")
1006
1032
 
1007
- for tableList in tableCreationList:
1008
- for table in tableList:
1033
+ # Create tables level by level
1034
+ for levelTables in tableCreationList:
1035
+ for table in levelTables:
1009
1036
  # Check if Table exist
1010
1037
  retDict = self.__checkTable(table, force=force)
1011
1038
  if not retDict["OK"]:
@@ -1035,18 +1062,17 @@ class MySQL:
1035
1062
  for index in indexDict:
1036
1063
  indexedFields = "`, `".join(indexDict[index])
1037
1064
  cmdList.append(f"UNIQUE INDEX `{index}` ( `{indexedFields}` )")
1065
+
1038
1066
  if "ForeignKeys" in thisTable:
1039
1067
  thisKeys = thisTable["ForeignKeys"]
1040
1068
  for key, auxTable in thisKeys.items():
1041
- forTable = auxTable.split(".")[0]
1042
- forKey = key
1043
- if forTable != auxTable:
1044
- forKey = auxTable.split(".")[1]
1069
+ try:
1070
+ forTable, forKey = self._parseForeignKeyReference(auxTable, key)
1071
+ except ValueError as e:
1072
+ return S_ERROR(DErrno.EMYSQL, str(e))
1045
1073
 
1046
- # cmdList.append( '`%s` %s' % ( forTable, tableDict[forTable]['Fields'][forKey] )
1047
1074
  cmdList.append(
1048
- "FOREIGN KEY ( `%s` ) REFERENCES `%s` ( `%s` )"
1049
- " ON DELETE RESTRICT" % (key, forTable, forKey)
1075
+ f"FOREIGN KEY ( `{key}` ) REFERENCES `{forTable}` ( `{forKey}` ) ON DELETE RESTRICT"
1050
1076
  )
1051
1077
 
1052
1078
  engine = thisTable.get("Engine", "InnoDB")
@@ -1058,7 +1084,7 @@ class MySQL:
1058
1084
  engine,
1059
1085
  charset,
1060
1086
  )
1061
- retDict = self._transaction([cmd])
1087
+ retDict = self._update(cmd)
1062
1088
  if not retDict["OK"]:
1063
1089
  return retDict
1064
1090
  # self.log.debug('Table %s created' % table)
@@ -22,7 +22,7 @@ from DIRAC import S_OK, S_ERROR, gLogger, gConfig
22
22
  from DIRAC.Core.Utilities import DErrno
23
23
  from DIRAC.Core.Utilities.Adler import fileAdler, compareAdler
24
24
  from DIRAC.Core.Utilities.File import makeGuid, getSize
25
- from DIRAC.Core.Utilities.List import randomize, breakListIntoChunks
25
+ from DIRAC.Core.Utilities.List import randomize
26
26
  from DIRAC.Core.Utilities.ReturnValues import returnSingleResult
27
27
  from DIRAC.Core.Security.ProxyInfo import getProxyInfo
28
28
  from DIRAC.Core.Security.ProxyInfo import getVOfromProxyGroup
@@ -1677,18 +1677,15 @@ class DataManager:
1677
1677
  """get replicas from catalogue and filter if requested
1678
1678
  Warning: all filters are independent, hence active and preferDisk should be set if using forJobs
1679
1679
  """
1680
- catalogReplicas = {}
1681
- failed = {}
1680
+
1682
1681
  if not protocol:
1683
1682
  protocol = self.registrationProtocol
1684
1683
 
1685
- for lfnChunk in breakListIntoChunks(lfns, 1000):
1686
- res = self.fileCatalog.getReplicas(lfnChunk, allStatus=allStatus)
1687
- if res["OK"]:
1688
- catalogReplicas.update(res["Value"]["Successful"])
1689
- failed.update(res["Value"]["Failed"])
1690
- else:
1691
- return res
1684
+ res = self.fileCatalog.getReplicas(lfns, allStatus=allStatus)
1685
+ if not res["OK"]:
1686
+ return res
1687
+ catalogReplicas = res["Value"]["Successful"]
1688
+ failed = res["Value"]["Failed"]
1692
1689
  if not getUrl:
1693
1690
  for lfn in catalogReplicas:
1694
1691
  catalogReplicas[lfn] = dict.fromkeys(catalogReplicas[lfn], True)
@@ -272,7 +272,7 @@ class FTS3Job(JSerializable):
272
272
 
273
273
  # If the file is failed, check if it is recoverable
274
274
  if file_state in FTS3File.FTS_FAILED_STATES:
275
- if not fileDict.get("Recoverable", True):
275
+ if not fileDict.get("recoverable", True):
276
276
  filesStatus[file_id]["status"] = "Defunct"
277
277
 
278
278
  # If the file is not in a final state, but the job is, we return an error
@@ -400,7 +400,7 @@ class FTS3Job(JSerializable):
400
400
  :return: S_OK( (job object, list of ftsFileIDs in the job))
401
401
  """
402
402
 
403
- log = gLogger.getSubLogger(f"constructTransferJob/{self.operationID}/{self.sourceSE}_{self.targetSE}")
403
+ log = gLogger.getLocalSubLogger(f"constructTransferJob/{self.operationID}/{self.sourceSE}_{self.targetSE}")
404
404
 
405
405
  isMultiHop = False
406
406
  useTokens = False
@@ -411,6 +411,15 @@ class FTS3Job(JSerializable):
411
411
  log.debug(f"Multihop job has {len(allLFNs)} files while only 1 allowed")
412
412
  return S_ERROR(errno.E2BIG, "Trying multihop job with more than one file !")
413
413
  allHops = [(self.sourceSE, self.multiHopSE), (self.multiHopSE, self.targetSE)]
414
+ if tokensEnabled:
415
+ tokensEnabled = all(
416
+ [
417
+ self.__seTokenSupport(StorageElement(seName))
418
+ for seName in (self.sourceSE, self.multiHopSE, self.targetSE)
419
+ ]
420
+ )
421
+ if not tokensEnabled:
422
+ log.warn("Not using token because not all hop supports it")
414
423
  isMultiHop = True
415
424
  else:
416
425
  allHops = [(self.sourceSE, self.targetSE)]
@@ -736,7 +745,7 @@ class FTS3Job(JSerializable):
736
745
  :return: S_OK( (job object, list of ftsFileIDs in the job))
737
746
  """
738
747
 
739
- log = gLogger.getSubLogger(f"constructStagingJob/{self.operationID}/{self.targetSE}")
748
+ log = gLogger.getLocalSubLogger(f"constructStagingJob/{self.operationID}/{self.targetSE}")
740
749
 
741
750
  transfers = []
742
751
  fileIDsInTheJob = set()
@@ -1,5 +1,5 @@
1
- """ SystemAdministrator service is a tool to control and monitor the DIRAC services and agents
2
- """
1
+ """SystemAdministrator service is a tool to control and monitor the DIRAC services and agents"""
2
+
3
3
  import socket
4
4
  import os
5
5
  import re
@@ -254,17 +254,38 @@ class SystemAdministratorHandler(RequestHandler):
254
254
  types_updateSoftware = [str]
255
255
 
256
256
  def export_updateSoftware(self, version):
257
+ """Update DIRAC, or its extension, to a version. Use extension_name==version for the DIRAC extension.
258
+
259
+ A version can be:
260
+ - a PEP440 valid version of DIRAC.
261
+ - a PEP440 valid version of a DIRAC extension.
262
+ - "integration" or "devel" or "master" or "main" would all be interpreted as git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]
263
+ - a git tag/branch like git+https://github.com/fstagni/DIRAC.git@test_branch#egg=DIRAC[server]
264
+ """
257
265
  # Validate and normalise the requested version
258
266
  primaryExtension = None
259
267
  if "==" in version:
260
268
  primaryExtension, version = version.split("==")
261
- try:
262
- version = Version(version)
263
- except InvalidVersion:
264
- self.log.exception("Invalid version passed", version)
265
- return S_ERROR(f"Invalid version passed {version!r}")
266
- isPrerelease = version.is_prerelease
267
- version = f"v{version}"
269
+
270
+ released_version = True
271
+ isPrerelease = False
272
+
273
+ # Special cases (e.g. installing the integration/main branch)
274
+ if version.lower() in ["integration", "devel", "master", "main"]:
275
+ released_version = False
276
+ version = "git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]"
277
+
278
+ if released_version:
279
+ try:
280
+ version = Version(version)
281
+ isPrerelease = version.is_prerelease
282
+ version = f"v{version}"
283
+ except InvalidVersion:
284
+ if "https://" in version:
285
+ released_version = False
286
+ else:
287
+ self.log.exception("Invalid version passed", version)
288
+ return S_ERROR(f"Invalid version passed {version!r}")
268
289
 
269
290
  # Find what to install
270
291
  otherExtensions = []
@@ -290,10 +311,11 @@ class SystemAdministratorHandler(RequestHandler):
290
311
  installer.flush()
291
312
  self.log.info("Downloaded DIRACOS installer to", installer.name)
292
313
 
314
+ directory = version if released_version else version.split("@")[1].split("#")[0]
293
315
  newProPrefix = os.path.join(
294
316
  rootPath,
295
317
  "versions",
296
- f"{version}-{datetime.utcnow().strftime('%s')}",
318
+ f"{directory}-{datetime.utcnow().strftime('%s')}",
297
319
  )
298
320
  installPrefix = os.path.join(newProPrefix, f"{platform.system()}-{platform.machine()}")
299
321
  self.log.info("Running DIRACOS installer for prefix", installPrefix)
@@ -313,7 +335,15 @@ class SystemAdministratorHandler(RequestHandler):
313
335
  cmd = [f"{installPrefix}/bin/pip", "install", "--no-color", "-v"]
314
336
  if isPrerelease:
315
337
  cmd += ["--pre"]
316
- cmd += [f"{primaryExtension}[server]=={version}"]
338
+ if released_version:
339
+ cmd += [f"{primaryExtension}[server]=={version}"]
340
+ else:
341
+ # from here on we assume a version like git+https://github.com/DIRACGrid/DIRAC.git@integration#egg=DIRAC[server]
342
+ # is specified, for the primaryExtension
343
+ if not version.startswith("git+"):
344
+ version = f"git+{version}"
345
+ cmd += [version]
346
+
317
347
  cmd += [f"{e}[server]" for e in otherExtensions]
318
348
  r = subprocess.run(
319
349
  cmd,
@@ -208,7 +208,9 @@ class Dirac(API):
208
208
  return S_OK("Nothing to do")
209
209
 
210
210
  #############################################################################
211
- def getInputDataCatalog(self, lfns, siteName="", fileName="pool_xml_catalog.xml", ignoreMissing=False):
211
+ def getInputDataCatalog(
212
+ self, lfns, siteName="", fileName="pool_xml_catalog.xml", ignoreMissing=False, inputDataPolicy=None
213
+ ):
212
214
  """This utility will create a pool xml catalogue slice for the specified LFNs using
213
215
  the full input data resolution policy plugins for the VO.
214
216
 
@@ -228,6 +230,10 @@ class Dirac(API):
228
230
  :type siteName: string
229
231
  :param fileName: Catalogue name (can include path)
230
232
  :type fileName: string
233
+ :param ignoreMissing: Flag to ignore missing files
234
+ :type ignoreMissing: bool
235
+ :param inputDataPolicy: Input data policy module to use
236
+ :type inputDataPolicy: string or None
231
237
  :returns: S_OK,S_ERROR
232
238
 
233
239
  """
@@ -248,8 +254,8 @@ class Dirac(API):
248
254
 
249
255
  self.log.verbose(localSEList)
250
256
 
251
- inputDataPolicy = Operations().getValue("InputDataPolicy/InputDataModule")
252
- if not inputDataPolicy:
257
+ inputDataModule = Operations().getValue("InputDataPolicy/InputDataModule")
258
+ if not inputDataModule:
253
259
  return self._errorReport("Could not retrieve /DIRAC/Operations/InputDataPolicy/InputDataModule for VO")
254
260
 
255
261
  self.log.info(f"Attempting to resolve data for {siteName}")
@@ -279,11 +285,13 @@ class Dirac(API):
279
285
 
280
286
  self.log.verbose(configDict)
281
287
  argumentsDict = {"FileCatalog": resolvedData, "Configuration": configDict, "InputData": lfns}
288
+ if inputDataPolicy:
289
+ argumentsDict["Job"] = {"InputDataPolicy": inputDataPolicy}
282
290
  if ignoreMissing:
283
291
  argumentsDict["IgnoreMissing"] = True
284
292
  self.log.verbose(argumentsDict)
285
293
 
286
- result = self.objectLoader.loadObject(inputDataPolicy)
294
+ result = self.objectLoader.loadObject(inputDataModule)
287
295
  if not result["OK"]:
288
296
  return result
289
297
  module = result["Value"](argumentsDict)
@@ -1,27 +1,28 @@
1
1
  """
2
- Job Base Class
2
+ Job Base Class
3
3
 
4
- This class provides generic job definition functionality suitable for any VO.
4
+ This class provides generic job definition functionality suitable for any VO.
5
5
 
6
- Helper functions are documented with example usage for the DIRAC API. An example
7
- script (for a simple executable) would be::
6
+ Helper functions are documented with example usage for the DIRAC API. An example
7
+ script (for a simple executable) would be::
8
8
 
9
- from DIRAC.Interfaces.API.Dirac import Dirac
10
- from DIRAC.Interfaces.API.Job import Job
9
+ from DIRAC.Interfaces.API.Dirac import Dirac
10
+ from DIRAC.Interfaces.API.Job import Job
11
11
 
12
- j = Job()
13
- j.setCPUTime(500)
14
- j.setExecutable('/bin/echo hello')
15
- j.setExecutable('yourPythonScript.py')
16
- j.setExecutable('/bin/echo hello again')
17
- j.setName('MyJobName')
12
+ j = Job()
13
+ j.setCPUTime(500)
14
+ j.setExecutable('/bin/echo hello')
15
+ j.setExecutable('yourPythonScript.py')
16
+ j.setExecutable('/bin/echo hello again')
17
+ j.setName('MyJobName')
18
18
 
19
- dirac = Dirac()
20
- jobID = dirac.submitJob(j)
21
- print 'Submission Result: ',jobID
19
+ dirac = Dirac()
20
+ jobID = dirac.submitJob(j)
21
+ print 'Submission Result: ',jobID
22
22
 
23
- Note that several executables can be provided and wil be executed sequentially.
23
+ Note that several executables can be provided and wil be executed sequentially.
24
24
  """
25
+
25
26
  import os
26
27
  import re
27
28
  import shlex
@@ -517,6 +518,50 @@ class Job(API):
517
518
  return S_OK()
518
519
 
519
520
  #############################################################################
521
+ def setRAMRequirements(self, ramRequired: int = 0, maxRAM: int = 0):
522
+ """Helper function.
523
+ Specify the RAM requirements for the job, in MB. 0 (default) means no specific requirements.
524
+
525
+ Example usage:
526
+
527
+ >>> job = Job()
528
+ >>> job.setRAMRequirements(ramRequired=2000)
529
+ means that the job needs at least 2 GBs of RAM to work. This is taken into consideration at job's matching time.
530
+ The job definition does not specify an upper limit.
531
+ From a user's point of view this is fine (normally, not for admins).
532
+
533
+ >>> job.setRAMRequirements(ramRequired=500, maxRAM=3800)
534
+ means that the job needs 500 MBs of RAM to work. 3.8 GBs will then be the upper limit for CG2 limits.
535
+
536
+ >>> job.setRAMRequirements(ramRequired=3200, maxRAM=3200)
537
+ means that we should match this job if there is at least 3.2 available GBs of run. At the same time, CG2 will not allow to use more than that.
538
+
539
+ >>> job.setRAMRequirements(maxRAM=4000)
540
+ means that the job does not set a min amount of RAM (so can match--run "everywhere"), but the 4 GBs will then be the upper limit for CG2 limits.
541
+
542
+ >>> job.setRAMRequirements(ramRequired=8000, maxRAM=4000)
543
+ Makes no sense, an error will be raised
544
+ """
545
+ if ramRequired and maxRAM and ramRequired > maxRAM:
546
+ return self._reportError("Invalid settings, ramRequired is higher than maxRAM")
547
+
548
+ if ramRequired:
549
+ self._addParameter(
550
+ self.workflow,
551
+ "MinRAM",
552
+ "JDL",
553
+ ramRequired,
554
+ "MBs of RAM requested",
555
+ )
556
+ if maxRAM:
557
+ self._addParameter(
558
+ self.workflow,
559
+ "MaxRAM",
560
+ "JDL",
561
+ maxRAM,
562
+ "Max MBs of RAM to be used",
563
+ )
564
+
520
565
  def setNumberOfProcessors(self, numberOfProcessors=None, minNumberOfProcessors=None, maxNumberOfProcessors=None):
521
566
  """Helper function to set the number of processors required by the job.
522
567
 
@@ -709,7 +754,7 @@ class Job(API):
709
754
  Example usage:
710
755
 
711
756
  >>> job = Job()
712
- >>> job.setTag( ['WholeNode','8GBMemory'] )
757
+ >>> job.setTag( ['WholeNode'] )
713
758
 
714
759
  :param tags: single tag string or a list of tags
715
760
  :type tags: str or python:list
@@ -106,7 +106,8 @@ class RequestTask:
106
106
  userDN, userGroup, requiredTimeLeft=1200, cacheTime=4 * 43200
107
107
  )
108
108
  if not getProxy["OK"]:
109
- return S_ERROR(f"unable to setup shifter proxy for {shifter}: {getProxy['Message']}")
109
+ self.log.error("unable to setup shifter proxy", f"{shifter}: {getProxy['Message']}")
110
+ continue
110
111
  chain = getProxy["chain"]
111
112
  fileName = getProxy["Value"]
112
113
  self.log.debug(f"got {shifter}: {userName} {userGroup}")
@@ -1,15 +1,18 @@
1
- """ The FileCatalogClient is a class representing the client of the DIRAC File Catalog
2
- """
1
+ """The FileCatalogClient is a class representing the client of the DIRAC File Catalog"""
2
+
3
3
  import json
4
4
  import os
5
5
 
6
6
  from DIRAC import S_OK, S_ERROR
7
+ from DIRAC.Core.Utilities.List import breakListIntoChunks
7
8
  from DIRAC.Core.Tornado.Client.ClientSelector import TransferClientSelector as TransferClient
8
9
 
9
10
  from DIRAC.ConfigurationSystem.Client.Helpers.Registry import getVOMSAttributeForGroup, getDNForUsername
10
11
  from DIRAC.Resources.Catalog.Utilities import checkCatalogArguments
11
12
  from DIRAC.Resources.Catalog.FileCatalogClientBase import FileCatalogClientBase
12
13
 
14
+ GET_REPLICAS_CHUNK_SIZE = 10_000
15
+
13
16
 
14
17
  class FileCatalogClient(FileCatalogClientBase):
15
18
  """Client code to the DIRAC File Catalogue"""
@@ -135,14 +138,22 @@ class FileCatalogClient(FileCatalogClientBase):
135
138
  @checkCatalogArguments
136
139
  def getReplicas(self, lfns, allStatus=False, timeout=120):
137
140
  """Get the replicas of the given files"""
138
- rpcClient = self._getRPC(timeout=timeout)
139
- result = rpcClient.getReplicas(lfns, allStatus)
141
+ successful = {}
142
+ failed = {}
140
143
 
141
- if not result["OK"]:
142
- return result
144
+ # We want to sort the lfns because of the way the server groups the db queries by
145
+ # directory. So if we sort them, the grouping is more efficient.
146
+ for chunk in breakListIntoChunks(sorted(lfns), GET_REPLICAS_CHUNK_SIZE):
147
+ rpcClient = self._getRPC(timeout=timeout)
148
+ result = rpcClient.getReplicas(chunk, allStatus)
149
+
150
+ if not result["OK"]:
151
+ return result
152
+ successful.update(result["Value"]["Successful"])
153
+ failed.update(result["Value"]["Failed"])
143
154
 
144
155
  # If there is no PFN returned, just set the LFN instead
145
- lfnDict = result["Value"]
156
+ lfnDict = {"Successful": successful, "Failed": failed}
146
157
  for lfn in lfnDict["Successful"]:
147
158
  for se in lfnDict["Successful"][lfn]:
148
159
  if not lfnDict["Successful"][lfn][se]:
@@ -1,5 +1,5 @@
1
- """ DIRAC FileCatalog client utilities
2
- """
1
+ """DIRAC FileCatalog client utilities"""
2
+
3
3
  import os
4
4
  import errno
5
5
  import functools
@@ -16,7 +16,7 @@ def checkArgumentFormat(path, generateMap=False):
16
16
  """Check and process format of the arguments to FileCatalog methods"""
17
17
  if isinstance(path, str):
18
18
  urls = {path: True}
19
- elif isinstance(path, list):
19
+ elif isinstance(path, (list, set)):
20
20
  urls = {}
21
21
  for url in path:
22
22
  urls[url] = True
@@ -300,7 +300,7 @@ srun -l -k %(wrapper)s
300
300
 
301
301
  waitingJobs = 0
302
302
  runningJobs = 0
303
- lines = output.split("\n")
303
+ lines = output.strip().split("\n")
304
304
  for line in lines[1:]:
305
305
  _jid, status = line.split()
306
306
  if status in ["PENDING", "SUSPENDED", "CONFIGURING"]:
@@ -75,7 +75,9 @@ class TimeLeft:
75
75
 
76
76
  resourceDict = self.batchPlugin.getResourceUsage()
77
77
  if not resourceDict["OK"]:
78
- self.log.warn(f"Could not determine timeleft for batch system at site {DIRAC.siteName()}")
78
+ self.log.warn(
79
+ f"Could not determine timeleft for batch system at site {DIRAC.siteName()}: {resourceDict['Message']}"
80
+ )
79
81
  return resourceDict
80
82
 
81
83
  resources = resourceDict["Value"]