Skip to content

Commit e9deea6

Browse files
d-w-moorealanking
andcommitted
[#534] implement replica truncate
Co-authored-by: Alan King <alanking@renci.org>
1 parent bf46679 commit e9deea6

5 files changed

Lines changed: 120 additions & 2 deletions

File tree

irods/api_number.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"OPEN_COLLECTION201_AN": 712,
159159

160160
"GET_LIBRARY_FEATURES_AN": 801,
161+
"REPLICA_TRUNCATE_AN": 802,
161162

162163
# 1000 - 1059 - NETCDF API calls
163164
"NC_OPEN_AN": 1000,

irods/data_object.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def unregister(self, **options):
115115
def truncate(self, size):
116116
self.manager.truncate(self.path, size)
117117

118+
def replica_truncate(self, size, **options):
119+
return self.manager.replica_truncate(self.path, size, **options)
120+
118121
def replicate(self, resource = None, **options):
119122
self.manager.replicate(self.path, resource = resource, **options)
120123

irods/exception.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class MultipleResultsFound(QueryException):
6969
pass
7070

7171

72-
class NotImplementedInIRODSServer(RuntimeError):
72+
class NotImplementedInIRODSServer(PycommandsException):
7373
def __init__(self, feature_description, required_iRODS_version = ()):
7474
super(NotImplementedInIRODSServer,self).__init__(feature_description + ': Not supported by the connected iRODS server.')
7575
self.required_iRODS_version = required_iRODS_version
@@ -641,6 +641,10 @@ class SYS_REPLICA_DOES_NOT_EXIST(iRODSException):
641641
code = -164000
642642

643643

644+
class SYS_REPLICA_INACCESSIBLE(iRODSException):
645+
code = -168000
646+
647+
644648
class SYS_NOT_ALLOWED(iRODSException):
645649
code = -169000
646650

irods/manager/data_object_manager.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,37 @@ def make_FileOpenRequest(**extra_opts):
534534
ret_value.seek(0,io.SEEK_END)
535535
return ret_value
536536

537+
def replica_truncate(self, path, desired_size, **options):
538+
539+
if self.sess.server_version == (4,3,2):
540+
message = 'replica_truncate responses can fail to parse with iRODS 4.3.2 due to routine omission of the JSON response string, so this method is not supported for iRODS 4.3.2.'
541+
raise ex.OperationNotSupported(message)
542+
else:
543+
required_server_version = (4,3,3)
544+
if self.sess.server_version < required_server_version:
545+
raise ex.NotImplementedInIRODSServer('replica_truncate', required_server_version)
546+
547+
message_body = FileOpenRequest(
548+
objPath=path,
549+
createMode=0,
550+
openFlags=0,
551+
offset=0,
552+
dataSize=desired_size,
553+
numThreads=self.sess.numThreads,
554+
oprType=0,
555+
KeyValPair_PI=StringStringMap(options),
556+
)
557+
message = iRODSMessage('RODS_API_REQ',
558+
msg=message_body,
559+
int_info=api_number["REPLICA_TRUNCATE_AN"])
560+
561+
with self.sess.pool.get_connection() as conn:
562+
conn.send(message)
563+
response = conn.recv()
564+
msg = response.get_main_message( STR_PI )
565+
566+
return json.loads(msg.myStr)
567+
537568
def trim(self, path, **options):
538569

539570
try:

irods/test/data_obj_test.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from datetime import datetime
44
import base64
55
import concurrent.futures
6-
import contextlib # check if redundant
6+
import contextlib
7+
import errno
78
import hashlib
89
import io
910
import itertools
@@ -2362,6 +2363,84 @@ def _update(n):
23622363

23632364
self.assertEqual(pbar.percent_done(), 100)
23642365

2366+
def test_replica_truncate_related_errors__issue_534(self):
2367+
sess = self.sess
2368+
data_objs = self.sess.data_objects
2369+
truncated_size = 16
2370+
2371+
try:
2372+
# Set path for a new, as yet nonexisting data object.
2373+
data_path = '{}/{}'.format(
2374+
helpers.home_collection(sess),
2375+
unique_name(my_function_name(), datetime.now(), truncated_size) + "issue_534_errors")
2376+
self.assertFalse (data_objs.exists(data_path), "Data object should not yet exist.")
2377+
2378+
# Assert appropriate error is raised for the nonexisting object.
2379+
with self.assertRaises(ex.OBJ_PATH_DOES_NOT_EXIST):
2380+
data_objs.replica_truncate(data_path, truncated_size)
2381+
2382+
d = data_objs.create(data_path)
2383+
2384+
# Assert appropriate error is raised for an existing object without a replica on the given resource.
2385+
with self.create_simple_resc() as newResc:
2386+
with self.assertRaises(ex.SYS_REPLICA_INACCESSIBLE):
2387+
d.replica_truncate(truncated_size, **{kw.RESC_NAME_KW: newResc})
2388+
2389+
# Assert appropriate error is raised for an invalid (negative) size argument.
2390+
try:
2391+
data_objs.replica_truncate(data_path, -truncated_size)
2392+
except Exception as e:
2393+
self.assertIsInstance(e, ex.UNIX_FILE_TRUNCATE_ERR)
2394+
self.assertEqual('EINVAL', errno.errorcode[abs(e.code) % 1000])
2395+
2396+
finally:
2397+
if data_objs.exists(data_path):
2398+
data_objs.unlink(data_path, force = True)
2399+
2400+
def test_replica_truncate__issue_534(self):
2401+
sess = self.sess
2402+
data_objs = self.sess.data_objects
2403+
original_size = 16
2404+
original_content = b'_' * original_size
2405+
with self.create_simple_resc() as newResc:
2406+
for truncated_size in (original_size//2, original_size + 1024):
2407+
try:
2408+
data_path = '{}/{}'.format(
2409+
helpers.home_collection(sess),
2410+
unique_name(my_function_name(), datetime.now(), truncated_size) + "_issue_534")
2411+
2412+
# Create a data object with size original_size.
2413+
with data_objs.open(data_path,'w') as f:
2414+
f.write(original_content)
2415+
d = data_objs.get(data_path)
2416+
2417+
# Ensure there are two replicas, one on newResc as well as one on 'demoResc'
2418+
d.replicate(resource = newResc)
2419+
response = d.replica_truncate(truncated_size, **{kw.RESC_NAME_KW: newResc})
2420+
2421+
# Stat data object again.
2422+
d = data_objs.get(data_path)
2423+
2424+
# Check that returned resource hierarchy and replica number match expectations.
2425+
self.assertEqual(response['replica_number'],
2426+
[_ for _ in data_objs.get(data_path).replicas if _.resource_name == newResc][0].number)
2427+
self.assertEqual(response['resource_hierarchy'], newResc)
2428+
2429+
# Make sure sizes are as expected.
2430+
self.assertEqual([_ for _ in d.replicas if _.resource_name == newResc][0].size, truncated_size)
2431+
self.assertEqual([_ for _ in d.replicas if _.resource_name != newResc][0].size, original_size)
2432+
2433+
# Check that content of truncated replicas is as expected.
2434+
if truncated_size <= original_size:
2435+
self.assertEqual( d.open('r').read(), original_content[:truncated_size])
2436+
else:
2437+
self.assertEqual( d.open('r').read(), original_content + b'\0' * (truncated_size - original_size))
2438+
2439+
finally:
2440+
if data_objs.exists(data_path):
2441+
data_objs.unlink(data_path, force = True)
2442+
2443+
23652444
if __name__ == '__main__':
23662445
# let the tests find the parent irods lib
23672446
sys.path.insert(0, os.path.abspath('../..'))

0 commit comments

Comments
 (0)