Coverage for python/lsst/rucio/register/rucio_interface.py: 65%
166 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 01:56 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 01:56 -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 logging
23import random
24import time
25import zlib
27import rucio.common.exception
28from rucio.client.didclient import DIDClient
29from rucio.client.replicaclient import ReplicaClient
31import lsst.daf.butler
32from lsst.daf.butler import DatasetRef
33from lsst.resources import ResourcePath
34from lsst.rucio.register.resource_bundle import ResourceBundle
35from lsst.rucio.register.rubin_meta import RubinMeta
36from lsst.rucio.register.rucio_did import RucioDID
38__all__ = ["RucioInterface"]
40logger = logging.getLogger(__name__)
43class RucioInterface:
44 """Add files as replicas in Rucio, along with metadata,
45 and attach them to datasets.
47 Parameters
48 ----------
49 butler : `lsst.daf.butler.Butler`
50 Butler we're operating upon
51 rucio_rse : `str`
52 Name of the RSE that the files live in.
53 scope : `str`
54 Rucio scope to register the files in.
55 rse_root : `str`
56 Full path to root directory of RSE directory structure
57 dtn_url : `str`
58 Base URL of the data transfer node for the Rucio physical filename.
59 rubin_butler_type: `str`
60 the type registered in "rubin_butler" metadata for rucio
61 """
63 def __init__(
64 self,
65 butler: lsst.daf.butler.Butler,
66 rucio_rse: str,
67 scope: str,
68 rse_root: str,
69 dtn_url: str,
70 rubin_butler_type: str,
71 ):
72 self.butler = butler
73 self.rse = rucio_rse
74 self.scope = scope
75 self.rse_root = rse_root
76 self.dtn_url = dtn_url
77 self.pfn_base = f"{dtn_url}"
78 self.replica_client = ReplicaClient()
79 self.did_client = DIDClient()
80 self.rubin_butler_type = rubin_butler_type
82 def _make_dataset_ref_bundle(self, dataset_id: str, dataset_ref: DatasetRef) -> ResourceBundle:
83 """Make a ResourceBundle
85 Parameters
86 ----------
87 dataset_id : `str`
88 Rucio dataset name
89 dataset_ref : `DatasetRef`
90 Butler DatasetRef
92 Returns
93 -------
94 rb : `ResourceBundle`
95 ResourceBundle consolidating dataset id and DatasetRef
96 """
97 logging.debug("%s", dataset_ref.to_json())
98 did = self._make_did(self.butler.getURI(dataset_ref), dataset_ref.to_json())
99 rb = ResourceBundle(dataset_id=dataset_id, did=did)
100 return rb
102 def _make_zip_bundle(self, dataset_id: str, resource_path: ResourcePath) -> ResourceBundle:
103 """Make a ResourceBundle
105 Parameters
106 ----------
107 dataset_id : `str`
108 Rucio dataset name
109 resouce_path : `ResourcePath`
110 ResourcePath to a file
112 Returns
113 -------
114 rb: ResourceBundle
115 ResourceBundle consolidating dataset id and ResourcePath
116 """
117 did = self._make_did(resource_path)
118 rb = ResourceBundle(dataset_id=dataset_id, did=did)
119 return rb
121 def _make_dim_bundle(self, dataset_id: str, resource_path: ResourcePath) -> ResourceBundle:
122 """Make a ResourceBundle
124 Parameters
125 ----------
126 dataset_id : `str`
127 Rucio dataset name
128 resouce_path : `lsst.resource.ResourcePath`
129 ResourcePath to a file
131 Returns
132 -------
133 rb: `lsst.rucio.register.rucio_bundle.ResourceBundle`
134 ResourceBundle consolidating dataset id and ResourcePath
135 """
136 did = self._make_did(resource_path)
137 rb = ResourceBundle(dataset_id=dataset_id, did=did)
138 return rb
140 def compute_hashes(self, resource_path: ResourcePath) -> tuple[int, str]:
141 """return the length and adler32 hash for a file.
143 Parameters
144 ----------
145 path: `lsst.resources.ResourcePath`
146 Path to the file.
148 Returns
149 -------
150 hashes: `tuple` [ `int`, `str` ]
151 Size in bytes and Adler32 hex hash.
152 """
154 info = resource_path.get_info()
155 size = info.size
156 checksums = info.checksums
157 if "adler32" in checksums:
158 adler32 = checksums["adler32"]
159 logger.debug("found adler32 for %s", resource_path)
160 return size, adler32
161 return size, self._compute_adler32(resource_path)
163 def _compute_adler32(self, resource_path: ResourcePath) -> tuple[int, str]:
164 logger.debug("computing adler32 for %s", resource_path)
165 adler32 = zlib.adler32(b"")
166 buffer_size = 10 * 1024 * 1024
167 with resource_path.open("rb") as f:
168 while buffer := f.read(buffer_size):
169 adler32 = zlib.adler32(buffer, adler32)
170 adler32_digest = f"{adler32:08x}"
171 return adler32_digest
173 def _make_did(self, resource_path: ResourcePath, metadata: str = None) -> RucioDID:
174 """Make a Rucio data identifier dictionary from a resource.
176 Parameters
177 ----------
178 resource_path: ResourcePath
179 ResourcePath object
181 metadata: `str`
182 String containing Rubin dataset specific metadata
184 Returns
185 -------
186 did : `dict` [`str`, `str`|`int`]
187 Rucio data identifier including physical and logical names,
188 byte length, adler32 checksum, meta, and scope.
189 """
191 size, adler32 = self.compute_hashes(resource_path)
192 path = resource_path.unquoted_path.removeprefix(self.rse_root)
193 pfn = self.pfn_base + path
194 logging.debug("pfn=%s", pfn)
195 name = path.removeprefix("/" + self.scope + "/")
196 logging.debug("name=%s", name)
197 logging.debug("path=%s", path)
199 if metadata: 199 ↛ 202line 199 didn't jump to line 202 because the condition on line 199 was always true
200 meta = RubinMeta(rubin_butler=self.rubin_butler_type, rubin_sidecar=metadata)
201 else:
202 meta = RubinMeta(rubin_butler=self.rubin_butler_type, rubin_sidecar="")
203 d = RucioDID(
204 pfn=pfn,
205 bytes=size,
206 adler32=adler32,
207 name=name,
208 scope=self.scope,
209 meta=meta,
210 )
212 return d
214 def _add_replicas(self, bundles: list[ResourceBundle]) -> None:
215 """Call the Rucio method add_replica for a list of DIDs
217 Parameters
218 ----------
219 bundles : `list` [`ResourceBundle`]
220 A list of ResourceBundles
221 """
222 dids = [bundle.get_did() for bundle in bundles]
223 retries = 0
224 max_retries = 5
225 while True:
226 try:
227 self.replica_client.add_replicas(rse=self.rse, files=dids)
228 break
229 except rucio.common.exception.RucioException:
230 retries += 1
231 if retries < max_retries:
232 seconds = random.randint(10, 20)
233 logger.debug("failed to add_replicas; sleeping %d seconds", seconds)
234 time.sleep(seconds)
235 self.replica_client = ReplicaClient() # XXX not sure we need to do this.
236 else:
237 raise Exception(f"Tried {max_retries} times and couldn't add_replicas")
239 def _add_files_to_dataset(self, dataset_id: str, dids: list[dict]) -> None:
240 """Attach a list of files specified by Rucio DIDs to a Rucio dataset.
242 Ignores already-attached files for idempotency.
244 Parameters
245 ----------
246 dataset_id : `str`
247 Logical name of the Rucio dataset.
248 dids : `list` [`dict` [`str`, `str`|`int`] ]
249 List of Rucio data identifiers.
250 """
251 retries = 0
252 max_retries = 5
253 while True:
254 try:
255 self.did_client.add_files_to_datasets(
256 attachments=[
257 {
258 "scope": self.scope,
259 "name": dataset_id,
260 "dids": dids,
261 "rse": self.rse,
262 }
263 ],
264 ignore_duplicate=True,
265 )
266 return
267 except rucio.common.exception.DataIdentifierNotFound as e:
268 raise e
269 except rucio.common.exception.RucioException:
270 retries += 1
271 if retries < max_retries:
272 seconds = random.randint(10, 20)
273 logger.debug("failed to register dids to %s; sleeping %d", dataset_id, seconds)
274 time.sleep(seconds)
275 continue
276 else:
277 raise Exception(f"Couldn't add files to dataset {dataset_id}")
279 def _add_dataset_with_retries(self, dataset_id: str, statuses: dict) -> None:
280 retries = 0
281 max_retries = 5
282 while True:
283 try:
284 self.did_client.add_dataset(
285 scope=self.scope,
286 name=dataset_id,
287 statuses=statuses,
288 rse=self.rse,
289 )
290 return
291 except rucio.common.exception.DataIdentifierAlreadyExists as e:
292 # If someone else created it in the meantime
293 raise e
294 except rucio.common.exception.RucioException:
295 retries += 1
296 if retries < max_retries:
297 seconds = random.randint(10, 20)
298 logger.debug("couldn't register dids to %s; waiting %d", dataset_id, seconds)
299 time.sleep(seconds)
300 continue
301 else:
302 raise Exception(f"Tried {max_retries} times and couldn't add dataset {dataset_id}")
304 def register_to_dataset(self, bundles) -> None:
305 """Register a list of files in Rucio.
307 Parameters
308 ----------
309 bundles : `list` [`ResourceBundle`]
310 List of resource bundles
311 """
312 logger.debug("register to dataset")
314 datasets = dict()
315 for bundle in bundles:
316 dataset_id = bundle.dataset_id
317 datasets.setdefault(dataset_id, []).append(bundle)
319 for dataset_id, bundles in datasets.items():
320 try:
321 dids = [rb.get_did() for rb in bundles]
322 names = [did["pfn"] for did in dids]
323 logger.info("Registering %s in dataset %s, RSE %s", names, dataset_id, self.rse)
324 self._add_files_to_dataset(dataset_id, dids)
325 except rucio.common.exception.DataIdentifierNotFound:
326 # No such dataset, so create it
327 try:
328 logger.info("Creating Rucio dataset %s", dataset_id)
329 self._add_dataset_with_retries(
330 dataset_id=dataset_id,
331 statuses={"monotonic": True},
332 )
333 except rucio.common.exception.DataIdentifierAlreadyExists:
334 # If someone else created it in the meantime
335 pass
336 # And then retry adding DIDs
337 self._add_files_to_dataset(dataset_id, dids)
339 logger.debug("Done with Rucio for %s", bundles)
341 def register_as_replicas(self, dataset_id, dataset_refs) -> None:
342 """Register a list of DatasetRefs to a Rucio dataset
344 Parameters
345 ----------
346 dataset_id : `str`
347 RUCIO dataset id
348 dataset_refs : `list` [`DatasetRef`]
349 list of Butler DatasetRefs
350 """
351 bundles = []
352 for dataset_ref in dataset_refs:
353 if type(dataset_ref) is list: 353 ↛ 354line 353 didn't jump to line 354 because the condition on line 353 was never true
354 for dsr in dataset_ref:
355 bundles.append(self._make_dataset_ref_bundle(dataset_id, dsr))
356 else:
357 bundles.append(self._make_dataset_ref_bundle(dataset_id, dataset_ref))
358 if len(bundles) == 0: 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true
359 return 0
360 self._add_replicas(bundles)
361 self.register_to_dataset(bundles)
362 return len(bundles)
364 def register_zips(self, dataset_id: str, zip_files: list) -> int:
365 """Register a list of zips to a Rucio Dataset
367 Parameters
368 ----------
369 dataset_id : `str`
370 RUCIO dataset id
371 zip_files : `list` [`ResourcePath`]
372 list of ResourcePath
374 Returns
375 -------
376 num : `int`
377 number of zip files ingested
378 """
379 bundles = []
380 for zip_file in zip_files:
381 bundles.append(self._make_zip_bundle(dataset_id, zip_file))
382 self._add_replicas(bundles)
383 self.register_to_dataset(bundles)
384 return len(bundles)
386 def register_dims(self, dataset_id: str, dim_files: list) -> int:
387 """Register a list of dimension files to a Rucio Dataset
389 Parameters
390 ----------
391 dataset_id : `str`
392 RUCIO dataset id
393 dim_files : `list` [`lsst.resource.ResourcePath`]
394 list of ResourcePath
396 Returns
397 -------
398 num : `int`
399 number of dimension files ingested
400 """
401 bundles = []
402 for dim_file in dim_files:
403 bundles.append(self._make_dim_bundle(dataset_id, dim_file))
404 self._add_replicas(bundles)
405 self.register_to_dataset(bundles)
406 return len(bundles)