PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/src/python/WMComponent/PhEDExInjector/PhEDExInjectorSubscriber.py

https://github.com/PerilousApricot/WMCore
Python | 294 lines | 271 code | 0 blank | 23 comment | 1 complexity | 062cc20f22cf8026bc299f557dec7877 MD5 | raw file
  1. #!/usr/bin/env python
  2. """
  3. _PhEDExInjectorSubscriber_
  4. Poll the DBSBuffer database for unsubscribed datasets, and make subscriptions
  5. according to the specs associated with this dataset.
  6. Each dataset can be associated with multiple task, each task specifies (through the spec) the following options:
  7. - CustodialSites: Sites where the dataset should be custodial
  8. - NonCustodialSites: Sites where the dataset should be non-custodial
  9. - AutoApproveSites: Sites where the subscription will be approved automatically with the request
  10. - Priority: Priority of the subscription, can be Low, Normal or High
  11. As different task can have different options, these are aggregated following this simple rules:
  12. - All sites in the first three parameters will be included, removing duplicates. If a site is in the custodial list, it will be
  13. removed from the NonCustodial and AutoApprove lists. Any site in the AutoApprove list that is not in the non-custodial
  14. list will be ignored.
  15. - The lowest configured priority will be chosen for each dataset
  16. There are 2 modes of operation for the subscriber, defined by config.PhEDExInjector.safeOperationMode. Defaults to False
  17. - "Unsafe" operation mode (False): All custodial subscriptions are Move subscriptions and
  18. are requested as soon as the unsuscribed dataset appears in the dbsbuffer table
  19. and there is at least one file in Global DBS and PhEDEx.
  20. All non-custodial subscriptions are Replica, and requested at the same time as the custodial ones.
  21. - "Safe" operation mode (True): Custodial subscriptions are done in two phases, when a dataset appears in dbsbuffer
  22. a Replica subscription will be requested. The dataset will be kept in a non-terminal
  23. subscription state in dbsbuffer, it will be polled and when there is no associated
  24. workflow in WMBS then a Move subscription will be requested.
  25. Non-custodial subscriptions behave as in "Unsafe" mode.
  26. Additional options are:
  27. - config.PhEDExInjector.subscribeDatasets, if False then this component doesn't run
  28. """
  29. import threading
  30. import logging
  31. from WMCore.WorkerThreads.BaseWorkerThread import BaseWorkerThread
  32. from WMCore.Services.PhEDEx import XMLDrop
  33. from WMCore.Services.PhEDEx.PhEDEx import PhEDEx
  34. from WMCore.Services.PhEDEx.DataStructs.SubscriptionList import PhEDExSubscription
  35. from WMCore.Services.SiteDB.SiteDB import SiteDBJSON
  36. from WMCore.DAOFactory import DAOFactory
  37. from WMCore.WMSpec.WMWorkload import WMWorkloadHelper
  38. """
  39. Useful lambda functions
  40. """
  41. # Add site lists without duplicates
  42. extendWithoutDups = lambda x, y : x + list(set(y) - set(x))
  43. # Choose the lowest priority
  44. solvePrioConflicts = lambda x, y : y if x == "High" or y == "Low" else x
  45. class PhEDExInjectorSubscriber(BaseWorkerThread):
  46. """
  47. _PhEDExInjectorSubscriber_
  48. Poll the DBSBuffer database and subscribe datasets as they are
  49. created.
  50. """
  51. def __init__(self, config):
  52. """
  53. ___init___
  54. Initialise class members
  55. """
  56. BaseWorkerThread.__init__(self)
  57. self.phedex = PhEDEx({"endpoint": config.PhEDExInjector.phedexurl}, "json")
  58. self.siteDB = SiteDBJSON()
  59. self.dbsUrl = config.DBSInterface.globalDBSUrl
  60. self.group = getattr(config.PhEDExInjector, "group", "DataOps")
  61. self.safeMode = getattr(config.PhEDExInjector, "safeOperationMode", False)
  62. self.replicaOnly = getattr(config.PhEDExInjector, "replicaOnly", False)
  63. # Subscribed state in the DBSBuffer table for datasets
  64. self.terminalSubscriptionState = 1
  65. if self.safeMode:
  66. self.terminalSubscriptionState = 2
  67. # We will map node names to CMS names, that what the spec will have.
  68. # If a CMS name is associated to many PhEDEx node then choose the MSS option
  69. self.cmsToPhedexMap = {}
  70. # initialize the alert framework (if available - config.Alert present)
  71. # self.sendAlert will be then be available
  72. self.initAlerts(compName = "PhEDExInjector")
  73. def setup(self, parameters):
  74. """
  75. _setup_
  76. Create a DAO Factory for the PhEDExInjector. Also load the SE names to
  77. PhEDEx node name mappings from the data service.
  78. """
  79. myThread = threading.currentThread()
  80. daofactory = DAOFactory(package = "WMComponent.PhEDExInjector.Database",
  81. logger = self.logger,
  82. dbinterface = myThread.dbi)
  83. self.getUnsubscribed = daofactory(classname = "GetUnsubscribedDatasets")
  84. self.markSubscribed = daofactory(classname = "MarkDatasetSubscribed")
  85. self.getPartiallySubscribed = daofactory(classname = "GetPartiallySubscribedDatasets")
  86. nodeMappings = self.phedex.getNodeMap()
  87. for node in nodeMappings["phedex"]["node"]:
  88. cmsName = self.siteDB.phEDExNodetocmsName(node["name"])
  89. if cmsName not in self.cmsToPhedexMap:
  90. self.cmsToPhedexMap[cmsName] = {}
  91. logging.info("Loaded PhEDEx node %s for site %s" % (node["name"], cmsName))
  92. if node["kind"] not in self.cmsToPhedexMap[cmsName]:
  93. self.cmsToPhedexMap[cmsName][node["kind"]] = node["name"]
  94. return
  95. def algorithm(self, parameters):
  96. """
  97. _algorithm_
  98. Poll the database for datasets and subscribe them.
  99. """
  100. myThread = threading.currentThread()
  101. myThread.transaction.begin()
  102. # Check for completely unsubscribed datasets
  103. unsubscribedDatasets = self.getUnsubscribed.execute(conn = myThread.transaction.conn,
  104. transaction = True)
  105. if self.safeMode:
  106. partiallySubscribedDatasets = self.getPartiallySubscribed.execute(conn = myThread.transaction.conn,
  107. transaction = True)
  108. unsubscribedDatasets.extend(partiallySubscribedDatasets)
  109. partiallySubscribedSet = set()
  110. for entry in partiallySubscribedDatasets:
  111. partiallySubscribedSet.add(entry["path"])
  112. # Map the datasets to their specs
  113. specDatasetMap = {}
  114. for unsubscribedDataset in unsubscribedDatasets:
  115. datasetPath = unsubscribedDataset["path"]
  116. workflow = unsubscribedDataset["workflow"]
  117. spec = unsubscribedDataset["spec"]
  118. if datasetPath not in specDatasetMap:
  119. specDatasetMap[datasetPath] = []
  120. specDatasetMap[datasetPath].append({"workflow" : workflow, "spec" : spec})
  121. specCache = {}
  122. siteMap = {}
  123. # Distribute the subscriptions by site, type and priority
  124. # This is to make as few subscriptions as possible
  125. # Site map values are dictionaries where the keys are tuples (Prio, Custodial, AutoApprove, Move)
  126. # Where Custodial is boolean, Prio is in ["Low", "Normal", "High"], AutoApprove is boolean and Move is boolean
  127. for dataset in specDatasetMap:
  128. # Aggregate all the different subscription configurations
  129. subInfo = {}
  130. for entry in specDatasetMap[dataset]:
  131. if not entry["spec"]:
  132. # Can't use this spec, there isn't one
  133. continue
  134. # Load spec if not in the cache
  135. if entry["spec"] not in specCache:
  136. helper = WMWorkloadHelper()
  137. try:
  138. helper.load(entry["spec"])
  139. specCache[entry["spec"]] = helper
  140. except Exception:
  141. #Couldn't load it , alert and carry on
  142. msg = "Couldn't load spec: %s" % entry["spec"]
  143. logging.error(msg)
  144. self.sendAlert(7, msg = msg)
  145. continue
  146. #If we are running in safe mode, we need to know if the workflow is ready
  147. # We have the spec, get the info
  148. helper = specCache[entry["spec"]]
  149. workflowSubInfo = helper.getSubscriptionInformation()
  150. datasetSubInfo = workflowSubInfo.get(dataset, None)
  151. if datasetSubInfo and subInfo:
  152. subInfo["CustodialSites"] = extendWithoutDups(subInfo["CustodialSites"], datasetSubInfo["CustodialSites"])
  153. subInfo["NonCustodialSites"] = extendWithoutDups(subInfo["NonCustodialSites"], datasetSubInfo["NonCustodialSites"])
  154. subInfo["AutoApproveSites"] = extendWithoutDups(subInfo["AutoApproveSites"], datasetSubInfo["AutoApproveSites"])
  155. subInfo["Priority"] = solvePrioConflicts(subInfo["Priority"], datasetSubInfo["Priority"])
  156. elif datasetSubInfo:
  157. subInfo = datasetSubInfo
  158. # We now have aggregated subscription information for this dataset in subInfo
  159. # Distribute it by site
  160. if not subInfo:
  161. #Nothing to do, log and continue
  162. msg = "No subscriptions configured for dataset %s" % dataset
  163. logging.warning(msg)
  164. self.markSubscribed.execute(dataset, subscribed = self.terminalSubscriptionState,
  165. conn = myThread.transaction.conn,
  166. transaction = True)
  167. continue
  168. # Make sure that a site is not configured both as non custodial and custodial
  169. # Non-custodial is believed to be the right choice
  170. subInfo["CustodialSites"] = list(set(subInfo["CustodialSites"]) - set(subInfo["NonCustodialSites"]))
  171. for site in subInfo["CustodialSites"]:
  172. if site not in siteMap:
  173. siteMap[site] = {}
  174. autoApprove = False
  175. if site in subInfo["AutoApproveSites"]:
  176. autoApprove = True
  177. if self.safeMode and dataset not in partiallySubscribedSet:
  178. tupleKey = (subInfo["Priority"], True, autoApprove, False)
  179. else:
  180. tupleKey = (subInfo["Priority"], True, autoApprove, True)
  181. if tupleKey not in siteMap[site]:
  182. siteMap[site][tupleKey] = []
  183. # Subscriptions are sorted by options, defined by tupleKey
  184. # The tuple key has 3 or 4 entries in this order
  185. # Priority, Custodial, Auto approve, Move (True) or Replica (False)
  186. siteMap[site][tupleKey].append(dataset)
  187. # If we are in safe mode and this is a partially subscribed dataset,
  188. # then the non-custodial were done in a previous cycle
  189. if self.safeMode and dataset in partiallySubscribedSet:
  190. self.markSubscribed.execute(dataset, subscribed = self.terminalSubscriptionState,
  191. conn = myThread.transaction.conn,
  192. transaction = True)
  193. continue
  194. for site in subInfo["NonCustodialSites"]:
  195. if site not in siteMap:
  196. siteMap[site] = {}
  197. autoApprove = False
  198. if site in subInfo["AutoApproveSites"]:
  199. autoApprove = True
  200. # Non-custodial is never move, so this tuple has only 3 entries
  201. # TODO: Change tuples to frozensets for clarity
  202. tupleKey = (subInfo["Priority"], False, autoApprove)
  203. if tupleKey not in siteMap[site]:
  204. siteMap[site][tupleKey] = []
  205. siteMap[site][tupleKey].append(dataset)
  206. self.markSubscribed.execute(dataset, subscribed = 1,
  207. conn = myThread.transaction.conn,
  208. transaction = True)
  209. # Actually request the subscriptions
  210. for site in siteMap:
  211. # Check that the site is valid
  212. if site not in self.cmsToPhedexMap:
  213. msg = "Site %s doesn't appear to be valid to PhEDEx" % site
  214. logging.error(msg)
  215. self.sendAlert(7, msg = msg)
  216. continue
  217. for subscriptionFlavor in siteMap[site]:
  218. datasets = siteMap[site][subscriptionFlavor]
  219. # Check that the site is valid
  220. isMSS = False
  221. if "MSS" in self.cmsToPhedexMap[site]:
  222. isMSS = True
  223. phedexNode = self.cmsToPhedexMap[site]["MSS"]
  224. else:
  225. phedexNode = self.cmsToPhedexMap[site]["Disk"]
  226. logging.info("Subscribing %s to %s" % (datasets, site))
  227. options = {"custodial" : "n", "requestOnly" : "y",
  228. "priority" : subscriptionFlavor[0].lower(),
  229. "move" : "n"}
  230. if subscriptionFlavor[1] and isMSS:
  231. # Custodial subscriptions are only allowed in MSS nodes
  232. # If custodial is requested on Non-MSS it fallsback to a non-custodial subscription
  233. options["custodial"] = "y"
  234. if subscriptionFlavor[3] and not self.replicaOnly:
  235. options["move"] = "y"
  236. if subscriptionFlavor[2]:
  237. options["requestOnly"] = "n"
  238. logging.info("Request options: Custodial - %s, Move - %s, Request Only - %s" % (options["custodial"].upper(),
  239. options["move"].upper(),
  240. options["requestOnly"].upper()))
  241. newSubscription = PhEDExSubscription(datasets, phedexNode, self.group,
  242. **options)
  243. xmlData = XMLDrop.makePhEDExXMLForDatasets(self.dbsUrl,
  244. newSubscription.getDatasetPaths())
  245. logging.debug(str(xmlData))
  246. self.phedex.subscribe(newSubscription, xmlData)
  247. myThread.transaction.commit()
  248. return