Skip to content

Commit 527417d

Browse files
d-w-moorealanking
authored andcommitted
[#606] attach server response message to exception as 'server_msg' attribute
Includes test with atomic_metadata. Other APIs that could use this feature include: replica_truncate atomic_apply_acls
1 parent b39db51 commit 527417d

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,30 @@ from an object:
732732
... Object.metadata.apply_atomic_operations( *[AVUOperation(operation='remove', avu=i) for i in avus_on_Object] )
733733
```
734734

735+
Extracting JSON encoded server information in case of error
736+
-----------------------------------------------------------
737+
738+
Some server apis, including atomic metadata and replica truncation, can fail for various reasons and generate an
739+
exception. In these cases the message object returned from the server is made available in the 'server_msg' attribute
740+
of the iRODSException object.
741+
742+
This enables an approach like the following, which logs server information possibly underlying the error that was evoked:
743+
744+
```python
745+
try:
746+
Object.metadata.apply_atomic_operations( ops )
747+
# or:
748+
DataObject.replica_truncate( size )
749+
except iRODSException as exc:
750+
log.error('Server API call failure. Traceback = %r ; iRODS Server info = %r',
751+
traceback.extract_tb(sys.exc_info()[2]),
752+
exc.server_msg.get_json_encoded_struct())
753+
```
754+
755+
For `DataObject.replica_truncate(...)`, note that exc.server_msg.get_json_encoded_struct() can be used in the exception-handling
756+
code path to retrieve the same information that would have been routinely returned from the truncate call itself, had it actually
757+
completed without error.
758+
735759
Special Characters
736760
------------------
737761

irods/connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ def recv(self, into_buffer = None
149149
except TypeError:
150150
err_msg = None
151151
if nominal_code(msg.int_info) not in acceptable_codes:
152-
raise get_exception_by_code(msg.int_info, err_msg)
152+
exc = get_exception_by_code(msg.int_info, err_msg)
153+
exc.server_msg = msg
154+
raise exc
153155
return msg
154156

155157
def recv_into(self, buffer, **options):

irods/message/__init__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ class BinBytesBuf(Message):
265265
class JSON_Binary_Response(BinBytesBuf):
266266
pass
267267

268+
class XMLMessageNotConvertibleToJSON(Exception):
269+
pass
270+
268271
class iRODSMessage(object):
269272

270273
class ResponseNotParseable(Exception):
@@ -285,13 +288,35 @@ def __init__(self, msg_type=b'', msg=None, error=b'', bs=b'', int_info=0):
285288
self.int_info = int_info
286289

287290
def get_json_encoded_struct (self):
291+
"""For messages having STR_PI and *BytesBuf_PI in the highest level XML tag.
292+
293+
Invoke this method to recover a (usually JSON-formatted) server message
294+
returned by a server API.
295+
"""
288296
Xml = ET().fromstring(self.msg.replace(b'\0',b''))
289-
json_str = Xml.find('buf').text
297+
298+
# Handle STR_PI case, which corresponds to server APIs with a 'char**' output parameter.
299+
if Xml.tag == 'STR_PI':
300+
STR_PI_element = Xml.find('myStr')
301+
if STR_PI_element is not None:
302+
return json.loads( STR_PI_element.text )
303+
304+
# Handle remaining cases, i.e. BinBytesBuf_PI and BytesBuf_PI.
305+
json_str = getattr(Xml.find('buf'), 'text', None)
306+
if json_str is None:
307+
error_text = "Message does not have a suitable 'buf' tag from which to extract text or binary content."
308+
raise XMLMessageNotConvertibleToJSON(error_text)
309+
290310
if Xml.tag == 'BinBytesBuf_PI':
291311
mybin = JSON_Binary_Response()
292312
mybin.unpack(Xml)
293313
json_str = mybin.buf.replace(b'\0',b'').decode()
294-
return json.loads( json_str )
314+
315+
if Xml.tag in ('BinBytesBuf_PI', 'BytesBuf_PI'):
316+
return json.loads( json_str )
317+
318+
error_text = "Inappropriate top-level tag '{Xml.tag}' used in iRODSMessage.get_json_encoded_struct".format(**locals())
319+
raise XMLMessageNotConvertibleToJSON(error_text)
295320

296321
@staticmethod
297322
def recv(sock):

irods/test/meta_test.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import time
77
import datetime
88
import unittest
9-
from irods.meta import (iRODSMeta, AVUOperation, BadAVUOperationValue, BadAVUOperationKeyword)
9+
import irods.exception as ex
1010
from irods.manager.metadata_manager import InvalidAtomicAVURequest
11+
from irods.meta import (iRODSMeta, AVUOperation, BadAVUOperationValue, BadAVUOperationKeyword)
1112
from irods.models import (DataObject, Collection, Resource, CollectionMeta)
1213
import irods.test.helpers as helpers
1314
import irods.keywords as kw
@@ -119,7 +120,39 @@ def tearDown(self):
119120
helpers.remove_unused_metadata(self.sess)
120121
self.sess.cleanup()
121122

122-
from irods.test.helpers import create_simple_resc_hierarchy
123+
from irods.test.helpers import (create_simple_resc, create_simple_resc_hierarchy)
124+
125+
def test_replica_truncate_json_error__issue_606(self):
126+
path = self.coll_path + "/atomic_meta_issue_606"
127+
obj = self.sess.data_objects.create(path)
128+
with self.create_simple_resc('repl_trunc_test_resc__issue_606') as f:
129+
try:
130+
obj.replica_truncate(1,**{kw.RESC_NAME_KW:f})
131+
except ex.iRODSException as e:
132+
resp = e.server_msg.get_json_encoded_struct()
133+
# Test that returned structure is a dict containing at least one item.
134+
self.assertIsInstance(resp, dict)
135+
self.assertTrue(resp)
136+
137+
def test_atomic_metadata_json_error__issue_606(self):
138+
path = self.coll_path + "/atomic_meta_issue_606"
139+
obj = self.sess.data_objects.create(path)
140+
obj.unlink(force = True)
141+
fail_message = ''
142+
try:
143+
obj.metadata.apply_atomic_operations(AVUOperation(operation="add", avu=iRODSMeta('a','b','c')))
144+
except ex.iRODSException as e:
145+
resp = e.server_msg.get_json_encoded_struct()
146+
self.assertIn(
147+
'Entity does not exist [entity_name={}]'.format(obj.path),
148+
resp['error_message'])
149+
except Exception as e:
150+
fail_message = 'apply_atomic_operations on a nonexistent object raised an unexpected exception {e!r}'.format(**locals())
151+
else:
152+
fail_message = 'apply_atomic_operations on a nonexistent object did not raise an exception as expected.'
153+
154+
if fail_message:
155+
self.fail(fail_message)
123156

124157
def test_atomic_metadata_operations_244(self):
125158
user = self.sess.users.get("rods")

0 commit comments

Comments
 (0)