Coverage for python/lsst/rucio/register/rucio_interface.py: 15%
176 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-09-05 01:49 -0700
« prev ^ index » next coverage.py v7.10.3, created at 2025-09-05 01:49 -0700
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/>.
22import hashlib
23import logging
24import random
25import time
26import zlib
28import rucio.common.exception
29from rucio.client.didclient import DIDClient
30from rucio.client.replicaclient import ReplicaClient
32import lsst.daf.butler
33from lsst.daf.butler import DatasetRef
34from lsst.resources import ResourcePath
35from lsst.rucio.register.resource_bundle import ResourceBundle
36from lsst.rucio.register.rubin_meta import RubinMeta
37from lsst.rucio.register.rucio_did import RucioDID
39__all__ = ["RucioInterface"]
41logger = logging.getLogger(__name__)
44class RucioInterface:
45 """Add files as replicas in Rucio, along with metadata,
46 and attach them to datasets.
48 Parameters
49 ----------
50 butler : `lsst.daf.butler.Butler`
51 Butler we're operating upon
52 rucio_rse : `str`
53 Name of the RSE that the files live in.
54 scope : `str`
55 Rucio scope to register the files in.
56 rse_root : `str`
57 Full path to root directory of RSE directory structure
58 dtn_url : `str`
59 Base URL of the data transfer node for the Rucio physical filename.
60 rubin_butler_type: `str`
61 the type registered in "rubin_butler" metadata for rucio
62 """
64 def __init__(
65 self,
66 butler: lsst.daf.butler.Butler,
67 rucio_rse: str,
68 scope: str,
69 rse_root: str,
70 dtn_url: str,
71 rubin_butler_type: str,
72 ):
73 self.butler = butler
74 self.rse = rucio_rse
75 self.scope = scope
76 self.rse_root = rse_root
77 self.dtn_url = dtn_url
78 self.pfn_base = f"{dtn_url}"
79 self.replica_client = ReplicaClient()
80 self.did_client = DIDClient()
81 self.rubin_butler_type = rubin_butler_type
83 def _make_dataset_ref_bundle(self, dataset_id: str, dataset_ref: DatasetRef) -> ResourceBundle:
84 """Make a ResourceBundle
86 Parameters
87 ----------
88 dataset_id : `str`
89 Rucio dataset name
90 dataset_ref : `DatasetRef`
91 Butler DatasetRef
93 Returns
94 -------
95 rb : `ResourceBundle`
96 ResourceBundle consolidating dataset id and DatasetRef
97 """
98 logging.debug("%s", dataset_ref.to_json())
99 did = self._make_did(self.butler.getURI(dataset_ref), dataset_ref.to_json())
100 rb = ResourceBundle(dataset_id=dataset_id, did=did)
101 return rb
103 def _make_zip_bundle(self, dataset_id: str, resource_path: ResourcePath) -> ResourceBundle:
104 """Make a ResourceBundle
106 Parameters
107 ----------
108 dataset_id : `str`
109 Rucio dataset name
110 resouce_path : `ResourcePath`
111 ResourcePath to a file
113 Returns
114 -------
115 rb: ResourceBundle
116 ResourceBundle consolidating dataset id and ResourcePath
117 """
118 did = self._make_did(resource_path)
119 rb = ResourceBundle(dataset_id=dataset_id, did=did)
120 return rb
122 def _make_dim_bundle(self, dataset_id: str, resource_path: ResourcePath) -> ResourceBundle:
123 """Make a ResourceBundle
125 Parameters
126 ----------
127 dataset_id : `str`
128 Rucio dataset name
129 resouce_path : `lsst.resource.ResourcePath`
130 ResourcePath to a file
132 Returns
133 -------
134 rb: `lsst.rucio.register.rucio_bundle.ResourceBundle`
135 ResourceBundle consolidating dataset id and ResourcePath
136 """
137 did = self._make_did(resource_path)
138 rb = ResourceBundle(dataset_id=dataset_id, did=did)
139 return rb
141 def _make_did(self, resource_path: ResourcePath, metadata: str = None) -> RucioDID:
142 """Make a Rucio data identifier dictionary from a resource.
144 Parameters
145 ----------
146 resource_path: ResourcePath
147 ResourcePath object
149 metadata: `str`
150 String containing Rubin dataset specific metadata
152 Returns
153 -------
154 did : `dict` [`str`, `str`|`int`]
155 Rucio data identifier including physical and logical names,
156 byte length, adler32 and MD5 checksums, meta, and scope.
157 """
158 with resource_path.open("rb") as f:
159 contents = f.read()
160 size = len(contents)
161 md5 = hashlib.md5(contents).hexdigest()
162 adler32 = f"{zlib.adler32(contents):08x}"
163 path = resource_path.unquoted_path.removeprefix(self.rse_root)
164 pfn = self.pfn_base + path
165 logging.debug("pfn=%s", pfn)
166 name = path.removeprefix("/" + self.scope + "/")
167 logging.debug("name=%s", name)
168 logging.debug("path=%s", path)
170 if metadata:
171 meta = RubinMeta(rubin_butler=self.rubin_butler_type, rubin_sidecar=metadata)
172 else:
173 meta = RubinMeta(rubin_butler=self.rubin_butler_type, rubin_sidecar="")
174 d = RucioDID(
175 pfn=pfn,
176 bytes=size,
177 adler32=adler32,
178 md5=md5,
179 name=name,
180 scope=self.scope,
181 meta=meta,
182 )
184 return d
186 def _add_replicas(self, bundles: list[ResourceBundle]) -> None:
187 """Call the Rucio method add_replica for a list of DIDs
189 Parameters
190 ----------
191 bundles : `list` [`ResourceBundle`]
192 A list of ResourceBundles
193 """
194 dids = [bundle.get_did() for bundle in bundles]
195 retries = 0
196 max_retries = 5
197 while True:
198 try:
199 self.replica_client.add_replicas(rse=self.rse, files=dids)
200 break
201 except rucio.common.exception.RucioException:
202 retries += 1
203 if retries < max_retries:
204 seconds = random.randint(10, 20)
205 logger.debug("failed to add_replicas; sleeping %d seconds", seconds)
206 time.sleep(seconds)
207 self.replica_client = ReplicaClient() # XXX not sure we need to do this.
208 else:
209 raise Exception(f"Tried {max_retries} times and couldn't add_replicas")
211 def _add_file_to_dataset_with_retries(self, dataset_id, did):
212 retries = 0
213 max_retries = 5
214 while True:
215 try:
216 self.did_client.add_files_to_dataset(
217 scope=self.scope, name=dataset_id, files=[did], rse=self.rse
218 )
219 break
220 except rucio.common.exception.FileAlreadyExists:
221 if "pfn" in did:
222 logger.debug("file %s already registered in dataset %s", did["pfn"], dataset_id)
223 return # we can return, because it's already in the dataset
224 except rucio.common.exception.RucioException:
225 retries += 1
226 if retries < max_retries:
227 seconds = random.randint(10, 20)
228 logger.debug("failed to register one did to %s; sleeping %d seconds", dataset_id, seconds)
229 time.sleep(seconds)
230 self.did_client = DIDClient() # XXX not sure we need to do this.
231 else:
232 # we tried max_retries times, and failed, so we'll bail out
233 raise Exception(f"Couldn't add {did['pfn']} to dataset {dataset_id}")
235 def _add_files_to_dataset(self, dataset_id: str, dids: list[dict]) -> None:
236 """Attach a list of files specified by Rucio DIDs to a Rucio dataset.
238 Ignores already-attached files for idempotency.
240 Parameters
241 ----------
242 dataset_id : `str`
243 Logical name of the Rucio dataset.
244 dids : `list` [`dict` [`str`, `str`|`int`] ]
245 List of Rucio data identifiers.
246 """
247 retries = 0
248 max_retries = 5
249 while True:
250 try:
251 self.did_client.add_files_to_dataset(
252 scope=self.scope,
253 name=dataset_id,
254 files=dids,
255 rse=self.rse,
256 )
257 return
258 except rucio.common.exception.FileAlreadyExists:
259 # At least one already is in the dataset.
260 # This shouldn't happen, but if it does,
261 # we have to retry each individually.
262 for did in dids:
263 self._add_file_to_dataset_with_retries(
264 dataset_id=dataset_id,
265 did=did,
266 )
267 return
268 except rucio.common.exception.DataIdentifierNotFound as e:
269 raise e
270 except rucio.common.exception.RucioException:
271 retries += 1
272 if retries < max_retries:
273 seconds = random.randint(10, 20)
274 logger.debug("failed to register dids to %s; sleeping %d", dataset_id, seconds)
275 time.sleep(seconds)
276 continue
277 else:
278 raise Exception(f"Couldn't add files to dataset {dataset_id}")
280 def _add_dataset_with_retries(self, dataset_id: str, statuses: dict) -> None:
281 retries = 0
282 max_retries = 5
283 while True:
284 try:
285 self.did_client.add_dataset(
286 scope=self.scope,
287 name=dataset_id,
288 statuses=statuses,
289 rse=self.rse,
290 )
291 return
292 except rucio.common.exception.DataIdentifierAlreadyExists as e:
293 # If someone else created it in the meantime
294 raise e
295 except rucio.common.exception.RucioException:
296 retries += 1
297 if retries < max_retries:
298 seconds = random.randint(10, 20)
299 logger.debug("couldn't register dids to %s; waiting %d", dataset_id, seconds)
300 time.sleep(seconds)
301 continue
302 else:
303 raise Exception(f"Tried {max_retries} times and couldn't add dataset {dataset_id}")
305 def register_to_dataset(self, bundles) -> None:
306 """Register a list of files in Rucio.
308 Parameters
309 ----------
310 bundles : `list` [`ResourceBundle`]
311 List of resource bundles
312 """
313 logger.debug("register to dataset")
315 datasets = dict()
316 for bundle in bundles:
317 dataset_id = bundle.dataset_id
318 datasets.setdefault(dataset_id, []).append(bundle)
320 for dataset_id, bundles in datasets.items():
321 try:
322 dids = [rb.get_did() for rb in bundles]
323 names = [did["pfn"] for did in dids]
324 logger.info("Registering %s in dataset %s, RSE %s", names, dataset_id, self.rse)
325 self._add_files_to_dataset(dataset_id, dids)
326 except rucio.common.exception.DataIdentifierNotFound:
327 # No such dataset, so create it
328 try:
329 logger.info("Creating Rucio dataset %s", dataset_id)
330 self._add_dataset_with_retries(
331 dataset_id=dataset_id,
332 statuses={"monotonic": True},
333 )
334 except rucio.common.exception.DataIdentifierAlreadyExists:
335 # If someone else created it in the meantime
336 pass
337 # And then retry adding DIDs
338 self._add_files_to_dataset(dataset_id, dids)
340 logger.debug("Done with Rucio for %s", bundles)
342 def register_as_replicas(self, dataset_id, dataset_refs) -> None:
343 """Register a list of DatasetRefs to a Rucio dataset
345 Parameters
346 ----------
347 dataset_id : `str`
348 RUCIO dataset id
349 dataset_refs : `list` [`DatasetRef`]
350 list of Butler DatasetRefs
351 """
352 bundles = []
353 for dataset_ref in dataset_refs:
354 if type(dataset_ref) is list:
355 for dsr in dataset_ref:
356 bundles.append(self._make_dataset_ref_bundle(dataset_id, dsr))
357 else:
358 bundles.append(self._make_dataset_ref_bundle(dataset_id, dataset_ref))
359 if len(bundles) == 0:
360 return 0
361 self._add_replicas(bundles)
362 self.register_to_dataset(bundles)
363 return len(bundles)
365 def register_zips(self, dataset_id: str, zip_files: list) -> int:
366 """Register a list of zips to a Rucio Dataset
368 Parameters
369 ----------
370 dataset_id : `str`
371 RUCIO dataset id
372 zip_files : `list` [`ResourcePath`]
373 list of ResourcePath
375 Returns
376 -------
377 num : `int`
378 number of zip files ingested
379 """
380 bundles = []
381 for zip_file in zip_files:
382 bundles.append(self._make_zip_bundle(dataset_id, zip_file))
383 self._add_replicas(bundles)
384 self.register_to_dataset(bundles)
385 return len(bundles)
387 def register_dims(self, dataset_id: str, dim_files: list) -> int:
388 """Register a list of dimension files to a Rucio Dataset
390 Parameters
391 ----------
392 dataset_id : `str`
393 RUCIO dataset id
394 dim_files : `list` [`lsst.resource.ResourcePath`]
395 list of ResourcePath
397 Returns
398 -------
399 num : `int`
400 number of dimension files ingested
401 """
402 bundles = []
403 for dim_file in dim_files:
404 bundles.append(self._make_dim_bundle(dataset_id, dim_file))
405 self._add_replicas(bundles)
406 self.register_to_dataset(bundles)
407 return len(bundles)