Skip to content

Commit 707cc58

Browse files
committed
[_746] choice of sort functions now available for data replicas
The iRODSDataObject has thus far sorted its replicas list by ascending replica order.
1 parent f3a4fa7 commit 707cc58

4 files changed

Lines changed: 166 additions & 56 deletions

File tree

irods/data_object.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
"""
2+
Interface for iRODS data objects.
3+
4+
Provides high level abstraction and POSIX-like facilities (create, open,
5+
read/write) allowing clients to manipulate data objects very much as if they
6+
were local files.
7+
"""
8+
9+
import ast
10+
import enum
111
import io
2-
import sys
312
import logging
413
import os
5-
import ast
14+
import sys
15+
from datetime import datetime, timezone
616

7-
from irods.models import DataObject
8-
from irods.meta import iRODSMetaCollection
917
import irods.keywords as kw
1018
from irods.api_number import api_number
1119
from irods.message import JSON_Message, iRODSMessage
20+
from irods.meta import iRODSMetaCollection
21+
from irods.models import DataObject
1222

1323
logger = logging.getLogger(__name__)
1424

@@ -41,11 +51,57 @@ def __repr__(self):
4151
return "<{}.{} {}>".format(self.__class__.__module__, self.__class__.__name__, self.resource_name)
4252

4353

54+
class _repl_status(enum.Enum): # noqa: N801
55+
STALE_REPLICA, GOOD_REPLICA, INTERMEDIATE_REPLICA, READ_LOCKED, WRITE_LOCKED = range(5)
56+
57+
58+
# An ordering of the various replica status values, by descending fitness for use/interface
59+
_REPL_STATUSES = tuple(
60+
getattr(_repl_status, ident).value
61+
for ident in (
62+
"GOOD_REPLICA",
63+
"STALE_REPLICA",
64+
"INTERMEDIATE_REPLICA",
65+
"READ_LOCKED",
66+
"WRITE_LOCKED",
67+
)
68+
)
69+
70+
# An appropriate reference datetime value for gauging replica age as part of
71+
# the default sort key in PRC4 and onward.
72+
_REFERENCE_DATETIME = datetime.fromtimestamp(0, timezone.utc)
73+
74+
# ruff: noqa: D103 off
75+
76+
# Key functions to dictate how replica row results will be sorted within an iRODSDataObject.
77+
78+
79+
def REPLICA_NUMBER_SORT_KEY_FN(row): # noqa: N802
80+
return row[DataObject.replica_number]
81+
82+
83+
def REPLICA_FITNESS_SORT_KEY_FN(row): # noqa: N802
84+
repl_status = int(row[DataObject.replica_status])
85+
86+
repl_status_rank = _REPL_STATUSES.index(repl_status) if _REPL_STATUSES.count(repl_status) else sys.maxsize
87+
88+
return (repl_status_rank, _REFERENCE_DATETIME - row[DataObject.modify_time])
89+
90+
91+
# ruff: noqa: D103 on
92+
93+
_DEFAULT_SORT_KEY_FN = REPLICA_NUMBER_SORT_KEY_FN
94+
95+
4496
class iRODSDataObject:
45-
def __init__(self, manager, parent=None, results=None):
97+
# iRODSDataObject's constructor is not usually directly accessed by iRODS client applications. See the main README.
98+
# ruff: noqa: D107 off
99+
100+
def __init__(self, manager, parent=None, results=None, replica_sort_function=None):
46101
self.manager = manager
47102
if parent and results:
48103
self.collection = parent
104+
results = sorted(results, key=(replica_sort_function or _DEFAULT_SORT_KEY_FN))
49105
for attr, value in DataObject.__dict__.items():
50106
if not attr.startswith("_"):
51107
try:
@@ -54,9 +110,8 @@ def __init__(self, manager, parent=None, results=None):
54110
# backward compatibility with older schema versions
55111
pass
56112
self.path = self.collection.path + "/" + self.name
57-
replicas = sorted(results, key=lambda r: r[DataObject.replica_number])
58113

59-
# The status quo before iRODS 5
114+
# Copy pre-iRODS 5 fields
60115

61116
replica_args = [
62117
(
@@ -75,18 +130,20 @@ def __init__(self, manager, parent=None, results=None):
75130
modify_time=r[DataObject.modify_time],
76131
),
77132
)
78-
for r in replicas
133+
for r in results
79134
]
80135

81136
# Adjust for adding access_time in the iRODS 5 case.
82137

83138
if self.manager.sess.server_version >= (5,):
84-
for n, r in enumerate(replicas):
139+
for n, r in enumerate(results):
85140
replica_args[n][1]['access_time'] = r[DataObject.access_time]
86141
self.replicas = [iRODSReplica(*a, **k) for a, k in replica_args]
87142

88143
self._meta = None
89144

145+
# ruff: noqa: D107 off
146+
90147
def __repr__(self):
91148
return f"<iRODSDataObject {self.id} {self.name}>"
92149

irods/manager/data_object_manager.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -218,27 +218,29 @@ def should_parallelize_transfer(
218218
if size is not None and isinstance(open_options, dict):
219219
open_options[kw.DATA_SIZE_KW] = size
220220

221-
def _download(self, obj, local_path, num_threads, updatables=(), **options):
221+
def _download(self, obj_path, local_path, num_threads, updatables=(), **options):
222222
"""Transfer the contents of a data object to a local file.
223223
224224
Called from get() when a local path is named.
225225
"""
226-
if os.path.isdir(local_path):
227-
local_file = os.path.join(local_path, irods_basename(obj))
228-
else:
229-
local_file = local_path
226+
227+
local_file = (
228+
os.path.join(local_path, irods_basename(obj_path)) # noqa: PTH118
229+
if os.path.isdir(local_path) # noqa: PTH112
230+
else local_path
231+
)
230232

231233
# Check for force flag if local_file exists
232234
if os.path.exists(local_file) and kw.FORCE_FLAG_KW not in options:
233235
raise ex.OVERWRITE_WITHOUT_FORCE_FLAG
234236

235237
data_open_returned_values_ = {}
236-
with self.open(obj, "r", returned_values=data_open_returned_values_, **options) as o:
238+
with self.open(obj_path, "r", returned_values=data_open_returned_values_, **options) as o:
237239
if self.should_parallelize_transfer(num_threads, o, open_options=options.items()):
238240
error = RuntimeError("parallel get failed")
239241
try:
240242
if not self.parallel_get(
241-
(obj, o),
243+
(obj_path, o),
242244
local_file,
243245
num_threads=num_threads,
244246
target_resource_name=options.get(kw.RESC_NAME_KW, ""),
@@ -265,6 +267,8 @@ def get(self, path, local_path=None, num_threads=DEFAULT_NUMBER_OF_THREADS, upda
265267
"""
266268
parent = self.sess.collections.get(irods_dirname(path))
267269

270+
replica_sort_function = options.pop('replica_sort_function', None)
271+
268272
# TODO: optimize
269273
if local_path:
270274
self._download(path, local_path, num_threads=num_threads, updatables=updatables, **options)
@@ -284,7 +288,7 @@ def get(self, path, local_path=None, num_threads=DEFAULT_NUMBER_OF_THREADS, upda
284288
results = query.all() # get up to max_rows replicas
285289
if len(results) <= 0:
286290
raise ex.DataObjectDoesNotExist()
287-
return iRODSDataObject(self, parent, results)
291+
return iRODSDataObject(self, parent, results, replica_sort_function=replica_sort_function)
288292

289293
@staticmethod
290294
def _resolve_force_put_option(options, default_setting=None, true_value=""):
@@ -317,23 +321,25 @@ def put(
317321
self._resolve_force_put_option(options, default_setting=client_config.data_objects.force_put_by_default)
318322

319323
if self.sess.collections.exists(irods_path):
320-
obj = iRODSCollection.normalize_path(irods_path, os.path.basename(local_path))
324+
obj_path = iRODSCollection.normalize_path(irods_path, os.path.basename(local_path)) # noqa: PTH119
321325
else:
322-
obj = irods_path
323-
if kw.FORCE_FLAG_KW not in options and self.exists(obj):
326+
obj_path = irods_path
327+
if kw.FORCE_FLAG_KW not in options and self.exists(obj_path):
324328
raise ex.OVERWRITE_WITHOUT_FORCE_FLAG
325329
options.pop(kw.FORCE_FLAG_KW, None)
326330

331+
replica_sort_function = options.pop('replica_sort_function', None)
332+
327333
with open(local_path, "rb") as f:
328334
sizelist = []
329335
if self.should_parallelize_transfer(num_threads, f, measured_obj_size=sizelist, open_options=options):
330-
o = deferred_call(self.open, (obj, "w"), options)
336+
o = deferred_call(self.open, (obj_path, "w"), options)
331337
f.close()
332338
error = RuntimeError("parallel put failed")
333339
try:
334340
if not self.parallel_put(
335341
local_path,
336-
(obj, o),
342+
(obj_path, o),
337343
total_bytes=sizelist[0],
338344
num_threads=num_threads,
339345
target_resource_name=options.get(kw.RESC_NAME_KW, "") or options.get(kw.DEST_RESC_NAME_KW, ""),
@@ -346,7 +352,7 @@ def put(
346352
except BaseException as e:
347353
raise error from e
348354
else:
349-
with self.open(obj, "w", **options) as o:
355+
with self.open(obj_path, "w", **options) as o:
350356
# Set operation type to trigger acPostProcForPut
351357
if kw.OPR_TYPE_KW not in options:
352358
options[kw.OPR_TYPE_KW] = 1 # PUT_OPR
@@ -360,10 +366,11 @@ def put(
360366
# Requested to register checksum without verifying, but source replica has a checksum. This can result
361367
# in multiple replicas being marked good with different checksums, which is an inconsistency.
362368
del repl_options[kw.REG_CHKSUM_KW]
363-
self.replicate(obj, **repl_options)
369+
self.replicate(obj_path, **repl_options)
364370

365371
if return_data_object:
366-
return self.get(obj)
372+
return self.get(obj_path, replica_sort_function=replica_sort_function)
373+
return None
367374

368375
def chksum(self, path, **options):
369376
"""
@@ -480,6 +487,7 @@ def create(
480487
raise ex.DataObjectExistsAtLogicalPath
481488

482489
options = {**options, kw.DATA_TYPE_KW: "generic"}
490+
replica_sort_function = options.pop('replica_sort_function', None)
483491

484492
if resource:
485493
options[kw.DEST_RESC_NAME_KW] = resource
@@ -508,7 +516,7 @@ def create(
508516
desc = response.int_info
509517
conn.close_file(desc)
510518

511-
return self.get(path)
519+
return self.get(path, replica_sort_function=replica_sort_function)
512520

513521
def open_with_FileRaw(self, *arg, **kw_options):
514522
holder = []

irods/test/access_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,8 @@ def test_atomic_acls__issue_505(self):
516516

517517
# Assert that the ACLs we added are now a subset of those now listed in the catalog.
518518
self.assertLessEqual(
519-
{acl.normalize(ses.zone) for acl in (a1,a2,a3,a4)},
520-
{acl.normalize(ses.zone) for acl in ses.acls.get(self.coll)}
519+
{acl.normalize(ses.zone) for acl in (a1, a2, a3, a4)},
520+
{acl.normalize(ses.zone) for acl in ses.acls.get(self.coll)},
521521
)
522522

523523
finally:

0 commit comments

Comments
 (0)