Skip to content

Commit 1f4010c

Browse files
Merge pull request #223 from NeurodataWithoutBorders/add_curl_retry
Add retry for curl requests
2 parents fd01962 + a4008a0 commit 1f4010c

File tree

3 files changed

+38
-9
lines changed

3 files changed

+38
-9
lines changed

nwbinspector/nwbinspector.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from types import FunctionType
1414
from warnings import filterwarnings, warn
1515
from distutils.util import strtobool
16+
from time import sleep
1617

1718
import click
1819
import pynwb
@@ -28,7 +29,7 @@
2829
)
2930
from .register_checks import InspectorMessage, Importance
3031
from .tools import get_s3_urls_and_dandi_paths
31-
from .utils import FilePathType, PathType, OptionalListOfStrings
32+
from .utils import FilePathType, PathType, OptionalListOfStrings, robust_s3_read
3233

3334
INTERNAL_CONFIGS = dict(dandi=Path(__file__).parent / "internal_configs" / "dandi.inspector_config.yaml")
3435

@@ -414,8 +415,9 @@ def inspect_nwb(
414415
ignore: OptionalListOfStrings = None,
415416
select: OptionalListOfStrings = None,
416417
importance_threshold: Union[str, Importance] = Importance.BEST_PRACTICE_SUGGESTION,
417-
driver: str = None,
418+
driver: Optional[str] = None,
418419
skip_validate: bool = False,
420+
max_retries: int = 10,
419421
) -> List[InspectorMessage]:
420422
"""
421423
Inspect a NWBFile object and return suggestions for improvements according to best practices.
@@ -449,6 +451,11 @@ def inspect_nwb(
449451
skip_validate : bool
450452
Skip the PyNWB validation step. This may be desired for older NWBFiles (< schema version v2.10).
451453
The default is False, which is also recommended.
454+
max_retries : int, optional
455+
When using the ros3 driver to stream data from an s3 path, occasional curl issues can result.
456+
AWS suggests using iterative retry with an exponential backoff of 0.1 * 2^retries.
457+
This sets a hard bound on the number of times to attempt to retry the collection of messages.
458+
Defaults to 10 (corresponds to 102.4s maximum delay on final attempt).
452459
"""
453460
importance_threshold = (
454461
Importance[importance_threshold] if isinstance(importance_threshold, str) else importance_threshold
@@ -460,6 +467,7 @@ def inspect_nwb(
460467
nwbfile_path = str(nwbfile_path)
461468
filterwarnings(action="ignore", message="No cached namespaces found in .*")
462469
filterwarnings(action="ignore", message="Ignoring cached namespace .*")
470+
463471
with pynwb.NWBHDF5IO(path=nwbfile_path, mode="r", load_namespaces=True, driver=driver) as io:
464472
if not skip_validate:
465473
validation_errors = pynwb.validate(io=io)
@@ -473,7 +481,7 @@ def inspect_nwb(
473481
)
474482

475483
try:
476-
nwbfile = io.read()
484+
nwbfile = robust_s3_read(command=io.read, max_retries=max_retries)
477485
for inspector_message in run_checks(nwbfile=nwbfile, checks=checks):
478486
inspector_message.file_path = nwbfile_path
479487
yield inspector_message
@@ -499,7 +507,7 @@ def run_checks(nwbfile: pynwb.NWBFile, checks: list):
499507
for nwbfile_object in nwbfile.objects.values():
500508
if check_function.neurodata_type is None or issubclass(type(nwbfile_object), check_function.neurodata_type):
501509
try:
502-
output = check_function(nwbfile_object)
510+
output = robust_s3_read(command=check_function, command_args=[nwbfile_object])
503511
# if an individual check fails, include it in the report and continue with the inspection
504512
except Exception:
505513
output = InspectorMessage(

nwbinspector/utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import re
33
import json
44
import numpy as np
5-
from typing import TypeVar, Optional, List
5+
from typing import TypeVar, Optional, List, Dict, Callable
66
from pathlib import Path
77
from importlib import import_module
88
from packaging import version
9+
from time import sleep
910

1011
PathType = TypeVar("PathType", str, Path) # For types that can be either files or folders
1112
FilePathType = TypeVar("FilePathType", str, Path)
@@ -113,3 +114,19 @@ def get_package_version(name: str) -> version.Version:
113114

114115
package_version = get_distribution(name).version
115116
return version.parse(package_version)
117+
118+
119+
def robust_s3_read(
120+
command: Callable, max_retries: int = 10, command_args: Optional[list] = None, command_kwargs: Optional[Dict] = None
121+
):
122+
"""Attempt the command (usually acting on an S3 IO) up to the number of max_retries using exponential backoff."""
123+
command_args = command_args or []
124+
command_kwargs = command_kwargs or dict()
125+
for retry in range(max_retries):
126+
try:
127+
return command(*command_args, **command_kwargs)
128+
except OSError: # cannot curl request
129+
sleep(0.1 * 2**retry)
130+
except Exception as exc:
131+
raise exc
132+
raise TimeoutError(f"Unable to complete the command ({command.__name__}) after {max_retries} attempts!")

tests/unit_tests/test_time_series.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from packaging import version
2+
from time import sleep
23

34
import numpy as np
45
import pynwb
@@ -14,7 +15,7 @@
1415
check_missing_unit,
1516
check_resolution,
1617
)
17-
from nwbinspector.utils import get_package_version
18+
from nwbinspector.utils import get_package_version, robust_s3_read
1819

1920
try:
2021
# Test ros3 on sub-YutaMouse54/sub-YutaMouse54_ses-YutaMouse54-160630_behavior+ecephys.nwb from #3
@@ -195,9 +196,12 @@ def test_check_none_matnwb_resolution_pass():
195196
load_namespaces=True,
196197
driver="ros3",
197198
) as io:
198-
nwbfile = io.read()
199-
time_series = nwbfile.processing["video_files"]["video"].time_series["20170203_KIB_01_s1.1.h264"]
200-
assert check_resolution(time_series) is None
199+
nwbfile = robust_s3_read(command=io.read)
200+
time_series = robust_s3_read(
201+
"20170203_KIB_01_s1.1.h264",
202+
command=nwbfile.processing["video_files"]["video"].time_series.get,
203+
)
204+
assert check_resolution(time_series) is None
201205

202206

203207
def test_check_resolution_fail():

0 commit comments

Comments
 (0)