Coverage for python/lsst/rucio/register/rucio_interface.py: 15%

143 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-16 01:02 -0800

1# This file is part of rucio_register 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22import hashlib 

23import logging 

24import random 

25import time 

26import zlib 

27 

28import rucio.common.exception 

29from rucio.client.didclient import DIDClient 

30from rucio.client.replicaclient import ReplicaClient 

31 

32import lsst.daf.butler 

33from lsst.rucio.register.resource_bundle import ResourceBundle 

34from lsst.rucio.register.rubin_meta import RubinMeta 

35from lsst.rucio.register.rucio_did import RucioDID 

36 

37__all__ = ["RucioInterface"] 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42class RucioInterface: 

43 """Add files as replicas in Rucio, along with metadata, 

44 and attach them to datasets. 

45 

46 Parameters 

47 ---------- 

48 butler: `lsst.daf.butler.Butler` 

49 Butler we're operating upon 

50 rucio_rse: `str` 

51 Name of the RSE that the files live in. 

52 scope: `str` 

53 Rucio scope to register the files in. 

54 rse_root: `str` 

55 Full path to root directory of RSE directory structure 

56 dtn_url: `str` 

57 Base URL of the data transfer node for the Rucio physical filename. 

58 """ 

59 

60 def __init__( 

61 self, 

62 butler: lsst.daf.butler.Butler, 

63 rucio_rse: str, 

64 scope: str, 

65 rse_root: str, 

66 dtn_url: str, 

67 ): 

68 self.butler = butler 

69 self.rse = rucio_rse 

70 self.scope = scope 

71 self.rse_root = rse_root 

72 self.dtn_url = dtn_url 

73 self.pfn_base = f"{dtn_url}" 

74 self.replica_client = ReplicaClient() 

75 self.did_client = DIDClient() 

76 

77 def _make_bundle(self, dataset_id, dataset_ref) -> ResourceBundle: 

78 """Make a ResourceBundle 

79 

80 Parameters 

81 ---------- 

82 dataset_id: `str` 

83 Rucio dataset name 

84 dataset_ref: `DatasetRef` 

85 Butler DatasetRef 

86 """ 

87 did = self._make_did(self.butler.getURI(dataset_ref), dataset_ref.to_json()) 

88 rb = ResourceBundle(dataset_id=dataset_id, did=did) 

89 return rb 

90 

91 def _make_did(self, resource_path, metadata: str) -> RucioDID: 

92 """Make a Rucio data identifier dictionary from a resource. 

93 

94 Parameters 

95 ---------- 

96 resource_path: ResourcePath 

97 ResourcePath object 

98 

99 metadata: `str` 

100 String containing Rubin dataset specific metadata 

101 

102 Returns 

103 ------- 

104 did: `dict [ str, str|int ]` 

105 Rucio data identifier including physical and logical names, 

106 byte length, adler32 and MD5 checksums, meta, and scope. 

107 """ 

108 with resource_path.open("rb") as f: 

109 contents = f.read() 

110 size = len(contents) 

111 md5 = hashlib.md5(contents).hexdigest() 

112 adler32 = f"{zlib.adler32(contents):08x}" 

113 path = resource_path.unquoted_path.removeprefix(self.rse_root) 

114 pfn = self.pfn_base + path 

115 logging.debug(f"{pfn=}") 

116 name = path.removeprefix("/" + self.scope + "/") 

117 logging.debug(f"{name=}") 

118 logging.debug(f"{path=}") 

119 

120 meta = RubinMeta(rubin_butler=1, rubin_sidecar=metadata) 

121 d = RucioDID( 

122 pfn=pfn, 

123 bytes=size, 

124 adler32=adler32, 

125 md5=md5, 

126 name=name, 

127 scope=self.scope, 

128 meta=meta, 

129 ) 

130 

131 return d 

132 

133 def _add_replicas(self, bundles: list[ResourceBundle]) -> None: 

134 """Call the Rucio method add_replica for a list of DIDs 

135 

136 Parameters 

137 ---------- 

138 bundles: `list[ResourceBundle]` 

139 A list of ResourceBundles 

140 """ 

141 dids = [bundle.get_did() for bundle in bundles] 

142 retries = 0 

143 max_retries = 5 

144 while True: 

145 try: 

146 self.replica_client.add_replicas(rse=self.rse, files=dids) 

147 break 

148 except rucio.common.exception.RucioException: 

149 retries += 1 

150 if retries < max_retries: 

151 seconds = random.randint(10, 20) 

152 logger.debug(f"failed to add_replicas; sleeping {seconds} seconds") 

153 time.sleep(seconds) 

154 self.replica_client = ReplicaClient() # XXX not sure we need to do this. 

155 else: 

156 raise Exception(f"Tried {max_retries} times and couldn't add_replicas") 

157 

158 def _add_file_to_dataset_with_retries(self, dataset_id, did): 

159 retries = 0 

160 max_retries = 5 

161 while True: 

162 try: 

163 self.did_client.add_files_to_dataset( 

164 scope=self.scope, name=dataset_id, files=[did], rse=self.rse 

165 ) 

166 break 

167 except rucio.common.exception.FileAlreadyExists: 

168 logger.debug(f"file {did} already registered in dataset {dataset_id}") 

169 return # we can return, because it's already in the dataset 

170 except rucio.common.exception.RucioException: 

171 retries += 1 

172 if retries < max_retries: 

173 seconds = random.randint(10, 20) 

174 logger.debug(f"failed to register one did to {dataset_id}; sleeping {seconds}") 

175 time.sleep(seconds) 

176 self.did_client = DIDClient() # XXX not sure we need to do this. 

177 else: 

178 # we tried max_retries times, and failed, so we'll bail out 

179 raise Exception(f"Couldn't add {did['pfn']} to dataset {dataset_id}") 

180 

181 def _add_files_to_dataset(self, dataset_id: str, dids: list[dict]) -> None: 

182 """Attach a list of files specified by Rucio DIDs to a Rucio dataset. 

183 

184 Ignores already-attached files for idempotency. 

185 

186 Parameters 

187 ---------- 

188 dataset_id: `str` 

189 Logical name of the Rucio dataset. 

190 dids: `list [ dict [ str, str|int ] ]` 

191 List of Rucio data identifiers. 

192 """ 

193 retries = 0 

194 max_retries = 5 

195 while True: 

196 try: 

197 self.did_client.add_files_to_dataset( 

198 scope=self.scope, 

199 name=dataset_id, 

200 files=dids, 

201 rse=self.rse, 

202 ) 

203 return 

204 except rucio.common.exception.FileAlreadyExists: 

205 # At least one already is in the dataset. 

206 # This shouldn't happen, but if it does, 

207 # we have to retry each individually. 

208 for did in dids: 

209 self._add_file_to_dataset_with_retries( 

210 dataset_id=dataset_id, 

211 did=did, 

212 ) 

213 return 

214 except rucio.common.exception.DataIdentifierNotFound as e: 

215 raise e 

216 except rucio.common.exception.RucioException: 

217 retries += 1 

218 if retries < max_retries: 

219 seconds = random.randint(10, 20) 

220 logger.debug(f"failed to register dids to {dataset_id}; sleeping {seconds}") 

221 time.sleep(seconds) 

222 continue 

223 else: 

224 raise Exception(f"Couldn't add files to dataset {dataset_id}") 

225 

226 def _add_dataset_with_retries(self, dataset_id: str, statuses: dict) -> None: 

227 retries = 0 

228 max_retries = 5 

229 while True: 

230 try: 

231 self.did_client.add_dataset( 

232 scope=self.scope, 

233 name=dataset_id, 

234 statuses=statuses, 

235 rse=self.rse, 

236 ) 

237 return 

238 except rucio.common.exception.DataIdentifierAlreadyExists as e: 

239 # If someone else created it in the meantime 

240 raise e 

241 except rucio.common.exception.RucioException: 

242 retries += 1 

243 if retries < max_retries: 

244 seconds = random.randint(10, 20) 

245 logger.debug(f"couldn't register dids to {dataset_id}; waiting {seconds}") 

246 time.sleep(seconds) 

247 continue 

248 else: 

249 raise Exception(f"Tried {max_retries} times and couldn't add dataset {dataset_id}") 

250 

251 def register_to_dataset(self, bundles) -> None: 

252 """Register a list of files in Rucio. 

253 

254 Parameters 

255 ---------- 

256 Bundles: `list [ ResourceBundle ]` 

257 List of resource bundles 

258 """ 

259 logger.debug("register to dataset") 

260 

261 datasets = dict() 

262 for bundle in bundles: 

263 dataset_id = bundle.dataset_id 

264 datasets.setdefault(dataset_id, []).append(bundle) 

265 

266 for dataset_id, bundles in datasets.items(): 

267 try: 

268 dids = [rb.get_did() for rb in bundles] 

269 logger.info("Registering %s in dataset %s, RSE %s", dids, dataset_id, self.rse) 

270 self._add_files_to_dataset(dataset_id, dids) 

271 except rucio.common.exception.DataIdentifierNotFound: 

272 # No such dataset, so create it 

273 try: 

274 logger.info("Creating Rucio dataset %s", dataset_id) 

275 self._add_dataset_with_retries( 

276 dataset_id=dataset_id, 

277 statuses={"monotonic": True}, 

278 ) 

279 except rucio.common.exception.DataIdentifierAlreadyExists: 

280 # If someone else created it in the meantime 

281 pass 

282 # And then retry adding DIDs 

283 self._add_files_to_dataset(dataset_id, dids) 

284 

285 logger.debug("Done with Rucio for %s", bundles) 

286 

287 def register_as_replicas(self, dataset_id, dataset_refs) -> None: 

288 """Register a list of DatasetRefs to a Rucio dataset 

289 

290 Parameters 

291 ---------- 

292 dataset_id: `str` 

293 RUCIO dataset id 

294 dataset_refs: `list[DatasetRef]` 

295 list of Butler DatasetRefs 

296 """ 

297 bundles = [] 

298 for dataset_ref in dataset_refs: 

299 bundles.append(self._make_bundle(dataset_id, dataset_ref)) 

300 if len(bundles) == 0: 

301 return 0 

302 self._add_replicas(bundles) 

303 self.register_to_dataset(bundles) 

304 return len(bundles)