Skip to content

Commit 82f269a

Browse files
author
andamian
authored
Cadc 9508 (#145)
* Added support for If-Match in caom2repo
1 parent 800a9a6 commit 82f269a

File tree

5 files changed

+129
-96
lines changed

5 files changed

+129
-96
lines changed

caom2/caom2/caom_util.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@
7979
from __future__ import (absolute_import, division, print_function,
8080
unicode_literals)
8181

82-
import collections
8382
import sys
83+
import collections
8484
from datetime import datetime
8585

8686
import six
87+
from six.moves import collections_abc
8788
from six.moves.urllib.parse import urlsplit
8889
from builtins import int, str as newstr
8990

@@ -203,7 +204,7 @@ def value_check(value, min_value, max_value, variable, override=None):
203204
return True
204205

205206

206-
class TypedList(collections.MutableSequence):
207+
class TypedList(collections_abc.MutableSequence):
207208
"""
208209
Class that implements a typed list in Python. Supported types
209210
are specified when instance is created. Example:
@@ -266,7 +267,7 @@ def key_type(self):
266267
return self._oktypes
267268

268269

269-
class TypedSet(collections.MutableSet):
270+
class TypedSet(collections_abc.MutableSet):
270271
"""
271272
Class that implements a typed set in Python. Supported types
272273
are specified when instance is created. Example:

caom2/setup.cfg

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ omit = */tests/*
2020

2121
[tool:pytest]
2222
minversion = 2.2
23-
norecursedirs = build docs/_build
24-
doctest_plus = enabled
2523
testpaths = caom2
2624

2725
[bdist_wheel]

caom2repo/caom2repo/core.py

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# ****************** CANADIAN ASTRONOMY DATA CENTRE *******************
44
# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES **************
55
#
6-
# (c) 2016. (c) 2016.
6+
# (c) 2021. (c) 2021.
77
# Government of Canada Gouvernement du Canada
88
# National Research Council Conseil national de recherches
99
# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6
@@ -80,8 +80,7 @@
8080
import sys
8181
from datetime import datetime
8282

83-
from cadcutils import net
84-
from cadcutils import util
83+
from cadcutils import net, util, exceptions
8584
from caom2.obs_reader_writer import ObservationReader, ObservationWriter
8685
from caom2 import obs_reader_writer
8786
from caom2.version import version as caom2_version
@@ -236,8 +235,8 @@ def visit(self, plugin, collection, start=None, end=None, obs_file=None,
236235
while len(observations) > 0:
237236
if nthreads is None:
238237
results = [
239-
self._process_observation_id(collection, observationID,
240-
halt_on_error)
238+
self.process_observation_id(collection, observationID,
239+
halt_on_error)
241240
for observationID in observations]
242241
for v, u, s, f in results:
243242
if v:
@@ -284,24 +283,39 @@ def visit(self, plugin, collection, start=None, end=None, obs_file=None,
284283
break
285284
return visited, updated, skipped, failed
286285

287-
def _process_observation_id(self, collection, observationID,
288-
halt_on_error):
289-
visited = None
286+
def process_observation_id(self, collection, observation_id,
287+
halt_on_error):
288+
"""
289+
Reads an observation, calls a plugin to update it and, if modified,
290+
uploads it to the repo.
291+
292+
:param collection:
293+
:param observation_id:
294+
:param halt_on_error: if true, raise and exception when error
295+
encountered otherwise log the error.
296+
:return: (visited, updated, skipped, failed) tuple with values
297+
equalled to None or observation_id depending on the outcome case
298+
(visited=observationID always)
299+
"""
300+
visited = observation_id
290301
updated = None
291302
skipped = None
292303
failed = None
293-
self.logger.info('Process observation: ' + observationID)
304+
self.logger.info('Process observation: ' + observation_id)
294305
try:
295-
observation = self.get_observation(collection, observationID)
306+
observation = self.get_observation(collection, observation_id)
307+
orig_checksum = observation.acc_meta_checksum
308+
if orig_checksum:
309+
orig_checksum = orig_checksum.uri
296310
if self.plugin.update(observation=observation,
297311
subject=self._subject) is False:
298312
self.logger.info('SKIP {}'.format(observation.observation_id))
299313
skipped = observation.observation_id
300314
else:
301-
self.post_observation(observation)
315+
self.post_observation(observation, orig_checksum)
302316
self.logger.debug(
303317
'UPDATED {}'.format(observation.observation_id))
304-
updated = observation.observation_id
318+
updated = observation_id
305319
except TypeError as e:
306320
if "unexpected keyword argument" in str(e):
307321
raise RuntimeError(
@@ -311,17 +325,30 @@ def _process_observation_id(self, collection, observationID,
311325
else:
312326
# other unexpected TypeError
313327
raise e
328+
except exceptions.UnexpectedException as e:
329+
if e.orig_exception.response.status_code == 412:
330+
self.logger.info(
331+
'Race condition: observation {} updated on the server '
332+
'while being visited. Re-trying.'.format(observation_id))
333+
return self.process_observation_id(collection, observation_id,
334+
halt_on_error)
335+
else:
336+
failed = observation_id
337+
self._handle_error(e, observation_id, halt_on_error)
314338
except Exception as e:
315-
failed = observationID
316-
self.logger.error(
317-
'FAILED {} - Reason: {}'.format(observationID, e))
318-
if halt_on_error:
319-
raise e
320-
321-
visited = observationID
322-
339+
failed = observation_id
340+
self._handle_error(e, observation_id, halt_on_error)
341+
visited = observation_id
323342
return visited, updated, skipped, failed
324343

344+
def _handle_error(self, exception, observation_id, halt_on_error):
345+
self.logger.error(
346+
'FAILED {} - Reason: {}'.format(observation_id, str(exception)))
347+
if halt_on_error:
348+
raise exception
349+
else:
350+
self.logger.debug(exception)
351+
325352
def _get_obs_from_file(self, obs_file, start, end, halt_on_error):
326353
obs = []
327354
failed = []
@@ -452,10 +479,13 @@ def get_observation(self, collection, observation_id):
452479
raise Exception('Got empty response for resource: {}'.format(path))
453480
return obs_reader.read(BytesIO(content))
454481

455-
def post_observation(self, observation):
482+
def post_observation(self, observation, orig_checksum=None):
456483
"""
457484
Updates an observation in the CAOM2 repo
458485
:param observation: observation to update
486+
:param orig_checksum: the checksum of the observation to be updated.
487+
Posting this value prevents race conditions when observations are
488+
updated concurrently
459489
:return: updated observation
460490
"""
461491
assert observation.collection is not None
@@ -469,6 +499,8 @@ def post_observation(self, observation):
469499
observation, ibuffer)
470500
obs_xml = ibuffer.getvalue()
471501
headers = {'Content-Type': 'application/xml'}
502+
if orig_checksum:
503+
headers['If-Match'] = orig_checksum
472504
self._repo_client.post(
473505
(self.capability_id, path), headers=headers, data=obs_xml)
474506

@@ -535,9 +567,9 @@ def multiprocess_observation_id(collection, observationID, plugin, subject,
535567
log_level, resource_id, host, agent,
536568
halt_on_error):
537569
"""
538-
Multi-process version of CAOM2RepoClient._process_observation_id().
570+
Multi-process version of CAOM2RepoClient.process_observation_id().
539571
Each process handles Control-C via KeyboardInterrupt, which is not needed
540-
in CAOM2RepoClient._process_observation_id().
572+
in CAOM2RepoClient.process_observation_id().
541573
:param collection: Name of the collection
542574
:param observationID: Observation identifier
543575
:param plugin: path to python file that contains the algorithm to be
@@ -551,13 +583,6 @@ def multiprocess_observation_id(collection, observationID, plugin, subject,
551583
:return: Tuple of observationID representing visited, updated, skipped
552584
and failed
553585
"""
554-
visited = None
555-
updated = None
556-
skipped = None
557-
failed = None
558-
observation = None
559-
# set up logging for each process
560-
subject = subject
561586
logging.basicConfig(
562587
format='%(asctime)s %(process)d %(levelname)-8s %(name)-12s ' +
563588
'%(funcName)s %(message)s',
@@ -566,34 +591,10 @@ def multiprocess_observation_id(collection, observationID, plugin, subject,
566591
'multiprocess_observation_id(): {}'.format(observationID))
567592

568593
client = CAOM2RepoClient(subject, log_level, resource_id, host, agent)
569-
try:
570-
observation = client.get_observation(collection, observationID)
571-
if plugin.update(observation=observation,
572-
subject=subject) is False:
573-
rootLogger.info('SKIP {}'.format(observation.observation_id))
574-
skipped = observation.observation_id
575-
else:
576-
client.post_observation(observation)
577-
rootLogger.debug('UPDATED {}'.format(observation.observation_id))
578-
updated = observation.observation_id
579-
except TypeError as e:
580-
if "unexpected keyword argument" in str(e):
581-
raise RuntimeError(
582-
"{} - To fix the problem, please add the **kwargs "
583-
"argument to the list of arguments for the update"
584-
" method of your plugin.".format(str(e)))
585-
else:
586-
# other unexpected TypeError
587-
raise e
588-
except Exception as e:
589-
failed = observationID
590-
rootLogger.error('FAILED {} - Reason: {}'.format(observationID, e))
591-
if halt_on_error:
592-
raise e
593-
594-
visited = observationID
595-
596-
return visited, updated, skipped, failed
594+
client.plugin = plugin
595+
client.logger = rootLogger
596+
return \
597+
client.process_observation_id(collection, observationID, halt_on_error)
597598

598599

599600
def main_app():

0 commit comments

Comments
 (0)